今天开始学习intel处理器的保护模式。书的第二章
这节讲述的是如何从实模式进入保护模式。用的例子是在保护模式下向屏幕上输出字符P
如何进入保护模式呢?主要步骤如下:
下面是书的例子:
; ========================================== ; pmtest1.asm ; 编译方法:nasm pmtest1.asm -o pmtest1.bin ; ========================================== %include "pm.inc" ; 常量, 宏, 以及一些说明 org 0100h jmp LABEL_BEGIN [SECTION .gdt] ; GDT ; 段基址, 段界限 , 属性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址 ; GDT 结束 GdtLen equ $ - LABEL_GDT ; GDT长度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; GDT 选择子 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ; END of [SECTION .gdt] [SECTION .s16] [BITS 16] LABEL_BEGIN: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0100h ; 初始化 32 位代码段描述符 xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_CODE32 mov word [LABEL_DESC_CODE32 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE32 + 4], al mov byte [LABEL_DESC_CODE32 + 7], ah ; 为加载 GDTR 作准备 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_GDT ; eax <- gdt 基地址 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 ; 加载 GDTR lgdt [GdtPtr] ; 关中断 cli ; 打开地址线A20 in al, 92h or al, 00000010b out 92h, al ; 准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正进入保护模式 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, ; 并跳转到 Code32Selector:0 处 ; END of [SECTION .s16] [SECTION .s32]; 32 位代码段. 由实模式跳入. [BITS 32] LABEL_SEG_CODE32: mov ax, SelectorVideo mov gs, ax ; 视频段选择子(目的) mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。 mov ah, 0Ch ; 0000: 黑底 1100: 红字 mov al, 'P' mov [gs:edi], ax ; 到此停止 jmp $ SegCode32Len equ $ - LABEL_SEG_CODE32 ; END of [SECTION .s32]
用到的Descriptor
在pm.inc
中定义,关于Descriptor
定义的内容如下:
; 描述符 ; usage: Descriptor Base, Limit, Attr ; Base: dd ; Limit: dd (low 20 bits available) ; Attr: dw (lower 4 bits of higher byte are always 0) %macro Descriptor 3 dw %2 & 0FFFFh ; 段界限1 dw %1 & 0FFFFh ; 段基址1 db (%1 >> 16) & 0FFh ; 段基址2 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2 db (%1 >> 24) & 0FFh ; 段基址3 %endmacro ; 共 8 字节
刚开始看到上面的代码,我有点束手无策。因为也是最近才开始学习汇编,上面的程序我连指令都认不全。所以下面这一节对上面程序中语法的部分做一些讲解
%include "pm.inc"
:包含文件。类似c语言中的包含.h
文件。
org 07c00h
:org
是origin的缩写。告诉编译器下一条汇编语句的偏移地址是07c00h
。
[SECTION .gdt]
:AT&T汇编语言格式,用于定义一个节。这里是定义一个结构体数组,数组名称是GDT
,数组内部是三个Descriptor
结构。
LABEL_GDT: Descriptor 0, 0, 0
:Descriptor
是在pm.inc
中定义的宏,8个字节。上面有列出来内部的定义。个人猜测定义中的%1
、%2
、%3
是这里传进去的参数,按照位置分别是1、2、3。猜测跟shell
中的位置参数
类似。(现在先猜测一下,到影响继续学习的时候再深究)。上面定义的Descriptor
这个宏能够用比较自动化的方法把段基址、段界限和段属性安排在一个描述符中合适的位置。这儿也不是很了解,不知道自动化是如何实现的
GdtLen equ $ - LABEL_GDT
:equ
是伪指令。这句话的意思是用GdtLen
来代替$ - LABEL_GDT
。从这儿看类似于c语言中的define
。
GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址
这里定义一个结构体数组GdtPtr
,共有6个字节。前2字节(处于低位)是GDT的界限;后4字节(处于高位)是GDT的基地址。
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
:这句话定义GDT的选择子(sector)SelectorCode32。在下面讲解GDT里面会有详细介绍。
继续向下看
[BITS 16]
:用来指明此节一个16位代码段。
lgdt [GdtPtr]
:lgdt
指令用来将GdtPtr
这个结构体装入寄存器GDTR
中
cli
:关中断,对应的开中断指令是sti
这里面还出现了新的寄存器eax,下面的图说明了eax
,ax
,ah
,al
的关系:
00000000 00000000 00000000 00000000
|===============EAX===============|--32个0,4个字节,2个字,1个双字
|======AX=======|--16个0,2个字节,1个字
|==AH===| --8个0,1个字节
|===AL==|--8个0,1个字节
eax是32位的寄存器,但它实际上只是在原有的8086CPU的寄存器ax上增加了一倍的数据位数而已。所以eax和ax二者并不是独立的,而是整体与部分的关系。举例来说,对eax直接赋值,若更改了低16位自然会改变了ax值,同样ax又会影响eax整体。而ah,al寄存器和ax之间的关系也是如此。同样还有ebx,ecx,edx。(上面摘抄互联网并作了一些补充)
IA32还加了两个段寄存器fs
与gs
。用来减缓es
的压力。用法与es
相同。
上面这些指令比较生疏。其他的指令在学习8086汇编的时候都是学过,比较熟悉。
那指令都认识了,但是对于上面代码的运行还是一头雾水,接下来对代码的含义进行分析。
看上面的代码,
程序首先被加载到内存07c00h
处,然后直接跳转到LABEL_BEGIN
处。
在LABEL_BEGIN处,程序首先使ds
和es
寄存器指向与cs
相同的段,上节里面说了,这是为了以后进行数据操作的时候能定位到正确的位置。然后初始化栈。
接下来初始化32位代码段描述符(32位代码段就是指程序最下面的[SECTION .s32]
)。这段初始化代码就是将下面那个32位代码段基址写到GDT
中对应的描述符结构中。你看GDT结构体中LABEL_DESC_CODE32
那一项的段界限与属性都定义好了,只有段基址没有定义,上面关于初始化32位代码描述符的作用就是初始化描述符中的基址。
要说上面的这步是干什么的,那么首先需要了解IA32的寻址过程了,下面有详细的介绍。不了解的需要先跳到下面关于GDT的讲解,再回来继续看。
到这里,GDT
已经初始化好了,接下来的lgdt [GdtPtr]
是把GDT
的基地址和段界限加载到GDTR
这个寄存器中。看看上面的GdtPtr
结构体,它可不是随意定义的。它的结构与gdtr
寄存器的结构是相同的,看看下面gdtr的结构,在对比上面介绍的GdtPtr,你就知道了。
32位基址 | 16位界限 |
再下面是关中断,因为进入保护膜是之后中断处理机制与现在是不同的,所以在进入之前需要关中断。如果不关中断将会出现错误。
关中断之后的代码就是纯粹为了进入保护模式做准备的了。这里主要有两个步骤:
首先打开地址线A20
。关于A20,书上是这样说的:
那么什么是A20呢?这又是一个历史问题。8086中,“段:偏移”这样的模式能表示的最大内存是FFFF:FFFF,即10FFEFh。可是8086只有20位的地址总线,只能寻址到1MB,那么如果试图访问超过1MB的地址时会怎样呢?实际上系统并不会发生异常,而是回卷(wrap)回去,重新从地址零开始寻址。可是,到了80286时,真的可以访问到1MB以上的内存了,如果遇到同样的情况,系统不会再回卷寻址,这就造成了向上不兼容,为了保证百分之百兼容,IBM想出一个办法,使用8042键盘控制器来控制第20个(从零开始数)地址位,这就是A20地址线,如果不被打开,第20个地址位将会总是零。显然,为了访问所有的内存,我们需要把A20打开,开机时它默认是关闭的。这里打开A20的方式是让
92h
这个端口的第1位(从低位0开始)的值置为1
接下来将cr0
这个寄存器的第0位置为1。为什么要这么做呢?这是因为当该位为0时,CPU运行于实模式,为1时,运行于保护模式。所以当将cr0的第0位置1之后,我们就相当欲闭合了进入保护模式的开关。
也就是说,“mov cr0, eax”这一句之后,系统就运行于保护模式之下了。但是,此时cs的值仍然是实模式下的值,我们需要把代码段的选择子装入cs。所以,我们需要第71行的jmp指令:
根据寻址机制我们知道,这个跳转的目标将是描述符DESC_CODE32对应的段的首地址,即标号LABEL_SEG_CODE32处。jmp dword SelectorCode32:0
到这里,执行jmp指令后,就真正进入了保护模式。
进入保护模式后,就开始运行[SECTION .s32]
段的代码。这段代码比较简单:就是在屏幕的第12行80列输出一个红色的P,然后进入无线循环。
至此,整个程序运行完毕。
上面的介绍中只是粗略的讲了一下GDT,下面对IA32为什么引入GDT进行详细介绍。
如果你熟悉Intel 8086汇编,那么你一定知道Intel 8086是16位的CPU,它有着16位的寄存器(Register)、16位的数据总线(Data Bus)以及20位的地址总线(Address Bus)和1MB的寻址能力。一个地址是由段和偏移两部分组成的,物理地址遵循这样的计算公式:
物理地址(Physical Address)=段值(Segment)×16+偏移(Offset)
其中,段值和偏移都是16位的。
从80386开始,Intel家族的CPU进入32位时代。80386有32位地址线,所以寻址空间可以达到4GB。所以,单从寻址这方面说,使用16位寄存器的方法已经不够用了。这时候,我们需要新的方法来提供更大的寻址能力。
在实模式下,16位的寄存器需要用“段:偏移”这种方法才能达到1MB的寻址能力,如今我们有了32位寄存器,一个寄存器就可以寻址4GB的空间,是不是从此段值就被抛弃了呢?实际上并没有,新政策下的地址仍然用“段:偏移”这样的形式来表示,只不过保护模式下“段”的概念发生了根本性的变化。实模式下,段值还是可以看做是地址的一部分的,段值为XXXXh表示以XXXX0h开始的一段内存。而保护模式下,虽然段值仍然由原来16位的cs、ds等寄存器表示,但此时它仅仅变成了一个索引,这个索引指向一个数据结构的一个表项,表项中详细定义了段的起始地址、界限、属性等内容。这个数据结构,就是GDT(还可能是LDT)。GDT中的表项也有一个专门的名字,叫做描述符(Descriptor)。
也就是说,GDT的作用是用来提供段式存储机制,这种机制是通过段寄存器和GDT中的描述符共同提供的。其中描述符有多种:代码段欲数据段描述符、系统段描述、门描述符。上面的程序用到了代码段的描述符,它的结构如下:
上面除了BYTE5和BTYE6中的一堆属性看上去有点复杂以外,其他三个部分倒还容易理解,它们分别定义了一个段的基址和界限。不过,由于历史问题,它们都被拆开存放。至于那些属性,我们暂时先不管它。
好了,我们回头再来看看代码,Descriptor这个宏用比较自动化的方法把段基址、段界限和段属性安排在一个描述符中合适的位置,有兴趣的读者可以研究这个宏的具体内容。本例的GDT中共有3个描述符,为方便起见,在这里我们分别称它们为DESC_DUMMY、DESC_CODE32和DESC_VIDEO。其中DESC_VIDEO的段基址是0B8000h,顾名思义,这个描述符指向的正是显存。
现在我们已经知道,GDT中的每一个描述符定义一个段,那么cs、ds等段寄存器是如何和这些段对应起来的呢?你可能注意到了,在[SECTION.s32]这个段中有两句代码是这样的:
mov ax, SelectorVideo
mov gs, ax
看上去,段寄存器gs的值变成了SelectorVideo,我们在上文中可以看到,SelectorVideo是这样定义的:SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT。直观地看,它好像是DESC_VIDEO这个描述符相对于GDT基址的偏移。实际上,它有一个专门的名称,叫做选择子(Selector),它也不是一个偏移,而是稍稍复杂一些,它的结构如图3.5所示。
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
描述符索引 | TI | RPL |
不难理解,当TI和RPL都为零时,选择子就变成了对应描述符相对于GDT基址的偏移,就好像我们程序中那样。这点还是不太了解
看到这里,你肯定已经明白了mov [gs:edi], ax
的意思,gs值为SelectorVideo,它指示对应显存的描述符DESC_VIDEO,这条指令将把ax的值写入显存中偏移位edi的位置。
上面关于GDT的内容引用自书本。
到这里,整个程序讲解完毕。
书上还有关于描述符属性的详细解释和突破软盘引导512字节的限制。