loop_in_codes

低调做技术__欢迎移步我的独立博客 codemaro.com 微博 kevinlynx

一段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开发
一不小心就跨年了。
 

posted on 2011-01-02 16:34 Kevin Lynx 阅读(4866) 评论(4)  编辑 收藏 引用 所属分类: c/c++

评论

# re: 一段tricky codes:函数调用的那些底层细节 2011-01-03 05:58 淘宝网

哈哈 不错  回复  更多评论   

# re: 一段tricky codes:函数调用的那些底层细节 2011-01-06 12:30 miosys

整个悬念就是放在 add eax, 3;
这条指令就是为了在跳转到最外层主调函数上时,留出一个指令空间来平栈。
如果用 ADD + WORD,应该是 3。当然不会BT到加 DWORD。  回复  更多评论   

# re: 一段tricky codes:函数调用的那些底层细节 2011-01-08 21:47 G++

围观,表示看不懂,哈哈哈哈哈~~~!  回复  更多评论   

# re: 一段tricky codes:函数调用的那些底层细节[未登录] 2011-03-15 14:36 dophi

已阅  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理