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

时间:2015-11-30 15:03:46

标签: c gcc disassembly

我已经编写了一些代码来了解调用堆栈。我已经使用一些内联汇编来完成此操作,以便在堆栈上传递参数。我用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
    **......**

4 个答案:

答案 0 :(得分:2)

使用espebp之间没有什么区别,只有esp更改为pushpopcall,{{ 1}},有时候很难知道某个局部变量或参数在堆栈中的位置。这就是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] [esp+4] {1}} {1}}的值。

在方法0中,如果我们想将arg加载到fooarg) 我们可以使用:

eax

因为...保留了 mov eax, [ebp + 4 + 4] 的先前值(来自[ebp + 0]), 和ebppush 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并不重要。使用esp可能更有效,因为您没有ebp

<小时/> 的更新

据我所知,没有办法保证你的内联汇编能够正常工作:Ubunty上的gcc 4.8.4优化了esp的使用,并用push ebp; mov ebp, esp; ... mov esp, ebp; pop ebp引用了所有内容。它不知道您的内联程序集会修改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

并在C:

.S

你也可以传递.text .global my_call my_call: subl $8, %esp fstpl (%esp) call fun addl $8, %esp ret 作为参数:

extern double my_call();

fun

答案 1 :(得分:1)

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

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

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

答案 2 :(得分:1)

大多数编译器都会创建基于EBP的堆栈帧。或者,至少他们习惯了。这是大多数人使用EBP作为固定基帧指针的方法。

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

您遇到的问题可能是因为您正在调用使用stdcall调用约定的API,最终会在返回调用方时无意中丢弃您的堆栈。必须通过cdecl和stdcall功能保留被调用者的EBP。但是,stdcall例程将清理堆栈,从而缩小其大小。被调用者必须补偿这些类型的事故,并在呼叫返回后重新分配堆栈上的空间。

GCC有-fomit-frame-pointer选项,它将关闭基于EBP的帧。我不知道相反的选择是什么。

答案 3 :(得分:0)

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

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

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

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