引言
在python中有一个库Construct ,可以用来解析二进制数据,用这个工具分析网络包,带格式的数据文件等等很方便。
前一阵子分析sqlite数据库文件格式时要是用这种工具,也可以省不少事。
不过Construct2.9与以前的版本,改动很大,原来用Construct写的python代码,基本上要重新改写一遍了。在查看Construct源码的过程中,发现Construct的基本实现思路是递归下降分析法。用对象的构造方法来动态地定义数据结构,在parse方法中实现对二进制数据的解析。
打算用php实现二进制数据的解析,但是在实现思路上与python的Construct完全不一样。
基本思路
由于python的Construct用python的对象语法来实现动态层次结构的定义与解析,受限于python的对象语法,有一些结构定义看起来很晦涩。
我的想法是定义一个小小的,专门用于描述动态层次结构的语言,这样一来就可以尽量的照顾人们常用的表达习惯。
以这个小项目来说,以C语言中的结构体定义语法为蓝本,在此基础上增加条件化,循环环的结构定义。
在二进制数据中有一种常见的结构,前面几个字节存放后面数据块的长度,接下来才是数据块。这种结构用C语言的结构体定义表示时并不方便。整个数据块的长度是变化的,在编译时无法确定,只能在解析时才能确定。因此有必要对C语言的结构体定义语法进行扩展。
第一步,实现对 C语言结构体的解析
在这一步,先不考虑动态的层次的结构体定义,而是实现这个小语言的最核心的部分,至少它要能理解C语言的结构体定义,并能根据这个结构体定义对二进制数据进行解析。这一步完成后再实现动态的结构体的定义与解析。
本项目基于前面文件中简单介绍过的ADOS脚本语言引擎。
先看一下我们的任务
这里有一个完全是C语言规格的结构体定义文件
blockStruct.h
struct student
{
char name[2];
int num;
int age;
char addr[3];
};
struct teacher
{
char name[2];
int num;
char addr[3];
};
待解析的二进制数据块
"\x41\x42\x01\x00\x00\x00\x02\x00\x00\x00\x41\x42\x43\x41\x42\x01\x00\x00\x00\x41\x42\x43"
希望得到的解析结果
[value] => Array
(
[name] => AB
[num] => 1
[age] => 2
[addr] => ABC
)
[value] => Array
(
[name] => AB
[num] => 1
[addr] => ABC
)
感兴趣的朋友可以留意一下这三者之间的关系
下而是仅实现了对C语言结构体进行编译的词法规则文件
<?php
/*!
* struckwkr的词法规则
* 45022300@qq.com
* Version 0.9.0
*
* Copyright 2019, Zhu Hui
* Released under the MIT license
*/
$GLOBALS['structwkr_lexRules'] = [
['/^\"(.*?)\"/','_cons' ,'"'],
['/^\(/','_lp' ,'('],
['/^\)/','_rp' ,')'],
['/^\[/','_lb' ,'['],
['/^\]/','_rb' ,']'],
['/^\{/','_lcb' ,'{'],
['/^\}/','_rcb' ,'}'],
['/^;/','_semi' ,';'],
['/^,/','_comma' ,','],
['/^==/','_bieq' ,'='],
['/^!=/','_uneq' ,'!'],
['/^\>=/','_greq' ,'>'],
['/^\<=/','_leeq' ,'<'],
['/^=/','_equa' ,'='],
['/^\>/','_grea' ,'>'],
['/^\</','_less' ,'<'],
['/^\+/','_add' ,'+'],
['/^-/','_sub' ,'-'],
['/^\*/','_mul' ,'*'],
['/^\//','_div' ,'/'],
['/^%/','_mod' ,'$'],
['/^&&/','_and' ,'&'],
['/^\|\|/','_or' ,'|'],
['/^!/','_not' ,'!'],
['/^struct/','_strukey' ,'s'],
['/^char/','_char' ,'c'],
['/^int/','_int' ,'i'],
['/^float/','_float' ,'f'],
['/^double/','_double' ,'d'],
['/^[0-9]+([.]{1}[0-9]+){0,1}/','_num',''],
['/^\./','_dot' ,'.'],
['/^[\x{4e00}-\x{9fa5}A-Za-z_][\x{4e00}-\x{9fa5}A-Za-z0-9_]*\b/u','_iden',''],
['/^\s*/','_null','']
];
下面是仅实现了对C语言结构体进行编译的语法规则文件
<?php
/*!
* structwkr的语法规则处理器
* 45022300@qq.com
* Version 0.9.0
*
* Copyright 2019, Zhu Hui
* Released under the MIT license
*/
namespace Ados;
require_once 'const.php';
require_once __SCRIPTCORE__.'syntax_rule/base_rules_handler.php';
class StructwkrRulesHandler extends BaseRulesHandler{
//语法分析的起始记号,归约到最后得到就是若干个结构体定义的列表
function startToken(){
return '_structList';
}
//附加类型
function extraType($extraArray){
if(count($extraArray)>0){
return $extraArray[0];
}else{
return '';
}
}
//求出放在附加信息中的数组长度
function elementSize($extraArray){
if(count($extraArray)>0){
return intval($extraArray[0]);
}else{
return 0;
}
}
//处理源代码中的多条声明语句
function handleStatementList($stack,$coder,$listIndex,$statementIndex){
$t1= $this->topItem($stack,$statementIndex);
if($listIndex>0){
$t2= $this->topItem($stack,$listIndex);
//将元素名所在项的附加信息数组找出来
$extraArray=$t2[TokenExtraIndex];
}else{ //$listIndex=0表示,statementList是上级节点,附加信息数组应该是空数组
$extraArray=[];
}
//将statement中的附加信息添加到statementList的附加信息中去
$extraArray[]=$t1[TokenExtraIndex];
return ['#',$extraArray];
}
//处理源代码中的一条声明语句
function handleStatement($stack,$coder,$typeItemIndex,$idenItemIndex){
$t1= $this->topItem($stack,$typeItemIndex);
$elementType = $t1[TokenValueIndex];
$t2= $this->topItem($stack,$idenItemIndex);
$elementName = $t2[TokenValueIndex];
//将元素名所在项的附加信息数组找出来
$extraArray=$t2[TokenExtraIndex];
$valLen =$extraArray[0];
//附加信息中包含 元素名称,元素类型,数据长度
return [$elementName,[$elementName,$elementType,$valLen]];
}
//语法规则处理函数名由规则右边部分与规则左边部分拼接而成
//语法规则定义的先后决定了归约时匹配的顺序,要根据实际的语法安排
//如果不熟悉语法,随意调整语法规则的先后次序将有可能导致语法错误
// struct list {{{
function _structList_0_structList_struct($stack,$coder){
$coder->pushBlockTail();
return ['#',[]];
}
function _structList_0_struct($stack,$coder){
$coder->pushBlockTail();
return ['#',[]];
}
// struct list }}}
// struct {{{
function _struct_0_structName_blockStatement_semi($stack,$coder){
$t1= $this->topItem($stack,3);
$structName = $t1[TokenValueIndex];
$t2= $this->topItem($stack,2);
$extraArray=$t2[TokenExtraIndex];
return [$structName,$extraArray];
}
// struct }}}
// struct name {{{
function _structName_0_strukey_iden($stack,$coder){
$t1= $this->topItem($stack,1);
$structName = $t1[TokenValueIndex];
$coder->pushBlockHeader($structName);
return $this->pass($stack,1);
}
// struct name }}}
// blockStatement {{{
function _blockStatement_0_lcb_statementList_rcb($stack,$coder){
return $this->pass($stack,2);
}
// blockStatement }}}
// statement list {{{
function _statementList_0_statementList_statement($stack,$coder){
return $this->handleStatementList($stack,$coder,2,1);
}
function _statementList_0_statement($stack,$coder){
//此处0表示statementList是上一级节点,要做特殊处理
return $this->handleStatementList($stack,$coder,0,1);
}
// statement list }}}
// statement {{{
function _statement_0_double_term_semi($stack,$coder){
$t1= $this->topItem($stack,2);
$elementName = $t1[TokenValueIndex];
$parseFuncName = 'parseDouble';
$coder->pushBlockBody($parseFuncName,$elementName);
return $this->handleStatement($stack,$coder,3,2);
}
function _statement_0_float_term_semi($stack,$coder){
$t1= $this->topItem($stack,2);
$elementName = $t1[TokenValueIndex];
$parseFuncName = 'parseFloat';
$coder->pushBlockBody($parseFuncName,$elementName);
return $this->handleStatement($stack,$coder,3,2);
}
function _statement_0_char_term_semi($stack,$coder){
$t1= $this->topItem($stack,2);
$elementName = $t1[TokenValueIndex];
$size = $this->elementSize($t1[TokenExtraIndex]);
$parseFuncName = 'parseFixStr';
$coder->pushBlockBody($parseFuncName,$elementName,$size);
return $this->handleStatement($stack,$coder,3,2);
}
function _statement_0_int_term_semi($stack,$coder){
$t1= $this->topItem($stack,2);
$elementName = $t1[TokenValueIndex];
$parseFuncName = 'parseInt';
$coder->pushBlockBody($parseFuncName,$elementName,4);
return $this->handleStatement($stack,$coder,3,2);
}
// statement }}}
// term {{{
function _term_0_term_array($stack,$coder){
$t1= $this->topItem($stack,1);
$valLen = $t1[TokenValueIndex];
//将数据长度放入附加信息
$t2= $this->topItem($stack,2);
return [$t2[TokenValueIndex],[$valLen]];
}
function _term_0_iden($stack,$coder){
$t1= $this->topItem($stack,1);
//未指定数据长度时将长度值设为0
$valLen = '0';
$t2= $this->topItem($stack,2);
return [$t1[TokenValueIndex],[$valLen]];
}
// term }}}
// array {{{
function _array_0_lb_num_rb($stack,$coder){
return $this->pass($stack,2);
}
// array }}}
} // end of class
下面是对基本数据类型,整数,字符串的解析函数(备注,用于实验,还没有覆盖全部的基本数据类型)
<?php
//用于解析基本数据类型的内置函数
namespace Ados;
//解析一个字节,得到无符号整数值
function parseByte($context,$size = 0){
$pos=$context['pos'];
$data = substr($context['data'], $pos);
if(strlen($data)>0){
$raw = substr($data, 0,1);
$value = unpack("C1",$raw)[1];
return ['value'=>$value,'size'=>1,'error'=>0,'msg'=>'ok'];
}else{
return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'not enough for parseByte'];
}
}
//解析一个有符号整数
function parseInt($context,$size = 4){
if(!($size == 2 or $size == 4 or $size == 8 )){
return ['value'=>False,'size'=>0,'error'=>2,'msg'=>'not a valid size'];
}
if($size == 2) $format = "s1";
if($size == 4) $format = "l1";
if($size == 8) $format = "q1";
$pos=$context['pos'];
$data = substr($context['data'], $pos);
if(strlen($data)>=$size){
$raw = substr($data, 0,$size);
$value = unpack($format,$raw)[1];
return ['value'=>$value,'size'=>$size,'error'=>0,'msg'=>'ok'];
}else{
return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'no data for parseInt'];
}
}
//解析一个大端无符号整数
function parseBUInt($context,$size = 4){
if(!($size == 2 or $size == 4 or $size == 8 )){
return ['value'=>False,'size'=>0,'error'=>2,'msg'=>'not a valid size'];
}
if($size == 2) $format = "n1";
if($size == 4) $format = "N1";
if($size == 8) $format = "J1";
$pos=$context['pos'];
$data = substr($context['data'], $pos);
if(strlen($data)>=$size){
$raw = substr($data, 0,$size);
$value = unpack($format,$raw)[1];
return ['value'=>$value,'size'=>$size,'error'=>0,'msg'=>'ok'];
}else{
return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'no data for parseBUInt'];
}
}
//解析一个小端无符号整数
function parseLUInt($context,$size = 4){
if(!($size == 2 or $size == 4 or $size == 8 )){
return ['value'=>False,'size'=>0,'error'=>2,'msg'=>'not a valid size'];
}
if($size == 2) $format = "v1";
if($size == 4) $format = "NL";
if($size == 8) $format = "P1";
$pos=$context['pos'];
$data = substr($context['data'], $pos);
if(strlen($data)>=$size){
$raw = substr($data, 0,$size);
$value = unpack($format,$raw)[1];
return ['value'=>$value,'size'=>$size,'error'=>0,'msg'=>'ok'];
}else{
return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'no data for parseLUInt'];
}
}
//解析一个null结束的字符串
function parseString($context,$size=0){
$pos=$context['pos'];
$data = substr($context['data'], $pos);
$p=0;
$raw = substr($data, $p,1);
$byte = unpack("C1",$raw)[1];
$result ='';
while($byte){
$result.=$raw;
$p++;
if($p>=strlen($data)){
return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'not find null end for parseString'];
}
$raw = substr($data, $p,1);
$byte = unpack("C1",$raw)[1];
}
return ['value'=>$result,'size'=>$p,'error'=>0,'msg'=>'ok'];
}
//解析一个定长字符串
function parseFixStr($context,$size=0){
$pos=$context['pos'];
$data = substr($context['data'], $pos);
//var_dump($data);
if(strlen($data)>=$size){
$result ='';
for($i=0;$i<$size;$i++){
$raw = substr($data, $i,1);
$value = unpack("a1",$raw)[1];
$result.=$value;
}
return ['value'=>$result,'size'=>$size,'error'=>0,'msg'=>'ok'];
}
return ['value'=>False,'size'=>0,'error'=>1,'msg'=>'not enough for parseFixedString'];
}
下面是用于模板替换的工作函数
<?php
//用于进行模板替换的工作函数
namespace Ados;
defined('__STRUCT_PARSE_TEMP__') or define('__STRUCT_PARSE_TEMP__', './');
define('_blockParsTempFile_',__STRUCT_PARSE_TEMP__.'parseFunc.template.php');
//取出两个记号串之间的内容
function strBetweenToke($src,$toke1,$toke2){
$p1 = strpos($src,$toke1);
$p2 = strpos($src,$toke2);
return substr($src,$p1+strlen($toke1),$p2-$p1-strlen($toke1));
}
//取得块分析函数的header
function blockHeaderStr(){
$src = file_get_contents(_blockParsTempFile_);
return strBetweenToke($src,'/*blockHeader{{*/','/*blockHeader}}*/');
}
//取得块分析函数的body
function blockBodyStr(){
$src = file_get_contents(_blockParsTempFile_);
return strBetweenToke($src,'/*blockBody{{*/','/*blockBody}}*/');
}
//取得块分析函数的tail
function blockTailStr(){
$src = file_get_contents(_blockParsTempFile_);
return strBetweenToke($src,'/*blockTail{{*/','/*blockTail}}*/');
}
define('_blockHeaderStr_',blockHeaderStr());
define('_blockBodyStr_',blockBodyStr());
define('_blockTailStr_',blockTailStr());
function makeBlockHeader($blockName){
return str_replace('parseBlock_Temp', 'parse'.$blockName, _blockHeaderStr_);
}
function makeblockBody($parseFuncName,$filedName='',$filedSize=0){
$tmp = str_replace('parseByte', $parseFuncName, _blockBodyStr_);
$tmp = str_replace('$filedName', $filedName, $tmp);
$tmp = str_replace('$filedSize', $filedSize, $tmp);
return $tmp;
}
function makeblockTail(){
return _blockTailStr_ ;
}
/*
echo blockHeaderStr();
echo blockBodyStr();
echo blockTailStr();
echo makeBlockHeader('Test1');
echo makeblockBody('parseInt');
echo makeblockBody('parseStr');
echo makeblockTail();
*/
有了这些准备工作后,就可以实现一个语法制导的编码器,用于生成最终的解析函数。
下而就是一个简单的编码器,在对结构体定义进行语法分析时,生成可用于解析二进制数据的脚本代码
<?php
/*!
* structwkr编码器,
*
* 45022300@qq.com
* Version 0.9.0
*
* Copyright 2019, Zhu Hui
* Released under the MIT license
*/
namespace Ados;
require_once __SCRIPTCORE__.'coder/base_coder.php';
require_once __STRUCT_PARSE_TEMP__.'templateReplaceFuncs.php';
class StructwkrCoder extends BaseCoder{
public function __construct($engine)
{
if($engine){
$this->engine = $engine;
}else{
exit('the engine is not valid in StructwkrCoder construct.');
}
}
//编译得到的最终结果
public function codeLines(){
if(count($this->codeLines)<1){
return '';
}
$script='';
for ($i=0;$i< count($this->codeLines);$i+=1) {
$script.=$this->codeLines[$i];
}
return $script;
}
//输出编译后的结果
public function printCodeLines(){
echo $this->codeLines();
}
//添加一个块解析函数头
public function pushBlockHeader($structName){
$structName=ucfirst($structName);
$content = makeBlockHeader($structName);
array_push($this->codeLines, $content);
$lineIndex=$this->lineIndex;
$this->lineIndex+=1;
return $lineIndex;
}
//添加一个块解析函数体
public function pushBlockBody($parseFuncName,$filedName='',$filedSize=0){
$content = makeblockBody($parseFuncName,$filedName,$filedSize);
array_push($this->codeLines, $content);
$lineIndex=$this->lineIndex;
$this->lineIndex+=1;
return $lineIndex;
}
//添加一个块解析函数尾
public function pushBlockTail(){
$content = makeblockTail();
array_push($this->codeLines, $content);
$lineIndex=$this->lineIndex;
$this->lineIndex+=1;
return $lineIndex;
}
}
自动生成的用于解析的脚本文件
对c语言的结构体进行编译,最终得到可以用于解析二进制数据的一系列函数,比如上述结构体定义文件中定义了两个结构体
struct student
{
char name[2];
int num;
int age;
char addr[3];
};
struct teacher
{
char name[2];
int num;
char addr[3];
};
那么就会生成与这两个结构体对应的解析函数parseStudent与parseTeacher。
下面就是自动生成的测试脚本
<?php
namespace Ados;
//加载常量定义文件
require_once 'const.php';
require_once __STRUCT_PARSE_TEMP__.'templateBuidinFuncs.php';
$context['pos']=0;
$context['data']="\x41\x42\x01\x00\x00\x00\x02\x00\x00\x00\x41\x42\x43\x41\x42\x01\x00\x00\x00\x41\x42\x43";
$expRes = parseStudent($context);
$context['pos']+=$expRes['size'];
print_r($expRes);
$expRes = parseTeacher($context);
$context['pos']+=$expRes['size'];
print_r($expRes);
function parseStudent($context,$size=0){
$valueArray=[];
$totalSize = 0;
$expRes = parseFixStr($context,2);
if($expRes['error']==0){
$filed = 'name';
if($filed){
$valueArray[$filed]=$expRes['value'];
}else{
$valueArray[]=$expRes['value'];
}
$context['pos']+=$expRes['size'];
$totalSize+= $expRes['size'];
}else{
return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
}
$expRes = parseInt($context,4);
if($expRes['error']==0){
$filed = 'num';
if($filed){
$valueArray[$filed]=$expRes['value'];
}else{
$valueArray[]=$expRes['value'];
}
$context['pos']+=$expRes['size'];
$totalSize+= $expRes['size'];
}else{
return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
}
$expRes = parseInt($context,4);
if($expRes['error']==0){
$filed = 'age';
if($filed){
$valueArray[$filed]=$expRes['value'];
}else{
$valueArray[]=$expRes['value'];
}
$context['pos']+=$expRes['size'];
$totalSize+= $expRes['size'];
}else{
return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
}
$expRes = parseFixStr($context,3);
if($expRes['error']==0){
$filed = 'addr';
if($filed){
$valueArray[$filed]=$expRes['value'];
}else{
$valueArray[]=$expRes['value'];
}
$context['pos']+=$expRes['size'];
$totalSize+= $expRes['size'];
}else{
return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
}
return ['value'=>$valueArray,'size'=>$totalSize,'error'=>0,'msg'=>'ok'];
}
function parseTeacher($context,$size=0){
$valueArray=[];
$totalSize = 0;
$expRes = parseFixStr($context,2);
if($expRes['error']==0){
$filed = 'name';
if($filed){
$valueArray[$filed]=$expRes['value'];
}else{
$valueArray[]=$expRes['value'];
}
$context['pos']+=$expRes['size'];
$totalSize+= $expRes['size'];
}else{
return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
}
$expRes = parseInt($context,4);
if($expRes['error']==0){
$filed = 'num';
if($filed){
$valueArray[$filed]=$expRes['value'];
}else{
$valueArray[]=$expRes['value'];
}
$context['pos']+=$expRes['size'];
$totalSize+= $expRes['size'];
}else{
return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
}
$expRes = parseFixStr($context,3);
if($expRes['error']==0){
$filed = 'addr';
if($filed){
$valueArray[$filed]=$expRes['value'];
}else{
$valueArray[]=$expRes['value'];
}
$context['pos']+=$expRes['size'];
$totalSize+= $expRes['size'];
}else{
return ['value'=>False,'size'=>0,'error'=>$expRes['error'],'msg'=>$expRes['msg']];
}
return ['value'=>$valueArray,'size'=>$totalSize,'error'=>0,'msg'=>'ok'];
}
运行此测试脚本,得到结果如下:
Array
(
[value] => Array
(
[name] => AB
[num] => 1
[age] => 2
[addr] => ABC
)
[size] => 13
[error] => 0
[msg] => ok
)
Array
(
[value] => Array
(
[name] => AB
[num] => 1
[addr] => ABC
)
[size] => 9
[error] => 0
[msg] => ok
)
OK ! 第一步的任务已经完成,接下来考虑实现一个带有条件判断的结构体的解析工作。