汇编如下
main:
....
call fun....
....
....
fun:
push ebp
mov ebp,esp
call fun1
....leave
ret
一图胜千言,图中的是一条指令执行完后栈的状态
初始状态 call fun push ebp mov ebp,esp
push 0x0012 push 0x0034 call fun1 push ebp mov ebp,esp
接下来是函数依次返回
mov esp,ebp pop ebp ret (fun1) pop pop
mov esp,ebp (这里ebp的值是0xF0) pop ebp ret (main)
1.call fun时,cpu将call fun的下一条指令内存地址eip+n(n视指令长度而定)入栈,之后esp-4->esp,(一个内存地址为4B)
2.push ebp, ebp内容入栈,之后esp<-(esp-4),(一个内存地址为4B)
3.mov ebp,esp, ebp<-esp,开始此函数的栈帧,这条汇编执行完之后,下条汇编之前,ebp中的值是和esp中的一样的,但是esp的值会随着后面的对栈的操作push、pop而变化。
这里的ebp就可以理解成是esp的一个拷贝。可以这么理解,ebp中存储的内存地址是此函数的栈底(不能在这个地址上存数据,要从ebp-4开始存数据)。接下来,我们可以向这个栈中存储函数中的参数了,或者再调用子函数call fun1。push,pop.....
4.leave的作用相当于以下语句:
mov esp,ebppop ebp
a. mov esp,ebp,esp<-ebp,恢复之前拷贝的esp值,一般来说在这一句之前esp==ebp,不排除可能push和pop不对称导致esp,ebp寄存器中内容不一致。
b. pop ebp,呃,恢复之前的存储的ebp值。
5.ret 函数返回,cpu从栈中弹出一个4B的数并装入eip中,并执行。对应于步骤1,这就是最刚开始压入的下条指令地址。
总结:
从call fun 开始,栈中先后压入了下条指令地址,ebp的值,以及函数执行时压入的各个值。
函数结束时,栈中弹出的依次是函数执行时压入的值,ebp的值,下条指令地址。
push ebp
mov ebp,esp
这两句和
leave
就构成了栈框架的建立和释放。
问:为什么ebp的值显得如此重要呢?
答:从第3步可知,ebp构成了一个函数的栈底,我们可以使用$(ebp-n)来方便的引用函数fun中的变量。
而且$(ebp)是调用此函数fun的函数的栈底,我们可以想象再fun函数中又执行了一个子函数foo,同样会建立栈帧....
通过ebp我们可以方便的回溯查看各个函数的详细情况。