【问题标题】:Using ftrace on simple program, Inline Assembly __asm__("leave") resulting in seg fault在简单程序上使用 ftrace,内联汇编 __asm__("leave") 导致段错误
【发布时间】:2019-06-27 01:35:21
【问题描述】:

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

在其上运行 ftrace 时,没有任何反应。如果我自己运行可执行文件,我只会遇到段错误。 我正在编译它: gcc -nostdlib test.c -o test

这是我的代码:

int foo(void) {  
}

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

预期结果展示了 ftrace 跟踪函数调用到执行的过程。

这是我要说的文字中的图片:

这是我正在使用的 ftrace:

https://github.com/elfmaster/ftrace

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

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

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

【问题讨论】:

  • 我也在虚拟机上尝试了 32 位发行版。 你永远不需要它,只需在 gcc 命令行中添加 -m32 即可制作 32 位代码 /可执行文件。 (所有现代发行版都有用于 gcc/libc 的 multilib 包,用于安装 32 位版本的 libc 和 32 位可执行文件在普通 64 位系统上所需的其他关键库。)
  • @PeterCordes 哦,好的,太棒了,谢谢。从现在开始我会这样做。
  • IIRC,_start 函数不应该返回(堆栈上不再有返回地址)。它应该显式调用退出系统调用。此外,leave 将更改为rsp。你为什么要插入leave
  • @AjayBrahmakshatriya 我打算离开书中的例子,我认为休假导致了段错误。但就像我说的那样,我问的是文本中的方法是否已经过时。由于作者展示了他的 ftrace 与小程序一起工作。
  • 还有一个有趣的事实:gcc -nostdlib 使静态可执行文件。即使您没有 安装了 32 位库,这也将起作用。您所需要的只是对 32 位进程和系统调用的内核支持。 (在 Windows WSL 上缺失,否则很普遍。)当然,您需要一个包含有用机器代码的 32 位可执行文件。

标签: c gcc x86 inline-assembly ftrace


【解决方案1】:

_start 末尾致电_exit(0);exit_group(0)。 (链接到gcc -static -nostartfiles 而不是-nostdlib,这样您就可以调用libc 系统调用包装函数;即使glibc init 函数尚未运行so malloc or printf would crash,它们也应该可以工作。

或者使用内联汇编手动进行exit_group(0) 系统调用。在 x86-64 Linux 上:
asm("mov $231, %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 函数。


这本书的代码完全被破坏了,原因有两个。 (我不知道它是如何在 i386 或 x86-64 上工作的。对我来说似乎超级奇怪。你确定它不只是应该崩溃,但你看看它在发生之前做了什么?)

  1. _start 在 Linux 中不是函数;你(或编译器生成的代码)不能ret 来自它。您需要进行_exit 系统调用。栈上没有返回地址1

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

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

    但最终,GCC 会将_start 编译为普通函数,从而最终运行ret 指令。任何地方都没有有效/有用的返回地址,所以ret 根本无法工作。

    如果你在没有优化的情况下编译(默认),gcc 将默认为-fno-omit-frame-pointer,因此它的函数序言将设置 EBP 或 RBP 作为帧指针,使leave 本身不会出错。如果你用优化编译(-O1 和更高版本启用-fomit-frame-pointer),gcc 不会弄乱 RBP,当你运行leave 时它会为零,从而直接导致段错误。 (因为它做了 RSP=RBP,然后使用新的 RSP 作为pop %rbp 的堆栈指针。)

无论如何,如果它没有出错,那么在编译器生成的pop %rbp 作为正常函数结尾的一部分之前,堆栈指针将再次指向argc因此编译器生成的ret 将尝试返回argv[0]。由于默认情况下堆栈是不可执行的,因此会出现段错误。(并且它指向 ASCII 字符,可能不会解码为有用的 x86-64 机器代码。)

您可以通过使用 GDB 单步执行 asm 来自己发现这一点。 (layout reg 并使用 stepi aka si)。

一般来说,你在编译器后面弄乱堆栈指针和其他寄存器通常只会让事情崩溃。如果堆栈中存在更高的返回地址,pop %rcx 将比leave 更有意义。


脚注 1:

在你的进程的地址空间中甚至没有任何机器代码可供有用的返回地址可以指向进行这样的系统调用,除非你注入一些机器代码作为参数或环境变量。

您与-nostdlib 链接,因此没有链接libc。如果您确实动态链接 libc 但仍然编写自己的 _start(例如使用 gcc -nostartfiles 而不是完整的 -nostdlib),则 ASLR 将意味着 libc _exit 函数位于某个运行时变量地址。

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

【讨论】:

  • 哇,谢谢。我很欣赏这个解释。他的 ftrace 对你有用还是不正确?
  • @Camilo:我不知道,我对 ftrace 不熟悉。我没有尝试运行可执行文件甚至编译它;我只是在脑海中想象了 GCC 的 asm 对在调试模式下编译和优化的微不足道的函数会做什么。如果想看编译器生成的asm,放在godbolt.org
  • 好的,我去看看 godbolt.org 再次感谢您的深入解释。
  • @Camilo:既然你说这是在书中发表的,我就试着想办法让它发挥作用。但我真的看不出任何可信的东西,除非进入 _start 时的内存和寄存器状态在古代历史上非常不同,就像在切换到 ELF 前几天的 a.out 一样。
  • @Camilo:stackoverflow.com/tags/x86/info 有很多很好的链接,特别是对于性能调整以及出于其他原因理解 x86 asm。包括指向汇编程序、ABI 和 x86 ISA 官方文档的文档的链接。
猜你喜欢
  • 1970-01-01
  • 2019-08-06
  • 2010-10-29
  • 2010-12-03
  • 1970-01-01
  • 1970-01-01
  • 2013-04-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多