匯編是指令集的助記符,由編譯器 + 風格 + 指令集架構 (ISA) 的組合決定語言特性,沒統一標準:
編譯器:
1)MASM:僅支持 Windows 平台,唯一完美支持按需編譯的編譯器,不支持輸出 bin 格式。
2)NASM:跨平台,支持多種輸出格式 (bin/coff/omf/elf/…)
風格:
1)Intel 風格
2)AT&T 風格
主流指令集架構:
1)x86-64/x64/amd64/Intel64
2)ARM64/AArch64
3)RISC-V
4)MIPS
x86 匯編#
以 intel 風格的匯編器為例
內存和尋址模式#
.DATA
聲明靜態數據區#
數據類型修飾原語:
)DB
: Byte, 1 Byte
)DW
: Word, 2 Bytes
)DD
: Double Word, 4 Bytes
)匯編中只有一維數組,只有沒有二維和多維數組。一維數組其實就是內存中的一塊連續區域。另外,DUP
和字符串常量也是聲明數組的兩種方法
.DATA
var DB 64 ; 聲明一字節大小的變量 var,並將其初始化為 64
var2 DB ? ; 聲明一字節大小的未初始化變量 var2,其初值未定義,直到在程序中明確賦值
DB 10 ; 聲明一字節大小的常量,值為 10,The Byte's location is var2 + 1.由於沒標籤,這個值並不能通過標籤訪問
X DW ? ; 聲明一字大小(16位)的未初始化變量 X,初值未定義,直到程序中明確賦值。
Y DD 30000 ; 聲明一個 4-byte 值, referred to as location Y, initialized to 30000.
Z DD 1, 2, 3 ; 聲明數組Z,每個元素4字節大小,初始化為 1, 2, 3.Z標籤表示數據存儲的起始位置 Z+下標*4 是元素地址
bytes DB 10 DUP(?) ; 聲明數組bytes,每個元素1字節大小;10 DUP(?)表示聲明 10 個未初始化的字節
arr DD 100 DUP(0) ; 聲明數組arr,每個元素4字節大小;100 DUP(0)表示聲明 100 個初始化為0的元素
str DB 'hello',0 ; 聲明 6 bytes starting at the address str, 初始化為 hello and the null (0) byte.
內存尋址 Addressing Memory#
MOV
將在內存和寄存器間移動數據 (默認移動 32 位數據),接受兩個參數:第一個參數是目的地,第二個是源。源和目的其實都是地址;[寄存器] 表示引用寄存器的值作為地址;[var] 表示引用符號 var 表示的地址;
;合法尋址的例子:加[]表示存儲的是地址,引用地址指向內存內容
mov eax, [ebx] ; Move the 4 bytes in memory at the address contained in EBX into EAX
mov [var], ebx ; Move the contents of EBX into the 4 bytes at memory address var. (Note, var is a 32-bit constant).
mov eax, [esi-4] ; Move 4 bytes at memory address ESI + (-4) into EAX
mov [esi+eax], cl ; Move the contents of CL into the byte at address ESI+EAX
mov edx, [esi+4*ebx] ; Move the 4 bytes of data at address ESI+4*EBX into EDX
;非法尋址的例子:
mov eax, [ebx-ecx] ; 只能對寄存器的值相加,不能相減
mov [eax+esi+edi], ebx ; 最多只能有 2 個寄存器參與地址計算
數據類型 (大小) 原語(Size Directives)#
修飾指針類型:就是表示引動數據的位數
)BYTE PTR
- 1 Byte
)WORD PTR
- 2 Bytes
)DWORD PTR
- 4 Bytes
mov BYTE PTR [ebx], 2 ; Move 2 into the single byte at the address stored in EBX.
mov WORD PTR [ebx], 2 ; Move the 16-bit integer representation of 2 into the 2 bytes starting at the address in EBX.
mov DWORD PTR [ebx], 2 ; Move the 32-bit integer representation of 2 into the 4 bytes starting at the address in EBX.
指令#
分類 | 指令 | 描述 | 例子 |
---|---|---|---|
數據移動 | mov | 將源操作數的值複製到目標操作數 | mov ax, bx ; 將 bx 寄存器的值複製到 ax 寄存器 |
push | 將數據壓入棧中 | push ax ; 將 ax 寄存器的值壓入棧 | |
pop | 將棧頂數據彈出到目標操作數 | pop bx ; 將棧頂的值彈出並存儲到 bx 寄存器 | |
lea | 加載有效地址,將地址存儲到目標寄存器 | lea ax, [bx + 4] ; 將 [bx + 4] 的有效地址存入 ax | |
算術 / 邏輯運算 | add | 執行加法操作 | add ax, bx ; 將 bx 加到 ax 寄存器中 |
sub | 執行減法操作 | sub ax, bx ; 從 ax 中減去 bx 的值 | |
inc | 對操作數加 1 | inc ax ; 將 ax 寄存器的值增加 1 | |
dec | 對操作數減 1 | dec bx ; 將 bx 寄存器的值減少 1 | |
imul | 有符號乘法 | imul ax, bx ; ax = ax * bx ,有符號乘法 | |
idiv | 有符號除法 | idiv bx ; 將 ax 除以 bx ,結果存儲在 ax 和 dx | |
and | 按位與操作 | and ax, bx ; 將 ax 和 bx 按位與操作結果存回 ax | |
or | 按位或操作 | or ax, bx ; 將 ax 和 bx 按位或操作結果存回 ax | |
xor | 按位異或操作 | xor ax, bx ; 將 ax 和 bx 按位異或操作結果存回 ax | |
not | 按位取反操作 | not ax ; 將 ax 寄存器的所有位取反 | |
neg | 求負操作 | neg ax ; 將 ax 寄存器的值取反(即加上相反數) | |
shl | 左移操作 | shl ax, 1 ; 將 ax 左移 1 位,移出的位被丟棄 | |
shr | 右移操作 | shr bx, 1 ; 將 bx 右移 1 位,移出的位被丟棄 | |
控制流 | jmp | 無條件跳轉 | jmp label ; 跳轉到 label 標籤處 |
je / jz | 如相等則跳轉(je :jump if equal,jz :jump if zero) | je label ; 如果零標誌設置(表示相等),跳轉到 label | |
jne | 如果不相等則跳轉(jump if not equal) | jne label ; 如果零標誌未設置(表示不相等),跳轉到 label | |
jg | 如果大於則跳轉(jump if greater) | jg label ; 如果大於跳轉到 label | |
jl | 如果小於則跳轉(jump if less) | jl label ; 如果小於跳轉到 label | |
cmp | 比較兩個操作數(通過設置標誌寄存器) | cmp ax, bx ; 比較 ax 和 bx 的值(設置標誌寄存器) | |
call | 調用過程,跳轉到子程序並將返回地址壓入棧 | call subroutine ; 調用 subroutine 子程序 | |
ret | 從過程返回,彈出返回地址並跳轉回調用點 | ret ; 從當前過程返回到調用點 |
Calling Convention#
子過程(函數)調用需遵守一套共同的協議,規定如何調用及如何從過程返回。例如,給定一組 calling convention rules,程序員無需查看子函數的定義就可以確定如何將參數傳給它。進一步地,給定一組 calling convention rules,高級語言編譯器只要遵循這些 rules,就可以使得匯編函數和高級語言函數互相調用。
C Language Calling Convention#
Calling conventions 有多種。C 語言調用約定使用最廣泛。遵循這個約定,可以使匯編代碼安全地被 C/C++ 調用,也可以從匯編代碼調用 C 函數庫。
1)強烈依賴硬件棧的支持 (hardwared-supported stack)
2)基於 push
, pop
, call
, ret
指令
3)子過程參數通過棧傳遞: 寄存器保存在棧上,子過程用到的局部變量也放在棧上
大部分處理器上實現的大部分高級過程式語言,都使用與此相似的調用慣例。調用慣例分為兩部分。第一部分用於 調用方(caller),第二部分用於被調用方(callee)。需要強調的是,錯誤地使用這些規則將導致棧被破壞,程序很快出錯;因此在你自己的子過程中實現 calling convention 時需要格外仔細。
調用方規則 Caller Rules#
調用方需保存現場再調用子過程:
1)調用方保存的寄存器caller-saved registers:EAX, ECX, EDX; 這幾個寄存器可能會被被 callee 修改,保存它們在調用結束後恢復棧的狀態。
2)將傳給子過程的參數 push onto stack:參數按逆序 push 入棧(最後一個參數先入)。由於棧是向下生長的,第一個參數會被存儲在最低地址(這個特性使得變長參數列表成為可能)。
3)使用 call 指令,調用子過程 (函數):call 會自動將返回地址 push onto stack,然後開始執行子過程代碼。子過程返回後(call 執行結束後),被調用方會將返回值放到 EAX 寄存器,調用方可從中讀取。為恢復機器狀態,調用方需要做:
a.從棧上刪除傳遞的參數
b.恢復由調用方保存的寄存器(EAX
, ECX
, EDX
)—— 從棧上 pop 出來;調用方可以認為,除這三個之外,其他寄存器值沒有被修改過。
;保存現場
push eax
push ecx
push edx
push [var] ; Push last parameter first
push 216 ; Push the second parameter
push eax ; Push first parameter last
call _myFunc ; Call the function (assume C naming);call 會自動將返回地址push onto stack
;恢復現場。
add esp, 24 ;清理棧空間,恢復棧指針,刪除傳遞的 6 個參數,棧指針向高地址移動24
mov result, eax ; 讀取返回值到 result 變量
被調用方規則 Callee Rules#
1)將原棧幀基址寄存器 EBP
的值入棧,然後 copy ESP
to EBP
;作為子過程棧幀的新基址
2)在棧上為局部變量分配空間;棧自頂向下生長,故隨著變量的分配,棧頂指針不斷減小
3)調用方保存的寄存器callee-saved
—— 將他們壓入棧。包括 EBX
, EDI
, ESI
; 這幾個寄存器是被調用方負責保存和恢復
)執行子過程的代碼,** 將返回值保存在 EAX
;** 當子過程返回後:
a. 恢復應由被調用方保存的寄存器(EDI
, ESI
) —— 從棧上 pop 出來
b. 釋放局部變量
c. 恢復調用方 base pointer EBP
—— 從棧上 pop 出來
d. 最後,執行 ret
,返回給調用方 (caller)
.486 ;告訴匯編器使用什麼什麼指令集架構編譯
.MODEL FLAT ;扁平內存模型
.CODE ;標記匯編文件中代碼段的開始
PUBLIC _myFunc ;外部可見/可鏈接/非本文件私有的函數
_myFunc PROC
;1)
push ebp
mov ebp, esp
;2)
sub esp, 4 ; 為一個局部變量分配空間(4 字節)
;3)
push edi ; 保存寄存器值,EDI 會被修改
push esi ; 保存寄存器值,ESI 會被修改
;)子程序主體
mov eax, [ebp+8] ; 將第一個參數的值移入 EAX
mov esi, [ebp+12] ; 將第二個參數的值移入 ESI
mov edi, [ebp+16] ; 將第三個參數的值移入 EDI
mov [ebp-4], edi ; 將 EDI 存入局部變量
add [ebp-4], esi ; 將 ESI 加入局部變量
add eax, [ebp-4] ; 將局部變量的內容加到 EAX 中,作為最終結果
;)a
pop esi ; 恢復寄存器 ESI 的值
pop edi ; 恢復寄存器 EDI 的值
;)b
mov esp, ebp ; 釋放局部變量
;)c
pop ebp ; 恢復調用方的基指針值
;)d
ret ;將棧中的返回地址彈出,跳轉到返回地址繼續執行調用方的代碼
_myFunc ENDP
END ;標記程序的結束
riscV 匯編#
arm 匯編#
Intel/AT&T 語法風格差異#
AT&T syntax | Intel syntax |
---|---|
insn source, destination | insn destination, source |
內存操作#
AT&T 內存尋址使用的是 ()
Intel 內存尋址使用的是 []
AT&T syntax | Intel syntax |
---|---|
movl -12(%rbp), %eax | mov eax, DWORD PTR -12[rbp] |
尋址#
AT&T 語法:disp (base, index, scale)
Intel 語法:[base + index*scale + disp]
最終地址為 base + disp + index * scale
AT&T syntax | Intel syntax |
---|---|
movl -12(%rbp), %eax | mov eax, DWORD PTR -12[rbp] |
leaq 0(,%rax,4), %rdx | lea rdx, 0[0+rax*8] |
以特定匯編格式輸出#
objdump 反匯編:linux 下 objdump 默認反匯編出 AT&T 格式,添加 -M 指定輸出格式
gcc -c test.c // 先用 gcc 編譯成二進制文件
objdump -d test.o // 默認輸出 AT&T 格式
objdump -M intel -d test.o // 可指定輸出 intel 格式
也可選擇輸出特定風格的匯編編譯器,再生成匯編文件查看
gcc -S test.c // 默認輸出 AT&T 格式
gcc -S -masm=intel test.c // 輸出 intel 的語法格式