我在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_SECTION
和GB_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的倍数。
在此程序中,宏用于将大型结构的成员划分为“节”,每个节均以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,大小0a
:偏移量0,大小4b
:偏移量4,大小2foo_section_end
:偏移量6,大小为0bar_section_start
:偏移量8,大小为0c
:偏移量8,大小2d
:偏移10,大小1bar_section_end
:偏移量11,大小0注意aligned
属性已确保bar_section_start
以及c
放置在偏移8
处,而不是放置在偏移6
处。在结构的字节7和8中有填充,但是请注意,此填充<< before >> bar_section_start
,因为为了使对齐有意义,必须填充。 bar_section_start
指向填充的第一个字节after,而不是填充本身。现在,可以使用offsetof
查找这些成员的偏移量,并像GB_SECTION_SIZE
一样使用它来计算每个节的大小。例如,here您可以看到他们将各种成员集写入文件,从而使用[]这样的代码来保存虚拟机状态的一部分。fwrite(GB_GET_SECTION(bar), GB_SECTION_SIZE(bar), 1, fd)
这具有写入结构的字节8到10的效果,即
c
和d
成员。这比一个接一个地写出所需的成员要方便得多,特别是因为在实际代码中有两个以上的成员。
尚不清楚为什么需要对齐,但是如果写入转储文件的所有内容都是8字节的倍数,则可能更方便。到处复制对齐的缓冲区也可能更有效率。
他们本可以为char
成员使用start/end
或某些其他标准类型,但是结构会不必要地变大。例如,在那种情况下,a
不能放置在偏移量0处,因此将放置在偏移量4处,以便为它提供与int
一样的4字节对齐方式。 b
将位于偏移量8,而bar_section_start
将位于偏移量16。这意味着foo
节将使用16个字节而不是8个字节,这浪费了一定数量的内存和磁盘空间(尽管实际上这不太可能非常重要)