總會(huì)有一個(gè)人需要你的分享~!- 唐風(fēng) -
前幾天和柯柯交流一個(gè)小問(wèn)題,說(shuō)是如何在一個(gè)函數(shù)內(nèi)得到調(diào)用該函數(shù)的函數(shù)地址。有點(diǎn)拗口,就是說(shuō)如果有一個(gè)函數(shù)A(當(dāng)然我們?cè)谶@個(gè)問(wèn)題中并不知道它是哪個(gè)函數(shù))調(diào)用了B函數(shù),現(xiàn)在希望用個(gè)什么辦法得到A函數(shù)的地址。
我首先聯(lián)想到的是,一般調(diào)試器都能給出嵌套的函數(shù)調(diào)用關(guān)系。那么肯定是有什么辦法解決這個(gè)問(wèn)題。上網(wǎng)查了一通之后只找到一些debug用的API和一些開發(fā)環(huán)境提供的調(diào)整宏等等,感覺(jué)不是很適用。后來(lái)想想,函數(shù)調(diào)用都涉及到“函數(shù)調(diào)用棧”(call stack),也許這里可以得到些什么信息。隱約回想起以前匯編課里老師講過(guò)的一些函數(shù)調(diào)用時(shí)要“壓棧”、“要保存現(xiàn)場(chǎng)”等,但已經(jīng)記得不太清楚了,于是就又上網(wǎng)找了些函數(shù)調(diào)用棧的知識(shí),發(fā)現(xiàn)了一些有意思的信息(上網(wǎng)時(shí)看到ChinaUnix上的一篇,也是轉(zhuǎn)的,原地址和作者不詳,如果你知道請(qǐng)告訴我):
這里我最關(guān)心的是:函數(shù)調(diào)用時(shí),會(huì)在棧里壓入返回地址,和EBP。
因?yàn)楹瘮?shù)調(diào)用的返回地址,正是調(diào)用指令Call的下一個(gè)指令的地址,那么,有了返回地址,就可以得到Call指令的位置了。有Call指令的位置又能干什么呢?幸好匯編課里的知識(shí)還記得一點(diǎn)點(diǎn):Call指令就是一個(gè)跳轉(zhuǎn)指令,它可以讓IP(instruction point[Thanks to RednaxelaFX])指向要跳轉(zhuǎn)的指令的地址,從那里開始執(zhí)行。對(duì)于函數(shù)調(diào)用來(lái)說(shuō),就是讓IP指向被調(diào)用的函數(shù)的地址。Call指令的操作數(shù)其實(shí)和被調(diào)用函數(shù)的地址有非常重要的關(guān)系。有了Call指令的操作數(shù),就可以計(jì)算出被調(diào)用函數(shù)的地址。
但僅僅有這個(gè)還不夠,比如,A調(diào)用了B,那么在A函數(shù)中肯定有一個(gè)Call指令,但這個(gè)Call指令中的操作數(shù)是和B函數(shù)地址相關(guān)的,與A的函數(shù)地址直接關(guān)系不大(至少在沒(méi)有其它信息的情況下,不能計(jì)算出A的地址)。而我們要得到的卻是A函數(shù)的地址。所以,得向上再找一層,找到調(diào)用A函數(shù)的地方,那個(gè)地方的Call指令里的操作數(shù)才和A函數(shù)地址有關(guān)。也就是說(shuō),Z函數(shù)調(diào)用了A函數(shù),A函數(shù)調(diào)用了B函數(shù)。現(xiàn)在要得到A函數(shù)的地址,我們得在Z函數(shù)里找Call指令的操作數(shù)。這時(shí)候EBP就派上用場(chǎng)了。本地編譯器在每個(gè)函數(shù)體之前插入的指令(PUSH EBP; MOV EBP ESP)構(gòu)造了一個(gè)巧妙的結(jié)構(gòu),使得我們可以順著函數(shù)調(diào)用棧一層一層向上,找到所有調(diào)用關(guān)系。
如何向上查找呢?我們看看函數(shù)調(diào)用時(shí)棧、EBP的值的情況就知道了。
假設(shè)現(xiàn)在函數(shù)在正Z函數(shù)內(nèi)執(zhí)行,那么此時(shí)棧和EBP的值可能是像下圖這樣的: 我們先不管現(xiàn)在EBP指向的內(nèi)存(0x000f)中的內(nèi)容XXX是什么(要不然會(huì)是雞生蛋生雞的問(wèn)題),總之目前在棧中的著色塊中的內(nèi)容是屬于函數(shù)Z的參數(shù),Z執(zhí)行結(jié)束后應(yīng)該返回的地址以及Z函數(shù)的局部變量值。 現(xiàn)在Z函數(shù)調(diào)用A函數(shù),會(huì)先將傳給A的參數(shù)壓棧,然后將現(xiàn)在這個(gè)指令(就是"Call A"啦)的下一個(gè)指令的地址壓入棧中,以便A函數(shù)完后返回到Z中繼續(xù)執(zhí)行。然后進(jìn)入A函數(shù)的內(nèi)存空間,首先就是調(diào)用PUSH EBP,也就是將Z的EPB的內(nèi)容(地址0x000f)壓入棧中,然后再M(fèi)OV EBP ESP,讓EBP有一個(gè)新的棧頂(此時(shí)棧頂中的內(nèi)容不就是Z函數(shù)時(shí)EBP的內(nèi)容么?),然后再將A函數(shù)的局部變量壓入棧中,開始執(zhí)行A函數(shù)的代碼。這時(shí),棧和EBP的情況就像如圖所示了: 哈,這樣就很清楚了,原來(lái)現(xiàn)在的EBP中的內(nèi)容,正是上一級(jí)函數(shù)的EBP中的內(nèi)容。而每一個(gè)函數(shù)的EBP指向的位置,向棧頂可以得到該函數(shù)的局部變量,向棧底可以得到函數(shù)的返回地址和參數(shù)。于是我們就可以根據(jù)這個(gè)結(jié)構(gòu)層層向上,找到任何一層我們想找的函數(shù)EBP,從而也就能得到相應(yīng)的返回地址了。 好,從B函數(shù)中得到Z函數(shù)對(duì)A函數(shù)調(diào)用點(diǎn)的返回地址的問(wèn)題也就解決了。現(xiàn)在就是處理Call指令的問(wèn)題了。 我在Visual Studio 2003的Debug版中進(jìn)行反匯編調(diào)試,發(fā)現(xiàn)Call指令對(duì)應(yīng)的機(jī)器指令都是5個(gè)byte,第一個(gè)byte(E8)是指令的器碼,猜想后面4個(gè)byte應(yīng)該就是它的轉(zhuǎn)移的目標(biāo)地址了。結(jié)果按這個(gè)地址去找,發(fā)現(xiàn)根本不對(duì),想想?yún)R編也忘得差不多了,于是又去找了教程看看,才記起原來(lái)Call的操作數(shù)并不是絕對(duì)地址,而是偏移地址(跳轉(zhuǎn)目標(biāo)地址-Call指令地址-sizeof(Call指令)),這樣就好辦了,我有返回地址,于是就有了向上5個(gè)byte就是Call的地址,再?gòu)倪@個(gè)地址中取出Call指令機(jī)器碼的后四個(gè)字節(jié),加上返回地址,就得到了目標(biāo)地址。 原以為已經(jīng)搞定了。不過(guò)還有一個(gè)小插曲,就是在VS的Debug版中,Call并不直接跳到一函數(shù)中去(不知道為什么),而是跳到一塊代碼區(qū),這塊區(qū)域內(nèi)排布了很多的Jmp指令用于各種跳轉(zhuǎn)(不知道為什么這么搞,也許是為調(diào)試的功能而設(shè)計(jì)的吧,誰(shuí)知道?還請(qǐng)不吝賜教),不過(guò)沒(méi)關(guān)系,也就是多走一點(diǎn)路而已,Jmp指令的操作數(shù)和Call指令的意義是一樣的,最終Jmp是跳到函數(shù)代碼塊中去的。于是也就得到了想要的結(jié)果。 下面是代碼: 1#include "stdafx.h" 2 3#include <string> 4 5unsigned int GetCallerAddress(void) 6{ 7 unsigned int _ebp; 8 __asm mov _ebp, ebp 910 for (int i=2; i != 0; --i) {11 _ebp = *(unsigned int *)(_ebp);12 }13 unsigned int* ipAddress = (unsigned int*)(*(unsigned int *)(_ebp + 4));1415 ipAddress = (unsigned int*)((unsigned char *)ipAddress - 5);16 unsigned int callInstructAddress = (unsigned int)ipAddress;17 ipAddress = (unsigned int*)((unsigned char *)ipAddress + 1);18 int funcAddrOffset = *ipAddress;19 unsigned int *jumAddr = (unsigned int*)(callInstructAddress + funcAddrOffset + 5); 20 callInstructAddress = (unsigned int)jumAddr;21 jumAddr = (unsigned int*)((unsigned char *)jumAddr + 1);22 funcAddrOffset = *jumAddr;23 24 return funcAddrOffset + callInstructAddress + 5; 25}2627void fun1();2829void fun2()30{31 fun1();32}3334void fun3()35{36 fun1();37}383940void fun1()41{42 unsigned int _ebp;43 __asm mov _ebp, ebp // 取當(dāng)前EBP44 unsigned int _preEbp = *(unsigned int *)(_ebp); //得到上層函數(shù)的EBP45 unsigned int* ipAddress = (unsigned int*)(*(unsigned int *)(_preEbp + 4)); // 取得返回地址46 ipAddress = (unsigned int*)((unsigned char *)ipAddress - 5); // 得到Call指令地址 47 unsigned int callInstructAddress = (unsigned int)ipAddress; // 保存Call指令地址 48 ipAddress = (unsigned int*)((unsigned char *)ipAddress + 1); 49 int funcAddrOffset = *ipAddress; // 得到Call指令操作數(shù)50 unsigned int *jumAddr = (unsigned int*)(callInstructAddress + funcAddrOffset + 5); // 找到Jmp指令51 callInstructAddress = (unsigned int)jumAddr; // 保存jmp指令地址52 jumAddr = (unsigned int*)((unsigned char *)jumAddr + 1); 53 funcAddrOffset = *jumAddr; // 得到j(luò)mp指令操作數(shù)54 unsigned int addr = funcAddrOffset + callInstructAddress + 5; //得到函數(shù)地址5556 // 或者:unsigned int addr = GetCallerAddress();57 printf("fun1 said : Caller Addres is 0x%08x\n", addr);58}5960int _tmain(int argc, _TCHAR* argv[])61{62 fun1();63 fun2();64 fun3();6566 return 0;67}6869
我們先不管現(xiàn)在EBP指向的內(nèi)存(0x000f)中的內(nèi)容XXX是什么(要不然會(huì)是雞生蛋生雞的問(wèn)題),總之目前在棧中的著色塊中的內(nèi)容是屬于函數(shù)Z的參數(shù),Z執(zhí)行結(jié)束后應(yīng)該返回的地址以及Z函數(shù)的局部變量值。
現(xiàn)在Z函數(shù)調(diào)用A函數(shù),會(huì)先將傳給A的參數(shù)壓棧,然后將現(xiàn)在這個(gè)指令(就是"Call A"啦)的下一個(gè)指令的地址壓入棧中,以便A函數(shù)完后返回到Z中繼續(xù)執(zhí)行。然后進(jìn)入A函數(shù)的內(nèi)存空間,首先就是調(diào)用PUSH EBP,也就是將Z的EPB的內(nèi)容(地址0x000f)壓入棧中,然后再M(fèi)OV EBP ESP,讓EBP有一個(gè)新的棧頂(此時(shí)棧頂中的內(nèi)容不就是Z函數(shù)時(shí)EBP的內(nèi)容么?),然后再將A函數(shù)的局部變量壓入棧中,開始執(zhí)行A函數(shù)的代碼。這時(shí),棧和EBP的情況就像如圖所示了:
哈,這樣就很清楚了,原來(lái)現(xiàn)在的EBP中的內(nèi)容,正是上一級(jí)函數(shù)的EBP中的內(nèi)容。而每一個(gè)函數(shù)的EBP指向的位置,向棧頂可以得到該函數(shù)的局部變量,向棧底可以得到函數(shù)的返回地址和參數(shù)。于是我們就可以根據(jù)這個(gè)結(jié)構(gòu)層層向上,找到任何一層我們想找的函數(shù)EBP,從而也就能得到相應(yīng)的返回地址了。
好,從B函數(shù)中得到Z函數(shù)對(duì)A函數(shù)調(diào)用點(diǎn)的返回地址的問(wèn)題也就解決了。現(xiàn)在就是處理Call指令的問(wèn)題了。
我在Visual Studio 2003的Debug版中進(jìn)行反匯編調(diào)試,發(fā)現(xiàn)Call指令對(duì)應(yīng)的機(jī)器指令都是5個(gè)byte,第一個(gè)byte(E8)是指令的器碼,猜想后面4個(gè)byte應(yīng)該就是它的轉(zhuǎn)移的目標(biāo)地址了。結(jié)果按這個(gè)地址去找,發(fā)現(xiàn)根本不對(duì),想想?yún)R編也忘得差不多了,于是又去找了教程看看,才記起原來(lái)Call的操作數(shù)并不是絕對(duì)地址,而是偏移地址(跳轉(zhuǎn)目標(biāo)地址-Call指令地址-sizeof(Call指令)),這樣就好辦了,我有返回地址,于是就有了向上5個(gè)byte就是Call的地址,再?gòu)倪@個(gè)地址中取出Call指令機(jī)器碼的后四個(gè)字節(jié),加上返回地址,就得到了目標(biāo)地址。
原以為已經(jīng)搞定了。不過(guò)還有一個(gè)小插曲,就是在VS的Debug版中,Call并不直接跳到一函數(shù)中去(不知道為什么),而是跳到一塊代碼區(qū),這塊區(qū)域內(nèi)排布了很多的Jmp指令用于各種跳轉(zhuǎn)(不知道為什么這么搞,也許是為調(diào)試的功能而設(shè)計(jì)的吧,誰(shuí)知道?還請(qǐng)不吝賜教),不過(guò)沒(méi)關(guān)系,也就是多走一點(diǎn)路而已,Jmp指令的操作數(shù)和Call指令的意義是一樣的,最終Jmp是跳到函數(shù)代碼塊中去的。于是也就得到了想要的結(jié)果。
下面是代碼:
PS:后經(jīng)柯柯驗(yàn)證,只有VC6、2003、2008的Debug版里才有效。Release版中不行,具體原因未細(xì)查(沒(méi)時(shí)間,畢竟不是"正務(wù)",呵呵)。以后再遇到時(shí)再細(xì)究吧。至少,現(xiàn)在對(duì)函數(shù)調(diào)用棧有了一些新的認(rèn)識(shí)。很開心,呵呵呵。
Powered by: C++博客 Copyright © 唐風(fēng)