是gcc的
在C语言中,编译器会按照声明的顺序排列结构体的成员,在成员之间插入可能的填充字节,或在最后一个成员之后插入,以确保每个成员都正确对齐。
gcc提供了一个语言扩展, __attribute__((packed))
,它告诉编译器不要插入填充,从而允许struct成员未对齐。 例如,如果系统通常要求所有int
对象具有4字节对齐方式,则__attribute__((packed))
会导致int
结构成员以奇数偏移方式分配。
引用gcc文档:
'packed'属性指定变量或结构字段应具有尽可能最小的对齐方式 - 一个字节用于变量,一个字段用于字段,除非您使用`aligned'属性指定较大的值。
很显然,使用这种扩展可以导致较小的数据需求,但代码较慢,因为编译器必须(在某些平台上)生成代码以一次访问一个字节的未对齐成员。
但是有没有这种情况是不安全的? 编译器是否总是生成正确(虽然较慢)的代码来访问打包结构的未对齐成员? 它在所有情况下都可以这样做吗?
是的, __attribute__((packed))
在某些系统上可能不安全。 这个症状可能不会出现在x86上,这只会让问题更加阴险; 在x86系统上进行测试不会发现问题。 (在x86上,未对齐的访问是在硬件中处理的;如果您取消引用指向奇数地址的int*
指针,它将比正确对齐时慢一点,但会得到正确的结果。)
在其他一些系统(如SPARC)上,试图访问未对齐的int
对象会导致总线错误,导致程序崩溃。
还有一些系统中,未对齐的访问悄悄地忽略了地址的低位,导致它访问错误的内存块。
考虑以下程序:
#include <stdio.h>
#include <stddef.h>
int main(void)
{
struct foo {
char c;
int x;
} __attribute__((packed));
struct foo arr[2] = { { 'a', 10 }, {'b', 20 } };
int *p0 = &arr[0].x;
int *p1 = &arr[1].x;
printf("sizeof(struct foo) = %dn", (int)sizeof(struct foo));
printf("offsetof(struct foo, c) = %dn", (int)offsetof(struct foo, c));
printf("offsetof(struct foo, x) = %dn", (int)offsetof(struct foo, x));
printf("arr[0].x = %dn", arr[0].x);
printf("arr[1].x = %dn", arr[1].x);
printf("p0 = %pn", (void*)p0);
printf("p1 = %pn", (void*)p1);
printf("*p0 = %dn", *p0);
printf("*p1 = %dn", *p1);
return 0;
}
在使用gcc 4.5.2的x86 Ubuntu上,它会生成以下输出:
sizeof(struct foo) = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = 0xbffc104f
p1 = 0xbffc1054
*p0 = 10
*p1 = 20
在具有gcc 4.5.1的SPARC Solaris 9上,它会生成以下内容:
sizeof(struct foo) = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = ffbff317
p1 = ffbff31c
Bus error
在这两种情况下,程序编译时都没有额外的选项,只是gcc packed.c -o packed
。
(一个使用单个结构而不是数组的程序并不能可靠地显示问题,因为编译器可以将结构分配给奇数地址,所以x
成员可以正确对齐。对于两个struct foo
对象的数组,至少有一个或者另一个会有一个错位的x
成员。)
(在这种情况下, p0
指向一个未对齐的地址,因为它指向一个char
成员之后的压缩int
成员, p1
恰好对齐,因为它指向数组第二个元素中的同一个成员,所以有在它之前的两个char
对象 - 在SPARC Solaris上,数组arr
似乎分配在偶数地址,但不是4的倍数)
当通过名称引用struct foo
的成员x
时,编译器知道x
可能未对齐,并将生成其他代码以正确访问它。
一旦arr[0].x
或arr[1].x
的地址被存储在一个指针对象中,编译器和正在运行的程序都不知道它指向了一个未对齐的int
对象。 它只是假定它正确对齐,导致(在某些系统上)出现总线错误或类似的其他故障。
在gcc中解决这个问题我相信是不切实际的。 (a)在编译时证明指针不指向打包结构的未对齐成员,或者(b)在指定任何类型的指针时,生成体积更大,速度更慢的代码,可以处理对齐或未对齐的对象。
我已经提交了一个gcc错误报告。 正如我所说的,我不认为修复它是可行的,但文档应该提及它(目前没有)。
只要您始终通过结构体访问值,它就非常安全.
(点)或->
符号。
什么是不安全的是采取未对齐的数据的指针,然后访问它没有考虑到这一点。
另外,尽管结构中的每个项目都是未知的,但已知它们是以特定的方式未对齐的,所以整个结构必须与编译器期望的一致,否则就会出现问题(在某些平台上,或者在将来如果发明一种新方法来优化未对齐的访问)。
正如上面所说的,不要拿一个指向包装结构的成员的指针。 这只是玩火。 当你说__attribute__((__packed__))
或者#pragma pack(1)
,你真正说的是“嘿gcc,我真的知道我在做什么。” 当事实证明你不这样做时,你不能正确地责怪编译器。
不过,也许我们可以责怪编译器自满。 虽然gcc确实有-Wcast-align
选项,但默认情况下不启用,也不启用-Wall
或-Wextra
。 这显然是由于海湾合作委员会的开发人员认为这种类型的代码是一个脑残的“憎恶”,不值得解决 - 可以理解的鄙视,但是当一个没有经验的程序员b into不前时,这并没有帮助。
考虑以下:
struct __attribute__((__packed__)) my_struct {
char c;
int i;
};
struct my_struct a = {'a', 123};
struct my_struct *b = &a;
int c = a.i;
int d = b->i;
int *e __attribute__((aligned(1))) = &a.i;
int *f = &a.i;
这里,类型a
是填充结构(如上所定义)。 同样, b
是一个指向打包结构体的指针。 表达式ai
的类型(基本上)是1个字节对齐的int l值。 c
和d
都是正常的int
。 在读取ai
,编译器会生成未对齐访问的代码。 当你阅读b->i
, b
的类型仍然知道它的包装,所以它们也没有问题。 e
是一个指向单字节对齐int的指针,因此编译器也知道如何正确解引用。 但是当你赋值f = &a.i
,你将一个未对齐的int指针的值存储在一个对齐的int指针变量中 - 这就是你出错的地方。 我同意,默认情况下,gcc应该启用此警告(甚至不在-Wall
或-Wextra
)。
上一篇: Is gcc's