在研究 C 代码的反汇编时,这让我印象深刻。通常,在保存帧指针后的函数汇编中,我们压入被调用者保存的寄存器并在返回之前将它们恢复。 x86 ABI 告诉我们哪些寄存器是被调用者/调用者保存的。然而,当我看到编译器在组装这些函数时表现不同时,我的问题就开始了。例如:
Case 1
(gdb) disassemble EVP_CipherInit_ex
Dump of assembler code for function EVP_CipherInit_ex:
0xb1258044 <+0>: push %ebp
0xb1258045 <+1>: mov %esp,%ebp
0xb1258047 <+3>: push %edi
0xb1258048 <+4>: push %esi
0xb1258049 <+5>: push %ebx
Case 2
(gdb) disassemble FIPS_mode
Dump of assembler code for function FIPS_mode:
0xb12614c4 <+0>: push %ebp
0xb12614c5 <+1>: mov %esp,%ebp
0xb12614c7 <+3>: push %ebx
0xb12614c8 <+4>: sub $0x4,%esp
Case 3
(gdb) disassemble OPENSSL_init
Dump of assembler code for function OPENSSL_init:
0xb124fae4 <+0>: push %ebp
0xb124fae5 <+1>: mov %esp,%ebp
0xb124fae7 <+3>: push %ebx
0xb124fae8 <+4>: sub $0x4,%esp
Case 4
(gdb) disassemble FIPS_module_mode
Dump of assembler code for function FIPS_module_mode:
0xb117dfdc <+0>: push %edi
0xb117dfdd <+1>: push %esi
0xb117dfde <+2>: push %ebx
0xb117dfdf <+3>: sub $0x10,%esp
Q1。在前三种情况下,我们保存了帧指针 ebp 和另一个通用寄存器 ebx 但其余的事情有所不同。编译器如何识别推送哪些和避免哪些?这是玩游戏的某种优化吗?对此的任何指示都会非常有帮助。
Q2。在FIPS_module_mode 的反汇编中,我们甚至没有保存帧指针ebp。我知道我们可以通过使用编译器选项对其进行优化来节省空间。我的兴趣在于了解缺少帧指针部分是由于显式编译器优化还是存在有助于确定这一点的某些其他参数。
Q3。像 gdb 这样的调试器如何检测到对于特定函数,在情况 4 中,核心转储中省略了帧指针?
发布的函数原型(prototype)为:
int FIPS_module_mode(void);
void OPENSSL_init(void);
int EVP_CipherInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher,
ENGINE *impl, const unsigned char *key,
const unsigned char *iv, int enc);
int FIPS_mode(void);
这是在 NetBSD5 上运行的,由 gdb 分析的 coredump
最佳答案
Q1。 gcc(与其他优化编译器一样)编译整个函数,使用尽可能多的有用的被调用者保存的寄存器,但仅在需要时使用。在 gcc 完成对整个函数(或编译单元或程序)的优化之前,不会生成 asm,因此 gcc 知道在发出序言时需要多少寄存器。
它使用的任何被调用者保存的寄存器都在序言中被压入并在尾声中弹出。在某些函数中,它使用被调用者保存的寄存器只是因为它用完了它可以使用而无需保存的调用者保存的寄存器(因此,仅用于寄存器总数)。在非叶函数中,被调用者保存的寄存器对于在 调用
中将某些内容保存在寄存器中也很有用,gcc 必须假定它会破坏所有调用者保存的寄存器。
看起来如果gcc只需要一个调用保留寄存器,它会选择ebx
。不过,如果它想使用 rep movs
或其他东西,它可能只使用 (save/restore) esi/edi
。
gcc 的行为有时是次优的:一些函数有一个不使用很多局部变量的快速路径,但 gcc 发出的代码在检查之前压入,因此必须再次弹出。 Linux 内核将一些函数提示为 noinline
以尽可能快地保持快速路径,但代价是在慢速路径中进行额外的函数调用。据我了解,这是 Linux 中 noinline 的主要原因,而不是代码大小膨胀。
Q2。是的,看起来 FIPS_module_mode
是用 -fomit-frame-pointer
编译的(这是较新的 gcc 中的默认设置)。如果您正在查看一个库,Makefile(或任何构建系统)可以很容易地使用不同的选项构建不同的文件。或者,即使使用 -fomit-frame-pointer
,具有可变大小局部变量的函数也会构建堆栈框架。例如
int func(int c) { int tmp[c]; ...; }
第 3 季度。我很好奇现代调试器如何在没有帧指针的情况下进行堆栈回溯。 This blog post sheds some light : .eh_frame_hdr
数据部分中有调试信息(未标记为“调试”信息,因此通常不会被剥离,因此您可以在调用堆栈通过函数时回溯剥离图书馆或其他东西)。使用 objdump -h
查看该部分的大小。如果/当抛出运行时异常时,该数据还用于展开堆栈,因此这是不剥离它的另一个原因。
在正常情况下(除非出现破坏堆栈的错误,或弄乱堆栈指针的编译器/asm 编程错误),它在没有帧指针的情况下工作,所以 -fomit-frame-pointer
是自 4.6 以来 gcc 中的默认值,即使对于 x86 也是如此。我认为这是 x86-64 的默认设置。
如果没有该信息,您可以扫描堆栈以查找在正确范围内的值作为返回地址。
关于c - x86 汇编中的序言和推送被调用者保存寄存器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33320380/