匯編語言(assembly language)是一種用於電子計算機、微處理器、微控制器或其他可編程器件的低級語言,亦稱為符號語言.在匯編語言中,用助記符(Mnemonics)代替機器指令的操作碼,用地址符號(Symbol)或標號(Label)代替指令或操作數的地址.在不同的設備中,匯編語言對應着不同的機器語言指令集,通過匯編過程轉換成機器指令,普遍地說,特定的匯編語言和特定的機器語言指令集是相互對應的,不同平台之間不可直接移植.
堆棧操作指令
在計算機領域,堆棧是一個不容忽視的概念,堆棧是一種后進先出(LIFO,Last-In,First-Out)
的數據結構,這是因為最后壓入堆棧的值總是最先被取出,而新數值在執行PUSH壓棧時總是被加到堆棧的最頂端,數據也總是從堆棧的最頂端被取出,堆棧是個特殊的存儲區
,主要功能是暫時存放數據和地址,通常用來保護斷點和現場.
當程序運行時,棧是由CPU直接管理
的線性
內存數組,它使用兩個寄存器(SS和ESP)
來保存堆棧的狀態.在保護模式下,SS寄存器存放段選擇符(Segment Selector)
運行在保護模式下的程序不能對其進行修改,而ESP寄存器
的值通常是指向特定位置的一個32位偏移值
,我們很少需要直接操作ESP寄存器,相反的ESP寄存器總是由CALL,RET,PUSH,POP
等這類指令間接性的修改.
接着來簡單介紹下關於堆棧操作的兩個寄存器,CPU系統提供了兩個特殊的寄存器用於標識位於系統棧頂端的棧幀.
ESP 棧指針寄存器: 棧指針寄存器,其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂.
EBP 基址指針寄存器: 基址指針寄存器,其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的底部.
◆堆棧參數傳遞◆
在通常情況下ESP是可變的,隨着棧的生產而逐漸變小,而EBP寄存器是固定的,只有當函數的調用后,發生入棧操作而改變.
1.在32位系統中,執行
PUSH壓棧
時,堆棧指針自動減4
,再將壓棧的值復制到堆棧指針所指向的內存地址.
2.在32位系統中,執行POP出棧
時,從棧頂移走一個值並將其復制給內存或寄存器,然后再將堆棧指針自動加4
.
3.在32位系統中,執行CALL調用
時,CPU會用堆棧保存當前被調用過程的返回地址,直到遇到RET指令再將其彈出.
PUSH/POP指令: 在32位環境下,分別將數組中的元素100h-300h
壓入堆棧,並且通過POP將元素反彈出來.
.data
Array DWORD 100h,200h,300h,400h
.code
main PROC
xor eax,eax
push eax ; push 0
push DWORD PTR [Array] ; push 100
push DWORD PTR [Array+4] ; push 200
push DWORD PTR [Array+8] ; push 300
pop eax ; pop 300
pop eax ; pop 200
pop eax ; pop 100
pop eax ; pop 0
push 0
call ExitProcess
main ENDP
END main
PUSHFD/POPFD指令: PUSHFD在堆棧上壓入EFLAGS寄存器的值,POPFD將堆棧的值彈出並送至EFLAGS寄存器.
.data
SaveFlage DWORD ?
.code
main PROC
pushfd ; 標識入棧
pop SaveFlage ; 彈出並保存到內存
push SaveFlage ; 從內存取出,並入棧
popfd ; 恢復到EFLAGS寄存器中
push 0
call ExitProcess
main ENDP
END main
PUSHAD/POPAD指令: 將通用寄存器按照EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI的順序壓棧保存.
.code
main PROC
pushad
mov eax,1000
mov ebx,2000
mov ecx,3000
mov edx,4000
popad
push 0
call ExitProcess
main ENDP
END main
◆聲明局部變量◆
高級語言程序中,在單個過程中創建使用和銷毀的變量我們稱它為局部變量(local variable)
,局部變量是在程序運行時,由系統動態的在棧上開辟的,在內存中通常在基址指針(EBP)
之下,盡管在匯編時不能給定默認值,但可以在運行時初始化,如下一段偽代碼:
void MySub()
{
int var1 = 10;
int var2 = 20;
}
上面的一段代碼經過C編譯器轉換后,會變成如下的樣子,其中EBP-4
必須是4的倍數,因為默認就是4字節存儲.
MySub PROC
push ebp ; 將EBP存儲在棧中
mov ebp,esp ; 堆棧框架的基址
sub esp,8 ; 創建局部變量空間
mov DWORD PTR [ebp-4],10 ; var1 = 10
mov DWORD PTR [ebp-8],20 ; var2 = 20
mov esp,ebp ; 從堆棧上刪除局部變量
pop ebp ; 恢復EBP指針
ret 8 ; 返回,清理堆棧
MySub ENDP
如果去掉了上面的mov esp,ebp
,那么當執行pop ebp
時將會得到EBP等於10,執行RET指令會導致控制轉移到內存地址10處執行,從而程序會崩潰.
為了使代碼更加的容易閱讀,可以在上面的代碼的基礎上給每個變量的引用地址都定義一個符號並在代碼中使用這些符號來完成編寫.
var1_local EQU DWORD PTR [ebp-4]
var2_local EQU DWORD PTR [ebp-8]
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
◆ENTER/LEAVE 偽指令◆
ENTRE指令自動為被調用過程創建堆棧框架,它為局部變量保留堆棧空間並在堆棧上保存EBP,該指令執行后會執行以下動作.
1.在堆棧上壓入EBP(push ebp)
2.把EBP設為堆棧框架的基指針(mov ebp,esp)
3.為局部變量保留適當的空間(sub esp,numbytes)
ENTER指令有兩個參數,第一個操作數是一個常量,用於指定要為局部變量保留多少堆棧空間(numbytes),第二個參數指定過程的嵌套層數,這兩個操作數都是立即數,numbytes總是向上取整為4的倍數,以使ESP按照雙字邊界地址對其.
比如以下代碼,使用ENTER為局部變量保存8字節的堆棧空間:
MySub PROC
enter 8,0
MySub ENDP
經過編譯器轉換后,會首先轉換為以下的樣子:
MySub PROC
push ebp
mov ebp,esp
sub esp,8
MySub ENDP
上面的代碼只有開頭沒有結尾,如果要使用ENTER指令分配空間的話,則必須在結尾加上LEAVE指令,這樣程序才完整.
MySub PROC
enter 8,0
....
leave
ret
MySub ENDP
下面代碼和上面代碼作用是相同的,它首先為局部變量保留8字節的堆棧空間然后丟棄.
MySub PROC
push ebp
mov ebp,esp
sub esp,8
....
mov esp,ebp
pop ebp
ret
MySub ENDP
◆USES/LOCAL 偽指令◆
USES操作符: 該操作符用於指定需要壓棧的寄存器,其會自動生成壓棧出棧代碼無需手動添加.
.code
main PROC
mov eax,1
mov ebx,2
mov ecx,3
call mycall
push 0
call ExitProcess
main ENDP
mycall PROC USES eax ebx ecx ; 生成壓棧代碼,自動壓eax,ebx,ecx
xor eax,eax ; 壓棧的寄存器可以隨意修改
xor ebx,ebx ; 過程結束后會自動恢復這些寄存器
ret
mycall ENDP
END main
LOCAL操作符: 在過程內聲明一個或多個命名局部變量,並賦予相應的尺寸屬性,該語句必須緊跟PROC指令后面.
.code
main PROC
LOCAL var1:WORD
LOCAL var2:DWORD,var3:BYTE
mov DWORD PTR [var1],1024
mov eax,DWORD PTR [var1]
mov [var2],1024 ; DWORD
mov eax,[var2]
mov [var3],10 ; BYTE
mov al,[var3]
push 0
call ExitProcess
main ENDP
END main
局部變量:
.code
lyshark PROC var1:WORD,var2:DWORD
LOCAL @loca1:BYTE,@loca2:DWORD
LOCAL @local_byte[100]:BYTE
mov ax,var1
mov ebx,@loca2
lea ecx,@local_byte
mov @local_byte[0],0
mov @local_byte[1],1
mov @local_byte[2],2
mov @local_byte[3],3
lyshark ENDP
main PROC
invoke lyshark,100,10000
ret
main ENDP
END main
LOCAL(申請數組):
.code
main PROC
LOCAL var[3]:DWORD
mov var[0],100
mov var[1],200
mov eax,var[0]
mov ebx,var[1]
main ENDP
END main
.code
main PROC
LOCAL ArrayDW[10]:DWORD
LOCAL ArrayB[10]:BYTE
lea eax,[ArrayDW]
mov [ArrayDW],10
mov [ArrayDW + 4],20
mov [ArrayDW + 8],30
main ENDP
END main
## 過程調用指令
CALL指令指示處理器在新的內存地址執行指令,當用戶調用CALL指令時,該指令會首先將CALL指令的下一條指令的內存地址壓入堆棧保存,然后將EIP寄存器修改為CALL指令的調用處,等調用結束后返回從堆棧彈出CALL的下一條指令地址.
1.當遇到CALL指令時,程序會經過計算得到CALL指令的下一條指令的地址,並將其壓入堆棧.
2.接着會將EIP寄存器的地址指向被調用過程的地址,被調用過程被執行.
3.最后過程內部通過RET指令返回,將從堆棧中彈出EIP的地址,程序繼續向下執行.
4.CALL相當於push+jmp,RET相當於pop+jmp.
普通參數傳遞:
.code
sum PROC var1:DWORD,var2:DWORD,var3:DWORD
mov eax,var1
mov ebx,var2
mov ecx,var3
ret
sum ENDP
main PROC
invoke sum,10,20,30 ; 調用並傳遞參數
ret
main ENDP
END main
寄存器傳遞參數:
.code
sum PROC
add eax,ebx
add eax,ecx
ret
sum ENDP
main PROC
mov eax,10
mov ebx,20
mov ecx,30
call sum
ret
main ENDP
END main
使用PROTO聲明: 如果調用的函數在之后實現, 須用 PROTO 提前聲明,否則會報錯
sum PROTO :DWORD,:DWORD,:DWORD ; 函數聲明的主要是參數類型,省略參數名
.code
main PROC
invoke sum,10,20,30 ; 現在調用的是之后的函數
ret
main ENDP
sum PROC var1,var2,var3
mov eax,var1
add eax,var2
add eax,var3
ret
sum ENDP
END main
CALL/RET指令: 編寫一個過程,實現對整數數組的求和,並將結果保存到EAX寄存器中.
.data
array DWORD 1000h,2000h,3000h,4000h,5000h
theSum DWORD ?
.code
main PROC
mov esi,offset array ; ESI指向array
mov ecx,lengthof array ; ECX=array元素個數
call ArraySum ; 調用求和指令
mov theSum,eax ; 將結果保存到內存
push 0
call ExitProcess
main ENDP
ArraySum PROC
push esi ; 保存ESI,ECX
push ecx
mov eax,0 ; 初始化累加寄存器
L1:
add eax,[esi] ; 每個整數都和EAX中的和相加
add esi,TYPE DWORD ; 遞增指針,繼續遍歷
loop L1
pop ecx ; 恢復寄存器
pop esi
ret
ArraySum ENDP
END main
通過該語句塊配合可以生成自定義過程,下面我們創建一個名為Sum
的過程,實現EBX+ECX
並將結果保存在EAX
寄存器中.
.data
TheSum DWORD ?
.code
main PROC
mov ebx,100 ; 傳遞ebx
mov ecx,100 ; 傳遞ecx
call Sum ; 調用過程
mov TheSum,eax ; 保存結果到TheSum
push 0
call ExitProcess
main ENDP
Sum PROC
xor eax,eax
add eax,ebx
add eax,ecx
ret
Sum ENDP
END main
INVOKE調用系統API: 默認情況下,會將返回結果保存在eax寄存器中.
.data
szCaption db "MsgBox",0
szText db "這是一個提示框,請點擊確定完成交互!",0
.code
main PROC
.WHILE (1)
invoke MessageBox,NULL,offset szText,offset szCaption,MB_YESNO
.break .if(eax == IDYES)
.ENDW
ret
main ENDP
END main
模塊化調用: 首先創建一個sum.asm
然后在main.asm
中引用sum這個文件中的函數.
; sum.asm 首先編譯這個文件,並將其放入指定目錄下
.386
.model flat, stdcall
.code
sum PROC v1, v2, v3
mov eax, v1
add eax, v2
add eax, v3
ret
sum ENDP
end
; main.asm 直接引用編譯后的lib文件即可
;這里的引入路徑可以是全路徑, 這里是相對路徑
includelib /masm32/lib/sum.lib
;子程序聲明
sum proto :dword, :dword, :dword
.code
main PROC
invoke sum,10,20,30 ;調用過程
ret
main ENDP
END main
## 結構與聯合
結構(struct)時邏輯上互相關聯的一組變量的模板或模式,結構中的單個變量稱為域(field)
,程序的語句可以把結構作為一個實體進行訪問,也可以對結構的單個域進行訪問,結構通常包括不同類型的域,而聯合(union)同樣也是把多個標識符組合在一起,不過與結構不同的是,聯合體共用用一塊內存區域,內存的大小取決於聯合體中最大的元素.
引用結構變量: 通過使用<>,{}
均可聲明結構體,同時可以初始化,對結構體賦初值.
;定義結構
MyPoint struct
pos_x DWORD ?
pos_y DWORD ?
MyPoint ends
.data
;聲明結構, 使用 <>、{} 均可
ptr1 MyPoint <10,20>
ptr2 MyPoint {30,40}
.code
main PROC
lea edx, ptr1
mov eax, (MyPoint ptr [edx]).pos_x ; 此時eax=10
mov ebx, (MyPoint ptr [edx]).pos_y ; 此時ebx=20
mov (MyPoint PTR [edx]).pos_x,100 ; 將100寫入MyPoint.pos_x結構中存儲
ret
main ENDP
END main
結構初始化: 以下定義了MyStruct
結構,並將user2
初始化,FName=lyshark,FAge=25.
MyStruct struct
FName db 20 dup(0)
FAge db 100
MyStruct ends
.data
user1 MyStruct <>
user2 MyStruct <'lyshark',25>
.code
main PROC
;lea edx, user1
;mov eax,DWORD PTR (MyStruct ptr[edx]).FName
;mov ebx,DWORD PTR (MyStruct ptr[edx]).FAge
mov eax,DWORD PTR [user2.FName] ; eax=lyshark
mov ebx,DWORD PTR [user2.FAge] ; ebx=25
ret
main ENDP
END main
使用系統結構: 通過調用GetLocalTime
獲取系統時間,並存儲到SYSTEMTIM
結構體中.
.data
sysTime SYSTEMTIME <> ; 聲明結構體
.code
main PROC
invoke GetLocalTime,addr sysTime ; 獲取系統時間並放入sysTime
mov eax,DWORD PTR sysTime.wYear ; 獲取年份
mov ebx,DWORD PTR sysTime.wMonth ; 獲取月份
mov ecx,DWORD PTR sysTime.wDay ; 獲取天數
ret
main ENDP
END main
結構體的嵌套定義:
MyPT struct
pt_x DWORD ?
pt_y DWORD ?
MyPT ends
Rect struct
Left MyPT <>
Right MyPT <>
Rect ends
.data
LyShark1 Rect <>
LyShark2 Rect {<10,20>,<100,200>}
.code
main PROC
mov [LyShark1.Left.pt_x],100
mov [LyShark1.Left.pt_y],200
mov [LyShark1.Right.pt_x],1000
mov [LyShark1.Right.pt_y],2000
mov eax,[LyShark1.Left.pt_x]
ret
main ENDP
END main
聯合體的聲明:
; 定義聯合體
MyUnion union
My_Dword DWORD ?
My_Word WORD ?
My_Byte BYTE ?
MyUnion ends
.data
test1 MyUnion {1122h}; ;只能存放初始值
.code
main PROC
mov eax, [test1.My_Dword]
mov ax, [test1.My_Word]
mov al, [test1.My_Byte]
ret
main ENDP
END main
## 關於宏匯編
宏過程(Macro Procedure)
是一個命名的語匯編語句塊,一旦定義后,宏過程就可以在程序中被調用任意多次,調用宏過程的時候,宏內的語句塊將替換到調用的位置,宏的本質是替換,但像極了子過程,宏可定義在源程序的任意位置,但一般放在.data前面.
一個簡單的宏:
MyCode macro
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
endm
.code
main PROC
MyCode ; 將被替換為上面兩行代碼
ret
main ENDP
END main
一個代替求和函數的宏
MySum macro var1, var2, var3
mov eax,var1
add eax,var2
add eax,var3
endm
.code
main PROC
MySum 10,20,30
MySum 10,20,30,40 ; 多余的參數40會被忽略
ret
main ENDP
END main
宏參數的默認值: 通過定義默認值,可以不給默認的變量傳遞參數.
; 參數 var1、var2 通過 REQ 標識說明是必備參數
MySum macro var1:req, var2:req, var3:=<30> ; var3默認值是30
mov eax,var1
add eax,var2
add eax,var3
endm
.code
main PROC
MySum 10,20
ret
main ENDP
END main
使用EXITM終止宏執行: 可使用關鍵字exitm 終止宏代碼的后面內容.
MySum macro
xor eax,eax
xor ebx,ebx
xor ecx,ecx
exitm ; 只會清空前三個寄存器,后面的跳過了
xor edx,edx
xor esi,esi
endm
.code
main PROC
MySum
ret
main ENDP
END main
使用PURGE取消指定宏的展開:
MySum macro
xor eax,eax
xor ebx,ebx
endm
.code
main PROC
MySum ; 這個會被展開
purge MySum ; 這個不會展開
MySum ; 這個宏也不會展開了
ret
main ENDP
END main
在宏內使用局部標號:
MyMax macro var1,var2
LOCAL jump
mov eax,var1
cmp eax,var2
jge jump
xor eax,eax
jump: ret
endm
.code
main PROC
MyMax 20,10
main ENDP
END main
特殊操作符: &、<>、%、!
& ;替換操作符
<> ;字符串傳遞操作符
% ;表達式操作符, 也用於得到一個變量或常量的值
! ;轉義操作符
;自定義的宏
mPrint macro Text
PrintText '* &Text& *'
endm
.code
main proc
;該宏會把參數直接替換過去
mPrint 1234 ;* 1234 *
;要保證參數的完整應該使用 <>
mPrint 12,34 ;* 12 *
mPrint <12,34> ;* 12,34 *
;需要計算結果應該使用 %()
mPrint 34+12 ;* 34+12 *
mPrint %(34+12) ;* 46 *
;用到 &、<、>、%、! 應該使用 ! 轉義
mPrint 10 !% 2 = %(10/2)!! ;* 10 % 2 = 5! *
ret
main endp
end main
## 過程小例子
整數求和: 通過使用匯編語言實現一個整數求和的小例子.
.data
String WORD 100h,200h,300h,400h,500h
.code
main PROC
;lea edi,String ; 取String數組的基址
mov edi,offset String ; 同上,兩種方式均可
mov ecx,lengthof String ; 取數組中的數據個數
mov ax,0 ; 累加器清零
L1:
add ax,[edi] ; 加上一個整數
add edi,TYPE String ; 指向下一個數組元素,type(2byte)
loop L1
push 0
call ExitProcess
main ENDP
END main
正向復制字符串: 使用匯編語言實現字符串的復制,將數據從source
復制到target
內存中.
.data
source BYTE "hello lyshark welcome",0h
target BYTE SIZEOF source DUP(0),0h ; 取源地址數據大小
.code
main PROC
mov esi,0 ; 使用變址寄存器
mov ecx,sizeof source ; 循環計數器
L1:
mov al,source[esi] ; 從源地址中取一個字符
mov target[esi],al ; 將該字符存儲在目標地址中
inc esi ; 遞增,將指針移動到下一個字符
loop L1
push 0
call ExitProcess
main ENDP
END main
反向復制字符串: 使用匯編語言實現字符串的復制,將數據從source
復制到target
內存中且反向存儲數據.
.data
source BYTE "hello lyshark welcome",0h
target BYTE SIZEOF source DUP(0),0h
.code
main PROC
mov esi,sizeof source
mov ecx,sizeof source
mov ebx,0
L1:
mov al,source[esi]
mov target[ebx],al
dec esi
inc ebx
loop L1
push 0
call ExitProcess
main ENDP
END main
查看內存與寄存器: 通過調用DumpMem/DumpRegs
顯示內存與寄存器的快照.
.data
array DWORD 1,2,3,4,5,6,7,8,9,0ah,0bh
.code
main PROC
mov esi,offset array ; 設置內存起始地址
mov ecx,lengthof array ; 設置元素數據,偏移
mov ebx,type array ; 設置元素尺寸(1=byte,2=word,4=dword)
call DumpMem ; 調用內存查詢子過程
call DumpRegs ; 調用查詢寄存器子過程
push 0
call ExitProcess
main ENDP
END main
匯編實現性能度量: 通過調用庫函數,實現對指定代碼執行的性能度量.
.data
StartTime DWORD ?
.code
main PROC
call GetMseconds ; 調用區本地時間過程
mov StartTime,eax ; 將返回值賦值給StartTime
mov ecx,10 ; 通過調用延時過程,模擬程序的執行
L1:
mov eax,1000 ; 指定延時1s=1000ms
call Delay ; 調用延時過程
loop L1
call GetMseconds ; 再次調用本地時間過程
sub eax,StartTime ; 結束時間減去開始時間
call WriteDec ; 以十進制形式輸出eax寄存器的值
push 0
call ExitProcess
main ENDP
END main
字符輸出: WriteString(字符串)
,WriteInt(整數)
,WriteHex(16進制)
,WriteChar(字符)
,WriteDec(10進制)
.
.data
Message BYTE "Input String:",0h
String DWORD ?
.code
main PROC
; 設置控制台背景顏色
mov eax,yellow +(blue*16) ; 設置為藍底黃字
call SetTextColor ; 調用設置過程
call Clrscr ; 清除屏幕,clear
; 提示用戶一段話
mov edx,offset Message ; 指定輸出的文字
call WriteString ; 調用回寫過程
call Crlf ; 調用回車
push 0
call ExitProcess
main ENDP
END main
字符輸入: ReadString(字符串)
,ReadInt(整數)
,ReadHex(16進制)
,ReadChar(字符)
,ReadDec(10進制)
.
.data
Buffer BYTE 21 DUP(0) ; 輸入緩沖區
ByteCount DWORD ? ; 存放計數器
.code
main PROC
mov edx,offset Buffer ; 指向緩沖區指針
mov ecx,sizeof Buffer ; 指定最多讀取的字符數
call ReadString ; 讀取輸入字符串
mov ByteCount,eax ; 保存讀取的字符數
push 0
call ExitProcess
main ENDP
END main
生成偽隨機數:
.code
main PROC
mov ecx,5 ; 循環生成5個隨機數
L1:
call Random32 ; 生成隨機數
call WriteDec ; 以十進制顯示
mov al,TAB ; 水平制表符
call WriteChar ; 顯示水平制表符
loop L1
call Crlf ; 回車
push 0
call ExitProcess
main ENDP
END main
生成自定義隨機數:
.code
main PROC
mov ecx,5 ; 循環生成5個隨機數
L1:
mov eax,100 ; 0-99之間
call RandomRange ; 生成隨機數
sub eax,50 ; 范圍在-50-49
call WriteInt ; 十進制輸出
mov al,TAB
call WriteChar ; 輸出制表符
loop L1
call Crlf ; 回車
push 0
call ExitProcess
main ENDP
END main
參考文獻:《Intel 匯編語言程序設計》,《琢石成器-Win32匯編語言程序設計》,《匯編語言-王爽》