0%

PHP7函数源码逐步阅读(一)debug_zval_dump

  1. debug_zval_dump() 无法输出int等类型的引用计数?
  2. 一起读源码
  3. xdebug_debug_zval与它的区别

环境变量:

MacOS High Sierra

PHP-7.2.17-debug

GDB-8.0.1

Q

debug_zval_dump()该函数可以打印变量的相关信息,记录变量被引用的次数并支持不定长参数。但是我在使用过程中发现对于不可变变量,例如FALSE、TRUE、INT、DOUBLE、NULL这些类型的变量。虽然被引用后增加了引用计数,但是该函数不会打印出来。运行结果如下:

1
2
3
4
5
6
7
8
9
10
<?php
$a = 1;
$b = &$a;
debug_zval_dump($a);
xdebug_debug_zval("a");
//结果:
/*
int(1)
a: (refcount=2, is_ref=1)=1
*/

以下是我使用GDB调试时的观察,执行完下面一段代码发生的变化有:

1
$b = &$a;
  • 变量的地址不变,类型变为引用类型。

  • 值存放在zend_reference中,引用计数为1。

  • 接着赋值给$b后,引用计数加1(了解这些可以看上一篇有关zval结构的文章)。

所以xdebug输出的信息符合预期。那在PHP7中,自带的这个debug函数,为什么不能输出我们预期的结果,它又是怎么实现该方法的呢?

一起读源码

主函数部分

下面这部分代码是主函数。第一块是变量的定义;

第二块宏是解析可变参数的,将传来的一个或多个参数放入args;

第三块for是调用函数php_debug_zval_dump对参数args进行循环打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* {{{ proto void debug_zval_dump(mixed var)
Dumps a string representation of an internal zend value to output. */
PHP_FUNCTION(debug_zval_dump)
{
zval *args;
int argc;
int i;

ZEND_PARSE_PARAMETERS_START(1, -1)
Z_PARAM_VARIADIC('+', args, argc)
ZEND_PARSE_PARAMETERS_END();

for (i = 0; i < argc; i++) {
php_debug_zval_dump(&args[i], 1);
}
}

php_debug_zval_dump

紧接着我们来阅读这个函数的代码,可以看到。在初始化完变量后,进入到一个对level的判断。不要迷惑,这是针对多级结构的空格输出,从而使输出格式规范。

然后进入again块,根据zval中的type去判断类型从而走不同的逻辑。

一眼就能看出从case IS_FALSEcase IS_DOUBLE都是直接输出,%s 对应 COMMON这个局部常量,判断is_ref是否为1。如果为1,则在前方输出一个&取地址符。

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
PHPAPI void php_debug_zval_dump(zval *struc, int level) /* {{{ */
{
HashTable *myht = NULL;
zend_string *class_name;
int is_ref = 0;
zend_ulong index;
zend_string *key;
zval *val;
uint32_t count;

if (level > 1) {
php_printf("%*c", level - 1, ' ');
}

again:
switch (Z_TYPE_P(struc)) {
case IS_FALSE:
php_printf("%sbool(false)\n", COMMON);
break;
case IS_TRUE:
php_printf("%sbool(true)\n", COMMON);
break;
case IS_NULL:
php_printf("%sNULL\n", COMMON);
break;
case IS_LONG:
php_printf("%sint(" ZEND_LONG_FMT ")\n", COMMON, Z_LVAL_P(struc));
break;
case IS_DOUBLE:
php_printf("%sfloat(%.*G)\n", COMMON, (int) EG(precision), Z_DVAL_P(struc));
break;

STRING

case IS_STRING这块,第一行输出固定格式,意义同上。字符串值的输出在第二行:PHPWRITE,以流形式输出字符串,这里需要传递两个参数,一个是value的指针,一个是value的长度指针。第三行接着打印出它的引用计数。三元运算符判断如果zend_string中不存在,则默认为1。

1
2
3
4
5
case IS_STRING:
php_printf("%sstring(%zd) \"", COMMON, Z_STRLEN_P(struc));
PHPWRITE(Z_STRVAL_P(struc), Z_STRLEN_P(struc));
php_printf("\" refcount(%u)\n", Z_REFCOUNTED_P(struc) ? Z_REFCOUNT_P(struc) : 1);
break;

ARRAY

case IS_ARRAY这块代码一开始就进行一个level判断,但主函数进行调用时,level的值为1,所以第一次不会进入此处。然后计算array的长度,按格式输出array的长度和引用计数,例如:array(3) refcount(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case IS_ARRAY:
myht = Z_ARRVAL_P(struc);
if (level > 1 && !(GC_FLAGS(myht) & GC_IMMUTABLE)) {
if (GC_IS_RECURSIVE(myht)) {
PUTS("*RECURSION*\n");
return;
}
GC_PROTECT_RECURSION(myht);
}
count = zend_array_count(myht);
php_printf("%sarray(%d) refcount(%u){\n", COMMON, count, Z_REFCOUNTED_P(struc) ? Z_REFCOUNT_P(struc) : 1);
ZEND_HASH_FOREACH_KEY_VAL_IND(myht, index, key, val) {
zval_array_element_dump(val, index, key, level);
} ZEND_HASH_FOREACH_END();
if (level > 1 && !(GC_FLAGS(myht) & GC_IMMUTABLE)) {
GC_UNPROTECT_RECURSION(myht);
}
if (level > 1) {
php_printf("%*c", level - 1, ' ');
}
PUTS("}\n");
break;

程序继续执行,到了宏定义ZEND_HASH_FOREACH_KEY_VAL_IND此处,循环hash数组,输出数组内的元素。但注意在宏内调用了zval_array_element_dump,此函数先输出键名,我们可以从它的注释看出来。分为数字型键名和string型键名,接着调用之前的方法,传递zval,根据类型不同输出不同格式。直到ZEND_HASH_FOREACH_END()结束。

1
2
3
4
5
6
7
8
9
10
11
static void zval_array_element_dump(zval *zv, zend_ulong index, zend_string *key, int level) /* {{{ */
{
if (key == NULL) { /* numeric key */
php_printf("%*c[" ZEND_LONG_FMT "]=>\n", level + 1, ' ', index);
} else { /* string key */
php_printf("%*c[\"", level + 1, ' ');
PHPWRITE(ZSTR_VAL(key), ZSTR_LEN(key));
php_printf("\"]=>\n");
}
php_debug_zval_dump(zv, level + 2);
}

关于case IS_ARRAY的部分大致上结束了,其中部分关于GC的逻辑代码我没有探究,推测是垃圾回收的一些判断。

OBJECT到RESOURCE

接下来的case IS_OBJECT也是差不多的逻辑。case IS_RESOURCE也是短短几行,非常容易举一反三。

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
36
37
38
39
40
41
42

case IS_OBJECT:
myht = zend_get_properties_for(struc, ZEND_PROP_PURPOSE_DEBUG);
if (myht) {
if (GC_IS_RECURSIVE(myht)) {
PUTS("*RECURSION*\n");
zend_release_properties(myht);
return;
}
GC_PROTECT_RECURSION(myht);
}
class_name = Z_OBJ_HANDLER_P(struc, get_class_name)(Z_OBJ_P(struc));
php_printf("%sobject(%s)#%d (%d) refcount(%u){\n", COMMON, ZSTR_VAL(class_name), Z_OBJ_HANDLE_P(struc), myht ? zend_array_count(myht) : 0, Z_REFCOUNT_P(struc));
zend_string_release_ex(class_name, 0);
if (myht) {
ZEND_HASH_FOREACH_KEY_VAL(myht, index, key, val) {
zend_property_info *prop_info = NULL;

if (Z_TYPE_P(val) == IS_INDIRECT) {
val = Z_INDIRECT_P(val);
if (key) {
prop_info = zend_get_typed_property_info_for_slot(Z_OBJ_P(struc), val);
}
}

if (!Z_ISUNDEF_P(val) || prop_info) {
zval_object_property_dump(prop_info, val, index, key, level);
}
} ZEND_HASH_FOREACH_END();
GC_UNPROTECT_RECURSION(myht);
zend_release_properties(myht);
}
if (level > 1) {
php_printf("%*c", level - 1, ' ');
}
PUTS("}\n");
break;
case IS_RESOURCE: {
const char *type_name = zend_rsrc_list_get_rsrc_type(Z_RES_P(struc));
php_printf("%sresource(%d) of type (%s) refcount(%u)\n", COMMON, Z_RES_P(struc)->handle, type_name ? type_name : "Unknown", Z_REFCOUNT_P(struc));
break;
}

REFERENCE(答案)

我们来看case IS_REFERENCE,它直接隐藏了真实的引用计数,当一个类型变为引用类型,它的引用计数在这个函数中只会输出1。在注释中我们也可以看出,它这样做是为了兼容性。

1
2
3
4
5
6
7
8
9
10
11
12
	case IS_REFERENCE:
//??? hide references with refcount==1 (for compatibility)
if (Z_REFCOUNT_P(struc) > 1) {
is_ref = 1;
}
struc = Z_REFVAL_P(struc);
goto again;
default:
php_printf("%sUNKNOWN:0\n", COMMON);
break;
}
}

总结

至此,我们看完了这个函数的源码。在这个函数中,对于没有引用计数的类型(LONG,FALSE,TRUE,DOUBLE,NULL)仅仅输出它的值。

对于有引用计数的类型(STRING,ARRAY,OBJECT,RESOURCE)根据逻辑判断,输出存放在zval结构中的refcount的值。

对于引用类型,则是用原有的数据重新判断输出。

xdebug_debug_zval和debug_zval_dump的区别

我们来看两段代码的运行结果:

1
2
3
4
5
6
7
8
9
<?php
$a = "hello";
debug_zval_dump($a);
xdebug_debug_zval('a');

/* 运行结果
string(5) "hello" refcount(1)
a: (interned, is_ref=0)='hello'
*/

一个字符串的默认引用计数为1。在上面这段代码的输出结果中,debug_zval_dump的结果是符合预期的。而xdebug输出的interned是什么意思?我们先保留疑问,再敲一段代码。

接下来这段代码输出的类型也是字符串:

1
2
3
4
5
6
7
8
9
<?php
$a = "timestamp is : " . time();
debug_zval_dump($a);
xdebug_debug_zval('a');

/* 运行结果
string(25) "timestamp is : 1585102731" refcount(2)
a: (refcount=1, is_ref=0)='timestamp is : 1585102731'
*/

为什么debug_zval_dump输出2?而xdebug这次的输出结果却是符合预期的?!

深入探究

Interned String

在PHP5.4的时候, 引入了Interned String机制, 用于优化PHP对字符串的存储和处理。

Interned String 可以包括变量名称、类名、方法名、字符串、注释等。第一段代码中的$a = 'hello',在多次编译下结果都不会产生变化。它们的生存周期存在于整个请求期间,请求结束后会统一销毁释放,自然也就无需通过引用计数进行内存管理。

Immutable array不可变数组

所有多次编译结果恒定不变的数组,都会被优化为不可变数组,是 opcache 扩展优化出的一种数组类型,多次编译结果不会产生变化,便会放入到OPcache里面进行优化。

函数调用增加引用计数

函数调用会增加引用计数,但在函数调用完成之后,函数内部变量销毁,引用计数减少到保持原有的值。而debug_zval_dump 这是一个php函数,它的输出是在函数内部的,所以输出的是它增加的引用计数。

总结

  • debug_zval_dump是PHP函数,函数调用会增加引用,而的输出正是在函数内部的,所以输出的引用计数是增加过的。
  • 使用xdebug_debug_zval是正确的,但需要理解Interned StringImmutable array的概念,这些内容可以在参考文章中详细了解。
  • 学会GDB调试,逐步理解PHP源码执行过程

猜测,推导,证明的过程。所以要不断动手尝试,不能浮于表面。

以上的内容还涉及到了写时复制、写时分离、引用计数等等,每一个概念在PHP7的实现都值得深入探究。

参考文章

PHP7各种数据类型的引用计数

OPcache工作原理