PHP内核是如何处理常量的

PHP内核中,有两种常量,一种是内核预定义常量,一种是魔术常量。

内核预定义常量

内核预定义常量它的值不会被改变。

内核预定义常量注册流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
graph-easy <<< "
graph { flow: down; }
[php_module_startup] ->
[zend_startup] ->
[zend_register_standard_constants \n for example E_ERROR] ->
[zend_register_constant]

[php_module_startup] ->
[register constants \n for example PHP_VERSION] ->
[zend_register_constant]
"

+-------------------------+ +----------------------------------+
| register constants | | php_module_startup |
| for example PHP_VERSION | <-- | |
+-------------------------+ +----------------------------------+
| |
| |
| v
| +----------------------------------+
| | zend_startup |
| +----------------------------------+
| |
| |
| v
| +----------------------------------+
| | zend_register_standard_constants |
| | for example E_ERROR |
| +----------------------------------+
| |
| |
| v
| +----------------------------------+
+---------------------------> | zend_register_constant |
+----------------------------------+

核心函数是zend_register_constant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ZEND_API zend_result zend_register_constant(zend_constant *c)
{
// 省略其他代码

/* Check if the user is trying to define any special constant */
if (zend_string_equals_literal(name, "__COMPILER_HALT_OFFSET__")
|| (!persistent && zend_get_special_const(ZSTR_VAL(name), ZSTR_LEN(name)))
|| zend_hash_add_constant(EG(zend_constants), name, c) == NULL
) {
zend_error(E_WARNING, "Constant %s already defined", ZSTR_VAL(name));
zend_string_release(c->name);
if (!persistent) {
zval_ptr_dtor_nogc(&c->value);
}
ret = FAILURE;
}
if (lowercase_name) {
zend_string_release(lowercase_name);
}
return ret;
}

我们发现,zend_hash_add_constant把内核预定义常量存在了EG(zend_constants)这个哈希表里面。

魔术常量

魔术常量它的值会随着代码的位置而改变,例如__FILE__

1
2
3
4
5
6
7
8
9
10
11
12
13
%token <ident> T_FILE            "'__FILE__'"

constant:
name { $$ = zend_ast_create(ZEND_AST_CONST, $1); }
| T_LINE { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_LINE); }
| T_FILE { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_FILE); }
| T_DIR { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_DIR); }
| T_TRAIT_C { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_TRAIT_C); }
| T_METHOD_C { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_METHOD_C); }
| T_FUNC_C { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_FUNC_C); }
| T_NS_C { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_NS_C); }
| T_CLASS_C { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_CLASS_C); }
;

我们发现,在词法分析的阶段,把__FILE__标注为了T_FILE这个token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static zend_bool zend_try_ct_eval_magic_const(zval *zv, zend_ast *ast) /* {{{ */
{
zend_op_array *op_array = CG(active_op_array);
zend_class_entry *ce = CG(active_class_entry);

switch (ast->attr) {
case T_LINE:
ZVAL_LONG(zv, ast->lineno);
break;
case T_FILE:
ZVAL_STR_COPY(zv, CG(compiled_filename));
break;
// 省略其他代码
}

然后,在语法分析阶段,直接把__FILE__替换成了当前正在编译的文件路径。

禁止常量替换

对于内核预定义常量,我们可以给CG(compiler_options)添加ZEND_COMPILE_NO_CONSTANT_SUBSTITUTIONZEND_COMPILE_NO_PERSISTENT_CONSTANT_SUBSTITUTION来禁止常量替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static zend_bool can_ct_eval_const(zend_constant *c) {
if (ZEND_CONSTANT_FLAGS(c) & CONST_DEPRECATED) {
return 0;
}
if ((ZEND_CONSTANT_FLAGS(c) & CONST_PERSISTENT)
&& !(CG(compiler_options) & ZEND_COMPILE_NO_PERSISTENT_CONSTANT_SUBSTITUTION)
&& !((ZEND_CONSTANT_FLAGS(c) & CONST_NO_FILE_CACHE)
&& (CG(compiler_options) & ZEND_COMPILE_WITH_FILE_CACHE))) {
return 1;
}
if (Z_TYPE(c->value) < IS_OBJECT
&& !(CG(compiler_options) & ZEND_COMPILE_NO_CONSTANT_SUBSTITUTION)) {
return 1;
}
return 0;
}

但是,对于魔术常量,我们是没有办法禁止的。

那么,什么场景下需要禁止编译期间的常量替换呢?比如我们在机器1上面,通过PHP7.3持久化了op_array,然后我们需要在机器2上面通过PHP7.4来跑,这时候就不能够在编译期间进行常量替换。否则当我们的代码依赖于PHP版本的时候,就会出现问题,例如:

1
2
3
<?php

assert(PHP_VERSION == 7.3);

在机器1上通过PHP7.3持久化op_array,如果进行常量替换的话,常量区存放的是7.3,在机器2通过PHP7.4执行这个脚本,就会断言出错。如果不进行常量替换,持久化op_array的时候,常量区存放的是PHP_VERSION这个字符串,然后程序在运行的时候,去EG(zend_constants)表里面找,这个时候,得到的就是7.4