php代码的编译与执行

解释性语言也需要编译?

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

词法分析

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

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

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

DFA:确定有穷自动机

对于正则表达式(a|b)*abb可以得到下面这样一个确定有穷自动机
在这里插入图片描述
成立举例: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 我们把赋值语句拆成了一棵树状的结构,也是多叉树,我们称这种树为语法分析数。在这里插入图片描述
那么如何把所有用到的语法都表示出来呢,比如说加减乘除,引出巴科斯范式
在这里插入图片描述
如上图,所有的这些通过递归的方式就可以把加减乘除都表达出来,那么巴科斯范式如何在实际中应用呢?引出语法分析工具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
AST
但是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代码经过词法解析语法解析,生成抽象语法树AST,通过AST编译成opcodes指令,然后通过zend虚拟机执行。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值