GCC原始碼分析(摘)
摘自https://blog.csdn.net/sonicling/article/details/6702031,https://blog.csdn.net/sonicling/article/details/670615,https://blog.csdn.net/sonicling/article/details/7915301,https://blog.csdn.net/sonicling/article/details/7916931,https://blog.csdn.net/sonicling/article/details/8246231,感謝分享。
以gcc-4.5.2為例。
GCC是一個編譯驅動器,驅動cc1、as和ld三個部件完成編譯、彙編和連線的工作。cc1將C語言原始檔編譯為彙編檔案(.s)。而將彙編程式碼轉換為二進位制指令的工作由AS完成,生成大家都很熟悉的物件檔案(.o);生成的這些物件檔案再由AR程式打包成靜態庫(.a),或者由LD程式連線成可執行程式(elf、.so或其他格式)。而LD就是所謂的聯結器。AS、AR、LD是屬於另外一個叫做binutils的軟體包的程式,所以要讓GCC能夠有效運作起來,除了在系統中安裝GCC外,還要安裝binutils才行。
以下是cc1、as、ld各司其責的配合完成一個編譯過程。
gcc test.c -S -o test.S
as test.S -o test.o
ld test.o -o test
通常所用的“gcc -c”就相當於“gcc -S” + as,而對於編譯單個原始檔一步到位生成可執行“gcc test.c -o test”相當於上面三個步驟的組合,中間檔案被放置在臨時目錄下。
從這一篇開始,我們將從原始碼的角度來分析GCC如何完成對C語言原始檔的處理。GCC的內部構架在GCC Internals(搜“gccint.pdf”,GNU Compiler Collection Internals
GCC的原始碼檔案非常多,總數大約有好幾萬。但是很多都是testsuite和lib。首先我們除去所有的testsuite目錄,然後lib打頭的目錄也可以基本上不看,那是各程式語言的gcc版標準庫和專為某種語言的編譯而設計的庫。我們只分析C語言的話,只用看其中的libcpp,它包含了C/C++的詞法分析和預處理。剩下的GCC原始碼大多集中在config、gcc兩個目錄下。
config目錄是Makefile為各跨平臺編譯準備的配置目錄。
gcc目錄下除去gcc/config目錄外的其他檔案是各個語言的編譯器前端原始檔,一般放在各自語言命名的目錄下,例如cp(C++)、java、fortran等。唯一例外的是C語言,它的前端原始檔同GCC的通用檔案(包括中間表示、中間優化等)一起,散放在gcc目錄下。gcc/config目錄是gcc在各種硬體或作業系統平臺下的後端原始檔,負責把GCC生成的中間表示轉換為各平臺相關的機器碼、位元組碼或其他目標語言。那我們可以從gcc的原始碼組織上大致看出gcc之所以能支援眾多前端和後端的原因,它將各種語言的原始檔按照各自的方法分析完之後,表示為由GENERIC、GIMPLE、RTL組成的統一的中間結構,再由各種後端將統一的結構轉換為各自平臺對應的目標語言。
詞法分析,通俗講,就是給原始檔斷詞。我們將原始檔看作一個字元流,並交由詞法分析器進行斷詞,詞法分析器必須能夠輸出一個一個的詞,也叫做記號(token),每個記號至少有三個屬性:
1.值:即斷出的那一段字串
2.型別:關鍵字、識別符號、文字常量、符號等
3.位置:這個記號在當前檔案的第幾行,用於報錯。
在《編譯原理》裡面,詞法分析是和NFA、DFA、正則表示式聯絡起來的,他們屬於III型語言。根據詞法定義,我們手頭已經有很多工具可以實現詞法分析器的自動構造,這些自動構造的程式碼無一例外的使用了DFA的概念,即構造出來的詞法分析器一定是一個DFA,裡面包含了初始狀態、終結狀態和狀態的轉移,而這些狀態都是自動構造中抽象出來的符號或者數字,一般人很難看出這些狀態在詞法定義中的位置。所以這也是自動構造的缺點——貪圖構造的方便,一定帶來修改的成本。
而GCC的詞法分析是手工構造的,實現在libcpp/lex.c檔案中,其中最重要的那個函式是_cpp_lex_direct,他反應了GCC詞法分析器的核心結構。
C語言的語法分析器實現在gcc/c-parser.c中。該檔案的起始函式是實現在檔案末尾的void c_parse_file(void)。它呼叫了c_parser_translation_unit(),然後按照文法定義一直遞迴呼叫下去。因此這是一個典型的遞迴下降分析法。C語言的語法分析從c_parser_translation_unit開始,往下呼叫c_parser_declaration_or_fndef,這是一個關鍵函式。
在c_parser_declaration_or_fndef函式裡有兩個分支,一個處理非函式宣告,最後總是呼叫到了finish_decl函式,而另一個分支用來處理函式宣告,最後總是呼叫到了finish_function函式,這兩個函式都實現在c-decl.c檔案中。這兩個函式開啟了接下來的工作:中間層翻譯。
在語法分析過程中,所有識別出來的語言部件都用一個叫TREE的變數儲存著。這個TREE就是gcc語法樹,叫做GENERIC。實際上它也是gcc的符號表,因為變數名、型別等等這些資訊都由TREE關聯起來。GENERIC的節點都定義在gcc/tree.h標頭檔案裡。
每個函式翻譯為GENERIC的語法樹之後,會進行gimplification(gimple化),在這一過程中函式的語法樹被翻譯為了控制流圖的形式。每個函式對應一個控制流圖。控制流由基本塊(Basic Block)組成。每個基本塊具有一串指令序列,並且只能有一個入口和一個出口,因此在這個序列內部不允許存在跳轉。gcc對基本塊的操作主要定義在gcc/basic-block.h裡。basic block在控制流中以連結串列的形式存放,它們由edge組成邏輯意義上的圖。gcc提供了對每個基本塊相關的邊進行遍歷的巨集FOR_EACH_EDGE。每個edge有flags標誌位,用來判別邊的型別,它決定了跳轉的方式(條件、無條件等等)。
gimple和RTL是gcc用來表示指令的兩種形式。因此每個基本塊都包含有兩組指令序列,一組是gimple指令,一組是RTL指令。每個函式將首先被gimple化,此時基本塊裡只包含gimple指令,之後由gimple生成RTL。
gimple是一種包含最多三個運算元的中間指令,也就是編譯原理裡講的四元碼(三個運算元,一個操作符),基本上也就是 dst = src1 @ src2 的這種形式。由於gimple最多隻能對兩個運算元進行計算,因此一個複雜的表示式會展開為一系列的gimple指令,這一過程就是gimple化。gimple化的程式碼實現在gcc/gimplify.c中,核心的思想就是對語法樹進行後序遍歷,對每個非葉子節點生成一條gimple指令,自動生成必要的中間變數,並正確識別出基本塊,從而生成完整的控制流。
從原始碼來看,語法分析中,每分析完一個函式,就會呼叫finish_function,它又會呼叫cgraph_finalize_function將函式新增到cgraph裡,只有這個函式被呼叫才會繼續處理它。分析整個檔案後,compile_file()函式會呼叫一個hook:lang_hooks.decls.final_write_globals ()。這個hook實際上是write_global_declarations() (in gcc/langhooks.c),它會呼叫註釋中提到的 cgraph_finalize_compilation_unit() 函式,接下來就是這樣的呼叫關係:
write_global_declarations()
cgraph_finalize_compilation_unit()
cgraph_analyze_function()
gimplify_function_tree() -> gimplification。
cgraph_lower_function() -> lowering
cgraph_optimize() -> 優化
在所有針對gimple的優化完成後,有一個叫做pass_expand的步驟,它將gimple展開為RTL。RTL是一種相對底層的指令,如果說gimple的重點在於控制流和資料流這種邏輯結構的話,那麼RTL的重點就在資料和控制的精確描述。通過RTL可以將運算元的長度、對齊、操作的型別、副作用等資訊表述出來,從而有利於自動化地進行最後的指令生成。
RTL的指令在gcc中稱之為insn,insn是有語法和語義的,它被gcc的生成工具所識別和處理,並生成對應的.c檔案作為gcc的一部分一同編譯到gcc的執行檔案中。
GENERIC、GIMPLE和RTL三者構成了gcc中間語言的全部,它們以GIMPLE為核心,由GENERIC承上,由RTL啟下,在原始檔和目標指令之間的鴻溝之上構建了一個三層的過渡。接下來,gcc的工作就是對中間語言進行平臺無關優化。
GCC的pass結構定義在gcc/tree-pass.h標頭檔案中。
大部分常用的pass都實現在gcc目錄下的某些檔案中,這些檔案的特點是聲明瞭一個全域性的xxx_pass結構體變數,而這些變數在tree-pass.h中用extern宣告一遍,並在passes.c中的 init_optimizations() 函式中串在一起。該函式通過使用NEXT_PASS()巨集,初始化了5串pass:all_lowering_passes -> all_small_ipa_passes -> all_regular_ipa_passes -> all_lto_gen_passes -> all_passes。
三大類Pass:GIMPLE Pass、RTL Pass、IPA Pass。
Pass被Pass管理器執行。執行每一個pass的程式碼實現在gcc/passes.c裡。在gcc/tree-optimize.c中定義了幾個特殊的pass:pass_all_optimizations、pass_early_local_passes、pass_all_early_optimizations、pass_cleanup_cfg、pass_cleanup_cfg_post_optimizing、pass_fixup_cfg、pass_init_datastructures。
呼叫關係:
cgraph_finalize_compilation_unit()
cgraph_analyze_functions()
cgraph_analyze_function()
gimplify_function_tree() -> gimplification。
cgraph_lower_function() -> lowering
cgraph_optimize()
ipa_passes()
if (!in_lto_p) execute_ipa_pass_list (all_small_ipa_passes); -> small IPA execute
if (!in_lto_p) execute_ipa_summary_passes(all_regular_ipa_passes) -> regular IPA summary
execute_ipa_summary_passes (all_lto_gen_passes); -> lto summary
if (!flag_ltrans) execute_ipa_pass_list (all_regular_ipa_passes); -> regular IPA (include LTO) execute
cgraph_expand_all_functions()
cgraph_expand_all_function()
tree_rest_of_compilation()
execute_all_ipa_transforms() -> regular IPA transform (include LTO) transform
execute_pass_list (all_passes) -> 程序內優化
普通的pass由gcc/passes.c中的execute_one_pass()函式來負責呼叫。
執行完所有Pass之後,gcc就進入了最後的階段:目的碼生成。
GCC的pass格局,它是GCC中間語言部分的核心架構,也是貫穿整個編譯流程的核心。在完成優化處理之後,GCC必須做的最後一步就是生成最後的編譯結果,通常情況下就是彙編檔案(文字或者二進位制並不重要)。
GCC中間語言的核心資料結構是GENERIC、GIMPLE和RTL。其中的RTL就是和指令緊密相關的一種結構,它是指令生成的起點。
RTL叫做暫存器轉移語言(Register Transfering Language)。說是暫存器,其實也包含記憶體操作。RTL被設計成一種函式式語言,由表示式和物件構成。其中物件指的是暫存器、記憶體和值(常數或者表示式的值),表示式就是對物件和子表示式的操作。這些在gcc internal裡面都有介紹。
RTL物件和操作組成RTL表示式,子表示式加上操作組成複合RTL表示式。當一個RTL表示式表示一條中間語言指令時,這個RTL表示式叫做INSN。RTL表示式(RTL Expression)在gcc程式碼中縮寫為RTX,程式碼中的rtx型別就是指向RTL表示式的指標。所以insn就是rtx,但是rtx不一定是insn。
RTL是由gimple生成的,從gimple到RTL的轉換叫做“expand”。在整個優化的pass鏈中,這一步由pass_expand完成。該pass實現在gcc/cfgexpand.c中。
針對每個CPU平臺,gcc有對應的Machine Description用指導指令生成。這些程式碼放在gcc/config/<平臺名稱>的目錄下,比如intel平臺的在gcc/config/i386/。一個Machine Description檔案是對應平臺的核心,比如gcc/config/i386/i386.md檔案。
在優化的pass序列的最後,有一個叫做pass_final的RTL Pass,這個pass負責將RTL翻譯為ASM。