[英]Does Linux kernel list implementation cause UB?
先决条件:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
#define list_next_entry(pos, member) \
list_entry((pos)->member.next, typeof(*(pos)), member)
#define list_first_entry(ptr, type, member) \
list_entry((ptr)->next, type, member)
#define list_entry_is_head(pos, head, member) \
(&pos->member == (head))
#define list_for_each_entry(pos, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member); \
!list_entry_is_head(pos, head, member); \
pos = list_next_entry(pos, member))
struct A
类型的struct A
,其中包含struct B
类型的struct B
列表的头部。 问:假设offsetof(struct B, entry_in_list) > offsetof(struct A, list_head)
并实现了以下循环:
struct A* A_ptr = something_meaningful;
struct B* pos = NULL;
list_for_each_entry(pos, &A_ptr->list_head, entry_in_list) {
do_something();
}
然后list_next_entry(pos, member)
最后(循环退出之前)评估将扩展到:
container_of(A_ptr->list_head, struct B, entry_in_list) =
= (char*)A_ptr->list_head - offsetof(struct B, entry_in_list) =
= (char*)A_ptr + offsetof(struct A, list_head) - offsetof(struct B, entry_in_list)
,根据我们的假设,它将指向 A 结构之前的区域。 假设这个区域不包含分配的内存, container_of()
宏的结果将是一个无效的指针,从而导致 Linux 中的 UB(一般情况下为 OFC)。 这种推理是合理的还是我弄错了?
还是该标准的某些部分普遍认为不值得遵循?
编译内核时所做的附加断言。 这些实际上到处都在使用。
一个指针可能加载了一个未分配的地址。 您可以在每个系统调用条目上看到这一点。 处理此类指针时必须特别小心,因为取消引用它们可能比崩溃更糟糕。
不保证取消引用 NULL 指针会崩溃; 并且不允许编译器假设取消引用 NULL 的路径是不可达的。 (这个是在 NULL 指针优化删除安全检查之后添加的。)在某些体系结构上实际上有一些东西; 在其他架构上,它只是另一个用户模式指针。
编译器选项告诉编译器这些都是真的。 (事实上,第一个通常在平面模型中被假设为真,内核就是这样。)
传递给gcc
的标志是-fno-delete-null-pointer-checks
。 空指针优化更改参考: https : //lwn.net/Articles/342420/
正如 OP 所怀疑的那样, list_for_each_entry(pos, head, member)
宏的实现取决于 C 语言中未定义的行为,以便循环终止条件!list_entry_is_head(pos, head, member)
变为 false。
假设列表是非空的,那么在最后一次迭代之后, for
循环的第三个“前进”表达式在地址offsetof(typeof(*pos), member)
字节之前生成一个指向无效typeof(*pos)
的指针head
指向的struct list_head
。 它仍然依赖于&pos->member
比较等于head
。
虽然它取决于未定义的行为,但编译器很难确定pos
从技术上讲是一个无效的指针。 只要pos
和head
指向同一个平面地址空间,Linux 内核就会设法摆脱这种规则的扭曲。
替代方法是#include <linux/list.h>
根本不提供list_for_each_entry(pos, head, member)
宏,并且代码使用list_for_each(pos, head)
和list_entry(ptr, type, member)
宏(其中pos
是struct list_head *
而ptr
是type *
),但这通常需要代码中的额外变量。
你是对的,这是未定义的行为。 根据 Richard Biener 的说法,这种未定义的行为不受-fno-strict-aliasing
支持/定义。 (Clang 也将其视为未定义。)
这种特殊的编译错误是在Open vSwitch中观察到的,但它的列表宏显然是在内核之后建模/复制的。 为什么内核会摆脱它?
-fno-strict-aliasing
构建的(尽管这对这种特殊情况没有帮助)。因此,编译器不会观察到无效/不可能的对象引用,也不会基于此进行优化。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.