本文基于的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
生成代码的过程结束了。