本文参考书籍
1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言 从实模式到保护模式
ps:基于x86硬件的pc系统
保护模式相关介绍
从实模式进入保护模式其实经历三个步骤就可以了,第一步,加载gdt,第二步,打开A20,第三步,置cro为1.
gdt介绍
首先简单介绍一下保护模式下的分段机制
由于在保护模式下,访问地址也是通过段基址加段内偏移来实现的,由于32位寄存器的寻址能力就是4GB,在保护模式下,段赋予了更多的属性和检查,以达到对代码、数据结构、程序和任务的保护。
分段机制就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存块单元。80386虚拟地址空间中的虚拟地址由一个段部分和一个偏移部分构成。段是虚拟地址到线性地址转换机制的基础。
每个段由以下几个参数定义;
1、段基址:指定段在线性地址空间中的开始地址,基地址是线性地址,对应于段中偏移0处;
2、段限长:是虚拟地址空间中段内最大可用偏移位置,定义了段的长度;
3、段属性:指定段的特性,例如段是否可读、可写或段的特权级等。
段限长定义了在虚拟地址空间中段的大小,段基址和段限长定义了段所映射的线性地址范围或区域。段内0到limit的地址范围对应线性地址中的base到base+limit。偏移量大于段限长地址是无意义的,多个段映射到线性地址中的范围可以部分重叠或覆盖。
段的基地址、段限长以及段的保护属性存储在一个称为段描述符的结构项中。在逻辑地址到线性地址的转换映射过程中会使用这个段描述符。
逻辑地址由16位的段选择符和32位的偏移量构成,如图所示;段选择符指定字节所在段,而偏移量指定该字节在段中相对于段基地址的位置。
为了把逻辑地址转换成一个线性地址,一般会执行以下操作;
1、使用段选择符中的偏移值在GDT或LDT表中定位相应的段描述符;
2、利用段描述符检验段的访问权限和范围,以确保该段是可访问的并且偏移量位于段界限内;
3、把段描述符中取得的段基地址加到偏移量上,最后形成一个线性地址。
如果没有开启分页,那么处理器直接把线性地址映射到物理地址,如果对线性地址空间进行了分页处理,那么就会使用二级地址转换把线性地址转换成物理地址。
段描述符表
段描述符表是段描述符的一个数组,如图所示,段描述符的长度可变,最多包含8192个8字节描述符,由于gdt的低16位是界限值,选择子一个是8字节所以,2的16次方除以8字节就是8192个,锁一个段描述符表最大包含8192个段描述符。描述符表又分为两种:全局描述符表GDT和局部描述符表LDT。
段选择符
段选择符是段的一个16位标识符,段选择符并不直接指向段,而是指向段描述符表中定义段的段描述符,段选择符的3个字段分别是请求特权级RPL、表指示标志T1、索引值。请求特权级字段RPL提供了段保护信息,表所以字段TI用来指出包含指定段描述符的段描述符表GDT或LDT,索引字段给出了描述符在GDT或LDT表中的索引项号。
段描述符
段描述符用于向处理器提供有关一个段的位置和大小信息以及访问控制的状态信息,如图所示。每个段描述符的长度是8个字节,含有三个主要字段:段基址、段限长和段属性。段描述符通常由编译器、链接器、加载器或者操作系统创建。有关段描述符中的具体含义可自行了解。
A20地址线
实模式下内存访问采用了段基址加段内偏移,由于在实模式下地址线是20位,最大寻址空间是1MB,超出1MB内存的部分在逻辑上也是正常的,但物理内存却没有对应的部分,为了解决该问题,cpu采取了将超过1MB的部分自动回绕到0地址,继续从0地址开始映射,相当于把地址对1MB求摸,超过1MB多余出来的内存被称为高端内存区HMA。
对于只有20位地址总线的cpu,不需要任何额外的操作便能自动实现地址回绕。由于地址进位到1MB以上,如0x100000,由于没有多余的地址线,相当于进位丢失,变成了0x00000,所以实现了自动回绕。
对于后续cpu通过20GATE来控制A20地址线,如果有该总线,当该地址线是开启的时候,如果访问0x100000~0x10FFEF之间的内存时,系统将直接访问这块物理内存,并不会像20根总线那样回绕。
为了解决此问题的第21根地址线就称为A20GATE;
如果A20GATE被打开,当访问0x100000~0x10FFEF之间的地址时,cpu将访问这块物理内存。
如果A20GATE被禁止,当访问0x100000~0x10FFEF之间的地址时,cpu将地址回绕。
保护模式的开关,cro寄存器的PE位
CR0寄存器的第0位,即PE位,此为用于启动保护模式,是保护模式的开关。
相关代码如下;
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start ; 此处的物理地址是:
;构建gdt及其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl已改为0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 此处预留60个描述符的slot
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上
;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
loadermsg db '2 loader in real.'
loader_start:
;------------------------------------------------------------
;INT 0x10 功能号:0x13 功能描述:打印字符串
;------------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX=字符串长度
;(DH、DL)=坐标(行、列)
;ES:BP=字符串地址
;AL=显示输出方式
; 0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
; 1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
; 2——字符串中含显示字符和显示属性。显示后,光标位置不变
; 3——字符串中含显示字符和显示属性。显示后,光标位置改变
;无返回值
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg ; ES:BP = 字符串地址
mov cx, 17 ; CX = 字符串长度
mov ax, 0x1301 ; AH = 13, AL = 01h
mov bx, 0x001f ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
mov dx, 0x1800 ;
int 0x10 ; 10h 号中断
;---------------------------------------- 准备进入保护模式 ------------------------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1
;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al
;----------------- 加载GDT ----------------
lgdt [gdt_ptr]
;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
;jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
jmp SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:160], 'P'
jmp $
按照如上所述步骤进入了32为保护模式。
Linux保护模式初始化过程
保护模式所需要的一些数据结构由处理器内存管理功能确定。处理器支持分段模型,可以使用单个、统一的地址空间平坦模型到每个任务都具有几个手保护地址空间的高度结构化的多段模型。处理器在切换到保护模式运行之前需要先设置好保护模式下使用的数据结构的基本信息,这些数据机构包括以下几种:
1、保护模式中断描述符表IDT;
2、全局描述符表GDT;
3、任务状态表TSS;
4、局部描述符表LDT;
5、若使用分页机制,则需设置页目录和页表;
6、处理器切换到保护模式下运行的代码段;
7、含有中断和异常处理程序的代码模块。
在切换到保护模式之前,软件初始化还必须设置全局描述符表基地址寄存器GDTR、中断描述符表基地址寄存器IDTR和控制寄存器CR1~CR3,在完成了这些操作后,通过设置CR0寄存器的保护模式标志PE(置0),处理器就可以切换到保护模式运行。
在刚进入保护模式中运行时,特权级是0,为了保证程序的兼容性,切换操作应该按照如下步骤进行:
1、禁止中断、使用CLI指令可以禁止可屏蔽硬件中断;
2、执行LGDT指令把GDT表的基地址加载进GDTR寄存器;
3、执行在控制寄存器CR)中设置PE标志;
4、在执行完成CR0指令之后立刻执行一个远跳转JMP或远调用CALL指令,这个操作通常是远跳转到货远调用指令流中的下一条指令;
5、若要使用局部描述符表,则执行LLDT指令把LDT段的选择符加载到LDTR寄存器中;
6、执行LTR指令,用初始保护模式任务的段选择符或者可写内存区域的段描述符加载任务寄存器TR。这个可写内存区域用于在任务切换时存放任务的TSS信息;
7、在进入保护模式后,段寄存器仍然含有在实模式时的内容,需要步骤4中的jmp或call指令重置cs寄存器,执行以下操作之一可以更新其余段寄存器的内容:其余段寄存器的内容可通过重新加载或切换到一个新任务来更新;
8、执行LIDT指令把保护模式IDT表的基地址和长度加载到IDTR寄存器中;
9、执行STI指令开启可屏蔽硬件中断。
Linux的保护模式初始化过程大致经历了如上步骤。