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

Using Contiguous Memory of C Struct Members

在将此标记为重复之前,请务必阅读问题。

所以这可能是一个潜在的非常愚蠢的问题,但它困扰着我。我知道,从阅读以及许多其他 SO 问题中可以看出,由于编译器添加的填充,C 中的结构中的字段不能保证是连续的。比如按照C标准:

13/ Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.

我正在编写一个类似于 unix readelfnm 的程序只是为了好玩,它涉及处理文件中特定偏移处的字节以读取某些内容的大量工作值。例如,object 文件的前 62 个字节包含 "file header"。文件 header 的字节 0x00-0x04 编码一个 int,而 0x20-0x28 编码一个指针等。但是,我注意到在 readelf.c 的原始实现中程序员做了这样的事情:

首先,他们声明了一个结构体(让我们称之为 ELF_H),其中包含与文件中的内容相对应的字段 header(即第一个字段是一个 int,就像中的前 4 个字节一样文件 header 是,第二个是字符,因为 elf header 中的字节 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

并且在这样做之后,只需使用结构的成员变量访问 header 的每个部分。因此,他们没有使用指针算法获取应该在字节 0x04 处的内容,而是只执行 hdr.member2(在结构中是第二个成员,后跟第一个是 int 的成员)。

如果不能保证结构中的字段是连续的,这意味着如何工作?

我能找到的最接近的答案是 here,但在那个例子中,结构的成员属于同一类型。在ELF_H中,它们是不同的类型。

提前谢谢你:)

您可以通过禁用结构填充来使结构的字段连续。对于 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();

如果文件中的数据是从正在读取的表单的填充结构写入的,那么填充是无关紧要的;该文件和内存表示一样包含填充。

的确,标准并没有特别严格,编译器可以在编写 ELF 的工具不匹配的 ELF reader 结构中插入随机填充。但在实践中,"unnamed padding" 是为了对齐目的,所有主要编译器都有可预测的行为;他们填充以对齐字段以匹配他们的类型。所以 int 字段(在具有四字节 int 的系统上)前面有 1-3 个填充字节,如果前一个字段没有以四字节边界结束,char 字段没有填充等。在这种情况下,据我所知,没有编译器会在前导 int 字段和随后的 char[2] 之间插入填充,因为 char 无论如何都不需要对齐。

也可以使用非标准编译器扩展来防止填充以对齐结构中的字段,但如果您的结构定义永远不会有未对齐的字段,则没有必要这样做(因为您总是将较小的字段放在较大的字段之后,或者因为您总是将较小的字段组合在一起以保持后续较大字段的对齐要求)。

为了快速起见,仅添加填充以获得32/64位对齐的值,并通过查看elf.h中的结构(精灵头结构,程序头结构,节头结构),您会注意到这些值已经根据它们的体系结构对齐,因此,您可以将内容从文件复制到内存并将缓冲区强制转换为您想要的任何结构,然后从内部访问值。

碰巧(巧合的是)我正在开发一个和你一样的工具(我正在尝试制作一个结合了 ReadelfObjdump 的功能的工具)。我取得了重大进展,我愿意在 GitHub.
上分享该项目 你可能想和我一起进一步开发它(联系我afr0ck.bin@gmail.com)。

How is this meant to work if fields in a struct aren't guaranteed to be contiguous?

标准不要求结构是连续的,但这并不意味着结构是随机或以不可预测的方式布置的。正在使用的特定编译器和链接器将始终以特定方式生成二进制文件,如应用程序二进制接口或 ABI 所规定的那样。碰巧在 GNU/Linux 机器上,ELF ABI 恰好对应于 GCC 布局和访问该结构的方式。

换句话说,您可以预测您描述的方法是否适用于任何给定的 ABI/编译器/链接器组合。它不能保证按标准工作,但可以保证通过 ABI 的兼容性工作。

有趣的是,我在精灵的 reference specification(第 2 页)中找到了答案。

据此:

All data structures that the object file format defines follow the "natural'' size and alignment guidelines for the relevant class. If necessary, data structures contain explicit padding to ensure 4-byte alignment for 4-byte objects, to force structure sizes to a multiple of 4, and so on. Data also have suitable alignment from the beginning of the file. Thus, for example, a structure containing an Elf32_Addr member will be aligned on a 4-byte boundary within the file.

这是专门针对 32 位架构的,但我相信同样的概念也适用于 64 位系统。所以看起来,所有为 ELF 定义的数据结构都是以允许对齐的方式制作的,这样一个结构就可以代表它们。

谢谢大家的回答;他们非常有帮助!