繁体   English   中英

Linux内核列表实现会导致UB吗?

[英]Does Linux kernel list implementation cause UB?

先决条件:

  1. 根据C 标准,会产生无效指针的指针算术会导致未定义的行为。
  2. Linux 源代码似乎符合C 标准,希望与大多数体系结构兼容。
  3. Linux 的列表实现包含以下代码(保留格式,可能另一个问题的想法是如何使用 Stackoverflow 语法设置适当的制表宽度):
#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))
  1. 上述列表实现的典型用例是具有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)。 这种推理是合理的还是我弄错了?

还是该标准的某些部分普遍认为不值得遵循?

编译内核时所做的附加断言。 这些实际上到处都在使用。

  1. 一个指针可能加载了一个未分配的地址。 您可以在每个系统调用条目上看到这一点。 处理此类指针时必须特别小心,因为取消引用它们可能比崩溃更糟糕。

  2. 不保证取消引用 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从技术上讲是一个无效的指针。 只要poshead指向同一个平面地址空间,Linux 内核就会设法摆脱这种规则的扭曲。

替代方法是#include <linux/list.h>根本不提供list_for_each_entry(pos, head, member)宏,并且代码使用list_for_each(pos, head)list_entry(ptr, type, member)宏(其中posstruct list_head *ptrtype * ),但这通常需要代码中的额外变量。

你是对的,这是未定义的行为。 根据 Richard Biener 的说法,这种未定义的行为不受-fno-strict-aliasing支持/定义 (Clang 也将其视为未定义。)

这种特殊的编译错误是在Open vSwitch中观察到的,但它的列表宏显然是在内核之后建模/复制的。 为什么内核会摆脱它?

  • 它是用-fno-strict-aliasing构建的(尽管这对这种特殊情况没有帮助)。
  • 内核很少使用 LTO 构建。
  • 内核内核不使用堆栈上的列表头。
  • 编译器不识别内核分配函数。

因此,编译器不会观察到无效/不可能的对象引用,也不会基于此进行优化。

暂无
暂无

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

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