標籤:c編譯器 ucc 彙編代碼產生
6.1 彙編代碼產生簡介
曆經詞法分析、文法分析、語義檢查和中間代碼產生階段,我們終於來到了“目標代碼產生階段”,由於UCC編譯器的目標代碼即為32位x86彙編代碼,因此我們就把本章稱為“彙編代碼產生”。UCC編譯器中的大部分原始碼都適用於Windows和Linux平台,但Windows平台上預設的彙編器支援Intel風格的x86彙編代碼,而Linux平台預設的彙編器則採用AT&T風格的x86彙編代碼,兩者在彙編文法上有一些差別,為節省篇幅,我們主要針對Linux平台的AT&T彙編進行討論。到了這一章,UCC編譯器面對的輸入早已不再是C原始碼,而主要是由各個基本塊構成的鏈表,我們要把基本塊中的中間代碼翻譯成x86彙編代碼。我們在“第1.5節結合C語言來學彙編”中,已介紹過x86彙編代碼的相關文法和語義,這裡不再重複。
還是通過一個簡單的例子來瞭解一下UCC編譯器的彙編代碼產生,6.1.1所示,第1至6行是一個簡單的C程式,第11至58行是由UCC編譯器產生的彙編代碼。形如第13行C++風格的注釋是我們人為加上去的,用於說明第14行的彙編代碼“.data”是由Segment()函數產生。我們省略了main函數對應的彙編代碼。
圖6.1.1 彙編代碼產生的例子
在C程式中出現的字串可被視為全域的字元數組(當然對C程式員而言,該字元數組的名稱不可見),通過第15行的EmitStrings()函數,UCC編譯器可以為字串產生形如第16行的字元數組,而通過第18行的EmitFloatConstants函數,則可以為形如“1.0”的浮點常數分配儲存空間。UCC編譯器會隱式地為字串和浮點常數命名,例如第16行的“.str0”和第19行的“.flt0”,這些名字都以“.”開始。按C語言的文法,由C程式員命名的變數名或函數名不會以“.”開始,這就可保證不會發生名稱上的重名。在彙編代碼中,符號名是允許用“.”開始的。第21行的EmitGlobals函數用於處理C程式員定義的全域變數,這會產生形如第22至23行的彙編代碼。第26行的EmitFunction()用於產生函數f對應的彙編代碼,第27至58行所示。
圖6.1.1第30至34行的代碼用於儲存寄存器的值,第35行用於在棧空間中預留記憶體空間,用來存放局部變數和臨時變數,這部分工作被稱為“Prologue序言”,即在函數開始執行時要處理的工作。而圖6.1.1第53至57行被稱為“Epilogue尾聲”,用於恢複原先儲存的寄存器值,第58行的彙編指令ret用於從棧中取出返回地址並返回。而函數f的返回值在第50至51行進行計算,並儲存於寄存器eax。第37行的EmitBlock函數用來為某一基本塊產生彙編代碼。UCC編譯器內部用英文單詞generate來表示中間代碼的產生,而用emit來表示彙編代碼的產生,這裡我們統一翻譯為“產生”。
我們還注意到,圖6.1.1第2行的形參num在彙編代碼中,對應的名稱是第50行的“20(%ebp)”,而第3行的局部變數b對應的是第40行的“-8(%ebp)”,這再次提醒我們,在彙編層次,我們需要為局部變數和形式參數設定新的名稱。為了得到某個符號對象p在彙編代碼中的名稱,UCC編譯器定義了函數GetAccessName,其介面如下所示,稍後,我們會對這個函數進行分析。
static char* GetAccessName(Symbol p);
下面,我們來分析一下用於產生圖6.1.1彙編代碼的函數EmitTranslationUnit,6.1.2所示,第1至11行的函數EmitTranslationUnit會為整個翻譯單元產生彙編代碼,我們已在圖6.1.1的注釋中標出BeginProgram等函數的作用。第8行的ImportFunctions用於在Windows平台Intel風格的彙編中,產生形如“EXTRN f:NEAR32”的函式宣告,在Linux平台上ImportFunctions並無作用。圖6.1.2第12至26行為EmitFunctions的代碼,通過第16行的while迴圈,我們在第21行調用EmitFunction來為各個函數產生彙編代碼。
圖6.1.2 EmitTranslationUnit()
圖6.1.2第27至58行為函數EmitFunction的代碼,在第33行調用的Export函數用於在Linux平台上產生形如“.globl f”的函式宣告,第35行的DefineLabel函數用於產生形如“f:”的標號。按C標準的規定,當“函數的返回值是結構體對象,且該對象的大小不落在{1,2,4,8}”時,C編譯器會隱式地為該函數添加一個參數,該參數的類型是指向結構體對象的指標。例如,以下結構體struct Data的對象要佔32位元組,C編譯器會為函數GetData隱式地添加一個structData * recvaddr參數,圖6.1.2第36至47行的if語句用於對此進行處理。
struct Data {int dt[8];};
// C程式員設定的函數介面
struct Data GetData(int num);
//被C編譯器隱式地改為
void GetData(struct Data * recvaddr, int num);
圖6.1.2第48行調用的LayoutFrame函數用來計算“形式參數、局部變數和臨時變數”在活動記錄中的位移,並返回“局部變數和臨時變數”在棧中所佔記憶體的總和,我們會在稍後對這個函數進行分析。圖6.1.2第50行調用EmitPrologue來產生“序言”,6.1.1第29至35行所示,圖6.1.1第35行“subl $16,%esp”中的常數16,就是“函數f裡的局部變數和臨時變數所佔棧記憶體的總和”。通過圖6.1.2第51至55行的while迴圈,我們可為各基本塊產生彙編代碼,這主要是由第54行調用的EmitBlock函數來完成。圖6.1.2第56行調用的EmitEpilogue函數來產生“尾聲”部分,6.1.1第52至58行所示。
圖6.1.2中調用的EmitStrings和EmitFloatConstants等函數並不複雜,我們就不再展開討論,在後續的章節中,我們把分析的焦點放在基本塊的翻譯上,即EmitBlock函數。在本節中,我們再來分析一下前面遇到的LayoutFrame和GetAccessName這兩個函數,其中LayoutFrame函數的代碼6.1.3所示。按照C標準的規定,在被調函數返回後,寄存器ebx、esi和edi的值要和函數調用前的值一樣,這些寄存器被稱為“保值寄存器”。UCC編譯器會在“函數的序言”中把這幾個寄存器的值入棧;而在“函數的尾聲”中恢複這幾個寄存器的值,從而實現“保值”的要求。另外,當被調函數返回時,寄存器ebp需要再次指向主調函數的活動記錄,因此被調函數也要在棧中先儲存ebp寄存器的值,因此總共需要由被調函數保護的寄存器有4個,即6.1.3第16行的宏PRESERVE_REGS所對應的值。
圖6.1.3 LayoutFrame()
圖6.1.3第3至13行給出了棧的布局圖,第1個形參的位置為“20(%ebp)”,而第1個局部變數或臨時變數的位置為“-4(%ebp)”。第24至35行的while迴圈用於為各個形參計算其位移,其位移從20開始依次遞增;第39至54行的while迴圈用於為局部變數和臨時變數計算位移,其位移從“-4”開始依次遞減。第56行返回“局部變數和臨時變數所佔用的棧空間的總和”。
接下來,我們來看一下Linux版的函數GetAccessName,其代碼6.1.4所示,第6至8行用於處理整型常數,在AT&T彙編指令中其符號形如”$4”;浮點常數對應的名稱已在圖6.1.1第18行的EmitFloatConstants函數中,被設定為形如“.flt0”;第9至12行用來產生形如“.str0”的字串名和“.BB2”標號。全域變數名和“處於函數體外的靜態變數名”,仍可用於彙編代碼中,第17行的賦值語句對此進行設定。為了避免重名,UCC編譯器會對“位於函數體內的靜態變數名”進行改名,得到的名稱形如第23行的“c.1”,第18至26行對此進行處理。
圖6.1.4 GetAccessName()
對於局部變數、形式參數和臨時變數,在彙編代碼中,我們用形如“20(%ebp)”這樣的符號來表示,圖6.1.4第27至32行會根據我們在圖6.1.3的LayoutFrame函數中計算出來的位移,來設定相應的符號名。在彙編代碼中,函數名仍然可以直接使用,圖6.1.4第33至34行對此進行處理。對於全域或靜態“數組元素和結構體成員”,我們可用形如第42行的“arr+12”符號來表示,而對於局部的數組或結構體對象,則使用形如“20(%ebp)”的符號,6.1.4第35至56行所示。當我們已經得到某一符號p在彙編代碼中的名稱p->aname後,就沒有必要再重複計算,第2行的if語句會對此進行判斷。
在這小節中,結合圖6.1.1的例子,討論了彙編代碼產生的總體流程。在彙編代碼產生中,一個很重要的問題就是寄存器的分配。在後面的章節中,我們會對“用於為基本塊產生彙編代碼”的函數EmitBlock進行討論。
C編譯器剖析_6.1 彙編代碼產生_簡介