特权级是保护模式中的一个重要概念。i386的分段机制中定义了4个特权级。分别使用数字0、1、2和3代表。数字越小,特权级越高,代码的权限越高,能使用的CPU指令就越多。
如图2-12所示,CPU的设计者希望核心的代码和数据被放到较高的特权级中,比如操作系统内核的代码最好运行在0号特权级。而操作系统服务,比如文件、网络服务等则运行在1号或者2号特权级。应用程序则运行在3号特权级。
图2-12 CPU的特权级
处理器使用这样的机制来确保低特权级的任务不能随意访问高特权级的代码和数据。如果处理器检测到一个访问请求是不合法的,将会产生保护错误(#GP)。后面的章节会逐步实现CPU的错误处理,这里就不再详细介绍了。
设想一下,如果每个进程都有显示器、IDT、页表等核心资源的权限访问,那么必然带来巨大的安全风险,所以这些被内核管理的资源一定要放在高特权级中。但同时,操作系统也要提供一些接口给应用程序,让应用程序可以向显示器上打印数据,从键盘上输入数据,甚至是可以向其他进程发送信号等,这些接口就是系统调用。可见系统调用的本质就是高特权级的内核程序向低特权级的应用程序提供的,用于使用计算机资源的接口,只是这种使用必须在内核的严密管理之下,而不是任意的。这就大幅提升了计算机系统的安全性,这也是保护模式中“保护”一词的一种体现。
到目前为止,本书提到特权级的场景包括段选择子的低两位是RPL(Request Privilege Level,请求特权级)和段描述符中的DPL(Descriptor Privilege Level,描述符特权级)。
段选择子的低2位,也就是第0位和第1位代表了段的特权级,它刚好可以表示4个特权级。Linux系统只使用了0号和3号特权级,并把0号特权级称为内核态,把3号特权级称为用户态。选择子的第2位则指示这个描述符位于GDT中还是LDT中。GDT中的段描述符都是内核使用的,运行在内核态,所以指向GDT的段选择子的低3位就全部是0。因为GDT中的第0项是保留项,不能使用,所以选择子0x0是无效的。段选择子0x8代表GDT的第一项,这是内核代码段,0x10代表GDT的第二项,这是内核数据段。我们也使用一个段描述符对显存进行管理,这就是GDT中的第三项,它的段选择子是0x18,所以当需要向显存中写字符的时候,就可以使用0x18段选择子。
关于特权级,这里就先介绍这么多,后面的章节将会深入介绍特权级的转换以及特权级相关的错误处理。2.4节将通过一个实验来验证保护模式下的中断处理机制。