底层剖析assert断言
关于 assert 断言函数的意义和用法,请参见assert与if ,本文不再赘述。在我们自制的 OS 中,会实现两种 assert 函数,一种为内核服务,另一种为用户进程服务。本节实现内核 assert 函数。
内核 assert 函数有以下几个要点需要注意:
1) 一旦内核 assert 函数被调用,就说明此时发生了严重的错误,系统可能面临崩溃的危险,所以应该立即停止运行。如何让系统停止运行呢?你可能会想到在 assert 函数末尾加上一个 while(1)
。没错,这也是我们的做法,但这还不够!还记得吗,操作系统是由中断来驱动并发的,即使当前代码正在 while(1)
中循环,只要发生中断,执行流依旧会转移到中断程序 。如果中断目的是任务调度,那么执行流转移到任务后,因为操作系统已经出现问题,所以任务的执行也是不可靠的。因此,我们还需要关闭中断 。视频演示如下:
显然,用户进程的 assert 函数无需关闭中断,即使 A 用户程序遇上 assert 而崩溃,B 程序可还要接着运行呢。再者,用户进程也没有关闭中断的权限,因为
cli
指令是 ring0 级别才能使用的指令。
2) 发生错误后,我们需要快速定位错误源,因此还需要用到几个常见的预定义宏:__FILE__
,__FUNC__
,__LINE__
,分别指示 assert 被触发的所在文件、函数、行号。
本分支新增了 debug.h
、debug.c
、intrmgr.c
三个文件,以下对此三个文件进行分析。
intrmgr
intrmgr
为 interrupt manager
的缩写,意为管理中断:
1 |
|
-
第 29 行,
intr_get_status()
函数用于获取中断的开闭状态。第 29 行内联汇编,先使用pushf
将 EFLAGS 寄存器压栈,再将其弹出到全局变量 EFLAGS 中,这样便获取了标志寄存器的值。注意,如果想要在内联汇编修改 C 语言变量的值,则该变量必须为全局变量!!因为局部变量是不会记录在符号表中的,所以编译器根本不认识局部变量的符号。这涉及到编译原理,详情请参阅《装载,链接与库》。其实,如果理解了笔者之前的文章函数调用过程,你也会明白为什么内联汇编不认识局部变量。简单来说,局部变量在函数栈中被创建,其定位是通过 EBP 进行的,而不是通过符号(符号本身代表地址)进行的。
随后第 30 行识别中断开闭状态,并返回该状态。另外,EFLAGS_IF 定义在 interrupt.h 中:
1
-
intr_enable()
和intr_disable()
很简单,不再赘述。唯一可能的疑惑是,为什么要返回修改之前的状态?这与以后的任务调度有关,后续还会用到这些函数,我们先提前在这做好准备。 -
intr_set_status()
并不多余,它可以提升代码的灵活性,后续我们也会看到这一点。
debug.h
1 |
|
-
再次强调,assert 仅在 Debug 模式下使用,当软件发行(release)后,就需要屏蔽 assert 。因此,在非 Debug 时,定义 NDEBUG 宏,则 assert 成为空值,不参与编译;在 Debug 下,assert 被定义为第 7~10 行代码段。
指定非 Debug 有两种方式:
1)使用 GCC 的-D
参数即可定义 NDEBUG:1
gcc -DNDEBUG
2)在 debug.h 第 2 行插入
#define NDEBUG
-
第 10 行使用到了
#
号,用于字符串化,详见C语言中的#与## 。 -
宏定义是以行为单位的,跨多行需要使用
\
进行连接。 -
__FILE__
,__FUNC__
,__LINE__
是预定义宏,分别指示该行所在文件、函数、行号,这是在编译阶段就确定了的。
debug.c
1 |
|
演示如下: