常用寄存器
寄存器
|
名稱
|
常見用途(未完)
|
eax
|
累加器(Accumulator) |
函數返回值
|
ebx |
基址寄存器(Base) |
可作為存儲指針來使用
|
ecx
|
計數器(Counter)
|
在循環和字符串操作時,用來控制循環次數 __thiscall中傳遞this指針 |
edx
|
數據寄存器(Data)
|
|
esp
|
堆棧指針寄存器(Stack)
|
|
ebp
|
基地址指針寄存器(Base)
|
|
esi
|
源地址寄存器(Source Index)
|
|
edi
|
目的地址寄存器(Destination)
|
|
常用匯編指令
push |
把一個32位的操作數壓入堆棧,這個操作會導致esp減4. |
pop |
與push相反,esp加4,一個數據出棧 |
call |
調用函數。將下一條指令的地址壓棧,然后跳轉到所調用函數的開始處,本質相當于push+jump |
ret |
與call相對應,跳轉到棧頂數據所指的地址,本質相當于pop+jump。對于_cdecl 調用的函數,通常會在ret之后進行exp-[n],用于清理調用參數堆棧 |
xor |
異或,常用于清零操作,例如: xor eax eax |
lea |
取得地址(第二個參數)后放入前面的寄存器中。 |
stosw |
將eax中的數據傳送給edi,之后edi+4。常與rep一起使用,用于初始化內存段 |
rep |
當eax>0時,重復后面的指令 |
jp,jl,jge |
根據eax中值與0的關系跳轉 |
cmp |
比較指令,將結果放入eax中,往往是jp,jl,jge之類跳轉指令的執行條件 |
函數調用規則
調用方式
|
簡要說明
|
堆棧清理 |
參數傳遞規則
|
_cdecl |
C 編譯器的默認調用規則 |
Caller
|
從右到左 |
_stdcall |
又稱為WINAPI |
Callee
|
從右到左 |
__thiscall |
C++成員函數調用方式
|
Callee |
this放入ecx,其他從右到左 |
__fastcall
|
|
Callee
|
前兩個等于或者小于DWORD大小的參數放入ecx和edx,其他參數從右到左
|
_cdecl調用通常的asm代碼:
被調用方:
1.保存ebp。ebp總是用來保存這個函數執行之前的esp值。執行完畢之后,我們用ebp回復esp;同時,調用此函數的上層函數也用ebp做同樣的事情。
2.保存esp到ebp中。
;保存ebp,并把esp放入ebp中,此時ebp與esp都為這次函數調用的棧頂
push ebp
mov ebp,esp
3.在堆棧中預留一個區域用于保存局部變量。方法是將esp減少一個數值,這樣就等于壓入了一堆變量。要恢復的時候直接把esp回復成ebp保存的數據就可以了。
4.保存ebx、esi、edi到堆棧中,函數調用完成后恢復。
;把esp往下移動一個范圍,等于在堆棧中預留一片新的空間來保存局部變量
sub esp,010h
push ebx
push esi
push edi
5.(debug版)把局部變量全部初始化為0xcccccccch.
;將保存局部變量的區域全部初始化為0xcccccccch
lea edi,[ebp-010h]
mov ecx,33h
mov eax,0xcccccccch
rep stos dword ptr [edi]
6.然后執行函數的具體邏輯。傳入參數的獲取為:ebp+4為函數的返回地址;ebp+8為第一個參數,ebp+12為第二個參數,以此類推。
7.回復ebx、esi、edi、esp、ebp,最后返回。如果有返回值,在返回之前將保存在eax中,供調用方式用。
pop edi ;恢復edi、esi、ebx
pop esi
pop ebx
mov esp, ebp ;恢復原來的ebp和esp
pop ebp
ret
調用方:
mov eax,dword ptr [b]
push eax
move ecx,dword ptr [a]
push ecx
call myfunction
add esp,8 ;回復堆棧
常見的基礎代碼結構
for循環
for(int i = 0; i < 20; ++i )
0040B93E mov dword ptr [i],0
0040B945 jmp wmain+30h (40B950h)
0040B947 mov eax,dword ptr [i]
0040B94A add eax,1
0040B94D mov dword ptr [i],eax
0040B950 cmp dword ptr [i],14h
0040B954 jge wmain+38h (40B958h)
{
}
0040B956 jmp wmain+27h (40B947h)
可以看到主循環主要由這么幾條指令來實現:mov進行初始化;jmp跳過修改循環變量的代碼;cmp實現跳轉判斷;jge根據條件跳轉。用jmp回到修改循環變量的代碼進行下一次循環。大體結構如下:
mov <循環變量>,<初始值> ;給循環變量賦值
jmp A ;跳到第一次循環處
A: (改動循環變量) ;修改循環變量

B: cmp <循環變量>,<限制變量> ;檢查循環變量
jge 跳出循環
(循環體)

jmp A ;跳回修改循環變量
do循環
int i = 0;
0040B93E mov dword ptr [i],0
do
{
++i;
0040B945 mov eax,dword ptr [i]
0040B948 add eax,1
0040B94B mov dword ptr [i],eax
} while (i<10);
0040B94E cmp dword ptr [i],0Ah
0040B952 jl wmain+25h (40B945h)
上面的do循環就是用一個簡單的條件比較指令跳轉回去:
cmp <循環變量><限制變量>
jl <循環開始>
while循環
int i = 0;
0040B93E mov dword ptr [i],0
while (i<10)
0040B945 cmp dword ptr [i],0Ah
0040B949 jge wmain+36h (40B956h)
{
++i;
0040B94B mov eax,dword ptr [i]
0040B94E add eax,1
0040B951 mov dword ptr [i],eax
}
0040B954 jmp wmain+25h (40B945h)
while要復雜一些,因為wile除了開始的時候判斷循環條件之外,后面還要有一條無條件跳轉指令:
A: cmp <循環變量>,<限制變量>
jge B
(循環體)

jmp A
B: (跳出循環)
if-else判斷分支
int i = 0;
0040B93E mov dword ptr [i],0
int j = 0;
0040B945 mov dword ptr [j],0
if ( i < 10 )
0040B94C cmp dword ptr [i],0Ah
0040B950 jge wmain+3Bh (40B95Bh)
{
j = 10;
0040B952 mov dword ptr [j],0Ah
0040B959 jmp wmain+51h (40B971h)
}
else if (i < 20 )
0040B95B cmp dword ptr [i],14h
0040B95F jge wmain+4Ah (40B96Ah)
{
j = 20;
0040B961 mov dword ptr [j],14h
}
else
0040B968 jmp wmain+51h (40B971h)
{
j = 30;
0040B96A mov dword ptr [j],1Eh
}
return 0;
0040B971 xor eax,eax
if 判斷都是使用cmp加上條件跳轉指令。
所以開始的反匯編為:
if ( i < 10 )
0040B94C cmp dword ptr [i],0Ah ;判斷點
0040B950 jge wmain+3Bh (40B95Bh) ;跳轉到下一個else if
else if和else的特點是,在開始的地方都有一條無條件跳轉指令,跳轉到判斷結束處,阻止前面的分支執行結束后,直接進入這個分支的可能,這個分支執行的唯一條件為前面的判斷不滿足。else則在jmp之后直接執行操作,而else if則開始重復if之后的操作,用cmp比較,然后用條件質量進行跳轉。
0040B959 jmp wmain+51h (40B971h) ;跳轉到判斷塊外
}
else if (i < 20 )
0040B95B cmp dword ptr [i],14h
0040B95F jge wmain+4Ah (40B96Ah) ;比較,條件跳轉,目標為下一個分支
{
j = 20;
0040B961 mov dword ptr [j],14h
}
switch-case 判斷分支
switch的特點是有多個判斷。因為switch顯然不會判斷大于小于,所以都是je,分別跳轉到每個case處,最有一個是無條件跳轉,直接跳到default處。
對于break,會增加一個無條件跳轉語句,跳轉至結尾
int i = 0;
0040B93E mov dword ptr [i],0
int j = 0;
0040B945 mov dword ptr [j],0
switch (i)
0040B94C mov eax,dword ptr [i]
0040B94F mov dword ptr [ebp-0DCh],eax
0040B955 cmp dword ptr [ebp-0DCh],0
0040B95C je wmain+49h (40B969h) ;判斷case 1
0040B95E cmp dword ptr [ebp-0DCh],1
0040B965 je wmain+52h (40B972h) ;判斷case 2
0040B967 jmp wmain+59h (40B979h) ;跳轉到default

{
case 0:
j = 0;
0040B969 mov dword ptr [j],0
break; ;跳轉到結束
0040B970 jmp wmain+60h (40B980h)
case 1:
j = 1;
0040B972 mov dword ptr [j],1
default:
j = 3;
0040B979 mov dword ptr [j],3
}

return 0;
0040B980 xor eax,eax
所以如果看到有多個連續的
cmp
je
標志著可能是swith語句
訪問結構體數組成員
對于以下代碼:
struct A


{
int a;
int b;
int c;
};

int wmain(int argc, wchar_t* argv[])


{
A ar[3];
for (int i=0;i<3;++i)

{
ar[i].a = 0;
ar[i].b = 0;
ar[i].c = 0;
}

return 0;
}
for循環中所對應的匯編為
ar[i].a = 0;
0040B956 mov eax,dword ptr [i] ;訪問第i個數據
0040B959 imul eax,eax,0Ch ;0ch為結構體的大小,這里得到訪問第i個機構體的地址偏移
0040B95C mov dword ptr ar[eax],0 ;取得第i個結構體的第一個元素地址
ar[i].b = 0;
0040B964 mov eax,dword ptr [i]
0040B967 imul eax,eax,0Ch
0040B96A mov dword ptr [ebp+eax-24h],0
ar[i].c = 0;
0040B972 mov eax,dword ptr [i]
0040B975 imul eax,eax,0Ch
0040B978 mov dword ptr [ebp+eax-20h],0
對于結構體數組的訪問有個很明顯的特征:使用imul取得某個數組元素的地址偏移,然后在加上所要訪問結構體成員的地址偏移。同時,大多數情況下結構的的大小都是在編譯期決定的,imul的最后一個參數會是個常量。
閱讀匯編代碼的一些技巧
1.將指令分類:
首先F(function)類指令:是函數調用相關代碼,這些代碼用于函數或者作為一個函數數被調用。幾乎凡是堆棧操作(備份集陳啟或者壓入參數)可全部歸入此類。此外還有call指令、堆棧恢復。
然后C(control)類指令 :設計判斷和跳轉指令,以及對循環變量操作的指令。這些代碼用于循環、判斷語句。
剩余D(data)類指令:數據處理指令,應該不包含函數調用,多半不含有堆操作,也不會含有跳轉。
2.翻譯D類指令。
3.表達式的合并與控制流程的結合。
Reference:
學 Win32 匯編[29] - 串指令: MOVS*、CMPS*、SCAS*、LODS*、REP、REPE、REPNE 等
《天書夜讀-從匯編語言到Windows內核編程》