過程的實現離不開堆棧的應用,堆棧是一種後進先出`(LIFO)`的數據結構,最後壓入棧的值總是最先被彈出,而新數值在執行壓棧時總是被壓入到棧的最頂端,棧主要功能是暫時存放數據和地址,通常用來保護斷點和現場。棧是由`CPU`管理的線性記憶體數組,它使用兩個寄存器`(SS和ESP)`來保存棧的狀態,SS寄存... ...
過程的實現離不開堆棧的應用,堆棧是一種後進先出(LIFO)
的數據結構,最後壓入棧的值總是最先被彈出,而新數值在執行壓棧時總是被壓入到棧的最頂端,棧主要功能是暫時存放數據和地址,通常用來保護斷點和現場。
棧是由CPU
管理的線性記憶體數組,它使用兩個寄存器(SS和ESP)
來保存棧的狀態,SS寄存器存放段選擇符,而ESP寄存器的值通常是指向特定位置的一個32位偏移值,我們很少需要直接操作ESP寄存器,相反的ESP寄存器總是由CALL,RET,PUSH,POP
等這類指令間接性的修改。
CPU提供了兩個特殊的寄存器用於標識位於系統棧頂端的棧幀。
- ESP 棧指針寄存器:棧指針寄存器,其記憶體放著一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。
- EBP 基址指針寄存器:基址指針寄存器,其記憶體放著一個指針,該指針永遠指向系統棧最上面一個棧幀的底部。
在通常情況下ESP是可變的,隨著棧的生成而逐漸變小,而EBP寄存器是固定的,只有當函數的調用後,發生入棧操作而改變。
- 執行PUSH壓棧時,堆棧指針自動減4,再將壓棧的值複製到堆棧指針所指向的記憶體地址。
- 執行POP出棧時,從棧頂移走一個值並將其複製給記憶體或寄存器,然後再將堆棧指針自動加4。
- 執行CALL調用時,CPU會用堆棧保存當前被調用過程的返回地址,直到遇到RET指令再將其彈出。
10.1 PUSH/POP
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
10.2 PROC/ENDP
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
10.3 局部參數傳遞
在彙編語言中,可以使用堆棧來傳遞函數參數和創建局部變數。當程式執行到函數調用語句時,需要將函數參數傳遞給被調用函數。為了實現參數傳遞,程式會將參數壓入棧中,然後調用被調用函數。被調用函數從棧中彈出參數並執行,然後將返回值存儲在寄存器中,最後通過跳轉返回到調用函數。
局部變數也可以通過在棧中分配記憶體來創建。在函數開始時,可以使用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
偽指令來實現自動計算局部變數空間,以及最後的平棧操作,將會極大的提高開發效率。
10.4 USES/ENTER
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
10.5 STRUCT/UNION
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 許可協議。轉載請註明出處!