是gcc的__attribute __((packed))/ #pragma pack不安全?
在C中,编译器将按照声明的顺序排列结构的成员,在成员之间插入可能的填充字节,或者在最后一个成员之后插入,以确保每个成员都正确alignment。
gcc提供了一个语言扩展, __attribute__((packed))
,它告诉编译器不要插入填充,允许结构成员错位。 例如,如果系统通常要求所有的int
对象都有4字节的alignment方式, __attribute__((packed))
会导致int
结构成员被分配到奇数的偏移量。
引用gcc文档:
“packed”属性指定variables或结构字段应具有尽可能最小的alignment方式 – 一个字节表示一个variables,一个字段表示一个字段,除非用“aligned”属性指定较大的值。
很明显,使用这个扩展可以导致较小的数据需求,但代码较慢,因为编译器必须(在某些平台上)一次生成一个字节访问未alignment成员的代码。
但是有没有这种情况是不安全的? 编译器是否总是生成正确(尽pipe较慢)的代码来访问打包结构的错位成员? 它在所有情况下都可以这样做吗?
是的, __attribute__((packed))
在某些系统上可能是不安全的。 症状可能不会显示在一个x86,这只是使问题更阴险; 在x86系统上testing不会发现问题。 (在x86上,未alignment的访问在硬件中处理;如果您取消引用指向奇数地址的int*
指针,它将比正确alignment时慢一点,但会得到正确的结果。
在其他一些系统(如SPARC)上,尝试访问未alignment的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) = %d\n", (int)sizeof(struct foo)); printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c)); printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x)); printf("arr[0].x = %d\n", arr[0].x); printf("arr[1].x = %d\n", arr[1].x); printf("p0 = %p\n", (void*)p0); printf("p1 = %p\n", (void*)p1); printf("*p0 = %d\n", *p0); printf("*p1 = %d\n", *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
成员就可以正确地alignment了,在两个struct foo
对象的数组中,至less有一个或者另一个会有一个错位的x
成员。)
(在这种情况下, p0
指向一个未alignment的地址,因为它指向一个char
成员之后的压缩int
成员,因为p1
指向数组第二个元素中的同一成员,所以p1
恰好alignment,在它之前的两个char
对象 – 在SPARC Solaris上,数组arr
似乎分配在偶数地址,但不是4的倍数)
当通过名称引用struct foo
的成员x
时,编译器知道x
可能未alignment,并将生成其他代码以正确访问它。
一旦arr[0].x
或arr[1].x
的地址被存储在一个指针对象中,编译器和正在运行的程序都不知道它指向一个未alignment的int
对象。 它只是假设它正确alignment,导致(在某些系统上)出现总线错误或类似的其他故障。
在gcc中解决这个问题我相信是不切实际的。 (a)在编译时certificate指针不指向打包结构的未alignment成员,或者(b)生成体积较大,速度较慢的代码,可以处理alignment或未alignment的对象。
我已经提交了一个gcc错误报告 。 正如我所说,我不认为修复它是可行的,但文件应该提到它(目前没有)。
只要你总是通过结构体访问这个值,这是非常安全的.
(点)或->
符号。
什么是不安全的是采取未alignment的数据的指针,然后访问它没有考虑到。
另外,尽pipe结构中的每个项目都是未alignment的,但已知它是以特定的方式未alignment的,所以整个结构必须与编译器期望的一致,否则就会出现问题(在某些平台上,或在将来如果发明新的方法来优化未alignment的访问)。
正如上面所说,不要把一个指向包装结构的成员的指针。 这只是玩火。 当你说__attribute__((__packed__))
或者#pragma pack(1)
,你真正说的是“嗨,gcc,我真的知道我在做什么”。 当事实certificate你没有,你不能正确地责怪编译器。
不过,也许我们可以责怪编译器自满。 虽然gcc确实有一个-Wcast-align
选项,但它并没有默认启用,也没有启用-Wall
或-Wextra
。 这显然是由于海湾合作委员会的开发人员认为这种types的代码是一个脑残的“ 憎恶 ”,不值得解决 – 可以理解的鄙视,但是当一个没有经验的程序员不屑一顾时,这种做法是无济于事的。
考虑以下:
struct __attribute__((__packed__)) my_struct { char c; int i; }; struct my_struct a = {'a', 123}; struct my_struct *b = &a; int c = ai; int d = b->i; int *e __attribute__((aligned(1))) = &a.i; int *f = &a.i;
这里,a的types是一个打包结构(如上定义)。 同样, b
是一个指向打包结构的指针。 expression式ai
的types(基本上)是1个字节alignment的int l值 。 c
和d
都是正常的。 在读取ai
,编译器会生成未alignment访问的代码。 当你阅读b->i
, b
的types仍然知道它的包装,所以也没有问题。 e
是一个指向单字节alignmentint的指针,因此编译器也知道如何正确解引用。 但是当你赋值f = &a.i
,你将一个未alignment的int指针的值存储在一个alignment的int指针variables中 – 这就是你出错的地方。 我同意, -Wextra
应该有默认启用此警告(甚至不在-Wall
或-Wextra
)。
(下面是一个非常人造的例子。)打包结构的一个主要用途是你有一个数据stream(比如说256个字节),你希望给它提供意义。 如果我举一个更小的例子,假设我有一个运行在我的Arduino上的程序,它通过串行发送一个16字节的数据包,其含义如下:
0: message type (1 byte) 1: target address, MSB 2: target address, LSB 3: data (chars) ... F: checksum (1 byte)
然后我可以声明类似的东西
typedef struct { uint8_t msgType; uint16_t targetAddr; // may have to bswap uint8_t data[12]; uint8_t checksum; } __attribute__((packed)) myStruct;
然后我可以通过aStruct.targetAddr来引用targetAddr字节,而不是用指针算术来摆弄。
现在发生alignment的情况, 除非编译器将结构视为打包(即按照指定的顺序存储数据并正好使用16), 否则将内存中的void *指针接收到的数据并将其转换为myStruct *将不起作用字节为这个例子)。 对未alignment的读取有性能损失,因此使用打包结构来处理程序正在积极处理的数据不一定是个好主意。 但是,当你的程序提供了一个字节列表时,打包结构可以更容易地编写访问内容的程序。
否则,你最终会使用C ++,并用accessor方法编写一个类,并在后台执行指针运算。 简而言之,打包结构是用于有效处理打包的数据,打包的数据可能是你的程序的工作。 在大多数情况下,你的代码应该从结构中读取值,使用它们,并在完成时写回。 所有其他的事情都应该在包装结构之外完成。 问题的一部分是C试图隐藏程序员的低级别的东西,如果这样的事情对程序员来说确实很重要的话,那就是需要跳跃的。 (你几乎需要一种不同的“数据布局”结构,这样你就可以说'这个东西是48个字节长,foo是指13个字节的数据,应该这样解释';还有一个单独的结构化数据结构,在这里你说'我想要一个包含两个整数的结构,叫做alice和bob,还有一个叫做carol的浮点数,我不在乎你是如何实现它的 – 在C中,这两个用例都被绑定到结构体中。