GCC的__attribute __((__ packed__))是否保留原始排序?
目的
我正在用C编写一个网络程序(特别是gnu89
),我想通过将某个struct X
重新解释为大字节数组(也就是char
),通过网络发送字节,并将它们重新解释为struct X
来简化事情。另一边。 为此我决定使用gcc的__attribute __((__ packed__))。 我已尽力确保正确完成(即我已经考虑了字节序和其他相关问题)。
题
除了保证struct X
尽可能小,gcc是否保证使用__attribute __((__ packed__))定义的struct
保留原始排序? 我已经做了相当多的搜索,我还没有找到关于这种保证是否存在的任何文件。
笔记
可以安全地假设发送方和接收方都不会遇到可移植性问题(例如服务器上的sizeof(int)
等于客户端上的sizeof(int)
)。
假设您在询问结构成员是否将保留其定义中指定的顺序,答案是肯定的。 标准要求后续成员的地址不断增加:
第§6.7.2.1p13节:
在结构对象中,非位字段成员和位字所在的单元具有按声明顺序增加的地址。
并且packed属性的文档明确指出只有填充/对齐受到影响:
packed属性指定变量或结构字段应具有最小可能的对齐 – 变量的一个字节和字段的一个位,除非您使用aligned属性指定更大的值。
是的, __attribute__((packed))
(不需要第二组下划线)是实现二进制(即非文本)网络协议的正确方法。 元素之间不会有差距。
但是你应该明白, packed
不仅包装结构,而且:
- 使其所需的对齐一个字节,和
- 确保其成员可能因包装错误以及结构本身缺乏对齐要求而被正确读取和写入,即其字段的错位由编译器在软件中处理 。
但是 ,如果直接访问struct成员,编译器将只处理错位。 您永远不应该指向打包结构的成员 (除非您知道成员的必需对齐为1,例如char或其他压缩结构)。 以下C代码演示了此问题:
#include #include #include struct packet { uint8_t x; uint32_t y; } __attribute__((packed)); int main () { uint8_t bytes[5] = {1, 0, 0, 0, 2}; struct packet *p = (struct packet *)bytes; // compiler handles misalignment because it knows that // "struct packet" is packed printf("y=%"PRIX32", ", ntohl(p->y)); // compiler does not handle misalignment - py does not inherit // the packed attribute uint32_t *py = &p->y; printf("*py=%"PRIX32"\n", ntohl(*py)); return 0; }
在x86系统上(不强制执行内存访问对齐),这将产生
y=2, *py=2
正如所料。 另一方面,在我的ARM Linux板上,它产生了看似错误的结果
y=2, *py=1
我们经常使用这种技术在字节数组和结构之间转换消息,并且从未遇到过它的问题。 您可能必须自己执行字节序转换,但字段顺序不是问题。 如果您对数据类型大小有任何疑虑,可以始终指定字段大小,如下所示:
struct foo { short someField : 16 __attribute__ ((packed)); };
这可以保证someField将存储为16位,并且不会重新排列或更改以适应字节边界。
是。
但是,使用__attribute__((__packed__))
并不是一个很好的方法来做你正在做的事情。
- 它不解决字节顺序问题
- 访问该结构将是缓慢的
- 尽管gcc的普及导致了其他编译器经常实现gcc扩展的情况,但使用这种特定于编译器的扩展意味着您没有符合C的程序。 这意味着如果另一个编译器甚至未来的gcc改变了包装或者根本没有实现它,那么你运气不好,甚至不能向任何人抱怨。 Gcc可能会在明天放弃它,仍然是C99编译器。 (好吧,确切的事情是不太可能的。)我们大多数人都试图编写符合规范的程序,不是因为我们对使用供应商软件有一些抽象的敌意,或者想要一些代码纯度的学术标准,而是因为我们知道只有符合要求的语言特征才有精确和通用的规范,所以如果我们这样做,依赖我们的代码每天做正确的事情和系统到系统要容易得多。
- 你正在重新发明轮子; 此问题已经以符合标准的方式解决:请参阅YAML , XML和JSON 。
- 如果它是YAML,XML和JSON不可用的低级协议,你真的应该采用单独的基本类型,将主机版本的hton?()和ntoh?()以及memcpy()应用于输出缓冲。 我意识到直接从结构中读取和写入有着悠久的传统,但是当它从32位环境转移到64位环境时,我也花了很长时间修复代码……
根据您的尝试,我强烈建议您也使用stdint.h中的固定大小的数据类型(即uint32_t,int16_t等)。 使用固定大小的数据类型将阻止您执行以下操作:
struct name { short field : 8; };
是的,C保证不会重新排序struct元素。 (可能有扩展或花哨的优化系统可能会改变这种情况,但默认情况下不会改变它。)