歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux資訊 >> 更多Linux >> GCC 編譯流程及中間 RTL 的探索

GCC 編譯流程及中間 RTL 的探索

日期:2017/2/27 9:29:47   编辑:更多Linux
  1. GCC 簡介  編譯器的工作是將源代碼(通常使用高級語言編寫)翻譯成目標代碼(通常是低級的目標代碼或者機器語言),在現代編譯器的實現中,這個工作一般是分為兩個階段來實現的:    第一階段,編譯器的前端接受輸入的源代碼,經過詞法、語法和語義分析等等得到源程序的某種中間表示方式。    第二階段,編譯器的後端將前端處理生成的中間表示方式進行一些優化,並最終生成在目標機器上可運行的代碼。    GCC(GNU Compiler Collection) 是在 UNIX 以及類 UNIX 平台上廣泛使用的編譯器集合,它能夠支持多種語言前端,包括 C, C++, Objective-C, Ada, Fortran, Java 和 treelang 等。    GCC 設計中有兩個重要的目標,其中一個是在構建支持不同硬件平台的編譯器時,它的代碼能夠最大程度的被復用,所以 GCC 必須要做到一定程度的硬件無關性;另一個是要生成高質量的可執行代碼,這就需要對代碼進行集中的優化。為了實現這兩個目標,GCC 內部使用了一種硬件平台無關的語言,它能對實際的體系結構做一種抽象,這個中間語言就是 RTL(Register Transfer Language)。    雖然關於 GCC 的研究和開發工作側重於 GCC 後端代碼優化方面,但本文中我們關注的目標是在 GCC 的編譯過程中前端是如何工作的。    把 GCC 的前端獨立出來研究目的在於,在設計新的編譯器的時候,我們僅僅需要關注如何設計新編譯器的前端,而將代碼優化和目標代碼的生成留給 GCC 後端去完成,避免了後端設計的重復性勞動。    本文將以 C 語言為例,介紹 gcc[2] 在接受一個 .c 文件的輸入之後,其前端是如何進行處理並得到一個中間表示並轉交給後端處理。然後,在了解了 gcc 的工作流程後,介紹一下作者嘗試在 gcc 內部的RTL表示層中 hack gcc 的過程,與大家分享一些經驗,希望能給對有興趣研究和開發 gcc 的讀者有所幫助。    2. gcc 的工作流程  gcc 是一個驅動程序,它接受並解釋命令行參數,根據對命令行參數分析的結果決定下一步動作,gcc 提供了多種選項以達到控制 gcc 編譯過程的目的,我們可以在 GCC 的手冊中查找這些編譯選項的詳細信息。    gcc 的使用是比較簡單的,但是要深入到其內部去了解編譯流程,情況就比較復雜了。面對龐大的[3] gcc,我們只能選擇感興趣的部分來分析。但我們無法獲得關於 gcc 編譯流程的詳盡文檔[4] ,這主要是由於 gcc 本身過於繁雜,而且它處於不斷的變化當中,所以我們只有通過其它途徑來了解 gcc。有兩個比較好的方法:一是閱讀 source,對感興趣的函數可以跟蹤過去看一看,閱讀代碼看起來可怕,但其實代碼中會有很多注釋說明它的功能,使得我們的閱讀變得更簡單一些,這種方法便於從整體上把握 gcc;另外一個是 debug gcc,就是使用調試器來跟蹤 gcc 的編譯過程,這樣可以看清 gcc 編譯的實際流程,也可以追蹤我們感興趣的細節部分。我們先從大處著眼,從 source 中看看 gcc 一些比較重要的函數以及它們之間的調用關系,然後在 hack gcc 的時候,對 gcc 進行 debug 來追蹤我們關心的細節,並且可以通過調試來發現和修改 patch 中的錯誤。    在開始閱讀 gcc 的代碼之前,推薦您閱讀一下 GCC internals 中 passes and files of the compiler 一章——如果您以前沒有看過的話,這段內容會幫助您對 gcc 的結構建立一個大概的映像。    好了,我們以 gcc 中的函數為單位,希望能夠盡量詳細地描述 gcc 中自頂向下的函數調用關系。在 gcc 源碼目錄中,很容易就發現了一個文件 main.c,應該是 gcc 的入口了,這個main.c 文件中只有一個函數 main,而這個 main 函數中也只有一條語句,調用了一下toplev_main 函數。之所以單獨用一個 main 函數來調用 toplev_main,是為了讓不同的語言前端可以方便設計不同的 main 函數。    toplev_main 函數是在 toplev.c 文件中定義的,從名字中就可以看出這個文件應該是用來控制 gcc 最頂層的編譯流程的,在程序開始的注釋中也說明了它是用來處理命令行參數、打開文件、以合適的順序調用各個分析程序 [5] 並記錄它們各自所用的處理時間。toplev_main 首先對 gcc 做了一下初始化,主要是設置環境變量和診斷信息等等,然後就開始解析命令行參數,我們對這些並不感興趣,重要的是接下來調用了 do_compile 函數,這個函數看從名字看就是做編譯工作的,而在此之後 toplev_main 函數就返回了。    do_compile 函數也是在 tolev.c 中定義的,它調用了一些函數來做進一步的初始化,比如對編譯過程中計時器的初始化、針對特定程序設計語言的初始化以及對後端的初始化等等,同時它還對 toplev_main 函數中解析的命令行參數做了進一步處理。在完成了上述工作後,調用了 compile_file() 函數,這個函數應該是用來進行真正的編譯工作了。    compile_file 函數還是在 toplev.c 中定義的,這裡提一下 compile_file 函數和上面的do_compile 函數,它們是參數和返回類型都為 void 的函數,在編譯的時候需要的各種參數包括編譯的文件名、編譯參數以及 gcc 內部使用的一些鉤子函數等等都是采用全局變量來表示的,當然,這些全局變量在前面各種初始化函數中都已經被適當地初始化了。接著說compile_file 函數,它又做了一些我們並不太關心的初始化工作,之後,它終於調用了一個鉤子函數來分析(parse)整個輸入文件了:    (*lang_hooks.parse_file)(set_yydebug);       這裡的 lang_hooks 是一個全局變量,不同語言的前端對此賦以不同的值,以便調用各自特有的分析程序,關於 lang_hooks 結構的定義和初始化等等可以參見源碼中的 langhooks.h、langhooks.c 和 langhooks-def.h 等文件,這裡就不詳細追究了。對於 C 語言來說,這條語句相當於調用了 c-opts.c 中的 c_common_parse_file 函數。    c_common_parse_file中調用了c-parse.c中的c_parse_file函數,在此函數中又調用了同樣位於c-parse.c中的yyparse函數。有必要介紹一下c-parse.c文件,它是由GNU bison [6] 從c-parse.y中得到的一個語法解析器。c-parse.y則是一個YACC文件,它使用BNF(Backus Naur Form)來描述了某種程序設計語言的語法。 [7]    至此,我們對gcc中主要的函數調用關系還是相當清楚的,從main函數層層深入,進入了c-parse.c中的yyparse函數。前面提到過c-parse.c文件是由GNU bison對c-parse.y這個YACC文件作用後自動生成的,這導致這段代碼閱讀起來比較困難,因為bison生成的c-parse.c文件中有很多條goto語句以及超過500個case的switch語句,如此多的選擇和跳轉語句無疑給追蹤gcc的函數調用帶來了極大的困難,我們不可能再繼續下去了。    再回過頭去看看前面那些代碼和注釋以及一些文檔,注意到多次提到過一個函數――rest_of_compilation,這似乎是一個很重要的函數,我們可以過去看看。       在toplev.c中我們找到了這個函數,注釋中說明它的作用是:在對程序中頂層的函數定義或者變量的定義處理以後,接著對這些函數或者變量進行編譯並輸出相應的匯編代碼,在此函數返回後,gcc內部使用的tree結構就消亡了。看來這個函數的功能比較復雜,它已經把源程序對應的匯編代碼生成了,並且把對應的tree結構占用的空間已經釋放了,而我們所感興趣的部分是gcc編譯過程中內部使用RTL表示的情況,這部分處理應該是在rest_of_compilation這個函數返回之前做的。    前面我們從main函數跟蹤到了yyparse函數,這裡又發現了一個很重要的rest_of_compilation函數,但中間這段過程gcc做了些什麼我們還不清楚,也許我們所關心的有關RTL的處理就在其中。    現在我們只有對gcc進行調試才能確切的看清進入yyparse後函數調用的情況了,這裡介紹一下調試gcc的方法:    對gcc進行調試,其實是對編譯gcc源代碼所得到的cc1程序調試,進入到cc1所在的目錄,運行命令:      $ gdb cc1  $ break main  $ run -dr /PATH/test.c       這樣就是以-dr為編譯參數運行gcc來編譯test.c文件了,並且在main函數的入口處設置了一個斷點,-dr作為編譯參數就是要求在RTL表示生成以後將其dump到一個以.rtl結尾的文件中去。接下來在rest_of_compilation之前再設置一個斷點,並用continue命令運行到該斷點,用backtrace命令查看此時函數棧幀的情況:    $ break rest_of_compilation  $ continue  $ backtrace    下表1給出了使用gdb調試時顯示出的從main到rest_of_compilation的函數調用情況:      表1. 部分函數調用棧幀列表    調試的結果證實我們前面的分析是正確的,從main函數到yyparse函數的調用順序與我們閱讀代碼時所分析得到的結果是吻合的。現在我們得到了gcc編譯時從yypare到rest_of_compilation之間的一系列函數調用,這些都是值得關注的目標,讓我們返回到源碼中去看看這些函數的功能。    時刻記得我們的目標:對於gcc如何生成tree結構我們並不關心,也不關心gcc是如何由中間表示層RTL生成匯編代碼的,我們感興趣的是RTL表示是如何生成的,並希望在RTL表示層做一些修改,以達到我們的目的。為了省去一些篇幅,本文中略去了對那些我們不太關心的函數的分析,直接跳轉到RTL生成和處理相關的部分。    終於,在tree-optimize.c中的tree_rest_of_compilation中,我們發現了一系列看起來是與RTL生成有關的函數調用,特別引起我們注意的又是一個鉤子函數:    (*lang_hooks.rtl_eXPand.stmt) (DECL_SAVED_TREE (fndecl));       這行代碼




Copyright © Linux教程網 All Rights Reserved