在简单程序上使用 ftrace,内联汇编 __asm__("leave") 导致段错误

Using ftrace on simple program, Inline Assembly __asm__("leave") resulting in seg fault

我正在通读这本关于学习 Linux 二进制分析的书。在书中,作者介绍了 ftrace,他在 github 上安装了它,并演示了如何使用它。他提供了一小段代码来测试 ftrace。

当运行在其上使用 ftrace 时,没有任何反应。如果我 运行 可执行文件本身,我只会遇到段错误。 我是这样编译的:gcc -nostdlib test.c -o test

这是我的代码:

int foo(void) {  
}

_start()
{
    foo();
    __asm__("leave");
}

预期结果表明 ftrace 通过执行跟踪函数调用。

这是我要讲的内容的图片:

这是我正在使用的 ftrace:

https://github.com/elfmaster/ftrace

我想问题是,我是否完全遗漏了什么,做错了什么,文本是否过时或者正确的方法是什么?如果这是一个愚蠢的问题,我深表歉意,我只是离开了正文。我还在 VM 上使用 32 位发行版尝试了此操作,但没有任何改变,但只是尝试了一下,因为作者的一些示例是在 32 位上。谢谢。

注意: 当我 运行 他的 ftrace 使用不会导致段错误的程序时,我得到

pid_read() failed: Input/output error <0x1>

_start的末尾调用_exit(0);exit_group(0)。 (Link 使用 gcc -static -nostartfiles 而不是 -nostdlib,因此您可以调用 libc 系统调用包装函数;即使 glibc init 函数尚未被 运行 ).

或使用内联汇编手动创建 exit_group(0) system-call。在 x86-64 上 Linux:
asm("mov 1, %eax; xor %edi,%edi; syscall");

另请参阅 How Get arguments value using inline assembly in C without Glibc? 以了解更多有关将 hacky x86-64 _start 编写到 运行 您自己的 C 函数的更多信息,作为您流程中的第一件事。 (但大部分答案都是关于破解调用约定以访问 argc / argv,这是令人讨厌的,我不推荐它。)Matteo 对这个问题的回答有一个用 asm 编写的最小 _start 调用正常的 C main 函数。


由于 2 个原因,本书的代码完全错误。 (我不知道它是如何在 i386 或 x86-64 上工作的。对我来说似乎很奇怪。你确定它不应该崩溃,但你看看它在崩溃之前做了什么?)

  1. _start 不是 Linux 中的函数;你(或 compiler-generated 代码)不能从它 ret。您需要进行 _exit 系统调用。栈上没有return地址1.

    如果函数有其 return 地址,ELF 入口点 _startargc,如 ABI 文档中所指定。 (x86-64 System V 或 i386 System V 取决于您构建的是 64 位还是 gcc -m32 32 位可执行文件。)

  2. 插入leavemov %ebp, %esp/pop %ebp或等效的RBP/RSP)进入compiler-generated代码在这里没有意义。它有点像额外的 pop,但会破坏编译器的 EBP/RBP,所以如果它碰巧选择 leave 而不是 pop %rbp 作为它自己的序言,那么编译器生成的代码将出错。 (进入 _start 的 RBP 在 statically-linked 可执行文件中为 0。或者在跳转到 PIE 可执行文件中的 _start 之前保留 RBP 中剩余的任何动态 linker。)

    但最终,GCC 会将 _start 编译为普通函数,因此最终 运行 会生成一条 ret 指令。任何地方都没有有效/有用的 return 地址,因此 ret 根本无法工作。

    如果你编译时没有优化(默认),gcc 将默认为 -fno-omit-frame-pointer,因此它的函数序言将设置 EBP 或 RBP 作为帧指针,使 leave 本身成为可能不要错。如果您使用优化进行编译(-O1 和更高版本启用 -fomit-frame-pointer),gcc 不会与 RBP 混淆,并且当您 运行 leave 时它将为零,因此直接导致段错误。 (因为它执行 RSP=RBP 然后使用新的 RSP 作为 pop %rbp 的堆栈指针。)

无论如何,如果它没有出错,作为正常函数尾声的一部分,在 compiler-generated pop %rbp 之前,堆栈指针将再次指向 argc因此 compiler-generated ret 将尝试 return 到 argv[0]。由于堆栈默认为 non-executable,这将导致段错误。(它指向 ASCII 字符,可能无法解码为有用的 x86-64 机器码。)

您可以通过 single-stepping GDB 的 asm 自己发现这一点。 (layout reg 并使用 stepi 又名 si)。

一般来说,你在编译器背后乱搞堆栈指针和其他寄存器通常只会让事情崩溃。如果在堆栈上有一个 return 地址,pop %rcx 将比 leave.

更有意义

脚注 1:

在您的进程的地址 space 中甚至没有任何机器代码,一个有用的 return 地址 可以 指向创建这样一个系统调用,除非您将一些机器代码作为 arg 或环境变量注入。

你 linked 与 -nostdlib 所以没有 libc linked。如果您动态地执行了 link libc,但仍然编写了自己的 _start(例如,使用 gcc -nostartfiles 而不是完整的 -nostdlib),ASLR 将意味着 libc _exit 函数在某个 runtime-variable 地址。

如果您静态 linked libc (gcc -nostartfiles -static),_exit() 的代码将不会被复制到您的可执行文件中,除非您实际引用它,而这段代码不会吨。但是您仍然需要以某种方式调用它;没有 return 地址指向它。