当前位置:   article > 正文

操作系统真象还原:保护模式入门

操作系统真象还原:保护模式入门

第4章-保护模式入门

这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件

Intel8086CPU由于自身设计存在诸多缺点,最致命的有两条:1、仅能寻址1MB内存空间;2、用户程序可以通过自由修改段基址来访问所有内存空间而引出的安全问题。所以后来的CPU自然就要解决以上的两个问题,CPU厂商为了凸显出自己新的CPU的安全性,将新开发出的CPU命名为工作在保护模式下——也就是提供了一种保护机制让程序不能随意访问所有内存空间,同时CPU的寻址范围也达到了4GB。而8086的那种工作模式由于保护模式的出现而被命名为实模式。这种具有新的工作模式的CPU被叫做IA32(工作在32位环境下)体系架构CPU,这是从80836CPU开始的一种架构。

但由于8086CPU在当时取得了非凡的市场成功,所以后来的IA32体系架构CPU必须兼容8086的那种工作模式,所以IA32体系架构CPU也必须可以运行在实模式下。8086只能运行实模式,它使用[段基址:偏移]这种寻址方式,所以IA32体系架构CPU为了兼容8086上开发的程序,也得用[段基址:偏移]这种模式,但是又同时为了能够寻址更大的地址空间以及获得安全性检查,所以就采用了将段寄存器提供的值(16位)不再作为段基址,而是作为一个选择子去GDT表中找到对应的表项,然后从这个表项中得到段基址(32位)与进行安全性检查。

由于IA32体系架构的CPU有两种主要工作模式,而BIOS加载MBR,MBR加载Loader的时候工作在实模式下,此后为了获得更大的地址空间,就必须从实模式切换到保护模式。模式的切换就意味着寻址方式的切换,所以在由实模式切换到保护模式的时候,就必须要在内存中初始化GDT表,也就是初始化GDT表中的表项(也叫段描述符)。

原文链接:https://blog.csdn.net/kanshanxd/article/details/130749718

保护模式下的被扩展的32位寄存器:
在这里插入图片描述
在这里插入图片描述

进入保护模式的三个步骤:

  1. 打开A20
  2. 加载gdt
  3. 将cr0的pe位置1

4.3全局描述符表

4.3.1段描述符

在这里插入图片描述

type:是用来指定本描述符的类型,配合S字段使用

S:判断当前是一个什么段描述符,0是系统段,1是数据段

DPL:描述符特权级字段,分别为0,1,2,3级特权,数字越小,特权级越大

P:判断段是否存在,存在为1,否则为0

AVL:可用字段

L:用来设置是否是64位代码段。L为1表示是64为代码段,否则为32为代码段

D/B:用来指示有效地址及操作数的大小,对于代码段来说此位是D位,若为0,表示指令中的有效地址和操作数为16位,指令有效地址用IP寄存器。为1,表示指令中的有效地址及操作数为32位,指令有效地址为EIP寄存器。 对于栈段来说,此位为B位,用来指定操作数大小,若为0,使用sp寄存器,也就是16为寄存器,为1,使用esp寄存器,也就是32为寄存器。

G:粒度,用来表示段界限的单位大小,为0表示段界限的单位为1,若为1,表示段界限的单位是4kb。

段界限边界值计算公式:

(描述符中段界限+1) * (段界限的粒度大小: 4KB或1) -1 。

这里我们使用的是平坦模式,G为1,段界限为0xFFFFF,段界限边界值为0xFFFFFFFF;

在这里插入图片描述

4.3.2全局描述符表GDT、局部描述符表LDT及选择子

在这里插入图片描述

在这里插入图片描述

GDT 中最多可容纳的描述符数量是 65536/8=8192个,即 GDT 中可容纳 8192 个段或门。

全局描述符表:GDT(对应的是GDTR寄存器指向这个表的内存,专有指令加载GDT,即lgdt),局部描述符表: LDT(对应的是LDTR寄存器指向这个表的内存,专有指令加载LDT,即lldt),TI位判断是存储在GDT(0)还是LDT(1)

选择子的作用主要是确定段描述符,确定描述符的目的,一是为了特权级、界限等安全考虑,最主要的还是要确定段的基地址。

虽然到了保护模式,但 IA32 架构始终脱离不了内存分段,即访问内存必须要用“段基址:段内偏移地址”的形式。保护模式下的段寄存器中已经是选择子,不再是直接的段基址。段基址在段描述符中,用给出的选择子索引到描述符后, CPU 了“段基址:段内偏移地址”的形式。 自动从段描述符中取出段基址,这样再加上段内偏移地址,便凑成了“段基址:段内偏移地址”的形式。

例子:

在这里插入图片描述

4.3.3打开A20地址线

打开A20是因为现在CPU为了兼容以前的版本在实时模式下当访问 0x100000~0x10FFEF之间的地址时, CPU 将采用 8086/8088 的地址回绕 。所以现在我们在保护模式下我们要突破第20条地址线去访问更大的内存空间。所以我们要关闭地址回绕,也就是打开A20 Gate,以下是代码实现:

#打开 A20Gate 的方式是极其简单的,将端口 Ox92 的第 1 位置 1 就可以了
in al,0x92
or al,0000_0010B
out 0x92,al
  • 1
  • 2
  • 3
  • 4
4.3.4保护模式的开关,CR0寄存器的PE位

控制寄存器是 CPU 的窗口,既可以用来展示 CPU的内部状态,也可用于控制 CPU 的运行机制 。

CRO寄存器的第 0 位,即 PE 位, Protection Enable,此位用于启用保护模式,是保护模式的开关。PE为0表示在实时模式下运行,PE为1表示在保护模式下运行。
在这里插入图片描述

mov eax,cr0
or	eax,0x00000001
mov cr0,eax
  • 1
  • 2
  • 3
4.3.5进入保护模式

boot.inc

;--------------------------loader和kernel----------------

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;--------------------------gdt描述符属性-----------------
DESC_G_4k	equ	1_00000000000000000000000b
DESC_D_32	equ	1_0000000000000000000000b
DESC_L		equ	0_000000000000000000000b	;64位代码标记,此处标记为0便可是32位
DESC_AVL	equ	0_00000000000000000000b		;CPU不用此位,暂为0
DESC_LIMIT_CODE2	equ	1111_0000000000000000b
DESC_LIMIT_DATA2	equ	DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2	equ	0000_000000000000000b
DESC_P		equ	1_000000000000000b
DESC_DPL_0	equ	00_0000000000000b
DESC_DPL_1	equ	01_0000000000000b
DESC_DPL_2	equ	10_0000000000000b
DESC_DPL_3	equ	11_0000000000000b
DESC_S_CODE	equ	1_000000000000b
DESC_S_DATA	equ	DESC_S_CODE
DESC_S_sys	equ	0_000000000000b
DESC_TYPE_CODE	equ	1000_00000000b	;x=1,c=0,r=0,a=0代码段是可执行的,非一致性,不可读,以访问位a清0
DESC_TYPE_DATA	equ	0010_00000000b	;x=0,e=0,w=1,a=0数据段是不可执行的,向上扩展的,可写,已访问位a清0

DESC_CODE_HIGH4	equ	(0x00<<24)+DESC_G_4k+DESC_D_32+DESC_L+DESC_AVL+DESC_LIMIT_CODE2+DESC_P+DESC_DPL_0+DESC_S_CODE+DESC_TYPE_CODE+0x00

DESC_DATA_HIGH4	equ	(0x00<<24)+DESC_G_4k+DESC_D_32+DESC_L+DESC_AVL+DESC_LIMIT_DATA2+DESC_P+DESC_DPL_0+DESC_S_DATA+DESC_TYPE_DATA+0x00

DESC_VIDEO_HIGH4 equ	(0x00<<24)+DESC_G_4k+DESC_D_32+DESC_L+DESC_AVL+DESC_LIMIT_VIDEO2+DESC_P+DESC_DPL_0+DESC_S_DATA+DESC_TYPE_DATA+0x0b

;----------------------------------选择子属性----------------------------
RPL0	equ	00b
RPL1	equ	01b
RPL2	equ	10b
RPL3	equ	11b
TI_GDT	equ	000b
TI_LDT	equ	100b
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

loader.s

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
	jmp loader_start	;z主要是跳过一段gdt段描述符数据

;构建gdt及其内部的描述符
GDT_BASE: 
	dd	0x00000000	;GDT低32位 
	dd	0x00000000	;GDT高32位

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	;此时dp1=0
	    
	GDT_SIZE equ $-GDT_BASE
	GDT_LIMIT equ GDT_SIZE-1
	times 60 dq 0	;预留60个描述符的空位置 dp表示8字节
	SELECTOR_CODE	equ	(0x0001<<3)+TI_GDT+RPL0	;相当于( CODE_DESC - GDT_BASE) / 8 + TI_GDT + RPLO
	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:
	mov sp, LOADER_BASE_ADDR
	mov bp, loadermsg ;ES:BP = 字符串地址,承接上面的是显存文字段区域
	mov cx, 17 ;ex =字符事长度
	mov ax, 0x1301 ;AH = 13, AL = Olh
	mov bx, 0x001f ;页号为 0 (BH = 0 )蓝底粉红字( BL = lfh)
	mov dx, 0x1800
	int 0x10 ;INT 0x10    功能号:0x13    功能描述:打印字符串

;一一一一一一一一一一 准备进入保护模式 一一一一一一一一一一一
;1.打开A20
;加载gdt
;将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	;刷新流水线

[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 $

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

这里我们的操作系统是在平坦模式下的,所以整个4GB 32位系统就只有一个段,所以他的代码段和数据段,都一样;显示段就是显示器映射的那一部分

4.4处理器架构简介

4.4.1流水线

CPU 是按照程序中指令顺序来填充流水线的,也就是说按照程序计数器 PC(x86中是 CS: ip)中的值来装载流水线的,当前指令和下一条指令在空间上是挨着的 。如果当前执行的指令是jmp,下一条指令已经被送上流水线译码了,第三条指令已经被送上流水线取指啦 仔细想想看,其实这个流水线没用了,因为 CPU 早已经跳到别处去执行了,第二、三条指令用不上了,所以 CPU 在遇到无条件转移指令 jmp 时,会清空流水线

4.4.2乱序执行

乱序执行,是指在 CPU 中运行的指令并不按照代码中的顺序执行,而是按照一定的策略打乱顺序执
行,也许后面的指令先执行,当然,得保证指令之间不具备相关性 。

乱序执行的好处:就是后面的操作可以放到前面来做,利于装载到流水线上提高效率 。

4.4.3缓存

低速存储设备是整个系统的瓶颈,缓存用来缓解“瓶颈设备”的压力。

DRAM:动态随机访问存储器

SRAM:静态随机存储器,现在CPU的缓存都是它。

4.4.4分支预测

4.5使用远跳转指令清空流水线

段描述符缓冲寄存器在 CPU 的实模式和保护模式中都同时使用,在不重新引用一个段时,段描述符
缓冲寄存器中的内容是不会更新的,无论是在实模式,还是保护模式’下, CPU 都以段描述符缓冲寄存器
中的内容为主。实模式进入保护模式时,由于段描述符缓冲寄存器中的内容仅仅是实模式下的 20 位的段
基址,很多属性位都是错误的值,这对保护模式来说必然会造成错误,所以需要马上更新段描述符缓冲寄
存器,也就是要想办法往相应段寄存器中加载选择子。

既要改变代码段描述符缓冲寄存器的值,又要清空流水线 。

代码段寄存器 cs,只有用远过程调用指令 call、远转移指令 jmp、远返回指令 retf 等指令间接改变,
没有直接改变 cs 的方法,如直接 mov cs, xx 是不行的。 故,用无条件远跳转指令 jmp 来解决上述两个问题将是一举两得的做法

4.6保护模式之内存段的保护

主要体现在段描述符的属性字段中。每个字段都不是多余的。这些属性只是用来描述一块内存的性质,是用来给 CPU 做参考的,当有实际动作在这片内存上发生时, CPU 用这些属性来检查动作的合法性,从而起到了保护的作用。

4.6.1向段寄存器加载选择子时的保护

首先根据选择子的值验证段描述符是否越界:描述符表基地址+选择子中的索引值*8+7<=描述符表基地址+描述符表界限值。

  1. 不超过描述符表的界限(描述符表基地址 + 描述符表界限值) 确定了描述符表的结束位置。条件确保选择的段描述符的地址不会超出这个范围。
  2. 指向正确的段描述符(描述符表基地址 + 段选择子索引 * 8) 是段选择子所指示的描述符在描述符表中的位置。由于每个描述符占用 8 字节(在 x86 中),因此乘以 8 就是为了找到正确的描述符。
  3. 最后一个字节的偏移不超过描述符表界限7 表示了描述符的长度(通常是 8 字节)减去 1,因为索引是从 0 开始的。所以要确保选择的描述符不超过这个界限,需要加上这个偏移量。

**+7:**他是从描述符表的基地址开始的,我们思考当我们现在只有0段时,他是从基地址开始的为基地址加上7字节就为一个0段,大小为8,他的界限为7,所以这也是这个加7的来源,后面剩余的段就大小是8,界限也是下一个开始不包含当前这个地址内容,为地址加8.

在选择子检查过后,就要检查段的类型了。 这里主要是检查段寄存器的用途和段类型是否匹配。

  • 只有具备可执行属性的段(代码段〉才能加载到 cs 段寄存器中。
  • 只具备执行属性的段(代码段〉不允许加载到除 cs 外的段寄存器中。
  • 只有具备可写属性的段(数据段)才能加载到 SS 枝段寄存器中。
  • 至少具备可读属性的段才能加载到 DS 、 ES 、陀、 GS 段寄存器中。

在这里插入图片描述

检查完 type 后,还会再检查段是否存在

4.6.2代码段和数据段的保护

对于代码段和数据段来说, CPU 每访问一个地址,都要确认该地址不能超过其所在内存段的范围 。

实际段界限值为:(描述符中段界限+1)*(段界限的粒度大小:4k或者1)-1 。

对于代码段来说,段基址存放在CS寄存器中,段内偏移地址,即有效地址,存放在EIP寄存器中。他要满足以下条件:EIP中的偏移地址+指令长度-1<=实际段界限大小

对于数据段也要遵循以下原则:偏移地址+数据长度-1<=实际段界限大小

4.6.3栈段的保护

段描述符 type 中的 e 位用来表示段的扩展方向,但它和别的描述符属性一样,仅仅是用来描述段的性质,即使 e 等于1向下扩展,依然可以引用不断向上递增的内存地址,即使e等于0向上扩展,也依然可以引用不断向下递减的内存地址。

检查是否超越段界限:

  • 对于向上扩展的段,实际的段界限是段内可以访问的最后一字节。
  • 对于向下扩展的段,实际的段界限是段内不可以访问的第一个字节。

内存分布

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/728494
推荐阅读