c - gcc 的 __attribute__((packed))/#pragma pack 不安全吗?

标签 c gcc pragma-pack

在 C 语言中,编译器将按照声明的顺序排列结构成员,并在成员之间或最后一个成员之后插入可能的填充字节,以确保每个成员都正确对齐。

gcc 提供了一个语言扩展,__attribute__((packed)),它告诉编译器不要插入填充,允许结构成员错位。例如,如果系统通常要求所有 int 对象都具有 4 字节对齐,则 __attribute__((packed)) 可以导致 int 结构成员以奇数偏移量分配。

引用 gcc 文档:

The `packed' attribute specifies that a variable or structure field should have the smallest possible alignment--one byte for a variable, and one bit for a field, unless you specify a larger value with the `aligned' attribute.

显然,使用此扩展会导致数据需求更小但代码速度更慢,因为编译器必须(在某些平台上)生成代码以一次访问未对齐的成员一个字节。

但是在某些情况下这是不安全的吗?编译器是否总是生成正确的(虽然速度较慢)代码来访问未对齐的压缩结构成员?甚至有可能在所有情况下都这样做吗?

最佳答案

是的,__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 指向一个未对齐的地址,因为它指向紧跟在 char 成员之后的打包 int 成员。p1 正好对齐,因为它指向数组第二个元素中的同一个成员,所以它前面有两个 char 对象——在 SPARC Solaris 上数组 arr 似乎分配在一个偶数地址,但不是 4 的倍数。)

当通过名称引用 struct foo 的成员 x 时,编译器知道 x 可能未对齐,并将生成额外的正确访问它的代码。

一旦arr[0].xarr[1].x 的地址被存储在一个指针对象中,编译器和运行程序都不知道它指向一个未对齐的 int 对象。它只是假定它已正确对齐,导致(在某些系统上)出现总线错误或类似的其他故障。

我认为,在 gcc 中修复此问题是不切实际的。一个通用的解决方案需要,对于每次尝试取消引用指向具有非平凡对齐要求的任何类型的指针,或者(a)在编译时证明指针不指向打包结构的未对齐成员,或(b)生成可以处理对齐或未对齐对象的更大、更慢的代码。

我已经提交了 gcc bug report .正如我所说,我认为修复它不切实际,但文档应该提到它(目前没有)。

更新:截至 2018 年 12 月 20 日,此错误已标记为已修复。该补丁将出现在 gcc 9 中,并添加了一个默认启用的新 -Waddress-of-packed-member 选项。

When address of packed member of struct or union is taken, it may result in an unaligned pointer value. This patch adds -Waddress-of-packed-member to check alignment at pointer assignment and warn unaligned address as well as unaligned pointer

我刚刚从源代码构建了那个版本的 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;
      |               ^~~~~~~~~

关于c - gcc 的 __attribute__((packed))/#pragma pack 不安全吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43087085/

相关文章:

c++ - __attribute__((__packed__)); 之间有什么区别?和#pragma pack(1)

c++ - __attribute __((__ packed__))有什么区别;和#pragma pack(1)

c - 使用动态数组按字母顺序对单词进行排序

c - "Redefinition - Different Basic Types"在 C 中使用指针时出错

c++ - 常量 T{};作品,常量T;当 T 是非 POD 时失败,

android - arm-linux-androideabi-g++ : -fuse-linker-plugin, 但找不到 liblto_plugin.so

c - gcc 的 __attribute__((packed))/#pragma pack 不安全吗?

php - 如何从另一个 php 扩展方法调用 php 扩展方法

c - 为什么不能在 C 中编写 scanf ("%.2lf", &a) ?

c++ - sstream : No such file or directory