PHP词法解析源码分析之PHP标签、关键字、类、数字

时间:2021-10-05 09:19:50

之前没搞过web端的程序,最近要研究webshell,发现php的语法太怪异了,干脆直接看看PHP内核词法分析的代码。

php的词法分析从zend_language_scanner.l文件中的lex_scan开始,开头代码如下:

int lex_scan(zval *zendlval TSRMLS_DC)
{
//设置当前token的首位置为当前位置
restart:
SCNG(yy_text) = YYCURSOR;

yymore_restart:
//这段注释定义了各个类型的正则表达式匹配,在词法解析程序(如bison、re2c等)程序将本文件转化为c代码时会用到
/*!re2c
re2c:yyfill:check = 0;
LNUM [0-9]+
DNUM ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*)
EXPONENT_DNUM (({LNUM}|{DNUM})[eE][+-]?{LNUM})
HNUM "0x"[0-9a-fA-F]+
BNUM "0b"[01]+
LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*
WHITESPACE [ \n\r\t]+
TABS_AND_SPACES [ \t]*
TOKENS [;:,.\[\]()|^&+-/*=%!~$<>?@]
ANY_CHAR [^]
NEWLINE ("\r"|"\n"|"\r\n")

/* compute yyleng before each rule */
<!*> := yyleng = YYCURSOR - SCNG(yy_text);

接下去就按照解析PHP标签、关键字、类和结构体、数字这几个方面来看lex_scan是如何解析PHP代码的。

1.匹配php标签

在zend_language_scanner.l文件里会匹配php的标签,并且匹配规则不止一种,打开了asp_tag开关还能兼容asp脚本,比较奇特。

1.1 <script language=php>

首先是匹配<script language=php>标签,源码如下,无论这里面有多少个空白字符全部无视,最后php也可以加上单引号或双引号:

<INITIAL>"<script"{WHITESPACE}+"language"{WHITESPACE}*"="{WHITESPACE}*("php"|"\"php\""|"'php'"){WHITESPACE}*">" {
YYCTYPE *bracket = (YYCTYPE*)zend_memrchr(yytext, '<', yyleng - (sizeof("script language=php>") - 1));

if (bracket != SCNG(yy_text)) {
/* Handle previously scanned HTML, as possible <script> tags found are assumed to not be PHP's */
YYCURSOR = bracket;
goto inline_html;
}

HANDLE_NEWLINES(yytext, yyleng);
ZVAL_STRINGL(zendlval, yytext, yyleng, 0); /* no copying - intentional */
BEGIN(ST_IN_SCRIPTING);
return T_OPEN_TAG;
}

因为<script>标签本身是在html中的,所以判断当前是否在扫描html,如果是的话就跳转到inline_html去,不然就将当前状态改为ST_IN_SCRIPTING并返回T_OPEN_TAG,表示这是一个php的标签。

1.2 <%=和<%

如果不是这个还会在匹配<%=和<%,匹配到时便会检查php.ini里面的asp_tags标签是否为On,如果是则表示进入脚本并返回T_OPEN_TAG,否则就转到inline_char_handler去执行,源码如下:

//<INITIAL>"<%=" {
<INITIAL>"<%" {
if (CG(asp_tags)) {
ZVAL_STRINGL(zendlval, yytext, yyleng, 0); /* no copying - intentional */
BEGIN(ST_IN_SCRIPTING);
return T_OPEN_TAG;
} else {
goto inline_char_handler;
}
}

1.3 <?=和<?

还有就是短标签<?=和<?,在php 5.3.3中遇到这两个都会判断short_open_tag是否为On,代码如下:

<INITIAL>"<?" {
if (CG(short_tags)) {
ZVAL_STRINGL(zendlval, yytext, yyleng, 0); /* no copying - intentional */
BEGIN(ST_IN_SCRIPTING);
return T_OPEN_TAG;
} else {
goto inline_char_handler;
}
}

但是现在看php5.6.3的源码发现<?=标签已经不需要short_open_tag标志了:

<INITIAL>"<?=" {
ZVAL_STRINGL(zendlval, yytext, yyleng, 0); /* no copying - intentional */
BEGIN(ST_IN_SCRIPTING);
return T_OPEN_TAG_WITH_ECHO;
}

1.4 <?php

最后就是最常规的<?php,代码如下:

<INITIAL>"<?php"([ \t]|{NEWLINE}) {
ZVAL_STRINGL(zendlval, yytext, yyleng, 0); /* no copying - intentional */
HANDLE_NEWLINE(yytext[yyleng-1]);
BEGIN(ST_IN_SCRIPTING);
return T_OPEN_TAG;
}

最后如果都不匹配的话则会匹配ANY_CHAR,判断是否扫描完了,是的话直接返回0,不是就接下去执行inline_char_handler和inline_html段的代码:

<INITIAL>{ANY_CHAR} {
if (YYCURSOR > YYLIMIT) {
return 0;
}

1.5 inline_char_handler

下面是inline_char_handler的代码,而inline_char_handler和inline_html扫描的是不在php标签里面的的代码,也就是说这些php代码可能夹杂在诸如html等代码中。

inline_char_handler中的代码是对整个字符串扫描,memchr表示的是从YYCURSOR开始的YYLIMIT - YYCURSOR长度内的字符串中搜索'<'字符,如果找到则匹配'?'、'%'、's'等字符,如果满足条件则结束循环,而匹配到's'或'S'则将YYCURSOR往回退一格并重新开始php标签的匹配

inline_char_handler:
while (1) {
YYCTYPE *ptr = memchr(YYCURSOR, '<', YYLIMIT - YYCURSOR);

YYCURSOR = ptr ? ptr + 1 : YYLIMIT;

if (YYCURSOR < YYLIMIT) {
switch (*YYCURSOR) {
case '?':
if (CG(short_tags) || !strncasecmp((char*)YYCURSOR + 1, "php", 3) || (*(YYCURSOR + 1) == '=')) { /* Assume [ \t\n\r] follows "php" */
break;
}
continue;
case '%':
if (CG(asp_tags)) {
break;
}
continue;
case 's':
case 'S':
/* Probably NOT an opening PHP <script> tag, so don't end the HTML chunk yet
* If it is, the PHP <script> tag rule checks for any HTML scanned before it */

YYCURSOR--;
yymore();
default:
continue;
}

YYCURSOR--;
}

break;
}

1.6 inline_html

再是inline_html的代码,直接复制这段代码,随后返回T_INLINE_HTML:

inline_html:
yyleng = YYCURSOR - SCNG(yy_text);

if (SCNG(output_filter)) {
int readsize;
size_t sz = 0;
readsize = SCNG(output_filter)((unsigned char **)&Z_STRVAL_P(zendlval), &sz, (unsigned char *)yytext, (size_t)yyleng TSRMLS_CC);
Z_STRLEN_P(zendlval) = sz;
if (readsize < yyleng) {
yyless(readsize);
}
} else {
Z_STRVAL_P(zendlval) = (char *) estrndup(yytext, yyleng);
Z_STRLEN_P(zendlval) = yyleng;
}
zendlval->type = IS_STRING;
HANDLE_NEWLINES(yytext, yyleng);
return T_INLINE_HTML;
}

2.简单关键字匹配

在词法解析中,针对简单的php关键字,匹配到了直接进行返回,注意开头的<ST_IN_SCRIPTING>标签表示的是当前状态在php脚本中,几个例子如下:

<ST_IN_SCRIPTING>"exit" {
return T_EXIT;
}
<ST_IN_SCRIPTING>"die" {
return T_EXIT;
}
<ST_IN_SCRIPTING>"function" {
return T_FUNCTION;
}
<ST_IN_SCRIPTING>"const" {
return T_CONST;
}
<ST_IN_SCRIPTING>"return" {
return T_RETURN;
}

诸如T_EXIT/T_FUNCTION/T_CONST这些宏都是定义在zend_language_parser.h文件中,几个例子如下:

#define T_INCLUDE_ONCE 261
#define T_INCLUDE 262
#define T_LOGICAL_OR 263
#define T_LOGICAL_XOR 264
#define T_LOGICAL_AND 265

3.类和结构体的处理

首先是ST_IN_SCRIPTING状态下的"->",它会先将ST_LOOKING_FOR_PROPERTY这个状态入栈并将当前状态设为寻找属性:

<ST_IN_SCRIPTING>"->" {
yy_push_state(ST_LOOKING_FOR_PROPERTY TSRMLS_CC);
return T_OBJECT_OPERATOR;
}

yy_push_state宏的函数如下,将前一个状态入栈并将新的状态设置为当前状态:

static void _yy_push_state(int new_state TSRMLS_DC)
{
zend_stack_push(&SCNG(state_stack), (void *) &YYGETCONDITION(), sizeof(int));
YYSETCONDITION(new_state);
}

而在ST_LOOKING_FOR_PROPERTY状态下的"->"则是直接返回T_OBJECT_OPERATOR

<ST_LOOKING_FOR_PROPERTY>"->" {
return T_OBJECT_OPERATOR;
}

如果接下去的是空白字符,则进行忽略:

<ST_IN_SCRIPTING,ST_LOOKING_FOR_PROPERTY>{WHITESPACE}+ {
ZVAL_STRINGL(zendlval, yytext, yyleng, 0); /* no copying - intentional */
HANDLE_NEWLINES(yytext, yyleng);
return T_WHITESPACE;
}

如果是找到LABEL则会将当前状态恢复为上一个状态ST_IN_SCRIPTING并复制扫描到的字符串的值,返回T_STRING:

<ST_LOOKING_FOR_PROPERTY>{LABEL} {
yy_pop_state(TSRMLS_C);
zend_copy_value(zendlval, yytext, yyleng);
zendlval->type = IS_STRING;
return T_STRING;
}

如果是其他的字符则直接恢复前一个状态并重新开始扫描:

<ST_LOOKING_FOR_PROPERTY>{ANY_CHAR} {
yyless(0);
yy_pop_state(TSRMLS_C);
goto restart;
}

4.对数字的处理

4.1 二进制数

首先是将最开始的0b和之后的数字0全部跳过,随后判断是否超过long类型的长度,不超过则直接转化为long的十进制类型,超过则转化为double类型,最后返回T_DNUMBER:

<ST_IN_SCRIPTING>{BNUM} {
char *bin = yytext + 2; /* Skip "0b" */
int len = yyleng - 2;

/* Skip any leading 0s */
while (*bin == '0') {
++bin;
--len;
}

if (len < SIZEOF_LONG * 8) {
if (len == 0) {
Z_LVAL_P(zendlval) = 0;
} else {
Z_LVAL_P(zendlval) = strtol(bin, NULL, 2);
}
zendlval->type = IS_LONG;
return T_LNUMBER;
} else {
ZVAL_DOUBLE(zendlval, zend_bin_strtod(bin, NULL));
return T_DNUMBER;
}
}

4.2 纯数字(八进制或十进制)

纯数字可能会有十进制和八进制两种情况,首先判断是否超过了十进制的表示长度,没有超过直接转化(调用的strtol函数将0开头的数字看作是八进制数),超过的话若第一个字符为0,是把数字作为八进制数转化为double类型,否则作为十进制数转化为double类型:

<ST_IN_SCRIPTING>{LNUM} {
if (yyleng < MAX_LENGTH_OF_LONG - 1) { /* Won't overflow */
Z_LVAL_P(zendlval) = strtol(yytext, NULL, 0);
} else {
errno = 0;
Z_LVAL_P(zendlval) = strtol(yytext, NULL, 0);
if (errno == ERANGE) { /* Overflow */
if (yytext[0] == '0') { /* octal overflow */
Z_DVAL_P(zendlval) = zend_oct_strtod(yytext, NULL);
} else {
Z_DVAL_P(zendlval) = zend_strtod(yytext, NULL);
}
zendlval->type = IS_DOUBLE;
return T_DNUMBER;
}
}

zendlval->type = IS_LONG;
return T_LNUMBER;
}

4.3 十六进制数

和二进制一样先排除掉前两个字符“0x”并去掉最前面的所有0,然后是超过0x7FFFFFFF则转化为double类型,否则转化为long类型:

<ST_IN_SCRIPTING>{HNUM} {
char *hex = yytext + 2; /* Skip "0x" */
int len = yyleng - 2;

/* Skip any leading 0s */
while (*hex == '0') {
hex++;
len--;
}
//即为是否在0X00000000-0X7FFFFFFF范围内
if (len < SIZEOF_LONG * 2 || (len == SIZEOF_LONG * 2 && *hex <= '7')) {
if (len == 0) {
Z_LVAL_P(zendlval) = 0;
} else {
Z_LVAL_P(zendlval) = strtol(hex, NULL, 16);
}
zendlval->type = IS_LONG;
return T_LNUMBER;
} else {
ZVAL_DOUBLE(zendlval, zend_hex_strtod(hex, NULL));
return T_DNUMBER;
}
}

4.4 小数和科学记数法

直接将小数和科学记数法转化为double类型:

<ST_IN_SCRIPTING>{DNUM}|{EXPONENT_DNUM} {
ZVAL_DOUBLE(zendlval, zend_strtod(yytext, NULL));
return T_DNUMBER;
}

这次分析了处理相对比较简单的情况,下一篇将会对非常复杂的引号匹配以及引号中内容的解析做分析。不知为何PHP要把引号中字符串支持那么多操作。