打包一个结构体会影响其子结构体吗?

14
我们最近发现一些提交到我们代码库中的代码,大致如下:
#pragma pack(push,1)
struct xyzzy {
    BITMAPINFOHEADER header;
    char plugh;
    long twisty;
} myVar;
我的问题是:这种打包技术是否仅适用于当前结构,还是可能会影响BITMAPINFOHEADER的打包方式。我无法想象后者会有多大的用处,因为这将使该结构与通过Windows API调用获取的结构不同。例如,假设该结构如下:
typedef struct {
    char aChar;
    DWORD biSize;
} BITMAPINFOHEADER;
那种结构如果使用1的打包而不是Windows的默认8(对于32位系统而言,64位系统可能为16),将会有很大的不同。 BITMAPINFOHEADER是否因其很可能早先被声明而免受打包的保护?如果它作为外部声明的一部分被声明,那么它是否会受到打包的影响呢?

1
幸运的是,这个问题本身不应该成为问题,因为“真正的”BITMAPINFOHEADER已经被“完美地”打包了。但我理解你的观点,这是一个有趣的问题,+1。 - Matteo Italia
1
@Yunnosch:标准对于#pragma pack一无所知,因此我们只能依赖供应商提供的文档。 - Matteo Italia
1
@MatteoItalia 是的,那就是我想要表达的意思。不过,至少提到一个特定的工具链及其文档会让答案更加完整(针对该工具)。 - Yunnosch
Win32应用程序不应声明BITMAPINFOHEADER,它由Windows提供。我们必须假设它被正确打包。 - Lundin
3
为避免此类问题,头文件声明被设计成以保持正确的变量布局,无论应用哪种打包方式。查看Windows或*nix头文件,你会发现很多填充变量来保持布局对齐。参见我的答案https://dev59.com/MVcP5IYBdhLWcg3wYpDi#44486156 ;-) - Frankie_C
5个回答

11

来自相关文件

pack在第一个structunionclass声明之后生效。 pack对定义没有影响。

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

如果它被声明为外部声明的一部分,它会受到打包的影响吗?

是的,因为它将成为一个struct声明。

此外,正如Lightness Races in Orbit在评论中指出的那样,在前面可以找到更有说服力的措辞:

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

也就是说,它没有说明这些成员本身包含什么内容,这可能是数据和/或填充。如上所述,打包性与类型相关联的事实似乎加强了这一点。



然而,文档解释不够清晰,最好测试一下这种解释是否正确;gccVC ++的行为与预期相符。我并不特别惊讶 - 任何不同的行为都会在类型系统中造成混乱(获取打包结构的成员指针实际上会提供与其类型不同的指针1)。

一般的理解是:定义一个struct之后,其二进制布局就固定了,包括紧密结构体的子对象在内,所有实例都必须符合它。当前的#pragma pack值仅在定义新结构时考虑,在这样做时,成员的二进制布局是一个固定的黑盒子。

注意事项

  1. 说实话,这有点偏向x86的观点;具有更强对齐要求的机器会反对即使指向布局正确但未对齐的结构的指针也不合法:虽然相对于给定指针的字段偏移量是正确的,但它们并不是真正可用的指针。

    另一方面,如果给出一个指向未对齐对象的指针,您总是可以检测到它未对齐,并将其memcpy到正确对齐的位置,因此它不像指向紧凑对象的假设指针,其布局实际上是未知的,除非您恰好知道其父级的包装方式。


header是一个成员定义,因此它不会受到影响。”我对这个解释并不完全信服。显然,xyzzy本身也是一个定义——MSDN的措辞有些可疑(想想看)。我怀疑它真正的意思是,如果xyzzy类型的定义本身没有被压缩,那么你不能将__attribute__((__packed__))应用于xyzzy类型实例的定义,并期望它被压缩。 - Lightness Races in Orbit
1
个人认为,除了你所做的测试之外,唯一的“证明”是这样的措辞:“打包一个类是将其成员直接放在内存中”。也就是说,它并没有说明这些成员本身包含什么内容,这可能是数据和/或填充。正如上面所探讨的那样,紧凑性与类型相关联,这一事实似乎加强了这一点。 - Lightness Races in Orbit
@LightnessRacesinOrbit 我同意这两个考虑因素,尽管我认为定义的事情既适用于成员也适用于实际变量定义(例如全局变量或者在 #pragma 激活时定义的本地变量)。话虽如此,你引用和解释它也很重要,我会将其添加到答案中。 - Matteo Italia

9

据我所见,它只适用于直接结构。

请看下面的代码片段:

#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


6
不用担心,在StackOverflow上,提供特定工具的“实际观察结果”(包含足够的工具名称和版本说明)被认为是对回答过程的充分参与。 - Yunnosch
考虑到问题涉及到 #pragma ...,我认为只有特定工具的答案才是可能的 - Andrew Henle
这与有关 MSVC 的问题无关。 - Ruslan

6
根据GCC参考文献

In the following example struct my_packed_struct's members are packed closely together, but the internal layout of its s member is not packed - to do that, struct my_unpacked_struct would need to be packed too.

  struct my_unpacked_struct
   {
      char c;
      int i;
   };

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

You may only specify this attribute on the definition of a enum, struct or union, not on a typedef which does not also define the enumerated type, structure or union.


3
TBH这是关于gcc的一个正确答案,并且针对其__attribute__((__packed__)),但实际上OP询问的是VC++ 2015上的#pragma pack。当然我预计它们应该是相同的(因此我的点赞),但鉴于我们谈论的是编译器特定行为和非标准扩展,我希望答案可以处理OP环境的具体情况。 - Matteo Italia
必须这样做,才能获取嵌套结构体的指针,并接收与该类型的任何其他指针相同的数据布局。 - boot4life
2
你为什么要引用GCC参考来回答关于MSVC的问题? - Ruslan

6
假设您使用 GCC(或仿效 GCC 的 Clang),您可以在结构布局指示语中找到相关信息,其中提到 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) 会发生什么。很可能会回到编译开始时的设置。

3

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

会影响子结构的打包方式会以微妙的方式破坏类型系统。

考虑以下情况:

//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不兼容。

这两种方法都明显存在问题。基于这个原因,即使我想编写支持子结构打包的编译器,我也会遇到许多后续问题。我预计我的用户很快就会开始质疑我在这方面的设计决策。


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