函数调用栈
函数调用栈
以 32 位程序的寄存器为例
在学汇编时我们知道,函数调用通常有如下写法:
main:
push ebp
mov ebp, esp
...
sub esp, 20h ; 假设这中间的进栈操作使 esp 减了 20h
...
call fun
...
leave
ret
fun:
push ebp
mov ebp, esp
...
sub esp, 30h ; 假设这中间的进栈操作使 esp 减了 30h
...
leave
ret
对应 C 语言中的:
int fun()
{
...
return 0;
}
int main()
{
...
fun();
...
return 0;
}
关于理解函数的调用过程,我们主要要抓住 ESP、EBP、EIP 这三个寄存器的变化
注意:学习这里必须要分清地址和地址中存放的值,这两者是不一样的,不然容易懵
就像 C 语言中指针 p 指向的是一个内存单元,也就是一个地址;而
*p
指的是这个内存单元中存放的数据,是一个值
执行 main 函数
首先来看 main 函数:
main:
push ebp
mov ebp, esp
...
sub esp, 20h ; 假设这中间的进栈操作使 esp 减了 20h
...
call fun
...
leave
ret
在执行
push ebp
时,假设初始 ESP 指向0xffffce2c
地址处,首先esp = esp - 4
,再将原本 EBP 的值push
到 ESP 所指向的0xffffce28
地址处在执行
mov ebp, esp
时,将 ESP 的值赋值给 EBP,即:让 EBP 指向当前 ESP 所在地址,故此时 ESP 和 EBP 都指向0xffffce28
地址处在执行
sub esp, 20h
时,这里假设是在模拟函数中的各种进栈操作,使得 ESP 指向0xffffce08
地址处,而 EBP 不会随着进栈而改变
执行到这里,栈中的变化如下:
- 当执行
call fun
时,call
指令相当于:
push eip
jmp
EIP 就是 call fun
这条指令的下一条指令的地址
首先 esp = esp - 4
,再将 call fun
这条指令的下一条指令的地址填到 ESP 所指向的 0xffffce04
地址处
跳转到 fun 函数
然后来看 fun 函数:
fun:
push ebp
mov ebp, esp
...
sub esp, 30h ; 假设这中间的进栈操作使 esp 减了 30h
...
leave
ret
- fun 中的指令一直执行到
sub esp, 30h
都与 main 中开始时一样,不再赘述:
- 当 fun 函数的功能执行完后,会执行
leave
指令,leave
指令相当于:
mov esp, ebp ; 恢复栈指针
pop ebp ; 恢复基址指针
注意:
在函数开始时
push ebp mov ebp, esp
这两条指令其实可以合并为一个
enter
指令,他们是等价的
enter
指令与leave
指令的操作正好相反,enter
指令位于函数的开始,leave
指令位于函数的结尾,用来恢复栈帧
执行 mov esp, ebp
后,会将 EBP 的值赋值给 ESP,此时 ESP 会回到 EBP 所指向的地址 0xffffce00
处
执行 pop ebp
后,会先将 ESP 所指向的地址 0xffffce00
中存放的数据 0xffffce28
出栈送入 EBP,因此这时 EBP 会指向 0xffffce28
地址处;然后,由于出栈的 pop 操作使得 esp = esp + 4
,因此执行 pop ebp
后 ESP 指向 0xffffce04
地址处
- 接着执行
ret
指令,ret
指令相当于:
pop eip ; 这样写是方便理解,实际上不存在 pop eip 这个汇编指令
先将此时 ESP 所指向的地址 0xffffce04
中存放的 call fun
指令的下一条指令的地址出栈送入 EIP,然后由于出栈的 pop 操作使得 esp = esp + 4
,因此执行 pop ebp
后 ESP 指向 0xffffce08
地址处
EIP 中存放的是下一条要执行的指令的地址,由于这里修改了 EIP 的值为 call fun
指令的下一条指令的地址
因此这时程序会转而执行 call fun
指令的下一条指令,程序也就从 fun 函数回到了 main 函数中
到这里,栈又回到了 main 中 call fun
这句执行之前的样子
注意:栈中的数据出栈后仍然会保存在内存单元中,只是 ESP 的值改变了,计算机认为出栈后的数据已经不在栈里面了(计算机根据 EBP 和 ESP 来识别栈空间),但这个数据还是在内存单元中保存着,不会因为出栈而被清空
回到 main 函数
- 当 main 中剩余的操作执行完后,也会执行
leave
指令
这时候 ESP 回到最开始的 0xffffce2c
地址处,EBP 也回到原本 EBP 所在的地址处
最后通过 ret
指令回到 main 函数被调用时的位置,整个程序的主函数执行到这里就结束了