### 3.1.2 抽象語法樹編譯流程
上一小節我們簡單介紹了從PHP代碼解析為抽象語法樹的過程,這一節我們再介紹下從 __抽象語法樹->Opcodes__ 的過程。
語法解析過程的產物保存于CG(AST),接著zend引擎會把AST進一步編譯為 __zend_op_array__ ,它是編譯階段最終的產物,也是執行階段的輸入,后面我們介紹的東西基本都是圍繞zend_op_array展開的,AST解析過程確定了當前腳本定義了哪些變量,并為這些變量 __順序編號__ ,這些值在使用時都是按照這個編號獲取的,另外也將變量的初始化值、調用的函數/類/常量名稱等值(稱之為字面量)保存到zend_op_array.literals中,這些字面量也有一個唯一的編號,所以執行的過程實際就是根據各指令調用不同的C函數,然后根據變量、字面量、臨時變量的編號對這些值進行處理加工。
我們首先看下zend_op_array的結構,明確幾個關鍵信息,然后再看下ast編譯為zend_op_array的過程。
#### 3.1.2.1 zend_op_array數據結構
PHP主腳本會生成一個zend_op_array,每個function也會編譯為獨立的zend_op_array,所以從二進制程序的角度看zend_op_array包含著當前作用域下的所有堆棧信息,函數調用實際就是不同zend_op_array間的切換。

```c
struct _zend_op_array {
//common是普通函數或類成員方法對應的opcodes快速訪問時使用的字段,后面分析PHP函數實現的時候會詳細講
...
uint32_t *refcount;
uint32_t this_var;
uint32_t last;
//opcode指令數組
zend_op *opcodes;
//PHP代碼里定義的變量數:op_type為IS_CV的變量,不含IS_TMP_VAR、IS_VAR的
//編譯前此值為0,然后發現一個新變量這個值就加1
int last_var;
//臨時變量數:op_type為IS_TMP_VAR、IS_VAR的變量
uint32_t T;
//PHP變量名數組
zend_string **vars; //這個數組在ast編譯期間配合last_var用來確定各個變量的編號,非常重要的一步操作
...
//靜態變量符號表:通過static聲明的
HashTable *static_variables;
...
//字面量數量
int last_literal;
//字面量(常量)數組,這些都是在PHP代碼定義的一些值
zval *literals;
//運行時緩存數組大小
int cache_size;
//運行時緩存,主要用于緩存一些znode_op以便于快速獲取數據,后面單獨介紹這個機制
void **run_time_cache;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};
```
zend_op_array.opcodes指向指令列表,具體每條指令的結構如下:
```c
struct _zend_op {
const void *handler; //指令執行handler
znode_op op1; //操作數1
znode_op op2; //操作數2
znode_op result; //返回值
uint32_t extended_value;
uint32_t lineno;
zend_uchar opcode; //opcode指令
zend_uchar op1_type; //操作數1類型
zend_uchar op2_type; //操作數2類型
zend_uchar result_type; //返回值類型
};
//操作數結構
typedef union _znode_op {
uint32_t constant;
uint32_t var;
uint32_t num;
uint32_t opline_num; /* Needs to be signed */
uint32_t jmp_offset;
} znode_op;
```
opcode各字段含義下面展開說明。
##### 3.1.2.1.1 handler
handler為每條opcode對應的C語言編寫的 __處理過程__ ,所有opcode對應的處理過程定義在`zend_vm_def.h`中,值得注意的是這個文件并不是編譯時用到的,因為opcode的 __處理過程__ 有三種不同的提供形式:CALL、SWITCH、GOTO,默認方式為CALL,這個是什么意思呢?
每個opcode都代表了一些特定的處理操作,這個東西怎么提供呢?一種是把每種opcode負責的工作封裝成一個function,然后執行器循環執行即可,這就是CALL模式的工作方式;另外一種是把所有opcode的處理方式通過C語言里面的label標簽區分開,然后執行器執行的時候goto到相應的位置處理,這就是GOTO模式的工作方式;最后還有一種方式是把所有的處理方式寫到一個switch下,然后通過case不同的opcode執行具體的操作,這就是SWITCH模式的工作方式。
假設opcode數組是這個樣子:
```c
int op_array[] = {
opcode_1,
opcode_2,
opcode_3,
...
};
```
各模式下的工作過程類似這樣:
```c
//CALL模式
void opcode_1_handler() {...}
void opcode_2_handler() {...}
...
void execute(int []op_array)
{
void *opcode_handler_list[] = {&opcode_1_handler, &opcode_2_handler, ...};
while(1){
void handler = opcode_handler_list[op_array[i]];
handler(); //call handler
i++;
}
}
//GOTO模式
void execute(int []op_array)
{
while(1){
goto opcode_xx_handler_label;
}
opcode_1_handler_label:
...
opcode_2_handler_label:
...
...
}
//SWITCH模式
void execute(int []op_array)
{
while(1){
switch(op_array[i]){
case opcode_1:
...
case opcode_2:
...
...
}
i++;
}
}
```
三種模式效率是不同的,GOTO最快,怎么選擇其它模式呢?下載PHP源碼后不要直接編譯,Zend目錄下有個文件:`zend_vm_gen.php`,在編譯PHP前執行:`php zend_vm_gen.php --with-vm-kind=CALL|SWITCH|GOTO`,這個腳本將重新生成:`zend_vm_opcodes.h`、`zend_vm_opcodes.c`、`zend_vm_execute.h`三個文件覆蓋原來的,然后再編譯PHP即可。
后面分析的過程使用的都是默認模式`CALL`,也就是opcode對應的handler為一個函數指針,編譯時opcode對應的handler是如何根據opcode索引到的呢?
opcode的數值各不相同,同時可以根據兩個zend_op的類型設置不同的處理handler,因此每個opcode指令最多有20個(25去掉重復的5個)對應的處理handler,所有的handler按照opcode數值的順序定義在一個大數組中:`zend_opcode_handlers`,每25個為同一個opcode,如果對應的op_type類型handler則可以設置為空:
```c
//zend_vm_execute.h
void zend_init_opcodes_handlers(void)
{
static const void *labels[] = {
ZEND_NOP_SPEC_HANDLER,
ZEND_NOP_SPEC_HANDLER,
...
};
zend_opcode_handlers = labels;
}
```
索引的算法:
```c
//zend_vm_execute.h
static const void *zend_vm_get_opcode_handler(zend_uchar opcode, const zend_op* op)
{
//因為op_type為2的倍數,所以這里做了下轉化,轉成了0-4
static const int zend_vm_decode[] = {
_UNUSED_CODE, /* 0 */
_CONST_CODE, /* 1 = IS_CONST */
_TMP_CODE, /* 2 = IS_TMP_VAR */
_UNUSED_CODE, /* 3 */
_VAR_CODE, /* 4 = IS_VAR */
_UNUSED_CODE, /* 5 */
_UNUSED_CODE, /* 6 */
_UNUSED_CODE, /* 7 */
_UNUSED_CODE, /* 8 = IS_UNUSED */
_UNUSED_CODE, /* 9 */
_UNUSED_CODE, /* 10 */
_UNUSED_CODE, /* 11 */
_UNUSED_CODE, /* 12 */
_UNUSED_CODE, /* 13 */
_UNUSED_CODE, /* 14 */
_UNUSED_CODE, /* 15 */
_CV_CODE /* 16 = IS_CV */
};
//根據op1_type、op2_type、opcode得到對應的handler
return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1_type] * 5 + zend_vm_decode[op->op2_type]];
}
ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
{
//設置zend_op的handler,這個操作是在編譯期間完成的
op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
}
#define _CONST_CODE 0
#define _TMP_CODE 1
#define _VAR_CODE 2
#define _UNUSED_CODE 3
#define _CV_CODE 4
```
##### 3.1.2.1.2 操作數(znode_op)
操作數類型實際就是個32位整形,它主要用于存儲一些變量的索引位置、數值記錄等等。
```c
typedef union _znode_op {
uint32_t constant;
uint32_t var;
uint32_t num;
uint32_t opline_num; /* Needs to be signed */
uint32_t jmp_offset;
} znode_op;
```
每條opcode都有兩個操作數(不一定都用到),操作數記錄著當前指令的關鍵信息,可以用于變量的存儲、訪問,比如賦值語句:"$a = 45;",兩個操作數分別記錄"$a"、"45"的存儲位置,執行時根據op2取到值"45",然后賦值給"$a",而"$a"的位置通過op1獲取到。當然操作數并不是全部這么用的,上面只是賦值時候的情況,其它操作會有不同的用法,如函數調用時的傳參,op1記錄的就是傳遞的參數是第幾個,op2記錄的是參數的存儲位置,result記錄的是函數接收參數的存儲位置。
##### 3.1.2.1.3 操作數類型(op_type)
每個操作都有5種不同的類型:
```c
#define IS_CONST (1<<0) //1
#define IS_TMP_VAR (1<<1) //2
#define IS_VAR (1<<2) //4
#define IS_UNUSED (1<<3) //8
#define IS_CV (1<<4) //16
```
* IS_CONST:字面量,編譯時就可確定且不會改變的值,比如:$a = "hello~",其中字符串"hello~"就是常量
* IS_TMP_VAR:臨時變量,比如:$a = "hello~" . time(),其中`"hello~" . time()`的值類型就是IS_TMP_VAR,再比如:$a = "123" + $b,`"123" + $b`的結果類型也是IS_TMP_VAR,從這兩個例子可以猜測,臨時變量多是執行期間其它類型組合現生成的一個中間值,由于它是現生成的,所以把IS_TMP_VAR賦值給IS_CV變量時不會增加其引用計數
* IS_VAR:PHP變量,這個很容易認為是PHP腳本里的變量,其實不是,這里PHP變量的含義可以這樣理解:PHP變量是沒有顯式的在PHP腳本中定義的,不是直接在代碼通過`$var_name`定義的。這個類型最常見的例子是PHP函數的返回值,再如`$a[0]`數組這種,它取出的值也是`IS_VAR`,再比如`$$a`這種
* IS_UNUSED:表示操作數沒有用
* IS_CV:PHP腳本變量,即腳本里通過`$var_name`定義的變量,這些變量是編譯階段確定的,所以是compile variable,
`result_type`除了上面幾種類型外還有一種類型`EXT_TYPE_UNUSED (1<<5)`,返回值沒有使用時會用到,這個跟`IS_UNUSED`的區別是:`IS_UNUSED`表示本操作返回值沒有意義(也可簡單的認為沒有返回值),而`EXT_TYPE_UNUSED`的含義是有返回值,但是沒有用到,比如函數返回值沒有接收。
##### 3.1.2.1.4 字面量、變量的存儲
我們先想一下C程序是如何讀寫字面量、變量的。
```c
#include <stdio.h>
int main()
{
char *name = "pangudashu";
printf("%s\n", name);
return 0;
}
```
我們知道指針name分配在棧上,而"pangudashu"分配在常量區,那么"name"變量名分配在哪呢?
實際上C里面是不會存變量名稱的,編譯的過程會將變量名替換為偏移量表示:`ebp - 偏移量`或`esp + 偏移量`,將上面的代碼轉為匯編:
```c
.LC0:
.string "pangudashu"
.text
.globl main
.type main, @function
main:
.LFB0:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq $.LC0, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call puts
movl $0, %eax
leave
```
可以看到`movq $.LC0, -8(%rbp)`,而`-8(%rbp)`就是name變量。
雖然PHP代碼不會直接編譯為機器碼,但編譯、執行的設計跟C程序是一致的,也有常量區、變量也通過偏移量訪問、也有虛擬的執行棧。

在編譯時就可確定且不會改變的量稱為字面量,也稱作常量(IS_CONST),這些值在編譯階段就已經分配zval,保存在`zend_op_array->literals`數組中(對應c程序的常量存儲區),訪問時通過`_zend_op_array->literals + 偏移量`讀取,舉個例子:
```c
<?php
$a = 56;
$b = "hello";
```
`56`通過`(zval*)(_zend_op_array->literals + 0)`取到,`hello`通過`(zval*)(_zend_op_array->literals + 16)`取到,具體變量的讀寫操作將在執行階段詳細分析,這里只分析編譯階段的操作。
#### 3.1.2.2 AST->zend_op_array
上面我們介紹了zend_op_array結構,接下來我們回過頭去看下語法解析(zendparse())之后的流程:
```c
ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
{
zend_op_array *op_array = NULL; //編譯出的opcodes
...
if (open_file_for_scanning(file_handle)==FAILURE) {//文件打開失敗
...
} else {
zend_bool original_in_compilation = CG(in_compilation);
CG(in_compilation) = 1;
CG(ast) = NULL;
CG(ast_arena) = zend_arena_create(1024 * 32);
if (!zendparse()) { //語法解析
zval retval_zv;
zend_file_context original_file_context; //保存原來的zend_file_context
zend_oparray_context original_oparray_context; //保存原來的zend_oparray_context,編譯期間用于記錄當前zend_op_array的opcodes、vars等數組的總大小
zend_op_array *original_active_op_array = CG(active_op_array);
op_array = emalloc(sizeof(zend_op_array)); //分配zend_op_array結構
init_op_array(op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE);//初始化op_array
CG(active_op_array) = op_array; //將當前正在編譯op_array指向當前
ZVAL_LONG(&retval_zv, 1);
if (zend_ast_process) {
zend_ast_process(CG(ast));
}
zend_file_context_begin(&original_file_context); //初始化CG(file_context)
zend_oparray_context_begin(&original_oparray_context); //初始化CG(context)
zend_compile_top_stmt(CG(ast)); //AST->zend_op_array編譯流程
zend_emit_final_return(&retval_zv); //設置最后的返回值
op_array->line_start = 1;
op_array->line_end = CG(zend_lineno);
pass_two(op_array);
zend_oparray_context_end(&original_oparray_context);
zend_file_context_end(&original_file_context);
CG(active_op_array) = original_active_op_array;
}
...
}
...
return op_array;
}
```
compile_file()操作中有幾個保存原來值的操作,這是因為這個函數在PHP腳本執行中并不會只執行一次,主腳本執行時會第一次調用,而include、require也會調用,所以需要先保存當前值,然后執行完再還原回去。
AST->zend_op_array編譯是在 __zend_compile_top_stmt()__ 中完成,這個函數是總入口,會被多次遞歸調用:
```c
//zend_compile.c
void zend_compile_top_stmt(zend_ast *ast)
{
if (!ast) {
return;
}
if (ast->kind == ZEND_AST_STMT_LIST) { //第一次進來一定是這種類型
zend_ast_list *list = zend_ast_get_list(ast);
uint32_t i;
for (i = 0; i < list->children; ++i) {
zend_compile_top_stmt(list->child[i]);//list各child語句相互獨立,遞歸編譯
}
return;
}
//各語句編譯入口
zend_compile_stmt(ast);
if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) {
zend_verify_namespace();
}
//function、class兩種情況的處理,非常關鍵的一步操作,后面分析函數、類實現的章節再詳細分析
if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {
CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;
zend_do_early_binding(); //很重要!!!
}
}
```
首先從AST的根節點開始編譯,根節點類型為ZEND_AST_STMT_LIST,這個類型表示當前節點下有多個獨立的節點,各child都是獨立的語句生成的節點,所以依次編譯即可,直到到達有效節點位置(非ZEND_AST_STMT_LIST節點),然后調用`zend_compile_stmt`編譯當前節點:
```c
void zend_compile_stmt(zend_ast *ast)
{
CG(zend_lineno) = ast->lineno;
switch (ast->kind) {
case xxx:
...
break;
case ZEND_AST_ECHO:
zend_compile_echo(ast);
break;
...
default:
{
znode result;
zend_compile_expr(&result, ast);
zend_do_free(&result);
}
}
if (FC(declarables).ticks && !zend_is_unticked_stmt(ast)) {
zend_emit_tick();
}
}
```
主要根據不同的節點類型(kind)作不同的處理,我們不會把每種類型的處理都講一遍,這里還是根據上一節最后的例子挑幾個看下具體的處理過程。
```php
$a = 123;
$b = "hi~";
echo $a,$b;
```
zendparse()階段生成的AST:

下面的過程比較復雜,有的函數會多次遞歸調用,我們根據例子一步步去看下,如果你對PHP各個語法實現比較熟悉再去看整個AST的編譯過程就會比較輕松。
> __(1)、__ 首先從根節點開始,有3個child,第一個節點類型為ZEND_AST_ASSIGN,zend_compile_stmt()中走到default分支
> __(2)、__ ZEND_AST_ASSIGN類型由zend_compile_expr()處理:
```c
void zend_compile_expr(znode *result, zend_ast *ast)
{
CG(zend_lineno) = zend_ast_get_lineno(ast);
switch (ast->kind) {
case ZEND_AST_ZVAL:
ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast));
result->op_type = IS_CONST;
return;
case ZEND_AST_VAR:
zend_compile_var(result, ast, BP_VAR_R);
return;
case ZEND_AST_ASSIGN:
zend_compile_assign(result, ast);
return;
...
}
}
```
> 繼續進入zend_compile_assign():
```c
void zend_compile_assign(znode *result, zend_ast *ast)
{
zend_ast *var_ast = ast->child[0]; //變量名
zend_ast *expr_ast = ast->child[1];//變量值表達式
znode var_node, expr_node;
zend_op *opline;
uint32_t offset;
if (is_this_fetch(var_ast)) { //檢查變量名是否為this,變量名不能是this
zend_error_noreturn(E_COMPILE_ERROR, "Cannot re-assign $this");
}
//比如這樣寫:my_function() = 123;即:將函數的返回值作為變量名將報錯
zend_ensure_writable_variable(var_ast);
switch (var_ast->kind) {
case ZEND_AST_VAR:
case ZEND_AST_STATIC_PROP:
offset = zend_delayed_compile_begin();
zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W); //生成變量名的znode,這個結構只在這個地方臨時用,所以直接分配在stack上
zend_compile_expr(&expr_node, expr_ast); //遞歸編譯變量值表達式,最終需要得到一個ZEND_AST_ZVAL的節點
zend_delayed_compile_end(offset);
zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); //生成一條op
return;
...
}
}
```
> 這個地方主要有三步關鍵操作:
>> __第1步:__ 變量賦值操作有兩部分:變量名、變量值,所以首先是針對變量名的操作,介紹zend_op_array時曾提到每個PHP變量都有一個編號,變量的讀寫都是根據這個編號操作的,這個編號最早就是這一步生成的。

>> 中間過程我們不再細看,這里重點看下變量編號的過程,這個過程比較簡單,每發現一個變量就遍歷zend_op_array.vars數組,看此變量是否已經保存,沒有保存的話則存入vars,然后后續變量的使用都是用的這個變量在數組中的下標,比如第一次定義的時候:`$a = 123;`將$a編號為0,然后:`echo $a;`再次使用時會遍歷vars,發現已經存在,直接用其下標操作$a。
```c
static int lookup_cv(zend_op_array *op_array, zend_string* name)
{
int i = 0;
zend_ulong hash_value = zend_string_hash_val(name);
//遍歷op_array.vars檢查此變量是否已存在
while (i < op_array->last_var) {
if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||
(ZSTR_H(op_array->vars[i]) == hash_value &&
ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&
memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {
zend_string_release(name);
return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);
}
i++;
}
//這是一個新變量
i = op_array->last_var;
op_array->last_var++;
if (op_array->last_var > CG(context).vars_size) {
CG(context).vars_size += 16; /* FIXME */
op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*));//擴容vars
}
op_array->vars[i] = zend_new_interned_string(name);
return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); //傳NULL時返回的是96 + i*sizeof(zval)
}
```
>> __注意:這里變量的編號從0、1、2、3...依次遞增的,但是實際使用中并不是直接用的這個下標,而是轉化成了內存偏移量offset,這個是`ZEND_CALL_VAR_NUM`宏處理的,所以變量偏移量實際是96、112、128...遞增的,這個96是根據zend_execute_data大小設定的(不同的平臺下對應的值可能不同),下一篇介紹zend執行流程時會詳細介紹這個結構。__
```c
#define ZEND_CALL_FRAME_SLOT \
((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))
#define ZEND_CALL_VAR_NUM(call, n) \
(((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))
```
>> __第2步:__ 編譯變量值表達式,再次調用zend_compile_expr()編譯,示例中的情況比較簡單,expr_ast.kind為ZEND_AST_ZVAL:
```c
void zend_compile_expr(znode *result, zend_ast *ast)
{
switch (ast->kind) {
case ZEND_AST_ZVAL:
ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast)); //將變量值復制到znode.u.constant中
result->op_type = IS_CONST; //類型為IS_CONST,這種value后面將會保存在zend_op_array.literals中
return;
...
}
}
```
>> __第3步:__ 上面兩步已經分別生成了變量賦值的op1、op2,下面就是根據這倆值生成opcode的過程。
```c
static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2)
{
zend_op *opline = get_next_op(CG(active_op_array)); //當前zend_op_array下生成一條新的指令
opline->opcode = opcode;
//將op1、op2內容拷貝到zend_op中,設置op_type
//如果znode.op_type == IS_CONST,則會將znode.u.contstant值轉移到zend_op_array.literals中
if (op1 == NULL) {
SET_UNUSED(opline->op1);
} else {
SET_NODE(opline->op1, op1);
}
if (op2 == NULL) {
SET_UNUSED(opline->op2);
} else {
SET_NODE(opline->op2, op2);
}
//如果此指令有返回值則想變量那樣為返回值編號(后面分配局部變量時將根據這個編號索引)
if (result) {
zend_make_var_result(result, opline);
}
return opline;
}
static inline void zend_make_var_result(znode *result, zend_op *opline)
{
opline->result_type = IS_VAR; //返回值類型固定為IS_VAR
opline->result.var = get_temporary_variable(CG(active_op_array)); //為返回值編個號,這個編號記在臨時變量T上,上面介紹zend_op_array時說過T、last_var的區別
GET_NODE(result, opline->result);
}
```
>> 到這我們示例中的第1條賦值語句就算編譯完了,第2條同樣是賦值,過程與上面相同,我們直接看最好一條輸出的語句。
> __(3)、__ echo語句的編譯:`echo $a,$b;`實際從編譯后的語法樹就可以看出,一次echo多個也被編譯為多次echo了,所以示例中的用法與:`echo $a; echo $b;`等價,我們只分析其中一個就可以了。

> `zend_compile_stmt()`中首先發現節點類型是`ZEND_AST_STMT_LIST`,然后調用`zend_compile_stmt_list()`分別編譯child,具體的流程如下圖所示:

> 最后生成`zend_op`的過程:
```c
void zend_compile_echo(zend_ast *ast)
{
zend_op *opline;
zend_ast *expr_ast = ast->child[0];
znode expr_node;
zend_compile_expr(&expr_node, expr_ast);
opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);//生成1條新的opcode
opline->extended_value = 0;
}
```
最終`zend_compile_top_stmt()`編譯完成后整個編譯流程基本是完成了,`CG(active_op_array)`結構如下圖所示,但是后面還有一個處理`pass_two()`。

```c
ZEND_API int pass_two(zend_op_array *op_array)
{
zend_op *opline, *end;
if (!ZEND_USER_CODE(op_array->type)) {
return 0;
}
//重置一些CG(context)的值,暫且忽略
...
opline = op_array->opcodes;
end = opline + op_array->last;
while (opline < end) {
switch(opline->opcode){
//這里對一些操作進行針對性的處理,后面有遇到的情況我們再看
...
}
//如果是IS_CONST會將數組下標轉化為內存偏移量,與IS_CV那種處理方式相同
//所以這里實際就是將0、1、2...轉為為16、32、48...(即:編號*sizeof(zval))
if (opline->op1_type == IS_CONST) {
ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1);
} else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {
//上面作相同的處理,不同的是這里的起始值是接著IS_CV的
opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var);
}
//與op1完全相同
if (opline->op2_type == IS_CONST) {
ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2);
} else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) {
opline->op2.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op2.var);
}
//返回值與op1/2相同處理
if (opline->result_type & (IS_VAR|IS_TMP_VAR)) {
opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->result.var);
}
//設置此opcode的處理handler
ZEND_VM_SET_OPCODE_HANDLER(opline);
opline++;
}
//標識當前op_array已執行過此操作
op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO;
return 0;
}
```
拋開特殊opcode的處理,`pass_two()`主要有兩個重要操作:
* (1)將IS_CONST、IS_VAR、IS_TMP_VAR類型的操作數、返回值轉化為內存偏移量,與上面提到的IS_CV變量的處理一樣,其中IS_CONST類型起始值為0,然后按照編號依次遞增sizeof(zval),而IS_VAR、IS_TMP_VAR唯一的不同時它的初始值接著IS_CV的,簡單的講就是先安排PHP變量的,然后接著才是各條語句的中間值、返回值
* (2)另外一個重要操作就是設置各指令的處理handler,這個前面《3.1.2.1.1 handler》已經介紹過其索引規則
經過`pass_two()`處理后opcodes的樣子:

__總結:__
到這里整個PHP編譯階段就算全部完成了,最終編譯的結果就是zend_op_array,其中最核心的操作就是AST的編譯了,有興趣的可以多寫幾個例子去看下不同節點類型的處理方式。
另外,編譯階段很關鍵的一個操作就是確定了各個 __變量、中間值、臨時值、返回值、字面量__ 的 __內存編號__ ,這個地方非常重要,后面介紹執行流程時也會用到。
- 前言
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本實現
- 1.3.3 FPM的初始化
- 1.3.4 請求處理
- 1.3.5 進程管理
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 第3章 Zend虛擬機
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 3.2 函數實現
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.1 什么是線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.3.1 擴展的構成
- 7.3.2 編譯工具
- 7.3.3 編寫擴展的基本步驟
- 7.3.4 config.m4
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.5.1 全局變量
- 7.5.2 ini配置
- 7.6 函數
- 7.6.1 內部函數注冊
- 7.6.2 函數參數解析
- 7.6.3 引用傳參
- 7.6.4 函數返回值
- 7.6.5 函數調用
- 7.7 zval的操作
- 7.7.1 新生成各類型zval
- 7.7.2 獲取zval的值及類型
- 7.7.3 類型轉換
- 7.7.4 引用計數
- 7.7.5 字符串操作
- 7.7.6 數組操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.1 概述
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- break/continue按標簽中斷語法實現
- defer推遲函數調用語法的實現
- 一起線上事故引發的對PHP超時控制的思考