Kei*_*son 149 c gcc pragma-pack
在C中,编译器将按照声明它们的顺序布置结构的成员,在成员之间插入可能的填充字节,或者在最后一个成员之后插入,以确保每个成员正确对齐.
gcc提供了一种语言扩展,__attribute__((packed))它告诉编译器不要插入填充,允许结构成员不对齐.例如,如果系统通常要求所有int对象都具有4字节对齐,则__attribute__((packed))可能导致int在奇数偏移处分配struct成员.
引用gcc文档:
`packed'属性指定变量或结构字段应该具有尽可能小的对齐 - 变量的一个字节和字段的一个位,除非您使用`aligned'属性指定更大的值.
显然,使用此扩展可以导致更小的数据要求但代码更慢,因为编译器必须(在某些平台上)生成代码,以便一次一个字节地访问未对齐的成员.
但有任何不安全的情况吗?编译器是否始终生成正确(但速度较慢)的代码来访问打包结构的未对齐成员?在所有情况下都可以这样做吗?
Kei*_*son 138
是的,__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;
}
Run Code Online (Sandbox Code Playgroud)
在带有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
Run Code Online (Sandbox Code Playgroud)
在带有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
Run Code Online (Sandbox Code Playgroud)
在这两种情况下,编译程序都没有额外的选项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错误报告.正如我所说,我认为修复它是不切实际的,但文档应该提到它(它目前没有).
更新:截至2018-12-20,此错误被标记为FIXED.补丁将出现在gcc 9中,并添加了一个-Waddress-of-packed-member默认启用的新选项.
当获取struct或union的压缩成员的地址时,可能会导致未对齐的指针值.这个补丁添加-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;
| ^~~~~~~~~
Run Code Online (Sandbox Code Playgroud)
Dan*_*tos 55
正如上面提到的ams,不要指向一个包装结构的成员.这只是玩火.当你说__attribute__((__packed__))或者#pragma pack(1),你真正说的是"嘿gcc,我真的知道我在做什么." 当事实证明你没有,你不能正确地责怪编译器.
也许我们可以责怪编译器因为它的自满情绪.虽然GCC确实有一个 -Wcast-align选项,但默认情况下不,也不启用-Wall或-Wextra.这显然是由于gcc开发人员认为这种类型的代码是一个脑子死的" 憎恶 "不值得解决 - 可以理解的蔑视,但是当没有经验的程序员陷入其中时它没有帮助.
考虑以下:
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;
Run Code Online (Sandbox Code Playgroud)
这里,类型a是打包结构(如上所定义).类似地,b是指向打包结构的指针.表达式的类型a.i(基本上)是1字节对齐的int l值. c并且d都是正常int的.在读取时a.i,编译器会生成用于未对齐访问的代码.当你读到时b->i,b它的类型仍然知道它是打包的,所以也没问题. e是一个指向一个字节对齐的int的指针,因此编译器也知道如何正确地取消引用.但是当你进行赋值时f = &a.i,你将一个未对齐的int指针的值存储在一个对齐的int指针变量中 - 这就是你出错的地方.我同意,gcc 默认启用此警告(甚至不在-Wall或中-Wextra).
ams*_*ams 48
只要您始终通过.(点)或->符号通过结构访问值,它就是完全安全的.
什么是不是安全的考虑是未对齐数据的指针,然后访问它没有考虑到这一点.
此外,即使已知结构中的每个项目都是未对齐的,但已知它是以特定方式未对齐的,因此整个结构必须按编译器期望的方式对齐,否则会出现问题(在某些平台上,或者将来如果发明一种新方法来优化未对齐的访问).
使用这个属性肯定是不安全的。
它破坏的一件特殊事情是union包含两个或多个结构的 a 的能力,如果结构具有共同的初始成员序列,则可以写入一个成员并读取另一个成员。C11 标准的第 6.5.2.3 节规定:
6为了简化联合的使用,做出了一项特殊保证:如果联合包含多个共享公共初始序列的结构(见下文),并且如果联合对象当前包含这些结构之一,则允许检查在联合的完整类型的声明可见的任何地方,它们中的任何一个的公共初始部分。如果对应的成员对于一个或多个初始成员的序列具有兼容的类型(并且对于位域,具有相同的宽度),则两个结构共享一个共同的初始序列。
...
9示例 3 以下是一个有效的片段:
Run Code Online (Sandbox Code Playgroud)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;
}
Run Code Online (Sandbox Code Playgroud)
输出:
sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678
Run Code Online (Sandbox Code Playgroud)
即使struct s1与struct s2有“共同初始序列”,包装适用于前者意味着相应的成员不住在同一字节偏移。结果是写入 memberx.b的值与从 member 读取的值不同y.b,即使标准说它们应该相同。