x86的指令集可分為以下4種:
- 通用指令
- x87 FPU指令,浮點數運算的指令
- SIMD指令,就是SSE指令
- 系統指令,寫OS核心時使用的特殊指令
下面介紹一些通用的指令。指令由標識命令種類的助記符(mnemonic)和作為引數的運算元(operand)組成。例如move指令:
指令 | 運算元 | 描述 |
movq | I/R/M,R/M | 從一個記憶體位置複製1個雙字(64位,8位元組)大小的資料到另外一個記憶體位置 |
movl | I/R/M,R/M | 從一個記憶體位置複製1個字(32位,4位元組)大小的資料到另外一個記憶體位置 |
movw | I/R/M, R/M | 從一個記憶體位置複製2個位元組(16位)大小的資料到另外一個記憶體位置 |
movb | I/R/M, R/M | 從一個記憶體位置複製1個位元組(8位)大小的資料到另外一個記憶體位置 |
movl為助記符。助記符有後綴,如movl中的字尾l表示作為運算元的物件的資料大小。l為long的縮寫,表示32位的大小,除此之外,還有b、w,q分別表示8位、16位和64位的大小。
指令的運算元如果不止1個,就將每個運算元以逗號分隔。每個運算元都會指明是否可以是立即模式值(I)、暫存器(R)或記憶體地址(M)。
另外還要提示一下,在x86的組合語言中,採用記憶體位置的運算元最多隻能出現一個,例如不可能出現mov M,M指令。
通用暫存器中每個操作都可以有一個字元的字尾,表明運算元的大小,如下表所示。
C宣告 | 通用暫存器字尾 | 大小(位元組) |
char | b | 1 |
short | w | 2 |
(unsigned) int / long / char* | l | 4 |
float | s | 4 |
double | l | 5 |
long double | t | 10/12 |
注意:通用暫存器使用字尾“l”同時表示4位元組整數和8位元組雙精度浮點數,這不會產生歧義,因為浮點數使用的是完全不同的指令和暫存器。
我們後面只介紹call、push等指令時,如果在研究HotSpot VM虛擬機器的彙編遇到了callq,pushq等指令時,千萬別不認識,字尾就是表示了運算元的大小。
下表為運算元的格式和定址模式。
格式 |
運算元值 |
名稱 |
樣例(通用暫存器 = C語言) |
$Imm |
Imm |
立即數定址 |
$1 = 1 |
Ea |
R[Ea] |
暫存器定址 |
%eax = eax |
Imm |
M[Imm] |
絕對定址 |
0x104 = *0x104 |
(Ea) |
M[R[Ea]] |
間接定址 |
(%eax)= *eax |
Imm(Ea) |
M[Imm+R[Ea]] |
(基址+偏移量)定址 |
4(%eax) = *(4+eax) |
(Ea,Eb) |
M[R[Ea]+R[Eb]] |
變址 |
(%eax,%ebx) = *(eax+ebx) |
Imm(Ea,Eb) |
M[Imm+R[Ea]+R[Eb]] |
定址 |
9(%eax,%ebx)= *(9+eax+ebx) |
(,Ea,s) |
M[R[Ea]*s] |
伸縮化變址定址 |
(,%eax,4)= *(eax*4) |
Imm(,Ea,s) |
M[Imm+R[Ea]*s] |
伸縮化變址定址 |
0xfc(,%eax,4)= *(0xfc+eax*4) |
(Ea,Eb,s) |
M(R[Ea]+R[Eb]*s) |
伸縮化變址定址 |
(%eax,%ebx,4) = *(eax+ebx*4) |
Imm(Ea,Eb,s) |
M(Imm+R[Ea]+R[Eb]*s) |
伸縮化變址定址 |
8(%eax,%ebx,4) = *(8+eax+ebx*4) |
注:M[xx]表示在儲存器中xx地址的值,R[xx]表示暫存器xx的值,這種表示方法將暫存器、記憶體都看出一個大陣列的形式。
彙編根據編譯器的不同,有2種書寫格式:
(1)Intel : Windows派系
(2)AT&T: Unix派系
下面簡單介紹一下兩者的不同。
下面就來認識一下常用的指令。
下面我們以給出的是AT&T彙編的寫法,這兩種寫法有如下不同。
1、資料傳送指令
將資料從一個地方傳送到另外一個地方。
1.1 mov指令
我們在介紹mov指令時介紹的全一些,因為mov指令是出現頻率最高的指令,助記符中的字尾也比較多。
mov指令的形式有3種,如下:
mov #普通的move指令
movs #符號擴充套件的move指令,將源運算元進行符號擴充套件並傳送到一個64位暫存器或儲存單元中。movs就表示符號擴充套件
movz #零擴充套件的move指令,將源運算元進行零擴充套件後傳送到一個64位暫存器或儲存單元中。movz就表示零擴充套件
mov指令後有一個字母可表示運算元大小,形式如下:
movb #完成1個位元組的複製
movw #完成2個位元組的複製
movl #完成4個位元組的複製
movq #完成8個位元組的複製
還有一個指令,如下:
movabsq I,R
與movq有所不同,它是將一個64位的值直接存到一個64位暫存器中。
movs指令的形式如下:
movsbw #作符號擴充套件的1位元組複製到2位元組
movsbl #作符號擴充套件的1位元組複製到4位元組
movsbq #作符號擴充套件的1位元組複製到8位元組
movswl #作符號擴充套件的2位元組複製到4位元組
movswq #作符號擴充套件的2位元組複製到8位元組
movslq #作符號擴充套件的4位元組複製到8位元組
movz指令的形式如下:
movzbw #作0擴充套件的1位元組複製到2位元組
movzbl #作0擴充套件的1位元組複製到4位元組
movzbq #作0擴充套件的1位元組複製到8位元組
movzwl #作0擴充套件的2位元組複製到4位元組
movzwq #作0擴充套件的2位元組複製到8位元組
movzlq #作0擴充套件的4位元組複製到8位元組
舉個例子如下:
movl %ecx,%eax
movl (%ecx),%eax
第一條指令將暫存器ecx中的值複製到eax暫存器;第二條指令將ecx暫存器中的資料作為地址訪問記憶體,並將記憶體上的資料載入到eax暫存器中。
1.2 cmov指令
cmov指令的格式如下:
cmovxx
其中xx代表一個或者多個字母,這些字母表示將觸發傳送操作的條件。條件取決於 EFLAGS 暫存器的當前值。
eflags暫存器中各個們如下圖所示。
其中與cmove指令相關的eflags暫存器中的位有CF(數學表示式產生了進位或者借位) 、OF(整數值無窮大或者過小)、PF(暫存器包含數學操作造成的錯誤資料)、SF(結果為正不是負)和ZF(結果為零)。
下表為無符號條件傳送指令。
指令對 | 描述 | eflags狀態 |
cmova/cmovnbe | 大於/不小於或等於 | (CF或ZF)=0 |
cmovae/cmovnb | 大於或者等於/不小於 | CF=0 |
cmovnc | 無進位 | CF=0 |
cmovb/cmovnae | 大於/不小於或等於 | CF=1 |
cmovc | 進位 | CF=1 |
cmovbe/cmovna | 小於或者等於/不大於 | (CF或ZF)=1 |
cmove/cmovz | 等於/零 | ZF=1 |
cmovne/cmovnz | 不等於/不為零 | ZF=0 |
cmovp/cmovpe | 奇偶校驗/偶校驗 | PF=1 |
cmovnp/cmovpo | 非奇偶校驗/奇校驗 | PF=0 |
無符號條件傳送指令依靠進位、零和奇偶校驗標誌來確定兩個運算元之間的區別。
下表為有符號條件傳送指令。
指令對 |
描述 |
eflags狀態 |
cmovge/cmovnl |
大於或者等於/不小於 |
(SF異或OF)=0 |
cmovl/cmovnge |
大於/不大於或者等於 |
(SF異或OF)=1 |
cmovle/cmovng |
小於或者等於/不大於 |
((SF異或OF)或ZF)=1 |
cmovo |
溢位 |
OF=1 |
cmovno |
未溢位 |
OF=0 |
cmovs |
帶符號(負) |
SF=1 |
cmovns |
無符號(非負) |
SF=0 |
舉個例子如下:
// 將vlaue數值載入到ecx暫存器中
movl value,%ecx
// 使用cmp指令比較ecx和ebx這兩個暫存器中的值,具體就是用ecx減去ebx然後設定eflags
cmp %ebx,%ecx
// 如果ecx的值大於ebx,使用cmova指令設定ebx的值為ecx中的值
cmova %ecx,%ebx
注意AT&T彙編的第1個運算元在前,第2個運算元在後。
1.3 push和pop指令
push指令的形式如下表所示。
指令 |
運算元 |
描述 |
push |
I/R/M |
PUSH 指令首先減少 ESP 的值,再將源運算元複製到堆疊。運算元是 16 位的, 則 ESP 減 2,運算元是 32 位的,則 ESP 減 4 |
pusha |
指令按序(AX、CX、DX、BX、SP、BP、SI 和 DI)將 16 位通用暫存器壓入堆疊。 |
|
pushad |
指令按照 EAX、ECX、EDX、EBX、ESP(執行 PUSHAD 之前的值)、 EBP、ESI 和 EDI 的順序,將所有 32 位通用暫存器壓入堆疊。 |
pop指令的形式如下表所示。
指令 |
運算元 |
描述 |
pop |
R/M |
指令首先把 ESP 指向的堆疊元素內容複製到一個 16 位或 32 位目的運算元中,再增加 ESP 的值。 如果運算元是 16 位的,ESP 加 2,如果運算元是 32 位的,ESP 加 4 |
popa |
指令按照相反順序將同樣的暫存器彈出堆疊 |
|
popad |
指令按照相反順序將同樣的暫存器彈出堆疊 |
1.4 xchg與xchgl
這個指令用於交換運算元的值,交換指令XCHG是兩個暫存器,暫存器和記憶體變數之間內容的交換指令,兩個運算元的資料型別要相同,可以是一個位元組,也可以是一個字,也可以是雙字。格式如下:
xchg R/M,R/M
xchgl I/R,I/R、
兩個運算元不能同時為記憶體變數。xchgl指令是一條古老的x86指令,作用是交換兩個暫存器或者記憶體地址裡的4位元組值,兩個值不能都是記憶體地址,他不會設定條件碼。
1.5 lea
lea計算源運算元的實際地址,並把結果儲存到目標運算元,而目標運算元必須為通用暫存器。格式如下:
lea M,R
lea(Load Effective Address)指令將地址載入到暫存器。
舉例如下:
movl 4(%ebx),%eax
leal 4(%ebx),%eax
第一條指令表示將ebx暫存器中儲存的值加4後得到的結果作為記憶體地址進行訪問,並將記憶體地址中儲存的資料載入到eax暫存器中。
第二條指令表示將ebx暫存器中儲存的值加4後得到的結果作為記憶體地址存放到eax暫存器中。
再舉個例子,如下:
leaq a(b, c, d), %rax
計算地址a + b + c * d,然後把最終地址載到暫存器rax中。可以看到只是簡單的計算,不引用源運算元裡的暫存器。這樣的完全可以把它當作乘法指令使用。
2、算術運算指令
下面介紹對有符號整數和無符號整數進行操作的基本運算指令。
2.1 add與adc指令
指令的格式如下:
add I/R/M,R/M
adc I/R/M,R/M
指令將兩個運算元相加,結果儲存在第2個運算元中。
對於第1條指令來說,由於暫存器和儲存器都有位寬限制,因此在進行加法運算時就有可能發生溢位。運算如果溢位的話,標誌暫存器eflags中的進位標誌(Carry Flag,CF)就會被置為1。
對於第2條指令來說,利用adc指令再加上進位標誌eflags.CF,就能在32位的機器上進行64位資料的加法運算。
常規的算術邏輯運算指令只要將原來IA-32中的指令擴充套件到64位即可。如addq就是四字相加。
2.2 sub與sbb指令
指令的格式如下:
sub I/R/M,R/M
sbb I/R/M,R/M
指令將用第2個運算元減去第1個運算元,結果儲存在第2個運算元中。
2.3 imul與mul指令
指令的格式如下:
imul I/R/M,R
mul I/R/M,R
將第1個運算元和第2個運算元相乘,並將結果寫入第2個運算元中,如果第2個運算元空缺,預設為eax暫存器,最終完整的結果將儲存到edx:eax中。
第1條指令執行有符號乘法,第2條指令執行無符號乘法。
2.4 idiv與div指令
指令的格式如下:
div R/M
idiv R/M
第1條指令執行無符號除法,第2條指令執行有符號除法。被除數由edx暫存器和eax暫存器拼接而成,除數由指令的第1個運算元指定,計算得到的商存入eax暫存器,餘數存入edx暫存器。如下圖所示。
edx:eax
------------ = eax(商)... edx(餘數)
暫存器
運算時被除數、商和除數的資料的位寬是不一樣的,如下表表示了idiv指令和div指令使用的暫存器的情況。
資料的位寬 | 被除數 | 除數 | 商 |
餘數 |
8位 | ax | 指令第1個運算元 | al | ah |
16位 | dx:ax | 指令第1個運算元 | ax | dx |
32位 | edx:eax | 指令第1個運算元 | eax | edx |
idiv指令和div指令通常是對位寬2倍於除數的被除數進行除法運算的。例如對於x86-32機器來說,通用暫存器的倍數為32位,1個暫存器無法容納64位的資料,所以 edx存放被除數的高32位,而eax暫存器存放被除數的低32位。
所以在進行除法運算時,必須將設定在eax暫存器中的32位資料擴充套件到包含edx暫存器在內的64位,即有符號進行符號擴充套件,無符號數進行零擴充套件。
對edx進行符號擴充套件時可以使用cltd(AT&T風格寫法)或cdq(Intel風格寫法)。指令的格式如下:
cltd // 將eax暫存器中的資料符號擴充套件到edx:eax
cltd將eax暫存器中的資料符號擴充套件到edx:eax。
2.5 incl與decl指令
指令的格式如下:
inc R/M
dec R/M
將指令第1個運算元指定的暫存器或記憶體位置儲存的資料加1或減1。
2.6 negl指令
指令的格式如下:
neg R/M
neg指令將第1個運算元的符號進行反轉。
3、位運算指令
3.1 andl、orl與xorl指令
指令的格式如下:
and I/R/M,R/M
or I/R/M,R/M
xor I/R/M,R/M
and指令將第2個運算元與第1個運算元進行按位與運算,並將結果寫入第2個運算元;
or指令將第2個運算元與第1個運算元進行按位或運算,並將結果寫入第2個運算元;
xor指令將第2個運算元與第1個運算元進行按位異或運算,並將結果寫入第2個運算元;
3.2 not指令
指令的格式如下:
not R/M
將運算元按位取反,並將結果寫入運算元中。
3.3 sal、sar、shr指令
指令的格式如下:
sal I/%cl,R/M #算術左移
sar I/%cl,R/M #算術右移
shl I/%cl,R/M #邏輯左移
shr I/%cl,R/M #邏輯右移
sal指令將第2個運算元按照第1個運算元指定的位數進行左移操作,並將結果寫入第2個運算元中。移位之後空出的低位補0。指令的第1個運算元只能是8位的立即數或cl暫存器,並且都是隻有低5位的資料才有意義,高於或等於6位數將導致暫存器中的所有資料被移走而變得沒有意義。
sar指令將第2個運算元按照第1個運算元指定的位數進行右移操作,並將結果寫入第2個運算元中。移位之後的空出進行符號擴充套件。和sal指令一樣,sar指令的第1個運算元也必須為8位的立即數或cl暫存器,並且都是隻有低5位的資料才有意義。
shl指令和sall指令的動作完全相同,沒有必要區分。
shr令將第2個運算元按照第1個運算元指定的位數進行右移操作,並將結果寫入第2個運算元中。移位之後的空出進行零擴充套件。和sal指令一樣,shr指令的第1個運算元也必須為8位的立即數或cl暫存器,並且都是隻有低5位的資料才有意義。
4、流程控制指令
4.1 jmp指令
指令的格式如下:
jmp I/R
jmp指令將程式無條件跳轉到運算元指定的目的地址。jmp指令可以視作設定指令指標(eip暫存器)的指令。目的地址也可以是星號後跟暫存器的棧,這種方式為間接函式呼叫。例如:
jmp *%eax
將程式跳轉至eax所含地址。
4.2 條件跳轉指令
條件跳轉指令的格式如下:
Jcc 目的地址
其中cc指跳轉條件,如果為真,則程式跳轉到目的地址;否則執行下一條指令。相關的條件跳轉指令如下表所示。
指令 |
跳轉條件 |
描述 |
指令 |
跳轉條件 |
描述 |
jz |
ZF=1 |
為0時跳轉 |
jbe |
CF=1或ZF=1 |
大於或等於時跳轉 |
jnz |
ZF=0 |
不為0時跳轉 |
jnbe |
CF=0且ZF=0 |
小於或等於時跳轉 |
je |
ZF=1 |
相等時跳轉 |
jg |
ZF=0且SF=OF |
大於時跳轉 |
jne |
ZF=0 |
不相等時跳轉 |
jng |
ZF=1或SF!=OF |
不大於時跳轉 |
ja |
CF=0且ZF=0 |
大於時跳轉 |
jge |
SF=OF |
大於或等於時跳轉 |
jna |
CF=1或ZF=1 |
不大於時跳轉 |
jnge |
SF!=OF |
小於或等於時跳轉 |
jae |
CF=0 |
大於或等於時跳轉 |
jl |
SF!=OF |
小於時跳轉 |
jnae |
CF=1 |
小於或等於時跳轉 |
jnl |
SF=OF |
不小於時跳轉 |
jb |
CF=1 |
大於時跳轉 |
jle |
ZF=1或SF!=OF |
小於或等於時跳轉 |
jnb |
CF=0 |
不大於時跳轉 |
jnle |
ZF=0且SF=OF |
大於或等於時跳轉 |
4.3 cmp指令
cmp指令的格式如下:
cmp I/R/M,R/M
cmp指令通過比較第2個運算元減去第1個運算元的差,根據結果設定標誌暫存器eflags中的標誌位。cmp指令和sub指令類似,不過cmp指令不會改變運算元的值。
運算元和所設定的標誌位之間的關係如表所示。
運算元的關係 | CF | ZF | OF |
第1個運算元小於第2個運算元 | 0 | 0 | SF |
第1個運算元等於第2個運算元 | 0 | 1 | 0 |
第1個運算元大於第2個運算元 | 1 | 0 | not SF |
4.4 test指令
指令的格式如下:
test I/R/M,R/M
指令通過比較第1個運算元與第2個運算元的邏輯與,根據結果設定標誌暫存器eflags中的標誌位。test指令本質上和and指令相同,只是test指令不會改變運算元的值。
test指令執行後CF與OF通常會被清零,並根據運算結果設定ZF和SF。運算結果為零時ZF被置為1,SF和最高位的值相同。
舉個例子如下:
test指令同時能夠檢查幾個位。假設想要知道 AL 暫存器的位 0 和位 3 是否置 1,可以使用如下指令:
test al,00001001b #掩碼為0000 1001,測試第0和位3位是否為1
從下面的資料集例子中,可以推斷只有當所有測試位都清 0 時,零標誌位才置 1:
0 0 1 0 0 1 0 1 <- 輸入值
0 0 0 0 1 0 0 1 <- 測試值
0 0 0 0 0 0 0 1 <- 結果:ZF=0 0 0 1 0 0 1 0 0 <- 輸入值
0 0 0 0 1 0 0 1 <- 測試值
0 0 0 0 0 0 0 0 <- 結果:ZF=1
test指令總是清除溢位和進位標誌位,其修改符號標誌位、零標誌位和奇偶標誌位的方法與 AND 指令相同。
4.5 sete指令
根據eflags中的狀態標誌(CF,SF,OF,ZF和PF)將目標運算元設定為0或1。這裡的目標運算元指向一個位元組暫存器(也就是8位暫存器,如AL,BL,CL)或記憶體中的一個位元組。狀態碼字尾(cc)指明瞭將要測試的條件。
獲取標誌位的指令的格式如下:
setcc R/M
指令根據標誌暫存器eflags的值,將運算元設定為0或1。
setcc中的cc和Jcc中的cc類似,可參考表。
4.6 call指令
指令的格式如下:
call I/R/M
call指令會呼叫由運算元指定的函式。call指令會將指令的下一條指令的地址壓棧,再跳轉到運算元指定的地址,這樣函式就能通過跳轉到棧上的地址從子函式返回了。相當於
push %eip
jmp addr
先壓入指令的下一個地址,然後跳轉到目標地址addr。
4.7 ret指令
指令的格式如下:
ret
ret指令用於從子函式中返回。X86架構的Linux中是將函式的返回值設定到eax暫存器並返回的。相當於如下指令:
popl %eip
將call指令壓棧的“call指令下一條指令的地址”彈出棧,並設定到指令指標中。這樣程式就能正確地返回子函式的地方。
從物理上來說,CALL 指令將其返回地址壓入堆疊,再把被呼叫過程的地址複製到指令指標暫存器。當過程準備返回時,它的 RET 指令從堆疊把返回地址彈回到指令指標暫存器。
4.8 enter指令
enter指令通過初始化ebp和esp暫存器來為函式建立函式引數和區域性變數所需要的棧幀。相當於
push %rbp
mov %rsp,%rbp
4.9 leave指令
leave通過恢復ebp與esp暫存器來移除使用enter指令建立的棧幀。相當於
mov %rbp, %rsp
pop %rbp
將棧指標指向幀指標,然後pop備份的原幀指標到%ebp
5.0 int指令
指令的格式如下:
int I
引起給定數字的中斷。這通常用於系統呼叫以及其他核心介面。
5、標誌操作
eflags暫存器的各個標誌位如下圖所示。
操作eflags暫存器標誌的一些指令如下表所示。
指令 | 運算元 | 描述 |
pushfd | R | PUSHFD 指令把 32 位 EFLAGS 暫存器內容壓入堆疊 |
popfd | R | POPFD 指令則把棧頂單元內容彈出到 EFLAGS 暫存器 |
cld | 將eflags.df設定為0 |
推薦閱讀:
第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法
第13篇-通過InterpreterCodelet儲存機器指令片段
如果有問題可直接評論留言或加作者微信mazhimazh
關注公眾號,有HotSpot VM原始碼剖析系列文章!