第18章-x86指令集之常用指令


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源碼剖析系列文章!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM