一、寄存器
1.1 通用寄存器
一個x86-64的中央處理單元(CPU)包含一組16個存儲64位值的通用寄存器。這些寄存器用來存儲整數數據和指針。下圖顯示了這16個寄存器。它們的名字都以%r開頭,不過後面還跟著不同命名規則的名字,這是由於指令集歷史演化造成的。最初的8086中有8個16位的寄存器,即圖中的%ax到%bp。每個寄存器都有特殊的用途,它們的名字就反映了這些不同的用途。擴展到IA32架構時,這些寄存器也擴展成32位寄存器,標號從%eax到%ebp。擴招到x86-64後,原來的8個寄存器擴展成64位,標號從%rax到%rbp。除此之外,還增加了8個新的寄存器,它們的標號是按照新的命名規則制定的:從%r8到%r15。
如上圖中嵌套的方框表明的,指令可以對這16個寄存器的低位字節中存放的不同大小的數據進行操作。字節級操作可以訪問最低的字節,16位操作可以訪問最低的2個字節,32位操作可以訪問最低的4個字節,而64位操作可以訪問整個寄存器。
Tips:當指令以寄存器作為目標時,對於生成小於8位元組結果的指令,寄存器中剩下的字節如何處理,有兩條規則:
生成1位元組和2位元組數字的指令會保持剩下的字節不變
生成4位元組的指令會把高位4位元組置為0。
後面這條規則是作為從IA32到x86-64的擴展的一部分而採用的。
1.2 標誌寄存器EFLAFS
EFLAGS標誌寄存器包含有狀態標誌位、控制標誌位以及系統標誌位,處理器在初始化時將EFLAGS標誌寄存器賦值為00000002H。
下圖描繪了EFLAGS標誌寄存器各位的功能,其中的第1、3、5、15以及22~31位保留未使用。由於64位模式不再支持VM和NT標誌位,所以處理器不應該再置位這兩個標誌位。
TIPs:在64位模式中,EFLAGS標誌寄存器已從32位擴展為64位,被稱作RFLAGS寄存器。其中高32位保留未使用,低32位與EFLAGS相同。
接下來,我們會根據標誌位功能將EFLAGS劃分位狀態標誌、方向標誌、系統標誌和IOPL區域等幾部分,並對各部分的標誌位功能進行逐一講解。(請參考Intel官方白皮書Volumn 1的3.4.3節)。
1.2.1 狀態標誌
EFLAGS標誌寄存器的狀態標誌(位0、2、4、6、7和11)可以反映出彙編指令計算結果的狀態,像add、sub、mul、div等彙編指令計算結果的奇偶性、溢出狀態、正負值皆可從上述狀態找那個反映出來。
下表是這些狀態標誌的功能描述:
縮寫 |
全稱 |
名稱 |
位置 |
描述 |
CF |
Carry Flag |
進位標誌 |
0 |
運算中,當數值的最高位產生了進位或者借位,CF位都會置1,否則為0。它可用於檢測無符號整數運算結果是否溢出。也可用於多精度運算中。 |
PF |
Parity Flag |
奇偶標誌 |
2 |
用於標記結果低8位中1的個數,如果為偶數, PF位為1,否則為0 。注意,是最低的那8位,不管操作數是16位,還是32位。奇偶校驗經常用於數據傳輸開始時和結束後的對比,判斷傳輸過程中是否出現錯誤。 |
AF |
Auxiliary Carry Flag |
輔助進位標誌 |
4 |
輔助進位標誌,用來記錄運算結果低4位的進、借位情況,即若低半字節有進、借位,AF為1,否則為0。 |
ZF |
Zero Flag |
零值標誌 |
6 |
若計算結果為0,此標誌位置1,否則為0。 |
SF |
Sign Flag |
符號標誌 |
7 |
若運算結果為負,則SF位為1,否則為0。 |
OF |
Overflow Flag |
溢出標誌 |
11 |
用來標識計算的結果是否超過了數據類型可以表示的範圍,若OF為1,表示有溢出,為0則未溢出。專門用於檢測有符號整數運算結果是否溢出。 |
這些標誌位可反映出三種數據類型的計算結果:無符號整數、有符號整數和BCD整數(Binary-coded decimal integers)。其中CF標誌位可反映出無符號整數運算結果的溢出狀態;OF標誌位可反映出有符號整數(補碼表示)運算結果的溢出狀態;AF標誌位表示BCD整數運算結果的溢出狀態;SF標誌位反應出有符號整數運算結果的正負值;ZF標誌位反映出有符號或無符號整數運算的結果是否為0。
以上這些標誌位,只有CF標誌位可通過stc、clc和cmc(Complement Carry Flag,計算原CF位的補碼)彙編指令更改位值。它也可藉助位操作指令(bt、bts、btr和btc指令)將指定位值複製到CF標誌位。而且,CF標誌位還可在多倍精度整數計算時,結合adc指令(含進位的加法計算)或sbb指令(含借位的減減法)將進位計算或借位計算擴展到下次計算中。
至於狀態跳轉指令Jcc、狀態字節置位指令SETcc、狀態循環指令LOOPcc以及狀態移動指令CMOVcc,它們可將一個或多個狀態標誌位作為判斷條件,進程分支跳轉、字節置位以及循環計數。
1.2.2 方向標誌
DF方向標誌位(Direction Flag)位於EFLAGS標誌寄存器的第 10 位,它控制著字符串指令(諸如MOVs、cmps、scas、lods、stos等)的操作方向。置位DF標誌位可使字符串指令按從高到低的地址方向(自減)操作數據,復位DF標誌位可使字符串指令按從低到高的地址方向(自增)操作數據。彙編指令std和cld可用於置位和復位DF方向標誌。
1.2.3 系統標誌和IOPL區域
第 8 位為TF位,即Trap Flag,意為陷阱標誌位。此位若為1,用於讓CPU進入單步運行方式,若為0,則為連續工作的方式。平時我們用的debug程序,在單步調試時,原理上就是讓TF位為1。
第 9 位為IF位,即Interrupt Flag,意為中斷標誌位。若IF位為1,表示中斷開啟,CPU可響應外部可屏蔽中斷。若為0,表示中斷關閉,CPU不再響應來自CPU外部的可屏蔽中斷,但CPU內部的異常還是要響應的。
第 12~13 位為IOPL,即 Input Output Privilege Level,這用在有特權級概念的CPU中。有4個任務特權級,即特權級0~3,故IOPL要占用2位來表示這4種特權級。
第 14 位為NT,即 Nest Task,意為任務嵌套標誌位。8088支持多任務,一個任務就是一個進程。當一個任務中又嵌套調用了另一個任務時,此NT位為1,否則為0。
第 16 位為RF位,即 Resume Flag,意為恢復標誌位。該標誌位用於程序調試,指示是否接受調試故障,它需要與調試寄存器一起使用。當RF為1時忽略調試故障,為0時接受。
第 17 位為VM位,即 Virtual 8086 Model,意為虛擬8086模式。
第 18 位為AC位,即 Alignment Check / Access Control,意為對齊檢查。若AC位為1時,則進行地址對齊檢查,位0時不檢查。
第 19 位為VIF位,即 Virtual Interrupt Flag,意為虛擬終端標誌位,虛擬模式下的中斷標誌。
第 20 位為VIP位,即 Virtual Interrupt Pending Flag,意為虛擬中斷掛起標誌位。在多任務情況下,為作業系統提供的虛擬中斷掛起信息,需要與 VIF 位配合。
第 21 位為ID位,即 Identification Flag,意為識別標誌位。系統經常要判斷CPU型號,若ID位為1,表示當前CPU支持CPUID指令,這樣便能獲取CPU的型號、廠商信息等;若ID位為0,則表示當前CPU不支持CPUID指令。
1.3 段寄存器
x86-64架構,擁有6個16位段寄存器(CS、DS、SS、ES、FS和GS),用於保存16位段選擇子。段選擇子是一種特殊的指針,用於標識內存中的段。要訪問內存中的特定段,該段的段選擇子必須存在於相應的段寄存器中。
在平坦內存模型中,段選擇子指向線性地址空間的地址0;在分段內存模型中,每個分段寄存器通常加載有不同的段選擇子,以便每個分段寄存器指向線性地址空間內的不同分段。
每一個段寄存器表示三種存儲類型之一:代碼,數據,棧。
CS寄存器保存了代碼段(code segment)選擇子。代碼段存儲的是需要執行的指令,處理器使用CS寄存器內的代碼段選擇子和RIP/EIP寄存器的內容生成的線性地址來從代碼段查詢指令。RIP/EIP寄存器存儲的是下一條要執行的指令在代碼段上的偏移。
DS、ES、FS和GS寄存器指向了四個數據段(data segment)。
SS寄存器包含棧段(stack segment)的段選擇子,其中存儲當前正在執行的程序、任務或處理程序的過程堆棧。
1.4 控制寄存器
目前,Intel處理器共擁有6個控制寄存器(CR0、CR1、CR2、CR3、CR4、CR8),它們有若干個標誌位組成,通過這些標誌位可以控制處理器的運行模式、開啟擴展特性以及記錄異常狀態等功能。
1.5 指令指針寄存器
RIP/EIP寄存器,即指令指針寄存器,有時稱為程序計數器。指令指針(RIP/EIP)寄存器包含當前代碼段中要執行的下一條指令的偏移量。
1.6 MSR寄存器組
MSR(Model-Specific Register)寄存器組可提供性能監測、運行軌跡跟蹤與調試以及其它處理器功能。在使用MSR寄存器組之前,我們應該通過CPUID.01h:EAX[5]來檢測處理器是否支持MSR寄存器組。處理器可以使用RDMSR和WRMSR對MSR寄存器組進行訪問,整個訪問過程藉助ECX寄存器索引寄存器地址,再由EDX:EAX組成的64位寄存器保持訪問值。(在處理器支持64位模式下,RCX、RAX和RDX寄存器的高32位將會被忽略)。
二、指令集
2.1 操作數和指令後綴
2.1.1 操作數
x86指令可以有0到3個操作數,多個操作數之間以逗號(「,」)分隔。對於有兩個操作數的指令,第一個是源操作數,第二個是目的操作數,指令的執行結果保存到第二個操作數表示的寄存器或內存地址中。
源數據值可以以常數形式給出,或是從寄存器或內存中讀取。結果可以存放在寄存器或內存中。因此,操作數被分為三種類型:
- 立即數(immediate),用來表示常數值。立即數通過在整數前加一個「$」來表示。比如,$-577或$0x1F;
- 寄存器(register),它表示某個寄存器的內容,通過在寄存器名聲前加上「%」來表示。比如:%eax或%al;
- 內存引用,它會根據計算出來的地址(通常稱為有效地址)訪問某個內存位置。內存引用的語法:segment:offset(base, index, scale)。
- segment 可以是 x86 架構的任意段寄存器。segment 是可選的,如果指定的話,後面要跟上冒號(「:」)來與offset隔離開;如果未指定指定的話,默認為數據段寄存器--%ds。
- offset 是一個立即數偏移量,是可選的。
- base表示基址寄存器,可以是16個通用寄存器中的任意一個。
- index表示變址寄存器,可以是16個通用寄存器中的任意一個。
- scale表示比例因子,scale會與index相乘再加上base來表示內存地址。比例因子必須是1、2、4、或者8,若果比例因子未指定,默認為1。
- 有效地址被計算為:D[segment]+offset+R[base]+R[index]*scale,其中D[]表示對應段寄存器的數據,R[]表示通用寄存器里的數據。我們用M[addr]來表示內存地址addr。
- 內存地址操作示例:
- 指令說明movl var, %eax把內存地址M[var]處的數據傳送到eax寄存器movl %cs:var, %eax把代碼段偏移量為var處的內存數據傳送到eax寄存器movl $var, %eax把立即數var傳送到eax寄存器movl var(%esi), %eax把內存地址 M[R[%esi] + var] 處的數據傳送到eax寄存器movl (%ebx, %esi, 4), %eax把內存地址 M[R[%ebx] + R[%esi]*4] 處的數據傳送到eax寄存器movl var(%ebx, %esi, 4), %eax把內存地址 M[var+R[%ebx] + R[%esi]*4] 處的數據傳送到eax寄存器
2.1.2 指令後綴
由於是從16位體系結構擴展成32位的,Intel用術語「字(word)」表示16位數據類型。因此,稱32位為「雙字(double words)」,稱64位數為「四字(quad words)」。標準int值存儲為雙字(32位)。指針存儲為8位元組的四字。x86-64中,數據類型long實現為64位。x86-64指令集同樣包括完整的針對字節、字和雙字的指令。
下表給出了C語言基本數據類型對應的x86-64表示。在64位機器中,指針長8位元組。
C聲明 |
Intel數據類型 |
彙編代碼後綴 |
大小(字節) |
char |
字節 |
b |
1 |
short |
字 |
w |
2 |
int |
雙字 |
l |
4 |
long |
四字 |
q |
8 |
char * |
四字(指針) |
q |
8 |
float |
單精度 |
s |
4 |
double |
雙精度 |
l |
8 |
浮點數主要有兩種形式:單精度(4位元組)值,對應於C語言數據類型float;雙精度(8位元組)值,對應於C語言數據類型double。
如上表所示,大多數彙編代碼指令都有一個字符的後綴,表明操作數的大小。例如:數據傳送指令有四個變種:movb(傳送字節)、movw(傳送字)、movl(傳送雙字)和movq(傳送四字)。注意,雖然彙編代碼使用後綴「l」來表示4位元組整數和8位元組浮點數,但並不會產生歧義,因為浮點使用的是一組完全不同的指令和寄存器。
2.2 數據傳送指令
2.2.1 簡單傳送指令
最簡單形式的數據傳送指令是MOV類。這些指令把數據從源位置複製到目的位置,不做任何變化。MOV類由四條指令組成:movb、movw、movl和movq。這些指令都執行同樣的操作,主要區別在於他們操作的數據大小不同:分別是1、2、4和8位元組。下表列出了MOV類指令:
指令 |
效果 |
描述 |
MOV S, D |
S → D |
傳送 |
movb |
|
傳送字節 |
movw |
|
傳送字 |
movl |
|
傳送雙字 |
movq |
|
傳送四字 |
movabsq I, R |
I → R |
傳送絕對的四字 |
源操作數指定的值是一個立即數,存儲在寄存器或者內存中。目的操作數指定一個位置,可以是寄存器或內存地址。x86-64加了一條限制,傳送指令的兩個操作數不能都指向內存位置。大多數情況下,MOV指令只會更新目的操作數指定的那些寄存器字節或內存位置。唯一的例外是movl指令以寄存器作為目的時,它會把該寄存器的高位4位元組設置為0。
movabsq指令是處理64位立即數數據的。常規的movq指令只能以表示為32位補碼數字的立即數作為源操作數,然後把這個值符號擴展得到64位的值,放到目的位置。movabsq指令能夠以任意64位立即數作為源操作數,並且只能以寄存器作為目的。
代碼示例:
movl $0x4050, %eax # 立即數 --> 寄存器,4位元組
movw %bp, %sp # 寄存器 --> 寄存器,4位元組
movb (%rbi, %rcx), %al # 內存 --> 寄存器,1位元組
movb $-17, (%rsp) # 立即數 --> 內存,1位元組
movq %rax, -12(%rap) # 寄存器 --> 內存,4位元組
理解數據傳送如何改變目的寄存器:
1 movabsq $0x0011223344556677, %rax # %rax = 0x0011223344556677
2 movb $-1, %al # %rax = 0x00112233445566FF
3 movw $-1, %ax # %rax = 0x001122334455FFFF
4 movl $-1, %eax # %rax = 0x00000000FFFFFFFF
5 movq $-1, %rax # %rax = 0xFFFFFFFFFFFFFFFF
在這個例子中,第一行的指令把寄存器%rax初始化為位模式 0011223344556677。剩下的指令源操作數是立即數-1。因此 movb 指令把%rax的低位字節設置為 FF,而 movw 指令把低 2 位字節設置為 FFFF,剩下的字節保持不變。movl 指令將低 4 個字節設置為 FFFFFFFF,同時把高位 4 字節設置為 00000000。最後 movq 指令把整個寄存器設置為 FFFFFFFFFFFFFFFF。
2.2.2 擴展傳送指令
MOVZ和MOVS是另外兩類數據移動指令,在將較小的源值複製到較大的目的時使用。MOVZ類中的指令把目的中剩餘的字節填充位0,而MOVS類中的指令通過符號擴展來填充,把源操作數的最高位進行複製。這兩類指令分別如下表所示。
零擴展的傳送指令:
指令 |
效果 |
描述 |
MOVZ S, R |
零擴展(S) → R |
以零擴展進行傳送 |
movzbw |
|
將做了零擴展的字節傳送到字 |
movzbl |
|
將做了零擴展的字節傳送到雙字 |
movzbq |
|
將做了零擴展的字節傳送到四字 |
movzwl |
|
將做了零擴展的字傳送到雙字 |
movzwq |
|
將做了零擴展的字傳送到四字 |
符號擴展的傳送指令:
指令 |
效果 |
描述 |
MOVS S, R |
符號擴展(S) → R |
傳送符號擴展的字節 |
movsbw |
|
將做了符號擴展的字節傳送到字 |
movsbl |
|
將做了符號擴展的字節傳送到雙字 |
movsbq |
|
將做了符號擴展的字節傳送到四字 |
movswl |
|
將做了符號擴展的字傳送到雙字 |
movswq |
|
將做了符號擴展的字傳送到四字 |
位擴展傳送指令:
指令 |
效果 |
描述 |
cbtw |
符號擴展(R[%al]) → R[%ax] |
把%al符號擴展到%ax |
cwtl |
符號擴展(R[%ax]) → R[%eax] |
把%ax符號擴展到%eax |
cwtd |
符號擴展(R[%ax]) → R[%dx]: R[%ax] |
把%ax符號擴展到%dx:%ax |
cltq |
符號擴展(R[%eax]) → R[%rax] |
把%eax符號擴展到%rax |
cltd |
符號擴展(R[%eax]) → R[%edx]: R[%eax] |
把%eax符號擴展到%edx:%eax |
cqto |
符號擴展(R[%rax]) → R[%rdx]: R[%rax] |
把%rax符號擴展為八字 |
cqtd |
符號擴展(R[%rax]) → R[%rdx]: R[%rax] |
把%rax符號擴展為八字 |
字節傳送指令比較:
1 movabsq $0x0011223344556677, %rax # %rax = 0x0011223344556677
2 movb $0xAA, %dl # %dl = 0xAA
3 movb %dl, %al # %rax = 0x00112233445566AA
4 movsbq %dl, %rax # %rax = 0xFFFFFFFFFFFFFFAA
5 movzbq %dl, %rax # %rax = 0x00000000000000AA
代碼的頭 2 行將寄存器 %rax 和 %dl 分別初始化為 0x0011223344556677 和 0xAA。剩下的指令都是將 %rdx的低位字節複製到 %rax的低位字節。movb 指令不改變其它字節。根據源字節的最高位,movsbq 指令將其它 7 個字節設為全 1 或全 0。由於十六進位 A 表示的二進位值為 1010,符號擴展會把高位字節都設置為 FF。movzbq 指令總是將其它 7 個字節全都設置為 0。
2.2.3 壓入和彈出棧數據
棧相關指令如下表所示:
指令 |
效果 |
描述 |
pushq S |
R[%rsp]-8 → R[%rsp]; S → M[R[%rsp]] |
將四字壓入棧 |
popq D |
M[R[%rsp]] → D; R[%rsp] + 8 → R[%rsp] |
將四字彈出棧 |
pushq 指令的功能是把數據壓入到棧上,而 popq 指令是彈出數據。這些指令都只有一個操作數--壓入的數據源和彈出的數據目的。
將一個四字值壓入棧中,首先要將棧指針減 8,然後將值寫到新的棧頂地址。因此,指令 pushq %rbp的行為等價於下面兩條指令:
subq $8, %rsp
movq %rbp, (%rsp)
彈出一個四字的操作包括從棧頂位置讀出數據,然後將棧指針加 8。因此,指令 popq %rax 等價於下面兩條指令:
movq (%rsp), %rax
addq $8, %rsp
2.2.4 加載有效地址
leaq 指令格式如下:
指令 |
效果 |
描述 |
leaq S, D |
&S → D |
加載有效地址 |
加載有效地址(load effective address)指令 leaq 實際上是 movq 指令的變形。它的指令形式是從內存讀數據到寄存器,但實際上它根本沒有引用內存。它的第一個操作數看上去是一個內存引用,但該指令不是從指定的位置讀入數據,而是將有效地址寫入到目的操作數。例如,如果寄存器 %rdx 的值為 x,那麼指令leaq 7(%rdx, %dx, 4), %rax將寄存器 %rax 的值為 5x+7。
2.3 算術和邏輯運算指令
2.3.1 算術運算指令
算術運算指令如下:
指令 |
效果 |
描述 |
inc{bwlq} D |
D+1 → D |
加 1 |
dec{bwlq} D |
D-1 → D |
減 1 |
neg{bwlq} D |
-D → D |
取負 |
add{bwlq} S, D |
D + S → D |
加 |
sub{bwlq} S, D |
D - S → D |
減 |
imul{bwlq} S, D |
D * S → D |
乘 |
2.3.2 邏輯運算指令
邏輯運算指令如下:
指令 |
效果 |
描述 |
not{bwlq} D |
~D → D |
邏輯非 |
or{bwlq} S, D |
D | S → D |
邏輯或 |
and{bwlq} S, D |
D & S → D |
邏輯與 |
xor{bwlq} S, D |
D ^ S → D |
邏輯異或 |
2.3.3 移位運算
指令 |
效果 |
描述 |
sal{bwlq} k, D |
D << k → D |
左移 |
shl{bwlq} k, D |
D << k → D |
左移(等同於asl) |
sar{bwlq} k, D |
D >>_A k → D |
算術右移 |
shr{bwlq} k, D |
D >>_L k → D |
邏輯右移 |
移位操作第一個操作數是移位量,第二個操作數是要移位的數。可以進行算術和邏輯移位。移位量可以是一個8位立即數,或者放在單字節寄存器%cl中。(這些指令很特別,因為只允許以這個特定的寄存器作為操作數。)原則上來說,1個字節的移位量使得移位量的編碼範圍可以達到2^8 - 1 = 255。x86-64中,移位操作對 w 位長的數據值進行操作,移位量是由%cl寄存器的低 m 位決定的,這裡2^m = w,高位會被忽略。所以,例如當寄存器 %cl 的十六進位值為 0xFF 時,指令 salb 會移 7 位,salw 會移 15 位, sall 會移 31 位, 而 salq 會移 63 位。
左移指令有兩個名字:sal 和 shl。兩者的效果是一樣的,都是將右邊填上0.右移指令不同,sar 執行算術移位(填上符號位),而 shr 執行邏輯移位(填上0)。移位操作的目的操作數可以是一個寄存器或者是一個內存位置。
代碼示例:
salq $4, %rax # 把 %rax里的數據左移4位(乘以16),即 %rax = %rax * 16
2.3.4 特殊的算術操作
imul指令有兩種不同的形式。其中一種,如 2.3.1 節所示,有兩個操作數,這種形式的imul指令是一個「雙操作數」乘法指令。但是,x86-64還提供了兩條不同的「單操作數」乘法指令。根據操作數大小,這類指令把源操作數與 %rax 對應大小的寄存器內容相乘,結果放入 %rax 對應的寄存器中。而imulq指令,由於兩個64位數相乘,結果可能為128位,所以使用了%rdx寄存器來保存運算結果的高64位,%rax保存運算結果的低64位。
另外,x86-64提供的除法指令,也是單操作數的。這類操作,根據操作數大小,以 %rdx 對應的寄存器保存餘數, %rax對應的寄存器保存商。其中 idivb 和 idiv是個例外,它們的餘數保存在 %ah 寄存器,商保存在 %al寄存器。
指令 |
效果 |
描述 |
imulb S |
S × R[%al] → R[%ax] |
8位有符號乘法 |
imulw S |
S × R[%ax] → R[%eax] |
16位有符號乘法 |
imull S |
S × R[%eax] → R[%rax] |
32位有符號乘法 |
imulq S |
S × R[%rax] → R[%rdx]: R[%rax] |
64位有符號乘法 |
|
|
|
mulb S |
S × R[%al] → R[%ax] |
8位無符號乘法 |
mulw S |
S × R[%ax] → R[%eax] |
16位無符號乘法 |
mull S |
S × R[%eax] → R[%rax] |
32位無符號乘法 |
mulq S |
S × R[%rax] → R[%rdx]: R[%rax] |
64位無符號乘法 |
|
|
|
idivb S |
R[%ax] mod S → R[%ah] R[%ax] ÷ S → R[%al] |
8位有符號除法 |
idivw S |
R[%dx]: R[%ax] mod S → R[%dx] R[%dx]: R[%ax] ÷ S → R[%ax] |
16位有符號除法 |
idivl S |
R[%edx]: R[%eax] mod S → R[%edx] R[%edx]: R[%eax] ÷ S → R[%eax] |
32位有符號除法 |
idivq S |
R[%rdx]: R[%rax] mod S → R[%rdx] R[%rdx]: R[%rax] ÷ S → R[%rax] |
64位有符號除法 |
|
|
|
divb S |
R[%ax] mod S → R[%ah] R[%ax] ÷ S → R[%al] |
8位無符號除法 |
divw S |
R[%dx]: R[%ax] mod S → R[%dx] R[%dx]: R[%ax] ÷ S → R[%ax] |
16位無符號除法 |
divl S |
R[%edx]: R[%eax] mod S → R[%edx] R[%edx]: R[%eax] ÷ S --> R[%eax] |
32位無符號除法 |
divq S |
R[%rdx]: R[%rax] mod S → R[%rdx] R[%rdx]: R[%rax] ÷ S → R[%rax] |
64位無符號除法 |
2.4 控制指令
2.4.1 條件碼
除了整數寄存器(通用寄存器)外,CPU還維護者一組單個位的條件碼寄存器(EFLAGS),它們描述了最近的算術或邏輯運算操作的屬性。條件碼的詳細描述詳見 1.2 節,這裡我們列出最常用的條件碼:
- CF:進位標誌。最近的操作使最高位產生了進位或借位,用來檢查無符號操作的溢出。
- ZF:零標誌。最近的操作得出的結果是 0。
- SF:符號標誌。最近的操作的得到的結果是否為負數。
- OF:溢出標誌。最近的操作導致一個補碼溢出,用來檢查有符號溢出。
比如說,假設我們用一條 ADD 指令完成等價於 C 表達式 t = a + b 的功能,這裡變量 a、b 和 t 都是整型的。然後,根據下面的C表達式來設置條件碼:
CF (unsigned)t < (unsigned)a # 無符號溢出
ZF (t == 0) # 零
SF (t < 0) # 負數
OF (a<0 == b<0) && (t<0 != a<0) # 有符號溢出
2.3 節列出的所有指令都會設置條件碼。對應邏輯操作,如 XOR,進位標誌和溢出標誌會設置成0。對於移位操作,進位標誌將設置為最後一個被移出的位,而溢出標誌設置為0。INC 和 DEC 指令會設置溢出和零標誌,但是不會改變進位標誌。
條件碼通常不會直接讀取,常用的使用方法有三種:
- 可以根據條件碼的某種組合,將一個字節設置為 0 或者 1;
- 可以條件跳轉到程序的某個地方
- 可以有條件的傳送數據
2.4.2 CMP 和 TEST 指令
除了 2.3 節列出的指令會設置條件碼,還有兩類指令( CMP 和 TEST),它們只設置條件碼而不改變任何其他寄存器。CMP 指令根據兩個操作數之差來設置條件碼。除了只設置條件碼而不更新目的寄存器之外,CMP 指令與 SUB 指令的行為是一樣的。如果兩個操作數相等,這些指令會將零標誌設置為1,而其他標誌可以用來確定兩個操作數之間的大小關係。 TEST 指令的行為與 AND 指令一樣,除了它們只設置條件碼而不改變目的寄存器。
指令 |
基於 |
描述 |
CMP S1, S2 |
S2 - S1 |
比較 |
cmpb |
|
比較字節 |
cmpw |
|
比較字 |
cmpl |
|
比較雙字 |
cmpq |
|
比較四字 |
|
|
|
TEST S1, S2 |
S1 & S2 |
測試 |
testb |
|
測試字節 |
testw |
|
測試字 |
testl |
|
測試雙字 |
testq |
|
測試四字 |
2.4.3 SET指令
SET 指令根據條件碼的某種組合,將一個字節設置為 0 或者 1。SET 指令是一組指令,指令的後綴表明了他們所考慮的條件碼組合。例如:指令 setl 和 setb 表示 「小於時設置(set less)」和「低於時設置(set blow)」。SET 指令的目的操作數是低位單字節寄存器或是一個字節的內存地址,指令會將這個字節設置成 0 或者 1。
SET 指令列表如下:
指令 |
同義指令 |
設置條件 |
條件說明 |
sete D |
setz |
ZF |
相等/零(set equal) |
setne D |
setnz |
~ZF |
不等/非零(set not equal) |
|
|
|
|
sets D |
|
SF |
負數 |
setns D |
|
~SF |
非負數 |
|
|
|
|
setg D |
setnle |
~(SF ^ OF) & ~ZF |
大於(有符號 > ) |
setge D |
setnl |
~(SF ^ OF) |
大於等於(有符號 >= ) |
setl D |
setnge |
SF ^ OF |
小於(有符號小於 < ) |
setle D |
setng |
(SF ^ OF) | ZF |
小於等於(有符號 <= ) |
|
|
|
|
seta D |
setnbe |
~CF & ~ZF |
超過(無符號 > ) |
setae D |
setnb |
~CF |
超過或相等(無符號 >=) |
setb D |
setnae |
CF |
低於(無符號 < ) |
setbe D |
setna |
CF | ZF |
低於或相等(無符號 <=) |
2.4.4 跳轉指令
跳轉指令分為無條件跳轉和有條件跳轉。
2.4.4.1 無條件跳轉
無條件跳轉指令 jmp,可以是直接跳轉,即跳轉目標是作為指令一部分編碼的;也可以是間接跳轉,即跳轉目標是從寄存器或內存位置中讀取的。彙編語言中,直接跳轉是給出一個標號作為跳轉目標的。間接跳轉的寫法是 「*」 後面跟一個操作數指示符。直接跳轉和間接跳轉示例如下:
######################### 直接跳轉示例 ##########################
movq $0, %rax
jmp .L1 # 直接跳轉
movq (%rax), %rdx
.L1:
popq %rdx
######################### 間接跳轉示例 ##########################
jmp *%rax # 用寄存器 %rax 中的值作為跳轉目的
jmp *(%rax) # 以 %rax中的值作為讀取地址,從內存中讀出跳轉目標
2.4.4.2 有條件跳轉
下表中所示的跳轉指令都是有條件的,它們根據條件碼的某種組合,要麼跳轉,要麼不跳轉繼續執行一條指令。條件跳轉只能是直接跳轉。
指令 |
同義指令 |
跳轉條件 |
描述 |
je Label |
jz |
ZF |
相等/零 |
jne Lable |
jnz |
~ZF |
不相等/非零 |
|
|
|
|
js Label |
|
SF |
負數 |
jns Label |
|
~SF |
非負數 |
|
|
|
|
jg Label |
jnle |
~(SF ^ OF) & ~ZF |
大於(有符號 > ) |
jge Label |
jnl |
~(SF ^ OF) |
大於等於(有符號 >= ) |
jl Label |
jnge |
SF ^ OF |
小於(有符號小於 < ) |
jle Label |
jng |
(SF ^ OF) | ZF |
小於等於(有符號 <= ) |
|
|
|
|
ja Label |
jnbe |
~CF & ~ZF |
超過(無符號 > ) |
jae Label |
jnb |
~CF |
超過或相等(無符號 >=) |
jb Label |
jnae |
CF |
低於(無符號 < ) |
jbe Label |
jna |
CF | ZF |
低於或相等(無符號 <=) |
2.4.5 條件傳送指令
指令 |
同義指令 |
傳送條件 |
說明 |
cmove |
cmovz |
ZF |
相等/零 |
cmovne |
cmovnz |
~ZF |
不相等/非零 |
|
|
|
|
cmovs |
|
SF |
負數 |
cmovns |
|
~SF |
非負數 |
|
|
|
|
cmovo |
|
OF |
有溢出 |
cmovno |
|
~OF |
無溢出 |
|
|
|
|
cmovc |
|
CF |
有進位 |
cmovnc |
|
~CF |
無進位 |
|
|
|
|
cmovp |
cmovpe |
PF |
PF = 1 |
cmovnp |
cmovpo |
~PF |
PF != 1 |
|
|
|
|
cmovg |
cmovnle |
~(SF ^ OF) & ~ZF |
大於(有符號 > ) |
cmove |
cmovnl |
~(SF ^ OF) |
大於等於(有符號 >= ) |
cmovl |
cmovnge |
SF ^ OF |
小於(有符號小於 < ) |
cmovle |
cmovng |
(SF ^ OF) | ZF |
小於等於(有符號 <= ) |
|
|
|
|
cmova |
cmovnbe |
~CF & ~ZF |
超過(無符號 > ) |
cmovae |
cmovnb |
~CF |
超過或相等(無符號 >=) |
cmovb |
cmovnae |
CF |
低於(無符號 < ) |
cmovbe |
cmovna |
CF | ZF |
低於或相等(無符號 <=) |
2.5 過程調用
x86-64的過程實現包括一組特殊的指令和一些對機器資源(例如寄存器和程序內存)使用規則的約定。
2.5.1 運行時棧
x86-64的棧向低地址方向增長,而棧指針 %rsp 指向棧頂元素。可以用 push 和 pop 相關指令將數據存入棧中或是從棧中取出數據。將棧指針減小一個適當的量可以為數據在棧上分配空間;類似的,可以通過增加棧指針來釋放空間。
當過程 P 調用過程 Q 時,其棧內結構如下圖所示:
當前正在執行的過程的棧幀總是在棧頂。當 過程 P 調用過程 Q 時,會把返回地址壓入棧中,指明當 Q 返回時,要從 P 程序的哪個位置繼續執行。Q 的代碼會擴展到當前棧的邊界,分配它所需要的棧幀空間。在這個空間中,它可以保存寄存器的值,分配局部變量空間,為它調用的過程設置參數。通過寄存器,過程 P 可以傳遞最多 6 個整數值(包括指針和整數),如果 Q 需要更過參數時,P 可以在調用 Q 之前在自己的棧幀里存儲好這些參數。
2.5.2 過程調用慣例
2.5.2.1 參數傳遞
x86-64中,最多允許 6 個參數通過寄存器來傳遞,多出的參數需要通過棧來傳遞,正如 2.5.1 節描述的那樣;傳遞參數時,參數的順序與寄存器的關係對應如下:
操作數大小(位) |
參數1 |
參數2 |
參數3 |
參數4 |
參數5 |
參數6 |
64 |
%rdi |
%rsi |
%rdx |
%rcx |
%r8 |
%r9 |
32 |
%edi |
%esi |
%edx |
%ecx |
%r8d |
%r9d |
16 |
%di |
%si |
%dx |
%cx |
%r8w |
%r9w |
8 |
%dil |
%sil |
%dl |
%cl |
%r8b |
%r9b |
如果一個函數 Q 有 n (n > 6)個整數參數,如果過程 P 調用過程 Q,需要把參數 1 ~ 6複製到對應的寄存器,把參數 7 ~ n放到棧上,而參數 7 位於棧頂。通過棧傳遞參數時,所有的數據大小都向 8 的倍數對齊。參數到位以後,程序就可以指向 call 指令將控制轉移到 Q 了。
2.5.2.2 返回值
被調用函數返回時,把返回結果放入 %rax中,供調用函數來獲取。
2.5.2.3 寄存器的其它約定
根據慣例,寄存器的 %rbx、%rbp和 %r12~%r15被劃分位被調用者保存寄存器。當過程 P 調用過程 Q 時,Q 必須保證這些寄存器的值在被調用前和返回時是一致的。也就是說, Q 要麼不去使用它,要麼先把寄存器原始值壓入棧,在使用完成後,返回到 P 之前再把這些寄存器的值從棧中恢復。
所有其它寄存器,除了棧指針 %rsp,都分類為調用者保存寄存器。這就意味著任何函數都能修改它們。可以這樣來理解「調用者保存」這個名字:過程 P 在某個此類寄存器中存有數據,然後調用過程 Q。因為 Q 可以隨意修改這個寄存器,所以在調用之前首先保存好這個數據時 P (調用者)的責任。
2.5.3 控制轉移
過程調用時,通過一下指令來進行調用及返回:
指令 |
描述 |
call Label |
過程調用 |
call *Operand |
過程調用 |
ret |
從過程調用返回 |
call 指令有一個目標,即指明被調用過程起始的指令地址。同跳轉指令一樣,調用可以是直接的,也可以是間接的。
當 call 指令執行時,調用者 P 已經按 2.5.2 的約定,把被調用者 Q 所需要的參數準備好了。該指令執行時,會把返回地址 A 壓入棧中,並將PC (%rip)設置為 Q 的起始地址。對應的,ret 指令會從棧中彈出返回地址 A,並把PC(%rip)設置為 A,程序從 A 處繼續執行。
2.6 字符串指令
字符串指令用於對字符串進行操作,這些操作包括在把字符串存入內存、從內存中加載字符串、比較字符串以及掃描字符串以查找子字符串。
2.6.1 movs、cmps類指令
movs 指令用於把字符串從內存中的一個位置拷貝到另一個位置。cmps 指令用於字符串比較。
在老的運行模式中,這些指令把字符串從 %ds: %(e)si 表示的內存地址拷貝到 %es: %(e)di 表示的內存地址。在64位模式中,這些指令把字符串從 %(r|e)si 表示的內存地址處拷貝到 %(r|e)di 表示的內存地址處。
當操作完成後, %(r|e)si 和 %(r|e)di 寄存器的值會根據 DF 標誌位的值自動增加或減少。當 DF 位為 0 時,%(r|e)si 和 %(r|e)di 寄存器的值會增加,當 DF 為 1 時,寄存器的值會減少。根據移動的字符串是字節、字、雙字、四字,寄存器會分別減少或增加1、2、4、8。
movs類指令:
指令 |
描述 |
movsb |
move byte string |
movsw |
move word string |
movsl |
move doubleword string |
movsq |
move qword string |
cmps類指令:
指令 |
描述 |
cmpsb |
compare byte string |
cmpsw |
compare word string |
cmpsl |
compare doubleword string |
cmpsq |
compare qword string |
2.6.2 lods指令
lods 指令把源操作數加載到 %al,%ax,%eax 或 %rax 寄存器,源操作數是一個內存地址。在老的模式下,這個地址會從 %ds:%esi 或者 %ds:%si讀取(根據操作數地址屬性是32還是16來決定使用不同的寄存器);在64位模式下內存地址從寄存器 %(r)si 處讀取。
在數據加載完成後,%(r|e)si 寄存器會根據 DF 標誌位自動增加或減少(如果 DF 為0,%(r|e)si 寄存器會增加;如果DF 為 1,%(r|e)si 寄存器會減少 )。根據移動的字符串是字節、字、雙字、四字,寄存器會分別減少或增加1、2、4、8。
指令 |
描述 |
說明 |
lodsb |
load byte string |
For legacy mode, Load byte at address DS:(E)SI into AL. For 64-bit mode load byte at address (R)SI into AL. |
lodsw |
load word string |
For legacy mode, Load word at address DS:(E)SI into AX. For 64-bit mode load word at address (R)SI into AX. |
lodsl |
load doubleword string |
For legacy mode, Load dword at address DS:(E)SI into EAX. For 64-bit mode load dword at address (R)SI into EAX. |
lodsq |
load qword string |
Load qword at address (R)SI into RAX. |
2.6.3 stos 指令
stos 指令把 %al,%ax,%eax 或 %rax 寄存器里的字節、字、雙字、四字數據,保存到目的操作數,目的操作數是一個內存地址。在老的模式下,這個地址會從 %ds:%esi 或者 %ds:%si讀取(根據操作數地址屬性是32還是16來決定使用不同的寄存器);在64位模式下內存地址從寄存器 %rdi 或 %edi 處讀取。
在數據加載完成後,%(r|e)di 寄存器會根據 DF 標誌位自動增加或減少(如果 DF 為0,寄存器會增加;如果DF 為 1, 寄存器會減少 )。根據移動的字符串是字節、字、雙字、四字,寄存器會分別減少或增加1、2、4、8。
指令 |
描述 |
說明 |
stosb |
store byte string |
For legacy mode, store AL at address ES:(E)DI; For 64-bit mode store AL at address RDI or EDI. |
stosw |
store word string |
For legacy mode, store AX at address ES:(E)DI; For 64-bit mode store AX at address RDI or EDI. |
stosl |
store dowble word string |
For legacy mode, store EAX at address ES:(E)DI; For 64-bit mode store EAX at address RDI or EDI. |
stosq |
store qword string |
Store RAX at address RDI or EDI. |
2.6.4 REP相關指令
上面幾節提到的字符串相關指令,都是單次執行的指令。可以在這些指令前面添加 rep 類前綴,讓指令重複執行。重複執行的次數通過計數寄存器-- %(r|e)cx 來指定,或者根據ZF 標誌位是否滿足條件進行判斷。
rep 類前綴包括: rep(repeat),repe(repeat while qeual),repne(repeat while not qeual),repz(repeat while zero) 和 repnz(repeat while not zero)。 rep 前綴可以放置在 ins,outs,movs,lods和stos指令前面;而repe、repne、repz和repnz前綴可以放置在 cmps 和 scas指令前面。
repe、repne、repz和repnz 前綴指令在每一次疊代執行後,會檢查 ZF 標誌是否滿足中止條件,如果滿足則中止循環。
中止條件:
前綴格式 |
中止條件1 |
中止條件2 |
rep |
%rcx 或 %(e)cx = 0 |
無 |
repe/repz |
%rcx 或 %(e)cx = 0 |
ZF = 0 |
repne/repnz |
%rcx 或 %(e)cx = 0 |
ZF = 1 |
三、彙編器指令
這部分指令是跟x86指令集無關的指令,這些指令以"."開頭。
3.1 段相關指令
指令 |
描述 |
.text |
代碼段 |
.rodata |
只讀數據段 |
.data |
數據段 |
.bss |
未初始化數據段。bss段用於本地通用變量存儲。您可以在bss段分配地址空間,但在程序執行之前,您不能指定要加載到其中的數據。當程序開始運行時,bss段的所有內容都被初始化為零。 |
3.2 數據相關指令
數據相關指令在.bss段無效。
指令 |
描述 |
.ascii |
文本字符串,末尾不會自動添加『『\0』字符 |
.asciz |
除了會在字符串末尾自動添加『\0』字符外,同.ascii |
.byte |
8位整數 |
.short |
16位整數 |
.int |
32位整數 |
.long |
32位整數 (same as .int) |
.quad |
8位元組整數 |
.octa |
16位元組整數 |
.single |
單精度浮點數 |
.float |
單精度浮點數 |
double |
雙精度浮點數 |
代碼示例:
.byte 74, 0112, 092, 0x4A, 0X4a, 'J, '\J # All the same value.
.ascii "Ring the bell\7" # A string constant.
.octa 0x123456789abcdef0123456789ABCDEF0 # A bignum.
.float 0f-314159265358979323846264338327\
95028841971.693993751E-40 # - pi, a flonum.
四、參考資料
1、《深入理解計算機系統》(第三版)
2、《作業系統真象還原》
3、《一個64位作業系統的設計與實現》
5、《x86彙編語言:從實模式到保護模式》
4、gnu as 文檔:https://sourceware.org/binutils/docs/as/index.html#SEC_Contents
5、Intel 64和IA-32架構軟體開發者手冊:https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
6、x86 Assembly Language Reference Manual(AT&T syntax):https://docs.oracle.com/cd/E19253-01/817-5477/817-5477.pdf