我已经编写了一些代码来了解调用堆栈。我已经使用一些内联汇编来完成此操作,以便在堆栈上传递参数。我用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
**......**
答案 0 :(得分:2)
使用esp
和ebp
之间没有什么区别,只有esp
更改为push
,pop
,call
,{{ 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
加载到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
并不重要。使用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作为通用寄存器,则仍需要保存它。