欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

php代码的编译与执行

程序员文章站 2022-06-16 17:26:49
...

解释性语言也需要编译?

我们来看下编译型语言和解释性语言的区别
php代码的编译与执行
解析
1:编译型的目标语言是二进制编码的机器码,可以在物理机上执行的
2:解释型语言如php也是需要编译的,是实时编译,由php内核实现,涉及到词法、语法分析,最后生成的是opcodes,opcodes是类似于机器码的编码,但是这种编码物理机是不认识的,也是无法执行的,那么我们就需要在物理机上层加一个虚拟机,php内核在物理机上加了一层软件实现的能够识别opcodes的虚拟机,称为zend虚拟机。

词法分析

原理:对于使用php编码的代码我们可以看作是一个很长的字符串,那么我们如何识别这个长字符串哪些是变量,哪些是赋值语句,哪些是函数呢,基本思想是用正则来对整个字符串进行分割,找到对应的标识,我们把这种标识称为token,找出所有token的过程就是词法分析

问题:如何判断一个字符串对于正则表达式(a|b)*abb成立?我们如何用代码来实现?引出NFA和DFA

NFA和DFA
NFA:不确定有穷自动机
对于正则表达式(a|b)*abb可以得到下面这样一个不确定有穷自动机
php代码的编译与执行
成立举例:abb/aabb/babb/aababb
不成立举例:a/ab/bb/acabb

DFA:确定有穷自动机

对于正则表达式(a|b)*abb可以得到下面这样一个确定有穷自动机
php代码的编译与执行
成立举例:abb/aabb/babb/aababb
不成立举例:a/ab/bb/acabb

NFA和DFA的区别
1:NFA会导致状态流转停在某一个状态,而DFA解决了这个问题

我们知道对于php语言来说,要切分开所有的token的话,那么正则表达式应该会有很多个,并且也是比较复杂的,那么我们需要手动去编写DFA么?答案是否定的,有一个工具比较完善的帮我们解决了这个问题,那就是re2c。

re2c
下载和安装

  1. wget https://github.com/skvadrik/re2c/releases/download/1.3/re2c-1.3.tar.xz
  2. 先xz -d filename.tar.xz 再tar -xvf filename.tar
  3. cd re2c-1.3
  4. ./configure
  5. make && make install

注意**.l**文件是re2c处理的源文件
举例integers.l:这个文件的意思是根据输入的参数来判断返回值是二进制、八进制、十进制、十六进制、error的哪一种类型,代码如下

#include <stdio.h>

enum num_t { ERR, BIN, OCT, DEC, HEX };

static num_t lex(const char *YYCURSOR)
{
    const char *YYMARKER;
    /*!re2c
        re2c:define:YYCTYPE = char;
        re2c:yyfill:enable = 0;

        end = "\x00";
        bin = '0b' [01]+;
        oct = "0" [0-7]*;
        dec = [1-9][0-9]*;
        hex = '0x' [0-9a-fA-F]+;

        *       { return ERR; }
        bin end { return BIN; }
        oct end { return OCT; }
        dec end { return DEC; }
        hex end { return HEX; }
    */
}

int main(int argc, char **argv)
{
    for (int i = 1; i < argc; ++i) {
        switch (lex(argv[i])) {
            case ERR: printf("error\n"); break;
            case BIN: printf("binary\n"); break;
            case OCT: printf("octal\n"); break;
            case DEC: printf("decimal\n"); break;
            case HEX: printf("hexadecimal\n"); break;
        }
    }
    return 0;
}

re2c命令处理
1:re2c integers.l -o integers.c //用re2c命令把integers.l输出成 integers.c
2:g++ integers.c -o integers //用g++生成一个可执行文件
3:./integers 0b10
4:输出了binary

我们来看下用re2c生成的c文件

/* Generated by re2c */
#include <stdio.h>

enum num_t { ERR, BIN, OCT, DEC, HEX };

static num_t lex(const char *YYCURSOR)
{
    const char *YYMARKER;

{
        char yych;
        yych = *YYCURSOR;
        switch (yych) {
        case '0':       goto yy4;
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':       goto yy5;
        default:        goto yy2;
        }
yy2:
        ++YYCURSOR;
yy3:
        { return ERR; }
yy4:
        yych = *(YYMARKER = ++YYCURSOR);
        switch (yych) {
        case 0x00:      goto yy6;
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':       goto yy8;
        case 'B':
        case 'b':       goto yy11;
        case 'X':
        case 'x':       goto yy12;
        default:        goto yy3;
        }
yy5:
        yych = *(YYMARKER = ++YYCURSOR);
        switch (yych) {
        case 0x00:      goto yy13;
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':       goto yy15;
        default:        goto yy3;
        }
yy6:
        ++YYCURSOR;
        { return OCT; }
yy8:
        yych = *++YYCURSOR;
        switch (yych) {
        case 0x00:      goto yy6;
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':       goto yy8;
        default:        goto yy10;
        }
yy10:
        YYCURSOR = YYMARKER;
        goto yy3;
yy11:
        yych = *++YYCURSOR;
        if (yych <= 0x00) goto yy10;
        goto yy18;
yy12:
        yych = *++YYCURSOR;
        if (yych <= 0x00) goto yy10;
        goto yy20;
yy13:
        ++YYCURSOR;
        { return DEC; }
yy15:
        yych = *++YYCURSOR;
        switch (yych) {
        case 0x00:      goto yy13;
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':       goto yy15;
        default:        goto yy10;
        }
yy17:
        yych = *++YYCURSOR;
yy18:
        switch (yych) {
        case 0x00:      goto yy21;
        case '0':
        case '1':       goto yy17;
        default:        goto yy10;
        }
yy19:
        yych = *++YYCURSOR;
yy20:
        switch (yych) {
        case 0x00:      goto yy23;
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case 'A':
        case 'B':
        case 'C':
        case 'D':
        case 'E':
        case 'F':
        case 'a':
        case 'b':
        case 'c':
        case 'd':
        case 'e':
        case 'f':       goto yy19;
        default:        goto yy10;
        }
yy21:
        ++YYCURSOR;
        { return BIN; }
yy23:
        ++YYCURSOR;
        { return HEX; }
}

}

int main(int argc, char **argv)
{
    for (int i = 1; i < argc; ++i) {
        switch (lex(argv[i])) {
            case ERR: printf("error\n"); break;
            case BIN: printf("binary\n"); break;
            case OCT: printf("octal\n"); break;
            case DEC: printf("decimal\n"); break;
            case HEX: printf("hexadecimal\n"); break;
        }
    }
    return 0;
}

其实re2c是把我们写的正则转化成了一个DFA,也就是确定有穷自动机,可以很方便的让我们去做一些词法解析的事情,我们只需要编写正则表达式,它就会帮我们生成对应的DFA,大大提升了我们的效率。

语法分析

对于$a=1我们可以通过词法分析将 $a、=、1分析出来,但是分析完后我们还没有建立它们三者间的关系,那么建立关系的过程就是语法分析。

语法分析入门
举例:关于a = b + c * 2 我们把赋值语句拆成了一棵树状的结构,也是多叉树,我们称这种树为语法分析数。php代码的编译与执行
那么如何把所有用到的语法都表示出来呢,比如说加减乘除,引出巴科斯范式
php代码的编译与执行
如上图,所有的这些通过递归的方式就可以把加减乘除都表达出来,那么巴科斯范式如何在实际中应用呢?引出语法分析工具bison

bison
下载和安装

  1. wget http://ftp.gnu.org/gnu/bison/bison-3.5.tar.gz
  2. tar xzvf bison-3.5.tar.gz
  3. cd bison-3.5
  4. ./configure
  5. make && make install

注意**.y**文件是bison处理的源文件
举例calc.y:这个文件是实现了一个计算器,代码如下

%code top {
  #include <ctype.h>  /* isdigit. */
  #include <stdio.h>  /* For printf, etc. */
  #include <string.h> /* strcmp. */

  int yylex (void);
  void yyerror (char const *);
}

%define api.header.include {"calc.h"}
%define api.value.type union /* Generate YYSTYPE from these types:  */
%token <double> NUM "number"
%type  <double> expr term fact

/* Generate the parser description file.  */
%verbose
/* Enable run-time traces (yydebug).  */
%define parse.trace

/* Formatting semantic values.  */
%printer { fprintf (yyo, "%g", $$); } <double>;

%% /* The grammar follows.  */
input:
  %empty
| input line
;

line:
  '\n'
| expr '\n'  { printf ("%.10g\n", $1); }
| error '\n' { yyerrok; }
;

expr:
  expr '+' term { $$ = $1 + $3; }
| expr '-' term { $$ = $1 - $3; }
| term
;

term:
  term '*' fact { $$ = $1 * $3; }
| term '/' fact { $$ = $1 / $3; }
| fact
;

fact:
  "number"
| '(' expr ')' { $$ = $2; }
;

%%

int
yylex (void)
{
  int c;

  /* Ignore white space, get first nonwhite character.  */
  while ((c = getchar ()) == ' ' || c == '\t')
    continue;

  if (c == EOF)
    return 0;

  /* Char starts a number => parse the number.         */
  if (c == '.' || isdigit (c))
    {
      ungetc (c, stdin);
      scanf ("%lf", &yylval.NUM);
      return NUM;
    }

  /* Any other character is a token by itself.        */
  return c;
}

/* Called by yyparse on error.  */
void
yyerror (char const *s)
{
  fprintf (stderr, "%s\n", s);
}

int
main (int argc, char const* argv[])
{
  /* Enable parse traces on option -p.  */
  for (int i = 1; i < argc; ++i)
    if (!strcmp (argv[i], "-p"))
      yydebug = 1;
  return yyparse ();
}

bison命令处理
1:bison -d calc.y -o calc.c //用bison命令把calc.y输出成 calc.c
2:gcc -lm calc.c -o calc //用gcc生成一个可执行文件
3:./calc
4:1+2 * 3 输出7
5:1+2 * (3+4) 输出15

代码太长就不贴了,总结下这个bison主要做了什么
它帮我们把整个的巴科斯范式转换成了各种各样的状态机,然后当我们输入一个多项式的加法或者乘法的时候,它就会把它通过递归的方式解成一个个number,并且进行计算,计算后得出结果输出。

AST(抽象语法树)

$a = 1; 经过词法和语法分析后,我们可以生成如下的AST
php代码的编译与执行
但是AST还是不能直接运行的,还需要接下来对他进行操作,生成对应的opcodes,引出opcodes。

opcodes

opcodes相关的数据结构
zend_op 结构体:对应于汇编里面的一条指令

struct _zend_op {
        const void *handler; //操作对应的handler
        znode_op op1; //操作数1           
        znode_op op2; //操作数2
        znode_op result; //操作的结果
        uint32_t extended_value; //扩展的value
        uint32_t lineno; //文件行数
        zend_uchar opcode;  //每个zend_op都对应一个opcode
        zend_uchar op1_type; 
        zend_uchar op2_type;
        zend_uchar result_type;
};

znode_op又是一个联合体

typedef union _znode_op {
        uint32_t      constant;  //常量用这个
        uint32_t      var;  //变量用这个
        uint32_t      num;
        uint32_t      opline_num; /*  Needs to be signed */
#if ZEND_USE_ABS_JMP_ADDR
        zend_op       *jmp_addr;
#else
        uint32_t      jmp_offset;
#endif
#if ZEND_USE_ABS_CONST_ADDR
        zval          *zv;
#endif
} znode_op;

zend_op_array结构体:对应的是指令集

struct _zend_op_array {
        /* Common elements */
        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 */

        int cache_size;     /* number of run_time_cache_slots * sizeof(void*) */
        int last_var;       /* number of CV variables */
        uint32_t T;         /* number of temporary variables */
        uint32_t last;      /* number of opcodes */

        zend_op *opcodes; //类似于一个数组,存的是所有opcodes的地址
        ZEND_MAP_PTR_DEF(void **, run_time_cache);
        ZEND_MAP_PTR_DEF(HashTable *, static_variables_ptr);
        HashTable *static_variables;
        zend_string **vars; /* names of CV variables */

        uint32_t *refcount;

        int last_live_range;
        int last_try_catch;
        zend_live_range *live_range;
        zend_try_catch_element *try_catch_array;

        zend_string *filename;
        uint32_t line_start;
        uint32_t line_end;
        zend_string *doc_comment;

        int last_literal;
        zval *literals;

        void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

zend_execute_data结构体:这个是在zend虚拟机上运行的

struct _zend_execute_data {
        const zend_op       *opline;           /* executed opline                */
        zend_execute_data   *call;             /* current call                   */
        zval                *return_value;
        zend_function       *func;             /* executed function              */
        zval                 This;             /* this + call_info + num_args    */
        zend_execute_data   *prev_execute_data;
        zend_array          *symbol_table;
        void               **run_time_cache;   /* cache op_array->run_time_cache */
};

补充知识
在物理机上执行汇编的时候,它涉及到几个概念指令集、符号表、执行的堆栈。

zend_vm_stack结构体:执行的堆栈

/* dedicated Zend executor functions - do not use! */
struct _zend_vm_stack {
        zval *top;
        zval *end;
        zend_vm_stack prev;
};

AST的编译

在对AST编译的过程中,我们就会确定每个op1、op2、result在zend虚拟机、zend_vm_stack上面的具体位置,那么AST通过遍历完后,只是计算好了op1、op2、result,还需要通过一个重要的函数passtwo去将handler和opcode对应起来,这样就完成了指令集的生成。

Zend虚拟机

什么是Zend虚拟机?
我们前面讲了opline了,知道里面有handler、op1、op2、result,执行引擎就是根据eg,也就是execute global全局变量里面去取每一条指令,然后对op1、op2通过handler进行运算,写到result中,得到结果供后续的代码或者我们输出使用,这就是Zend虚拟机的基本原理。

和虚拟机相关的文件
zend_vm_gen.php:flag的定时、opcode的定义
zend_vm_execute.skl:这个里面是opcode对应的handler的一些模板
zend_vm_execute.h:定义了所有opcode的handler(这个文件是用 zend_vm_gen.php 脚本+ zend_vm_execute.skl模板生成的)
php代码的编译与执行

总结

php代码经过词法解析语法解析,生成抽象语法树AST,通过AST编译成opcodes指令,然后通过zend虚拟机执行。