进入保护模式(二)
最后更新于:2022-04-01 16:22:29
首先来段题外话:之前我发现我贴出的代码都没有行号,给讲解带来不便。所以从现在起,我要给代码加上行号。我写博客用的这个插入代码的插件,确实不支持自动插入行号。我真的没有找到什么好方法,无奈之下,只能按照网友的说法,在VIM中给每行代码加上行号,然后再贴出来。
在VIM中每一行都添加上行号的方法是:
:%s/^/\=line(".")/
对,只要执行这个命令就可以了。至于为什么这样写,可以参考我的另一篇博文
《在VIM中添加行号的方法》[http://blog.csdn.net/longintchar/article/details/50569851](http://blog.csdn.net/longintchar/article/details/50569851)
我们接着上篇博文 [进入保护模式(一)——《x86汇编语言:从实模式到保护模式》读书笔记12](http://blog.csdn.net/longintchar/article/details/50513772) 说。
### (五)设置PE位
~~~
44 cli ;保护模式下中断机制尚未建立,应
45 ;禁止中断
46 mov eax,cr0
47 or eax,1
48 mov cr0,eax ;设置PE位
~~~
第44行,用于关中断。因为保护模式下的中断和实模式不同,所以原来的中断向量表不再适用,BIOS中断也不能再用,因为它们都是实模式下的代码。在重新配置保护模式下的中断环境之前,我们必须关中断。
CR0是处理器内部的一个控制寄存器,也是32位的(如下图,图片来自赵炯的《Linux内核完全剖析》)。
它的bit0是保护模式允许位(Protection Enable,PE)。当PE=1时,则处理器进入保护模式。
[![cr0](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd0c09d.jpg "cr0")](http://img.blog.csdn.net/20160116204449463)
第46~48用于设置CR0的bit0为1.
### (六)关于段寄存器
我们知道,32位模式下,段寄存器有CS,DS,ES,SS,FS,GS. 这些段寄存器每个都分为2个部分,一个是16位的可见部分,一个是隐藏部分,称为描述符高速缓存器,用来存放段的线性基地址、段界限和段属性。如下图:
[![New0002段寄存器的格式](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd1fd04.jpg "New0002段寄存器的格式")](http://img.blog.csdn.net/20160116204457964)
### 1.实模式下的内存访问
在32位处理器上的实模式下,假如执行下面的代码。
~~~
mov cx,0x2000
mov ds,cx
mov [0xc0],al
~~~
CPU在把0x2000传送到DS的同时,还会把0x2000左移4位(0x20000),传送到DS描述符高速缓存寄存器(段基地址部分仅低20位有效,高12位全部是0)。此后,只要不改变DS的内容,那么每次访问内存都直接使用DS描述符高速缓存寄存器的内容作为段地址。
### 2.保护模式下的内存访问
在保护模式下,实模式的6个段寄存器叫做“段选择器”。尽管在访问内存的时候也要指定一个段,但是传送到段选择器的内容不是逻辑段地址,而是**段选择子(也叫段选择符)**。
如下图(图片来自赵炯的《Linux内核完全剖析》)所示,段选择子由三部分组成。
- 请求特权级RPL(Requested Privilege Level):提供了段保护信息,我们以后会学习。现在只需设置为00即可。
- 表指示标志TI(Table Index):TI=0时,表示描述符在GDT中;TI=1时,表示描述符在LDT(我们以后会学习)中。
- 索引值(Index):描述符在GDT或者LDT中的索引项号。
[![段选择子](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd3592b.jpg "段选择子")](http://img.blog.csdn.net/20160116204500249)
为了说明保护模式下的内存访问,我们回到代码。
~~~
56 mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
57 mov ds,cx
58
59 ;以下在屏幕上显示"Protect mode OK."
60 mov byte [0x00],'P'
61 mov byte [0x02],'r'
62 mov byte [0x04],'o'
63 mov byte [0x06],'t'
64 mov byte [0x08],'e'
65 mov byte [0x0a],'c'
66 mov byte [0x0c],'t'
~~~
第56、57行,将段选择子10000b传到段选择器DS中,从段选择子可以看出,RPL=0;TI=0(表示GDT);索引号为2;
当处理器执行任何改变段选择器的指令时(比如mov、jmp far、call far、iret、retf等),就将指令中提供的索引号*8作为偏移地址,同GDTR寄存器中的线性基地址相加,然后访问GDT。如果没有什么问题(比如超过了GDT的界限),就把找到的描述符加载到不可见的描述符高速缓存寄存器。此后,每当有访问内存的指令时,就不再访问GDT中的描述符,而是直接使用段寄存器的描述符高速缓存寄存器。
结合代码来说,第57行,处理器把2*8(=16)作为偏移地址,同GDTR的内容(内容为0x00007e00)相加,得到0x0000_7e16,根据这个地址找到描述符(就是我们之前创建的#2描述符)
~~~
27 ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
28 mov dword [bx+0x10],0x8000ffff
29 mov dword [bx+0x14],0x0040920b
~~~
然后,把这个描述符加载到高速缓存寄存器(包括线性基地址0x000b8000,段界限,段属性)。
第60行,执行这条指令时,处理器用DS描述符高速缓存寄存器中的线性基地址(0x000b8000,文本模式的显存起始地址)加上指令中的偏移量0x00,形成32位的物理地址0x000b8000,并将字符‘P’写入该处。
不仅仅是访问数据段,处理器访问代码段取指令的时候,也是采用相同的方法。假设CS描述符高速缓存寄存器已经装载了正确的32位线性基地址,那么处理器取指令的时候,会使用CS描述符高速缓存寄存器中的32位线性基地址加上EIP中的偏移量,构成32位的物理地址,根据这个物理地址从内存中取得指令。
### (七)清空流水线并串行化处理器
正如前文所述,即使在实模式下,段寄存器的高速缓存寄存器也被用于访问内存。当处理器进入保护模式后,高速缓存寄存器的内容依然残留,但是这些内容在保护模式下是无效的。因此,比较安全的做法是尽快刷新段选择器,包括描述符高速缓存寄存器。
另外,在进入保护模式之前,很多指令已经进入了流水线。因为处理器工作在实模式下,所以它们都是按照16位操作数和地址长度进行译码的,即使是那些用bits32编译的指令,为了防止执行结果不正确,所以必须清空流水线。还用,那些通过乱序执行得到的中间结果也是无效的,所以必须清理掉,让处理器串化执行。
为了达到上述目的,我们可以采用远转移指令jmp或者远过程调用指令call。遇到这类指令,处理器一般会清空流水线并且串化执行;另一方面,远转移会重新加载CS,并刷新描述符高速缓存寄存器的内容。所以,强烈建议在设置了PE位后,立刻用jmp或者call转移到当前指令流的下一条指令上。
于是代码中有:
~~~
50 ;以下进入保护模式... ...
51 jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移
52 ;清流水线并串行化处理器
53 [bits 32]
54
55 flush:
56 mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
57 mov ds,cx
~~~
第51行,是一条远转移指令。如果你忘记了jmp的用法,没有关系,可以参考我的另一篇博文[8086处理器的无条件转移指令——《x86汇编语言:从实模式到保护模式》读书笔记13](http://blog.csdn.net/longintchar/article/details/50529164)。
这条指令和位于它前面的指令一样,是默认用[bits 16]编译的。但是因为使用了关键字dword(注意:这里的dword是修饰偏移地址flush的),所以编译后的偏移地址是32位的。
如果51行这样写:
~~~
51 jmp 0x0008:flush ;16位的描述符选择子:16位偏移
~~~
这样写是不严谨的。因为这样编译出来的目标地址是16位的。如果flush代表的地址是0x12345678,那么编译后会被截断成为0x5678,这显然是错的。所以这个跳转一定要加dword.
注意:因为设置了PE位,所以现在已经处于保护模式下了。所以处理器会把第一个操作数(0x0008)理解为段选择子,而不是是模式下的逻辑段基址。当51行的指令执行时,处理器会把选择子0x0008(索引号为1,TI=0,RPL=00)加载到CS,并把#1描述符(定义了一个代码段,基地址是0x7c00,段界限是0x1ff,长度为0x200)加载到CS描述符高速缓存寄存器中。所以程序会转移到基地址为0x0000_7c00的代码段内的某个位置执行。这个位置取决于偏移地址。偏移地址就是标号flush的汇编地址(因为指定了dword,所以编译后是32位的),处理器会用这个32位的数值来代替EIP的原有内容。于是,程序就转移到flush处了。
第53行,使用了伪指令[bits 32],从这以后,指令是按照32位编译的。因为指令执行到这里的时候,已经真真正正地进入了保护模式了。
### (八)进入保护模式的主要步骤
我们总结一下进入保护模式的主要步骤:
1.安装段描述符,构造GDT
2.用lgdt指令加载GDTR
3.打开A20
4.设置CR0的PE位为1
5.跳转,真正进入保护保护模式。
### (九)在屏幕上显示字符
~~~
55 flush:
56 mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
57 mov ds,cx
58
59 ;以下在屏幕上显示"Protect mode OK."
60 mov byte [0x00],'P'
61 mov byte [0x02],'r'
62 mov byte [0x04],'o'
63 mov byte [0x06],'t'
64 mov byte [0x08],'e'
65 mov byte [0x0a],'c'
66 mov byte [0x0c],'t'
67 mov byte [0x0e],' '
68 mov byte [0x10],'m'
69 mov byte [0x12],'o'
70 mov byte [0x14],'d'
71 mov byte [0x16],'e'
72 mov byte [0x18],' '
73 mov byte [0x1a],'O'
74 mov byte [0x1c],'K'
~~~
56、57行,前文已经说过,令DS指向文本模式的显示缓冲区。
60~74行,就是在屏幕左上角显示"Protect mode OK." 需要说明的是:不管是实模式还是保护模式,外围设备是不受影响的。
最后注意一点:
保护模式下,不允许使用mov指令改变段寄存器CS的内容。比如
mov cs,ax
这样写是不对的。这样做会导致处理器产生一个无效操作码的异常中断。