结构包装是确定性的吗?
Is struct packing deterministic?
例如,假设我在不同的项目中有两个等效结构 a
和 b
:
typedef struct _a
{
int a;
double b;
char c;
} a;
typedef struct _b
{
int d;
double e;
char f;
} b;
假设我没有使用任何像 #pragma pack
这样的指令,并且这些结构是在同一架构的同一编译器上编译的,它们在变量之间会有相同的填充吗?
是的。您应该始终假设编译器具有确定性行为。
[编辑] 从下面的评论中,很明显有很多 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 */
will they have identical padding between variables?
实际上,他们大多喜欢具有相同的内存布局。
从理论上讲,由于该标准并未详细说明应如何在对象上使用填充,因此您不能对元素之间的填充做出任何假设。
另外,我什至不明白你为什么要 know/assume 结构成员之间的填充。只需编写标准的、兼容的 C 代码就可以了。
任何理智的编译器都会为这两个结构生成相同的内存布局。编译器通常被编写为完全确定性的程序。需要明确和有意地添加非确定性,我个人看不到这样做的好处。
但是,不 允许您将 struct _a*
转换为 struct _b*
并通过两者访问其数据。 Afaik,即使内存布局相同,这仍然违反了严格的别名规则,因为它允许编译器通过 struct _a*
重新排序访问,通过 struct _b*
访问,这将导致不可预测的、未定义的行为。
编译器是确定性的;如果不是,单独的编译将是不可能的。具有相同 struct
声明的两个不同翻译单元将协同工作;这是由 §6.2.7/1: Compatible types and composite types.
保证的
此外,同一平台上的两个不同编译器应该互操作,尽管标准不保证这一点。 (这是实现质量问题。)为了实现互操作性,编译器编写者同意平台 ABI(应用程序二进制接口),其中将包括如何表示复合类型的精确规范。这样,用一个编译器编译的程序就可以使用用不同编译器编译的库模块。
但你不仅对决定论感兴趣;您还希望两种不同类型的布局相同。
根据标准,如果两个 struct
类型的成员(按顺序)兼容,并且它们的标签和成员名称相同,则它们是兼容的。由于您的示例 structs
具有不同的标签和名称,因此即使它们的成员类型兼容,它们也不兼容,因此您不能在需要另一个的地方使用一个。
该标准允许标签和成员名称影响兼容性,这似乎很奇怪。该标准要求结构的成员按声明顺序排列,因此名称不能更改结构中成员的顺序。那么,为什么它们会影响填充?我不知道他们在哪里使用任何编译器,但该标准的灵活性基于这样的原则,即要求应该是保证正确执行所需的最低限度。翻译单元内不允许使用不同标记的结构别名,因此无需在不同翻译单元之间宽恕它。所以标准不允许这样做。 (实现在 struct
的填充字节中插入有关类型的信息是合法的,即使它需要确定性地添加填充以提供 space 此类信息。唯一的限制是填充不能放在 struct
的第一个成员之前。)
平台 ABI 很可能指定复合类型的布局而不引用其标记或成员名称。在特定平台上,使用具有此类规范的平台 ABI 和记录为符合平台 ABI 的编译器,您可以摆脱别名,尽管它在技术上不正确,而且显然先决条件使其不可移植.
C 标准本身对此没有任何说明,因此原则上您无法确定。
但是:很可能你的编译器遵循某些特定的 ABI,否则与其他库和操作系统的通信将是一个恶梦。在最后一种情况下,ABI 通常会确切地 规定包装的工作方式。
例如:
上 x86_64 Linux/BSD, SystemV AMD64 ABI is the reference. Here (§3.1) for every primitive processor data type it is detailed the correspondence with the C type, its size and its alignment requirement, and it's explained how to use this data to make up the memory layout of bitfields, structs and unions; everything (besides the actual content of the padding) is specified and deterministic. The same holds for many other architectures, see these links.
ARM recommends its EABI 表示它的处理器,一般后面跟着 Linux 和 Windows;聚合对齐在 "Procedure Call Standard for the ARM Architecture Documentation", §4.3.
中指定
在 Windows 上没有跨供应商标准,但 VC++ 本质上规定了 ABI,几乎所有编译器都遵守该标准;可以找到here for x86_64, here for ARM(但对于这个问题感兴趣的部分,它仅指ARM EABI)。
您无法确定地处理不同系统上 C 语言中结构或联合的布局。
虽然很多时候不同的编译器生成的布局看起来是相同的,但您必须考虑这些情况是编译器设计的实用性和功能便利性所决定的收敛,在留给程序员的选择自由范围内标准,因此无效。
C11标准ISO/IEC9899:2011,与之前的标准几乎没有变化,在6.7.2.1段明确说明结构和联合说明符:
Each non-bit-field member of a structure or union object is aligned in an implementation defined manner appropriate to its type.
更糟糕的情况是留给程序员很大自主权的位域:
An implementation may allocate any addressable storage unit large enough to hold a bitfield.
If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.
数一数 'implementation-defined' 和 'unspecified' 这两个词在文本中出现的次数。
同意在使用在不同系统上生成的结构或联合之前 运行 检查编译器版本、机器和目标体系结构 负担不起 你应该有一个对你的问题的正确回答。
现在让我们说是的,有办法。
请注意,这不是绝对的解决方案,而是一种常见的方法,您可以在不同系统之间共享数据结构交换时找到:根据值打包结构元素1(标准字符大小)。
使用打包和准确的结构定义可以产生足够可靠的声明,可以在不同的系统上使用。打包强制编译器删除实现定义的对齐,减少由于标准而导致的最终不兼容。此外,避免使用位域可以消除残留的依赖于实现的不一致。最后,由于缺少对齐,可以通过在元素之间手动添加一些虚拟声明来重新创建访问效率,以强制每个字段正确对齐的方式制作。
作为残差情况,您必须考虑一些编译器添加的结构末尾的填充,但因为没有相关的有用数据,您可以忽略它(除非是动态 space 分配,但同样您可以处理它)。
ISO C 表示,如果不同翻译单元中的两个 struct
类型具有相同的标签和成员,则它们是兼容的。更准确地说,这是来自 C99 标准的确切文本:
6.2.7 Compatible type and composite type
Two types have compatible type if their types are the same. Additional rules for determining whether two types are compatible are described in 6.7.2 for type specifiers, in 6.7.3 for type qualifiers, and in 6.7.5 for declarators. Moreover, two structure, union, or enumerated types declared in separate translation units are compatible if their tags and members satisfy the following requirements: If one is declared with a tag, the other shall be declared with the same tag. If both are complete types, then the following
additional requirements apply: there shall be a one-to-one correspondence between their members such that each pair of corresponding members are declared with compatible types, and such that if one member of a corresponding pair is declared with a name, the other member is declared with the same name. For two structures, corresponding members shall be declared in the same order. For two structures or unions, corresponding bit-fields shall have the same widths. For two enumerations, corresponding members shall have the same values.
如果我们从 "what, the tag or member names could affect padding?" 的角度来解释它,这似乎很奇怪,但基本上规则只是尽可能严格,同时允许常见情况:多个翻译单元共享确切的text 通过头文件的结构声明。如果程序遵循更宽松的规则,它们就没有错;他们只是不依赖于标准的行为要求,而是来自其他地方。
在您的示例中,您 运行 违反了语言规则,因为只有结构等价,但没有等价的标签和成员名称。实际上,这实际上并没有强制执行;无论如何,在不同翻译单元中具有不同标签和成员名称的结构类型 事实上 物理兼容。各种技术都依赖于此,例如从非 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)
。然后,当对象被传递给另一个软件组件(可能来自另一个项目)时,它可以根据 its sizeof (a)
检查大小。如果 size 字段较小,则它知道构造 a
的软件使用的是成员较少的旧声明。所以不存在的成员一定不能访问
任何特定的编译器都应该是确定性的,但在任何两个之间
编译器,甚至是具有不同编译选项的相同编译器,
甚至在同一编译器的不同版本之间,所有赌注都关闭了。
如果你不依赖于细节,你会过得更好
结构,或者如果你这样做,你应该嵌入代码以在运行时检查
该结构实际上是您所依赖的。
这方面的一个很好的例子是最近从 32 位到 64 位的变化
架构,即使你没有改变整数的大小
在结构中使用,更改了部分整数的默认包装;
以前连续 3 个 32 位整数可以完美打包,现在
它们装入两个 64 位插槽中。
你不可能预料到未来会发生什么变化;
如果您依赖于语言无法保证的细节,例如
作为结构打包,你应该在运行时验证你的假设。
例如,假设我在不同的项目中有两个等效结构 a
和 b
:
typedef struct _a
{
int a;
double b;
char c;
} a;
typedef struct _b
{
int d;
double e;
char f;
} b;
假设我没有使用任何像 #pragma pack
这样的指令,并且这些结构是在同一架构的同一编译器上编译的,它们在变量之间会有相同的填充吗?
是的。您应该始终假设编译器具有确定性行为。
[编辑] 从下面的评论中,很明显有很多 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 */
will they have identical padding between variables?
实际上,他们大多喜欢具有相同的内存布局。
从理论上讲,由于该标准并未详细说明应如何在对象上使用填充,因此您不能对元素之间的填充做出任何假设。
另外,我什至不明白你为什么要 know/assume 结构成员之间的填充。只需编写标准的、兼容的 C 代码就可以了。
任何理智的编译器都会为这两个结构生成相同的内存布局。编译器通常被编写为完全确定性的程序。需要明确和有意地添加非确定性,我个人看不到这样做的好处。
但是,不 允许您将 struct _a*
转换为 struct _b*
并通过两者访问其数据。 Afaik,即使内存布局相同,这仍然违反了严格的别名规则,因为它允许编译器通过 struct _a*
重新排序访问,通过 struct _b*
访问,这将导致不可预测的、未定义的行为。
编译器是确定性的;如果不是,单独的编译将是不可能的。具有相同 struct
声明的两个不同翻译单元将协同工作;这是由 §6.2.7/1: Compatible types and composite types.
此外,同一平台上的两个不同编译器应该互操作,尽管标准不保证这一点。 (这是实现质量问题。)为了实现互操作性,编译器编写者同意平台 ABI(应用程序二进制接口),其中将包括如何表示复合类型的精确规范。这样,用一个编译器编译的程序就可以使用用不同编译器编译的库模块。
但你不仅对决定论感兴趣;您还希望两种不同类型的布局相同。
根据标准,如果两个 struct
类型的成员(按顺序)兼容,并且它们的标签和成员名称相同,则它们是兼容的。由于您的示例 structs
具有不同的标签和名称,因此即使它们的成员类型兼容,它们也不兼容,因此您不能在需要另一个的地方使用一个。
该标准允许标签和成员名称影响兼容性,这似乎很奇怪。该标准要求结构的成员按声明顺序排列,因此名称不能更改结构中成员的顺序。那么,为什么它们会影响填充?我不知道他们在哪里使用任何编译器,但该标准的灵活性基于这样的原则,即要求应该是保证正确执行所需的最低限度。翻译单元内不允许使用不同标记的结构别名,因此无需在不同翻译单元之间宽恕它。所以标准不允许这样做。 (实现在 struct
的填充字节中插入有关类型的信息是合法的,即使它需要确定性地添加填充以提供 space 此类信息。唯一的限制是填充不能放在 struct
的第一个成员之前。)
平台 ABI 很可能指定复合类型的布局而不引用其标记或成员名称。在特定平台上,使用具有此类规范的平台 ABI 和记录为符合平台 ABI 的编译器,您可以摆脱别名,尽管它在技术上不正确,而且显然先决条件使其不可移植.
C 标准本身对此没有任何说明,因此原则上您无法确定。
但是:很可能你的编译器遵循某些特定的 ABI,否则与其他库和操作系统的通信将是一个恶梦。在最后一种情况下,ABI 通常会确切地 规定包装的工作方式。
例如:
上 x86_64 Linux/BSD, SystemV AMD64 ABI is the reference. Here (§3.1) for every primitive processor data type it is detailed the correspondence with the C type, its size and its alignment requirement, and it's explained how to use this data to make up the memory layout of bitfields, structs and unions; everything (besides the actual content of the padding) is specified and deterministic. The same holds for many other architectures, see these links.
ARM recommends its EABI 表示它的处理器,一般后面跟着 Linux 和 Windows;聚合对齐在 "Procedure Call Standard for the ARM Architecture Documentation", §4.3.
中指定
在 Windows 上没有跨供应商标准,但 VC++ 本质上规定了 ABI,几乎所有编译器都遵守该标准;可以找到here for x86_64, here for ARM(但对于这个问题感兴趣的部分,它仅指ARM EABI)。
您无法确定地处理不同系统上 C 语言中结构或联合的布局。
虽然很多时候不同的编译器生成的布局看起来是相同的,但您必须考虑这些情况是编译器设计的实用性和功能便利性所决定的收敛,在留给程序员的选择自由范围内标准,因此无效。
C11标准ISO/IEC9899:2011,与之前的标准几乎没有变化,在6.7.2.1段明确说明结构和联合说明符:
Each non-bit-field member of a structure or union object is aligned in an implementation defined manner appropriate to its type.
更糟糕的情况是留给程序员很大自主权的位域:
An implementation may allocate any addressable storage unit large enough to hold a bitfield. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.
数一数 'implementation-defined' 和 'unspecified' 这两个词在文本中出现的次数。
同意在使用在不同系统上生成的结构或联合之前 运行 检查编译器版本、机器和目标体系结构 负担不起 你应该有一个对你的问题的正确回答。
现在让我们说是的,有办法。
请注意,这不是绝对的解决方案,而是一种常见的方法,您可以在不同系统之间共享数据结构交换时找到:根据值打包结构元素1(标准字符大小)。
使用打包和准确的结构定义可以产生足够可靠的声明,可以在不同的系统上使用。打包强制编译器删除实现定义的对齐,减少由于标准而导致的最终不兼容。此外,避免使用位域可以消除残留的依赖于实现的不一致。最后,由于缺少对齐,可以通过在元素之间手动添加一些虚拟声明来重新创建访问效率,以强制每个字段正确对齐的方式制作。
作为残差情况,您必须考虑一些编译器添加的结构末尾的填充,但因为没有相关的有用数据,您可以忽略它(除非是动态 space 分配,但同样您可以处理它)。
ISO C 表示,如果不同翻译单元中的两个 struct
类型具有相同的标签和成员,则它们是兼容的。更准确地说,这是来自 C99 标准的确切文本:
6.2.7 Compatible type and composite type
Two types have compatible type if their types are the same. Additional rules for determining whether two types are compatible are described in 6.7.2 for type specifiers, in 6.7.3 for type qualifiers, and in 6.7.5 for declarators. Moreover, two structure, union, or enumerated types declared in separate translation units are compatible if their tags and members satisfy the following requirements: If one is declared with a tag, the other shall be declared with the same tag. If both are complete types, then the following additional requirements apply: there shall be a one-to-one correspondence between their members such that each pair of corresponding members are declared with compatible types, and such that if one member of a corresponding pair is declared with a name, the other member is declared with the same name. For two structures, corresponding members shall be declared in the same order. For two structures or unions, corresponding bit-fields shall have the same widths. For two enumerations, corresponding members shall have the same values.
如果我们从 "what, the tag or member names could affect padding?" 的角度来解释它,这似乎很奇怪,但基本上规则只是尽可能严格,同时允许常见情况:多个翻译单元共享确切的text 通过头文件的结构声明。如果程序遵循更宽松的规则,它们就没有错;他们只是不依赖于标准的行为要求,而是来自其他地方。
在您的示例中,您 运行 违反了语言规则,因为只有结构等价,但没有等价的标签和成员名称。实际上,这实际上并没有强制执行;无论如何,在不同翻译单元中具有不同标签和成员名称的结构类型 事实上 物理兼容。各种技术都依赖于此,例如从非 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)
。然后,当对象被传递给另一个软件组件(可能来自另一个项目)时,它可以根据 its sizeof (a)
检查大小。如果 size 字段较小,则它知道构造 a
的软件使用的是成员较少的旧声明。所以不存在的成员一定不能访问
任何特定的编译器都应该是确定性的,但在任何两个之间 编译器,甚至是具有不同编译选项的相同编译器, 甚至在同一编译器的不同版本之间,所有赌注都关闭了。
如果你不依赖于细节,你会过得更好 结构,或者如果你这样做,你应该嵌入代码以在运行时检查 该结构实际上是您所依赖的。
这方面的一个很好的例子是最近从 32 位到 64 位的变化 架构,即使你没有改变整数的大小 在结构中使用,更改了部分整数的默认包装; 以前连续 3 个 32 位整数可以完美打包,现在 它们装入两个 64 位插槽中。
你不可能预料到未来会发生什么变化; 如果您依赖于语言无法保证的细节,例如 作为结构打包,你应该在运行时验证你的假设。