scz 发表于 2021-3-26 13:40

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

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

```
目录:

    ☆ 前情提要
    ☆ 进阶问题
      1) 将一批整数映射到字符串
      2) 显示格式化过的时间字符串
      3) <size=n>
      4) 结构优化对齐
      5) 变长结构中的变长成员变量
      6) 元素为变长结构的结构数组
      7) 变量的作用域
      8) 字符串拼接
      9) parentof()/startof()
    ☆ 参考资源
```

☆ 前情提要

之前分享过

```
《MISC系列(51)--010 Editor模板编写入门》
http://scz.617.cn:8/misc/202103211820.txt
https://www.52pojie.cn/thread-1398493-1-1.html
```

后来在实战某特定版本PHP Zend VM OPcache文件解析时遇到一批典型问题,算是进阶问题,此处汇总一下。前文已更新,包含本文内容。

阅读本文需要有010 Editor模板编写基础,若无请勿给自己找堵。

☆ 进阶问题

1) 将一批整数映射到字符串

假设有

```cpp
#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文件中如下代码将这些数字映射成字符串

```cpp
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"。

这是比较优雅的办法,第二种不优雅的方案是:

```cpp
local struct
{
    stringstr;
} OpcodeMap;

OpcodeMap.str="NOP";
OpcodeMap.str="ADD";

string GetOpcodeStr ( uchar opcode )
{
    return( OpcodeMap.str );
}
```

GetOpcodeStr(0)返回"NOP"。如果数组元素特别多,第二种方案很难看,写起来倒没什么,用awk处理一下C语言的#define即可。

第三种更蠢的方案是switch,不多说。

bt模板不支持多维数组,字符串数组本质上是多维数组,在帮助中Limitations有讲。

尝试过下面这几种失败的写法,报语法错。

```cpp
local struct
{
    stringstr;
} OpcodeMap=
{
    "NOP",
    "ADD"
}

local struct
{
    stringstr;
} OpcodeMap=
{
    {"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列生效,有一种方案

```cpp
typedef struct _time_t_struct
{
    uint64t <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列的显示有某种执念,上述技巧是种通用思路,不局限于时间变量,适用于任意数据类型,要点就是用自定义结构再封装,动用&lt;read=callback&gt;。

3) &lt;size=n&gt;

以64位为例,对比如下结构定义,第二种定义尾部使用了&lt;size=0x50&gt;属性

```cpp
typedef struct _zend_file_cache_metainfo
{
    char      magic;
    char      system_id;
    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;
    char      system_id;
    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) 结构优化对齐

前一小节&lt;size=n&gt;是结构优化对齐问题中的特例。

bt模板中结构定义未启用结构优化对齐,不知有无官方启用方案?

若binary中相应结构是优化对齐过的,从C代码向bt模板移植结构定义,应该显式定义各处的填充变量,比如

```cpp
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模板移植结构定义之前应仔细检查各成员偏移,判断是否存在隐式变量对齐。参,推荐用FatalError提供的py脚本进行此检查。

5) 变长结构中的变长成员变量

```cpp
typedef struct _zend_string
{
    zend_refcounted_h   gc;
    uint64            h <format=hex>;
    size_t            len;
    if ( 0 != len )
    {
    char                val;
    }
} zend_string <read=zend_string_callback,optimize=false>;
```

上述结构中val[]的长度由len成员控制。若len为0,不用if而直接"char val"会引发一个告警,但不消除该告警也没事。问题不在于这种告警,而是后续引用val之前务必判断len是否为0。对于bt模板来说,如下结构定义并不确保val成员变量存在!


```cpp
typedef struct _zend_string
{
    zend_refcounted_h   gc;
    uint64            h <format=hex>;
    size_t            len;
    char                val;
} zend_string <read=zend_string_callback,optimize=false>;
```

当len为0时,没有val变量,若强行引用,不是告警而是报错。这点与C语言的直觉不同。

所有变长结构都不能求sizeof(),不管实际上变不变长,会报错。

6) 元素为变长结构的结构数组

```cpp
typedef struct _MyString
{
    uint    id;
    uint    len;
    char    val;
} MyString;

MyString    vars;
```

MyString是变长结构,vars[]是元素为变长结构的结构数组,模板中上述写法有问题!

假设Python代码这样构造vars[]:

```
vars    =   \
[
MyString( 0, "Test_0" ),
MyString( 1, "Test_1_1" ),
MyString( 2, "Test_2_2_2" ),
]
```

vars、vars、vars均不等长,生成的序列化数据用前述模板解析时,只有vars被正确解析,vars、vars均解析错误,与此同时Output窗口有警告:

```
Optimizing array of structures may cause incorrect results. Use <optimize=true|false> to override.
```

模板结构有默认属性&lt;optimize=true&gt;,此时010 Editor假设结构数组中所有元素大小同第一个元素,若结构元素定长,这是自然而然的事儿;但对于变长结构形成的结构数组,这种假设显然有问题,上例中vars被截断成vars的大小,vars就更错位了。

解决此类问题有两种办法,第一种办法:

```cpp
typedef struct _MyString
{
    uint    id;
    uint    len;
    if ( 0 != len )
    {
    char    val;
    }
} MyString <optimize=false>;
```

对于变长结构,建议始终在定义结构时使用&lt;optimize=false&gt;。第二种办法:

```
MyString    vars <optimize=false>;
```

在具体声明变长结构数组时指定&lt;optimize=false&gt;。

第一种办法更理想。无论使用哪种办法,都有一个副作用,&lt;optimize=false&gt;的变长结构数组退化成"Duplicate Array",而不是普通Array。

Duplicate Array与Array在GUI中的显示方式不同,后者的所有元素可以收缩成一行,前者直接显示所有元素,无法收缩成一行。如果元素个数很多,无法收缩将非常不美好。

7) 变量的作用域

bt模板同C代码一样有作用域的概念,大致就是外层定义的模板变量、local变量可为内层所用,内层可以定义同名local变量进行覆盖。如果需要全局local变量,就在最外层定义,不需要特别技巧。

8) 字符串拼接

可以用+号进行字符串拼接,也可以用SPrintf()函数。

9) parentof()/startof()

```cpp
typedef struct _zend_op_array
{
    ...
    zend_op         opcodes;
    ...
} 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 )
{
    uint64opcodes_base, current_off;
    uint64i;
    uchar   opcode;

    opcodes_base    = startof( parentof( op1 ) );
    current_off   = startof( op1 );
    i               = ( current_off - opcodes_base ) / sizeof( zend_op );
    opcode          = parentof( parentof( op1 ) ).opcodes.opcode;
    ...
}
```

注意看回调函数op1_comment()的实现,有几点需要特别强调。

parentof(op1)不是opcodes,而是opcodes[],startof(parentof(op1))取到的是opcodes[]的起始地址,而不是opcodes的地址,这点与直觉相悖。换个更明显的角度看这个坑,parentof(opcodes.op1)不对应opcodes。若代码中出现parentof(op1).op1_type,将固定返回opcodes.op1_type,而你本来想要的是opcodes.op1_type。op1_comment()中已正确处理该问题。

此外,不能直接写parentof(op1).opcode,报语法错,必须写成parentof(parentof(op1)).opcodes.opcode。

zend_op结构中并无指针指向zend_op_array结构,若是C编程,从地址空间布局反推即可,若是其他语言编程,想从zend_op找回zend_op_array就不大方便。而parentof()/startof()的存在为bt模板编写带来很大便利。

☆ 参考资源

```
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

010Editor脚本语法入门
    https://www.jianshu.com/p/ba60ebd8f916

How to get the relative address of a field in a structure dump -
    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
```

xixicoco 发表于 2021-3-27 17:45

楼主是大佬级的人物

tengyun 发表于 2021-4-15 15:04

谢谢四哥的文章!
页: [1]
查看完整版本: 010 Editor模板编写进阶问题汇总