如何将给定C头文件中的深度嵌套结构解包为字典?

9 投票
3 回答
4832 浏览
提问于 2025-04-18 17:55

我想找到一种方法来解包二进制数据。这些数据在一个C语言的头文件中被描述成一整棵结构体树(最多四层,整个结构体的大小几乎是64k)。

关于这个问题:在Python中解包嵌套的C结构体,唯一的回答是把整个数据描述“扁平化”成一个字符串,但我不太想这样做,因为这样会很难维护。

我希望能有一种方法,能够读取头文件,然后神奇地创建一个函数,这个函数可以接受二进制数据,并返回一个(正确嵌套的)字典,里面包含所有数据。

有没有什么Python模块可以做到这一点,或者实现其中的一部分?

或者在Python中处理这种数据还有其他方法吗?

3 个回答

7

你可以使用 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"

不过,这种方法有几个需要注意的地方:

  1. cffi 模块需要有编译器的支持,而且你需要把适用于所有平台的二进制文件和你的代码一起分发。
  2. 机器的“字节序”会影响内存中数字的表示方式,以及它们是如何被解析的。
  3. 你无法获取结构字段的名称。

尽管如此,这种方法可能还是适合你的使用场景。

8

我不知道怎么可靠地解析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
1

这是一个旧的讨论,但我遇到了同样的问题,没找到任何支持嵌套结构的Python库。

所以我自己写了一个叫做 pycstruct 的库。

这个库支持结构体、联合体、位域和枚举。它还支持所有的字节顺序、对齐方式,以及任意层级的嵌套。

撰写回答