在 C 中,编译器将按照声明的顺序排列结构的成员,并在成员之间或最后一个成员之后插入可能的填充字节,以确保每个成员正确对齐。
gcc 提供了一个语言扩展 ,__attribute__((packed))它告诉编译器不要插入填充,从而允许结构成员不对齐。例如,如果系统通常要求所有int对象具有 4 字节对齐,__attribute__((packed))可能会导致int结构成员以奇数偏移量分配。
__attribute__((packed))
int
引用 gcc 文档:
packed' 属性指定变量或结构字段应该具有最小可能的对齐方式——变量一个字节,字段一个位,除非您使用aligned’ 属性指定更大的值。
packed' 属性指定变量或结构字段应该具有最小可能的对齐方式——变量一个字节,字段一个位,除非您使用
显然,使用此扩展可能会导致更小的数据需求但更慢的代码,因为编译器必须(在某些平台上)生成代码以一次访问一个字节的未对齐成员。
但是在任何情况下这是不安全的吗?编译器是否总是生成正确的(尽管速度较慢)代码来访问打包结构的未对齐成员?它甚至有可能在所有情况下都这样做吗?
是的,__attribute__((packed))在某些系统上可能不安全。症状可能不会出现在 x86 上,这只会使问题更加隐蔽;在 x86 系统上进行测试不会发现问题。(在 x86 上,未对齐的访问是在硬件中处理的;如果您取消引用int*指向奇数地址的指针,它会比正确对齐时慢一点,但您会得到正确的结果。)
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.
gcc packed.c -o packed
(使用单个结构而不是数组的程序不会可靠地显示问题,因为编译器可以在奇数地址上分配结构,因此x成员正确对齐。对于两个对象的数组struct foo,至少一个或另一个将有一个未对齐的x成员。)
x
struct foo
(在这种情况下,p0指向一个未对齐的地址,因为它指向一个int成员之后的一个打包成员char。p1恰好是正确对齐的,因为它指向数组的第二个元素中的同一个成员,所以char它前面有两个对象– 在 SPARC Solaris 上,阵列arr似乎分配在一个偶数地址,但不是 4 的倍数。)
p0
char
p1
arr
当按名称引用xa 的成员时struct foo,编译器知道它x可能未对齐,并将生成额外的代码以正确访问它。
一旦arr[0].xor的地址arr[1].x被存储在一个指针对象中,编译器和运行程序都不知道它指向了一个未对齐的int对象。它只是假设它已正确对齐,从而(在某些系统上)导致总线错误或类似的其他故障。
arr[0].x
arr[1].x
我相信在 gcc 中解决这个问题是不切实际的。一个通用的解决方案将要求,对于每次尝试取消引用指向具有非平凡对齐要求的任何类型的指针,要么(a)在编译时证明指针不指向打包结构的未对齐成员,要么(b)生成可以处理对齐或未对齐对象的更大更慢的代码。
我已经提交了gcc 错误报告。正如我所说,我认为修复它不切实际,但文档应该提到它(目前没有)。
更新:截至 2018 年 12 月 20 日,此错误被标记为已修复。该补丁将出现在 gcc 9 中,并添加了一个-Waddress-of-packed-member默认启用的新选项。
-Waddress-of-packed-member
当获取struct或union的packed成员的地址时,可能会导致指针值未对齐。此补丁添加了 -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; | ^~~~~~~~~