C - 非标准结构 "compatibility"
C - non-standard struct "compatibility"
简而言之,我的问题是:
C 标准明确规定结构成员的相对地址应按照声明的顺序增长。它也没有说明结构成员应该如何精确对齐的任何细节。显然,这样做是为了允许填充结构和打包结构的实现。然而,从理论上讲,可以有一个符合标准的编译器,只要结构成员的增长顺序与成员声明的顺序相同,它就会为结构成员提供完全随机的地址。但是这样的编译器存在吗?
这里有一些细节。考虑以下两个结构:
struct s1 {
int var1;
char var2;
long var3;
};
struct s2 {
int var1;
char var2;
long var3;
char var4;
int var5;
};
和以下代码:
printf("offsetof(struct s1, var2) = %d\n",
offsetof(struct s1, var2));
printf("offsetof(struct s2, var2) = %d\n",
offsetof(struct s2, var2));
printf("offsetof(struct s1, var3) = %d\n",
offsetof(struct s1, var3));
printf("offsetof(struct s2, var3) = %d\n",
offsetof(struct s2, var3));
gcc (GCC) 4.8.3 20140911 产生以下输出:
offsetof(struct s1, var2) = 4
offsetof(struct s2, var2) = 4
offsetof(struct s1, var3) = 8
offsetof(struct s2, var3) = 8
这非常有道理:常规的符合标准的编译器(不重新排序结构成员的编译器)在为结构成员执行填充时,仅考虑前一个结构成员的大小和偏移量。这意味着具有相应类型的两个结构的第一个成员的相对地址在此类编译器上将始终相同。反过来,这意味着在我们的示例中,我们可以安全地执行以下操作:
struct s2 test_s2, *ptest_s2;
struct s1 test_s1, *ptest_s1;
ptest_s2 = &test_s2;
ptest_s1 = &test_s1;
ptest_s2->var1 = 1;
ptest_s2->var2 = '2';
ptest_s1 = (struct s1*)ptest_s2;
printf("ptest_s1->var1 = %d\n", ptest_s1->var1);
printf("ptest_s1->var2 = %c\n", ptest_s1->var2);
编译和运行良好,并在同一个编译器上给出输出
ptest_s1->var1 = 1
ptest_s1->var2 = 2
由于按照标准,所有指向结构的指针都具有相同的表示和对齐方式,因此这里 UB 的唯一来源实际上是期望具有相应类型的第一个结构成员的相对地址在两个结构中是相同的.
现在,真正的问题来了:现实世界中是否存在相对地址可以不同的编译器(那些不对结构成员重新排序的编译器)?
P.S. 我知道在 C11 中,我可以通过替换第二个结构中的第一个结构的成员以明确定义的方式获得完全相同的结果通过第一个结构的匿名实例(顺便说一下,据我所知,它应该以相同的方式在内部工作),但我想编写可以在不支持匿名结构的编译器版本上执行相同操作的代码。
这个问题出现的次数比您想象的要多。
据我所知,答案是合格的 'no'.
共识似乎是编译器没有真正的理由填充成员,除了确保它们与它们的开始正确对齐并且可以占据数组中的连续位置。
标准要求第一个成员位于 struct
的开头。
我只能找到一些人(在这里、网络等)相信以下是确定类型 T 对齐的最便携的已知方法,并且没有人提供过不兼容的平台。
#include<stddef.h>
#define alignment(T) (offsetof(struct {char w;T v;},v))
编译器开发人员不会无缘无故地浪费内存。然而,从理论上讲,(比如说)有人可能决定将未对齐的成员放置在填充区域的末尾而不是开始处。
甚至可以想象调试编译器可以在数组类型的末尾添加 'overwrite sentinels'。
但是我找不到一个编译器的样本(或声明)(当不打包数据时)除了从第一个成员开始之外做任何事情,为下一个成员填充最少然后为最严格对齐的成员结束填充.
然而,不同的编译器即使在单一架构上也可能对原始类型做出不同的决定,因此 struct
即使在相同的硬件架构上也可能有不同的布局。
所以您不能依赖它来实现互操作性。
struct s3 {
int var1;
int var2;
int var3;
};
struct s4 {
int var1;
int var2;
int var3;
long long var4;
};
当你添加一个具有更强对齐要求的类型时,你就改变了整个结构的对齐方式。
然后当你强制转换和取消引用指针时,它就是 UB。
在上面的代码中,我相信在末尾添加一个 var4
会将 var1
从字对齐更改为双字对齐,假设 int
是字对齐的并且long long
是双字对齐的。
long
是一个非常糟糕的例子,因为它在 32 位 gcc 中是 32 位,但在 64 位 gcc 中是 64 位。
简而言之,我的问题是:
C 标准明确规定结构成员的相对地址应按照声明的顺序增长。它也没有说明结构成员应该如何精确对齐的任何细节。显然,这样做是为了允许填充结构和打包结构的实现。然而,从理论上讲,可以有一个符合标准的编译器,只要结构成员的增长顺序与成员声明的顺序相同,它就会为结构成员提供完全随机的地址。但是这样的编译器存在吗?
这里有一些细节。考虑以下两个结构:
struct s1 {
int var1;
char var2;
long var3;
};
struct s2 {
int var1;
char var2;
long var3;
char var4;
int var5;
};
和以下代码:
printf("offsetof(struct s1, var2) = %d\n",
offsetof(struct s1, var2));
printf("offsetof(struct s2, var2) = %d\n",
offsetof(struct s2, var2));
printf("offsetof(struct s1, var3) = %d\n",
offsetof(struct s1, var3));
printf("offsetof(struct s2, var3) = %d\n",
offsetof(struct s2, var3));
gcc (GCC) 4.8.3 20140911 产生以下输出:
offsetof(struct s1, var2) = 4
offsetof(struct s2, var2) = 4
offsetof(struct s1, var3) = 8
offsetof(struct s2, var3) = 8
这非常有道理:常规的符合标准的编译器(不重新排序结构成员的编译器)在为结构成员执行填充时,仅考虑前一个结构成员的大小和偏移量。这意味着具有相应类型的两个结构的第一个成员的相对地址在此类编译器上将始终相同。反过来,这意味着在我们的示例中,我们可以安全地执行以下操作:
struct s2 test_s2, *ptest_s2;
struct s1 test_s1, *ptest_s1;
ptest_s2 = &test_s2;
ptest_s1 = &test_s1;
ptest_s2->var1 = 1;
ptest_s2->var2 = '2';
ptest_s1 = (struct s1*)ptest_s2;
printf("ptest_s1->var1 = %d\n", ptest_s1->var1);
printf("ptest_s1->var2 = %c\n", ptest_s1->var2);
编译和运行良好,并在同一个编译器上给出输出
ptest_s1->var1 = 1
ptest_s1->var2 = 2
由于按照标准,所有指向结构的指针都具有相同的表示和对齐方式,因此这里 UB 的唯一来源实际上是期望具有相应类型的第一个结构成员的相对地址在两个结构中是相同的.
现在,真正的问题来了:现实世界中是否存在相对地址可以不同的编译器(那些不对结构成员重新排序的编译器)?
P.S. 我知道在 C11 中,我可以通过替换第二个结构中的第一个结构的成员以明确定义的方式获得完全相同的结果通过第一个结构的匿名实例(顺便说一下,据我所知,它应该以相同的方式在内部工作),但我想编写可以在不支持匿名结构的编译器版本上执行相同操作的代码。
这个问题出现的次数比您想象的要多。 据我所知,答案是合格的 'no'.
共识似乎是编译器没有真正的理由填充成员,除了确保它们与它们的开始正确对齐并且可以占据数组中的连续位置。
标准要求第一个成员位于 struct
的开头。
我只能找到一些人(在这里、网络等)相信以下是确定类型 T 对齐的最便携的已知方法,并且没有人提供过不兼容的平台。
#include<stddef.h>
#define alignment(T) (offsetof(struct {char w;T v;},v))
编译器开发人员不会无缘无故地浪费内存。然而,从理论上讲,(比如说)有人可能决定将未对齐的成员放置在填充区域的末尾而不是开始处。 甚至可以想象调试编译器可以在数组类型的末尾添加 'overwrite sentinels'。
但是我找不到一个编译器的样本(或声明)(当不打包数据时)除了从第一个成员开始之外做任何事情,为下一个成员填充最少然后为最严格对齐的成员结束填充.
然而,不同的编译器即使在单一架构上也可能对原始类型做出不同的决定,因此 struct
即使在相同的硬件架构上也可能有不同的布局。
所以您不能依赖它来实现互操作性。
struct s3 {
int var1;
int var2;
int var3;
};
struct s4 {
int var1;
int var2;
int var3;
long long var4;
};
当你添加一个具有更强对齐要求的类型时,你就改变了整个结构的对齐方式。
然后当你强制转换和取消引用指针时,它就是 UB。
在上面的代码中,我相信在末尾添加一个 var4
会将 var1
从字对齐更改为双字对齐,假设 int
是字对齐的并且long long
是双字对齐的。
long
是一个非常糟糕的例子,因为它在 32 位 gcc 中是 32 位,但在 64 位 gcc 中是 64 位。