c-结构打包是确定的吗?

例如,假设我在不同的项目中有两个等效的结构a#pragma pack

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

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

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

Govind Parmar asked 2020-08-11T16:21:57Z
8个解决方案
55 votes

编译器是确定性的; 如果不是这样,则不可能进行单独的编译。 具有相同struct声明的两个不同的翻译部门将一起工作; §6.2.7/ 1保证:兼容类型和复合类型。

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

但是,您不仅对确定性感兴趣。 您还希望两种不同类型的布局相同。

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

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

平台ABI可能会在不参考其标记或成员名称的情况下指定复合类型的布局。 在特定的平台上,如果平台ABI具有这样的规范,并且记录了符合平台ABI的编译器,则可以避免使用别名,尽管从技术上讲这不是正确的,并且显然前提条件使其不可移植 。

rici answered 2020-08-11T16:22:29Z
15 votes

C标准本身对此什么也没说,因此原则上您不能确定。

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

例如:

  • 在x86_64 Linux / BSD上,SystemV AMD64 ABI是参考。 这里(第3.1节)详细介绍了每种原始处理器数据类型与C类型的对应关系,其大小和对齐要求,并说明了如何使用此数据来构成位域,结构和联合的内存布局; 所有内容(填充的实际内容除外)均已指定并具有确定性。 对于许多其他体系结构也是如此,请参见这些链接。

  • ARM建议将EABI用于其处理器,Linux和Windows都紧随其后。 聚合对齐方式在“ ARM体系结构文档的过程调用标准”的第4.3节中指定。

  • 在Windows上,没有跨供应商标准,但是VC ++本质上决定了ABI,几乎所有编译器都遵守该标准。 可以在x86_64和ARM处找到它(但对此问题感兴趣的部分仅指ARM EABI)。

Matteo Italia answered 2020-08-11T16:23:13Z
10 votes

任何明智的编译器都会为这两个结构产生相同的内存布局。 编译器通常被编写为完全确定性的程序。 非确定性将需要被明确地和故意地添加,我对此一无所知。

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

cmaster answered 2020-08-11T16:23:38Z
8 votes

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

实际上,他们大多喜欢具有相同的内存布局。

从理论上讲,由于该标准并未过多地说明应如何在对象上使用填充,因此您不能在元素之间的填充上真正承担任何责任。

另外,我什至看不到为什么您想知道/假设一些关于结构成员之间的填充的知识。 只需编写符合标准的C代码,您就可以了。

David Haim answered 2020-08-11T16:24:12Z
5 votes

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

虽然很多时候似乎由不同的编译器生成的布局是相同的,但是您必须考虑这种情况,这是由编译器设计的实用性和功能性便利性所决定的,这种收敛是在标准留给程序员的自由范围之内的,因此不是 有效。

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

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

更糟糕的是,位域的情况将留给程序员:

一个实现可以分配任何足够大的可寻址存储单元来容纳位域。 如果有足够的空间,应将紧随结构中另一个位域的位域打包到同一单元的相邻位中。 如果剩余空间不足,则将实现不当的位字段放入下一个单元还是与相邻单元重叠。 单位内的位域分配顺序(从高位到低位或从低位到高位)是实现定义的。 未指定可寻址存储单元的对齐方式。

只需计算一下术语“实现定义的”和“未指定的”出现在文本中的次数。

同意在使用另一系统上生成的结构或联合之前检查运行的编译器版本,机器和目标体系结构是不可承受的,您应该对问题有一个不错的答案。

现在说是的,有一个解决方法。

需要明确的是,它并不是绝对的解决方案,而是在不同系统之间共享数据结构交换时可以找到的一种通用方法:将结构元素打包为值1(标准字符大小)。

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

作为一种剩余的情况,您必须考虑某些编译器在结构末尾添加的填充,但是由于没有关联的有用数据,您可以忽略它(除非对于动态空间分配,但是您仍然可以处理它)。

Frankie_C answered 2020-08-11T16:25:24Z
4 votes

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

6.2.7兼容类型和复合类型

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

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

在您的示例中,您仅具有结构上的等效性,而没有等效的标记和成员名,因此违反了语言规则。 实际上,这实际上并没有强制执行。 在不同翻译单元中具有不同标签和成员名称的struct类型实际上在物理上是兼容的。 各种各样的技术都依赖于此,例如从非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;

这个想法是,构造3006903934389889840实例的任何人都必须将3006903934389849841字段初始化为sizeof (a)。然后,当将该对象传递给另一个软件组件(可能来自另一个项目)时,它可以对照其3006903934389849847043检查大小。 较小的对象,则说明构造a的软件使用的是成员少的旧声明。 因此,不得访问不存在的成员。

Kaz answered 2020-08-11T16:26:20Z
2 votes

任何特定的编译器都应该是确定性的,但必须介于两者之间编译器,甚至是具有不同编译选项的同一编译器,甚至在同一编译器的不同版本之间,所有选项都关闭。

如果您不依赖于详细信息,则情况会好得多。结构,或者如果这样做,则应嵌入代码以在运行时进行检查结构实际上取决于您。

一个很好的例子是最近从32位更改为64位架构,即使您不更改整数的大小在结构中使用时,部分整数的默认打包已更改;以前连续3个32位整数会完美地打包,现在它们打包成两个64位插槽。

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

ddyer answered 2020-08-11T16:26:55Z
-1 votes

是。 您应该始终假设编译器具有确定性行为。

[编辑]从下面的评论中可以明显看出,有很多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 */
Michaël Roy answered 2020-08-11T16:27:26Z
translate from https://stackoverflow.com:/questions/44485168/is-struct-packing-deterministic