x86彙編語法基礎(gnu格式)

碼農之心 發佈 2024-04-28T13:25:49.157479+00:00

下圖顯示了這16個寄存器。它們的名字都以%r開頭,不過後面還跟著不同命名規則的名字,這是由於指令集歷史演化造成的。

一、寄存器

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

關鍵字: