通常高級語言程序編譯的過程:預處理、編譯、彙編、連接。gcc在後臺實際上也經歷了這幾個過程,咱們能夠經過-v參數查看它的編譯細節,若是想看某個具體的編譯過程,則能夠分別使用-E,-S,-c和 -O,對應的後臺工具則分別爲cpp,cc1,as,ld。下面咱們將逐步分析這幾個過程以及相關的內容,諸如語法檢查、代碼調試、彙編語言等。html
1、預處理java
預處理是C語言程序從源代碼變成可執行程序的第一步,主要是C語言編譯器對各類預處理命令進行處理,包括頭文件的包含、宏定義的擴展、條件編譯的選擇等。打印出預處理以後的結果:gcc -E hello.c 或者 cpp hello.c這樣咱們就能夠看到源代碼中的各類預處理命令是如何被解釋的,從而方便理解和查錯。linux
gcc調用了cpp的(雖然咱們經過gcc的-v僅看到cc1),cpp即The C Preprocessor,主要用來預處理宏定義、文件包含、條件編譯等。下面介紹它的一個比較重要的選項-D。在命令行定義宏:gcc –Dmacro=1 hello.c 或者 cpp –Dmacro=1 hello.c等同於在文件的開頭定義宏,即#define maco,可是在命令行定義更靈活。例如,在源代碼中有這些語句:c++
#ifdef DEBUG printf("this code is for debuggingn"); #endif
2、編譯程序員
編譯以前,C語言編譯器會進行詞法分析、語法分析(-fsyntax-only),接着會把源代碼翻譯成中間語言,即彙編語言。若是想看到這個中間結果,能夠用-S選項。編程
編譯程序工做時,先分析,後綜合,從而獲得目標程序。所謂分析,是指詞法分析和語法分析;所謂綜合是指代碼優化,存儲分配和 代碼生成。爲了完成這些分析綜合任務,編譯程序採用對源程序進行屢次掃描的辦法,每次掃描集中完成一項或幾項任務,也有一項任務分散到幾回掃描去完成的。 下面舉一個四遍掃描的例子:第一遍掃描作詞法分析;第二遍掃描作語法分析;第三遍掃描作代碼優化和存儲分配;第四遍掃描作代碼生成。 ubuntu
值得一提的是,大多數的編譯程序直接產生機器語言的目標代碼,造成可執行的目標文件,但也有的編譯程序則先產生彙編語言一級的符號代碼文件,而後再調用匯編程序進行翻譯加工處理,最後產生可執行的機器語言目標文件。 sass
語法檢查以後是翻譯動做,gcc提供了一個優化選項-O,以便根據不一樣的運行平臺和用戶要求產生通過優化的彙編代碼。例如,app
$ gcc -o hello hello.c #採用默認選項,不優化
$ gcc -O2 -o hello2 hello.c #優化等次是2
$ gcc -Os -o hellos hello.c #優化目標代碼的大小編輯器
$ time ./hello #查看代碼運行時間
hello, world
根據上面的簡單演示,能夠看出gcc有不少不一樣的優化選項,主要看用戶的需求了,目標代碼的大小和效率之間貌似存在一個「糾纏」,須要開發人員本身權衡。
下面咱們經過-S選項來看看編譯出來的中間結果,彙編語言,仍是以以前那個hello.c爲例。
$ gcc -S hello.c #默認輸出是hello.s,可本身指定 $ cat hello.s cat hello.s .file "hello.c" .section .rodata .LC0: .string "hello, world" .text .globl main .type main, @function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $4, %esp movl $.LC0, (%esp) call puts movl $0, %eax addl $4, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)" .section .note.GNU-stack,"",@progbits
和intel的彙編語法不太同樣,這裏用的是AT&T語法格式。這裏須要補充的是,在寫C語言代碼時,若是可以對編譯器比較熟悉(工做原理和一些細節)的話,可能會頗有幫助。包括這裏的優化選項(有些優化選項可能在彙編時採用)和可能的優化措施。
3、彙編
把做爲中間結果的彙編代碼翻譯成了機器代碼,即目標代碼,不過它還不能夠運行。若是要產生這一中間結果,可用gcc的-c選項,固然,也可經過as命令_彙編_彙編語言源文件來產生。
$ file hello.s
hello.s: ASCII assembler program text
$ gcc -c hello.s #用gcc把彙編語言編譯成目標代碼
$ file hello.o #file命令能夠用來查看文件的類型
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
$as -o hello.o hello.s #用as把彙編語言編譯成目標代碼
$ file hello.o
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
gcc和as默認產生的目標代碼都是ELF格式的,所以這裏主要討論ELF格式的目標代碼。目標代碼再也不是普通的文本格式,沒法直接經過文本編輯器瀏覽,須要一些專門的工具。
binutils(GNU Binary Utilities)的不少工具都採用這個庫來操做目標文件,這類工具備objdump, objcopy, nm, strip等,不過另一款很是優秀的分析工具readelf並非基於這個庫,因此你也應該能夠直接用elf.h頭文件中定義的相關結構來操做ELF文件。
ELF文件的結構:
1. ELF Header (ELF文件頭)說明了文件的類型,大小,運行平臺,節區數目等。
2. Porgram Headers Table (程序頭表,實際上叫段表好一些,用於描述可執行文件和可共享庫)
Section 1
Section 2
...
3. Section Headers Table(節區頭部表,用於連接可重定位文件成可執行文件或共享庫)
能夠分別經過 readelf文件的-h,-l和-S參數查看ELF文件頭(ELF Header)、程序頭部表(Program Headers Table,段表)和節區表(Section Headers Table)。
下面經過這幾段代碼來演示經過readelf -h參數查看ELF的不一樣類型。期間將演示如何建立動態鏈接庫(便可共享文件)、靜態鏈接庫,並比較它們的異同。
$ gcc -c myprintf.c test.c #編譯產生兩個目標文件myprintf.o和test.o,它們都是可重定位文件(REL)
$ readelf -h test.o | grep Type
Type: REL (Relocatable file)
$ readelf -h myprintf.o | grep Type
Type: REL (Relocatable file)
$ gcc -o test myprintf.o test.o #根據目標代碼鏈接產生可執行文件,這裏的文件類型是可執行的(EXEC)
$ readelf -h test | grep Type
Type: EXEC (Executable file)
$ ar rcsv libmyprintf.a myprintf.o #用ar命令建立一個靜態鏈接庫
$ readelf -h libmyprintf.a | grep Type #所以,使用靜態鏈接庫和可重定位文件同樣,它們之間惟一不一樣是前者能夠是多個可重定位文件的「集合」。
Type: REL (Relocatable file)
$ gcc -o test test.o -llib -L./ #能夠直接鏈接進去,也可使用-l參數,-L指定庫的搜索路徑
$ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0 #編譯產生動態連接庫,並支持major和minor版本號,動態連接庫類型爲DYN
$ ln -sf libmyprintf.so.0.0 libmyprintf.so.0
$ ln -sf libmyprintf.so.0 libmyprintf.so
$ readelf -h libmyprintf.so | grep Type
Type: DYN (Shared object file)
$ gcc -o test test.o -llib -L./ #編譯時和靜態鏈接庫相似,可是執行時須要指定動態鏈接庫的搜索路徑
$ LD_LIBRARY_PATH=./ ./test #LD_LIBRARY_PATH爲動態連接庫的搜索路徑
$ gcc -static -o test test.o -llib -L./ #在不指定static時會優先使用動態連接庫,指定時則阻止使用動態鏈接庫這個時候會把全部靜態鏈接庫文件加入到可執行文件中.
可重定位文件自己不能夠運行,僅僅是做爲可執行文件、靜態鏈接庫(也是可重定位文件)、動態鏈接庫的 「組件」。
下面來看看ELF文件的主體內容,節區(Section)。ELF文件具備很大的靈活性,它經過文件頭組織整個文件的整體結構,經過節區表 (Section Headers Table)和程序頭(Program Headers Table或者叫段表)來分別描述可重定位文件和可執行文件。在可重定位文件中,節區表描述的就是各類節區自己;而在可執行文件中,程序頭描述的是由各個節區組成的段(Segment),以便程序運行時動態裝載器知道如何對它們進行內存映像,從而方便程序加載和運行。
能夠經過readelf的-S參數查看ELF的節區。先來看看可重定位文件的節區信息,經過節區表來查看:
$ gcc -c myprintf.c #默認編譯好myprintf.c,將產生一個可重定位的文件myprintf.o
$ readelf -S myprintf.o #經過查看myprintf.o的節區表查看節區信息
There are 11 section headers, starting at offset 0xc0:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000018 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000334 000010 08 9 1 4
[ 3] .data PROGBITS 00000000 00004c 000000 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 00004c 000000 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 00004c 00000e 00 A 0 0 1
[ 6] .comment PROGBITS 00000000 00005a 000012 00 0 0 1
[ 7] .note.GNU-stack PROGBITS 00000000 00006c 000000 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 00006c 000051 00 0 0 1
[ 9] .symtab SYMTAB 00000000 000278 0000a0 10 10 8 4
[10] .strtab STRTAB 00000000 000318 00001a 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
$ objdump -d -j .text myprintf.o #這裏是程序指令部分,用objdump的-d選項能夠看到反編譯的結果,-j指定須要查看的節區
myprintf.o: file format elf32-i386
Disassembly of section .text:
00000000 : 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 ec 0c sub $0xc,%esp 9: 68 00 00 00 00 push $0x0 e: e8 fc ff ff ff call f 13: 83 c4 10 add $0x10,%esp 16: c9 leave 17: c3 ret
$ readelf -r myprintf.o #用-r選項能夠看到有關重定位的信息,這裏有兩部分須要重定位
Relocetion section '.rel.text' at offset 0x334 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0000000a 00000501 R_386_32 00000000 .rodata
0000000f 00000902 R_386_PC32 00000000 puts
$ readelf -x .rodata myprintf.o #.rodata節區包含只讀數據,即咱們要打印的hello, world!.
Hex dump of section '.rodata':
0x00000000 68656c6c 6f2c2077 6f726c64 2100 hello, world!.
$ readelf -x .data myprintf.o #沒有這個節區,.data應該包含一些初始化的數據
Section '.data' has no data to dump.
$ readelf -x .bss myprintf.o #也沒有這個節區,.bss應該包含一些未初始化的數據,程序默認初始爲0
Section '.bss' has no data to dump.
$ readelf -x .comment myprintf.o #是一些註釋,能夠看到是是GCC的版本信息
Hex dump of section '.comment':
0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1.
0x00000010 3200 2.
$ readelf -x .note.GNU-stack myprintf.o #這個也沒有內容
Section '.note.GNU-stack' has no data to dump.
$ readelf -x .shstrtab myprintf.o #包括全部節區的名字
Hex dump of section '.shstrtab':
0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
0x00000010 002e7368 73747274 6162002e 72656c2e ..shstrtab..rel.
0x00000020 74657874 002e6461 7461002e 62737300 text..data..bss.
0x00000030 2e726f64 61746100 2e636f6d 6d656e74 .rodata..comment
0x00000040 002e6e6f 74652e47 4e552d73 7461636b ..note.GNU-stack
0x00000050 00 .
$ readelf –x .symtab myprintf.o #符號表,包括全部用到的相關符號信息,如函數名、變量名
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS myprintf.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 6
8: 00000000 24 FUNC GLOBAL DEFAULT 1 myprintf
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND puts
$ readelf -x .strtab myprintf.o #字符串表,用到的字符串,包括文件名、函數名、變量名等。
Hex dump of section '.strtab':
0x00000000 006d7970 72696e74 662e6300 6d797072 .myprintf.c.mypr
0x00000010 696e7466 00707574 7300 intf.puts.
從上表能夠看出,對於可重定位文件,會包含這些基本節區.text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab和.strtab。
看一看myprintf.c產生的彙編代碼。
$ gcc -S myprintf.c
$ cat myprintf.s .file "myprintf.c" .section .rodata .LC0: .string "hello, world!" .text .globl myprintf .type myprintf, @function myprintf: pushl %ebp movl %esp, %ebp subl $8, %esp subl $12, %esp pushl $.LC0 call puts addl $16, %esp leave ret .size myprintf, .-myprintf .ident "GCC: (GNU) 4.1.2" .section .note.GNU-stack,"",@progbits
4、連接
連接是處理可重定位文件,把它們的各類符號引用和符號定義轉換爲可執行文件中的合適信息(通常是虛擬內存地址)的過程。連接又分爲靜態連接和動態連接,前者是程序開發階段程序員用ld(gcc實際上在後臺調用了ld)靜態連接器手動連接的過程,而動態連接則是程序運行期間系統調用動態連接器(ld-linux.so)自動連接的過程。好比,若是連接到可執行文件中的是靜態鏈接庫libmyprintf.a,那麼.rodata節區在連接後須要被重定位到一個絕對的虛擬內存地址,以便程序運行時可以正確訪問該節區中的字符串信息。而對於puts,由於它是動態鏈接庫libc.so中定義的函數,因此會在程序運行時經過動態符號連接找出puts函數在內存中的地址,以便程序調用該函數。
靜態連接過程主要是把可重定位文件依次讀入,分析各個文件的文件頭,進而依次讀入各個文件的節區,並計算各個節區的虛擬內存位置,對一些須要重定位的符號進行處理,設定它們的虛擬內存地址等,並最終產生一個可執行文件或者是動態連接庫。這個連接過程是經過ld來完成的,ld在連接時使用了一個連接腳本(linker scripq),該連接腳本處理連接的具體細節。這裏主要介紹可重定位文件中的節區(節區表描述的)和可執行文件中段(程序頭描述的)的對應關係以及gcc編譯時採用的一些默認連接選項。
下面先來看看可執行文件的節區信息,經過程序頭(段表)來查看:
=======================================================================
$ readelf -S test.o #爲了比較,先把test.o的節區表也列出
There are 10 section headers, starting at offset 0xb4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000024 00 AX 0 0 4
[ 2] .rel.text REL 00000000 0002ec 000008 08 8 1 4
[ 3] .data PROGBITS 00000000 000058 000000 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000058 000000 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 000058 000012 00 0 0 1
[ 6] .note.GNU-stack PROGBITS 00000000 00006a 000000 00 0 0 1
[ 7] .shstrtab STRTAB 00000000 00006a 000049 00 0 0 1
[ 8] .symtab SYMTAB 00000000 000244 000090 10 9 7 4
[ 9] .strtab STRTAB 00000000 0002d4 000016 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
=======================================================================
$ gcc -o test test.o libmyprintf.o
$ readelf -l test #咱們發現,test和test.o,libmyprintf.o相比,多了不少節區,如.interp和.init等
Elf file type is EXEC (Executable file)
Entry point 0x80482b0
There are 7 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x0047c 0x0047c R E 0x1000
LOAD 0x00047c 0x0804947c 0x0804947c 0x00104 0x00108 RW 0x1000
DYNAMIC 0x000490 0x08049490 0x08049490 0x000c8 0x000c8 RW 0x4
NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
上表給出了可執行文件的以下幾個段(segment),
PHDR: 給出了程序表自身的大小和位置,不能出現一次以上。
INTERP: 由於程序中調用了puts(在動態連接庫中定義),使用了動態鏈接庫,所以須要動態裝載器/連接器(ld-linux.so)
LOAD: 包括程序的指令,.text等節區都映射在該段,只讀(R)
LOAD: 包括程序的數據,.data, .bss等節區都映射在該段,可讀寫(RW)
DYNAMIC: 動態連接相關的信息,好比包含有引用的動態鏈接庫名字等信息
NOTE: 給出一些附加信息的位置和大小
GNU_STACK: 這裏爲空,應該是和GNU相關的一些信息
這裏的段可能包括以前的一個或者多個節區,也就是說通過連接以後原來的節區被重排了,並映射到了不一樣的段,這些段將告訴系統應該如何把它加載到內存中。這些新的節區來自哪裏?它們的做用是什麼呢?先來經過gcc的-v參數看看它的後臺連接過程。
=======================================================================
$ gcc -v -o test test.o myprintf.o #把可重定位文件連接成可執行文件Reading specs from /usr/lib/gcc/i486-slackware-linux/4.1.2/specsTarget: i486-slackware-linuxConfigured with: ../gcc-4.1.2/configure --prefix=/usr --enable-shared --enable-languages=ada,c,c++,fortran,java,objc --enable-threads=posix --enable-__cxa_atexit --disable-checking --with-gnu-ld --verbose --with-arch=i486 --target=i486-slackware-linux --host=i486-slackware-linuxThread model: posixgcc version 4.1.2 /usr/libexec/gcc/i486-slackware-linux/4.1.2/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/../crt1.o /usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/../crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/http://www.cnblogs.com/i486-slackware-linux/lib -L/usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/.. test.o myprintf.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/../crtn.o