gcc的__attribute __((打包))/ #pragma包不安全吗?

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的倍数.)

当引用按名称的成员xstruct foo,编译器知道x可能未对齐,并将生成其他代码以正确访问它.

一旦存储在指针对象中arr[0].xarr[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)

  • 在gcc中修复它的另一种方法是使用类型系统:要求指向打包结构成员的指针只能分配给自己标记为打包的指针(即可能未对齐).但实际上:包装结构,只是说不. (14认同)
  • @Flavius:我的主要目的是获取信息.另见http://meta.stackexchange.com/questions/17463/can-i-answer-my-own-questions-even-those-where-i-knew-the-answer-before-asking (9认同)
  • 包装本身似乎是安全的,但如何使用包装成员可能是不安全的.较旧的基于ARM的CPU也不支持未对齐的内存访问,较新的版本可以,但我知道在这些较新的版本上运行时,Symbian OS仍然不允许未对齐的访问(支持已关闭). (8认同)
  • ARM上错位的struct元素确实很奇怪:有些访问会导致错误,有些会导致检索到的数据反直觉地重新排列或者包含相邻的意外数据. (5认同)
  • @SF.:考虑一个带有 `int*` 参数的外部函数。该函数已经并且应该没有意识到它可能会收到未对齐的指针。最简单的解决方案是将打包结构的成员视为位字段,不允许获取它们的地址(这也意味着无法对打包结构的数组成员进行索引)。或者 gcc 可以提供一种新的指针类型,与 `int*` 不兼容,可以指向未对齐的对象;`&amp;obj.x` 会产生一个未对齐的指针,它不能直接传递给需要 `int*` 的东西。 (3认同)

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).

  • +1用于解释如何使用带有未对齐结构的指针! (4认同)
  • @Soumya可悲的是,`__ attribute __((aligned(x)))`现在在用于指针时似乎被忽略了.:(我还没有完整的细节,但使用`__builtin_assume_aligned(ptr,align)`似乎得到gcc生成正确的代码.当我更简洁的答案(并希望一个错误报告)我会更新我的回答. (3认同)
  • [在x86-64上,看起来很无辜的代码发生了崩溃](/sf/answers/3275357081/)... (3认同)

ams*_*ams 48

只要您始终通过.(点)或->符号通过结构访问值,它就是完全安全的.

什么是不是安全的考虑是未对齐数据的指针,然后访问它没有考虑到这一点.

此外,即使已知结构中的每个项目都是未对齐的,但已知它是以特定方式未对齐的,因此整个结构必须按编译器期望的方式对齐,否则会出现问题(在某些平台上,或者将来如果发明一种新方法来优化未对齐的访问).

  • @antonm:是的,打包结构中的结构可能是未对齐的,但是,编译器再次知道每个字段的对齐方式,只要不尝试使用指针进入结构,它就是完全安全的.您应该将结构中的结构想象为一个平坦的字段系列,额外的名称只是为了便于阅读. (3认同)

dbu*_*ush 8

使用这个属性肯定是不安全的。

它破坏的一件特殊事情是union包含两个或多个结构的 a 的能力,如果结构具有共同的初始成员序列,则可以写入一个成员并读取另一个成员。C11 标准的第 6.5.2.3 节规定:

6为了简化联合的使用,做出了一项特殊保证:如果联合包含多个共享公共初始序列的结构(见下文),并且如果联合对象当前包含这些结构之一,则允许检查在联合的完整类型的声明可见的任何地方,它们中的任何一个的公共初始部分。如果对应的成员对于一个或多个初始成员的序列具有兼容的类型(并且对于位域,具有相同的宽度),则两个结构共享一个共同的初始序列。

...

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)
/*
...
*/
Run Code Online (Sandbox Code Playgroud)

__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 s1struct s2有“共同初始序列”,包装适用于前者意味着相应的成员不住在同一字节偏移。结果是写入 memberx.b的值与从 member 读取的值不同y.b,即使标准说它们应该相同。

  • 有人可能会争辩说,如果您打包一个结构而不打包另一个,那么您就不会期望它们具有一致的布局。但是,是的,这是它可能违反的另一个标准要求。 (2认同)