看汇编代码时一定要牢牢把握栈的变化情况,脑里浮现栈帧图,并时刻跟踪ESP,这样会更容易读懂汇编。
下面是函数调用时callee要做的事情(被调用者负责平衡堆栈: 分配和释放栈空间),所有的函数本质上都是callee,由别的函数调用指行,包括main函数:
1. 保存旧的帧指针(把新的栈帧链接到栈帧链表中)
其实栈被分成一个个连续的栈帧(由于函数调用的原因),这些栈帧就像链表一样被EBP链接了起来,每次被调用函数callee要做的第一件事情就是把新的栈帧链接到原来的栈帧链表上,即汇编代码:
push ebp
把EBP的值压栈,而EBP恰是caller函数的帧指针,这就相当于挂接到栈帧链表上。
2. 建立新的帧指针
mov ebp , esp
3. 分配新的栈空间
sub esp , 0xC0h
4. 把寄存器压栈
push ebx
push esi
push edi
5. 初始化栈空间
初始化栈空间为0xCCh,
0xCCh是汇编指令
int 3的二进制码,便于中断纠错。
lea edi , [ebp - 0xC0h]
mov ecx , 30h ;长度,30h * 4 = 0xC0h
mov eax , 0xCCCCCCCCh
rep stosd
6. 执行函数的算法代码
7. 平衡堆栈:弹出保存的寄存器,恢复栈空间,恢复被保存的EBP,即从栈帧链表中删除callee的栈帧。
pop edi
pop esi
pop ebx
mov esp , ebp ;恢复栈空间,重置栈顶esp, 此时esp = ebp = 旧的ebp
pop ebp ;恢复旧的EBP,即把新的栈帧从栈帧链表中删除了,此时esp指向被保存的函数返回地址,即call之后的地址
retn ; 函数返回,此指令相当于pop eip,把esp指向的函数返回地址赋值给EIP
栈帧结构图: