基于ebp的寻址和esp寻址之间的区别

Difference between ebp based addressing and esp addressing

我写了一些代码来了解调用堆栈。我已经使用一些内联程序集完成了此操作,用于在堆栈上传递参数。我用 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 , %esp\n"
                 "fstpl (%esp)\n");
    myfun();
    asm volatile("addl , %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   [=11=]x8048384,-0xc(%ebp)

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

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

asm volatile("addl , %esp\n");
    0x080483dc <+46>:     add    [=11=]x8,%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   [=12=]x80483ed,0x4(%esp)

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

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

asm volatile("addl ,%esp\n");
    0x0804842d <+41>:   add    [=12=]x8,%esp
    **......**

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

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

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

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

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

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

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

使用 espebp 之间没有真正的区别,只是 esppushpopcall 而变化, ret,这有时会让人很难知道某个局部变量或参数在栈中的位置。这就是 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] 将保存 return 地址,并且 [esp+4] arg0 值。

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

    mov eax, [ebp + 4 + 4]

因为 [ebp + 0] 保留了 ebp 的前一个值(来自 push ebp), 和 [ebp + 4]esp 的原始值),保存 return 地址。

但我们也可以使用 esp:

引用参数
   mov eax, [esp + 16 + 4 + 4]

我们添加 16 因为 sub esp, 16,然后 4 因为 push ebp,另一个 4 跳过 return地址,到达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 更改时,这些说明也必须更改。所以,最后,使用 esp 还是 ebp 都没有关系。使用 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 , %esp\n"
                 "fstpl (%esp)\n"
                 "call  fun\n"
                 "addl , %esp\n");
}

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

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

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

在 C:

extern double my_call();

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

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

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

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

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

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

后在堆栈上重新分配 space

GCC 有选项 -fomit-frame-pointer 可以关闭基于 EBP 的帧。它在大多数优化级别默认情况下处于启用状态。您可以使用-O2 -fno-omit-frame-pointer正常优化,除了仍然将EBP设置为帧指针。