baiyan
全部視頻:https://segmentfault.com/a/11...
復習 基本概念
首先復習幾個基本概念:
opline:在zend虛擬機中,每條指令都是一個opline,每個opline由操作數、指令操作、返回值組成
opcode:每個指令操作都對應一個opcode(如ZEND_ASSIGN/ZEND_ADD等等),在PHP7中,有100多種指令操作,所有的指令集被稱作opcodes
handler:每個opcode指令操作都對應一個handler指令處理函數,處理函數中有具體的指令操作執行邏輯
我們知道,在經過編譯階段(zend_compile函數)中,我們生成AST并對其遍歷,生成一條條指令,每一條指令都是一個opline。之后通過pass_two函數生成了這些指令所對應的handler,這些信息均存在op_array中。既然指令和handler已經生成完畢,接下來的任務就是要交給zend虛擬機,加載這些指令,并最終執行對應的handler邏輯。
指令在PHP7中,由以下元素構成:
struct _zend_op { const void *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; //返回值的類型 };
在PHP7中,每個操作數有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,一般用于操作的中間結果
IS_VAR類型:值為4,表示變量,但是這個變量并不是PHP中常見的聲明變量,而是返回的臨時變量,如$a = time()中的time()
IS_UNUSED:值為8,表示沒有使用的操作數
IS_CV:值為16,表示形如$a這樣的變量
對AST進行遍歷之后,最終存放所有指令集(oplines)的地方為op_array:
struct _zend_op_array { uint32_t last; //下面oplines數組大小 zend_op *opcodes; //oplines數組,存放所有指令 int last_var;//操作數類型為IS_CV的個數 uint32_t T;//操作數類型為IS_VAR和IS_TMP_VAR的個數之和 zend_string **vars;//存放IS_CV類型操作數的數組 ... int last_literal;//下面常量數組大小 zval *literals;//存放IS_CONST類型操作數的數組 };op_array的存儲情況
為了復習op_array的存儲情況,我們具體gdb一下,使用下面的測試用例:
根據以上測試用例,在zend_execute處打一個斷點,這里完成了對AST的遍歷并生成了最終的op_array,已經進入到虛擬機執行指令的入口。首先我們先觀察傳入的參數op_array,它是經過AST遍歷之后生成的最終的op_array:
last = 2;表示一共有兩個opcodes:一個是賦值ASSIGN,另一個是腳本為我們自動生成的返回語句return 1,opcodes是一個數組,每個數組單元具體存儲了每條指令的信息(操作數、返回值等等),我們打印一下數組的內容:
last_var = 1;表示有一個CV類型的變量,這里就是$a
T = 1;表示IS_TMP_VAR和IS_VAR變量類型的數量之和,而我們腳本中并沒有這樣的變量,它是在存儲中間的返回值的時候,這個返回值類型就是一個IS_VAR類型,所以T的值一開始就為1
vars是一個二級指針,可以理解為外層的一級指針首先指向一個數組,這個數組里每個存儲單元都是一個zend_string*類型的指針,而每個指針都指向了一個zend_string結構體,我們打印數組第一個單元的值,發現其指向的zend_string值為a:
last_literal = 2;表示腳本中一共有2個常量,一個是我們自己復制的值2,另一個是腳本為我們自動生成的返回語句return 1中的值1:
literals是一個zend_array,里面每一個單元都是一個zval,存儲這些常量的實際的值,我們可以看到,其值為2和1,與上面的描述相符:
我們可以畫出最終的op_array存儲結構圖:
這樣一來,我們就可以清晰地看出指令在op_array中是如何存儲的。那么接下來,我們需要將其加載到虛擬機的執行棧楨上,來最終執行這些指令。
在虛擬機上執行指令下面讓我們真正執行op_array中的指令,執行指令的入口為zend_execute函數,傳入參數為op_array以及一個zval指針:
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value) { zend_execute_data *execute_data; if (EG(exception) != NULL) { return; } execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE, (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data))); if (EG(current_execute_data)) { execute_data->symbol_table = zend_rebuild_symbol_table(); } else { execute_data->symbol_table = &EG(symbol_table); } EX(prev_execute_data) = EG(current_execute_data); i_init_code_execute_data(execute_data, op_array, return_value); zend_execute_ex(execute_data); zend_vm_stack_free_call_frame(execute_data); }觀察第一行,聲明了一個zend_execute_data類型的指針,這個類型非常重要,存儲了虛擬機執行指令時的基本信息:
struct _zend_execute_data { const zend_op *opline; //當前執行的指令 8B zend_execute_data *call; //指向自己的指針 8B zval *return_value; //存儲返回值 8B zend_function *func; //執行的函數 8B zval This; /* this + call_info + num_args 16B */ zend_execute_data *prev_execute_data; //鏈表,指向前一個zend_execute_data 8B zend_array *symbol_table; //符號表 8B #if ZEND_EX_USE_RUN_TIME_CACHE void **run_time_cache; /* cache op_array->run_time_cache 8B*/ #endif #if ZEND_EX_USE_LITERALS zval *literals; /* cache op_array->literals 8B */ #endif };可以看到,這個zend_execute_data一共是80個字節
隨后執行zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,(zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));這個函數,我們s進去看下:
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object) { uint32_t used_stack = zend_vm_calc_used_stack(num_args, func); return zend_vm_stack_push_call_frame_ex(used_stack, call_info, func, num_args, called_scope, object); }先不看復雜的函數參數,直接看zend_vm_calc_used_stack(num_args, func);這個函數調用,它用來計算虛擬機在執行棧楨上所用的空間,此時應該沒有占用任何空間,我們打印一下used_stack:
發現這里的used_stack果然是0,然后進入下一個if中,繼續執行used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);這個與函數相關,我們還沒有講,那么我們直接看這個函數外層返回的used_stack值,為112B:
那么繼續往下執行zend_vm_stack_push_call_frame_ex(used_stack, call_info,func, num_args, called_scope, object):
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object) { zend_execute_data *call = (zend_execute_data*)EG(vm_stack_top); ZEND_ASSERT_VM_STACK_GLOBAL; if (UNEXPECTED(used_stack > (size_t)(((char*)EG(vm_stack_end)) - (char*)call))) { call = (zend_execute_data*)zend_vm_stack_extend(used_stack); ZEND_ASSERT_VM_STACK_GLOBAL; zend_vm_init_call_frame(call, call_info | ZEND_CALL_ALLOCATED, func, num_args, called_scope, object); return call; } else { EG(vm_stack_top) = (zval*)((char*)call + used_stack); zend_vm_init_call_frame(call, call_info, func, num_args, called_scope, object); return call; } }同樣忽略復雜的函數參數,只關注傳入的used_stack = 112即可。我們首先看第一行:把executor_globals中的vm_stack_top字段賦值給當前的zend_execute_data指向自己的指針,說明zend_execute_data的起始地址為EG這個宏的返回值,查看這個值:
可以看到,zend_execute_data的起始地址為0x7ffff5e1c030,繼續往下執行代碼:
下面的if是用來判斷棧上是否有足夠的空間,如果已經使用的棧空間太多,那么需要重新分配棧空間,顯然我們這里沒有進這個if,說明棧空間還是夠的,那么執行下面的else。重點在于:
EG(vm_stack_top) = (zval*)((char*)call + used_stack);現在這個棧頂的位置變成了0x7ffff5e1c0a0,也就是0x7ffff5e1c030 + 112的結果。至于指針加法步長的運算,本質上就是地址a + 步長 * sizeof(地址類型)(地址類型如果是char *,步長就是1;如果是Int *,步長就是4),舉例子:
int *p; p+3;假如p的地址是0x7ffff5e1c030,那么p+3的結果就應該是0x7ffff5e1c030 + 3 * sizeof(int) = 0x7ffff5e1c03c
我們畫出此時棧上的結構圖:
此時這個返回值call就是棧頂的位置,但是top指針并不指向棧頂,而是指向棧的中間:
接下來回到最外層的zend_execute函數,繼續往下執行:
可以看到,接下來將符號表中的內容賦值給了execute_data中的symbol_table字段,這個符號表是一個zend_array,此時還只有幾個默認的_GET這幾個預先添加的符號,并沒有我們自己的$a:
那么我們繼續往下走,關注i_init_code_execute_data()函數:
static zend_always_inline void i_init_code_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value) /* {{{ */ { ZEND_ASSERT(EX(func) == (zend_function*)op_array); EX(opline) = op_array->opcodes; EX(call) = NULL; EX(return_value) = return_value; zend_attach_symbol_table(execute_data); if (!op_array->run_time_cache) { op_array->run_time_cache = emalloc(op_array->cache_size); memset(op_array->run_time_cache, 0, op_array->cache_size); } EX_LOAD_RUN_TIME_CACHE(op_array); EX_LOAD_LITERALS(op_array); EG(current_execute_data) = execute_data; }這里的EX宏對應全局變量execute_data,EG宏對應全局變量executor_globals,要區分開
重點關注zend_attach_symbol_table(execute_data)函數:
ZEND_API void zend_attach_symbol_table(zend_execute_data *execute_data) /* {{{ */ { zend_op_array *op_array = &execute_data->func->op_array; HashTable *ht = execute_data->symbol_table; /* copy real values from symbol table into CV slots and create INDIRECT references to CV in symbol table */ // 從符號表中拷貝真實的值到CV槽中,并且創建對符號表中CV變量的間接引用 if (EXPECTED(op_array->last_var)) { zend_string **str = op_array->vars; zend_string **end = str + op_array->last_var; zval *var = EX_VAR_NUM(0); do { zval *zv = zend_hash_find(ht, *str); if (zv) { if (Z_TYPE_P(zv) == IS_INDIRECT) { zval *val = Z_INDIRECT_P(zv); ZVAL_COPY_VALUE(var, val); } else { ZVAL_COPY_VALUE(var, zv); } } else { ZVAL_UNDEF(var); zv = zend_hash_add_new(ht, *str, var); } ZVAL_INDIRECT(zv, var); str++; var++; } while (str != end); } }我們此時的符號表只包含_GET這類默認初始化的變量,并不包含我們自己的$a。首先進入if,因為last_var = 1($a),所以將str和end賦值,他們分別指向vars和vars后面1偏移量的位置,如圖:
接下來在符號表ht中遍歷,查找是否有$a這個CV型變量,現在肯定是沒有的,所以進入else分支,執行ZVAL_UNDEF(var)與zv = zend_hash_add_new(ht, *str, var);
上面 EX_VAR_NUM(0)這個宏是一個申請一個CV槽大小的空間,但是在這里我們沒有使用,所以ZVAL_UNDEF(var)將這個槽中的zval類型置為IS_UNDEF類型,然后通過zend_hash_add_new將$a加入到符號表這個zend_array中。那么如果下一次再引用$a的時候,就會走上面的if分支,這樣CV槽就有了用武之地。把$a拷貝到CV槽中,那么在符號表中通過間接引用找到它即可,就不用多次將其加入到符號表中,節省時間與空間。最后將str與var指針的位置往后挪,說明本次遍歷完成
回到i_init_code_execute_data函數,下面幾行是用來操作運行時緩存的代碼,我們暫時跳過,回到zend_execute主函數,接下來會調用zend_execute()函數,在這里真正執行指令所對應的handler邏輯:
賦值操作對應的是ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,我們看看這個handler里具體做了什么:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE zval *value; zval *variable_ptr; SAVE_OPLINE(); //從literals數組中獲取op2對應的值,也就是值2 value = EX_CONSTANT(opline->op2); //在execute_data的符號表中獲取op1的位置,也就是$a variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var); ... //最終將1賦值給$a value = zend_assign_to_variable(variable_ptr, value, IS_CONST); ... }這樣,一個賦值指令就被虛擬機執行完畢,那么還有一個return 1默認的腳本返回值的指令,也是同理,這里不再展開,那么最終的虛擬機執行棧楨的情況如下:
回到zend_execute主函數,最后調用了zend_vm_stack_free_call_frame(execute_data)函數,最終釋放虛擬機占用的棧空間,完畢。
參考資料【PHP7源碼分析】PHP7源碼研究之淺談Zend虛擬機
【PHP7源碼分析】如何理解PHP虛擬機(一)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31649.html
摘要:此文用于匯總跟隨陳雷老師及團隊的視頻,學習源碼過程中的思考整理與心得體會,此文會不斷更新視頻傳送門每日學習記錄使用錄像設備記錄每天的學習源碼學習源碼學習內存管理筆記源碼學習內存管理筆記源碼學習內存管理筆記源碼學習基本變量筆記 此文用于匯總跟隨陳雷老師及團隊的視頻,學習源碼過程中的思考、整理與心得體會,此文會不斷更新 視頻傳送門:【每日學習記錄】使用錄像設備記錄每天的學習 PHP7...
摘要:操作數本身并無數據類型,它的數據類型由操作碼確定任何架構的計算機都會對外提供指令集合運算器通過執行指令直接發出控制信號控制計算機各項操作。 順風車運營研發團隊 李樂 1.從物理機說起 虛擬機也是計算機,設計思想和物理機有很多相似之處; 1.1馮諾依曼體系結構 馮·諾依曼是當之無愧的數字計算機之父,當前計算機都采用的是馮諾依曼體系結構;設計思想主要包含以下幾個方面: 指令和數據不加區別...
摘要:中詞法語法分析,生成抽象語法樹,然后編譯成及被執行均由虛擬機完成。通常情況下這部分是可選部分,主要為便于程序的讀寫方便而使用。指令虛擬機的指令稱為,每條指令對應一個。 作者 陳雷編程語言的虛擬機是一種可以運行中間語言的程序。中間語言是抽象出的指令集,由原生語言編譯而成,作為虛擬機執行階段的輸入。很多語言都實現了自己的虛擬機,比如Java、C#和Lua。PHP語言也有自己的虛擬機,稱為Z...
閱讀 3480·2021-10-13 09:39
閱讀 1463·2021-10-08 10:05
閱讀 2265·2021-09-26 09:56
閱讀 2284·2021-09-03 10:28
閱讀 2681·2019-08-29 18:37
閱讀 2039·2019-08-29 17:07
閱讀 606·2019-08-29 16:23
閱讀 2198·2019-08-29 11:24