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 :) ! "

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351