程序的機(jī)器級(jí)表示

計(jì)算機(jī)執(zhí)行機(jī)器代碼,用字節(jié)序列編碼低級(jí)的操作,包括處理數(shù)據(jù)、管理存儲(chǔ)器、讀寫(xiě)存儲(chǔ)設(shè)備上的數(shù)據(jù),以及利用網(wǎng)絡(luò)通信。編譯器基于變成語(yǔ)言的原則、目標(biāo)機(jī)器的指令集和操作系統(tǒng)遵循的規(guī)則,經(jīng)過(guò)一系列的階段產(chǎn)生機(jī)器代碼。GCC C語(yǔ)言編譯器以匯編代碼的形式產(chǎn)生輸出,匯編代碼是機(jī)器代碼的文本表示,給出程序中的每一條指令。然后GCC調(diào)用匯編器和鏈接器,從而根據(jù)匯編代碼生成可執(zhí)行的機(jī)器代碼。

Intel系列有好幾個(gè)名字,包括IA32,也就是“Intel 32 位體系結(jié)構(gòu)”(Intel Architecure 32-bit),以及后來(lái)的Intel64,即IA32的64位擴(kuò)展,也稱為x86-64,最常用的名字是“x86”,用它指代整個(gè)系列。

計(jì)算是機(jī)系統(tǒng)使用了多種不同形式的抽象,利用更簡(jiǎn)單的抽象模型來(lái)隱藏實(shí)現(xiàn)的細(xì)節(jié)。對(duì)于機(jī)器級(jí)編程來(lái)說(shuō),其中兩種抽象尤為重要。第一種是機(jī)器級(jí)程序的格式和行為,定義為指令集體系結(jié)構(gòu)(Instruction set architecture,ISA),它定義了處理器狀態(tài)、指令的格式,以及每條指令對(duì)狀態(tài)的影響。大多數(shù)ISA,將程序的行為描述成好像每條指令是按順序執(zhí)行的,一條指令結(jié)束后,下一條指令開(kāi)始。處理器的硬件遠(yuǎn)比描述的精細(xì)復(fù)雜,它們并發(fā)地執(zhí)行許多指令,但是可以采取措施保證整體行為與ISA指定的順序執(zhí)行完全一致。第二種抽象是,機(jī)器級(jí)程序使用的存儲(chǔ)器地址是虛擬地址,提供的存儲(chǔ)器模型看上去是一個(gè)非常大的字節(jié)數(shù)組。存儲(chǔ)器系統(tǒng)的實(shí)際實(shí)現(xiàn)是將多個(gè)硬件存儲(chǔ)器和操作系統(tǒng)軟件組合起來(lái)進(jìn)行操作的。

int accum=0;

int sum(int x, int y) {
    int t = x + y;
    accum++;
    return t;
}

在命令行上使用“-S”選項(xiàng),就能得到C語(yǔ)言編譯器產(chǎn)生的匯編代碼,產(chǎn)生一個(gè)匯編文件sum.s。

$ gcc -O1 -S sum.c

編譯選項(xiàng)-O1告訴編譯器使用第一級(jí)優(yōu)化。通常,提高優(yōu)化級(jí)別會(huì)使最終程序運(yùn)行得更快,但是編譯時(shí)間可能會(huì)變長(zhǎng),用調(diào)試工具對(duì)代碼進(jìn)行調(diào)試會(huì)更困難。

sum.s文件內(nèi)容如下:

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 15    sdk_version 10, 15
    .globl  _sum                    ## -- Begin function sum
    .p2align    4, 0x90
_sum:                                   ## @sum
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
                                        ## kill: def $esi killed $esi def $rsi
                                        ## kill: def $edi killed $edi def $rdi
    leal    (%rdi,%rsi), %eax
    incl    _accum(%rip)
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .globl  _accum                  ## @accum
.zerofill __DATA,__common,_accum,4,2

.subsections_via_symbols

GCC產(chǎn)生的匯編代碼對(duì)我們來(lái)說(shuō)有點(diǎn)難懂。一方面,它包含一些我們不需要關(guān)心的信息;另一方面,它不提供任何程序的描述或它是如何工作的描述。所有以“.”開(kāi)頭的行都是匯編器和鏈接器的命令,我們?cè)诮庾x時(shí)通??梢院雎赃@些行,專注到機(jī)器指令上來(lái)。

再進(jìn)一步,在命令行上使用“-c”選項(xiàng),GCC會(huì)編譯并匯編該代碼:

$ gcc -O1 -c sum.c

這就會(huì)產(chǎn)生目標(biāo)代碼文件sum.o,它是二進(jìn)制格式,不能直接以文本形式查看。在Linux系統(tǒng)中,可以通過(guò)xxd來(lái)查看字節(jié)編碼。

$ xxd sum.o
00000000: cffa edfe 0700 0001 0300 0000 0100 0000  ................
00000010: 0400 0000 0802 0000 0020 0000 0000 0000  ......... ......
00000020: 1900 0000 8801 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 7400 0000 0000 0000 2802 0000 0000 0000  t.......(.......
00000050: 7000 0000 0000 0000 0700 0000 0700 0000  p...............
00000060: 0400 0000 0000 0000 5f5f 7465 7874 0000  ........__text..
00000070: 0000 0000 0000 0000 5f5f 5445 5854 0000  ........__TEXT..
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0f00 0000 0000 0000 2802 0000 0400 0000  ........(.......
000000a0: 9802 0000 0100 0000 0004 0080 0000 0000  ................
000000b0: 0000 0000 0000 0000 5f5f 636f 6d6d 6f6e  ........__common
000000c0: 0000 0000 0000 0000 5f5f 4441 5441 0000  ........__DATA..
000000d0: 0000 0000 0000 0000 7000 0000 0000 0000  ........p.......
000000e0: 0400 0000 0000 0000 0000 0000 0200 0000  ................
000000f0: 0000 0000 0000 0000 0100 0000 0000 0000  ................
00000100: 0000 0000 0000 0000 5f5f 636f 6d70 6163  ........__compac
00000110: 745f 756e 7769 6e64 5f5f 4c44 0000 0000  t_unwind__LD....
00000120: 0000 0000 0000 0000 1000 0000 0000 0000  ................
00000130: 2000 0000 0000 0000 3802 0000 0300 0000   .......8.......
00000140: a002 0000 0100 0000 0000 0002 0000 0000  ................
00000150: 0000 0000 0000 0000 5f5f 6568 5f66 7261  ........__eh_fra
00000160: 6d65 0000 0000 0000 5f5f 5445 5854 0000  me......__TEXT..
00000170: 0000 0000 0000 0000 3000 0000 0000 0000  ........0.......
00000180: 4000 0000 0000 0000 5802 0000 0300 0000  @.......X.......
00000190: 0000 0000 0000 0000 0b00 0068 0000 0000  ...........h....
000001a0: 0000 0000 0000 0000 3200 0000 1800 0000  ........2.......
000001b0: 0100 0000 000f 0a00 000f 0a00 0000 0000  ................
000001c0: 0200 0000 1800 0000 a802 0000 0200 0000  ................
000001d0: c802 0000 1000 0000 0b00 0000 5000 0000  ............P...
000001e0: 0000 0000 0000 0000 0000 0000 0200 0000  ................
000001f0: 0200 0000 0000 0000 0000 0000 0000 0000  ................
00000200: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000210: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000220: 0000 0000 0000 0000 5548 89e5 8d04 37ff  ........UH....7.
00000230: 0500 0000 005d c300 0000 0000 0000 0000  .....]..........
00000240: 0f00 0000 0000 0001 0000 0000 0000 0000  ................
00000250: 0000 0000 0000 0000 1400 0000 0000 0000  ................
00000260: 017a 5200 0178 1001 100c 0708 9001 0000  .zR..x..........
00000270: 2400 0000 1c00 0000 b0ff ffff ffff ffff  $...............
00000280: 0f00 0000 0000 0000 0041 0e10 8602 430d  .........A....C.
00000290: 0600 0000 0000 0000 0900 0000 0000 001d  ................
000002a0: 0000 0000 0100 0006 0600 0000 0f02 0000  ................
000002b0: 7000 0000 0000 0000 0100 0000 0f01 0000  p...............
000002c0: 0000 0000 0000 0000 005f 7375 6d00 5f61  ........._sum._a
000002d0: 6363 756d 0000 0000                      ccum....

xxd默認(rèn)會(huì)以十六進(jìn)制的形式來(lái)進(jìn)行顯示,兩個(gè)字節(jié)為一組。可以通過(guò)指定 -b 選項(xiàng)來(lái)顯示二進(jìn)制,-g n 選項(xiàng)來(lái)指定每組顯示n個(gè)字節(jié)的編碼。

要查看目標(biāo)代碼文件的所代表的內(nèi)容,最有價(jià)值的是反匯編器(disassembler)。這些程序根據(jù)目標(biāo)代碼產(chǎn)生一種類(lèi)似于匯編代碼的格式。在Linux系統(tǒng)中,帶“-d”命令行標(biāo)志的程序objdump可以充當(dāng)這個(gè)角色。

$ objdump -d sum.o
sum.o:  file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
_sum:
       0:   55  pushq   %rbp
       1:   48 89 e5    movq    %rsp, %rbp
       4:   8d 04 37    leal    (%rdi,%rsi), %eax
       7:   ff 05 00 00 00 00   incl    (%rip)
       d:   5d  popq    %rbp
       e:   c3  retq

objdump -d 可以直接作用于可執(zhí)行文件,用于反匯編出文件代碼,比如hello的可執(zhí)行文件。

數(shù)據(jù)格式

由于是從16位體系結(jié)構(gòu)擴(kuò)展成32位的,Intel用術(shù)語(yǔ)“字”(word)表示16位數(shù)據(jù)類(lèi)型。因此,稱32位數(shù)為“雙字”(double words),稱64位數(shù)為“四字”(quad words)。

C聲明 Intel數(shù)據(jù)類(lèi)型 匯編代碼后綴 大小(字節(jié))
char 字節(jié) b 1
short w 2
int 雙字 l 4
long int 雙字 l 4
long long int 4
char * 雙字 l 4
float 單精度 s 4
double 雙精度 l 8
long double 擴(kuò)展精度 t 10/12

上表是C語(yǔ)言數(shù)據(jù)類(lèi)型在IA32中的大小。IA32不支持64位整數(shù)運(yùn)算。編譯帶有 long long 數(shù)據(jù)的代碼,需要產(chǎn)生一些操作序列,以32位塊為單位執(zhí)行運(yùn)算。

大多數(shù)GCC生成的匯編代碼指令都有一個(gè)字符后綴,表明操作數(shù)的大小。例如,數(shù)據(jù)傳送指令有三個(gè)變種:movb(傳送字節(jié))、movew(傳送字)、movel(傳送雙字)。后綴 “l(fā)” 用來(lái)表示雙字,因?yàn)閷?2位數(shù)看成是“長(zhǎng)字”(long word),這是由于沿用了16位字為標(biāo)準(zhǔn)那個(gè)時(shí)代的習(xí)慣。匯編代碼也使用后綴 “l(fā)” 來(lái)表示4字節(jié)整數(shù)和8字節(jié)雙精度浮點(diǎn)數(shù)。這不會(huì)產(chǎn)生歧義,因?yàn)楦↑c(diǎn)數(shù)使用的是一組完全不同的指令和寄存器。

一個(gè)IA32中央處理單元(CPU)包含一組8個(gè)存儲(chǔ)32位值的寄存器。這些寄存器用來(lái)存儲(chǔ)整數(shù)數(shù)據(jù)和指針。這8個(gè)寄存器的名字都以 %e 開(kāi)頭,不過(guò)它們都另有特殊的名字。在大多數(shù)情況,前6個(gè)寄存器都可以看成通用寄存器,對(duì)它們的使用沒(méi)有限制。最后兩個(gè)寄存器(%ebp和%esp)保存著指向程序棧中重要位置的指針,只有根據(jù)棧管理的標(biāo)準(zhǔn)才能修改這兩個(gè)寄存器中的值。

IA32的整數(shù)寄存器

所有8個(gè)寄存器都可以作為16位(字)或32位(雙字)來(lái)訪問(wèn)。字節(jié)操作指令可以獨(dú)立地讀或者寫(xiě)前4個(gè)寄存器的2個(gè)低位字節(jié)。當(dāng)一條字節(jié)指令更新這些單字節(jié)“寄存器元素”中的一個(gè)時(shí),該寄存器余下的3個(gè)字節(jié)不會(huì)改變。類(lèi)似地,字操作指令可以讀或者寫(xiě)每個(gè)寄存器的低16位。這個(gè)特性源自IA32從16位微處理器演化而來(lái)的這個(gè)傳統(tǒng),當(dāng)對(duì)大小指示符為short的整數(shù)運(yùn)算時(shí),也會(huì)用到這個(gè)特性。

大多數(shù)指令有一個(gè)或多個(gè)操作數(shù),指示出執(zhí)行一個(gè)操作中要引用的源數(shù)據(jù)值,以及放置結(jié)果的目標(biāo)位置。源數(shù)據(jù)值可以以常數(shù)形式給出,或是從寄存器或存儲(chǔ)器中讀出。結(jié)果可以存放在寄存器或存儲(chǔ)器中。各種不同的操作數(shù)的可能性被分為三種類(lèi)型:立即數(shù)、寄存器和存儲(chǔ)器引用。

立即數(shù)(immediate)也就是常數(shù)值。在ATT格式的匯編代碼中,立即數(shù)的書(shū)寫(xiě)方式是“$”后面跟一個(gè)用標(biāo)準(zhǔn)C表示法表示的整數(shù),比如$-577或$0x1F。任何能放進(jìn)一個(gè)32位的字里的數(shù)值都可以用作立即數(shù),不過(guò)匯編器在可能時(shí)會(huì)使用一個(gè)或兩個(gè)字節(jié)的編碼。
寄存器(register)表示某個(gè)寄存器的內(nèi)容。我們用符號(hào)E_a來(lái)表示任意寄存器a,用引用R[E_a]來(lái)表示它的值,這是將寄存器集合看成一個(gè)數(shù)組R,用寄存器標(biāo)識(shí)符作為索引。
存儲(chǔ)器(memory)引用會(huì)根據(jù)計(jì)算出來(lái)的有效地址訪問(wèn)某個(gè)存儲(chǔ)器位置。因?yàn)閷⒋鎯?chǔ)器看成一個(gè)很大的字節(jié)數(shù)組,我們用符號(hào)M_b[Addr]表示對(duì)存儲(chǔ)在存儲(chǔ)器中從地址Addr開(kāi)始的b個(gè)字節(jié)的引用。為了簡(jiǎn)便,我們通常省去下方的b

有多種不同的尋址模式,允許不同形式的存儲(chǔ)器引用。表中底部用語(yǔ)法 Imm(E_b,E_i,s) 表示的是最常用的形式。這樣的引用由四個(gè)部分組成:一個(gè)立即數(shù)偏移 Imm,一個(gè)基址寄存器 E_b,一個(gè)變址寄存器 E_i 和一個(gè)比例因子 s,這里 s 必須是 1、2、4或者8。然后,有效地址被計(jì)算為 Imm+R[E_b]+R[E_i]\cdot s。引用數(shù)組元素時(shí),會(huì)用到這種通用形式。

類(lèi)型 格式 操作數(shù)值 名稱
立即數(shù) $Imm Imm 立即數(shù)尋址
寄存器 E_a R[E_a] 寄存器尋址
存儲(chǔ)器 Imm M[Imm] 絕對(duì)尋址
存儲(chǔ)器 (E_a) M[R[E_a]] 間接尋址
存儲(chǔ)器 Imm(E_b) M[Imm+R[E_b]] (基址+偏移量)尋址
存儲(chǔ)器 (E_b,E_i) M[R[E_b]+R[E_i]] 變址尋址
存儲(chǔ)器 Imm(E_b,E_i) M[Imm+R[E_b]+R[E_i]] 變址尋址
存儲(chǔ)器 (,E_i,s) M[R[E_i]\cdot s] 比例變址尋址
存儲(chǔ)器 Imm(,E_i,s) M[Imm+R[E_i]\cdot s] 比例變址尋址
存儲(chǔ)器 (E_b,E_i,s) M[R[E_b]+R[E_i]\cdot s] 比例變址尋址
存儲(chǔ)器 $Imm(E_b,E_i,s) M[Imm+R[E_b]+R[E_i]\cdot s] 比例變址尋址
數(shù)據(jù)傳送指令

以下所有指令表中的S代表Source,即源數(shù)據(jù),D代表Destination,即結(jié)果數(shù)據(jù),或者單獨(dú)使用時(shí),僅僅是作為一個(gè)整數(shù)的代數(shù)符號(hào)。

指令 效果 描述
MOV S,D D \leftarrow S 傳送
movb 傳送字節(jié)
movw 傳送字
movl 傳送雙字
MOVS S,D D \leftarrow 符號(hào)擴(kuò)展(S) 傳送符號(hào)擴(kuò)展的字節(jié)
movsbw 將做了符號(hào)擴(kuò)展的字節(jié)傳送到字
movsbl 將做了符號(hào)擴(kuò)展的字節(jié)傳送到雙字
movswl 將做了符號(hào)擴(kuò)展的字傳送到雙字
MOVZ S,D D \leftarrow 零擴(kuò)展(S) 傳送零擴(kuò)展的字節(jié)
movzbw 將做了零擴(kuò)展的字節(jié)傳送到字
movzbl 將做了零擴(kuò)展的字節(jié)傳送到雙字
movzwl 將做了零擴(kuò)展的字傳送到雙字
pushl S R[\text%esp] \leftarrow R[\text%esp]-4;
M[R[\text%esp]] \leftarrow S
將雙字壓棧
popl D D \leftarrow M[R[\text%esp];
R[\text%esp] \leftarrow R[\text%esp]+4;
將雙字出棧

棧可以實(shí)現(xiàn)為一個(gè)數(shù)組,總是從數(shù)組的一端插入和刪除元素,這一端稱為棧頂。在IA32中,程序棧存放在存儲(chǔ)器中某個(gè)區(qū)域,并且棧是從高地址向低地址方向增長(zhǎng)的;所以壓棧是減小棧指針(寄存器%esp)的值,并將數(shù)據(jù)存放在存儲(chǔ)器中,而出棧是從存儲(chǔ)器中讀,并增加棧指針的值。

算術(shù)和邏輯操作
指令 效果 描述
leal S,D D \leftarrow \&S 加載有效地址
INC D D \leftarrow D+1 加1
DEC D D \leftarrow D-1 減1
NEG D D \leftarrow -D 取負(fù)
NOT D D \leftarrow ~D 取補(bǔ)
ADD S,D D \leftarrow D+S
SUB S,D D \leftarrow D-S
IMUL S,D D \leftarrow D*S
XOR S,D D \leftarrow D \text^ S 異或
OR S,D D \leftarrow D | S
AND S,D D \leftarrow D \& S
SAL k,D D \leftarrow D<<k 左移
SHL k,D D \leftarrow D<<k 左移(等同于SAL)
SAR k,D D \leftarrow D>>_Ak 算術(shù)右移
SHR k,D D \leftarrow D>>_Lk 邏輯右移

加載有效地址(load effective address)指令 leal 實(shí)際上是movl的變形,它的指令形式是從存儲(chǔ)器讀數(shù)據(jù)到寄存器,但實(shí)際上它根本就沒(méi)有引用存儲(chǔ)器。它的第一個(gè)操作數(shù)看上去是一個(gè)存儲(chǔ)器引用,但該指令并不是從指定的位置讀入數(shù)據(jù),而是將有效地址寫(xiě)入到目的操作數(shù)。這條指令可以為后面的存儲(chǔ)器引用產(chǎn)生指針。另外,它還可以簡(jiǎn)潔地描述普通的算術(shù)操作。例如,如果寄存器%edx的值為x,那么指令 leal 7(%edx,%edx,4),%eax 將設(shè)置寄存器%eax的值為5x+7。(采用尋址規(guī)則,7+R[E_d]+R[E_d] \cdot 4=7+x+x\cdot4=5x+7

一元操作只有一個(gè)操作數(shù),既是源又是目的。這個(gè)操作數(shù)可以是一個(gè)寄存器,也可以是一個(gè)存儲(chǔ)器位置。

二元操作的第二個(gè)操作數(shù)既是源又是目的。第一個(gè)操作數(shù)可以是立即數(shù)、寄存器或是存儲(chǔ)器位置,第二個(gè)操作數(shù)可以是寄存器或是存儲(chǔ)器位置。不過(guò),同movl指令一樣,兩個(gè)操作數(shù)不能同時(shí)是存儲(chǔ)器位置。

移位操作,先給出移位量,然后第二項(xiàng)給出的是要移位的位數(shù)。移位量是單個(gè)字節(jié)編碼,因?yàn)橹辉试S進(jìn)行0到31位的移位。移位量可以是一個(gè)立即數(shù),或者放在單字節(jié)寄存器元素%cl中。(這些指令很特別,因?yàn)橹辉试S以這個(gè)特定的寄存器作為操作數(shù)。)兩個(gè)左移指令的效果是一樣的,都是將右邊填上0。右移指令不同,算術(shù)右移是填上符號(hào)位,邏輯右移是填上0。移位操作的目的操作數(shù)可以是一個(gè)寄存器或是一個(gè)存儲(chǔ)器位置。

特殊的算術(shù)操作
指令 效果 描述
imull S R[\%edx]:R[\%eax]\leftarrow S \times R[\%eax] 有符號(hào)全64位乘法
mull S R[\%edx]:R[\%eax]\leftarrow S \times R[\%eax] 無(wú)符號(hào)全64位乘法
cltd S R[\%edx]:R[\%eax]\leftarrow SignExtend(R[\%eax]) 轉(zhuǎn)為4字
idivl S R[\%edx]\leftarrow R[\%edx]:R[\%eax] mod S;
R[\%eax]\leftarrow R[\%edx]:R[\%eax] \div S
有符號(hào)除法
divl S R[\%edx]\leftarrow R[\%edx]:R[\%eax] mod S;
R[\%eax]\leftarrow R[\%edx]:R[\%eax] \div S
無(wú)符號(hào)除法

這些操作提供了有符號(hào)和無(wú)符號(hào)的全64位乘法和除法。一對(duì)寄存器 %edx 和 %eax 組成一個(gè)64位的四字。

imull和mull兩條指令都要求一個(gè)參數(shù)必須在寄存器%eax中,而另一個(gè)作為指令的源操作數(shù)給出。然后乘積存放在寄存器%edx(高32位)和%eax(低32位)中。

有符號(hào)除法指令idivl將寄存器%edx(高32位)和%eax(低32位)中的64位數(shù)作為被除數(shù),而除數(shù)作為指令的操作數(shù)給出。指令將商存儲(chǔ)在寄存器%eax中,將余數(shù)存儲(chǔ)在寄存器%edx中。無(wú)符號(hào)除法使用的是divl指令,通常會(huì)事先將寄存器%edx設(shè)置為0。

cltd指令將%eax符號(hào)擴(kuò)展到%edx,通常用來(lái)設(shè)置被除數(shù)。

條件碼

除了整數(shù)寄存器,CPU還維護(hù)著一組單個(gè)位的條件碼(condition code)寄存器,它們描述了最近的算術(shù)或邏輯操作的屬性??梢詸z測(cè)這些寄存器來(lái)執(zhí)行條件分支指令。最常用的條件碼有:

  • CF:進(jìn)位標(biāo)志。最近的操作使最高位產(chǎn)生了進(jìn)位??梢杂脕?lái)檢查無(wú)符號(hào)操作數(shù)的溢出。
  • ZF:零標(biāo)志。最近的操作得到的結(jié)果是0。
  • SF:符號(hào)標(biāo)志。最近的操作得到的結(jié)果為負(fù)數(shù)。
  • OF:溢出標(biāo)志。最近的操作導(dǎo)致一個(gè)補(bǔ)碼溢出(正溢出或負(fù)溢出)。

leal指令不改變?nèi)魏螚l件碼,因?yàn)樗怯脕?lái)進(jìn)行地址計(jì)算的。其它整數(shù)算術(shù)操作指令都會(huì)設(shè)置條件碼。對(duì)于邏輯操作,例如XOR,進(jìn)位標(biāo)志和溢出標(biāo)志會(huì)設(shè)置成0。對(duì)于移位操作,進(jìn)位標(biāo)志將設(shè)置為最后一個(gè)被移出的位,而溢出標(biāo)志設(shè)置為0。INC和DEC指令會(huì)設(shè)置溢出和零標(biāo)志,但是不會(huì)改變進(jìn)位標(biāo)志。

指令 基于 描述
CMP S_2,S_1 S_1-S_2 比較
cmpb 比較字節(jié)
cmpw 比較字
cmpl 比較雙字
TEST S_2,S_1 S_1\&S_2 測(cè)試
testb 測(cè)試字節(jié)
testw 測(cè)試字
testl 測(cè)試雙字

比較和測(cè)試指令只設(shè)置條件碼而不改變?nèi)魏纹渌拇嫫?。所以,除了不設(shè)置寄存器,CMP指令和SUB指令的行為是一樣的,TEST和ADD指令的行為也是一樣的。

訪問(wèn)條件碼

條件碼通常不會(huì)直接讀取。每條SET指令根據(jù)條件碼的某個(gè)組合,將一個(gè)字節(jié)設(shè)置為0或1。SET指令名字的不同后綴指明了它們所考慮的條件碼的組合,而不是操作數(shù)的大小,請(qǐng)注意與其他組指令在名字上的這點(diǎn)區(qū)別。

指令 同義名 效果 設(shè)置條件
sete D setz D \leftarrow ZF 相等/零
setne D setnz D \leftarrow \text~ZF 不等/非零
sets D D \leftarrow SF 負(fù)數(shù)
setns D D \leftarrow \text~SF 非負(fù)數(shù)
setg D setnle D \leftarrow \text~(SF\text^OF)\&\text ~ZF 大于(有符號(hào)>
setge D setnl D \leftarrow \text~(SF\text^OF) 大于等于(有符號(hào)\geqslant
setl D setnge D \leftarrow SF\text^OF 小于(有符號(hào)<
setle D setng D \leftarrow (SF\text^OF)|ZF 大于等于(有符號(hào)\leqslant
seta D setnbe D \leftarrow \text~CF\&\text~ZF 超過(guò)(無(wú)符號(hào)>
setae D setnb D \leftarrow \text~CF 超過(guò)或相等(無(wú)符號(hào)\geqslant
setb D setnae D \leftarrow CF 低于(無(wú)符號(hào)<
setbe D setna D \leftarrow CF|ZF 低于或相等(無(wú)符號(hào)\leqslant

一條SET指令的目的操作數(shù)是8個(gè)單字節(jié)寄存器元素之一,或是存儲(chǔ)一個(gè)字節(jié)的存儲(chǔ)器位置,將這個(gè)字節(jié)設(shè)置成0或1。為了得到一個(gè)32位結(jié)果,我們必須對(duì)最高的24位清零。某些底層的機(jī)器指令可能有多個(gè)名字,我們稱為“同義名”(synonym)。比如,setg(設(shè)置大于)和setnle(設(shè)置不小于等于)指的就是同一條機(jī)器指令。

跳轉(zhuǎn)指令

正常執(zhí)行的情況下,指令按照它們出現(xiàn)的順序一條一條地執(zhí)行。跳轉(zhuǎn)(jump)指令會(huì)導(dǎo)致執(zhí)行切換到程序中一個(gè)全新的位置。在匯編代碼中,這些跳轉(zhuǎn)的目的地通常用一個(gè)標(biāo)號(hào)(label)指明。

指令 同義名 跳轉(zhuǎn)條件 描述
jmp Label 1 直接跳轉(zhuǎn)
jmp *Operand 1 間接跳轉(zhuǎn)
je Label jz ZF 相等/零
jne Label jnz \text~ZF 不等/非零
js Label SF 負(fù)數(shù)
jns Label \text~SF 非負(fù)數(shù)
jg Label jnle \text~(SF\text^OF)\&\text ~ZF 大于(有符號(hào)>
jge Label jnl \text~(SF\text^OF) 大于等于(有符號(hào)\geqslant
jl Label jnge SF\text^OF 小于(有符號(hào)<
jle Label jng (SF\text^OF)|ZF 大于等于(有符號(hào)\leqslant
ja Label jnbe \text~CF\&\text~ZF 超過(guò)(無(wú)符號(hào)>
jae Label jnb \text~CF 超過(guò)或相等(無(wú)符號(hào)\geqslant
jb Label jnae CF 低于(無(wú)符號(hào)<
jbe Label jna CF|ZF 低于或相等(無(wú)符號(hào)\leqslant

jmp指令是無(wú)條件跳轉(zhuǎn),它可以是直接跳轉(zhuǎn),即跳轉(zhuǎn)目標(biāo)是作為指令的一部分編碼的;也可以是間接跳轉(zhuǎn),即跳轉(zhuǎn)目標(biāo)是從寄存器或存儲(chǔ)器位置中讀出的。

匯編語(yǔ)言中,直接跳轉(zhuǎn)是給出一個(gè)標(biāo)號(hào)作為跳轉(zhuǎn)目標(biāo)的。

  jmp  .L1
  movl  (%eax),%edx
.L1:
  popl  %edx

而間接跳轉(zhuǎn)的寫(xiě)法是“*”后面緊跟一個(gè)操作數(shù)指示符。

  jmp  *%eax      // 用寄存器%eax中的值作為跳轉(zhuǎn)目標(biāo)
  jmp  *(%eax)    // 以%eax中的值作為讀地址,從存儲(chǔ)器中讀出跳轉(zhuǎn)目標(biāo)

其它跳轉(zhuǎn)指令都是有條件跳轉(zhuǎn),它們根據(jù)條件碼的組合,或者跳轉(zhuǎn),或者繼續(xù)執(zhí)行代碼序列中的下一條指令。這些指令的名字和它們的跳轉(zhuǎn)條件與SET指令是相匹配的。

過(guò)程

一個(gè)過(guò)程調(diào)用包括將數(shù)據(jù)(以過(guò)程參數(shù)和返回值的形式)和控制從代碼的一部分傳遞到另一部分。另外,它還必須在進(jìn)入時(shí)為過(guò)程的局部變量分配空間,并在退出時(shí)釋放這些空間。大多數(shù)機(jī)器,只提供轉(zhuǎn)移控制到過(guò)程中和從過(guò)程中轉(zhuǎn)移出控制這種簡(jiǎn)單的命令。數(shù)據(jù)傳遞、局部變量的分配和釋放通過(guò)操縱程序棧來(lái)實(shí)現(xiàn)。

棧幀結(jié)構(gòu)

機(jī)器用棧來(lái)傳遞過(guò)程參數(shù)、存儲(chǔ)返回信息、保存寄存器用于以后恢復(fù),以及本地存儲(chǔ)。為單個(gè)過(guò)程分配的那部分棧稱為棧幀(stack frame)。棧幀的最頂端以兩個(gè)指針界定,寄存器%ebp為幀指針,而寄存器%esp為棧指針。當(dāng)程序執(zhí)行時(shí),棧指針可以移動(dòng),因此大多數(shù)信息的訪問(wèn)都是相對(duì)于幀指針的。

假設(shè)過(guò)程P(調(diào)用者)調(diào)用過(guò)程Q(被調(diào)用者),則Q的參數(shù)放在P的棧幀中。另外,當(dāng)P調(diào)用Q時(shí),P中的返回地址被壓入棧中,形成P的棧幀的末尾。返回地址就是當(dāng)程序從Q返回時(shí)應(yīng)該繼續(xù)執(zhí)行的地方。Q的棧幀從保存的幀指針的值(例如%ebp)開(kāi)始,后面是保存的其他寄存器的值。

轉(zhuǎn)移控制
指令 描述
call Label 過(guò)程調(diào)用
call *Operand 過(guò)程調(diào)用
leave 為返回準(zhǔn)備棧
ret 從過(guò)程調(diào)用中返回

call指令有一個(gè)目標(biāo),即指明被調(diào)用過(guò)程起始的指令地址。同跳轉(zhuǎn)一樣,調(diào)用可以直接的,也可以是間接的。call指令的效果是將返回地址入棧,并跳轉(zhuǎn)到被調(diào)用過(guò)程的起始處。返回地址是在程序中緊跟在call后面的那條指令的地址,這樣當(dāng)被調(diào)用過(guò)程返回時(shí),執(zhí)行會(huì)從此處繼續(xù)。

ret指令從棧中彈出地址,并跳轉(zhuǎn)到這個(gè)位置。leave指令可以使棧做好準(zhǔn)備,使棧指針指向前面call指令存儲(chǔ)返回地址的位置。

  leave
# 等價(jià)于
  movl %ebp,%esp
  popl %ebp

程序寄存器組是唯一能被所有過(guò)程共享的資源。雖然在給定時(shí)刻只能有一個(gè)過(guò)程是活動(dòng)的,但是我們必須保證當(dāng)一個(gè)過(guò)程(調(diào)用者)調(diào)用另一個(gè)過(guò)程(被調(diào)用者)時(shí),被調(diào)用者不會(huì)覆蓋某個(gè)調(diào)用者稍后會(huì)使用的寄存器的值。為此,IA32采用了一組統(tǒng)一的寄存器使用慣例,所有的過(guò)程都必須遵守,包括程序庫(kù)中的過(guò)程。

根據(jù)慣例,寄存器%eax、%edx、%ecx被劃分為調(diào)用者保存寄存器。當(dāng)過(guò)程Q調(diào)用Q時(shí),Q可以覆蓋這些寄存器,而不會(huì)破壞任何P所需要的數(shù)據(jù)。另一方面,寄存器%ebx、%esi、%edi被劃分為被調(diào)用者保存寄存器。這意味著Q必須在覆蓋這些寄存器的值之前,先把它們保存到棧中,并在返回前恢復(fù)它們,因?yàn)镻(或某個(gè)更高層次的過(guò)程)可能會(huì)在后面的計(jì)算中需要這些值。此外,根據(jù)慣例,必須保存寄存器%ebp和%esp。

棧和鏈接慣例使得過(guò)程可以遞歸地調(diào)用它們自身。因?yàn)槊總€(gè)調(diào)用在棧中都有它自己的私有空間,多個(gè)未完成調(diào)用的局部變量還不會(huì)相互影響。此外,棧的原則很自然地提供了適當(dāng)?shù)牟呗裕?dāng)過(guò)程被調(diào)用時(shí)分配局部變量,當(dāng)返回時(shí)釋放存儲(chǔ)。

遞歸調(diào)用一個(gè)函數(shù)本身與調(diào)用其他函數(shù)是一樣的。棧規(guī)則提供了一種機(jī)制,每次函數(shù)調(diào)用都有它自己私有的狀態(tài)信息(保存的返回位置、棧指針和被調(diào)用者保存寄存器的值)存儲(chǔ)。如果需要,它還可以提供局部變量的存儲(chǔ)。分配和釋放的棧規(guī)則很自然地就與函數(shù)調(diào)用—返回的順序匹配。這種實(shí)現(xiàn)函數(shù)調(diào)用和返回的方法甚至對(duì)于更復(fù)雜的情況也適用,包括相互遞歸調(diào)用(例如,當(dāng)過(guò)程P調(diào)用Q,Q再調(diào)用P)。

將IA32擴(kuò)展到64位

以下是x86-64的標(biāo)準(zhǔn)數(shù)據(jù)類(lèi)型大小,與IA32比較,長(zhǎng)整數(shù)和指針需要8個(gè)字節(jié),而IA32只需要4個(gè)字節(jié)。

C聲明 Intel數(shù)據(jù)類(lèi)型 匯編代碼后綴 x86-64大小(字節(jié)) IA32大小
char 字節(jié) b 1 1
short w 2 2
int 雙字 l 4 4
long int 四字 q 8 4
long long int 四字 q 8 8
char * 四字 q 8 4
float 單精度 s 4 4
double 雙精度 l 8 8
long double 擴(kuò)展精度 t 10/16 10/12

Intel進(jìn)入64位計(jì)算機(jī)領(lǐng)域打響的第一槍是Itanium處理器,它基于一種全新的指令集,稱為“IA64”。Intel的一貫的策略是每次引入新一代微處理器時(shí)還要維持后向兼容性,而這次不同,IA64是基于與Hewlett-Packard一起開(kāi)發(fā)的一種嶄新的方法。IA64的實(shí)現(xiàn)是很難的,因此第一批Itanium芯片直到2001年才出現(xiàn),而且在真實(shí)應(yīng)用上沒(méi)有達(dá)到預(yù)期的性能。大多數(shù)用戶寧愿使用比較便宜并且常常更快的基于IA32的系統(tǒng)。
同時(shí),Intel的主要競(jìng)爭(zhēng)對(duì)手,Advanced Micro Devices(AMD)看到一個(gè)機(jī)會(huì)去利用Intel在IA64上的失敗。2003年,AMD推出了基于“x86-64”指令集的64位微處理器。顧名思義,x86-64是Intel指令集到64位的一個(gè)演化。它保持了與IA32完全的后向兼容性,并且又增加了新的數(shù)據(jù)格式,以及其他一些特性,使得能力更強(qiáng),性能更高。通過(guò)x86-64,AMD獲得了以前屬于Intel的一些高端市場(chǎng)。AMD后來(lái)將這個(gè)指令集更名為AMD64,但是大家還是對(duì)“x86-64”這個(gè)名字更喜歡一些。

x86-64下的通用寄存器組,與IA32相比,有以下區(qū)別:

  • 寄存器的數(shù)量翻倍至16個(gè)。
  • 所有的寄存器都是64位長(zhǎng)。IA32寄存器的64位擴(kuò)展分別命名為%rax、%rcx、%rdx、%rbx、%rsi、%rdi、%rbp和%rsp,新增加的寄存器命名為%r8~%r15。
  • 可以直接訪問(wèn)每個(gè)寄存器的低32位。這就給了我們IA32中熟悉的那些寄存器:%eax、%ecx、%edx、%ebx、%esi、%edi、%ebp和%esp,以及8個(gè)新32位寄存器:%r8d~%r15d。
  • 可以直接訪問(wèn)每個(gè)寄存器的低16。新寄存器的字大小版本命名為 %r8w~%r15w。
  • 可以直接訪問(wèn)每個(gè)寄存器的低8位,在IA32中只有對(duì)前4個(gè)寄存器(%al、%cl、%dl和%bl)才可以這樣。其他IA32寄存器的字節(jié)大小版本命名為%sil、%dil、%bpl和%spl。新寄存器的字節(jié)大小版本命名為%r8b~%r15b。
  • 為了向后兼容,具有單字節(jié)操作數(shù)的指令可以直接訪問(wèn)%rax、%rcx、%rdx和%rbx的第二個(gè)字節(jié),即%ah、%ch、%dh和%bh。
x86-64整數(shù)寄存器

同IA32一樣,大多數(shù)寄存器可以互換使用,但是有一些特殊情況。寄存器%rsp有特殊的狀態(tài),它會(huì)保存指向棧頂元素的指針。與IA32不同的是,沒(méi)有幀指針寄存器,可以將%rbp作為通用寄存器使用。

指令 類(lèi)型 描述
movabsq I,R R \leftarrow I 傳送絕對(duì)四字
MOV S,D D \leftarrow S 傳送
movq 傳送四字
MOVS S,D D \leftarrow SignExtend(S) 符號(hào)擴(kuò)展傳遞
movsbq 符號(hào)擴(kuò)展字節(jié)傳送四字
movswq 符號(hào)擴(kuò)展字傳送四字
movslq 符號(hào)擴(kuò)展雙字傳送四字
MOVZ S,D D \leftarrow ZeroExtend(S) 零擴(kuò)展傳遞
movzbq 零擴(kuò)展字節(jié)傳送四字
movzwq 零擴(kuò)展字傳送四字
pushq S R[\%rsp] \leftarrow R[\%rsp]-8;
M[R[\%rsp]] \leftarrow S
將四字壓棧
popq D D \leftarrow M[R[\%rsp]];
R[\%rsp] \leftarrow R[\%rsp]+8;
將四字出棧

這些指令是對(duì)IA32傳送指令的補(bǔ)充。movabsq只允許立即數(shù)(用I表示)作為源值,其他指令允許立即數(shù)、寄存器或存儲(chǔ)器(用S表示)。有些指令要求目的是寄存器(用R表示),而其他的指令允許用寄存器或存儲(chǔ)器作為目的(用D表示)。

同樣地,算術(shù)運(yùn)算指令和控制指令也增加了對(duì)四字的擴(kuò)展支持。

指令 效果或根據(jù) 描述
imulq S R[\%rdx]:R[\%rax] \leftarrow S \times R[\%rax] 有符號(hào)全乘法
mulq S R[\%rdx]:R[\%rax] \leftarrow S \times R[\%rax] 無(wú)符號(hào)全乘法
cltq R[\%rax] \leftarrow SignExtend(R[\%eax]) \%eax轉(zhuǎn)換成四字
cqto R[\%rdx]:R[\%rax] \leftarrow SignExtend(R[\%rax]) \%rax轉(zhuǎn)換成八字
idivq S R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] mod S;
R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] \div S
有符號(hào)除法
divq S R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] mod S;
R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] \div S
無(wú)符號(hào)除法
cmpq S_2,S_1 S_1-S_2 比較四字
testq S_2,S_1 S_1-S_2 測(cè)試四字

過(guò)程調(diào)用的x86-64實(shí)現(xiàn)與IA32實(shí)現(xiàn)有很大的不同,通過(guò)將寄存器組翻倍,程序不再需要依賴于棧來(lái)存儲(chǔ)和獲取過(guò)程信息。這極大地減少了過(guò)程調(diào)用和返回的開(kāi)銷(xiāo)。以下是x86-64實(shí)現(xiàn)過(guò)程的一些重點(diǎn):

  • 參數(shù)(最多是前六個(gè))通過(guò)寄存器傳遞到過(guò)程,而不是在棧上。這消除了在棧上存儲(chǔ)和檢索值的開(kāi)銷(xiāo)。
  • callq指令將一個(gè)64位返回地址存儲(chǔ)在棧上。
  • 許多函數(shù)不需要棧幀。
  • 函數(shù)最多可以訪問(wèn)超過(guò)當(dāng)前棧指針值128個(gè)字節(jié)的棧上存儲(chǔ)空間(地址低于當(dāng)前棧指針的值)。這允許一些函數(shù)將信息存儲(chǔ)在棧上而無(wú)需修改棧指針。
  • 沒(méi)有幀指針。作為替代,對(duì)棧位置的引用相對(duì)于棧指針。大多數(shù)函數(shù)在調(diào)用開(kāi)始時(shí)分配所需要的整個(gè)棧存儲(chǔ),并保存棧指針指向固定位置。
  • 同IA32一樣,有些寄存器被指定為被調(diào)用者保存寄存器。任何要修改這些寄存器的過(guò)程都必須保存并恢復(fù)它們。

函數(shù)需要棧幀的原因如下:

  • 局部變量太多,不能都放在寄存器中。
  • 有些局部變量是數(shù)組或者結(jié)構(gòu)。
  • 函數(shù)用取地址操作符(&)來(lái)計(jì)算一個(gè)局部變量的地址。
  • 函數(shù)必須將棧上的某些參數(shù)傳遞到另一個(gè)函數(shù)。
  • 在修改一個(gè)被調(diào)用者保存寄存器之前,函數(shù)需要保存它的狀態(tài)。
    當(dāng)上述條件有任何一條被滿足時(shí),函數(shù)編譯出來(lái)的代碼就會(huì)創(chuàng)建棧幀。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容