Win32匯編 - 字符串/浮點運算/過程


整理復習匯編語言的知識點,以前在學習《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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM