有一天,被同事問到了下面這段代碼,就簡單分析了一下,發(fā)覺還有點意思:
__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函數(shù)的大致作用,就是調(diào)用傳遞進去的函數(shù)print_str,并將參數(shù)"a string"傳遞給目標
函數(shù)。
但是它是怎么做到的呢?雖然call只有簡單的幾句匯編代碼,但是卻包含了很多函數(shù)在編譯
器中的匯編層實現(xiàn)。要了解這段代碼的意思,需要知道如下相關(guān)知識:
0、函數(shù)調(diào)用的實現(xiàn)中,編譯器通過系統(tǒng)堆棧(ESP寄存器指向)傳遞參數(shù);
1、C語言默認的函數(shù)調(diào)用規(guī)則(_cdecl)中,調(diào)用者從右往左將參數(shù)壓入堆棧,并且調(diào)用者負
責堆棧平衡,也就是保證調(diào)用函數(shù)的前后,ESP不變;
2、匯編指令call本質(zhì)上是先將返回地址,通常是該條指令的下一條指令壓入堆棧,然后直
接跳轉(zhuǎn)到目標位置;
3、匯編指令ret則是先從堆棧棧頂取出返回地址,然后跳轉(zhuǎn)過去;
4、匯編指令add加上其操作數(shù),貌似占3個字節(jié)長度;
5、在visual studio中,DEBUG模式下編譯器會在我們的代碼中插入各種檢測代碼,而
__declspec(naked)則是告訴編譯器:別往這里添加代碼。
了解了以上常識后,再看這段代碼,其本質(zhì)無非就是利用了這些規(guī)則,在代碼段跳來跳去。
我們來逐步分析一下:
在調(diào)用call函數(shù)的地方,大概的代碼為:
caller:
// 堆棧狀態(tài),從左往右分別表示棧頂至下
// ret_addr是call后的地址,即add esp, 8的位置
// a1, a2表示函數(shù)參數(shù),callee_addr是這里的print_str
// stack: ret_addr, callee_addr, a1, a2, 
call( print_str, "a string" );
add esp, 8 //清除參數(shù)傳遞所占用的堆棧空間,維持堆棧平衡
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并跳轉(zhuǎn),也就跳轉(zhuǎn)到print_str函數(shù)的入口,此時堆棧
// stack: end_label, a1, a2, 

callee(print_str):



無視函數(shù)內(nèi)容


ret // print_str返回,此時正常情況下,堆棧stack: end_label, a1, a2, 
// 取出end_label并跳轉(zhuǎn),stack: a1, a2, 

那么當callee結(jié)束時,則跳轉(zhuǎn)回caller函數(shù)中。不過,如過你所見,此時堆棧中還保留著再
調(diào)用call函數(shù)時傳入的參數(shù):stack: a1, a2, ...,所以,DEBUG模式下,VS就會提示你堆
棧不平衡。這里簡單的處理就是手動來進行堆棧平衡:
call( print_str, "a string" );
__asm

{
add esp, 4;
}
傳入了多少個參數(shù),就得相應地改變esp的值。
話說距離上篇博客都有半年了,自己都不知道時間晃得如此之快。最近業(yè)余折騰了下android開發(fā),
一不小心就跨年了。