👌

VLDを使ってPHPのopcodeを出力する

に公開

先日、PHPカンファレンス福岡にこのようなプロポーザルを出しました。
https://fortee.jp/phpcon-fukuoka-2025/proposal/f3ac417e-4de6-4950-b684-3272ecc841ef
(気になったら ☆ もらえると嬉しいです)

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
scripts/test.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_INITWelcome まで作られて、その後に $name に格納されているものを、ROPE_ADD で結合して、最後に ROPE_END で終端の ! を付けるという流れで、順々に作られていることが読み取れます。

終わりに

VLDを使うことで、PHPのopcodeを人間にわかりやすい形式で出力することができ、PHPで書いたスクリプトの処理の流れを知るために便利です。

GitHubで編集を提案
LAMP Inc. Tech Blog

Discussion