是否可以保证"归零"结构的填充位将在 C 中归零?



文章中的这句话让我很尴尬:

C允许实现将填充插入到结构中(但不插入到数组中),以确保所有字段对目标具有有用的对齐方式如果你将一个结构置零,然后设置一些字段,填充位会全部为零吗根据调查结果,36%的人确信他们会,29%的人不知道。根据编译器(和优化级别)的不同,可能是也可能不是

还不完全清楚,所以我转向了标准。ISO/IEC 9899在§6.2.6.1中规定:

当一个值存储在结构或并集类型的对象中时,包括在成员对象中,与任何填充字节相对应的对象表示的字节采用未指定的值

同样在§6.7.2.1中

单元内位字段的分配顺序(从高阶到低阶或从低阶到高阶)由实现定义。未指定可寻址存储单元的对齐方式。

我刚刚记得我最近实现了某种破解,我使用了位字段拥有的字节的未声明部分。它有点像:

/* This struct is always allocated on the heap and is zeroed. */
struct some_struct {
/* initial part ... */
enum {
ONE,
TWO,
THREE,
FOUR,
} some_enum:8;
unsigned char flag:1;
unsigned char another_flag:1;
unsigned int size_of_smth;
/* ... remaining part */
};

结构不在我的支配范围内,因此我无法更改它,但我迫切需要通过它传递一些信息。所以我计算了相应字节的地址,如:

unsigned char *ptr = &some->size_of_smth - 1;
*ptr |= 0xC0; /* set flags */

后来我以同样的方式检查了标志。

我还应该提到,目标编译器和平台已经定义,所以这不是一个跨平台的事情。然而,目前的问题仍然存在:

  1. memset/kzalloc/之后以及在随后的一些使用之后,结构(堆中)的填充位仍将为零,我能相信这一事实吗?(这篇文章没有透露关于struct进一步使用的标准和保障措施的主题)。那个么像= {0}这样在堆栈上置零的结构呢?

  2. 如果是,这是否意味着我可以安全地使用位字段的"未命名"/"未声明"部分来在C中的任何地方(不同的平台、编译器等)传输一些信息?(如果我确信没有一个疯子试图在这个字节中存储任何东西)。

第一个问题的简短答案是"否"。

虽然memset()的适当调用(如memset(&some_struct_instance, 0, sizeof(some_struct)))会将结构中的所有字节设置为零,但在"使用"some_struct_instance(如设置其中的任何成员)之后,这种更改不需要持久。

因此,例如,不能保证some_struct_instance.some_enum = THREE(即将值存储到成员中)将保持some_struct_instance中的任何填充位不变。该标准中唯一的要求是结构的其他构件的值不受影响。然而,编译器可以(在发出的目标代码或机器指令中)使用一些逐位操作集来实现赋值,并被允许以不留下填充位的方式走捷径(例如,不发出以其他方式确保填充位不受影响的指令)。

更糟糕的是,像some_struct_instance = some_other_struct_instance这样的简单赋值(根据定义,它是将值存储到some_struct_instance中)并不能保证填充位的值。不能保证some_struct_instance中的填充位将被设置为与some_other_struct_instance中的填充比特相同的逐位值,也不能保证some_struct_instance中的填充位元将保持不变。这是因为编译器可以用它认为最"有效"的方式来实现赋值(例如逐字复制内存、某组成员赋值或其他方式),但由于赋值后的填充位值未指定,因此不需要确保填充位保持不变。

如果你运气好,并且篡改填充位符合你的目的,那就不是因为C标准中有任何支持。这将是因为编译器供应商的良好声誉(例如,选择发出一组机器指令,以确保填充位不会更改)。而且,实际上,不能保证编译器供应商会继续以同样的方式做事——例如,当编译器更新时,当你选择不同的优化设置时,依赖于这种东西的代码可能会中断。

既然你第一个问题的答案是"不",就没有必要回答你的第二个问题。然而,从哲学上讲,如果试图将数据存储在结构的填充位中,那么可以合理地断言其他人(无论是否疯狂)可能会尝试做同样的事情,但使用的方法会混淆您试图传递的数据。

来自标准规范的第一个字:

C允许实现将填充插入结构(但不插入数组),以确保所有字段都有有用的对齐。。。

这些词的意思是,为了优化(可能是为了速度优化,但也为了避免数据/地址总线上的架构限制),编译器可以使用隐藏的、未使用的位或字节。未使用,因为它们将被禁止或处理成本高昂。

这也意味着,从编程的角度来看,这些字节或位不应该是可见的,尝试访问这些隐藏的数据应该被视为编程错误。

关于这些添加的数据,该标准表示,它们的内容"未指明",实际上没有更好的方法来说明实现可以对它们做些什么。想想那些位字段声明,你可以声明任何位宽的整数:没有一个普通的硬件允许以小于8位的块从内存中读/写,所以CPU总是读或写至少8位(有时甚至更多)。为什么编译器(一个实现)要负责对那些程序员指定他不关心的其他位做一些有用的事情?这是没有意义的:程序员没有给某个内存地址起名字,但他想操纵它?

字段之间的填充字节与以前几乎相同:那些添加的字节是必要的,但程序员对它们不感兴趣——以后他不应该改变主意!

当然,人们可以研究一个实现,并得出一些结论,比如"填充字节将始终为零"或类似的结论。这是有风险的(你确定它们总是为零吗?)但更重要的是,这完全没有用:如果你在一个结构中需要更多的数据,只需声明它们!而且,即使将源代码移植到不同的平台或实现,也不会有任何问题。

从期望标准中列出的内容得到正确实现开始是合理的。您正在为特定的体系结构寻找进一步的保证。就我个人而言,如果我能找到关于那个特定架构的文档细节,我会放心;如果没有,我会很谨慎。

什么是"谨慎"取决于我需要有多自信。例如,构建一个详细的测试集并在我的目标架构上定期运行它会给我一个合理的信心,但这一切都取决于你想承担多大的风险。如果这真的非常重要,坚持他们的标准保证你;如果不是这样,测试一下,看看你是否能获得足够的信心来满足你的需求。

最新更新