PHP获取Opcode及C源码

是什么

在开始之前, 必须要先介绍一下Opcode是什么.

众所周知, Java在执行的时候, 会将.java后缀的文件预先编译为.class字节码文件, JVM加载字节码文件进行解释执行. 而字节码文件存在的意义, 就是为了加速执行.

那么PHPOpcode与之类似, 也是从.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

image-20220623215444490

对于vld的输出结果, 这里有作者的一篇说明文章: https://derickrethans.nl/more-source-analysis-with-vld.html

vld扩展支持的配置. php的扩展配置可以在跑脚本的时候, 通过-d参数临时修改, 也可以直接修改php.ini文件. 这里建议临时修改, 毕竟并不是所有脚本都要输出opcode.

  • vld.active: 是否输出opcode. 默认为0
  • vld.execute: 是否要运行代码. 默认为1
    • 当为0时, 不会输出require的其他文件内容.
  • vld.verbosity: 显示更详细的信息. 默认为0, 可能值为0123
  • 等等吧, 还有一些其他的配置项, 不过感觉没什么用就不列举了. 可通过命令php -r 'phpinfo();' | grep vld 查看支持的所有配置.

phpdbg

按理说, 这么常用的操作, 应该是带有官方工具才对的吧. 哎, 这不就来了么. phpdbgphp程序的调试器(迄今为止, 我从来没有用过. 甚至没有用过断掉调试). 但同时它也可以用来生成opcode.

命令: phpdbg -p test.php

image-20220623221234892

生成结果与vld扩展基本一致.

还可以通过opcache来生成, 不过就有些绕了, 在这里就不介绍了. 简单介绍一下这两种方式就好.

phpdbg生成的话, 貌似只支持单文件生成(也可能是我没找到使用方法), vld则可以带着引入的文件一起打印出来.

不过对于我们分析程序来说, phpdbg一般是够用的了.

使用

那么上述生成的opcode是什么意思呢? 很遗憾, 官网对opcode的解释已经找不到了, 不过zend opcode document为关键词搜索的话, 还是能搜到一大堆的. 这里就不再重复罗列其含义了了.

我就简单说一下它有什么用吧. 总不能咱这折腾了半天, 拿到了opcode然后就没有然后了.

opcodephp文件翻译后的中间码, 通过它, 我们大致可以知道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

如下所示输出结果:

image-20220625102703295

同时会在当前目录生成opcode.log文件, 内容如下:

image-20220625102750935

可查看到opcode及每一个操作码具体执行的c函数是哪个.

其中require所对应的opcodeINCLUDE_OR_EVAL, 所执行的c函数为ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER.

总结

至此, opcode我们也见过了, 也能将php文件转换为opcode了. 不过说实话, 这玩意在平常的开发中不能说是用不到, 可以说是根本用不到.

它的作用我觉得还是在分析源码的时候. 可以方便的看到php代码的每一步操作, 其对应的源码执行.

以后研究源码, 或者是对php行为感到疑惑的时候, 有这个工具就可以加速解惑的过程啦.

调试镜像

介绍

此镜像是为了方便查看phpopcode及操作码对应执行的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

此命令产生如下结果:

  1. 获取php文件的opcode
  2. 获取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

订阅评论
提醒
guest
2 评论
最新
最旧
内联反馈
查看所有评论
2
0
希望看到您的想法,请发表评论。x