本文基于的PHP8 commit为:14806e0824ecd598df74cac855868422e44aea53
首先,zend_vm_opcodes.h这个文件是通过脚本Zend/zend_vm_gen.php来生成的。而zend_vm_gen.php这个脚本依赖zend_vm_def.h和zend_vm_execute.skl来生成文件zend_vm_execute.h和zend_vm_opcodes.h:
1 | +--------------------+ +--------------------+ |
我们以文件zend_vm_gen.php分析的起点,来看看生成zend_vm_opcodes.h的zend_vm_execute.h的关键步骤。
首先,是函数gen_vm。这个函数会逐行扫描zend_vm_def.h里面的代码。
当扫描到ZEND_VM_HELPER的时候,就会执行下面的代码:
1 | if (strpos($line,"ZEND_VM_HELPER(") === 0 || |
这段代码具体的细节我们不去深究,总结起来就是去正则匹配zend_vm_def.h里面当前行的ZEND_VM_HELPER,然后把相关的信息存在全局变量$helpers里面。例如:
1 | ZEND_VM_HELPER(zend_add_helper, ANY, ANY, zval *op_1, zval *op_2) |
然后
1 | else if ($handler !== null) { |
就是去拼接zend_vm_def.h里面的代码。如果是ZEND_VM_HELPER类型的代码,就执行$helpers[$helper]["code"] .= $line;。例如,当拼接完毕的时候,就会得到下面的信息:
1 | ZEND_VM_HELPER(zend_add_helper, ANY, ANY, zval *op_1, zval *op_2) |
当扫描到ZEND_VM_HANDLER的代码之后,就会执行下面的代码:
1 | if (strpos($line,"ZEND_VM_HANDLER(") === 0 || |
这段代码具体的细节我们不去深究,总结起来就是去正则匹配zend_vm_def.h里面当前行的ZEND_VM_HANDLER,然后把相关的信息存在全局变量$opcodes里面。例如:
1 | ZEND_VM_HOT_NOCONSTCONST_HANDLER(1, ZEND_ADD, CONST|TMPVARCV, CONST|TMPVARCV) |
其中
1 | 1 => [ |
实际上就是ZEND_VM_HOT_NOCONSTCONST_HANDLER(1, ZEND_ADD, CONST|TMPVARCV, CONST|TMPVARCV)里面的1和ZEND_ADD,这会用来定义opcode,对应zend_vm_opcodes.h文件里面的:
1 |
1 | "CONST" => 0, |
代表CONST|TMPVARCV的序号。实际上就是:
1 | array_flip(explode("|", CONST|TMPVARCV)) |
之后的结果。
1 | "flags" => 2827 |
计算方法是(CONST|TMPVARCV) | ((CONST|TMPVARCV) << 8)。至于CONST和TMPVARCV的值,我们可以在文件zend_vm_gen.php的变量$vm_op_decode里面找到。
接着,对于ZEND_VM_HANDLER就会执行$opcodes[$handler]["code"] .= $line;了,和ZEND_VM_HELPER的类似。
1 | // Generate opcode #defines (zend_vm_opcodes.h) |
这段代码就很简单了,直接往zend_vm_opcodes.h文件里面写这些内容。
1 | foreach($vm_op_flags as $name => $val) { |
这段代码是把zend_vm_gen.php文件里面的$vm_op_flags内容以16进制的格式写在zend_vm_opcodes.h文件里面:
1 | $vm_op_flags = array( |
接着
1 | foreach ($opcodes as $code => $dsc) { |
会去用我们上面搜集好的$opcodes来定义我们的opcode,例如:
1 |
|
接着
1 | $code = str_pad((string)$max_opcode,$code_len," ",STR_PAD_LEFT); |
会去定义PHP内核一共有多少个opcode,例如:
1 |
至此,我们的zend_vm_opcodes.h文件生成完毕了。接着,开始生成zend_vm_opcodes.c文件。
其中:
1 | fputs($f,"static const char *zend_vm_opcodes_names[".($max_opcode + 1)."] = {\n"); |
用来定义我们所有opcode对应的名字,例如:
1 | static const char *zend_vm_opcodes_names[200] = { |
这个zend_vm_opcodes_names数组的索引实际上就是opcode对应的id。所以,如果我们要得到一个opcode的名字,那么可以通过以下方式拿到:
1 | zend_vm_opcodes_names[ZEND_ADD] |
接着
1 | fputs($f,"static uint32_t zend_vm_opcodes_flags[".($max_opcode + 1)."] = {\n"); |
用来定义opcode对应的flags。例如:
1 | static uint32_t zend_vm_opcodes_flags[200] = { |
flags的值的算法我们已经在上面介绍过了,这里再总结下:
1 | $flags = $flags1 | ($flags2 << 8); |
接着:
1 | fputs($f, "ZEND_API const char* ZEND_FASTCALL zend_get_opcode_name(zend_uchar opcode) {\n"); |
定义一个获取opcode name的函数。生成的结果如下:
1 | ZEND_API const char* ZEND_FASTCALL zend_get_opcode_name(zend_uchar opcode) { |
首先是判断一下是否有这个opcode,有的话返回它的name,没有的话返回NULL。
接着:
1 | puts($f, "ZEND_API uint32_t ZEND_FASTCALL zend_get_opcode_flags(zend_uchar opcode) {\n"); |
定义一个获取opcode flags的函数。生成的结果如下:
1 | ZEND_API uint32_t ZEND_FASTCALL zend_get_opcode_flags(zend_uchar opcode) { |
首先是判断一下是否有这个opcode,有的话返回它的flags,没有的话返回ZEND_NOP的flags(也就是0)。
至此,我们的zend_vm_opcodes.c文件生成完毕了。接着,开始生成zend_vm_execute.h文件。
1 | // Support for ZEND_USER_OPCODE |
用来定义一个zend_user_opcode_handlers数组,这个数组初始的时候全都是NULL。生成结果如下:
1 | static user_opcode_handler_t zend_user_opcode_handlers[256] = { |
接着:
1 | out($f, "static zend_uchar zend_user_opcodes[256] = {"); |
用来定义我们的zend_user_opcodes,生成结果如下:
1 | static zend_uchar zend_user_opcodes[256] = {0, |
说明一共支持256个zend_user_opcodes。
接着,开始调用gen_executor来按照模板文件Zend/zend_vm_execute.skl生成代码。这个函数也是逐行扫描zend_vm_execute.skl文件。
其中zend_vm_execute.skl文件的第一行是:
1 | {%DEFINES%} |
意味着我们在zend_vm_execute.h里面需要生成一些定义。具体的生成过程如下:
1 | out($f,"#define SPEC_START_MASK 0x0000ffff\n"); |
这是一些opcode对应的操作数的规则,例如SPEC_RULE_OP1意味着需要用到操作数1,并且支持的类型至少是2种。对应的代码如下:
1 | if (isset($dsc["op1"]) && !isset($dsc["op1"]["ANY"])) { |
接着:
1 | out($f,"static const uint32_t *zend_spec_handlers;\n"); |
这个是根据ZEND_VM_KIND来定义一些变量和函数,生成结果如下:
1 | static const uint32_t *zend_spec_handlers; |
zend_vm_gen.php默认是ZEND_VM_KIND_HYBRID模式。
接着,会有一大段的代码来定义一些如下宏:
1 | HYBRID_NEXT() |
接着,会调用gen_executor_code来生成opcode的详细handler。例如,我们的操作数有如下类型:
1 | $op_types_ex = array( |
那么,就最大就会有op1_type * op1_type个handler。所以,就会有如下代码:
1 | // Produce specialized executor |
对于这段代码,$list里面存放了所有的helper的名字和opcode的值,例如:
1 | "helper" => "zend_add_helper", |
如果是helper,那么我们从$helpers里面获取到这个helper函数的信息。
如果是handler,那么我们从$opcodes里面获取到这个opcode的信息。
其中:
1 | $opcodes[$num]["op1"] |
里面存放的就是这个opcode对应的操作数1和操作数2支持的所有类型,我们在前面解析的时候就拿到了这些信息。
无论是是helper还是opcode类型的handler,都会调用extra_spec_handler来生成spec函数。在生成spec的时候,会将zend_vm_def.h里面对应的handler的code进行替换,替换的规则在函数gen_code里面。
生成了handler对应的specs之后,就完成了模板文件里面{%DEFINES%}的替换了。
接着,开始替换模板文件里面的{%EXECUTOR_NAME%},也就是开始生成我们的zend_execute函数了:
1 | case "EXECUTOR_NAME": |
这里是名字是execute。
接着替换模板文件的{%HELPER_VARS%}:
1 | case "HELPER_VARS": |
生成结果如下:
1 |
|
接着替换模板文件的{%INTERNAL_LABELS%}:
1 | out($f,$prolog."if (UNEXPECTED(execute_data == NULL)) {\n"); |
这里定义了一个名字叫做labels的静态变量,也就意味着每次调用zend_execute是共享的。生成的代码如下:
1 | if (UNEXPECTED(execute_data == NULL)) { |
也就意味着,当第一次调用zend_execute的时候,会初始化这个labels变量。
接着,我们会生成一堆的HYBRID_SWITCH和HYBRID_CASE。这个和labels变量里面的指针是对应的,并且和我们生成的handler是对应的。我们后面会写一个小demo来解释下这个switch ... case的原理。
接着,会生成$specs:
1 | static const uint32_t specs[] = { |
其中,SPEC_RULE_OP1和SPEC_RULE_OP2解释过了。那么它们前面的数字是什么呢?实际上,前面的数字是第一个当前opcode的第一个spec handler在labels变量的索引。这么说比较抽象,我用下面的图来解释一下:
1 | +-------------+ +-------------+ |
至此,zend_vm_gen.php生成代码的过程结束了。