x86的指令集可分為以下4種:

  1. 通用指令
  2. x87 FPU指令,浮點數運算的指令
  3. SIMD指令,就是SSE指令
  4. 系統指令,寫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 

推薦閱讀:

第1篇-關於JVM執行時,開篇說的簡單些

第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法

第3篇-CallStub新棧幀的建立

第4篇-JVM終於開始呼叫Java主類的main()方法啦

第5篇-呼叫Java方法後彈出棧幀及處理返回結果

第6篇-Java方法新棧幀的建立

第7篇-為Java方法建立棧幀

第8篇-dispatch_next()函式分派位元組碼

第9篇-位元組碼指令的定義

第10篇-初始化模板表

第11篇-認識Stub與StubQueue

第12篇-認識CodeletMark

第13篇-通過InterpreterCodelet儲存機器指令片段

第14篇-生成重要的例程

第15章-直譯器及直譯器生成器

第16章-虛擬機器中的彙編器

第17章-x86-64暫存器

如果有問題可直接評論留言或加作者微信mazhimazh

關注公眾號,有HotSpot VM原始碼剖析系列文章!