過程的實現離不開堆疊的應用,堆疊是一種後進先出(LIFO)
的資料結構,最後壓入棧的值總是最先被彈出,而新數值在執行壓棧時總是被壓入到棧的最頂端,棧主要功能是暫時存放資料和地址,通常用來保護斷點和現場。
棧是由CPU
管理的線性記憶體陣列,它使用兩個暫存器(SS和ESP)
來儲存棧的狀態,SS暫存器存放段選擇符,而ESP暫存器的值通常是指向特定位置的一個32位元偏移值,我們很少需要直接操作ESP暫存器,相反的ESP暫存器總是由CALL,RET,PUSH,POP
等這類指令間接性的修改。
CPU提供了兩個特殊的暫存器用於標識位於系統棧頂端的棧幀。
在通常情況下ESP是可變的,隨著棧的生成而逐漸變小,而EBP暫存器是固定的,只有當函數的呼叫後,發生入棧操作而改變。
PUSH和POP是組合語言中用於堆疊操作的指令,它們通常用於儲存和恢復暫存器的值,引數傳遞和函數呼叫等。
PUSH指令用於將運算元壓入堆疊中,它執行的操作包括將運算元複製到堆疊的棧頂,並將堆疊指標(ESP)減去相應的位元組數。指令格式如下:
PUSH operand
其中,operand可以是8位元,16位元或32位元的暫存器,立即數,以及記憶體中的某個值。例如,要將暫存器EAX的值壓入堆疊中,可以使用以下指令:
PUSH EAX
從組合程式碼的角度來看,PUSH指令將運算元儲存到堆疊中,它實際上是一個入棧操作。
POP指令用於將堆疊中棧頂的值彈出到指定的目的運算元中,它執行的操作包括將堆疊頂部的值移動到指定的運算元,並將堆疊指標增加相應的位元組數。指令格式如下:
POP operand
其中,operand可以是8位元,16位元或32位元的暫存器,立即數,以及記憶體中的某個位置。例如,要將從堆疊中彈出的值儲存到BX暫存器中,可以使用以下指令:
POP EBX
從組合程式碼的角度來看,POP指令將從堆疊中取出一個值,並將其儲存到目的運算元中,它是一個出棧操作。
在函數呼叫時,PUSH指令被用於向堆疊中推播函數的引數,這些引數可以是暫存器、立即數或者記憶體中的某個值。在函數返回之前,POP指令被用於將堆疊頂部的值彈出,並將其儲存到暫存器或者記憶體中。
讀者需要特別注意,在使用PUSH
和POP
指令時需要保證堆疊的平衡,也就是說,每個PUSH
指令必須有對應的POP
指令,否則堆疊會失去平衡,最終導致程式出現錯誤。
在讀者瞭解了這兩條指令時則可以執行一些特殊的操作,如下程式碼我們以陣列入棧與出棧為例,執行PUSH
指令時,首先減小ESP
的值,然後把源運算元複製到堆疊上,執行POP
指令則是先將資料彈出到目的運算元中,然後再執行ESP
值增加4,並以此分別將陣列中的元素壓入棧,最終再通過POP將元素反彈出來。
.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指令將陣列正向入棧
mov eax,0
mov ecx,10
S1:
push dword ptr ds:[Array + eax * 4]
inc eax
loop S1
; 使用pop指令將陣列反向彈出
mov ecx,10
S2:
push ecx ; 保護ecx
pop ebx ; 將Array陣列元素彈出到ebx
invoke crt_printf,addr szFmt,ebx
pop ecx ; 彈出ecx
loop S2
int 3
main ENDP
END main
至此當讀者理解了這兩個指令之後,那麼利用堆疊的先進後出特定,我們就可以實現將特殊的字串反轉後輸出的效果,首先我們迴圈將字串壓入堆疊,然後再從堆疊中反向彈出來,這樣就可以實現字串的反轉操作,這段程式碼的實現也相對較為容易;
.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
PROC/ENDP 偽指令是用於定義過程(函數)的偽指令,這兩個偽指令可分別定義過程的開始和結束位置。此處讀者需要注意,這兩條偽指令並非是組合語言中所相容的,而是MASM
編譯器為我們提供的一個宏,是MASM
的一部分,它允許程式設計師使用組合語言定義過程(函數)可以像標準組合指令一樣使用。
對於不使用宏定義來建立函數時我們通常會自己管理函數棧引數,而有了宏定義這些功能都可交給編譯器去管理,下面的一個案例中,我們通過使用過程建立ArraySum
函數,實現對整數陣列求和操作,函數預設將返回值儲存在EAX
中,並列印輸出求和後的引數。
.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
; 陣列求和過程
ArraySum PROC
push esi ; 儲存ESI,ECX
push ecx
xor eax,eax
S1: add eax,dword ptr ds:[esi] ; 取值並相加
add esi,4 ; 遞增陣列指標
loop S1
pop ecx ; 恢復ESI,ECX
pop esi
ret
ArraySum endp
main PROC
lea esi,dword ptr ds:[MyArray] ; 取出陣列基址
mov ecx,lengthof MyArray ; 取出元素數目
call ArraySum ; 呼叫方法
mov dword ptr ds:[Sum],eax ; 得到結果
invoke crt_printf,addr szFmt,Sum
int 3
main ENDP
END main
接著我們來實現一個具有獲取亂數功能的案例,在C語言中如果需要獲得一個亂數一般會呼叫Seed
函數,如果讀者逆向分析過這個函數的實現原理,那麼讀者應該能理解,在呼叫取亂數之前會生成一個亂數種子,這個亂數種子的生成則依賴於0x343FDh
這個特殊的常數地址,當我們每次存取該地址都會產出一個隨機的資料,當得到該資料後,我們再通過除法運算取出溢位資料作為亂數使用實現了該功能。
.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 '亂數: %d',0dh,0ah,0
.code
; 生成 0 - FFFFFFFFh 的隨機種子
Random32 PROC
push edx
mov eax, 343FDh
imul seed
add eax, 269EC3h
mov seed, eax
ror eax,8
pop edx
ret
Random32 endp
; 生成亂數
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
; 呼叫後取出亂數
call RandomRange
invoke crt_printf,addr szFmt,eax
int 3
main ENDP
END main
在組合語言中,可以使用堆疊來傳遞函數引數和建立區域性變數。當程式執行到函數呼叫語句時,需要將函數引數傳遞給被呼叫函數。為了實現引數傳遞,程式會將引數壓入棧中,然後呼叫被呼叫函數。被呼叫函數從棧中彈出引數並執行,然後將返回值儲存在暫存器中,最後通過跳轉返回到呼叫函數。
區域性變數也可以通過在棧中分配記憶體來建立。在函數開始時,可以使用push指令將區域性變數壓入棧中。在函數結束時,可以使用pop指令將變數從棧中彈出。由於棧是後進先出的資料結構,區域性變數的建立可以很方便地通過在棧上壓入一些資料來實現。
區域性變數是在程式執行時由系統動態的在棧上開闢的,在記憶體中通常在基址指標(EBP)
之下,儘管在組合時不能給定預設值,但可以在執行時初始化,如下一段C語言虛擬碼:
void MySub()
{
int var1 = 10;
int var2 = 20;
}
上述的程式碼經過C編譯後,會變成如下組合指令,其中EBP-4
必須是4的倍數,因為預設就是4位元組儲存,如果去掉了mov esp,ebp
,那麼當執行pop ebp
時將會得到EBP
等於10,執行RET
指令會導致控制轉移到記憶體地址10處執行,從而程式會崩潰。
MySub PROC
push ebp ; 將EBP儲存在棧中
mov ebp,esp ; 堆疊框架的基址
sub esp,8 ; 建立區域性變數空間(分配2個區域性變數)
mov DWORD PTR [ebp-8],10 ; var1 = 10
mov DWORD PTR [ebp-4],20 ; var2 = 20
mov esp,ebp ; 從堆疊上刪除區域性變數
pop ebp ; 恢復EBP指標
ret 8 ; 返回,清理堆疊
MySub ENDP
為了使上述程式碼片段更易於理解,可以在上述的程式碼的基礎上給每個變數的參照地址都定義一個符號,並在程式碼中使用這些符號,如下程式碼所示,程式碼中定義了一個名為MySub
的過程,該過程將兩個區域性變數分別設定為10
和20
。
在該過程中,首先使用push ebp
指令將舊的基址指標壓入棧中,並將ESP
暫存器的值儲存到ebp
中。這個舊的基址指標將在函數執行完畢後被恢復。然後,我們使用sub esp,8
指令將8
位元組的空間分配給兩個區域性變數。在堆疊上分配的空間可以通過var1_local
和var2_local
符號來存取。在這裡,我們定義了兩個符號,將它們與ebp
暫存器進行偏移以存取這些區域性變數。var1_local
的地址為[ebp-8]
,var2_local
的地址為[ebp-4]
。然後,我們使用mov
指令將10
和 20
分別儲存到這些區域性變數中。最後,我們將ESP
暫存器的值儲存回ebp
中,並使用pop ebp
指令將舊的基址指標彈出堆疊。現在,棧頂指標(ESP)下移恢復上面分配的8個位元組的空間,最後通過ret 8
返回到呼叫函數。
在使用堆疊傳參和建立區域性變數時,需要謹慎考慮棧指標的位置,並確保遵守呼叫約定以確保正確地傳遞引數和返回值。
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
接著我們來實現一個具有功能的案例,首先為了能更好的讓讀者理解我們先使用C語言方式實現MakeArray()
函數,該函數的內部是動態生成的一個MyString
陣列,並通過迴圈填充為星號字串,最後使用POP
彈出,並輸出結果,觀察後嘗試用組合實現。
void makeArray()
{
char MyString[30];
for(int i=0;i<30;i++)
{
myString[i] = "*";
}
}
call makeArray()
上述C語言程式碼如果翻譯為組合格式則如下所示,程式碼使用組合語言實現makeArray
的程式,該程式開闢了一個長度為30
的陣列,將其中的元素填充為*
,然後彈出兩個元素,並將它們輸出到控制檯。
.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 '出棧資料: %x ',0dh,0ah,0
.code
makeArray PROC
push ebp
mov ebp,esp
; 開闢區域性陣列
sub esp,32 ; MyString基地址位於 [ebp - 30]
lea esi,[ebp - 30] ; 載入MyString的地址
; 填充資料
mov ecx,30 ; 迴圈計數
S1: mov byte ptr ds:[esi],'*' ; 填充為*
inc esi ; 每次遞增一個位元組
loop S1
; 彈出2個元素並輸出,出棧資料
pop eax
invoke crt_printf,addr szFmt,eax
pop eax
invoke crt_printf,addr szFmt,eax
; 以下平棧,由於我們手動彈出了2個資料
; 則平棧 32 - (2 * 4) = 24
add esp,24 ; 平棧
mov esp,ebp
pop ebp ; 恢復EBP
ret
makeArray endp
main PROC
call makeArray
invoke ExitProcess,0
main ENDP
END main
在該程式的開始部分,我們首先通過push ebp
和mov ebp,esp
指令儲存舊的基址指標並將當前棧頂指標(ESP)
儲存到ebp
中。然後,我們使用sub esp, 32
指令開闢一個長度為30
的陣列MyString
。我們將MyString
陣列的基地址儲存在[ebp - 30]
的位置。使用lea esi, [ebp - 30]
指令將MyString
的基地址載入到esi
暫存器中。該指令偏移ebp-30
是因為ebp-4
是MakeArray
函數的第一個引數的位置,因此需要增加四個位元組。我們利用MOV byte ptr ds:[esi],'*'
指令將MyString
中的所有元素填充為*
。
然後,使用pop eax
和invoke crt_printf, addr szFmt, eax
指令兩次彈出兩個元素,並使用crt_printf
函數輸出這些元素。該函數在msvcrt.dll
庫中實現,用於將格式化的資訊輸出到控制檯。在輸出資料之後,我們通過add esp,24
和mov esp,ebp
指令將堆疊平衡,恢復舊的基址指標ebp
,然後從堆疊中彈出ebp
,並通過ret
指令返回到呼叫程式。
接著我們繼續來對比一下堆疊中引數傳遞的異同點,平棧的方式一般可分為呼叫者平棧和被呼叫者平棧,在使用堆疊傳參時,需要平衡棧以恢復之前的堆疊指標位置。
當平棧由被呼叫者完成時,被呼叫函數使用ret
指令將控制權返回到呼叫函數,並從堆疊中彈出返回地址。此時,被呼叫函數需要將之前分配的區域性變數從堆疊中彈出,以便呼叫函數能夠恢復堆疊指標的位置。因此,被呼叫函數必須知道其在堆疊上分配的記憶體大小,並將該大小與其ret
指令中的引數相匹配,以便呼叫函數可以正確恢復堆疊指標位置。
當平棧由呼叫者完成時,呼叫函數需要在呼叫子函數之前平衡堆疊。因此,呼叫函數需要知道子函數在堆疊上分配的記憶體大小,並在呼叫子函數之前向堆疊提交額外的空間。呼叫函數可以使用add esp, N
指令來恢復堆疊指標的位置,其中 N 是被呼叫函數在堆疊上分配的記憶體大小。然後,呼叫函數呼叫被呼叫函數,該函數將返回並將堆疊指標恢復到呼叫函數之前的位置。
如下這段組合程式碼中筆者分別實現了兩種呼叫方式,其中MyProcA
函數是一種被呼叫者平棧,由於呼叫者並沒有堆疊修正所以需要在函數內部通過使用ret 12
的方式平棧,之所以是12是因為我們使用了三個區域性變數,而第二個MyProcB
函數則是呼叫者平棧,該方式在函數內部並沒有返回任何引數,所以在呼叫函數結束後需要通過add esp,4
的方式對堆疊進行修正。
.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 '資料: %d ',0dh,0ah,0
.code
; 第一種方式:被呼叫者平棧
MyProcA PROC
push ebp
mov ebp,esp
xor eax,eax
mov eax,dword ptr ss:[ebp + 16] ; 獲取第一個引數
mov ebx,dword ptr ss:[ebp + 12] ; 獲取第二個引數
mov ecx,dword ptr ss:[ebp + 8] ; 獲取第三個引數
add eax,ebx
add eax,ebx
add eax,ecx
mov esp,ebp
pop ebp
ret 12 ; 此處ret12可平棧,也可使用 add ebp,12
MyProcA endp
; 第二種方式:呼叫者平棧
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
; 第一種被呼叫者MyProcA平棧 3*4 = 12
push 1
push 2
push 3
call MyProcA
invoke crt_printf,addr szFmt,eax
; 第二種方式:呼叫者平棧
push 10
call MyProcB
add esp,4
invoke crt_printf,addr szFmt,eax
int 3
main ENDP
END main
當然瞭如果讀者認為自己維護堆疊很繁瑣,則此時可以直接使用MASM
組合器提供的PROC
定義過程,使用該偽指令組合器會自行計算所需要使用的變數數量並自行在結尾處新增對應的平棧語句,這段程式碼實現起來將變得非常容易理解。
.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 '計算引數: %d ',0dh,0ah,0
.code
my_proc PROC x:DWORD,y:DWORD,z:DWORD ; 定義過程區域性引數
LOCAL @sum:DWORD ; 定義區域性變數存放總和
mov eax,dword ptr ds:[x]
mov ebx,dword ptr ds:[y] ; 分別獲取到區域性引數
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 ; 傳遞引數
call my_proc
mov @ret_sum,eax ; 獲取結果並列印
invoke crt_printf,addr szFmt,@ret_sum
int 3
main ENDP
END main
這裡筆者還需要擴充套件一個偽指令LOCAL
,LOCAL是一種組合語言中的偽指令,用於定義儲存在堆疊上的區域性變數。使用LOCAL
指令定義的區域性變數只在函數執行時存在,當函數返回後,該變數將被刪除。根據使用LOCAL
指令時指定的記憶體空間大小,組合器將為每個變數保留足夠的空間。
例如,下面是一個使用LOCAL定義區域性變數的範例:
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
main PROC
; 定義區域性變數,自動壓棧/平棧
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
; 填充陣列方式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
; 填充陣列方式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
指令定義了幾個區域性變數,包括一個位元組型別的變數var_byte
、一個字型別的變數var_word
、一個雙字型別的變數var_dword
和一個包含三個雙字元素的陣列var_array
。
在程式碼中,我們使用mov
指令填充這些變數的值。對於位元組型別、字型別和雙字型別的變數,使用mov byte ptr ds:[var_byte], 1
、mov word ptr ds:[var_word], 2
和mov dword ptr ds:[var_dword], 3
指令將相應的常數值儲存到變數中。在填充陣列時,分別使用了兩種不同的方式。一種方式是使用lea
指令將陣列的地址載入到esi
暫存器中,然後使用mov dword ptr ds:[esi],10
等指令將相應的常數值儲存到陣列中。另一種方式是直接存取陣列元素,如mov var_array[0], 100
等指令。需要注意,由於陣列元素在記憶體中是連續儲存的,因此可以使用[]
操作符存取陣列元素。
在組合中使用LOCAL
偽指令來實現自動計算區域性變數空間,以及最後的平棧操作,將會極大的提高開發效率。
USES是組合語言中的偽指令,用於儲存一組暫存器的狀態,以便函數呼叫過程中可以使用這些暫存器。使用USES時,程式可以儲存一組需要保護的暫存器,組合器將在程式入口處自動向堆疊壓入這些暫存器的值。讀者需注意,我們可以在需要儲存暫存器的程式段中使用USES來保護暫存器,但不應在整個程式中重複使用暫存器。
ENTER也是一種偽指令,用於建立函數呼叫過程中的堆疊幀。使用ENTER時,程式可以定義一個名為ENTER的指定大小的堆疊幀。該指令會將新的基準指標ebp 壓入堆疊同時將當前的基準指標ebp儲存到另一個暫存器ebx中,然後將堆疊指標esp減去指定大小的值,獲取新的基地址,並將新的基地址儲存到ebp 中。之後,程式可以在此幀上建立和存取區域性變數,並使用LEAVE指令將堆疊幀刪除,將ebp恢復為舊的值,同時將堆疊指標平衡。
在使用USES和ENTER指令時,需要了解這些指令在具體的平臺上的支援情況,以及它們適用的呼叫約定。通常情況下,在函數開頭,我們將使用ENTER建立堆疊幀,然後使用USES指定需要保護的暫存器。在函數末尾,我們使用LEAVE刪除堆疊幀。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
; USES 自動壓入 eax,ebx,ecx,edx
my_proc PROC USES eax ebx ecx edx x:DWORD,y:DWORD
enter 8,0 ; 自動保留8位元組堆疊空間
add eax,ebx
leave
my_proc endp
main PROC
mov eax,10
mov ebx,20
call my_proc
int 3
main ENDP
END main
STRUCT和UNION是組合語言中的資料型別,STRUCT是一種複合資料型別,它將多個不同型別的變數按順序放置在一起,並使用單個名稱來參照集合。使用STRUCT時,我們可以將不同型別的變數組合成一個結構體並定義其屬性,如結構體中包含的成員變數的資料型別、名稱和位置。
例如,下面是一個使用STRUCT定義自定義型別的範例:
; 定義一個名為 MyStruct 的結構體,包含兩個成員變數。
MyStruct STRUCT
Var1 DWORD ?
Var2 WORD ?
MyStruct ENDS
在上述範例程式碼中,我們使用STRUCT
定義了一個名為MyStruct
的結構體,其中包含兩個成員變數Var1
和Var2
。其中,Var1
是DWORD
型別的資料型別,以問號?
形式指定了其預設值,Var2
是WORD
型別的資料型別。
另一個資料型別是UNION
,它也是一種複合資料型別,用於將多個不同型別的變數疊加在同一記憶體位置上。使用UNION
時,程式記憶體中的資料將只佔用所有成員變數中最大的資料型別變數的大小。與結構體不同,聯合中的所有成員變數共用相同的記憶體位置。我們可以使用一種成員變數來參照記憶體位置,但在任何時候僅能有一種成員變數儲存在該記憶體位置中。
例如,下面是一個使用UNION定義自定義型別的範例:
; 定義一個名為 MyUnion 的聯合,包含兩個成員變數。
MyUnion UNION
Var1 DWORD ?
Var2 WORD ?
MyUnion ENDS
在上述範例程式碼中,我們使用UNION
定義了一個名為MyUnion
的聯合,其中包含兩個不同型別的成員變數Var1
和Var2
,將它們相對應地置於聯合的同一記憶體位置上。
讀者在使用STRUCT
和UNION
時,需要根據記憶體分佈和變數型別來正確存取成員變數的值。在組合語言中,結構體和聯合主要用於定義自定義資料型別、通訊協定和系統資料結構等,如下一段程式碼則是組合語言中實現結構體賦值與取值的總結。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
; 定義座標結構
MyPoint Struct
pos_x DWORD ?
pos_y DWORD ?
pos_z DWORD ?
MyPoint ends
; 定義人物結構
MyPerson Struct
Fname db 20 dup(0)
fAge db 100
fSex db 20
MyPerson ends
.data
; 宣告結構: 使用 <>,{}符號均可
PtrA MyPoint <10,20,30>
PtrB MyPoint {100,200,300}
; 宣告結構: 使用MyPerson宣告結構
UserA MyPerson <'lyshark',24,1>
.code
main PROC
; 獲取結構中的資料
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
; 向結構中寫入資料
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
; 直接獲取結構中的資料
mov eax,dword ptr ds:[UserA.Fname]
mov ebx,dword ptr ds:[UserA.fAge]
int 3
main ENDP
END main
接著我們來實現一個輸出結構體陣列的功能,結構陣列其實就是一維的空間,因此使用兩個比例因子即可實現定址操作,如下程式碼我們先來實現一個簡單的功能,只遍歷第一層,結構陣列外層的資料。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
; 定義座標結構
MyPoint Struct
pos_x DWORD ?
pos_y DWORD ?
pos_z DWORD ?
MyPoint ends
; 定義迴圈結構
MyCount Struct
count_x DWORD ?
count_y DWORD ?
MyCount ends
.data
; 宣告結構: 使用 <>,{}符號均可
PtrA MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
Count MyCount <0,0>
szFmt BYTE '結構資料: %d',0dh,0ah,0
.code
main PROC
; 獲取結構中的資料
lea esi,dword ptr ds:[PtrA]
mov eax,(MyPoint ptr ds:[esi]).pos_x ; 獲取第一個結構X
mov eax,(MyPoint ptr ds:[esi + 12]).pos_x ; 獲取第二個結構X
; while 迴圈輸出結構的每個首元素元素
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
接著我們遞增難度,通過每次遞增將兩者的偏移相加,獲得比例因子,通過因子巢狀雙層迴圈實現定址列印。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
; 定義座標結構
MyPoint Struct
pos_x DWORD ?
pos_y DWORD ?
pos_z DWORD ?
MyPoint ends
; 定義迴圈結構
MyCount Struct
count_x DWORD ?
count_y DWORD ?
MyCount ends
.data
; 宣告結構: 使用 <>,{}符號均可
PtrA MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
Count MyCount <0,0>
szFmt BYTE '結構資料: %d',0dh,0ah,0
.code
main PROC
; 獲取結構中的資料
lea esi,dword ptr ds:[PtrA]
mov eax,(MyPoint ptr ds:[esi]).pos_x ; 獲取第一個結構X
mov eax,(MyPoint ptr ds:[esi + 12]).pos_x ; 獲取第二個結構X
; while 迴圈輸出結構的每個首元素元素
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
結構體同樣支援內嵌的方式,如下Rect
指標中內嵌兩個MyPoint
分別指向左子域和右子域,這裡順便定義一個MyUnion
聯合體把,其使用規範與結構體完全一致,只不過聯合體只能儲存一個資料.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
; 定義座標結構
MyPoint Struct
pos_x DWORD ?
pos_y DWORD ?
pos_z DWORD ?
MyPoint ends
; 定義左右結構
Rect Struct
Left MyPoint <>
Right MyPoint <>
Rect ends
; 定義聯合體
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 '結構資料: %d',0dh,0ah,0
.code
main PROC
; 巢狀結構的賦值
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] ; 定位第一個MyPoint
mov eax,dword ptr ds:[PointB + 12] ; 定位第二個內嵌MyPoint
; 聯合體的使用
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
當然有了結構體這一成員的加入,我們同樣可以在組合層面實現連結串列的定義與輸出,如下程式碼所示,首先定義一個ListNode
用於儲存連結串列結構的資料域與指標域,接著使用TotalNodeCount
定義連結串列節點數量,最後使用REPEAT
偽指令開闢ListNode
物件的多個範例,其中的NodeData
域包含一個1-15
的資料,後面的($ + Counter * sizeof ListNode)
則是指向下一個連結串列的頭指標,通過不斷遍歷則可輸出整個連結串列。
.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> ; 標誌著結構連結串列的結束
szFmt BYTE '結構地址: %x 結構資料: %d',0dh,0ah,0
.code
main PROC
mov esi,offset LinkList
; 判斷下一個節點是否為<0,0>
L1: mov eax,(ListNode PTR [esi]).NextPtr
cmp eax,0
je lop_end
; 顯示節點資料
mov eax,(ListNode PTR [esi]).NodeData
invoke crt_printf,addr szFmt,esi,eax
; 獲取到下一個節點的指標
mov esi,(ListNode PTR [esi]).NextPtr
jmp L1
lop_end:
int 3
main ENDP
END main
本文作者: 王瑞
本文連結: https://www.lyshark.com/post/e43f6d19.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!