第2课:保护模式 声明:转载请保留: 译者:http://www./jinglexy 原作者:xiaoming.mo at skelix dot org MSN & Email: jinglexy at yahoo dot com dot cn目标 下载源程序 如前文所述,系统上电时处理器处于实模式。事实上,它还有另外一种工作模式:保护模式。skelix从磁盘启动后即进入该模式。在本课中我们进入保护模式并打印"Hello World!"。
保护模式最大的好处就是可以直接范围最大4GB的地址空间,但是经过多年的更新换代,我们的机器还没有达到4GB内存,于是引入了虚拟内存的概念,它可以使用硬盘存储空间作为内存使用。保护模式下对内存访问进行保护,它阻止用户程序对内核代码或数据的访问,应用程序的crash也不会影响到整个系统。单个进程可以访问自己独有的4GB虚拟地址空间,而不是混乱在整个内存里面使用,它是通过地址映射来实现的,即逻辑地址转换成虚拟地址的过程。更详细的内容可以参考Intel的文档。
好了,让我们结束无聊的理论知识吧,本课的目的是使我们的程序进入到保护模式。在保护模式中,我们仍然使用段(事实上,我们无法在处理器上禁用段特性),每个段可以访问单独的4GB地址空间。段转载在寄存器中,它表示一个描述符选择子,和实模式一样使用cs,ds等16位寄存器。这样说吧:一个内存段描述符寄存器 CS = 0x8,我们可以直接访问0到 我上面提到段是用选择子来表示的,这个说法可能不是很准确,实际上选择子是段描述符表的索引。这个描述符表是系统所有可以使用的段的地址和范围表的入口,一个描述符包括段起始地址,长度,类型(数据/代码/门),特权级等。为了范围到特定的内存地址,段选择子和偏移地址表示为如下形式:selector:offset,和实模式一样。例如,我们让 0x08选择子指向B8000(视频内存区域) 开始的内存范围,这样我们可以使用8:00000000来范围视频内存区域的第一个字节。在系统中存在以下几种描述符表:GDT(全局描述符表),LDT(局部描述符表),IDT(中断描述符表)。当进入到保护模式后,所有的内存范围都通过GDT或LDT。 在本课中我们使用GDT,正如它的名字“全局”,GDT可以被所有任务共享。现在我们来使用一个代码段和一个数据段。 下面是代码段/数据段描述符的格式,一个描述符是8字节长(64位): 63_______________56__55__54__53__52__51_____________48_ | 基地址(31到24位) | G |D/B| X | U | 长度(19到16位) | |_______________________________________________________|
_47__46__45__44____41______40____39_________________32_ | P | DPL | 类型
| A | 基地址(23到16位) | |_______________________________________________________|
31____________________________________________________16 |
基地址(15到0位)
| |_______________________________________________________| 16_____________________________________________________ |
长度(15到0位)
| |_______________________________________________________| 解释一下:为什么长度只有20位呢,这是因为粒度一般设置位4K,所以可以表示0到4GB大小的长度范围。 表-域说明
我们从上面看到,一个描述符保护32位基地址和20位段长界限等属性。32位基地址表示32位物理地址,是一个段的开始地址,20位长度界限表示这个段的长度。读者可能注意到2^20只能表示1M大小范围。为了访问4GB地址范围,描述符中使用了G位来表示粒度。当G位为1时,粒度为4K,这是可以访问的范围是1M * 4K,即4GB大小;如果G为为0,粒度为1字节,可以访问的范围是1M字节大小。 特权级保护是保护模式的重要概念,为了解释这个,我们来看一下描述符选择子。上面已经提到了,选择子是描述符表的一个索引: 15______________________________3___2____1___0__ |
Index |
TI | RPL
| |_______________________________________________|
应用程序特权级(PL)和 cs寄存器中的PL(即RPL)是类似的。程序在低的特权级(即PL值更高)不能访问高特权级的数据段或执行高特权级的代码段。当选择子载入到寄存器中时,处理器会检查CPL和RPL,根据这两个PL得到一个EPL(恕我直言,作者增加了一个新的概念并不明智),然后比较EPL和描述符中的DPL。当EPL的特权级更高时,才能正确访问目标段。注意,这里只是大致遵循该法则,处理器还要检测读写属性,存在位等。正如上面图所描述,选择子Index是13位的,所以最多可以索引2^13个描述符,即8096个。这只是在GDT中最多索引的描述符个数,另外每个进程都可以有自己的LDT。处理器会保留第一个GDT中的描述符,它应当被清0,不应当用作访问内存使用。
02/bootsect.s gdt: 可以看到,我们在上面定义了5个GDT描述符,但暂时只用到了第2个和第3个。第一个dummy描述符是Intel规定的,第2个是cs段(代码段)描述符,下面我们仔细分析一下这个8字节值:(红色表示cs描述符的值域)
根据上面的解释,这个段描述符描述的段从00000000地址开始,界限是FFFFF*4K,即4G的32位代码段。第3个描述符用于数据段或堆栈段,区别在于第43位,设置为0表示数据段。
02/bootsect.s gdt_48: .set CODE_SEL, 0x08
# 内核代码段选择子,二进制值是00001000,表示GDT的第2项(索引值为1) 我们将所有数据设置为固定地址,IDT表(后面课程会介绍到)是所有数据的起始部分。 .set IDT_SIZE, (256*8) # IDT 大小 我们用GDT_ADDR,而不是用bootsector.s文件中的gdt符合,是因为在进入保护模式后7c00地址将被覆盖,于是我们把系统中用到的一些表搬移到固定地址。
在skelix我们使用了5个GDT描述符,这里我们先介绍前3个,最后两个将会在后面的课程中介绍。 下载我们来看一下引导程序 .text # 我们将加载内核到地址 0x10000 我们先把内核读到0x10000这个临时地址,然后再把它搬移到0x0(进入保护模式后搬移)。这个函数讲起来有些烦,读者可以自己分析:)
cld # 将内核的前512字节移到0x0 为什么要这样做?因为内核的这个部分是load.s这个文件编译出来的(本课后面会介绍到),load.s会读取“真正的内核”到0x200处,但是在这一课,我们只准备打印"Hello
World!",除此之外什么都不做。
这种开启a20地址线的方法来自一本书:"The Undocumented PC",中文纸版是《PC技术内幕》,可惜已绝版。a20地址线通过键盘控制器一个端口使能(ibm早期这样设计),当系统启动时,该地址线是关闭的,使能它之后才能访问1MB以外的地址空间。
lgdt
gdt_48
# 加载gdt地址到寄存器中 现在我们已经进入到保护模式了,是不是简单的另你不敢相信?呵呵
我们还需要进行一个绝对地址跳转,因为解码管线中预取了16位指令,需要刷新成后面的32位指令。关于ia32的指令预取和解码管线,网络上有很多相关的文章,建议读者阅读一下相关文章。这个指令跳转到0x08描述符选择子指向的偏移0x的指令处,并开始执行,这个描述符即GDT中的第2项:内核代码段描述符。代码就是load.s的开始处,一会我们开始分析load.s这个程序。 bootsector.s中的函数: # 输入: si: LBA 地址,从0开始
popw %bx
# 读取到:es:bx .org 0x1fe,
0x90
# 填充nop指令,机器码是0x90 当我们进入到保护模式后,所有的通用寄存器和段寄存器保持原来实模式的值,代码段从特权级0开始执行。load.s文件将从地址0处开始执行。 .text
现在我们用图来清晰的描述它,引导程序被加载在00007c00,它设置栈顶在00001000,然后读取内核到00001000,然后把内核映象的前一个sector(即load.s)程序读到地址0。在load.s程序中移到内核到地址0。 图1 图2 |
| |___________________|a0000
|
| |
内核栈 |
| GDT
| | |
| IDT
| | GDT/IDT |
|___________________| 8000:系统数据 |___________________|80000 |
| | |
|
| | |
|
| | |
| 内核
| | |
|
| | |
|___________________|10000 | |
|
| | |
|
| | |
|___________________|7e00 | |
| bootsector.s
| | |
|___________________|7c00 | | |
| | |
|
| |___________________|
|
| | | |
| | | |___________________|1000 | 内核 | | stack
| | | |___________________|200 |___________________|200 | load.s | | | |___________________|0 |___________________|0
最后,我们翻开Makefile看看: AS=as
-Iinclude -I选项告诉汇编工具查找头文件的路径 内核代码段链接在0x0000 |
|
来自: astrotycoon > 《protected》