繁体   English   中英

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

[英]Is gcc's __attribute__((packed)) / #pragma pack unsafe?

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

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

引用gcc文档:

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

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

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

是的, __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 ,仅需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错误报告 正如我所说,我认为修复它不切实际,但是文档中应该提到它(目前还没有)。

更新 :从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;
      |               ^~~~~~~~~

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

也许我们可以责怪编译器的自满。 尽管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是指向压缩结构的指针。 表达式ai的类型(基本上)是一个1字节对齐的int l值 cd都是正常的int 读取ai ,编译器会生成未对齐访问的代码。 当您阅读b->ib的类型仍然知道它是压缩的,因此它们也没问题。 e是指向1字节对齐的int的指针,因此编译器也知道如何正确地对其取消引用。 但是,当您使赋值f = &a.i ,您f = &a.i未对齐的int指针的值存储在对齐的int指针变量中-这就是您出错的地方。 我同意,gcc应该默认情况下启用此警告(即使在-Wall-Wextra也不应该启用)。

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

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

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

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

它破坏的一件事是包含两个或多个结构的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 (unalltypes == 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 s1struct s2具有“公共初始序列”,应用于前者的打包也意味着相应的成员不在相同的字节偏移量处生存。 结果是写入成员xb的值与从成员yb读取的值不同,即使标准规定它们应该相同。

(下面是一个非常人为的示例,用来说明问题。)打包结构的主要用途之一是,您希望提供数据流(例如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个字节的数据,因此应对此进行解释”;以及一个单独的结构化数据结构,在这里您说“我想要一个包含两个int的结构,分别称为alice和bob和一个称为carol的浮点数,并且我不在乎您如何实现它” –在C中,这两个用例都被塞入struct结构中。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM