【问题标题】:Does packing a structure affect sub-structures?打包结构会影响子结构吗?
【发布时间】:2018-08-13 23:25:49
【问题描述】:

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

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

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

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

如果打包一个而不是 Windows 的默认 8 个(无论如何,32 位,64 位可能是 16 个),这种结构会有很大的不同。

BITMAPINFOHEADER 是否因为几乎可以肯定早先声明的事实而不受包装的“保护”?如果它被声明为外部声明的一部分,那么它是否会受到包装?

【问题讨论】:

  • 幸运的是,这个例子无论如何都不是问题,因为“真实的”BITMAPINFOHEADER 已经“完美”地打包了。但我明白这一点,这是一个有趣的问题,+1。
  • @Yunnosch:标准对#pragma pack一无所知,所以我们只剩下供应商特定的文档。
  • @MatteoItalia 是的,这就是我想要暗示的。尽管如此,参考至少一个指定的工具链及其文档将是一个(特定于工具的)体面的答案。
  • Win32 应用程序不应声明 BITMAPINFOHEADER,它是 provided by Windows。我们必须假设它是正确包装的。
  • 为避免此类问题,标题声明的制作方式是为了保持正确的变量布局,无论应用哪种包装。查看 Win 或 *nix 标头,您会发现很多填充变量来保持布局对齐。看我的回答stackoverflow.com/questions/44485168/… ;-)

标签: c windows visual-studio-2015


【解决方案1】:

来自relevant documentation

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

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

它被声明为外部声明的一部分,那么它会受到包装吗?

是的,因为它将是 struct 声明。

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

打包一个类就是将它的成员直接放在内存中。

即它没有说明这些成员本身包含的内容,可能是数据和/或填充。 (如上文所探讨的)包装与类型相关的事实似乎强化了这一点


不过,文档还是不够含糊,所以最好测试一下这种解释是否正确; both gccVC++ 的行为符合预期。并不是说我特别惊讶 - 任何不同的东西都会破坏类型系统(将指针指向一个压缩结构的成员实际上会提供一个指向与其类型不同的东西的指针说1)。

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


备注

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

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

【讨论】:

  • "header 是成员定义,因此不受影响。" 我并不完全相信这个理由。显然xyzzy 本身也是一个定义——MSDN 的措辞有些狡猾(看图)。我怀疑它 真正 的意思是,如果 xyzzy 的定义本身是'还没有。
  • 就我个人而言,我认为唯一的“证明”(除了你所做的测试)是这样的措辞:“打包一个类就是将它的成员直接放在内存中” 。即它没有说明这些成员本身包含的内容,可能是数据和/或填充。 type (如上所述)packedness 附加到一个事实似乎强化了这一点。
  • @LightnessRacesinOrbit 我同意这两个考虑,尽管我认为定义的方式是同时适用于成员和实际变量定义(例如,在#pragma 处于活动状态时定义的全局变量甚至局部变量)。话虽如此,您的引用及其解释也很重要,我将其添加到答案中。
【解决方案2】:

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

看看下面的sn-p:

#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 的声明方式。

【讨论】:

  • 别担心,在 StackOverflow 上提供特定于工具的“野外观察”(充分说明工具名称和版本)被认为是充分参与回答过程。
  • @Yunnosch 考虑到问题涉及#pragma ...,我想说一个特定于工具的答案是可能的
  • 这与关于 MSVC 的问题无关。
【解决方案3】:

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

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

  1. #pragma pack(n) 只是设置新的对齐方式。
  2. #pragma pack() 将对齐设置为编译开始时生效的对齐方式(另请参见命令行选项 -fpack-struct[=n] 参见 Code Gen Options)。
  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) 会发生什么。很可能,它会回到编译开始时的设置。

【讨论】:

    【解决方案4】:

    根据GCC reference

    在以下示例中,struct my_packed_struct 的成员是 紧密排列在一起,但其成员的内部布局是 未打包 - 为此,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;
        };
    

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

    【讨论】:

    • TBH 这是一个正确的答案 for gcc 和它的__attribute__ ((__packed__)),OP 实际上在 VC++ 2015 上询问了#pragma pack。当然我希望它是一样的(因此赞成),但鉴于我们正在讨论编译器特定的行为和非标准扩展,我希望得到一个解决 OP 环境细节的答案。
    • 它必须是这样的,这样才能获得指向嵌套结构的指针并接收与该类型的任何其他指针相同的数据布局。
    • 您为什么引用 GCC 参考来回答有关 MSVC 的问题?
    【解决方案5】:

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

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

    考虑:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-01-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-29
      • 2014-09-17
      • 1970-01-01
      相关资源
      最近更新 更多