打包结构会影响子结构吗?

Does packing a structure affect sub-structures?

我们最近发现一些代码提交到我们的代码库,大致如下:

#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"protected"是不是因为几乎肯定是提前申报的包装?如果作为外层申报的一部分申报,那么是否需要装箱?

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

看看下面的代码片段:

#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 标准的(我只是不得不阅读),它更多地与编译器的工作有关。

Is the BITMAPINFOHEADER "protected" from the packing by virtue of the fact it's almost certainly be declared earlier?

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

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

For compatibility with Microsoft Windows compilers, GCC supports a set of #pragma directives that change the maximum alignment of members of structures (other than zero-width bit-fields), unions, and classes subsequently defined. The n value below always is required to be a small power of two and specifies the new alignment in bytes.

  1. #pragma pack(n) simply sets the new alignment.
  2. #pragma pack() sets the alignment to the one that was in effect when compilation started (see also command-line option -fpack-struct[=n] see Code Gen Options).
  3. #pragma pack(push[,n]) pushes the current alignment setting on an internal stack and then optionally sets the new alignment.
  4. #pragma pack(pop) restores the alignment setting to the one saved at the top of the internal stack (and removes that stack entry). Note that #pragma pack([n]) does not influence this internal stack; thus it is possible to have #pragma pack(push) followed by multiple #pragma pack(n) instances and finalized by a single #pragma pack(pop).

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

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

来自relevant documentation

pack takes effect at the first struct, union, or class declaration after the pragma is seen. pack has no effect on definitions.

header是成员定义,所以不受影响。

it was declared as part of the outer declaration, would it be subject to the packing then?

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

另外,正如Lightness Races in Orbit在评论中所说,更令人信服的措辞可以在前面找到:

To pack a class is to place its members directly after each other in memory.

即它没有说明这些成员本身包含的内容,可能是数据 and/or 填充。事实上(如上文所探讨的)打包是附加到一个类型上的,这似乎强化了这一点


不过,文档已经够模糊了,所以最好测试一下这个解释是否正确; both gcc and VC++ 表现符合预期。并不是说我特别惊讶——任何不同的东西都会破坏类型系统(获取一个指向打包结构成员的指针实际上会提供一个指向不同于其类型的指针 1).

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


备注

  1. 老实说,这有点以 x86 为中心的观点;具有更强对齐要求的机器会反对甚至指向 layout-correct 但未对齐结构的指针也不合乎规范:尽管字段相对于给定指针的偏移量是正确的,但它们并不是真正可以按原样使用的指针.

    OTOH,给定一个指向未对齐对象的指针,您总是可以检测到它是未对齐的,并且 memcpy 它指向 correctly-aligned 位置,所以它不像假设的指向打包对象的指针那么糟糕,其布局实际上是未知的,除非您碰巧知道其父级的包装。

根据GCC reference

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.

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

传递影响 sub-structures 的包装会以微妙的方式破坏类型系统。

考虑:

//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);
}

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

  • 透明地将 sub-struct 解压到函数调用的临时文件中,然后 re-pack 结果。
  • 在内部将 xyzzy 中 header 字段的类型更改为表示它现在是打包类型并且与正常 BITMAPINFOHEADER.
  • 不兼容的类型

这两个明显是有问题的。有了这个推理,即使我想编写一个支持 sub-structs 打包的编译器,我也会 运行 成许多 follow-up 问题。我希望我的用户很快就会开始质疑我在这方面的设计决策。