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. 函式與區域變數
  2. 第9步:1個字的區域變數

修改分析器

遞迴下降分析法中,只要理解文法,就可以機械性地對應到函式呼叫。因此,考慮到分析器要做的修改,必須思考加上變數名(識別符號)的新文法會長什麼樣。

識別符號我們以ident代表,和num一樣是終端符號。變數在所有能用數值的地方都可以用,所以我們在原本是num的地方改成num | ident,文法中使用數值的地方就也可以使用變數了。

同時,文法還得補上代入式。變數得要可以代入才算是變數,我們想要文法能接受像a=1這樣的式子。在此配合C的語法,來讓文法可以接受像a=b=1這樣的寫法吧。

還有,我們希望可以用分號來區分不同敘述(statement),最終文法會變成以下這樣:

program    = stmt*
stmt       = expr ";"
expr       = assign
assign     = equality ("=" assign)?
equality   = relational ("==" relational | "!=" relational)*
relational = add ("<" add | "<=" add | ">" add | ">=" add)*
add        = mul ("+" mul | "-" mul)*
mul        = unary ("*" unary | "/" unary)*
unary      = ("+" | "-")? primary
primary    = num | ident | "(" expr ")"

首先請先確認看看像42;或a=b=2; a+b這樣的程式是否合乎文法。然後,請修改我們到目前做好的分析器,讓其能分析上述的文法吧。在現階段,像a+1=5這樣的式子也可以普通地被分析出來,這是正常運作的結果。要排除像那樣不合法的式子,我們等下一階段再來做。改造分析器,並沒有特別難解之處,只要照至今為止的作法讓文法的成員原原本本對應到函式呼叫應該就可以完成。

因為要加上以分號來區分複數式子,我們得把分析的結果存成複數的結點才行。在這個階段,請準備如下的全域陣列,在其中照分析出來的順序把結點存入。在最後一個結點填上NULL,就可以知道尾端在哪裡。新增的程式碼的部份如下所示:

Node *code[100];

Node *assign() {
  Node *node = equality();
  if (consume("="))
    node = new_node(ND_ASSIGN, node, assign());
  return node;
}

Node *expr() {
  return assign();
}

Node *stmt() {
  Node *node = expr();
  expect(";");
  return node;
}

void program() {
  int i = 0;
  while (!at_eof())
    code[i++] = stmt();
  code[i] = NULL;
}

抽象語法樹也需要能代表「表示區域變數的結點」。為此,將區域變數這個新的型態作為結點的新成員加入。應該變得像會如下範例所示。此資料結構中,分析器遇到識別符號標記會做出ND_LVAR型的結點回傳。

// 
抽象語法樹結點的種類
typedef enum {
  ND_ADD,  // +
  ND_SUB,  // -
  ND_MUL,  // *
  ND_DIV,  // /
  ND_LVAR, // 區域變數
  ND_NUM,  // 整數
} NodeKind;

typedef struct Node Node;

// 抽象語法樹結點的結構
struct Node {
  NodeKind kind; // 結點的型態
  Node *lhs;     // 左邊
  Node *rhs;     // 右邊
  int val;       // 只在kind為ND_NUM時使用
  int offset;    // 只在kind為ND_LVAR時使用
};

offset這個成員代表離基底指標的偏移量(離基底指標的距離)。現階段,變數a為 RBP-8、變數b為 RBP-16……如此,不同名字的區域變數有固定的位置,偏移量可以在語法分析的階段就決定好。以下為讀進識別符號回傳ND_LVAR型結點的程式碼:

Node *primary() {
  ...

  Token *tok = consume_ident();
  if (tok) {
    Node *node = calloc(1, sizeof(Node));
    node->kind = ND_LVAR;
    node->offset = (tok->str[0] - 'a' + 1) * 8;
    return node;
  }

  ...

小知識:ASCII碼

在ASCII碼中,以0~127的數值對應到不同文字。ASCII碼和文字的對應如下表所示:

0

NUL

SOH

STX

ETX

EOT

ENQ

ACK

BEL

8

BS

HT

NL

VT

NP

CR

SO

SI

16

DLE

DC1

DC2

DC3

DC4

NAK

SYN

ETB

24

CAN

EM

SUB

ESC

FS

GS

RS

US

32

sp

!

"

#

$

%

&

'

40

(

)

*

+

,

-

.

/

48

0

1

2

3

4

5

6

7

56

8

9

:

;

<

=

>

?

64

@

A

B

C

D

E

F

G

72

H

I

J

K

L

M

N

O

80

P

Q

R

S

T

U

V

W

88

X

Y

Z

[

\

]

^

_

96

`

a

b

c

d

e

f

g

104

h

i

j

k

l

m

n

o

112

p

q

r

s

t

u

v

w

120

x

y

z

{

|

}

~

DEL

0~31為控制符號。現在除了NUL和換行符號之外,幾乎沒有使用到這類控制符號的機會,這些控制符號成了佔據編碼表的最佳位置的無用存在,但是ASCII碼制定是在1963年,在當時,實際上是非常常用到這類控制符號的。在制定的時候,還有過比起放進小寫字母應該放進更多控制符號的提案(註)。

在C語言中,文字只是小的整數型態的值,把對應文字的編碼用數值的方式寫也不改變其意義。也就是說,在 ASCII 下,'a'和97、'0'和48等價。上述程式中有以數值的意義用文字減去a的式子,這樣可以算出給定的文字和 a 距離有多遠。這是因為在 ASCII 編碼中字母是連續排列才能使用的技巧。

There was some debate at the time whether there should be more control characters rather than the lowercase alphabet.

Previous修改標記解析器Next左邊值與右邊值

Last updated 5 years ago

Was this helpful?

48~58是數字、65~90為大寫字母、97~122則是對應到小寫字母。可以發現,這些是以連續的方式對應到編碼,也就是說,0123456789或abcdefg...在編碼上是連續的。讀者可能會覺得定義上連續的文字像這樣以連續方式配置是理所當然的,但是當時主流的 編碼受到打孔機卡的影響,字母在編碼上是不連續的。

EBCDIC
https://en.wikipedia.org/wiki/ASCII#cite_note-Mackenzie_1980-1