在研究C代码的拆解时,这让我很震惊。通常,在保存帧指针之后的函数组合中,我们推送被调用者保存的寄存器并在返回之前将它们恢复。 x86 ABI告诉我们哪些寄存器是callee / caller save。但是,当我看到编译器在组装这些函数时表现不同时,我的问题就出现了。例如:
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中,核心转储中是否省略了帧指针?
发布的功能原型是:
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上运行,而coredump是由gdb
分析的答案 0 :(得分:3)
<强> Q1 即可。 gcc(和其他优化编译器一样)编译整个函数,使用尽可能多的被调用者保存的寄存器,但只需要多少。在gcc完成优化整个函数(或编译单元或程序)之前,不会生成asm,因此gcc知道在发出序言时需要多少个寄存器。
它使用的任何被调用者保存的寄存器都会在序言中被推送并在结尾中弹出。在某些函数中,它使用被调用者保存的寄存器,因为它用尽了调用者保存的寄存器,它可以使用而无需保存(因此,仅用于寄存器总数)。在非叶子函数中,被调用者保存的寄存器对于在call
中保存寄存器中的内容也很有用,gcc必须假定所有调用者保存的寄存器都是clobbers。
如果gcc只需要一个调用保留寄存器,它就会选择ebx
。如果它想要使用esi/edi
或其他东西,它可能只使用(保存/恢复)rep movs
。
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]; ...; }
<强> Q3 即可。我很好奇现代调试器如何在没有帧指针的情况下进行堆栈回溯。 This blog post sheds some light:.eh_frame_hdr
数据部分中有调试信息(未标记为“debug”信息,因此通常不会被剥离,因此您可以在调用堆栈通过函数时进行回溯剥离的图书馆或其他东西)。使用objdump -h
查看该部分的大小。如果/当抛出运行时异常时,该数据也用于展开堆栈,这是不剥离它的另一个原因。
在正常情况下(除了破坏堆栈的错误,或编译器/ asm编程错误弄乱堆栈指针),它没有帧指针,因此-fomit-frame-pointer
是gcc中的默认值,因为4.6,甚至对于x86。我认为这是x86-64的默认值。
如果没有该信息,您可以扫描堆栈,查找正确范围内的值作为返回地址。