PHP 7词法与语法相关数据结构

PHP 7 的词法和语法分析用到了很多数据结构,最核心的是维护了一个全局变量 compiler_globals,该变量维护了词法和语法分析的核心数据,同时为了方便存取,定义了 CG 的宏,即 CG(v) 可以存取 compiler_globals 中的成员变量。整个 compiler_globals 占用了 2616 字节,用到了 zend_stackzend_astzend_arena 等数据结构。为了后面能够比较好地理解词法和语法分析的过程,本节首先对基础数据结构做一些阐述,为后面的详细过程做一个铺垫。

CG(v)宏

在 PHP 7 源码中,存在一个宏 CG(v),取的是 compiler_globals.v,而 compiler_globals 对应的是 zend_compiler_globals,其结构如图10-7所示。

image 2024 06 10 17 39 12 722
Figure 1. 图10-7 compiler_globals示意图

从图10-7可以看出,compiler_globals 用到了一些数据结构,下面我们一一分析,然后看各自对应的是什么内容。

  1. loop_var_stack:对应 zend_stack 栈,主要用在循环语法的支持上,如 while/do while/for/foreach/switch 中。

  2. active_class_entry:对应类的实现。

  3. compiled_filename:编译文件的名称。

  4. zend_lineno:记录编译文件的行号。

  5. active_op_array:对应 op_array,此部分内容会在第 11 章重点阐述。

  6. function_tableclass_table:都是 HashTable,分别存储函数和类的列表。

  7. ast:对应 zend_ast,存放抽象语法树的数组。

  8. 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;
    }
}
image 2024 06 10 17 43 23 675
Figure 2. 图10-8 zend_stack示意图

由上面的函数可以看出 zend_stack 结构非常简单,该结构可以存储任意类型的数据,通过 size 来确定每个数据的大小,而 top 指栈顶的位置,max 指栈的最大容量,如图10-8所示。

在图10-8中,虚线代表的不是指针,而是逻辑的位置。我们可以看出,size 代表的是 elements 对应的数据的大小,top 指向下一个可以入栈的位置,max 指的是栈的最大位置。

zend_ast 相关结构

PHP 7 源码根据 AST 中节点子女的个数,定义了 zend_astzend_ast_listzend_ast_zval, zend_ast_znode 以及 zend_ast_decl,具体结构如图10-9所示。

image 2024 06 10 17 44 41 556
Figure 3. 图10-9 zend_ast示意图

从图10-9中可以看出,zend_ast 有如下变量。

  1. kind:表示 AST 的类型。

  2. attr:后面在 ASTopcodes 时,会细分对应不同的操作,比如当 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);
            ……
  3. lineno:代表 PHP 文件的行号。

zend_astchild 的个数是与 kind 相关的,其中只有 1childkind 对应的 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 个 childkind 对应的 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 个 childkind 对应的 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 个 childkind 对应的 enum 的值如下:

/* 4 child nodes */
ZEND_AST_FOR = 1024,
ZEND_AST_FOREACH,

这样,根据 kind 可以获取到 child 的个数,然后在 child 中存取;在词法和语法分析时,可以根据 child 的个数存入 child[1] 的柔性数组中;同样在后面的 ASTOpcodes 的过程中,可以根据 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_linenoend_lineno 分别是函数等的起始行号和结束行号。

另外还有两个特殊的 kind,分别对应 zend_ast_zvalzend_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}}

可以看到第一个 childkind 为 517,对应 ZEND_AST_ASSIGN。从上面的代码中,我们知道 ZEND_AST_ASSIGN 是有两个 childkind,我们分别输出一下:

(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所示。

image 2024 06 10 17 58 22 793
Figure 4. 图10-10 AST示意图

这样我们学习了 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;
};

从定义中可以看出,该结构体非常简单,本身是一个链表,而每个链表会占一大块内存;有两个指针 ptrend,其中指针 ptr 指向将要使用的内存地址,指针 end 指向内存的最后位置。zend_arena 示意图如图 10-11 所示。

image 2024 06 10 17 59 57 672
Figure 5. 图10-11 zend_arena示意图

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

zend_parser_stack_elem

语法分析会用到一个数据结构—— zend_parser_stack_elem,其定义如下:

typedef union _zend_parser_stack_elem {
    zend_ast *ast;
    zend_string *str;
    zend_ulong num;
} zend_parser_stack_elem;

zend_parser_stack_elem 是一个联合体,可以存放 zend_ast 指针或 zend_string 指针,或者 zend_ulong 类型的数据。该结构体在语法分析中用的是 zend_ast* ast