Add refrences and do some fixes
[thunix.git] / doc / Chapter3-Protect-Mode
blob84f5eb827aee7e656cdf376b321a347deb1b54e7
1                 Chapter 3 保护模式
2 3.1 保护模式概述
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默认的都是使用分断模式。 另外, 这两种模式可以共存, 也就是使用分断的同时也使用分页。
23 3.2 保护模式的分断访问模式
24 刚才也已经提及到, cs不再是代码段的地址, 而最终的地址(叫作物理地址)也不再是SEG * 16 + OFF. 在保护模式下, cs指的是段选择符(Segment Selector)。 为了能说明cs现在装的是什么东西, 什么又叫作段选择符, 我有必要先说明下GDT(Global Descriptor Table)。
26 首先, GDT, 是张表, 或是从语言的角度来说, 是一个数组, 一个每一项(叫作段描述符)都是8字节64位的整形数组。每一项的每一位都有特定的意义, 整个段描述符的作用如下:
28 前四个字节:
29 位       功能                      描述
30 0-15    最大值(Limit) 0:15    表示该段的最大值的前16位
31 16-31   基址 (Base) 0:15          基址的前16位
33 后四个字字 :
34 位       功能              描述
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位
45 所以, 我们得建一张GDT表, 大概如下:
46 0       -----------------  
47         |  全0                           |  注, 第一项必需得全0
48 8       -----------------  
49         |  代码段                  |
50 c       -----------------
51         |  数据段                  |
52 10      -----------------
53         |  .....                        |
54 xx      -----------------
56 重要的项, 现有两个:一是基址, 也就是说这个段始于内存的那个地方, 二是最大值, 表示该段的最大表示范围。 有点值得注意的就是, 这两个数据都分别分散在不同的位上。
58 所以, cs指向的就是这些东西, 里面存的相应的段在GDT的索引值。注意, 实际上段寄存器存放的是索引值, 而不偏移值。 但段选择符并不仅仅是16位的来存储这个索引值的, 它像段描述符一样, 也有一定的格式。格式很简单:
59 16                      3  2  1  0
60 ------------+----
61 |                       | |      |
62 ------------+----
64 对于现在来说, 我们只关于后13位, 那里就是存着段在GDT中的索引值, 后三位全0。
66 好的, 现在知道了如何通过一个段寄存器找着具体的段起始地址, 那问题是, GDT该放在哪呢? 答案是可以放在任何地方. 那CPU如何找着它呢? 答案是CPU有一个48位的gdt描述符, 格式如下:
67 前两个字节: GDT的大小
68 后四个字节: GDT所在物理地址
70 有了这个, CPU就可以通过lgdt指令加载GDT表, 之后cs, ds之类的就能正常访问到相应的段了。 所以, 在你的代码中, 应该有如下几何代码 :
72 gdt:                            
73                 .quad   0x0000000000000000 # null descriptor
74                 .quad   0x00cf9a000000ffff # cs
75                 .quad   0x00cf92000000ffff # ds
76                 .fill   22,8,0
77 gdt_48:
78                 .word   .-gdt-1
79                 .long   gdt     
81                 lgdt    gdt_48
83 3.3 进入保护模式
84 上面讲了这么多, 是该讲怎么进入保护模式了。
86 不过, 在此之前又有几个问题必需讲明白。
88 3.3.1 A20 地址线
89 之前也说了, 在实模式下内存地址空间是很有限的, 不能访问超过1M的地址。 然而,在进入保护模式之前, 我们得手动做些设置让CPU能访问到高于1M的内存。 这个开关就存在于键盘控制器的A20线上。 开启代码如下:
90 enable_a20:
91                 inb     $0x64,  %al     
92                 testb   $0x2,   %al
93                 jnz     enable_a20
94                 movb    $0xdf,  %al
95                 outb    %al,    $0x64
97 向键盘控制器的0x64端口写入命令字0xdf开启A20. 之后, 我们就能访问到整个4GB的物理地址空间了。
99 3.3.2 CR0 控制寄存器
100 好的, 我们现在能访问整个的4GB物理地址空间了。 但是, 真正能开启进入保护模式的开关并不是在键盘控制器,而是在CPU的另一个寄存器上, cr0, 也就是控制寄存器0。 若把CR0的第0位置1, 就表示要开启保护模式, 因此, 代码很简单:
101                 ## enter pmode
102                 movl    %cr0,   %eax
103                 orl     $0x1,   %eax
104                 movl    %eax,   %cr0
105 是的, 我们已经进入了保护模式的世界了!
107 3.4 进入保护模式之后的初始化
108 其实在上面的一段代码之后应该加上一行这样的代码:
109                 ljmp    $0x08, $OFF
110 表示要跳转到代码段(还记得$0x08是代码段的选择符么), 偏移值为OFF处执行。 ljmp是必要的, 因为这是从实模式跳到保护模式!
111 回到操作模式, 所有的段寄存器以及栈又得重新初始化, 不过, 还好, 代码很简单:
112 pm_mode:
113                 movl    $0x0c,  %eax
114                 movw    %ax,    %ds
115                 movw    %ax,    %es
116                 movw    %ax,    %fs
117                 movw    %ax,    %gs
118                 movw    %ax,    %ss
119                 movl    $STACK_BOT,%esp
120 注意, 0x0c, 也就是数据段的选择符。
123 3.5 进入保护模式的最后一件事
124 内核已经载入内存, 保护模式也已经进入。 接下来, 最后一件事就是调用内核的init函数了, 该函数负责一切关于该内核的所有相关的初始化操作。 所以, 最后一行代码, 应该是:
127 # Call the kernel init function do the init stufff
129                 call init   # we count on it...
132 3.6 小结
133 接下来, 我们就会跳到init函数了, 这是一个C函数, 也即代表了我们将要进入C的世界了!