13

我试图理解 GCC (4.4.3) 为在 Ubuntu Linux 下运行的 x86_64 机器生成的可执行代码。特别是,我不明白代码如何跟踪堆栈帧。在过去,在 32 位代码中,我习惯于在几乎每个函数中看到这个“序言”:

push %ebp
movl %esp, %ebp

然后,在功能结束时,会出现一个“尾声”

sub $xx, %esp   # Where xx is a number based on GCC's accounting.
pop %ebp
ret

或者干脆

leave
ret

它完成了同样的事情:

  • 将堆栈指针设置为当前帧的顶部,就在返回地址的下方
  • 恢复旧的帧指针值。

在 64 位代码中,正如我通过 objdump 反汇编看到的那样,许多函数不遵循这个约定——它们不推送 %rbp 然后将 %rsp 保存到 %rbp,像 GDB 这样的调试器如何构建回溯?

我的真正目标是尝试找出一个合理的地址,当执行到达程序中任意函数的开头时,将其视为用户堆栈的顶部(最高地址),堆栈指针可能已向下移动。例如,对于“顶部”,argv 的原始地址是理想的——但我无法从 main 调用的任意函数访问它。起初我以为可以使用旧的回溯方法:追逐保存的帧指针值,直到保存的值为 0——然后,之后的下一个可以算作最高实用值。(这与获取 argv 的地址不同,但它可以——比如说,找出 _start 或任何 _start 调用的堆栈指针值[例如,__libc_start_main]。)现在,我不知道

谢谢。

4

4 回答 4

5

我认为不同之处在于在 amd64 中更鼓励省略帧指针。abi第16页的脚注说

通过使用 %rsp(堆栈指针)来索引堆栈帧,可以避免传统使用 %rbp 作为堆栈帧的帧指针。这种技术在序言和尾声中保存了两条指令,并使一个额外的通用寄存器 (%rbp) 可用。

我不知道 GDB 是做什么的。我假设当用 编译时-g,对象有神奇的调试信息,允许 GDB 重建它需要的东西。我认为我没有在没有调试信息的情况下在 64 位机器上尝试过 GDB。

于 2011-12-24T15:42:51.450 回答
3

GDB 使用 DWARF CFI 来展开。对于使用 -g 编译的未剥离的二进制文件,这将位于 .debug_info 部分。对于剥离的 x86-64 二进制文件,.eh_frame 部分中有展开信息。这是在x86-64 ABI的第 3.7 节,第 56 页中定义的。自己处理这些信息非常困难,因为解析 DWARF 非常复杂,但我相信libunwind包含对它的支持。

于 2012-08-24T11:19:03.810 回答
1

Assuming that I am linking with glibc (which I am doing), it looks as if I can solve this problem for practical purposes with the glibc global symbol __libc_stack_end:

extern void * __libc_stack_end;

void myfunction(void) {
  /* ... */
  off_t stack_hi = (off_t)__libc_stack_end;
  /* ... */
}
于 2011-12-25T00:55:17.747 回答
1

如果 argv 的地址是你想要的,为什么不在 main 中保存一个指向它的指针呢?
尝试展开堆栈将是高度不可移植的,即使你让它工作。
即使您确实设法返回堆栈,第一个函数的帧指针是否为 NULL 也不是很明显。堆栈上的第一个函数不会返回,而是调用系统调用退出,因此它的帧指针永远不会被使用。没有充分的理由将其初始化为 NULL。

于 2011-12-24T15:54:21.037 回答