c - x86 汇编中的序言和推送被调用者保存寄存器

标签 c assembly x86 compiler-optimization cpu-registers

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

相关文章:

c - 在已编译的 C 程序中查找 asm 指令

运行时的 C 预处理器?

c - 在 Lua 之外使用 Lua 的哈希表是否可能/实用?

组装键盘IO口

c# - x64 和 x86 之间字节数组访问的巨大性能差异

assembly - 无符号 64 位到 double 转换 : why this algorithm from g++

c++ - 如何编写一个可重复的伪随机数生成器?

c - flite 中的 register_cmu_us_kal() 函数

assembly - 在 DOSBox 中打开文件句柄会清除文件的数据

c++ - 为什么在 32 位 ARM 平台上 SIGSEGV 的故障地址是 0x00000006?