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
下载和安装
- wget https://github.com/skvadrik/re2c/releases/download/1.3/re2c-1.3.tar.xz
- 先xz -d filename.tar.xz 再tar -xvf filename.tar
- cd re2c-1.3
- ./configure
- 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
下载和安装
- wget http://ftp.gnu.org/gnu/bison/bison-3.5.tar.gz
- tar xzvf bison-3.5.tar.gz
- cd bison-3.5
- ./configure
- 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还是不能直接运行的,还需要接下来对他进行操作,生成对应的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虚拟机执行。
上一篇: 自定义一个Java运行时注解框架
下一篇: JAVA 注解之自定义注解