函数调用栈

以 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 函数被调用时的位置,整个程序的主函数执行到这里就结束了