MYC編譯器源碼之代碼生成

前面講過語法的解析之后辆沦,代碼生成方面就簡單很多了玖像。雖然myc是一個簡單的示例編譯器,但是它還是在解析的過程中生成了一個小的語法樹蕾羊,這個語法樹將會用在生成exe可執(zhí)行文件和il源碼的過程中侮叮。

編譯器在解析時避矢,使用emit類來產生中間的語法樹,語法樹的數據結構和操作方法在iasm這個類型里完成,源程序的語法解析完畢后审胸,Exe和Asm兩個類分別遍歷生成的語法樹產生最終的代碼亥宿。

我們來看幾個代碼的例子,下表的函數 Parser.program 里砂沛,在函數開始和結束的地方分別調用了 prolog 和 epilog 兩個函數烫扼,這兩個函數的目的就是在語法解析的前后執(zhí)行一些準備和掃尾工作。如在編譯過程的開始階段碍庵,根據.net assembly的要求創(chuàng)建好模塊(module)和類(class)映企,雖然c語言是一個面向過程的語言,但是在.net是一個面向對象的環(huán)境静浴,所有的代碼都應該保存在一個類里堰氓。

public void program()
  {
  prolog();
  while (tok.NotEOF())
    {
    outerDecl();
    }
  if (Io.genexe && !mainseen)
    io.Abort("Generating executable with no main entrypoint");
  epilog();
  }

void prolog()
  {
  emit = new Emit(io);
  emit.BeginModule();       // need assembly module
  emit.BeginClass();
  }

而在emit里,BeginModule和BeginClass這兩個函數的代碼如下:

public void BeginModule()
  {
  // 委托給Exe類來創(chuàng)建這個module苹享,雖然在.net里双絮,一個assembly可以由
  // 多個module組成,但是在C程序里得问,只要一個module就足夠了掷邦,因此
  // 下面的代碼并沒有在生成IL源碼的時候產生module。
  exe.BeginModule(io.GetInputFilename());
  }

public void BeginClass()
  {
  // 委托給Exe類來創(chuàng)建class椭赋,再進一步跟蹤代碼的時候,會發(fā)現它其實
  // 是根據反射技術來創(chuàng)建類型的或杠。
  exe.BeginClass(Io.GetClassname(), TypeAttributes.Public);
  // 如果在執(zhí)行程序的命令行里哪怔,啟用了生成源碼的開關,那么
  // 將會輸出IL class的源碼定義向抢。
  if (Io.genlist)
    io.Out(".class " + Io.GetClassname() + "{\r\n");
  }

.NET里认境,可以使用反射技術來生成assembly、類型和函數挟鸠,下表就是Exe類的BeginModule函數的源碼:

public void BeginModule(string ifile)
  {
  // .net的動態(tài)assembly創(chuàng)建功能叉信,要求跟appdomain綁定
  appdomain = System.Threading.Thread.GetDomain();
  appname = getAssemblyName(filename);
  // 調用AppDomain.DefineDynamiceAssembly創(chuàng)建一個Assembly,以這個為
  // 起點艘希,可以創(chuàng)建類型硼身,創(chuàng)建函數并執(zhí)行。實際上覆享,.net上的IronPython等
  // 動態(tài)語言的實現就非常依賴這個技術佳遂。
  appbuild = appdomain.DefineDynamicAssembly(appname,
                         AssemblyBuilderAccess.Save,
                         Io.genpath);
  // 在.net里,所有的代碼實際上都應該保存在一個module里撒顿。
  emodule = appbuild.DefineDynamicModule(
        filename+"_module",
        Io.GetOutputFilename(),
        Io.gendebug);
  Guid g = System.Guid.Empty;
  if (Io.gendebug)
    srcdoc = emodule.DefineDocument(ifile, g, g, g);
  }

準備工作做好了以后丑罪,就可以生成語法樹了,編譯器在解析語法的過程當中,不停的往語法樹里添加元素吩屹,如在編譯函數的過程中跪另,以處理while循環(huán)為例(其中一個調用路徑是:program -> outerDecl -> declFunc -> blockOuter -> fcWhile

void fcWhile()
  {
  // 在一般的il或者匯編語言里,循環(huán)和判斷語句一般都是在不同路徑的入口
  // 出定義好標簽(label)煤搜,再通過判斷條件的方式跳轉到指定的label實現的
  String label1 = newLabel();
  String label2 = newLabel();
 
  // 記錄當前源碼的位置免绿,以便生成IL源碼的時候可以把源代碼和IL代碼對照生成
  CommentHolder();  /* mark the position in insn stream */
  // 一般來說,循環(huán)語句至少有兩個分支代碼塊宅楞,一個是繼續(xù)循環(huán)的代碼塊针姿,
  // 一個是跳出循環(huán)的代碼塊,看后面的代碼厌衙,這個label是循環(huán)中執(zhí)行的代碼塊
  // 開始的地方距淫,以便滿足條件的時候跳到開頭繼續(xù)執(zhí)行
  emit.Label(label1);
  tok.scan();
  // 做一些錯誤判斷
  if (tok.getFirstChar() != '(')
    io.Abort("Expected '('");

  // 處理循環(huán)條件的判斷語句相關代碼
  boolExpr();
  CommentFillPreTok();
  // 跳出循環(huán)的label
  emit.Branch("brfalse", label2);

  // 循環(huán)內部的代碼塊,進入blockInner進行循環(huán)里面的編譯
  blockInner(label2, label1);   /* outer label, top of loop */
  // 如果滿足循環(huán)條件婶希,跳轉到代碼塊開頭繼續(xù)執(zhí)行
  emit.Branch("br", label1);
  
  // 循環(huán)結束榕暇,跳出循環(huán)的地方
  emit.Label(label2);
  }

而在emit類型里,各個方法只是將解析出來的語法元素添加到語法樹里喻杈,語法樹的節(jié)點彤枢、數據結構和操作方法都在IAsm這個類里定義,如下表是 Branch 的源碼:

public void Branch(String s, String lname)
  {             // this is the branch source
  NextInsn(1);
  // 往語法樹里添加一個類別為 Branch 的元素
  icur.setIType(IAsm.I_BRANCH);
  // 指令名稱
  icur.setInsn(s);
  // 指令參數
  icur.setLabel(lname);
  }

當程序編譯完成后筒饰,Exe類和Asm類則分別遍歷語法樹生成最終的結果缴啡,在myc編譯器的源碼里,Parser.declFunc函數通過調用Emit.IL函數來完成程序的生成:

// 因為C程序大部分都是由函數組成的瓷们,而且函數使用到的變量或者其他函數业栅,
// 都必須在函數之前定義,所以只需要在解析函數的時候實時生成代碼即可
void declFunc(Var e)
  {
#if DEBUG
  Console.WriteLine("declFunc token=["+tok+"]\n");
#endif
  CommentHolder();  // start new comment
  // 記錄解析出來的函數名
  e.setName(tok.getValue()); /* value is the function name */
  // 如果函數名是main谬晕,則設置一個標識位 - mainseen為true
  // 在外層的函數里碘裕,會通過判斷這個標志來確定程序是否有語義錯誤
  if (e.getName().Equals("main"))
    {
    if (Io.gendll)
      io.Abort("Using main entrypoint when generating a DLL");
    mainseen = true;
    }
  // 函數名也是一個全局變量,放到全局變量表里攒钳,以便做語義分析
  // 例如要調用的函數之前沒有定義帮孔,則應該報錯,在后文我們將
  // 看到語義方面的處理
  staticvar.add(e);     /* add function name to static VarList */
  paramvar = paramList();   // track current param list
  e.setParams(paramvar);    // and set it in func var
  // 記錄函數里面定義的局部變量
  localvar = new VarList(); // track new local parameters
  CommentFillPreTok();

  // 開始生成函數的prolog不撑,例如參數傳遞文兢,this對象等
  emit.FuncBegin(e);
  if (tok.getFirstChar() != '{')
    io.Abort("Expected ‘{'");
  // 遞歸分析函數里面的源碼
  blockOuter(null, null);
  emit.FuncEnd();
  
  // 解析完整個函數后,執(zhí)行代碼生成操作
  emit.IL();
  // 如果需要生成IL源碼焕檬,則調用LIST函數生成IL源碼
  if (Io.genlist)
    emit.LIST();
  emit.Finish();
  }

而emit.IL函數就是用Exe類型遍歷整個語法樹禽作,生成結果程序:

public void IL()
  {
  IAsm a = iroot;
  IAsm p;

  // 循環(huán)遍歷整個語法樹
  while (a != null)
    {
    // 根據語法樹里各個節(jié)點的類型來執(zhí)行對應的操作
    switch (a.getIType())
      {
      case IAsm.I_INSN:
    exe.Insn(a);
    break;
      case IAsm.I_LABEL:
    exe.Label(a);
    break;
      case IAsm.I_BRANCH:
    exe.Branch(a);
    break;
      // 省略一些代碼
      default:
    io.Abort("Unhandled instruction type " + a.getIType());
    break;
      }
    p = a;
    a = a.getNext();
    }
  }

而Exe類型執(zhí)行真正的代碼生成,如前面IL函數揩页,在碰到I_BRANCH類型的節(jié)點時旷偿,調用Exe.Branch函數在動態(tài)Assembly (DynamicAssemby) 里生成代碼:

public void Branch(IAsm a)
  {
  Object o = opcodehash[a.getInsn()];
  if (o == null)
    Io.ICE("Instruction branch opcode (" + a.getInsn() + ") not found in hash”);
  // 使用 ILGenerator 類生成跳轉IL指令烹俗。
  il.Emit((OpCode) o, (Label) getILLabel(a));
  }

而Asm類也采用類似的方法生成IL源碼。

最后萍程,myc編譯器里也有一些語義方面的處理幢妄,如前面講到的函數調用時,如果被調用的函數沒有定義的話茫负,應該拋出異常的情況蕉鸳,在Parser.statement(即編譯實際的C語句的函數)中就有所體現

void statement()
  {
  Var e;
  String vname = tok.getValue();

  CommentHolder();  /* mark the position in insn stream */
  switch (io.getNextChar())
    {
    case '(':           /* this is a function call */
      // 省略一些語法處理方面的代碼
     tok.scan();        /* move to next token */
     // 下面這一行即在生成函數調用代碼之前,在全局變量列表里
   // 查找要調用的函數是否已經定義了忍法,如果沒有定義潮尝,則應該報告此錯誤
      e = staticvar.FindByName(vname); /* find the symbol (e cannot be null) */
      emit.Call(e);
     
      // 省略后面的代碼
   }
  if (tok.getFirstChar() != ';')
    io.Abort("Expected ';'");
  tok.scan();
  }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市饿序,隨后出現的幾起案子勉失,更是在濱河造成了極大的恐慌,老刑警劉巖原探,帶你破解...
    沈念sama閱讀 210,835評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乱凿,死亡現場離奇詭異,居然都是意外死亡咽弦,警方通過查閱死者的電腦和手機徒蟆,發(fā)現死者居然都...
    沈念sama閱讀 89,900評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來型型,“玉大人段审,你說我怎么就攤上這事矮锈∪缡В” “怎么了观挎?”我有些...
    開封第一講書人閱讀 156,481評論 0 345
  • 文/不壞的土叔 我叫張陵馒索,是天一觀的道長。 經常有香客問我板驳,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,303評論 1 282
  • 正文 為了忘掉前任嘱函,我火速辦了婚禮,結果婚禮上埂蕊,老公的妹妹穿的比我還像新娘往弓。我一直安慰自己,他們只是感情好蓄氧,可當我...
    茶點故事閱讀 65,375評論 5 384
  • 文/花漫 我一把揭開白布函似。 她就那樣靜靜地躺著,像睡著了一般喉童。 火紅的嫁衣襯著肌膚如雪撇寞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,729評論 1 289
  • 那天,我揣著相機與錄音蔑担,去河邊找鬼牌废。 笑死,一個胖子當著我的面吹牛啤握,可吹牛的內容都是我干的鸟缕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,877評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼排抬,長吁一口氣:“原來是場噩夢啊……” “哼懂从!你這毒婦竟也來了?” 一聲冷哼從身側響起蹲蒲,我...
    開封第一講書人閱讀 37,633評論 0 266
  • 序言:老撾萬榮一對情侶失蹤番甩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后悠鞍,有當地人在樹林里發(fā)現了一具尸體对室,經...
    沈念sama閱讀 44,088評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,443評論 2 326
  • 正文 我和宋清朗相戀三年咖祭,在試婚紗的時候發(fā)現自己被綠了掩宜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,563評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡么翰,死狀恐怖牺汤,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情浩嫌,我是刑警寧澤檐迟,帶...
    沈念sama閱讀 34,251評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站码耐,受9級特大地震影響追迟,放射性物質發(fā)生泄漏。R本人自食惡果不足惜骚腥,卻給世界環(huán)境...
    茶點故事閱讀 39,827評論 3 312
  • 文/蒙蒙 一敦间、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧束铭,春花似錦廓块、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至懈万,卻和暖如春拴清,著一層夾襖步出監(jiān)牢的瞬間靶病,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評論 1 264
  • 我被黑心中介騙來泰國打工贷掖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫡秕,地道東北人。 一個月前我還...
    沈念sama閱讀 46,240評論 2 360
  • 正文 我出身青樓苹威,卻偏偏與公主長得像昆咽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子牙甫,可洞房花燭夜當晚...
    茶點故事閱讀 43,435評論 2 348

推薦閱讀更多精彩內容

  • 整個編譯器是用C#語言寫的掷酗,上圖列出了MyC編譯器編譯一個C源文件的過程,編譯主路徑如下: 首先是入口Main函數...
    懿民閱讀 469評論 0 2
  • 上次我們翻譯了由Unity開發(fā)人員JOSH PETERSON所寫的窟哺、IL2CPP深入講解系列的第一期泻轰,現在第二期的...
    IndieACE閱讀 9,431評論 0 11
  • 一、溫故而知新 1. 內存不夠怎么辦 內存簡單分配策略的問題地址空間不隔離內存使用效率低程序運行的地址不確定 關于...
    SeanCST閱讀 7,781評論 0 27
  • 我不能理解為什么才剛迎接以2開頭的這個年齡就要得考慮那么多問題且轨?為什么是父母的事情浮声,要牽動到孩子?為什么就想輕輕松...
    你不知道的成全6閱讀 127評論 0 0