gcc的__attribute __((packed))/ #pragma pack是否不安全?


164

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

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

引用gcc文档:

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

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

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


1
现在,gcc错误报告已标记为已修复,并在指针分配上添加了警告(以及禁用警告的选项)。我的答案中有细节。
基思·汤普森

Answers:


148

是的,__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紧随一个char成员的压缩成员。p1碰巧正确对齐,因为它指向数组第二个元素中的同一成员,因此在char它之前有两个对象-并且在SPARC Solaris上,阵列arr分配的地址似乎是偶数,但不是4的倍数。)

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

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

我认为,在gcc中修复此问题不切实际。对于每次尝试将指针解除对具有非平凡对齐要求的类型的尝试,通用解决方案都需要(a)在编译时证明指针没有指向打包结构的未对齐成员,或者(b)生成可以处理对齐或未对齐对象的笨重且较慢的代码。

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

更新:从2018年12月20日起,此错误被标记为已修复。该补丁将在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;
      |               ^~~~~~~~~

1
可能未对齐,并会生成...什么?
Almo

5
ARM上未对齐的struct元素确实很奇怪:某些访问会导致错误,而另一些访问则会导致检索到的数据反直观地重新排列或合并相邻的意外数据。
wallyk 2011年

8
看起来包装本身是安全的,但是如何使用包装成员是不安全的。基于ARM的旧版CPU也不支持未对齐的内存访问,较新的版本也支持,但我知道Symbian OS在这些较新的版本上运行时仍不允许未对齐的访问(支持已关闭)。
詹姆斯

14
在gcc中修复它的另一种方式是使用类型系统:要求指向压缩结构成员的指针只能分配给自身标记为压缩(即可能未对齐)的指针。但实际上:打包的结构,只说不。
caf

9
@Flavius:我的主要目的是从那里获取信息。另请参阅meta.stackexchange.com/questions/17463/…–
Keith Thompson

62

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

也许我们可以责怪编译器的自满。虽然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;

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


6
+1说明如何使用未对齐结构的指针!
Soumya 2014年

@Soumya感谢您的观点!:)但是请记住,这__attribute__((aligned(1)))是gcc扩展,不能移植。据我所知,在C中进行不对齐访问的唯一真正可移植的方法(使用任何编译器/硬件组合)是按字节的内存副本(memcpy或类似的副本)。某些硬件甚至没有针对未对齐访问的说明。我的专业知识是可以同时使用arm和x86,尽管未对齐的访问速度较慢。因此,如果您需要以高性能执行此操作,则需要嗅探硬件并使用特定于Arch的技巧。
Daniel Santos

4
@Soumya不幸的是,__attribute__((aligned(x)))现在当用于指针时,它似乎被忽略了。:(我还没有这样做的全部细节,但使用__builtin_assume_aligned(ptr, align)似乎得到gcc生成正确的代码时,我更简洁的答案(希望bug报告)我会更新我的答案。
丹尼尔·桑托斯

@DanielSantos:我使用的高质量编译器(Keil)可以识别指针的“打包”限定符;如果一个结构被声明为“压缩”,则采用一个uint32_t成员的地址将产生一个uint32_t packed*;。尝试从例如Cortex-M0上的此类指针读取时,IIRC会调用一个子例程,如果指针未对齐,则该例程将花费正常读取时间的〜7倍,如果对齐则将花费〜3x的时间,但是无论哪种情况,其行为都可以预测[无论对齐还是不对齐,内联代码需要5倍的时间]。
超级猫


49

只要您始终通过.(点)或->表示法通过该结构访问值,这是绝对安全的。

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

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


嗯,我想知道如果将一个打包结构放在另一个对齐方式不同的打包结构中会发生什么情况?有趣的问题,但不应改变答案。
AMS

GCC也不会总是对齐结构本身。例如:struct foo {int x; 字符c; } __attribute __((packed)); struct bar {char c; struct foo f; }; 我发现bar :: f :: x不一定对齐,至少在某些MIPS风格上。
安东

3
@antonm:是的,打包的结构中的结构很可能是未对齐的,但是,再次,编译器知道每个字段的对齐方式,并且只要您不尝试在结构中使用指针,这是绝对安全的。您应该将一个结构中的一个结构想象成一个平面系列的字段,并使用额外的名称来表示可读性。
2012年

6

使用此属性绝对是不安全的。

它破坏的一件事是a的能力,union其中包含两个或多个结构,如果一个结构具有相同的成员初始序列,则该结构可以写入一个成员,而读取另一个。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))被引入时,它打破了这一点。以下示例是在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,即使标准规定它们应该相同。


一个人可能会争辩说,如果您打包其中一个结构而不打包另一个结构,那么您将不会期望它们具有一致的布局。但是,是的,这是它可能违反的另一个标准要求。
基思·汤普森

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字节,而不必摆弄指针算法。

现在发生对齐问题,除非在编译器将结构视为打包后(即,它以指定的顺序存储数据并使用正好16),否则在内存中将void *指针指向接收到的数据并将其强制转换为myStruct *将不起作用。字节)。对于未对齐的读取会产生性能损失,因此使用打包的结构存储程序正在积极使用的数据不一定是一个好主意。但是,当为程序提供字节列表时,打包的结构使编写访问内容的程序更容易。

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


除非我缺少任何东西,否则无法回答问题。您认为结构打包很方便(确实如此),但是您没有解决它是否安全的问题。同样,您断言对未对齐读取的性能有影响;正如我在回答中所展示的,这对于x86是正确的,但并非对所有系统都是如此。
基思·汤普森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.