【PHP】从PHP的代码中提取出opcode的故事
経緯
– 事件的过程 de
– 经过
– 背景
– 经历我希望能够使用opcode来创建一些就职作品,因此我查找了有关opcode的信息,并试图找到提取其值的方法。虽然可能有一些不完善的地方,但如果您只是将其作为消遣阅读,我会非常感激。
环境XAMPP 第3.3.0版
PHP 版本为8.1.6
Win10操作系统
请参考下列文章。用 PHP 學習 hello world
標準函數在 ZendEngine 中受到偏愛(上卷)
【PHP】在本地環境中顯示操作碼(phpdbg・vld)
首先以下是关于我在想要将opcode作为值进行处理时所进行的反复尝试和错误的内容的详细描述。
PHP 版本为8.1.6
Win10操作系统
请参考下列文章。用 PHP 學習 hello world
標準函數在 ZendEngine 中受到偏愛(上卷)
【PHP】在本地環境中顯示操作碼(phpdbg・vld)
首先以下是关于我在想要将opcode作为值进行处理时所进行的反复尝试和错误的内容的详细描述。
写上了卡在中途的问题等,我认为有些内容读起来很无聊。
对于你的想法我不在乎,如果想知道结论该怎么得到,请直接跳到最后结果。
另外,在这里我几乎不会详细解释opcode。
请参阅用PHP编写的hello world入门教程以了解更多详细信息。
指示码的展示获取 opcode 的方法有两种。
用原汁原味的中文解释:phpdbg是一种调试工具。phpdbg是一个内置的调试器。
phpdbg -e hello.php
promot> print exec
<?php echo "hello world";
phpdbg -e hello.php
promot> print exec
<?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,需要下载pecl扩展模块。
您可以参考官方网站上的下载方法,或者参考我参考的《【PHP】在本地环境中显示操作码(phpdbg·vld)》等。
php -d vld.active=1 -d vld.execute=0 hello.php
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,
获取操作码由于VLD的下载略显繁琐,且提取输出结果感觉困难,所以我决定本次从phpdbg的输出结果中提取。
首先,因为需要获取该输出结果的值,所以我尝试使用了exec()。

$command = 'echo print exec | phpdbg -e hello.php';
exec($command, $output);
echo "<pre>";
print_r($output);
echo "</pre>";

$output里面没有进去。。。
失败的原因根据exec()函数的结果
exec(string $command, array &$output = null, int &$result_code = null): string|false
如果存在参数output,则指定的数组将被填充为来自命令输出的每一行。不会包含后续的空格,例如”\n”。
exec(string $command, array &$output = null, int &$result_code = null): string|false
如果存在参数output,则指定的数组将被填充为来自命令输出的每一行。不会包含后续的空格,例如”\n”。
据说程序的输出被写入了某个地方,虽然没有明确指出,但我推测这里所说的输出指的就是标准输出(STDOUT)。
为了验证这一点,我进行了以下实验。
如果你对实验之类的事情不太在乎,请根据失败经验继续获取输出。
做实验完成的实验是用于确定显示结果是标准输出还是标准错误输出的。
echo A 1> result.txt
echo A 1> result.txt
为了证明这一点,让我们看一下输出错误时的情况。
qwerty 1> result.txt
当执行不存在的命令qwerty时,没有标准输出,并且result.txt的内容也为空。
接下来,我将尝试将错误信息写入到2> result.txt中。
qwerty 2> result.txt
'qwerty' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
错误消息已写入。
我会尝试将其传递给主要的phpdbg。
确认phpdbg是标准错误输出+中文 (简体)
(echo print exec | phpdbg -e hello.php) 1> 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>
(echo print exec | phpdbg -e hello.php) 1> 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
$_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的命令。
<?php
$command = '(echo print exec | phpdbg -e hello.php 1>nul) 2>&1';
exec($command, $output);
echo "<pre>";
print_r($output);
echo "</pre>";
<?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删除,然后将空的标准输出重定向到错误输出。

分析数值
将其改造成易于加工的形状。首先,不需要的数值为0到3,会被删除。
<?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>";
<?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中,每个空间都会显示一次执行内容,所以最开始我想使用explode函数来解决这个问题。
$explode = explode(” “, $val);
只有这样还不能判断字符串(“hello world”),所以需要采取其他方法。
使用正则表达式进行判断。
因为似乎必须进行详细判断才能获取,所以这次决定使用正则表达式。
在这里,再次确认当前能够获取的值。
Array
(
[0] => L0001 0000 ECHO string("hello world")
[1] => L0001 0001 RETURN int(1)
)
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并查看输出结果。
考虑其他代码的情况
<?php
for ($i = 0; $i < 10; $i++) {
echo 'A';
}
phpdbg hello.php
print exec
<?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循环的运行过程时
-
- 将CV0存储为0
-
- 跳转到0004
-
- 检查CV0是否小于10
-
- 由于T3不为0,在JMPNZ上跳转到0002
-
- 输出”A”
-
- 将CV0递增
-
- 检查CV0是否小于10…
-
- CV0赋值为0
-
- 跳到0004
-
- 判断CV0是否小于10
-
- 由于T3不等于0,跳到0002
-
- 显示”A”
-
- 增加CV0
- 检查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>";
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)
)
最后的结果
到目前为止,我已经能够确定所有的值,并且能够顺利地获取它们。我认为只要将它们存储在数组中,我们就可以说已经获得了作为操作码的值。
<?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>";
<?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>";
产出结果
数组
(
[0] => 数组
(
[文件行] => L0002
[行] => 0000
[代码] => 赋值
[操作数1] => CV0($i)
[操作数2] => 整数(0)
)[1] => 数组
(
[文件行] => L0002
[行] => 0001
[代码] => 跳转
[操作数1] => 0004
[操作数2] =>
)
[2] => 数组
(
[文件行] => L0003
[行] => 0002
[代码] => 输出
[操作数1] => 字符串(“A”)
[操作数2] =>
)
[3] => 数组
(
[文件行] => L0002
[行] => 0003
[代码] => 预增
[操作数1] => CV0
[操作数2] =>
)
[4] => 数组
(
[文件行] => L0002
[行] => 0004
[代码] => 小于
[操作数1] => CV0($i)
[操作数2] => 整数(10)
)
[5] => 数组
(
[文件行] => L0002
[行] => 0005
[代码] => 非零跳转
[操作数1] => T3
[操作数2] => 0002
)
[6] => 数组
(
[文件行] => L0012
[行] => 0006
[代码] => 返回
[操作数1] => 整数(1)
[操作数2] =>
)
)
有其他人想要将opcode作为值获取吗,我并不认为除了我之外还有其他人会有这样的想法,但如果能将其作为娱乐文章令大家开心就好了。
另外,我知道有许多地方做得不够好,请您多多包涵。