基本信息

  • PHP 版本:7.1.0
  • 调试环境:Ubuntu(WSL)
  • 调试工具:GDB、Clion

编译 PHP

下载并安装 PHP:

1
2
3
4
$ wget http://cn2.php.net/distributions/php-7.1.0.tar.gz
$ tar -xzvf php-7.1.0.tar.gz
$ cd php-7.1.0
$ ./configure --prefix=$HOME/php-7.1.0/build --enable-fpm

注意:$HOME/php-7.1.0/build 是 PHP 执行文件和库文件安装的目录,可以自定义。--enable-fpm 表示同时安装 php-fpm。

接下来修改编译优化等级,方便我们在调试的过程中查看变量的内容。

首先在 php-7.1.0/Makefile 文件中搜索 CFLAGS_CLEAN,然后将 -02 改为 -o0,表示不需要优化。

1
CFLAGS_CLEAN = -I/usr/include -g -O0 -fvisibility=hidden -DZEND_SIGNALS $(PROF_FLAGS)

执行 make 命令:

1
$ make && make install

执行完成后,就可以在 php-7.1.0/build/bin 目录下看到 PHP 相关的可执行文件了。

1
pear  peardev  pecl  phar  phar.phar  php  php-cgi  php-config  phpdbg  phpize

调试 PHP

使用 Clion 进行调试

首先用 Clion 打开 php-7.1.0 目录,Clion 会在根目录下自动创建 CMakeLists.txt 文件,复制下面的内容覆盖 CMakeLists.txt 中的内容:

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
cmake_minimum_required(VERSION 3.10)

project(php_7_1_0)

set(CMAKE_CXX_STANDARD 14)

set(PHP_SOURCE .)

file(GLOB_RECURSE HEAD_FILES FOLLOW_SYMLINKS ${PHP_SOURCE})
file(GLOB_RECURSE HEAD_FILES FOLLOW_SYMLINKS ${PHP_SOURCE}/ext)
file(GLOB_RECURSE HEAD_FILES FOLLOW_SYMLINKS ${PHP_SOURCE}/Zend)
file(GLOB_RECURSE HEAD_FILES FOLLOW_SYMLINKS ${PHP_SOURCE}/sapi)
file(GLOB_RECURSE HEAD_FILES FOLLOW_SYMLINKS ${PHP_SOURCE}/pear)
file(GLOB_RECURSE HEAD_FILES FOLLOW_SYMLINKS ${PHP_SOURCE}/TSRM)

file(GLOB_RECURSE SRC_LIST FOLLOW_SYMLINKS
${PHP_SOURCE}/*.c
${PHP_SOURCE}/ext/*.c
${PHP_SOURCE}/main/*.c
${PHP_SOURCE}/Zend/*.c
${PHP_SOURCE}/sapi/*.c
${PHP_SOURCE}/pear/*.c
${PHP_SOURCE}/TSRM/*.c
)

include_directories(${PHP_SOURCE})
include_directories(${PHP_SOURCE}/ext)
include_directories(${PHP_SOURCE}/main)
include_directories(${PHP_SOURCE}/Zend)
include_directories(${PHP_SOURCE}/sapi)
include_directories(${PHP_SOURCE}/pear)
include_directories(${PHP_SOURCE}/TSRM)

add_executable(${PROJECT_NAME} ${SRC_LIST})
add_custom_target(makefile COMMAND make && make install WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})

注意:/home/ubuntu/php-7.1.0 要替换成你自己的 PHP 所在的路径。

点击右上角绿色锤子旁边的下拉框 => Edit Configurations => makefile,按照下面的步骤进行操作:

  • Target:makefile(一般不用动)
  • Executable:点击右边的 ... 按钮,选择 php-7.1.0/bin/php 文件
  • Program arguments:输入你要调试的 PHP 脚本的文件名,比如 index.php
  • Working directory:选择上面 PHP 脚本所在的目录
  • 删除 Before launch 中的 Build,避免每次调试都重新构建 PHP

点击右上角绿色的小虫子或者使用快捷键 Shift + F9 开始调试

使用 GDB 进行调试

用来测试的 index.php :

1
2
3
4
5
<?php

$a = 'hello';

var_dump($a);

开始 GDB 调试:

1
gdb /php-7.1.0/build/bin/php

使用 b 命令在 var_dump 处打断点:

1
2
(gdb) b zif_var_dump
Breakpoint 1 at 0x48f3bc: file /home/ubuntu/php-7.1.0/ext/standard/var.c, line 200.

为啥这里是 zif_var_dump?

因为 zif_var_dump 是 var_dump 函数的底层实现, 是由 PHP_FUNCITON(var_dump) 宏展开后的名称,所以直接搜 zif_var_dump 是搜不到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define ZEND_FN(name)   zif_##name
#define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS)
#define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name))

/* PHP-named Zend macro wrappers */
#define PHP_FUNCTION ZEND_FUNCTION

PHP_FUNCTION(var_dump)
{
zval *args;
int argc;
int i;

if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {
return;
}

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

开始运行 PHP 文件:

1
2
3
4
5
6
7
(gdb) r index.php
Starting program: /home/ubuntu/php-7.1.0/build/bin/php index.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, zif_var_dump (execute_data=0x7ffffb6130d0, return_value=0x7ffffffea400)
at /home/ubuntu/php-7.1.0/ext/standard/var.c:200
200 {

代码执行到 zif_var_dump 就停止了,说明命中了断点。我们可以使用 l 命令查看附近的源码:

1
2
3
4
5
6
7
8
9
10
11
(gdb) l
195 /* }}} */
196
197 /* {{{ proto void var_dump(mixed var)
198 Dumps a string representation of variable to output */
199 PHP_FUNCTION(var_dump)
200 {
201 zval *args;
202 int argc;
203 int i;
204

使用 bt 命令查看调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) bt
#0 zif_var_dump (execute_data=0x7ffffb6130d0, return_value=0x7ffffffea400)
at /home/ubuntu/php-7.1.0/ext/standard/var.c:200
#1 0x0000000008642151 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER ()
at /home/ubuntu/php-7.1.0/Zend/zend_vm_execute.h:628
#2 0x000000000864038e in execute_ex (ex=0x7ffffb613030) at /home/ubuntu/php-7.1.0/Zend/zend_vm_execute.h:429
#3 0x0000000008640d05 in zend_execute (op_array=0x7ffffb67e000, return_value=0x0)
at /home/ubuntu/php-7.1.0/Zend/zend_vm_execute.h:474
#4 0x00000000085a2ca6 in zend_execute_scripts (type=8, retval=0x0, file_count=3)
at /home/ubuntu/php-7.1.0/Zend/zend.c:1474
#5 0x00000000084eb1ce in php_execute_script (primary_file=0x7ffffffecd00) at /home/ubuntu/php-7.1.0/main/main.c:2533
#6 0x0000000008743462 in do_cli (argc=2, argv=0x90c1ef0) at /home/ubuntu/php-7.1.0/sapi/cli/php_cli.c:990
#7 0x000000000874483e in main (argc=2, argv=0x90c1ef0) at /home/ubuntu/php-7.1.0/sapi/cli/php_cli.c:1378

使用 u 命令让代码运行到指定的行:

1
2
3
4
(gdb) u 209
zif_var_dump (execute_data=0x7ffffb6130d0, return_value=0x7ffffffea400)
at /home/ubuntu/php-7.1.0/ext/standard/var.c:209
209 for (i = 0; i < argc; i++) {

使用 p 命令打印变量 $a 的值:

1
2
(gdb) p args[0].value.str.val
$1 = "h"

使用 @ 指定输出长度:

1
2
(gdb) p args[0].value.str.val@5
$2 = {"h", "e", "l", "l", "o"}