进入保护模式(二)——《x86汇编语言:从实模式到保护模式》读书笔记14

首先来段题外话:之前我发现我贴出的代码都没有行号,给讲解带来不便。所以从现在起,我要给代码加上行号。我写博客用的这个插入代码的插件,确实不支持自动插入行号。我真的没有找到什么好方法,无奈之下,只能按照网友的说法,在VIM中给每行代码加上行号,然后再贴出来。

在VIM中每一行都添加上行号的方法是:

:%s/^/\=line(".")/

对,只要执行这个命令就可以了。至于为什么这样写,可以参考我的另一篇博文

《在VIM中添加行号的方法》http://blog.csdn.net/longintchar/article/details/50569851         
        

 

我们接着上篇博文 进入保护模式(一)——《x86汇编语言:从实模式到保护模式》读书笔记12 说。

 

(五)设置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

第46~48用于设置CR0的bit0为1.

(六)关于段寄存器

我们知道,32位模式下,段寄存器有CS,DS,ES,SS,FS,GS. 这些段寄存器每个都分为2个部分,一个是16位的可见部分,一个是隐藏部分,称为描述符高速缓存器,用来存放段的线性基地址、段界限和段属性。如下图:

New0002段寄存器的格式

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中的索引项号。

段选择子

为了说明保护模式下的内存访问,我们回到代码。

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

这条指令和位于它前面的指令一样,是默认用[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

这样写是不对的。这样做会导致处理器产生一个无效操作码的异常中断。

posted @ 2016-01-16 20:45  漂泊的指针  阅读(1566)  评论(0编辑  收藏  举报