VLDを使ってPHPのopcodeを出力する
先日、PHPカンファレンス福岡にこのようなプロポーザルを出しました。 (気になったら ☆ もらえると嬉しいです)
php-srcの実装には興味があるけれど、いきなりC言語のコードを読むのはハードルが高い…と思ったあなたにおすすめなのがVLDです。
VLD (Vulcan Logic Disassembler) は、PHPのopcodeを、ダンプしてくれるツールです。VLDを使ってopcodeを出力することで、PHPの構文解析がどのように行われているかを理解する助けになります。
今回はDockerを使ってVLDをセットアップして、PHPスクリプトのopcodeを出力する方法を紹介します。
セットアップ方法
services:
php-vld:
build: .
volumes:
- ./scripts:/app/scripts
tty: true
stdin_open: true
FROM php:8.4-cli
# Install VLD extension
RUN pecl install channel://pecl.php.net/vld-0.19.1 && \
docker-php-ext-enable vld
# Set working directory
WORKDIR /app
# Keep container running
CMD ["tail", "-f", "/dev/null"]
使用方法
ディレクトリ構成
検証したいPHPスクリプトを./scriptsディレクトリに配置します。例えば、以下のような構成になります。
.
├── Dockerfile
├── docker-compose.yml
└── scripts
├── test.php
├── test2.php
└── test3.php
<?php
echo 'Hello World!' . PHP_EOL;
実行
docker compose run --rm php-vld \
php -d vld.active=1 -d vld.execute=0 \
/app/scripts/test.php
php のあとの -d vld.active=1 はVLDを有効にするオプションで、-d vld.execute=0 は実行を無効にするオプションです。これにより、PHPスクリプトのopcodeが出力されます。
-d vld.execute=1 とすると、opcodeの出力の後に、スクリプトの実行も行われます。
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename: /app/scripts/test.php
function name: (null)
number of ops: 2
compiled vars: none
line #* E I O op fetch ext return operands
-----------------------------------------------------------------------------------------
2 0 E > ECHO 'Hello+World%21%0A'
1 > RETURN 1
branch: # 0; line: 2- 2; sop: 0; eop: 1; out0: -2
path #1: 0,
'Hello+World%21%0A' と言うオペランドに対して、 ECHO というオペレーションが実行されることがわかります。
ソースコードと実行結果の例
演算子と戻り値とオペランドが行になって表示され、分岐ではJUMやJMPZでどこに飛んでいるかがわかります。
(この辺りの読み取り方は勉強中です🙏)
1. 条件分岐の例
<?php
if(mt_rand(1,6) % 2 === 0){
echo 'cho';
}else{
echo 'han';
}
Finding entry points
Branch analysis from position: 0
2 jumps found. (Code = 43) Position 1 = 7, Position 2 = 9
Branch analysis from position: 7
1 jumps found. (Code = 42) Position 1 = 10
Branch analysis from position: 10
1 jumps found. (Code = 62) Position 1 = -2
Branch analysis from position: 9
1 jumps found. (Code = 62) Position 1 = -2
filename: /app/scripts/test.php
function name: (null)
number of ops: 11
compiled vars: none
line #* E I O op fetch ext return operands
-----------------------------------------------------------------------------------------
3 0 E > INIT_FCALL 'mt_rand'
1 SEND_VAL 1
2 SEND_VAL 6
3 DO_ICALL $0
4 MOD ~1 $0, 2
5 IS_IDENTICAL ~1, 0
6 > JMPZ ~2, ->9
4 7 > ECHO 'cho'
3 8 > JMP ->10
6 9 > ECHO 'han'
7 10 > > RETURN 1
branch: # 0; line: 3- 3; sop: 0; eop: 6; out0: 7; out1: 9
branch: # 7; line: 4- 3; sop: 7; eop: 8; out0: 10
branch: # 9; line: 6- 7; sop: 9; eop: 9; out0: 10
branch: # 10; line: 7- 7; sop: 10; eop: 10; out0: -2; out1: -2
path #1: 0, 7, 10,
path #2: 0, 9, 10,
6から9に飛んで10で終了するルート(奇数のとき)と6で飛ばずに7に進んで8から10に飛んで終了するルート(偶数のとき)があることが読み取れます
2. 関数の例
<?php
function greet($name) {
echo "Hello, $name!";
}
greet("Alice");
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename: /app/scripts/test.php
function name: (null)
number of ops: 4
compiled vars: none
line #* E I O op fetch ext return operands
-----------------------------------------------------------------------------------------
5 0 E > INIT_FCALL 'greet'
1 SEND_VAL 'Alice'
2 DO_FCALL 0
6 3 > RETURN 1
branch: # 0; line: 5- 6; sop: 0; eop: 3; out0: -2
path #1: 0,
Function greet:
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename: /app/scripts/test.php
function name: greet
number of ops: 6
compiled vars: !0 = $name
line #* E I O op fetch ext return operands
-----------------------------------------------------------------------------------------
2 0 E > RECV !0
3 1 ROPE_INIT 3 ~2 'Hello%2C+'
2 ROPE_ADD 1 ~2 ~2, !0
3 ROPE_END 2 ~1 ~2, '%21'
4 ECHO ~1
4 5 > RETURN null
branch: # 0; line: 2- 4; sop: 0; eop: 5; out0: -2
path #1: 0,
End of function greet
関数は本流とは別で出力され、本流ではoperandにgreetが入って関数呼び出しが行われていることがわかります。
3. 入力を受け取る例
<?php
echo "Type your name: ";
$name = trim(fgets(STDIN));
echo "Welcome $name!" . PHP_EOL;
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename: /app/scripts/test.php
function name: (null)
number of ops: 13
compiled vars: !0 = $name
line #* E I O op fetch ext return operands
-----------------------------------------------------------------------------------------
3 0 E > ECHO 'Type+your+name%3A+'
4 1 INIT_FCALL 'fgets'
2 FETCH_CONSTANT ~1 'STDIN'
3 SEND_VAL ~1
4 DO_ICALL $2
5 FRAMELESS_ICALL_1 trim ~3 $2
6 ASSIGN !0, ~3
6 7 ROPE_INIT 3 ~6 'Welcome+'
8 ROPE_ADD 1 ~6 ~6, !0
9 ROPE_END 2 ~5 ~6, '%21'
10 CONCAT ~8 ~5, '%0A'
11 ECHO ~8
7 12 > RETURN 1
branch: # 0; line: 3- 7; sop: 0; eop: 12; out0: -2
path #1: 0,
"Welcome $name!"の部分は、最初 ROPE_INIT で Welcome まで作られて、その後に $name に格納されているものを、ROPE_ADD で結合して、最後に ROPE_END で終端の ! を付けるという流れで、順々に作られていることが読み取れます。
終わりに
VLDを使うことで、PHPのopcodeを人間にわかりやすい形式で出力することができ、PHPで書いたスクリプトの処理の流れを知るために便利です。
Discussion