PHP 7底层设计与源码实现
上QQ阅读APP看书,第一时间看更新

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这里输出的opcode均省略了ZEND_前缀,例如ASSIGN的实际定义为ZEND_ASSIGN,其值为38。

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