一、概述
對于編譯原理,如果要系統的學習的話,知識點多的完全可以寫成一本書,推薦大家可以去閱讀一下《編譯原理》這本書,可以全面深入的學習編譯原理,很經典。接下來的文章我僅僅對GCC/G++編譯原理寫一些記錄。
其中gcc指令是對c語言檔案的編譯,g++指令是對c++檔案的編譯,其指令的原理其實是一樣的。
二、編譯流程
gcc指令編譯一般分為四個步驟及調用的指令:
- 預處理(preprocessing) :gcc -E
- 編譯(compilation) :gcc -S
- 彙編(assembly) :gcc -c對應調用的是as指令
- 連接配接(linking) :gcc對應調用的是ld 指令
先上一個醜陋的概念圖
源檔案,以下是hello.c代碼
#include<stdio.h>
int main(){
printf("hello world!");
return 0;
}
預處理階段
預處理階段主要處理#include和#define,它把#include包含進來的.h 檔案插入到#include所在的位置,把源程式中使用到的用#define定義的宏用實際的字元串代替,删除注釋。
執行指令,會生成hello.i檔案
$ gcc -E hello.c -o hello.i
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 29 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/_ansi.h" 1 3 4
# 10 "/usr/include/_ansi.h" 3 4
# 1 "/usr/include/newlib.h" 1 3 4
# 14 "/usr/include/newlib.h" 3 4
# 1 "/usr/include/_newlib_version.h" 1 3 4
# 15 "/usr/include/newlib.h" 2 3 4
# 11 "/usr/include/_ansi.h" 2 3 4
# 1 "/usr/include/sys/config.h" 1 3 4
# 1 "/usr/include/machine/ieeefp.h" 1 3 4
# 5 "/usr/include/sys/config.h" 2 3 4
# 1 "/usr/include/sys/features.h" 1 3 4
# 6 "/usr/include/sys/config.h" 2 3 4
# 234 "/usr/include/sys/config.h" 3 4
# 1 "/usr/include/cygwin/config.h" 1 3 4
# 235 "/usr/include/sys/config.h" 2 3 4
# 12 "/usr/include/_ansi.h" 2 3 4
# 30 "/usr/include/stdio.h" 2 3 4
.....此處忽略一千行
# 797 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
# 3 "hello.c"
int main(){
printf("hello world!");
return 0;
}
編譯階段
編譯階段首先檢查代碼的規範性,文法錯誤,最終把代碼翻譯成彙編語言
執行指令:
$ gcc -S hello.i -o hello.s
以下是生成的hello.s彙編代碼,有興趣的同學可以研究一下,對于反彙編,這一份代碼是必需看懂的^-^。
.file "hello.c"
.text
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "hello world!\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
leaq .LC0(%rip), %rcx
call printf
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (GNU) 7.3.0"
.def printf; .scl 2; .type 32; .endef
彙編階段
彙編階段把hello.s檔案翻譯成二進制機器指令,把hello.s檔案轉換成hello.o檔案,執行指令:
$ gcc -c hello.s -o hello.o
生成的檔案是二進制格式,普通人是看不懂了,這是一份讓機器了解的代碼
當然,我們也可以通過ida,od等工具反彙編得到彙編代碼
連結階段
很多人都會認為到了彙編階段産出的.o檔案就可以使用了,當然這個想法是錯誤的,還需要最後一個階段,就是連結階段(Linking),可以通俗的了解為關聯系統的api,我們的程式調用stdio.h中的printf函數,而這個标準的列印函數是由系統提供的,而連結階段就是告訴我們應用調用prinft的地方,該去哪裡調用這個函數的實作地方,有可能是系統的标準庫函數庫,也有可能是外面提供的函數庫,反正結果就是讓我們的應用知道在哪裡去找到這個函數。
執行指令,最終生成hello可執行的檔案:
$ gcc hello.o -o hello
函數庫一般分為靜态庫和動态庫兩種
- 靜态庫是指編譯連結時,把庫檔案的代碼全部加入到可執行檔案中,是以生成的檔案比較大,但在運作時也就不再需要庫檔案了,其字尾名一般為”.a”,如前面的應用,會把printf函數相關的依賴函數都會打包進來。
- 動态庫與之相反,在編譯連結時并沒有把庫檔案的代碼加入到可執行檔案中,而是在程式執行時由運作時連結檔案加載庫,這樣可以節省系統的開銷。動态庫一般字尾名為”.so”,gcc在編譯時預設使用動态庫,如果系統把printf函數不小心删除了,前面的應用在運作的時候就會報找不到libc.so.6庫的錯誤提示。
文章所有的例子代碼工程