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

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

引用gcc文件:

"packed"属性指定变量或 struct 字段

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

但是有没有这样做不安全的情况呢?编译器是否总是生成正确(虽然较慢)的代码来访问打包 struct 的未对齐成员?它是否有可能在所有情况下都这样做呢?

推荐答案

是的,__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 Solaris9上,它产生以下结果:

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

(使用单个 struct 而不是数组的程序不会可靠地出现该问题,因为编译器可以在奇数地址上分配 struct ,从而使x成员正确对齐.对于包含两个struct foo个对象的数组,至少有一个或另一个将具有未对齐的x成员.)

(在本例中,p0指向未对齐的地址,因为它指向char成员之后的压缩int成员.p1恰好正确对齐,因为它指向数组第二个元素中的同一个成员,所以它前面有两个char对象——在SPARC Solaris上,数组arr似乎分配在一个偶数,但不是4的倍数.)

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

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

我认为,在gcc中解决这个问题是不切实际的.一般的解决方案要求,每次try 取消对任何类型的指针的引用时,都需要(A)在编译时证明指针没有指向压缩 struct 的未对齐成员,或者(b)生成更大、更慢的代码,可以处理对齐或未对齐的对象.

我已经提交了一份gcc bug report美元的申请.正如我所说的,我不认为修复它是切实可行的,但文档应该提到它(目前没有).

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

当获取 struct 或联合的打包成员的地址时,它可能

我刚刚从源头上构建了那个版本的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++相关问答推荐

单指针和空参数列表之间的函数指针兼容性

如何正确地索引C中的 struct 指针数组?

我应该如何解决我自己为iOS编译的xmlsec1库的问题?转换Ctx.first在xmlSecTransformCtxPrepare()之后为空

在C语言中,在数学运算过程中,为什么浮点数在变量中的行为不同

_泛型控制表达式涉及数组碰撞警告的L值转换错误?

C在声明带有值的数组时,声明大小有用吗?

我的程序在收到SIGUSR1信号以从PAUSE()继续程序时总是崩溃()

如何读取文件并将内容保存在字符串中?(在C语言中,没有崩溃或核心转储错误)

Wcstok导致分段故障

我在C程序的Flex/Bison中遇到语法错误

为什么我在C代码中得到一个不完整的类型?

从不兼容的指针类型返回&&警告,但我看不出原因

如何在C中定义指向函数的指针并将该指针赋给函数?

为什么会导致分段故障?(C语言中的一个程序,统计文件中某个单词的出现次数)

用C++初始化局部数组变量

在哪里可以找到叮当返回码的含义?

DennisM.Ritchie的C编程语言一书中关于二进制搜索的代码出现错误?

将指针的地址加载到寄存器内联拇指组件中

GnuCobol 使用 double 类型的参数调用 C 函数

使用邻接表创建图