函数调用栈

以 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
  1. 在执行 push ebp 时,假设初始 ESP 指向 0xffffce2c 地址处,首先 esp = esp - 4,再将原本 EBP 的值 push 到 ESP 所指向的 0xffffce28 地址处

  2. 在执行 mov ebp, esp 时,将 ESP 的值赋值给 EBP,即:让 EBP 指向当前 ESP 所在地址,故此时 ESP 和 EBP 都指向 0xffffce28 地址处

  3. 在执行 sub esp, 20h 时,这里假设是在模拟函数中的各种进栈操作,使得 ESP 指向 0xffffce08 地址处,而 EBP 不会随着进栈而改变

执行到这里,栈中的变化如下:

函数调用栈1.png

  1. 当执行 call fun 时,call 指令相当于:
push eip
jmp

EIP 就是 call fun 这条指令的下一条指令的地址

首先 esp = esp - 4,再将 call fun 这条指令的下一条指令的地址填到 ESP 所指向的 0xffffce04 地址处

函数调用栈2.png


跳转到 fun 函数

然后来看 fun 函数:

fun:
	push ebp
	mov ebp, esp
	...
	sub esp, 30h   ; 假设这中间的进栈操作使 esp 减了 30h
	...
	leave
	ret
  1. fun 中的指令一直执行到 sub esp, 30h 都与 main 中开始时一样,不再赘述:

函数调用栈3.png

  1. 当 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 地址处

函数调用栈4.png

  1. 接着执行 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 函数中

函数调用栈5.png

到这里,栈又回到了 main 中 call fun 这句执行之前的样子

注意:栈中的数据出栈后仍然会保存在内存单元中,只是 ESP 的值改变了,计算机认为出栈后的数据已经不在栈里面了(计算机根据 EBP 和 ESP 来识别栈空间),但这个数据还是在内存单元中保存着,不会因为出栈而被清空


回到 main 函数

  1. 当 main 中剩余的操作执行完后,也会执行 leave 指令

函数调用栈6.png

这时候 ESP 回到最开始的 0xffffce2c 地址处,EBP 也回到原本 EBP 所在的地址处

最后通过 ret 指令回到 main 函数被调用时的位置,整个程序的主函数执行到这里就结束了