探究函数的调用过程。
一、call、ret指令
call 标号
:将下一条指令的偏移地址入栈后,转到标号处执行指令。
ret
:将栈顶的值出栈,赋值给ip。
1 | assume cs:code, ds:data, ss:stack |
call和ret联合使用的作用其实就是高级语言中的函数调用(函数的本质)。
二、函数
函数的返回值其实是直接放到通用寄存器ax中的(也可以直接放在数据段中,但大部分情况下都是使用通用寄存器)。
2.1. 函数调用
1 | assume cs:code, ds:data, ss:stack |
2.2. 函数参数(栈平衡)
函数的参数也可以通过寄存器进行传递(使用寄存器速度会更快),但是寄存器不够的情况下怎么办?使用栈。
1 | assume cs:code, ds:data, ss:stack |
栈平衡是在函数调用结束后的操作,如果函数嵌套很多层,数据不断地入栈,也还是可能会引起栈顶超出(例如递归函数,这也是为什么递归函数一定要有递归边界)。
部分高级语言可以修改函数的栈平衡方式:
__cdecl
:外平栈,参数从右至左入栈。__stdcall
:内平栈,参数从右至左入栈。__fastcall
:内平栈,使用cx,dx寄存器分别传递前面两个参数(一般ax用来存放返回值),其他参数从右往左入栈。
2.3. 函数的局部变量
1 | assume cs:code, ds:data, ss:stack |
函数的调用流程(内存):
- push 参数
- push 函数的返回地址
- push bp (保留bp之前的值,方便以后恢复)
- mov bp, sp (用bp保留sp之前的值,方便以后恢复)
- sub sp,空间大小 (sp指针向上移动,分配空间给局部变量)
- 保护可能要用到的寄存器
- 使用CC(int 3)填充局部变量的空间
- 执行业务逻辑
- 恢复寄存器之前的值
- mov sp, bp (恢复sp之前的值)
- pop bp (恢复bp之前的值)
- ret (将函数的返回地址出栈,执行下一条指令)
- 恢复栈平衡 (add sp,参数所占的空间)
三、栈帧
栈帧(Stack Frame Layout)就是一个函数执行的环境,包括:参数、局部变量、返回地址等。
栈帧也可以从字面意思理解:函数操作基本上是在栈上进行的(入栈/出栈),一个栈中可能会压入多个函数,为了统一规范,使用帧代表函数。
如上图,bp到sp之间的内容就是一个栈帧(函数内部发生的相关操作)。当一个函数执行完成后就会被释放(栈帧被释放)。