FreeNOS中的boot.S中全局描述表GDT、中断描述符表IDT以及分页机制的分析(五)

先了解一下概念性的东西,后面结合源码分析。

一、CPU工作方式,实时模式与保护模式
在80286以前,CPU只有实时模式,地址总线有20位,而内存地址是16位,也就是最多能够访问 2^20 =1M 的内存空间。

在80286及以后,内存地址改为16位或32位,至少可以访问到 2^32=4G 的内存空间。但为了保证后续的CPU能够运行旧的CPU,只能保持向下兼容。因此,80286及以后的CPU首先进入实模式,然后通过切换机制再进入到保护模式。

CPU复位(reset)或加电(power on)的时候以实模式启动,处理器以实模式工作。在实模式下,内存寻址方式和8086相同,由16位段寄存器的内容乘以16(10H)当做段基地址,加上16位偏移地址形成20位的物理地址,最大寻址空间1MB,最大分段64KB。可以使用32位指令。32位的x86 CPU用做高速的8086。在实模式下,所有的段都是可以读、写和可执行的。

保护模式与实模式相比,主要是两个差别:一是提供了段间的保护机制,防止程序间胡乱访问地址带来的问题,二是访问的内存空间变大。
更详细的介绍可以去网上查找。

二、全局描述表GDT和中断描述符表IDT
(1)全局描述符表 GDT(Global Descriptor Table) 在Protected Mode下,是一个一个重要的必不可少的数据结构。

为什么要有GDT?我们首先考虑一下在Real Mode下的编程模型:
在Real Mode下,我们对一个内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是一个段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。而Offset则是相对于此Segment Base Address的偏移量。Base Address+Offset就是一个内存绝对地址。由此,我们可以看出,一个段具备两个因素:Base Address和Limit(段的最大长度),而对一个内存地址的访问,则是需要指出两点:1.使用的是哪个段 2.相对于这个段Base Address的Offset,这个Offset应该小于此段的Limit。当然对于16-bit系统,Limit不用指定,默认为最大长度64KB,而 16-bit的Offset也永远不可能大于此Limit。我们在实际编程的时候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段寄存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。

既然是这样,我们就先不去考虑页模式。对于段模式来讲,访问一个内存地址仍然使用Segment:Offset的方式,这是很自然的。由于 Protected Mode运行在32-bit系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32位所能表示的任何值(Limit则可以被设为32位所能表示的2^12的整数倍的任何值),而不象Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,Protected Mode,顾名思义,就是为段访问提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。

所以,在Protected Mode下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段寄存器装入这个段描述符。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit(尽管每个段寄存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段寄存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段寄存器来直接引用64-bit的段描述符。怎么办?解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 -bit的内容作为索引)。这个全局的数组就是GDT。

(2)中断描述符表(Interrupt Descriptor Table,IDT)将每个异常或中断向量分别与它们的处理过程联系起来。与GDT表类似,IDT也是由8字节长描述符组成的一个数组。与GDT不同的是,表中第1项可以包含描述符。为了构成IDT表中的一个索引值,处理器把异常或中断的向量号乘以8。因为最多只有256个中断或异常向量,所以IDT无需包含多于256个描述符。IDT中可以含有少于256个描述符,因为只有可能发生的异常或中断才需要描述符。不过IDT中所有空描述符项应该设置其存在位(标志)为0。IDT表可以驻留在线性地址空间的任何地方,处理器使用IDTR寄存器来定位IDT表的位置。这个寄存器中含有IDT表32位的基地址和16位的长度(限长)值。IDT表基地址应该对齐在8字节边界上以提高处理器的访问效率。限长值是以字节为单位的IDT表的长度。

三、全局描述表GDT和中断描述符表IDT描述符包含的信息

每项描述描述符长度8个字节,包含的信息如下:
(1) 全局描述表GDT描述符
在这里插入图片描述
其中:
  G 位是粒度位,用于解释段界限的含义。当 G 位是“ 0”时,段界限以字节为单位。此时,段的扩展范围是从 1 字节到 1 兆字节( 1B~1MB),因为描述符中的界限值是 20 位的。相反,如果该位是“ 1”,那么,段界限是以 4KB 为单位的。这样,段的扩展范围是从 4KB到 4GB
  S 位用于指定描述符的类型( Descriptor Type)。当该位是“ 0”时,表示是一个系统段;为“ 1”时,表示是一个代码段或者数据段(堆栈段也是特殊的数据段)。
  
  DPL 表示描述符的特权级( Descriptor Privilege Level, DPL)。这两位用于指定段的特权级。共有 4 种处理器支持的特权级别,分别是 0、 1、 2、 3,其中 0 是最高特权级别, 3 是最低特权级别。刚进入保护模式时执行的代码具有最高特权级 0(可以看成是从处理器那里继承来的),这些代码通常都是操作系统代码,因此它的特权级别最高。每当操作系统加载一个用户程序时,它通常都会指定一个稍低的特权级,比如 3 特权级。不同特权级别的程序是互相隔离的,其互访是严格限制的,而且有些处理器指令(特权指令)只能由 0 特权级的程序来执行,为的就是安全。这里再次点明了为何叫保护模式。
  
  P 是段存在位( Segment Present)。 P 位用于指示描述符所对应的段是否存在。一般来说,描述符所指示的段都位于内存中。但是,当内存空间紧张时,有可能只是建立了描述符,对应的内存空间并不存在,这时,就应当把描述符的 P 位清零,表示段并不存在。P 位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果 P 位是“ 0”,处理器就会产生一个异常中断。
  
  D/B 位是“默认的操作数大小”( Default Operation Size)或者“默认的堆栈指针大小”,又或者“上部边界”标志。设立该标志位,主要是为了能够在 32 位处理器上兼容运行 16 位保护模式的程序。D=0 表示指令中的偏移地址或者操作数是 16 位的; D=1,指示 32 位的偏移地址或者操作数。
  举个例子来说, 如果代码段描述符的 D 位是 0,那么,当处理器在这个段上执行时,将使用 16位的指令指针寄存器 IP 来取指令,否则使用 32 位的 EIP。
  对于堆栈段来说,该位被叫做“ B”位,用于在进行隐式的堆栈操作时,是使用 SP 寄存器还是ESP 寄存器。
  L 位是 64 位代码段标志,保留此位给 64 位处理器使用。目前,我们将此位置“ 0”即可。
  
  TYPE 字段共 4 位,用于指示描述符的子类型:
  在这里插入图片描述
  
(2) 中断描述符表IDT描述符
在这里插入图片描述
主要门描述符是:
· 中断门(Interrupt gate)
其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。当控制权通过中断门进入中断处理程序时,处理器清IF标志,即关中断,以避免嵌套中断的发生。中断门中的DPL(Descriptor Privilege Level)为0,因此,用户态的进程不能访问Intel的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。

· 陷阱门(Trap gate)
其类型码为111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。

四、boot.S中的全局描述符GDT表项以及中断描述符IDT表项的内容
上节(三)略过了gdt以及idt的分析,接下来继续分析。
(1)GDT——从boot.S文件找到:
在这里插入图片描述
lgdt gdtPtr——在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。
在保护模式初始化过程中必须给GDTR加载一个新值
GDTR结构:
在这里插入图片描述
即gdtPtr志向存放GDT的入口地址:
在这里插入图片描述
可以看到,gdt有六个全局描述符表项,第一项为全零。
,第二项到第五项可以将值按二进制展开,结合上图全局描述表GDT描述符定义,可归纳入下:
1).内核代码段
.quad 0x00cf9a000000ffff /
Kernel CS. /
代码段,执行可读,具有最高特权等级(0特权),描述符对应的段存在,使用32位的EIP,段界限是以4KB为单位,段的扩展范围4KB~4GB
2).内核数据段
.quad 0x00cf92000000ffff /
Kernel DS. /
数据段,读写,具有最高特权等级(0特权),描述符对应的段存在,使用32位的EIP,段界限是以4KB为单位,段的扩展范围4KB~4GB
3). 用户代码段
.quad 0x00cffa000000ffff /
User CS. /
代码段,执行可读,具有最低特权级别(3特权),描述符对应的段存在,使用32位的EIP,段界限是以4KB为单位,段的扩展范围4KB~4GB
4).用户数据段
.quad 0x00cff2000000ffff /
User DS. /
数据段,读写,具有最低特权级别(3特权),描述符对应的段存在,使用32位的EIP,段界限是以4KB为单位,段的扩展范围4KB~4GB
5).任务状态段(Task State Segment,TSS)
TSS中包含着当前执行任务的重要信息,后续再任务切换部分会继续研究。

(2)IDT
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在上述代码中,idt指向一个256*8字节的区域,被初始化为0,可以将idt看做一个数组的首地址,数组有256个元素,每个元素大小8byte
idtEntry 0, 0x8f部分是充填idt表,解释如下
在这里插入图片描述
在这里插入图片描述
对照上图,对于中断号为vnum的IDT表项分析如下:
(1) Offset 31…16 = (vnum >> 16) --------idt[8*vnum + 6] = (vnum>>16)
(2) Offset 15…0 = (vnum&0xff) -------- idt[8*vnum]=(vmum & 0xff)
(3) Segment Selector = KERNEL_CS_SEL
----------------idt[8*vnum + 2]=KERNEL_CS_SEL(0x08)
(4) 第五字节为0----------idt[8*vnum + 4] = 0
(5) 第六字节为vtype -------- idt[8*vnum + 5] = vtype
vtype =0x8f–代表Trap Gate,32bit,段存在,具有最高特权等级(0特权)
vtype =0x8e–代表Interrupt Gate, 32bit,段存在,具有最高特权等级(0特权)

综上所述,可以看到IDT表项总共有256项,初始值全部为0,后续充填结果如下:
中断号0~16:代表Trap Gate,32bit,内核代码段,段存在,具有最高特权等级(0特权)
中断号32~47以及147(0x90):代表Interrupt Gate, 32bit,内核代码段,段存在,具有最高特权等级(0特权)

在保护模式下,中断描述符表在内存的位置不再限于从地址0开始的地方,而是可以放在内存的任何地方。为此,CPU中增设了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始地址。中断描述符表寄存器IDTR是一个48位的寄存器,其低16位保存中断描述符表的大小,高32位保存IDT的基址.,如下:
在这里插入图片描述
在这里插入图片描述
上图138~142行指令将ds(数据段寄存器)、ss(堆栈段寄存器) 以及es(附加段寄存器)赋值为KERNEL_DS_SEL (0x10 ),选择内核数据段。
KERNEL_DS_SEL 与前面GDT表中表项有关系,即对应以下表项:
.quad 0x00cf92000000ffff /* Kernel DS. */

五、分页机制
上图中第144~180行出现页目录和页表项:
在这里插入图片描述
线性地址到物理地址的转换采用两级页表:
页目录大小为一页,即4KB,页表大小也为一页(4KB);
以32bit地址计算,每个表项占4个字节,那么页目录能存放1024个页表,而页表也可以存放1024个页面(大小4KB),故线性地址大小为4GB(102410244096).
在这里插入图片描述
144~149设置了页目录中的第一项对应的页表项,可读写,页表项置存在标志,该项不能被释放。
页目录中的一项可以转换4MB的物理地址(1*1024*4096----页表数量*页面大小)
150 ~ 160行充填了页目录中的第一项对应的页表,共有1024个页面,而且是从物理地址0开始转换的,结果是将页目录的第一项对应的4MB线性地址转换为物理地址0~4MB。此处为什么要将开始出的0 ~ 4MB标记为已存在,可读写并且页面不能被释放呢?
还记得Kernel.ld连接脚本里出现几个地址:在这里插入图片描述
在这里插入图片描述
内核镜像加载地址(物理地址)起始0x00100000(1MB),结束于0x00400000(4MB),前1MB开始的区间留作他用,所以前4MB的物理空间已经被使用,所以对应的线性地址应该设置存在位置且对应页面不会被释放。
在这里插入图片描述
接下来,将页目录表对应的物理地址转换成以4MB开始的线性地址。
最后对控制寄存器操作,将页目录表页面的物理地址存放在CR3,开启分页机制。
控制寄存器CR0、CR2、CR3、CR4:
(1)CR0
80x86的分页机制由CR0中的PG位启用。如PG=1,启用分页机制,把线性地址转换为物理地址。如PG=0,禁用分页机制,直接把前面段机制产生的线性地址当作物理地址使用
(2)CR2和CR3
CR2和CR3用于分页机制。CR3含有存放页目录表页面的物理地址,因此CR3也被称为PDBR。因为页目录表页面是页对齐的,所以该寄存器只有高20位是有效的。而低12位保留供更高级处理器使用,因此在往CR3中加载一个新值时低12位必须设置为0。
使用MOV指令加载CR3时具有让页高速缓冲无效的副作用。为了减少地址转换所要求的总线周期数量,最近访问的页目录和页表会被存放在处理器的页高速缓冲器件中,该缓冲器件被称为转换查找缓冲区(Translation Lookaside Buffer,TLB)。只有当TLB中不包含要求的页表项时才会使用额外的总线周期从内存中读取页表项。
即使CR0中的PG位处于复位状态(PG=0),我们也能先加载CR3。以允许对分页机制进行初始化。当切换任务时,CR3的内容也会随之改变。但是如果新任务的CR3值与原任务的一样,处理器就无需刷新页高速缓冲。这样共享页表的任务可以执行得更快。
CR2用于出现页异常时报告出错信息。在报告页异常时,处理器会把引起异常的线性地址存放在CR2中。因此操作系统中的页异常处理程序可以通过检查CR2的内容来确定线性地址空间中哪一个页面引发了异常
(3)CR4
CR4在Pentium系列(包括486的后期版本)处理器中才实现,它处理的事务包括诸如何时启用虚拟8086模式等.
在这里插入图片描述
至此,最后跳转到kmain函数了,也就是以C++编写的函数,代码分析就方便多了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值