本文所谈论到的cpu是Intel的x86体系架构的cpu,作者才疏学浅,其他体系的暂不讨论。本文跳转的保护模式是32位保护模式,所涉及到的通用寄存器均是32位。
读者可以未经作者允许随意转载,但请注明出处并且保证文章的完整性
本文主要谈一下几个方面:
1、保护模式和实模式是个什么鬼
2、怎么由保护模式进入实模式
3、实模式跳转到保护模式的示例代码
在讲之前照例讲几句闲话:
1、实模式是操作系统之源,没有它,操作系统就是无本之木、无水之源
2、现在操作系统的书很少会有涉及这个方面的内容,可能这部分不属于操作系统原理的范畴,但是并不代表不重要
3、没有这部分知识,对OS的一些谈论都只能停留在我知道XXX是这么设计的而不是我尝试过
一、实模式和保护模式
X86体系的cpu有两种工作模式:实模式和保护模式,实模式就是之前8086的运行状态,寻址能力差一些,没有保护机制。进而引入了保护模式,保护模式说白了也就是在保护内存的访问,以防止程序访问了其他程序的数据;实模式下16位数据线和20位地址线,所以寻址能力只有1M,但是保护模式32条地址线 ,寻址能力可以达到4G。
计算机刚开机的时候运行在实模式下(这个刚开机指的是刚刚加电,CPU RESET值产生了以后,这时候CPU就可以工作了,顺便一提这时候BIOS还在ROM中没有加载进来),之后经过一系列的操作,进入保护模式。
关于实模式和保护模式,暂且讨论这么多。
二、怎么进入保护模式
1、实模式和保护模式下的寻址
前面讲到实模式有20位地址线,但是保护模式有32位地址总线。实模式下的地址是段基址:偏移量。段基址存在cs(代码段寄存器),ds(数据段寄存器中)等等这些段寄存器中。16位段寄存器的内容乘以16(10H),加上16位偏移地址形成20位的物理地址,所以实模式下可以表示段的最大长度是64K(16位偏移量可以寻64k的地址)。
到了保护模式下,地址变成了32位。因为保护模式允许段的长度是0~4G,也就是说段基址和偏移量都变成了32位。那么问题来了, 16位的段寄存器(保护模式下段寄存器还是16位)怎么表示32位段基址呢?intel提出了一个解决方案:把段基址存在一个表里,段寄存器里边填充的内容就是子项这个表的索引,这个索引被称为段选择子,这个表被称为GDT(Global Description Table,全局描述符表),表里填充的每一项内容被称为描述符。
前面我们还提到了,保护模式对内存访问提供了保护机制。是怎么保护的呢?权限,这也是最节省空间的一种保护方式了。所以不难理解描述符并不仅仅是由段基址构成,还有各种各样的权限。
2、进入保护模式之前需要做什么
根据之前的描述首先要做的就是填充gdt表,没有这个表cpu就没办法寻址。
其次,386之后的每个cpu既支持实模式也支持保护模式,也就是说地址总线既可以20位也可以32位。这个是怎么做到的呢?很简单,计算机刚开机的时候地20位地址总线(A20)处于关闭状态,等要进入保护模式的时候再打开。地址总线不是20位就是32位,所以这里我们并不需要把后面的每一位挨个打开,只需要打开A20就好了。
这里还要提一下计算机怎么知道现在处于那个模式下的呢。答案就存在cr0寄存器里边。计算机有四个很重要的控制寄存器,cr0,cr2,cr3,cr4(cr1并当前没有使用)。这四个寄存器估计很多人对cr3很熟悉,cr2和cr3都是在用于支持分页机制的,这个不是本文的重点,不再详述。cr0是存储控制操作模式和处理器状态的寄存器,是个32位寄存器,但是里边有很大一部分当前并没有使用,我们本文只需要说明这个寄存器的第0位——PE位。这一位就是用来表示计算机当前处于实模式还是保护模式——0表示实模式,1表示保护模式。计算机刚开机的时候这一位是0,所以我们跳转到保护模式之前的最后一个步骤就是把这一位置1。
大概捋一下这个过程就是:
1、申请gdt并填充内容,
2、加载gdtr(专门给gdt用的寄存器)
3、打开A20
4、设置PE位
5、激动人心的jmp
没错就是这样的,这样就进入了保护模式了,有没有很激动,有没有很开心。那让我们撸起袖管、摩拳擦掌开始写代码吧~~~顺便趁着你开心讲明白,这部分代码要用汇编来实现。不过你不用担心,都是很简单的代码,连逻辑都没有就实现上面我们总结的五个步骤就可以了。
三、示例代码
贴上代码:
[section .gdt]
LABLE_GDT: Descriptor 0, 0,0 ;空描述符
LABLE_DESC_CODE32: Descriptor 0,SegCode32Len-1,DA_C + DA_32 ;
GdtLen equ $-LABLE_GDT ;gdt长度
GdtPtr dw GdtLen-1 ;gdt界限
dd 0 ;gdt基地址
SelectorCode32 equ LABLE_DESC_CODE32 - LABLE_GDT
DA_32 EQU 4000h
DA_C EQU 98h
SegCode32Len equ $-LABLE_SEG_CODE32
%macro Descriptor 3
dw %2 & 0FFFFh
dw %1 & 0FFFFh
db (%1 >> 16) & 0FFh
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)
db (%1 >> 24) & 0FFh
%endmacro
上面都是一些需要初始化的数据,第一个是申请一个空的是一个空的gdt表,段基址段界限和段属性都是0,第二个是保护模式下要执行的代码所在的代码段的描述符,段基址是0,段界限是$-LABLE_SEG_CODE32,属性是40098h,在这里我先说这个代表的意思是这是一个32位、存在于内存中的、可执行代码段,描述符的格式每一本操作系统的书都有描述,请读者自行查阅,把40098h换成2进制并查看为1的位对应的意思即可,在此不详述。
下面来看代码的执行:
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,LABLE_SEG_CODE32
mov word[LABLE_DESC_CODE32 + 2],ax
shr eax,16
mov byte [LABLE_DESC_CODE32 + 4],al
mov byte [LABLE_DESC_CODE32 + 7],ah
;为加载gdtr做准备
xor eax,eax
mov ax,ds
shl eax,4
add eax,LABLE_GDT
mov dword [GdtPtr+2],eax ;GdtPtr+2表示gdt的基地址
lgdt [GdtPtr]
cli ;关中断
in al,92h
or al,00000010b
out 92h,al
;准备切换到保护模式
mov eax,cr0 ;修改PE位
or eax,1
mov cr0,eax
jmp dword SelectorCode32:0 ;修改cs值
用cs的值初始化ds(代码段寄存器),es(附加段寄存器),ss(堆栈段寄存器),并设置sp(堆栈指针寄存器)寄存器的初值位0100h。清除eax(32位通用寄存器)的值,对当前的cs进行一些加工填充到描述符里,对当前的ds进行一些加工用以填充gdt。加载gdtr,并关中断打开A20,修改PE位,跳转到保护模式下的代码段。