关于C#:gcc的__attribute __((打包))/ #pragma pack不安全吗?

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

C,编译出来的一个成员会躺在struct顺序在其中声明他们是可能的,与padding位元组插入构件之间,或在最后的成员,每个成员是aligned,确保正确地。

gcc提供语言扩展,__attribute__((packed))告诉的编译,其中不允许插入padding,struct misaligned成为成员。例如,如果所有的系统,通常需要int对象有4字节对齐的,因为intstruct __attribute__((packed))可以在odd offsets allocated成为成员。

quoting的gcc文件:

The `packed' attribute specifies that a variable or structure field
should have the smallest possible alignment--one byte for a variable,
and one bit for a field, unless you specify a larger value with the
`aligned' attribute.

使用这个扩展的obviously可以导致在较小的数据要求,但必须作为较慢的代码,编译代码(在一些platforms)的统一,也产生一个misaligned成员访问一个字节在一个时间。

但在任何情况下,这是unsafe哪里有?并不总是统一,也产生correct编译的代码(虽然较慢)成员的访问misaligned structs包装?它甚至有可能为它,所以在所有的情况下?


是的,是潜在的不安全__attribute__((packed))在一些系统。的症状可能不会显示在x86,它只是使这个问题更insidious x86系统上,测试不会揭示问题。(在x86硬件misaligned accesses是给予;如果你在这一点dereference int*在奇数地址的指针,它将比如果它是一个时间对准,但你会得到正确的结果。)

在其他一些制度,如SPARC,N的访问对象A misaligned intcauses a总线错误,程序崩溃。

有一个系统的访问是在misaligned盘从低阶位的地址,错误的信息,访问内存块。

考虑下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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
"
, (int)sizeof(struct foo));
    printf("offsetof(struct foo, c) = %d
"
, (int)offsetof(struct foo, c));
    printf("offsetof(struct foo, x) = %d
"
, (int)offsetof(struct foo, x));
    printf("arr[0].x = %d
"
, arr[0].x);
    printf("arr[1].x = %d
"
, arr[1].x);
    printf("p0 = %p
"
, (void*)p0);
    printf("p1 = %p
"
, (void*)p1);
    printf("*p0 = %d
"
, *p0);
    printf("*p1 = %d
"
, *p1);
    return 0;
}

在Ubuntu和GCC 4.5.2 x86,它包含以下输出:

1
2
3
4
5
6
7
8
9
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

在SPARC Solaris 9和GCC 4.5.1,副车架的下面的信息:

1
2
3
4
5
6
7
8
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

这一程序(使用单一阵列结构还比不reliably文化问题,因为编译器可以分配在奇数地址的结构是在这样一个x对准。一个二维阵列的struct foo对象,至少有一个或另一misaligned x)会员。

(本案例中,A misaligned p0点的地址,因为它intA点以下A char填充成员成员。p1发生正确的对准同一个点,因为它在第二个数组的元素,有两char对象preceding IT在Solaris和SPARC的数组arr似乎是分配的地址,是在平衡,但不多(A)4。

当一个成员xstruct foored的名字,编译器知道x是潜在的misaligned生成额外的代码,和要访问正确的信息。

一旦arr[0].x或地址指针存储在一arr[1].x已编译的对象,也不知道它的运行程序。misaligned intA点的对象。它只是在它的assumes对准,或(在一些系统误差)或其他类似的总线故障。

这是固定在一个海湾,impractical相信。一般的解决方案是需要的,对于每个dereference指向任何类型的一个非平凡的对准要求。(A)试验,在编译时指针不到一个点misaligned of a填充结构,或(b)和生成的代码,可以将bulkier对准或是misaligned对象句柄。

我已经提交bug报告A gcc。当我说,我不相信它是实际的固定文档应该提到它,但它(它目前没有)。

更新:截至2018年12 20,这是固定的标记为错误。GCC的补丁要出现在9与一-Waddress-of-packed-member添加新选项,默认启用。

When address of packed member of struct or union is taken, it may
result in an unaligned pointer value. This patch adds
-Waddress-of-packed-member to check alignment at pointer assignment and warn unaligned address as well as unaligned pointer

我刚刚建立,从源版本的GCC。在上面的程序,它包含的主要诊断:

1
2
3
4
5
6
7
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;
      |               ^~~~~~~~~


如AMS上面所说,不要将指针指向打包的结构的成员。这只是玩火。当你说__attribute__((__packed__))#pragma pack(1)的时候,你真正说的是"嘿,gcc,我真的知道我在做什么。"当你不这样做时,你不能责怪编译器。

也许我们可以把它的自满归咎于编译器。虽然GCC有一个-Wcast-align选项,但它没有默认启用,也没有使用-Wall-Wextra启用。这显然是由于GCC的开发人员认为这种类型的代码是一种死脑筋的"可憎的"不值得寻址——可以理解的轻蔑,但是当一个没有经验的程序员闯入它时,它没有帮助。

考虑以下内容:

1
2
3
4
5
6
7
8
9
10
11
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值。cd都是正常的ints。当读取a.i时,编译器生成用于未对齐访问的代码。当你读到b->i时,b的类型仍然知道它是打包的,所以也没问题。e是指向单字节对齐int的指针,因此编译器也知道如何正确地取消引用。但是,在执行f = &a.i赋值时,您将把未对齐的int指针的值存储在对齐的int指针变量中——这就是您出错的地方。我同意,GCC应该默认启用此警告(即使在-Wall-Wextra中也不启用)。


它是完全安全的只要你总是访问值,通过结构,以.->(点)或符号。

什么是不安全的指针数据是以大学unaligned然后访问它,以是没有考虑到。

因此,即使是在每个项目的unaligned结构已知,它是已知的unaligned A的方式,因此,结构作为一个整体必须对准为编译器将expects或有故障(或在一些平台上,如果一个新的方式是创造未来。unaligned accesses优化)。


使用此属性绝对不安全。

它破坏的一个特殊的东西是union的能力,它包含两个或多个结构,如果结构有一个共同的初始成员序列,则可以写入一个成员并读取另一个成员。C11标准第6.5.2.3节规定:

6 One special guarantee is made in order to simplify the use of unions:
if a union contains several structures that share a common
initial sequence (see below), and if the union object
currently contains one of these structures, it is permitted
to inspect the common initial part of any of them anywhere that a
declaration of the completed type of the union is visible. Tw o
structures share a common initial sequence if corresponding
members have compatible types (and, for bit-fields, the same widths)
for a sequence of one or more initial members.

...

9 EXAMPLE 3 The following is a valid fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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上运行,禁用了优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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
"
, sizeof(struct s1), sizeof(struct s2));
    printf("s.y.a=%hx, s.y.b=%x
"
, s.y.a, s.y.b);
    return 0;
}

输出:

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

虽然struct s1struct s2有一个"公共初始序列",但对前者应用的封装意味着对应的成员不在相同的字节偏移量上。结果是写入成员x.b的值与从成员y.b读取的值不同,即使标准规定它们应该相同。


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

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

那么我可以申报类似的东西

1
2
3
4
5
6
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字节,应该这样解释";以及一个单独的结构化数据构造,其中您说"我想要一个包含两个in t的结构,称为alice和bob,以及一个名为carol的float,我不在乎"如何实现它"——在C中,这两个用例都被shoehorned到struct结构中。)