大家肯定都知道計算機程序設計語言通常分為機器語言、匯編語言和高級語言三類。高級語言需要通過翻譯成機器語言才能執行,而翻譯的方式分為兩種,一種是編譯型,另一種是解釋型,因此我們基本上將高級語言分為兩大類,一種是編譯型語言,例如C,C++,Java,另一種是解釋型語言,例如Python、Ruby、MATLAB 、JavaScript。 ????本文將介紹如何將高層的C/C++語言編寫的程序轉換成為處理器能夠執行的二進制代碼的過程,包括四個步驟:
預處理(Preprocessing)
編譯(Compilation)
匯編(Assembly)
鏈接(Linking)
GCC 工具鏈介紹 ? ????通常所說的GCC是GUN Compiler Collection的簡稱,是Linux系統上常用的編譯工具。GCC工具鏈軟件包括GCC、Binutils、C運行庫等。 GCC ??? GCC(GNU C Compiler)是編譯工具。本文所要介紹的將C/C++語言編寫的程序轉換成為處理器能夠執行的二進制代碼的過程即由編譯器完成。 Binutils ????一組二進制程序處理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。這一組工具是開發和調試不可缺少的工具,分別簡介如下:
addr2line:用來將程序地址轉換成其所對應的程序源文件及所對應的代碼行,也可以得到所對應的函數。該工具將幫助調試器在調試的過程中定位對應的源代碼位置。
as:主要用于匯編,有關匯編的詳細介紹請參見后文。
ld:主要用于鏈接,有關鏈接的詳細介紹請參見后文。
ar:主要用于創建靜態庫。為了便于初學者理解,在此介紹動態庫與靜態庫的概念:
如果要將多個.o目標文件生成一個庫文件,則存在兩種類型的庫,一種是靜態庫,另一種是動態庫。
在windows中靜態庫是以 .lib 為后綴的文件,共享庫是以 .dll 為后綴的文件。在linux中靜態庫是以.a為后綴的文件,共享庫是以.so為后綴的文件。
靜態庫和動態庫的不同點在于代碼被載入的時刻不同。靜態庫的代碼在編譯過程中已經被載入可執行程序,因此體積較大。共享庫的代碼是在可執行程序運行時才載入內存的,在編譯過程中僅簡單的引用,因此代碼體積較小。在Linux系統中,可以用ldd命令查看一個可執行程序依賴的共享庫。
如果一個系統中存在多個需要同時運行的程序且這些程序之間存在共享庫,那么采用動態庫的形式將更節省內存。
ldd:可以用于查看一個可執行程序依賴的共享庫。
objcopy:將一種對象文件翻譯成另一種格式,譬如將.bin轉換成.elf、或者將.elf轉換成.bin等。
objdump:主要的作用是反匯編。有關反匯編的詳細介紹,請參見后文。
readelf:顯示有關ELF文件的信息,請參見后文了解更多信息。
size:列出可執行文件每個部分的尺寸和總尺寸,代碼段、數據段、總大小等,請參見后文了解使用size的具體使用實例。
C運行庫 ??? C語言標準主要由兩部分組成:一部分描述C的語法,另一部分描述C標準庫。C標準庫定義了一組標準頭文件,每個頭文件中包含一些相關的函數、變量、類型聲明和宏定義,譬如常見的printf函數便是一個C標準庫函數,其原型定義在stdio頭文件中。 ??? C語言標準僅僅定義了C標準庫函數原型,并沒有提供實現。因此,C語言編譯器通常需要一個C運行時庫(C Run Time Libray,CRT)的支持。C運行時庫又常簡稱為C運行庫。與C語言類似,C++也定義了自己的標準,同時提供相關支持庫,稱為C++運行時庫。 ? 準備工作 ? ????由于GCC工具鏈主要是在Linux環境中進行使用,因此本文也將以Linux系統作為工作環境。為了能夠演示編譯的整個過程,本節先準備一個C語言編寫的簡單Hello程序作為示例,其源代碼如下所示:
#include?? 編譯過程 ? 1. 預處理 ????預處理的過程主要包括以下過程:? //此程序很簡單,僅僅打印一個Hello World的字符串。 int?main(void) { ????printf("Hello?World!? "); ????return?0; }
將所有的#define刪除,并且展開所有的宏定義,并且處理所有的條件預編譯指令,比如#if #ifdef #elif #else #endif等。
處理#include預編譯指令,將被包含的文件插入到該預編譯指令的位置。
刪除所有注釋“//”和“/* */”。
添加行號和文件標識,以便編譯時產生調試用的行號及編譯錯誤警告行號。
保留所有的#pragma編譯器指令,后續編譯過程需要使用它們。
使用gcc進行預處理的命令如下:
?
?
$?gcc?-E?hello.c?-o?hello.i?//?將源文件hello.c文件預處理生成hello.i //?GCC的選項-E使GCC在進行完預處理后即停止? ??? hello.i文件可以作為普通文本文件打開進行查看,其代碼片段如下所示:
//?hello.i代碼片段 extern?void?funlockfile?(FILE?*__stream)?__attribute__?((__nothrow__?,?__leaf__)); #?942?"/usr/include/stdio.h"?3?4 #?2?"hello.c"?2 #?3?"hello.c" int?main(void) { ????printf("Hello?World!"?" "); ????return?0; }? 2. 編譯 ????編譯過程就是對預處理完的文件進行一系列的詞法分析,語法分析,語義分析及優化后生成相應的匯編代碼。 ????使用gcc進行編譯的命令如下:
$?gcc?-S?hello.i?-o?hello.s?//?將預處理生成的hello.i文件編譯生成匯編程序hello.s //?GCC的選項-S使GCC在執行完編譯后停止,生成匯編程序? ????上述命令生成的匯編程序hello.s的代碼片段如下所示,其全部為匯編代碼。
//?hello.s代碼片段 main: .LFB0: ????.cfi_startproc ????pushq???%rbp ????.cfi_def_cfa_offset?16 ????.cfi_offset?6,?-16 ????movq????%rsp,?%rbp ????.cfi_def_cfa_register?6 ????movl????$.LC0,?%edi ????call????puts ????movl????$0,?%eax ????popq????%rbp ????.cfi_def_cfa?7,?8 ????ret ????.cfi_endproc? 3. 匯編 ????匯編過程調用對匯編代碼進行處理,生成處理器能識別的指令,保存在后綴為.o的目標文件中。由于每一個匯編語句幾乎都對應一條處理器指令,因此,匯編相對于編譯過程比較簡單,通過調用Binutils中的匯編器as根據匯編指令和處理器指令的對照表一一翻譯即可。 ????當程序由多個源代碼文件構成時,每個文件都要先完成匯編工作,生成.o目標文件后,才能進入下一步的鏈接工作。注意:目標文件已經是最終程序的某一部分了,但是在鏈接之前還不能執行。???? ????使用gcc進行匯編的命令如下:
$?gcc?-c?hello.s?-o?hello.o?//?將編譯生成的hello.s文件匯編生成目標文件hello.o //?GCC的選項-c使GCC在執行完匯編后停止,生成目標文件 //?或者直接調用as進行匯編 $?as?-c?hello.s?-o?hello.o?//使用Binutils中的as將hello.s文件匯編生成目標文件? ????注意:hello.o目標文件為ELF(Executable and Linkable Format)格式的可重定向文件。 4. 鏈接 ????鏈接也分為靜態鏈接和動態鏈接,其要點如下:
靜態鏈接是指在編譯階段直接把靜態庫加入到可執行文件中去,這樣可執行文件會比較大。鏈接器將函數的代碼從其所在地(不同的目標文件或靜態鏈接庫中)拷貝到最終的可執行程序中。為創建可執行文件,鏈接器必須要完成的主要任務是:符號解析(把目標文件中符號的定義和引用聯系起來)和重定位(把符號定義和內存地址對應起來然后修改所有對符號的引用)。
動態鏈接則是指鏈接階段僅僅只加入一些描述信息,而程序執行時再從系統中把相應動態庫加載到內存中去。
在Linux系統中,gcc編譯鏈接時的動態庫搜索路徑的順序通常為:首先從gcc命令的參數-L指定的路徑尋找;再從環境變量LIBRARY_PATH指定的路徑尋址;再從默認路徑/lib、/usr/lib、/usr/local/lib尋找。
在Linux系統中,執行二進制文件時的動態庫搜索路徑的順序通常為:首先搜索編譯目標代碼時指定的動態庫搜索路徑;再從環境變量LD_LIBRARY_PATH指定的路徑尋址;再從配置文件/etc/ld.so.conf中指定的動態庫搜索路徑;再從默認路徑/lib、/usr/lib尋找。
在Linux系統中,可以用ldd命令查看一個可執行程序依賴的共享庫。
由于鏈接動態庫和靜態庫的路徑可能有重合,所以如果在路徑中有同名的靜態庫文件和動態庫文件,比如libtest.a和libtest.so,gcc鏈接時默認優先選擇動態庫,會鏈接libtest.so,如果要讓gcc選擇鏈接libtest.a則可以指定gcc選項-static,該選項會強制使用靜態庫進行鏈接。以Hello World為例:
如果使用命令“gcc hello.c -o hello”則會使用動態庫進行鏈接,生成的ELF可執行文件的大小(使用Binutils的size命令查看)和鏈接的動態庫(使用Binutils的ldd命令查看)如下所示:
$?gcc?hello.c?-o?hello $?size?hello??//使用size查看大小 ???text????data?????bss?????dec?????hex?filename ???1183?????552???????8????1743?????6cf?????hello $?ldd?hello?//可以看出該可執行文件鏈接了很多其他動態庫,主要是Linux的glibc動態庫 ????????linux-vdso.so.1?=>??(0x00007fffefd7c000) ????????libc.so.6?=>?/lib/x86_64-linux-gnu/libc.so.6?(0x00007fadcdd82000) ????????/lib64/ld-linux-x86-64.so.2?(0x00007fadce14c000)
如果使用命令“gcc -static hello.c -o hello”則會使用靜態庫進行鏈接,生成的ELF可執行文件的大小(使用Binutils的size命令查看)和鏈接的動態庫(使用Binutils的ldd命令查看)如下所示:
$?gcc?-static?hello.c?-o?hello $?size?hello?//使用size查看大小 ?????text????data?????bss?????dec?????hex?filename ?823726????7284????6360??837370???cc6fa?????hello?//可以看出text的代碼尺寸變得極大 $?ldd?hello ???????not?a?dynamic?executable?//說明沒有鏈接動態庫????鏈接器鏈接后生成的最終文件為ELF格式可執行文件,一個ELF可執行文件通常被鏈接為不同的段,常見的段譬如.text、.data、.rodata、.bss等段。 ? 分析ELF文件 ? 1. ELF文件的段 ??? ELF文件格式如下圖所示,位于ELF Header和Section Header Table之間的都是段(Section)。一個典型的ELF文件包含下面幾個段:
.text:已編譯程序的指令代碼段。
.rodata:ro代表read only,即只讀數據(譬如常數const)。
.data:已初始化的C程序全局變量和靜態局部變量。
.bss:未初始化的C程序全局變量和靜態局部變量。
.debug:調試符號表,調試器用此段的信息幫助調試。
可以使用readelf -S查看其各個section的信息如下:
$?readelf?-S?hello There?are?31?section?headers,?starting?at?offset?0x19d8: Section?Headers: ??[Nr]?Name??????????????Type?????????????Address???????????Offset ???????Size??????????????EntSize??????????Flags??Link??Info??Align ??[?0]???????????????????NULL?????????????0000000000000000??00000000 ???????0000000000000000??0000000000000000???????????0?????0?????0 …… ??[11]?.init?????????????PROGBITS?????????00000000004003c8??000003c8 ???????000000000000001a??0000000000000000??AX???????0?????0?????4 …… ??[14]?.text?????????????PROGBITS?????????0000000000400430??00000430 ???????0000000000000182??0000000000000000??AX???????0?????0?????16 ??[15]?.fini?????????????PROGBITS?????????00000000004005b4??000005b4 ……? 2. 反匯編ELF ????由于ELF文件無法被當做普通文本文件打開,如果希望直接查看一個ELF文件包含的指令和數據,需要使用反匯編的方法。 ????使用objdump -D對其進行反匯編如下:
$?objdump?-D?hello …… 0000000000400526?? ????使用objdump -S將其反匯編并且將其C語言源代碼混合顯示出來::??//?main標簽的PC地址 //PC地址:指令編碼??????????????????指令的匯編格式 ??400526:????55??????????????????????????push???%rbp? ??400527:????48?89?e5????????????????mov????%rsp,%rbp ??40052a:????bf?c4?05?40?00??????????mov????$0x4005c4,%edi ??40052f:????e8?cc?fe?ff?ff??????????callq??400400? ??400534:????b8?00?00?00?00??????????mov????$0x0,%eax ??400539:????5d??????????????????????pop????%rbp ??40053a:????c3??????????????????????????retq??? ??40053b:????0f?1f?44?00?00??????????nopl???0x0(%rax,%rax,1) ……
$?gcc?-o?hello?-g?hello.c?//要加上-g選項 $?objdump?-S?hello …… 0000000000400526?? 編輯:黃飛: #include? int?main(void) { ????400526:????55??????????????????push???%rbp ????400527:????48?89?e5????????????mov????%rsp,%rbp ????printf("Hello?World!"?" "); ????40052a:????bf?c4?05?40?00??????mov????$0x4005c4,%edi ????40052f:????e8?cc?fe?ff?ff??????callq??400400? ????return?0; ????400534:????b8?00?00?00?00??????mov????$0x0,%eax } ????400539:????5d??????????????????pop????%rbp ????40053a:????c3??????????????????retq??? ????40053b:????0f?1f?44?00?00??????nopl???0x0(%rax,%rax,1) ……
?
?
評論