Compiler Theory(編譯原理)、詞法/語法/AST/中間代碼優化在Webshell檢測上的應用


catalog

0. 引論
1. 構建一個編譯器的相關科學
3. 程序設計語言基礎
4. 一個簡單的語法制導翻譯器
5. 簡單表達式的翻譯器(源代碼示例)
6. 詞法分析
7. 生成中間代碼
8. 詞法分析器的實現
9. 詞法分析器生成工具Lex
10. PHP Lex(Lexical Analyzer)
11. 語法分析
12. 構造可配置詞法語法分析器生成器
13. 基於PHP Lexer重寫一份輕量級詞法分析器
14. 在Opcode層面進行語法還原WEBSHELL檢測 

 

0. 引論

在所有計算機上運行的所有軟件都是用某種程序設計語言編寫的,但是在一個程序可以運行之前,它首先需要被翻譯成一種能夠被計算機執行的形式,完成這項翻譯工作的軟件系統稱為編譯器(compiler)

0x1: 語言處理器

1. 編譯器
簡單地說,一個編譯器就是一個程序,它可以閱讀以某一種語言(源語言)編寫的程序,並把該程序翻譯成一個等價、用另一種語言(目標語言)編寫的程序,編譯器的重要任務之一是報告它在翻譯過程中發現的原程序中的錯誤
2. 解釋器
解釋器(interpreter)是另一種常見的語言處理器,它並不通過翻譯的方式生成目標程序,解釋器直接利用用戶提供的輸入執行源程序中指定的操作
//在把用於輸入映射成為輸出的過程中,由一個編譯器產生的機器語言目標程序通常比一個解釋器要快很多,然而,解釋器的錯誤診斷效果通常比編譯器更好,因為它逐個語句地執行源程序

java語言處理器結合了編譯和解釋過程,一個java源程序首先被編譯成一個稱為字節碼(bytecode)的中間表示形式,然后由一個虛擬機對得到的字節碼加以解釋執行,這樣設計的好處之一是在一台機器上編譯得到的字節碼可以在另一台機器上解釋執行,通過網絡就可以完成機器之間的遷移
為了更快地完成輸入到輸出的處理,有些被稱為即時(just in time)編譯器的java編譯器在運行中間程序處理輸入的前一刻先把字節碼翻譯成為機器語言,然后再執行程序

0x2: 一個編譯器的結構

編譯器能夠把源程序映射為在語義上等價的目標程序,這個映射過程由兩個部分組成: 分析部分、綜合部分

1. 分析部分(編譯器的前端 front end)
    1) 分析(analysis)部分把源程序分解成多個組成要素,並在這些要素之上加上語法結構
    2) 然后,它使用這個結構來創建該源程序的一個中間表示,如果分析部分檢查出源程序沒有按照正確的語法構成,或者語義上不一致,它就必須提供有用的信息,使得用戶可以按此建議進行改正
    3) 分析部分還會收集有關源程序的信息,並把信息存放在一個稱為符號表(symbol table)的數據結構中(調用其他obj中的函數就需要用到符號表),符號表和中間表示形式一起傳送給綜合部分

2. 綜合部分(編譯器的后端 back end)
    1) 綜合(synthesis)部分根據中間表示和符號表中的信息來構造用戶期待的目標程序

如果我們更加詳細地研究編譯過程,會發現它順序執行了一組步驟(phase),每個步驟把源程序的一種表現形式轉換為另一種表現形式,在實踐中,多個步驟可能被組合在一起,而這些組合在一起的步驟之間的中間表示不需要被明確地構造出來,存放整個源程序的信息的符號表可由編譯器的各個步驟使用
有些編譯器在前端和后端之間有一個與機器無關的優化步驟,這個優化步驟的目的是在中間表示之上進行轉換,以便后端程序能夠生成更好的目標程序

1. 詞法分析

編譯器的第一個步驟稱為詞法分析(lexical analysis)或掃描(scanning),詞法分析器讀入組成源程序的字符流,並且將它們組織成為有意義的詞素(lexeme)的序列,對於每個詞素,詞法分析器產生如下形式的詞法單元(token)作為輸出

<token-name、attribute-value>
1. token-name: 由語法分析步驟使用的抽象符號
2. attribute-value: 指向符號表中關於這個詞法單元的條目
//符號表條目的信息會被語義分析和代碼生成步驟使用

這個詞法單元被傳送給下一個步驟,即語法分析,分割詞素的空格會被詞法分析器忽略掉

可以看到,在靜態分析、編譯原理應用領域,代碼優化器這一步可以推廣到WEBSHELL惡意代碼檢測技術上,利用這一步得到的"歸一化"代碼,可以進行純詞法層面的"惡意特征字符串模式匹配"

2. 語法分析

編譯器的第2個步驟稱為語法分析(syntax analysis)或解析(parsing)。語法分析器使用由詞法分析器生成的各個詞法單元的第一個分量來創建樹形的中間表示,該中間表示給出了詞法分析產生的詞法單元流的語法結構,一個常用的表示方法是語法樹(syntax tree),樹中的每個內部結點表示一個運算,而該結點的子節點表示該預算的分量(左右參數)
編譯器的后續步驟使用這個語法結構來幫助分析源程序,並生成目標程序

3. 語義分析

語義分析器(semantic analyzer)使用語法樹和符號表中的信息來檢查源程序是否和語言定義的語義一致,它同時也收集類型信息,並把這些信息存放在語法樹或符號表中,以便在隨后的中間代碼生成過程中使用
語義分析的一個重要部分是類型檢查(type checking),編譯器檢查每個運算符是否具有匹配的運算分量
程序設計語言可能允許某些類型轉換,即自動類型轉換(coercion)

4. 中間代碼生成

在把一個源程序翻譯成目標代碼的過程中,一個編譯器可能構造出一個或多個中間表示,這些中間表示可以有多種形式(語法樹就是一種中間表示,它們通常在語法分析和語義分析中使用)
在源程序的語法分析和語義分析完成之后,編譯器會生成一個明確的低級或類機器語言的中間表示,我們可以把這個表示看作是某種抽象機器的程序,該中間表示應該具有兩個重要的性質

1. 易於生成
2. 能夠輕松地翻譯為目標機器上的語言

5. 代碼優化

機器無關的代碼優化步驟試圖改進中間代碼,以便生成更好的目標代碼,不同的編譯器所做的代碼優化工作量相差很大,那些優化工作做得最多的編譯器,即所謂的"優化編譯器",會在優化階段花相當多的時間

6. 代碼生成

代碼生成器以源程序的中間表示形式作為輸入,並把它映射到目標語言,如果目標語言是機器代碼,那么就必須為程序使用的每個變量選擇寄存器或內存地址,然后中間指令被翻譯成能夠完成相同任務的機器指令序列。代碼生成的一個至關重要的方面是合理分配寄存器以存放變量的值
需要明白的是,運行時刻的存儲組織方式依賴於被編譯的語言,編譯器在中間代碼生成或代碼生成階段做出有關存儲的分配的決定

7. 符號表管理

編譯器的重要功能之一是記錄源程序中使用的變量的名字,並收集和每個名字的各種屬性有關的信息,這些屬性可以提供一個名字的存儲分配、類型、作用域等信息,對於過程,這些信息還包括

1. 名字的存儲分配
2. 類型
3. 作用域(在程序的哪些地方可以使用這個名字的值)
4. 參數數量
5. 參數類型
6. 每個參數的傳遞方式(值傳遞或引用傳遞)
7. 返回類型

符號表數據結構為每個變量名創建了一個記錄條目,記錄的各個字段就是名字的各個屬性,這個數據結構允許編譯器迅速查找到每個名字的記錄,並向記錄中快速存放和獲取記錄中的數據

8. 編譯器構造工具

一個常用的編譯器構造工具包括

1. 掃描器的生成器: 可以根據一個語言的語法單元的正則表達式描述生成詞法分析器
2. 語法分析器的生成器: 可以根據一個程序設計語言的語法描述自動生成語法分析器
3. 語法制導的翻譯引擎: 可以生成一組用於遍歷分析樹並生成中間代碼的例程
4. 代碼生成器的生成器: 依據一組關於如何把中間語言的每個運算翻譯成目標機器上的機器語言的規則,生成一個代碼生成器
5. 數據流分析引擎: 可以幫助收集數據流信息,即程序中的值如何從程序的一個部分傳遞到另一部分,數據流分析是代碼優化的一個重要部分
6. 編譯器構造工具集: 提供了可用於構造編譯器的不同階段的例程的完整集合

 

1. 構建一個編譯器的相關科學

編譯器的設計中有很多通過數學方法抽象出問題本質從而解決現實世界復雜問題的例子,這些例子可以被用來說明如何使用抽象方法來解決問題: 接受一個問題,寫出抓住了問題的關鍵特性的數學抽象表示,並用數學技術來解決它,問題的表達必須根植於對計算機程序特性的深入理解,而解決方法必須使用經驗來驗證和精化

0x1: 編譯器設計和實現中的建模

對編譯器的研究主要是有關如何設計正確的數學模型和選擇正確算法的研究,設計和選擇時,還需要考慮到對通用性及功能的要求與簡單性及有效性之間的平衡

1. 最基本的數學模型是有窮狀態自動機和正則表達式,這些模型可以用於描述程序的詞法單位(關鍵字、標識符等)以及描述被編譯器用來識別這些單位的算法
2. 上下文無關文法,它用於描述程序設計語言的語法結構,例如嵌套的括號和控制結構
3. 樹形結構是表示程序結構以及程序到目標代碼的翻譯方法的重要模型

0x2: 代碼優化的科學

現在,編譯器所作的代碼優化變得更加重要,而且更加復雜,因為處理器體系結構變得更加復雜,也有了更多改進代碼執行方式的機會,之所以變得更加重要,是因為巨型並發計算機要求實質性的優化,否則它們的性能將會呈數量級下降,隨着多核計算機的發展,所有的編譯器將面臨充分利用多核計算機的優勢的問題
即使有可能通過隨意的方法來建造一個健壯的編譯器,實現起來也是非常困難,因為,研究人員已經圍繞代碼優化建立了一套廣泛且有用的理論,應用嚴格的數學基礎,使得我們可以證明一個優化是正確的,並且它對所有可能的輸入都產生預期的效果
需要明白的是,如果想使得編譯器產生經過良好優化的代碼,圖、矩陣、線性規划之類的模型是必不可少的,編譯器優化必須滿足下面的設計目標

1. 優化必須是正確的,也就是說,不能改變被編譯程序的原始含義
2. 優化必須能夠改善很多程序的性能
3. 優化所需的時間必須保持在合理的范圍內
4. 所需要的工程方面的工作必須是可管理的

0x3: 針對計算機體系結構的優化

計算機體系結構的快速發展也對新編譯技術提出了越來越多的需求,幾乎所有的高性能系統都利用了兩種技術: 並行(parallelism)、內存層次結構(memory hierarchy)

1. 並行性
並行可以出現在多個層次上
    1) 指令層次上: 多個運算可以被同時執行,所有的現代微處理器都采用了指令集的並行性,但這種並行性可以對程序員隱藏起來,硬件動態地檢測指令流之間的依賴關系,並且在可能的時候並行地發出指令,不管硬件是否對指令進行重新排序,編譯器都可以重新安排指令,以使得指令級並行更加有效
    指令級的並行也顯示地出現在指令集匯總,VLIW(Very Long Instruction Word 非常長指令字)機器擁有可並行執行多個運算的指令,Intel IA64是這種體系結構的一個有名的例子
    所有的高性能通用微處理器還包含了可以同時對一個向量中的所有數據進行運算的指令,人們已經開發出相應的編譯器技術,從順序程序除法為這樣的機器自動生成代碼

    2) 處理器層次上: 同一個應用的多個不同線程在不同的處理器上運行
    程序員可以為多處理器編寫多線程的代碼,也可以通過編譯器從傳統的順序程序自動生成並行代碼,編譯器對程序員隱藏了一些細節

2. 內存層次結構
一個內存層次結構由幾層具有不同速度和大小的存儲器組成,離處理器距離越近,速度越快,但存儲空間越小,高效使用寄存器可能是優化一個程序時要處理的最重要的問題,同時高速緩存和物理內存是對指令集集合隱藏的,並由硬件管理

0x4: 程序翻譯

1. 二進制翻譯

編譯器技術可以用於把一個機器的二進制代碼翻譯成另一個機器的二進制代碼,使得可以在一個機器上運行原本為另一個指令集編譯的程序

2. 硬件合成

不僅僅大部分軟件是用高級語言描述的,連大部分硬件設計也是使用高級硬件描述語言描述的,例如Verilog、VHDL(Very High-Speed Intefrated Circuit Hardware Description Language 超高速集成電路硬件描述語言)
硬件設計通常是在寄存器傳輸層(Register Transfer Level RTL)上描述的,在這個層面中,變臉代表寄存器,而表達式代表組合邏輯,硬件合成工具把RTL描述自動翻譯為門電路,而門電路再被翻譯成為晶體管,最后生成一個物理布局

3. 數據查詢解釋器

除了描述軟件和硬件,語言在很多應用中都是有用的,比例,查詢語言(例如SQL語言 Structured Query Language 結構化查詢語言)被用來搜索數據庫,數據庫查詢由包含了關系和布爾運算符的斷言組成,它們可以被解釋,也可以編譯為代碼,以便在一個數據庫中搜索滿足這個斷言的記錄

 

3. 程序設計語言基礎

0x1: 靜態和動態的區別

在為一個語言設計一個編譯器時,我們所面對的最重要的問題之一是編譯器能夠對一個程序做出哪些判定

1. 如果一個語言使用的策略支持編譯器靜態決定某個問題,那么我們說這個語言使用了一個靜態(static)策略,或者說這個問題可以在編譯時刻(compile time)決定
2. 另一方面,一個只允許在運行程序的時候做出決定的策略被稱為動態策略(dynamic policy),或者被認為需要在運行時刻(run time)做出決定

0x2: 環境與狀態

我們在討論程序設計語言時必須了解的另一個重要區別是在程序運行時發生的改變是否會影響數據元素的值,還是僅僅影響了對那個數據的名字的解釋
名字和內存(存儲)位置的關聯,及之后和值的關聯可以用兩個映射來描述,這兩個映射隨着程序的運行而改變

1. 環境(environment)是一個從名字到存儲位置的映射,因為變量就是指內存位置(即C語言中的術語"左值"),我們還可以換一種方法,把環境定義為從名字到變量的映射
//環境的改變需要遵守語言的作用域規則
2. 狀態(state)是一個從內存位置到它們的值的映射,以C語言的術語來說,即狀態把左值映射為它們的相應的右值

0x3: 靜態作用域和塊結構

包括C語言和它的同類語言在內的大多數語言使用靜態作用域,C語言的作用域規則是基於程序結構的,一個聲明的作用域由該聲明在程序中出現的位置隱含地決定
0x4: 顯式訪問控制

類和結構為它們的成員引入了新的作用域,通過public、private、protected這樣的關鍵字的使用,像C++和Java這樣的面向對象語言提供了對超類中的成員名字的顯式訪問控制,這些關鍵字通過限制訪問來支持封裝(encapsulation),因此,私有(private)名字被有意地限定了作用域,這個作用域僅僅包含了該類和"友類"(C++的術語)相關的方法聲明和定義,被保護的(protected)名字可以由子類訪問,而公共的(public)名字可以從類外訪問
在C++中,一個類的定義可能和它的部分或全部方法的定義分離,因此對於一個和類C相關聯的名字,可能存在一個在它作用域之外的代碼區域,然后又跟着一個在它作用域內的代碼區域(一個方法定義),實際上,在這個作用域之內和之外的代碼區域可能相互交替,直到所有的方法都被定義完畢

0x5: 動態作用域

從技術上講,如果一個作用域策略依賴於一個或多個只有在程序執行時刻才能知道的因素,它就是動態的,然而,術語動態作用域通常指的是下面的策略

對一個名字x的使用指向的是最近被調用但還沒有終止且聲明了x的過程中的這個聲明,這種類型的動態作用域僅僅在一些特殊情況下才會出現,例如
1. C預處理起中的宏擴展
2. 面向對象編程中的方法解析

動態作用域解析對多態過程是必不可少的,所謂多態過程是指對於同一個名字根據參數類型具有兩個或多個定義的過程,在這種情況下,編譯器可以把每個過程調用替換為相應的過程代碼的引用

0x6: 參數傳遞機制

所有的程序設計語言都有關於過程的概念,但是在這些過程如何獲取它們的參數方面,不同的語言之間有所不同

1. 值調用

在值調用(call-by-value)中,會對實參求值(如果它是表達式)或拷貝(如果它是變量),這些值被放在屬於被調用過程的相應形式參數的內存位置上(即入棧),值調用的效果是,被調用過程所做的所有有關形式參數的計算都局限於這個過程,對應的實參本身不會被改變
需要注意的是,我們同樣可以傳遞變量的指針,這樣對於過程來說雖然依然是值傳遞,但是從效果上等同於傳遞了對應參數的引用

2. 引用調用

在引用調用(call-by-reference)中,實參的地址作為相應的形式參數的值被傳遞給被調用者,在被調用者的代碼中使用該形參時,實現方法是沿着這個指針找到調用者指明的內存位置,因此,改變形式參數看起來就像是改變餓了實參一樣

3. 名調用

0x7: 別名

引用調用或者其他類似的方法,比如像Java中那樣把對象的引用當值傳遞,會引起一個有趣的結果,即兩個形參指向同一個位置,這樣的變量稱為另一個變量的別名(alias),結果是,任意兩個看起來從兩個不同的形參中獲得值的變量也可能變成對方的別名,這個現象在PHP中也同樣存在
事實上,如果編譯器要優化一個程序,就要理解別名現象以及產生這一現象的機制,必須在確認某些變量相互之間不是別名之后才可以優化程序

 

4. 一個簡單的語法制導翻譯器

0x1: 引言

編譯器在分析(scanning)階段把一個源程序划分成各個組成部分,並生成源程序的內部表示形式,這種內部表示稱為中間代碼,然后,編譯器在合成階段將這個中間代碼翻譯成目標程序
分析階段的工作是圍繞着待編譯語言的的"語法"展開的,一個程序設計語言的語法(syntax)描述了該語言的程序的正確形式,而該語言的語義(semantics)則定義了程序的含義,即每個程序在運行時做什么事情
我們將在接下來討論一個廣泛使用的表示方法來描述語法,即上下文無關文法或BNF(Backus-Naur范式),描述語義的難度遠遠大於描述語言語法的難度,因此,我們將結合非形式化描述和啟發式描述的來描述語言的語義


詞法分析器使得翻譯器可以處理由多個字符組成的構造,比例標識符。標識符由多個字符組成,但是在語法分析階段被當作一個單元進行處理,這樣的單元稱作詞法單元(token)
接下來考慮中間代碼的生成

圖中顯示了兩種中間代碼形式

1. 左: 抽象語法樹(abstract syntax tree): 表示了源程序的層次化語法結構
2. 右: "三地址"指令序列: 三地址指令最多只執行一個運算,通常是計算、比較、分支跳轉

0x2: 語法定義

在本節中,我們將討論一種用於描述程序設計語言語法的表示方法"上下文無關文法",或簡稱"文法",文法將被用於組織編譯器前端
文法自然地描述了大多數程序設計語言構造的層次化語法結構,例如,Java中的if-else語句通常具有如下形式

if (expression) statement else statement
//即一個if-else語句由關鍵字if、左括號、表達式、右括號、語句塊、關鍵字else、語句塊組成,這個構造規則可以表示為
stmt -> if (expr) stmt else stmt
//其中箭頭(->)表示"可以具有如下形式"

這樣的規則稱為產生式(production),在一個產生式中,像關鍵字if和括號這樣的詞法元素稱為終結符號(terminal),像expr和stmt這樣的變量表示終結符號的序列,它們稱為非終結符號(nonterminal)

1. 文法定義

一個上下文無關文法(context-free grammar)由四個元素組成

1. 終結符號集合,也稱為"詞法單元",終結符號是該文法所定義的語言的基本符號的集合
2. 非終結符號集合,也稱為"語法變量",每個非終結符號表示一個終結符號串的集合
3. 產生式集合,其中每個產生式包括
    1) 一個稱為產生式頭或左部的非終結符號
    2) 一個箭頭
    3) 一個稱為產生式體或右部的由終結符號及非終結符號組成的序列,產生式主要用來表示某個構造的某種書寫形式,如果產生式頭非終結符號代表一個構造,那么該產生式體就代表了該構造的一種書寫方式
    4) 指定一個非終結符號為開始符號

在編譯器中,詞法分析器讀入源程序中的字符序列,將它們組織為具有詞法含義的詞素,生成並輸出代表這些詞素的詞法單元序列,詞法單元由兩個部分組成: 名字和屬性值

1. 詞法單元的名字是詞法分析器進行語法分析時使用的抽象符號,我們通常把這些詞法單元名字稱為終結符號,因為它們在描述程序設計語言的文法中是以終結符號的形式出現的
2. 如果詞法單元具有屬性值,那么這個值就是一個指向符號表的指針,符號表中包含了該詞法單元的附加信息,這些附加信息不是文法的組成部分,因此我們在討論語法分析時,通產將詞法單元和終結符號當作同義詞

如果某個非終結符號是某個產生式的頭部,我們就說該產生式是該非終結符號的產生式,一個終結符號串是由零個或多個終結符號組成的序列,零個終結符號組成的串稱為空串(empty string)

2. 推導

根據文法推導符號串時,我們首先從開始符號出發,不斷將某個非終結符號替換為該非終結符號的某個產生式的體,可以從開始符號推導得到的所有終結符號串的集合稱為該文法定義的語言(language)
語法分析(parsing)的任務是: 接受一個終結符號串作為輸入,找出從文法的開始符號推導出這個串的方法,如果不能從文法的開始符號推導得到該終結符號,則報告該終結符號串中包含的語法錯誤
一般情況下,一個源程序中會包含由多個字符組成的詞素,這些詞素由詞法分析器組成詞法單元,而詞法單元的第一個分量就是被語法分析器處理的終結符號

3. 語法分析樹

語法分析樹用圖形方式展現了從文法的開始符號推導出相應語言中的符號串的過程,如果非終結符號A有一個產生式A -> XYZ,那么在語法分析樹中就有可能有一個標號為A的內部結點,該結點有三個子節點,從左向右的標號分別為X、Y、Z

從本質上說,給定一個上下文無關文法,該文法的一棵語法分析樹(parse tree)是具有以下性質的樹

1. 根結點的標號為文法的開始符號
2. 每個葉子結點的標號為一個終結符號或空串
3. 每個內部結點的標號為一個非終結符號
4. 如果非終結符號A是某個內部結點的標號,並且它的子結點的標號從左至右分別為X1、X2、...、Xn,那么必然存在產生式A -> X1 X2 .. Xn,其中X1 X2 .. Xn既可以是終結符號,也可以是非終結符號,作為一個特殊情況,如果A -> 空串是一個產生式,那么一個標號為A的結點可以只有標號為空串的子結點

一棵語法分析樹的葉子結點從左向右構成了樹的結果(yield),也就是從這課語法分析樹的根節點上的非終結符號推導得到(生成)的符號串
一個文法的語言的另一個定義是指任何能夠由某課語法分析樹生成的符號串的集合,為一個給定的終結符號串構建一棵語法分析樹的過程稱為對該符號串進行語法分析

4. 二義性

在根據一個文法討論某個符號串的結構時,我們必須非常小心,一個文法可能有多課語法分析樹能夠生成同一個給定的終結符號串,這樣的文法稱為具有二義性(ambiguous),要證明一個文法具有二義性,我們只需要找到一個終結符號串,說明它是兩棵以上語法分析樹的結果
因為具有兩棵以上語法分析樹的符號串通常具有多個含義,所以我們需要為編譯應用設計出沒有二義性的文法,或者在使用二義性文法時使用附加規則來消除二義性

5. 運算符的結合性

在大多數程序設計語言中,加減乘除4種算術運算符都是左結合的,某些常用運算符是右結合的,例如賦值運算符,對於左結合的文法來說,語法樹向左下端延伸,而右結合的文法語法樹向有下端延伸

6. 運算符的優先級

算術表達式的文法可以根據表示運算符結合性和優先級的表格來建立

expr -> expr + term | expr - term | term
term -> term * factor | term / factor | factor
factor -> digit | (expr)

0x3: 語法制導翻譯

語法制導翻譯是通過向一個文法的產生式附加一些規則或程序片段而得到的

1. 語法制導翻譯相關的概念

1. 屬性(attribute): 屬性表示與某個程序構造相關的任意的量,屬性可以是多種多樣的,比如
    1) 表達式的數據類型
    2) 生成的代碼中的指令數目
    3) 為某個構造生成的代碼中第一條指令的位置
因為我們用文法符號(終結符號、或非終結符號)來表示程序構造,所以我們將屬性的概念從程序構造擴展到表示這些構造的文法符號上

2. (語法制導的)翻譯方案(translation scheme): 翻譯方案是一種將程序片段附加到一個文法的各個產生式上的表示法,當在語法分析過程中使用一個產生式時,相應的程序片段就會執行,這些程序片段的執行效果按照語法分析過程的順序組合起來,得到的結果就是這次分析/綜合過程處理源程序得到的翻譯結果

2. 后綴表示

一個表達式E的后綴表示(postfix notation)可以按照下面的方式進行歸納定義

1. 如果E是一個變量或者常量,則E的后綴表示是E本身
2. 如果E是一個形如"E1 op E2"的表達式,其中op是一個二目運算符,那么E的后綴表示是E1E2op
3. 如果E是一個形如(E1)的被括號括起來的表達式,則E的后綴表示就是E1的后綴表示

例如,9-(5+2)的后綴表達式是952+-,即5+2首先被翻譯成52+,然后這個表達式又成為減號的第二個運算分量
運算符的位置和它的運算分量個數(anty)使得后綴表達式只有一種解碼方式,所以在后綴表示中不需要括號,處理后綴表達式的技巧就是

1. 從左邊開始不斷掃描后綴串,直到發現一個運算符為止
2. 然后向左找出適當數目的運算分量,並將這個運算符和它的運算分量組合在一起
3. 計算出這個運算符作用於這些運算分量上后得到的結果
4. 並用這個結果替換原來的運算分量和運算符,然后繼續這個過程,向右搜尋另一個運算符

3. 綜合屬性

將量和程序構造關聯起來(比如把數值及類型和表達式相關聯)的想法可以基於文法來表示,我們將屬性和文法的非終結符號及終結符號相關聯,然后,我們給文法的各個產生式附加上語義規則。對於語法分析樹中的一個結點,如果它和它的子結點之間的關系符合某個產生式,那么該產生式對應的規則就描述了如何計算這個結點上的屬性

語法制導定義(syntax-direted definition)把每個文法符號和一個屬性集合相關聯,並且把每個產生式和一組語義規則(semantic rule)相關聯,這些規則用於計算與該產生式中符號相關聯的屬性值

屬性可以按照如下方式求值,對於一個給定的輸入串x,構造x的一個語法分析樹,然后按照下面的方法應用語義規則來計算語法分析樹中各個結點的屬性

1. 假設語法分析樹的一個結點N的標號為文法符號X,我們用X.a表示該結點上X的屬性a的值
2. 如果一棵語法分析樹的各個結點上標記了相應的屬性值,那么這課語法分析樹就稱為注釋(annotated)語法分析樹(注釋分析樹)

如果某個屬性在語法分析樹結點N上的值是由N的子結點以及N本身的屬性值確定的,那么這個屬性就稱為綜合屬性(synthesized attribute),綜合屬性有一個很好的性質: 只需要對語法分析樹進行一次自底向上的遍歷,就可以計算出屬性的值
4. 簡單語法制導定義

語法制導定義具有下面的重要性質

要得到代表產生式頭部的非終結符號的翻譯結果的字符串,只需要將產生式體中各非終結符號的翻譯結果按照它們在非終結符號中的出現順序連接起來,並在其中穿插一些附加的串即可,具有這個性質的語法制導定義稱為簡單(simple)語法制導定義

5. 樹的遍歷

樹的遍歷將用於描述屬性的求值過程,以及描述一個翻譯方案中的各個代碼片段的執行過程。一個樹的遍歷(traversal)從根節點開始,並按照某個順序訪問樹的各個結點
一次深度優先(depth-first)遍歷從根節點開始,遞歸地按照任意順序訪問各個結點的子結點,並不一定要按照從左向右的順序遍歷,之所以稱之為深度優先,是因為這種遍歷總是盡可能地訪問一個結點的尚未被訪問的子節點(盡量一次就從一個結點追溯它的葉子),因為它總是盡可能快地訪問離根節點最遠的結點(即最深的結點) 

1. 語法制導定義沒有規定一棵語法分析樹中各個屬性值的求值順序,只要一個順序能夠保證計算屬性a的值時,a所依賴的其他屬性都已經計算完畢,這個順序就是可以接受的
2. 綜合屬性可以在自底向上遍歷的時候計算
3. 自頂向下遍歷指在計算完成某個結點的所有子結點的屬性值之后才開始計算該結點的屬性值的過程
4. 一般來說,當既有綜合屬性又有繼承屬性時,關於求值順序的問題就變得相當復雜

0x4: 語法分析

語法分析是決定如何使用一個文法生成一個終結符號串的過程,我們接下來將討論一種稱為"遞歸下降"的語法分析方法,該方法可以用於語法分析和實現語法制導翻譯器
程序設計語言的語法分析器幾乎總是一次性地從左到右掃描輸入,每次向前看一個終結符號,並在掃描時構造出分析樹的各個部分
大多數語法分析方法都可以歸納為以下兩類

1. 自頂向下(top-down)方法
自頂向下(top-down)構造過程從葉子結點開始,逐步構造出根結點,這種方法很容易地手工構造出高效的語法分析器

2. 自底向上(bottorn-up)方法
自底向上(bottorn-up)分析方法可以處理更多種文法和翻譯方案,所以直接從文法生成語法分析器的軟件工具常常使用自底向上的方法

1. 自頂向下分析方法

2. 預測分析法

遞歸下降分析方法(recursive-descent parsing)是一種自頂向下的語法分析方法,它使用一組遞歸過程來處理輸入,文法的每個非終結符都有一個相關聯的過程,這里我們考慮遞歸下降分析法的一種簡單形式,稱為預測分析法(predictive parsing),在預測分析法中,各個非終結符對應的過程中的控制流可以由"向前看符號"無二義地確定,在分析輸入串時出現的過程調用序列隱式地定義了該輸入串的一棵語法分析樹,如果需要,還可以通過這些過程調用來構建一個顯式的語法分析樹

3. 設計一個預測分析器

對於文法的任何非終結符號,它的各個產生式體的FIRST集合互不相交,如果我們有一個翻譯方案,即一個增加了語義動作的文法,那么我們可以將這些語義動作當作此語法分析器的過程的一部分執行
一個預測分析器(predictive parser)程序由各個非終結符對應的過程組成,對應於非終結符A的過程完成以下兩項任務

1. 檢查"向前看符號",決定使用A的哪個產生式,如果一個產生式的體為a(a為非空串)且向前看符號在FIRST(a)中,那么就選擇這個產生式
    1) 對於任何向前看符號,如果兩個非空的產生式體之間存在沖突,我們就不能對這種文法使用預測語法分析
    2) 如果A有空串產生式,那么只有當向前看符號不在A的其他產生式體的FIRST集合中時,才會使用A的空串產生式
2. 然后,這個過程模擬被選中產生式的體,也就是說,從左邊開始逐個"執行"此產生式體中的符號,"執行"一個非終結符號的方法是調用該非終結符號對應的過程,一個與向前看符號匹配的的終結符號的"執行"方法則是讀入下一個輸入符號,如果在某個點上,產生式體中的終結符號和向前看符號不匹配,那么語法分析器就會報告一個語法錯誤

4. 左遞歸

通過下降語法分析器有可能進入無限循環,當出現如下所示的"左遞歸"產生式時,分析器就會出現無限循環

expr -> expr + term

在這里,產生式的最左邊的符號和產生式頭部的非終結符號相同,假設expr對應的過程決定使用這個產生式,因為產生式體的開頭是expr,所以expr對應的過程將被遞歸調用,由於只有當產生式體中的一個終結符號被成功匹配時,向前看符號才會發生改變,因此在對expr的兩次調用之間輸入符號沒有發生改變,結果,第二次expr調用所做的事情與第一次調用所做的事情完全相同,這意味着會對expr進行第三次調用,並不斷重復,進入無限循環

 

5. 簡單表達式的翻譯器(源代碼示例)

語法制導翻譯方案常常作為翻譯器的規約

0x1: 抽象語法和具體語法

設計一個翻譯器時,名為抽象語法樹(abstract syntax tree)的數據結構是一個很好的起點,在一個表達式的抽象語法樹中,每個內部結點代表一個運算符,該結點的子結點代表這個運算符的運算分量。對於一個更加一般化的情況,當我們處理任意的程序設計語言構造時,我們可以創建一個針對這個構造的運算符,並把這個構造的具有語義信息的組成部分作為這個運算符的運算分量
抽象語法樹也簡稱語法樹(syntax tree),在某種程序上和語法分析樹相似

1. 在抽象語法樹中,內部結點代表的是程序構造: 
2. 在語法分析樹中,內部結點代表的是非終結符號: 具體語法樹(concrete syntax tree),相應的文法稱為該語言的具體語法(concrete syntax)

文法中的很多非終結符號都是代表程序的構造,但也有一部分是各種各樣的輔助符號,比如代表項、因子或其他表達式變體的非終結符號,在抽象語法樹中,通常不需要這些輔助符號,因此會將這些符號省略掉,為了強調他們的區別,我們有時把語法分析樹稱為具體語法樹(concrete syntex tree),而相應的文法稱為該語言的具體語法(concrete syntax)

0x2: 調整翻譯方案

0x3: 非終結符號的過程

0x4: 翻譯器的簡化

0x5: 完整的程序

/**
 * Created by zhenghan.zh on 2016/1/18.
 */

import java.io.*;

class Parser
{
    static int lookahead;

    public Parser() throws IOException
    {
        lookahead = System.in.read();
    }

    void expr() throws IOException
    {
        term();
        while(true)
        {
            if (lookahead == '+')
            {
                match('+');
                term();
                System.out.write('+');
            }
            else if (lookahead == '-')
            {
                match('-');
                term();
                System.out.write('-');
            }
            else
                return;
        }
    }

    void term() throws IOException
    {
        if (Character.isDigit((char)lookahead))
        {
            System.out.write((char)lookahead);
            match(lookahead);
        }
        else
        {
            throw new Error("syntax error");
        }
    }

    void match(int t) throws IOException
    {
        if (lookahead == t)
        {
            lookahead = System.in.read();
        }
        else
        {
            throw new Error("syntax error");
        }
    }
}

public class Postfix
{
    public static void main(String[] args) throws IOException
    {
        System.out.println("hello");
        Parser parse = new Parser();
        parse.expr();
        System.out.write('\n');
    }
}

對整個編譯過程有了一個整體的認識之后,下面我們從詞法分析開始逐步深入學習編譯原理

 

6. 詞法分析

一個詞法分析器從輸入中讀取字符,並將它們組成"詞法單元對象"。除了用於語法分析的終結符號之外,一個詞法單元對象還包含一些附加信息,這些信息以屬性值的形式出現
構成一個詞法單元的輸入字符稱為詞素(lexern),因此,"詞法分析器"使得"語法分析器"不需要考慮詞法單元的詞素表示方法

0x1: 刪除空白和注釋

大部分語言語序詞法單元之間出現任意數量的空白,在語法分析過程中同樣會忽略源程序中的注釋,所以這些注釋也可以當作空白處理

for (;; peek = next input character)
{
    if( peek is a blank or a tab ) do nothing;
    else if( peek is a newline ) line = line + 1;
    else break;
}

0x2: 預讀

在決定向語法分析器返回哪個詞法單元之前,詞法分析器可能需要預先讀入一些字符,例如C或Java的詞法分析器在遇到字符">"之后必須預先讀入一個字符

1. 如果下一個字符是"=",那么">"就是字符序列">="的一部分。這個序列是代表"大於等於"運算符的詞法單元的詞素
2. 否則,">"本身形成了一個"大於"運算符,詞法分析器就多讀了一個字符

一個通用的預先讀取輸入的方法是使用輸入緩沖區,詞法分析器可以從緩沖區中讀取一個字符,也可以把字符放回緩沖區,我們可以用一個指針來跟蹤已被分析的輸入部分,向緩沖區放回一個字符可以通過回移指針來實現
因為通常只需要預讀一個字符,所以一種簡單的解決方法是使用一個變量,比如peek,來保存下一個輸入字符,在讀入一個數字的數位或一個標識符的字符時,詞法分析器會預讀一個字符,例如在1后面預讀一個字符來區別1、10,在t后預讀一個字符來區分t和true
詞法分析器只有在必要的時候才進行預讀,像"*"這樣的運算符不需要預讀就能夠識別,在這種情況下,peek的值被設置為空白符,詞法分析器在尋找下一個詞法單元時會跳過這個空白符

//詞法分析起的不變式斷言
當詞法分析器返回一個詞法單元時,變量peek要么保存了當前詞法單元的詞素后的那個字符,要么保存空白符

0x3: 常量

在一個表達式的文法中,任何允許出現數位的地方都應該允許出現任意的整型常量,要使得表達式中可以出現整數常量,我們可以創建一個代表整型常量的終結符號,例如num,也可以將整數常量的語法加入到文法中
將字符組成整數並計算它的數值的工作通常是由詞法分析器完成的,因此在語法分析和翻譯過程中可以將數字當作一個單元進行處理

當在輸入流中出現一個數位序列時,詞法分析器將向語法分析器傳送一個詞法單元,該詞法單元包含終結符號num、及根據這些數位計算得到的整型屬性值,如果我們把詞法單元寫成用<>括起來的元祖,那么輸入31+28+59就被轉換成序列

<num, 31> <+> <num, 28> <+> <num, 59>

0x4: 識別關鍵字和標識符

大多數程序設計語言使用for、do、if這樣的固定字符串作為標點符號,或者用於標識某種構造,這些字符串稱為關鍵字(keyword)
字符串還可以作為標識符,來為變量、數組、函數等命名,為了簡化語法分析器,語言的文法通常把標識符當作終結符號進行處理,當某個標識符出現在輸入中時,語法分析器都會得到相同的終結符號,如id,例如在處理如下輸入時

count = count + increment
//語法分析器處理的是終結符號序列id = id + id

詞法單元id有一個屬性保存它的詞素,將詞法單元寫作元祖形式,輸入流的元祖序列是

<id, "count"> <=> <id, "count"> <+> <id, "increment"> <;>

關鍵字通常也滿足標識符的組成規則,因此我們需要某種機制來確定一個詞素什么時候組成一個關鍵字,什么時候組成一個標識符

1. 如果關鍵字作為保留字: 只有當一個字符串不是關鍵字時它才能組成一個標識符
2. 關鍵字作為標識符 

本章中的詞法分析器使用一個表來保存字符串,解決了如下問題

1. 單一表示: 一個字符串可以將編譯器的其余部分和表中字符串的具體表示隔離開,因為編譯器后續的步驟可以只使用指向表中字符串的指針或引用,操作引用要比操作字符串本身更加高效
2. 保留字: 要實現保留字,可以在初始化時在字符串表中加入保留的字符串以及它們對應的詞法單元。當詞法分析器讀到一個可以組成標識符的字符串或詞素時,它首先檢查這個字符串表中是否有這些詞素,如是,它就返回表中的詞法單元,否則返回帶有終結符號id的詞法單元

偽代碼如下

Hashtable words = new Hashtable();

if(peek 存放了一個字母)
{
    將字母或數位讀入一個緩沖區b;
    s = b中的字符形成的字符串;
    w = words.get(s)返回的詞法單元;
    if(w 不是 null) return w;
    else
    {
        將鍵-值對(s, <id, s>)加入到words;
        return 詞法單元<id, s>
    }
}

0x5: 詞法分析器

將上文給出的偽代碼片段組合起來,可以得到一個返回詞法單元對象的函數scan

Token scan()
{
    跳過空白符;
    處理數字;
    處理保留字和標識符;
    //如果程序運行到這里,就將預讀字符peek作為一個詞法單元
    Token t = new Toekn(peek);
    peek = 空白符;
    return t;
}

0x6: 符號表

符號表(symbol table)是一種供編譯器用於保存有關源程序構造的各種信息的數據結構

1. 這些信息在編譯器的分析階段被逐步收集並放入符號表
2. 它們在綜合(scan)階段用於生成目標代碼
2. 符號表的每個條目中包含與一個標識符相關的信息,例如
    1) 字符串(詞素)
    2) 類型
    3) 存儲位置
    4) 其他相關信息
3. 符號表通常需要支持同一個標識符在一個程序中的多重聲明

我們知道,一個聲明的作用域是指該聲明起作用的那一部分程序,我們將為每個作用域建立一個單獨的符號表來實現作用域,每個帶有聲明的程序塊都會有自己的符號表,這個塊中的每個聲明都在此符號表中有一個對應的條目,這種方法對其他能夠設立作用域的程序設計語言構造同樣有效,例如每個類也可以擁有自己的符號表,它的每個域和方法都在此表中有一個對應的條目

1. 符號表條目是在分析階段由詞法分析器、語法分析器和語義分析器創建並使用的,相對於詞法分析器而言,語法分析器通常更適合創建條目,它可以更好地區分一個標識符的不同聲明
2. 在有些情況下,詞法分析器可以在它碰到組成一個詞素的字符串時立刻建立一個符號表條目,但是在更多的情況下,詞法分析器只能向語法分析器返回一個詞法單元以及指向這個詞素的指針,只有語法分析器才能決定是使用之前已經創建的符號表條目,還是為這個標識符創建一個新條目

1. 為每個作用域設置一個符號表

術語"標識符x的作用域"實際上指的是x的某個聲明的作用域,術語作用域(scope)本身是指一個或多個聲明起作用的程序部分
作用域是非常重要的,因為在程序的不同部分,可能會出於不同的目的而多次聲明相同的標識符,再例如,子類可能重新聲明一個方法名字以覆蓋父類中的相應方法
如果程序塊可以嵌套,那么同一個標識符的多次聲明就可能出現在同一個塊中

1. 塊的符號表的實現可以利用作用域的最近嵌套原則,嵌套的結構確保可應用的符號表形成一個棧
2. 在棧的頂部是當前塊的符號表,棧中這個表的下方是包含這個塊的各個塊的符號表,即語句塊的最近嵌套(most-closely)規則
3. 符號表可以按照類似於棧的方式來分配和釋放

有些編譯器維護了一個散列表來存放可訪問的符號表條目,這樣的散列表實際上支持常量時間的查詢,但是在進入和離開塊時需要插入和刪除相應的條目,並且在從一個塊B離開時,編譯器必須撤銷所有因為B中的聲明而對此散列表作出的修改,為此可以在處理B的時候維護一個輔助的棧來跟蹤對這個散列表所做的修改,實現語句塊的最近嵌套原則時,我們可以將符號表鏈接起來,也就是使得內嵌語句塊的符號表指向外圍語句塊的符號表

2. 符號表的使用

從效果上看,一個符號表的作用是將信息從聲明的地方傳遞到實際使用的地方

1. 當分析標識符x的聲明時,一個語義動作將有關x的信息"放入"符號表中
2. 然后,一個像factor -> id這樣的產生式的相關語義動作從符號表中"取出"這個標識符的信息,因為對一個表達式E1 or E2的翻譯只依賴於對E1、E2的翻譯,不直接依賴於符號表,所以我們可以加入任意數量的運算符,而不會影響從聲明通過符號表到達使用地點的基本信息流

 

7. 生成中間代碼

編譯器的前端構造出源程序的中間表示,而后根據這個中間表示生成目標程序

0x1: 兩種中間表示形式

1. 樹型結構
    1) 語法分析樹: 在語法分析過程中,將創建抽象語法樹的結點來表示有意義的程序構造,隨着分析的進行,信息以與結點相關的屬性的形式被添加到這些結點上,選擇哪些屬性要依據待完成的翻譯來決定
    2) 抽象語法樹
2. 線性表示形式
    1) 三地址代碼: 三地址代碼是一個由基本程序步驟(例如兩個值相加)組成的序列,和樹形結構不一樣,它沒有層次化的結構,如果我們想對代碼做出顯著的優化,就需要這種表示形式,在那種情況下,我們可以把組成程序的很長的三地址語句序列分解為"基本塊",所謂基本塊就是一個總是順序執行的語句序列,執行時不會出現分支跳轉

除了創建一個中間表示之外,編譯器前端還會檢查源程序是否遵循源語言的語法和語義規則,這種檢查稱為靜態檢查(static check)

0x2: 語法樹的構造

0x3: 靜態檢查

靜態檢查是指在編譯過程中完成的各種一致性檢查,這些檢查不僅可以確保一個程序被順利地編譯,而且還能在程序運行之前發現編程錯誤,靜態檢查包括 

1. 語法檢查: 語法檢查要求比文法中的要求更多,例如
    1) 任何作用域內同一個標識符最多只能聲明一次
    2) 一個break語句必須處於一個循環或switch語句之內
//這些約束都是語法要求,但是它們並沒有包括在用於語法分析的文法中
2. 類型檢查: 一種語言的類型規則確保一個運算符或函數被應用到類型和數量都正確的運算分量上,如果必須要進行類型轉換,比如將一個浮點數與一個整數相加,類型檢查器就會在語法樹中插入一個運算符來表示這個轉換

1. 左值和右值

靜態檢查要確保一個賦值表達式的左部表示的是一個左值,一個像i這樣的標識符是一個左值,像a[2]這樣的數組訪問也是左值,但2這樣的常量不可以出現在一個賦值表達式的左部

2.  類型檢查

類型檢查確保一個構造的類型符合其上下文對它的期望,例如在if語句中

if(expr) stmt
//期望表達式expr是boolean型的

0x4: 三地址碼

一旦抽象語法樹構造完成,我們就可以計算樹中各結點的屬性值並執行各結點中的代碼片段,進行進一步的分析和綜合

1. 三地址指令

三地址代碼是由如下形式的指令組成的序列

x = y op z
//x、y、z可以是名字、常量或由編譯器生成的臨時量;而op表示一個運算符

三地址指令將被順序執行,當時當遇到一個條件或無條件跳轉指令時,執行過程就會跳轉

2. 語句的翻譯

通過利用跳轉指令實現語句內部的控制流,我們可以將語句轉換成三地址代碼

3. 表達式的翻譯

我們將考慮包含二目運算符op、數組訪問和賦值運算,並包含常量及標識符的表達式,以此來說明對表達式的翻譯

0x5: 小結

1. 構造一個語法制導翻譯器要從源語言的文法開始,一個文法描述了程序的層次結構。文法的定義使用了稱為"終結符號"的基本符號和稱為"非終結符號"的變量符號,這些符號代表了語言的構造。一個文法的規則,即產生式,由一個作為"產生式頭""產生式左部"的非終結符,以及稱為"產生式體""產生式右部"的終結符號/非終結符號序列組成。文法中有一個非終結符被指派為開始符號
2. 在描述一個翻譯器時,在程序構造中附加屬性是非常有用的,屬性是指與一個程序構造關聯的任何量值,因為程序構造是使用文法符號來表示的,因此屬性的概念也被擴展到文法符號上。屬性的例子包括與一個表示數字的終結符號num相關的整數值,或與一個表示標識符的終結符號id相關聯的字符串
3. 詞法分析器從輸入中逐個讀取字符,並輸出一個詞法單元的流,其中詞法單元由一個終結符號以及以屬性值形式出現的附加信息組成
4. 語法分析要解決的問題是指如何從一個文法的開始符號推導出一個給定的終結符號串。推導的方法是反復將某個非終結符號替換為它的某個產生式的體。從概念上講,語法分析器會創建一棵語法分析樹
5. 語法制導翻譯通過在文法中添加規則或程序片段來完成
6. 語法分析的結果是源代碼的一種中間表示形式,稱為中間代碼(AST或三地址碼)

 

8. 詞法分析器的實現

如果要手動地實現詞法分析器,需要首先建立起每個詞法單元的詞法結構圖或其他描述,然后我們可以編寫代碼來識別輸入中出現的每個詞素,並返回識別到的詞法單元的有關信息
我們也可以通過如下方式自動生成一個詞法分析器

1. 向一個詞法分析器生成工具(lexical-analyzer generator)描述出詞素的模式
2. 然后將這些模式編譯為具有詞法分析器功能的代碼

在學習詞法分析器生成工具之前,我們先學習正則表達式,正則表達式是一種可以很方便地描述詞素模式的方法 

1. 正則表達式首先轉換為不確定有窮自動機
2. 然后再轉換為確定有窮自動機
3. 得到的結果作為"驅動程序"的輸入,這個驅動程序就是一段模擬這些自動機的代碼,它使用這些自動機來確定下一個詞法單元
4. 這個驅動程序以及對自動機的規約形成了詞法分析器的核心部分

0x1: 詞法分析器的作用

詞法分析是編譯的第一個階段,詞法分析器的主要任務是讀入源程序將、它們組成詞素、生成並輸出一個詞法單元序列,每個詞法單元對應於一個詞素,這個詞法單元序列被輸出到語法分析器進行語法分析,詞法分析器通常還要和符號表進行交互,當詞法分析器發現了一個標識符的詞素時,它要將這個詞素添加到符號表中,在某些情況下,詞法分析器會從符號表中讀取有關標識符種類的信息,以確定向語法分析器傳送哪個詞法單元

詞法分析器在編譯器中負責讀取源程序,因此它還會完成一些識別詞素之外的其他任務

1. 任務之一是過濾掉源程序中的注釋和空白(空格、換行符、制表符以及在輸入中用於分割詞法單元的其他字符)
2. 另一個任務是將編譯器生成的錯誤消息與源程序的位置關聯起來

有時,詞法分析器可以分成兩個級聯的處理階段

1. 掃描階段主要負責完成一些不需要生成詞法單元的簡單處理,比如刪除注釋和將多個連續的空白字符壓縮成一個字符
2. 詞法分析階段是較為復雜的部分,它處理掃描階段的輸出並生成詞法單元

1. 詞法分析及解析

把編譯過程的分析部分划分為詞法分析和語法分析階段有如下幾個原因

1. 最重要的考慮是簡化編譯器的設計,將詞法分析和語法分析分離通常使我們至少可以簡化其中的一項任務,例如如果一個語法分析器必須把空白符和注釋當作語法進行處理,那么它就會比那些假設空白和注釋已經被詞法分析器過濾掉的處理器復雜得多,如果我們正在設計一個新的語言,將詞法和語法分開考慮有助於我們得到一個更加清晰的語言設計方案
2. 提高編譯器效率,把詞法分析器獨立出來使我們能夠使用專用語詞法分析任務、不進行語法分析的技術,此外,我們可以使用專門的用於讀取輸入字符的緩沖技術來顯著提高編譯器的速度
3. 增強編譯器的可移植性,輸入設備相關的特殊性可以被限制在詞法分析器中

2. 詞法單元、模式、詞素

在討論詞法分析時,我們使用三個相關但有區別的術語

1. 詞法單元由一個詞法單元名和一個可選的屬性值組成,詞法單元名是一個表示某種詞法單位的抽象符號,比如一個特定的關鍵字,或者代表一個標識符的輸入字符序列。詞法單元名字是由語法分析器處理的輸入符號
2. 模式描述了一個詞法單元的詞素可能具有的形式,當詞法單元是一個關鍵字時,它的模式就是組成這個關鍵字的字符序列。對於標識符和其他詞法單元,模式是一個更加復雜的結構,它可以和很多符號串匹配
3. 詞素是源程序中的字符序列,它和某個詞法單元的模式匹配,並被詞法分析器識別為該詞法單元的一個實例

在很多程序設計語言中,下面的類別覆蓋了大部分或所有的詞法單元

1. 每個關鍵字有一個詞法單元,一個關鍵字的模式就是該關鍵字本身
2. 表示運算符的詞法單元,它可以表示單個運算符,也可以表示一類運算符
3. 一個表示所有標識符的詞法單元
4. 一個或多個表示常量的詞法單元,例如數字和字面值字符串
5. 每一個標點符號有一個詞法單元,例如左右括號、逗號、分號

3. 詞法單元的屬性

如果有多個詞素可以和一個模式匹配,那么詞法分析器必須向編譯器的后續階段提供有關被匹配詞素的附加信息,例如0、1都能和詞法單元number的模式匹 配,但是對於代碼生成器而言,至關重要的是知道在源程序中找到了哪個詞素,很多時候,詞法分析器不僅向語法分析器返回一個詞法單元名字,還會返回一個描述 該詞法單元的詞素的屬性值
詞法單元的名字將影響語法分析過程中的決定,而屬性值會影響語法分析之后對這個詞法單元的翻譯
通常,一個標識符的屬性值是一個指向符號表中該標識符對應條目的指針

4. 詞法錯誤

如果沒有其他組件的幫助,詞法分析器很難發現源代碼中的錯誤,然而,假設出現所有詞法單元的模式都無法和剩余輸入的某個前綴相匹配的情況,此時詞法分析器就不能繼續處理輸入,當出現這種情況時,最簡單的錯誤恢復策略是"恐慌模式"恢復,我們從剩余的輸入中不斷刪除字符,直到詞法分析器能夠在剩余輸入的開頭發現一個正確的詞法單元為止
可能采取的其他錯誤恢復動作包括

1. 從剩余的輸入中刪除一個字符
2. 從剩余的輸入中插入一個遺漏的字符
3. 用一個字符來替換另一個字符
4. 交換兩個相鄰的字符

這些變換可以在試圖修復錯誤輸入時進行,最簡單的策略是檢查是否可以通過一次變換將剩余輸入的某個前綴變成一個合法的詞素,這種策略的合理性在於,在實踐中,大多數詞法錯誤只涉及一個字符

0x2: 輸入緩沖

在討論如何識別輸入流中的詞素之前,我們首先討論幾種可以加快源程序讀入速度的方法。源程序讀入雖然簡單卻很重要,由於我們常常需要查看一下詞素之后的若干字符才能確定是否找到了正確的詞素,因此這個任務變得有些困難

1. 緩沖區對

由於在編譯一個大型源程序時需要處理大量的字符,處理這些字符需要很多的時間
因此開發了一些特殊的緩沖區技術來減少用於處理單個輸入字符的時間開銷,一種重要的機制就是利用兩個交替讀入的緩沖區

每個緩沖區的容量都是N個字符,通常N是一個磁盤塊的大小,如4096字節,這使得我們可以使用系統讀取指令一次將N個字符讀入到緩沖區中,而不是每讀入一個字符調用一次系統讀取命令。如果輸入文件中的剩余字符不足N個,那么就會有一個特殊字符(EOF)來標記源文件的結束,這個特殊字符不同於任何可能出現在源程序中的字符
程序為輸入維護了兩個指針

1. lexemeBegin指針: 該指針指向當前詞素的開始處,當前我們正試圖確定這個詞素的結尾
2. forward指針: 它一直向前掃描,直到發現某個模式匹配位置

一旦確定了下一個詞素,forward指針將指向該詞素結尾的字符,詞法分析器將這個詞素作為某個返回給語法分析器的詞法單元的屬性值記錄下來,然后使lexemeBegin指針指向剛剛找到的詞素之后的第一個字符
將forward指針前移(即歸零)要求我們首先檢查是否已經到達某個緩沖區的末尾,如果是,我們必須將N個新字符讀到另一個緩沖區中,且將forward指針指向這個新載入字符的緩沖區的頭部
只要我們從不需要越過實際的詞素向前看很遠,以至於這個詞素的長度加上我們向前看的距離大於N,我們就決不會在識別這個詞素之前覆蓋叼這個尚在緩沖區中的待別試詞素

2. 哨兵標記

如果我們擴展每個緩沖區,使它們在末尾包含一個"哨兵(sentinel)"字符,我們就可以把對緩沖區末端的檢測和對當親字符的測試合二為一,這個哨兵字符必須是一個不會在源程序中出現的特殊字符,一個自然的選擇就是字符EOF

0x3: 詞法單元的規約

正則表達式是一種用來描述詞素模式的重要表示方法,雖然正則表達式不能表達出所有可能的模式,但是它們可以高效地描述在處理詞法單元時要用到的模式類型

1. 串和語言

字母表(alphabet)是一個有限的符號集合,符號的典型例子包括字母、數位、標點符號
某個字母表上的一個串(string)是該字母表中符號的一個有窮序列,在語言理論中,術語"句子"、"字"常常被當作"串"的同義詞,而語言(language)是某個給定字母表上一個任意的可數的串集合,這個定義非常寬泛
下面是關於串相關的常用術語

1. 串s的前綴(prefix)是從s的尾部刪除0個或多個符號后得到的串
2. 串s的后綴(suffix)是從s的開始處刪除0個或多個符號后得到的串
3. 串s的子串(substring)是刪除s的某個前綴和某個后綴之后得到的串
4. 串s的真(true)前綴、真后綴、真子串分別是s的既不等於空串、也不等於s本身的前綴、后綴的子串
5. 串s的子序列(subsequence)是從s中刪除0個或多個符號后得到的串,這些被刪除的符號可能不相鄰

2. 語言上的運算

在詞法分析中,最重要的語言上的運算是並、連接、閉包運算

3. 正則表達式

人們常常使用一種稱為正則表達式的表示方法來描述語言,正則表達式
可以描述所有通過對某個字母表上的符號應用這些運算符而得到的語言
正則表達式可以由較小的正則表達式按照如下規則遞歸地構建,每個正則表達式r表示一個語言L(r),這個語言也是根據r的子表達式所表示的語言遞歸地定義的

1. 可以用一個正則表達式定義的語言叫做正則集合(regular set)
2. 如果兩個正則表達式r、s表示同樣的語言,則稱r、s等價(equivalent),記作r = s
3. 正則表達式遵守一些代數定律,每個定律都斷言兩個具有不同形式的表達式等價

4. 正則定義

5. 正則表達式的擴展

0x4: 詞法單元的識別

我們已經討論了如何使用正則表達式來表示一個模式,接下來,我們繼續討論如何根據各個需要識別的詞法單元的模式來構造出一段代碼

1. 狀態轉換圖

作為構造詞法分析器的一個中間步驟,我們首先將模式轉換成具有特定風格的流圖,稱為"狀態轉換圖"
狀態轉換圖(transition diagram)有一組被稱為"狀態(state)"的結點或圓圈,詞法分析器在掃描輸入串的過程中尋找和某個模式匹配的詞素,而轉換圖中的每個狀態代表一個可能在這個過程中出現的情況,我們可以將一個狀態看作是對我們已經看到的位於lexemeBegin指針和forward指針之間的字符的總結,它包含了我們在進行詞法分析時需要的全部信息
一些關於狀態轉換圖的重要約定如下

1. 某些狀態稱為接受狀態或最終狀態,這些狀態表明已經找到了一個詞素,雖然實際的詞素可能並不包括lexemeBegin指針和forward指針之間的所有字符,我們用雙層的圈來表示一個接受狀態,並且如果該狀態要執行一個動作的話,通常是向語法分析器返回一個詞法單元和相關屬性值,我們把這個動作附加到該接收狀態上
2. 另外,如果需要將forward回退到一個位置,那么我們將在該接受狀態的附近加上一個*
3. 有一個狀態被指定為開始狀態,也稱為初始狀態,該狀態由一條沒有出發結點的、標號為"start"的邊指明,在讀入任何輸入符號之前,狀態轉換圖總是處於它的開始狀態

2. 保留字和標識符的識別

我們可以使用兩種方法來處理那些看起來很像標識符的保留字

1. 初始化時就將各個保留字填入符號表,符號表條目的某個字段會指明這些串不是普通的標識符,並指出它們所代表的詞法單元
2. 為每個關鍵字建立單獨的狀態轉換圖,要注意的是,這樣的狀態轉換圖包含的狀態表示看到該關鍵字的各個后續字母后的情況

3. 連續性例子
4. 基於狀態轉換圖的詞法分析器的體系結構

有幾種方法可以根據一組狀態轉換圖構造出一個詞法分析器,不管整體的策略是什么,每個狀態總是對應於一段代碼,我們可以想象有一個變量state保存了一個狀態轉換圖的當前狀態的編號,有一個switch語句根據state的值將我們轉到對應於各個可能狀態的相應的代碼段,我們可以在那里找到該狀態需要執行的動作,一個狀態的代碼本身常常也是一條switch語句或多路分支語句,這個語句讀入並檢查下一個輸入字符,由此確定下一個狀態

1. 我們可以讓詞法分析器順序地嘗試各個詞法單元的狀態轉換圖
2. 我們可以"並行地"運行各個狀態轉換圖
3. 我們可以將所有的狀態轉換圖合並為一個圖,我們允許合並后的狀態轉換圖盡量多的讀取輸入,直到不存在下一個狀態位置,然后去最長的和某個模式匹配的最長詞素

0x5: Code Example

// getTokenExample.cpp : 定義控制台應用程序的入口點。
//

#include "stdafx.h" 
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <stdlib.h>

using namespace std;

char ch;
char stra[256];
struct keyword
{
    int number;
    char attribute[20];
}keywords[17]=
{
    {1,"break"},
    {2,"char"},
    {3,"continue"},
    {4,"do"},
    {5,"double"},
    {6,"else"},
    {7,"extern"},
    {8,"float"},
    {9,"for"},
    {10,"int"},
    {11,"if"},
    {12,"long"},
    {13,"short"},
    {14,"static"},
    {15,"switch"},
    {16,"void"},
    {17,"while"}
};
 
int IsLetter(char ch);
int IsDigit(char ch);
int checkReserve(char str[]);
char Concat(char str[],char a);
void lexer();
void GetBC();
void resetScanBuffer(char str[]);
void inPut();
void GetChar();

void lexer()
{
    char strToken[50] = "";
    if(IsLetter(ch))
    {
        //get a token
        while(IsLetter(ch) || IsDigit(ch))
        {
            Concat(strToken, ch);
            GetChar();
        }
        //check if keyword
        checkReserve(strToken);
        if(checkReserve(strToken))
        {
            cout << '<' << checkReserve(strToken) << ',' << strToken << '>' << endl;
            //clear scan buffer
            resetScanBuffer(strToken);
        }
        else 
        {
            cout << '<' << "70," << strToken << '>' << endl; 
            resetScanBuffer(strToken);
        }
    }
    else if(IsDigit(ch))
    {
        while(IsDigit(ch))
        {
            Concat(strToken,ch);
            GetChar();
        }
        cout << '<' << "80," << strToken << '>' << endl;
    }
    else 
    {
        //check calculate symbol
        switch(ch)
        {
            case '<' : 
                GetChar();
                if(ch == '=') 
                    cout << '<' << "31," << "<=" << '>' << endl;
                else if(ch == '>') 
                    cout << '<' << "32," << "<>" << '>' << endl;
                else 
                    cout << '<' << "30," << '<' << '>' << endl;
                break;
            case '>' : 
                GetChar();
                if(ch == '=') 
                {
                    cout << '<' << "34," << ">=" << '>' << endl;
                    break;
                }
                else 
                {
                    cout << '<' << "33," << '>' << '>' << endl;
                    break;
                }
            case '=' :
                cout << '<' << "35," << '=' << '>' << endl;
                break;
            case '(' :
                cout << '<' << "36," << '(' << '>' << endl;
                break;
            case ')' :
                cout << '<' << "37," << ')' << '>' << endl;
                break;
            case '*' :
                GetChar();
                if(ch == '*') 
                {
                    cout << '<' << "38," << "**" << '>' << endl;
                    break;
                }
                else 
                {
                    cout << '<' << "39," << '*' << '>' << endl;
                    break;
                }
            case ':' : 
                GetChar();
                if(ch == '=') 
                {
                    cout << '<' << "40," << ":=" << '>' << endl;
                    break;
                }
                else 
                    break;
            case '+' : 
                cout << '<' << "41," << '+' << '>' << endl;
                break;
            case '-' : 
                cout << '<' << "42," << '-' << '>' << endl;
                break;
            case '?' : 
                cout << '<' << "43," << '?' << '>' << endl;
                break;
            case ',' : 
                cout << '<' << "44," << ',' << '>' << endl;
                break;
            case ';' : 
                cout << '<' << "45," << ';' << '>' << endl;
                break;
            case '\n' : 
                break;
            default : 
                cout << '<' << "0," << ch << '>' << endl;
                break;
        }
    }
}

    
void GetBC()
{
    while(ch == ' ' || ch == '\n' || ch == '\t')
        GetChar();
}
int IsLetter(char ch)
{
    if((ch <= 90) && (ch >= 65) || (ch <= 122) && (ch >= 97))
        return 1;
    else 
        return 0;
}

int IsDigit(char ch)
{
        if((ch <= 57) && (ch >= 48))
            return 1;
        else 
            return 0;
}

int checkReserve(char str[])
{
    int i;
    for(i = 0; i < 17; i++)
    {
          if(strcmp(str, keywords[i].attribute) == 0)
            return keywords[i].number;
    }
    return 0;
}

char Concat(char str[],char a)
{
    int i = 0;
    i = strlen(str);
    str[i] = a;
    str[i+1] = '\0';
    return *str;
}

void resetScanBuffer(char str[])
{
    int i,j;
    i = strlen(str);
    for(j = 0; j < i; j++)
        str[i] = '\0';
}

void inPut()
{
    int i;
    for(i=0;ch!='$';i++)
    {stra[i]=ch;
    ch=getchar();}
}

void GetChar()
{
    int i=1;
    ch = stra[i];
    i++;
}
    


int _tmain(int argc, _TCHAR* argv[])
{
    GetChar();
    GetBC();
    while(ch != ' ' && ch != '\n' && ch != '\t')
    {
        lexer();
        ch = getchar();
        GetBC();
    } 
    return 0;
}

Relevant Link:

《編譯原理 中文第二版》 89頁
http://www.ymsky.net/views/64074.shtml
http://rosettacode.org/wiki/Tokenize_a_string#C.2B.2B
http://www.hackingwithphp.com/21/5/6/how-to-parse-text-into-tokens
http://www.cnblogs.com/yanlingyin/archive/2012/04/17/2451717.html

 

9. 詞法分析器生成工具Lex

Lex(在最新的實現中也稱為Flex),它支持使用正則表達式來描述各個詞法單元的模式,由此給出一個詞法分析器的規約,Lex工具的輸入表示方法稱為Lex語言(Lex Language),而工具本身則稱為Lex編譯器(Lex Compiler),在它的核心部分,Lex編譯器將輸入的模式轉換成一個狀態轉換圖,並生成相應的實現代碼,存放到文件lex.yy.c中,這些代碼模擬了狀態轉換圖

0x2: Lex程序的結構

一個Lex程序具有如下形式

聲明部分
%%
轉換規則
%%
輔助函數

1. 聲明部分

聲明部分包括變量和明示常量(manifest constant,被聲明的表示一個常數的標識符,如一個詞法單元的名字)的聲明

2. 轉換規則

Lex程序的每個轉換規則具有如下形式

模式 { 動作 }
1. 模式: 是一個正則表達式,它可以使用聲明部分中給出的正則定義
2. 動作: 動作部分是代碼片段

3. 輔助函數

包含了各個動作需要使用的所有輔助函數

0x3: Lex中的沖突解決

Lex解決沖突的兩個規則,當輸入的多個前綴與一個或多個模式匹配時,Lex用如下規則選擇正確的詞素

1. 總是選擇最長的前綴
2. 如果最長的可能前綴與多個模式匹配,總是選擇在Lex程序中先被列出的模式

0x4: 向前看運算符

0x5: 有窮自動機

我們接下來學習Lex是如何將它的輸入程序變成一個詞法分析器的,轉換的核心是被稱為有窮自動機(finite automate)的表示方法,這些自動機在本質上是與狀態轉換圖類似的圖,但有如下幾點不同

1. 有窮自動機是識別器(recognizer),它們只能對每個可能的輸入串返回""、或""兩種結果
2. 有窮自動機分為兩類
    1) 不確定的有窮自動機(nondeterministic finite automate NFA)對其邊上的標號沒有任何限制,一個符號標記離開同一狀態的多條邊,並且空串也可以作為標號
    2) 對於每個狀態及自動機輸入字母表中的每個符號,確定的有窮自動機(deterministic finite automate DFA)有且只有一條離開該狀態、以該符號為標號的邊

確定的和不確定的有窮自動機能識別的語言的集合是相同的,事實上,這些語言的集合正好是能夠用正則表達式描述的語言的集合,這個集合中的語言稱為正則語言(regular language)

1. 不確定的有窮自動機

0x6: 從正則表達式到自動機

0x7: 詞法分析器生成工具的設計

0x8: Lex使用學習

1. Hello world

example1.lt

%{
#include <stdio.h>
%}
%%
stop    printf("Stop command received\n");
start   printf("Start command received\n");
%%

編譯

lex example1.lt
gcc lex.yy.c -o example -ll
./example

可以看到,示例程序會自動讀取輸入,並根據正則詞法規則進行詞法解析,並生成對應的Token制導結果

2. 正則匹配

接下來在Lex中引用正則,本質上這也是詞法分析狀態機的基礎

%{
#include <stdio.h>
%}
%%
[0123456789]+           printf("NUMBER\n");
[a−zA−Z][a−zA−Z0−9]*    printf("WORD\n");
%%

3. 一個更復雜的類C語法示例

待解析文件

logging {
    category lame−servers { null; };
    category cname { null; };
};

zone "." {
    type hint;
    file "/etc/bind/db.root";
};

example1.lt

%{
#include <stdio.h>
%}
%%
[a−zA−Z][a−zA−Z0−9]*    printf("WORD ");
[a−zA−Z0−9\/.−]+        printf("FILENAME ");
\"                      printf("QUOTE ");
\{                      printf("OBRACE ");
\}                      printf("EBRACE ");
;                       printf("SEMICOLON ");
\n                      printf("\n");
[ \t]+                  /* ignore whitespace */;
%%

0x9: Yacc學習

YACC沒有輸入流的概念,它僅接受預處理過的符號集,Yacc被用作編譯器的解析文析的工具。計算機語言不允許有二義性。因此,YACC在遇到有歧義時會抱怨移進/歸約或者歸約/歸約沖突

1. 入門例程

待編譯文件

heat on
    Heater on!
heat off
    Heater off!
target temperature 22
    New temperature set!

example1.lt

%{
#include <stdio.h>
#include "y.tab.h"
%}
%%
[0-9]+                  return NUMBER;
heat                    return TOKHEAT;
on|off                  return STATE;
target                  return TOKTARGET;
temperature             return TOKTEMPERATURE;
\n                      /* ignore end of line */;
[ \t]+                  /* ignore whitespace */;
%%

注意兩個重要的變化

1. 引入了頭文件y.tab.h
2. 不再使用print函數,而是直接返回符號的名字。這樣做的目的是為了接下來將它嵌入到YACC中,而后者對打印到屏幕的內容根本不關心。Y.tab.h定義了這些符號

example1.y

%{
#include <stdio.h>
#include <string.h>

void yyerror(const char *str)
{
    fprintf(stderr,"error: %s\n",str);
}

int yywrap()
{
    return 1;
}

main()
{
    yyparse();
}

%}

%token NUMBER TOKHEAT STATE TOKTARGET TOKTEMPERATURE

%%

commands: /* empty */
    | commands command
    ;


command:
    heat_switch
    |
    target_set
    ;

heat_switch:
    TOKHEAT STATE 
    {
        printf("\tHeat turned on or off\n");
    }
    ;

target_set:
    TOKTARGET TOKTEMPERATURE NUMBER
    {
        printf("\tTemperature set\n");
    }
    ;

編譯

yacc -d example1.y
//如果調用YACC時啟用了-d選項,會將這些符號會輸出到y.tab.h文件
lex example1.lt
gcc lex.yy.c y.tab.c -o example1

2. 拓展溫度調節器使其可處理參數

上面的示例可以正確的解析溫度調節器的命令,但是它並不知道應該做什么,它並不能取到你輸入的溫度值
接下來工作就是向其中加一點功能使之可以讀取出具體的溫度值。為此我們需要學習如何將Lex中的數字(NUMBER)匹配轉化成一個整數,使其可以在YACC中被讀取
當Lex匹配到一個目標時,它就會將匹配到的文字放到yytext中。YACC從變量yylval中取值

%{
#include <stdio.h>
#include "y.tab.h"
%}
%%
[0-9]+                  yylval=atoi(yytext); return NUMBER;
heat                    return TOKHEAT;
on|off                  yylval=!strcmp(yytext,"on"); return STATE;
target                  return TOKTARGET;
temperature             return TOKTEMPERATURE;
\n                      /* ignore end of line */;
[ \t]+                  /* ignore whitespace */;
%%

example1.y

%{
#include <stdio.h>
#include <string.h>

void yyerror(const char *str)
{
    fprintf(stderr,"error: %s\n",str);
}

int yywrap()
{
    return 1;
}

main()
{
    yyparse();
}

%}

%token NUMBER TOKHEAT STATE TOKTARGET TOKTEMPERATURE

%%

commands:
    | commands command
    ;


command:
    heat_switch
    |
    target_set
    ;

heat_switch:
    TOKHEAT STATE 
    {
        if($2)
            printf("\tHeat turned on\n");
        else
            printf("\tHeat turned off\n");
    }
    ;

target_set:
    TOKTARGET TOKTEMPERATURE NUMBER
    {
        printf("\tTemperature set to %d\n",$3);
    }
    ;

0x10: Lex和YACC內部工作原理

1. 在YACC文件中,main函數調用了yyparse(),此函數由YACC自動生成,在y.tab.c文件中
2. 函數yyparse從yylex中讀取符號/值組成的流。你可以自己編碼實現這點,或者讓Lex幫你完成。在我們的示例中,我們選擇將此任務交給Lex 
3. Lex中的yylex函數從一個稱作yyin的文件指針所指的文件中讀取字符。如果你沒有設置yyin,默認是標准輸入(stdin)。輸出為yyout,默認為標准輸出(stdout)
4. 可以在yywrap函數中修改yyin,此函數在每一個輸入文件被解析完畢時被調用,它允許你打開其它的文件繼續解析,如果是這樣,yywarp的返回值為0。如果想結束解析文件,返回1
5. 每次調用yylex函數用一個整數作為返回值,表示一種符號類型,告訴YACC當前讀取到的符號類型,此符號是否有值是可選的,yylval即存放了其值
6. 默認yylval的類型是整型(int),但是可以通過重定義YYSTYPE以對其進行重寫。分詞器需要取得yylval,為此必須將其定義為一個外部變量。原始YACC不會幫你做這些,因此你得將下面的內容添加到你的分詞器中,就在#include<y.tab.h>下即可:
extern YYSTYPE yylval;
Bison會自動完成剩下的事情

Relevant Link:

http://ds9a.nl/lex-yacc/
http://segmentfault.com/a/1190000000396608#articleHeader18

 

10. PHP Lex(Lexical Analyzer)

詞法分析階段就是從輸入流里邊一個字符一個字符的掃描,識別出對應的詞素,最后把源文件轉換成為一個TOKEN序列,然后丟給語法分析器
PHP在最開始的詞法解析器是使用的是flex,后來PHP的改為使用re2c
我們通過一個簡單的例子來看下re2c。如下是一個簡單的掃描器,它的作用是判斷所給的字符串是數字/小寫字母/大小字母

#include <stdio.h>

char *scan(char *p)
{
    #define YYCTYPE char
    #define YYCURSOR p
    #define YYLIMIT p
    #define YYMARKER q
    #define YYFILL(n)
        /*!re2c
          [0-9]+ {return "number";}
          [a-z]+ {return "lower";}
          [A-Z]+ {return "upper";}
          [^] {return "unkown";}
         */
}

int main(int argc, char* argv[])
{
    printf("%s\n", scan(argv[1])); 
    return 0;
}
/*
re2c -o a.c a.l
gcc a.c -o a
chmod +x a
./a 1000

output: number
*/

代碼中用到的幾個re2c約定的宏定義如下

1. YYCTYPE: 用於保存輸入符號的類型,通常為char型和unsigned char型
2. YYCURSOR: 指向當前輸入標記,當開始時,它指向當前標記的第一個字符,當結束時,它指向下一個標記的第一個字符
3. YYFILL(n): 當生成的代碼需要重新加載緩存的標記時,則會調用YYFILL(n)
4. YYLIMIT: 緩存的最后一個字符,生成的代碼會反復比較YYCURSOR和YYLIMIT,以確定是否需要重新填充緩沖區 

0x1: RE2C
re2c - convert regular expressions to C/C++
re2c is a lexer generator for C/C++. It finds regular expression specifications inside of C/C++ comments and replaces them with a hard-coded DFA. The user must supply some interface code in order to control and customize the generated DFA.
re2c本質上是一個生成詞法生成器的生成器

1. Given the following code

unsigned int stou (const char * s)
{
#   define YYCTYPE char
    const YYCTYPE * YYCURSOR = s;
    unsigned int result = 0;

    for (;;)
    {
        /*!re2c
            re2c:yyfill:enable = 0;

            "\x00" { return result; }
            [0-9]  { result = result * 10 + c; continue; }
        */
    }
}

2. re2c -is will generate

unsigned int stou (const char * s)
{
#   define YYCTYPE char
    const YYCTYPE * YYCURSOR = s;
    unsigned int result = 0;

    for (;;)
    { 
    {
        YYCTYPE yych;

        yych = *YYCURSOR;
        if (yych <= 0x00) goto yy3;
        if (yych <= '/') goto yy2;
        if (yych <= '9') goto yy5;
    yy2:
    yy3:
        ++YYCURSOR;
        { return result; }
    yy5:
        ++YYCURSOR;
        { result = result * 10 + c; continue; }
    }

    }
}

SYNTAX
Code for re2c consists of a set of rules, named definitions and inplace configurations.

0x2: PHP Lexer代碼分析

\php-src-master\Zend\zend_language_scanner.l
zend_language_scanner.l 文件是re2c的規則文件,如果安裝了re2c,可以通過以下命令來生成c文件

re2c -F -c -o zend_language_scanner.c zend_language_scanner.l

在re2c生成的詞法解析器中,有兩個維度的狀態機

1. 第一個維度是從"字符串"的維度來維護的狀態
2. 第二個是從"字符"的維度來維護狀態

例如在Zend引擎中,當掃描到"<?php"時,Zend會將當前第一維度的狀態設置為ST_IN_SCRIPTING,表示現在我們已經進入了PHP腳本解析的狀態了。這個維度的狀態可以很方便的在lex文件中作為各種前置條件,例如在lex文件中有很多這樣的聲明

其表達的意思就是:當我們詞法解析器處於ST_IN_SCRIPTING這個狀態時,遇到"exit"這個字符串就返回一個T_EXIT的Token標志(在Zend引擎中Token的宏都是以T_開頭,其實際對應是一個數字)

在詞法解析器掃描字符的過程中,需要記錄掃描過程的各個參數以及當前狀態,這些變量都是以yy開頭命名。常用到的就是:yy_state, yy_text, yyleng, yy_cursor, yy_limit
各個變量的狀態掃描前后的變化示意圖。
掃描echo前

掃描echo后:

通過一個字符一個字符的掃描最終會得到一個Token序列,然后交由語法分析器去解析

0x3: Zend詞法解析狀態

Zend引擎在做詞法解析時會自己維護掃描過程的狀態,其實就是將yy_text等變量自己封裝一個結構體,我們可以在lex文件中看到很多SCNG的宏調用,例如

static void yy_scan_buffer(char *str, unsigned int len)
{
    YYCURSOR       = (YYCTYPE*)str;
    YYLIMIT        = YYCURSOR + len;
    if (!SCNG(yy_start)) {
        SCNG(yy_start) = YYCURSOR;
    }
}

定位一下#define SCNG

/* Globals Macros */
#define SCNG    LANG_SCNG

$PHPSRC/Zend/zend_globals_macros.h

#else
# define LANG_SCNG(v) (language_scanner_globals.v)
extern ZEND_API zend_php_scanner_globals language_scanner_globals;
#endif 

可以看到Zend引擎維護了一個zend_php_scanner_globals的結構體
$PHPSRC/Zend/zend_globals.h

struct _zend_php_scanner_globals 
{
    zend_file_handle *yy_in;
    zend_file_handle *yy_out;

    unsigned int yy_leng;
    unsigned char *yy_start;
    unsigned char *yy_text;
    unsigned char *yy_cursor;
    unsigned char *yy_marker;
    unsigned char *yy_limit;
    int yy_state;
    zend_stack state_stack;
    zend_ptr_stack heredoc_label_stack;

    /* original (unfiltered) script */
    unsigned char *script_org;
    size_t script_org_size;

    /* filtered script */
    unsigned char *script_filtered;
    size_t script_filtered_size;

    /* input/output filters */
    zend_encoding_filter input_filter;
    zend_encoding_filter output_filter;
    const zend_encoding *script_encoding;

    /* initial string length after scanning to first variable */
    int scanned_string_len;
};

0x4: 掃描過程

詞法掃描的入口在zend_language_scanner.l的int lex_scan(zval *zendlval)中

int lex_scan(zval *zendlval)
{
restart:
    ////設置當前token的首位置為當前位置
    SCNG(yy_text) = YYCURSOR;

//這段注釋定義了各個類型的正則表達式匹配,在詞法解析程序(如bison、re2c等)程序將本文件轉化為c代碼時會用到
/*!re2c
re2c:yyfill:check = 0;
LNUM    [0-9]+
DNUM    ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*)
EXPONENT_DNUM    (({LNUM}|{DNUM})[eE][+-]?{LNUM})
HNUM    "0x"[0-9a-fA-F]+
BNUM    "0b"[01]+
LABEL    [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*
WHITESPACE [ \n\r\t]+
TABS_AND_SPACES [ \t]*
TOKENS [;:,.\[\]()|^&+-/*=%!~$<>?@]
ANY_CHAR [^]
NEWLINE ("\r"|"\n"|"\r\n")

/* compute yyleng before each rule */
<!*> := yyleng = YYCURSOR - SCNG(yy_text);

//對於一些無需復雜處理的關鍵字,我們掃描到對應的關鍵字,直接生成對應的Token標志即可
<ST_IN_SCRIPTING>"exit" {
    return T_EXIT;
}

/*
<ST_IN_SCRIPTING>是指掃描到這個關鍵字的前置條件是詞法解析器要處於ST_IN_SCRIPTING這個狀態
在lex文件里邊有以下幾種方式可以設置當前的詞法解析器狀態
1. #define YYGETCONDITION()  SCNG(yy_state)
2. #define YYSETCONDITION(s) SCNG(yy_state) = s
3. #define BEGIN(state) YYSETCONDITION(STATE(state))
4. static void _yy_push_state(int new_state TSRMLS_DC)
{
    //將當前狀態壓棧,然后重設當前狀態為新狀態
    zend_stack_push(&SCNG(state_stack), (void *) &YYGETCONDITION(), sizeof(int));
    YYSETCONDITION(new_state);
}
*/
<ST_IN_SCRIPTING>"die" {
    return T_EXIT;
}

<ST_IN_SCRIPTING>"function" {
    return T_FUNCTION;
}

<ST_IN_SCRIPTING>"const" {
    return T_CONST;
} 
...

<INITIAL>"<?=" {
    BEGIN(ST_IN_SCRIPTING);
    return T_OPEN_TAG_WITH_ECHO;
}


<INITIAL>"<?php"([ \t]|{NEWLINE}) {
    HANDLE_NEWLINE(yytext[yyleng-1]);
    BEGIN(ST_IN_SCRIPTING);
    return T_OPEN_TAG;
}


<INITIAL>"<?" {
    if (CG(short_tags)) {
        BEGIN(ST_IN_SCRIPTING);
        return T_OPEN_TAG;
    } else {
        goto inline_char_handler;
    }
}
//進入PHP解析狀態 inline_char_handler: //我們知道PHP是嵌入式的,只有包含在<?php ?>或者<? ?>標簽中的字符才會被執行解析 while (1) { YYCTYPE *ptr = memchr(YYCURSOR, '<', YYLIMIT - YYCURSOR); YYCURSOR = ptr ? ptr + 1 : YYLIMIT; if (YYCURSOR >= YYLIMIT) { break; } if (*YYCURSOR == '?') { if (CG(short_tags) || !strncasecmp((char*)YYCURSOR + 1, "php", 3) || (*(YYCURSOR + 1) == '=')) { /* Assume [ \t\n\r] follows "php" */ YYCURSOR--; break; } } }

從這里也可以看出php的open tag的多種寫法,接着我們看一下PHP里邊注釋是怎么掃描的,接着我們看一下PHP里邊注釋是怎么掃描的

<ST_IN_SCRIPTING>"#"|"//" 
{
    while (YYCURSOR < YYLIMIT) 
    {
        switch (*YYCURSOR++) 
        {
            case '\r':
                if (*YYCURSOR == '\n') 
                {
                    YYCURSOR++;
                }
                /* fall through */
            case '\n':
                CG(zend_lineno)++;
                break;
            case '?':
                if (*YYCURSOR == '>') 
                {
                    YYCURSOR--;
                    break;
                }
                /* fall through */
            default:
                continue;
        }

        break;
    }

    yyleng = YYCURSOR - SCNG(yy_text);

    return T_COMMENT;
}

可以看出,PHP是支持#以及//兩種方式的單行注釋。處於ST_IN_SCRIPTING狀態下,遇到"#"|"//",變觸發了單行注釋的掃描,從當前字符開始一直掃描到流緩沖區的末尾(也即是while(YYCURSOR < YYLIMIT))
遇到\r\n以及\n時,遞增記錄當前解析的行(zend_lineno++),為了更好容錯性,PHP還兼容了//?>這樣的語法,也即是說當行注釋是不會注釋到?>的,可以從case '?'這個分支看出Zend的處理,先讓當前指針YYCURSOR--,回到?>前一個字符,然后跳出循環,這樣才不會吃掉"?>"導致后邊認不到PHP的關閉標簽
多行注釋的掃描邏輯如下

<ST_IN_SCRIPTING>"/*"|"/**"{WHITESPACE} 
{
    int doc_com;

    if (yyleng > 2) 
    {
        doc_com = 1;
        RESET_DOC_COMMENT();
    } 
    else 
    {
        doc_com = 0;
    }

    while (YYCURSOR < YYLIMIT) 
    {
        if (*YYCURSOR++ == '*' && *YYCURSOR == '/') 
        {
            break;
        }
    }

    if (YYCURSOR < YYLIMIT) 
    {
        YYCURSOR++;
    } 
    else 
    {
        zend_error(E_COMPILE_WARNING, "Unterminated comment starting line %d", CG(zend_lineno));
    }

    yyleng = YYCURSOR - SCNG(yy_text);
    HANDLE_NEWLINES(yytext, yyleng);

    if (doc_com) 
    {
        CG(doc_comment) = zend_string_init(yytext, yyleng, 0);
        return T_DOC_COMMENT;
    }

    return T_COMMENT;
}

如果一直到文件結尾都沒掃到*/,那就zend_error一個Waring錯誤,但是不會影響接下去的解析

數字類型的解析,從一開始的正則規則里邊可以知道PHP支持5中類型的數字常量聲明

LNUM    [0-9]+
DNUM    ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*)
EXPONENT_DNUM    (({LNUM}|{DNUM})[eE][+-]?{LNUM})
HNUM    "0x"[0-9a-fA-F]+
BNUM    "0b"[01]+

其實對於代碼來說,數字其實也是字符,詞法分析器掃描到這5個規則的時候,需要把當前的zendlval對應的解析成數字存起來,同時返回一個數字類型的Token標志,我們跟進最簡單的LNUM規則處理

<ST_IN_SCRIPTING>{LNUM} 
{
    char *end;
    //首先檢查一下當前的字符串是否超出C語言的long類型長度,如果不超過,直接接調用strtol把字符串轉換成long int類型
    if (yyleng < MAX_LENGTH_OF_LONG - 1) 
    { 
        /* Won't overflow */
        errno = 0;
        ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
        /* This isn't an assert, we need to ensure 019 isn't valid octal
         * Because the lexing itself doesn't do that for us
         */
        if (end != yytext + yyleng) 
        {
            zend_error_noreturn(E_COMPILE_ERROR, "Invalid numeric literal");
        }
        ZEND_ASSERT(!errno);
    } 
    else 
    {
        errno = 0;
        ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
        //如果超出了long的范圍,Zend還是嘗試看看能不能轉,如果發生溢出(error == ERANGE)那就把當前數字轉成double類型
        if (errno == ERANGE) 
        { 
            /* Overflow */
            errno = 0;
            if (yytext[0] == '0')
            { 
                /* octal overflow */
                errno = 0;
                ZVAL_DOUBLE(zendlval, zend_oct_strtod(yytext, (const char **)&end));
            } 
            else 
            {
                ZVAL_DOUBLE(zendlval, zend_strtod(yytext, (const char **)&end));
            }
            /* Also not an assert for the same reason */
            if (end != yytext + yyleng) 
            {
                zend_error_noreturn(E_COMPILE_ERROR, "Invalid numeric literal");
            }
            ZEND_ASSERT(!errno);
            return T_DNUMBER;
        }
        /* Also not an assert for the same reason */
        if (end != yytext + yyleng) 
        {
            zend_error_noreturn(E_COMPILE_ERROR, "Invalid numeric literal");
        }
        ZEND_ASSERT(!errno);
    }
    return T_LNUMBER;
}

PHP變量類型,PHP的變量是以美元符$開頭,從詞法規則里邊可以看到

//$var->prop
<ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE>"$"{LABEL}"->"[a-zA-Z_\x7f-\xff] {
    yyless(yyleng - 3);
    yy_push_state(ST_LOOKING_FOR_PROPERTY);
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    return T_VARIABLE;
}

/* A [ always designates a variable offset, regardless of what follows
 */
//$var["key"]
<ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE>"$"{LABEL}"[" {
    yyless(yyleng - 1);
    yy_push_state(ST_VAR_OFFSET);
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    return T_VARIABLE;
}
//$var
<ST_IN_SCRIPTING,ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE,ST_VAR_OFFSET>"$"{LABEL} {
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    return T_VARIABLE;
}

有三種變量的聲明調用方式,$var, $var->prop, $var["key"],接着,通過zend_copy_value拷貝變量名到zendlval里邊記錄起來供之后語法解析階段插入到符號表里邊去

PHP的字符串類型在詞法分析階段應該是最復雜的,PHP里邊的字符串可以由"單引號"或"雙引號"來圍住,單引號的字符串比雙引號的字符串效率會更高

<ST_IN_SCRIPTING>b?['] 
{
    register char *s, *t;
    char *end;
    int bprefix = (yytext[0] != '\'') ? 1 : 0;

    while (1) 
    {
        if (YYCURSOR < YYLIMIT) 
        {
            if (*YYCURSOR == '\'') 
            {
                YYCURSOR++;
                yyleng = YYCURSOR - SCNG(yy_text);

                break;
            } 
            else if (*YYCURSOR++ == '\\' && YYCURSOR < YYLIMIT) 
            {
                //YYCURSOR++的目的就是為了跳過下一個字符,例如:'\'',如果不跳過第二個單引號的話,我們掃描到第二個引號就會認為字符串結束了
                YYCURSOR++;
            }
        } 
        else 
        {
            yyleng = YYLIMIT - SCNG(yy_text);

            /* Unclosed single quotes; treat similar to double quotes, but without a separate token
             * for ' (unrecognized by parser), instead of old flex fallback to "Unexpected character..."
             * rule, which continued in ST_IN_SCRIPTING state after the quote */
            ZVAL_NULL(zendlval);
            //從輸入流中取出字符串的內容,返回一個T_CONSTANT_ENCAPSED_STRING的Token標志
            return T_ENCAPSED_AND_WHITESPACE;
        }
    }

    ZVAL_STRINGL(zendlval, yytext+bprefix+1, yyleng-bprefix-2);

    /* convert escape sequences */
    s = t = Z_STRVAL_P(zendlval);
    end = s+Z_STRLEN_P(zendlval);
    while (s<end) {
        if (*s=='\\') {
            s++;

            switch(*s) {
                case '\\':
                case '\'':
                    *t++ = *s;
                    Z_STRLEN_P(zendlval)--;
                    break;
                default:
                    *t++ = '\\';
                    *t++ = *s;
                    break;
            }
        } else {
            *t++ = *s;
        }

        if (*s == '\n' || (*s == '\r' && (*(s+1) != '\n'))) {
            CG(zend_lineno)++;
        }
        s++;
    }
    *t = 0;

    if (SCNG(output_filter)) {
        size_t sz = 0;
        char *str = NULL;
        s = Z_STRVAL_P(zendlval);
        // TODO: avoid reallocation ???
        SCNG(output_filter)((unsigned char **)&str, &sz, (unsigned char *)s, (size_t)Z_STRLEN_P(zendlval));
        ZVAL_STRINGL(zendlval, str, sz);
    }
    return T_CONSTANT_ENCAPSED_STRING;
}

雙引號的字符串處理就復雜一點

<ST_IN_SCRIPTING>b?["] 
{
    int bprefix = (yytext[0] != '"') ? 1 : 0;

    while (YYCURSOR < YYLIMIT) 
    {
        switch (*YYCURSOR++) 
        {
            //如果雙引號字符串里邊沒有變量,直接就返回一個字符串了,從這里看出,其實雙引號字符串在沒有包含$的情況下的效率跟單引號字符串是差不多的
            case '"':
                yyleng = YYCURSOR - SCNG(yy_text);
                zend_scan_escape_string(zendlval, yytext+bprefix+1, yyleng-bprefix-2, '"');
                return T_CONSTANT_ENCAPSED_STRING;
            //雙引號里邊是支持變量的!$hello = "Hello"; $str = "${hello} World";
            case '$':
                if (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == '{') {
                    break;
                }
                continue;
            case '{':
                if (*YYCURSOR == '$') {
                    break;
                }
                continue;
            case '\\':
                if (YYCURSOR < YYLIMIT) {
                    YYCURSOR++;
                }
                /* fall through */
            default:
                continue;
        }

        YYCURSOR--;
        break;
    }

    /* Remember how much was scanned to save rescanning */
    //如果遇到了變量!這個時候就要切換到ST_DOUBLE_QUOTES狀態了
    SET_DOUBLE_QUOTES_SCANNED_LENGTH(YYCURSOR - SCNG(yy_text) - yyleng);

    YYCURSOR = SCNG(yy_text) + yyleng;

    BEGIN(ST_DOUBLE_QUOTES);
    return '"';
}

PHP魔術變量分為"編譯時替換"以及"運行時替換"

<ST_IN_SCRIPTING>"__CLASS__" {
    return T_CLASS_C;
}

<ST_IN_SCRIPTING>"__TRAIT__" {
    return T_TRAIT_C;
}

<ST_IN_SCRIPTING>"__FUNCTION__" {
    return T_FUNC_C;
}

<ST_IN_SCRIPTING>"__METHOD__" {
    return T_METHOD_C;
}

<ST_IN_SCRIPTING>"__LINE__" {
    return T_LINE;
}

<ST_IN_SCRIPTING>"__FILE__" {
    return T_FILE;
}

<ST_IN_SCRIPTING>"__DIR__" {
    return T_DIR;
}

<ST_IN_SCRIPTING>"__NAMESPACE__" {
    return T_NS_C;
}

PHP的容錯機制

<ST_LOOKING_FOR_VARNAME>{ANY_CHAR} 
{
    yyless(0);
    yy_pop_state();
    yy_push_state(ST_IN_SCRIPTING);
    goto restart;
}

<ST_IN_SCRIPTING,ST_VAR_OFFSET>{ANY_CHAR} 
{
    if (YYCURSOR > YYLIMIT) {
        return 0;
    }

    zend_error(E_COMPILE_WARNING,"Unexpected character in input:  '%c' (ASCII=%d) state=%d", yytext[0], yytext[0], YYSTATE);
    goto restart;
}

Relevant Link: 

http://blog.csdn.net/hguisu/article/details/7490027
http://sourceforge.net/projects/re2c/
http://sourceforge.net/projects/re2c/files/re2c/
http://re2c.org/
http://www.phppan.com/2011/09/php-lexical-re2c/#comment-4699 
http://re2c.org/manual.html
http://www.secoff.net/archives/331.html
http://blog.csdn.net/raphealguo/article/details/16941531

 

11. 語法分析

在設計語言時,每種程序設計語言都有一組精確的規則來描述良構(well-formed)程序的語法結構。程序設計語言構造的語法可以使用"上下文無關文法"、或者"BNF范式"表示法來描述。文法為語言設計者和編譯器編寫者都提供了很大的便利

1. 文法給出了一個程序設計語言的精確易懂的語法規約
2. 對於某些類型的文法,我們可以自動地構造出高效的語法分析器,它能夠確定一個源程序的語法結構。同時,語法分析器的構造過程可以揭示出語法的二義性,同時很可能發現一些容易在語言的初始設計階段被忽略的問題
3. 一個正確設計的文法給出了一個語言的結構,該結構有助於把源程序翻譯為正確的目標代碼,也有助於檢測錯誤
4. 一個文法支持逐步加入可以完成新任務的新語言構造從而迭代地演化和開發語言

0x1: 引論

1. 語法分析器的作用

在我們的編譯器模型中,語法分析器從詞法分析器獲得一個由詞法單元組成的串,並驗證這個串可以由源語言的文法生成,對於良構的程序,語法分析器構造出一棵語法分析樹,並把它傳遞給編譯器的其他部分進一步處理,實際上,並不需要顯示地構造出這課語法分析樹,這僅僅是在內存中的一個數據結構
處理文法的語法分析器大體上可以分為幾種類型

1. 通用型
2. 自頂向下: 從語法分析樹的頂部(根節點)開始向底部(葉子結點)構造語法分析樹
3. 自底向上: 從葉子結點開始,逐漸向根節點方向構造
//這兩種分析方法中,語法分析器的輸入總是按照從左到右的方式被掃描,每次掃描一個符號

2. 代表性的文法

下面的文法指明了運算符的結合性和優先級

E -> E + T | T
//E表示一組以+號分隔的項所組成的表達式

T -> T * F | F
//T表示由一組以*分隔的因子所組成的項

F -> (E) | id
//F表示因子,它可能是括號括起來的表達式,也可能是標識符

3. 語法錯誤的處理

程序可能有不同層次的錯誤

1. 詞法錯誤
    1) 標識符、關鍵字、運算符拼寫錯誤
    2) 沒有在字符串文本上正確地加上引號
2. 語法錯誤
    1) 分號放錯地方
    2) 花括號多余或缺失
3. 語義錯誤
    1) 運算符和運算分量之間的類型不匹配,例如,返回類型為void的某個方法中出現了一個返回某個int值的return語句
4. 邏輯錯誤
    1) 可以是因程序員的錯誤推理而引起的任何錯誤,比如在一個C程序中應該使用比較運算符==的地方使用了賦值運算符=,這樣的程序可能是良構的,但是卻沒有正確反映出程序員的意圖

語法分析方法的精確性使得我們可以非常高效地檢測出語法錯誤,有些語法分析方法,比如LL和LR方法,能夠在第一時間發現錯誤。也就是說,當來自詞法分析器的詞法單元流不能根據該語言的文法進一步分析時就會發現錯誤,更精確地講,它們具有"可行前綴特性(viable-prefix property)",也就是說,一旦發現輸入的某個前綴不能通過添加一些符號而形成這個語言的串,就可以立刻檢測到語法錯誤

4. 錯誤恢復策略

1. 恐慌模式的恢復 
2. 短語層次的恢復
3. 錯誤產生式
4. 全局糾正

0x2: 上下文無關文法

1. 上下文無關文法的正式定義

一個上下文無關文法由以下元素組成

1. 終結符號: 組成串的基本符號,"詞法單元名字""終結符號"是同義詞,因為在大多數編程語言中,終結符號就是一個獨立的詞法單元
2. 非終結符號: 表示串的集合的語法變量,非終結符號表示的串集合用於定義由文法生成的語言。非終結符號給出了語言的層次結構,而這種層次結構是語法分析和翻譯的關鍵
3. 在一個文法中,某個非終結符號被指定為開始符號,這個符號表示的串集合就是這個文法生成的語言
4. 一個文法的產生式描述了將終結符號和非終結符號組合成串的方法,每個產生式由下列元素組成
    1) 一個被稱為產生式頭或左部的非終結符號,這個產生式定義了這個頭所代表的串集合的一部分
    2) 符號->,有時也使用::=來替代箭頭
    3) 一個由零頭或多個終結符號與非終結符號組合的產生式體或右部。產生式體中的成分描述了產生式頭上的非終結符號所對應的串的某個構造方法

2. 符號表示的約定

3. 推導

將產生式看作重寫規則,就可以從推導的角度精確地描述構造語法分析樹的方法,從開始符號出發,每個重寫步驟把一個非終結符號替換為它的某個產生式的體,這個推導思想對應於自頂向下構造語法分析樹的過程,但是推導概念所給出的精確性在自底向上的語法分析過程中尤其有用 

4. 語法分析樹和推導

語法分析樹是推導的圖形表示形式,它過濾掉了推導過程中對非終結符號應用產生式的順序,語法分析樹的每個內部結點表示一個產生式的應用,該內部結點的標號是此產生式頭中的非終結符號A,這個結點的子節點的標號從左到右組成了在推導過程中替換這個A的產生式體
一棵語法分析樹的葉子結點的標號既可以是非終結符號,也可以是終結符號,從左到右排列這些符號就可以得到一個句型,它稱為這棵樹的結果(yield)或邊緣(frontier)

5. 二義性

如果一個文法可以為某個句子生成多課語法分析樹,那么它就是二義性(ambiguous),換句話說,二義性文法就是對同一個句子有多個最左推導或多個最右推導文法

6. 驗證文法生成的語言

驗證文法G生成語言L的過程可以分成兩個部分

1. 證明G生成的每個串都在L中
2. 並且反向證明L中的每個串都確實能由G生成

7. 上下文無關文法和正則表達式

需要明白的是,文法是比正則表達式表達能力更強的表示方法,每個可能使用正則表達式描述的構造都可以使用文法來描述,但是反之不成立。換句話說,每個正則語言都是一個上下文無關語言,但是反之不成立

0x3: 設計文法

文法能夠描述程序設計語言的大部分(但不是全部)語法,比如,在程序中標識符必須先聲明后使用,但是這個要求不能通過一個上下文無關文法來描述。因此,一個詞法分析器接受的詞法單元序列構成了程序設計語言的超集。編譯器的后續步驟必須對語法分析器的輸出進行分析,以保證源程序遵守那些沒有被語法分析器檢查的規則

1. 詞法分析和語法分析

我們知道,任何能夠使用正則表達式描述的語言都可以使用文法描述,但是,為什么lex/flex/re2c這些詞法解析器都使用正則表達式來定義一個語言的詞法語法,理由有以下幾個

1. 將一個語言的語法結構分為詞法和非詞法兩部分可以很方便地將編譯器前端模塊化,將前端分解為兩個大小適中的組件
2. 一個語言的詞法規則通常很簡單,我們不需要使用像文法這樣的功能強大且復雜的表示方法來描述這些規則
3. 和文法相比,正則表達式通常提供了更加簡潔且易於理解的表示詞法單元的方法(易於編寫)
4. 根據正則表達式自動構造得到的詞法分析器的效率要高於根據任意文法自動構造得到的分析器

原則上,並不存在一個嚴格的制導方針來規定哪些東西應該放到詞法規則中,正則表達式最適合描述諸如標識符、常量、關鍵字、空白這樣的語言構造的結構,另一方面,文法最適合描述嵌套結構,比如對稱的括號對,匹配的begin-end、相互對應的if-then-else等,這些嵌套結構不能使用正則表達式描述

2. 消除二義性

一個二義性文法可以被改寫為無二義性的文法

3. 左遞歸的消除

如果一個文法中有一個非終結符號A使得對某個串a存在一個推導A => Aa,那么這個文法就是左遞歸的(left rescursive),自頂向下語法分析方法不能處理左遞歸的文法,因此需要一個轉換方法來消除左遞歸,同時,這樣的替換不能改變可從A推導得到的串的集合

4. 提取左公因子

提取左公因子是一種文法轉換方法,它可以產生適用於預測分析技術或自頂向下分析技術的文法。當不清楚應該在兩個A產生式中如何選擇時,我們可以通過改寫產生式來推后這個決定,等我們讀入了足夠多的輸入,獲得足夠信息后再做出正確選擇

5. 非上下文無關語言的構造

0x4: 自頂向下的語法分析

自頂向下語法分析可以被看作是為輸入串構造語法分析樹的問題,它從語法分析樹的根節點開始,按照先根次序(深度優先)創建這課語法分析樹的各個結點,自頂向下語法分析也可以被看作尋找輸入串的最左推導的過程
在一個自頂向下語法分析的每一步中,關鍵問題是確定對一個非終結符號(例如A)應用哪個產生式,一旦選擇了某個A產生式,語法分析過程的其余部分負責將相應產生式體中的終結符號和輸入相匹配

1. 遞歸下降的語法分析

一個遞歸下降語法分析程序由一組過程組成,每個非終結符號有一個對應的過程(產生式翻譯過程),程序的執行從開始符號對應的過程開始,如果這個過程的過程體掃描了整個輸入串,它就停止執行並宣布語法分析成功完成

void A()
{
    選擇一個A產生式, A -> X1X2..Xk;
    for(i = 1 to k)
    {
        if(Xi是一個非終結符號)
            調用過程Xi();
        else if(Xi等於當前的輸入符號a)
            讀入下一個輸入符號;
        else /*發生了一個錯誤*/
    }
}

通用的遞歸下降分析技術可能需要回溯,也就是說,它可能需要重復掃描輸入,然而,在對程序設計語言的構造進行語法分析時很少需要回溯,因此需要回溯的語法分析器並不常見,即使在自然語言語法分析這樣的場合,回溯也不是很高效,因此我們更傾向於基於表格的方法,例如動態程序規划算法或者Earley方法

2. FIRST和FOLLOW

自頂向下和自底向上語法分析器的構造可以使用和文法G相關的兩個函數FIRST和FOLLOW來實現。在自頂向下語法分析過程中,FIRST和FOLLOW使得我們可以根據下一個輸入符號來選擇應用哪個產生式。在恐慌模式的錯誤恢復中,由FOLLOW產生的詞法單元集合可以作為同步詞法單元
計算各個文法符號X的FIRST(X)時,不斷應用下列規則,直到再沒有新的終結符號或e可以被加入到任何FIRST集合中為止

1. 如果X是一個終結符號,那么FIRST(X) = X
2. 如果X是一個非終結符號,且X -> Y1Y2...Yk是一個產生式,其中k >= 1,那么如果對於某個i、a在FIRST(Yi)中且e在所有的FIRST(Y1)、FIRST(Y2)...FIRST(Yi-1)中,就把a加入到FIRST(X)中
3. 如果X -> e是一個產生式,那么將e加入到FIRST(X)中

3. LL(1)文法

對於稱為LL(1)的文法,我們可以構造出預測分析器,即不需要回溯的遞歸下降語法分析器,LL(1)中的第一個"L"表示從左向右掃描輸入,第二個"L"表示最左推導,而"1"則表示在每一步中只需要向前看一個輸入符號來決定語法分析動作 

4. 非遞歸的預測分析

我們可以構造出一個非遞歸的預測分析器,它顯式地維護一個棧結構,而不是通過遞歸調用的方式隱式地維護棧。這樣的語法分析器可以模擬最左推導的過程

0x5: 自底向上的語法分析

一個自底向上的語法分析過程對應於為一個輸入串構造語法分析樹的過程,它從葉子結點(底部)開始逐漸向上到達根節點(頂部)

1. 規約

我們可以將自底向上語法分析過程看成一個串w"規約"為文法開始符號的過程,在每個規約(reduction)步驟中,一個與某產生式體相匹配的特定子串被替換為該產生式頭部的非終結符號
在自底向上語法分析過程中,關鍵問題是何時進行規約以及應用哪個產生式進行規約

2. 句柄剪枝

3. 移入-規約語法分析技術

移入-規約語法分析是自底向上語法分析的一種形式,它使用一個棧來保存文法符號,並用一個輸入緩沖區來存放將要進行語法分析的其余符號,句柄在被識別之前,總是出現在棧的頂部

0x6: LR語法分析技術

0x7: 更強大的LR語法分析器

0x8: 使用二義性文法

0x9: 語法分析器生成工具

我們接下來使用語法分析器生成工具來構造一個編譯器的前端,它們使用LALR語法分析器生成工具Yacc

1. 語法分析器生成工具Yacc

一個Yacc源程序由三個部分組成

/*
一個Yacc程序的聲明部分分為以下幾個部分,它們都是可選
1. 通常的C聲明
2. 翻譯過程中使用的臨時變量
3. 對詞法單元的聲明: 如果向Yacc語法分析器傳送詞法單元的詞法分析器是使用Lex創建的,則Lex生成的詞法分析器也可以使用這里聲明的詞法單元
*/
聲明

%%
/*
每個規則由一個文法產生式和一個相關聯的語義動作組成
產生式頭: <產生式體>1 { <語義動作>1 }
| <產生式體>2 { <語義動作>2 }
..
| <產生式體>n { <語義動作>n }

1. 在一個Yacc產生式中,如果一個由字母和數字組成的字符串沒有加引號且未被聲明為詞法單元,它就會被當作非終結符號處理。帶引號的單個字符,比如'c',會被當作終結符號c以及它所代表的詞法單元所對應的整數編碼(即Lex將把'c'的字符編碼當作整數返回給詞法分析器)
2. 不同的產生式體用豎線分開,每個產生式頭以及它的可選產生式體及語義動作之后跟一個分號
3. 第一個產生式的頭符號被看作開始符號
4. 一個Yacc語義動作是一個C語言的序列
*/
翻譯規則
%%

/*
1. 這里必須提供一個名為yylex()的詞法分析器(框架規約),用Lex來生成yylex()是一個常用的選擇
2. 詞法分析器yylex()返回一個由詞法單元名和相關屬性值組成的詞法單元。如果要返回一個詞法單元名字,比如DIGIT,那么這個名字必須先在Yacc規約的第一部分進行聲明
3. 一個詞法單元的相關屬性值通過一個Yacc定義的變量yylval傳送給語法分析器
*/
輔助性C語言例程

2. 使用帶有二義性文法的Yacc規約

除非另行指定,否則Yacc會使用下面的兩個規則來解決所有的語法分析動作沖突

1. 解決一個歸約/歸約沖突時,選擇在Yacc規約中列在前面的那個沖突產生式
2. 解決移入/規約沖突時總是選擇移入,這個規則正確地解決了因為懸空else二義性而產生的移入/歸約沖突
3. 詞法單元的優先級是根據它們在聲明部分的出現順序而定的,優先級最低的詞法單元最先出現。同一個聲明中的詞法單元具有相同的優先級

3. 用Lex創建Yacc的詞法分析器

Lex的作用是生成可以和Yacc一起使用的詞法分析器。Lex庫ll將提供一個名為yylex()的驅動程序。Yacc要求它的詞法分析器的名字為yylex(),如果用Lex來生成詞法分析器,那么我們可以將Yacc規約的第三部分的例程yylex()替換為語句: #include "lex.yy.c"
並令每個Lex動作都返回Yacc已知的終結符號,通過使用語句#include "lex.yy.c",程序yylex能夠訪問Yacc定義的詞法單元名字,因為Lex的輸出文件是作為Yacc的輸出文件y.tab.c的一部分被編譯的

4. Yacc中的錯誤恢復

Yacc的錯誤恢復使用了錯誤產生式的形式

Relevant Link:

https://github.com/luapower/lexer/tree/master/media/lexers 

 

12. 構造可配置詞法語法分析器生成器

源程序在被編譯為目標程序需要經過如下6個過程:詞法分析,語法分析,語義分析,中間代碼生成,代碼優化,目標代碼生成。詞法分析和語法分析是編譯過程的初始階段,是編譯器的重要組成部分,早期相關理論和工具缺乏的環境下,編寫詞法語法分析器是很繁瑣的事情。上世紀70年代,貝爾實驗室的M. E. Lesk,E. Schmidt和Stephen C. Johnson分別為Unix系統編寫了詞法分析器生成器Lex和語法分析器生成器Yacc,Lex接受由正則表達式定義的詞法規則,Yacc接受由BNF范式描述的文法規則,它們能夠自動生成分析對應詞法和語法規則的C源程序,其強大的功能和靈活的特性很大程度上簡化了詞法分析和語法分析的構造難度。如今Lex和Yacc已經成為著名的Unix標准內置工具(Linux下對應的工具是Flex和Bison),並被廣泛用於編譯器前端構造,它已經幫助人們實現了數百種語言的編譯器前端,比較著名的應用如mysql的SQL解析器,PHP,Ruby,Python等腳本語言的解釋引擎,瀏覽器內核Webkit,早期的GCC等。本文將介紹可配置詞法分析器和語法分析器生成器的內部原理

Relevant Link:

http://blog.csdn.net/xinghongduo/article/details/39455543
http://blog.csdn.net/xinghongduo/article/details/39505193
http://blog.csdn.net/xinghongduo/article/details/39529165 

 

13. 基於PHP Lexer重寫一份輕量級詞法分析器

我們以PHP命令行模式為切入點,理解PHP從接收輸入到詞法分析的全過程

D:\wamp\bin\php\php5.5.12\php.exe -f test.php
//執行文件

$PHPSRC/sapi/cli/php_cli.c

..
case 'f': /* parse file */
if (behavior == PHP_MODE_CLI_DIRECT || behavior == PHP_MODE_PROCESS_STDIN) 
{
    param_error = param_mode_conflict;
    break;
} 
else if (script_file) 
{
    param_error = "You can use -f only once.\n";
    break;
}
script_file = php_optarg;
break;
..
case PHP_MODE_STANDARD:
if (strcmp(file_handle.filename, "-")) {
    cli_register_file_handles();
}

if (interactive && cli_shell_callbacks.cli_shell_run) {
    exit_status = cli_shell_callbacks.cli_shell_run();
} else {
    php_execute_script(&file_handle);
    exit_status = EG(exit_status);
}
break;
..

php_execute_script(&file_handle);    
$PHPSRC/main/main.c

PHPAPI int php_execute_script(zend_file_handle *primary_file)
{
    zend_file_handle *prepend_file_p, *append_file_p;
    zend_file_handle prepend_file = {{0}, NULL, NULL, 0, 0}, append_file = {{0}, NULL, NULL, 0, 0};
    ..
    if (CG(start_lineno) && prepend_file_p) {
            int orig_start_lineno = CG(start_lineno);

            CG(start_lineno) = 0;
            if (zend_execute_scripts(ZEND_REQUIRE, NULL, 1, prepend_file_p) == SUCCESS) {
                CG(start_lineno) = orig_start_lineno;
                retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 2, primary_file, append_file_p) == SUCCESS);
            }
        } else {
            retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS);
        }
        ..

zend_execute_scripts()
Zend/Zend.c

ZEND_API int zend_execute_scripts(int type, zval *retval, int file_count, ...) /* {{{ */
{
    va_list files;
    int i;
    zend_file_handle *file_handle;
    zend_op_array *op_array;

    va_start(files, file_count);
    for (i = 0; i < file_count; i++) {
        file_handle = va_arg(files, zend_file_handle *);
        if (!file_handle) {
            continue;
        }
        
        //通過zend_compile_file把文件解析成opcode中間代碼(這一步會經過詞法語法分析)
        op_array = zend_compile_file(file_handle, type);
        if (file_handle->opened_path) {
            zend_hash_add_empty_element(&EG(included_files), file_handle->opened_path);
        }
        zend_destroy_file_handle(file_handle);
        if (op_array) {
            //用zend_execute執行這個生成的中間代碼
            zend_execute(op_array, retval);
            ..

接下來是opcode的生成和zend虛擬機對opcode的執行流程,我們留待之后深入研究,我們回到PHP的語法高亮過程,聚焦PHP的詞法分析

D:\wamp\bin\php\php5.5.12\php.exe -s test.php
//PHP的詞法解析過程通過文件操作完成,通過指針移動,逐個從文件中抽取出一個"狀態機匹配命中Token",然后輸出給語法分析器,在語法高亮邏輯這里,語法分析器就是一個HTML高亮顯示器,並沒有做多余的語法分析

$PHPSRC/sapi/cli/php_cli.c

..
case 's': /* generate highlighted HTML from source */
if (behavior == PHP_MODE_CLI_DIRECT || behavior == PHP_MODE_PROCESS_STDIN) 
{
    param_error = "Source highlighting only works for files.\n";
    break;
}
behavior=PHP_MODE_HIGHLIGHT;
break;
..
case PHP_MODE_HIGHLIGHT:
{
    zend_syntax_highlighter_ini syntax_highlighter_ini;

    if (open_file_for_scanning(&file_handle)==SUCCESS) {
        php_get_highlight_struct(&syntax_highlighter_ini);
        zend_highlight(&syntax_highlighter_ini);
    }
    goto out;
}
break;
..

我們接下里從PHP打開待執行文件、詞法解析、返回Token詞素幾個部分逐步理解PHP的詞法分析過程

0x1: PHP打開待執行文件

..
zend_file_handle file_handle;
..
open_file_for_scanning(&file_handle)
..

1. Zend引擎的全局宏定義

1. CG宏
本宏關聯的數據結構定義為_zend_compiler_globals. 宏中包含了以下主要數據,這些數據都是在Zend解釋PHP代碼過程中定義 
    1) function_table: 定義的函數的符號表
    2) class_table: 定義的類的符號表
    3) filenames_table: 文件名列表,是PHP Zend引擎打開的文件
    4) autoglobals: 自動全局變量符號表,這個表存放了超全局變量,比如$SESSION, $GLOBALS之類的
/* 
#define CG(v) (compiler_globals.v) 
extern ZEND_API struct _zend_compiler_globals compiler_globals;
*/

2. EG宏
本宏關聯的數據結構定義為_zend_executor_globals. 宏中包含了以下主要數據 
    1) included_files: 包含的文件列表
    2) function_table: 執行過程中定義的函數符號表
    3) class_table: 定義的類的符號表
    4) zend_constants: 定義的常量表
    5) ini_directives: ini文件定義信息
    6) modifiedinidirectives: 更新后的ini定義信息
    7) symbol_table: 變量符號表
/*
# define EG(v) (executor_globals.v) 
extern zend_executor_globals executor_globals; 
*/

3. LANG_SCNG宏
/*
# define LANG_SCNG(v) (language_scanner_globals.v) 
extern zend_php_scanner_globals language_scanner_globals; 
*/

4. INI_SCNG宏
/*
# define INI_SCNG(v) (ini_scanner_globals.v)  
extern zend_ini_scanner_globals ini_scanner_globals; 
*/

5. TSRMG宏

6. PG宏
main/php_globals.h:
# define PG(v) TSRMG(core_globals_id, php_core_globals *, v)

7. SG宏
main/SAPI.h:
# define SG(v) TSRMG(sapi_globals_id, sapi_globals_struct *, v)
//SG宏主要用於獲取SAPI層范圍內的全局變量 

PHP內核代碼中大量使用了全局變量和extern修飾符,全局變量的賦值和使用貫穿了腳本編譯、中間代碼執行、運行時整個生命周期,同時值得注意的是,之所以在PHP代碼中能夠使用$GLOBAL、$SESSION這樣的超全局變量,也得益於PHP內核中全局變量的使用

2. 全局宏定義對應的數據結構

struct _zend_compiler_globals 
{
    zend_stack loop_var_stack;

    zend_class_entry *active_class_entry;

    zend_string *compiled_filename;

    int zend_lineno;

    zend_op_array *active_op_array;

    HashTable *function_table;    /* function symbol table */
    HashTable *class_table;        /* class table */

    HashTable filenames_table;

    HashTable *auto_globals;

    zend_bool parse_error;
    zend_bool in_compilation;
    zend_bool short_tags;

    zend_declarables declarables;

    zend_bool unclean_shutdown;

    zend_bool ini_parser_unbuffered_errors;

    zend_llist open_files;

    struct _zend_ini_parser_param *ini_parser_param;

    uint32_t start_lineno;
    zend_bool increment_lineno;

    znode implementing_class;

    zend_string *doc_comment;

    uint32_t compiler_options; /* set of ZEND_COMPILE_* constants */

    zend_string *current_namespace;
    HashTable *current_import;
    HashTable *current_import_function;
    HashTable *current_import_const;
    zend_bool  in_namespace;
    zend_bool  has_bracketed_namespaces;

    HashTable const_filenames;

    zend_compiler_context context;
    zend_stack context_stack;

    zend_arena *arena;

    zend_string *empty_string;
    zend_string *one_char_string[256];

    HashTable interned_strings;

    const zend_encoding **script_encoding_list;
    size_t script_encoding_list_size;
    zend_bool multibyte;
    zend_bool detect_unicode;
    zend_bool encoding_declared;

    zend_ast *ast;
    zend_arena *ast_arena;

    zend_stack delayed_oplines_stack;
};


struct _zend_executor_globals 
{
    zval uninitialized_zval;
    zval error_zval;

    /* symbol table cache */
    zend_array *symtable_cache[SYMTABLE_CACHE_SIZE];
    zend_array **symtable_cache_limit;
    zend_array **symtable_cache_ptr;

    zend_array symbol_table;        /* main symbol table */

    HashTable included_files;    /* files already included */

    JMP_BUF *bailout;

    int error_reporting;
    int exit_status;

    HashTable *function_table;    /* function symbol table */
    HashTable *class_table;        /* class table */
    HashTable *zend_constants;    /* constants table */

    zval          *vm_stack_top;
    zval          *vm_stack_end;
    zend_vm_stack  vm_stack;

    struct _zend_execute_data *current_execute_data;
    zend_class_entry *scope;

    zend_long precision;

    int ticks_count;

    HashTable *in_autoload;
    zend_function *autoload_func;
    zend_bool full_tables_cleanup;

    /* for extended information support */
    zend_bool no_extensions;

#ifdef ZEND_WIN32
    zend_bool timed_out;
    OSVERSIONINFOEX windows_version_info;
#endif

    HashTable regular_list;
    HashTable persistent_list;

    int user_error_handler_error_reporting;
    zval user_error_handler;
    zval user_exception_handler;
    zend_stack user_error_handlers_error_reporting;
    zend_stack user_error_handlers;
    zend_stack user_exception_handlers;

    zend_error_handling_t  error_handling;
    zend_class_entry      *exception_class;

    /* timeout support */
    zend_long timeout_seconds;

    int lambda_count;

    HashTable *ini_directives;
    HashTable *modified_ini_directives;
    zend_ini_entry *error_reporting_ini_entry;

    //zend_objects_store objects_store;
    //zend_object *exception, *prev_exception;
    const zend_op *opline_before_exception;
    zend_op exception_op[3];

    struct _zend_module_entry *current_module;

    zend_bool active;
    zend_bool valid_symbol_table;

    zend_long assertions;

    uint32_t           ht_iterators_count;     /* number of allocatd slots */
    uint32_t           ht_iterators_used;      /* number of used slots */
    HashTableIterator *ht_iterators;
    HashTableIterator  ht_iterators_slots[16];

    void *saved_fpu_cw_ptr;
#if XPFPA_HAVE_CW
    XPFPA_CW_DATATYPE saved_fpu_cw;
#endif

    void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

struct _zend_ini_scanner_globals {
    zend_file_handle *yy_in;
    zend_file_handle *yy_out;

    unsigned int yy_leng;
    unsigned char *yy_start;
    unsigned char *yy_text;
    unsigned char *yy_cursor;
    unsigned char *yy_marker;
    unsigned char *yy_limit;
    int yy_state;
    zend_stack state_stack;

    char *filename;
    int lineno;

    /* Modes are: ZEND_INI_SCANNER_NORMAL, ZEND_INI_SCANNER_RAW, ZEND_INI_SCANNER_TYPED */
    int scanner_mode;
};

struct _zend_php_scanner_globals 
{
    zend_file_handle *yy_in;
    zend_file_handle *yy_out;

    unsigned int yy_leng;
    unsigned char *yy_start;
    unsigned char *yy_text;
    unsigned char *yy_cursor;
    unsigned char *yy_marker;
    unsigned char *yy_limit;
    int yy_state;
    zend_stack state_stack;
    zend_ptr_stack heredoc_label_stack;

    /* original (unfiltered) script */
    unsigned char *script_org;
    size_t script_org_size;

    /* filtered script */
    unsigned char *script_filtered;
    size_t script_filtered_size;

    /* input/output filters */
    zend_encoding_filter input_filter;
    zend_encoding_filter output_filter;
    const zend_encoding *script_encoding;

    /* initial string length after scanning to first variable */
    int scanned_string_len;
};

3. PHP ZVAL結構體

1. PHP是一門動態的弱類型語言
2. PHP的寫機制里會使用內存處理的引用計數的復本 
3. PHP變量,通常來說,由兩部分組成:標簽(例如,可能是符號表中的一個條目)和實際變量容器 
4. 變量容器,在代碼中稱為zval,掌握了所需處理變量的所有數據。包括
    1) 實際值
    2) 當前類型
    3) 統計指向此容器的標簽的數量
    4) 指示這些標簽是引用還是副本的標志 

注意到zval_struct->zend_value value成員,它是一個聯合體

typedef union _zend_value {
    zend_long         lval;                /* long value */
    double            dval;                /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        ZEND_ENDIAN_LOHI(
            uint32_t w1,
            uint32_t w2)
    } ww;
} zend_value;

PHP是一種若類型語言,通過ZVAL這種變量容器機制,PHP中的變量賦值變得異常靈活,可以將變量賦值為任何東西
注意到PHP對類Class的賦值和保存,我們知道PHP是一種面向對象的開發語言,從內核層面來看,這是因為PHP底層是基於C++/C開發的,PHP的對象最終是通過C++的對象/繼承最終是通過C++實現的,所以PHP中聲明的對象都從一個基類繼承而來,每個類都默認包含了一些"默認函數"

union _zend_function *constructor;
union _zend_function *destructor;
union _zend_function *clone;
union _zend_function *__get;
union _zend_function *__set;
union _zend_function *__unset;
union _zend_function *__isset;
union _zend_function *__call;
union _zend_function *__callstatic;
union _zend_function *__tostring;
union _zend_function *__debugInfo;
union _zend_function *serialize_func;
union _zend_function *unserialize_func;

zend_class_iterator_funcs iterator_funcs;

我們在創建類的時候,可以重載這些函數

4. PHP的變量傳遞

我們知道,PHP的變量傳遞是引用傳遞的,通過ZVAL機制,PHP內核在處理變量傳遞賦值的時候只是將新變量同樣指向被賦值的引用,同時將被賦值的引用計數加1,這在copy_string的實現機制中可以看出來

static zend_always_inline zend_string *zend_string_copy(zend_string *s)
{
    if (!IS_INTERNED(s)) {
        GC_REFCOUNT(s)++;
    }
    return s;
}

5. PHP的哈希表實現
PHP內核中的哈希表是十分重要的數據結構,PHP的大部分的語言特性都是基於哈希表實現的,例如

1. 變量的作用域
2. 函數表
3. 類的屬性、方法等
4. Zend引擎內部的很多數據都是保存在哈希表中的 

數據結構及說明

PHP中的哈希表是使用拉鏈法來解決沖突的,具體點講就是使用鏈表來存儲哈希到同一個槽位的數據, Zend為了保存數據之間的關系使用了雙向列表來鏈接元素
PHP中的哈希表實現在Zend/zend_hash.c中,PHP使用如下兩個數據結構來實現哈希表,HashTable結構體用於保存整個哈希表需要的基本信息, 而Bucket結構體用於保存具體的數據內容

typedef struct _hashtable { 
    uint nTableSize;        // hash Bucket的大小,最小為8,以2x增長。
    uint nTableMask;        // nTableSize-1 , 索引取值的優化
    uint nNumOfElements;    // hash Bucket中當前存在的元素個數,count()函數會直接返回此值 
    ulong nNextFreeElement; // 下一個數字索引的位置
    Bucket *pInternalPointer;   // 當前遍歷的指針(foreach比for快的原因之一)
    Bucket *pListHead;          // 存儲數組頭元素指針
    Bucket *pListTail;          // 存儲數組尾元素指針
    Bucket **arBuckets;         // 存儲hash數組
    dtor_func_t pDestructor;    // 在刪除元素時執行的回調函數,用於資源的釋放
    zend_bool persistent;       //指出了Bucket內存分配的方式。如果persisient為TRUE,則使用操作系統本身的內存分配函數為Bucket分配內存,否則使用PHP的內存分配函數。
    unsigned char nApplyCount; // 標記當前hash Bucket被遞歸訪問的次數(防止多次遞歸)
    zend_bool bApplyProtection;// 標記當前hash桶允許不允許多次訪問,不允許時,最多只能遞歸3次
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

哈希表初始化

ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)
{
    uint i = 3;
    //...
    if (nSize >= 0x80000000) {
        /* prevent overflow */
        ht->nTableSize = 0x80000000;
    } else {
        while ((1U << i) < nSize) {
            i++;
        }
        ht->nTableSize = 1 << i;
    }
    // ...
    ht->nTableMask = ht->nTableSize - 1;
 
    /* Uses ecalloc() so that Bucket* == NULL */
    if (persistent) {
        tmp = (Bucket **) calloc(ht->nTableSize, sizeof(Bucket *));
        if (!tmp) {
            return FAILURE;
        }
        ht->arBuckets = tmp;
    } else {
        tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Bucket *));
        if (tmp) {
            ht->arBuckets = tmp;
        }
    }
 
    return SUCCESS;
} 

如果設置初始大小為10,則上面的算法將會將大小調整為16。也就是始終將大小調整為接近初始大小的 2的整數次方
mask的作用就是將哈希值映射到槽位所能存儲的索引范圍內。 例如:某個key的索引值是21, 哈希表的大小為8,則mask為7,則求與時的二進制表示為: 10101 & 111 = 101 也就是十進制的5。 因為2的整數次方-1的二進制比較特殊:后面N位的值都是1,這樣比較容易能將值進行映射, 如果是普通數字進行了二進制與之后會影響哈希值的結果。那么哈希函數計算的值的平均分布就可能出現影響
設置好哈希表大小之后就需要為哈希表申請存儲數據的空間了,如上面初始化的代碼, 根據是否需要持久保存而調用了不同的內存申請方法。如前面PHP生命周期里介紹的,是否需要持久保存體現在:持久內容能在多個請求之間訪問,而非持久存儲是會在請求結束時釋放占用的空間。 具體內容將在內存管理章節中進行介紹HashTable中的nNumOfElements字段很好理解,每插入一個元素或者unset刪掉元素時會更新這個字段。 這樣在進行count()函數統計數組元素個數時就能快速的返回
nNextFreeElement字段非常有用。先看一段PHP代碼

<?php
$a = array(10 => 'Hello');
$a[] = 'TIPI';
var_dump($a);
 
// ouput
array(2) {
  [10]=>
  string(5) "Hello"
  [11]=>
  string(5) "TIPI"
}

PHP中可以不指定索引值向數組中添加元素,這時將默認使用數字作為索引, 和C語言中的枚舉類似, 而這個元素的索引到底是多少就由nNextFreeElement字段決定了。 如果數組中存在了數字key,則會默認使用最新使用的key + 1,例如上例中已經存在了10作為key的元素, 這樣新插入的默認索引就為11了

數據容器: 槽位

typedef struct bucket {
    ulong h;            // 對char *key進行hash后的值,或者是用戶指定的數字索引值
    uint nKeyLength;    // hash關鍵字的長度,如果數組索引為數字,此值為0
    void *pData;        // 指向value,一般是用戶數據的副本,如果是指針數據,則指向pDataPtr
    void *pDataPtr;     //如果是指針數據,此值會指向真正的value,同時上面pData會指向此值
    struct bucket *pListNext;   // 整個hash表的下一元素
    struct bucket *pListLast;   // 整個哈希表該元素的上一個元素
    struct bucket *pNext;       // 存放在同一個hash Bucket內的下一個元素
    struct bucket *pLast;       // 同一個哈希bucket的上一個元素
    // 保存當前值所對於的key字符串,這個字段只能定義在最后,實現變長結構體
    char arKey[1];              
} Bucket;

如上面各字段的注釋。h字段保存哈希表key哈希后的值。這里保存的哈希值而不是在哈希表中的索引值,這是因為如下原因

1. 索引值和哈希表的容量有直接關系,如果哈希表擴容了,那么這些索引還得重新進行哈希在進行索引映射,這也是一種優化手段
2. 在PHP中可以使用字符串或者數字作為數組的索引。數字索引直接就可以作為哈希表的索引,數字也無需進行哈希處理。h字段后面的nKeyLength字段是作為key長度的標示, 索引是數字的話,則nKeyLength為0
3. 在PHP數組中如果索引字符串可以被轉換成數字也會被轉換成數字索引。 所以在PHP中例如'10''11'這類的字符索引和數字索引10,11沒有區別(但是這里涉及到一個轉換約定)

結構體的最后一個字段用來保存key的字符串,而這個字段卻申明為只有一個字符的數組, 其實這里是一種長見的變長結構體,主要的目的是增加靈活性。 以下為哈希表插入新元素時申請空間的代碼

p = (Bucket *) pemalloc(sizeof(Bucket) - 1 + nKeyLength, ht->persistent);
if (!p) {
    return FAILURE;
}
memcpy(p->arKey, arKey, nKeyLength);

如代碼,申請的空間大小加上了字符串key的長度,然后把key拷貝到新申請的空間里。 在后面比如需要進行hash查找的時候就需要對比key這樣就可以通過對比p->arKey和查找的key是否一樣來進行數據的 查找。申請空間的大小-1是因為結構體內本身的那個字節還是可以使用的
在PHP5.4中將這個字段定義成const char* arKey類型了

1. Bucket結構體維護了兩個雙向鏈表,pNext和pLast指針分別指向本槽位所在的鏈表的關系(HASH沖突拉鏈法)
2. pListNext和pListLast指針指向的則是整個哈希表所有的數據之間的鏈接關系。 ashTable結構體中的pListHead和pListTail則維護整個哈希表的頭元素指針和最后一個元素的指針 

PHP中數組的操作函數非常多,例如:array_shift()和array_pop()函數,分別從數組的頭部和尾部彈出元素。 哈希表中保存了頭部和尾部指針,這樣在執行這些操作時就能在常數時間內找到目標。 PHP中還有一些使用的相對不那么多的數組操作函數:next(),prev()等的循環中, 哈希表的另外一個指針就能發揮作用了:pInternalPointer,這個用於保存當前哈希表內部的指針。 這在循環時就非常有用

Relevant Link:

http://php.net/manual/zh/internals2.variables.intro.php
http://docstore.mik.ua/orelly/webprog/php/ch14_06.htm
http://php.net/manual/zh/language.oop5.overloading.php
http://www.php-internals.com/book/?p=chapt03/03-01-02-hashtable-in-php
http://segmentfault.com/a/1190000000718519

6. PHP內存池

PHP的內存管理器是分層(hierarchical)的,這個管理器共有三層

1. 存儲層(storage)
2. 堆(heap)層
3. emalloc/efree層

存儲層(storage)

存儲層通過 malloc()、mmap() 等函數向系統真正的申請內存,並通過 free() 函數釋放所申請的內存。存儲層通常申請的內存塊都比較大,這里申請的內存大並不是指storage層結構所需要的內存大,只是堆層通過調用存儲層的分配方法時,其以段的格式申請的內存比較大,存儲層的作用是將內存分配的方式對堆層透明化

4種內存方案

PHP在存儲層共有4種內存分配方案

1. malloc: 默認使用malloc分配內存
2. win32: 如果設置了ZEND_WIN32宏,則為windows版本,調用HeapAlloc分配內存

//剩下兩種內存方案為匿名內存映射,並且PHP的內存方案可以通過設置變量來修改
3. mmap_anon: Anonymous Memory Mapping
    1) 匿名內存映射 與 使用 /dev/zero 類型,都不需要真實的文件。要使用匿名映射之需要向 mmap 傳入 MAP_ANON 標志,並且 fd 參數 置為 -1  
    2) 所謂匿名,指的是映射區並沒有通過 fd 與 文件路徑名相關聯。匿名內存映射用在有血緣關系的進程間 

4. mmap_zero: /dev/zero Memory Mapping 
    1) 可以將偽設備 "/dev/zero" 作為參數傳遞給 mmap 而創建一個映射區。/dev/zero 的特殊在於,對於該設備文件所有的讀操作都返回值為 0 的指定長度的字節流 ,任何寫入的內容都被丟棄。我們的興趣在於用它來創建映射區,用 /dev/zero 創建的映射區,其內容被初始為 0
    2) 使用 /dev/zero 的優點在於,mmap創建映射區時,不需要一個實際存在的文件,偽文件 /dev/zero 就足夠了。缺點是只能用在相關進程間。相對於相關進程間的通信,使用線程間通信效率要更高一些。不管使用那種技術,對共享數據的訪問都需要進行同步 

Relevant Link:

http://www.phppan.com/2010/11/php-source-code-30-memory-pool-storage/

0x2: PHP Lexer
$PHPSRC/Zend/zend_language_scanner.c

ZEND_API int open_file_for_scanning(zend_file_handle *file_handle)
{
    char *buf;
    size_t size, offset = 0;
    zend_string *compiled_filename;

    /* The shebang line was read, get the current position to obtain the buffer start */
    if (CG(start_lineno) == 2 && file_handle->type == ZEND_HANDLE_FP && file_handle->handle.fp) {
        if ((offset = ftell(file_handle->handle.fp)) == -1) {
            offset = 0;
        }
    }

    if (zend_stream_fixup(file_handle, &buf, &size) == FAILURE) {
        return FAILURE;
    }

    zend_llist_add_element(&CG(open_files), file_handle);
    if (file_handle->handle.stream.handle >= (void*)file_handle && file_handle->handle.stream.handle <= (void*)(file_handle+1)) 
    {
        zend_file_handle *fh = (zend_file_handle*)zend_llist_get_last(&CG(open_files));
        size_t diff = (char*)file_handle->handle.stream.handle - (char*)file_handle;
        fh->handle.stream.handle = (void*)(((char*)fh) + diff);
        file_handle->handle.stream.handle = fh->handle.stream.handle;
    }

    /* Reset the scanner for scanning the new file */
    SCNG(yy_in) = file_handle;
    SCNG(yy_start) = NULL;

    if (size != -1) {
        if (CG(multibyte)) {
            SCNG(script_org) = (unsigned char*)buf;
            SCNG(script_org_size) = size;
            SCNG(script_filtered) = NULL;

            zend_multibyte_set_filter(NULL);

            if (SCNG(input_filter)) {
                if ((size_t)-1 == SCNG(input_filter)(&SCNG(script_filtered), &SCNG(script_filtered_size), SCNG(script_org), SCNG(script_org_size))) {
                    zend_error_noreturn(E_COMPILE_ERROR, "Could not convert the script from the detected "
                            "encoding \"%s\" to a compatible encoding", zend_multibyte_get_encoding_name(LANG_SCNG(script_encoding)));
                }
                buf = (char*)SCNG(script_filtered);
                size = SCNG(script_filtered_size);
            }
        }
        SCNG(yy_start) = (unsigned char *)buf - offset;
        yy_scan_buffer(buf, (unsigned int)size);
    } else {
        zend_error_noreturn(E_COMPILE_ERROR, "zend_stream_mmap() failed");
    }

    BEGIN(INITIAL);

    if (file_handle->opened_path) {
        compiled_filename = zend_string_copy(file_handle->opened_path);
    } else {
        compiled_filename = zend_string_init(file_handle->filename, strlen(file_handle->filename), 0);
    }

    zend_set_compiled_filename(compiled_filename);
    zend_string_release(compiled_filename);

    if (CG(start_lineno)) {
        CG(zend_lineno) = CG(start_lineno);
        CG(start_lineno) = 0;
    } else {
        CG(zend_lineno) = 1;
    }

    RESET_DOC_COMMENT();
    CG(increment_lineno) = 0;
    return SUCCESS;
}
END_EXTERN_C()

0x3: 解析Token詞素

int lex_scan(zval *zendlval)
{
restart:
    SCNG(yy_text) = YYCURSOR;


#line 1079 "Zend/zend_language_scanner.c"
{
    YYCTYPE yych;
    unsigned int yyaccept = 0;
    if (YYGETCONDITION() < 5) {
        if (YYGETCONDITION() < 2) {
            if (YYGETCONDITION() < 1) {
                goto yyc_ST_IN_SCRIPTING;
            } else {
                goto yyc_ST_LOOKING_FOR_PROPERTY;
            }
        } else {
            if (YYGETCONDITION() < 3) {
                goto yyc_ST_BACKQUOTE;
            } else {
                if (YYGETCONDITION() < 4) {
                    goto yyc_ST_DOUBLE_QUOTES;
                } else {
                    goto yyc_ST_HEREDOC;
                }
            }
        }
    } else {
        if (YYGETCONDITION() < 7) {
            if (YYGETCONDITION() < 6) {
                goto yyc_ST_LOOKING_FOR_VARNAME;
            } else {
                goto yyc_ST_VAR_OFFSET;
            }
        } else {
            if (YYGETCONDITION() < 8) {
                goto yyc_INITIAL;
            } else {
                if (YYGETCONDITION() < 9) {
                    goto yyc_ST_END_HEREDOC;
                } else {
                    goto yyc_ST_NOWDOC;
                }
            }
        }
    }
/* *********************************** */
yyc_INITIAL:

    YYDEBUG(0, *YYCURSOR);
    YYFILL(7);
    yych = *YYCURSOR;
    if (yych != '<') goto yy4;
    YYDEBUG(2, *YYCURSOR);
    ++YYCURSOR;
    if ((yych = *YYCURSOR) == '?') goto yy5;
yy3:
    YYDEBUG(3, *YYCURSOR);
    yyleng = YYCURSOR - SCNG(yy_text);
#line 1760 "Zend/zend_language_scanner.l"
    {
    if (YYCURSOR > YYLIMIT) {
        return 0;
    }

inline_char_handler:

    while (1) {
        YYCTYPE *ptr = memchr(YYCURSOR, '<', YYLIMIT - YYCURSOR);

        YYCURSOR = ptr ? ptr + 1 : YYLIMIT;

        if (YYCURSOR >= YYLIMIT) {
            break;
        }

        if (*YYCURSOR == '?') {
            if (CG(short_tags) || !strncasecmp((char*)YYCURSOR + 1, "php", 3) || (*(YYCURSOR + 1) == '=')) { /* Assume [ \t\n\r] follows "php" */

                YYCURSOR--;
                break;
            }
        }
    }

    yyleng = YYCURSOR - SCNG(yy_text);

    if (SCNG(output_filter)) {
        size_t readsize;
        char *s = NULL;
        size_t sz = 0;
        // TODO: avoid reallocation ???
        readsize = SCNG(output_filter)((unsigned char **)&s, &sz, (unsigned char *)yytext, (size_t)yyleng);
        ZVAL_STRINGL(zendlval, s, sz);
        efree(s);
        if (readsize < yyleng) {
            yyless(readsize);
        }
    } else {
      ZVAL_STRINGL(zendlval, yytext, yyleng);
    }
    HANDLE_NEWLINES(yytext, yyleng);
    return T_INLINE_HTML;
}
#line 1178 "Zend/zend_language_scanner.c"
yy4:
    YYDEBUG(4, *YYCURSOR);
    yych = *++YYCURSOR;
    goto yy3;
yy5:
    YYDEBUG(5, *YYCURSOR);
    yyaccept = 0;
    yych = *(YYMARKER = ++YYCURSOR);
    if (yych <= 'O') {
        if (yych == '=') goto yy7;
    } else {
        if (yych <= 'P') goto yy9;
        if (yych == 'p') goto yy9;
    }
yy6:
    YYDEBUG(6, *YYCURSOR);
    yyleng = YYCURSOR - SCNG(yy_text);
#line 1751 "Zend/zend_language_scanner.l"
    {
    if (CG(short_tags)) {
        BEGIN(ST_IN_SCRIPTING);
        return T_OPEN_TAG;
    } else {
        goto inline_char_handler;
    }
}
#line 1205 "Zend/zend_language_scanner.c"
yy7:
    YYDEBUG(7, *YYCURSOR);
    ++YYCURSOR;
    YYDEBUG(8, *YYCURSOR);
    yyleng = YYCURSOR - SCNG(yy_text);
#line 1738 "Zend/zend_language_scanner.l"
    {
    BEGIN(ST_IN_SCRIPTING);
    return T_OPEN_TAG_WITH_ECHO;
}
#line 1216 "Zend/zend_language_scanner.c"
yy9:
    YYDEBUG(9, *YYCURSOR);
    yych = *++YYCURSOR;
    if (yych == 'H') goto yy11;
    if (yych == 'h') goto yy11;
yy10:
    YYDEBUG(10, *YYCURSOR);
    YYCURSOR = YYMARKER;
    goto yy6;
yy11:
    YYDEBUG(11, *YYCURSOR);
    yych = *++YYCURSOR;
    if (yych == 'P') goto yy12;
    if (yych != 'p') goto yy10;
yy12:
    YYDEBUG(12, *YYCURSOR);
    yych = *++YYCURSOR;
    if (yych <= '\f') {
        if (yych <= 0x08) goto yy10;
        if (yych >= '\v') goto yy10;
    } else {
        if (yych <= '\r') goto yy15;
        if (yych != ' ') goto yy10;
    }
yy13:
    YYDEBUG(13, *YYCURSOR);
    ++YYCURSOR;
yy14:
    YYDEBUG(14, *YYCURSOR);
    yyleng = YYCURSOR - SCNG(yy_text);
#line 1744 "Zend/zend_language_scanner.l"
    {
    HANDLE_NEWLINE(yytext[yyleng-1]);
    BEGIN(ST_IN_SCRIPTING);
    return T_OPEN_TAG;
}
#line 1253 "Zend/zend_language_scanner.c"
yy15:
    YYDEBUG(15, *YYCURSOR);
    ++YYCURSOR;
    if ((yych = *YYCURSOR) == '\n') goto yy13;
    goto yy14;
/* *********************************** */
yyc_ST_BACKQUOTE:
    {
        static const unsigned char yybm[] = {
              0,   0,   0,   0,   0,   0,   0,   0, 
              0,   0,   0,   0,   0,   0,   0,   0, 
              0,   0,   0,   0,   0,   0,   0,   0, 
              0,   0,   0,   0,   0,   0,   0,   0, 
              0,   0,   0,   0,   0,   0,   0,   0, 
              0,   0,   0,   0,   0,   0,   0,   0, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128,   0,   0,   0,   0,   0,   0, 
              0, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128,   0,   0,   0,   0, 128, 
              0, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128,   0,   0,   0,   0, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
            128, 128, 128, 128, 128, 128, 128, 128, 
        };
        YYDEBUG(16, *YYCURSOR);
        YYFILL(2);
        yych = *YYCURSOR;
        if (yych <= '_') {
            if (yych != '$') goto yy23;
        } else {
            if (yych <= '`') goto yy21;
            if (yych == '{') goto yy20;
            goto yy23;
        }
        YYDEBUG(18, *YYCURSOR);
        ++YYCURSOR;
        if ((yych = *YYCURSOR) <= '_') {
            if (yych <= '@') goto yy19;
            if (yych <= 'Z') goto yy26;
            if (yych >= '_') goto yy26;
        } else {
            if (yych <= 'z') {
                if (yych >= 'a') goto yy26;
            } else {
                if (yych <= '{') goto yy29;
                if (yych >= 0x7F) goto yy26;
            }
        }
yy19:
        YYDEBUG(19, *YYCURSOR);
        yyleng = YYCURSOR - SCNG(yy_text);
#line 2170 "Zend/zend_language_scanner.l"
        {
    if (YYCURSOR > YYLIMIT) {
        return 0;
    }
    if (yytext[0] == '\\' && YYCURSOR < YYLIMIT) {
        YYCURSOR++;
    }

    while (YYCURSOR < YYLIMIT) {
        switch (*YYCURSOR++) {
            case '`':
                break;
            case '$':
                if (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == '{') {
                    break;
                }
                continue;
            case '{':
                if (*YYCURSOR == '$') {
                    break;
                }
                continue;
            case '\\':
                if (YYCURSOR < YYLIMIT) {
                    YYCURSOR++;
                }
                /* fall through */
            default:
                continue;
        }

        YYCURSOR--;
        break;
    }

    yyleng = YYCURSOR - SCNG(yy_text);

    zend_scan_escape_string(zendlval, yytext, yyleng, '`');
    return T_ENCAPSED_AND_WHITESPACE;
}
#line 1364 "Zend/zend_language_scanner.c"
yy20:
        YYDEBUG(20, *YYCURSOR);
        yych = *++YYCURSOR;
        if (yych == '$') goto yy24;
        goto yy19;
yy21:
        YYDEBUG(21, *YYCURSOR);
        ++YYCURSOR;
        YYDEBUG(22, *YYCURSOR);
        yyleng = YYCURSOR - SCNG(yy_text);
#line 2114 "Zend/zend_language_scanner.l"
        {
    BEGIN(ST_IN_SCRIPTING);
    return '`';
}
#line 1380 "Zend/zend_language_scanner.c"
yy23:
        YYDEBUG(23, *YYCURSOR);
        yych = *++YYCURSOR;
        goto yy19;
yy24:
        YYDEBUG(24, *YYCURSOR);
        ++YYCURSOR;
        YYDEBUG(25, *YYCURSOR);
        yyleng = YYCURSOR - SCNG(yy_text);
#line 2101 "Zend/zend_language_scanner.l"
        {
    Z_LVAL_P(zendlval) = (zend_long) '{';
    yy_push_state(ST_IN_SCRIPTING);
    yyless(1);
    return T_CURLY_OPEN;
}
#line 1397 "Zend/zend_language_scanner.c"
yy26:
        YYDEBUG(26, *YYCURSOR);
        yyaccept = 0;
        YYMARKER = ++YYCURSOR;
        YYFILL(3);
        yych = *YYCURSOR;
        YYDEBUG(27, *YYCURSOR);
        if (yybm[0+yych] & 128) {
            goto yy26;
        }
        if (yych == '-') goto yy31;
        if (yych == '[') goto yy33;
yy28:
        YYDEBUG(28, *YYCURSOR);
        yyleng = YYCURSOR - SCNG(yy_text);
#line 1825 "Zend/zend_language_scanner.l"
        {
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    return T_VARIABLE;
}
#line 1418 "Zend/zend_language_scanner.c"
yy29:
        YYDEBUG(29, *YYCURSOR);
        ++YYCURSOR;
        YYDEBUG(30, *YYCURSOR);
        yyleng = YYCURSOR - SCNG(yy_text);
#line 1549 "Zend/zend_language_scanner.l"
        {
    yy_push_state(ST_LOOKING_FOR_VARNAME);
    return T_DOLLAR_OPEN_CURLY_BRACES;
}
#line 1429 "Zend/zend_language_scanner.c"
yy31:
        YYDEBUG(31, *YYCURSOR);
        yych = *++YYCURSOR;
        if (yych == '>') goto yy35;
yy32:
        YYDEBUG(32, *YYCURSOR);
        YYCURSOR = YYMARKER;
        goto yy28;
yy33:
        YYDEBUG(33, *YYCURSOR);
        ++YYCURSOR;
        YYDEBUG(34, *YYCURSOR);
        yyleng = YYCURSOR - SCNG(yy_text);
#line 1818 "Zend/zend_language_scanner.l"
        {
    yyless(yyleng - 1);
    yy_push_state(ST_VAR_OFFSET);
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    return T_VARIABLE;
}
#line 1450 "Zend/zend_language_scanner.c"
yy35:
        YYDEBUG(35, *YYCURSOR);
        yych = *++YYCURSOR;
        if (yych <= '_') {
            if (yych <= '@') goto yy32;
            if (yych <= 'Z') goto yy36;
            if (yych <= '^') goto yy32;
        } else {
            if (yych <= '`') goto yy32;
            if (yych <= 'z') goto yy36;
            if (yych <= '~') goto yy32;
        }
yy36:
        YYDEBUG(36, *YYCURSOR);
        ++YYCURSOR;
        YYDEBUG(37, *YYCURSOR);
        yyleng = YYCURSOR - SCNG(yy_text);
#line 1809 "Zend/zend_language_scanner.l"
        {
    yyless(yyleng - 3);
    yy_push_state(ST_LOOKING_FOR_PROPERTY);
    zend_copy_value(zendlval, (yytext+1), (yyleng-1));
    return T_VARIABLE;
}

Relevant Link:

http://bbs.chinaunix.net/thread-727747-1-1.html

 

14. 在Opcode層面進行語法還原WEBSHELL檢測

1. 詞法Token樹解析(opcodes)
2. 詞法規范化還原
    1) 賦值傳遞
    2) API函數執行
3. opcodes -> sourcecode
4. 正則規則檢查

需要的相關信息

1. filename: zend_op_array->filename
2. opcode名稱: opcodes[op.opcode].name
3. 表達式計算結果: opcodes[op.opcode].result
4. 參數1: opcodes[op.opcode].op1_type, opcodes[op.opcode].op1
5. 參數2: opcodes[op.opcode].op2_type, opcodes[op.opcode].op2

但是這種方案也存在一個問題,Zend把PHP用戶態源代碼翻譯為Opcode匯編源代碼之后,用戶態語法層面的特征已經被極大地弱化了,例如if、foreach這類語法會翻譯為了if/goto這種形態的匯編模式
隨之而來的,如果要在Opcode匯編層面進行代碼優化、還原、甚至是Opcode反轉用戶態代碼,都是十分困難的

 

Copyright (c) 2015 LittleHann All rights reserved

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM