【问题标题】:Is gcc's __attribute__((packed)) / #pragma pack unsafe?gcc 的 __attribute__((packed)) / #pragma pack 不安全吗?
【发布时间】:2012-01-24 00:03:31
【问题描述】:

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

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

引用 gcc 文档:

`packed' 属性指定一个变量或结构字段 应该有最小可能的对齐 - 一个变量的字节, 和一个字段,除非你指定一个更大的值 `aligned' 属性。

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

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

【问题讨论】:

  • gcc 错误报告现在被标记为已修复,并在指针分配上添加了警告(以及禁用警告的选项)。详情见my answer

标签: c gcc pragma-pack


【解决方案1】:

是的,__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 成员正确对齐。使用两个 @987654330 的数组@ 对象,至少一个或另一个将有一个未对齐的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 选项,默认启用。

当获取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;
      |               ^~~~~~~~~

【讨论】:

  • ARM 上未对齐的结构元素会做一些奇怪的事情:一些访问会导致错误,另一些会导致检索到的数据反直觉地重新排列或合并相邻的意外数据。
  • 看起来packing本身是安全的,但是packed成员的使用方式可能是不安全的。较旧的基于 ARM 的 CPU 也不支持未对齐的内存访问,较新的版本支持,但我知道 Symbian OS 在这些较新版本上运行时仍然不允许未对齐的访问(支持已关闭)。
  • 在 gcc 中修复它的另一种方法是使用类型系统:要求指向压缩结构成员的指针只能分配给本身标记为压缩(即可能未对齐)的指针。但实际上:打包结构,说不。
  • @SF.: 当然,并且 gcc 在按名称访问未对齐的成员时已经这样做了(或等效的)。但是示例程序将未对齐的int 成员的地址存储在int* 指针中。当指针被取消引用时,编译器很难检测到未对齐,除非它对每个取消引用添加检查,这会减慢不使用压缩结构的代码。
  • @SF.:考虑一个带有int* 参数的外部函数。该函数已经并且应该没有意识到它可能会收到未对齐的指针。最简单的解决方案是将打包结构的成员视为位字段,不允许获取它们的地址(这也意味着打包结构的数组成员无法被索引)。或者 gcc 可以提供一种新的指针类型,与int* 不兼容,它可能指向未对齐的对象; &amp;obj.x 会产生一个未对齐的指针,它不能直接传递给期望 int* 的东西。
【解决方案2】:

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

也许我们可以责怪编译器的自满。虽然 gcc 确实有一个 -Wcast-align 选项,但默认情况下并没有启用它,-Wall-Wextra 也没有启用它。这显然是因为 gcc 开发人员认为这种类型的代码是脑死亡的“abomination”,不值得解决——可以理解的鄙视,但当一个没有经验的程序员陷入困境时,它也无济于事。

考虑以下几点:

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-valuecd 都是正常的ints。读取a.i 时,编译器会生成未对齐访问的代码。当您阅读b-&gt;i 时,b 的类型仍然知道它已打包,所以它们也没有问题。 e 是指向一个字节对齐的 int 的指针,因此编译器也知道如何正确取消引用。但是当您进行赋值f = &amp;a.i 时,您将未对齐的 int 指针的值存储在对齐的 int 指针变量中——这就是您出错的地方。我同意,gcc 应该通过 default 启用此警告(即使在 -Wall-Wextra 中也不启用)。

【讨论】:

  • +1 用于解释如何使用未对齐结构的指针!
  • @Soumya 感谢您的积分! :) 但请记住,__attribute__((aligned(1))) 是 gcc 扩展,不可移植。据我所知,在 C 中进行非对齐访问(使用任何编译器/硬件组合)的唯一真正可移植的方法是使用逐字节的内存副本(memcpy 或类似的)。一些硬件甚至没有未对齐访问的说明。我的专长是 arm 和 x86 两者都可以,尽管未对齐的访问速度较慢。因此,如果您需要以高性能执行此操作,则需要嗅探硬件并使用特定于架构的技巧。
  • @Soumya 遗憾的是,__attribute__((aligned(x))) 现在在用于指针时似乎被忽略了。 :( 我还没有完整的细节,但是使用__builtin_assume_aligned(ptr, align) 似乎可以让 gcc 生成正确的代码。当我得到更简洁的答案(希望是错误报告)时,我会更新我的答案。
  • @DanielSantos:我使用的质量编译器(Keil)识别指针的“打包”限定符;如果一个结构被声明为“打包”,则获取uint32_t 成员的地址将产生一个uint32_t packed*;试图从例如这样的指针中读取Cortex-M0 将 IIRC 调用一个子例程,如果指针未对齐,则该子例程将花费 ~7x 的正常读取时间,如果指针对齐,则将花费 ~3x 的时间,但在任何一种情况下都会表现出可预测的 [in-line code 将花费 5x 作为长,无论对齐还是不对齐]。
【解决方案3】:

只要您始终通过.(点)或-&gt; 表示法通过结构访问值,这是非常安全的。

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

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

【讨论】:

  • 嗯,我想知道如果你将一个打包结构放入另一个打包结构中,对齐方式会有所不同,会发生什么?有趣的问题,但它不应该改变答案。
  • GCC 也不总是对齐结构本身。例如: struct foo { int x;字符 c; } __attribute__((packed));结构栏 { 字符 c;结构 foo f; };我发现 bar::f::x 不一定是对齐的,至少在某些 MIPS 风格上是这样。
  • @antonm:是的,打包结构中的结构很可能是未对齐的,但是,编译器再次知道每个字段的对齐方式,只要您不这样做,它就完全安全尝试使用指向结构的指针。您应该将结构中的结构想象为一系列平坦的字段,额外的名称只是为了便于阅读。
【解决方案4】:

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

它破坏的一个特别的事情是union 的能力,它包含两个或多个结构,如果结构具有共同的初始成员序列,则可以写入一个成员并读取另一个成员。 C11 standard 第 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)) 被引入时,它打破了这一点。以下示例在 Ubuntu 16.04 x64 上运行,使用 gcc 5.4.0 并禁用优化:

#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 s1struct s2 有一个“共同的初始序列”,但应用于前者的打包意味着相应的成员不在相同的字节偏移处。结果是写入成员 x.b 的值与从成员 y.b 读取的值不同,尽管标准规定它们应该相同。

【讨论】:

  • 有人可能会争辩说,如果你打包其中一个结构而不是另一个,那么你就不会期望它们具有一致的布局。但是,是的,这是它可能违反的另一个标准要求。
【解决方案5】:

(下面是一个非常人为的例子来说明。)打包结构的一个主要用途是你有一个数据流(比如 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 个字节的数据,应该这样解释”;以及一个单独的结构化数据结构,你说“我想要一个包含两个整数的结构,称为 alice 和 bob,以及一个称为 carol 的浮点数,我不在乎你如何实现它”——在 C 中,这两个用例都被硬塞到了 struct 构造中。)

【讨论】:

  • 除非我遗漏了什么,否则这并不能回答问题。您认为结构包装很方便(确实如此),但您没有解决它是否安全的问题。此外,您断言未对齐读取的性能损失; x86 确实如此,但并非所有系统都如此,正如我在回答中所展示的那样。
猜你喜欢
  • 2012-08-01
  • 2016-01-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多