0x01 前言

在很多编程语言中,都原生支持断言,比如c和rust中的assert宏,java中的assert关键字等。

而另外一些编程语言,比如go,它们虽然没有对断言的原生支持,但却有很多第三方库可以实现这个功能。

在linux内核中,其实也有类似实现断言的方式,它们就是 BUG_ON 以及 WARN_ON 等一系列宏。

这篇文章主要就是讲,linux内核是如何通过这些宏,来实现类似断言功能的。

为了便于理解下文,我画了一张这些宏内部运行机制的全景图:

下面,我们就根据这张图,来详细看下 BUG_ON 和 WARN_ON 等宏的内部实现。

0x02 BUILD_BUG_ON

在讲 BUG_ON 和 WARN_ON 之前,我们先来看下 BUILD_BUG_ON 宏。

BUILD_BUG_ON 在功能上和 BUG_ON 类似,只不过它应用于编译期。

看上图中的 x86_64_start_kernel 函数,它就是大量使用了 BUILD_BUG_ON,在编译期检查各种条件是否符合预期。

当编译器编译这个函数时,如果检测到 BUILD_BUG_ON 里的条件成立,编译器就会报错。

注意,这里是条件成立会报错,而各种编程语言中的assert,是条件不成立报错,它们是相反的。

因为 BUILD_BUG_ON 只是编译期检查,它没有运行时逻辑,所以在这里我们就不继续深究其内部实现了。

0x03 BUG_ON 和 WARN_ON

从文章最开始那张全景图可以看到,类似 BUG_ON 和 WARN_ON 的宏有很多,它们分别有不同的使用场景。

但因为它们的内部机制基本类似,所以这篇文章只讲 BUG_ON 和 WARN_ON 这两个宏。

在linux内核运行过程中,如果检测到 BUG_ON 里的条件成立,则会输出一些错误日志,然后杀掉当前进程。

而如果是检测到 WARN_ON 里的条件成立,则只会输出一些警告日志,然后继续执行。

下面,我们就分别从编译期,以及运行时,来讲 BUG_ON 和 WARN_ON 是如何实现类似断言功能的。

0x04 BUG_ON 和 WARN_ON 的编译期

在编译期,BUG_ON 和 WARN_ON 宏会展开成如下代码:

其中 UD2 是一条汇编指令,它的作用就是触发一个 invalid opcode 异常。

BUG_ON 和 WARN_ON 除了生成以上代码外,还会在 __bug_table 这个section里生成一条记录,这个记录里包含了很多信息,比如,UD2这条汇编指令的地址,它所处文件,所处行,以及各种flags。

这些信息构成了一个 struct bug_entry 结构体。

所以,我们其实可以把 __bug_table 这个section看成一个数组,而数组的元素类型,就是 struct bug_entry 结构体。

上图就是最终构建内核的所有section,其中选中行,就是上面讲的 __bug_table。

BUG_ON 和 WARN_ON 最主要的区别,就是 WARN_ON 生成的 struct bug_entry 结构体,它的flags字段的 BUGFLAG_WARNING 位被设置为了1。

内核在运行时,就是通过检查 BUGFLAG_WARNING 位,来区分是要产生 BUG 还是 WARN 的。

这个在文章最开始的图里,也有画出来。

以上就是 BUG_ON 和 WARN_ON 在编译期的实现。

0x05 BUG_ON 和 WARN_ON 的运行时

在运行时,如果内核检测到 BUG_ON 和 WARN_ON 里的条件是成立的,则会执行UD2汇编指令。

这个汇编指令会触发一个 invalid opcode 异常。

这个异常会使cpu跳转到对应的异常处理函数 exc_invalid_op,然后开始执行这个函数里的逻辑。

exc_invalid_op函数会先尝试使用 handle_bug 来处理这个异常。

如果产生这个异常的汇编指令地址,对应于一个 struct bug_entry 结构体,即 report_bug 里的 find_bug 函数可以从 __bug_table 这个section里找到一个这样的结构体,则说明这个异常是由 BUG_ON 或者 WARN_ON 等宏触发的。

如果是这样的话,则继续检查 struct bug_entry 里的flags字段,看是否有 BUGFLAG_WARNING 标志,如果有,则表示这个异常是由 WARN_ON 触发,如果没有,则表示这个异常是由 BUG_ON 触发。

如果这个异常是由 WARN_ON 触发,report_bug里就会调用__warn函数,来输出各种警告日志,然后返回 BUG_TRAP_TYPE_WARN,表示这个异常是个warn类型。

如果这个异常是由 BUG_ON 触发,report_bug里就会输出有关这个异常的各种信息,然后返回 BUG_TRAP_TYPE_BUG,表示这个异常是个bug类型。

在handle_bug函数里,如果检测到report_bug返回的是 BUG_TRAP_TYPE_WARN,那就说明这个异常是个warn类型,且report_bug里已经处理过这个异常了,它就返回true,告诉exc_invalid_op函数,这个异常已经处理过了,不用再继续处理了。

如果handle_bug函数检测到report_bug返回的不是 BUG_TRAP_TYPE_WARN,那就说明这个异常没有处理过,所以它返回false,告诉exc_invalid_op函数,要继续处理这个异常。

注意,在handle_bug函数返回true之前,它还执行了 regs->ip += LEN_UD2 这行代码。

这行代码的作用,就是当cpu退出异常处理函数,返回到触发异常的代码继续执行时,要跳过UD2这个汇编指令,继续执行UD2后面的代码。

这也是 WARN_ON 触发异常的处理逻辑,即在异常处理函数中输出警告日志后,退回到原代码,并跳过UD2汇编指令,继续往下执行。

UD2汇编指令触发异常时,regs->ip里存放的,就是UD2汇编指令所在的内存地址。

如果handle_bug函数在返回true之前,没有修改regs->ip,来跳过UD2汇编指令,那当exc_invalid_op这个异常处理函数退出后,cpu执行的还是UD2这个汇编指令,这样就又会产生一个 invalid opcode 异常。

如此,就会造成死循环。

以上这些都是linux内核异常处理的各种细节,这里我们只要知道大致流程就好了,就不展开讲了。

如果大家有兴趣的话,后面我可以单独写一篇文章,来详细讲。

如果handle_bug不能处理这个异常,也就是说它返回的是false,exc_invalid_op就会调用handle_invalid_op函数,以更通用方式来处理这个异常。

对于 BUG_ON 产生的异常,就是使用这种方式来处理的。

处理的结果也非常简单,就是通过层层函数调用,最终使用do_exit函数,把产生异常的进程杀掉。

但是,如果要被杀掉的进程是一个非常重要的系统进程,比如idle进程,或者init进程,因为杀掉这些进程本身就会使整个系统不可用,所以在这种情况下,handle_invalid_op函数就会直接panic,直接使整个系统挂掉。

以上就是 BUG_ON 和 WARN_ON 的运行时实现,大家可以结合文章最开始的全景图,理解这部分的内容。

0x06 BUG_ON 实验

接下来我们就通过一些实验,来看下BUG_ON的运行时影响。

在init/main.c文件中的kernel_init函数的1492行,添加如下BUG_ON:

该函数的逻辑,是运行在init进程里的,init进程也就是pid为1的进程。

在此处加 BUG_ON,预期的效果是内核会尝试杀掉init进程。

构建内核并运行:

上图就是新添加的 BUG_ON 触发的异常日志。

看图中的第二行,它就是说,在 init/main.c:1492 位置,产生了一个kernel bug,而这个位置,正是我们刚添加的BUG_ON代码的位置。

在处理 BUG_ON 异常时,正常情况下是杀掉触发该异常的进程,但因为此次触发异常的进程是init,它是一个比较特殊的进程,所以内核就直接panic了,也就是说,整个系统就直接挂掉了。

这个panic,可以在上图的最后几行日志看到。

既然在特殊进程里添加 BUG_ON 会导致整个系统panic,那我们就挑一个非特殊进程,再添加 BUG_ON 看下效果:

上图就是在oom_reaper进程里添加的BUG_ON,我们运行这个内核看下:

这次我们可以看到,oom_reaper进程被杀死了,且因为这个进程并不是特殊进程,所以并没有发生panic,内核是启动成功了的。

我们可以用ps命令再确认下,检查该系统上确实没有了oom_reaper进程:

而如果我们没有添加上面的BUG_ON语句,运行内核时,oom_reaper进程是有的:

0x07 WARN_ON 实验

接下来我们再看一下 WARN_ON 在运行时的效果。

我们还是在init进程的同样位置添加 WARN_ON:

运行内核看下:

这次我们可以看到,它只输出了警告日志,没有尝试杀掉init进程,系统也没有发生panic,内核是正常运行的。

0x08 注意事项

虽然在内核里,我们可以看到很多地方还在使用BUG_ON或BUG,但因为它们的破坏性,这些宏已经被禁用了。

相关解释可以看内核文档 BUG() and BUG_ON()Do not crash the kernel

在需要使用BUG_ON的地方,我们可以把它替换为WARN_ON,并尽可能的处理这个BUG。

另外,我们可以通过内核参数panic_on_warn,使WARN_ON在输出完警告日志后,系统直接panic。

0x09 其他

如果有对linux及linux内核感兴趣的,可以扫描右侧二维码添加我的微信。

另外,我开设了一门 linux内核启动流程源码分析 课程,有对内核源码感兴趣的,欢迎报名。