【PHP原始碼】PHP 函式呼叫
阿新 • • 發佈:2020-04-07
## 想法
我以前對於 C 語言的印象是有很強的確定性,而 PHP 在執行的時候會被翻譯為 C 語言執行,所以一直很好奇 PHP 怎麼呼叫底層函式。
換句話說就是已知函式名字的情況下如何呼叫 C 語言中對應名字的函式?
解決這個問題前,首先根據過往的經驗做出假設,然後再去驗證。
之前在寫《用 C 語言實現面向物件》的時候,就意識到使用 void 指標實現很多功能,包括指向任意的函式。接著在寫《PHP 陣列底層實現》的時候,瞭解了 HashTable 的實現,即在 C 語言層面通過字串 key 找到任意型別值。
現在把兩者結合起來,是否就能解決以上問題了?比如說把函式名作為 HashTable 的 key,函式指標作為 HashTable 的 value,這樣就可以通過函式名獲取函式指標來呼叫函數了。
接下來通過檢視 PHP 的原始碼來看這個假設與真實情況有多少差距。
總體分為三個步驟:
1. 從 PHP 層進入 C 語言層
2. 找到字串函式名與函式的關係
3. 函式的呼叫
注:這篇部落格的原始碼對應的版本是 PHP 7.4.4 。
> [https://github.com/php/php-src/tree/php-7.4.4](https://github.com/php/php-src/tree/php-7.4.4)
## 從 PHP 層進入 C 語言層
首先要找到 C 語言層呼叫函式的地方。怎麼找?
經常使用 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`
```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`
```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;
}
/* }}} */
```
這裡的關鍵點在於和函式名以及函式呼叫相關的詞。關鍵詞有:
- function name
- call
- return value
上面的程式碼片段中,我把幾個有可能的點抽出來了。從這幾個點出發,往前追溯引數來源或者檢視後面使用它的地方就行了。
> 如果被這個函式裡面大量的 `EG(...)` 吸引而想知道其內部結構的話,就離結果非常近了。如果沒有被其吸引,那也沒關係,繼續看。
優先深入看哪個呢?根據以前看陣列原始碼的經驗, “查詢” 這個行為更容易獲得資訊,於是先看 `zend_is_callable_check_func()` 。
`Zend/zend_API.c`
```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`
```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`
```c
# define EG(v) (executor_globals.v)
```
再跳轉一次。
`Zend/zend_compile.c`
```c
ZEND_API zend_executor_globals executor_globals;
```
`zend_executor_globals` 是一個結構體。
PHP 的原始碼中,結構體的真實定義會以下劃線開頭。
於是找 `_zend_executor_globals` 。
`Zend/zend_globals.h`
```c
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 裡面,以實現更多功能。從下面這一句就可以看出。
```c
zval *zv = zend_hash_find(EG(function_table), name);
```
看看 zval 裡面有什麼。
`Zend/zend_types.h`
```c
typedef struct _zval_struct zval;
struct _zval_struct {
zend_value value; /* value */
// ...
};
```
繼續:
`Zend/zend_types.h`
```c
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` 。
```c
union _zend_function {
// ...
zend_internal_function internal_function;
};
```
在 zend_value 聯合體中可以看到 `zend_internal_function` 這個內部函式專用結構體,呼叫內部函式時用到它。搜尋 `_zend_internal_function`。
`Zend/zend_compile.h`
```c
/* 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`
```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);
// ...
}
// ...
}
/* }}} */
```
可以看到呼叫函式的地方:
```c
func->internal_function.handler(call, fci->retval);
```
handler 的引數固定是兩個。這裡要結合之前的 `PHP_FUNCTION(call_user_func)` 來看。
為了將 `PHP_FUNCTION(call_user_func)` 展開,以下連續列出三個定義:
`main/php.h`
```c
#define PHP_FUNCTION ZEND_FUNCTION
```
`Zend/zend_API.h`
```c
#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`
```c
#define INTERNAL_FUNCTION_PARAMETERS zend_execute_data *execute_data, zval *return_value
```
根據這三個地方的程式碼展開 `PHP_FUNCTION(call_user_func)` 可以得到:
```c
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`
```c
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`
```c
// ...
# 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`
```c
#define EX(element) ((execute_data)->element)
```
對 OPLINE 展開後,得到 `execute_data->opline` 。
再根據 `execute_ex()` 前面的定義對整行展開得到:
```c
if (UNEXPECTED((ret = ((opcode_handler_t)(execute_data->opline)->handler)(execute_data)) != 0))
```
現在出現四個新問題:
- opline 的 handler 存在哪個結構體?
- opline 的 handler 指向哪些函式?
- opline 的 handler 在哪裡被賦值?
- 呼叫 opline 的 handler 就真的開始執行函數了嗎?
### opline 的 handler 存在哪個結構體?
要解決這個問題,得先找到 opline 是哪來的。
回到 `Zend/zend_vm_execute.h` 的 `zend_execute()` :
`Zend/zend_vm_execute.h`
```c
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`
```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`
```c
struct _zend_op_array {
// ...
zend_op *opcodes;
// ...
};
```
opcodes 是 zend_op 這種結構體,搜尋 `_zend_op` 。
`Zend/zend_compile.h`
```c
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` 可以找到這些整數和字串的對應。
### opline 的 handler 指向哪些函式?
由於 handler 是函式指標,可以指向任意函式,所以無法直接定位。於是通過除錯執行下面這一句來找一些線索:
`Zend/zend_vm_execute.h`
```c
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`
```c
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`
```c
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 裡面包含的所有函式。
### opline 的 handler 在哪裡被賦值?
上一節列出的 `zend_vm_init()` 把所有函式都放到了 labels 數組裡面,並賦值給了 zend_opcode_handlers ,找找用到它的地方。
`Zend/zend_vm_execute.h`
```c
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`
```c
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);
}
```
### 呼叫 opline 的 handler 就真的開始執行函數了嗎?
把上面舉的例子 handler 指向的函式 `ZEND_INIT_FCALL_SPEC_CONST_HANDLER` 再拿出來。
為了更加明顯,此處不省略程式碼。
`Zend/zend_vm_execute.h`
```c
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`
```c
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`
```c
# 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`
```c
#define ZEND_VM_LOOP_INTERRUPT_CHECK() do { \
if (UNEXPECTED(EG(vm_interrupt))) { \
ZEND_VM_LOOP_INTERRUPT(); \
} \
} while (0)
```
繼續:
`Zend/zend_vm_execute.h`
```c
#define ZEND_VM_LOOP_INTERRUPT() zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
```
繼續:
`Zend/zend_vm_execute.h`
```c
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`
```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`
```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`
```c
#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`
```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 函式執行的大致流程。
雖然瞭解了也沒什麼用的樣子,但好奇心得到了滿