PHP 7词法与语法相关数据结构
PHP 7 的词法和语法分析用到了很多数据结构,最核心的是维护了一个全局变量 compiler_globals
,该变量维护了词法和语法分析的核心数据,同时为了方便存取,定义了 CG
的宏,即 CG(v)
可以存取 compiler_globals
中的成员变量。整个 compiler_globals
占用了 2616 字节,用到了 zend_stack
、zend_ast
、zend_arena
等数据结构。为了后面能够比较好地理解词法和语法分析的过程,本节首先对基础数据结构做一些阐述,为后面的详细过程做一个铺垫。
CG(v)宏
在 PHP 7 源码中,存在一个宏 CG(v)
,取的是 compiler_globals.v
,而 compiler_globals
对应的是 zend_compiler_globals
,其结构如图10-7所示。

从图10-7可以看出,compiler_globals
用到了一些数据结构,下面我们一一分析,然后看各自对应的是什么内容。
-
loop_var_stack
:对应zend_stack
栈,主要用在循环语法的支持上,如while/do while/for/foreach/switch
中。 -
active_class_entry
:对应类的实现。 -
compiled_filename
:编译文件的名称。 -
zend_lineno
:记录编译文件的行号。 -
active_op_array
:对应op_array
,此部分内容会在第 11 章重点阐述。 -
function_table
和class_table
:都是HashTable
,分别存储函数和类的列表。 -
ast
:对应zend_ast
,存放抽象语法树的数组。 -
ast_arena
:对应zend_arena
,用来存放ast
。
zend_stack
CG
多处用到了 zend_stack
,这是一个栈结构,用来存储相关的数据,对应的定义如下:
typedef struct _zend_stack {
int size, top, max;
void *elements;
} zend_stack;
如何理解这个数据结构呢,我们看一下对应的 push 函数和 pop 函数:
ZEND_API int zend_stack_push(zend_stack *stack, const void *element)
{
/* We need to allocate more memory */
if (stack->top >= stack->max) {
stack->max += STACK_BLOCK_SIZE;
stack->elements = safe_erealloc(stack->elements, stack->size, stack->max, 0);
}
memcpy(ZEND_STACK_ELEMENT(stack, stack->top), element, stack->size);
return stack->top++;
}
ZEND_API void *zend_stack_top(const zend_stack *stack)
{
if (stack->top > 0) {
return ZEND_STACK_ELEMENT(stack, stack->top -1);
} else {
return NULL;
}
}

由上面的函数可以看出 zend_stack
结构非常简单,该结构可以存储任意类型的数据,通过 size
来确定每个数据的大小,而 top
指栈顶的位置,max
指栈的最大容量,如图10-8所示。
在图10-8中,虚线代表的不是指针,而是逻辑的位置。我们可以看出,size
代表的是 elements
对应的数据的大小,top
指向下一个可以入栈的位置,max
指的是栈的最大位置。
zend_ast 相关结构
PHP 7 源码根据 AST
中节点子女的个数,定义了 zend_ast
、zend_ast_list
、zend_ast_zval
, zend_ast_znode
以及 zend_ast_decl
,具体结构如图10-9所示。

从图10-9中可以看出,zend_ast
有如下变量。
-
kind
:表示AST
的类型。 -
attr
:后面在AST
转opcodes
时,会细分对应不同的操作,比如当kind=ZEND_AST_BINARY_OP
时,根据attr
对应不同的操作(+
、-
、*
、/
等),示例代码如下:case ZEND_AST_BINARY_OP: switch (ast->attr) { case ZEND_ADD: BINARY_OP(" + ", 200, 200, 201); case ZEND_SUB: BINARY_OP(" - ", 200, 200, 201); case ZEND_MUL: BINARY_OP(" * ", 210, 210, 210); case ZEND_DIV: BINARY_OP(" / ", 210, 210, 210); ……
-
lineno
:代表PHP
文件的行号。
zend_ast
的 child
的个数是与 kind
相关的,其中只有 1
个 child
的 kind
对应的 enum
的值如下:
/* 1 child node */
ZEND_AST_VAR = 256,
ZEND_AST_CONST,
ZEND_AST_UNPACK,
ZEND_AST_UNARY_PLUS,
ZEND_AST_UNARY_MINUS,
ZEND_AST_CAST,
ZEND_AST_EMPTY,
ZEND_AST_ISSET,
ZEND_AST_SILENCE,
ZEND_AST_SHELL_EXEC,
ZEND_AST_CLONE,
ZEND_AST_EXIT,
ZEND_AST_PRINT,
ZEND_AST_INCLUDE_OR_EVAL,
ZEND_AST_UNARY_OP,
ZEND_AST_PRE_INC,
ZEND_AST_PRE_DEC,
ZEND_AST_POST_INC,
ZEND_AST_POST_DEC,
ZEND_AST_YIELD_FROM,
ZEND_AST_GLOBAL,
ZEND_AST_UNSET,
ZEND_AST_RETURN,
ZEND_AST_LABEL,
ZEND_AST_REF,
ZEND_AST_HALT_COMPILER,
ZEND_AST_ECHO,
ZEND_AST_THROW,
ZEND_AST_GOTO,
ZEND_AST_BREAK,
ZEND_AST_CONTINUE,
有 2 个 child
的 kind
对应的 enum
的值如下:
/* 2 child nodes */
ZEND_AST_DIM = 512,
ZEND_AST_PROP,
ZEND_AST_STATIC_PROP,
ZEND_AST_CALL,
ZEND_AST_CLASS_CONST,
ZEND_AST_ASSIGN,
ZEND_AST_ASSIGN_REF,
ZEND_AST_ASSIGN_OP,
ZEND_AST_BINARY_OP,
ZEND_AST_GREATER,
ZEND_AST_GREATER_EQUAL,
ZEND_AST_AND,
ZEND_AST_OR,
ZEND_AST_ARRAY_ELEM,
ZEND_AST_NEW,
ZEND_AST_INSTANCEOF,
ZEND_AST_YIELD,
ZEND_AST_COALESCE,
ZEND_AST_STATIC,
ZEND_AST_WHILE,
ZEND_AST_DO_WHILE,
ZEND_AST_IF_ELEM,
ZEND_AST_SWITCH,
ZEND_AST_SWITCH_CASE,
ZEND_AST_DECLARE,
ZEND_AST_USE_TRAIT,
ZEND_AST_TRAIT_PRECEDENCE,
ZEND_AST_METHOD_REFERENCE,
ZEND_AST_NAMESPACE,
ZEND_AST_USE_ELEM,
ZEND_AST_TRAIT_ALIAS,
ZEND_AST_GROUP_USE,
有 3 个 child
的 kind
对应的 enum
的值如下:
/* 3 child nodes */
ZEND_AST_METHOD_CALL = 768,
ZEND_AST_STATIC_CALL,
ZEND_AST_CONDITIONAL,
ZEND_AST_TRY,
ZEND_AST_CATCH,
ZEND_AST_PARAM,
ZEND_AST_PROP_ELEM,
ZEND_AST_CONST_ELEM,
有 4 个 child
的 kind
对应的 enum
的值如下:
/* 4 child nodes */
ZEND_AST_FOR = 1024,
ZEND_AST_FOREACH,
这样,根据 kind
可以获取到 child
的个数,然后在 child
中存取;在词法和语法分析时,可以根据 child
的个数存入 child[1]
的柔性数组中;同样在后面的 AST
转 Opcodes
的过程中,可以根据 child
的个数从柔性数组中取 child
的值。这部分会在 10.5 节详细展开。
对于不确定子女个数的 kind
,采用 zend_ast_list
结构体,这些 kind
如下:
/* list nodes */
ZEND_AST_ARG_LIST = 128,
ZEND_AST_ARRAY,
ZEND_AST_ENCAPS_LIST,
ZEND_AST_EXPR_LIST,
ZEND_AST_STMT_LIST,
ZEND_AST_IF,
ZEND_AST_SWITCH_LIST,
ZEND_AST_CATCH_LIST,
ZEND_AST_PARAM_LIST,
ZEND_AST_CLOSURE_USES,
ZEND_AST_PROP_DECL,
ZEND_AST_CONST_DECL,
ZEND_AST_CLASS_CONST_DECL,
ZEND_AST_NAME_LIST,
ZEND_AST_TRAIT_ADAPTATIONS,
ZEND_AST_USE,
对于这些 kind
,可以将 zend_ast
强转为 zend_ast_list
,根据 zend_ast_list
中的 children
值确定 child
的个数。
与函数、闭包、方法和类相关的 kind
如下:
/* declaration nodes */
ZEND_AST_FUNC_DECL=66,
ZEND_AST_CLOSURE,
ZEND_AST_METHOD,
ZEND_AST_CLASS,
对于这些 kind
,需要将 zend_ast
强转为 zend_ast_decl
,该结构中的 start_lineno
和 end_lineno
分别是函数等的起始行号和结束行号。
另外还有两个特殊的 kind
,分别对应 zend_ast_zval
和 zend_ast_znode
。代码如下:
/* special nodes */
ZEND_AST_ZVAL = 1 ,
ZEND_AST_ZNODE,
如何理解这几个结构体呢?下面我们举一个例子来理解一下,编写如下 PHP
代码 t.php
:
<?php
$a = 1;
代码非常简单,下面我们看一下生成的 AST
, gdb
如下:
$gdb ./php
(gdb) b zend_compile
Breakpoint 1 at 0x87682f: file Zend/zend_language_scanner.l, line 578.
(gdb) r t.php
Breakpoint 1, zend_compile (type=2) at Zend/zend_language_scanner.l:578
(gdb) n
……
(gdb) n
585 if (! zendparse()) {//这里进行了词法和语法的分析
(gdb) p *compiler_globals.ast
$1 = {kind = 132, attr = 0, lineno = 1, child = {0x1}}
可以看到 kind=132
,对应 ZEND_AST_STMT_LIST
,因此需要将其强转为 zend_ast_list
, gdb
如下:
(gdb) p *(zend_ast_list*)compiler_globals.ast
$2 = {kind = 132, attr = 0, lineno = 1, children = 1, child = {
0x7ffff7c7b088}}
由此可见,kind=132
的节点有 1 个 child
,我们可以输出这个 child
:
(gdb) p *((zend_ast_list*)compiler_globals.ast).child[0]
$3 = {kind = 517, attr = 0, lineno = 2, child = {0x7ffff7c7b060}}
可以看到第一个 child
的 kind
为 517,对应 ZEND_AST_ASSIGN
。从上面的代码中,我们知道 ZEND_AST_ASSIGN
是有两个 child
的 kind
,我们分别输出一下:
(gdb) p *(((zend_ast_list*)compiler_globals.ast).child[0]).child[0]
$4 = {kind = 256, attr = 0, lineno = 2, child = {0x7ffff7c7b048}}
(gdb) p *(((zend_ast_list*)compiler_globals.ast).child[0]).child[1]
$5 = {kind = 64, attr = 0, lineno = 0, child = {0x1}}
对于第二个 child
, kind
是 64,对应 ZEND_AST_ZVAL
,需要强转为 zend_ast_zval
,输出一下:
(gdb) p *(zend_ast_zval*)(((zend_ast_list*)compiler_globals.ast).child[0]).child[1]
$6 = {kind = 64, attr = 0, val = {value = {lval = 1,
dval = 4.9406564584124654e-324, counted = 0x1, str = 0x1, arr = 0x1,
obj = 0x1, res = 0x1, ref = 0x1, ast = 0x1, zv = 0x1, ptr = 0x1,
ce = 0x1, func = 0x1, ww = {w1 = 1, w2 = 0}}, u1 = {v = {
type = 4 '\004', type_flags = 0 '\000', const_flags = 0 '\000',
reserved = 0 '\000'}, type_info = 4}, u2 = {next = 2,
cache_slot = 2, lineno = 2, num_args = 2, fe_pos = 2, fe_iter_idx = 2,
access_flags = 2, property_guard = 2}}}
从上面输出中,我们可以看出 zend_ast_zval
中的 val
是一个 zval
,其中 u1.v.type=IS_LONG
(值为 4),因此 lval=1
,对应 PHP
代码中的 1。
对于第一个 child
, kind
是 256,对应 ZEND_AST_VAR
,有 1 个 child
,我们输出一下:
(gdb) p *(((zend_ast_list*)compiler_globals.ast).child[0]).child[0].child[0]
$7 = {kind = 64, attr = 0, lineno = 0, child = {0x7ffff7c5c700}}
对应的 kind=64
,同样将其强转为 zend_ast_zval
,输出如下:
(gdb) p *(zend_ast_zval*)(((zend_ast_list*)compiler_globals.ast).child[0]).child[0].
child[0]
$8 = {kind = 64, attr = 0, val = {value = {lval = 140737350321920,
dval = 6.9533489880785172e-310, counted = 0x7ffff7c5c700,
str = 0x7ffff7c5c700, arr = 0x7ffff7c5c700, obj = 0x7ffff7c5c700,
res = 0x7ffff7c5c700, ref = 0x7ffff7c5c700, ast = 0x7ffff7c5c700,
zv = 0x7ffff7c5c700, ptr = 0x7ffff7c5c700, ce = 0x7ffff7c5c700,
func = 0x7ffff7c5c700, ww = {w1 = 4156933888, w2 = 32767}}, u1 = {v = {
type = 6 '\006', type_flags = 20 '\024', const_flags = 0 '\000',
reserved = 0 '\000'}, type_info = 5126}, u2 = {next = 2,
cache_slot = 2, lineno = 2, num_args = 2, fe_pos = 2, fe_iter_idx = 2,
access_flags = 2, property_guard = 2}}}
从上面输出中,我们可以看出 zend_ast_zval
中的 val
是一个 zval
,其中 u1.v.type=IS_STRING
(值为 6),我们看一下对应的 value.str
,输出如下:
(gdb) p *($8).val.value.str$10 = {gc = {refcount = 1, u = {v = {type = 6 '\006', flags =
0 '\000',
gc_info = 0}, type_info = 6}}, h = 0, len = 1, val = "a"}
可以看到长度 len=1
,对应的值为 a
,即 PHP 代码中的 $a
。
根据上面的输出过程,我们可以绘制 AST
示意图,如图10-10所示。

这样我们学习了 zend_ast
相关的 5 种数据结构,简单生成了一个 ASSIGN
操作的 AST
,10.5 节会详细阐述 AST
的生成过程。
zend_arena
对于 zend_ast
的存放位置,PHP 7 定义了一个称为 zend_arena
的结构体,其定义如下:
struct _zend_arena {
char *ptr;
char *end;
zend_arena *prev;
};
从定义中可以看出,该结构体非常简单,本身是一个链表,而每个链表会占一大块内存;有两个指针 ptr
和 end
,其中指针 ptr
指向将要使用的内存地址,指针 end
指向内存的最后位置。zend_arena
示意图如图 10-11 所示。

词法、语法分析过程中生成的 AST
节点都会存放在 CG(ast_arena)
。