ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
首先来段题外话:之前我发现我贴出的代码都没有行号,给讲解带来不便。所以从现在起,我要给代码加上行号。我写博客用的这个插入代码的插件,确实不支持自动插入行号。我真的没有找到什么好方法,无奈之下,只能按照网友的说法,在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://box.kancloud.cn/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://box.kancloud.cn/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://box.kancloud.cn/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 这样写是不对的。这样做会导致处理器产生一个无效操作码的异常中断。