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