☆ 前情提要
之前分享过
《MISC系列(51)--010 Editor模板编写入门》
后来在实战某特定版本PHP Zend VM OPcache文件解析时遇到一批典型问题,算是进阶问题,此处汇总一下。
阅读本文需要有010 Editor模板编写基础,若无请勿给自己找堵。
☆ 进阶问题
1) 将一批整数映射到字符串
假设有
#define IS_CONST (1<<0)
#define IS_TMP_VAR (1<<1)
#define IS_VAR (1<<2)
#define IS_UNUSED (1<<3)
#define IS_CV (1<<4)
在binary中只有整数1、2、4、8、16,bt文件中如下代码将这些数字映射成字符串
local enum <uchar> OpcodeTypeMap
{
IS_CONST = 1,
IS_TMP_VAR = 2,
IS_VAR = 4,
IS_UNUSED = 8,
IS_CV = 16
};
string GetOpcodeTypeStr ( OpcodeTypeMap opcode_type )
{
return( EnumToString( opcode_type ) );
}
GetOpcodeTypeStr(1)返回”IS_CONST”,GetOpcodeTypeStr(16)返回”IS_CV”。
这是比较优雅的办法,第二种不优雅的方案是:
local struct
{
string str;
} OpcodeMap[2];
OpcodeMap[0].str="NOP";
OpcodeMap[1].str="ADD";
string GetOpcodeStr ( uchar opcode )
{
return( OpcodeMap[opcode].str );
}
GetOpcodeStr(0)返回”NOP”。如果数组元素特别多,第二种方案很难看,写起来倒没什么,用awk处理一下C语言的#define即可。
第三种更蠢的方案是switch,不多说。
bt模板不支持多维数组,字符串数组本质上是多维数组,在帮助中Limitations有讲。
尝试过下面这几种失败的写法,报语法错。
local struct
{
string str;
} OpcodeMap[2] =
{
"NOP",
"ADD"
}
local struct
{
string str;
} OpcodeMap[2] =
{
{"NOP"},
{"ADD"}
}
2) 显示格式化过的时间字符串
C代码的time_t随32/64位自动变化,bt中time_t是32位的,time64_t才是64位的,若从C代码移植结构定义,务必注意这点。
time64_t的格式串固定为”MM/dd/yyyy hh:mm:ss”,若想换其他格式串,并使之在GUI中Value列生效,有一种方案
typedef struct _time_t_struct
{
uint64 t <format=hex,comment=t2str>;
} time_t_struct <read=time_t_struct_callback>;
string time_t_struct_callback ( time_t_struct &obj )
{
return( t2str( obj.t ) );
}
string t2str ( uint64 t )
{
return( TimeTToString( t, "yyyy/MM/dd hh:mm:ss" ) );
}
然后在模板中不用time64_t,转用自定义结构time_t_struct。当然,此处说的是修改GUI中Value列的显示,若只想修改Comment列的显示,无需这种技巧。
若对GUI中Value列的显示有某种执念,上述技巧是种通用思路,不局限于时间变量,适用于任意数据类型,要点就是用自定义结构再封装,动用<read=callback>。
3) <size=n>
以64位为例,对比如下结构定义,第二种定义尾部使用了<size=0x50>属性
typedef struct _zend_file_cache_metainfo
{
char magic[8];
char system_id[32];
size_t mem_size <format=hex>;
size_t str_size <format=hex>;
size_t script_offset <format=hex>;
time64_t timestamp;
uint checksum <format=hex>;
} zend_file_cache_metainfo;
typedef struct _zend_file_cache_metainfo
{
char magic[8];
char system_id[32];
size_t mem_size <format=hex>;
size_t str_size <format=hex>;
size_t script_offset <format=hex>;
time64_t timestamp;
uint checksum <format=hex>;
} zend_file_cache_metainfo <size=0x50>;
第一种定义使得结构只有0x4c字节。若binary中该结构对齐在64位边界上,第一种结构定义就会惹麻烦;修正方案是在checksum成员后面显式定义”uint pad”,或者用FSkip()占位,又或者采用第二种结构定义,不显式占位。
4) 结构优化对齐
前一小节<size=n>是结构优化对齐问题中的特例。
bt模板中结构定义未启用结构优化对齐,不知有无官方启用方案?
若binary中相应结构是优化对齐过的,从C代码向bt模板移植结构定义,应该显式定义各处的填充变量,比如
typedef struct _zend_persistent_script
{
zend_script script <open=true>;
uint64 compiler_halt_offset <format=hex>;
uint ping_auto_globals_mask;
//
// 结构优化对齐带来的填充
//
uint pad_0;
time64_t timestamp;
uchar corrupted;
uchar is_phar;
//
// 结构优化对齐带来的填充
//
uint16 pad_1;
uint32 pad_2;
...
} zend_persistent_script;
在结构优化对齐场景FSkip()占位没有特别优势,但如果是快速定义未知结构的场景,FSkip()占位是个不错的选择。
结构优化对齐给bt模板编写带来不小麻烦,从C代码向bt模板移植结构定义之前应仔细检查各成员偏移,判断是否存在隐式变量对齐。参[3],推荐用FatalError提供的py脚本进行此检查。
5) 变长结构中的变长成员变量
typedef struct _zend_string
{
zend_refcounted_h gc;
uint64 h <format=hex>;
size_t len;
if ( 0 != len )
{
char val[len];
}
} zend_string <read=zend_string_callback,optimize=false>;
上述结构中val[]的长度由len成员控制。若len为0,不用if而直接”char val[len]”会引发一个告警,但不消除该告警也没事。问题不在于这种告警,而是后续引用val之前务必判断len是否为0。对于bt模板来说,如下结构定义并不确保val成员变量存在!
typedef struct _zend_string
{
zend_refcounted_h gc;
uint64 h <format=hex>;
size_t len;
char val[len];
} zend_string <read=zend_string_callback,optimize=false>;
当len为0时,没有val变量,若强行引用,不是告警而是报错。这点与C语言的直觉不同。
所有变长结构都不能求sizeof(),不管实际上变不变长,会报错。
6) 元素为变长结构的结构数组
typedef struct _MyString
{
uint id;
uint len;
char val[len];
} MyString;
MyString vars[last_var];
MyString是变长结构,vars[]是元素为变长结构的结构数组,模板中上述写法有问题!
假设Python代码这样构造vars[]:
vars = \
[
MyString( 0, "Test_0" ),
MyString( 1, "Test_1_1" ),
MyString( 2, "Test_2_2_2" ),
]
vars[0]、vars[1]、vars[2]均不等长,生成的序列化数据用前述模板解析时,只有vars[0]被正确解析,vars[1]、vars[2]均解析错误,与此同时Output窗口有警告:
Optimizing array of structures may cause incorrect results. Use <optimize=true|false> to override.
模板结构有默认属性<optimize=true>,此时010 Editor假设结构数组中所有元素大小同第一个元素,若结构元素定长,这是自然而然的事儿;但对于变长结构形成的结构数组,这种假设显然有问题,上例中vars[1]被截断成vars[0]的大小,vars[2]就更错位了。
解决此类问题有两种办法,第一种办法:
typedef struct _MyString
{
uint id;
uint len;
if ( 0 != len )
{
char val[len];
}
} MyString <optimize=false>;
对于变长结构,建议始终在定义结构时使用<optimize=false>。第二种办法:
MyString vars[last_var] <optimize=false>;
在具体声明变长结构数组时指定<optimize=false>。
第一种办法更理想。无论使用哪种办法,都有一个副作用,<optimize=false>的变长结构数组退化成”Duplicate Array”,而不是普通Array。
Duplicate Array与Array在GUI中的显示方式不同,后者的所有元素可以收缩成一行,前者直接显示所有元素,无法收缩成一行。如果元素个数很多,无法收缩将非常不美好。
7) 变量的作用域
bt模板同C代码一样有作用域的概念,大致就是外层定义的模板变量、local变量可为内层所用,内层可以定义同名local变量进行覆盖。如果需要全局local变量,就在最外层定义,不需要特别技巧。
8) 字符串拼接
可以用+号进行字符串拼接,也可以用SPrintf()函数。
9) parentof()/startof()
typedef struct _zend_op_array
{
...
zend_op opcodes[last];
...
} zend_op_array;
typedef struct _zend_op
{
...
znode_op op1 <comment=op1_comment>;
...
uchar opcode <comment=GetOpcodeStr>;
...
} zend_op <read=zend_op_callback,fgcolor=0xffffff,bgcolor=cLtGreen>;
string op1_comment ( znode_op &op1 )
{
uint64 opcodes_base, current_off;
uint64 i;
uchar opcode;
opcodes_base = startof( parentof( op1 ) );
current_off = startof( op1 );
i = ( current_off - opcodes_base ) / sizeof( zend_op );
opcode = parentof( parentof( op1 ) ).opcodes[i].opcode;
...
}
注意看回调函数op1_comment()的实现,有几点需要特别强调。
parentof(op1)不是opcodes[i],而是opcodes[],startof(parentof(op1))取到的是opcodes[]的起始地址,而不是opcodes[i]的地址,这点与直觉相悖。换个更明显的角度看这个坑,parentof(opcodes[i].op1)不对应opcodes[i]。若代码中出现parentof(op1).op1_type,将固定返回opcodes[0].op1_type,而你本来想要的是opcodes[i].op1_type。op1_comment()中已正确处理该问题。
此外,不能直接写parentof(op1)[i].opcode,报语法错,必须写成parentof(parentof(op1)).opcodes[i].opcode。
zend_op结构中并无指针指向zend_op_array结构,若是C编程,从地址空间布局反推即可,若是其他语言编程,想从zend_op找回zend_op_array就不大方便。而parentof()/startof()的存在为bt模板编写带来很大便利。
☆ 参考资源
[1] 010 Editor Online Manual
https://www.sweetscape.com/010editor/manual/
How does scope work when defining local variables
https://www.sweetscape.com/support/kb/kb1025.html
Limitations
https://www.sweetscape.com/010editor/manual/TemplateLimitations.htm
[2] 010Editor脚本语法入门
https://www.jianshu.com/p/ba60ebd8f916
[3] How to get the relative address of a field in a structure dump - [2012-03-20]
https://stackoverflow.com/questions/9788679/how-to-get-the-relative-address-of-a-field-in-a-structure-dump-c
《有调试符号的情况下在GDB中获取结构成员的偏移》
http://scz.617.cn:8/unix/201203201430.txt