PHP 7 安装和调试

学习了 PHP 7 的新特性后,再来了解 PHP 7 的编译安装和调试方式。

编译安装

以 Linux 环境为例来进行安装。

首先下载 PHP 7。在 http://php.net/releases/ 上能够获取各个版本的 PHP 源码和修改记录(建议对 PHP 源码感兴趣的读者关注一下修改记录,以了解 PHP 源码开发者的开发思路)。本书以 7.1.0 版本为例,下载源码包并编译安装(源码包 URL 为 http://cn2.php.net/distributions/php-7.1.0.tar.gz )。

$ wget  http://cn2.php.net/distributions/php-7.1.0.tar.gz
$ tar -zxvf php-7.1.0.tar.gz
$ cd php-7.1.0
$ ./configure --prefix=$HOME/php7/book/php-7.1.0/output --enable-fpm
bash

注意:默认情况下,make install 命令会把执行文件和库文件安装到 /usr/local/bin/usr/local/lib 目录。为了后续研究方便,我们使用 --prefix 将 PHP 7 安装到当前目录的 output 目录下,同时安装 php-fpm

执行 make 命令:

$ make && make install
$ cd output
$ ls
bin  etc  include  lib  php  sbin var
bash

到此,完成了 php-7.1.0 的编译安装,生成的可执行文件 php-fpmsbin 中,其他部分在 bin 目录下:

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

其中,phpCLI 模式下的 PHP 脚本执行程序。

PEAR(PHP Extension and Application Repository, PHP 扩展与应用库),是 PHP 官方开源类库,可以使用 pear list 列出所有已经安装的包。通过 pear install 可以安装需要的包。

PECL 是 PHP 的扩展库,可以通过 PEAR 的 Package Manager 的管理方式来下载和安装扩展代码。

以安装 yaconf 为例:

$ ./pecl install yaconf
...
install ok: channel://pecl.php.net/yaconf-1.0.6
configuration option "php_ini" is not set to php.ini location
You should add "extension=yaconf.so" to php.ini
bash

php-config 是输出 PHP 编译信息的辅助命令。

phpdbg 是一个轻量级,具有丰富功能的调试平台。PHP 5.4 以上版本支持,比如可以使用它查看 opcode

$ phpdbg -p* t.php
function name: (null)
L1-5 {main}()
L2    #0     ASSIGN                   $a                    1
L3    #1     ECHO                     $a
L5    #2     RETURN                   1
bash

phpdbg 的其他功能可以通过 phpdbg --help 查看。

phpize 命令用来动态安装扩展,如果在安装 PHP 时没有安装某个扩展,可以通过这个命令随时安装。

使用GDB调试PHP 7

GDB 是一个由 GNU 开源组织发布的、UNIX/Linux 操作系统下的、基于命令行的、功能强大的程序调试工具。当程序发生 coredump,通过 GDB 可以从 core 文件中复现场景,定位问题。

这里演示一下如何通过 GDB 来调试 PHP 程序。首先编写一段简单的代码 test.php

<?php
$a = '1';
echo $a;
php

下面开始进行 GDB 调试,运行 gdb php

$ gdb php
  (gdb)
bash

使用 b 命令在 main 函数入口增加断点:

(gdb) b main
Breakpoint 1 at 0x797df0: file /home/vagrant/php7/php-7.1.0/sapi/cli/php_cli.c,line 1181.
bash

使用 r 命令运行 test.php

(gdb) r test.php
Starting program: /home/vagrant/php7/php-7.1.0/output/bin/php test.php
[Thread debugging using libthread_db enabled]
Breakpoint 1, main (argc=2, argv=0x7fffffffe1b8) at /home/vagrant/php7/php-7.1.0/sapi/cli/php_cli.c:1181
1181        {
bash

从上面的输出中可以看到,代码执行在 main 函数处停止。接下来,使用 n 命令执行下一步:

(gdb) n
1288  memcpy(ini_entries + ini_entries_len + len, "\n\0", sizeof("\n\0"));
bash

使用 p 命令查看某个变量的信息:

(gdb) p ini_entries
$1 = 0x10c2150
"html _errors=0\nregister_argc_argv=1\nimplicit_flush=1\noutput_buffering=0\nmax_execution_time=0\nmax_input_time=-1\n"
(gdb)
bash

如果出现 <value optimized out>,是由于 GCC 编译器在编译过程中默认使用 -O2 优化选项所致,使用 -O0 选项可以关闭编译器的优化。在这里,通过修改 MakeFile 禁止编译器优化。查找 CFLAGS_CLEAN

CFLAGS_CLEAN = -I/usr/include -g -O2-fvisibility=hidden -DZEND_SIGNALS $(PROF_FLAGS)
makefile

将其中的 -O2 改为 -O0,然后执行 make clean && make && make install

另外,对于在 php-fpm 下运行的 PHP 程序如何调试呢?

在本地建立一个名为 www.local 的本地项目,来演示 php-fpm 运行模式下的调试:

$ mkdir /data/htdocs/www.local
$ touch /data/htdocs/www.local/index.php
bash

略过 Nginx 的配置过程,着重看一下 php-fpm 的配置:

$ vim ~/php7/book/php-7.1.0/output/conf/php-fpm.conf
// 添加以下配置项
[www.local]
pm=static
pm.max_children=1
pm.start_servers=1
pm.min_spare_servers=1
pm.max_spare_servers=1
bash

这段配置设定 php-fpm 的运行模式为 static,其最大进程数为 1、启动进程数为 1、最大和最小的空余进程数为 1。为什么要这么设定呢?这是为了保证 GDB 调试的进程一定是我们当前访问的进程。

完成 Nginxphp-fpm 的配置以后,重启这两个服务,可以看到 www.local 的项目只有一个进程,其 pid4459(后文还会用到该 pid):

$ systemctl restart php-fpm.service
$ systemctl restart nginx.service
$ ps aux|grep php-fpm
root      4458  0.0  0.8343816  4056 ?         Ss   13:11   0:00 php-fpm: master
process (/home/vagrant/php7/book/php-7.1.0/output/conf/php-fpm.conf)
www        4459  0.0  1.0344012  5140 ?         S     13:11   0:00 php-fpm: pool
www.local
bash

接下来开始调试,执行如下命令:

$ gdb php
(gdb) attach 4459
Attaching to process 4459
Reading symbols ...

如果没有报错,当前 GDB 已经 attachwww.local 的 php-fpm 进程上了。新开一个终端 2 执行 “curl www.local” 或者使用浏览器访问 www.local,然后回到终端 1,就可以和 CLI 模式一样进行调试了。

在学习和研究 PHP 7 的过程中,经常需要查看 opcodes,除了上文提到的 phpdbg 可以查看,另外还有一个 vld 扩展也非常好用,下面介绍下 vld 扩展。

vld扩展

PHP 代码的执行实际上是在执行代码解析后的各种 opcode。通过 vld 扩展可以很方便地看到执行过程中的 opcode。扩展可以从 https://github.com/derickr/vld 下载安装,下面是安装示例:

$ git clone https://github.com/derickr/vld.git
$ cd vld
$ /home/vagrant/php7/book/php-7.1.0/output/phpize
$ ./configure  --with-php-config=/home/vagrant/php7/book/php-7.1.0/output/php-config --enable-vld
$ make && make install
bash

到这里,扩展就安装完成了,接下来只需要在 PHP 的配置文件 php.ini 中启用该扩展即可:

extension=vld.so
bash

然后执行下边的命令:

$ php -m | grep vld
bash

看到有 vld 的输出,即表示扩展启用成功。

现在来写一段简单的 PHP 代码,看看生成的 opcode

<?php
$str = 'hello php7';
var_dump($str);
php

保存这段代码为 vld.php,然后在命令行执行:

$ php -dvld.active=1 vld.php
Finding entry points
Branch analysis from position: 0
Jump found. (Code = 62) Position 1 = -2
filename:       /home/vagrant/vld.php
function name:  (null)
number of ops:  5
compiled vars:  !0 = $str
line     #* E I O op           fetch  ext  return  operands
-------------------------------------------------------------------
    3     0  E >   ASSIGN                            !0, 'hello+php7'
    4     1        INIT_FCALL                        'var_dump'
          2        SEND_VAR                          !0
          3        DO_ICALL
    6     4      > RETURN                            1

  branch: #  0; line:     3-    6; sop:     0; eop:     4; out1:  -2
  path #1: 0,
string(10) "hello php7"
bash

从上边的输出可以看到这段代码一共有 5opcode

vld 扩展有下边几个参数。

  1. vld.active:是否在执行 PHP 的同时激活 vld——1 激活,0 不激活(默认不激活)。

  2. vld.execute:是否输出程序的执行结果——1 输出,0 不输出(默认输出)。

  3. vld.verbosity:显示更详细的 opcode 信息,开启后可以显示每个 opcode 的操作数的类型等信息。

    例如:

    3 0  E > ASSIGN  OP1[IS_CV !0 ] OP2[IS_CONST (0) 'hello+php7' ]
    bash
  4. vld.skip_prepend:是否跳过 php.ini 配置文件中 auto_prepend_file 配置项指定的文件,默认为 0,即不跳过包含的文件。vld.execute0 时有效;

  5. vld.skip_append:是否跳过 php.ini 配置文件中 auto_append_file 指定的文件,默认为 0,即不跳过包含的文件。vld.execute0 时有效;

  6. vld.format:是否启用自定义输出格式——1 启用,0不启用(默认不启用);

  7. vld.col_sep:自定义输出格式间隔符,vld.format 为 1 时有效;

  8. vld.save_dir:指定文件输出的路径,默认路径为 /tmp

  9. vld.save_paths:控制是否输出 dot 语言文件,默认为 0,表示不输出;

  10. vld.dump_paths:控制是否输出分支及路径信息——1 输出,0 不输出(默认输出)。

dot 是一种描述图形的语言,可以由 Graphviz 工具包来绘制 dot 描述的图形。vld 扩展可以直接通过命令来生成 dot 脚本,现以下面的代码来演示一下:

$ vim vld.php
<?php

class Test
{
    public $num;

    public function __construct($num)
    {
        $this->num = $num;
    }

    public function increase()
    {
        return $this->num + 1;
    }
}

$a = new Test(10);
var_dump($a->increase());
php

在命令行执行以下命令:

$ php -dvld.active=1 -dvld.save_paths=1 vld.php
$ ll /tmp
-rw-rw-r--1 vagrant vagrant  791 11? 30 02:41 paths.dot
$ dot -Tpng /tmp/paths.dot -o paths.png
bash

这样就可以生成一张调用图片,如图1-1所示。

image 2024 06 06 16 06 41 054
Figure 1. 图1-1 vld 生成的图片

介绍完 PHP 7 的安装和调试后,下面介绍几种不同平台上的代码阅读工具,基于它们,可以有效地提高源码阅读的效率。