目录

调试PHP源码

https://s1.ax1x.com/2020/07/05/U91xmt.png

缘由

有时候,我们想看看一个变量底层对应底层的数据结构或者PHP脚本是如何执行的,gdb就是这样一个好工具,之前有篇文章写过如何简单使用gdb。

本文环境:

  • PHP版本:PHP 7.1.16 (cli) (built: Apr 8 2020 11:56:59) ( ZTS )
  • OS:Ubuntu 18.04.4 LTS
  • gdb: GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git

编译

你可以从PHP官网下载PHP源码的压缩包,者是从git.php.net(或者是github的镜像)的git库clone最新的代码库,然后切换到对应的PHP版本的分支,本文使用的是PHP7.1,你可以使用下面的命令完成这些工作:

1
2
3
git clone http://git.php.net/repository/php-src.git
cd php-src
git checkout PHP-7.1

如果你是从git库中clone的代码,那么你先要运行下buildconf命令:

1
~/php-src> ./buildconf 

这个命令会生成configure脚本,从官网下载的源码包中会直接包含这个脚本,如果你执行buildconf出错,那么很可能是因为你的系统中没有autoconf这个工具,你可以使用包安装工具进行安装。
如果你已经成功生成了configure脚本文件(或者是使用已包含这个脚本文件的源码包),那就可以开始编译了。为了调式PHP源码,我们的编译会disable所有的扩展(除了一些必须包含的外,这些PHP的编译脚本会自行处理),我们使用下面的命令来完成编译安装的工作,假设安装的路径为$HOME/myphp:

1
2
3
~/php-src> ./configure --disable-all --enable-debug --prefix=$HOME/myphp
~/php-src> make -jN
~/php-src> make install

注意这里的prefix的参数必须为绝对路径,所以你不能写成~/myphp,另外我们这次编译只是为了调式,所以建议一定要设置prefix参数,要不然PHP会被安装到默认路径中,大多数时候是/usr/local/php中,这可能会造成一些没必要的污染。另外我们使用了两个选项,一个是–disable-all,这个表示禁止安装所有扩展(除了一个必须安装的),另外一个就是–enable-debug,这个选项表示以debug模式编译PHP源码,相当于gcc的-g选项,它会把调试信息编译进最终的二进制程序中。

上面的命令make -jN,N表示你的CPU数量(或者是CPU核心的数量),设置了这个参数后就可以使用多个CPU进行并行编译,这可以提高编译效率。

调试PHP

我们调试一段简单的PHP代码:

1
2
3
4
5
<?php
$a = 10;
$b = 42;

echo $b;

我们想看下$a对应的底层变量结构,那我们应该在哪个函数上叫断点呢?通过查阅资料(如《PHP7内核分析》)我们发现,ZendVM的执行器就是一个white循环,在这个循环中依次调用opline指令的handler,然后根据handler的返回决定下一步的动作。执行调度器为zend_execute_ex,这是函数指针,默认为execute_ex,我们看下这个函数的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//删除了预处理语句
ZEND_API void execute_ex(zend_execute_data *ex)
{
    DCL_OPLINE

    const zend_op *orig_opline = opline;
    zend_execute_data *orig_execute_data = execute_data; /* execute_data是一个全局变量 */
    execute_data = ex; 


    LOAD_OPLINE();

    while (1) {
        ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); //执行OPCode对应的C函数OPLINE是一个全局变量
        if (UNEXPECTED(!OPLINE)) { //当前OPArray执行完
            execute_data = orig_execute_data;
            opline = orig_opline;
            return;
        }
    }
    zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen");
}

所以我们可以在给execute_ex函数打断点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
gdb ~/myphp/bin/php

(gdb) r index.php
Starting program: /home/salamander/myphp/bin/php index.php

Breakpoint 1, execute_ex (ex=0x7ffff7014030) at /home/salamander/php-7.1.16/Zend/zend_vm_execute.h:411
411             const zend_op *orig_opline = opline;
(gdb) n
414             zend_execute_data *orig_execute_data = execute_data;
(gdb) n
415             execute_data = ex;
(gdb) n
421             LOAD_OPLINE();
(gdb) n
422             ZEND_VM_LOOP_INTERRUPT_CHECK();
(gdb) n
429                     ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);

现在就要调用opline指令的handler,我们应该键入s,跳到对应函数内部去:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(gdb) s
ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER () at /home/salamander/php-7.1.16/Zend/zend_vm_execute.h:39506
39506           SAVE_OPLINE();
(gdb) n
39507           value = EX_CONSTANT(opline->op2);
(gdb) n
39508           variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);
(gdb) n
39516                   value = zend_assign_to_variable(variable_ptr, value, IS_CONST);
(gdb) p value
$1 = (zval *) 0x7ffff707b460
(gdb) p *$1
$3 = {value = {lval = 10, dval = 4.9406564584124654e-323, counted = 0xa, str = 0xa, arr = 0xa, obj = 0xa, res = 0xa, ref = 0xa, ast = 0xa, zv = 0xa, 
    ptr = 0xa, ce = 0xa, func = 0xa, ww = {w1 = 10, w2 = 0}}, u1 = {v = {type = 4 '\004', type_flags = 0 '\000', const_flags = 0 '\000', 
      reserved = 0 '\000'}, type_info = 4}, u2 = {next = 4294967295, cache_slot = 4294967295, lineno = 4294967295, num_args = 4294967295, 
    fe_pos = 4294967295, fe_iter_idx = 4294967295, access_flags = 4294967295, property_guard = 4294967295, extra = 4294967295}}

我们第一行PHP代码是$a = 10;,这是一条赋值语句,ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER是把一个常量赋值给一个变量,EX_CONSTANT(opline->op2)是获取常量的值,$a为CV变量,分配在zend_execute_data动态变量区,通过_get_zval_ptr_cv_undef_BP_VAR_W取到这个变量的地址,剩下的好理解了,就是把变量值赋值给CV变量。
value就是我们的变量值,$a对应的底层变量就是它。
回忆一下PHP7变量的数据结构,是一个叫zval的结构体,zend_value保存具体的变量值:

 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
typedef union _zend_value {
	zend_long         lval;				/* long value */
	double            dval;				/* double value */
	zend_refcounted  *counted;
	zend_string      *str;
	zend_array       *arr;
	zend_object      *obj;
	zend_resource    *res;
	zend_reference   *ref;
	zend_ast_ref     *ast;
	zval             *zv;
	void             *ptr;
	zend_class_entry *ce;
	zend_function    *func;
	struct {
		uint32_t w1;
		uint32_t w2;
	} ww;
} zend_value;

struct _zval_struct {
	zend_value        value;			/* value */
	union {
		struct {
			ZEND_ENDIAN_LOHI_4(
				zend_uchar    type,			/* 变量类型 */
				zend_uchar    type_flags,
				zend_uchar    const_flags,
				zend_uchar    reserved)	    /* call info for EX(This) */
		} v;
		uint32_t type_info;
	} u1;
	union {
		uint32_t     next;                 /* hash collision chain */
		uint32_t     cache_slot;           /* literal cache slot */
		uint32_t     lineno;               /* line number (for ast nodes) */
		uint32_t     num_args;             /* arguments number for EX(This) */
		uint32_t     fe_pos;               /* foreach position */
		uint32_t     fe_iter_idx;          /* foreach iterator index */
		uint32_t     access_flags;         /* class constant access flags */
		uint32_t     property_guard;       /* single property guard */
		uint32_t     extra;                /* not further specified */
	} u2;
};

#define IS_UNDEF                                        0
#define IS_NULL                                         1
#define IS_FALSE                                        2
#define IS_TRUE                                         3
#define IS_LONG                                         4
#define IS_DOUBLE                                       5
#define IS_STRING                                       6
#define IS_ARRAY                                        7
#define IS_OBJECT                                       8
#define IS_RESOURCE                                     9
#define IS_REFERENCE                                    10

/* constant expressions */
#define IS_CONSTANT                                     11
#define IS_CONSTANT_AST                                 12

我们打印出来的底层变量,lval是10,u1里的type是4,也正好是IS_LONG,别的字段的值大家也可以分析看看。

参考: