多多色-多人伦交性欧美在线观看-多人伦精品一区二区三区视频-多色视频-免费黄色视屏网站-免费黄色在线

國內最全IT社區平臺 聯系我們 | 收藏本站
阿里云優惠2
您當前位置:首頁 > 互聯網 > 關于編譯型語言函數的調用(一)

關于編譯型語言函數的調用(一)

來源:程序員人生   發布時間:2014-11-05 08:53:23 閱讀次數:3271次

終究真是團團轉,真可以說是好事做盡,壞事做絕,

但是想一想寫點東西既有助于記憶,又有益于他人參考,所以還是決定抽點時間草書此文

之前在有關破解的博文中也略微提到這個問題,現在就深入1點去考究它吧


狹義的編譯1般指的是將程序語言代碼轉為CPU能履行的機器碼,比如C++(VC++)

VB6的主程序也是切實編譯的,但是大部份卻類似java,生成了中間代碼,由虛擬機在運行時解釋為機器碼

這1點跟腳本很類似,只是中間代碼是2進制的,不容易為人所理解,腳本則更直觀

對.NET(VB,C#等)則是純潔的生成中間代碼(微軟中間語),因此這些語言生成的程序可以很容易的"反編譯"并任意轉換語言

生成中間代碼,廣義上也算是編譯.


我們今天要說的主要是狹義的編譯,而且主要以VC6為例子,考究函數調用的那些細節,其實我還是比較關注細節的

VC中經常使用的函數調用有以下幾種:

1、_stdcall
2、__cdec(默許)
3、__fastcall
4、thiscall(隱式)
5、naked(裸函數)

其實naked不是1種調用約定,而是函數修飾符,是面向編譯的,它允許http://www.vxbq.cn自由的控制函數的堆棧.

編譯以后可以與thiscall之外所有調用方式相同.我們寫個小demo來分別看看這些函數都是怎樣調用的.

// call.h ... #ifndef __CALL_H_ #define __CALL_H_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 //#ifdef __cplusplus //extern "C" { //#endif class CCall { public: CCall(); ~CCall(); int Call(int arg1, short arg2, char arg3, void *arg4); protected: int m_Var1; }; //#ifdef __cplusplus //} //#endif #endif
處于種種目的, 我還是把函數體寫在類外面:

// call.cpp ... #include "call.h" CCall::CCall() { m_Var1 = 18; } CCall::~CCall() { } int CCall::Call(int arg1, short arg2, char arg3, void *arg4) { int var1; short var2; char var3; int *p; var1 = arg1; var2 = arg2; var3 = arg3; p = (int *)arg4; *p = m_Var1; return 0; }
還有入口和全局函數:

// main.cpp ... #include <windows.h> #include "call.h" int g_var1; void fnVoid(int arg1, short arg2, char arg3) { int var1; short var2; char var3; var1 = arg1; var2 = arg2; var3 = arg3; arg1 = ⑴; g_var1 = 111; return; } int fnDefaultCall(int arg1, short arg2, char arg3, void *arg4) { int var1; short var2; char var3; int *p; var1 = arg1; var2 = arg2; var3 = arg3; p = (int *)arg4; *p = 7; return 0; } int __stdcall fnStandardCall(int arg1, short arg2, char arg3, void *arg4) { int var1; short var2; char var3; int *p; var1 = arg1; var2 = arg2; var3 = arg3; p = (int *)arg4; *p = 11; return 0; } int __fastcall fnFastCall(int arg1, short arg2, char arg3, void *arg4) { int var1; short var2; char var3; int *p; var1 = arg1; var2 = arg2; var3 = arg3; p = (int *)arg4; *p = 14; return 0; } __declspec(naked) int __cdecl fnNakedCall(int arg1, short arg2, char arg3, void *arg4) { // 1. 到這里所有寄存器的值與調用前1樣 // 2. 用變量名援用任何局部變量同等于援用主調函數變量或參數 // 3. 必須負責寄存器的保護, 這里函數作為__cdecl __asm{ push ebp ; prolog begin mov ebp, esp sub esp, 50h push ebx push esi push edi lea edi, [ebp⑸0h] mov ecx, 14h mov eax, 0CCCCCCCCh rep stos dword ptr [edi] ; prolog end // var1 = arg1; mov eax, dword ptr [ebp + 8] ; [esp + 8] mov dword ptr [ebp⑷], eax ; [esp - 4] // var2 = arg2; mov cx, word ptr [ebp + 0Ch] mov word ptr [ebp - 8], cx // var3 = arg3; mov dl, byte ptr [ebp + 10h] mov byte ptr [ebp - 0Ch], dl // p = (int *)arg4; mov eax, dword ptr [ebp + 14h] mov dword ptr [ebp - 10h], eax // *p = ⑴; mov ecx, dword ptr [ebp - 10h] mov dword ptr [ecx], 0FFFFFFFFh // return 22; mov eax, 16h ; 0x16 = 22 pop edi ; epilog begin pop esi pop ebx mov esp, ebp pop ebp ; epilog end // return to caller function(do not use ret 10h) ret } } int main(int argc, char **argv) { CCall *pCall; int var1; int ret; fnVoid(1, 2, 3); ret = fnDefaultCall(4, 5, 6, &var1); ret = fnStandardCall(8, 9, 10, &var1); ret = fnFastCall(11, 12, 13, &var1); pCall = new CCall(); ret = pCall->Call(15, 16, 17, &var1); delete pCall; // pCall = NULL; ret = fnNakedCall(19, 20, 21, &var1); return 0; }

下面在DEBUG下看看調用進程,注意如果是VS.NET,VC編譯時會在每一個變量前后都加1個DWORD,目的是檢測緩沖區溢出

首先是調用無返回值的void函數,默許是__cdecl調用:

120: fnVoid(1, 2, 3); 0040135D push 3 0040135F push 2 00401361 push 1 00401363 call @ILT+5(fnVoid) (0040100a) 00401368 add esp,0Ch 121:
可以看出,參數被從右向左壓入堆棧,而后call函數地址,然后add esp,清算堆棧

注:

堆棧是從高地址向低地址延伸的,比如第1個push之前esp(棧頂指針)=0x0012FF04,那末push 3以后esp=0x0012FF00

以此類推,push 2,esp=0x0012FEFC; push 1,esp=0x0012FEF8

接著是call指令,這個指令將返回地址,即下1條指令位置(eip,指令指針)壓入堆棧,比如

call之前eip=0x00401363(下1條eip=0x00401368)

call以后eip=0x0040100A,esp=0x0012FEF4

然后調用結束,__cdecl約定函數最后的ret指令會pop 棧頂給eip指針

eip=0x00401368 ESP=0x0012FEF8

而后add esp,0xc,這里0xC=12即3個DWORD就是前面push的數量(pop要彈出給某個寄存器,add直接修改棧頂位置,減少堆棧大小)

到此,堆棧和eip恢復調用前的狀態.


接著,我們進入函數內部,看看它都做了甚么見不得人的勾當:

7: void fnVoid(int arg1, short arg2, char arg3) 8: { 00401140 push ebp 00401141 mov ebp,esp 00401143 sub esp,4Ch 00401146 push ebx 00401147 push esi 00401148 push edi 00401149 lea edi,[ebp⑷Ch] 0040114C mov ecx,13h 00401151 mov eax,0CCCCCCCCh 00401156 rep stos dword ptr [edi] 9: int var1; 10: short var2; 11: char var3; 12: var1 = arg1; 00401158 mov eax,dword ptr [ebp+8] 0040115B mov dword ptr [ebp⑷],eax 13: var2 = arg2; 0040115E mov cx,word ptr [ebp+0Ch] 00401162 mov word ptr [ebp⑻],cx 14: var3 = arg3; 00401166 mov dl,byte ptr [ebp+10h] 00401169 mov byte ptr [ebp-0Ch],dl 15: 16: arg1 = ⑴; 0040116C mov dword ptr [ebp+8],0FFFFFFFFh 17: g_var1 = 111; 00401173 mov dword ptr [g_var1 (0042ae74)],6Fh 18: return; 19: } 0040117D pop edi 0040117E pop esi 0040117F pop ebx 00401180 mov esp,ebp 00401182 pop ebp 00401183 ret --- No source file -------------------------------------------------------------- 00401184 int 3
首先ebp是棧底指針,是高地址(比esp高),函數的堆棧應在esp到ebp之間,不應當讀寫高于ebp的堆棧內存

注意,不應當不是不可以,黑客所用的緩沖區溢出攻擊就是利用這1點,當你的程序不謹慎寫入了這些地方的時候他們就能夠履行任意代碼

包括添加管理員帳戶等等,這類通常是strcpy之類的函數,比如char szText[256],但是源字符串超越256字節

push ebp是保存棧底的值,這個棧底是調用之前的,然后

mov ebp, esp把棧頂賦值給棧底,相當于調用前的棧頂作為現在的棧底,再接著

sub esp, 4Ch棧頂減小4C=76(19個DWORD),相當于堆棧大小是76字節,這樣就創建了1個當前函數所使用的堆棧

接下來

push ebx將基址寄存器入棧,編譯器是很機械的,其實到現在為止,其實不需要基址寄存器,固然不需要暫存它的值,不過編譯器其實不是人,它不管這個

接著push esi和edi是串操作的原指針和目的指針,了解匯編語言的就知道,這小子開始批量處理了

lea edi,[ebp⑷Ch]其實ebp⑷Ch就是esp就是棧頂,棧頂地址作為目的(內存地址較低)

mov  ecx,13h數量0x13=19,還記得剛剛說的19個DWORD嗎?

mov  eax,0CCCCCCCCh,串操作的值,0xCCCCCCCC

rep stos dword ptr [edi],向edi指向的dword寫入eax的值,即0xcccccccc,如果ecx不為零,edi遞增1個dword繼續寫入

知道為何VC變量為何默許值總是0xCC了吧,局部變量都保存在堆棧上,現在全部堆棧都是這個值

其實還有1個用途,等下函數返回時我們再說.

現在"春田花花同學會"正式開始,


// var1 = arg1;

mov eax,dword ptr [ebp+8]

mov dword ptr [ebp⑷],eax

ebp是新的棧底指針,也就是原來的棧頂,前面調用的時候說過,call會push返回地址(指令地址不是返回值地址),

也就是說現在ebp指向的是返回地址?錯!注意開始的push ebp,它又壓入了1個DWORD,因此此時ebp指向的是原來的ebp

堆棧向低地址擴大,那末ebp+4就是函數的返回地址,順序倒過來,ebp+8就是最后1個push壓入的參數,也就是第1個參數!

堆棧向低地址擴大,那末ebp⑷就是第1個局部變量了,有人問為何要mov到eax,再從eax放到第1個局部變量?狄春說:這不是屢次1舉嗎

元芳說:mov指令兩個參數不能都是存儲器,也就是內存,這就是為何叫寄存器的緣由,英文為REGISTER是登記的意思,既是名詞也是動詞


想通了這1點,后面的就好理解了,只不過用低字,低字節來轉移而已

接著我們修改參數的值,其實也好理解了,由于調用后直接add esp,xx參數直接拋棄,因此其實不改變甚么,除臨時廢棄的堆棧

接著是賦值全局變量,將1個立即數傳送給全局變量的內存地址,也好理解了

沒有返回值單函數,函數結尾return沒有任何意義,如果在上面return會生成1條jmp指令,跳到這里來


最后,清算現場,最早push的最后pop恢復他們之前的值,恢復原來棧頂的值,pop恢復原來的棧底

最后1條ret指令,在函數調用時我們已說了,這里說1下的是,如果此時堆棧中的返回地址(恢復后的棧頂esp指向的地址)被修改了,會有甚么情況產生呢?

比如指向了ShellExecute這個API的地址,參數是cmd /c net user admin1 123456 /add

這個就留給大家思考吧, 還記得剛剛說0xCC的另外一個用途嗎,如果此時沒有ret,履行到后面就是0xCC這個機器碼對應的是int 3中斷

在debug,比如OllyDebug等會在斷點處插入0xCC,調試者繼續運行才恢復這個字節原來的值再繼續履行

所以,不經意間的緩沖區,常常釀成的是內存制止訪問,或中斷,而有些人卻對此10分敏感,就像有個美女裙子被吹起來,

阿彌陀佛,罪過!罪過!


文章好像很長了,我先int31下,下文繼續吧

生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈
程序員人生
------分隔線----------------------------
分享到:
------分隔線----------------------------
關閉
程序員人生
主站蜘蛛池模板: 69av在线播放| 日本久久网 | 亚洲免费大片 | 五月天婷婷一区二区三区久久 | 欧美jizz| 国产亚洲精品线观看77 | 手机看片精品国产福利盒子 | www.毛片.com| 亚洲欧美日韩专区一 | 一级a性色生活片毛片 | 欧美一级高清免费播放 | 精品国产综合成人亚洲区 | 亚洲欧洲综合网 | 香港黄页精品视频在线 | 欧美一区二区视频在线观看 | 国产成人精品免费视频网页大全 | 亚洲特一级毛片 | 成人午夜在线观看 | 高清视频在线观看+免费 | 欧美ucjizz免费播放器 | 最近中文字幕mv免费高清视频免费 | 欧美日韩国产不卡在线观看 | 边摸边吃奶边做3p视频 | 中文成人在线视频 | 国内精品视频在线播放一区 | 人善交video欧美 | 亚洲性色永久网址 | 亚洲免费黄网 | 92精品国产自产在线观看48页 | 精品一区二区三区高清免费观看 | 婷婷激情五月 | 精品国产日韩一区三区 | 超刺激福利丝袜网站 | 亚洲爱| 欧美日韩精品在线 | www亚洲成人| 国产乱码一区二区三区四区 | 亚洲爱爱久久精品 | 欧美一级精品高清在线观看 | 精品在线免费观看 | 一区二区三区四区在线不卡高清 |