C x86_64调用约定和堆栈帧

C x86_64调用约定和堆栈帧,c,stack,x86-64,calling-convention,backtrace,C,Stack,X86 64,Calling Convention,Backtrace,我试图理解GCC(4.4.3)为运行在Ubuntu Linux下的x86_64机器生成的可执行代码。特别是,我不明白代码是如何跟踪堆栈帧的。在过去,在32位代码中,我习惯于在几乎每个函数中看到这个“开场白”: push %ebp movl %esp, %ebp 然后,在功能结束时,也会出现“尾声” 或者干脆 leave ret 它完成了同样的事情: 将堆栈指针设置为当前帧的顶部,就在 回信地址 恢复旧的帧指针值 在64位代码中,正如我通过objdump反汇编所看到的,许多函数不遵循此约定

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

push %ebp
movl %esp, %ebp
然后,在功能结束时,也会出现“尾声”

或者干脆

leave
ret
它完成了同样的事情:

  • 将堆栈指针设置为当前帧的顶部,就在 回信地址
  • 恢复旧的帧指针值
在64位代码中,正如我通过objdump反汇编所看到的,许多函数不遵循此约定——它们不推送%rbp,然后将%rsp保存到%rbp,像GDB这样的调试器如何构建回溯

我的真正目标是试图在一个任意函数进一步进入程序的开始时找出一个合理的地址来考虑用户堆栈的顶部(最高地址),也许堆栈指针已经下移。例如,对于“top”,argv的原始地址是理想的——但我无法从main调用的任意函数访问它。我起初认为我可以使用旧的回溯方法:跟踪保存的帧指针值,直到保存的值为0——然后,之后的下一个值可以算作最高实用值。(这与获取argv的地址不同,但它可以——比如,找到_start或任何_start调用的堆栈指针值[例如,u libc_start_main])。现在,我不知道如何在64位代码中获取等效地址


谢谢。

我认为区别在于amd64更鼓励省略帧指针。报告第16页的脚注中写道:

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


我不知道GDB做什么。我假设当使用
-g
编译时,对象具有神奇的调试信息,允许GDB重构它所需要的内容。我想我没有在没有调试信息的64位机器上尝试过GDB。

如果argv的地址是您想要的,为什么不在main中保存指向它的指针呢?
即使您让堆栈正常工作,尝试展开堆栈也是非常不可移植的。

即使您设法返回堆栈,第一个函数的帧指针是否为空也不是很明显。堆栈上的第一个函数不返回,而是调用系统调用退出,因此它的帧指针永远不会被使用。没有充分的理由将其初始化为NULL。

假设我正在与glibc链接(我正在这样做),似乎我可以使用glibc全局符号\uuuu libc\u stack\u end解决此问题:

extern void * __libc_stack_end;

void myfunction(void) {
  /* ... */
  off_t stack_hi = (off_t)__libc_stack_end;
  /* ... */
}

GDB使用DWARF CFI进行退绕。对于使用-g编译的非压缩二进制文件,这将出现在.debug_info部分。对于精简的x86-64二进制文件,.eh_frame部分中有展开信息。这在第56页第3.7节中有定义。自己处理这些信息相当困难,因为解析DWARF非常复杂,但我相信它包含对它的支持。

Hm确实如此。而且不仅仅是使用
-fomit帧指针
。您是否尝试过-fno省略帧指针?你能用这个标志编译其他代码吗?libunwind的源代码可能很有用。谢谢所有这三条注释。我认为这里的问题是,我的库实际上是GCC libgomp的一个修改版本,所以我使用Gnu构建系统构建它,并尽可能避免更改默认值。我相信GCC在默认情况下使用-O2编译,我很确定它包含-fomit帧指针。发帖后,但在我看到Firoze的评论之前,我确实看了glibc的debug/backtrace.c的代码,这让我开始寻找“libc\u stack\u end”,这就是我如何找到一个稍微合理且通用的解决方案的原因。
sub$xx,%esp
是序言的一部分。它在堆栈上保留空间。尾声添加$xx,%esp返回堆栈指针,指向需要弹出的对象。(或者在简单的情况下,您可以在不调整ESP的情况下使用它。)我使用x86-64的经验表明,调试器使用额外的信息来了解堆栈帧大小,这保存了指令,但使调试和展开变得困难。是的,正如我所怀疑的。当编译可执行文件时没有调试信息,这一切都会停止吗?谢谢。ABI中的建议确实说明了发生了什么——但它仍然让我想知道如何解决我的问题。粗略地说,我需要从调用图中main之后的任意函数中获取当执行进入main时堆栈指针的值。该值可以高于main堆栈顶部框架的实际值,只要它位于进程堆栈内,但越靠近main堆栈顶部框架越好。谢谢。唉,不行,我无法在main中保存指针。我正在编写一个用户级库来链接任意代码,因此我不能接触原始代码(除了添加一个#include)——或者如果可能的话,我宁愿避免这样做。至于你的第二点,我的印象是,像Linux这样的内核确实遵循这样的惯例,即在将控制权传递给用户进程之前,将帧指针设置为NULL,这正是出于这个目的。但也许这只是一个古老的惯例,并非所有系统都遵循它。我很确定它总是在
.eh_frame
部分,这就是为什么它在剥离后仍然存在的原因。按照您描述的方式,
strip
必须找到该信息
extern void * __libc_stack_end;

void myfunction(void) {
  /* ... */
  off_t stack_hi = (off_t)__libc_stack_end;
  /* ... */
}