Linux 0.11——从实模式到保护模式

综述

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/33/

最近在阅读Linux 0.11的源码时,对于setup.s文件中设置GDT表的部分不是很理解,后来经过刘国军老师的指点,结合赵炯博士的《Linux内核完全注释》的第四章《80X86保护模式及其编程》,对于保护模式有了一些粗浅的了解和认识。备忘。

本文章主要讲解保护模式的寻址机制与setup.s中的切换部分。

保护模式

保护模式运行在80286及其之后的所有CPU上,但是为了保证向前兼容性,正常的CPU在启动时并不会默认进入保护模式,而是会进入实模式,随后通过一系列设定转入保护模式。

在16位实模式下,CPU寻址时使用16位段寄存器的内容乘以16当作段基地址,加上16位段偏移地址形成20位的物理地址,所以最大寻址仅为1MB字节,最大段长度64KB。在实模式下,所有的段都是可以任意访问的,即任意读、写和执行。

虽然在80286点CPU上已经出现了保护模式,但是其寄存器的位宽仍然是16位,只不过其地址线由20位扩大到了24位,寻址空间随即扩大到了16MB。真正的32位保护模式出现在80386上,其地址总线和寄存器都是32位宽的,因此寻址空间扩大到了4GB。

保护模式下,CPU寻址主要有两种模式,一是分段模式,二是分段和分页相结合,分页无法单独出现。保护模式的分段模式为每一段增加了段属性来限制用户程序对内存中一些段的操作。在全局描述符表(GDT)中,每个段的表项存储了一个段的基本属性,例如段的基地址、段的界限、段的类型(代码段、数据段)、段的执行权限等。

分页模式的出现使得程序员可以编写远远大于内存的程序而无需担心内存的容量,在该模式下,内存被划分为“页”存储,磁盘的一部分用作虚拟内存,当应用程序执行时需要的某些代码或数据所在的页不在内存中时,CPU就会产生一个缺页异常,从磁盘中将所需的页调入内存中后恢复执行,在应用程序看来,所需的代码或数据仿佛一直存在内存上。

重要数据结构

在保护模式中,有几个长得很像的名字一直是我们心头噩梦:GDT、GDTR、LGDT、LDT、LDTR、LLDT……

事实上,并不是那么难区分。保护模式下,每个段新增了一些基本的属性,连带着段的基地址一起存储在全局描述符表(GDT),每一条表项被称为一条描述符。一条描述符保存了访问一个段所需的全部信息,包括段基地址、段界限、段类型和段权限(DPL)。一条段描述符的结构如下:

段描述符

GDT本质上就是个表,保存在内存中的某一块区域,那么CPU如何获取到这张表呢,即CPU如何知道这个表在内存中的位置呢。CPU内部有一个全局描述符表寄存器(GDTR),该寄存器内保存了GDT的基地址,CPU可以通过该寄存器内的地址直接找到GDT表。GDTR和LDTR的结构如下:

GDTR与LDTR

在系统启动时,GDTR被设置为默认值:基地址0,表长0xFFFF。当我们需要切换到保护模式的时候,就需要为GDTR中写入GDT表的基地址,所需的指令就是LGDT。

类似的,局部描述符表(LDT)为多进程提供了便利,每个进程都有自己的数据段、代码段和堆栈段,使用LDT就可以将每个进程的三个段封装在一起,只要改变LDTR就可以实现对不同进程的段的访问,改变LDT的指令为LLDT。

在32位保护模式下,原先的16位段寄存器(包括CS、SS、DS、ES、FS、GS)已经无法描述一个段的具体位置,于是保护模式中的段寄存器被用于存储段选择符,段选择符用于从段描述符表中选择出正确的段描述符。段选择符结构如下:

段选择符

RPL字段提供了段保护信息,TI字段指出包含指定的段描述符是否位于LDT中,TI=0时表示位于GDT,TI=1表示位于LDT,索引指出了段描述符位于GDT或LDT表中的索引项号。

至此,保护模式下寻址就变得清晰明了(仅限于段模式),《Linux内核完全注释》中有一张明了的图:

段模式寻址

使用段选择符在描述符表中找到正确的段描述符,进而获得段基地址,再加上偏移量即获得目标位置的线形地址(如果未开分页模式的话线形地址等同于物理地址)。

在CPU中,有一套控制寄存器(CR0、CR1、CR2和CR3)用于控制CPU的操作模式和当前任务特性。结构如下:

控制寄存器

其中CR0上的第0位PE位是启用保护(Protection Enable)表示,只有设置该位时,CPU才会进入保护模式。可以使用lmsw指令来改变CR0,例如Linux 0.11的实现:

mov	ax,#0x0001	! protected mode (PE) bit
lmsw	ax		! This is it!

于是,0x0001的低4位会被赋给CR0,即改变PE、MP、EM和TS位,其中PE就被置为1。

来自《Linux内核完全注释》的提醒事项:

在修改该了 PE 位之后程序必须立刻使用一条跳转指令,以刷新处理器执行管道中已经获取的不同模 式下的任何指令。在设置 PE 位之前,程序必须初始化几个系统段和控制寄存器。在系统刚上电时,处理 器被复位成 PE=0、PG=0(即实模式状态),以允许引导代码在启用分段和分页机制之前能够初始化这些寄 存器和数据结构。

jmpi 0,8

Linux 0.11与0.12版本中,在setup.s文件里实现了从实模式到保护模式的切换,我们来研究一下具体的步骤。具体的实现从107行(0.11)开始,之前只是在初始化一些硬件参数。

首先使用cli指令屏蔽了所有的中断,防止切换过程被诸如键盘中断之类的打断导致错误,接着从111行到127行将system模块移动到0x00000处,由于事先bootsect.s将system模块读到了0x10000的位置,并且假设system模块的长度不会超过512KB(0x80000),于是这个移动过程本质上是将0x10000到0x8ffff的内存块移动到0x00000到0x7ffff的位置。

133和134行加载了GDTR和IDTR(中断描述符表寄存器):

lidt	idt_48		! load idt with 0,0
lgdt	gdt_48		! load gdt with whatever appropriate

我们看一下GDTR的结构,在文件末尾处的gdt_48,就是要被加载入GDTR的内容:

gdt_48:		! GDTR
	.word	0x800		! gdt limit=2048, 256 GDT entries
	.word	512+gdt,0x9	! gdt base = 0X9xxxx

记得上面的GDTR的结构吗,低16位为表长度,高32位为GDT表基地址。由于INTEL使用的小端存储(little endian),那么最先出现的字0x800应当被存储在低16位,0x800即2048,说明该GDT最大存储2048字节的数据,由于单条描述符项(不是描述符)为8字节,那么该GDT一共可存储256个描述符项。后面的两个字512+gdt、0x9就落在GDTR的高32位上,0x9在前,512+gdt(注意这里的512是十进制数,即0x200)在后,于是基地址就是拼接的结果,即0x9 << 16 + 0x200 + gdt,即0x90200+gdt,在bootsect中,setup模块就被移动到了0x90200处,而gdt就是GDT在本程序段中的偏移地址,最终获得的就是GDT表的物理地址。

回到136行,136行到145行,开启了A20地址线,A20地址线是INTEL的一个历史遗留问题,这个东西是为了保证向下兼容产生的。在保护模式下,如果不打开A20地址线,会导致无法访问到完整的内存。具体原因可以看这篇文章的讲解,不赘述。

146到180行重新定义了中断,不表。

最终,从191行开始,是关键性的三行代码:

mov	ax,#0x0001	! protected mode (PE) bit
lmsw	ax		! This is it!
jmpi	0,8		! jmp offset 0 of segment 8 (cs)

上面讲解过前两行,其实就是打开了PE位,第三行跳到了一个奇怪的地方,直观上看,它跳转到了段8的0偏移位置,但是注意,现在PE位已经被置为1,CPU已经进入保护模式,CPU讲按照段模式去寻找这个内存地址。

那么此时在CS中存储的,并不是段的地址,而是段选择符,那么0x8将段选择符的RPL和TI字段都置为0,而描述符索引就是0x1(这里和mooc的李志军老师讲的有点不一样,存疑)。

lgdt指令设置的GDTR指向的GDT内容如下:

gdt:
	.word	0,0,0,0		! dummy

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9A00		! code read/exec
	.word	0x00C0		! granularity=4096, 386

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9200		! data read/write
	.word	0x00C0		! granularity=4096, 386

其中第0项的四个字都被置为0,通用操作。我们看下第一项(64位,四字):

.word 0x07FF .word 0x0000 .word 0x9A00 .word 0x00C0

我们再把段描述符拿来对照下:

段描述符

如果按照图中的格式给上面那一堆字排个顺序,那应该是下面这样的:

00 C0 9A 00
00 00 07 FF

对照下,把基地址的部分挑出来,拼接在一起,就可以得到基地址,为0x00000000,实际上就是0。

由于jmpi 0,8中的段偏移量也是0,那么这条语句实际上就是跳转到线形地址0x0处,由于setup将system模块移动到了0x00000到0x7ffff的位置,实际上就是跳转到了system的开头,从头开始执行system。

End

为什么要将这部分,大概是因为保护模式这里看着还挺劝退的,很多东西很乱,很难理清思路,所以记下来,备忘一下。

保护模式的内容当然不止这么一点,除了分段,还有虚拟内存(分页)、段权限等等,这里整理的只有我在阅读setup.s时遇到的问题。任重而道远啊。

文中的图都引用自赵炅博士的《Linux内核完全注释》第五版,感谢他写出了这本好书。

最后引用Linus的话作为结束语吧:

" RTFSC – Read The F**king Source Code 😃 ! "

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值