01 |
#include
<stdio.h> |
02 |
long test( int a, int b) |
03 |
{ |
04 |
a
= a + 1; |
05 |
b
= b + 100; |
06 |
return a
+ b; |
07 |
} |
08 |
09 |
void main() |
10 |
{ |
11 |
printf ( "%d" ,test(1000,2000)); |
12 |
} |
寫成32位彙編就是這樣:
01 |
; ////////////////////////////////////////////////////////////////////////////////////////////////////// |
02 |
.386 |
03 |
.model
flat,stdcall ;這裏我們用stdcall 就是函數參數 壓棧的時候從最後一個開始壓,和被調用函數負責清棧 |
04 |
option
casemap:none ;區分大小寫 |
05 |
06 |
includelib
msvcrt.lib ;這裏是引入類庫 相當於 #include<stdio.h>了 |
07 |
printf PROTO
C: DWORD ,:VARARG
;這個就是聲明一下我們要用的函數頭,到時候 彙編程序會自動到msvcrt.lib裏面找的了 |
08 |
;:VARARG
表後面的參數不確定 因爲C就是這樣的 printf ( const char *,
...); |
09 |
;這樣的函數要注意
不是被調用函數負責清棧 因爲它本身不知道有多少個參數 |
10 |
;而是有調用者負責清棧
下面會詳細說明 |
11 |
.data |
12 |
szTextFmt BYTE '%d' ,0
;這個是用來類型轉換的,跟C的一樣,字符用字節類型 |
13 |
a
dword 1000 ;假設 |
14 |
b
dword 2000 ;處理數值都用雙字 沒有 int 跟 long 的區別 |
15 |
16 |
; ///////////////////////////////////////////////////////////////////////////////////////// |
17 |
.code |
18 |
19 |
_test
proc ;A: DWORD ,B: DWORD |
20 |
push
ebp |
21 |
mov
ebp,esp |
22 |
mov
eax,dword ptr ss:[ebp+8] |
23 |
add
eax,1 |
24 |
mov
edx,dword ptr ss:[ebp+0Ch] |
25 |
add
edx,100 |
26 |
add
eax,edx |
27 |
pop
ebp |
28 |
retn
8 |
29 |
_test
endp |
30 |
31 |
_main
proc |
32 |
push
dword ptr ds:b ;反彙編我們看到的b就不是b了而是一個[*****]數字 dword ptr 就是我們在ds(數據段)把[*****] |
33 |
;開始的一個雙字長數值取出來 |
34 |
push
dword ptr ds:a ;跟她對應的還有 byte ptr ****就是取一個字節出來 比如這樣 mov al,byte ptr ds:szTextFmt |
35 |
;就把
% 取出來 而不包括 d |
36 |
call
_test |
37 |
push
eax ;假設push eax的地址是××××× |
38 |
push
offset szTextFmt |
39 |
call printf |
40 |
add
esp,8 |
41 |
ret |
42 |
_main
endp |
43 |
end
_main |
44 |
45 |
; //////////////////////////////////////////////////////////////
下面介紹堆棧的變化 |
46 |
</stdio.h> |
首先要明白的是操作堆棧段, ss 只能用 esp或ebp寄存器 其他的寄存器eax ebx edx等都不能夠用。而esp永遠指向堆棧棧頂,ebp用來在堆棧段裏面尋址。
push 指令是壓棧 ESP=ESP-4,pop 指令是出棧 ESP=ESP+4。
我們假設main函數一開始堆棧定是 ESP=400。
01 |
push
dword ptr ds:b ;ESP-4=396 ->裏面的值就是 2000 就是b的數值 |
02 |
push
dword ptr ds:a ;ESP-4=392 ->裏面的值就是 1000 就是a的數值 |
03 |
call
test ;ESP-4=388->裏面的數值是什麼?這個太重要了 就是我們用來找遊戲函數的原理所在。 |
04 |
裏面的數值就是call
test 指令下一條指令的地址->即push eax的地址××××× |
05 |
06 |
到了test函數裏面 |
07 |
08 |
push
ebp ;ESP-4=384->裏面保存了當前ebp的值 而不是把ebp清零 |
09 |
mov
ebp,esp ;這裏ESP=384就沒變化了,但是 ebp=esp=384,爲什麼要這樣做呢 因爲我們要用ebp到堆棧裏面找參數 |
10 |
mov
eax,dword ptr ss:[ebp+8] ;反彙編是這樣的 想想爲什麼a就是[ebp+8]呢 |
11 |
;我們往上看看堆棧裏地址392處就保存着a的值
這裏ebp=384 加上8正好就是392了 |
12 |
;這樣就把傳遞過來的1000拿了出來eax=1000 |
13 |
add
eax,1 ;相當於 a+1了 eax=1001 |
14 |
mov
edx,dword ptr ss:[ebp+0Ch] ; 0Ch=12 一樣道理這裏指向堆棧的地址是384+12=396 就是2000了 edx=2000 |
15 |
add
edx,100 ;相當於 b+100 edx=2100 |
16 |
add
eax,edx ;eax=eax+edx=1001+2100=3101 這裏eax已經保存了最終的結果了 |
17 |
;因爲win32彙編一般用eax返回結果
所以如果最終結果不是在eax裏面的話 還要把它放到eax |
18 |
;比如假設我的結果保存在變量nRet裏面
最後還是要這樣 mov eax,dword ptr nRet |
19 |
pop
ebp ;ESP=384+4=388 而保存在棧頂384的值 保存到 ebp中 即恢復ebp原來的值 |
20 |
;因爲一開始我們就把ebp的值壓棧了,mov
ebp,esp已經改變了ebp的值,這裏恢復就是保證了堆棧平衡 |
21 |
retn
8 ;ESP+8->396 這裏retn是由系統調用的 我們不用管 系統會自動把EIP指針指向 原來的call的下一條指令 |
22 |
;由於是系統自動恢復了call那裏的壓棧所以
真正返回到的時候ESP+4就是恢復了call壓棧的堆棧 |
23 |
;到了這個時候
ESP=400 就是函數調用開始的堆棧,就是說函數調用前跟函數調用後的堆棧是一樣的 |
24 |
;這就是堆棧平衡 |
25 |
由於我們用stdcall上面retn
8就是被調用者負責恢復堆棧的意思了,函數test是被調用者,所以負責把堆棧加8,call 那裏是系統自動恢復的 |
26 |
27 |
push
eax ;ESP-4=396->裏面保存了eax的值3101 |
28 |
;上面已經看到了eax保存着返回值,我們要把它傳給 printf 也是通過堆棧傳遞 |
29 |
push
offset szTextFmt ;ESP-4=392->裏面保存了szTextFmt的地址 也就是C裏面的指針 實際上沒有什麼把字符串傳遞的,我們傳的都是地址 |
30 |
;無論是在彙編或C
所以在彙編裏沒有什麼字符串類型 用最多的就是 DWORD 。嘿嘿遊戲裏面傳遞參數
簡單多了 |
31 |
call printf ;ESP-4=388->裏面保存了下一條指令的地址 |
32 |
add
esp,8 ;ESP+8=400 恢復了調用 printf 前的堆棧狀態 |
33 |
;上面說了由於 printf 後面參數是:VARARG
這樣的類型是有調用者恢復堆棧的 所以 printf 裏面沒有retn
8之類的指令 |
34 |
;這是由調用者負責清棧
main是調用者 所以下面一句就是 add esp,8 把堆棧恢復到調用 printf 之前 |
35 |
;而call printf 那裏的壓棧
是由系統做的 恢復的工作也是系統完成 我們不用理 只是知道里面保存是返回地址就夠 |
36 |
37 |
;了 |
38 |
ret
;main 函數返回 其他的事情是系統自動搞定 我們不用理 任務完成 |