LoginSignup
2
0

More than 1 year has passed since last update.

【PHP】PHPのコードからopcodeを抜き出した話

Last updated at Posted at 2023-03-15

経緯

就職作品としてopcodeを利用したモノを作りたかった為、opcodeについて調べ、値として取り出す方法を模索したものです。
拙い部分もあると思いますが、ヒマつぶし程度に読んでいただけるとありがたいです。

環境

XAMPP v3.3.0
PHP Version 8.1.6
Win10

参考記事

PHP による hello world 入門
ZendEngineにえこひいきされる標準関数たち (前篇)
【PHP】ローカル環境でオペコード(phpdbg・vld)を表示する

はじめに

ここからは私がopcodeを値として扱いたかった際に試行錯誤した内容がひたすら書かれています。

途中詰まった点なども書いており、読むにはつまらない内容があると思います。
お前の考えとかどうでもいい、結論どうすれば取得できるんだと言う方は#最終結果までお進みください。

また、ここではopcodeについてほとんど解説しません。
詳しくはPHP による hello world 入門をご覧ください。

opcodeの表示

opcodeの取得方法は2種類あります

phpdbg

phpdbgは標準で搭載されているデバッガです。

phpdbg -e hello.php
promot> print exec
hello.php
<?php echo "hello world";

出力結果

$_main:
     ; (lines=3, args=0, vars=0, tmps=0)
     ; I:\xampp\htdocs\実験\vld\hello.php:1-1
L0001 0000 EXT_STMT
L0001 0001 ECHO string("hello world")
L0001 0002 RETURN int(1)

vld

vldはpecl拡張モジュールです、使用するにはダウンロードする必要があります
ダウンロード方法は公式ホームページを参考にするか、私が参考にした「【PHP】ローカル環境でオペコード(phpdbg・vld)を表示する」を参考にするなどしてください。

php -d vld.active=1 -d vld.execute=0 hello.php

出力結果

Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename:       I:\xampp\htdocs\hello.php
function name:  (null)
number of ops:  2
compiled vars:  none
line      #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
    1     0  E >   ECHO                                                     'hello+world'
          1      > RETURN                                                   1

branch: #  0; line:     1-    1; sop:     0; eop:     1; out0:  -2
path #1: 0,

opcodeの取得

vldはダウンロードが少し面倒なのと、出力結果的に抜き出しずらいと思われたので今回はphpdbgの出力結果から抜き出すことにしました。

まずはこの出力結果を値を取得する必要があったのでexec()を使ってみました

phpdbgを使う場合は対話モードに対してprint execを渡す必要があるため、echo print exec | phpdbg -e hello.phpという風にパイプ「|」で繋いだコマンドを実行することで1つのコマンドで値を取得することができました。
image.png

これをexec()に渡せば値を受け取れるはず!

exec.php
$command = 'echo print exec | phpdbg -e hello.php';
exec($command, $output);

echo "<pre>";
print_r($output);
echo "</pre>";

出力結果
image.png

あれ?
$outputに入っていない。。。

失敗原因

exec()によると

exec(string $command, array &$output = null, int &$result_code = null): string|false

引数 output が存在する場合、指定した配列は、 コマンドからの出力の各行で埋められます。 \n のような後に続く空白は、この配列には含まれません。

とあるように出力が書き込まれるそうです、明記されていないので推測になりますが、ここで言う出力とは標準出力(STDOUT)のことだと思います。
それを裏付ける実験として以下のものを行いました。

実験とかどうでもいいという方は#失敗を踏まえて出力を取得までお進みください。

実験

行った実験は表示されているものが標準出力か標準エラー出力かを特定するものです。

echo A 1> result.txt

echo コマンドの後の1> result.txtは出力が標準出力をresult.txtに書き込むというものです。
それを実証するためにエラー出力の場合を見てみます。

qwerty 1> result.txt

存在しないコマンドqwertyを実行した際は標準出力がなく、result.txtの内容も空にりました。

次にエラーの際に書き込む2> result.txtでも試してみます。

qwerty 2> result.txt
result.txt
'qwerty' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

エラーの内容が書き込まれました。
これを本題のphpdbgにも試してみます。

phpdbgが標準エラー出力か確認

(echo print exec | phpdbg -e hello.php) 1> result.txt
result.txt
[Welcome to phpdbg, the interactive PHP debugger, v8.1.6]
To get help using phpdbg type "help" and press enter
[Please report bugs to <http://bugs.php.net/report.php>]
[Successful compilation of I:\xampp\htdocs\opcode\hello.php]
prompt> [Context I:\xampp\htdocs\opcode\hello.php (3 ops)]
prompt> 

これはexec()を実行した際の結果と同じです。

ではエラーの場合を確認してみましょう。

(echo print exec | phpdbg -e hello.php) 2> result.txt
result.txt

$_main:
     ; (lines=3, args=0, vars=0, tmps=0)
     ; I:\xampp\htdocs\opcode\hello.php:1-1
L0001 0000 EXT_STMT
L0001 0001 ECHO string("hello world")
L0001 0002 RETURN int(1)

このようにエラーの場合に結果が書き込まれているのでphpdbgで表示されるものは標準エラー出力であることが分かると思います。
更に、exec()の$outputで取得できるものは標準出力だと思われます。

失敗を踏まえて出力を取得

ひとつ前の実験結果からexec()は標準出力を受け取り、phpdbgの出力結果は標準エラー出力と分かりました。
そこでexec.phpのコマンドを少し変更します。

exec.php
<?php
$command = '(echo print exec | phpdbg -e hello.php 1>nul) 2>&1';
exec($command, $output);

echo "<pre>";
print_r($output);
echo "</pre>";

元の$commandはecho print exec | phpdbg -e hello.phpのみだったが、標準エラー出力が欲しいので 2>&1リダイレクトさせています。
ただこのままだと元の標準出力に付け足されるので( )内で標準出力を1>nul先に消しておき、その後空になった標準出力にエラー出力をリダイレクトさせています。

出力結果
image.png

上手く望みの値を取ることが出来ました。

値を解析

加工しやすい形にする

まず0~3の値は不要なので消します

exec.php
<?php
$command = '(echo print exec | phpdbg -e hello.php 1>nul) 2>&1';
exec($command, $output);

// 不要な値を削除
unset($output[0], $output[1], $output[2], $output[3]);
$output = array_values($output);

echo "<pre>";
print_r($output);
echo "</pre>";

次にL0001 0000 EXT_STMTは必要ないので出力されないようにコマンドを少し変更します。

<?php
$command = '(echo print exec | phpdbg hello.php 1>nul) 2>&1';

-eオプションがあることで要らない表示が出ていたようです。

意味毎に抜き出し

phpdbgではスペース毎に1回の実行内容が表示されているそうです、
なので初めはexplodeで解決しようと思いました。

$explode = explode(" ", $val);
ただこれだとstring("hello world")は判定できないので他の方法を取る必要があります。

正規表現で判定

どうしても細かく判定しなければ取得できなさそうだったので今回は正規表現を使うことにしました。
ここでもう一度現時点で取得できている値を確認してみます。

Array
(
    [0] => L0001 0000 ECHO string("hello world")
    [1] => L0001 0001 RETURN int(1)
)

まずはファイルの何行目で実行されているかのL***を取得します

foreach ($output as $val) {
    $regex = '/^L\d+\s/';
    preg_match($regex, $val, $matches);
    $file_line = trim($matches[0]);

    var_dump($file_line);
    echo "<br>";
}

出力結果

string(5) "L0001"
string(5) "L0001"

次に、L0001の後の数値、実行回数を取得
取得するにあたって既に取得したL0001を削除し、新たに先頭から取得するようにします。

foreach ($output as $val) {
    $regex = '/^L\d+\s/';
    preg_match($regex, $val, $matches);
    $file_line = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    $regex = '/^\d+\s/';
    preg_match($regex, $val, $matches);
    $line = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    var_dump($line);
    echo "<br>";
}

出力結果

string(4) "0000"
string(4) "0001"

次の値を取得する前にhello.phpを変更して出力結果を見てみます。

他のコードの場合を考慮

hello.php
<?php
for ($i = 0; $i < 10; $i++) {
    echo 'A';
}
phpdbg hello.php
print exec

出力結果

$_main:
     ; (lines=7, args=0, vars=1, tmps=3)
     ; I:\xampp\htdocs\opcode\hello.php:1-4       
L0002 0000 ASSIGN CV0($i) int(0)
L0002 0001 JMP 0004
L0003 0002 ECHO string("A")
L0002 0003 PRE_INC CV0($i)
L0002 0004 T3 = IS_SMALLER CV0($i) int(10)        
L0002 0005 JMPNZ T3 0002
L0004 0006 RETURN int(1)

0004を見てみると T3 = IS_SMALLER CV0($i) int(10)と他とは雰囲気が違うものがあります。
簡単に説明するとIS_SMALLERの結果をT3に格納しているものです、詳しくは下記の蛇足を。

蛇足

IS_SMALLER()はその後にあるCV0($i)がint(10)より小さいかを確認しており、その結果をT3に格納しています。
IS_SMALLER()の返り値は引数1(CV0) < 引数2(int(10))なら1、違うなら0になり、0005のJMPNZは第一引数(T3)が0じゃ無ければ第二引数(0002)にジャンプするという物です。

for文の動きを改めて1行づつ確認すると

  1. CV0に0を格納
  2. 0004にジャンプ
  3. CV0が10より小さいか確認
  4. JMPNZでT3が0じゃないので0002にジャンプ
  5. "A"が出力
  6. CV0をインクリメント
  7. CV0が10より小さいか確認...

となっています。

蛇足終了。

結果を取得する

同時にopcodeも取得するようにします。

foreach ($output as $val) {
    $regex = '/^L\d+\s/';
    preg_match($regex, $val, $matches);
    $file_line = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    $regex = '/^\d+\s/';
    preg_match($regex, $val, $matches);
    $line = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    $regex = '/^(?:([A-Z]+\d+)\s=\s)?([A-Z_]+)\s?/';
    preg_match($regex, $val, $matches);
    $return = trim ($matches[1]);
    $code   = trim($matches[2]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    var_dump($return, $code);
    echo "<BR>";
}

echo "<pre>";
print_r($output);
echo "</pre>";

出力結果

string(0) "" string(6) "ASSIGN"
string(0) "" string(3) "JMP"
string(0) "" string(4) "ECHO"
string(0) "" string(7) "PRE_INC"
string(2) "T3" string(10) "IS_SMALLER"
string(0) "" string(5) "JMPNZ"
string(0) "" string(6) "RETURN"

Array
(
    [0] => L0002 0000 ASSIGN CV0($i) int(0)
    [1] => L0002 0001 JMP 0004
    [2] => L0003 0002 ECHO string("hello world")
    [3] => L0002 0003 PRE_INC CV0($i)
    [4] => L0002 0004 T3 = IS_SMALLER CV0($i) int(10)
    [5] => L0002 0005 JMPNZ T3 0002
    [6] => L0012 0006 RETURN int(1)
)

最後の引数を取得

foreach ($output as $val) {
    $regex = '/^L\d+\s/';
    preg_match($regex, $val, $matches);
    $file_line = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    $regex = '/^\d+\s/';
    preg_match($regex, $val, $matches);
    $line = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    $regex = '/^(?:([A-Z]+\d+)\s=\s)?([A-Z_]+)\s?/';
    preg_match($regex, $val, $matches);
    $return = trim ($matches[1]);
    $code   = trim($matches[2]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);


    $regex = '/^(string\(.*\)|int\(\d+\)|array\(\.+\)|CV\d+.+\s|[A-Z]+\d+|\d+)?\s?/';
    preg_match($regex, $val, $matches);
    $op1 = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    preg_match($regex, $val, $matches);
    $op2 = trim($matches[0]);

    var_dump($op1, $op2);
    echo "<BR>";
}

echo "<pre>";
print_r($output);
echo "</pre>";

出力結果

string(7) "CV0($i)" string(6) "int(0)"
string(4) "0004" string(0) ""
string(11) "string("A")" string(0) ""
string(3) "CV0" string(0) ""
string(7) "CV0($i)" string(7) "int(10)"
string(2) "T3" string(4) "0002"
string(6) "int(1)" string(0) ""

Array
(
    [0] => L0002 0000 ASSIGN CV0($i) int(0)
    [1] => L0002 0001 JMP 0004
    [2] => L0003 0002 ECHO string("hello world")
    [3] => L0002 0003 PRE_INC CV0($i)
    [4] => L0002 0004 T3 = IS_SMALLER CV0($i) int(10)
    [5] => L0002 0005 JMPNZ T3 0002
    [6] => L0012 0006 RETURN int(1)
)

最終結果

ここまでで全ての値を特定できており、問題なく取れているかと思います。
あとはこれを配列にでも格納すればopcodeを値として取得できたと言えると思います。


<?php
$command = '(echo print exec | phpdbg hello.php 1>nul) 2>&1';
exec($command, $output);

// 不要な値を削除
unset($output[0], $output[1], $output[2], $output[3]);
$output = array_values($output);

$result = [];
foreach ($output as $val) {
    $regex = '/^L\d+\s/';
    preg_match($regex, $val, $matches);
    $file_line = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    $regex = '/^\d+\s/';
    preg_match($regex, $val, $matches);
    $line = trim($matches[0]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    $regex = '/^(?:([A-Z]+\d+)\s=\s)?([A-Z_]+)\s?/';
    preg_match($regex, $val, $matches);
    $return = trim ($matches[1]);
    $code   = trim($matches[2]);

    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    $regex = '/^(string\(.*\)|int\(\d+\)|array\(\.+\)|CV\d+.+\s|[A-Z]+\d+|\d+)?\s?/';
    preg_match($regex, $val, $matches);
    $op1 = trim($matches[0]);
    // 取得した部分を消す
    $val = preg_replace($regex, '', $val);

    preg_match($regex, $val, $matches);
    $op2 = trim($matches[0]);


    $result[] = [
        'file_line' => $file_line,
        'line'      => $line,
        'code'      => $code,
        'op1'       => $op1,
        'op2'       => $op2,
    ];    
}

echo "<pre>";
print_r($result);
echo "</pre>";

出力結果

出力結果
Array
(
    [0] => Array
        (
            [file_line] => L0002
            [line] => 0000
            [code] => ASSIGN
            [op1] => CV0($i)
            [op2] => int(0)
        )

    [1] => Array
        (
            [file_line] => L0002
            [line] => 0001
            [code] => JMP
            [op1] => 0004
            [op2] => 
        )

    [2] => Array
        (
            [file_line] => L0003
            [line] => 0002
            [code] => ECHO
            [op1] => string("A")
            [op2] => 
        )

    [3] => Array
        (
            [file_line] => L0002
            [line] => 0003
            [code] => PRE_INC
            [op1] => CV0
            [op2] => 
        )

    [4] => Array
        (
            [file_line] => L0002
            [line] => 0004
            [code] => IS_SMALLER
            [op1] => CV0($i)
            [op2] => int(10)
        )

    [5] => Array
        (
            [file_line] => L0002
            [line] => 0005
            [code] => JMPNZ
            [op1] => T3
            [op2] => 0002
        )

    [6] => Array
        (
            [file_line] => L0012
            [line] => 0006
            [code] => RETURN
            [op1] => int(1)
            [op2] => 
        )

)

いかがだったでしょうか、opcodeを値として取得したい人が私以外にいるとはあまり思えませんが、ネタ記事としてでも楽しんでもらえたら幸いです。
また、詰めが甘い部分が多々あるとは思いますが、そこはご容赦ください。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0