探究以下程序在函数调用过程中 栈顶EBP 栈底ESP 的变化过程 int Plus(int x,int y){ int z=2; return x+y+z;}int main(int argc, char* argv[]){ int r=Plus(3,4); return 0;}反汇编后代码是这样的 1. 00C113EE 6A 04 PUSH 4 //参数3压栈 2. 00C113F0 6A 03 PUSH 3 //参数4压栈3. 00C113F2 E8 A9FCFFFF CALL 00C110A0 //调用函数4. 00C113F7 83C4 08 ADD ESP,8 //平栈 其中第3步函数调用内部汇编如下 1. 00C113EE 6A 04 PUSH 42. 00C113F0 6A 03 PUSH 33. 00C113F2 E8 A9FCFFFF CALL 00C110A0 //调用函数 指令跳转----3.1 00C110A0 E9 DB020000 JMP 00C11380 //跳转到函数地址准备调用3.2 00C11380 55 PUSH EBP //为了还原现状,保存当前栈底3.3 00C11381 8BEC MOV EBP,ESP //开辟新的函数栈,栈底提升到原栈顶的位置3.4 00C11383 81EC CC000000 SUB ESP,0CC //开辟新的函数栈,栈顶向上偏移一段距离,这段距离就是缓冲区3.5 00C11389 53 PUSH EBX //为了还原现状,保存当前EBX3.6 00C1138A 56 PUSH ESI //为了还原现状,保存当前ESI3.7 00C1138B 57 PUSH EDI //为了还原现状,保存当前EDI3.8 00C1138C 8DBD 34FFFFFF LEA EDI,[EBP-0CC] //循环填充缓冲区,设置填充的起始地址3.9 00C11392 B9 33000000 MOV ECX,33 //循环填充缓冲区,设置填充次数3.10 00C11397 B8 CCCCCCCC MOV EAX,CCCCCCCC //循环填充缓冲区,设置填充内容3.11 00C1139C F3:AB REP STOS DWORD PTR ES:[EDI] //循环填充缓冲区,开始填充3.12 00C1139E C745 F8 0200000 MOV DWORD PTR SS:[EBP-8],2 //将变量2写入栈中3.13 00C113A5 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] //计算相加,结果保存到EAX中3.14 00C113A8 0345 0C ADD EAX,DWORD PTR SS:[EBP+0C] //计算相加,结果保存到EAX中3.15 00C113AB 0345 F8 ADD EAX,DWORD PTR SS:[EBP-8] //计算相加,结果保存到EAX中3.16 00C113AE 5F POP EDI //还原EDI3.17 00C113AF 5E POP ESI //还原ESI3.18 00C113B0 5B POP EBX //还原EBX3.19 00C113B1 8BE5 MOV ESP,EBP //消除新栈,还原栈况,新栈底为原栈顶3.20 00C113B3 5D POP EBP //消除新栈,还原栈况,原栈底之前已经保存在栈中,POP即可还原3.21 00C113B4 C3 RETN //指令跳转到函数调用处----4. 00C113F7 83C4 08 ADD ESP,8 //平栈5. 00C113FA 8945 F8 MOV DWORD PTR SS:[EBP-8],EAX
程序未运行时 EBP ESP EIP 如下
0.堆栈初始状态是这样的1. PUSH 4
当第一个参数压栈时,栈底EBP不变,栈顶ESP上移一个字节,即ESP=ESP-4,指令EIP跳转到下一条指令,因为PUSH 4 这条指令是两个字节 所以EIP指令下移两个字节,EIP=EIP+22. PUSH 3
当第二个参数压栈时,栈底EBP不变,栈顶ESP上移一个字节,即ESP=ESP-4,指令EIP跳转到下一条指令
3. CALL 00C110A0 = PUSH EIP + jmp
当调用函数Plus时,先将EIP的下一条指令(00C113F7)压栈保存 为的是当被调用函数结束时程序可以回来继续运行 ,栈底不变,因为有指令被压栈,所以栈顶上移
3.1 JMP 00C11380
继续跟进程序 发现00C110A0 会 JMP 00C11380 此时 EIP指向 00C11380 因为是jmp 所以堆栈没有影响
3.2 PUSH EBP
因为CALL了一个函数,所以会生成一个新的函数栈,此时需要先将原来的栈底保存起来,为了函数调用结束之后可以还原当前的栈况,不保存会发生栈底丢失。 push操作导致栈顶上移 ESP=ESP-4
3.3 MOV EBP,ESP
3.4 SUB ESP,0CC
生成新栈
因为CALL了一个函数,所以会生成一个新的函数栈,在原栈顶头上开辟一个新栈,原来的栈顶当作新栈的栈底,原来的栈顶上移0CC个字节 ESP=ESP-0CC,上移的空间就是缓冲区3.5 PUSH EBX
保存现场,为了函数调用结束后 可以还原寄存器当前的状态,所以先将各个寄存器的状态压栈保存
3.6 PUSH ESI 3.7 PUSH EDI
3.8 LEA EDI,[EBP-0CC]
3.9 MOV ECX,33
3.10 MOV EAX,CCCCCCCC
3.11 REP STOS DWORD PTR ES:[EDI] 循环填充缓冲区 LEA EDI,[EBP-0CC] 表示将 EBP-0CC 这个地址 送入EDI EDI影响填充位置,这里是为了设置填充的起始地址 , 即从缓冲区的头部开始填充
MOV ECX,33 给ECX赋值为33 ECX影响了循环次数
MOV EAX,CCCCCCCC 将EAX赋值CCCCCCCC EAX影响填充的内容
REP STOS DWORD PTR ES:[EDI] 用EAX的值循环33次 填充EDI地址中的内容
3.12 MOV DWORD PTR SS:[EBP-8],2
对应函数中的一个变量 int z=2 在EBP-8的位置写入2
3.13 MOV EAX,DWORD PTR SS:[EBP+8]
3.14 ADD EAX,DWORD PTR SS:[EBP+0C]
3.15 ADD EAX,DWORD PTR SS:[EBP-8]
计算2+3+4 结果放入EAX
3.16 POP EDI
还原寄存器的状态 3.5 3.6 3.7保存的 现在用到了
3.17 POP ESI
3.18 POP EBX
注意 此时虽然POP了 只是给寄存器赋值 栈顶跟随改变,但是内存中的数据还是存在的 并没有随着栈顶的降低而消除 3.19 MOV ESP,EBP
消除新栈,修改栈顶
3.20 POP EBP
消除新栈,修改栈底。EBP之前已经保存到栈中,直接POP即可还原
3.21 RETN= POP EIP
4. ADD ESP,8
外平栈,由于此时多PUSH了3和4 为了栈平衡 在函数外部ESP+8 使得栈可以还原到最初状态 至此 函数调用前和函数调用后 栈空间是完全一样的
|