如何将给定C头文件中的深度嵌套结构解包为字典?
我想找到一种方法来解包二进制数据。这些数据在一个C语言的头文件中被描述成一整棵结构体树(最多四层,整个结构体的大小几乎是64k)。
关于这个问题:在Python中解包嵌套的C结构体,唯一的回答是把整个数据描述“扁平化”成一个字符串,但我不太想这样做,因为这样会很难维护。
我希望能有一种方法,能够读取头文件,然后神奇地创建一个函数,这个函数可以接受二进制数据,并返回一个(正确嵌套的)字典,里面包含所有数据。
有没有什么Python模块可以做到这一点,或者实现其中的一部分?
或者在Python中处理这种数据还有其他方法吗?
3 个回答
你可以使用 cffi
这个 Python 模块来理解 C 语言中的 struct
定义,最终解析原始数据。下面是一个解析嵌套结构的例子:
from cffi import FFI
ffi = FFI()
## Define a set of (possibly nested) structures
ffi.cdef("""
typedef struct { int32_t inner; } Ax;
typedef struct { Ax a1; Ax a2; } Bx;
typedef struct { Bx b1; Bx b2; } Cx;
typedef struct { Cx c1; Cx c2; } Dx;
""")
def parse_Dx(raw_data):
D_struct = ffi.new("Dx*")
D_buffer = ffi.buffer(D_struct)
D_buffer[:] = raw_data # Assign raw bytes
return D_struct
## Make up some data (of the right length)
data = "\x01\x00\x00\x00" * (ffi.sizeof("Dx") / ffi.sizeof("int32_t"))
structure = parse_Dx(data)
print structure.c1.b1.a1.inner ## Print "1"
不过,这种方法有几个需要注意的地方:
cffi
模块需要有编译器的支持,而且你需要把适用于所有平台的二进制文件和你的代码一起分发。- 机器的“字节序”会影响内存中数字的表示方式,以及它们是如何被解析的。
- 你无法获取结构字段的名称。
尽管如此,这种方法可能还是适合你的使用场景。
我不知道怎么可靠地解析C语言的头文件,因为有时候你可能还需要依赖其他包含文件中的声明。例如,你可能会用到一个 struct stat
,它依赖于 sys/stat.h
,还有可能依赖其他标准的包含文件。不过我稍后会再提到这个问题……
所以我建议你手动完成这部分工作,并使用一种层次化的描述方式,这样可以从结构体模块返回的平面字段列表中重建出层次结构。
假设我们有一个用C语言描述的结构体:
struct S4 {
char c;
char d;
};
struct my_struct {
int a[4];
struct {
int b;
struct {
struct S4 z[2];
int e;
} y;
} x;
int f;
struct {
int g;
struct {
struct S4 v;
int j;
} u;
} t;
};
我首先手动写下我的初始结构描述:
'''\
S4: { c: b, d: b, c: 2x}
{ a: 4h, x: { b: h, y: { z: 2 S4, e: h}}, f: h, t: { g: h, u: { v: S4, j: h}}}\
'''
这会给出字段的名称和类型(以 struct
模块格式表示)
然后一个解析器(使用PLY)会解析它,并返回一个对象,这个对象可以将二进制结构体转换成一个命名元组,里面递归地包含整个结构体及其正确的名称,所有内部结构体也都是命名元组(struct
格式字符串作为一个副产品生成)
- 所有这些都能正确处理显式填充
- 可以使用命名结构体(示例中的
S4
),它们必须在使用前声明 - 数组(简单元素或命名结构体)通过在类型前加一个数字来实现(
a : 4 h
<=>short a[4];
或x : 2 S4
<=>struct S4 x[2];
在示例中)
不过这不是一个轻量级的单文件解决方案,它需要大量的注释和文档。另一种选择是直接使用 pycparser
和原始的C头文件,但这会更复杂,我不太喜欢导入标准C库头文件的想法。虽然我说不出具体原因,但我觉得这样做不够稳妥。
下面是一个使用示例:
>>> s = '''\
S4: { c: b, d: b, c: 2x}
{ a: 4h, x: { b: h, y: { z: 2 S4, e: h}}, f: h, t: { g: h, u: { v: S4, j: h}}}\
'''
>>> ns = Named_struct(s)
>>> import struct
>>> b = struct.pack(ns.fmt, * range(15))
>>> b
b'\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x06\x00\x00\x07\x08\x00\x00\t\x00\n\x00\x0b\x00\x0c\r\x00\x00\x0e\x00'
>>> t = ns.unpack(b)
>>> t
_X_(a=[0, 1, 2, 3], x=x(b=4, y=y(z=[x(c=5, d=6), x(c=7, d=8)], e=9)), f=10, t=t(g=11, u=u(v=v(c=12, d=13), j=14)))
>>> t.x.y.z[1]
x(c=7, d=8)
>>> t.x.y.z[1].d
8
如果你感兴趣,我可以把它放到github上或者……
回到那个难点。我做了一些研究,但目前我只能给你一些可能的方向,剩下的工作量会很大。你可以尝试使用 pycparser,但它只支持C(不支持C++),而且文档不是很详细。如果你需要C++,可以尝试Eli Bendersky(pycparser
的作者)网站上提到的CLang Python绑定,链接是 用Clang在Python中解析C++,但这里几乎没有文档。
现在关于代码,尽管我仍然认为它是一个正在进行中的项目,因为缺少注释和正式的测试——但它确实可以工作。
h_lexer.py
import ply.lex as lex
states = ( ('value', 'exclusive'),)
tokens = ( 'BEG', 'NAME', 'COLON', 'END', 'COMMA', 'TYPE', 'PAD', 'NUMBER')
linepos = 0
def t_COLON(t):
r':'
t.lexer.begin('value')
return t
def t_ANY_BEG(t):
r'{'
t.lexer.begin('INITIAL')
return t
def t_value_TYPE(t):
r'[cbB?hHiIlLqQnNfdp]|\d*[sP]'
t.lexer.begin('INITIAL')
return t
def t_value_PAD(t):
r'x'
t.lexer.begin('INITIAL')
return t
def t_value_NUMBER(t):
r'\d+'
return t
def t_ANY_NAME(t):
r'\w+'
t.lexer.begin('INITIAL')
return t
t_COMMA = ','
t_ANY_ignore= ' \t\r'
t_END=r'}'
def t_newline(t):
r'\n'
t.lexer.lineno += 1
global linepos
linepos = t.lexpos
# Error handling rule
def t_ANY_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)
def getpos(lexer):
global linepos
return lexer.lineno, lexer.lexpos - linepos
h_yacc.py:
import ply.yacc as yacc
from h_lexer import tokens
named_structs = {}
def p_prog(p):
'prog : named_strucs struc'
p[0] = p[2]
def p_named_structs_empty(p):
'named_strucs : '
def p_named_structs(p):
'named_strucs : named_strucs named_struc'
def p_named_struct(p):
'named_struc : NAME COLON struc'
global named_structs
named_structs[p[1]] = p[3]
def p_struc(p):
'struc : BEG affects END'
# print("STRUCT", p[2])
p[0] = p[2]
def p_typ(p):
'typ : TYPE'
p[0] = p[1]
def p_pad(p):
'pad : PAD'
p[0] = p[1]
def p_pad_n(p):
'pad : NUMBER PAD'
p[0] = p[1] + p[2]
def p_affect_pad(p):
'affect : NAME COLON pad'
p[0] = (None, p[3])
def p_val(p):
'''val : typ
| struc'''
p[0] = p[1]
def p_val_struct(p):
'val : NAME'
test_named_struct(p[1])
p[0] = named_structs[p[1]]
def p_nval_single(p):
'nval : val'
p[0] = p[1]
def p_nval_multi(p):
'nval : NUMBER val'
n = int(p[1])
if n == 0:
raise SyntaxError
if n == 1:
print('WARN: array of one single element')
p[0] = tuple([ p[2] for i in range(n) ])
def p_affect(p):
'affect : NAME COLON nval'
p[0] = (p[1], p[3])
def p_affects_init(p):
'affects : affect'
p[0] = [ p[1] ]
def p_affects_multi(p):
'affects : affects COMMA affect'
p[0] = p[1] + [ p[3] ]
def p_error(p):
print ("Syntax error", p)
def test_named_struct(name):
global named_structs
if name not in named_structs:
raise SyntaxError
h_named_struct.py
import h_yacc
import h_lexer
import collections
import struct
class Named_struct(object):
yacc = h_yacc.yacc.yacc(module = h_yacc)
lexer = h_lexer.lex.lex(module = h_lexer)
def __init__(self, desc_str, name='_X_', struc_typ = '='):
l = self.__class__.yacc.parse(desc_str, self.__class__.lexer)
if l is None:
col, line = h_lexer.getpos(self.__class__.lexer)
raise Exception(
"Syntax error in description string near line %d col %d" %
(line, col - 1))
fmts = []
self.t = self.__make_named(l, fmts, name)
self.fmt = struc_typ + ''.join(fmts)
self.struct = struct.Struct(self.fmt)
self.name = name
self.l = l
def __make_named(self, l, fmts, name):
names = []
values = []
for elt in l:
if elt[0] is None:
fmts.append(elt[1])
else:
val = None
names.append(elt[0])
if isinstance(elt[1], str):
fmts.append(elt[1])
elif isinstance(elt[1], tuple):
val = self.__make_array(elt[1], fmts, elt[0])
else:
val = self.__make_named(elt[1], fmts, elt[0])
values.append(val)
t = collections.namedtuple(name, names)
return t, t(*values)
def __make_array(self, l, fmts, name):
values = []
for elt in l:
if isinstance(elt, str):
fmts.append(elt)
values.append(None)
else:
val = self.__make_named(elt, fmts, "x")
values.append(val)
t = self.__class__.__lister
return t, t(*values)
staticmethod
def __lister(*l):
return list(l)
def __gen(self, t, it):
l = []
for g in t[1]:
if g is None:
l.append(next(it))
else:
l.append(self.__gen(g, it))
return t[0](*l)
def unpack(self, buffer):
l = struct.unpack(self.fmt, buffer)
t = self.__gen(self.t, iter(l))
return t
这是一个旧的讨论,但我遇到了同样的问题,没找到任何支持嵌套结构的Python库。
所以我自己写了一个叫做 pycstruct 的库。
这个库支持结构体、联合体、位域和枚举。它还支持所有的字节顺序、对齐方式,以及任意层级的嵌套。