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