x86_64 调用约定和堆栈帧
Posted
技术标签:
【中文标题】x86_64 调用约定和堆栈帧【英文标题】:x86_64 calling conventions and stack frames 【发布时间】:2012-01-27 08:41:43 【问题描述】:我试图理解 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 这样的调试器如何构建一个回溯?
我的真正目标是尝试找出一个合理的地址,当执行到达程序中任意函数的开头时,将其视为用户堆栈的顶部(最高地址),堆栈指针可能在该处下移了。例如,对于“top”,argv 的原始地址是理想的——但我无法从 main 调用的任意函数访问它。起初我以为可以使用旧的回溯方法:追逐保存的帧指针值,直到保存的值为 0——然后,之后的下一个可以算作最高实用值。 (这与获取 argv 的地址不同,但它会做——例如,找出 _start 或任何 _start 调用的堆栈指针值[例如,__libc_start_main]。)现在,我不知道如何获取 64 位代码中的等效地址。
谢谢。
【问题讨论】:
确实。不仅仅是-fomit-frame-pointer
。
你试过-fno-omit-frame-pointer吗?你能用那个标志编译这个其他代码吗?
libunwind
的源代码可能有用。
感谢所有这三个 cmets。我认为这里的问题是我的库实际上是 GCC libgomp 的修改版本,所以我使用 Gnu 构建系统构建它,并尽可能避免更改默认值。我相信 GCC 默认使用 -O2 编译,我很确定它包括 -fomit-frame-pointer。发布后,但在看到 Firoze 的评论之前,我确实查看了 glibc 的 debug/backtrace.c 的代码,这就是我去寻找 __libc_stack_end 的原因,这就是我找到一个有点合理和通用的解决方案的方法。
sub $xx, %esp
是序幕的一部分。它在堆栈上保留空间。结语确实add $xx, %esp
将堆栈指针返回到指向需要弹出的东西。 (或者在简单的情况下leave
includes mov %ebp, %esp
,所以你可以在不先调整 ESP 的情况下使用它。)
【参考方案1】:
我认为区别在于在 amd64 中更鼓励省略帧指针。 abi 第 16 页的脚注说
可以通过使用 %rbp 作为堆栈帧的帧指针来避免常规使用 %rsp(堆栈指针)索引到堆栈帧。这种技术在序言和尾声中保存了两条指令,并使一个额外的通用寄存器 (%rbp) 可用。
我不知道 GDB 是做什么的。我假设当使用-g
编译时,对象具有神奇的调试信息,允许 GDB 重建它需要的内容。我认为我没有在没有调试信息的情况下在 64 位机器上尝试过 GDB。
【讨论】:
我使用 x86-64 的经验表明,调试器使用附加信息来了解堆栈帧大小,这样可以节省指令,但会使调试和展开很痛苦。 是的,正如我所怀疑的那样。当可执行文件在没有调试信息的情况下编译时,这一切都会失败吗? 谢谢。 ABI 中的建议确实说明了正在发生的事情——但它仍然让我想知道如何解决我的问题。我需要——粗略地说——当执行进入 main 时堆栈指针的值,来自调用图中 main 之后的任意函数。该值可以高于实际的main栈帧顶部的值,只要它在进程的栈中,但越靠近main栈帧的顶部越好。【参考方案2】:如果 argv 的地址是你想要的,为什么不在 main 中保存一个指向它的指针呢? 尝试展开堆栈将是高度不可移植的,即使您让它工作。 即使您确实设法返回堆栈,第一个函数的帧指针是否为 NULL 也不是很明显。堆栈上的第一个函数不会返回,而是调用系统调用退出,因此它的帧指针永远不会被使用。没有充分的理由将其初始化为 NULL。
【讨论】:
谢谢。唉,不,我无法将指针保存在 main 中。我正在编写一个用户级库来链接任意代码,因此我无法触及原始代码(添加#include 除外)——或者如果可能的话,我宁愿避免这样做。至于您的第二点,我的印象是,Linux 等内核确实遵循将帧指针设置为 NULL 的约定,然后再将控制权传递给用户进程,正是为此目的。但也许这只是一个旧的约定,并非所有系统都遵循。【参考方案3】:假设我正在链接 glibc(我正在这样做),看起来我可以使用 glibc 全局符号 __libc_stack_end 来解决这个问题:
extern void * __libc_stack_end;
void myfunction(void)
/* ... */
off_t stack_hi = (off_t)__libc_stack_end;
/* ... */
【讨论】:
【参考方案4】:GDB 使用 DWARF CFI 来展开。对于使用 -g 编译的未剥离的二进制文件,这将位于 .debug_info 部分。对于剥离的 x86-64 二进制文件,.eh_frame 部分中有展开信息。这在x86-64 ABI,第 3.7 节,第 56 页中定义。自己处理这些信息非常困难,因为解析 DWARF 非常复杂,但我相信libunwind 包含对它的支持。
【讨论】:
我很确定它总是在.eh_frame
部分,这就是为什么剥离后它仍然存在。你描述它的方式,strip
必须在.debug_info
中找到该信息并将其复制到.eh_frame
,并且 unwind 必须检查这两个位置......
.debug_info
包含有关堆栈帧内的局部变量的额外信息,但.eh_frame
始终包含足够的信息来展开堆栈。 (即每个堆栈帧的大小,以及被调用者保存的寄存器保存在哪里,但不是哪个变量存储在哪里。)以上是关于x86_64 调用约定和堆栈帧的主要内容,如果未能解决你的问题,请参考以下文章