结构包装是否具有确定性?

时间:2017-06-11 14:52:52

标签: c padding

例如,假设我在不同项目中有两个等效结构ab

typedef struct _a
{
    int a;
    double b;
    char c;
} a;

typedef struct _b
{
    int d;
    double e;
    char f;
} b;

假设我没有使用#pragma pack之类的任何指令,并且这些结构是在同一架构上的同一编译器上编译的,它们在变量之间是否有相同的填充?

8 个答案:

答案 0 :(得分:55)

编译器是确定性的;如果它不是,那么单独的编译是不可能的。具有相同struct声明的两个不同翻译单元将一起工作;由§6.2.7/1: Compatible types and composite types保证。

此外,同一平台上的两个不同编译器应该进行互操作,尽管标准不能保证这一点。 (这是一个实施质量问题。)为了允许互操作性,编译器编写者就平台ABI(应用程序二进制接口)达成一致,该平台将包括如何表示复合类型的精确规范。通过这种方式,使用一个编译器编译的程序可以使用使用不同编译器编译的库模块。

但你不仅对决定论感兴趣;您还希望两种不同类型的布局相同。

根据标准,如果两个struct类型的成员(按顺序排列)兼容,并且它们的标签和成员名称相同,则它们是兼容的。由于您的示例structs具有不同的标记和名称,即使它们的成员类型不兼容,它们也不兼容,因此您不能在需要另一个的情况下使用其中一个。

标准允许标签和成员名称影响兼容性似乎很奇怪。该标准要求结构的成员按声明顺序排列,因此名称不能更改结构中成员的顺序。那么,为什么它们会影响填充?我不知道他们所做的任何编译器,但标准的灵活性基于这样的原则,即要求应该是保证正确执行所必需的最低要求。在翻译单元中不允许使用不同标记的结构别名,因此不需要在不同的翻译单元之间宽恕它。所以标准不允许这样做。 (实现在struct的填充字节中插入有关类型的信息是合理的,即使它需要确定性地添加填充以为这些信息提供空间。唯一的限制是填充不能放在struct的第一个成员之前。)

平台ABI可能会指定复合类型的布局,而不会引用其标记或成员名称。在特定平台上,使用具有此类规范的平台ABI和记录符合平台ABI的编译器,您可以摆脱混叠,虽然它在技术上不正确,显然前提条件使其不可移植

答案 1 :(得分:15)

C标准本身对此没有任何说明,所以按照原则你不能确定。

但是 :很可能你的编译器坚持某些特定的ABI,否则与其他库和操作系统进行通信将是一场噩梦。在最后一种情况下,ABI通常会明确 包装是如何工作的。

例如:

    在x86_64 Linux / BSD上的
  • SystemV AMD64 ABI是引用。这里(§3.1)对于每个原始处理器数据类型,它详细说明了与C类型,其大小和对齐要求的对应关系,并解释了如何使用这些数据来构成位域,结构和联合的存储器布局; 所有内容 (除了填充的实际内容外) 已指定且确定性 。许多其他架构也是如此,请参阅these links

  • ARM recommends its EABI用于其处理器,通常后面是Linux和Windows;聚合对齐在“ARM体系结构文档的过程调用标准”,§4.3中指定。

  • Windows上的
  • 没有跨供应商标准,但VC ++实质上决定了ABI,几乎所有编译器都遵守ABI;对于x86_64可以找到它here,对于ARM可以找到here(但是对于这个问题感兴趣的部分,它只是指ARM EABI)。

答案 2 :(得分:10)

任何理智的编译器都会为这两个结构生成相同的内存布局。编译器通常被编写为完全确定的程序。需要明确而刻意地添加非决定论,而且我没有看到这样做的好处。

但是,允许您将struct _b*转换为struct _a*并通过两者访问其数据。 Afaik,即使内存布局相同,这仍然会违反严格的别名规则,因为它允许编译器通过struct _b*重新排序通过{{1}}访问的访问,这将导致不可预知的,未定义的行为。

答案 3 :(得分:8)

  

它们在变量之间是否有相同的填充?

在实践中,他们最喜欢拥有相同的内存布局。

理论上,由于标准没有说明如何在对象上使用填充,你不能在元素之间的填充上真正假设任何东西。

另外,我甚至看不出为什么你想知道/假设结构成员之间的填充。只需编写标准的,兼容的C代码,你就可以了。

答案 4 :(得分:5)

您无法在不同系统上确定性地接近C语言中结构或联合的布局。

虽然很多时候看起来不同编译器生成的布局是相同的,但是你必须考虑这些情况是由编译器设计的实际和功能方便所决定的,在标准的程序员选择自由的范围内,因而无效。

C11标准 ISO / IEC 9899:2011 ,与先前标准几乎没有变化,在 6.7.2.1结构和联合说明符段落中明确说明:

  

结构或联合对象的每个非位字段成员都以适合其类型的实现定义方式对齐。

甚至最糟糕的是位域的情况下,程序员还有很大的自治权:

  

实现可以分配足够大的任何可寻址存储单元来保存位域。   如果剩余足够的空间,则紧跟在结构中的另一个位字段之后的位字段将被打包到相同单元的相邻位中。如果剩余的空间不足,则是否将不适合的位域放入下一个单元或重叠相邻单元是实现定义的。单元内的位域分配顺序(高阶到低阶或低阶到高阶)是实现定义的。可寻址存储单元的对齐未指定。

只计算条款'实施定义的次数'和未指明的'出现在文本中。

同意检查编译器版本,机器和目标架构,每次运行之前使用在不同系统上生成的结构或联合负担不起你应该对你的问题有一个不错的答案。

现在让我们说是的,有一种方法。

要明确它不是解决方案,但是在不同系统之间共享数据结构交换时,您可以找到一种常见的方法:在值1上打包结构元素(标准字符大小)。

使用包装和精确的结构定义可以产生足够可靠的声明,可以在不同的系统上使用。打包强制编译器删除实现定义的对齐,从而减少由于标准而导致的最终不兼容性。此外,避免使用位域,您可以删除残留的实现依赖不一致。最后,由于缺少对齐而导致的访问效率可以通过在元素之间手动添加一些虚拟声明来重新创建,以这种方式制作,以强制正确对齐每个字段。

作为一个剩余案例,你必须考虑一些编译器添加的结构端填充,但由于没有相关的有用数据,你可以忽略它(除非动态空间分配,但你可以再次处理它)。

答案 5 :(得分:4)

ISO C表示,如果不同翻译单元中的两个struct类型具有相同的标记和成员,则它们是兼容的。更确切地说,这是C99标准的确切文本:

  

6.2.7兼容类型和复合类型

     

如果类型相同,则两种类型具有兼容类型。用于确定两种类型是否兼容的附加规则在6.7.2中描述了类型说明符,在6.7.3中描述了类型限定符,在6.7.5中描述了声明符。此外,如果它们的标记和成员满足以下要求,则在单独的转换单元中声明的两个结构,联合或枚举类型是兼容的:如果使用标记声明一个,则另一个应使用相同的标记声明。如果两者都是完整类型,则以下内容   附加要求适用:其成员之间应存在一对一的对应关系,以便每对相应的成员都被声明为兼容类型,并且如果相应对的一个成员被声明具有名称,则另一个成员是用同名声明。对于两个结构,相应的成员应按相同的顺序声明。对于两个结构或联合,相应的位域应具有相同的宽度。对于两个枚举,相应的成员应具有相同的值。

如果我们从“什么,标签或成员名称可能影响填充?”的角度解释它,这似乎很奇怪。但基本上规则只是尽可能严格,同时允许常见的情况:多个翻译单元通过头文件共享结构声明的确切文本。如果程序遵循更宽松的规则,那么它们就没有错;他们只是不依赖于标准行为的要求,而是来自其他地方。

在您的示例中,您正在违反语言规则,仅具有结构等效性,但不具有等效的标记和成员名称。在实践中,这实际上并未实施;不同翻译单元中具有不同标签和成员名称的结构类型无论如何都是事实上物理兼容的。各种技术都依赖于此,例如从非C语言到C库的绑定。

如果您的项目都是C(或C ++),那么尝试将定义放入公共标题可能是值得的。

对版本问题进行一些防御也是一个好主意,例如大小字段:

// Widely shared definition between projects affecting interop!
// Do not change any of the members.
// Add new ones only at the end!
typedef struct a
{
    size_t size; // of whole structure
    int a;
    double b;
    char c;
} a;

我们的想法是,构建a实例的人必须将size字段初始化为sizeof (a)。然后,当对象传递给另一个软件组件(可能来自另一个项目)时,它可以检查 sizeof (a)的大小。如果size字段较小,则它知道构造a的软件正在使用具有较少成员的旧声明。因此,不得访问不存在的成员。

答案 6 :(得分:2)

任何特定的编译器都应该是确定性的,但在任何两个之间 编译器,甚至是具有不同编译选项的相同编译器, 甚至在同一编译器的不同版本之间,所有的赌注都是关闭的。

如果你不依赖于细节,你会好得多 结构,或者如果你这样做,你应该嵌入代码以在运行时检查 结构实际上就是你所依赖的。

这方面的一个很好的例子是最近从32位改为64位 架构,即使你没有改变整数的大小 在结构中使用,部分整数的默认包装发生了变化; 现在,以前连续3个32位整数将完美打包 它们打包成两个64位插槽。

您无法预料未来会发生什么变化; 如果你依赖于语言无法保证的细节,例如 作为结构包装,你应该在运行时验证你的假设。

答案 7 :(得分:-1)

是。您应始终从编译器中承担确定性行为。

[编辑]从下面的评论中可以看出,很多Java程序员正在阅读上述问题。让我们明确一点:C结构不会在目标文件,库或dll中生成任何名称,哈希或类似内容。 C函数签名也没有引用它们。这意味着,成员名称可以随心所欲地改变 - 真的! - 提供成员变量的类型和顺序是相同的。在C中,示例中的两个结构是等效的,因为包装不会改变。这意味着以下滥用在C中是完全有效的,并且在一些最广泛使用的库中肯定会发现更严重的滥用。

[EDIT2]没有人敢在C ++中做任何以下的事情

/* the 3 structures below are 100% binary compatible */
typedef struct _a { int a; double b; char c; }
typedef struct _b { int d; double e; char f; }
typedef struct SOME_STRUCT { int my_i; double my_f; char my_c[1]; }

struct _a a = { 1, 2.5, 'z' };
struct _b b;

/* the following is valid, copy b -> a  */
*(SOME_STRUCT*)&a = *(SOME_STRUCT*)b;
assert((SOME_STRUCT*)&a)->my_c[0] == b.f);
assert(a.c == b.f);

/* more generally these identities are always true. */
assert(sizeof(a) == sizeof(b));
assert(memcmp(&a, &b, sizeof(a)) == 0);
assert(pure_function_requiring_a(&a) == pure_function_requiring_a((_a*)&b));
assert(pure_function_requiring_b((b*)&a) == pure_function_requiring_b(&b));

function_requiring_a_SOME_STRUCT_pointer(&a);  /* may generate a warning, but not all compiler will */
/* etc... the name space abuse is limited to the programmer's imagination */