3 自Intel i386开始引进保护模式开始, 一直引用至今。 它解决了很多8086所面临的问题, 如用SEG:OFF方式寻址, 不能访问超过内存1M以上的内存空间, 没有任何保护方式, 一般的程序都可以把整个内存给篡改掉, 也就无所谓的系统安全可言, 这种对应于保护模式的就是16位的8086实模式。 典型的运行于实模式的操作系统就是DOS了。 就如上一章所讲的, 一个简单的bootsector就可能把整个BIOS的内存空间给覆盖掉, 这样的系统还有什么安全性可言呢?
5 这是我采用保护模式的原因之一, 再者就是学习保护模式对于今后对操作系统的学习有很大的帮助, 毕竟对于系统级的编程来说, 底层就是王道。
7 保护模式虽然很强大, 但由于其直接架构于CPU之后, 更者几乎没有什么好的中文资料, 再者学校开设的都是16汇编, 所以, 仍有很多人不知道保护模式或是觉得很难学。 但其实, Intel官方给出了有关保护模式相当详细的资料[Intel Manual], 它们是:
8 Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 1: Basic Architecture
9 Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 2A: Instruction Set Reference, A-M
10 Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 2B: Instruction Set Reference, N-Z
11 Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3A: System Programming Guide Part 1
12 Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3B: System Programming Guide Part 2
14 关于保护模式这一块, 就是第3A卷。 但, 都是全英文的!
16 在讲保护模式之前, 先得说明下内存的访问方式。 如今天的CPU有两种内存访问模式:
17 a) 分断, 在实模式下, 就是典型的SEG:OFF方式, 但对于32位CPU来说, 就相对复杂些了, 虽然仍是SEG+OFF, 但cs包含的不再是代码段的段地址, 而最终的地址也不再是SEG * 16 + OFF. 这点, 稍后会作出解释。
18 b) 分页, 意为把整个内存空间分为一个个固定大小的页(比如, 常用的是4KB), 这个多用于虚拟内存的实现到中。 其中最大一好处就是,当我们要运行一个很大的程序时, 我们并不需要把整个程序给load进内存, 而只需把少数的几页数据加入内存即可。 当我们访问到某一页时, 发现该数据没有被load进内存, 就会引发一个缺页中断, 这时内核(也就是我们要实现的操作系统)得把它load进内存, 以完成之后的访问。 当然, 这涉及到中断, 以及32位保护模式的内存管理, 因此, 我们现就使用分断模式。
20 那如何让CPU使用分断模式呢? 这很好办, 因为CPU默认的都是使用分断模式。 另外, 这两种模式可以共存, 也就是使用分断的同时也使用分页。
24 刚才也已经提及到, cs不再是代码段的地址, 而最终的地址(叫作物理地址)也不再是SEG * 16 + OFF. 在保护模式下, cs指的是段选择符(Segment Selector)。 为了能说明cs现在装的是什么东西, 什么又叫作段选择符, 我有必要先说明下GDT(Global Descriptor Table)。
26 首先, GDT, 是张表, 或是从语言的角度来说, 是一个数组, 一个每一项(叫作段描述符)都是8字节64位的整形数组。每一项的每一位都有特定的意义, 整个段描述符的作用如下:
30 0-15 最大值(Limit) 0:15 表示该段的最大值的前16位
31 16-31 基址 (Base) 0:15 基址的前16位
35 0-7 基址 16:23 基址的第16-23位
36 8-12 类型 (Type) 段的类型(代码段,或是数据段), 以及各种属性
37 13-14 权限(PL) 0 = 最高权限 (操作系统), 3 = 最低权限 (用户程序)
38 15 存在标志(Present flag) 置1表示段存在
39 16-19 最大值 16:19 最大值的第16-19位
40 20-22 属性 不同类型的段,对应着不同的属性
41 23 粒度(Granularity) 和最大值一起使用, 用来表示该段所能表示的最大字节数
42 24-31 基址 24:31 基址的最后8位
56 重要的项, 现有两个:一是基址, 也就是说这个段始于内存的那个地方, 二是最大值, 表示该段的最大表示范围。 有点值得注意的就是, 这两个数据都分别分散在不同的位上。
58 所以, cs指向的就是这些东西, 里面存的相应的段在GDT的索引值。注意, 实际上段寄存器存放的是索引值, 而不偏移值。 但段选择符并不仅仅是16位的来存储这个索引值的, 它像段描述符一样, 也有一定的格式。格式很简单:
64 对于现在来说, 我们只关于后13位, 那里就是存着段在GDT中的索引值, 后三位全0。
66 好的, 现在知道了如何通过一个段寄存器找着具体的段起始地址, 那问题是, GDT该放在哪呢? 答案是可以放在任何地方. 那CPU如何找着它呢? 答案是CPU有一个48位的gdt描述符, 格式如下:
70 有了这个, CPU就可以通过lgdt指令加载GDT表, 之后cs, ds之类的就能正常访问到相应的段了。 所以, 在你的代码中, 应该有如下几何代码 :
73 .quad 0x0000000000000000 # null descriptor
74 .quad 0x00cf9a000000ffff # cs
75 .quad 0x00cf92000000ffff # ds
84 上面讲了这么多, 是该讲怎么进入保护模式了。
89 之前也说了, 在实模式下内存地址空间是很有限的, 不能访问超过1M的地址。 然而,在进入保护模式之前, 我们得手动做些设置让CPU能访问到高于1M的内存。 这个开关就存在于键盘控制器的A20线上。 开启代码如下:
97 向键盘控制器的0x64端口写入命令字0xdf开启A20. 之后, 我们就能访问到整个4GB的物理地址空间了。
100 好的, 我们现在能访问整个的4GB物理地址空间了。 但是, 真正能开启进入保护模式的开关并不是在键盘控制器,而是在CPU的另一个寄存器上, cr0, 也就是控制寄存器0。 若把CR0的第0位置1, 就表示要开启保护模式, 因此, 代码很简单:
108 其实在上面的一段代码之后应该加上一行这样的代码:
110 表示要跳转到代码段(还记得$0x08是代码段的选择符么), 偏移值为OFF处执行。 ljmp是必要的, 因为这是从实模式跳到保护模式!
111 回到操作模式, 所有的段寄存器以及栈又得重新初始化, 不过, 还好, 代码很简单:
120 注意, 0x0c, 也就是数据段的选择符。
124 内核已经载入内存, 保护模式也已经进入。 接下来, 最后一件事就是调用内核的init函数了, 该函数负责一切关于该内核的所有相关的初始化操作。 所以, 最后一行代码, 应该是:
127 # Call the kernel init function do the init stufff
129 call init # we count on it...
133 接下来, 我们就会跳到init函数了, 这是一个C函数, 也即代表了我们将要进入C的世界了!