catalogphp
0. 引論 1. 構建一個編譯器的相關科學 3. 程序設計語言基礎 4. 一個簡單的語法制導翻譯器 5. 簡單表達式的翻譯器(源代碼示例) 6. 詞法分析 7. 生成中間代碼 8. 詞法分析器的實現 9. 詞法分析器生成工具Lex 10. PHP Lex(Lexical Analyzer) 11. 語法分析 12. 構造可配置詞法語法分析器生成器 13. 基於PHP Lexer重寫一份輕量級詞法分析器 14. 在Opcode層面進行語法還原WEBSHELL檢測
0. 引論html
在全部計算機上運行的全部軟件都是用某種程序設計語言編寫的,可是在一個程序能夠運行以前,它首先須要被翻譯成一種可以被計算機執行的形式,完成這項翻譯工做的軟件系統稱爲編譯器(compiler)前端
0x1: 語言處理器java
1. 編譯器 簡單地說,一個編譯器就是一個程序,它能夠閱讀以某一種語言(源語言)編寫的程序,並把該程序翻譯成一個等價、用另外一種語言(目標語言)編寫的程序,編譯器的重要任務之一是報告它在翻譯過程當中發現的原程序中的錯誤 2. 解釋器 解釋器(interpreter)是另外一種常見的語言處理器,它並不經過翻譯的方式生成目標程序,解釋器直接利用用戶提供的輸入執行源程序中指定的操做 //在把用於輸入映射成爲輸出的過程當中,由一個編譯器產生的機器語言目標程序一般比一個解釋器要快不少,然而,解釋器的錯誤診斷效果一般比編譯器更好,由於它逐個語句地執行源程序
java語言處理器結合了編譯和解釋過程,一個java源程序首先被編譯成一個稱爲字節碼(bytecode)的中間表示形式,而後由一個虛擬機對獲得的字節碼加以解釋執行,這樣設計的好處之一是在一臺機器上編譯獲得的字節碼能夠在另外一臺機器上解釋執行,經過網絡就能夠完成機器之間的遷移
爲了更快地完成輸入到輸出的處理,有些被稱爲即時(just in time)編譯器的java編譯器在運行中間程序處理輸入的前一刻先把字節碼翻譯成爲機器語言,而後再執行程序node
0x2: 一個編譯器的結構mysql
編譯器可以把源程序映射爲在語義上等價的目標程序,這個映射過程由兩個部分組成: 分析部分、綜合部分ios
1. 分析部分(編譯器的前端 front end) 1) 分析(analysis)部分把源程序分解成多個組成要素,並在這些要素之上加上語法結構 2) 而後,它使用這個結構來建立該源程序的一箇中間表示,若是分析部分檢查出源程序沒有按照正確的語法構成,或者語義上不一致,它就必須提供有用的信息,使得用戶能夠按此建議進行改正 3) 分析部分還會收集有關源程序的信息,並把信息存放在一個稱爲符號表(symbol table)的數據結構中(調用其餘obj中的函數就須要用到符號表),符號表和中間表示形式一塊兒傳送給綜合部分 2. 綜合部分(編譯器的後端 back end) 1) 綜合(synthesis)部分根據中間表示和符號表中的信息來構造用戶期待的目標程序
若是咱們更加詳細地研究編譯過程,會發現它順序執行了一組步驟(phase),每一個步驟把源程序的一種表現形式轉換爲另外一種表現形式,在實踐中,多個步驟可能被組合在一塊兒,而這些組合在一塊兒的步驟之間的中間表示不須要被明確地構造出來,存放整個源程序的信息的符號表可由編譯器的各個步驟使用
有些編譯器在前端和後端之間有一個與機器無關的優化步驟,這個優化步驟的目的是在中間表示之上進行轉換,以便後端程序可以生成更好的目標程序git
1. 詞法分析程序員
編譯器的第一個步驟稱爲詞法分析(lexical analysis)或掃描(scanning),詞法分析器讀入組成源程序的字符流,而且將它們組織成爲有意義的詞素(lexeme)的序列,對於每一個詞素,詞法分析器產生以下形式的詞法單元(token)做爲輸出github
<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是某個內部結點的標號,而且它的子結點的標號從左至右分別爲X一、X二、...、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後面預讀一個字符來區別一、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的翻譯只依賴於對E一、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