☆ 背景介绍
上周在看Zend VM OPcache,它的bin文件格式是版本强相关的,随PHP版本不同需要不同的解析方式。010 Editor提供的模板不适用于我当时看的版本。起初我在原模板基础上小修小改对付着用,后来发现需要修改的地方比较多,也不太适应原作者的解析思路,就打算自己写一个匹配版本的解析模板。
从未完整写过010 Editor模板,直接解析Zend VM OPcache的bin,会碰上很多与目标不直接相关的低级问题,比如基础语法、功能函数等等。决定先写一个Python脚本,对一组稍显复杂的自定义数据结构进行自定义序列化输出,然后写010 Editor模板解析前述序列化数据,以此掌握010 Editor模板基础语法、功能函数。
☆ SerializeTest.py
#! /usr/bin/env python
# -*- encoding: utf-8 -*-
import struct
import hexdump
#
#########################################################################
#
#
# little-endian
#
def i2b_8_l ( n ) :
return( struct.pack( '<B', ( n & 0xff ) ) )
#
# end of i2b_8_l
#
def i2b_32_l ( n ) :
return( struct.pack( '<I', ( n & 0xffffffff ) ) )
#
# end of i2b_32_l
#
def i2b_64_l ( n ) :
return( struct.pack( '<Q', ( n & 0xffffffffffffffff ) ) )
#
# end of i2b_64_l
#
def b2i_8_l ( buf ) :
return( int( struct.unpack( '<B', buf[0:1] )[0] ) )
#
# end of b2i_8_l
#
def b2i_32_l ( buf ) :
return( int( struct.unpack( '<I', buf[0:4] )[0] ) )
#
# end of b2i_32_l
#
def b2i_64_l ( buf ) :
return( int( struct.unpack( '<Q', buf[0:8] )[0] ) )
#
# end of b2i_64_l
#
#
# str转bytes
#
def s2b ( s, e="utf-8" ) :
return( s.encode( encoding=e ) )
#
# end of s2b
#
#
# bytes转str
#
def b2s ( b, e="utf-8" ) :
return( str( b, encoding=e ) )
#
# end of b2s
#
#
# 替换bytes中指定偏移、指定长度的内容
#
def PatchBytes ( buf, off, size, sth ) :
return( buf[0:off] + sth + buf[off+size:] )
#
# PatchBytes
#
def WriteBytes ( filename, buf, mode='wb' ) :
with open( filename, mode ) as f :
f.write( buf )
#
# end of WriteBytes
#
#
#########################################################################
#
#
# count + off_array + buf_array
#
def SerializeArray ( array ) :
count = len( array )
off = b''
buf = b''
j = 4 + 4 * count
for i in range( count ) :
off += i2b_32_l( j )
tmp = array[i].Serialize()
buf += tmp
j += len( tmp )
return( i2b_32_l( count ) + off + buf )
#
# end of SerializeArray
#
#
# count + off_array + buf_array
#
def DeserializeArray ( buf, cls ) :
if ( len( buf ) < 4 ) :
return( None )
count = b2i_32_l( buf[0:4] )
if ( 4 + 4 * count > len( buf ) ) :
return( None )
array = []
for i in range( count ) :
j = b2i_32_l( buf[4+i*4:4+i*4+4] )
tmp = cls.Deserialize( buf[j:] )
if ( tmp is None ) :
return( None )
array.append( tmp )
return( array )
#
# end of DeserializeArray
#
#
#########################################################################
#
#
# struct _zend_string {
# uint32_t id;
# size_t len;
# char val[1];
# }
#
class MyString () :
def __init__ ( self, id, sth ) :
self.id = id
self.sth = sth
#
# end of __init__
#
def __repr__ ( self ) :
return( "MyString [%u:%s]" % ( self.id, self.sth ) )
#
# end of __repr__
#
def show ( self ) :
print( "MyString [%u:%s]" % ( self.id, self.sth ) )
#
# end of show
#
#
# 返回bytes
#
def Serialize ( self ) :
b = s2b( self.sth )
return( i2b_32_l( self.id ) + i2b_32_l( len( b ) ) + b )
#
# end of Serialize
#
@staticmethod
def Deserialize ( buf ) :
if ( len( buf ) < 8 ) :
return( None )
id = b2i_32_l( buf[0:4] )
i = b2i_32_l( buf[4:4+4] )
if ( 8 + i > len( buf ) ) :
return( None )
sth = b2s( buf[8:8+i] )
return( MyString( id, sth ) )
#
# end of Deserialize
#
# #
# # count + off_array + buf_array
# #
# @staticmethod
# def SerializeArray ( array ) :
# count = len( array )
# off = b''
# buf = b''
# j = 4 + 4 * count
# for i in range( count ) :
# off += i2b_32_l( j )
# tmp = array[i].Serialize()
# buf += tmp
# j += len( tmp )
# return( i2b_32_l( count ) + off + buf )
# #
# # end of SerializeArray
# #
# #
# # count + off_array + buf_array
# #
# @staticmethod
# def DeserializeArray ( buf ) :
# if ( len( buf ) < 4 ) :
# return( None )
# count = b2i_32_l( buf[0:4] )
# k = 4 + 4 * count
# if ( k > len( buf ) ) :
# return( None )
# array = []
# for i in range( count ) :
# j = b2i_32_l( buf[4+i*4:4+i*4+4] )
# if ( k != j ) :
# return( None )
# tmp = MyString.Deserialize( buf[j:] )
# if ( tmp is None ) ;
# return( None )
# array.append( tmp )
# k += len( tmp )
# return( array )
# #
# # end of DeserializeArray
# #
#
# end of MyString
#
#
#########################################################################
#
#
# struct _zval_struct {
# zend_value value;
# union {
# uint32_t type_info;
# } u1;
# union {
# uint32_t lineno;
# } u2;
# }
#
class MyZval () :
def __init__ ( self, value, type_info, lineno ) :
#
# 64位整数
#
self.value = value
#
# 32位整数
#
self.type_info = type_info
self.lineno = lineno
#
# end of __init__
#
def __repr__ ( self ) :
return( "MyZval [%#x:%u:%u]" % ( self.value, self.type_info, self.lineno ) )
#
# end of __repr__
#
def show ( self ) :
print( "MyZval [%#x:%u:%u]" % ( self.value, self.type_info, self.lineno ) )
#
# end of show
#
#
# 返回bytes
#
def Serialize ( self ) :
return( i2b_64_l( self.value ) + i2b_32_l( self.type_info ) + i2b_32_l( self.lineno ) )
#
# end of Serialize
#
@staticmethod
def Deserialize ( buf ) :
if ( len( buf ) < 16 ) :
return( None )
value = b2i_64_l( buf[0:8] )
type_info = b2i_32_l( buf[8:8+4] )
lineno = b2i_32_l( buf[12:12+4] )
return( MyZval( value, type_info, lineno ) )
#
# end of Deserialize
#
#
# end of MyZval
#
#
#########################################################################
#
#
# struct _zend_op {
# znode_op op1;
# znode_op op2;
# znode_op result;
# uint32_t lineno;
# zend_uchar opcode;
# zend_uchar op1_type;
# zend_uchar op2_type;
# zend_uchar result_type;
# }
#
class MyZendOp () :
def __init__ ( self, op1, op2, result, lineno, opcode, op1_type, op2_type, result_type ) :
#
# 32位整数
#
self.op1 = op1
self.op2 = op2
self.result = result
self.lineno = lineno
#
# unsigned char
#
self.opcode = opcode
self.op1_type = op1_type
self.op2_type = op2_type
self.result_type = result_type
#
# end of __init__
#
def __repr__ ( self ) :
return( "MyZendOp [%#x:%#x:%#x (%u)]" % ( self.opcode, self.op1, self.op2, self.lineno ) )
#
# end of __repr__
#
def show ( self ) :
print( "MyZendOp [%#x:%#x:%#x (%u)]" % ( self.opcode, self.op1, self.op2, self.lineno ) )
#
# end of show
#
#
# 返回bytes
#
def Serialize ( self ) :
return \
(
i2b_32_l( self.op1 ) +
i2b_32_l( self.op2 ) +
i2b_32_l( self.result ) +
i2b_32_l( self.lineno ) +
i2b_8_l( self.opcode ) +
i2b_8_l( self.op1_type ) +
i2b_8_l( self.op2_type ) +
i2b_8_l( self.result_type )
)
#
# end of Serialize
#
@staticmethod
def Deserialize ( buf ) :
if ( len( buf ) < 20 ) :
return( None )
op1 = b2i_32_l( buf[0:4] )
op2 = b2i_32_l( buf[4:4+4] )
result = b2i_32_l( buf[8:8+4] )
lineno = b2i_32_l( buf[12:12+4] )
# opcode = b2i_8_l( buf[16:16+1] )
# op1_type = b2i_8_l( buf[17:17+1] )
# op2_type = b2i_8_l( buf[18:18+1] )
# result_type = b2i_8_l( buf[19:19+1] )
opcode = buf[16]
op1_type = buf[17]
op2_type = buf[18]
result_type = buf[19]
return( MyZendOp( op1, op2, result, lineno, opcode, op1_type, op2_type, result_type ) )
#
# end of Deserialize
#
#
# end of MyZendOp
#
#
#########################################################################
#
#
# struct _zend_op_array {
# zend_uchar type;
# uint32_t last;
# zend_op *opcodes;
# int last_var;
# zend_string **vars;
# zend_string *filename;
# int last_literal;
# zval *literals;
# }
#
class MyZendOpArray () :
def __init__ ( self, type, opcodes, vars, filename, literals ) :
self.type = type
#
# MyZendOp[]
#
self.opcodes = opcodes
#
# MyString[]
#
self.vars = vars
self.filename = filename
#
# MyZval[]
#
self.literals = literals
#
# end of __init__
#
def __repr__ ( self ) :
return( "MyZendOpArray [%u:%s]" % ( self.type, self.filename ) )
#
# end of __repr__
#
def show ( self ) :
print( "MyZendOpArray [%u:%s]" % ( self.type, self.filename ) )
for i in range( len( self.opcodes ) ) :
print( self.opcodes[i] )
for i in range( len( self.vars ) ) :
print( self.vars[i] )
for i in range( len( self.literals ) ) :
print( self.literals[i] )
#
# end of show
#
#
# 返回bytes
#
# 只是一种演示方案,不是真实案例,有些字段安排甚至说不上合理,源自Zend
# VM OPcache,
#
def Serialize ( self ) :
buf = i2b_8_l( self.type )
last = len( self.opcodes )
buf += i2b_32_l( last )
opcodes_index = len( buf )
#
# 占位
#
buf += i2b_32_l( 0 )
opcodes_buf = SerializeArray( self.opcodes )[4:]
last_var = len( self.vars )
buf += i2b_32_l( last_var )
vars_index = len( buf )
#
# 占位
#
buf += i2b_32_l( 0 )
vars_buf = SerializeArray( self.vars )[4:]
filename_index = len( buf )
#
# 占位
#
buf += i2b_32_l( 0 )
filename_buf = self.filename.Serialize()
last_literal = len( self.literals )
buf += i2b_32_l( last_literal )
literals_index = len( buf )
#
# 占位
#
buf += i2b_32_l( 0 )
literals_buf = SerializeArray( self.literals )[4:]
buf = PatchBytes( buf, opcodes_index, 4, i2b_32_l( len( buf ) ) )
buf += opcodes_buf
buf = PatchBytes( buf, vars_index, 4, i2b_32_l( len( buf ) ) )
buf += vars_buf
buf = PatchBytes( buf, filename_index, 4, i2b_32_l( len( buf ) ) )
buf += filename_buf
buf = PatchBytes( buf, literals_index, 4, i2b_32_l( len( buf ) ) )
buf += literals_buf
return( buf )
#
# end of Serialize
#
@staticmethod
def Deserialize ( buf ) :
if ( len( buf ) < 29 ) :
return( None )
type = b2i_8_l( buf[0:1] )
last = b2i_32_l( buf[1:1+4] )
opcodes_off = b2i_32_l( buf[5:5+4] )
opcodes = DeserializeArray( i2b_32_l( last ) + buf[opcodes_off:], MyZendOp )
if ( opcodes is None ) :
return( None )
last_var = b2i_32_l( buf[9:9+4] )
vars_off = b2i_32_l( buf[13:13+4] )
vars = DeserializeArray( i2b_32_l( last_var ) + buf[vars_off:], MyString )
if ( vars is None ) :
return( None )
filename_off = b2i_32_l( buf[17:17+4] )
filename = MyString.Deserialize( buf[filename_off:] )
if ( filename is None ) :
return( None )
last_literal = b2i_32_l( buf[21:21+4] )
literals_off = b2i_32_l( buf[25:25+4] )
literals = DeserializeArray( i2b_32_l( last_literal ) + buf[literals_off:], MyZval )
if ( literals is None ) :
return( None )
return( MyZendOpArray( type, opcodes, vars, filename, literals ) )
#
# end of Deserialize
#
#
# end of MyZendOpArray
#
#
#########################################################################
#
# mystr = MyString( 0, "Test" )
# mystr.show()
# buf = mystr.Serialize()
# hexdump.hexdump( buf )
# mystr = MyString.Deserialize( buf )
# mystr.show()
#
# myzval = MyZval( 0xffffffff00112233, 6, 12 )
# myzval.show()
# buf = myzval.Serialize()
# hexdump.hexdump( buf )
# myzval = MyZval.Deserialize( buf )
# myzval.show()
#
# myzendop = MyZendOp \
# (
# 1, 2, 3, 4, 5, 6, 7, 8
# )
# myzendop.show()
# buf = myzendop.Serialize()
# hexdump.hexdump( buf )
# myzendop = MyZendOp.Deserialize( buf )
# myzendop.show()
type = 0x41
opcodes = \
[
MyZendOp( 1, 2, 3, 4, 5, 6, 7, 8 ),
MyZendOp( 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38 )
]
vars = \
[
MyString( 0, "Test_0" ),
MyString( 1, "Test_1" )
]
filename = MyString( 2, "Test_2.php" )
literals = \
[
MyZval( 0xffffffff00112233, 6, 12 ),
MyZval( 0xfefefefe44556677, 7, 13 )
]
myzendoparray \
= MyZendOpArray( type, opcodes, vars, filename, literals )
myzendoparray.show()
buf = myzendoparray.Serialize()
hexdump.hexdump( buf )
WriteBytes( 'SerializeTest.bin', buf )
myzendoparray \
= MyZendOpArray.Deserialize( buf )
# print( myzendoparray )
myzendoparray.show()
$ python3 SerializeTest.py
MyZendOpArray [65:MyString [2:Test_2.php]]
MyZendOp [0x5:0x1:0x2 (4)]
MyZendOp [0x35:0x31:0x32 (52)]
MyString [0:Test_0]
MyString [1:Test_1]
MyZval [0xffffffff00112233:6:12]
MyZval [0xfefefefe44556677:7:13]
00000000: 41 02 00 00 00 1D 00 00 00 02 00 00 00 4D 00 00 A............M..
00000010: 00 71 00 00 00 02 00 00 00 83 00 00 00 0C 00 00 .q..............
00000020: 00 20 00 00 00 01 00 00 00 02 00 00 00 03 00 00 . ..............
00000030: 00 04 00 00 00 05 06 07 08 31 00 00 00 32 00 00 .........1...2..
00000040: 00 33 00 00 00 34 00 00 00 35 36 37 38 0C 00 00 .3...4...5678...
00000050: 00 1A 00 00 00 00 00 00 00 06 00 00 00 54 65 73 .............Tes
00000060: 74 5F 30 01 00 00 00 06 00 00 00 54 65 73 74 5F t_0........Test_
00000070: 31 02 00 00 00 0A 00 00 00 54 65 73 74 5F 32 2E 1........Test_2.
00000080: 70 68 70 0C 00 00 00 1C 00 00 00 33 22 11 00 FF php........3"...
00000090: FF FF FF 06 00 00 00 0C 00 00 00 77 66 55 44 FE ...........wfUD.
000000A0: FE FE FE 07 00 00 00 0D 00 00 00 ...........
MyZendOpArray [65:MyString [2:Test_2.php]]
MyZendOp [0x5:0x1:0x2 (4)]
MyZendOp [0x35:0x31:0x32 (52)]
MyString [0:Test_0]
MyString [1:Test_1]
MyZval [0xffffffff00112233:6:12]
MyZval [0xfefefefe44556677:7:13]
1) 序列化输出结构
直接看SerializeTest.py的实现就能明白序列化方案,下面简介一二。
SerializeTest.py在序列化输出这个结构:
struct _zend_op_array {
zend_uchar type;
uint32_t last;
zend_op *opcodes; // opcodes[last]
int last_var;
zend_string **vars; // vars[last_var]
zend_string *filename;
int last_literal;
zval *literals; // literals[last_literal]
}
这是个简化版结构,但足够复杂。其中vars[]本来是指针数组,但对序列化而言,当成zend_string[]序列化出去没啥不同,毕竟在序列化过程中指针都得换个方式处理。
下面这个结构将所有指针换成some_off的形式:
struct _zend_op_array {
zend_uchar type;
uint32_t last;
uint32_t opcodes_off; // opcodes[last]
int last_var;
uint32_t vars_off; // vars[last_var]
uint32_t filename_off; // filename
int last_literal;
uint32_t literals_off; // literals[last_literal]
}
将来序列化输出的格式大致如下:
head + opcodes[] + vars[] + filename + literals[]
head就是上面那个结构,其中的some_off分别对应opcodes[]、vars[]、filename、literals[]的偏移,基址是head的首字节。
序列化输出中的some[]不是简单的结构数组,而是结构数组的序列化输出。本例中some[]的格式大致如下:
off[] + buf[]
off[]是一个偏移数组,靠off[i]定位buf[i]。buf[i]是单个some结构的序列化输出。在SerializeTest.py的实现中,off[i]的基址并不是head的首字节。
参看SerializeTest.py中SerializeArray()、DeserializeArray()的实现。SerializeArray()本来输出的是:
count + off[] + buf[]
off[i]的基址是count的首字节。但some[]不包含count,count被放到head中去了。为啥会这样?本文参考了Zend VM OPcache的实现,在简化结构的同时想展示一些复杂之处,保留了某些原设定。
无论如何,这只是一种自定义序列化方案,仅作演示,没啥道理可言。
☆ SerializeTest.bt
$ xxd -g 1 SerializeTest.bin
00000000: 41 02 00 00 00 1d 00 00 00 02 00 00 00 4d 00 00 A............M..
00000010: 00 71 00 00 00 02 00 00 00 83 00 00 00 0c 00 00 .q..............
00000020: 00 20 00 00 00 01 00 00 00 02 00 00 00 03 00 00 . ..............
00000030: 00 04 00 00 00 05 06 07 08 31 00 00 00 32 00 00 .........1...2..
00000040: 00 33 00 00 00 34 00 00 00 35 36 37 38 0c 00 00 .3...4...5678...
00000050: 00 1a 00 00 00 00 00 00 00 06 00 00 00 54 65 73 .............Tes
00000060: 74 5f 30 01 00 00 00 06 00 00 00 54 65 73 74 5f t_0........Test_
00000070: 31 02 00 00 00 0a 00 00 00 54 65 73 74 5f 32 2e 1........Test_2.
00000080: 70 68 70 0c 00 00 00 1c 00 00 00 33 22 11 00 ff php........3"...
00000090: ff ff ff 06 00 00 00 0c 00 00 00 77 66 55 44 fe ...........wfUD.
000000a0: fe fe fe 07 00 00 00 0d 00 00 00 ...........
SerializeTest.bt是用于解析SerializeTest.bin的010 Editor模板。
//
// struct _zend_string {
// uint32_t id;
// size_t len;
// char val[1];
// }
//
typedef struct _MyString
{
uint id;
uint len;
//
// 自动用前面的len成员定义此处的char[]
//
char val[len];
//
// Read functions can also be used to show information beside a struct
// without having to open the struct in the Template Results. When
// using read functions with a struct, the read function receives a
// reference to the struct and the '&' symbol should be used when
// declaring the parameter.
//
// read回调会影响GUI中Value列的显示,只是增强显示效果,可以不提供
//
} MyString <read=MyStringRepr>;
string MyStringRepr ( MyString &obj )
{
if ( 0 == obj.len )
{
return( "(null)" );
}
return( obj.val );
}
//
//////////////////////////////////////////////////////////////////////////
//
//
// struct _zend_op {
// znode_op op1;
// znode_op op2;
// znode_op result;
// uint32_t lineno;
// zend_uchar opcode;
// zend_uchar op1_type;
// zend_uchar op2_type;
// zend_uchar result_type;
// }
//
typedef struct _MyZendOp
{
uint op1;
uint op2;
uint result;
uint lineno;
uchar opcode;
uchar op1_type;
uchar op2_type;
uchar result_type;
} MyZendOp <read=MyZendOpRepr>;
string MyZendOpRepr ( MyZendOp &obj )
{
string s;
SPrintf( s, "%#x ( %#x, %#x ) : %u", obj.opcode, obj.op1, obj.op2, obj.lineno );
return( s );
}
//
//////////////////////////////////////////////////////////////////////////
//
//
// struct _zval_struct {
// zend_value value;
// union {
// uint32_t type_info;
// } u1;
// union {
// uint32_t lineno;
// } u2;
// }
//
typedef struct _MyZval
{
uint64 value <format=hex>;
uint type_info;
uint lineno;
} MyZval <read=MyZvalRepr>;
string MyZvalRepr ( MyZval &obj )
{
string s;
//
// 010 Editor显示64位整数时有自己的格式符,不完全同C语言
//
SPrintf( s, "0x%Lx : %u : %u", obj.value, obj.type_info, obj.lineno );
return( s );
}
//
//////////////////////////////////////////////////////////////////////////
//
//
// struct _zend_op_array {
// zend_uchar type;
// uint32_t last;
// zend_op *opcodes;
// int last_var;
// zend_string **vars;
// zend_string *filename;
// int last_literal;
// zval *literals;
// }
//
typedef struct _MyZendOpArray
{
local int saved_pos;
//
// local int i, j, k;
//
uchar type;
uint last;
//
// An alternate way of specifying the format for a variable is to use
// the syntax '<format=hex|decimal|octal|binary>' after a variable
// declaration or a typedef.
//
uint opcodes_off <format=hex,hidden=true>;
//
// Returns the current read position of the file. This read position
// is used when defining variables in a Template. Every time a
// variable is defined in a template, the read position moves ahead
// the number of bytes used by the variable.
//
saved_pos = FTell();
//
// Sets the current read position to the address pos.
//
// FSeek( opcodes_off );
// j = opcodes_off - 4;
// for ( i = 0; i < last; i++ )
// {
// //
// // Returns data read from the file at address pos. If no pos is
// // given, pos defaults to the current read position as reported by
// // FTell. These functions can be used in a Template to read data
// // from a file without declaring a variable and note that these
// // functions do not affect the current read position.
// //
// k = ReadUInt( opcodes_off + i * 4 );
// FSeek( j + k );
// MyZendOp myzendop;
// }
//
FSeek( opcodes_off + last * 4 );
MyZendOp opcodes[last];
FSeek( saved_pos );
uint last_var;
//
// The syntax '<hidden=true>' can be used to hide the display of
// variables in the Template Results.
//
uint vars_off <format=hex,hidden=true>;
saved_pos = FTell();
//
// FSeek( vars_off );
// j = vars_off - 4;
// for ( i = 0; i < last_var; i++ )
// {
// k = ReadUInt( vars_off + i * 4 );
// FSeek( j + k );
// MyString mystring;
// }
//
FSeek( vars_off + last_var * 4 );
MyString vars[last_var];
FSeek( saved_pos );
uint filename_off <format=hex,hidden=true>;
saved_pos = FTell();
FSeek( filename_off );
MyString filename ;
FSeek( saved_pos );
uint last_literal;
uint literals_off <format=hex,hidden=true>;
saved_pos = FTell();
//
// FSeek( literals_off );
// j = literals_off - 4;
// for ( i = 0; i < last_literal; i++ )
// {
// k = ReadUInt( literals_off + i * 4 );
// FSeek( j + k );
// MyZval myzval;
// }
//
FSeek( literals_off + last_literal * 4 );
MyZval literals[last_literal];
FSeek( saved_pos );
} MyZendOpArray;
//
//////////////////////////////////////////////////////////////////////////
//
//
// Indicates that all subsequent reads and writes from the file should use
// little-endian byte order.
//
LittleEndian();
MyZendOpArray myzendoparray;
在010 Editor中打开SerializeTest.bin,Ctrl-F5打开SerializeTest.bt,F5运行之。
上述模板文件是自解释的,实际试试就知道怎么写、怎么读。从最后一行开始读,依次套用结构、解析成员、显示数据、调用回调等等。
SerializeTest.bin中包含有some_off,但SerializeTest.bt用将之隐藏了,转而在附近显示反序列化之后的some[],尽可能接近人类可读状态。
MyString是个复杂结构,不是单纯的char[],模板用回调函数获取其中的char[],使得GUI中Value列的显示更直观。
010 Editor自带的帮助还可以,我都没怎么看在线帮助,直接看二进制自带的帮助。
☆ 参考资源
[1] 010 Editor Online Manual
https://www.sweetscape.com/010editor/manual/