一段tricky codes:函数调用的那些底层细节

一段tricky codes:函数调用的那些底层细节


有一天,被同事问到了下面这段代码,就简单分析了一下,发觉还有点意思:

__declspec(naked)
void  call( void *  pfn, 
{
    __asm 
    
{
        pop eax;
        add eax, 
3;
        xchg dword ptr[esp], eax;
        push eax;
        ret;
    }

}

 

再看它的用法:

 

void  print_str(  const   char   * s )
{
    printf( 
"%s\n", s );
}

call( print_str, 
" a string "  );

 

call函数的大致作用,就是调用传递进去的函数print_str,并将参数"a string"传递给目标
函数。

但是它是怎么做到的呢?虽然call只有简单的几句汇编代码,但是却包含了很多函数在编译
器中的汇编层实现。要了解这段代码的意思,需要知道如下相关知识:

0、函数调用的实现中,编译器通过系统堆栈(ESP寄存器指向)传递参数;
1、C语言默认的函数调用规则(_cdecl)中,调用者从右往左将参数压入堆栈,并且调用者负
责堆栈平衡,也就是保证调用函数的前后,ESP不变;
2、汇编指令call本质上是先将返回地址,通常是该条指令的下一条指令压入堆栈,然后直
接跳转到目标位置;
3、汇编指令ret则是先从堆栈栈顶取出返回地址,然后跳转过去;
4、汇编指令add加上其操作数,貌似占3个字节长度;
5、在visual studio中,DEBUG模式下编译器会在我们的代码中插入各种检测代码,而
__declspec(naked)则是告诉编译器:别往这里添加代码。

了解了以上常识后,再看这段代码,其本质无非就是利用了这些规则,在代码段跳来跳去。
我们来逐步分析一下:

在调用call函数的地方,大概的代码为:

 

caller:
//  堆栈状态,从左往右分别表示栈顶至下
//  ret_addr是call后的地址,即add esp, 8的位置
//  a1, a2表示函数参数,callee_addr是这里的print_str
//  stack: ret_addr, callee_addr, a1, a2, 
call( print_str,  " a string "  ); 
add esp, 
8   // 清除参数传递所占用的堆栈空间,维持堆栈平衡
end_label  // 位于add后的指令,后面会提到

call:
//  此时堆栈stack: ret_addr, a1, a2
pop eax  //  eax = ret_addr; stack: callee_addr, a1, a2, 
add eax,  3   //  eax = end_label; stack: callee_addr, a1, a2, 
xchg dword ptr[esp], eax  //  eax = callee_addr; stack: end_label, a1, a2, 
push eax  //  stack: callee_addr, end_label, a1, a2, 
ret  //  取出callee_addr并跳转,也就跳转到print_str函数的入口,此时堆栈
    
//  stack: end_label, a1, a2, 

callee(print_str):

 无视函数内容

ret 
//  print_str返回,此时正常情况下,堆栈stack: end_label, a1, a2, 
 
//  取出end_label并跳转,stack: a1, a2, 

 

那么当callee结束时,则跳转回caller函数中。不过,如过你所见,此时堆栈中还保留着再
调用call函数时传入的参数:stack: a1, a2, ...,所以,DEBUG模式下,VS就会提示你堆
栈不平衡。这里简单的处理就是手动来进行堆栈平衡:

 

    call( print_str,  " a string "  );
    __asm
    
{
        add esp, 
4
    }

 

传入了多少个参数,就得相应地改变esp的值。

话说距离上篇博客都有半年了,自己都不知道时间晃得如此之快。最近业余折腾了下android开发,
一不小心就跨年了。
 

你可能感兴趣的:(一段tricky codes:函数调用的那些底层细节)