關于編譯型語言函數的調用(一)
來源:程序員人生 發布時間: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下,下文繼續吧
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈