【PHP源碼】PHP 函數調用

想法

我以前對於 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

從 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

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.ccall_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;
}
/* }}} */

這裏的關鍵點在於和函數名以及函數調用相關的詞。關鍵詞有:

  • function name
  • call
  • return value

上面的代碼片段中,我把幾個有可能的點抽出來了。從這幾個點出發,往前追溯參數來源或者查看後面使用它的地方就行了。

如果被這個函數裏面大量的 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 的 handler 存在哪個結構體?
  • opline 的 handler 指向哪些函數?
  • opline 的 handler 在哪裏被賦值?
  • 調用 opline 的 handler 就真的開始執行函數了嗎?

opline 的 handler 存在哪個結構體?

要解決這個問題,得先找到 opline 是哪來的。

回到 Zend/zend_vm_execute.hzend_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 可以找到這些整數和字符串的對應。

opline 的 handler 指向哪些函數?

由於 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 裏面包含的所有函數。

opline 的 handler 在哪裏被賦值?

上一節列出的 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);
}

調用 opline 的 handler 就真的開始執行函數了嗎?

把上面舉的例子 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(&params[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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章