【经典阅读】CSAPP-3.2-程序的机器级表示-程序编码
0.導(dǎo)讀
假設(shè)一個(gè)C程序,有兩個(gè)文件 p1.c和 p2.c。我們用Unix命令行編譯這些代碼:
linux> gcc -og -o p p1.c p2.c
? ? ? ?命令 gcc指的就是GCC C編譯器。因?yàn)檫@是Linux上默認(rèn)的編譯器,我們也可以簡(jiǎn)單地用cc來啟動(dòng)它。編譯選項(xiàng)-Og告訴編譯器使用會(huì)生成符合原始C代碼整體結(jié)構(gòu)的機(jī)器代碼的優(yōu)化等級(jí)。使用較高級(jí)別優(yōu)化產(chǎn)生的代碼會(huì)嚴(yán)重變形,以至于產(chǎn)生的機(jī)器代碼和初始源代碼之間的關(guān)系非常難以理解。因此我們會(huì)使用-Og 優(yōu)化作為學(xué)習(xí)工具,然后當(dāng)我們?cè)黾觾?yōu)化級(jí)別時(shí),再看會(huì)發(fā)生什么。實(shí)際中,從得到的程序的性能考慮,較高級(jí)別的優(yōu)化(例如,以選項(xiàng)-O1或-O2指定)被認(rèn)為是較好的選擇。
? ? ? ?實(shí)際上gcc命令調(diào)用了一整套的程序,將源代碼轉(zhuǎn)化成可執(zhí)行代碼。首先,C預(yù)處理器擴(kuò)展源代碼,插人所有用#include命令指定的文件,并擴(kuò)展所有用#define聲明指定的宏。其次,編譯器產(chǎn)生兩個(gè)源文件的匯編代碼,名字分別為p1.s和p2.s。接下來,匯編器會(huì)將匯編代碼轉(zhuǎn)化成二進(jìn)制目標(biāo)代碼文件p1.o和p2.o。目標(biāo)代碼是機(jī)器代碼的一種形式,它包含所有指令的二進(jìn)制表示,但是還沒有填入全局值的地址。最后,鏈接器將兩個(gè)目標(biāo)代碼文件與實(shí)現(xiàn)庫函數(shù)(例如printf)的代碼合并,并產(chǎn)生最終的可執(zhí)行代碼文件p(由命令行指示符-o p指定的)。可執(zhí)行代碼是我們要考慮的機(jī)器代碼的第二種形式,也就是處理器執(zhí)行的代碼格式。我們會(huì)在第7章更詳細(xì)地介紹這些不同形式的機(jī)器代碼之間的關(guān)系以及鏈接的過程。
1.機(jī)器級(jí)代碼
? ? ? ?正如在1.9.3節(jié)中講過的那樣,計(jì)算機(jī)系統(tǒng)使用了多種不同形式的抽象,利用更簡(jiǎn)單的抽象模型來隱藏實(shí)現(xiàn)的細(xì)節(jié)。對(duì)于機(jī)器級(jí)編程來說,其中兩種抽象尤為重要。第一種是由指令集體系結(jié)構(gòu)或指令集架構(gòu)(Instruction Set Architecture,ISA)來定義機(jī)器級(jí)程序的格式和行為,它定義了處理器狀態(tài)、指令的格式,以及每條指令對(duì)狀態(tài)的影響。大多數(shù)ISA,包括x86-64,將程序的行為描述成好像每條指令都是按順序執(zhí)行的,一條指令結(jié)束后,下一條再開始。處理器的硬件遠(yuǎn)比描述的精細(xì)復(fù)雜,它們并發(fā)地執(zhí)行許多指令,但是可以采取措施保證整體行為與ISA指定的順序執(zhí)行的行為完全一-致。第二種抽象是,機(jī)器級(jí)程序使用的內(nèi)存地址是虛擬地址,提供的內(nèi)存模型看上去是一個(gè)非常大的字節(jié)數(shù)組。存儲(chǔ)器系統(tǒng)的實(shí)際實(shí)現(xiàn)是將多個(gè)硬件存儲(chǔ)器和操作系統(tǒng)軟件組合起來,這會(huì)在第9章中講到。
? ? ? ?在整個(gè)編譯過程中,編譯器會(huì)完成大部分的工作,將把用C語言提供的相對(duì)比較抽象的執(zhí)行模型表示的程序轉(zhuǎn)化成處理器執(zhí)行的非常基本的指令。匯編代碼表示非常接近于機(jī)器代碼。與機(jī)器代碼的二進(jìn)制格式相比,匯編代碼的主要特點(diǎn)是它用可讀性更好的文本格式表示。能夠理解匯編代碼以及它與原始C代碼的聯(lián)系,是理解計(jì)算機(jī)如何執(zhí)行程序的關(guān)鍵一步。
? ? ? ?x86-64的機(jī)器代碼和原始的C代碼差別非常大。一些通常對(duì)C語言程序員隱藏的處理器狀態(tài)都是可見的:
? ? ?雖然C語言提供了一種模型,可以在內(nèi)存中聲明和分配各種數(shù)據(jù)類型的對(duì)象,但是機(jī)器代碼只是簡(jiǎn)單地將內(nèi)存看成一個(gè)很大的、按字節(jié)尋址的數(shù)組。C語言中的聚合數(shù)據(jù)類型,例如數(shù)組和結(jié)構(gòu),在機(jī)器代碼中用一組連續(xù)的字節(jié)來表示。即使是對(duì)標(biāo)量數(shù)據(jù)類型,匯編代碼也不區(qū)分有符號(hào)或無符號(hào)整數(shù),不區(qū)分各種類型的指針,甚至于不區(qū)分指針和整數(shù)。
? ? ? ?程序內(nèi)存包含:程序的可執(zhí)行機(jī)器代碼,操作系統(tǒng)需要的一些信息,用來管理過程調(diào)用和返回的運(yùn)行時(shí)棧,以及用戶分配的內(nèi)存塊(比如說用malloc庫函數(shù)分配的)。正如前面提到的,程序內(nèi)存用虛擬地址來尋址。在任意給定的時(shí)刻,只有有限的一部分虛擬地址被認(rèn)為是合法的。例如,x86-64的虛擬地址是由64位的字來表示的。在目前的實(shí)現(xiàn)中,這些地址的高16位必須設(shè)置為0,所以一個(gè)地址實(shí)際上能夠指定的是248或64TB范圍內(nèi)的一個(gè)字節(jié)。較為典型的程序只會(huì)訪問幾兆字節(jié)或幾千兆字節(jié)的數(shù)據(jù)。操作系統(tǒng)負(fù)責(zé)管理虛擬地址空間,將虛擬地址翻譯成實(shí)際處理器內(nèi)存中的物理地址。
? ? ? ?一條機(jī)器指令只執(zhí)行一個(gè)非常基本的操作。例如,將存放在寄存器中的兩個(gè)數(shù)字相加,在存儲(chǔ)器和寄存器之間傳送數(shù)據(jù),或是條件分支轉(zhuǎn)移到新的指令地址。編譯器必須產(chǎn)生這些指令的序列,從而實(shí)現(xiàn)(像算術(shù)表達(dá)式求值、循環(huán)或過程調(diào)用和返回這樣的)程序結(jié)構(gòu)。
旁注 不斷變化的生成代碼的格式在本書的表述中,我們給出的代碼是由特定版本的GCC在特定的命令行選項(xiàng)設(shè)置下產(chǎn)生的。如果你在自己的機(jī) 器上編譯代碼,很有可能用到其他的編譯器或者不同版本的GCC,因而會(huì)產(chǎn)生不同的代碼。支持GCC的開源社區(qū)一 直在修改代碼產(chǎn)生器,試圖根據(jù)微處理器制造商提供的不斷變化的代碼規(guī)則,產(chǎn)生更有效的代碼。本書示例的目標(biāo)是展示如何查看匯編代碼,并將它反向映射到高級(jí)編程語言中的結(jié)構(gòu)。你需要將這些技術(shù)應(yīng)用 到你的特定的編譯器產(chǎn)生的代碼格式上。2.代碼示例
假設(shè)我們寫了一個(gè)C語言代碼文件 mstore.c,包含如下的函數(shù)定義:
long mult2(long,long);
 void multstor(long x,long y,long*dest)
 {
 ????long t = mult2(x,y);
 ????*dest = t ;
 }
在命令行上使用“-S”選項(xiàng),就能看到C語言編譯器產(chǎn)生的匯編代碼:
linux>gcc -Og -S mstore.c
這會(huì)使GCC運(yùn)行編譯器,產(chǎn)生一個(gè)匯編文件mstore.s,但是不做其他進(jìn)一步的工作。(通常情況下,它還會(huì)繼續(xù)調(diào)用匯編器產(chǎn)生目標(biāo)代碼文件)。匯編代碼文件包含各種聲明,包括下面幾行:
上面代碼中每個(gè)縮進(jìn)去的行都對(duì)應(yīng)于一條機(jī)器指令。比如,pushq指令表示應(yīng)該將寄存器%rbx的內(nèi)容壓入程序棧中。這段代碼中已經(jīng)除去了所有關(guān)于局部變量名或數(shù)據(jù)類型的信息。
如果我們使用“-c”命令行選項(xiàng),GCC會(huì)編譯并匯編該代碼:
linux> gcc -Og -c mstore.c
這就會(huì)產(chǎn)生目標(biāo)代碼文件mstore.o,它是二進(jìn)制格式的,所以無法直接查看。1368字節(jié)的文件mstore.o中有一段14字節(jié)的序列,它的十六進(jìn)制表示為:
53 48 89 d3 e8 00 o0 o0 o0 48 89 03 5b c3
這就是上面列出的匯編指令對(duì)應(yīng)的目標(biāo)代碼。從中得到一個(gè)重要信息,即機(jī)器執(zhí)行的程序只是一個(gè)字節(jié)序列,它是對(duì)一系列指令的編碼。機(jī)器對(duì)產(chǎn)生這些指令的源代碼幾乎一無所知。
要查看機(jī)器代碼文件的內(nèi)容,有一類稱為反匯編器(disassembler)的程序非常有用。這些程序根據(jù)機(jī)器代碼產(chǎn)生一種類似于匯編代碼的格式。在Linux系統(tǒng)中,帶'-d'命令行標(biāo)志的程序OBJDUMP(表示“object dump")可以充當(dāng)這個(gè)角色:
linux> objdump -d mstore.o
結(jié)果如下(這里,我們?cè)谧筮呍黾恿诵刑?hào),在右邊增加了斜體表示的注解):
在左邊,我們看到按照前面給出的字節(jié)順序排列的14個(gè)十六進(jìn)制字節(jié)值,它們分成了若干組,每組有1~5個(gè)字節(jié)。每組都是一條指令,右邊是等價(jià)的匯編語言。
其中一些關(guān)于機(jī)器代碼和它的反匯編表示的特性值得注意:
?生成實(shí)際可執(zhí)行的代碼需要對(duì)一組目標(biāo)代碼文件運(yùn)行鏈接器,而這一組目標(biāo)代碼文件中必須含有一個(gè)main函數(shù)。假設(shè)在文件 main.c 中有下面這樣的函數(shù):
#include <stdio.h> void multstore(long ,long ,long*); int main(){long d;multstore(2,3,&d);printf("2*3 --> %ld\n",d);return 0; } long mult2(long a,long b){long s = a*b;return s; }然后,我們用如下方法生成可執(zhí)行文件prog:
linux> gcc -Og -o prog main.c mstore.c
反匯編器會(huì)抽取出各種代碼序列,包括下面這段:
這段代碼與 mstore.c 反匯編產(chǎn)生的代碼幾乎完全一樣。其中 一個(gè)主要的區(qū)別是左邊列出的地址不同——鏈接器將這段代碼的地址移到了一段不同的地址范圍中。 第二個(gè)不同之處在于鏈接器填上了 callq 指令調(diào)用函數(shù) mult2 需要使用的地址(反匯編代碼第 4 行)。鏈接器的任務(wù)之一就是為函數(shù)調(diào)用找到匹配的函數(shù)的可執(zhí)行代碼的位置。 最后一個(gè)區(qū)別是多了兩行代碼(第 8 和 9 行)。這兩條指令對(duì)程序沒有影響,因?yàn)樗鼈兂霈F(xiàn)在返回指令后面(第 7 行)。插入這些指令是為了使函數(shù)代碼變?yōu)?16 字節(jié),使得就存儲(chǔ)器系統(tǒng)性能而言,能更好地放置下一個(gè)代碼塊。
Notice :我自己編的和書上的差了一點(diǎn)點(diǎn),差了最后兩行nop的匯編。?
旁注 如何展示程序的字節(jié)表示要展示程序(比如說mstore)的二進(jìn)制目標(biāo)代碼,我們用反匯編器(后面會(huì)講到)確定該過程的代碼長(zhǎng)度是14 字節(jié)。然后,在文件mstore.o上運(yùn)行GNU調(diào)試工具GDB,輸入命令: (gdb) x/14xb multstore這條命令告訴GDB顯示(簡(jiǎn)寫為’x’)從函數(shù)multstore所處地址開始的14個(gè)十六進(jìn)制格式表示(也簡(jiǎn)寫為’x’) 的字節(jié)(簡(jiǎn)寫為‘b')。你會(huì)發(fā)現(xiàn),GDB有很多有用的特性可以用來分析機(jī)器級(jí)程序,我們會(huì)在3.10.2節(jié)中討論。?3.關(guān)于格式的注解
GCC產(chǎn)生的匯編代碼對(duì)我們來說有點(diǎn)兒難讀。一方面,它包含一些我們不需要關(guān)心的信息,另一方面,它不提供任何程序的描述或它是如何工作的描述。例如,假設(shè)我們用如下命令生成文件mstore.s。
linux>gcc -Og -s mstore.c
mstore.s的完整內(nèi)容如下:.file "3.2.2-mstore.c".text.globl multstor.type multstor, @function multstor: .LFB0:.cfi_startprocpushq %rbx.cfi_def_cfa_offset 16.cfi_offset 3, -16movq %rdx, %rbxcall mult2movq %rax, (%rbx)popq %rbx.cfi_def_cfa_offset 8ret.cfi_endproc .LFE0:.size multstor, .-multstor.ident "GCC: (GNU) 7.3.1 20180303 (Red Hat 7.3.1-5)".section .note.GNU-stack,"",@progbits所有以‘.’開頭的行都是指導(dǎo)匯編器和鏈接器工作的偽指令。我們通常可以忽略這些行。另一方面,也沒有關(guān)于指令的用途以及它們與源代碼之間關(guān)系的解釋說明。
為了更清楚地說明匯編代碼,我們用這樣一種格式來表示匯編代碼,它省略了大部分偽指令,但包括行號(hào)和解釋性說明。對(duì)于我們的示例,帶解釋的匯編代碼如下:
void multstore(long x, long y,long *dest) x in %rdi, y in %rsi , dest in %rdx 1??multstore: 2????pushq ??? %rbx? ? ? ? ? ? ? ? ? ? ? ??Save %rbx 3????movq ? %rdx,%rbx? ? ? ? ? ? ? ? ? Copy dest to %rbx 4????call ??? mult2? ? ? ? ? ? ? ? ? ? ? ? Call mult2(x,y) 5????movq ?? %rax,(%rbx) ?????? ??? Store result at *dest 6????popq ?? %rbx? ? ? ? ? ? ? ? ? ? ? ? ??Restore %rbx 7????ret? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Return???? ? ? ?通常我們只會(huì)給出與討論內(nèi)容相關(guān)的代碼行。每一行的左邊都有編號(hào)供引用,右邊是注釋,簡(jiǎn)單地描述指令的效果以及它與原始C語言代碼中的計(jì)算操作的關(guān)系。這是一種匯編語言程序員寫代碼的風(fēng)格。
? ? ? 我們還提供網(wǎng)絡(luò)旁注,為專門的機(jī)器語言愛好者提供一些資料。一個(gè)網(wǎng)絡(luò)旁注描述的是IA32機(jī)器代碼。有了x86-64的背景,學(xué)習(xí)IA32會(huì)相當(dāng)簡(jiǎn)單。另外一個(gè)網(wǎng)絡(luò)旁注簡(jiǎn)要描述了在C語言中插入?yún)R編代碼的方法。對(duì)于一些應(yīng)用程序,程序員必須用匯編代碼來訪問機(jī)器的低級(jí)特性。一種方法是用匯編代碼編寫整個(gè)函數(shù),在鏈接階段把它們和C函數(shù)組合起來。另一種方法是利用GCC的支持,直接在C程序中嵌入?yún)R編代碼。
旁注 ATT與Intel匯編代碼格式我們的表述是ATT(根據(jù)“AT&T”命名的,AT&T是運(yùn)營(yíng)貝爾實(shí)驗(yàn)室多年的公司)格式的匯編代碼,這是 GCC、OBJDUMP 和其他一些我們使用的工具的默認(rèn)格式。其他一些編程工具,包括 Microsoft的工具,以及來自Intel的文檔,其匯編代碼都是Intel格式 的。這兩種格式在許多方面有所不同。例如,使用下述命令行,GCC可以產(chǎn)生multstore函數(shù)的Intel格 式的代碼: linux> gcc -Og -S -masm=intel mstore.c 這個(gè)命令得到下列匯編代碼: multstore:push rbpmov rbx,rdxcall mult2mov QWORD PTR [rbx],raxpop rbxret 我們看到Intel和ATT格式在如下方面有所不同: 1.Intel代碼省略了指示大小的后綴。我們看到指令push和mov,而不是pushq和movq。 2.Intel代碼省略了寄存器名字前面的‘%’符號(hào),用的是rbx,而不是%rbx。 3.Intel代碼用不同的方式來描述內(nèi)存中的位置,例如是‘QWORD PTR[rbx]’而不是‘(%rbx)’。 4.在帶有多個(gè)操作數(shù)的指令情況下,列出操作數(shù)的順序相反。當(dāng)在兩種格式之間進(jìn)行轉(zhuǎn)換的時(shí)候,這一點(diǎn)非常令人困惑。雖然在我們的表述中不使用Intel格式,但是在來自Intel和Microsoft的文檔中,你會(huì)遇到它。 網(wǎng)絡(luò)旁注 ASM:EASM 把C程序和匯編代碼結(jié)合起來雖然C編譯器在把程序中表達(dá)的計(jì)算轉(zhuǎn)換到機(jī)器代碼方面表現(xiàn)出色,但是仍然有一些機(jī)器特性是C程序訪 問不到的。例如,每次 x86-64處理器執(zhí)行算術(shù)或邏輯運(yùn)算時(shí),如果得到的運(yùn)算結(jié)果的低8位中有偶數(shù)個(gè)1, 那么就會(huì)把一個(gè)名為PF的1位條件碼(condition code)標(biāo)志設(shè)置為1,否則就設(shè)置為0。這里的PF表示“parityflag(奇偶標(biāo)志)”。在C語言中計(jì)算這個(gè)信息需要至少7次移位、掩碼和異或運(yùn)算(參見習(xí)題2.65)。即使作為 每次算術(shù)或邏輯運(yùn)算的一部分,硬件都完成了這項(xiàng)計(jì)算,而C程序卻無法知道PF條件碼標(biāo)志的值。在程序中插 入幾條匯編代碼指令就能很容易地完成這項(xiàng)任務(wù)。在C程序中插入?yún)R編代碼有兩種方法。第一種是,我們可以編寫完整的函數(shù),放進(jìn)一個(gè)獨(dú)立的匯編代碼文 件中,讓匯編器和鏈接器把它和用C語言書寫的代碼合并起來。第二種方法是,我們可以使用GCC的內(nèi)聯(lián)匯編 (inline assembly)特性,用asm偽指令可以在C程序中包含簡(jiǎn)短的匯編代碼。這種方法的好處是減少了與機(jī) 器相關(guān)的代碼量。當(dāng)然,在C程序中包含匯編代碼使得這些代碼與某類特殊的機(jī)器相關(guān)(例如x86-64),所以只應(yīng)該在想要 的特性只能以此種方式才能訪問到時(shí)才使用它。總結(jié)
以上是生活随笔為你收集整理的【经典阅读】CSAPP-3.2-程序的机器级表示-程序编码的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 位移传感器的原理和选型方式
- 下一篇: 数据挖掘:降低汽油精制过程中的辛烷值损失
