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
注意:默认情况下,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
到此,完成了 php-7.1.0 的编译安装,生成的可执行文件 php-fpm 在 sbin 中,其他部分在 bin 目录下:
pear peardev pecl phar phar.phar php php-cgi php-config phpdbg phpize
其中,php 是 CLI 模式下的 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
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
phpdbg 的其他功能可以通过 phpdbg --help 查看。
phpize 命令用来动态安装扩展,如果在安装 PHP 时没有安装某个扩展,可以通过这个命令随时安装。
使用GDB调试PHP 7
GDB 是一个由 GNU 开源组织发布的、UNIX/Linux 操作系统下的、基于命令行的、功能强大的程序调试工具。当程序发生 coredump,通过 GDB 可以从 core 文件中复现场景,定位问题。
这里演示一下如何通过 GDB 来调试 PHP 程序。首先编写一段简单的代码 test.php:
<?php
$a = '1';
echo $a;
下面开始进行 GDB 调试,运行 gdb php:
$ gdb php
(gdb)
使用 b 命令在 main 函数入口增加断点:
(gdb) b main
Breakpoint 1 at 0x797df0: file /home/vagrant/php7/php-7.1.0/sapi/cli/php_cli.c,line 1181.
使用 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 {
从上面的输出中可以看到,代码执行在 main 函数处停止。接下来,使用 n 命令执行下一步:
(gdb) n
1288 memcpy(ini_entries + ini_entries_len + len, "\n\0", sizeof("\n\0"));
使用 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)
如果出现 <value optimized out>,是由于 GCC 编译器在编译过程中默认使用 -O2 优化选项所致,使用 -O0 选项可以关闭编译器的优化。在这里,通过修改 MakeFile 禁止编译器优化。查找 CFLAGS_CLEAN:
CFLAGS_CLEAN = -I/usr/include -g -O2-fvisibility=hidden -DZEND_SIGNALS $(PROF_FLAGS)
将其中的 -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
略过 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
这段配置设定 php-fpm 的运行模式为 static,其最大进程数为 1、启动进程数为 1、最大和最小的空余进程数为 1。为什么要这么设定呢?这是为了保证 GDB 调试的进程一定是我们当前访问的进程。
完成 Nginx 和 php-fpm 的配置以后,重启这两个服务,可以看到 www.local 的项目只有一个进程,其 pid 为 4459(后文还会用到该 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
接下来开始调试,执行如下命令:
$ gdb php (gdb) attach 4459 Attaching to process 4459 Reading symbols ...
如果没有报错,当前 GDB 已经 attach 到 www.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
到这里,扩展就安装完成了,接下来只需要在 PHP 的配置文件 php.ini 中启用该扩展即可:
extension=vld.so
然后执行下边的命令:
$ php -m | grep vld
看到有 vld 的输出,即表示扩展启用成功。
现在来写一段简单的 PHP 代码,看看生成的 opcode:
<?php
$str = 'hello php7';
var_dump($str);
保存这段代码为 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"
从上边的输出可以看到这段代码一共有 5 个 opcode。
vld 扩展有下边几个参数。
-
vld.active:是否在执行 PHP 的同时激活 vld——1 激活,0 不激活(默认不激活)。 -
vld.execute:是否输出程序的执行结果——1 输出,0 不输出(默认输出)。 -
vld.verbosity:显示更详细的opcode信息,开启后可以显示每个opcode的操作数的类型等信息。例如:
3 0 E > ASSIGN OP1[IS_CV !0 ] OP2[IS_CONST (0) 'hello+php7' ] -
vld.skip_prepend:是否跳过php.ini配置文件中auto_prepend_file配置项指定的文件,默认为0,即不跳过包含的文件。vld.execute为0时有效; -
vld.skip_append:是否跳过php.ini配置文件中auto_append_file指定的文件,默认为0,即不跳过包含的文件。vld.execute为0时有效; -
vld.format:是否启用自定义输出格式——1 启用,0不启用(默认不启用); -
vld.col_sep:自定义输出格式间隔符,vld.format为 1 时有效; -
vld.save_dir:指定文件输出的路径,默认路径为/tmp; -
vld.save_paths:控制是否输出dot语言文件,默认为 0,表示不输出; -
vld.dump_paths:控制是否输出分支及路径信息——1 输出,0 不输出(默认输出)。
|
在命令行执行以下命令:
这样就可以生成一张调用图片,如图1-1所示。
Figure 1. 图1-1 vld 生成的图片
|
介绍完 PHP 7 的安装和调试后,下面介绍几种不同平台上的代码阅读工具,基于它们,可以有效地提高源码阅读的效率。