181

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

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

引用 gcc 文档:

`packed' 属性指定变量或结构字段应该具有最小可能的对齐方式——变量一个字节,字段一个位,除非您使用 `aligned' 属性指定更大的值。

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

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

4

5 回答 5

164

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

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

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

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

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

更新:截至 2018 年 12 月 20 日,此错误被标记为已修复。该补丁将出现在 gcc 9 中,并添加了一个-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;
      |               ^~~~~~~~~
于 2011-12-19T22:29:19.083 回答
68

正如 ams 上面所说,不要将指针指向已打包的结构的成员。这简直是​​在玩火。当你说__attribute__((__packed__))or#pragma pack(1)时,你真正想说的是“嘿 gcc,我真的知道我在做什么。” 当事实证明你没有这样做时,你不能正确地责怪编译器。

也许我们可以责怪编译器的自满。虽然 gcc 确实有一个 -Wcast-align选项,但默认情况下也没有启用它,也没有使用-Wallor -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;

在这里,类型a是一个压缩结构(如上定义)。同样,b是一个指向压缩结构的指针。表达式的类型a.i(基本上)是具有 1 字节对齐 的 int l 值。c并且d都是正常int的。读取a.i时,编译器生成未对齐访问的代码。当您阅读b->i,b的类型时,仍然知道它已打包,因此它们也没有问题。 e是指向一个字节对齐的 int 的指针,因此编译器也知道如何正确取消引用。但是当你进行赋值时f = &a.i,你将一个未对齐的 int 指针的值存储在一个对齐的 int 指针变量中——这就是你出错的地方。我同意,gcc 应该启用此警告默认(甚至不在-Wallor中-Wextra)。

于 2013-04-08T04:30:03.160 回答
52

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

不安全的是获取未对齐数据的指针,然后在不考虑这一点的情况下访问它。

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

于 2011-12-20T10:53:12.703 回答
10

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

它破坏的一件事是,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)
/*
...
*/

__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;
}

输出:

sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678

即使struct s1并且struct s2有一个“共同的初始序列”,应用于前者的打包意味着相应的成员不会生活在相同的字节偏移量中。结果是写入 memberx.b的值与从 member 读取的值不同y.b,即使标准说它们应该相同。

于 2019-04-02T18:43:46.280 回答
1

(下面是一个非常人为的例子来说明。)打包结构的一个主要用途是你有一个数据流(比如 256 字节),你希望为其提供意义。如果我举一个较小的例子,假设我有一个程序在我的 Arduino 上运行,它通过串行发送一个 16 字节的数据包,其含义如下:

0: message type (1 byte)
1: target address, MSB
2: target address, LSB
3: data (chars)
...
F: checksum (1 byte)

然后我可以声明类似

typedef struct {
  uint8_t msgType;
  uint16_t targetAddr; // may have to bswap
  uint8_t data[12];
  uint8_t checksum;
} __attribute__((packed)) myStruct;

然后我可以通过 aStruct.targetAddr 引用 targetAddr 字节,而不是摆弄指针算术。

现在发生对齐的事情,将内存中的 void* 指针指向接收到的数据并将其转换为 myStruct* 将不起作用,除非编译器将结构视为已打包(即,它以指定的顺序存储数据并恰好使用 16本示例的字节数)。未对齐读取会降低性能,因此对程序正在使用的数据使用打包结构不一定是一个好主意。但是当您的程序提供一个字节列表时,打包结构使编写访问内容的程序变得更容易。

否则,您最终将使用 C++ 并编写一个带有访问器方法的类以及在幕后进行指针运算的东西。简而言之,打包结构用于有效处理打包数据,而打包数据可能是您的程序需要处理的内容。在大多数情况下,您的代码应该从结构中读取值,使用它们,并在完成后将它们写回。所有其他事情都应该在打包结构之外完成。问题的一部分是 C 试图向程序员隐藏的低级内容,以及如果这些事情对程序员真的很重要,则需要进行箍跳。(您几乎需要在语言中使用不同的“数据布局”结构,以便您可以说“这个东西长 48 个字节,foo 指的是 13 个字节的数据,应该这样解释”;以及一个单独的结构化数据结构,

于 2015-08-16T14:45:51.913 回答