是什麼
在開始之前, 必須要先介紹一下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