C編譯器入門~想懂低階系統從自幹編譯器開始~
  • 譯者序
  • 前言
    • 符號與規範
    • 本書的開發環境
    • 關於作者
    • 結束前言之前
  • 機械語言與組譯器
    • CPU 與記憶體
    • 什麼是組譯器
    • C程式和所對應的組合語言
      • 簡單的範例
      • 包含呼叫函式的範例
    • 本章小結
  • 創造計算機等級的語言
    • 第1步:創造能編譯1個整數的語言
    • 第2步:製作可以算加減法的編譯器
    • 第3步:加入標記解析器(tokenizer)
    • 第4步:改良錯誤訊息
    • 文法的記法與遞迴下降分析法
      • 將文法結構表示為樹(tree)
      • 以生成規則定義文法
      • 以 BNF 描述生成規則
      • 簡單的生成規則
      • 以生成規則描述運算子的優先順序
      • 包含遞迴的生成規則
      • 遞迴下降語法分析
    • 堆疊機
      • 堆疊機的概念
      • 編譯成堆疊機指令
      • 以x86-64實作堆疊機的方法
    • 第5步:製作可進行四則運算的編譯器
    • 第6步:單項加與單項減
    • 第7步:比較運算子
      • 修改標記解析器
      • 新的文法
      • 產生組合語言指令
  • 分離編譯與連結
    • 分離編譯
      • 分離編譯與其必要性
      • 標頭檔的必要性與其內容
      • 連結錯誤
      • 全域變數的宣告與定義
    • 第8步:分割檔案與修改 Makefile
      • 分割檔案
      • 修改 Makefile
  • 函式與區域變數
    • 第9步:1個字的區域變數
      • 堆疊上的變數空間
      • 修改標記解析器
      • 修改分析器
      • 左邊值與右邊值
      • 從任意的記憶體位址取得其值
      • 修改指令產生器
      • 修改主函式
    • 第10步:複數文字的區域變數
    • 第11步:return
    • 1973年的C編譯器
Powered by GitBook
On this page

Was this helpful?

  1. 函式與區域變數

第11步:return

本節會加上return的支援,讓我們可以編譯底下的程式碼:

a = 3;
b = 5 * 6 - 8;
return a + b / 2;

return也可以出現在程式的中途。就像普通的C語言,程式執行到最初的return就會結束,從函式中返回。舉例來說底下的程式會回傳最初return的值,也就是5:

return 5;
return 8;

為了實作這個功能,我們先來想想加上return的文法會長什麼樣吧。至今為止我們的陳述都只有式子,新的文法則需要接受return <式子>;這樣的陳述。所以文法會變成像這樣:

program = stmt*
stmt    = expr ";"
        | "return" expr ";"
...

實作上,標記解析器、分析器、指令產生器全部都需要逐步修改。

首先讓標記解析器可以識別return這個標記,以TK_RETURN型的標記來代表。像return或while、int這類文法上有特別意義的標記(非關鍵字)數量非常有限,像這樣以不同型態來代表不同標記的作法會比較簡潔。

接下來,要判斷標記是否為return,似乎標記解吸器只要判斷剩餘的輸入字串是否以 return 開頭就可以了,但這樣的話像returnx這樣的標記會被標記解吸器誤判為return和x。所以,除了輸入要以 return 開頭,還需要判斷接下來下一個字不是標記的一部份。

判斷所給定的文字是否為組成標記的字,也就是英文字母、數字或底線的函式如下所示:

int is_alnum(char c) {
  return ('a' <= c && c <= 'z') ||
         ('A' <= c && c <= 'Z') ||
         ('0' <= c && c <= '9') ||
         (c == '_');
}

利用這個函式,在tokenize裡加上底下的程式碼,就可以把return做標記解析為TK_RETURN型了:

if (strncmp(p, "return", 6) == 0 && !is_alnum(p[6])) {
  tokens[i].ty = TK_RETURN;
  tokens[i].str = p;
  i++;
  p += 6;
  continue;
}

接著對分析器動手,讓其可以分析包含有TK_RETURN型的標記列。為此,我們先加上代表return的結點型態ND_RETURN。然後,修改讀取陳述的函式,讓其可以分析return句的文法。如往例,只要把文法直接對應到函式上,就可以成功分析文法了。新的stmt函式如下所示:

Node *stmt() {
  Node *node;

  if (consume(TK_RETURN)) {
    node = calloc(1, sizeof(Node));
    node->kind = ND_RETURN;
    node->lhs = expr();
  } else {
    node = expr();
  }

  if (!consume(';'))
    error_at(tokens[pos].str, "不是';'標記");
  return node;
}

ND_RETURN型的結點只有在這裡可以產生,所以在此我們不寫新的函式,而是直接在該處進行malloc並設定其值。

最後修改指令產生器,讓其可以輸出對應ND_RETURN結點的合適指令。新的gen函式的一部份如下所示:

void gen(Node *node) {
  if (node->kind == ND_RETURN) {
    gen(node->lhs);
    printf("  pop rax\n");
    printf("  mov rsp, rbp\n");
    printf("  pop rbp\n");
    printf("  ret\n");
    return;
  }
  ...

上述程式碼的gen(node->lhs)函式呼叫中,會輸出將作為return回傳值式子的指令。該段指令應該會在堆疊頂部留下1個值。gen(node->lhs)後接著的指令,會把該值從堆疊中彈出並放在 RAX,然後從函式中回傳。

到前一章為止所實作的功能,在函式的最後一定會輸出1個ret指令。照本章所說明的方法實作return後,就會在return句以外還會輸出多的ret指令。也可以這些指令統整起來,但此處為了實作簡單,我們允許其輸出複數的ret指令。在現在這個時間點,太在意這些瑣碎的細節也沒用,重要的是以實作簡單為優先。雖然能寫難的程式是很有用的能力,但有時讓程式不要變得太難,是更有用的能力。

Previous第10步:複數文字的區域變數Next1973年的C編譯器

Last updated 5 years ago

Was this helpful?