背景

前两期主要是关于系统设计方面的,第 3 期做一期关于 php 源码方面的内容。 我们日常开发和做一些小修改时,尤其是在服务器上,经常使用 php -l 来检测修改的代码是否会导致语法错误,做一些基础的语法检查,所以探究下 php lint 的工作原理很有必要。

目标

  • 理解 php -l 工作的原理

了解 php lint

  ~ php -h
Usage: php [options] [-f] <file> [--] [args...]
   php [options] -r <code> [--] [args...]
   php [options] [-B <begin_code>] -R <code> [-E <end_code>] [--] [args...]
   php [options] [-B <begin_code>] -F <file> [-E <end_code>] [--] [args...]
   php [options] -S <addr>:<port> [-t docroot] [router]
   php [options] -- [args...]
   php [options] -a

  ......
  ......
  -h               This help
  -i               PHP information
  -l               Syntax check only (lint)
  ......
  ......

在 php 官方的文档中解释如下:

Provides a convenient way to perform only a syntax check on the given PHP code. On success, the text No syntax errors detected in is written to standard output and the shell return code is 0. On failure, the text Errors parsing in addition to the internal parser error message is written to standard output and the shell return code is set to -1.

This option won’t find fatal errors (like undefined functions). Use the -f to test for fatal errors too.

Note:

This option does not work together with the -r option.

相关源码解读(v5.5.38)

关于 php 源码的结构和目录功能不在这里复述了,因为线上依然采用的 5.5.x 版本,所以我们也主要以 5.5.38 版本的代码来解读,相关的目录功能在 php 的扩展开发里有很多解释和说明。

因为 php -l 时通过命令行的方式执行的,所以我们主要关注 CLI 部分(涉及 SAPI 概念请自行搜索)。

sapi/cli/php_cli.c

static int do_cli(int argc, char **argv TSRMLS_DC)
{
    // 部分代码省略

    case 'l': /* syntax check mode */
        if (behavior != PHP_MODE_STANDARD) {
            break;
        }
        behavior=PHP_MODE_LINT; // 注意 behavior,将根据 behavior 来做相应的处理动作
        break;

    // 部分代码省略

    case PHP_MODE_LINT:
        // 注意 php_lint_script 这个函数是 main/php_main.h 定义的。
        exit_status = php_lint_script(&file_handle TSRMLS_CC);
        if (exit_status==SUCCESS) {
            zend_printf("No syntax errors detected in %s\n", file_handle.filename);
        } else {
            zend_printf("Errors parsing %s\n", file_handle.filename);
        }
        break;
}

main/main.c

PHPAPI int php_lint_script(zend_file_handle *file TSRMLS_DC)
{
    zend_op_array *op_array;
    int retval = FAILURE;

    zend_try {
        // 此处对文件进行了“编译”,如果失败就会展示上面代码中的错误信息
        op_array = zend_compile_file(file, ZEND_INCLUDE TSRMLS_CC);
        zend_destroy_file_handle(file TSRMLS_CC);

        if (op_array) {
            destroy_op_array(op_array TSRMLS_CC);
            efree(op_array);
            retval = SUCCESS;
        }
    } zend_end_try();

    return retval;
}

下面会涉及两个比较重要的文件以及 lexer 词法分析生成器yacc Yet Another Compiler-Compilerre2c lexer generatorbison

  1. Zend/zend_language_scanner.l 词法分析描述文件
  2. Zend/zend_language_parser.y 语法描述文件

CG 和 EG

  • CG compiler_globals
  • EG executor_globals

CG 和 EG 对应的源码在 Zend/zend_globals.h 文件内

SCNG 操作的就是下面这个 struct,也定义在了 Zend/zend_globals.h 文件内

_zend_php_scanner_globals

Zend/zend_language_scanner.c

ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type TSRMLS_DC)
{
    zend_lex_state original_lex_state;
    zend_op_array *op_array = (zend_op_array *) emalloc(sizeof(zend_op_array));
    zend_op_array *original_active_op_array = CG(active_op_array);
    zend_op_array *retval=NULL;
    int compiler_result;
    zend_bool compilation_successful=0;
    znode retval_znode;
    zend_bool original_in_compilation = CG(in_compilation);

    retval_znode.op_type = IS_CONST;
    retval_znode.u.constant.type = IS_LONG;
    retval_znode.u.constant.value.lval = 1;
    Z_UNSET_ISREF(retval_znode.u.constant);
    Z_SET_REFCOUNT(retval_znode.u.constant, 1);

    zend_save_lexical_state(&original_lex_state TSRMLS_CC);

    retval = op_array; /* success oriented */

    if (open_file_for_scanning(file_handle TSRMLS_CC)==FAILURE) {
        if (type==ZEND_REQUIRE) {
            zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, file_handle->filename TSRMLS_CC);
            zend_bailout();
        } else {
            zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, file_handle->filename TSRMLS_CC);
        }
        compilation_successful=0;
    } else {
        init_op_array(op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE TSRMLS_CC);
        CG(in_compilation) = 1;
        CG(active_op_array) = op_array;
        zend_stack_push(&CG(context_stack), (void *) &CG(context), sizeof(CG(context)));
        zend_init_compiler_context(TSRMLS_C);
        // 注意 zendparse
        compiler_result = zendparse(TSRMLS_C);
        zend_do_return(&retval_znode, 0 TSRMLS_CC);
        CG(in_compilation) = original_in_compilation;
        if (compiler_result != 0) { /* parser error */
            zend_bailout();
        }
        compilation_successful=1;
    }

    if (retval) {
        CG(active_op_array) = original_active_op_array;
        if (compilation_successful) {
            pass_two(op_array TSRMLS_CC);
            zend_release_labels(0 TSRMLS_CC);
        } else {
            efree(op_array);
            retval = NULL;
        }
    }
    zend_restore_lexical_state(&original_lex_state TSRMLS_CC);
    return retval;
}

zendparse 实际上是 yyparse,在 make 的时候生成在 Zend/zend_language_parse.c 文件中。

/* Substitute the variable and function names.  */
#define yyparse         zendparse
#define yylex           zendlex
#define yyerror         zenderror
#define yylval          zendlval
#define yychar          zendchar
#define yydebug         zenddebug
#define yynerrs         zendnerrs

yyparse 的作用就是对输入文件进行语法分析,如果分析成功没有错误则返回 0,否则返回非 0,在语法分析的这个阶段,来判断是否有错误,所以有些执行过程中的 fatal error 是没办法发现的。具体操作的代码,可以通过 GDB 来查看整体流程。

(gdb) break main
(gdb) break zendparse
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000005ff6f3 in zendparse
                                                   at /root/github/php-src-php-5.5.38/Zend/zend_language_parser.c:3284
    breakpoint already hit 1 time
2       breakpoint     keep y   0x0000000000607c36 in compile_file at Zend/zend_language_scanner.l:554

为了方便大家的调试,基于 ubuntu 的镜像自己做了一个 docker image,因为 GDB 需要权限,所以 pull 镜像以后,通过下面的命令增加授权参数进入即可

docker pull 0utman/ubuntu-php55-gdb-debug
docker run --privileged -it <image name>

自己建立一个有语法错误的文件 syntax_check.php 进行实验吧。

cd /root/
echo "<?php \$0 = 1; " > syntax_check.php
gdb --args php -l syntax_check.php

总结

文章内容不长,但是涉及的知识点特别多,希望大家能举一反三。

  • php 代码执行过程以及 zend 一些相关知识
  • lexer yacc bison re2c 编译相关的知识
  • gdb 调试
  • docker 运用

如果有必要再把里面的一些知识点分为其它的 topic