在C中,编译器将按照声明它们的顺序布置结构的成员,在成员之间插入可能的填充字节,或者在最后一个成员之后插入,以确保每个成员都正确对齐。
gcc提供了一个语言扩展名__attribute__((packed))
,它告诉编译器不要插入填充,允许结构成员不对齐。例如,如果系统通常要求所有int
个对象具有4字节对齐,__attribute__((packed))
可以导致int
结构成员在奇数偏移处分配。
引用gcc文档:
`packed'属性指定变量或结构字段 应该具有尽可能小的对齐 - 变量的一个字节, 和一个字段的字段,除非你指定一个更大的值 `aligned'属性。
显然,使用此扩展可能会导致较小的数据要求但代码较慢,因为编译器必须(在某些平台上)生成代码,以便一次一个字节地访问未对齐的成员。
但是有任何不安全的情况吗?编译器是否始终生成正确(但速度较慢)的代码来访问打包结构的未对齐成员?在所有情况下都可以这样做吗?
答案 0 :(得分:127)
是的,__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) = %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
成员正确对齐。使用两个数组struct foo
个对象,至少有一个或其他对象会有一个未对齐的x
成员。)
(在这种情况下,p0
指向未对齐的地址,因为它指向int
成员后面的已打包的char
成员。p1
碰巧正确对齐,因为它指向数组的第二个元素中的同一个成员,所以它前面有两个char
个对象 - 而在SPARC Solaris上,数组arr
似乎是在一个地址处分配的甚至,但不是4的倍数。)
当按名称引用x
的成员struct foo
时,编译器知道x
可能未对齐,并将生成其他代码以正确访问它。
一旦arr[0].x
或arr[1].x
的地址存储在指针对象中,编译器和正在运行的程序都不会知道它指向未对齐的int
对象。它只是假设它正确对齐,导致(在某些系统上)总线错误或类似的其他故障。
我相信,将其固定在gcc中是不切实际的。一般解决方案需要,每次尝试取消引用具有非平凡对齐要求的任何类型的指针要么(a)在编译时证明指针不指向压缩结构的未对齐成员,或者(b)生成更大,更慢的代码,可以处理对齐或未对齐的对象。
我已经提交了gcc bug report。正如我所说,我认为修复它是不切实际的,但文档应该提到它(目前没有)。
更新:截至2018-12-20,此错误标记为已修复。补丁将出现在gcc 9中,并添加了一个新的-Waddress-of-packed-member
选项,默认情况下已启用。
当结构或联合的打包成员的地址被采用时,它可以 导致未对齐的指针值。这个补丁增加了 -Waddress-of-packed-member检查指针赋值的对齐并警告未对齐的地址以及未对齐的指针
我刚从源代码构建了那个版本的gcc。对于上述程序,它会产生以下诊断信息:
c.c: In function ‘main’:
c.c:10:15: warning: taking address of packed member of ‘struct foo’ may result in an unaligned pointer value [-Waddress-of-packed-member]
10 | int *p0 = &arr[0].x;
| ^~~~~~~~~
c.c:11:15: warning: taking address of packed member of ‘struct foo’ may result in an unaligned pointer value [-Waddress-of-packed-member]
11 | int *p1 = &arr[1].x;
| ^~~~~~~~~
答案 1 :(得分:50)
如上所述,不要将指针指向已打包的结构的成员。这只是玩火。当你说__attribute__((__packed__))
或#pragma pack(1)
时,你真正说的是“嘿gcc,我真的知道我在做什么。”当事实证明你没有,你不能正确地责怪编译器。
也许我们可以责怪编译器因为它的自满情绪。虽然gcc确实有-Wcast-align
选项,但默认情况下也未启用,也未启用-Wall
或-Wextra
。这显然是因为gcc开发人员认为这种类型的代码是一个脑子死的“abomination”不值得解决 - 可以理解的蔑视,但是当没有经验的程序员遇到它时它无济于事。
请考虑以下事项:
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
是指向压缩结构的指针。表达式a.i
的类型(基本上)是一个带有1个字节对齐的int l-value。 c
和d
都是正常的int
。读取a.i
时,编译器会生成未对齐访问的代码。当你阅读b->i
时,b
的类型仍然知道它已经打包,所以也没问题。 e
是指向一个字节对齐的int的指针,因此编译器也知道如何正确地取消引用。但是当您进行赋值f = &a.i
时,您将未对齐的int指针的值存储在对齐的int指针变量中 - 这就是您出错的地方。我同意,gcc应该通过默认启用此警告(甚至不在-Wall
或-Wextra
中)。
答案 2 :(得分:48)
只要您始终通过.
(点)或->
表示法通过结构访问值,这是完全安全的。
什么不安全的是获取未对齐数据的指针,然后在不考虑这一点的情况下访问它。
此外,即使已知结构中的每个项目都是未对齐的,但已知它是以特定方式未对齐的,因此整个结构必须按照编译器期望的那样对齐或在那里会遇到麻烦(在某些平台上,或者将来如果发明了一种新方法来优化未对齐的访问)。
答案 3 :(得分:2)
使用此属性绝对不安全。
它打破的一件事是union
的能力,它包含两个或更多结构,如果一个结构具有相同的成员初始序列,则它们可以写入一个成员并读取另一个成员。 C11 standard的6.5.2.3节规定:
6 为了简化联合的使用,我们做出了一项特殊保证: 如果一个联合包含几个共享共同点的结构 初始序列(请参见下文),以及并集对象 当前包含这些结构之一,允许 在任何地方检查其中任何一个的共同初始部分 联合完成类型的声明可见。 o 对应的结构共享一个共同的初始序列 成员具有兼容的类型(对于位域,宽度相同) 对于一个或多个初始成员的序列。
...
9 示例3以下是有效的片段:
union { struct { int alltypes; }n; struct { int type; int intnode; } ni; struct { int type; double doublenode; } nf; }u; u.nf.type = 1; u.nf.doublenode = 3.14; /* ... */ if (u.n.alltypes == 1) if (sin(u.nf.doublenode) == 0.0) /* ... */
引入__attribute__((packed))
会破坏这一点。以下示例在gcc 5.4.0禁用优化的情况下在Ubuntu 16.04 x64上运行:
#include <stdio.h>
#include <stdlib.h>
struct s1
{
short a;
int b;
} __attribute__((packed));
struct s2
{
short a;
int b;
};
union su {
struct s1 x;
struct s2 y;
};
int main()
{
union su s;
s.x.a = 0x1234;
s.x.b = 0x56789abc;
printf("sizeof s1 = %zu, sizeof s2 = %zu\n", sizeof(struct s1), sizeof(struct s2));
printf("s.y.a=%hx, s.y.b=%x\n", s.y.a, s.y.b);
return 0;
}
输出:
sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678
即使struct s1
和struct s2
具有“共同的初始序列”,但应用于前者的打包意味着相应的成员不在相同的字节偏移量处生存。结果是,写入写入成员x.b
的值与从成员y.b
读取的值不同,即使标准规定它们应该相同。