分析内核的入口-寻址方式
作者:陈莉君老师

在操作系统教学的过程中,让我如噎在喉就是感到那虚拟内存管理像虚无飘渺的海市。分段,分页,地址转换,虚拟内存,快表……就像云层里的星星,我们只能仰望,而无法触摸。

汤子赢96版《计算机操作系统》一书在OS/2存储器管理中,提到了Intel80386的寻址方式和其存储管理方式,虽然内容有限,但知道了GDT表(也就是段表),实实在在的页表,CR3寄存器等等这些力挺虚存管理的硬件兄弟。于是,保护模式成了我们从云端站到地上的一座灯塔,几经搜索,找到了《保护模式下80386及其编程》这本书。

依然记得硬啃这本书时的那种艰辛。段,那个出现在8086中的概念,在80386(简称IA32)中变得复杂繁琐。这不仅是因为从16位寻址到32位寻址的变化,还因为段在IA32中承载更多的东西:基地址(32位),界限(20位),属性(12位)。显然,16位的段寄存器无法容纳这总共64位的信息,于是这些信息进段表(所谓的段描述符表),而段寄存器存放的就是段号(其实就是这么简单,尽管英文中把段寄存器存放的内容称为选择符selector)。

IA32上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在IA32上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让Linux具有更好的可移植性,我们需要去掉段机制而只使用分页机制。

但不幸的是,IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。万般无奈之下,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量 < 4GB”,所以偏移量的范围为0HFFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。

另外,由于IA32段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB的段描述符。不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级别3,根据IA32的段保护机制规定,特权级3的程序是无法访问特权级为0的段的,所以Linux必须为内核和用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。

如果这么定义段,则段保护的第一个作用就失去了,因为这些段使用完全相同的线性地址空间(04GB),它们互相覆盖。可以设想,如果不使用分页的话,线性地址空间直接被映射到物理空间,则你修改任何一个段的数据,都会同时修改其它段的数据,段机制所提供的通过“基地址:界限”方式本来将线性地址空间分割,以让段与段之间完全隔离,这种实现段保护的方式根本就不起作用了。那么,这是不是意味着用户可以随意修改内核数据?显然不是的,这是因为,一方面用户段和内核段具有不同的特权级别,另一方面,Linux之所以这么定义段,正是为了实现一个纯的分页,而分页机制会提供给我们所需要的保护。