php Zend虛擬機


在前⾯的章節中,我們了解到⼀個PHP⽂件在服務器端的執⾏過程包括以下兩個⼤的過程:
1. 遞給php程序需要執⾏的⽂件, php程序完成基本的准備⼯作后啟動PHP及Zend引擎, 加載注冊的擴展模塊。
2. 初始化完成后讀取腳本⽂件,Zend引擎對腳本⽂件進⾏詞法分析,語法分析。然后編譯成opcode執⾏。 如果安裝了apc之類的opcode緩存, 編譯環節可能會被跳過⽽直接從緩存中讀取opcode執⾏。

在第⼆步中,詞法分析、語法分析,編譯中間代碼,執⾏中間代碼等各個部分統稱為Zend虛擬機。 與Java、C#等編譯型語⾔相⽐,PHP少了⼀個⼿動編譯的過程,它們⽆需編譯即可運⾏,我們稱其為解釋性語⾔。 Java有⾃⼰的Java虛擬機,它在多個平台上實現統⼀語⾔; C#有⾃⼰的.NET虛擬機,它在單⼀平
台實現多種語⾔; PHP跟他們⼀樣,也有屬於⾃⼰的Zend虛擬機。它們在本質是相同的,它們都是抽象的計算機。 這些虛擬機都是在某種較底層的語⾔上抽象出另外⼀種語⾔,有⾃⼰的指令集,有⾃⼰的內存管理體系。 它們最終都會將抽象級別較⾼的語⾔實現轉化為抽象級別較低的語⾔實現, 並且實現其它輔助功
能,如內存管理,垃圾回收等機制, 以減少程序員在具體實現上的⼯作,從⽽可以將更多的時間和精⼒投⼊到業務邏輯中。 從抽象層次看,Zend虛擬機⽐Java等語⾔更⾼級⼀些,這⾥的⾼級不是說功能更強⼤或效率更⾼, 簡單點說,Zend虛擬機離真正的機器實現更遠⼀些。 最近這些年,語⾔的發展只是不斷的抽象,不斷的遠離機器,沒有根本性的變化。

本章,我們從虛擬機的前世今⽣講起,敘述Zend虛擬機的實現原理,關鍵的數據結構, 並其中穿插⼀個關於語法實現的⽰例和源碼加密解密的過程說明。

第⼀節 Zend虛擬機概述

在wiki中虛擬機的定義是: 虛擬機(Virtual Machine),在計算機科學中的體系結構⾥,是指⼀種特殊的軟件, 他可以在計算機平台和終端⽤戶之間創建⼀種環境,⽽終端⽤戶則是基於這個軟件所創建的環境來操作軟件。 在計算機科學中,虛擬機是指可以像真實機器⼀樣運⾏程序的計算機的軟件實現。

虛擬機是⼀種抽象的計算機,它有⾃⼰的指令集,有⾃⼰的內存管理體系。 在此類虛擬機上實現的語⾔⽐較低抽象層次的語⾔更加明了,更加簡單易學。

Zend虛擬機核⼼實現代碼

為了⽅便讀者對Zend引擎的實現有個全⾯的感覺,下⾯列出涉及到Zend引擎實現的核⼼代碼⽂件功能參考。

Zend引擎的核⼼⽂件都在$PHP_SRC/Zend/⽬錄下⾯。不過最為核⼼的⽂件只有如下⼏個:

1. PHP語法實現

  • Zend/zend_language_scanner.l
  • Zend/zend_language_parser.y

2. Opcode編譯

  • Zend/zend_compile.c

3. 執⾏引擎

  • Zend/zend_vm_*
  • Zend/zend_execute.c

Zend虛擬機體系結構

從概念層將Zend虛擬機的實現進⾏抽象,我們可以將Zend虛擬機的體系結構分為:解釋層、執⾏引擎、中間數據層,如圖7.1所⽰:

圖7.1 Zend虛擬機體系結構圖

當⼀段PHP代碼進⼊Zend虛擬機,它會被執⾏兩步操作:編譯和執⾏。 對於⼀個解釋性語⾔來說,這是⼀個創造性的舉動,但是,現在的實現並不徹底。 現在當PHP代碼進⼊Zend虛擬機后,它雖然會被執⾏這兩步操作,但是這兩步操作對於⼀個常規的執⾏過程來說卻是連續的, 也就是說它並沒有轉變成和Java
這種編譯型語⾔⼀樣:⽣成⼀個中間⽂件存放編譯后的結果。 如果每次執⾏這樣的操作,對於PHP腳本的性能來說是⼀個極⼤的損失雖然有類似於APC,eAccelerator等緩存解決⽅案。但是其本質上是沒有變化的,並且不能將兩個步驟分離,各⾃發展壯⼤

解釋層

解釋層是Zend虛擬機執⾏編譯過程的位置。它包括詞法解析、語法解析和編譯⽣成中間代碼三個部分。 詞法分析就是將我們要執⾏的PHP源⽂件,去掉空格,去掉注釋,切分為⼀個個的標記(token), 並且處理程序的層級結構(hierarchical structure)。

語法分析就是將接受的標記(token)序列,根據定義的語法規則,來執⾏⼀些動作,Zend虛擬機現在使⽤的Bison使⽤巴科斯范式(BNF)來描述語法。 編譯⽣成中間代碼是根據語法解析的結果對照Zend虛擬機制定的opcode⽣成中間代碼, 在PHP5.3.1中,Zend虛擬機⽀持135條指令(見
Zend/zend_vm_opcodes.h⽂件), ⽆論是簡單的輸出語句還是程序復雜的遞歸調⽤,Zend虛擬機最終都會將所有我們編寫的PHP代碼轉化成這135條指令的序列, 之后在執⾏引擎中按順序執⾏

中間數據層

當Zend虛擬機執⾏⼀個PHP代碼時,它需要內存來存儲許多東⻄, ⽐如,中間代碼,PHP⾃帶的函數列表,⽤戶定義的函數列表,PHP⾃帶的類,⽤戶⾃定義的類, 常量,程序創建的對象,傳遞給函數或⽅法的參數,返回值,局部變量以及⼀些運算的中間結果等。 我們把這些所有的存放數據的地⽅稱為中間數據層

如果PHP以mod擴展的⽅式依附於Apache2服務器運⾏,中間數據層的部分數據可能會被多個線程共享,如果PHP⾃帶的函數列表等。 如果只考慮單個進程的⽅式,當⼀個進程被創建時它就會被加載PHP⾃帶的各種函數列表,類列表,常量列表等。 當解釋層將PHP代碼編譯完成后,各種⽤戶⾃定義的函數,類
或常量會添加到之前的列表中, 只是這些函數在其⾃⾝的結構中某些字段的賦值是不⼀樣的。

當執⾏引擎執⾏⽣成的中間代碼時,會在Zend虛擬機的棧中添加⼀個新的執⾏中間數據結構(zend_execute_data), 它包括當前執⾏過程的活動符號列表的快照、⼀些局部變量等

執⾏引擎

Zend虛擬機的執⾏引擎是⼀個⾮常簡單的實現,它只是依據中間代碼序列(EX(opline)),⼀步⼀步調⽤對應的⽅法執⾏。 在執⾏引擎中沒並有類似於PC寄存器⼀樣的變量存放下⼀條指令,當Zend虛擬機執⾏到某條指令時,當它所有的任務都執⾏完了, 這條指令會⾃⼰調⽤下⼀條指令,即將序列的指針向前移動⼀個位置,從⽽執⾏下⼀條指令,並且在最后執⾏return語句,如此反復。 這在本質上是⼀個函數嵌套調⽤。 

回到開頭的問題,PHP通過詞法分析、語法分析和中間代碼⽣成三個步驟后,PHP⽂件就會被解析成PHP的中間代碼opcode。 ⽣成的中間代碼與實際的PHP代碼之間並沒有完全的⼀⼀對應關系。只是針對⽤戶所給的PHP代碼和PHP的語法規則和⼀些內部約定⽣成中間代碼, 並且這些中間代碼還需要依靠⼀些全局變量中轉數據和關聯。⾄於⽣成的中間代碼的執⾏過程是依據中間代碼的順利, 依賴於執⾏過程中的全局變量,⼀步步執⾏。當然,在遇到⼀些函數跳轉也會發⽣偏移,但是最終還是會回到偏移點。

第⼆節 語法的實現

世上沒有⽆緣⽆故的愛,也沒有⽆緣⽆故的恨。

語⾔從⼴義上來講是⼈們進⾏溝通交流的各種表達符號。每種語⾔都有專屬於⾃⼰的符號,表達⽅式和規則。 就編程語⾔來說,它也是由特定的符號,特定的表達⽅式和規則組成。 語⾔的作⽤是溝通,不管是⾃然語⾔,還是編程語⾔,它們的區別在於⾃然語⾔是⼈與⼈之間溝通的⼯具, ⽽編程語⾔是⼈與機器
之間的溝通渠道。相對於⾃然語⾔,編程語⾔的歷史還⾮常短, 雖然編程語⾔是站在歷史巨⼈的基礎上創建的,但是它還很⼩,還是⼀個⼩孩。 它只能按編程⼈員所給的指令翻譯成對應的機器可以識別的語⾔。它就相當於⼀個轉化⼯具, 將⼈們的知識或者業務邏輯轉化成機器碼(機器的語⾔),讓其執⾏對應的的
操作。 ⽽這些指令是⼀些規則,⼀些約定,這些規則約定都是由編程語⾔來處理。 

就PHP語⾔來說,它也是⼀組符合⼀定規則的約定的指令。 在編程⼈員將⾃⼰的想法以PHP語⾔實現后,通過PHP的虛擬機將這些PHP指令轉變成C語⾔ (可以理解為更底層的⼀種指令集)指令,⽽C語⾔⼜會轉變成匯編語⾔, 最后匯編語⾔將根據處理器的規則轉變成機器碼執⾏。這是⼀個更⾼層次抽象的不斷具體化,不斷細化的過程。

在這⼀章,我們討論PHP虛擬機是如何將PHP語⾔轉化成C語⾔。 從⼀種語⾔到另⼀種語⾔的轉化稱之為編譯,這兩種語⾔分別可以稱之為源語⾔和⽬標語⾔。 這種編譯過程通過發⽣在⽬標語⾔⽐源語⾔更低級(或者說更底層)。 語⾔轉化的編譯過程是由編譯器來完成, 編碼器通常被分為⼀系列的過程:詞法分析、語法分析、語義分析、中間代碼⽣成、代碼優化、⽬標代碼⽣成等。 前⾯⼏個階段(詞法分析、語法分析和語義分析)的作⽤是分析源程序,我們可以稱之為編譯器的前端。 后⾯的⼏個階段(中間代碼⽣成、代碼優化和⽬標代碼⽣成)的作⽤是構造⽬標程序,我們可以稱之為編譯器的后端。 ⼀種語⾔被稱為
編譯類語⾔,⼀般是由於在程序執⾏之前有⼀個翻譯的過程, 其中關鍵點是有⼀個形式上完全不同的等價程序⽣成。 ⽽PHP之所以被稱為解釋類語⾔,就是因為並沒有這樣的⼀個程序⽣成, 它⽣成的是中間代碼,這只是PHP的⼀種內部數據結構

在本章我們會介紹PHP編譯器的前端的兩個階段,語法分析、語法分析;后端的⼀個階段,中間代碼⽣成。 在第⼀節我們介紹PHP的詞法分析過程及其⽤到的⼯具re2c, 第⼆節我們介紹在詞法分析后的語法分析過程, 第三節我們以PHP的⼀個簡單語法實現作為本章的結束。

詞法解析

在前⾯我們提到語⾔轉化的編譯過程⼀般分為詞法分析、語法分析、語義分析、中間代碼⽣成、代碼優化、⽬標代碼⽣成等六個階段。 不管是編譯型語⾔還是解釋型語⾔,掃描(詞法分析)總是將程序轉化成⽬標語⾔的第⼀步。 詞法分析的作⽤就是將整個源程序分解成⼀個⼀個的單詞, 這樣做可以在⼀定程度
上減少后⾯分析⼯作需要處理的個體數量,為語法分析等做准備。 除了拆分⼯作,更多的時候它還承擔着清洗源程序的過程,⽐如清除空格,清除注釋等。 詞法分析作為編譯過程的第⼀步,在業界已經有多種成熟⼯具,如PHP在開始使⽤的是Flex,之后改為re2c, MySQL的詞法分析使⽤的Flex,除此之外還有作為
UNIX系統標准詞法分析器的Lex等。 這些⼯具都會讀進⼀個代表詞法分析器規則的輸⼊字符串流,然后輸出以C語⾔實做的詞法分析器源代碼。 這⾥我們只介紹PHP的現版詞法分析器,re2c。 

re2c是⼀個掃描器制作⼯具,可以創建⾮常快速靈活的掃描器。 它可以產⽣⾼效代碼,基於C語⾔,可以⽀持C/C++代碼。與其它類似的掃描器不同, 它偏重於為正則表達式產⽣⾼效代碼(和他的名字⼀樣)。因此,這⽐傳統的詞法分析器有更⼴泛的應⽤范圍。 你可以在sourceforge.net獲取源碼。

PHP在最開始的詞法解析器是使⽤的是Flex,后來改為使⽤re2c。 在源碼⽬錄下的Zend/zend_language_scanner.l ⽂件是re2c的規則⽂件, 如果需要修改該規則⽂件需要安裝re2c才能重新編譯,⽣成新的規則⽂件。

re2c調⽤⽅式:

re2c [-bdefFghisuvVw1] [-o output] [-c [-t header]] file

我們通過⼀個簡單的例⼦來看下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;
}

如果你是在ubuntu環境下,可以執⾏下⾯的命令⽣成可執⾏⽂件。

re2c -o a.c a.l
gcc a.c -o a
chmod +x a
./a 1000

此時程序會輸出number。

我們解釋⼀下我們⽤到的⼏個re2c約定的宏。

  • YYCTYPE ⽤於保存輸⼊符號的類型,通常為char型和unsigned char型
  • YYCURSOR 指向當前輸⼊標記, -當開始時,它指向當前標記的第⼀個字符,當結束時,它指向下⼀個標記的第⼀個字符
  • YYFILL(n) 當⽣成的代碼需要重新加載緩存的標記時,則會調⽤YYFILL(n)。
  • YYLIMIT 緩存的最后⼀個字符,⽣成的代碼會反復⽐較YYCURSOR和YYLIMIT,以確定是否需要重新填充緩沖區。

參照如上⼏個標識的說明,可以較清楚的理解⽣成的a.c⽂件,當然,re2c不會僅僅只有上⾯代碼所顯⽰的標記, 這只是⼀個簡單⽰例,更多的標識說明和幫助信息請移步 re2c幫助⽂檔http://re2c.org/manual.html

我們回過頭來看PHP的詞法規則⽂件zend_language_scanner.l。 你會發現前⾯的簡單⽰例與它最⼤的區別在於每個規則前⾯都會有⼀個條件表達式。

NOTE re2c中條件表達式相關的宏為YYSETCONDITION和YYGETCONDITION,分別表⽰設置條件范圍和獲取條件范圍。 在PHP的詞法規則中共有10種,其全部在zend_language_scanner_def.h⽂件中。此⽂件並⾮⼿寫, ⽽是re2c⾃動⽣成的。如果需要⽣成和使⽤條件表達式,在編譯成c時需要添加-c 和-t參數。

在PHP的詞法解析中,它有⼀個全局變量:language_scanner_globals,此變量為⼀結構體,記錄當前re2c解析的狀態,⽂件信息,解析過程信息等。 它在zend_language_scanner.l⽂件中直接定義如下:

#ifdef ZTS
ZEND_API ts_rsrc_id language_scanner_globals_id;
#else
ZEND_API zend_php_scanner_globals language_scanner_globals;
#endif

在zend_language_scanner.l⽂件中寫的C代碼在使⽤re2c⽣成C代碼時會直接復制到新⽣成的C代碼⽂件中。 這個變量貫穿了PHP詞法解析的全過程,並且⼀些re2c的實現也依賴於此, ⽐如前⾯說到的條件表達式的存儲及獲取,就需要此變量的協助,我們看這兩個宏在PHP詞法中的定義:

// 存在於zend_language_scanner.l⽂件中
#define YYGETCONDITION() SCNG(yy_state)
#define YYSETCONDITION(s) SCNG(yy_state) = s
#define SCNG LANG_SCNG
// 存在於zend_globals_macros.h⽂件中
# define LANG_SCNG(v) (language_scanner_globals.v)

結合前⾯的全局變量和條件表達式宏的定義,我們可以知道PHP的詞法解析是通過全局變量在⼀次解析過程中存在。 那么這個條件表達式具體是怎么使⽤的呢?我們看下⾯⼀個例⼦。這是⼀個可以識別為結束, 識別字符,數字等的簡單字符串識別器。它使⽤了re2c的條件表達式,代碼如下:

#include <stdio.h>
#include "demo_def.h"
#include "demo.h"
Scanner scanner_globals;
#define YYCTYPE char
#define YYFILL(n)
#define STATE(name) yyc##name
#define BEGIN(state) YYSETCONDITION(STATE(state))
#define LANG_SCNG(v) (scanner_globals.v)
#define SCNG LANG_SCNG
#define YYGETCONDITION() SCNG(yy_state)
#define YYSETCONDITION(s) SCNG(yy_state) = s
#define YYCURSOR SCNG(yy_cursor)
#define YYLIMIT SCNG(yy_limit)
#define YYMARKER SCNG(yy_marker)
int scan(){
    /*!re2c
    <INITIAL>"<?php" {BEGIN(ST_IN_SCRIPTING); return T_BEGIN;}
    <ST_IN_SCRIPTING>[0-9]+ {return T_NUMBER;}
    <ST_IN_SCRIPTING>[ \n\t\r]+ {return T_WHITESPACE;}
    <ST_IN_SCRIPTING>"exit" { return T_EXIT; }
    <ST_IN_SCRIPTING>[a-z]+ {return T_LOWER_CHAR;}
    <ST_IN_SCRIPTING>[A-Z]+ {return T_UPPER_CHAR;}
    <ST_IN_SCRIPTING>"?>" {return T_END;}
    <ST_IN_SCRIPTING>[^] {return T_UNKNOWN;}
    <*>[^] {return T_INPUT_ERROR;}
    */
}
void print_token(int token) {
    switch (token) {
        case T_BEGIN: printf("%s\n", "begin");break;
        case T_NUMBER: printf("%s\n", "number");break;
        case T_LOWER_CHAR: printf("%s\n", "lower char");break;
        case T_UPPER_CHAR: printf("%s\n", "upper char");break;
        case T_EXIT: printf("%s\n", "exit");break;
        case T_UNKNOWN: printf("%s\n", "unknown");break;
        case T_INPUT_ERROR: printf("%s\n", "input error");break;
        case T_END: printf("%s\n", "end");break;
    }
}
int main(int argc, char* argv[])
{
    int token;
    BEGIN(INITIAL); // 全局初始化,需要放在scan調⽤之前
    scanner_globals.yy_cursor = argv[1]; //將輸⼊的第⼀個參數作為要解析的字符串
    while(token = scan()) {
        if (token == T_INPUT_ERROR) {
            printf("%s\n", "input error");
            break;
        }
        if (token == T_END) {
            printf("%s\n", "end");
            break;
        }
        print_token(token);
    }
    return 0;
}

和前⾯的簡單⽰例⼀樣,如果你是在linux環境下,可以使⽤如下命令⽣成可執⾏⽂件

re2c -o demo.c -c -t demo_def.h demo.l
gcc demo.c -o demo -g
chmod +x demo

在使⽤re2c⽣成C代碼時我們使⽤了-c -t demo_def.h參數,這表⽰我們使⽤了條件表達式模式,⽣成條件的定義頭⽂件。 main函數中,在調⽤scan函數之前我們需要初始化條件狀態,將其設置為INITIAL狀態。 然后在掃描過程中會直接識別出INITIAL狀態,然后匹配<?php字符串識別為開始,如果開始不為
<?php,則輸出input error。 在掃描的正常流程中,當掃描出<?php后,while循環繼續向下⾛,此時會再次調⽤scan函數,當前條件狀態為ST_IN_SCRIPTING, 此時會跳過INITIAL狀態,直接匹配<ST_IN_SCRIPTING>狀態后的規則。如果所有的<ST_IN_SCRIPTING>后的規則都⽆法匹配,輸出unkwon。 這只是⼀個簡單的識別⽰例,但是它是從PHP的詞法掃描器中抽離出來的,其實現過程和原理類似。

那么這種條件狀態是如何實現的呢?我們查看demo.c⽂件,發現在scan函數開始后有⼀個跳轉語句:

int scan(){
#line 25 "demo.c"
{
YYCTYPE yych;
    switch (YYGETCONDITION()) {
        case yycINITIAL: goto yyc_INITIAL;
        case yycST_IN_SCRIPTING: goto yyc_ST_IN_SCRIPTING;
    }
    ...
}

在zend_language_scanner.c⽂件的lex_scan函數中也有類型的跳轉過程,只是過程相對這⾥來說if語句多⼀些,復雜⼀些。 這就是re2c條件表達式的實現原理。

語法分析

Bison是⼀種通⽤⽬的的分析器⽣成器。它將LALR(1)上下⽂⽆關⽂法的描述轉化成分析該⽂法的C程序。 使⽤它可以⽣成解釋器,編譯器,協議實現等多種程序。 Bison向上兼容Yacc,所有書寫正確的Yacc語法都應該可以不加修改地在Bison下⼯作。 它不但與Yacc兼容還具有許多Yacc不具備的特性。

Bison分析器⽂件是定義了名為yyparse並且實現了某個語法的函數的C代碼。 這個函數並不是⼀個可以完成所有的語法分析任務的C程序。 除此這外我們還必須提供額外的⼀些函數: 如詞法分析器、分析器報告錯誤時調⽤的錯誤報告函數等等。 我們知道⼀個完整的C程序必須以名為main的函數開頭,如果我們要⽣成⼀個可執⾏⽂件,並且要運⾏語法解析器, 那么我們就需要有main函數,並且在某個地⽅直接或間接調⽤yyparse,否則語法分析器永遠都不會運⾏。

先看下bison的⽰例:逆波蘭記號計算器

%{
#define YYSTYPE double
#include <stdio.h>
#include <math.h>
#include <ctype.h>
int yylex (void);
void yyerror (char const *);
%}
%token NUM
%%
input: /* empty */
    | input line
    ;
line: '\n'
    | exp '\n' { printf ("\t%.10g\n", $1); }
;
exp: NUM { $$ = $1; }
    | exp exp '+' { $$ = $1 + $2; }
    | exp exp '-' { $$ = $1 - $2; }
    | exp exp '*' { $$ = $1 * $2; }
    | exp exp '/' { $$ = $1 / $2; }
    /* Exponentiation */
    | exp exp '^' { $$ = pow($1, $2); }
    /* Unary minus */
    | exp 'n' { $$ = -$1; }
; %%

#include
<ctype.h> int yylex (void) { int c; /* Skip white space. */ while ((c = getchar ()) == ' ' || c == '\t') ; /* Process numbers. */ if (c == '.' || isdigit (c)) { ungetc (c, stdin); scanf ("%lf", &yylval); return NUM; } /* Return end-of-input. */ if (c == EOF) return 0; /* Return a single char. */ return c; } void yyerror (char const *s) { fprintf (stderr, "%s\n", s); } int main (void) { return yyparse (); }

我們先看下運⾏的效果:

bison demo.y
gcc -o test -lm test.tab.c
chmod +x test
./test

gcc命令需要添加-lm參數。因為頭⽂件僅對接⼝進⾏描述,但頭⽂件不是負責進⾏符號解析的實體。此時需要告訴編譯器應該使⽤哪個函數庫來完成對符號的解析。 GCC的命令參數中,-l參數就是⽤來指定程序要鏈接的庫,-l參數緊接着就是庫名,這⾥我們在-l后⾯接的是m,即數學庫,他的庫名是m,他的庫⽂件名是libm.so。

這是⼀個逆波蘭記號計算器的⽰例,在命令⾏中輸⼊ 3 7 + 回車,輸出10。

⼀般來說,使⽤Bison設計語⾔的流程,從語法描述到編寫⼀個編譯器或者解釋器,有三個步驟:

  • 以Bison可識別的格式正式地描述語法。對每⼀個語法規則,描述當這個規則被識別時相應的執⾏動作,動作由C語句序列。即我們在⽰例中看到的%%和%%這間的內容。
  • 描述編寫⼀個詞法分析器處理輸⼊並將記號傳遞給語法分析器(即yylex函數⼀定要存在)。詞法分析器既可是⼿⼯編寫的C代碼, 也可以由lex產⽣,后⾯我們會討論如何將re2c與bison結合使⽤。上⾯的⽰例中是直接⼿⼯編寫C代碼實現⼀個命令⾏讀取內容的詞法分析器。
  • 編寫⼀個調⽤Bison產⽣的分析器的控制函數,在⽰例中是main函數直接調⽤。編寫錯誤報告函數(即yyerror函數)。

將這些源代碼轉換成可執⾏程序,需要按以下步驟進⾏:

  • 按語法運⾏Bison產⽣分析器。對應⽰例中的命令,bison demo.y
  • 同其它源代碼⼀樣編譯Bison輸出的代碼,鏈接⽬標⽂件以產⽣最終的產品。即對應⽰例中的命令gcc -o test -lm test.tab.c

我們可以將整個Bison語法⽂件划分為四個部分。 這三個部分的划分通過%%',%{' 和`%}'符號實現。⼀般來說,Bison語法⽂件結構如下:

%{ 這⾥可以⽤來定義在動作中使⽤類型和變量,或者使⽤預處理器命令在那⾥來定義宏, 或者使⽤
#include包含需要的⽂件。
如在⽰例中我們聲明了YYSTYPE,包含了頭⽂件math.h等,還聲明了詞法分析器yylex和錯誤打印程序yyerror。
%}
Bison 的⼀些聲明
在這⾥聲明終結符和⾮終結符以及操作符的優先級和各種符號語義值的各種類型
如⽰例中的%token NUM。我們在PHP的源碼中可以看到更多的類型和符號聲明,如%left,%right的使⽤
%%
在這⾥定義如何從每⼀個⾮終結符的部分構建其整體的語法規則。
%%
這⾥存放附加的內容
這⾥就⽐較⾃由了,你可以放任何你想放的代碼。
在開始聲明的函數,如yylex等,經常是在這⾥實現的,我們的⽰例就是這么搞的。

 

我們在前⾯介紹了PHP是使⽤re2c作為詞法分析器,那么PHP是如何將re2c與bison集成在⼀起的呢?我們以⼀個從PHP源碼中剝離出來的⽰例來說明整個過程。這個⽰例的功能與上⼀⼩節的⽰例類似,作⽤都是識別輸⼊參數中的字符串類型。 本⽰例是在其基礎上添加了語法解析過程。 ⾸先我們看這個⽰例的語
法⽂件:demo.y。

%{
#include <stdio.h>
#include "demo_scanner.h"
extern int yylex(znode *zendlval);
void yyerror(char const *);
#define YYSTYPE znode //關鍵點⼀,znode定義在demo_scanner.h
%}
%pure_parser // 關鍵點⼆
%token T_BEGIN
%token T_NUMBER
%token T_LOWER_CHAR
%token T_UPPER_CHAR
%token T_EXIT
%token T_UNKNOWN
%token T_INPUT_ERROR
%token T_END
%token T_WHITESPACE

%%
begin: T_BEGIN {printf("begin:\ntoken=%d\n", $1.op_type);} | begin variable { printf("token=%d ", $2.op_type); if ($2.constant.value.str.len > 0) { printf("text=%s", $2.constant.value.str.val); } printf("\n"); } variable: T_NUMBER {$$ = $1;} |T_LOWER_CHAR {$$ = $1;} |T_UPPER_CHAR {$$ = $1;} |T_EXIT {$$ = $1;} |T_UNKNOWN {$$ = $1;} |T_INPUT_ERROR {$$ = $1;} |T_END {$$ = $1;} |T_WHITESPACE {$$ = $1;} %%
void
yyerror(char const *s) { printf("%s\n", s); }

這個語法⽂件有兩個關鍵點:

214頁。。。

 


免責聲明!

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



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