碎碎念
為什么《編譯原理》這本書叫做?龍書(Dragon book)?
這本書很有意思,它的書名是?《Compilers: Principles, Techniques, and Tools》,也就是編譯器的原則、技術和工具。但它卻畫出了一個恐龍和騎士,恐龍身上寫的是?Complexity of Compiler Design,也就是復雜的編譯器設計,騎士的盾上寫的是?Syntax Directed Translation,也就是語法翻譯。騎士的劍上看的不是很清楚,我猜測應該是優秀的編譯器的意思。這是征服復雜性的隱喻。優秀的編譯器會直接征服復雜的編譯,復雜的編譯設計永遠無法攻破語法翻譯。
什么是編譯原理
計算機是只認識二進制的,但是我們平常開發中根本不會使用二進制進行開發,我們使用的都是 Java、C 這類的高級語言,每種語言都會經過一系列的轉換才能被計算機識別,那么到底是誰做的這項工作呢?一個被稱為?編譯器(compiler)?的大佬出場了。
語言處理器
首先考慮一下一個例子,你如何才能和老外對話?你是不是需要學英語?我們有一些同學可能認為英語難學,經常會在英語書上做一些漢語標記方便理解。
?
那么,誰做了由英語到方便記憶?的英語之間的轉換呢?答案是你的大腦。所以,我們可以歸納一下這個過程。
因為我們懂漢語(自己的一套語法規則),我們把英語(需要學習的語言)轉換為我們便于理解的漢語(大腦翻譯規則),我們才能學會英語和老外對話(轉換為目標語言)。
回到正題,我們上面舉出的這個學英語的例子,其實就是一個由原程序經過某種機制轉換,把它變成目標語言的過程。也就是
編譯器就是一個翻譯官的角色,它負責把源程序的語法翻譯成目標程序能夠理解的語法。
回到計算機中,我們肯定需要目標程序來做一些事情的。
也就是,我們通過某個渠道獲得的輸入信息,會經過編譯器的轉換,變為輸出信息進行展示。
除了編譯器之外,還有一種稱為?解釋器(interpreter)?的語言處理器,它不是做翻譯工作的,而是把用戶提供的輸入執行源程序中指定的操作。
我們熟知的 Java 語言,就結合了編譯和解釋的過程,我們寫的 Java 源文件首先被編譯成?字節碼(bytecode),字節碼是一種中間碼,它通常被看成是可執行的二進制文件。然后再由 Java 虛擬機對字節碼解釋執行。這樣,在一臺機器上編譯的字節碼就能夠在其他機器上解釋執行,這種體現了 Java 語言的平臺無關性。
為了提高編譯速度,Java 中有一種?just-in-time,JIT?即時編譯器會一邊編譯一邊執行。
一個源文件程序可能被劃分為多個模塊,并存放在多個文件中,還需要把文件鏈接在一起,所以,除了編譯器之外,還需要一種能鏈接文件的部件參與,預處理器(preprossor)?是做這件事情的。如下圖所示
預處理器經過預處理后會作為輸入傳遞給編譯器,編譯器對源程序進行編譯,編譯完成后生成匯編代碼,作為匯編器的輸入傳遞給匯編器,匯編器進行匯編處理轉換為機器代碼,注意這個時候還不是目標代碼,還要經過鏈接器與系統庫函數進行鏈接,最后由加載器把目標代碼加載到內存中執行
編譯器的結構
我們上面大概了解了一下語言的處理過程,下面我們就來了解一下編譯器的內部結構,編譯器內部其實具有兩種結構:分析(analysis)部分和?整合(synthesis)?部分。
分析過程相當于是把源程序分成多個結構,每個結構都有特定的語法格式進行校驗,在經由每個校驗后,如果不滿足指定的語法格式則進行提醒,使用戶進行修改。分析部分還會收集有關源程序的信息,會把收集到的信息存放在一個被稱為?符號表(symbol table)?的數據結構中。符號表和中間表示形式一起傳給整合部分。
整合過程是根據分析過程傳遞的信息來構造用戶期待的目標程序。分析和整合統稱為?前端(front end)?和?后端(back end)?,哈哈哈哈。
這里你需要知道符號表(Symbol Table)?的概念:符號表是編譯器使用和維護的數據結構,由標識符和類型組成。符號表的主要作用是幫助編譯器快速定位。
下面是一個編譯器的典型結構
下面我們就針對編譯器結構的每一層進行描述和討論
詞法分析
詞法分析(Lexical Analyzer)是編譯器的第一個步驟,它也被稱為?掃描(scanning)。詞法分析器通過讀入外部的字符流對其進行掃描,并且把它們組成有意義的詞素(lexeme)序列,對于每個詞素,詞法分析器都會產生詞法單元(token)?作為輸出。這個詞法單元會傳遞給下一個步驟,也就是語法分析。
這里需要解釋一下 Token 、詞素和詞法分析器的概念
我們常用的編程語言就是具有詞素的單詞和符號的集合,比如 C 語言中有 (),-> 等等。關鍵字 if...while...,變量或函數名稱以及數字和字符串常量也被視為詞素。并不是所有的自負都屬于詞素,例如空格和注釋就不屬于。
詞法分析器用來分析詞素有兩個規則
跳過不能以字母開頭的字符
然后找到剩余的最長前綴,也就是詞素
這兩句話比較抽象,舉個例子來說明一下
比如 C 語言中有這么一個語句
?
ifx?=?20*30;
?
那么第一個詞素就是 ifx,為什么不是 if 呢?因為 if 不是最長的前綴。然后后面的詞素依次是 =,20,*,30和;。
詞素、詞法分析器、token 的關系如下
詞素是 Token 的實例,詞法分析器的主要任務就是從源程序中讀取字符并產生 token。token 也是有結構的,一般結構如下
在詞法分析生成的?token?中,第一個詞 token-name 是語法分析期間使用的抽象符號,第二個詞 attribute-value 指向的是符號表中關于這個詞法單元的條目數。
我們舉個例子來看一下詞法分析的拆解過程。
比如現在源程序中有一個賦值語句
?
income?=?mainjob?+?sideline?//?收入?=?主業?+?副業
?
這個賦值語句中的字符可以組合成如下詞素,并轉換成為 token,并傳遞給語法分析階段。
首先,income 是一個詞素,它會被映射為
然后是賦值符號 = ,它也是一個詞素,被映射稱為 token 中的 < = >。這個 token 不需要屬性值,所以沒有第二個詞。
mainjob 是一個詞素,它被映射成為 token 中的
+也是一個詞素,它被映射稱為 < + >,沒有條目數
sideline 是一個詞素,它被映射稱為 token 中的
所以,經過詞法分析后,上面的源程序會變為
?
?=?>? ?+?>?
?
在上面的表達式中, = 和 + 分別表示賦值和加法運算符的抽象符號。用圖來表示的話就是
語法分析
編譯器的第二個步驟是?語法分析(syntax analysis)?或者稱為?解析(parsing)。語法分析器使用由詞法分析器生成的各個詞法單元的第一個分量來創建樹形的中間表示。常用的方法就是?語法樹(syntax tree)。編譯器的后續步驟都會使用這個語法結構來幫助分析源程序,并生成目標程序。
語義分析
語義分析是由?語義分析器(semantic analyzer)?完成的,它使用語法樹和符號表中的信息來檢查源程序是否和語言定義的語義一致。語義分析器也收集類型信息,并把這些信息放在語法樹或者符號表中,以便后續的中間代碼生成器使用。
語義分析會進行類型檢查(type checking),這是語義分析器的一個最重要的功能。編譯器會檢查每個運算符是否具有匹配的運算分量。舉個例子比如設計語言要求一個數組的下標是整數,如果你用浮點數作為下標,編譯器就會出錯。
某些程序設計語言比如 Java 會允許自動類型轉換(coercion)。如果整數和浮點數進行運算,編譯器會把整數轉換為浮點數。
中間代碼生成
在源程序的語法分析和語義分析完成后,很多編譯器生成一個明確的低級類機器語言的中間表示。我們可以把中間表示形式看作是抽象,中間形式的代碼應該具有兩個重要的性質:易于生成,并且能夠輕松的被翻譯。一般常用的一種是?三地址指令(three-address instructions)的中間表示形式。我們后面會細說。
代碼優化
代碼優化會試圖改進代碼以便生成更好的目標代碼。更好通常情況下意味著更快,但是也可能會有其他目標,比如更短或能耗更低的目標代碼。
代碼生成
代碼生成通過中間代碼作為輸入,并把它映射為目標語言。如果目標語言是機器代碼的話,那么必須要為每個變量分配寄存器或內存位置。解釋一下上面的運行結果。
每個指令的第一個運算分量指定了一個目標地址,各個指令中的 F 告訴我們它處理的是?浮點數, 上面代碼首先把 id3 裝載進 R2 寄存器中,然后把 id2 裝載進 R1 寄存器中,再對 R1 目標進行 R1 和 R2 寄存器相加的操作。最后把寄存器 R1 的值存放到 id1 的地址中。
符號表管理
我們上面提到了符號表的概念,它是一個編譯器很重要的功能。符號表能夠記錄源程序中使用變量的名稱,并收集和每個名稱相關的屬性信息。它相當于一個秘書的作用。符號表還記錄了每個變量名字的條目。后面我們會詳細的介紹符號表。
編譯器構造工具
和軟件開發一樣,寫編譯器的人可以充分利用現代的軟件開發環境進行開發。通常也有?語言編輯器、調試工具、版本管理、測試工具等。除此之外,還需要一些更專業的工具來實現編輯器不同階段的代碼生成。
一些常用的編譯器構造工具有
語法分析器生成器:可以根據程序設計語言的語法描述自動生成語法分析器
掃描器生成器:可以根據一個語言的語法單元的正則描述生成詞法分析器
語法制導的翻譯引擎:用于生成一組遍歷分析樹并生成中間代碼
代碼生成器:用于把中間代碼轉換為目標代碼
數據流分析引擎:用于分析輸入是如何傳遞到另一部分的
編譯器構造工具:提供用于構造編譯器不同階段的例程
簡要聊一聊程序設計語言的發展歷程
計算機從 20 世紀 40 年代創建至今都只能理解二進制語言,亙古不變。這個 0 、 1 組成的序列能夠告訴計算機以什么樣的順序執行怎樣的運算。運算本身是很底層的:比如把一個數據從一個位置進行移動;把兩個寄存器的內容進行相加、比較兩個值,為了避免如此枯燥的運算,我們開發了各種各樣的編程語言,但是計算機底層的計算方式一直沒變,所以學習哪個技術性價比高,明白了嗎?下面我們就來一起認識一下程序設計語言的歷程。
高級設計語言
首先被開發出來的是 20 世紀 50 年代的匯編語言,5 年后發生了重要的進步,用于科學計算的?Fortran?被開發出來,用于商業處理的?Cobol?語言和用于符號計算的?Lisp?語言被開發出來;然后接下來的時間,慢慢很多編程語言被開發出來,比如 C、C++、Java、JavaScript、Python 等。后面還有用于數據處理的 SQL 語言。
語言分類
說到給這些編程語言分類,那可是有太多了,不過我們專注一下高頻的分類。
如何完成計算任務的語言稱為?強制式(imperative)語言,而把程序中指明要進行哪些計算的語言稱為?聲明式(declarative)語言。C、C++、Java 這些都是強制式語言,它們能夠改變程序的狀態;聲明式比如 HTML Prolog 等。
馮·諾伊曼?語言指的是以馮·諾伊曼計算機體系為基礎的編程語言,今天很多編程語言都是馮·諾伊曼語言。
面向對象語言(object-oriented language)?是一種描述對象的語言,比如 C、C++、Java。
腳本語言(scripting language)?是具有高層次的解釋型語言,它通常把多個過程粘在一起,比如 JavaScript、Perl、PHP、Python 等。
后 記
平時我們大多工作于應用層面,底層的一些概念和流程容易遺忘。
每隔一段時間適時回味一下,感覺還是不錯的。
審核編輯:湯梓紅
評論
查看更多