為了更深入的了解程式設計的底層,就想研究下編譯。
找了幾本編譯原理看,什麼正規表示式、自動機把我都搞糊塗了。
把複雜的問題弄得更複雜,這不是我希望的。
還是自己做個東西模擬下編譯過程。
任務:讀入原始碼,輸出彙編。
為什麼不直接生成機器碼?彙編可以直接看出編譯是否錯誤。
先設定乙個語言模型:
只有運算子、識別符號、數字、分號四種語法元素。
識別符號都定為全域性變數。
運算子只有= + - * /代表賦值、加、減、乘、整除五種運算。
數字僅僅是32位整數。
分號表示一條語句結束。
如:aa02=15/kl+kh*kl/51;
問題一:建立乙個符號表,按順序寫入必要的語法元素,
可以查詢或修改符號的型別等資訊。
把符號表做成乙個指標陣列,指向什麼呢?
pp=^tx
tx=record
next:pp //指向下個符號
lx:integer;//型別
wz:integer;//在該型別中的位置
end;
fhb:array of pp;//符號表
js:integer; //符號表記數
原來是指向乙個記錄,它可以構建鍊錶(為什麼用鍊錶?可以方便去掉
已經處理的符號),乙個符號乙個pp。
lx(型別)預定義四種:
1:運算子 2:識別符號 3:數值 4:暫存器(儲存中間結果)
運算子的wz(位置):
1:= 3:+ 4:- 6:* 7:/
暫存器的位置:與sjcq對應
sjcq:array[0..5]of string=('eax','ebx','ecx','edi','esi','edx');
edx怎麼放到後面去了?除法要用這個暫存器啊。
是不是還要記錄暫存器被哪個符號使用?
bjcq:array [0..5]of pp; //對應的暫存器使用者
至於識別符號和數值我把他放到bsf中,wz就是它們在bsf中的index。
bsf:tstringlist;
(若是fhb:array of tx 一次性申請記憶體,可以免去頻繁的new(pp),原始碼裡忘了釋放了)
好象還缺點什麼,讀入原始碼是為了輸出**服務的,編譯是一條一條語句
編譯的,所以還要記錄一條語句是從符號表**開始**結束。
yj=record //語句
qd,zd:integer;//起點,終點
end;
yjz:array[0..100]of yj; //語句組
yjzjs:integer; //語句組記數
這樣讀取原始碼建立符號表、語句組應該沒有難度了,詳見procedure dq;
問題二、判斷運算優先順序
現在嘗試處理一條語句,也就是fhb[qd]到fhb[zd]的一段鍊錶。
我們要做的就是遍歷這段鍊錶,找出最先運算的運算子,
實際上運算子的位置就是優先順序;
varxp,xq,last,qm,hm:pp;
x,y,z1,z2:integer;
xp:=fhb[qd]; x:=-1;xq:=nil;
while xq<>fhb[zd].next do //不能超過一條語句終點
begin
if xp^.lx=1 then //如果是運算子
begin
if xp^.wz<=x+1 then break; //判斷運算優先
qm:=last; //運算子前面乙個符號
hm:=xp^.next; //運算子後面乙個符號
x:=xp^.wz; //x是具體哪個運算子
end;//endif
last:=xp;
xq:=xp^.next;
xp:=xq;
end;//endwhile
x+1是為了同級別運算按順序來;
應該看明白了吧,測試下 aa=55-hk*jj/51;
x=6(乘法),qm(前面)指向hk,hm(後面)指向jj,
這樣二元運算的三個關鍵符號都到手了。
問題三、前後都是數值編譯器應該先計算
這個簡單,看原始碼。
問題四、處理qm(前面的符號)
先看一些彙編:
sub eax,10
add eax,[s]
mul eax,ecx,7
idiv ecx
如果你對彙編有點了解的話就該知道,qm必須轉成暫存器型別。
如果qm^.lx=4(暫存器)那不用處理.
如果qm^.lx=2(識別符號)就要轉成暫存器型別.
qm^.wz:=newjcq(qm);//申請暫存器
qm^.lx:=4; //改為暫存器型別
當然x=1(賦值運算)是不用轉的
如果qm^.lx=3(數值)最麻煩,要分兩種情況:
如果x是乘,加運算,qm和hm的內容要交換;(為了優化**)
如果x是除,減運算,qm要轉成暫存器型別;(必須這樣啦)
問題五、乘法的處理
要按hm的型別分三種情況。
hm是暫存器: 生成類似** imul ebx
hm是數值: 生成類似** imul eax,ecx,5
可以優化成 lea eax,[ecx+ecx*4](麻煩)
hm是識別符號:這也分兩種情況
qm是eax, 生成類似** imul dword ptr [b]
qm不是eax,生成類似** imul ebx,[b]
問題六、除法最麻煩(俺還是在用整除呢)
除法要求,被除數在eax中,edx不能用。(所以在sjcq中俺把edx弄到最後)
如果qm不是eax怎麼辦?交換。
假如qm是ecx,生成類似** xchg eax,ecx
eax和qm的暫存器相關也要交換,不然會出錯的。
//原eax的與qm 交換
tp:=bjcq[0]; //取得原來使用eax的符號
bjcq[qm^.wz]:=tp;
tp^.wz:=qm^.wz;
qm^.wz:=0;
bjcq[0]:=qm;
現在可以生成**了。
首先 cdq (用xor edx,edx也可以,但是指令多乙個位元組)
hm是數值: 首先要把hm轉成暫存器型別
hm是暫存器: 生成類似** idiv ebx
hm是識別符號: 生成類似** idiv dword ptr [b]
問題七、賦值運算
hm是數值: 生成類似** mov [a],18
hm是暫存器: 生成類似** mov [a],eax
hm是識別符號: 生成類似** mov eax,[b]
mov [a],eax
問題八、收尾工作
if hm^.lx=4 then bjcq[hm^.wz]:=nil ; // 不要的暫存器要歸還
if qm=fhb[qd] then exit; //一條語句弄完了
qm^.next:=hm^.next; //把已經運算的從鍊錶中刪除
yx(qd,zd); //再來
用鍊錶刪除資料就是簡單
測試:與delphi生成的**對比
設 a:=8;
b:=4;
d:=10;
編譯a:=b*5-d div 5;
當然在我的編譯器中寫做a=b*5-d/5;
delphi**:
mov eax,[d]
mov ecx,$00000005
cdqidiv ecx
mov edx,[b]
lea edx,[edx+edx*4]
sub edx,eax
mov [a],edx
我生成的**:
mov eax ,[b]
imul eax,eax,5
mov ebx ,[d]
xchg eax,ebx
mov ecx ,5
cdqidiv ecx
sub ebx,eax
mov [a],ebx
經過1千萬次迴圈delphi的用時250毫秒,我的200毫秒。
但是delphi的**佔30位元組,我的佔35位元組
如果你在delphi中用for迴圈測試要小心,
for迴圈的迴圈變數會占用 ebx。
怎麼處理,改下sjcq中成員的次序。
最後的問題:擴充套件
括號是最容易擴充套件的,你可以把括號當成語句。
比較運算最後要test或cmp。
……
自己動手寫編譯器 鏈結器致謝
自己動手寫編譯器 鏈結器 本書投稿後,有幸請csdn暨 程式設計師 雜誌總編 劉江老師閱讀了本書的初稿,並為本書作序,在此向劉老師表示最衷心的感謝。本書臨近出版之際,承蒙清華大學王生原老師閱讀了本書終稿,並對書稿做了中肯評價 本書特色鮮明,內容有深度,文筆也很不錯,很值得出版。本書最大的特色是所選的...
自己動手寫編譯器 鏈結器致謝
本書投稿後,有幸請csdn暨 程式設計師 雜誌總編 劉江老師閱讀了本書的初稿,並為本書作序,在此向劉老師表示最衷心的感謝。本書臨近出版之際,承蒙清華大學王生原老師閱讀了本書終稿,並對書稿做了中肯評價 本書特色鮮明,內容有深度,文筆也很不錯,很值得出版。本書最大的特色是所選的目標平台,即x86處理器以...
手工打造編譯器之語義分析1
語義分析階段的任務是 將變數的定義與它們的各個使用聯絡起來,檢查每乙個表示式是否有正確的型別,並將抽象語法變為更簡單的更適合生成機器 的表示。符號表是繫結的集合。這些繫結是識別符號與其含義的一種對映關係。因為作用域的改變,符號表也隨之改變。如下面的c語言 struct m 上面 的符號表可能為 t1...