baiyan
全部視頻:https://segmentfault.com/a/11...
原視頻地址:http://replay.xesv5.com/ll/24...
引入先看上一節(jié)筆記中展示的AST示例:
在PHP中,構(gòu)造出來的抽象語法樹如圖所示:
那么,這個AST后面能夠用來做什么呢?因為我們最終需要執(zhí)行這段PHP代碼,所以需要將其轉(zhuǎn)化為可執(zhí)行的指令,讓虛擬機(jī)最終來解釋執(zhí)行
指令的幾個要素:
操作數(shù):參與指令操作的變量或常量等,只需OP1和OP2最多兩個操作數(shù)就夠了,因為多元運算可以轉(zhuǎn)化成二元運算(op1/op2)
指令操作:用來描述具體的賦值/加減乘除等指令操作(opcode)
返回值:用來存儲中間運算結(jié)果(result)
處理函數(shù):用來具體實現(xiàn)加減乘除等指令的操作邏輯(handler)這些指令要素是我們自己定義的,而寄存器是無法理解這些自定義指令的,這就需要zend虛擬機(jī)(zendvm) 去進(jìn)行指令轉(zhuǎn)換工作并且真正的執(zhí)行這些指令。
舉例:$a = 1 + 2; 這行代碼中,$a/1/2是操作數(shù),1+2計算的中間結(jié)果3是返回值,加法和賦值是指令做的具體操作,加法對應(yīng)加法的opcode與相應(yīng)的handler,賦值對應(yīng)賦值的opcode與相應(yīng)的handler接下來講一下這些指令在PHP7中是如何存儲的:
指令的基本概念及存儲結(jié)構(gòu)在PHP的zend虛擬機(jī)中,每條指令都是一個opline,每個opline由操作數(shù)、指令操作、返回值組成。每個指令操作都對應(yīng)一個opcode(如ZEND_ASSIGN/ZEND_ADD等等),而每個opcode又對應(yīng)一個handler處理函數(shù)。這樣,zend虛擬機(jī)就可以根據(jù)生成的指令,找到對應(yīng)的指令處理函數(shù),把操作數(shù)作為參數(shù)傳入,即可完成指令的執(zhí)行
基本概念總結(jié)opline:在zend虛擬機(jī)中,每條指令都是一個opline,每個opline由操作數(shù)、指令操作、返回值組成存儲結(jié)構(gòu)
opcode:每個指令操作都對應(yīng)一個opcode(如ZEND_ASSIGN/ZEND_ADD等等),在PHP7中,有100多種指令操作,所有的指令集被稱作opcodes
handler:每個opcode指令操作都對應(yīng)一個handler指令處理函數(shù),處理函數(shù)中有具體的指令操作執(zhí)行邏輯下面看一下PHP內(nèi)部的具體實現(xiàn),回想昨天調(diào)試的zend_compile斷點處,它是編譯的入口,并返回生成結(jié)果op_array:
static zend_op_array *zend_compile(int type) { zend_op_array *op_array = NULL; zend_bool original_in_compilation = CG(in_compilation); CG(in_compilation) = 1;//CG宏可以取得compile_globals結(jié)構(gòu)體中的字段 CG(ast) = NULL;//一開始的AST為NULL CG(ast_arena) = zend_arena_create(1024 * 32);//給AST分配空間 if (!zendparse()) { //進(jìn)行詞法和語法分析 ....... //初始化op_array,用來存放指令 op_array = emalloc(sizeof(zend_op_array)); init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE); CG(active_op_array) = op_array; ...... //對AST進(jìn)行遍歷并生成指令,并存儲到zend_op_array中 zend_compile_top_stmt(CG(ast)); ...... //設(shè)置handler pass_two(op_array); } return op_array; }重點關(guān)注zend_op_array這個類型,它是一個數(shù)組,用來存儲所有的指令集(即所有的opline)。來看看它的結(jié)構(gòu):
struct _zend_op_array { uint32_t last; //下面oplines數(shù)組大小 zend_op *opcodes; //oplines數(shù)組,存放所有指令 int last_var;//操作數(shù)類型為IS_CV的個數(shù) uint32_t T;//操作數(shù)類型為IS_VAR和IS_TMP_VAR的個數(shù)之和 zend_string **vars;//存放IS_CV類型操作數(shù)的數(shù)組 ... int last_literal;//下面常量數(shù)組大小 zval *literals;//存放IS_CONST類型操作數(shù)的數(shù)組 };zend_op_array是指令的集合,那么每條指令在zendvm中是一個opline。它由指令操作、操作數(shù)、返回值、以及指令操作對應(yīng)的handler組成。每一條指令opline對應(yīng)的結(jié)構(gòu)體是zend_op:
struct _zend_op { const void *handler; //操作 znode_op op1; //操作數(shù)1 znode_op op2; //操作數(shù)2 znode_op result; //操作結(jié)果 uint32_t extended_value; uint32_t lineno; //行號 zend_uchar opcode; //opcode值 zend_uchar op1_type; //操作數(shù)1類型 zend_uchar op2_type; //操作數(shù)2類型 zend_uchar result_type; //返回值類型 };每一條指令opline,對應(yīng)一個zend_op,存放指令集的zend_op_array由多條zend_op所構(gòu)成
現(xiàn)在我們知道了指令中的具體操作是由opcode和handler來表示和處理,那么繼續(xù)看一下操作數(shù)和返回值具體是如何表示的,如zend_op結(jié)構(gòu)體中所示,它們的類型均為znode_op類型:
typedef union _znode_op { uint32_t constant; uint32_t var; uint32_t num; uint32_t opline_num; /* Needs to be signed */ #if ZEND_USE_ABS_JMP_ADDR zend_op *jmp_addr; #else uint32_t jmp_offset; #endif #if ZEND_USE_ABS_CONST_ADDR zval *zv; #endif } znode_op;可以看到,constant、var、num都是uint32類型的,這個uint32類型并不足以表示所有操作數(shù)。這里存儲的是相對于虛擬機(jī)執(zhí)行棧幀首地址的偏移量。因為CV/臨時變量這些都是分配在棧上的(后面會講)。通過計算,我們才能得出最終操作數(shù)在棧楨中的位置
在PHP7中,操作數(shù)有5種類型可選,如下:
#define IS_CONST (1<<0) #define IS_TMP_VAR (1<<1) #define IS_VAR (1<<2) #define IS_UNUSED (1<<3) /* Unused variable */ #define IS_CV (1<<4) /* Compiled variable */IS_CONST類型:值為1,表示常量,如$a = 1中的1或者$a = "hello world"中的hello world
IS_TMP_VAR類型:值為2,表示臨時變量,如$a=”123”.time(); 這里拼接的臨時變量”123”.time()的類型就是IS_TMP_VAR,一般用于操作的中間結(jié)果
IS_VAR類型:值為4,表示變量,但是這個變量并不是PHP中常見的聲明變量,而是返回的臨時變量,如$a = time()中的time()
IS_UNUSED:值為8,表示沒有使用的操作數(shù)
IS_CV:值為16,表示形如$a這樣的變量既然如何我們知道了如何存儲指令,那么下面講一下如何遍歷這棵抽象語法樹,來得到這些指令集:
遍歷抽象語法樹 編譯根節(jié)點(LIST)我們現(xiàn)在僅僅關(guān)注$a = 1這一行代碼并對其AST進(jìn)行遍歷關(guān)注zend_compile中的zend_compile_top_stmt(CG(ast))這個函數(shù)調(diào)用,它會對這棵AST進(jìn)行遍歷:
void zend_compile_top_stmt(zend_ast *ast) { if (!ast) { return; } if (ast->kind == ZEND_AST_STMT_LIST) { //如果是這個AST是LIST類型 zend_ast_list *list = zend_ast_get_list(ast);//將其轉(zhuǎn)換成ZEND_AST_LIST類型即可(上一篇筆記是在gdb下直接強(qiáng)轉(zhuǎn)的) uint32_t i; for (i = 0; i < list->children; ++i) { zend_compile_top_stmt(list->child[i]); //遞歸調(diào)用,進(jìn)行深度遍歷 } return; } zend_compile_stmt(ast); //編譯的入口。遞歸調(diào)用的時候,如果往下走的時候并非LIST型結(jié)點,會調(diào)用這個函數(shù) if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) { zend_verify_namespace(); } 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(); } }內(nèi)聯(lián)函數(shù)(inline):普通的函數(shù)調(diào)用是需要壓棧的,而內(nèi)聯(lián)函數(shù)直接將函數(shù)體代碼嵌入到調(diào)用位置,提高代碼執(zhí)行效率
具體代碼執(zhí)行過程(按照最開始的AST圖來講):
根節(jié)點進(jìn)來,判斷是ZEND_AST_LIST類型(132),故將其強(qiáng)轉(zhuǎn)成ZEND_AST_LIST類型
遞歸調(diào)用函數(shù)本身,參數(shù)傳入其第一個子結(jié)點(517,賦值運算符)
賦值運算符不是LIST類型,往下走到zend_compile_stmt(ast)中
接下來看下編譯入口zend_compile_stmt()的具體實現(xiàn):
void zend_compile_stmt(zend_ast *ast) { if (!ast) { return; } CG(zend_lineno) = ast->lineno; if ((CG(compiler_options) & ZEND_COMPILE_EXTENDED_INFO) && !zend_is_unticked_stmt(ast)) { zend_do_extended_info(); } switch (ast->kind) { case ZEND_AST_STMT_LIST: zend_compile_stmt_list(ast); break; ...... default: //最終會走到這里,因為所有的case中,沒有ZEND_AST_ASSIGN類型與之匹配 { znode result; //聲明了一個znode類型變量,存儲返回值(像$a = 1 + 2)這種需要存儲中間結(jié)果3的表達(dá)式才需要使用這個result zend_compile_expr(&result, ast); //調(diào)用這個函數(shù)處理當(dāng)前表達(dá)式,下面會展開 zend_do_free(&result); } } ...... }代碼最終會走到default分支。會首先聲明一個znode類型的變量result,看一下znode類型的結(jié)構(gòu):
typedef struct _znode { zend_uchar op_type; zend_uchar flag; union { znode_op op; //操作數(shù)變量的位置 zval constant; //常量 } u; } znode;重點關(guān)注這個聯(lián)合體u中的op以及constant字段,在后面可以用來存儲編譯過程中的中間值,先記下這個結(jié)構(gòu)體
接下來會調(diào)用zend_compile_expr函數(shù),繼續(xù)跟進(jìn)zend_compile_expr(&result, ast),注意這里的ast是517位根節(jié)點的子樹而非最開始的ast。看一下這個函數(shù)的具體實現(xiàn):
void zend_compile_expr(znode *result, zend_ast *ast) { ...... switch (ast->kind) { ...... case ZEND_AST_ASSIGN: zend_compile_assign(result, ast); //代碼走到這里,調(diào)用這個函數(shù),下面繼續(xù)跟進(jìn) return; ...... } }編譯賦值(ASSIGN)等號最終上面代碼會走到ZEND_AST_ASSIGN的case中,并調(diào)用zend_compile_assign(result, ast)函數(shù),繼續(xù)跟進(jìn)這個函數(shù):
void zend_compile_assign(znode *result, zend_ast *ast) { zend_ast *var_ast = ast->child[0]; //517的第一個孩子256 zend_ast *expr_ast = ast->child[1]; //517的第二個孩子64 znode var_node, expr_node; //存儲編譯后的中間結(jié)果 zend_op *opline; uint32_t offset; ... 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); //編譯$a,中間結(jié)果放到var_node這個znode上 zend_compile_expr(&expr_node, expr_ast); //編譯1,中間結(jié)果放到expr_node這個znode上 zend_delayed_compile_end(offset); zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); //生成opline return; ...... } }首先取出賦值等號(517)的第一個子結(jié)點,其類型是ZEND_AST_VAR(256),然后取出第二個子結(jié)點,其類型是ZEND_AST_ZVAL(64),switch之后來到ZEND_AST_STATIC_PROP這個case,直接看第二行這個函數(shù)zend_delayed_compile_var,它的功能是編譯左邊的$a
編譯賦值等號左邊的$a接下來調(diào)用zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W); 這個函數(shù)被用來編譯左邊的$a,它會將編譯產(chǎn)生的中間結(jié)果放到var_node這個znode中:
void zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t type) { zend_op *opline; switch (ast->kind) { case ZEND_AST_VAR: zend_compile_simple_var(result, ast, type, 1); return; ... } }代碼會執(zhí)行到ZEND_AST_VAR這個case中,然后調(diào)用zend_compile_simple_var(result, ast, type, 1),繼續(xù)跟進(jìn):
static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type, int delayed) { zend_op *opline; if (is_this_fetch(ast)) { ...... } else if (zend_try_compile_cv(result, ast) == FAILURE) { ...... } }首先第一個if中is_this_fetch(ast)是判斷是否和this(對象)相關(guān),我們這里不是,那么走到下一個else if分支,調(diào)用zend_try_compile_cv(result, ast)函數(shù),繼續(xù)跟進(jìn):
static int zend_try_compile_cv(znode *result, zend_ast *ast) { zend_ast *name_ast = ast->child[0]; if (name_ast->kind == ZEND_AST_ZVAL) { zend_string *name = zval_get_string(zend_ast_get_zval(name_ast)); if (zend_is_auto_global(name)) { zend_string_release(name); return FAILURE; } result->op_type = IS_CV; //將其類型標(biāo)記為CV,CV變量在運行時是存在棧上的 result->u.op.var = lookup_cv(CG(active_op_array), name); //返回這個CV變量在運行時棧上的偏移量 name = CG(active_op_array)->vars[EX_VAR_TO_NUM(result->u.op.var)]; return SUCCESS; } return FAILURE; }這個函數(shù)首先取它的第一個孩子結(jié)點,因為當(dāng)前傳過來的ast是256(ZEND_AST_VAR類型),它的孩子結(jié)點只有一個,且類型為64(ZEND_AST_ZVAL類型),所以第一個if判斷為true,并且調(diào)用了zval_get_string(zend_ast_get_zval(name_ast))這個函數(shù)。我們由內(nèi)往外看,首先對這個64的ast結(jié)點進(jìn)行zend_ast_get_zval()函數(shù)調(diào)用,它會將ZEND_AST類型轉(zhuǎn)化成ZEND_AST_ZVAL類型,和之前在gdb中調(diào)試的效果一樣。接下來外部對這個ast結(jié)點調(diào)用zval_get_string()函數(shù),看下它的內(nèi)部實現(xiàn):
static zend_always_inline zend_string *_zval_get_string(zval *op) { return Z_TYPE_P(op) == IS_STRING ? zend_string_copy(Z_STR_P(op)) : _zval_get_string_func(op); }首先,Z_TYPE_P這個宏取得zval中的u1.v.type字段,如果是IS_STRING的話,調(diào)用zend_string_copy(z_str_p(op))函數(shù),首先調(diào)用了Z_STR_P這個宏,它會取得zend_value中的str字段,就是指向zend_string的指針,我們可以看到以下上面宏的定義:
/* we should never set just Z_TYPE, we should set Z_TYPE_INFO */ #define Z_TYPE(zval) zval_get_type(&(zval)) #define Z_TYPE_P(zval_p) Z_TYPE(*(zval_p)) static zend_always_inline zend_uchar zval_get_type(const zval* pz) { return pz->u1.v.type; } ... #define Z_STR(zval) (zval).value.str #define Z_STR_P(zval_p) Z_STR(*(zval_p))我們繼續(xù)看一下外層zend_string_copy()的實現(xiàn):
static zend_always_inline zend_string *zend_string_copy(zend_string *s) { if (!ZSTR_IS_INTERNED(s)) { GC_REFCOUNT(s)++; } return s; }我們看到僅僅是做了一個對zend_string中的refcount字段++的操作,并沒有真正去做具體的拷貝
回到zend_try_compile_cv()這個函數(shù),我們在調(diào)用完之后將結(jié)果賦值給name變量。接下來因為變量不是全局的,所以不進(jìn)這個if。接下來對result變量的兩個字段進(jìn)行了賦值操作:
result->op_type = IS_CV; result->u.op.var = lookup_cv(CG(active_op_array), name);首先為它賦值IS_CV。那么什么是CV型變量呢?CV,即compiled variable,是PHP編譯過程中產(chǎn)生的一種變量類型,以類似于緩存的方式,提高某些變量的存儲速度。這里$a = 1中的$a,就是CV型變量,CV型變量在運行時是存儲在zend_execute_data(后面會講)虛擬機(jī)上的棧中的
第二行,給result中的u.op.var字段賦值,而result我們講過,是一個znode。重點關(guān)注右邊lookup_cv()函數(shù),它返回一個int類型的地址,是sizeof(zval)的整數(shù)倍,通過它可以得到每個變量的偏移量(80(后面會講) + 16 * i),i是變量的編號。這樣就規(guī)定了運行時在棧上相對于zend_execute_data的偏移量,從而在棧上方便地存儲了$a這個變量(下一篇筆記會詳細(xì)講)。而$a在zend_op_array的vars數(shù)組上也冗余存了一份,這樣如果后面又用到了$a的話,直接去zend_op_array的vars數(shù)組中查找找,如果存在,那么直接使用之前的編號i,如果不存在則按序分配一個編號,然后再插入zend_op_array的vars數(shù)組,節(jié)省了分配編號的時間
static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{ int i = 0; zend_ulong hash_value = zend_string_hash_val(name); 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*)); } op_array->vars[i] = zend_new_interned_string(name); return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); }編譯賦值等號右邊的值1接下來回到外部zend_compile_assign函數(shù),繼續(xù)往下執(zhí)行zend_compile_expr(&expr_node, expr_ast)函數(shù),處理等號右邊的值1,它會將編譯產(chǎn)生的中間結(jié)果放到expr_node這個znode中:
接下來會走到zend_compile_expr函數(shù)的ZEND_AST_ZVAL這個case,看下這個case:
case ZEND_AST_ZVAL: ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast)); result->op_type = IS_CONST; return;它調(diào)用了一個ZVAL_COPY宏,將這個ZEND_AST_ZVAL類型的結(jié)點的zval字段中存儲的值(即1對應(yīng)的zval),拷貝到之前聲明的znode類型變量result的u.constant字段中,這樣操作數(shù)就存放完畢了
看一下這個zend_ast_get_zval(ast)的具體實現(xiàn):
static zend_always_inline zval *zend_ast_get_zval(zend_ast *ast) { ZEND_ASSERT(ast->kind == ZEND_AST_ZVAL); return &((zend_ast_zval *) ast)->val; }先忽略斷言,它直接利用強(qiáng)轉(zhuǎn)并取出val字段的值,就是1對應(yīng)的zval,并返回了它的地址
接下來再看一下ZVAL_COPY這個宏,它的功能是把一個zval(v)拷貝到另外一個zval(z)中
我們首先回顧一下zval的結(jié)構(gòu):
struct _zval_struct { zend_value value; /* 存儲變量的值 */ union { struct { ZEND_ENDIAN_LOHI_4( //大小端問題,詳情看"PHP內(nèi)存管理3筆記” zend_uchar type, //注意這里就是存放變量類型的地方,char類型 zend_uchar type_flags, //類型標(biāo)記 zend_uchar const_flags, //是否是常量 zend_uchar reserved) //保留字段 } v; uint32_t type_info; } u1; union { uint32_t next; /* 數(shù)組模擬鏈表,鏈地址法解決哈希沖突時使用 */ uint32_t cache_slot; /* literal cache slot */ uint32_t lineno; /* line number (for ast nodes) */ uint32_t num_args; /* arguments number for EX(This) */ uint32_t fe_pos; /* foreach position */ uint32_t fe_iter_idx; /* foreach iterator index */ uint32_t access_flags; /* class constant access flags */ uint32_t property_guard; /* single property guard */ uint32_t extra; /* not further specified */ } u2; };由zval結(jié)構(gòu),我們可以知道:復(fù)制zval,就是將老zval中的value/u1/u2三個字段拷貝到新zval中即可。
那么,源碼中是怎么實現(xiàn)的呢:
#define ZVAL_COPY(z, v) do { zval *_z1 = (z); const zval *_z2 = (v); zend_refcounted *_gc = Z_COUNTED_P(_z2); uint32_t _t = Z_TYPE_INFO_P(_z2); ZVAL_COPY_VALUE_EX(_z1, _z2, _gc, _t); if ((_t & (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT)) != 0) { GC_REFCOUNT(_gc)++; } } while (0)首先將z賦值給_z1,它是一個地址,然后將v賦值給_z2,也是一個地址,注意這個地址的值是常量,表示不能夠修改v指向的zval的值(因為它是被賦值的zval,所以沒必要修改它的值)
然后通過Z_COUNTED_P(_z2)這個宏取出_z2(v)這個zval中的refcounted字段,看下這個宏的具體實現(xiàn):
#define Z_COUNTED(zval) (zval).value.counted #define Z_COUNTED_P(zval_p) Z_COUNTED(*(zval_p))可以看到,它取出了zval中的zend_value字段中的counted字段的值,那么,為什么要取counted這個字段呢?我們回顧一下zend_value的結(jié)構(gòu):
typedef union _zend_value { zend_long lval; //整型 double dval; //浮點 zend_refcounted *counted; //引用計數(shù),這里取出這個字段的值 zend_string *str; //字符串 zend_array *arr; //數(shù)組 zend_object *obj; //對象 zend_resource *res; //資源 zend_reference *ref; //引用 zend_ast_ref *ast; //抽象語法樹 zval *zv; //內(nèi)部使用 void *ptr; //不確定類型,取出來之后強(qiáng)轉(zhuǎn) zend_class_entry *ce; //類 zend_function *func;//函數(shù) struct { uint32_t w1; uint32_t w2; } ww; //這個union一共8B,這個結(jié)構(gòu)體每個字段都是4B,因為所有聯(lián)合體字段共用一塊內(nèi)存,故相當(dāng)于取了一半的union } zend_value;這里一定要注意zend_value是一個聯(lián)合體。由于其內(nèi)部所有字段共用一塊內(nèi)存空間,源碼中取counted字段的值,和取lval/dval/str/arr這些字段值的效果是完全一樣的,其本質(zhì)上就是取到了zval中的zend_value這個字段的值。
那么現(xiàn)在我們完成了從老的zval中取到zend_value這個字段的值,還剩下u1/u2兩個字段需要我們?nèi)ツ?/p>
接下來它使用了Z_TYPE_INFO_P(_z2);這個宏,看下它的實現(xiàn):
#define Z_TYPE_INFO(zval) (zval).u1.type_info #define Z_TYPE_INFO_P(zval_p) Z_TYPE_INFO(*(zval_p))它直接取到了zval中u1的type_info字段。由于u1也是一個聯(lián)合體,實際上就是取得了v這個結(jié)構(gòu)體中的type/type_flags/const_flags/reserved這四個字段的值,理由同上。這樣,u1也拿到了,那么現(xiàn)在還剩下u2沒有去取
接下來又去調(diào)用了ZVAL_COPY_VALUE_EX(_z1, _z2, _gc, _t)這個宏,我們看下它的實現(xiàn):
# define ZVAL_COPY_VALUE_EX(z, v, gc, t) do { Z_COUNTED_P(z) = gc; Z_TYPE_INFO_P(z) = t; } while (0) #else它就是直接將gc.counted字段,即zend_value的值)、以及t(即u1的值)直接拷貝到z這個zval中,這樣就完成了zend_value以及u1的復(fù)制,那么u2為什么沒有拷貝呢?因為u2這個聯(lián)合體中的字段并不重要,不對其進(jìn)行復(fù)制不會對代碼邏輯有任何影響
下面的if我們可以先忽略,由于在ZVAL_COPY_VALUE_EX宏中完成了復(fù)制,可以不去考慮下面的邏輯
那么現(xiàn)在針對這個宏中出現(xiàn)的語法,提兩個問題:
為什么會出現(xiàn)(z)這種語法,不加括號可以嗎?
為什么要do{}while(0),反正都是只執(zhí)行一次這個宏的代碼,可以去掉do{}while(0)嗎?針對第一個問題,是出于安全性的考慮,看下面一個例子:
#define X(a, b) a = b * 3;如果我們這樣調(diào)用宏:X(a, 1+2);那么宏展開的結(jié)果為 a = 1 + 2 * 3 ,即a = 7 ,而我們預(yù)期的結(jié)果是a = (1+2) * 3 = 9,出現(xiàn)了運算符優(yōu)先級的問題,所以當(dāng)傳入的參數(shù)是一個表達(dá)式的時候,不加括號會出現(xiàn)運算符優(yōu)先級不符合預(yù)期的問題,所以加上括號能夠更加安全
下面看第二個為什么要加上do-while(0)的問題,擴(kuò)展一下上面的宏X:
#define X(a, b) a = b * 3; a = a + 1;那么我們?nèi)绻诖a中編寫:
if (true) X(a, b)那么它的效果等同于:
if (true) a = b * 3; a = a +1;可以看到,如果if不加{}大括號的話,只會執(zhí)行第一條語句a = b * 3,并不符合預(yù)期
如果加上do{}while(0),其效果等同于:
if (true) do { a = b * 3; a = a +1; } while(0)由于do-while語句的存在,兩個表達(dá)式變成了一個整體,所以宏體中兩個表達(dá)式都會被執(zhí)行。所以,這樣做能夠保證宏體里所有語句都會被完整的執(zhí)行。
目前為止,變量$a和表達(dá)式1的信息,均已存儲在兩個不同的znode中,均已編譯完成,下面就開始生成指令了:
指令的生成接下來回到外部zend_compile_assign函數(shù),繼續(xù)往下執(zhí)行zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);這個函數(shù),它負(fù)責(zé)整合之前編譯過程中的中間結(jié)果,并生成相應(yīng)的指令:
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)); opline->opcode = opcode; 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); } zend_check_live_ranges(opline); if (result) { zend_make_var_result(result, opline); } return opline; }首先觀察傳遞進(jìn)去的參數(shù),注意這里的result并非之前給result賦值的那個result(那個result是var_node和expr_node這兩個臨時znode),這個result在之前外層的default分支聲明還未賦值,為NULL。第二個就是指令所要執(zhí)行的操作opcode,即ASSIGN;第三個參數(shù)和第四個參數(shù)是操作數(shù),我們這里就是$a和1,也將其存儲編譯期間信息的znode傳遞進(jìn)去
有了操作數(shù)$a、指令操作(ASSIGN)、操作數(shù)(1),我們現(xiàn)在就可以生成指令了
一條指令是一個opline,且opline是存儲在zend_op_array上的。那么我們首先在zend_op_array上分配一個opline出來用于存儲即將生成的指令。隨后將opcode的信息也賦值到這個opline上去。那么現(xiàn)在opline上只有一個opcode,還沒有操作數(shù)$a和操作數(shù)(1)的信息,也沒有result的信息。因為op1不是空,調(diào)用SET_NODE宏:
#define SET_NODE(target, src) do { target ## _type = (src)->op_type; if ((src)->op_type == IS_CONST) { target.constant = zend_add_literal(CG(active_op_array), &(src)->u.constant); } else { target = (src)->u.op; } } while (0)從SET_NODE(opline->op1, op1)的兩個參數(shù)可以看出,它將操作數(shù)$a的信息拷貝到opline上
op2也不為空,也與op1同理,將值1的znode信息拷貝到opline上
下面zend_check_live_ranges這個函數(shù)先忽略,那么現(xiàn)在還剩下一個返回值沒有設(shè)置,由于我們這里result的值還是空,所以不進(jìn)這個if。因為我們$a = 1這個簡單的賦值表達(dá)式,是沒有返回值這一說法的。但是類似$a = 1 + 2;這樣的表達(dá)式,返回的中間值3的信息可以存在result這個znode中,然后同樣拷貝到opline上,這個時候才會用到result。這個函數(shù)的細(xì)節(jié)不再展開
最后全部編譯完之后,看下這幾個znode中的值:
由于CV型變量是存儲在zend_execute_data棧楨上的,故圖中的80即是$a在執(zhí)行棧楨上的偏移量,通過計算能夠找到$a。1是等號右側(cè)表達(dá)式1的值,而result中的值沒有意義,在這里沒有使用result存儲中間結(jié)果
具體圖解請參考如下兩篇參考文獻(xiàn)
參考資料【PHP7源碼分析】PHP7源碼研究之淺談Zend虛擬機(jī)
【PHP7源碼分析】如何理解PHP虛擬機(jī)(一)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/31491.html
摘要:在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容閆昌李樂階段李樂李樂李樂李樂李樂李樂馬運運李樂李樂李樂源碼集群閆昌源碼閆昌源碼主從復(fù)制李樂源碼施洪寶源碼施洪寶韓天 在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容: 2019-06-24 ~ 2019-06-28 06-27 nginx by 閆昌 06-26 nginx module by 李樂 06-25 nginx http ...
摘要:在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容執(zhí)行潘森執(zhí)行潘森執(zhí)行潘森趙俊峰紅黑樹景羅紅黑樹景羅配置三叉樹田志澤新建模塊馬運運配置田志澤田志澤田志澤李樂田志澤田志澤文件系統(tǒng) 在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容: 2019-07-15 ~ 2019-07-19 07-18 nginx http 執(zhí)行 by 潘森 07-17 nginx http 執(zhí)行 by 潘森 07...
閱讀 3917·2021-11-24 09:38
閱讀 3088·2021-11-17 09:33
閱讀 3863·2021-11-10 11:48
閱讀 1234·2021-10-14 09:48
閱讀 3123·2019-08-30 13:14
閱讀 2543·2019-08-29 18:37
閱讀 3386·2019-08-29 12:38
閱讀 1410·2019-08-29 12:30