为啥ebx保存在一个简单函数的栈帧中,调用gets?

Posted

技术标签:

【中文标题】为啥ebx保存在一个简单函数的栈帧中,调用gets?【英文标题】:Why is ebx saved in the stack frame of a simple function, calling gets?为什么ebx保存在一个简单函数的栈帧中,调用gets? 【发布时间】:2017-06-08 06:11:48 【问题描述】:

我正在尝试用 c 为学生编写缓冲区溢出练习。

通常堆栈帧由函数参数、返回地址、基址指针和局部变量组成。但我检测到,有时额外的寄存器会与基指针一起保存。我记得在课堂上 calee 保存的寄存器必须在使用之前保存。但是在某些情况下,C代码的编译会产生汇编,这显然是没有目的的保存和使​​用寄存器。请向我解释这种行为。

假设主函数

int main (int argc, char** argv) 
    func();

    return 0;

和功能

void func() 
    char buf[5];
    strcpy(buf,"AAAA");
    strcpy(buf,"BBBB");

如果我使用 gdb 调试生成的可执行文件

break func
run
info frame

一切正常,堆栈帧只包含 ebp 和 eip。

如果我使用

void func() 
    char buf[5];
    gets(buf);

我明白了

 Saved registers:
  ebx at 0xffffd1cc, ebp at 0xffffd1d0, eip at 0xffffd1d4

那么 ebx 是额外保存在栈帧中的吗?为什么?如果我跑

disas func

我明白了

Dump of assembler code for function func:
   0x56555730 <+0>: push   %ebp
   0x56555731 <+1>: mov    %esp,%ebp
   0x56555733 <+3>: push   %ebx
   0x56555734 <+4>: sub    $0x8,%esp
   0x56555737 <+7>: call   0x5655576e <__x86.get_pc_thunk.ax>
   0x5655573c <+12>:    add    $0x18c4,%eax
=> 0x56555741 <+17>:    lea    -0x9(%ebp),%edx
   0x56555744 <+20>:    push   %edx
   0x56555745 <+21>:    mov    %eax,%ebx
   0x56555747 <+23>:    call   0x56555590 <gets@plt>
   0x5655574c <+28>:    add    $0x4,%esp
   0x5655574f <+31>:    nop
   0x56555750 <+32>:    mov    -0x4(%ebp),%ebx
   0x56555753 <+35>:    leave  
   0x56555754 <+36>:    ret    
End of assembler dump.

所以 ebx 被保存了。好的。但它是用来做什么的? eax 在调用gets() 之前被移入ebx。但是后来就不用了。旧的 ebx 只是在离开和返回之前从堆栈中恢复。似乎没用。 顺便提一句。 call get_pc_thunk 的全部内容是什么?

类似的行为,如果我使用 printf 而不是 gets:

void func() 
    char buf[5];
    strcpy(buf, "AAAA");
    printf("%s",buf);

gdb 输出:

(gdb) info frame
Stack level 0, frame at 0xffffd1d8:
 eip = 0x56555741 in func (/home/mischa/stuff/test/test.c:35); saved eip = 0x56555779
 called by frame at 0xffffd1e0
 source language c.
 Arglist at 0xffffd1d0, args: 
 Locals at 0xffffd1d0, Previous frame's sp is 0xffffd1d8
 Saved registers:
  ebx at 0xffffd1cc, ebp at 0xffffd1d0, eip at 0xffffd1d4
(gdb) disas func
Dump of assembler code for function func:
   0x56555730 <+0>: push   %ebp
   0x56555731 <+1>: mov    %esp,%ebp
   0x56555733 <+3>: push   %ebx
   0x56555734 <+4>: sub    $0x8,%esp
   0x56555737 <+7>: call   0x56555780 <__x86.get_pc_thunk.ax>
   0x5655573c <+12>:    add    $0x18c4,%eax
=> 0x56555741 <+17>:    movl   $0x41414141,-0x9(%ebp)
   0x56555748 <+24>:    movb   $0x0,-0x5(%ebp)
   0x5655574c <+28>:    lea    -0x9(%ebp),%edx
   0x5655574f <+31>:    push   %edx
   0x56555750 <+32>:    lea    -0x17f0(%eax),%edx
   0x56555756 <+38>:    push   %edx
   0x56555757 <+39>:    mov    %eax,%ebx
   0x56555759 <+41>:    call   0x565555a0 <printf@plt>
   0x5655575e <+46>:    add    $0x8,%esp
   0x56555761 <+49>:    nop
   0x56555762 <+50>:    mov    -0x4(%ebp),%ebx
   0x56555765 <+53>:    leave  
   0x56555766 <+54>:    ret    
End of assembler dump.

谁能给我解释一下?

我使用 cmake 编译以下 CMakeLists.txt:

cmake_minimum_required (VERSION 2.8)

# projectname is the same as the main-executable
project(test)

# compile with 32 bit
add_definitions('-m32')

# Disable compiler optimization
add_definitions('-O0')

# include debugging information
add_definitions('-g')

# Align items on the stack to 4 bytes. This makes stuff easier.
# See https://***.com/questions/1061818/stack-allocation-padding-and-alignment
add_definitions('-mpreferred-stack-boundary=2')

# disable compiler buffer overflow protection
add_definitions('-z execstack -z norelro -fno-stack-protector')

# executable source code
add_executable(test test.c)

cmake 好像用 gcc。

【问题讨论】:

因为你禁用了优化? ebx 用作指向 GOT 的指针,但这实际上只与位置无关代码相关。从您的 cmake 内容中不清楚您是否如此编译它。要重现该行为,我必须将 -fPIC 命令行选项传递给 gcc 值得注意的是 strcpy(buf,"AAAA"); 被内联到 movl 后跟 movb,因此根本没有调用任何函数来执行它。 (请参阅上次反汇编中指向的代码。) 【参考方案1】:

您的编译器工具链已配置(可能由您的发行版)默认生成与位置无关的可执行文件 (PIE)。在 32 位 x86 上,为了让与位置无关的代码调用可能驻留在与调用者不同的库中的函数,调用模块的 GOT 的地址必须在调用时加载到 ebx;这是 ABI 要求。由于ebx 是 x86 ABI 中的调用保存寄存器,调用者必须先保存并稍后恢复它,然后再返回给自己的调用者。

我不久前就该主题写的这篇文章可能提供了丰富的信息:

https://ewontfix.com/18/

在最新版本的 gcc 上,新的 -fno-plt 选项可以通过内联来自 GOT 的负载而不是使用依赖于 ebx 的 PLT 来避免此问题。

【讨论】:

谢谢!如何禁用 PIE 创建?我想比较创建的代码。 @MichaelPalm:在编译时使用-fno-PIE,在链接时使用-no-pie-nopie,或者根据需要同时使用编译和链接。您是否需要-no-pie-nopie 取决于您使用的是GCC 6+,支持上游pie-by-default(前者)或源自Gentoo 的旧gcc 的default-pie 补丁(后者形式) . 为我工作 :-)

以上是关于为啥ebx保存在一个简单函数的栈帧中,调用gets?的主要内容,如果未能解决你的问题,请参考以下文章

frame pointer及其用途

Linux - 函数的栈帧

栈帧

栈帧

函数的调用过程(栈帧)

尾调用优化