基于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作为通用寄存器,还是需要保存。
使用 esp
和 ebp
之间没有真正的区别,只是 esp
随 push
、pop
、call
而变化, 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]
arg
的 0
值。
在方法 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设置为帧指针。
我写了一些代码来了解调用堆栈。我已经使用一些内联程序集完成了此操作,用于在堆栈上传递参数。我用 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作为通用寄存器,还是需要保存。
使用 esp
和 ebp
之间没有真正的区别,只是 esp
随 push
、pop
、call
而变化, 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]
arg
的 0
值。
在方法 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.
GCC 有选项 -fomit-frame-pointer 可以关闭基于 EBP 的帧。它在大多数优化级别默认情况下处于启用状态。您可以使用-O2 -fno-omit-frame-pointer
正常优化,除了仍然将EBP设置为帧指针。