是什么
在开始之前, 必须要先介绍一下Opcode
是什么.
众所周知, Java
在执行的时候, 会将.java
后缀的文件预先编译为.class
字节码文件, JVM
加载字节码文件进行解释执行. 而字节码文件存在的意义, 就是为了加速执行.
那么PHP
的Opcode
与之类似, 也是从.php
文件到执行的过程中, 所生成的预编译中间文件.
或者也可以这样粗鲁的理解, PHP
程序是由C
写的二进制程序, Opcode
就是将.php
文件翻译为c
代码的结果.
Opcode
有什么用我们最后再说, 先让我们看一下它长什么样子
获得
如何获得php
文件的opcode
呢? 在PHP
的源码中, 可以通过c
函数zend_compile_string
获取PHP
代码解析后的Opcode
. 但是我们要是为了获取Opcode
得深入到c
, 是在有些得不偿失. 好在, 已经有前辈做好的扩展可直接获取. 既: vld
.
vld 扩展
安装扩展:
# 安装扩展
pecl install https://pecl.php.net/get/vld
# 启用扩展. 若不是 docker, 将"extension=vld.so" 写入 php.ini 即可
docker-php-ext-enable vld
# 命令行查看, 确保扩展安装成功
php -m | grep vld
我们查看这段小小代码的opcode
:
<?php
require 't.php';
$a = 1;
$b = $a;
echo $a;
var_dump($b);
exit(0);
执行如下命令可查看:
php -d vld.active=1 -d vld.execute=0 test.php
对于vld
的输出结果, 这里有作者的一篇说明文章: https://derickrethans.nl/more-source-analysis-with-vld.html
vld
扩展支持的配置. php
的扩展配置可以在跑脚本的时候, 通过-d
参数临时修改, 也可以直接修改php.ini
文件. 这里建议临时修改, 毕竟并不是所有脚本都要输出opcode
.
vld.active
: 是否输出opcode
. 默认为0vld.execute
: 是否要运行代码. 默认为1- 当为0时, 不会输出
require
的其他文件内容.
- 当为0时, 不会输出
vld.verbosity
: 显示更详细的信息. 默认为0, 可能值为0123
- 等等吧, 还有一些其他的配置项, 不过感觉没什么用就不列举了. 可通过命令
php -r 'phpinfo();' | grep vld
查看支持的所有配置.
phpdbg
按理说, 这么常用的操作, 应该是带有官方工具才对的吧. 哎, 这不就来了么. phpdbg
是php
程序的调试器(迄今为止, 我从来没有用过. 甚至没有用过断掉调试). 但同时它也可以用来生成opcode
.
命令: phpdbg -p test.php
生成结果与vld
扩展基本一致.
还可以通过opcache
来生成, 不过就有些绕了, 在这里就不介绍了. 简单介绍一下这两种方式就好.
phpdbg
生成的话, 貌似只支持单文件生成(也可能是我没找到使用方法), vld
则可以带着引入的文件一起打印出来.
不过对于我们分析程序来说, phpdbg
一般是够用的了.
使用
那么上述生成的opcode
是什么意思呢? 很遗憾, 官网对opcode
的解释已经找不到了, 不过zend opcode document
为关键词搜索的话, 还是能搜到一大堆的. 这里就不再重复罗列其含义了了.
我就简单说一下它有什么用吧. 总不能咱这折腾了半天, 拿到了opcode
然后就没有然后了.
opcode
是php
文件翻译后的中间码, 通过它, 我们大致可以知道php
文件的执行过程.
又因为php
是通过c
层面进行解析的, 每一条opcode
都会解析为一个c
函数进行执行. 对于分析源码、查找问题等等, 可直接定位到php
代码在c
源码级别的执行, 方便得很嘛. (类似需求我之前碰到过很多次, 比如查找sort
的实现原理等等)
所有操作码都定义在源码文件zend_vm_opcodes.h
中. 既然php
会根据不同的操作码, 执行不同的操作. 那么, 我们是不是就可以根据操作码, 来还原php
底层执行的操作了呢? 不好意思, 可以但是很难. php
通过函数zend_vm_get_opcode_handler
来获取操作码对应的handle
函数. 但是, 当看过源码后, 我失望了, 函数zend_vm_get_opcode_handler
获取的过程是一个动态解析的过程. 也就是说, 同一个操作码, 解析后可能会是不同的函数. 啊这不就尴尬了么.
于是, 不信邪的我, 决定通过修改PHP
源码来实现. 为了方便使用, 我将其封装为了一个docker
镜像, 对实现方式感兴趣的, 请移至Dockerfile. 使用方式如下(镜像的详情见: 调试镜像):
docker run --rm -it -v
pwd:
pwd -w
pwd hujingnb/php_opcode:8.1.7 php test.php
如下所示输出结果:
同时会在当前目录生成opcode.log
文件, 内容如下:
可查看到opcode
及每一个操作码具体执行的c
函数是哪个.
其中require
所对应的opcode
为INCLUDE_OR_EVAL
, 所执行的c
函数为ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER
.
总结
至此, opcode
我们也见过了, 也能将php
文件转换为opcode
了. 不过说实话, 这玩意在平常的开发中不能说是用不到, 可以说是根本用不到.
它的作用我觉得还是在分析源码的时候. 可以方便的看到php
代码的每一步操作, 其对应的源码执行.
以后研究源码, 或者是对php
行为感到疑惑的时候, 有这个工具就可以加速解惑的过程啦.
调试镜像
介绍
此镜像是为了方便查看php
的opcode
及操作码对应执行的c
函数. 为了方便对php
源码进行分析. 通过结果, 可通过php
文件直接定位到php
源码的c
函数.
此镜像在vld
扩展的基础上, 额外输出了:
- 操作码对应的
c
执行handle
函数
此镜像基于php
分支php-8.1.7
, commitId 为d35e577a1bd0b35b9386cea97cddc73fd98eed6d
.
镜像地址. 这里就不说明我是怎么做的了, 感兴趣的可查看Dockerfile
使用
通过此镜像获得操作码简单方式:
docker run --rm -it -v `pwd`:`pwd` -w `pwd` hujingnb/php_opcode:8.1.7 php test.php
此命令产生如下结果:
- 获取
php
文件的opcode
- 获取
opcode
操作码对应的执行c
函数. 将结果输出到当前目录的opcode.log
文件中
高级
若需要安装扩展, 可进入镜像后执行如下操作:
- 从
php
源码编译安装gd
扩展:docker-php-ext-configure gd
- 从
php
源码安装gd
扩展:docker-php-ext-install gd
- 启用
gd
扩展:docker-php-ext-enable gd
- 通过官方库安装扩展:
pecl install redis && docker-php-ext-enable redis
环境变量:
PHP_SRC_DIR
: 源码位置PHP_INI_DIR
: 配置文件位置PHP_INSTALL_DIR
: 安装路径
若需要添加额外操作, 可基于此镜像进行操作, 请根据Dockerfile自行修改.
若想要修改php
源码, 可在修改后执行命令重新安装: docker-php-install