【问题标题】:Difference between ebp based addressing and esp addressing基于 ebp 的寻址和 esp 寻址之间的区别
【发布时间】:2015-11-30 15:03:46
【问题描述】:

我编写了一些代码来了解调用堆栈。我已经使用一些内联程序集来完成此操作,用于在堆栈上传递参数。我用 gcc 4.1.2(在 CentOS5.4 上)编译它,它运行良好,然后我用 gcc 4.8.4(在 Ubuntu14.04.3 上)编译它并运行程序,但它总是崩溃。

我发现变量的引用方式存在差异。局部变量使用 gcc 4.1.2(CentOS5.4) 中的 EBP 寄存器寻址,而局部变量使用 gcc 4.8.4(Ubuntu14.04.3) 中的 ESP 寄存器寻址。这似乎是它崩溃的原因。

我的问题是,我如何控制 gcc 是使用 EBP 还是 ESP?还有,它们有什么区别?

这是 C 代码:

double fun(double d) {
    return d;
}

int main(void) {
    double a = 1.6;
    double (*myfun)() = fun;
    asm volatile("subl $8, %esp\n"
                 "fstpl (%esp)\n");
    myfun();
    asm volatile("addl $8, %esp\n");
    return 0;
}

这是 gcc 4.1.2 中的程序集,它可以工作

int main(void) {
    **......**

double a = 1.6;
    0x080483bf <+17>:     fldl   0x80484d0
    0x080483c5 <+23>:     fstpl  -0x18(%ebp)

double (*myfun) () = fun;
    0x080483c8 <+26>:     movl   $0x8048384,-0xc(%ebp)

asm volatile("subl $8, %esp\n"
             "fstpl (%esp)\n");                 
    0x080483cf <+33>:     sub    $0x8,%esp
    0x080483d2 <+36>:     fstpl  (%esp)        

myfun();
    0x080483d5 <+39>:     mov    -0xc(%ebp),%eax
    0x080483d8 <+42>:     call   *%eax
    0x080483da <+44>:     fstp   %st(0)

asm volatile("addl $8, %esp\n");
    0x080483dc <+46>:     add    $0x8,%esp

    **......**

这是 gcc 4.8.4 中的程序集。这就是崩溃的原因:

int main(void) {
    **......**

double a = 1.6;
    0x0804840d <+9>:    fldl   0x80484d0
    0x08048413 <+15>:   fstpl  0x8(%esp)

double (*myfun)() = fun;
    0x08048417 <+19>:   movl   $0x80483ed,0x4(%esp)

asm volatile("subl $8,%esp\n"
             "fstpl (%esp)\n");
    0x0804841f <+27>:   sub    $0x8,%esp
    0x08048422 <+30>:   fstpl  (%esp)      

myfun();
    0x08048425 <+33>:   mov    0x4(%esp),%eax
    0x08048429 <+37>:   call   *%eax
    0x0804842b <+39>:   fstp   %st(0)

asm volatile("addl $8,%esp\n");
    0x0804842d <+41>:   add    $0x8,%esp
    **......**

【问题讨论】:

  • 我昨天粘贴了错误的代码,很抱歉浪费了您所有的时间。代码现在更新。

标签: c gcc disassembly


【解决方案1】:

使用espebp 并没有真正的区别,只是esp 会随着pushpopcallret 而变化,这有时会让人很难知道某个局部变量或参数位于堆栈中。这就是为什么ebp 会加载esp,以便有一个稳定的参考点来引用函数参数和局部变量。

对于这样的函数:

int foo( int arg ) {
    int a, b, c, d;
    ....
}

通常会生成以下程序集:

# using Intel syntax, where `mov eax, ebx` puts the value in `ebx` into `eax`
.intel_syntax noprefix

foo:                                                          
    push ebp          # preserve
    mov ebp, esp      # remember stack
    sub esp, 16       # allocate local variables a, b, c, d

    ...

    mov esp, ebp      # de-allocate the 16 bytes
    pop ebp           # restore ebp
    ret

调用此方法 (foo(0)) 会生成如下内容:

    pushd 0           # the value for arg; esp becomes esp-4
    call  foo         
    add   esp, 4      # free the 4 bytes of the argument 'arg'.

call 指令执行之后,在foo 方法的第一条指令执行之前,[esp] 将保存返回地址,[esp+4] 将保存0arg 值。

在方法foo中,如果我们想将arg加载到eax(在...) 我们可以使用:

    mov eax, [ebp + 4 + 4]

因为[ebp + 0] 持有ebp 的先前值(来自push ebp), 和[ebp + 4]esp的原始值),保存返回地址。

但我们也可以使用esp引用参数:

   mov eax, [esp + 16 + 4 + 4]

我们添加16,因为sub esp, 16,然后4,因为push ebp,另一个4跳过返回地址,到达arg

类似地访问四个局部变量可以通过两种方式完成:

  mov eax, [ebp -  4]
  mov eax, [ebp -  8]
  mov eax, [ebp - 12]
  mov eax, [ebp - 16]

 mov eax, [esp + 12]
 mov eax, [esp +  8]
 mov eax, [esp +  4]
 mov eax, [esp +  0]

但是,每当esp 更改时,这些说明也必须更改。因此,最终使用espebp 都没有关系。使用esp 可能更有效,因为您不必使用push ebp; mov ebp, esp; ... mov esp, ebp; pop ebp


更新

据我所知,无法保证您的内联汇编能够正常工作:Ubunty 上的 gcc 4.8.4 优化了 ebp 的使用并使用 esp 引用所有内容。它不知道你的内联汇编修改了esp,所以当它试图调用myfun()时,它会从[esp + 4]获取它,但它应该从[esp + 4 + 8]获取它。

这是一种解决方法:不要在使用内联汇编进行堆栈操作的函数中使用局部变量(或参数)。要绕过将double fun(double) 转换为double fn() 的问题,直接在汇编中调用函数:

void my_call() {     
    asm volatile("subl $8, %esp\n"
                 "fstpl (%esp)\n"
                 "call  fun\n"
                 "addl $8, %esp\n");
}

int main(void) {
    my_call();
    return 0;
}

您也可以将my_call 函数放在单独的.s(或.S)文件中:

.text
.global my_call
my_call:
    subl  $8, %esp
    fstpl (%esp)
    call  fun
    addl  $8, %esp
    ret

在 C 中:

extern double my_call();

您也可以将fun 作为参数传递:

extern double my_call( double (*myfun)() );
...
     my_call( fun );

.text
.global my_call
my_call:
    sub  $8, %esp
    fstp (%esp)
    call *12(%esp)
    add  $8, %esp
    ret

【讨论】:

  • 感谢您的回答,我昨天粘贴了错误的代码,很抱歉浪费您的时间。我只是想通过内联汇编传递参数,所以我在调用函数之前展开堆栈,然后我恢复堆栈。我改了内联程序集中的esp,所以如果myfunesp寻址,肯定是地址错误,但是如果 ebp 是基址寄存器,一切OK!
  • 是的,我想知道sub, fstp, add :-) 我添加了对答案的更新,希望对您有用!
【解决方案2】:

大多数编译器都会创建基于 EBP 的堆栈帧。或者,至少他们曾经这样做过。这是大多数人都被教导的使用EBP作为固定基帧指针的方法。

一些编译器会创建基于 ESP 的堆栈帧。原因很简单。它释放了 EBP 以用于其他用途,并消除了设置和恢复堆栈帧的开销。显然更难可视化,因为堆栈指针可以不断变化。

您遇到的问题可能是因为您正在调用使用 stdcall 调用约定的 API,当它们返回给调用者时,最终会无意中破坏您的堆栈。 EBP 必须由被调用者通过 cdecl 和 stdcall 函数保存。但是,stdcall 例程将使用ret 4 清理堆栈,从而缩小其大小。调用者必须补偿这些类型的错误,并在调用返回后重新分配堆栈空间。

GCC 有选项 -fomit-frame-pointer 将关闭基于 EBP 的帧。它在大多数优化级别默认开启。您可以使用-O2 -fno-omit-frame-pointer 进行正常优化,但仍将EBP设置为帧指针。

【讨论】:

    【解决方案3】:

    如果您想了解堆栈和参数传递约定 (ABI),建议您查看编译器生成的程序集。您可以在此站点上以交互方式执行此操作:http://gcc.godbolt.org/#

    尝试各种参数类型、可变参数函数、传递和返回浮点数、双精度数、不同大小的结构......

    使用内联汇编处理堆栈太困难且不可预测。它可能会在很多方面失败,你不会学到任何有用的东西。

    【讨论】:

      【解决方案4】:

      ebp 通常用于帧指针。使用帧指针的函数的第一条指令是

              push    ebp           ;save ebp
              mov     ebp,esp       ;ebp = esp
              sub     esp,...       ;allocate space for local variables
      

      那么参数和局部变量是从 ebp 的 +/- 偏移量

      大多数编译器都有不使用帧指针的选项,在这种情况下 esp 用作基指针。如果非帧指针代码使用ebp作为通用寄存器,还是需要保存的。

      【讨论】:

        猜你喜欢
        • 2017-01-08
        • 2011-02-13
        • 1970-01-01
        • 2019-02-27
        • 2015-05-09
        • 2019-11-05
        • 1970-01-01
        • 2015-04-11
        • 2016-06-05
        相关资源
        最近更新 更多