问题摘要

在Cybrics2020的一个题目中,题目已经设置了short_open_tag = Off,但是在代码中仍然使用了<?=....的形式,原来的猜想是docker部署的时候由于某些原因使该配置项没有生效,从而导致短标签能继续使用,并且PHP Manual中的描述也佐证了这个猜想,其中对于short_open_tag的描述中有如下一段:

本指令也会影响到缩写形式 <?=,它和 <? echo 等价。使用此缩写需要 short_open_tag 的值为 On。 从 PHP 5.4.0 起, <?= 总是可用的。

最初我和队友将从 PHP 5.4.0 起, <?= 总是可用的理解为了在PHP5.4.0之后默认生效,但可以通过设置short_open_tag = Off来进行关闭。但是随后@Smile在PHP7.4.1下的测试推翻了这个认识:

1
2
3
4
5
6
7
// 1.php
<?=phpinfo();?>

// 2.php
<? phpinfo();?>

// 已设置 short_open_tag = Off

实际运行时却发现1.php的代码能够正常执行,2.php的内容不执行,因此这就和可以通过关闭short_open_tag来使<?=失效这个结论冲突了。后面我在PHP5.6.31下测试,效果同PHP7.4.1,排除了PHP7和PHP5大版本差异导致的解析不一致。

PHP代码解析过程

PHP的代码在真正执行会经过一长串的处理,其中和代码解析相关的部分则是Zend引擎对代码进行词法、语法分析,编译为opcode执行。

在进行词法、语法分析时,PHP会按照已规定好的词法规则将代码切分为单独的Token,然后再通过bison进行语法处理,当语法出现错误时则会终止执行,也就是我们所看到的各种语法错误提示。

与词法、语法分析相关的规则主要存放在Zend/zend_language_parser.y以及zend_language_scanner.l两个文件中,我们着重关注<?=<?,其中,在Zend/zend_language_parser.y文件中,对<?=进行了定义:

1
%token T_OPEN_TAG_WITH_ECHO "'<?='"

zend_language_scanner.l中则定义了相关的re2c规则,<?=<?<?php一样,都可以作为PHP代码的起始符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<INITIAL>"<?=" {
BEGIN(ST_IN_SCRIPTING);
if (PARSER_MODE()) {
/* We'll reject this as an identifier in zend_lex_tstring. */
RETURN_TOKEN_WITH_IDENT(T_ECHO);
}
RETURN_TOKEN(T_OPEN_TAG_WITH_ECHO);
}

<INITIAL>"<?" {
if (CG(short_tags)) {
BEGIN(ST_IN_SCRIPTING);
RETURN_OR_SKIP_TOKEN(T_OPEN_TAG);
} else {
goto inline_char_handler;
}
}

<?= 的解析

为了理解其差异,我们先看<?=的处理流程,它的处理流程主要涉及PARSER_MODERETURN_TOKEN_WITH_IDENTRETURN_TOKEN三个方法,其定义分别为:

1
2
3
4
5
6
7
8
9
10
11
12
#define PARSER_MODE() \
EXPECTED(elem != NULL)

#define RETURN_TOKEN_WITH_IDENT(_token) do { \
token = _token; \
goto emit_token_with_ident; \
} while (0)

#define RETURN_TOKEN(_token) do { \
token = _token; \
goto emit_token; \
} while (0)

PARSER_MODE会判断是否elem是否为空,elem是个union结构:zend_parser_stack_elem,主要用于表示词法元素,其定义为:

1
2
3
4
5
6
7
typedef union _zend_parser_stack_elem {
zend_ast *ast;
zend_string *str;
zend_ulong num;
unsigned char *ptr;
zend_lexer_ident_ref ident;
} zend_parser_stack_elem;

因此PARSER_MODE主要作用为判断当前是否有词法元素需要解析。RETURN_TOKEN_WITH_IDENTRETURN_TOKEN的实现较为相似,都是一个goto语句,对应的代码块为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
emit_token:
if (SCNG(on_event)) {
SCNG(on_event)(ON_TOKEN, token, start_line, yytext, yyleng, SCNG(on_event_context));
}
return token;

emit_token_with_ident:
if (PARSER_MODE()) {
elem->ident.offset = SCNG(yy_text) - SCNG(yy_start);
elem->ident.len = SCNG(yy_leng);
}
if (SCNG(on_event)) {
SCNG(on_event)(ON_TOKEN, token, start_line, yytext, yyleng, SCNG(on_event_context));
}
return token;

SCNG宏可以对定义的scanner_global全局变量进行取值,scanner_global主要用于存放lexer的状态信息,比如当前处理的指针位置、最后一次处理的token位置等,在上面的elem->ident.offset = SCNG(yy_text) - SCNG(yy_start);这一句中,便是计算当前位置和开始位置从而得出偏移量。

SCNG(on_event)则是取出存放在on_event上的同名函数,on_event可以根据token类型进行对应的处理,函数参数类型为:

1
on_event(zend_php_scanner_event event, int token, int line,const char *text, size_t length, void *context)

对于分析<?=做了什么这个问题来说,分析到这里基本结束了,因此可以得出结论,对于<?=来说,在解析完了echo就会继续后续token的处理,且未看到对于短标签配置的取值判断。

<? 的解析

<?<?=最关键的区别就在于CG(short_tags)的判断上,CG宏含义为Compiler Globals,与Zend编译器相关的全局变量相关,CG(short_tags)则是取出有关于短标签的配置,随后根据取值来决定是否继续后续的代码执行。inline_char_handler用于扫描不在PHP标签中的内容,因此以<?表示的PHP代码也就不会进行执行。

上述代码的分析在PHP7.4.9下进行,在PHP5.6.31版本下的分析要更为简单,对<?=的写法更为直观:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<INITIAL>"<?=" {
ZVAL_STRINGL(zendlval, yytext, yyleng, 0); /* no copying - intentional */
BEGIN(ST_IN_SCRIPTING);
return T_OPEN_TAG_WITH_ECHO;
}

<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;
}
}