010 Editor模板编写进阶问题汇总

☆ 前情提要

之前分享过

《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

Spread the word. Share this post!

Meet The Author

C/ASM程序员

Leave Comment