在 C 中引用对齐的空结构?

Referencing an aligned empty struct in 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 将指向垃圾数据。

那么是哪一个呢?

标准 C 不允许空结构,但 gcc 提供 extension。它们就是它们看起来的样子,一个大小为 0 的对象,并且它们的作用完全符合您的预期,这实际上什么也没有。他们没有可以访问的成员。您可以将一个分配给另一个,但这是一个空操作。它们最适合用作占位符,如本例所示。

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

在这个程序中,宏被用来将一个大结构的成员分成"sections",每个都从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 位于偏移量 8,而不是偏移量 6否则可能会这样。在结构的字节 7 和 8 中有填充,但请注意,此填充出现在 之前 bar_section_start,因为它必须是为了使对齐有意义。 bar_section_start 指向第一个字节 填充之后,而不是填充本身。

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

的代码
fwrite(GB_GET_SECTION(bar), GB_SECTION_SIZE(bar), 1, fd)

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

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

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