1

前言

php 使用 lex 和 bison 进行语法分析和词法分析,本文以 bison 语法定义文件为起点,使用 find, grep 等命令行工具搜索相关源码,以此来展示探索 PHP 语法分析源码思路

bison 语法定义文件

在 源代码 根目录下通过 find 命令查找 *.y 文件

# find . -name *.y
./sapi/phpdbg/phpdbg_parser.y
./ext/json/json_parser.y
./Zend/zend_ini_parser.y
./Zend/zend_language_parser.y

我们找到了 zend_language_parser.y 文件,里面定义了 PHP 脚本 的语法

语法分析树(AST)

AST 节点类型: YYSTYPE

在查看具体的语法规则前,我们先看看 PHP 使用什么样的数据结构表示 AST 根节点,使用 grep 命令 搜索 YYSTYPE

# grep -rin --color --include=*.h --include=*.c "#define YYSTYPE"
Zend/zeng_language_parser.c:108:#define YYSTYPE zend_parser_stack_elem

zend_parser_stack_elem

grep zend_parser_stack_elem 结构体定义

# grep -rin --color --include=*.h --include=*.c "zend_parser_stack_elem"
Zend/zend_compile.h:126:typedef union _zend_parser_stack_elem

打开 zend_compile.h 文件

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

zend_parser_stack_elem 是一个联合体,一个 AST 节点可能是 num(数值),str(字符串)或者 ast(非终结符)

zend_ast

搜索 _zend_ast 结构体定义

# grep -rin --color --include=*.h --include=*.c _zend_ast *
Zend/zend_ast.h:153:struct _zend_ast {

打开 zend_ast.h 文件

struct _zend_ast {
    zend_ast_kind kind;
    zend_ast_attr attire;
    uint32_t linen;
    zend_ast *child[1];
}
kind

Type of the node(ZEND_AST_* enum constant)
zend_ast.h 文件头部 enum 枚举类型包含了各个 ZEND_AST_* 定义

enum _zend_ast_kind {
    /* special nodes */
    ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT,
    ZEND_AST_ZNODE,

    /* declaration nodes */
    ZEND_AST_FUNC_DECL,
    ZEND_AST_CLOSURE,
    ZEND_AST_METHOD,
    ZEND_AST_CLASS,
    ...
}
attr

Additional attribute,use depending on node type

linen

Line number

child

Array of children(using struct hack)

zend_ast 其它子类

zend_ast.h 文件中还包含其它 和 zend_ast 在结构上类似的结构,类似 OOP 中的 子类

zend_ast_list

zend_ast_zval

zend_ast_decl

创建 zend_ast

zend_ast.c 中有一系列的函数用于创建 zend_ast, zend_list

  • zend_ast_create

  • zend_ast_create_ex

  • zend_ast_create_list

zend_ast_create

zend_ast_create 函数根据 kind 和一个或多个 child zend_ast 创建一个新的 zend_ast,它在内部调用 zend_ast_create_from_va_list

ZEND_API zend_ast *zend_ast_create(zend_ast_kind kind, ...) {
    va_list va;
    zend_ast *ast;

    va_start(va, kind);
    ast = zend_ast_create_from_va_list(kind, 0, va);
    va_end(va);

    return ast;
}
zend_ast_create_from_va_list

yyparse

bison 语法分析工具一般以 yyparse 函数为入口

# grep --color -rinw --include=*.h --include=*.c yyparse *
Zend/zend_language_parser.c:63:#define yyparse zendparse

看来 PHP 给 yyparse 起了个别名,我们再看看代码里面哪些地方引用了 zendparse

# grep --color -rinw --include=*.h --include=*.c zendparse *
Zend/zend_language_scanner.c:587: if (!zendparse()) {
Zend/zend_language_parser.c:436:int zendparse (void);

我们看到 zend_language_scanner.c 文件中使用了 zenparse()

static zend_op_array *zend_compile(int type) {
    ...
    if (!zendparse()) {
        ...
    }
    ...
}

PHP 命名规范还是不错的,从 zend_compile 可以推测出这个函数应该是用来编译一段 PHP 代码,返回值 zend_op_array 估计是 PHP 虚拟机字节码数组

# grep --color -rinw --include=*.h --include=*.c zend_compile

Zend/zend_language_scanner.c:637: op_array = zend_compile
Zend/zend_language_scanner.c:769: op_array = zend_compile

我们在 zend_language_scanner.c 的 637 和 769 行找到了两处对 zend_compile 的引用

623 ZEND_API zend_op_array *compile_file(...)
645 zend_op_array *compile_filename(int type, zval *filename)

顺藤摸瓜,我们接着查找 compile_file,compile_filename

# grep --color -rinw --include=*.h --include=*.c compile_file
Zend/zend.c:705 zend_compile_file = compile_file;
Zend/zend.c:711 zend_compile_file = compile_file;

zend.c 705 在函数 zend_startup 内

int zend_startup(zend_utility_functions *utility_functions, char **extensions)

zend_compile

compiler globals(CG)

语法分析和中间代码生成过程中使用了 全局变量 CG 来保存中间结果(AST),搜索 CG 宏定义

/* Compiler */
#ifdef ZTS
# define CG(v) ZEND_TSRMG(compiler_globals_id, zend_compiler_globals *, v)
#else
# define CG(v) (compiler_globals.v)
extern ZEND_API struct _zend_compiler_globals compiler_globals;
#endif

这里有一个条件编译开关 ZTS,PHP 老鸟因该都知道 "要使用 pthreads 扩展,需要构建 PHP 时启用 ZTS (Zend Thread Safety)",很自然想到:因为 CG 是一个全局变量,所以为了在多线程环境下保证线程安全,需要使用特殊机制(类似 Java 中的 TLS,thread local storage)访问 CG 中的字段

实现

有了关于 CG 的铺垫,我们马上来看 zend_compile 函数的实现

// zend_language_scanner.c

static zend_op_array *zend_compile(int type) {
    // 编译生成的字节码数组
    zend_op_array *op_array = NULL;
    zend_bool original_in_compilation = CG(in_compilation);

    CG(in_compilation) = 1;
    CG(ast) = NULL;
    CG(ast_arena) = zend_arena_create(1024 * 32);
    if (!zendparse()) {
        ...
        op_array = emalloc(sizeof(zend_op_array));
        init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);
        ...
        zend_compile_top_stmt(CG(ast));
        zend_emit_final_return(type == ZEND_USER_FUNCTION);
        pass_two(op_array);

        CG(active_op_array) = original_active_op_array;
    }
    ...
    CG(in_compilation) = original_in_compilation;

    return op_array;
}

这里忽略了一些细节,突出语法分析和字节码生成的流程

  • 调用 zend_parse 进行语法分析,生成的 AST 根节点保存在 GC(ast) 中

  • 为字节码数组分配内存

  • 调用 zend_compile_top_stmt 根据 AST 生成字节码数组保存在 GC(active_op_array) 中

总结


xingpingz
122 声望64 粉丝

博学,审问,慎思,明辨,力行