php ast opcode - yaokun123/php-wiki GitHub Wiki
php使用的re2c在:Zend/zend_language_scanner.l
搜索re2c,查看设置的正则,发现每个关键字都返回一个token
token 在 Zend/zend_language_parser.c中定义
php使用的bison在:Zend/zend_language_parser.y
入口函数:
main->php_module_startup->zend_startup函数中设置了编译与执行入口
zend_compile_file = compile_file;
zend_execute_ex = execute_ex;
php_execute_script->zend_execute_scripts->zend_compile_file(compile_file)
compile_file->zendparse(); // 进行词法语法解析生成AST
// yyparse() 不断调用 yylex() 得到token,然后根据token匹配语法规则
compile_file->zend_compile_top_stmt(CG(ast)); // 解析AST生成opcode
compile_file->pass_two(op_array); // 为opcade设置handler
php_execute_script->zend_execute_scripts->zend_execute(op_array, retval);
二、AST相关数据结构
1、_zend_ast
2、_zend_ast_list
3、_zend_ast_zval
4、_zend_ast_decl
具体定义在Zend/zend_ast.h文件中
_zend_ast
struct _zend_ast {
zend_ast_kind kind; /* Type of the node (ZEND_AST_* enum constant) */
zend_ast_attr attr; /* Additional attribute, use depending on node type */
uint32_t lineno; /* Line number */
zend_ast *child[1]; /* Array of children (using struct hack) */
};
//child的个数由kind确定—_zend_ast_kind
//对于_zend_ast_kind中定义的list nodes需要强转为_zend_ast_list
_zend_ast_list
typedef struct _zend_ast_list {
zend_ast_kind kind;
zend_ast_attr attr;
uint32_t lineno;
uint32_t children;//比_zend_ast多了个children,记录child的数量
zend_ast *child[1];
} zend_ast_list;
_zend_ast_zval
typedef struct _zend_ast_zval {
zend_ast_kind kind;
zend_ast_attr attr;
zval val;//
} zend_ast_zval;
_zend_ast_decl
//函数和类的定义
/* Separate structure for function and class declaration, as they need extra information. */
typedef struct _zend_ast_decl {
zend_ast_kind kind;
zend_ast_attr attr; /* Unused - for structure compatibility */
uint32_t start_lineno;
uint32_t end_lineno;
uint32_t flags;
unsigned char *lex_pos;
zend_string *doc_comment;
zend_string *name;
zend_ast *child[4];
} zend_ast_decl;
三、调试
1、编辑源文件
vim index.php
<?php
$a = 1;
2、gdb调试(重点词法解析)
> gdb php
> b php_execute_script //词法和语法分析发生在生命周期中的执行阶段
> b open_file_for_scanning //第一步先要读取要解析的php文件
> b zendparse //语法分析的入口
> b zendlex //词法分析的入口(语法分析时会调用词法分析)
> run index.php //开始调试源文件
> bt //查看一下堆栈
> p *primary_file //查看php_execute_script函数传入的文件指针
> c //进入下一个断点open_file_for_scanning
> bt //查看一下堆栈
> n //下一步
> p *file_handle //打印文件
> c //进入下一个断点zendparse
> p language_scanner_globals//在open_file_for_scanning阶段会设置全局变量language_scanner_globals
//yy_cursor = 0x0000000100ef7000 "<?php\n$a = 1;\n"
//光标指向文件的头部,下面可以通过这个yy_cursor来查看解析过程
> c //进入下一个断点zendlex,进行词法分析
> n //下一步,ZVAL_UNDEF(&zv),词法分析部分要先创建一个undefine zval
> n //下一步,retval = lex_scan(&zv);
> b lex_scan //在词法分析的入口打个断点
> s //进入lex_scan函数
> p language_scanner_globals//全局变量language_scanner_globals
> n...... //使用n可以查看词法解析过程,注意观察yy_cursor
//匹配顺序:< ? p h p \n
//RETURN_TOKEN(T_OPEN_TAG);返回一个tag的token
> p language_scanner_globals//yy_cursor = 0x0000000100ef7006 "$a = 1;\n"
//此时已经解析了<?php
> c //继续lex_scan函数
> n...... //使用n可以查看词法解析过程,注意观察yy_cursor
//匹配顺序:$ a
//RETURN_TOKEN(T_VARIABLE);返回一个tag的token
> p language_scanner_globals//yy_cursor = 0x0000000100ef7008 " = 1;\n"
//此时已经解析了$a
> c //继续lex_scan函数
> n...... //使用n可以查看词法解析过程,注意观察yy_cursor
//匹配顺序:' '
//RETURN_TOKEN(T_WHITESPACE);返回一个tag的token
> p language_scanner_globals//yy_cursor = 0x0000000100ef7009 "= 1;\n"
//此时已经解析了' '
> c //继续lex_scan函数
> n...... //使用n可以查看词法解析过程,注意观察yy_cursor
//匹配顺序:=
//RETURN_TOKEN(yytext[0]);返回一个tag的token
> p language_scanner_globals//yy_cursor = 0x0000000100ef700a " 1;\n"
//此时已经解析了=
...相同步骤再解析出' '(RETURN_TOKEN(T_WHITESPACE);)
...相同步骤再解析出1(RETURN_TOKEN(T_LNUMBER);)
...相同步骤再解析出;(RETURN_TOKEN(yytext[0]);)
...相同步骤再解析出\n(RETURN_TOKEN(T_WHITESPACE);)
3、调试语法解析(重点语法解析)
> gdb php
> b zendparse //语法分析的入口
> b zendlex //词法分析的入口(语法分析时会调用词法分析)
> run index.php //开始调试源文件
> p language_scanner_globals//yy_cursor = 0x0000000100ef4000 "<?php\n$a = 1;\n
//Zend/zend_language_parser.c 4749行创建ast的过程
> b 4749 //如果不需要查看详细过程,只要ast结果,可以不要这个断点
> c //进入4749断点
> p yyn //(int) $1 = 80
> n
> c
> p language_scanner_globals//yy_cursor = 0x0000000100ef4000 "<?php\n$a = 1;\n"
> c
> p yyn //(int) $3 = 439
.........
//这个ast存在了CG(ast)下/compiler_globals
p compiler_globals.ast
四、Opcode相关数据结构
1、_zend_op:相当于指令
2、_zend_op_array:相当于指令集
3、_zend_execute_data:要在zend虚拟机上运行的
4、_zend_vm_stack:虚拟机的堆栈
_zend_op
struct _zend_op {
const void *handler;//指令执行handler
znode_op op1;//操作数1
znode_op op2;//操作数2
znode_op result;//返回值
uint32_t extended_value;
uint32_t lineno;
zend_uchar opcode;//opcode指令
zend_uchar op1_type;//操作数1类型
zend_uchar op2_type;//操作数2类型
zend_uchar result_type;//返回值类型
};
typedef union _znode_op {
uint32_t constant;
uint32_t var;
uint32_t num;
uint32_t opline_num; /* Needs to be signed */
} znode_op;
_zend_op_array
struct _zend_op_array {
/* Common elements common是普通函数或类成员方法对应的opcodes快速访问时使用的字段*/
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_arg_info *arg_info;
/* END of common elements */
uint32_t *refcount;
uint32_t this_var;
uint32_t last; //指令的数量
zend_op *opcodes; //opcode指令数组
int last_var; //PHP代码里定义的变量数:op_type为IS_CV的变量,不含IS_TMP_VAR、IS_VAR的//编译前此值为0,然后发现一个新变量这个值就加1
uint32_t T;//临时变量数:op_type为IS_TMP_VAR、IS_VAR的变量
zend_string **vars;//PHP变量名数组 //这个数组在ast编译期间配合last_var用来确定各个变量的编号,非常重要的一步操作
int last_brk_cont;
int last_try_catch;
zend_brk_cont_element *brk_cont_array;
zend_try_catch_element *try_catch_array;
/* static variables support */
HashTable *static_variables;//静态变量符号表:通过static声明的
zend_string *filename;
uint32_t line_start;
uint32_t line_end;
zend_string *doc_comment;
uint32_t early_binding; /* the linked list of delayed declarations */
int last_literal;//字面量数量
zval *literals;//字面量(常量)数组,这些都是在PHP代码定义的一些值
int cache_size;//运行时缓存数组大小
void **run_time_cache;//运行时缓存,主要用于缓存一些znode_op以便于快速获取数据
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};
_zend_execute_data
struct _zend_execute_data {
const zend_op *opline; //executed opline
zend_execute_data *call; //current call
zval *return_value;
zend_function *func;
zval This;
zend_class_entry *called_scope;
zend_execute_data *prev_execute_data;
zend_array *symbol_table; //符号表
#if ZEND_EX_USE_RUN_TIME_CACHE
void **run_time_cache;
#endif
#if ZEND_EX_USE_LITERALS
zval *literals;
#endif
}
_zend_vm_stack
struct _zend_vm_stack {
zval *top;
zval *end;
zend_vm_stack prev;
};
五、AST编译成opcode的过程
> gdb php
> b zendparse
> run index.php
//> b zend_compile_top_stmt
//> c
//> p *ast
//> b zend_compile_stmt
//> c
> finish
> n... //CG(active_op_array) = op_array;
> p compiler_globals.active_op_array
> n... //pass_two(op_array);会进行handler的赋值操作
> p *op_array //last = 2,opcodes = 0x0000000101a5c400
> p $4.opcodes[0] //handler = 0x0000000000000000,op1= 96,op2=0,result=0,opcode=&(38)代表assign操作
> p $4.opcodes[1] //handler = 0x0000000000000000,op1= 1,op2=0,result=0,opcode=>(62)代表return操作
> n //执行pass_two,进行handler的赋值操作
> p.... //此时的handler已经被赋值了
六、Zend VM
> b zend_execute
> bt
> p *op_array //last = 2
> p $1.opcodes[0] //handler=ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,op1=80,op2=0,result=0
//op1=80,80就涉及到堆栈的信息
> p sizeof(zend_execute_data) //80,zend_execute_data结构体的大小就是80
//对于一个虚拟机来说,最顶部放的就是当前执行的execute_data
//op1占用了一个zval大小(16k),起始位置就是80,这就是80的来历
//op1要进行压栈操作
> n n //execute_data = zend_vm_stack_push_call_frame(...将execute_data压栈
> n ...n //zend_execute_ex(execute_data);
> s
> n //((opcode_handler_t)OPLINE->handler)(..调用handler
> s //ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER
> n //value = EX_CONSTANT(opline->op2);取出op2
> n
> p *value //1