乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      GCC 的編譯流程及中間表示層 RTL 的初步探索

       skywood 2006-01-24
      本文將以 C 語(yǔ)言為例,介紹 gcc 在接受一個(gè) .c 文件的輸入之后,其前端是如何進(jìn)行處理并得到一個(gè)中間表示并轉(zhuǎn)交給后端處理。然后,在了解了 gcc [1] 的工作流程后,介紹一下作者嘗試在 gcc 內(nèi)部的 RTL 表示層中 hack gcc 的過程,與大家分享一些經(jīng)驗(yàn),希望能給對(duì)有興趣研究和開發(fā) gcc 的讀者有所幫助。

      1. GCC 簡(jiǎn)介
      編譯器的工作是將源代碼(通常使用高級(jí)語(yǔ)言編寫)翻譯成目標(biāo)代碼(通常是低級(jí)的目標(biāo)代碼或者機(jī)器語(yǔ)言),在現(xiàn)代編譯器的實(shí)現(xiàn)中,這個(gè)工作一般是分為兩個(gè)階段來實(shí)現(xiàn)的:

      第一階段,編譯器的前端接受輸入的源代碼,經(jīng)過詞法、語(yǔ)法和語(yǔ)義分析等等得到源程序的某種中間表示方式。

      第二階段,編譯器的后端將前端處理生成的中間表示方式進(jìn)行一些優(yōu)化,并最終生成在目標(biāo)機(jī)器上可運(yùn)行的代碼。

      GCC(GNU Compiler Collection) 是在 UNIX 以及類 UNIX 平臺(tái)上廣泛使用的編譯器集合,它能夠支持多種語(yǔ)言前端,包括 C, C++, Objective-C, Ada, Fortran, Java 和 treelang 等。

      GCC 設(shè)計(jì)中有兩個(gè)重要的目標(biāo),其中一個(gè)是在構(gòu)建支持不同硬件平臺(tái)的編譯器時(shí),它的代碼能夠最大程度的被復(fù)用,所以 GCC 必須要做到一定程度的硬件無(wú)關(guān)性;另一個(gè)是要生成高質(zhì)量的可執(zhí)行代碼,這就需要對(duì)代碼進(jìn)行集中的優(yōu)化。為了實(shí)現(xiàn)這兩個(gè)目標(biāo),GCC 內(nèi)部使用了一種硬件平臺(tái)無(wú)關(guān)的語(yǔ)言,它能對(duì)實(shí)際的體系結(jié)構(gòu)做一種抽象,這個(gè)中間語(yǔ)言就是 RTL(Register Transfer Language)。

      雖然關(guān)于 GCC 的研究和開發(fā)工作側(cè)重于 GCC 后端代碼優(yōu)化方面,但本文中我們關(guān)注的目標(biāo)是在 GCC 的編譯過程中前端是如何工作的。

      把 GCC 的前端獨(dú)立出來研究目的在于,在設(shè)計(jì)新的編譯器的時(shí)候,我們僅僅需要關(guān)注如何設(shè)計(jì)新編譯器的前端,而將代碼優(yōu)化和目標(biāo)代碼的生成留給 GCC 后端去完成,避免了后端設(shè)計(jì)的重復(fù)性勞動(dòng)。

      本文將以 C 語(yǔ)言為例,介紹 gcc [2] 在接受一個(gè) .c 文件的輸入之后,其前端是如何進(jìn)行處理并得到一個(gè)中間表示并轉(zhuǎn)交給后端處理。然后,在了解了 gcc 的工作流程后,介紹一下作者嘗試在 gcc 內(nèi)部的RTL表示層中 hack gcc 的過程,與大家分享一些經(jīng)驗(yàn),希望能給對(duì)有興趣研究和開發(fā) gcc 的讀者有所幫助。


      [1] 如無(wú)特別說明,下文中 gcc 均指GNU C Compiler,即GCC編譯器集合中的C語(yǔ)言編譯器。

      2. gcc 的工作流程
      gcc 是一個(gè)驅(qū)動(dòng)程序,它接受并解釋命令行參數(shù),根據(jù)對(duì)命令行參數(shù)分析的結(jié)果決定下一步動(dòng)作,gcc 提供了多種選項(xiàng)以達(dá)到控制 gcc 編譯過程的目的,我們可以在 GCC 的手冊(cè)中查找這些編譯選項(xiàng)的詳細(xì)信息。

      gcc 的使用是比較簡(jiǎn)單的,但是要深入到其內(nèi)部去了解編譯流程,情況就比較復(fù)雜了。面對(duì)龐大的 [3] gcc,我們只能選擇感興趣的部分來分析。但我們無(wú)法獲得關(guān)于 gcc 編譯流程的詳盡文檔 [4] ,這主要是由于 gcc 本身過于繁雜,而且它處于不斷的變化當(dāng)中,所以我們只有通過其它途徑來了解 gcc。有兩個(gè)比較好的方法:一是閱讀 source,對(duì)感興趣的函數(shù)可以跟蹤過去看一看,閱讀代碼看起來可怕,但其實(shí)代碼中會(huì)有很多注釋說明它的功能,使得我們的閱讀變得更簡(jiǎn)單一些,這種方法便于從整體上把握 gcc;另外一個(gè)是 debug gcc,就是使用調(diào)試器來跟蹤 gcc 的編譯過程,這樣可以看清 gcc 編譯的實(shí)際流程,也可以追蹤我們感興趣的細(xì)節(jié)部分。我們先從大處著眼,從 source 中看看 gcc 一些比較重要的函數(shù)以及它們之間的調(diào)用關(guān)系,然后在 hack gcc 的時(shí)候,對(duì) gcc 進(jìn)行 debug 來追蹤我們關(guān)心的細(xì)節(jié),并且可以通過調(diào)試來發(fā)現(xiàn)和修改 patch 中的錯(cuò)誤。


      [3] 完整的gcc 3.4.0解壓縮后占用超過150M的空間,有超過13000個(gè)代碼和文檔文件。

      在開始閱讀 gcc 的代碼之前,推薦您閱讀一下 GCC internals 中 passes and files of the compiler 一章——如果您以前沒有看過的話,這段內(nèi)容會(huì)幫助您對(duì) gcc 的結(jié)構(gòu)建立一個(gè)大概的映像。

      好了,我們以 gcc 中的函數(shù)為單位,希望能夠盡量詳細(xì)地描述 gcc 中自頂向下的函數(shù)調(diào)用關(guān)系。在 gcc 源碼目錄中,很容易就發(fā)現(xiàn)了一個(gè)文件 main.c,應(yīng)該是 gcc 的入口了,這個(gè)main.c 文件中只有一個(gè)函數(shù) main,而這個(gè) main 函數(shù)中也只有一條語(yǔ)句,調(diào)用了一下toplev_main 函數(shù)。之所以單獨(dú)用一個(gè) main 函數(shù)來調(diào)用 toplev_main,是為了讓不同的語(yǔ)言前端可以方便設(shè)計(jì)不同的 main 函數(shù)。


      [4] 作者認(rèn)為關(guān)于GCC學(xué)習(xí)比較好的文檔列在本文后面的參考文檔一節(jié)中,但本文不準(zhǔn)備重復(fù)這些文檔中已有的內(nèi)容。

      toplev_main 函數(shù)是在 toplev.c 文件中定義的,從名字中就可以看出這個(gè)文件應(yīng)該是用來控制 gcc 最頂層的編譯流程的,在程序開始的注釋中也說明了它是用來處理命令行參數(shù)、打開文件、以合適的順序調(diào)用各個(gè)分析程序 [5] 并記錄它們各自所用的處理時(shí)間。toplev_main 首先對(duì) gcc 做了一下初始化,主要是設(shè)置環(huán)境變量和診斷信息等等,然后就開始解析命令行參數(shù),我們對(duì)這些并不感興趣,重要的是接下來調(diào)用了 do_compile 函數(shù),這個(gè)函數(shù)看從名字看就是做編譯工作的,而在此之后 toplev_main 函數(shù)就返回了。

      do_compile 函數(shù)也是在 tolev.c 中定義的,它調(diào)用了一些函數(shù)來做進(jìn)一步的初始化,比如對(duì)編譯過程中計(jì)時(shí)器的初始化、針對(duì)特定程序設(shè)計(jì)語(yǔ)言的初始化以及對(duì)后端的初始化等等,同時(shí)它還對(duì) toplev_main 函數(shù)中解析的命令行參數(shù)做了進(jìn)一步處理。在完成了上述工作后,調(diào)用了 compile_file() 函數(shù),這個(gè)函數(shù)應(yīng)該是用來進(jìn)行真正的編譯工作了。


      [5] 分析程序?qū)?yīng)的英文是pass,即多趟掃描編譯器中某一趟掃描所使用的掃描程序。

      compile_file 函數(shù)還是在 toplev.c 中定義的,這里提一下 compile_file 函數(shù)和上面的do_compile 函數(shù),它們是參數(shù)和返回類型都為 void 的函數(shù),在編譯的時(shí)候需要的各種參數(shù)包括編譯的文件名、編譯參數(shù)以及 gcc 內(nèi)部使用的一些鉤子函數(shù)等等都是采用全局變量來表示的,當(dāng)然,這些全局變量在前面各種初始化函數(shù)中都已經(jīng)被適當(dāng)?shù)爻跏蓟恕=又f compile_file 函數(shù),它又做了一些我們并不太關(guān)心的初始化工作,之后,它終于調(diào)用了一個(gè)鉤子函數(shù)來分析(parse)整個(gè)輸入文件了:

      (*lang_hooks.parse_file)(set_yydebug);

      這里的 lang_hooks 是一個(gè)全局變量,不同語(yǔ)言的前端對(duì)此賦以不同的值,以便調(diào)用各自特有的分析程序,關(guān)于 lang_hooks 結(jié)構(gòu)的定義和初始化等等可以參見源碼中的 langhooks.h、langhooks.c 和 langhooks-def.h 等文件,這里就不詳細(xì)追究了。對(duì)于 C 語(yǔ)言來說,這條語(yǔ)句相當(dāng)于調(diào)用了 c-opts.c 中的 c_common_parse_file 函數(shù)。

      c_common_parse_file中調(diào)用了c-parse.c中的c_parse_file函數(shù),在此函數(shù)中又調(diào)用了同樣位于c-parse.c中的yyparse函數(shù)。有必要介紹一下c-parse.c文件,它是由GNU bison [6] 從c-parse.y中得到的一個(gè)語(yǔ)法解析器。c-parse.y則是一個(gè)YACC文件,它使用BNF(Backus Naur Form)來描述了某種程序設(shè)計(jì)語(yǔ)言的語(yǔ)法。 [7]


      [6] GNU bison是一個(gè)通用的生成語(yǔ)法解析器的工具,它可以把LALR(1)上下文無(wú)關(guān)的語(yǔ)法描述轉(zhuǎn)換成C語(yǔ)言程序,以便對(duì)使用該語(yǔ)法描述的語(yǔ)言進(jìn)行語(yǔ)法分析。

      至此,我們對(duì)gcc中主要的函數(shù)調(diào)用關(guān)系還是相當(dāng)清楚的,從main函數(shù)層層深入,進(jìn)入了c-parse.c中的yyparse函數(shù)。前面提到過c- parse.c文件是由GNU bison對(duì)c-parse.y這個(gè)YACC文件作用后自動(dòng)生成的,這導(dǎo)致這段代碼閱讀起來比較困難,因?yàn)閎ison生成的c-parse.c文件中有很多條goto語(yǔ)句以及超過500個(gè)case的switch語(yǔ)句,如此多的選擇和跳轉(zhuǎn)語(yǔ)句無(wú)疑給追蹤gcc的函數(shù)調(diào)用帶來了極大的困難,我們不可能再繼續(xù)下去了。

      再回過頭去看看前面那些代碼和注釋以及一些文檔,注意到多次提到過一個(gè)函數(shù)――rest_of_compilation,這似乎是一個(gè)很重要的函數(shù),我們可以過去看看。


      [7] 關(guān)于詞法解析器和語(yǔ)法解析器的生成,本文不作詳細(xì)討論,有興趣的讀者可以參考GNU bison和flex的手冊(cè)。

      在toplev.c 中我們找到了這個(gè)函數(shù),注釋中說明它的作用是:在對(duì)程序中頂層的函數(shù)定義或者變量的定義處理以后,接著對(duì)這些函數(shù)或者變量進(jìn)行編譯并輸出相應(yīng)的匯編代碼,在此函數(shù)返回后,gcc內(nèi)部使用的tree結(jié)構(gòu)就消亡了??磥磉@個(gè)函數(shù)的功能比較復(fù)雜,它已經(jīng)把源程序?qū)?yīng)的匯編代碼生成了,并且把對(duì)應(yīng)的tree結(jié)構(gòu)占用的空間已經(jīng)釋放了,而我們所感興趣的部分是gcc編譯過程中內(nèi)部使用RTL表示的情況,這部分處理應(yīng)該是在rest_of_compilation這個(gè)函數(shù)返回之前做的。

      前面我們從main函數(shù)跟蹤到了yyparse函數(shù),這里又發(fā)現(xiàn)了一個(gè)很重要的rest_of_compilation函數(shù),但中間這段過程gcc做了些什么我們還不清楚,也許我們所關(guān)心的有關(guān)RTL的處理就在其中。

      現(xiàn)在我們只有對(duì)gcc進(jìn)行調(diào)試才能確切的看清進(jìn)入yyparse后函數(shù)調(diào)用的情況了,這里介紹一下調(diào)試gcc的方法:

      對(duì)gcc進(jìn)行調(diào)試,其實(shí)是對(duì)編譯gcc源代碼所得到的cc1程序調(diào)試,進(jìn)入到cc1所在的目錄,運(yùn)行命令:

      $ gdb cc1 $ break main $ run -dr /PATH/test.c

      這樣就是以-dr為編譯參數(shù)運(yùn)行 gcc來編譯test.c文件了,并且在main函數(shù)的入口處設(shè)置了一個(gè)斷點(diǎn),-dr作為編譯參數(shù)就是要求在RTL表示生成以后將其dump到一個(gè)以. rtl結(jié)尾的文件中去。接下來在rest_of_compilation之前再設(shè)置一個(gè)斷點(diǎn),并用continue命令運(yùn)行到該斷點(diǎn),用 backtrace命令查看此時(shí)函數(shù)棧幀的情況:

      $ break rest_of_compilation $ continue $ backtrace

      下表1給出了使用gdb調(diào)試時(shí)顯示出的從main到rest_of_compilation的函數(shù)調(diào)用情況:

      調(diào)用順序 函數(shù)名字 所在文件名 #1

      #2

      #3

      #4

      #5

      #6

      #7

      #8

      #9

      #10

      #11

      #12

      #13

      #14

      #15

      main

      toplev_main

      do_compile

      compile_file

      c_common_parse_file

      c_parse_file

      yyparse

      finish_function

      cgraph_finalize_function

      cgraph_assemble_pending_functions

      cgraph_expand_function

      c_expand_body

      c_expand_body_1

      tree_rest_of_compilation

      rest_of_compilation

      main.c

      toplev.c

      toplev.c

      toplev.c

      c-opts.c

      c-parse.y

      c-parse.y

      c-decl.c

      cgraphunit.c

      cgraphunit.c

      cgraphunit.c

      c-decl.c

      c-decl.c

      tree-optimize.c

      toplev.c

      表1. 部分函數(shù)調(diào)用棧幀列表

      調(diào)試的結(jié)果證實(shí)我們前面的分析是正確的,從main函數(shù)到y(tǒng)yparse函數(shù)的調(diào)用順序與我們閱讀代碼時(shí)所分析得到的結(jié)果是吻合的?,F(xiàn)在我們得到了gcc編譯時(shí)從yypare到rest_of_compilation之間的一系列函數(shù)調(diào)用,這些都是值得關(guān)注的目標(biāo),讓我們返回到源碼中去看看這些函數(shù)的功能。

      時(shí)刻記得我們的目標(biāo):對(duì)于gcc如何生成tree結(jié)構(gòu)我們并不關(guān)心,也不關(guān)心gcc是如何由中間表示層RTL生成匯編代碼的,我們感興趣的是RTL表示是如何生成的,并希望在RTL表示層做一些修改,以達(dá)到我們的目的。為了省去一些篇幅,本文中略去了對(duì)那些我們不太關(guān)心的函數(shù)的分析,直接跳轉(zhuǎn)到RTL生成和處理相關(guān)的部分。

      終于,在tree-optimize.c中的tree_rest_of_compilation中,我們發(fā)現(xiàn)了一系列看起來是與RTL生成有關(guān)的函數(shù)調(diào)用,特別引起我們注意的又是一個(gè)鉤子函數(shù):

      (*lang_hooks.rtl_expand.stmt) (DECL_SAVED_TREE (fndecl));

      這行代碼的注釋說這個(gè)鉤子函數(shù)用來生成一個(gè)被編譯函數(shù)的RTL表示,接下來還調(diào)用了幾個(gè)函數(shù)來進(jìn)行RTL生成階段的最后處理(包括調(diào)用gcc編譯時(shí)內(nèi)部使用的垃圾收集函數(shù)),然后就調(diào)用了rest_of_compilation了。前面已經(jīng)提到了,rest_of_compilation的作用是對(duì)RTL表示做優(yōu)化并且生成匯編代碼輸出,至此我們可以做出這樣的推斷:在tree_rest_of_compilation調(diào)用了一系列生成RTL表示的函數(shù)之后,到調(diào)用rest_of_compilation之前,gcc的內(nèi)部保存了一個(gè)原始的、未優(yōu)化的RTL中間表示。如果我們希望對(duì)函數(shù)的RTL表示做一些修改,在這里插入代碼做改動(dòng)應(yīng)該是一個(gè)不錯(cuò)的選擇。

      到這里,我們所關(guān)心的gcc編譯流程基本已經(jīng)結(jié)束了,也搞清了RTL表示在什么地方生成的,我們應(yīng)該有一定的信心在RTL表示層上對(duì)gcc進(jìn)行hack了。

      3. RTL簡(jiǎn)介
      我們的目標(biāo)是在RTL表示層上hack gcc,所以有必要對(duì)RTL做一些介紹。在gcc internals中有專門的一章描述RTL,如果對(duì)RTL沒有任何了解,那么它很值得您一看;同時(shí),在理解和插入RTL語(yǔ)句的時(shí)候,這份文檔也可以作為比較詳盡的手冊(cè)來參照。

      在gcc的編譯過程中,有三次比較重要的轉(zhuǎn)換:

      1. 待編譯的源代碼―<gcc抽象語(yǔ)法樹表示。
      2. gcc抽象語(yǔ)法樹―<RTL表示。
      3. RTL表示-<匯編代碼輸出。

      RTL是gcc內(nèi)部使用的中間表示語(yǔ)言,為了對(duì)其有一個(gè)直觀點(diǎn)的印象,我們可以把它dump出來看一看。使用

      $ gcc -dr test.c

      就可以得到test.c的RTL表示,文件名一般為test.c.00.rtl。

      RTL 的設(shè)計(jì)據(jù)說是從LISP語(yǔ)言得到了靈感,所以我們dump出來的.rtl文件看起來也像是一個(gè)LISP程序,每條RTL語(yǔ)句都是用來描述需要輸出的指令的,可以對(duì)照我們dump出的.rtl文件以及上面提到的文檔來深入學(xué)習(xí)RTL。但我們的要求不僅如此,我們需要插入自己的RTL語(yǔ)句來hack cc,必須閱讀gcc源代碼提供的RTL操作的接口,這個(gè)過程比較繁瑣而且沒有文檔可以參考,唯一有幫助的就是已有的在RTL表示層上對(duì)gcc做的補(bǔ)丁,以吸取其他gcc hackers的經(jīng)驗(yàn),作者在嘗試自己的補(bǔ)丁時(shí)曾經(jīng)參考過StackGuard [8] 的代碼,另外可以在gcc的maillist上看到有些hacker提供的patch,這些已有的工作對(duì)于gcc hacker newbie來說是很有裨益的。


      [8] StackGuard實(shí)際上是一個(gè)做過補(bǔ)丁的GCC,可以做為在RTL表示層上hack GCC的一個(gè)例子來參考。

      僅僅這么多文字來介紹RTL還遠(yuǎn)遠(yuǎn)不夠,但是如果希望把RTL描述得十分清楚,那應(yīng)該由另外一篇文章來完成了,本文就不再詳述了。

      4. Let‘s hack gcc!
      下面進(jìn)入hack gcc的實(shí)戰(zhàn)階段了,先說一下我的目的:我希望使用修改過的gcc編譯程序的時(shí)候,能夠在每個(gè)函數(shù)的開始和結(jié)束的地方插入一個(gè)函數(shù)調(diào)用語(yǔ)句,也就是說,在每個(gè)函數(shù)的第一條指令之前,由編譯器強(qiáng)制插入一個(gè)函數(shù)調(diào)用,在函數(shù)最后一條指令結(jié)束之后,也要插入一個(gè)函數(shù)調(diào)用。下面用兩段C語(yǔ)言代碼來表達(dá)這個(gè)補(bǔ)丁的效果:

      int foo()
      {
          first statement;
          …
          …
          …
          last statement;
      }
      int foo()
      {
          my_function_begin;
          first statement;
          …
          last statement;
          my_function_end;
      }

      左邊一列是程序員正常編寫的普通函數(shù),我希望使用修改過的gcc編譯該函數(shù)后,能夠得到相當(dāng)于編譯右邊這段函數(shù)的結(jié)果,就是對(duì)程序員透明地在每個(gè)函數(shù)的第一條語(yǔ)句之前和最后一條語(yǔ)句之后自動(dòng)插入兩個(gè)函數(shù)調(diào)用:my_function_begin和my_function_end。當(dāng)然,這兩個(gè)函數(shù)具體實(shí)現(xiàn)什么功能可以由程序員來編寫,最簡(jiǎn)單的實(shí)現(xiàn)可以僅僅在標(biāo)準(zhǔn)輸出上分別打印一句話表示該函數(shù)確實(shí)被調(diào)用了即可。

      gcc 中生成抽象語(yǔ)法樹表示和RTL表示都是以一個(gè)完整的函數(shù)定義或者top level的聲明為單位的,這也就意味著在tree_rest_of_compilation這個(gè)函數(shù)調(diào)用了一系列用于生成RTL表示的函數(shù)之后,我們所得到的只是當(dāng)前正在被編譯的函數(shù)的RTL表示,而并不是整個(gè)源程序的RTL表示,這正好方便我們以函數(shù)為單位來進(jìn)行修改。

      我們?cè)趖ree_rest_of_compilation函數(shù)中調(diào)用rest_of_compilation之前插入一條語(yǔ)句,調(diào)用一個(gè)新函數(shù) modify_rtl來對(duì)gcc生成的RTL表示做一些處理。函數(shù)modify_rtl的定義放在function.c文件中,這是因?yàn)間cc在生成 RTL表示時(shí)需要的相關(guān)函數(shù)大部分都定義在這個(gè)文件中,我們的補(bǔ)丁也可以看作是gcc生成RTL表示的一部分工作,所以把modify_rtl放到這個(gè)文件中定義是最合適的。

      接下來工作的關(guān)鍵就集中到如何定義modify_rtl函數(shù)了?,F(xiàn)在我們得到了當(dāng)前編譯函數(shù)的RTL表示,我們可以對(duì)這個(gè)RTL單元進(jìn)行掃描,找到合適的位置分別調(diào)用my_function_begin和my_function_end函數(shù)即可。函數(shù)的RTL表示是一個(gè)雙向連接的鏈表結(jié)構(gòu),其中每個(gè)節(jié)點(diǎn)稱為一個(gè)insn [9] ,有的insn可能表示一條真實(shí)的匯編指令,有的則表示jump指令跳轉(zhuǎn)的標(biāo)簽或者其它各種聲明信息。為了簡(jiǎn)便起見,這里直接給出一個(gè)常用的gcc所提供的訪問insn的宏和函數(shù)列表,并給出它們的功能:


      [9] StackGuard實(shí)際上是一個(gè)做過補(bǔ)丁的GCC,可以做為在RTL表示層上hack GCC的一個(gè)例子來參考。

      宏(函數(shù))名 功能 INSN_UID(insn) 獲取該insn的id PREV_INSN(insn) 獲取insn鏈表中該insn的前一個(gè)insn NEXT_INSN(insn) 獲取insn鏈表中該insn的后一個(gè)insn GET_CODE(insn) 獲取該insn的code NOTE_LINE_NUMBER(insn) 如果insn的code是NOTE,則返回該insn對(duì)應(yīng)源代碼的行號(hào),否則返回一個(gè)負(fù)數(shù) Get_insns() 獲取當(dāng)前函數(shù)RTL表示的第一個(gè)insn Get_last_insn() 返回當(dāng)前函數(shù)RTL表示的最后一個(gè)insn

      表2. 部分gcc提供的insn操作接口列表

      一個(gè)函數(shù)完整的、未被優(yōu)化的RTL表示中會(huì)有兩個(gè)note insn表示函數(shù)的開始和結(jié)束,gcc定義了兩個(gè)全局變量NOTE_INSN_FUNCTION_BEGIN和 NOTE_INSN_FUNCTION_END來表示這兩個(gè)note insn的行數(shù)。這樣我們就可以掃描當(dāng)前RTL單元,當(dāng)碰到這兩個(gè)note insn的時(shí)候,就可以插入相應(yīng)的函數(shù)調(diào)用語(yǔ)句了。

      gcc提供了emit_library_call函數(shù)來插入一個(gè)函數(shù)調(diào)用,這個(gè)函數(shù)返回的是一個(gè)表示函數(shù)調(diào)用的RTL表達(dá)式,并默認(rèn)地把這個(gè)RTL表達(dá)式插入到當(dāng)前RTL單元的最后一個(gè)insn之后。所以如果直接調(diào)用emit_library_call,就會(huì)把函數(shù)調(diào)用語(yǔ)句插入到RTL單元最后一個(gè)insn之后,而不是我們所希望的函數(shù)開始和結(jié)束的地方,我們可以使用start_sequence和end_sequence函數(shù),它們產(chǎn)生一個(gè)相對(duì)獨(dú)立的sequence并把函數(shù)調(diào)用語(yǔ)句保存到一個(gè)RTL表達(dá)式中以備后用。

      我們已經(jīng)找到插入函數(shù)調(diào)用的點(diǎn),并且也生成了表示函數(shù)調(diào)用的RTL語(yǔ)句,現(xiàn)在就可以使用gcc提供的emit_insn_before和emit_insn_after函數(shù)來插入RTL語(yǔ)句了。

      到這里,modify_rtl函數(shù)的實(shí)現(xiàn)基本已經(jīng)成型了,下面這段示例代碼就可以完成在每個(gè)函數(shù)的開始處插入RTL語(yǔ)句的功能:

      int modify_rtl() { rtx insn; rtx seq; //emit my_function_begin at the beginnig of each function start_sequence(); emit_libarary_call(gen_rtx(SYMBOL_REF, Pmode, my_function_begin), 0, VOIDmode, 0); seq = get_insns(); end_sequence(); for(insn = get_insns(); ; insn = NEXT_INSN(insn)) if((GET_CODE(insn) == NOTE) && (NOTE_LINE_NUMBER(insn) == NOTE_INSN_FUNCTION_BEGIN)) break; emit_insn_after(seq, insn); … }

      這段代碼中所使用數(shù)據(jù)結(jié)構(gòu)、函數(shù)的具體功能和用法,屬于十分細(xì)節(jié)的內(nèi)容,無(wú)須在這里描述清楚,請(qǐng)讀者參考gcc源代碼。

      對(duì)于在函數(shù)結(jié)束的地方插入my_function_end函數(shù)同樣如此,我們可以用get_last_insn得到RTL單元的最后一個(gè)insn,然后使用 PREV_INSN(insn)開始向前掃描,遇到行號(hào)為NOTE_INSN_FUNCTION_END的note insn時(shí),用emit_insn_before把相應(yīng)的函數(shù)調(diào)用RTL表達(dá)式插入到這個(gè)insn之前即可。

      現(xiàn)在這個(gè)patch的基本功能已經(jīng)完成了,我們還可以再做一些工作使得它功能更強(qiáng)大和實(shí)用一些,比如加入一個(gè)編譯選項(xiàng)(比如-finsert- function)來指定是否啟用這個(gè)patch的,當(dāng)編譯的命令行參數(shù)中沒有提供這個(gè)編譯選項(xiàng)時(shí),我們所作的補(bǔ)丁就不起作用。關(guān)于如何增加編譯選項(xiàng),我們可以參考o(jì)pts.c中的decode-options函數(shù),在此就不詳細(xì)分析了。

      在modify_rtl中調(diào)用current_function_name函數(shù)可以得到當(dāng)前正在被編譯的函數(shù)名,我們可以把這些函數(shù)名寫到一個(gè)文件中去,這樣可以記錄我們對(duì)哪些函數(shù)做了修改;還可以實(shí)現(xiàn)一個(gè)過濾器,在啟用了patch的情況下,對(duì)于指定的函數(shù),我們還可以將其過濾掉,不對(duì)其做處理,這些功能也是很容易實(shí)現(xiàn)的。

      我們還可以再實(shí)現(xiàn)一些功能,比如在掃描RTL的時(shí)候,如果發(fā)現(xiàn)一條call_insn,可以把這條call指令所調(diào)用的函數(shù)名記錄下來,這樣我們甚至可以得到一個(gè)程序運(yùn)行時(shí)刻的動(dòng)態(tài)的函數(shù)調(diào)用關(guān)系圖,這就可以描繪程序的實(shí)際運(yùn)行軌跡。

      最后,還需要把my_function_begin和my_function_end兩個(gè)函數(shù)實(shí)現(xiàn)一下,可以把它們的功能擴(kuò)展一下,不是僅僅輸出一條語(yǔ)句到標(biāo)準(zhǔn)輸出,而是記錄一些信息到文件中,這樣就可以得到一個(gè)以函數(shù)為粒度的運(yùn)行時(shí)刻日志,甚至可以使這兩個(gè)函數(shù)與linux內(nèi)核聯(lián)系起來,做一些特殊的檢查工作等等,這樣就使得我們的patch有一些實(shí)用性了。這兩個(gè)函數(shù)我們可以在mylib.c中實(shí)現(xiàn),編譯成一個(gè)shared object,使用如下命令編譯:

      $ gcc mylib.c -c -fPIC $ gcc mylib.o -shared -o libmylib.so

      把libmylib.so放到/usr/lib目錄下,那么在編譯的時(shí)候只需加上-lmylib參數(shù)就可以使用這個(gè)shared object中的函數(shù)了。

      剩下的工作就是進(jìn)行調(diào)試和測(cè)試了,當(dāng)我們解決了各種問題,使這個(gè)修改過的編譯器能夠完美的運(yùn)行起來的時(shí)候,也許我們就能體會(huì)到gcc hacker的那種成就感和喜悅之情了。

      5. 經(jīng)驗(yàn)總結(jié)
      先說一下我自己嘗試的結(jié)果,我是基于gcc version 3.4.0工作的,給gcc加入了一個(gè)編譯選項(xiàng)以選擇是否啟用添加的補(bǔ)丁,可以在每個(gè)函數(shù)的開始和結(jié)束的時(shí)候插入函數(shù)調(diào)用,也可以在函數(shù)調(diào)用之前和返回之后插入函數(shù)調(diào)用,實(shí)現(xiàn)了一個(gè)過濾器,可以忽略一些函數(shù)不對(duì)其做處理,并且可以在運(yùn)行時(shí)將一些信息記錄到文件中去留待分析。這個(gè)補(bǔ)丁的功能基本上就是這些了,實(shí)現(xiàn)方法可能和本文中的方法有所不同,文中描述的方法是較早的時(shí)候我采用的方法,現(xiàn)在則進(jìn)行了一些改動(dòng),這里就不詳加介紹了。我已經(jīng)成功的使用“我的”gcc編譯了emacs和lynx等實(shí)用軟件,運(yùn)行正常,補(bǔ)丁功能也正常,可以說是取得了一個(gè)小小的成功。但是我沒有空間可以上載我的補(bǔ)丁,有興趣的讀者可以通過e-mail向我索取。

      最后談?wù)勎业慕?jīng)驗(yàn):

      在理解gcc的編譯流程以及試圖找到做補(bǔ)丁的思路的時(shí)候,需要多閱讀文檔,包括學(xué)習(xí)已有的工作是怎么做的。不要貿(mào)然嘗試,不要奢望可以憑運(yùn)氣達(dá)成目的,盡量找到最合適的實(shí)現(xiàn)方法,在確立了一個(gè)基本思路之后,可以在gcc的maillist上咨詢一下,看看有沒有人提供更好的思路,在確信自己思路的可行性之后再開始具體的工作。

      在做具體實(shí)現(xiàn)的時(shí)候,肯定會(huì)遇到各種各樣的問題,比如在編譯自己修改過的gcc時(shí)會(huì)出錯(cuò),或者用patch過的gcc編譯程序時(shí)出錯(cuò),或者是編譯通過運(yùn)行時(shí)刻出錯(cuò)等等,這時(shí)候需要耐心地檢查代碼和進(jìn)行debug,盡量自己解決問題,不要把一些特別細(xì)節(jié)地問題拿到maillist上討論。我記得在maillist上曾經(jīng)有人嚴(yán)厲地告誡我:“you won‘t go very far if you ask a question each time you get an error”,自己debug才是解決問題的最好方法,當(dāng)然如果實(shí)在不明白的問題必須拿到maillist上去討論,這時(shí)候要盡量詳細(xì)的描述自己的目的和問題,才能夠得到有效的幫助。

      好了,這就是我自己學(xué)習(xí)和嘗試hack gcc的工作過程,希望我的一些經(jīng)驗(yàn)?zāi)軌蚪o您幫助,如果對(duì)本文中的觀點(diǎn)有疑問或者在學(xué)習(xí)gcc的時(shí)候碰到困難,歡迎與我探討。

      參考資料

      1. GCC internals. http://gcc./onlinedocs/gccint/,這是得到公認(rèn)的除了gcc源碼以外,學(xué)習(xí)gcc最好的材料,雖然比較難讀,但它的權(quán)威性和全面性是毋庸置疑的,GCC的手冊(cè)和info也是很好的參考資料。
      2. GCC Front end HOWTO, 通過自己設(shè)計(jì)一個(gè)GCC前端來幫助GCC hacker newbie來學(xué)習(xí)和了解GCC, http://www./Linux/Howto/GCC-Frontend-HOWTO.html#toc1,適合新手閱讀,作者還提供了相關(guān)程序的軟件包。
      3. StackGuard, http://www./stackguard.html,可以作為一個(gè)在RTL表示層hack gcc的實(shí)例來學(xué)習(xí)。

      關(guān)于作者
      王逸,2002 年秋季入學(xué)的南京大學(xué)計(jì)算機(jī)科學(xué)與技術(shù)系碩士研究生,在分布式系統(tǒng)國(guó)家重點(diǎn)實(shí)驗(yàn)室學(xué)習(xí)。對(duì)于軟件安全、linux系統(tǒng)和無(wú)線網(wǎng)絡(luò)有興趣,目前工作主要集中在緩沖區(qū)溢出攻擊的防范上。您可以通過cnnjuwy@hotmail.com與我聯(lián)系,也可以在南京大學(xué)小百合bbs上( http://bbs.或者 http://)與LinuxLover賬號(hào)聯(lián)系。



        本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評(píng)論

        發(fā)表

        請(qǐng)遵守用戶 評(píng)論公約

        類似文章 更多