MYC編譯器源碼分析之程序入口
整個編譯器是用C#語言寫的,上圖列出了MyC編譯器編譯一個C源文件的過程,編譯主路徑如下:
- 首先是入口Main函數用來解析命令行參數,讀取源文件,並開始編譯過程。Main函數在MyC.cs文件,而IO.cs文件主要保存讀取源碼文件的相關操作。下表是Main函數的源碼(批註用注釋的方式顯示),IO.cs文件用單獨的一個小節說明:
public static void Main()n{n tryn{n // 看源碼注釋,代碼是99年寫的,也就是說.NET當年正在開發中n // 可能那個時候虛擬機都沒有做好向Main函數傳遞命令行參數的開發n // 用了下面這個奇葩方法獲取程序的命令行參數n String[] args = Environment.GetCommandLineArgs();n // 初始化讀取源文件的IO對象,該對象負責將源文件以位元組流的方式n // 輸出給下一個對象 – 詞法分析n Io prog = new Io(args);n // 詞法分析對象,該對象的工作是過濾掉源碼中不必要的字元,比如空格n // 注釋之類的,並且把源碼中的字元歸類 – Tokenize,以便語法分析器n // 更方便的解析語法n Tok tok = new Tok(prog);n // 語法分析對象,解析完畢後即是代碼生成階段,但一般語法分析過程n // 都只會生成語法樹,這樣的設計可以對接多種結果文件輸出手段。比如n // 說,本例中生成可執行文件的exe.cs和生成IL源碼的asm.cs都是通過n // 遍歷語法樹,使用不同的輸出策略生成結果文件的n Parse p = new Parse(prog, tok);n // 採用自頂向下的方式進行語法解析n p.program();n // 編譯工作已經完成,關閉打開的源文件句柄等資源n prog.Finish();n }n catch (Exception e)n{n // 編譯過程中有任何錯誤,即中斷處理,列印錯誤消息並退出程序n Console.WriteLine("Compiler aborting: " + e.ToString());n }n}n
- MyC的語法很簡單,因此編譯過程是很乾凈的詞法分析、語法分析、代碼生成和結果輸出的過程。其中詞法分析代碼在tok.cs,語法分析代碼在parse.cs中,Emit.cs處理代碼生成,而Asm.cs和Exe.cs分別根據命令行參數的設置,來生成最終的可執行文件並選擇是否輸出IL源碼。 IO.cs - IO處理 將源文件讀取進內存,並採用流式處理的代碼都放在IO這個類裡面,IO的構造函數解析命令行參數,並打開源文件,等待Tok.cs裡面代碼的指令將源文件的字元一個個讀進內存並處理,下面是它的構造函數的源碼:
public Io(String[] a)n {n int i;n n args = a;n // 解析命令行參數,並根據參數打開內部的控制開關,詳情請看下面對ParseArgsn // 函數的源碼解讀n ParseArgs();n n // 打開要編譯的源文件n ifile = new FileStream(ifilename, FileMode.Open,n FileAccess.Read, FileShare.Read, 8192);n // 如果源文件不存在,報錯退出n if (ifile == null)n {n Abort("Could not open file "+ifilename+"n");n}n // 採用流式處理方式讀取源文件n rfile = new StreamReader(ifile); // open up a stream for readingn n // 根據源文件的名稱設定結果輸出文件的文件名n i = ifilename.LastIndexOf(.);n if (i < 0)n Abort("Bad filename "+ifilename+"");n int j = ifilename.LastIndexOf();n if (j < 0)n j = 0;n elsen j++;n n classname = ifilename.Substring(j,i-j);n n // 根據命令行參數決定是生成.exe、.dll等可執行文件,還是輸出包含n // IL源碼的.lst文件n if (genexe)n ofilename = classname+".exe";n if (gendll)n ofilename = classname+".dll";n if (genlist)n{n// 如果是要輸出IL源碼,因為原來的可執行文件也要輸出,需要創建一個新的文件n lst_ofilename = classname+".lst";n lst_ofile = new FileStream(lst_ofilename, FileMode.Create,n FileAccess.Write, FileShare.Write, 8192);n if (lst_ofile == null)n Abort("Could not open file "+ofilename+"n");n lst_wfile = new StreamWriter(lst_ofile);n }n }n
- 編譯器是在IO類里處理命令行參數的,參數解析實際上是一些字元串處理的活,本文解釋下關鍵代碼:
void ParseArgs()n {n int i = 1;n n // 程序至少需要兩個參數,否則就輸出幫助文字並退出n if (args.Length < 2)n {n Abort("myc [/debug] [/nodebug] [/list] [/dll] [/exe] [/outdir:path] filename.mycn");n }n n // 逐個遍曆命令行參數n while (true)n {n if (args[i][0] != /)n break;n // 處理 /? 這個參數,即輸出幫助文本n if (args[i].Equals("/?"))n {n Console.WriteLine("Compiler options:n myc [/debug] [/nodebug] [/list] [/dll] [/exe] [/outdir:path] filename.mycn");n Environment.Exit(1);n }n// 如果有 /debug 參數,則打開內部的 gendebug 開關,這個開關在代碼生成的過程n// 中會用到n if (args[i].Equals("/debug"))n {n gendebug = true;n i++;n continue;n }n// ... ... 跳過類似的代碼n// 如果有 /outdir 參數,則獲取命令行中指定的目錄路徑n if (args[i].Length > 8 && args[i].Substring(0,8).Equals("/outdir:"))n {n genpath = args[i].Substring(8);n i++;n continue;n }n// 前面那麼多的if相當於switch … case … default 塊裡面的 case 處理路徑n// 下面這段代碼即是 default 處理路徑 – 如果命令行參數符合前面的if條件n// 都會執行裡面的 continue 子句跳出循環,能執行到這裡,說明參數n// 是無法識別的參數,因此報告錯誤並退出執行n Abort("Unmatched switch = "+args[i]+"nArguments are:nmyc [/debug] [/nodebug] [/list] [/dll] [/exe] [/outdir:path] filename.mycn");n }n n // 如果前面的循環執行完畢,還有參數列表未處理,說明輸入了不支持的參數n if (args.Length-i != 1)n {n Abort("myc [/debug] [/nodebug] [/list] [/dll] [/exe] [/outdir:path] filename.mycn");n}n n // 最後一個參數是要編譯的源文件路徑n ifilename = args[args.Length-1]; // filename is lastn }n
IO類中大部分函數都是為Tok.cs服務的,因此其它函數在解釋詞法分析的時候說明
推薦閱讀:
※為什麼沒有國產的C/C++的編譯器?
※想用用flex和bison寫個C的編譯器,應該如何處理C的宏?
※如何理解自頂向下分析中的First表和Follow表?
※繼續學習編譯原理的意義是什麼?
※為什麼下面這種寫法沒法typedef指向const對象的指針?