使用C结构体成员的连续内存

4
在将此问题标记为重复之前,请仔细阅读问题。
可能这是一个非常愚蠢的问题,但它一直在困扰着我。从阅读和许多其他SO问题中,我知道C语言结构体中的字段由于编译器添加的填充而不能保证是连续的。例如,根据C标准:
13 /:在结构对象内,非位域成员和位域所在的单元按其声明顺序递增地具有地址。指向结构对象的指针,在适当转换后,指向其初始成员(或者如果该成员是位域,则指向其中所包含的单元),反之亦然。结构对象中可能存在未命名的填充,但不在其开头。
我正在编写类似于Unix中的readelf和nm的程序,只是出于兴趣,需要处理特定偏移量处的字节以读取某些值。例如,在对象文件的前62个字节中包含“文件头”。文件头的0x00-0x04字节对应一个整数,而0x20-0x28字节对应一个指针等。然而,我注意到在readelf.c的原始实现中,程序员会做类似于以下这样的事情:
首先,他们声明了一个结构体(称之为ELF_H),该结构体的字段对应于文件头中的内容(即第一个字段与文件头中的前4个字节一样,第二个字段是一个字符,因为elf头中的0x04-0x05字节编码了一个字符等)。然后,他们将整个elf文件复制到内存中,并将指向该内存起始位置的指针强制转换为ELF_H类型。类似于:
FILE *file = fopen('filename', rb);
void *start_of_file = malloc(/* size_of_file */);
fread(start_of_file, 1, /* size_of_file */,file);  // copies entire file into memory
ELF_H hdr = *(ELF_H) start_of_file;               // type case pointer to be of type struct and dereference

在这样做之后,只需使用结构体的成员变量访问标头的每个部分。因此,他们不是使用指针算术来获取应该在0x04字节处的内容,而是使用hdr.member2(在结构体中是第二个成员,其后是一个int类型的第一个成员)。
如果结构体中的字段不能保证连续,那么这是如何工作的?
我能找到的最接近答案的是这里,但在那个例子中,结构体的成员都是相同类型的。在ELF_H中,它们是不同类型的。
提前感谢您 :)

也许我没有理解你的逻辑。如果member1需要填充以使member2正确对齐,那么在分配结构时编译器为什么会知道这一点,但在访问member2时突然不知道呢? - Tibrogargan
@Tibrogargan 因为空指针最初指向文件中按顺序紧凑(无填充)的字节。然后将其类型转换为结构体,希望结构体的 member1 和 member2 之间没有额外的间隔。想象一下,如果 member1 是一个从结构体开头偏移为 0 的 int,而 member2 是一个从结构体偏移为 5(不是 4,因为有填充)的 char。它将尝试访问 member2,并从文件的 0x05-0x06 读取,而不是 0x04-0x05。 - gowrath
你确定 ELF 已经被打包了吗? - Tibrogargan
@Tibrogargan,标题肯定是需要的。它的每个相关条目都必须位于设置的内存偏移量之间。 - gowrath
当编译器将未打包的ELF头文件创建并压缩后写入磁盘时,其中一定有一些有趣的魔法正在发生。 - Tibrogargan
顺便提一下,你的程序可以使用<stddef.h>中的offsetof()在编译时获取结构成员的布局。这可能对你的测试很有用。 - Davislor
5个回答

3
如果文件中的数据是从一个填充的结构体中写入的,那么填充是无关紧要的;文件包含了填充,就像内存表示一样。
标准确实不是特别严格,编译器可以在ELF读取器结构体中插入随机填充,而写ELF工具没有匹配。但在实践中,“未命名填充”是用于对齐目的,所有主要编译器在这里都有可预测的行为;它们对齐字段以匹配其类型。因此,如果前一个字段没有以四字节边界结束,则int字段(在具有四个字节int的系统上)前面会有1-3个填充字节,char字段不会填充等等。在这种情况下,我所知道的没有编译器会在前导int字段和后续的char[2]之间插入填充,因为char根本没有必需的对齐方式。
还可以使用非标准编译器扩展来防止填充以对齐结构体中的字段,但如果您的结构体定义永远不会有未对齐的字段(因为您总是将较小的字段放在较大的字段之后,或者因为您总是将较小的字段组合在一起以保持后续较大字段的对齐要求),则不需要这样做。

有道理。对齐是将内存地址与类型大小的倍数匹配有关,对吧?所以如果你有一个 short 后面跟着一个 int,可能会有填充。我给出的例子很简单。实际上,格式是:short、short、int、long、long、int、……你说的还适用吗? - gowrath
@gowrath:在我所了解的每个系统上,这种结构不需要填充。如果“short”为2个字节,“int”为4个字节,“long”为4或8个字节,则每个字段已正确对齐;两个“short”使下一个“int”在4字节对齐,两个“short”后跟“int”则将以下的“long”对齐到8字节,依此类推。 - ShadowRanger
希望我的澄清问题没有打扰到您。假设第一个short是4的倍数,那么您所说的是正确的。但是,如果它位于2 mod 4的地址上,则下一个short将位于0 mod 4,下一个int将位于2 mod 4,这将导致不对齐。编译器在对齐时,是否先将整个结构体移位,如果没有可行的位置匹配所有对齐,则添加填充? - gowrath
@gowrath:对于全局或堆栈分配的结构体,编译器会确保结构体从适当的对齐位置开始。动态/堆内存分配函数通常返回与最大预期字段宽度相匹配的内存(八个字节很常见);有一些奇怪的情况需要更严格的对齐方式,例如出于性能考虑或为了有效地使用特定的CPU指令;在这些情况下,您需要具有显式对齐要求的非标准(或最近的标准)动态分配函数,例如aligned_alloc(C11),posix_memalign(POSIX 2001ish)等。 - ShadowRanger

2
如果结构体中的字段不保证连续,那么这个方法该如何工作呢?
标准并不要求结构体必须是连续的,但这并不意味着结构体是随机或不可预测地布局。具体的编译器和链接器将根据应用程序二进制接口(ABI)以特定的方式生成二进制文件。恰好在GNU/Linux机器上,ELF ABI与GCC布局和访问结构体的方式完全对应。
换句话说,你可以预测你描述的方法是否适用于任何给定的ABI / 编译器 / 链接器组合。虽然标准不能保证其可行性,但它可能会被ABI的兼容性所保证。

1
有趣的是,我在elf参考规范(第2页)中找到了这个问题的答案。
根据该规范:
所有对象文件格式定义的数据结构都遵循相关类别的“自然”大小和对齐指南。如果必要,数据结构包含显式填充以确保4字节对齐的4字节对象,强制结构大小为4的倍数等。数据从文件开头开始具有适当的对齐方式。因此,例如,包含Elf32_Addr成员的结构将在文件内部对齐到4字节边界上。
这是针对32位架构的,但我相信相同的概念也适用于64位系统。因此,似乎所有为ELF定义的数据结构都是以一种允许对齐的方式制作的,以便一个结构体可以表示它们。
感谢您所有人的回答;它们非常有帮助!

1

您可以通过禁用结构填充来使结构的字段连续。对于 gcc,应该是这样的:

typedef struct Port
{
    uint32_t reg0;
    uint32_t reg1;
    uint32_t reg2;
} __attribute__((__packed__));

对于VS C ++:

#pragma pack(push, 1)

typedef struct Port
{
    uint32_t reg0;
    uint32_t reg1;
    uint32_t reg2;
};

#pragma pop();

0
为了提高速度,填充仅添加以获取32/64位对齐值,并通过查看elf.h中的结构(Elf头结构、程序头结构、节头结构),您将注意到这些值已根据其体系结构对齐,因此,您可以从文件复制内容到内存并将缓冲区强制转换为任何结构,然后从中访问值。
恰巧我正在开发一个类似于你的工具(我试图制作一个结合了ReadelfObjdump功能的工具)。我已经取得了重大进展,愿意在GitHub上分享这个项目。
您可能想加入我一起进一步开发它(请通过afr0ck.bin@gmail.com与我联系)。

这是GitHub上的elf.h链接,可能会有所帮助: link - Karim Manaouil
是的,那就是我一直用作参考的。还有readelf.c代码。你有关于填充只添加到32/64位值的参考资料吗? - gowrath
@gowrath,这不仅适用于32/64位值的添加,而且还适用于获取此32/64位对齐方式...以便结构体的大小始终是4或8(以字节为单位)的倍数...&所有结构体(在此类项目中大多需要)都以这种方式对齐,在elf.h中,它们不需要任何进一步的填充。 - Karim Manaouil
一个定义如下的结构体:struct a { int a; short b;}; 需要填充2个字节才能在32/64位系统上对齐。但是一个定义为:struct b {int a; short b; short c;} 的结构体不需要进一步填充,因为它已经是4/8字节对齐的(其大小是4/8的倍数)。 而 elf.h 中的所有结构体都与第二个示例类似,在对齐方面是4/8对齐的(它们具有不同的数据类型,但结构最终是4/8对齐的)。 - Karim Manaouil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接