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