整理復習匯編語言的知識點,以前在學習《Intel匯編語言程序設計 - 第五版》時沒有很認真的整理筆記,主要因為當時是以學習理解為目的沒有整理的很詳細,這次是我第三次閱讀此書,每一次閱讀都會有新的收獲,這次復習,我想把書中的重點,再一次做一個歸納與總結(注:16位匯編部分跳過),並且繼續嘗試寫一些有趣的案例,這些案例中所涉及的指令都是逆向中的重點,一些不重要的我就直接省略了,一來提高自己,二來分享知識,轉載請加出處,敲代碼備注挺難受的。
該筆記重點復習字符串操作指令的一些使用技巧,以及浮點數運算相關內容,浮點數運算也是非常重要的知識點,在分析大型游戲時經常會碰到針對浮點數的運算指令,例如槍械換彈動作,人物跳躍時的狀態,都屬於浮點數運算范圍,也就一定會用到浮點數寄存器棧,浮點指令集主要可分為,傳送指令,算數指令,比較指令,超越指令,常量加載指令等。
再次強調:該筆記主要學習的是匯編語言,不是研究編譯特性的,不會涉及到編譯器的優化與代碼還原。
字符串操作指令
移動串指令: MOVSB、MOVSW、MOVSD ;從 ESI -> EDI; 執行后, ESI 與 EDI 的地址移動相應的單位
比較串指令: CMPSB、CMPSW、CMPSD ;比較 ESI、EDI; 執行后, ESI 與 EDI 的地址移動相應的單位
掃描串指令: SCASB、SCASW、SCASD ;依據 AL/AX/EAX 中的數據掃描 EDI 指向的數據, 執行后 EDI 自動變化
儲存串指令: STOSB、STOSW、STOSD ;將 AL/AX/EAX 中的數據儲存到 EDI 給出的地址, 執行后 EDI 自動變化
載入串指令: LODSB、LODSW、LODSD ;將 ESI 指向的數據載入到 AL/AX/EAX, 執行后 ESI 自動變化
移動串指令: 移動串指令包括MOVSB、MOVSW、MOVSD
原理為從ESI到EDI中,執行后將ESI地址里面的內容移動到EDI指向的內存空間中,該指令常用於對特定字符串的復制操作.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
; 逐字節拷貝
SrcString BYTE "hello lyshark",0h ; 源字符串
SrcStringLen EQU $ - SrcString - 1 ; 計算出原始字符串長度
DstString BYTE SrcStringLen dup(?),0h ; 目標內存地址
szFmt BYTE '字符串: %s 長度: %d ',0dh,0ah,0
; 四字節拷貝
ddSource DWORD 10h,20h,30h ; 定義三個四字節數據
ddDest DWORD lengthof ddSource dup(?) ; 得到目標地址
.code
main PROC
; 第一種情況: 實現逐字節拷貝
cld ; 清除方向標志
mov esi,offset SrcString ; 取源字符串內存地址
mov edi,offset DstString ; 取目標字符串內存地址
mov ecx,SrcStringLen ; 指定循環次數,為原字符串長度
rep movsb ; 逐字節復制,直到ecx=0為止
lea eax,dword ptr ds:[DstString]
mov ebx,sizeof DstString
invoke crt_printf,addr szFmt,eax,ebx
; 第二種情況: 實現4字節拷貝
lea esi,dword ptr ds:[ddSource]
lea edi,dword ptr ds:[ddDest]
cld
rep movsd
; 使用loop循環逐字節復制
lea esi,dword ptr ds:[SrcString]
lea edi,dword ptr ds:[DstString]
mov ecx,SrcStringLen
cld ; 設置方向為正向復制
@@: movsb ; 每次復制一個字節
dec ecx ; 循環遞減
jnz @B ; 如果ecx不為0則循環
lea eax,dword ptr ds:[DstString]
mov ebx,sizeof DstString
invoke crt_printf,addr szFmt,eax,ebx
invoke ExitProcess,0
main ENDP
END main
比較串指令: 比較串指令包括CMPSB、CMPSW、CMPSD
比較ESI、EDI
執行后將ESI指向的內存操作數同EDI指向的內存操作數相比較,其主要從ESI指向內容減去EDI的內容來影響標志位.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
; 逐字節比較
SrcString BYTE "hello lyshark",0h
DstStringA BYTE "hello world",0h
.const
szFmt BYTE '字符串: %s',0dh,0ah,0
YES BYTE "相等",0
NO BYTE "不相等",0
.code
main PROC
; 實現字符串對比,相等/不相等輸出
lea esi,dword ptr ds:[SrcString]
lea edi,dword ptr ds:[DstStringA]
mov ecx,lengthof SrcString
cld
repe cmpsb
je L1
jmp L2
L1: lea eax,YES
invoke crt_printf,addr szFmt,eax
jmp lop_end
L2: lea eax,NO
invoke crt_printf,addr szFmt,eax
jmp lop_end
lop_end:
int 3
invoke ExitProcess,0
main ENDP
END main
CMPSW 是對比一個字類型的數組,只有當數組中的數據完全一致的情況下才會返回真,否則為假.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
Array1 WORD 1,2,3,4,5 ; 必須全部相等才會清空ebx
Array2 WORD 1,3,5,7,9
.const
szFmt BYTE '數組: %s',0dh,0ah,0
YES BYTE "相等",0
NO BYTE "不相等",0
.code
main PROC
lea esi,Array1
lea edi,Array2
mov ecx,lengthof Array1
cld
repe cmpsw
je L1
lea eax,NO
invoke crt_printf,addr szFmt,eax
jmp lop_end
L1: lea eax,YES
invoke crt_printf,addr szFmt,eax
jmp lop_end
lop_end:
int 3
invoke ExitProcess,0
main ENDP
END main
CMPSD則是比較雙字數據,同樣可用於比較數組,這里就演示一下比較單數的情況.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
var1 DWORD 1234h
var2 DWORD 5678h
.const
szFmt BYTE '兩者: %s',0dh,0ah,0
YES BYTE "相等",0
NO BYTE "不相等",0
.code
main PROC
lea esi,dword ptr ds:[var1]
lea edi,dword ptr ds:[var2]
cmpsd
je L1
lea eax,dword ptr ds:[YES]
invoke crt_printf,addr szFmt,eax
jmp lop_end
L1: lea eax,dword ptr ds:[NO]
invoke crt_printf,addr szFmt,eax
jmp lop_end
lop_end:
int 3
invoke ExitProcess,0
main ENDP
END main
掃描串指令: 掃描串指令包括SCASB、SCASW、SCASD
其作用是把AL/AX/EAX
中的值同EDI尋址的目標內存中的數據相比較,這些指令在一個長字符串或者數組中查找一個值的時候特別有用.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
szText BYTE "ABCDEFGHIJK",0
.const
szFmt BYTE '字符F所在位置: %d',0dh,0ah,0
.code
main PROC
; 尋找單一字符找到會返回第幾個字符
lea edi,dword ptr ds:[szText]
mov al,"F"
mov ecx,lengthof szText -1
cld
repne scasb ; 如果不相等則重復掃描
je L1
xor eax,eax ; 如果沒找到F則清空eax
jmp lop_end
L1: sub ecx,lengthof szText -1
neg ecx ; 如果找到輸出第幾個字符
invoke crt_printf,addr szFmt,ecx
lop_end:
int 3
main ENDP
END main
如果我們想要對數組中某個值是否存在做判斷可以使用SCASD指令,對數組進行掃描.
.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 65,88,93,45,67,89,34,67,89,22
.const
szFmt BYTE '數值: %d 存在',0dh,0ah,0
.code
main PROC
lea edi,dword ptr ds:[MyArray]
mov eax,34
mov ecx,lengthof MyArray - 1
cld
repne scasd
je L1
xor eax,eax
jmp lop_end
L1: sub ecx,lengthof MyArray - 1
neg ecx
invoke crt_printf,addr szFmt,ecx,eax
lop_end:
int 3
main ENDP
END main
儲存串指令: 存儲指令主要包括STOSB、STOSW、STOSD
起作用是把AL/AX/EAX
中的數據儲存到EDI給出的地址中,執行后EDI的值根據方向標志的增加或減少,該指令常用於初始化內存或堆棧.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
Count DWORD 100
String BYTE 100 DUP(?),0
.code
main PROC
; 利用該指令初始化字符串
mov al,0ffh ; 初始化填充數據
lea di,byte ptr ds:[String] ; 待初始化地址
mov ecx,Count ; 初始化字節數
cld ; 初始化:方向=前方
rep stosb ; 循環填充
; 存儲字符串: 使用A填充內存
lea edi,dword ptr ds:[String]
mov al,"A"
mov ecx,Count
cld
rep stosb
int 3
main ENDP
END main
載入串指令: 載入指令主要包括LODSB、LODSW、LODSD
起作用是將ESI指向的內存位置向AL/AX/EAX
中裝載一個值,同時ESI的值根據方向標志值增加或減少,如下分別完成加法與乘法計算,並回寫到內存中.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
ArrayW WORD 1,2,3,4,5,6,7,8,9,10
ArrayDW DWORD 1,2,3,4,5
ArrayMulti DWORD 10
szFmt BYTE '計算結果: %d ',0dh,0ah,0
.code
main PROC
; 利用載入命令計算數組加法
lea esi,dword ptr ds:[ArrayW]
mov ecx,lengthof ArrayW
xor edx,edx
xor eax,eax
@@: lodsw ; 將輸入加載到EAX
add edx,eax
loop @B
mov eax,edx ; 最后將相加結果放入eax
invoke crt_printf,addr szFmt,eax
; 利用載入命令(LODSD)與存儲命令(STOSD)完成乘法運算
mov esi,offset ArrayDW ; 源指針
mov edi,esi ; 目的指針
cld ; 方向=向前
mov ecx,lengthof ArrayDW ; 循環計數器
L1: lodsd ; 加載[esi]至EAX
mul ArrayMulti ; 將EAX乘以10
stosd ; 將結果從EAX存儲至[EDI]
loop L1
; 循環讀取數據(存在問題)
mov esi,offset ArrayDW ; 獲取基地址
mov ecx,lengthof ArrayDW ; 獲取長度
xor eax,eax
@@: lodsd
invoke crt_printf,addr szFmt,eax
dec ecx
loop @B
int 3
main ENDP
END main
統計字符串: 過程StrLength()
通過循環方式判斷字符串結尾的0標志,來統計字符串的長度,最后將結果存儲在EAX中.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
String BYTE "hello lyshark",0
szFmt BYTE '計算結果: %d ',0dh,0ah,0
.code
; 計算字符串長度
StrLength PROC USES edi,pString:PTR BYTE
mov edi,offset String ; 取出字符串的基地址
xor eax,eax ; 清空eax用作計數器
L1: cmp byte ptr [edi],0 ; 分別那[edi]的值和0作比較
je L2 ; 上一步為零則跳轉得到ret
inc edi ; 否則繼續執行
inc eax
jmp L1
L2: ret
StrLength endp
main PROC
invoke StrLength, addr String
invoke crt_printf,addr szFmt,eax
int 3
main ENDP
END main
字符串轉換: 字符串轉換是將小寫轉為大寫,或者將大寫轉為小寫,其原理是將二進制位第五位置1或0則可實現.
.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
szFmt BYTE '結果: %s ',0dh,0ah,0
.code
main PROC
mov esi,offset MyString ; 取出字符串的偏移地址
L1: cmp byte ptr [esi],0 ; 分別拿出每一個字節,與0比較
je L2 ; 如果相等則跳轉到L2
and byte ptr [esi],11011111b ; 執行按位與操作
inc esi ; 每次esi指針遞增1
jmp L1 ; 重復循環
L2: lea eax,dword ptr ds:[MyString]
invoke crt_printf,addr szFmt,eax
ret
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
source BYTE "hello lyshark welcome",0h
target BYTE SIZEOF source DUP(0),0h ; 取源地址數據大小
szFmt BYTE '結果: %s ',0dh,0ah,0
.code
main PROC
; 實現正向拷貝字符串
mov esi,0 ; 使用變址寄存器
mov ecx,sizeof source ; 循環計數器
L1:
mov al,byte ptr ds:[source + esi] ; 從源地址中取一個字符
mov byte ptr ds:[target + esi],al ; 將該字符存儲在目標地址中
inc esi ; 遞增,將指針移動到下一個字符
loop L1
lea eax,dword ptr ds:[target]
invoke crt_printf,addr szFmt,eax
; 實現反向拷貝字符串
mov esi,sizeof source
mov ecx,sizeof source
mov ebx,0
L2:
mov al,byte ptr ds:[source + esi]
mov byte ptr ds:[target + esi],al
dec esi
inc ebx
loop L2
lea eax,dword ptr ds:[target]
invoke crt_printf,addr szFmt,eax
push 0
call ExitProcess
main ENDP
END main
浮點數操作指令集(重點)
浮點數的計算是不依賴於CPU的,運算單元是從80486處理器開始才被集成到CPU中的,該運算單元被稱為FPU浮點運算模塊,FPU不使用CPU中的通用寄存器,其有自己的一套寄存器,被稱為浮點數寄存器棧,FPU將浮點數從內存中加載到寄存器棧中,完成計算后在回寫到內存中.
FPU有8個可獨立尋址的80位寄存器,分別名為R0-R7
他們以堆棧的形式組織在一起,棧頂由FPU狀態字中的一個名為TOP的域組成,對寄存器的引用都是相對於棧頂而言的,棧頂通常也被叫做ST(0),最后一個棧底則被記作ST(7)其實用方式與堆棧完全一致.
浮點數運算通常會使用一些更長的數據類型,如下就是MASM匯編器定義的常用數據類型.
.data
var1 QWORD 10.1 ; 64位整數
var2 TBYTE 10.1 ; 80位(10字節)整數
var3 REAL4 10.2 ; 32位(4字節)短實數
var4 REAL8 10.8 ; 64位(8字節)長實數
var5 REAL10 10.10 ; 80位(10字節)擴展實數
此外浮點數對於指令的命名規范也遵循一定的格式,浮點數指令總是以F開頭,而指令的第二個字母則表示操作位數,例如:B表示二十進制操作數,I表示二進制整數操作,如果沒有指定則默認則是針對實數的操作fld
等.
FLD/FSTP 操作指令: 這兩個指令是最基本的浮點操作指令,其中的FLD入棧指令,后面的FSTP則是將浮點數彈出堆棧.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
var1 QWORD 10.0
var2 QWORD 20.0
var3 QWORD 30.0
var4 QWORD 40.0
result QWORD ?
.code
main PROC
; 初始化浮點單元
finit
; 依次將數據入棧
fld qword ptr ds:[var1]
fld qword ptr ds:[var2]
fld qword ptr ds:[var3]
fld qword ptr ds:[var4]
; 獲取當前ST(0)棧幀元素
fst qword ptr ds:[result]
; 從棧中彈出元素
fstp qword ptr ds:[result]
fstp qword ptr ds:[result]
fstp qword ptr ds:[result]
fstp qword ptr ds:[result]
int 3
main ENDP
END main
壓棧時會自動向下填充,而出棧時則相反,不但要出棧,還會將地址回繞到底部,覆蓋掉底部的數據。
當壓棧參數超出了最大承載范圍,就會覆蓋掉正常的數據,導致錯誤。
壓棧同樣支持變址尋址的方式,如下我們可以通過循環將一個數組壓入浮點數寄存器,其中使用FLD指令時壓入一個浮點實數,而FILD則是將實數轉換為雙精度浮點數后壓入堆棧.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
Array QWORD 10.0,20.0,30.0,40.0,50.0
Count DWORD ?
Result QWORD ?
.code
main PROC
; 初始化浮點單元
finit
mov dword ptr ds:[Count],0
jmp L1
L2: mov eax,dword ptr ds:[Count]
add eax,1
mov dword ptr ds:[Count],eax
L1: mov eax,dword ptr ds:[Count]
cmp eax,5
jge lop_end
; 使用此方式壓棧
fld qword ptr ds:[Array + eax * 8] ; 壓入浮點實數
fild qword ptr ds:[Array + eax * 8] ; 壓入雙精度浮點數
jmp L2
lop_end:
int 3
main ENDP
END main
浮點交換指令: 浮點交換有兩個指令需要特別注意,第一個是FCHS該指令把ST(0)中的值的符號變反,FABS指令則是取ST(0)中值的絕對值,這兩條指令無傳遞操作數.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
Array QWORD 10.0,20.0,30.0,40.0,50.0
Result QWORD ?
szFmt BYTE 'ST寄存器: %f ',0dh,0ah,0
.code
main PROC
; 初始化壓棧
finit
fld qword ptr ds:[Array]
fld qword ptr ds:[Array + 8]
fld qword ptr ds:[Array + 16]
fld qword ptr ds:[Array + 24]
fld qword ptr ds:[Array + 32]
; 對ST(0)數據取反 (不影響浮點堆棧)
fchs ; 對ST(0)取反
fchs ; 再次取反
fst qword ptr ds:[Result] ; 取ST(0)賦值到Result
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 循環將數組取反后回寫如Array中
mov ecx,5
S1:
fchs
fstp qword ptr ds:[Array + ecx * 8]
loop S1
; 讀入Array中的數據到ST寄存器
mov ecx,5
S2:
fld qword ptr ds:[Array + ecx * 8]
loop S2
; 通過FABS取絕對值,並反寫會Array中
mov ecx,5
S3:
fabs ; 取ST(0)的絕對值
fstp qword ptr ds:[Array + ecx * 8] ; 反寫
loop S3
int 3
main ENDP
END main
浮點加法指令: 浮點數加法,該加法分為FADD/FADDP/FIADD
分別針對不同的場景,此外還會區分無操作數模式,寄存器操作數,內存操作數,整數相加等.
第一種無操作數模式,執行FADD時,ST(0)寄存器和ST(1)寄存器相加后,結果臨時存儲在ST(1)中,然后將ST(0)彈出堆棧,最終結果就會存儲在棧頂部,使用FST指令即可取出來.
第二種則是兩個浮點寄存器相加,最后的結果會存儲在源操作數ST(0)中.
第三種則是內存操作數,就是ST寄存器與內存相加.
第四種是與整數相加,默認會將整數擴展為雙精度,然后在於ST(0)相加.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
Array QWORD 10.0,20.0,30.0,40.0,50.0
IntA DWORD 10
Result QWORD ?
szFmt BYTE 'ST寄存器: %f ',0dh,0ah,0
.code
main PROC
finit
fld qword ptr ds:[Array]
fld qword ptr ds:[Array + 8]
fld qword ptr ds:[Array + 16]
fld qword ptr ds:[Array + 24]
fld qword ptr ds:[Array + 32]
; 第一種:無操作數 fadd = faddp
;fadd
;faddp
; 第二種:兩個浮點寄存器相加
fadd st(0),st(1) ; st(0) = st(0) + st(1)
fst qword ptr ds:[Result] ; 取出結果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
fadd st(0),st(2) ; st(0) = st(0) + st(2)
fst qword ptr ds:[Result] ; 取出結果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第三種:寄存器與內存相加
fadd qword ptr ds:[Array] ; st(0) = st(0) + Array
fst qword ptr ds:[Result] ; 取出結果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
fadd real8 ptr ds:[Array + 8]
fst qword ptr ds:[Result] ; 取出結果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第四種:與整數相加
fiadd dword ptr ds:[IntA]
fst qword ptr ds:[Result] ; 取出結果
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
int 3
main ENDP
END main
浮點減法指令: 浮點數減法,該加法分為FSUB/FSUBP/FISUB
該指令從目的操作數中減去原操作數,把差存儲在目的操作數中,目的操作數必須是ST寄存器,源操作數可以是寄存器或內存,運算的過程與加法指令完全一致.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
Array QWORD 10.0,20.0,30.0,40.0,50.0
IntQWORD QWORD 20
Result QWORD ?
szFmt BYTE 'ST寄存器: %f ',0dh,0ah,0
.code
main PROC
finit
fld qword ptr ds:[Array]
fld qword ptr ds:[Array + 8]
fld qword ptr ds:[Array + 16]
fld qword ptr ds:[Array + 24]
fld qword ptr ds:[Array + 32]
; 第一種:無操作數減法
;fsub
;fsubp ; st(0) = st(0) - st(1)
; 第二種:兩個浮點數寄存器相減
fsub st(0),st(1) ; st(0) = st(0) - st(1)
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第三種:寄存器與內存相減
fsub qword ptr ds:[Array] ; st(0) = st(0) - Array
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第四種:與整數相減
fisub dword ptr ds:[IntQWORD] ; st(0) = st(0) - IntQWORD
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
int 3
main ENDP
END main
浮點乘除法指令: 浮點數乘法指令有FMUL/FMULP/FIMUL
,浮點數除法則包括FDIV/FDIVP/FIDIV
這三種,其主要的使用手法與前面的加減法保持一致,下面是乘除法的總結.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
Array QWORD 10.0,20.0,30.0,40.0,50.0
IntQWORD QWORD 20
Result QWORD ?
szFmt BYTE 'ST寄存器: %f ',0dh,0ah,0
.code
InitFLD PROC
finit
fld qword ptr ds:[Array]
fld qword ptr ds:[Array + 8]
fld qword ptr ds:[Array + 16]
fld qword ptr ds:[Array + 24]
fld qword ptr ds:[Array + 32]
ret
InitFLD endp
main PROC
invoke InitFLD
; 第一種:無操作數乘法與除法
fmul
fmulp ; st(0) = st(0) * st(1)
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
fdiv
fdivp ; st(0) = st(0) / st(1)
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第二種:兩個浮點數寄存器之間的乘法與除法
invoke InitFLD
fmul st(0),st(4) ; st(0) = st(0) * st(4)
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
fdiv st(0),st(2) ; st(0) = st(0) / st(2)
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第三種:寄存器與內存之間的乘法與除法
invoke InitFLD
fmul qword ptr ds:[Array + 8] ; st(0) = st(0) * [Array + 8]
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
fdiv qword ptr ds:[Array + 16] ; st(0) = st(0) / [Array + 16]
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
; 第四種:與整數之間的乘法與除法
invoke InitFLD
fimul dword ptr ds:[IntQWORD] ; st(0) = st(0) * IntQWORD
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
fidiv dword ptr ds:[IntQWORD] ; st(0) = st(0) / IntQWORD
fst qword ptr ds:[Result]
invoke crt_printf,addr szFmt,qword ptr ds:[Result]
int 3
main ENDP
END main
浮點數比較指令: 浮點數比較指令包括FCOM/FCOMP/FCOMPP
這三個指令都是比較ST(0)和源操作數,源操作數可以是內存操作數或FPU寄存器,FCOM和FCOMP格式基本一致,唯一區別在於FCOMP在執行對比后還要從堆棧中彈出元素,FCOMP和FCOMPP也基本一致,最后都是要從堆棧中彈出元素.
比較指令的重點就是比較條件碼的狀態,FPU中包括三個條件狀態,分別是C3(零標志),C2(奇偶標志),C0(進位標志)
,我們可以使用FNSTSW
指令將這些狀態字送入AX寄存器中,然后通過SAHF
指令把AH賦值到EFLAGS
標志中,一旦標志狀態被送入EFLAGS
寄存器,那么就可以使用標准的標志位對跳轉指令進行影響了,例如以下代碼的匯編案例.
double x = 1.2; double y = 3.0; int n = 0;
if(x<y)
{
n=1;
}
; ----------------------------------------------------
; C語言偽代碼的匯編指令如下
; ----------------------------------------------------
.data
x REAL8 1.2
y REAL8 3.0
n DWORD 0
.code
main PROC
fld x ; st(0) = x
fcomp y ; cmp x,y ; pop x
fnstsw ax ; 取出狀態值送入AX
sahf ; 將狀態字送入EFLAGS
jnb L1 ; x < y 小於
mov n,1 ; 滿足則將n置1
L1: xor eax,eax ; 否則清空寄存器
int 3
main ENDP
END main
對於前面的案例來說,由於浮點數運算比整數運算在開銷上會更大一些,因此Intel新版處理器新增加了FCOMI指令,專門用於比較兩個浮點數的值,並自動設置零標志,基偶標志,和進位標志,唯一的缺點是其不支持內存操作數,針對上方案例的修改如下.
.data
x REAL8 1.2
y REAL8 3.0
n DWORD 0
.code
main PROC
fld y
fld x
fcomi st(0),st(1)
jnb L1 ; st(0) not st(1) ?
mov n,1
L1: xor eax,eax
int 3
main ENDP
END main
對於浮點數的比較來說,例如比較X與Y是否相等,如果比較X==y?
則可能會出現近似值的情況,導致無法計算出正確結果,正確的做法是取其差值的絕對值,並和用戶自定義的小的正數相比較,小的正整數作為兩個值相等時其差值的臨界值.
.data
epsilon REAL8 1.0E-12
var2 REAL8 0.0
var3 REAL8 1.001E-13
.code
main PROC
fld epsilon
fld var2
fsub var3
fabs
fcomi st(0),st(1) ; cmp epsilon,var2
ja skip
xor ebx,ebx ; 相等則清空ebx
skip:
int 3 ; 不相等則結束
main ENDP
END main
浮點表達式: 通過浮點數計算表達式valD = -valA + (valB * valC)
其計算過程,首先加載ValA並取反,加載valB至ST(0),這時-ValA保存在ST(1)中,valC和ST(0)相乘,乘基保存在ST(0)中,最后ST(0)與ST(1)相加后存入ValD中.
.data
valA REAL8 1.5
valB REAL8 2.5
valC REAL8 3.0
valD REAL8 ?
.code
main PROC
fld valA ; 加載valA
fchs ; 取反-valA
fld valB ; 加載valB = st(0)
fmul valC ; st(0) = st(0) * valC
fadd ; st(0) = st(0) + st(1)
fstp valD ; valD = st(0)
main ENDP
END main
通過循環計算一個雙精度數組中所有元素的總和.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.data
MyArray REAL8 10.0,20.0,30.0,40.0,50.0
.code
main PROC
mov esi,0 ; 設置因子
fldz ; st(0)清空
mov ecx,5 ; 設置數組數
L1: fld MyArray[esi] ; 壓入棧
fadd ; st(0) = st(0) + MyArray[esi]
add esi,TYPE REAL8 ; esi += 8
loop L1
main ENDP
END main
求ValA與ValB兩數的平方根,FSQRT指令計算ST(0)的平方根並把結果存儲在ST(0)中,如下是計算平方根方法.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.data
valA REAL8 25.0
valB REAL8 39.0
.code
main PROC
fld valA
fsqrt ; st(0) = sqrt(valA)
fld valB ; push valB
fsqrt ; st(0) = sqrt(valB)
fadd ; add st(0),st(1)
main ENDP
END main
接着看一下計算數組的點積面,例如(Array[0] * Array[1]) + (Array[2] * Array[3])
這種計算就叫做點積面計算.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.data
Array REAL4 6.0,3.0,5.0,7.0
.code
main PROC
fld Array
fmul [Array + 4]
fld [Array + 8]
fmul [Array + 12]
fadd
main ENDP
END main
有時候我們需要混合計算,也就是整數與雙精度浮點數進行運算,此時在執行運算前會將整數自動提升為浮點數,例如下面的兩個案例,第一個是整數與浮點數相加時,整數自動提升為浮點數,第二個則需要調用FIST指令對Z向上裁剪保留整數部分.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.data
N DWORD 20
X REAL8 3.5
Z REAL8 ?
.code
main PROC
; 計算 int N = 20; double X = 3.5; double Z = N + X;
fild N ; 加載整數到ST(0)
fadd X ; ST(0) = ST(0) + X
fstp Z ; 存儲到Z中
; 計算 int N = 20; double X = 3.5; int Z=(int)(N+X)
fild N
fadd X
fist E ; 將浮點數裁剪,只保留整數部分
main ENDP
END main
過程與結構體(擴展知識點)
過程的實現離不開堆棧的應用,堆棧是一種后進先出(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指令再將其彈出.
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 偽指令: 該指令可用於創建過程化流程,過程使用PROC和ENDP偽指令來聲明,下面我們通過使用過程創建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
接着來實現一個獲取隨機數的案例,具體原理就是獲取隨機種子,使用除法運算取出溢出數據作為隨機數使用,特殊常量地址343FDh
每次訪問也會產出一個隨機的數據.
.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
局部變量與堆棧傳參: 局部變量是在程序運行時,由系統動態的在棧上開辟的,在內存中通常在基址指針(EBP)之下,盡管在匯編時不能給定默認值,但可以在運行時初始化,如下一段偽代碼:
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
為了使代碼更容易閱讀,可以在上面的代碼的基礎上給每個變量的引用地址都定義一個符號,並在代碼中使用這些符號.
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()
匯編代碼如下,唯一需要注意的地方就是出棧是平棧參數,例如我們使用了影響堆棧操作的指令,則平棧要手動校驗並修復.
.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
接着來看一下堆棧傳參中平棧方的區別,平棧方可以是調用者平棧也可以由被調用者平,如下案例分別演示了兩種平棧方式.
.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
如果使用PROC定義過程,則傳遞參數是可以使用push的方式實現堆棧傳參,如下所示.
.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偽指令來實現自動計算局部變量空間,以及最后的平棧,極大的提高了開發效率.
.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
USES/ENTER 偽指令: 指令USES的作用是當我們需要壓棧保存指定寄存器時,可以使用此關鍵字,匯編器會自動為我們保存寄存器中參數,ENTER指令則是預定義保留局部變量的指令.
.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
堆棧傳參(遞歸階乘): 通過EAX寄存器傳遞一個數值,然后使用Factorial過程遞歸調用自身,實現對該數階乘的計算.
.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
Factorial PROC
push ebp
mov ebp,esp
mov eax,dword ptr ss:[ebp + 8] ; 取出參數
cmp eax,0 ; eax > 0 ?
ja L1
mov eax,1 ; 否則返回1
jmp L2
L1: dec eax
push eax
call Factorial ; 調用自身
mov ebx,dword ptr ss:[ebp + 8]
mul ebx ; 取參數/相乘
L2: mov esp,ebp
pop ebp
ret 4
Factorial endp
main PROC
; 第一組
push 3
call Factorial
invoke crt_printf,addr szFmt,eax
; 第二組
push 5
call Factorial
invoke crt_printf,addr szFmt,eax
int 3
main ENDP
END main
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
結構數組的構造與尋址,第一次總結,存在問題的,尋址是否可以這樣 mov eax,dword ptr ds:[PtrA + esi + ecx * 4]
.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
.data
; 聲明結構: 使用 <>,{}符號均可
PtrA MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
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
; 循環遍歷結構中的所有值
mov esi,0 ; 遍歷每個結構
mov ecx,4 ; 循環4個大結構
S1:
push ecx
mov ecx,3
S2:
mov eax,dword ptr ds:[PtrA + esi + ecx * 4]
invoke crt_printf,addr szFmt,eax
pop ecx
loop S2
add esi,12
loop S1
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
; while 煦暖輸出整個PtrA結構中的成員
mov (MyCount ptr ds:[Count]).count_x,0 ; 初始化 count_x
S2: 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 (MyCount ptr ds:[Count]).count_y,0
S4: cmp (MyCount ptr ds:[Count]).count_y,12 ; 內層循環 3 * 4 = 12
jge S3
mov ebx,(MyCount ptr ds:[Count]).count_x
add ecx,(MyCount ptr ds:[Count]).count_y
mov eax,dword ptr ds:[PtrA + ecx]
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 S4
S3: 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
jge S2
mov eax,(MyCount ptr ds:[Count]).count_x
add eax,(MyCount ptr ds:[Count]).count_y
invoke crt_printf,addr szFmt,eax
mov eax,(MyCount ptr ds:[Count]).count_y
add eax,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
最終可以完成尋址,輸出這個結構數組中的所有數據了
.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
ListNode Struct
NodeData DWORD ?
NextPtr DWORD ?
ListNode ends
TotalNodeCount = 15
NULL = 0
Counter = 0
.data
LinkList LABEL PTR ListNode
REPEAT TotalNodeCount
Counter = Counter + 1
ListNode <Counter,($ + Counter * sizeof ListNode)>
ENDM
ListNode<0,0>
.code
main PROC
mov esi,offset LinkList
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