发现自己看过php 虚拟机一些部分好多都忘了(,最近需要根据php AST 写一个自己的IR,来分析一些东西,于是看看原本的php 原本的Interpreter是怎么翻译的,找找感觉,以后可能也经常发一些有趣的php ast里面节点的翻译过程,以便自己忘记了看,也分享给大家.... 感觉近几年static analysis 会大火 0),很多人越来越关注PL了。不能掉队...
0x01 基本AST如下
+-----------------+
| zend_ast_assign |
+-----------------+
/- --\
/--- -----\
+--------------+ +-----------+
| zend_ast_dim | | 111 |
+--------------+ +-----------+
/--- --\
/------- ---\
+------+ +-------+
| $a | | 1 |
+------+ +-------+
注意const 和 cv 都是单纯的znode 基本操作数
0x02 将AST转换成IR
过程中我探究过的问题:
-
CG里面有一个delayed_oplines_stack ,没有细究在做个别节点翻译时会用到这个栈,最后把这个栈里面的内容都复制到真正的opline_array 里面,知道这里为什么吗?我想了一会为什么这样,我自己也用双栈在某些地方,我用双栈是为了部分计数,他这里其实是为了维护某些opcode的顺序:
$b="m"; $c="c"; $a[$b.$c]=$b.$c; //看上面的例子,关注第三行 //直觉上可以先翻译等式左边,拿到一个目标引用,而后再翻译等式右边得到一个值引用,这里我们假设这里没有delayed_oplines_stack这个,会是什么效果? //ASSIGN !0, 'm' //1 //ASSIGN !1, 'c' //2 //CONCAT ~5 !0, !1 //3 //ASSIGN_DIM !2, ~5 //4 //CONCAT ~7 !0, !1 //5 //OP_DATA 111 //6 //这里就出现问题了,第4条需要和第6应该要紧密结合在一起,语义出现问题了 //所以这里在一开始就把ASSIGN_DIM先放到delayed_oplines_stack,等到等式两边翻译结束,在把它拿出来,最后加一个OP_DATA. //有点绕,可能现在知道,以后又忘了 ((
-
在翻译zend_ast_dim中,当结束翻译zend_ast_dim 会产生一条opcode 为
ZEND_FETCH_DIM_W
三地址。(zend_delayed_compile_dim) -
ZEND_FETCH_DIM_W
会变成ZEND_ASSIGN_DIM
. -
最后还有一条
OP_DATA
完整IR:
ASSIGN_DIM !1, 1
OP_DATA 111
0x03 如何确定每条IR的handler
这里我php默认用handler调度是 hybrid模式,hybrid模式是goto 和 switch的混合,hybird模式下每条IR的handler是具体的labels地址, 供goto使用。
-
Hybrid模式的初始化,记录一下:
void zend_vm_init(){ ... #if (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) zend_opcode_handler_funcs = labels; zend_spec_handlers = specs; execute_ex(NULL); //注意 这里执行了一个空参数的execute_ex } void execute_ex(zend_execute_data *ex){ ... #if (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) if (UNEXPECTED(execute_data == NULL)) {//对应上前面的NULL为了这里的初始化 labels[]={ ... } zend_opcode_handlers = (const void **) labels; zend_handlers_count = sizeof(labels) / sizeof(void*); memset(&hybrid_halt_op, 0, sizeof(hybrid_halt_op)); hybrid_halt_op.handler = (void*)&&HYBRID_HALT_LABEL; goto HYBRID_HALT_LABEL; //直接退出 } }
-
记录一下遗忘的handler映射寻址
ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler(zend_op* op) { ... op->handler = zend_opcode_handlers[zend_vm_get_opcode_handler_idx(zend_spec_handlers[opcode], op)]; } //zend_opcode_handlers 已经初始化了(goto的labels),剩下的工作就是计算索引 //zend_spec_handlers[opcode] 确定一个具体opcode handler相对位置,并且确定这个opcode 是几操作数敏感的 和 一些额外的属性 //比如这里的zend_assign_dim => 736 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_OP_DATA uint32_t ZEND_FASTCALL zend_vm_get_opcode_handler_idx(uint32_t spec, const zend_op* op){ static const int zend_vm_decode[] = { _UNUSED_CODE, /* 0 = IS_UNUSED */ _CONST_CODE, /* 1 = IS_CONST */ _TMP_CODE, /* 2 = IS_TMP_VAR */ _UNUSED_CODE, /* 3 */ _VAR_CODE, /* 4 = IS_VAR */ _UNUSED_CODE, /* 5 */ _UNUSED_CODE, /* 6 */ _UNUSED_CODE, /* 7 */ _CV_CODE /* 8 = IS_CV */ };//0 1 2 4 8 做了一个小映射到0-4 uint32_t offset = 0; if (spec & SPEC_RULE_OP1) offset = offset * 5 + zend_vm_decode[op->op1_type]; if (spec & SPEC_RULE_OP2) offset = offset * 5 + zend_vm_decode[op->op2_type]; // 5*5 笛卡尔积 if (spec & SPEC_EXTRA_MASK) { ... } else if (spec & SPEC_RULE_OP_DATA) { //这里zend_assign_dim 考虑后一个OP_DATA的属性来计算索引 offset = offset * 5 + zend_vm_decode[(op + 1)->op1_type]; } .... } return (spec & 0xffff) + offset; //opcode 相对位置 + offset //done }