x86汇编和推送Callee Save寄存器的序言

时间:2015-10-24 16:16:45

标签: c assembly x86 compiler-optimization cpu-registers

在研究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

分析的

1 个答案:

答案 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的默认值。

如果没有该信息,您可以扫描堆栈,查找正确范围内的值作为返回地址。