1.2 PHP 7安装和调试
学习了PHP 7的新特性后,再来了解PHP 7的编译安装和调试方式。
1.2.1 编译安装
以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时没有安装某个扩展,可以通过这个命令随时安装。
1.2.2 使用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扩展。
1.2.3 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扩展有下边几个参数。
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' ]
4)vld.skip_prepend:是否跳过php.ini配置文件中auto_prepend_file配置项指定的文件,默认为0,即不跳过包含的文件。vld.execute为0时有效;
5)vld.skip_append:是否跳过php.ini配置文件中auto_append_file指定的文件,默认为0,即不跳过包含的文件。vld.execute为0时有效;
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 -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
这样就可以生成一张调用图片,如图1-1所示。
图1-1 vld生成的图片
介绍完PHP 7的安装和调试后,下面介绍几种不同平台上的代码阅读工具,基于它们,可以有效地提高源码阅读的效率。