首页 > 解决方案 > 在C中引用对齐的空结构?

问题描述

我在 SameBoy 模拟器 (v0.13) 中遇到了一组奇怪的宏,它们似乎使用空结构来寻址数据。它看起来像这样:

#define GB_PADDING(type, old_usage) type old_usage##__do_not_use

#define GB_SECTION(name, ...)     \
        __attribute__ ((aligned (8))) struct {} name##_section_start;  \
        __VA_ARGS__;   \
        struct {} name##_section_end
#define GB_SECTION_OFFSET(name)   \
        (offsetof(GB_gameboy_t, name##_section_start))
#define GB_SECTION_SIZE(name)     \ 
        (offsetof(GB_gameboy_t, name##_section_end) - offsetof(GB_gameboy_t, name##_section_start))
#define GB_GET_SECTION(gb, name)  \
        ((void*)&((gb)->name##_section_start))

它似乎GB_gameboy_t是某种类型(可能是 GameBoy 的内部结构)。然而,困扰我的部分是GB_SECTIONGB_GET_SECTION宏。很明显,这些宏的目的是对齐数据。但是,我对空结构(标记为name##_section_start)的扩展感到迷茫。它是否扩展为空(即 0 字节)?如果是这样,那么GB_GET_SECTION将指向任何__VA_ARGS__内容。但是,__attribute__ ((aligned (8)))预选赛的意义何在?还是空结构扩展为一些垃圾填充字节?如果是这样,那么GB_GET_SECTION将指向垃圾数据。

那么是哪一个呢?

标签: cgccstructpaddingmemory-alignment

解决方案


标准 C 不允许空结构,但它是gcc 提供的扩展。它们正是它们看起来的样子,一个大小为 0 的对象,它们完全按照你的期望做,这基本上什么都不是。他们没有可访问的成员。您可以将一个分配给另一个,但它是无操作的。在这种情况下,它们作为占位符最有用。

__attribute__((aligned (8)))与通常做的事情相同:保证具有此属性的对象在 8 字节边界上对齐。换句话说,它的地址将是 8 的倍数。

在这个程序中,宏用于将大型结构的成员划分为“段”,每个段以 8 字节的边界开始,并创建零字节的空结构成员来标记每个段的开始和结束. 代码看起来像:

struct GB_gameboy_s {
    GB_SECTION(foo, int a; short b;);
    GB_SECTION(bar, char c; char d;);
};

typedef struct GB_gameboy_s GB_gameboy_t;

扩展到

struct GB_gameboy_s {
    __attribute__ ((aligned (8))) struct {} foo_section_start;
    int a;
    short b;
    struct {} foo_section_end;
    __attribute__ ((aligned (8))) struct {} bar_section_start;
    short c;
    char d;
    struct {} bar_section_end;
};

因此结构的布局类似于:

  • foo_section_start: 偏移量 0,大小 0
  • a: 偏移量 0,大小 4
  • b:偏移量 4,大小 2
  • foo_section_end: 偏移量 6,大小 0
  • bar_section_start:偏移量 8,大小 0
  • c:偏移量 8,大小 2
  • d: 偏移 10,大小 1
  • bar_section_end:偏移量 11,大小 0

请注意,该aligned属性已确保bar_section_start,因此也c,被放置在 offset 8,而不是在 offset 6,因为它们可能是。结构的第 7 和 8 字节有填充,但请注意,此填充位于之前 bar_section_start,因为它必须这样才能使对齐有意义。 bar_section_start指向填充的第一个字节,而不是填充本身。

现在,可以使用offsetof来找到这些成员的偏移量,并使用它来计算每个部分的大小,就像GB_SECTION_SIZE这样做一样。例如,在这里您可以看到他们将各种成员集写入文件,以保存部分虚拟机状态,使用如下代码

fwrite(GB_GET_SECTION(bar), GB_SECTION_SIZE(bar), 1, fd)

这具有写入结构的字节 8 到 10 的效果,即candd成员。这比一个一个地写出所需的成员要方便一些,尤其是因为在实际代码中不止两个。

目前尚不清楚为什么需要对齐,但如果写入转储文件的所有内容都是 8 字节的倍数,可能会更方便。复制对齐的缓冲区也可能更有效。

他们可以char为成员使用或其他一些标准类型start/end,但结构会变得不必要地大。例如,在这种情况下,a不能将其放置在偏移量 0 处,因此将放置在偏移量 4 处,以便像往常一样为其提供 4 字节对齐intb将在偏移量 8 处,并且bar_section_start将在偏移量 16 处。这将意味着该foo部分使用 16 个字节而不是 8 个字节,浪费了一定数量的内存和磁盘空间(尽管它确实不太可能非常重要)


推荐阅读