C语言 包装结构是否会影响子结构



我们最近发现了一些提交到代码库的代码,如下所示:

#pragma pack(push,1)
struct xyzzy {
BITMAPINFOHEADER header;
char plugh;
long twisty;
} myVar;

我的问题是:填料是否仅适用于直接结构,或者是否也会影响BITMAPINFOHEADER的填料。我看不出后一种情况非常有用,因为它会使结构与您从 Windows API 调用中获得的结构不同。举个例子,让我们假设结构是:

typedef struct {
char aChar;
DWORD biSize;
} BITMAPINFOHEADER;

对于 Windows,打包一个而不是默认的八个(无论如何是 32 位,对于 64 位可能是 16 位),这种结构将大不相同。

BITMAPINFOHEADER是否因为几乎可以肯定更早地被宣布而受到包装的"保护"?如果它被宣布为外部申报的一部分,那么它是否需要包装?

从相关文档中:

pack在看到杂注后的第一个structunionclass声明时生效。pack对定义没有影响。

header是成员定义,因此不受影响。

它是作为外部申报的一部分宣布的,那么它会受到包装吗?

是的,因为这将是一个struct宣言。

此外,正如《轨道上的轻盈竞赛》在评论中所说,可以在前面找到更令人信服的措辞:

打包类就是将其成员直接放在内存中

也就是说,它没有说明这些成员本身包含的内容,可能是数据和/或填充。(如上所述)包装性附加到类型上的事实似乎强化了这一点


尽管如此,文档还是很模糊,所以最好测试一下这种解释是否正确;gcc 和 VC++ 的行为都符合预期。并不是说我特别惊讶 - 任何不同的东西都会破坏类型系统(获取指向打包结构成员的指针实际上会提供指向与其类型不同的内容的指针1)。

一般的想法是:一旦你完成了一个struct的定义,它的二进制布局是固定的,它的任何实例化都将符合它,包括打包结构的子对象。当前#pragma pack值仅在定义新结构时考虑,并且这样做时,成员的二进制布局是一个固定的黑盒。

<小时 />

注释

  1. 老实说,这有点以x86为中心的观点;具有更强对齐要求的机器会反对即使是指向布局正确但未对齐结构的指针也不是犹太洁食:尽管字段相对于给定指针的偏移量是正确的,但它们并不是可以按原样使用的指针。

    OTOH,给定一个指向未对齐对象的指针,您始终可以检测到它未对齐并将其memcpy到正确对齐的位置,因此它并不像指向打包对象的假设指针那么糟糕,其布局实际上是未知的,除非您碰巧知道其父对象的包装。

据我所知,它仅适用于即时结构。

看看下面的片段:

#include <stdio.h>
struct /*__attribute__((__packed__))*/ struct_Inner {
char a;
int b;
char c;
};
struct __attribute__((__packed__)) struct_Outer {
char a;
int b;
char c;
struct struct_Inner stInner;
};
int main() 
{
struct struct_Inner oInner;
struct struct_Outer oOuter;
printf("n%zu Bytes", sizeof(oInner));
printf("n%zu Bytes", sizeof(oOuter));
}

指纹:

12 Bytes
18 Bytes

当我打包它打印的struct_Inner时:

6 Bytes
12 Bytes

这是使用 GCC 7.2.0 时。

同样,这绝不是特定于 C 标准的(我只需要阅读),它更多地与编译器的作用有关。

BITMAPINFOHEADER是否因为几乎可以肯定更早地声明而受到打包的"保护"?

我想是的。这完全取决于BITMAPINFOHEADER的声明方式。

假设 GCC(或 Clang 模拟 GCC),您可以在 Structure Layout Pragma 中找到一些相关信息,其中说push的存在保留了状态堆栈上的当前打包状态:

为了与Microsoft Windows 编译器兼容,GCC 支持一组#pragma指令,这些指令更改结构(零宽度位字段除外)、联合和随后定义的类的成员的最大对齐方式。下面的n值始终需要是 2 的小幂,并以字节为单位指定新的对齐方式。

  1. #pragma pack(n)只是设置新的对齐方式。
  2. #pragma pack()将对齐方式设置为编译开始时生效的对齐方式(另请参阅命令行选项-fpack-struct[=n]请参阅代码生成选项)。
  3. #pragma pack(push[,n])在内部堆栈上推送当前对齐设置,然后选择性地设置新的对齐方式。
  4. #pragma pack(pop)会将对齐设置恢复为保存在内部堆栈顶部的对齐设置(并删除该堆栈条目)。请注意,#pragma pack([n])不会影响此内部堆栈;因此,可以#pragma pack(push)后跟多个#pragma pack(n)实例,并由单个#pragma pack(pop)完成。

因此,添加的代码中的#pragma也会影响所有后续结构定义,直到被#pragma pack(pop)抵消。 我会担心的。

该文档没有说明在内部堆栈上没有状态时执行#pragma pack(pop)会发生什么情况。 最有可能的是,它会回退到编译开始时的设置。

根据海湾合作委员会参考:

在下面的示例中struct my_packed_struct的成员是 紧密地挤在一起,但其 s 成员的内部布局是 未打包- 要做到这一点,struct my_unpacked_struct需要 也要打包。

struct my_unpacked_struct
{
char c;
int i;
};
struct my_packed_struct __attribute__ ((__packed__))
{
char c;
int  i;
struct my_unpacked_struct s;
};

您只能在枚举、结构或联合的定义中指定此属性,而不能在未定义 枚举类型、结构或联合。

这并没有直接回答这个问题,但可能会提供一个想法,为什么现有的编译器决定不打包子结构,以及为什么未来的编译器不太可能改变这一点。

传递影响子结构的填料会以微妙的方式破坏类型系统。

考虑:

//Header A.h
typedef struct {
char aChar;
DWORD biSize;
} BITMAPINFOHEADER;

// File A.c
#include <A.h>
void doStuffToHeader(BITMAPINFOHEADER* h)
{
// compute stuff based on data stored in h
// ...
}

// File B.c
#include <A.h>
#pragma pack(push,1)
struct xyzzy {
BITMAPINFOHEADER header;
char plugh;
long twisty;
} myVar;
void foo()
{
doStuffToHeader(&myVar.header);
}

我将指向打包结构的指针传递给一个不知道打包的函数。函数从结构中读取或写入数据的任何尝试都很容易以可怕的方式中断。如果编译器认为这是不可接受的,它有两种解决问题的可能性:

  • 透明地将子结构解压缩为函数调用的临时结构,并在以后重新打包结果。
  • 在内部将xyzzy中的标头字段的类型更改为指示它现在是打包类型并且与正常BITMAPINFOHEADER不兼容的类型。

这两者显然都是有问题的。有了这个推理,即使我想编写一个支持子结构打包的编译器,我也会遇到许多后续问题。我希望我的用户很快就会开始质疑我在这方面的设计决策。

最新更新