我之前對於 C 語言的印象是有很強的肯定性,而 PHP 在執行的時候會被翻譯爲 C 語言執行,因此一直很好奇 PHP 怎麼調用底層函數。php
換句話說就是已知函數名字的狀況下如何調用 C 語言中對應名字的函數?node
解決這個問題前,首先根據過往的經驗作出假設,而後再去驗證。git
以前在寫《用 C 語言實現面向對象》的時候,就意識到使用 void 指針實現不少功能,包括指向任意的函數。接着在寫《PHP 數組底層實現》的時候,瞭解了 HashTable 的實現,即在 C 語言層面經過字符串 key 找到任意類型值。github
如今把二者結合起來,是否就能解決以上問題了?好比說把函數名做爲 HashTable 的 key,函數指針做爲 HashTable 的 value,這樣就能夠經過函數名獲取函數指針來調用函數了。數組
接下來經過查看 PHP 的源碼來看這個假設與真實狀況有多少差距。app
整體分爲三個步驟:函數
注:這篇博客的源碼對應的版本是 PHP 7.4.4 。oop
首先要找到 C 語言層調用函數的地方。怎麼找?ui
常用 PHP 的同窗看到前面的問題描述很容易聯想到 PHP 中的一個傳入函數名及其參數就能夠調用函數的函數 call_user_func()
。能夠從這裏入手。
怎麼找到 call_user_func()
在 PHP 源碼中的位置?這就要根據 PHP 源碼的規律來找了。
固然也能夠直接全代碼搜索,只是比較慢。
PHP 源碼裏面在定義一個 PHP 函數的時候會用 PHP_FUNCTION(函數名)
,因此只要找到 PHP_FUNCTION(call_user_func)
就能夠了。
另外 call_user_func()
不像 array_column()
這種函數有特定前綴 array_
,因此屬於比較基礎的函數,而 PHP 的基礎函數會放在兩個地方:
Zend/zend_buildin_functions.c
;ext/standard/
。ext/standard/array.c
裏有 array_column()
之類的函數。在這兩個地方搜索就能找到 PHP_FUNCTION(call_user_func)
,以下:
ext/standard/basic_functions.c
PHP_FUNCTION(call_user_func) { // ... if (zend_call_function(&fci, &fci_cache) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) { // ... } }
如今咱們已經從 PHP 層面進入到 C 語言層面,接下去就是在 C 語言代碼裏面探索了。
從上文展現位於 ext/standard/basic_functions.c
的 call_user_func()
函數定義能夠找到關鍵點 zend_call_function()
,如今要找到這個函數。
這種以 zend_
開頭的函數都在 Zend/
文件夾底下,因此咱們要換個目錄了。
在 Zend/
文件夾裏面隨便搜索 zend_call_function
,從搜索結果裏面隨便挑一個跳轉,而後經過 IDE 的功能(ctrl + 鼠標左鍵)跳轉到它定義的地方就能夠了。
若是 IDE 能直接跳轉就不用在
Zend/
文件夾搜索了,這裏是由於 VS Code 無法直接跳轉。
注:如下代碼中的 // ...
都表示我省略了一部分代碼,但我會盡可能保持代碼結構。
第一遍看代碼的時候不須要掌握全部細節,只須要了解總體概念或者先後關係,不然會陷入細節沒法自拔。
Zend/zend_execute_API.c
int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache) /* {{{ */ { // ... if (!fci_cache || !fci_cache->function_handler) { // ... if (!zend_is_callable_ex(&fci->function_name, fci->object, IS_CALLABLE_CHECK_SILENT, NULL, fci_cache, &error)) { // ... } // ... } func = fci_cache->function_handler; // ... call = zend_vm_stack_push_call_frame(call_info, func, fci->param_count, object_or_called_scope); // ... if (func->type == ZEND_USER_FUNCTION) { // ... } else if (func->type == ZEND_INTERNAL_FUNCTION) { // ... func->internal_function.handler(call, fci->retval); // ... } else { // ... } // ... return SUCCESS; } /* }}} */
這裏的關鍵點在於和函數名以及函數調用相關的詞。關鍵詞有:
上面的代碼片斷中,我把幾個有可能的點抽出來了。從這幾個點出發,往前追溯參數來源或者查看後面使用它的地方就好了。
若是被這個函數裏面大量的
EG(...)
吸引而想知道其內部結構的話,就離結果很是近了。若是沒有被其吸引,那也不要緊,繼續看。
優先深刻看哪一個呢?根據之前看數組源碼的經驗, 「查找」 這個行爲更容易得到信息,因而先看 zend_is_callable_check_func()
。
Zend/zend_API.c
static zend_always_inline int zend_is_callable_check_func(int check_flags, zval *callable, zend_fcall_info_cache *fcc, int strict_class, char **error) /* {{{ */ { // ... if (!ce_org) { // ... /* Check if function with given name exists. * This may be a compound name that includes namespace name */ if (UNEXPECTED(Z_STRVAL_P(callable)[0] == '\\')) { // ... func = zend_fetch_function(lmname); // ... } // ... } // ... }
zend_fetch_function()
與咱們想要的答案有很強的相關性,看它怎麼實現的。
Zend/zend_execute.c
ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function(zend_string *name) { zval *zv = zend_hash_find(EG(function_table), name); // ... }
來了來了!在這裏就能夠看到函數的確存在於 HashTable 裏面。而這個 HashTable 經過 EG 獲取。
Zend/zend_globals_macros.h
# define EG(v) (executor_globals.v)
再跳轉一次。
Zend/zend_compile.c
ZEND_API zend_executor_globals executor_globals;
zend_executor_globals
是一個結構體。
PHP 的源碼中,結構體的真實定義會如下劃線開頭。
因而找 _zend_executor_globals
。
Zend/zend_globals.h
struct _zend_executor_globals { // ... HashTable *function_table; /* function symbol table */ HashTable *class_table; /* class table */ HashTable *zend_constants; /* constants table */ // ... }
到這裏就找到存儲函數的地方了。驗證了函數名做爲 key,函數指針做爲 value 的可行性。
不過 PHP 並無把函數指針直接做爲 value,而是包裝到了 zval 裏面,以實現更多功能。從下面這一句就能夠看出。
zval *zv = zend_hash_find(EG(function_table), name);
看看 zval 裏面有什麼。
Zend/zend_types.h
typedef struct _zval_struct zval; struct _zval_struct { zend_value value; /* value */ // ... };
繼續:
Zend/zend_types.h
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 { uint32_t w1; uint32_t w2; } ww; } zend_value;
注:這個結構體很重要,我保留了全貌。
看到 zend_function
這個結構體,搜索 _zend_function
。
union _zend_function { // ... zend_internal_function internal_function; };
在 zend_value 聯合體中能夠看到 zend_internal_function
這個內部函數專用結構體,調用內部函數時用到它。搜索 _zend_internal_function
。
Zend/zend_compile.h
/* zend_internal_function_handler */ typedef void (ZEND_FASTCALL *zif_handler)(INTERNAL_FUNCTION_PARAMETERS); typedef struct _zend_internal_function { /* Common elements */ zend_uchar type; zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */ uint32_t fn_flags; zend_string* function_name; zend_class_entry *scope; zend_function *prototype; uint32_t num_args; uint32_t required_num_args; zend_internal_arg_info *arg_info; /* END of common elements */ zif_handler handler; struct _zend_module_entry *module; void *reserved[ZEND_MAX_RESERVED_RESOURCES]; } zend_internal_function;
結構體 _zend_internal_function
裏面的 handler 成員是 zif_handler
類型。 從前面的定義能夠知道 zif_handler
是一個函數指針類型,這就是用來存函數指針的地方。
如今知道函數指針是存放在 handler 裏面了,接着就是找到使用它的地方。
此時再回過頭看 zend_call_function
這個函數。
Zend/zend_execute_API.c
int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache) /* {{{ */ { // ... if (func->type == ZEND_USER_FUNCTION) { // ... } else if (func->type == ZEND_INTERNAL_FUNCTION) { // ... func->internal_function.handler(call, fci->retval); // ... } // ... } /* }}} */
能夠看到調用函數的地方:
func->internal_function.handler(call, fci->retval);
handler 的參數固定是兩個。這裏要結合以前的 PHP_FUNCTION(call_user_func)
來看。
爲了將 PHP_FUNCTION(call_user_func)
展開,如下連續列出三個定義:
main/php.h
#define PHP_FUNCTION ZEND_FUNCTION
Zend/zend_API.h
#define ZEND_FN(name) zif_##name #define ZEND_MN(name) zim_##name #define ZEND_NAMED_FUNCTION(name) void ZEND_FASTCALL name(INTERNAL_FUNCTION_PARAMETERS) #define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name))
Zend/zend.h
#define INTERNAL_FUNCTION_PARAMETERS zend_execute_data *execute_data, zval *return_value
根據這三個地方的代碼展開 PHP_FUNCTION(call_user_func)
能夠獲得:
void ZEND_FASTCALL call_user_func(zend_execute_data *execute_data, zval *return_value)
再看一次 func->internal_function.handler(call, fci->retval);
。聯繫起來了!
上文以 PHP_FUNCTION(call_user_func)
做爲入口只是其中一種思路。實際上 PHP 在調用函數的時候不是經過 call_user_func
,否則 call_user_func
自己又是如何被調用的呢?
PHP 執行的時候,會在 PHP 虛擬機裏面去調用函數。PHP 虛擬機首先會讀取 PHP 文件,而後解析爲 OPCode (操做碼)執行。這裏就要藉助調試器的力量了。
這裏跳過 OPCode 的生成,由於與本次要探索的內容關係不是很大。
開啓調試。而後不斷往下走,能夠找到一個比較接近答案的地方。
Zend/zend_vm_execute.h
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value) { zend_execute_data *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_ex
:
Zend/zend_vm_execute.h
// ... # define OPLINE EX(opline) // ... # define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU execute_data // ... ZEND_API void execute_ex(zend_execute_data *ex) { DCL_OPLINE // ... zend_execute_data *execute_data = ex; // ... LOAD_OPLINE(); ZEND_VM_LOOP_INTERRUPT_CHECK(); // ... while (1) { // ... int ret; // ... if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) { // ... if (EXPECTED(ret > 0)) { execute_data = EG(current_execute_data); ZEND_VM_LOOP_INTERRUPT_CHECK(); } else { // ... return; } // ... } } zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen"); }
又看到了 handler,這裏難道就是真正執行函數的地方?
先找到 OPLINE 的真身,根據:
Zend/zend_compile.h
#define EX(element) ((execute_data)->element)
對 OPLINE 展開後,獲得 execute_data->opline
。
再根據 execute_ex()
前面的定義對整行展開獲得:
if (UNEXPECTED((ret = ((opcode_handler_t)(execute_data->opline)->handler)(execute_data)) != 0))
如今出現四個新問題:
要解決這個問題,得先找到 opline 是哪來的。
回到 Zend/zend_vm_execute.h
的 zend_execute()
:
Zend/zend_vm_execute.h
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value) { zend_execute_data *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_ex()
前面有個 i_init_code_execute_data()
:
Zend/zend_execute.c
static zend_always_inline void i_init_code_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value) /* {{{ */ { // ... EX(opline) = op_array->opcodes; // ... }
opline 來自於 zend_op_array 的 opcodes ,搜索 _zend_op_array
。
Zend/zend_compile.h
struct _zend_op_array { // ... zend_op *opcodes; // ... };
opcodes 是 zend_op 這種結構體,搜索 _zend_op
。
Zend/zend_compile.h
struct _zend_op { const void *handler; znode_op op1; znode_op op2; znode_op result; uint32_t extended_value; uint32_t lineno; zend_uchar opcode; zend_uchar op1_type; zend_uchar op2_type; zend_uchar result_type; };
到這裏就找到了 handler 存儲的位置。
注:在
Zend/zend_vm_opcodes.h
能夠找到 OPCode 對應的整數,在Zend/zend_vm_opcodes.c
能夠找到這些整數和字符串的對應。
因爲 handler 是函數指針,能夠指向任意函數,因此沒法直接定位。因而經過調試執行下面這一句來找一些線索:
Zend/zend_vm_execute.h
ZEND_API void execute_ex(zend_execute_data *ex) { // ... while (1) { // ... if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) { // ... } } // ... }
在這一句的位置使用 「jump into」,會跳轉到一個函數。這個函數就是 handler 指向的函數了。
因爲每次跳到的函數均可能不同,因此選其中一個來查。
Zend/zend_vm_execute.h
static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_FCALL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { // ... }
搜索函數名 ZEND_INIT_FCALL_SPEC_CONST_HANDLER
。
Zend/zend_vm_execute.h
void zend_vm_init(void) { static const void * const labels[] = { // ... ZEND_INIT_FCALL_SPEC_CONST_HANDLER, // ... }; static const uint32_t specs[] = { // ... }; // ... zend_opcode_handlers = labels; zend_handlers_count = sizeof(labels) / sizeof(void*); zend_spec_handlers = specs; // ... }
handler 能夠指向 labels 裏面包含的全部函數。
上一節列出的 zend_vm_init()
把全部函數都放到了 labels 數組裏面,並賦值給了 zend_opcode_handlers ,找找用到它的地方。
Zend/zend_vm_execute.h
static const void* ZEND_FASTCALL zend_vm_get_opcode_handler_ex(uint32_t spec, const zend_op* op) { // ... return zend_opcode_handlers[(spec & SPEC_START_MASK) + offset]; }
若是搜索調用 zend_vm_get_opcode_handler_ex
的代碼,那麼就很容易找到給 handler 賦值的地方了。
Zend/zend_vm_execute.h
ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler(zend_op* op) { // ... op->handler = zend_vm_get_opcode_handler_ex(zend_spec_handlers[opcode], op); }
把上面舉的例子 handler 指向的函數 ZEND_INIT_FCALL_SPEC_CONST_HANDLER
再拿出來。
爲了更加明顯,此處不省略代碼。
Zend/zend_vm_execute.h
static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_FCALL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE zval *fname; zval *func; zend_function *fbc; zend_execute_data *call; fbc = CACHED_PTR(opline->result.num); if (UNEXPECTED(fbc == NULL)) { fname = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_ex(EG(function_table), Z_STR_P(fname), 1); if (UNEXPECTED(func == NULL)) { ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); } fbc = Z_FUNC_P(func); if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { init_func_run_time_cache(&fbc->op_array); } CACHE_PTR(opline->result.num, fbc); } call = _zend_vm_stack_push_call_frame_ex( opline->op1.num, ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); EX(call) = call; ZEND_VM_NEXT_OPCODE(); }
從中看不到執行的地方。找到的 func 也只是被放入 fcb,而後 push 到虛擬機調用棧裏面。
注:這裏另外一個值得注意的地方是
ZEND_VM_NEXT_OPCODE();
。由於最開始的execute_ex
函數(下一節列出了代碼)裏面只是一個死循環,且沒有修改 OPLINE 的指向,而是在這些 handler 函數裏面修改。
那真正調用函數的地方在哪呢?
回到最開始的 execute_ex()
。
Zend/zend_vm_execute.h
ZEND_API void execute_ex(zend_execute_data *ex) { DCL_OPLINE // ... zend_execute_data *execute_data = ex; // ... LOAD_OPLINE(); ZEND_VM_LOOP_INTERRUPT_CHECK(); // ... while (1) { // ... int ret; // ... if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) { // ... if (EXPECTED(ret > 0)) { execute_data = EG(current_execute_data); ZEND_VM_LOOP_INTERRUPT_CHECK(); } else { // ... return; } // ... } } zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen"); }
經過調試能夠知道,若是是一些簡單的操做, handler 就會直接處理。好比加減法。可是像函數調用這種,就不會在 handler 這裏處理。
那麼只能看下面的代碼。
只有當 ret 大於 0 的時候會有額外的操做。經過調試能夠看到有如下幾個大於 0 的狀況。
Zend/zend_vm_execute.h
# define ZEND_VM_ENTER_EX() return 1 # define ZEND_VM_ENTER() return 1 # define ZEND_VM_LEAVE() return 2
這個信息沒有多大影響。
那麼接下來就得看 ZEND_VM_LOOP_INTERRUPT_CHECK();
了。
Zend/zend_execute.c
#define ZEND_VM_LOOP_INTERRUPT_CHECK() do { \ if (UNEXPECTED(EG(vm_interrupt))) { \ ZEND_VM_LOOP_INTERRUPT(); \ } \ } while (0)
繼續:
Zend/zend_vm_execute.h
#define ZEND_VM_LOOP_INTERRUPT() zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
繼續:
Zend/zend_vm_execute.h
static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) { EG(vm_interrupt) = 0; if (EG(timed_out)) { zend_timeout(0); } else if (zend_interrupt_function) { SAVE_OPLINE(); zend_interrupt_function(execute_data); ZEND_VM_ENTER(); } ZEND_VM_CONTINUE(); }
搜索 zend_interrupt_function
發現它是一個函數指針。那麼轉成搜索 zend_interrupt_function =
,看看哪一個函數的指針傳給了它。
這時搜索到了兩條線。一條是 ext/pcntl/pcntl.c
,另外一條是 win32/signal.c
。
這裏選 win32/signal.c
:
win32/signal.c
PHP_WINUTIL_API void php_win32_signal_ctrl_handler_init(void) {/*{{{*/ // ... zend_interrupt_function = php_win32_signal_ctrl_interrupt_function; // ... }/*}}}*/
接着找函數 php_win32_signal_ctrl_interrupt_function
。
win32/signal.c
static void php_win32_signal_ctrl_interrupt_function(zend_execute_data *execute_data) {/*{{{*/ if (IS_UNDEF != Z_TYPE(ctrl_handler)) { zval retval, params[1]; ZVAL_LONG(¶ms[0], ctrl_evt); /* If the function returns, */ call_user_function(NULL, NULL, &ctrl_handler, &retval, 1, params); zval_ptr_dtor(&retval); } if (orig_interrupt_function) { orig_interrupt_function(execute_data); } }/*}}}*/
感受很接近了。
call_user_function
傳了兩個 NULL,爲了不理解上有誤差,把它的定義列出來。
Zend/zend_API.h
#define call_user_function(function_table, object, function_name, retval_ptr, param_count, params) \ _call_user_function_ex(object, function_name, retval_ptr, param_count, params, 1)
繼續:
Zend/zend_execute_API.c
int _call_user_function_ex(zval *object, zval *function_name, zval *retval_ptr, uint32_t param_count, zval params[], int no_separation) /* {{{ */ { zend_fcall_info fci; fci.size = sizeof(fci); fci.object = object ? Z_OBJ_P(object) : NULL; ZVAL_COPY_VALUE(&fci.function_name, function_name); fci.retval = retval_ptr; fci.param_count = param_count; fci.params = params; fci.no_separation = (zend_bool) no_separation; return zend_call_function(&fci, NULL); }
繞了一圈仍是繞回來了。又一次見到 zend_call_function
。上文已經分析過這個函數了,再也不重複。
本文經過假設 PHP 函數調用方式和查詢源碼驗證,獲得了 PHP 底層將 C 語言函數存儲到 HashTable 而後經過函數名字找到函數指針來調用這一結論。同時也瞭解了 PHP 函數執行的大體流程。
雖然瞭解了也沒什麼用的樣子,但好奇心獲得了知足 233