摘要:編譯工具是進行軟件系統(tǒng)開發(fā)和測試的強大工具?,F(xiàn)有的編譯工具復(fù)雜、龐大并且為數(shù)不多,諸如Vtune之類的程序剖面分析工具更是昂貴,但是對程序進行靜態(tài)分析和動態(tài)跟蹤評估等工作在很多方面十分有用,因此,以現(xiàn)有編譯器為主體進行編譯器擴展來現(xiàn)相應(yīng)工具是非常有必要的。LCC是一款廣為使用的ANSI C編譯器,由于自身的簡單和使用高效特點,由它出發(fā)通過修改、定制,構(gòu)造其他特殊目的的編譯器相關(guān)工具就變得相對簡單。
關(guān)鍵詞:編譯器 編譯工具 LCC 編譯后端
Implementation of compilation-tools based on lcc
Abstract Compilation tools are powerfull tools for development and testing of software system. Compilation-tools existed are complex, large and infrequent and program profiling tools as VTune are costly. However, static analysing and dynamic tracing and evaluating for program is useful sometimes, so that, implementation these tools by extending a compiler existed is necessary. Lcc is a widely used compiler for ANSI C. construction compilation-tools by modifying, and configurating it is relatively simple because of its simple and effectiveness.
Keywords compiler, compilation- tools, lcc, compiler back-end
1.簡介
編譯器是編譯工具的一種,是進行軟件開發(fā)不可或缺的工具。編譯器強大而復(fù)雜,其他編譯相關(guān)工具也是如此??v觀各種編譯相關(guān)的工具,如GCC編譯器、lint語法檢查器,以及許多程序剖面分析器等等,一直在強大、龐大、復(fù)雜和昂貴之間游走。對于許多特定目的的工作而言,一款輕便、高效和廉價的編譯工具是十分具有吸引力的,因此,以一個簡單高效的編譯器為主體,針對不同的用途進行定制,有時就變得十分有必要。本文以此為目的,介紹了一種以LCC為主體進行配置和實現(xiàn)編譯器相關(guān)工具的具體方法。
LCC是一款免費、開放源代碼并被廣泛使用的ANSI C編譯器。它的作者是美國ATT實驗室的Christopher W. Fraser和美國普林斯頓大學(xué)教授David R. Hanson,當(dāng)前版本為4.2。LCC的分析代碼由手工編寫而成,編譯速度非??欤籐CC沒有單獨的優(yōu)化遍,但對于大多數(shù)應(yīng)用來說,LCC產(chǎn)生的代碼已經(jīng)足夠快了。[1],與廣為使用的GCC形成鮮明對比的是,LCC的源代碼簡單、緊湊,十分有利于進行修改。以可變目標(biāo)為目的的設(shè)計,移植或?qū)Ω鞣N不同用途后端的實現(xiàn)比較簡單。我們的工作就針對該編譯器后端進行自定義配置的方法展開,它與LCC編譯器的移植工作沒有區(qū)別。
2.數(shù)據(jù)表示
類型和符號的數(shù)據(jù)結(jié)構(gòu)是編譯器的核心數(shù)據(jù)表示。對于編譯后端而言,中間代碼有關(guān)的數(shù)據(jù)結(jié)構(gòu)也是非常重要的。LCC使用DAG(有向無環(huán)圖)對中間代碼進行描述,它使用二叉樹的鏈表形式進行組織。所有的中間代碼通過代碼表進行管理,這種代碼表與優(yōu)化器中用于控制流分析的基本塊有所區(qū)別,但可以看作是一種擴展基本塊[1],通過它可以方便進行基本塊的劃分,從而插入單獨的優(yōu)化遍,使LCC成為優(yōu)化編譯器。
編譯器后端的實現(xiàn)者需要熟悉至少四種LCC的核心數(shù)據(jù)結(jié)構(gòu),分別是類型、符號、DAG節(jié)點和后端接口描述,本文不對其細(xì)節(jié)進行贅述,可參考LCC編譯器源代碼。
對于這四種數(shù)據(jù)結(jié)構(gòu)中,LCC從概念上將其劃分為三部分,一部分為編譯前端私有數(shù)據(jù),根據(jù)LCC后端的訪問約定,LCC后端并不訪問這個部分的數(shù)據(jù);第二部分為前后端共享的數(shù)據(jù);第三部分為后端私有數(shù)據(jù),前端對此一無所知。其中,前兩個部分的數(shù)據(jù)從形式上看沒有區(qū)別,而第三部分,作為后端的私有數(shù)據(jù)結(jié)構(gòu)由后端自己實現(xiàn),由各結(jié)構(gòu)中的x域成員進行維護,它們的組織形式如下:
LCC為生成可執(zhí)行目標(biāo)代碼的代碼生成器提供了一種自動代碼生成的方法,使用這種方法,后端的實現(xiàn)者只需要提供一份規(guī)范描述的目標(biāo)機器描述文本,由一個叫做iburg的程序據(jù)其自動生成相關(guān)代碼,而上圖所示的x域成員類型由相應(yīng)的后端文件提供,其數(shù)據(jù)由自動生成的代碼進行訪問,相關(guān)類型和數(shù)據(jù)的聲明在config.h文件中聲明。對于手工生成的代碼,這些x域成,由實現(xiàn)者自行實現(xiàn)和訪問,可以將config.h文件替換為自己的實現(xiàn)。
LCC的中間代碼表示使用的是DAG(有向無環(huán)圖),其中多入口的節(jié)點就是公共子表達式節(jié)點。使用這樣的表示方法起到了刪除公共子表達式的目的,為后端生成高質(zhì)量的目標(biāo)代碼提供了有力的保障。
LCC中間代碼所提供的指令是通過仔細(xì)篩選的,能夠匹配大多數(shù)機器的硬件指令[2]:
實際使用中的中間代碼操作符由上表所示的操作碼和具體表示數(shù)據(jù)類型的后綴組合使用。
3.后端的移植和實現(xiàn)
LCC的后端是通過實現(xiàn)后端接口Interface結(jié)構(gòu)聲明的數(shù)據(jù)和函數(shù)來實現(xiàn)的。Interface接口包括了一些接口標(biāo)記和函數(shù)指針,還包含了一個可自定義的擴展接口Xinterface,它的作用是為基于iburg緊縮規(guī)范自動生成的代碼提供統(tǒng)一的接口。LCC的后端的任務(wù)就是實現(xiàn)并設(shè)置這些接口數(shù)據(jù)和函數(shù)。除開Xinterface接口,根據(jù)它們作用,下面分類介紹:
4.接口標(biāo)記
后端接口包括數(shù)量適中的接口標(biāo)記數(shù)據(jù),它們描述了目標(biāo)機器和后端實現(xiàn)的各種特性,為生成正確的代碼和數(shù)據(jù)提供依據(jù):
其中CALLB操作碼是中間語言操作符CALL和后綴B的組合,它表示了對一個返回值類型為結(jié)構(gòu)(struct)的函數(shù)的調(diào)用,由于一個類型為結(jié)構(gòu)的變量常常不能存儲在一個寄存器之內(nèi),因此它需要進行特殊處理;同理,ARGB表示傳遞一個結(jié)構(gòu)類型的函數(shù)參數(shù)。
5.接口函數(shù)
后端接口包含一些重要的接口函數(shù),編譯器前端通過它們實現(xiàn)代碼的生成和發(fā)送工作,這些函數(shù)接口描述如下:
6.手工實現(xiàn)
實際上手工實現(xiàn)LCC編譯器后端是簡繁參半。簡單的是,后端的實現(xiàn)者可以自己定義Xinterface接口和Xnode、Xtype以及Xsymbol,而暴露給編譯前端的接口就會變得十分簡潔,只要實現(xiàn)Interface接口函數(shù),就能使整個LCC編譯器順利的工作;麻煩的是,如此一來所有代碼生成工作包括指令選擇、寄存器分配等就需要全部自行設(shè)計和實行。
但是對于并不生成實際代碼但完成其他重要任務(wù)的編譯后端而言,這種做法則十分可行。這樣,可以完全拋棄config.h中聲明的結(jié)構(gòu)和函數(shù),僅提供一個簡潔的接口即可。實際上,正確的設(shè)置接口標(biāo)記,然后將Interface接口函數(shù)都設(shè)置為什么都不做的空函數(shù),并定義Xinterface為空結(jié)構(gòu),實現(xiàn)了一個很好的語法檢查器,它不實際生成并發(fā)送代碼,只是進行語法語義檢查,類似于UNIX平臺下的lint。
具體而言手工生成LCC后端的工作如下:
●聲明相應(yīng)的X類型
●定義一個Interface接口實例
●設(shè)置所有接口標(biāo)記
●實現(xiàn)所有接口函數(shù)
7.基于iburg緊縮規(guī)范的自動代碼生成
對于使用基于iburg緊縮規(guī)范自動代碼生成的后端實現(xiàn)方法而言,后端接口數(shù)據(jù)和函數(shù)分為兩個部分:一部分是LCC為優(yōu)化后端接口函數(shù)而業(yè)已實現(xiàn)的那部分接口函數(shù);而另一部分是根據(jù)代碼生成規(guī)范自動生成的接口數(shù)據(jù)和函數(shù)。在LCC的發(fā)展過程中,開發(fā)者將后端函數(shù)分為目標(biāo)無關(guān)與目標(biāo)相關(guān)部分,并實現(xiàn)了目標(biāo)無關(guān)代碼的部分代碼。這樣,iburg規(guī)范中需要的內(nèi)容就減到了最少。這樣做的目的是人工編寫一個規(guī)模中等的代碼生成器需要1000到1500行的C代碼。如果盡可能地隔離與目標(biāo)機器相關(guān)的特性,這個數(shù)字就會銳減一半。盡管這樣做的代價增加了大約1000行與機器無關(guān)的代碼,但是,只要有兩個目標(biāo)機器,就能從這種方法中獲得益處。更重要的是,如果我們盡可能多的使用已有(即與機器無關(guān))代碼,開發(fā)一個新的代碼生成器就變得更加容易了。[1]
iburg是代碼生成器的生成器,它接受一份用類似于YACC語法規(guī)范的機器描述和自定義代碼段,生成用于編譯器后端的接口函數(shù)和接口數(shù)據(jù),以配合編譯器進行正確的工作。
使用iburg自動生成代碼生成器需要使用LCC提供的config.h文件,它定義了相關(guān)的X開頭的結(jié)構(gòu)類型并聲明了一些后端目標(biāo)無關(guān)的工作函數(shù)。iburg生成相關(guān)的接口數(shù)據(jù)和接口函數(shù)。
iburg規(guī)范的語法如下,term和nonterm分別代表終結(jié)符和非終結(jié)符[4]:
grammar: ‘%{‘ 配置文本 ‘%}’ { dcl } %% { rule } [ %% C代碼 ]
dcl: %startnonterm
%term { term = 整數(shù) }
rule:
nonterm : tree template [ C表達式 ]
tree:
term [ ‘(‘ tree [ , tree ] ‘)’ ]
nonterm
template:
“ { 任意非引號字符 } “
iburg規(guī)范是按行組織的,由它自動生成的程序被叫做BURM。單詞“%{”、“%}”和“%%”必須單獨一行,每個dcl或rule必須出現(xiàn)在一行上,rule指定了代碼選擇進行的模式匹配規(guī)則,template指定了指令模版。配置文本是C語言代碼,它被原封不動的復(fù)制到BURM的開頭。如果第二個“%%”出現(xiàn),那么它之后的正文也被原封不動的復(fù)制到BURM的末尾。%start聲明了待分析樹的根,%term聲明了樹節(jié)點的編碼。
自動生成LCC后端的工作如下:
創(chuàng)建一份iburg規(guī)范文本
在規(guī)范文本中書寫代碼生成規(guī)則
在規(guī)范文本中書寫代碼選擇以外的接口函數(shù)
定義接口實例并進行接口綁定
8.結(jié)論
LCC由于自身緊湊的設(shè)計和簡單的代碼以及比較清晰的接口設(shè)計使得修改和定制的工作比較簡單,這樣,對于很多具有實用價值的工具的實現(xiàn)不再因為編譯器相關(guān)工具的復(fù)雜而變得遙不可及,它為有特殊用途的編譯工具的實現(xiàn)提供了一個有價值的選擇。