汇编基础四 --函数调用与堆栈平衡
函数
将高级语言中定义的函数,被编译位汇编代码执行时,会被编译为一堆指令的集合,用来实现特定的功能,并获得执行后的结果。如果不关注函数中的具体实现,就可以将一个函数看作一个整体,函数调用过程等同于执行了一个操作,只不过这个操作比较复杂而已。
汇编中实现一个函数可以使用JMP 和 CALL 指令完成。
函数是一堆完成特定功能的指令集,这些指令集同样需要按照顺序依次执行,所以只要知道函数执行的第一条指令的地址(函数的首地址),函数将会依次执行这些指令,完成函数。
计算两数之和
我们使用C语言实现简单的加法函数
函数分为带参函数和无参函数,对于无参函数,直接使用CALL指令跳转到函数首地址然后开始执行即可,并且CALL指令会将下一行指令地址入栈保存,用于函数结束返回时跳回到原地址。
// 函数体 首地址 指令 00401019 ADD EAX, 4
0040101D RETN // 将栈中的地址 00401038 取出, 赋值给ESI,下次执行回到00401038地址处执行
... ... 00401034 CALL 00401019 // 将下一行指令地址 00401038存入栈空间,然后执行 00401019地址处的函数
00401038 ...
如果函数带参数,我们使用C语言实现简单的加法函数为例
int add(int x, int y) { int z = 0; z = x + y; return z; } void main() { int x = 1; int y = 2; int z; z = add(x, y) }
该函数执行时可以分为以下的步骤。
- add函数需要x, y两个参数至,所以在调用函数前,将参数push 到栈中,为了在函数中使用,多个参数多次执行push即可。 push 1, push 2
- 执行CALL 指令,该指令将下一指令地址存入栈中,并将函数首地址写入EIP寄存器,下一次将会执行函数体中的指令。
- 函数中的指令开始执行,但是执行我们加法逻辑前,会执行一些操作
- 首先进行堆栈提升,并开启一段缓冲区空间用于储存函数中的临时变量,使用 SUB ESP, 40h指令,将ESP向上偏移40个数据宽度。堆栈提升后,使用EBP位置进行寻址。
- 将三个寄存器中的值写入栈中,函数执行的过程中会覆盖寄存器的值,保存在栈中后,函数结束时可以从该处恢复。
- 开始执行函数的主逻辑
- 获取栈中 x,y的值进行加法操作。通过EBP寄存器中保存的位置,函数参数在栈中的位置分别为,EBP+8和EBP+C(16进制)
- 执行加法操作
- MOV EAX, 0 -- EAX寄存器中的值设置为0
- ADD EAX, ptr ds:[ EBP+8 ] -- EAX寄存器中的值 + 内存地址 EBP +8位置值,结果保存在EAX中
- ADD EAX, ptr ds:[ EBP+C] -- EAX寄存器中的值 + 内存地址 EBP +C 位置值,结果保存在EAX中
- 执行结束后,结果被保存到了EAX寄存器中。
- 函数执行结束,开始恢复堆栈,清除函数栈中的信息,保证函数执行前的堆栈信息和函数执行后的堆栈相同,也就是满足堆栈平衡。
- 恢复三个寄存器edi, esi, ebx中值,pop edi, pop esi, pop ebx
- 清除缓冲区,这里的清除并不删除其中的数据,将ESP指针恢复即可,这样缓冲区空间会被作为未使用区域,新的数据写入时候,将原来的数据覆盖。由于ESP提升执行了SUB,所以ADD ESP, 40h即可
- EBP恢复到原EBP位置,此时栈中保存了原EBP中的位置,所以 POP EBP ,将堆栈中的原EBP地址保存到EBP中,EBP地址恢复。
- 下一行指令地址赋值给EPI寄存器。到此为止,函数执行结束,并回到了CALL指令的下一行指令,但是函数参数空间还没有清除,所以需要在函数外部恢复堆栈平衡。
- CALL指令的下一行,清除函数参数。同样的,ESP向下移动,ADD EBP, C 即可。
上面是执行过程,汇编代码为。
// 主程序入口 。。。 push 1 push 2 call 0040107D -- 调用函数 add esp, 8 -- 清楚堆栈中的参数值,恢复堆栈
-- 执行结果在eaxz中,需要使用时,获取即可 。。。
// 此处为函数首地址为 0040107D push ebp -- 堆栈提升,保存原ebp值,然后将esp赋值给esp mov ebp, esp sub esp, 40h push ebx push esi push edi mov eax, 0 add eax, ptr ds:[ebp+8] add eax, ptr ds:[ebp+c] pop edi pop esi pop ebx add esp, 40h cmp ebp esp -- 比较esp和ebp是否相同,清除栈信息后应该相,否则说明栈中的内容没有被清除。堆栈不平衡 pop ebp -- 从栈中取出原ebp值,存入ebp即恢复原ebp值 ret
函数执行过程中的堆栈空间是在函数执行时才分配的,这里发生了一次堆栈提升,所以我们总是说函数执行时有独立的栈空间,也是通过这种方式实现。函数执行前后始终需要保证堆栈平衡。
总结
总结函数的执行过程
- 参数入栈
- 保存当前执行指令的地址,入栈
- 进入函数,
- ESP 和 EBP 分别进行堆栈提升
- sub esp 开辟缓冲区空间,缓冲区空间地址为 EBP + 4 -> ESP
- 三个寄存器值保存到栈中
- 执行函数中内容,EBP为基址,获取通过+8 +C获取函数参数,+4位置为函数返回时跳转的地址
- 函数执行结束:
- 恢复三个寄存器
- add esp恢复 缓冲区,
- ESP和EBP位置恢复
- ret
- 函数外部add ebp 恢复参数空间。
- 在eax中获取函数返回值即可。