子程序在其任务完成后,执行的最后一条汇编指令是RET。根据对该子程序的调用是段内调用还是段间调用,其要实现的操作是不相同的。如果是段内调用,那么RET指令的功能只是把存放在堆栈里的返回地址送IP寄存器;如果是段间调用,那么RET指令的功能就是要把存放在堆栈里的返回地址和段地址,分别送IP寄存器和CS寄存器。因此,RET指令的使用也有多种形式:段内返回、段内带立即数返回、段间返回和段间带立即数返回。下面对它们一一加以介绍。
(1)段内返回
格式:RET
执行的操作:IP←(SP+1)(SP)
SP←SP+2
即将当前栈顶元素内容(就是断点的地址)送寄存器IP,然后往栈底方向调整堆栈指针,达到栈顶元素出栈的目的。
(2)段内带立即数返回
格式:RET n(或RET <表达式>)
立即数n通常是正偶数。如果汇编格式指令中的n或表达式计算出的值是奇数,汇编时会自动变成n+1。
执行的操作:IP←(SP+1)(SP)
SP←SP+2
SP←SP+n
(3)段间返回
格式:RET
执行的操作:IP←(SP+1)(SP)
SP←SP+2
CS←(SP+1)(SP)
SP←SP+2
(4)段间带立即数返回
格式:RET n
执行的操作:IP←(SP+1)(SP),SP←SP+2
CS←(SP+1)(SP),SP←SP+2
SP←SP+n
段内返回和段间返回指令的汇编指令格式都是RET,但是汇编后产生的机器代码指令却是不同的。当子程序的属性是NEAR时,对应于段内返回RET指令的机器代码是C3H,对应于段内带立即数返回RET指令的机器代码是C2H;当子程序的属性是FAR时,对应于段间返回RET指令的机器代码是CBH,对应于段间带立即数返回RET指令的机器代码是CAH。所以,定义一个子程序为段间调用时,其类型属性FAR是绝对不能省略的,否则就不能正确实现返回。属性是FAR的子程序也可以被同一代码段的主程序调用。这时,CALL指令虽然与子程序同在一个代码段中,但它执行的却是段间调用操作。由此也表明,CALL指令的属性是由过程的属性决定的。
函数调用栈过程中通过ebp寄存器暂时存储当前函数的栈顶,在函数返回后再通过ebp得到esp完成退栈
DWORD _stdcall xxxx(int a ,int b);
push b
push a
call xxxx: 实际执行为 push @+5 jmp xxxx
add esp,8;编译时添加
DWORD _delspec(naked) xxxx(int a,int b)
{
push ebp; 现保存ebp的值
mov ebp,esp
sub esp,0xxx;分配局部变量
.......
add esp,0xxx;局部变量退栈
mov esp,ebp;esp指向了之前push的ebp值处
pop ebp ;将ebp值还原为前一栈帧的值,esp指向之前push的返回地址处
ret; ;mov eip,esp add esp,4;跳到之前push的地址处,并把push的地址出栈
}
在编译时编译器会自动在call后添加add esp,0xxx(参数占用的字节数),后来函数调用ret时后跳转到此地址处
1 条评论:
DWORD xxx();
call xxx;其实对应着一个0xe8一个0xe9的跳转 0xe8 aaaaaa
aaaaaa: 0xe9 bbbbbb
bbbbbb+aaaaaa+5处才是真正的函数指令处
如果_asm mov eax,xxx
eax里的内容为aaaaaa值需要做些转换才能得到真实的地址aaaaaa+5+bbbbbb
发表评论