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

原文地址: https://hujingnb.com/archives/836

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章