这节我们讨论linux是如何利用x86结构中的段机制的,更确切的说是如何绕过linux的段机制的。
我们决定从linux的可移植性开始讨论。我们说linux是一个广泛移植的操作移动,它支持x86,Alpha,arm等多种体系结构。但是很多的结构其实都是不支持段机制的,比如arm,Alpha等,但是他们都支持分页机制。linux为了能移植到x86上,做了不少工作。
首先我们说,x86是肯定有段机制的,那么我们要在x86上运行程序,那不可避免要用到段机制。于是我们想到我们先前所想到的段描述符中有一个表示以字节为单位还是以页为单位表示一个段长度的属性位。我们当时说,当G=1时表示以页(4KB)为单位,那么一个段最大长度能到4GB。根据这一点,我们把一个段的段基址固定设置为0,然后让G=1,于是我们一个段的最大长度就是4GB了,呐,这个很显然就能和我们4GB的线性地址空间一一映射了。通过这样的处理,我们说现在x86的段机制已经形同虚设了,逻辑地址和线性地址可以混为一谈了。
但是x86还规定说,必须为代码段和数据段创建不同的段,所以linux为代码段和数据段分别创建了一个基地址为0,段长度为4GB的段描述符。不仅如此,由于linux内核运行在特权级0,用户程序运行在特权级3,x86规定说特权级为3的用户程序是不能访问特权级为0的内核代码的,所以linux又分别为内核和用户程序分别创建代码段和数据段。
于是在arch/x86/include/asm/segment.h中这样定义四个段(即在机器启动过程中段寄存器中放的值):
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS*8) #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS*8) #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS*8+3) #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS*8+3)
其中:
#define GDT_ENTRY_DEFAULT_USER_CS 14 #define GDT_ENTRY_DEFAULT_USER_DS 15 #define GDT_ENTRY_KERNEL_BASE (12) #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE+0) #define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE+1)
于是上边的定义的结果是下边这样:
#define __KERNEL_CS 0x00C0 /*内核代码段,index=12,TI=0,RPL=0*/ #define __KERNEL_DS 0x00D0 /*内核代码段,index=13,TI=0,RPL=0*/ #define __USER_DS 0x00E3 /*用户代码段,index=14,TI=0,RPL=3*/ #define __USER_CS 0x00F3 /*用户代码段,index=15,TI=0,RPL=3*/
于是我们可以用12,13,14,15四个索引来找到我们四个段所对应的段描述符,并且我们把内核代码段的特权声明为0,用户代码段的特权为3,TI为0表示我们总是访问全局描述符表。
在我们对应的段描述符中我们把G设置为1,段上限规定为0xfffff,就巧妙的绕过了x86的段机制。
但在这里我不能忽略的一个问题就是,我们把四个段的上限全部设置为4G,那就完全破坏了段的保护,就是说,我们有可能随随便便就修改了我们的其他段的数据。所幸,我们现在还是个线性地址,所幸此时我们还没把数据装载进内存,因此,我们就有处理这个问题的办法,这就是下面要讲的分页机制了。