5.10 匯編語言:匯編過程與結(jié)構(gòu)
過程的實(shí)現(xiàn)離不開堆棧的應(yīng)用,堆棧是一種后進(jìn)先出`(LIFO)`的數(shù)據(jù)結(jié)構(gòu),最后壓入棧的值總是最先被彈出,而新數(shù)值在執(zhí)行壓棧時(shí)總是被壓入到棧的最頂端,棧主要功能是暫時(shí)存放數(shù)據(jù)和地址,通常用來保護(hù)斷點(diǎn)和現(xiàn)場。
棧是由`CPU`管理的線性內(nèi)存數(shù)組,它使用兩個(gè)寄存器`(SS和ESP)`來保存棧的狀態(tài),SS寄存器存放段選擇符,而ESP寄存器的值通常是指向特定位置的一個(gè)32位偏移值,我們很少需要直接操作ESP寄存器,相反的ESP寄存器總是由`CALL,RET,PUSH,POP`等這類指令間接性的修改。
CPU提供了兩個(gè)特殊的寄存器用于標(biāo)識位于系統(tǒng)棧頂端的棧幀。
? - ESP 棧指針寄存器:棧指針寄存器,其內(nèi)存放著一個(gè)指針,該指針永遠(yuǎn)指向系統(tǒng)棧最上面一個(gè)棧幀的棧頂。
? - EBP 基址指針寄存器:基址指針寄存器,其內(nèi)存放著一個(gè)指針,該指針永遠(yuǎn)指向系統(tǒng)棧最上面一個(gè)棧幀的底部。
在通常情況下ESP是可變的,隨著棧的生成而逐漸變小,而EBP寄存器是固定的,只有當(dāng)函數(shù)的調(diào)用后,發(fā)生入棧操作而改變。
?- 執(zhí)行PUSH壓棧時(shí),堆棧指針自動(dòng)減4,再將壓棧的值復(fù)制到堆棧指針?biāo)赶虻膬?nèi)存地址。
?- 執(zhí)行POP出棧時(shí),從棧頂移走一個(gè)值并將其復(fù)制給內(nèi)存或寄存器,然后再將堆棧指針自動(dòng)加4。
?- 執(zhí)行CALL調(diào)用時(shí),CPU會用堆棧保存當(dāng)前被調(diào)用過程的返回地址,直到遇到RET指令再將其彈出。
### 10.1 PUSH/POP
PUSH和POP是匯編語言中用于堆棧操作的指令,它們通常用于保存和恢復(fù)寄存器的值,參數(shù)傳遞和函數(shù)調(diào)用等。
PUSH指令用于將操作數(shù)壓入堆棧中,它執(zhí)行的操作包括將操作數(shù)復(fù)制到堆棧的棧頂,并將堆棧指針(ESP)減去相應(yīng)的字節(jié)數(shù)。指令格式如下:
```ASM
PUSH operand
```
其中,operand可以是8位,16位或32位的寄存器,立即數(shù),以及內(nèi)存中的某個(gè)值。例如,要將寄存器EAX的值壓入堆棧中,可以使用以下指令:
```ASM
PUSH EAX
```
從匯編代碼的角度來看,PUSH指令將操作數(shù)存儲到堆棧中,它實(shí)際上是一個(gè)入棧操作。
POP指令用于將堆棧中棧頂?shù)闹祻棾龅街付ǖ哪康牟僮鲾?shù)中,它執(zhí)行的操作包括將堆棧頂部的值移動(dòng)到指定的操作數(shù),并將堆棧指針增加相應(yīng)的字節(jié)數(shù)。指令格式如下:
```ASM
POP operand
```
其中,operand可以是8位,16位或32位的寄存器,立即數(shù),以及內(nèi)存中的某個(gè)位置。例如,要將從堆棧中彈出的值存儲到BX寄存器中,可以使用以下指令:
```ASM
POP EBX
```
從匯編代碼的角度來看,POP指令將從堆棧中取出一個(gè)值,并將其存儲到目的操作數(shù)中,它是一個(gè)出棧操作。
在函數(shù)調(diào)用時(shí),PUSH指令被用于向堆棧中推送函數(shù)的參數(shù),這些參數(shù)可以是寄存器、立即數(shù)或者內(nèi)存中的某個(gè)值。在函數(shù)返回之前,POP指令被用于將堆棧頂部的值彈出,并將其存儲到寄存器或者內(nèi)存中。
讀者需要特別注意,在使用`PUSH`和`POP`指令時(shí)需要保證堆棧的平衡,也就是說,每個(gè)`PUSH`指令必須有對應(yīng)的`POP`指令,否則堆棧會失去平衡,最終導(dǎo)致程序出現(xiàn)錯(cuò)誤。
在讀者了解了這兩條指令時(shí)則可以執(zhí)行一些特殊的操作,如下代碼我們以數(shù)組入棧與出棧為例,執(zhí)行`PUSH`指令時(shí),首先減小`ESP`的值,然后把源操作數(shù)復(fù)制到堆棧上,執(zhí)行`POP`指令則是先將數(shù)據(jù)彈出到目的操作數(shù)中,然后再執(zhí)行`ESP`值增加4,并以此分別將數(shù)組中的元素壓入棧,最終再通過POP將元素反彈出來。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
? Array DWORD 1,2,3,4,5,6,7,8,9,10
? szFmt BYTE '%d ',0dh,0ah,0
.code
? main PROC
? ? ; 使用Push指令將數(shù)組正向入棧
? ? mov eax,0
? ? mov ecx,10
? S1:
? ? push dword ptr ds:[Array + eax * 4]
? ? inc eax
? ? loop S1
? ??
? ? ; 使用pop指令將數(shù)組反向彈出
? ? mov ecx,10
? S2:
? ? push ecx? ? ? ? ? ? ? ? ? ? ? ? ?; 保護(hù)ecx
? ? pop ebx? ? ? ? ? ? ? ? ? ? ? ? ? ; 將Array數(shù)組元素彈出到ebx
? ? invoke crt_printf,addr szFmt,ebx
? ? pop ecx? ? ? ? ? ? ? ? ? ? ? ? ? ; 彈出ecx
? ? loop S2
? ??
? ? int 3
? main ENDP
END main
```
至此當(dāng)讀者理解了這兩個(gè)指令之后,那么利用堆棧的先進(jìn)后出特定,我們就可以實(shí)現(xiàn)將特殊的字符串反轉(zhuǎn)后輸出的效果,首先我們循環(huán)將字符串壓入堆棧,然后再從堆棧中反向彈出來,這樣就可以實(shí)現(xiàn)字符串的反轉(zhuǎn)操作,這段代碼的實(shí)現(xiàn)也相對較為容易;
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
? MyString BYTE "hello lyshark",0
? NameSize DWORD ($ - MyString) - 1
? szFmt BYTE '%s',0dh,0ah,0
.code
? main PROC
? ? ; 正向壓入字符串
? ? mov ecx,dword ptr ds:[NameSize]
? ? mov esi,0
? S1: movzx eax,byte ptr ds:[MyString + esi]
? ? push eax
? ? inc esi
? ? loop S1
? ? ; 反向彈出字符串
? ? mov ecx,dword ptr ds:[NameSize]
? ? mov esi,0
? S2: pop eax
? ? mov byte ptr ds:[MyString + esi],al
? ? inc esi
? ? loop S2
? ??
? ? invoke crt_printf,addr szFmt,addr MyString
? ? int 3
? main ENDP
END main
```
### 10.2 PROC/ENDP
PROC/ENDP 偽指令是用于定義過程(函數(shù))的偽指令,這兩個(gè)偽指令可分別定義過程的開始和結(jié)束位置。此處讀者需要注意,這兩條偽指令并非是匯編語言中所兼容的,而是`MASM`編譯器為我們提供的一個(gè)宏,是`MASM`的一部分,它允許程序員使用匯編語言定義過程(函數(shù))可以像標(biāo)準(zhǔn)匯編指令一樣使用。
對于不使用宏定義來創(chuàng)建函數(shù)時(shí)我們通常會自己管理函數(shù)棧參數(shù),而有了宏定義這些功能都可交給編譯器去管理,下面的一個(gè)案例中,我們通過使用過程創(chuàng)建`ArraySum`函數(shù),實(shí)現(xiàn)對整數(shù)數(shù)組求和操作,函數(shù)默認(rèn)將返回值存儲在`EAX`中,并打印輸出求和后的參數(shù)。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
? MyArray? DWORD 1,2,3,4,5,6,7,8,9,10
? Sum? ? ? DWORD ?
? szFmt? ? BYTE '%d',0dh,0ah,0
.code
? ; 數(shù)組求和過程
? ArraySum PROC
? ? push esi? ? ? ? ? ? ? ? ? ? ?; 保存ESI,ECX
? ? push ecx
? ? xor eax,eax
? ??
? S1: add eax,dword ptr ds:[esi]? ?; 取值并相加
? ? add esi,4? ? ? ? ? ? ? ? ? ? ; 遞增數(shù)組指針
? ? loop S1
? ? pop ecx? ? ? ? ? ? ? ? ? ? ? ; 恢復(fù)ESI,ECX
? ? pop esi
? ? ret
? ArraySum endp
? main PROC
? ? lea esi,dword ptr ds:[MyArray]? ?; 取出數(shù)組基址
? ? mov ecx,lengthof MyArray? ? ? ? ?; 取出元素?cái)?shù)目
? ? call ArraySum? ? ? ? ? ? ? ? ? ? ; 調(diào)用方法
? ? mov dword ptr ds:[Sum],eax? ? ? ?; 得到結(jié)果
? ? invoke crt_printf,addr szFmt,Sum
? ? int 3
? main ENDP
END main
```
接著我們來實(shí)現(xiàn)一個(gè)具有獲取隨機(jī)數(shù)功能的案例,在C語言中如果需要獲得一個(gè)隨機(jī)數(shù)一般會調(diào)用`Seed`函數(shù),如果讀者逆向分析過這個(gè)函數(shù)的實(shí)現(xiàn)原理,那么讀者應(yīng)該能理解,在調(diào)用取隨機(jī)數(shù)之前會生成一個(gè)隨機(jī)數(shù)種子,這個(gè)隨機(jī)數(shù)種子的生成則依賴于`0x343FDh`這個(gè)特殊的常量地址,當(dāng)我們每次訪問該地址都會產(chǎn)出一個(gè)隨機(jī)的數(shù)據(jù),當(dāng)?shù)玫皆摂?shù)據(jù)后,我們再通過除法運(yùn)算取出溢出數(shù)據(jù)作為隨機(jī)數(shù)使用實(shí)現(xiàn)了該功能。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
? seed DWORD 1
? szFmt? ? BYTE '隨機(jī)數(shù): %d',0dh,0ah,0
.code
? ; 生成 0 - FFFFFFFFh 的隨機(jī)種子
? Random32 PROC
? ? push? edx
? ? mov? ?eax, 343FDh
? ? imul? seed
? ? add? ?eax, 269EC3h
? ? mov? ?seed, eax
? ? ror? ?eax,8
? ? pop? ?edx
? ? ret
? Random32 endp
??
? ; 生成隨機(jī)數(shù)
? RandomRange PROC
? ? push? ebx
? ? push? edx
? ??
? ? mov? ?ebx,eax
? ? call? Random32
? ? mov? ?edx,0
? ? div? ?ebx
? ? mov? ?eax,edx
? ? pop? ?edx
? ? pop? ?ebx
? ? ret
? RandomRange endp
? main PROC
??
? ? ; 調(diào)用后取出隨機(jī)數(shù)
? ? call RandomRange
? ? invoke crt_printf,addr szFmt,eax
? ? int 3
? main ENDP
END main
```
### 10.3 局部參數(shù)傳遞
在匯編語言中,可以使用堆棧來傳遞函數(shù)參數(shù)和創(chuàng)建局部變量。當(dāng)程序執(zhí)行到函數(shù)調(diào)用語句時(shí),需要將函數(shù)參數(shù)傳遞給被調(diào)用函數(shù)。為了實(shí)現(xiàn)參數(shù)傳遞,程序會將參數(shù)壓入棧中,然后調(diào)用被調(diào)用函數(shù)。被調(diào)用函數(shù)從棧中彈出參數(shù)并執(zhí)行,然后將返回值存儲在寄存器中,最后通過跳轉(zhuǎn)返回到調(diào)用函數(shù)。
局部變量也可以通過在棧中分配內(nèi)存來創(chuàng)建。在函數(shù)開始時(shí),可以使用push指令將局部變量壓入棧中。在函數(shù)結(jié)束時(shí),可以使用pop指令將變量從棧中彈出。由于棧是后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),局部變量的創(chuàng)建可以很方便地通過在棧上壓入一些數(shù)據(jù)來實(shí)現(xiàn)。
局部變量是在程序運(yùn)行時(shí)由系統(tǒng)動(dòng)態(tài)的在棧上開辟的,在內(nèi)存中通常在基址指針`(EBP)`之下,盡管在匯編時(shí)不能給定默認(rèn)值,但可以在運(yùn)行時(shí)初始化,如下一段C語言偽代碼:
```C
void MySub()
{
? int var1 = 10;
? int var2 = 20;
}
```
上述的代碼經(jīng)過C編譯后,會變成如下匯編指令,其中`EBP-4`必須是4的倍數(shù),因?yàn)槟J(rèn)就是4字節(jié)存儲,如果去掉了`mov esp,ebp`,那么當(dāng)執(zhí)行`pop ebp`時(shí)將會得到`EBP`等于10,執(zhí)行`RET`指令會導(dǎo)致控制轉(zhuǎn)移到內(nèi)存地址10處執(zhí)行,從而程序會崩潰。
```ASM
MySub PROC
? push ebp? ? ? ? ? ? ? ? ? ; 將EBP存儲在棧中
? mov ebp,esp? ? ? ? ? ? ? ?; 堆棧框架的基址
? sub esp,8? ? ? ? ? ? ? ? ?; 創(chuàng)建局部變量空間(分配2個(gè)局部變量)
? mov DWORD PTR [ebp-8],10? ; var1 = 10
? mov DWORD PTR [ebp-4],20? ; var2 = 20
? mov esp,ebp? ? ? ? ? ? ? ?; 從堆棧上刪除局部變量
? pop ebp? ? ? ? ? ? ? ? ? ?; 恢復(fù)EBP指針
? ret 8? ? ? ? ? ? ? ? ? ? ?; 返回,清理堆棧
MySub ENDP
```
**********
為了使上述代碼片段更易于理解,可以在上述的代碼的基礎(chǔ)上給每個(gè)變量的引用地址都定義一個(gè)符號,并在代碼中使用這些符號,如下代碼所示,代碼中定義了一個(gè)名為`MySub`的過程,該過程將兩個(gè)局部變量分別設(shè)置為`10`和`20`。
在該過程中,首先使用`push ebp`指令將舊的基址指針壓入棧中,并將`ESP`寄存器的值存儲到`ebp`中。這個(gè)舊的基址指針將在函數(shù)執(zhí)行完畢后被恢復(fù)。然后,我們使用`sub esp,8`指令將`8`字節(jié)的空間分配給兩個(gè)局部變量。在堆棧上分配的空間可以通過`var1_local`和`var2_local`符號來訪問。在這里,我們定義了兩個(gè)符號,將它們與`ebp`寄存器進(jìn)行偏移以訪問這些局部變量。`var1_local`的地址為`[ebp-8]`,`var2_local`的地址為`[ebp-4]`。然后,我們使用`mov`指令將`10`和 `20`分別存儲到這些局部變量中。最后,我們將`ESP`寄存器的值存儲回`ebp`中,并使用`pop ebp`指令將舊的基址指針彈出堆?!,F(xiàn)在,棧頂指針(ESP)下移恢復(fù)上面分配的8個(gè)字節(jié)的空間,最后通過`ret 8`返回到調(diào)用函數(shù)。
在使用堆棧傳參和創(chuàng)建局部變量時(shí),需要謹(jǐn)慎考慮棧指針的位置,并確保遵守調(diào)用約定以確保正確地傳遞參數(shù)和返回值。
```ASM
var1_local EQU DWORD PTR [ebp-8]? ?; 添加符號1
var2_local EQU DWORD PTR [ebp-4]? ?; 添加符號2
MySub PROC
? push ebp
? mov ebp,esp
? sub esp,8
? mov var1_local,10
? mov var2_local,20
? mov esp,ebp
? pop ebp
? ret 8
MySub ENDP
```
**********
接著我們來實(shí)現(xiàn)一個(gè)具有功能的案例,首先為了能更好的讓讀者理解我們先使用C語言方式實(shí)現(xiàn)`MakeArray()`函數(shù),該函數(shù)的內(nèi)部是動(dòng)態(tài)生成的一個(gè)`MyString`數(shù)組,并通過循環(huán)填充為星號字符串,最后使用`POP`彈出,并輸出結(jié)果,觀察后嘗試用匯編實(shí)現(xiàn)。
```C
void makeArray()
{
? char MyString[30];
? for(int i=0;i<30;i++)
? {
? ? myString[i] = "*";
? }
}
call makeArray()
```
上述C語言代碼如果翻譯為匯編格式則如下所示,代碼使用匯編語言實(shí)現(xiàn)`makeArray`的程序,該程序開辟了一個(gè)長度為`30`的數(shù)組,將其中的元素填充為`*`,然后彈出兩個(gè)元素,并將它們輸出到控制臺。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
? szFmt BYTE '出棧數(shù)據(jù): %x ',0dh,0ah,0
.code
? makeArray PROC
? ? push ebp
? ? mov ebp,esp
? ??
? ? ; 開辟局部數(shù)組
? ? sub esp,32? ? ? ? ? ? ? ? ? ? ; MyString基地址位于 [ebp - 30]
? ? lea esi,[ebp - 30]? ? ? ? ? ? ; 加載MyString的地址
? ??
? ? ; 填充數(shù)據(jù)
? ? mov ecx,30? ? ? ? ? ? ? ? ? ? ; 循環(huán)計(jì)數(shù)
? S1: mov byte ptr ds:[esi],'*'? ? ?; 填充為*
? ? inc esi? ? ? ? ? ? ? ? ? ? ? ?; 每次遞增一個(gè)字節(jié)
? ? loop S1
? ??
? ? ; 彈出2個(gè)元素并輸出,出棧數(shù)據(jù)
? ? pop eax
? ? invoke crt_printf,addr szFmt,eax
? ??
? ? pop eax
? ? invoke crt_printf,addr szFmt,eax??
? ??
? ? ; 以下平棧,由于我們手動(dòng)彈出了2個(gè)數(shù)據(jù)
? ? ; 則平棧 32 - (2 * 4) = 24?
? ? add esp,24? ? ? ? ? ? ? ? ? ? ; 平棧
? ? mov esp,ebp
? ? pop ebp? ? ? ? ? ? ? ? ? ? ? ?; 恢復(fù)EBP
? ? ret
? makeArray endp
? main PROC
? ? call makeArray
? ? invoke ExitProcess,0
? main ENDP
END main
```
在該程序的開始部分,我們首先通過`push ebp`和`mov ebp,esp`指令保存舊的基址指針并將當(dāng)前棧頂指針`(ESP)`存儲到`ebp`中。然后,我們使用`sub esp, 32`指令開辟一個(gè)長度為`30`的數(shù)組`MyString`。我們將`MyString`數(shù)組的基地址存儲在`[ebp - 30]`的位置。使用`lea esi, [ebp - 30]`指令將`MyString`的基地址加載到`esi`寄存器中。該指令偏移`ebp-30`是因?yàn)閌ebp-4`是`MakeArray`函數(shù)的第一個(gè)參數(shù)的位置,因此需要增加四個(gè)字節(jié)。我們利用`MOV byte ptr ds:[esi],'*'`指令將`MyString`中的所有元素填充為`*`。
然后,使用`pop eax`和`invoke crt_printf, addr szFmt, eax`指令兩次彈出兩個(gè)元素,并使用`crt_printf`函數(shù)輸出這些元素。該函數(shù)在`msvcrt.dll`庫中實(shí)現(xiàn),用于將格式化的信息輸出到控制臺。在輸出數(shù)據(jù)之后,我們通過`add esp,24`和`mov esp,ebp`指令將堆棧平衡,恢復(fù)舊的基址指針`ebp`,然后從堆棧中彈出`ebp`,并通過`ret`指令返回到調(diào)用程序。
**********
接著我們繼續(xù)來對比一下堆棧中參數(shù)傳遞的異同點(diǎn),平棧的方式一般可分為調(diào)用者平棧和被調(diào)用者平棧,在使用堆棧傳參時(shí),需要平衡棧以恢復(fù)之前的堆棧指針位置。
- 當(dāng)平棧由被調(diào)用者完成時(shí),被調(diào)用函數(shù)使用`ret`指令將控制權(quán)返回到調(diào)用函數(shù),并從堆棧中彈出返回地址。此時(shí),被調(diào)用函數(shù)需要將之前分配的局部變量從堆棧中彈出,以便調(diào)用函數(shù)能夠恢復(fù)堆棧指針的位置。因此,被調(diào)用函數(shù)必須知道其在堆棧上分配的內(nèi)存大小,并將該大小與其`ret`指令中的參數(shù)相匹配,以便調(diào)用函數(shù)可以正確恢復(fù)堆棧指針位置。
- 當(dāng)平棧由調(diào)用者完成時(shí),調(diào)用函數(shù)需要在調(diào)用子函數(shù)之前平衡堆棧。因此,調(diào)用函數(shù)需要知道子函數(shù)在堆棧上分配的內(nèi)存大小,并在調(diào)用子函數(shù)之前向堆棧提交額外的空間。調(diào)用函數(shù)可以使用`add esp, N`指令來恢復(fù)堆棧指針的位置,其中 N 是被調(diào)用函數(shù)在堆棧上分配的內(nèi)存大小。然后,調(diào)用函數(shù)調(diào)用被調(diào)用函數(shù),該函數(shù)將返回并將堆棧指針恢復(fù)到調(diào)用函數(shù)之前的位置。
如下這段匯編代碼中筆者分別實(shí)現(xiàn)了兩種調(diào)用方式,其中`MyProcA`函數(shù)是一種被調(diào)用者平棧,由于調(diào)用者并沒有堆棧修正所以需要在函數(shù)內(nèi)部通過使用`ret 12`的方式平棧,之所以是12是因?yàn)槲覀兪褂昧巳齻€(gè)局部變量,而第二個(gè)`MyProcB`函數(shù)則是調(diào)用者平棧,該方式在函數(shù)內(nèi)部并沒有返回任何參數(shù),所以在調(diào)用函數(shù)結(jié)束后需要通過`add esp,4`的方式對堆棧進(jìn)行修正。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
? szFmt BYTE '數(shù)據(jù): %d ',0dh,0ah,0
.code
? ; 第一種方式:被調(diào)用者平棧
? MyProcA PROC
? ? push ebp
? ? mov ebp,esp
? ??
? ? xor eax,eax
? ? mov eax,dword ptr ss:[ebp + 16]? ?; 獲取第一個(gè)參數(shù)
? ? mov ebx,dword ptr ss:[ebp + 12]? ?; 獲取第二個(gè)參數(shù)
? ? mov ecx,dword ptr ss:[ebp + 8]? ? ; 獲取第三個(gè)參數(shù)
? ??
? ? add eax,ebx
? ? add eax,ebx
? ? add eax,ecx
? ??
? ? mov esp,ebp
? ? pop ebp
? ? ret 12? ? ? ?; 此處ret12可平棧,也可使用 add ebp,12
? MyProcA endp
? ; 第二種方式:調(diào)用者平棧
? MyProcB PROC
? ? push ebp
? ? mov ebp,esp
? ??
? ? mov eax,dword ptr ss:[ebp + 8]
? ? add eax,10
? ??
? ? mov esp,ebp
? ? pop ebp
? ? ret
? MyProcB endp
? main PROC
? ? ; 第一種被調(diào)用者M(jìn)yProcA平棧 3*4 = 12
? ? push 1
? ? push 2
? ? push 3
? ? call MyProcA
? ? invoke crt_printf,addr szFmt,eax
? ??
? ? ; 第二種方式:調(diào)用者平棧
? ? push 10
? ? call MyProcB
? ? add esp,4
? ? invoke crt_printf,addr szFmt,eax
? ??
? ? int 3
? main ENDP
END main
```
當(dāng)然了如果讀者認(rèn)為自己維護(hù)堆棧很繁瑣,則此時(shí)可以直接使用`MASM`匯編器提供的`PROC`定義過程,使用該偽指令匯編器會自行計(jì)算所需要使用的變量數(shù)量并自行在結(jié)尾處添加對應(yīng)的平棧語句,這段代碼實(shí)現(xiàn)起來將變得非常容易理解。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
? szFmt BYTE '計(jì)算參數(shù): %d ',0dh,0ah,0
.code
? my_proc PROC x:DWORD,y:DWORD,z:DWORD? ?; 定義過程局部參數(shù)
? ? LOCAL @sum:DWORD? ? ? ? ? ? ? ?; 定義局部變量存放總和
? ??
? ? mov eax,dword ptr ds:[x]
? ? mov ebx,dword ptr ds:[y]? ? ? ?; 分別獲取到局部參數(shù)
? ? mov ecx,dword ptr ds:[z]
? ??
? ? add eax,ebx
? ? add eax,ecx? ? ? ? ? ? ? ? ? ? ; 相加后放入eax
? ? mov @sum,eax
? ? ret
? my_proc endp
? main PROC
? ? LOCAL @ret_sum:DWORD
? ? push 10
? ? push 20
? ? push 30? ? ? ? ? ; 傳遞參數(shù)
? ? call my_proc
? ? mov @ret_sum,eax ; 獲取結(jié)果并打印
? ??
? ? invoke crt_printf,addr szFmt,@ret_sum
? ??
? ? int 3
? main ENDP
END main
```
這里筆者還需要擴(kuò)展一個(gè)偽指令`LOCAL`,LOCAL是一種匯編語言中的偽指令,用于定義存儲在堆棧上的局部變量。使用`LOCAL`指令定義的局部變量只在函數(shù)執(zhí)行時(shí)存在,當(dāng)函數(shù)返回后,該變量將被刪除。根據(jù)使用`LOCAL`指令時(shí)指定的內(nèi)存空間大小,匯編器將為每個(gè)變量保留足夠的空間。
例如,下面是一個(gè)使用LOCAL定義局部變量的示例:
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
? main PROC
? ? ; 定義局部變量,自動(dòng)壓棧/平棧
? ? LOCAL var_byte:BYTE,var_word:WORD,var_dword:DWORD
? ? LOCAL var_array[3]:DWORD
? ??
? ? ; 填充局部變量
? ? mov byte ptr ds:[var_byte],1
? ? mov word ptr ds:[var_word],2
? ? mov dword ptr ds:[var_dword],3
? ??
? ? ; 填充數(shù)組方式1
? ? lea esi,dword ptr ds:[var_array]
? ? mov dword ptr ds:[esi],10
? ? mov dword ptr ds:[esi + 4],20
? ? mov dword ptr ds:[esi + 8],30
? ??
? ? ; 填充數(shù)組方式2
? ? mov var_array[0],100
? ? mov var_array[1],200
? ? mov var_array[2],300
? ??
? ? invoke ExitProcess,0
? main ENDP
END main
```
在上述示例代碼中,`main`過程使用`LOCAL`指令定義了幾個(gè)局部變量,包括一個(gè)字節(jié)類型的變量`var_byte`、一個(gè)字類型的變量`var_word`、一個(gè)雙字類型的變量`var_dword`和一個(gè)包含三個(gè)雙字元素的數(shù)組`var_array`。
在代碼中,我們使用`mov`指令填充這些變量的值。對于字節(jié)類型、字類型和雙字類型的變量,使用`mov byte ptr ds:[var_byte], 1`、`mov word ptr ds:[var_word], 2`和`mov dword ptr ds:[var_dword], 3`指令將相應(yīng)的常數(shù)值存儲到變量中。在填充數(shù)組時(shí),分別使用了兩種不同的方式。一種方式是使用`lea`指令將數(shù)組的地址加載到`esi`寄存器中,然后使用`mov dword ptr ds:[esi],10`等指令將相應(yīng)的常數(shù)值存儲到數(shù)組中。另一種方式是直接訪問數(shù)組元素,如`mov var_array[0], 100`等指令。需要注意,由于數(shù)組元素在內(nèi)存中是連續(xù)存儲的,因此可以使用`[]`操作符訪問數(shù)組元素。
在匯編中使用`LOCAL`偽指令來實(shí)現(xiàn)自動(dòng)計(jì)算局部變量空間,以及最后的平棧操作,將會極大的提高開發(fā)效率。
### 10.4 USES/ENTER
USES是匯編語言中的偽指令,用于保存一組寄存器的狀態(tài),以便函數(shù)調(diào)用過程中可以使用這些寄存器。使用USES時(shí),程序可以保存一組需要保護(hù)的寄存器,匯編器將在程序入口處自動(dòng)向堆棧壓入這些寄存器的值。讀者需注意,我們可以在需要保存寄存器的程序段中使用USES來保護(hù)寄存器,但不應(yīng)在整個(gè)程序中重復(fù)使用寄存器。
ENTER也是一種偽指令,用于創(chuàng)建函數(shù)調(diào)用過程中的堆棧幀。使用ENTER時(shí),程序可以定義一個(gè)名為ENTER的指定大小的堆棧幀。該指令會將新的基準(zhǔn)指針ebp 壓入堆棧同時(shí)將當(dāng)前的基準(zhǔn)指針ebp存儲到另一個(gè)寄存器ebx中,然后將堆棧指針esp減去指定大小的值,獲取新的基地址,并將新的基地址存儲到ebp 中。之后,程序可以在此幀上創(chuàng)建和訪問局部變量,并使用LEAVE指令將堆棧幀刪除,將ebp恢復(fù)為舊的值,同時(shí)將堆棧指針平衡。
在使用USES和ENTER指令時(shí),需要了解這些指令在具體的平臺上的支持情況,以及它們適用的調(diào)用約定。通常情況下,在函數(shù)開頭,我們將使用ENTER創(chuàng)建堆棧幀,然后使用USES指定需要保護(hù)的寄存器。在函數(shù)末尾,我們使用LEAVE刪除堆棧幀。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
? ; USES 自動(dòng)壓入 eax,ebx,ecx,edx
? my_proc PROC USES eax ebx ecx edx x:DWORD,y:DWORD
? ? enter 8,0? ? ? ? ? ; 自動(dòng)保留8字節(jié)堆??臻g
? ? add eax,ebx
? ? leave
? my_proc endp
? main PROC
? ? mov eax,10
? ? mov ebx,20
? ? call my_proc
? ??
? ? int 3
? main ENDP
END main
```
### 10.5 STRUCT/UNION
STRUCT和UNION是匯編語言中的數(shù)據(jù)類型,STRUCT是一種復(fù)合數(shù)據(jù)類型,它將多個(gè)不同類型的變量按順序放置在一起,并使用單個(gè)名稱來引用集合。使用STRUCT時(shí),我們可以將不同類型的變量組合成一個(gè)結(jié)構(gòu)體并定義其屬性,如結(jié)構(gòu)體中包含的成員變量的數(shù)據(jù)類型、名稱和位置。
例如,下面是一個(gè)使用STRUCT定義自定義類型的示例:
```ASM
; 定義一個(gè)名為 MyStruct 的結(jié)構(gòu)體,包含兩個(gè)成員變量。
MyStruct STRUCT
? ?Var1 DWORD ?
? ?Var2 WORD ?
MyStruct ENDS
```
在上述示例代碼中,我們使用`STRUCT`定義了一個(gè)名為`MyStruct `的結(jié)構(gòu)體,其中包含兩個(gè)成員變量`Var1`和`Var2`。其中,`Var1`是`DWORD`類型的數(shù)據(jù)類型,以問號`?`形式指定了其默認(rèn)值,`Var2`是`WORD`類型的數(shù)據(jù)類型。
另一個(gè)數(shù)據(jù)類型是`UNION`,它也是一種復(fù)合數(shù)據(jù)類型,用于將多個(gè)不同類型的變量疊加在同一內(nèi)存位置上。使用`UNION`時(shí),程序內(nèi)存中的數(shù)據(jù)將只占用所有成員變量中最大的數(shù)據(jù)類型變量的大小。與結(jié)構(gòu)體不同,聯(lián)合中的所有成員變量共享相同的內(nèi)存位置。我們可以使用一種成員變量來引用內(nèi)存位置,但在任何時(shí)候僅能有一種成員變量存儲在該內(nèi)存位置中。
例如,下面是一個(gè)使用UNION定義自定義類型的示例:
```ASM
; 定義一個(gè)名為 MyUnion 的聯(lián)合,包含兩個(gè)成員變量。
MyUnion UNION
? ?Var1 DWORD ?
? ?Var2 WORD ?
MyUnion ENDS
```
在上述示例代碼中,我們使用`UNION`定義了一個(gè)名為`MyUnion`的聯(lián)合,其中包含兩個(gè)不同類型的成員變量`Var1`和`Var2`,將它們相對應(yīng)地置于聯(lián)合的同一內(nèi)存位置上。
讀者在使用`STRUCT`和`UNION`時(shí),需要根據(jù)內(nèi)存分布和變量類型來正確訪問成員變量的值。在匯編語言中,結(jié)構(gòu)體和聯(lián)合主要用于定義自定義數(shù)據(jù)類型、通信協(xié)議和系統(tǒng)數(shù)據(jù)結(jié)構(gòu)等,如下一段代碼則是匯編語言中實(shí)現(xiàn)結(jié)構(gòu)體賦值與取值的總結(jié)。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
; 定義坐標(biāo)結(jié)構(gòu)
MyPoint Struct
? pos_x DWORD ?
? pos_y DWORD ?
? pos_z DWORD ?
MyPoint ends
; 定義人物結(jié)構(gòu)
MyPerson Struct
? Fname db 20 dup(0)
? fAge? db 100
? fSex? db 20
MyPerson ends
.data
? ; 聲明結(jié)構(gòu): 使用 <>,{}符號均可
? PtrA MyPoint <10,20,30>
? PtrB MyPoint {100,200,300}
??
? ; 聲明結(jié)構(gòu): 使用MyPerson聲明結(jié)構(gòu)
? UserA MyPerson <'lyshark',24,1>
.code
? main PROC
? ? ; 獲取結(jié)構(gòu)中的數(shù)據(jù)
? ? lea esi,dword ptr ds:[PtrA]
? ? mov eax,(MyPoint ptr ds:[esi]).pos_x
? ? mov ebx,(MyPoint ptr ds:[esi]).pos_y
? ? mov ecx,(MyPoint ptr ds:[esi]).pos_z
? ??
? ? ; 向結(jié)構(gòu)中寫入數(shù)據(jù)
? ? lea esi,dword ptr ds:[PtrB]
? ? mov (MyPoint ptr ds:[esi]).pos_x,10
? ? mov (MyPoint ptr ds:[esi]).pos_y,20
? ? mov (MyPoint ptr ds:[esi]).pos_z,30
? ??
? ? ; 直接獲取結(jié)構(gòu)中的數(shù)據(jù)
? ? mov eax,dword ptr ds:[UserA.Fname]
? ? mov ebx,dword ptr ds:[UserA.fAge]
? ? int 3
? main ENDP
END main
```
接著我們來實(shí)現(xiàn)一個(gè)輸出結(jié)構(gòu)體數(shù)組的功能,結(jié)構(gòu)數(shù)組其實(shí)就是一維的空間,因此使用兩個(gè)比例因子即可實(shí)現(xiàn)尋址操作,如下代碼我們先來實(shí)現(xiàn)一個(gè)簡單的功能,只遍歷第一層,結(jié)構(gòu)數(shù)組外層的數(shù)據(jù)。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
; 定義坐標(biāo)結(jié)構(gòu)
MyPoint Struct
? pos_x DWORD ?
? pos_y DWORD ?
? pos_z DWORD ?
MyPoint ends
; 定義循環(huán)結(jié)構(gòu)
MyCount Struct
? count_x DWORD ?
? count_y DWORD ?
MyCount ends
.data
? ; 聲明結(jié)構(gòu): 使用 <>,{}符號均可
? PtrA? MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
? Count MyCount <0,0>
? szFmt BYTE '結(jié)構(gòu)數(shù)據(jù): %d',0dh,0ah,0
.code
? main PROC
? ? ; 獲取結(jié)構(gòu)中的數(shù)據(jù)
? ? lea esi,dword ptr ds:[PtrA]
? ? mov eax,(MyPoint ptr ds:[esi]).pos_x? ? ? ? ? ; 獲取第一個(gè)結(jié)構(gòu)X
? ? mov eax,(MyPoint ptr ds:[esi + 12]).pos_x? ? ?; 獲取第二個(gè)結(jié)構(gòu)X
? ??
? ? ; while 循環(huán)輸出結(jié)構(gòu)的每個(gè)首元素元素
? ? mov (MyCount ptr ds:[Count]).count_x,0
? S1: cmp (MyCount ptr ds:[Count]).count_x,48? ? ? ? ; 12 * 4 = 48
? ? jge lop_end
? ??
? ? mov ecx,(MyCount ptr ds:[Count]).count_x
? ? mov eax,dword ptr ds:[PtrA + ecx]? ? ? ? ? ? ? ; 尋找首元素
? ? invoke crt_printf,addr szFmt,eax
? ??
? ? mov eax,(MyCount ptr ds:[Count]).count_x
? ? add eax,12? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?; 每次遞增12
? ? mov (MyCount ptr ds:[Count]).count_x,eax
? ? jmp S1
? lop_end:
? ? int 3
? main ENDP
END main
```
接著我們遞增難度,通過每次遞增將兩者的偏移相加,獲得比例因子,通過因子嵌套雙層循環(huán)實(shí)現(xiàn)尋址打印。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
; 定義坐標(biāo)結(jié)構(gòu)
MyPoint Struct
? pos_x DWORD ?
? pos_y DWORD ?
? pos_z DWORD ?
MyPoint ends
; 定義循環(huán)結(jié)構(gòu)
MyCount Struct
? count_x DWORD ?
? count_y DWORD ?
MyCount ends
.data
? ; 聲明結(jié)構(gòu): 使用 <>,{}符號均可
? PtrA? MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
? Count MyCount <0,0>
? szFmt BYTE '結(jié)構(gòu)數(shù)據(jù): %d',0dh,0ah,0
.code
? main PROC
? ? ; 獲取結(jié)構(gòu)中的數(shù)據(jù)
? ? lea esi,dword ptr ds:[PtrA]
? ? mov eax,(MyPoint ptr ds:[esi]).pos_x? ? ? ? ? ; 獲取第一個(gè)結(jié)構(gòu)X
? ? mov eax,(MyPoint ptr ds:[esi + 12]).pos_x? ? ?; 獲取第二個(gè)結(jié)構(gòu)X
? ??
? ? ; while 循環(huán)輸出結(jié)構(gòu)的每個(gè)首元素元素
? ? mov (MyCount ptr ds:[Count]).count_x,0
? S1: cmp (MyCount ptr ds:[Count]).count_x,48? ? ? ? ; 12 * 4 = 48
? ? jge lop_end
? ??
? ? mov (MyCount ptr ds:[Count]).count_y,0
? S3: cmp (MyCount ptr ds:[Count]).count_y,12? ? ? ? ; 3 * 4 = 12
? ? jge S2
? ??
? ? mov eax,(MyCount ptr ds:[Count]).count_x
? ? add eax,(MyCount ptr ds:[Count]).count_y? ? ? ?; 相加得到比例因子
? ??
? ? mov eax,dword ptr ds:[PtrA + eax]? ? ? ? ? ? ? ; 使用相對變址尋址
? ? invoke crt_printf,addr szFmt,eax
? ??
? ? mov eax,(MyCount ptr ds:[Count]).count_y
? ? add eax,4? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ; 每次遞增4
? ? mov (MyCount ptr ds:[Count]).count_y,eax
? ? jmp S3?
? ??
? S2: mov eax,(MyCount ptr ds:[Count]).count_x
? ? add eax,12? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?; 每次遞增12
? ? mov (MyCount ptr ds:[Count]).count_x,eax
? ? jmp S1
? lop_end:
? ? int 3
? main ENDP
END main
```
結(jié)構(gòu)體同樣支持內(nèi)嵌的方式,如下`Rect`指針中內(nèi)嵌兩個(gè)`MyPoint`分別指向左子域和右子域,這里順便定義一個(gè)`MyUnion`聯(lián)合體把,其使用規(guī)范與結(jié)構(gòu)體完全一致,只不過聯(lián)合體只能存儲一個(gè)數(shù)據(jù).
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
; 定義坐標(biāo)結(jié)構(gòu)
MyPoint Struct
? pos_x DWORD ?
? pos_y DWORD ?
? pos_z DWORD ?
MyPoint ends
; 定義左右結(jié)構(gòu)
Rect Struct
? Left MyPoint <>
? Right MyPoint <>
Rect ends
; 定義聯(lián)合體
MyUnion Union
? my_dword DWORD ?
? my_word WORD ?
? my_byte BYTE ?
MyUnion ends
.data
? PointA Rect <>
? PointB Rect {<10,20,30>,<100,200,300>}
? test_union MyUnion {1122h}
? szFmt BYTE '結(jié)構(gòu)數(shù)據(jù): %d',0dh,0ah,0
.code
? main PROC
? ? ; 嵌套結(jié)構(gòu)的賦值
? ? mov dword ptr ds:[PointA.Left.pos_x],100
? ? mov dword ptr ds:[PointA.Left.pos_y],200
? ? mov dword ptr ds:[PointA.Right.pos_x],100
? ? mov dword ptr ds:[PointA.Right.pos_y],200
? ??
? ? ; 通過地址定位
? ? lea esi,dword ptr ds:[PointB]
? ? mov eax,dword ptr ds:[PointB]? ? ? ? ; 定位第一個(gè)MyPoint
? ? mov eax,dword ptr ds:[PointB + 12]? ?; 定位第二個(gè)內(nèi)嵌MyPoint
? ? ; 聯(lián)合體的使用
? ? mov eax,dword ptr ds:[test_union.my_dword]
? ? mov ax,word ptr ds:[test_union.my_word]
? ? mov al,byte ptr ds:[test_union.my_byte]
? main ENDP
END main
```
當(dāng)然有了結(jié)構(gòu)體這一成員的加入,我們同樣可以在匯編層面實(shí)現(xiàn)鏈表的定義與輸出,如下代碼所示,首先定義一個(gè)`ListNode`用于存儲鏈表結(jié)構(gòu)的數(shù)據(jù)域與指針域,接著使用`TotalNodeCount`定義鏈表節(jié)點(diǎn)數(shù)量,最后使用`REPEAT`偽指令開辟`ListNode`對象的多個(gè)實(shí)例,其中的`NodeData`域包含一個(gè)`1-15`的數(shù)據(jù),后面的`($ + Counter * sizeof ListNode)`則是指向下一個(gè)鏈表的頭指針,通過不斷遍歷則可輸出整個(gè)鏈表。
```ASM
? .386p
? .model flat,stdcall
? option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
ListNode Struct
? NodeData DWORD ?
? NextPtr? DWORD ?
ListNode ends
TotalNodeCount = 15
Counter = 0
.data
? LinkList LABEL PTR ListNode
? REPEAT TotalNodeCount
? ? Counter = Counter + 1
? ? ListNode <Counter,($ + Counter * sizeof ListNode)>
? ENDM
? ListNode<0,0>? ? ? ? ? ? ? ? ; 標(biāo)志著結(jié)構(gòu)鏈表的結(jié)束
? szFmt BYTE '結(jié)構(gòu)地址: %x 結(jié)構(gòu)數(shù)據(jù): %d',0dh,0ah,0
.code
? main PROC
? ? mov esi,offset LinkList
? ??
? ? ; 判斷下一個(gè)節(jié)點(diǎn)是否為<0,0>
? L1: mov eax,(ListNode PTR [esi]).NextPtr
? ? cmp eax,0
? ? je lop_end
? ??
? ? ; 顯示節(jié)點(diǎn)數(shù)據(jù)
? ? mov eax,(ListNode PTR [esi]).NodeData
? ? invoke crt_printf,addr szFmt,esi,eax
? ??
? ? ; 獲取到下一個(gè)節(jié)點(diǎn)的指針
? ? mov esi,(ListNode PTR [esi]).NextPtr
? ? jmp L1
? ??
? lop_end:
? ? int 3
? ??
? main ENDP
END main
```
本文作者: 王瑞
本文鏈接: https://www.lyshark.com/post/e43f6d19.html
版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協(xié)議。轉(zhuǎn)載請注明出處!