实模和保护模式的概念

接下来比较重要的一点就是从实模式到保护模式的切换了. 这一块概念比较多, 我也是参考了多份资料才分析清楚了之间的关系. 好在这部分网上相关的文章很多, 这里我挑重点用自己的理解概括一下.

CPU有多种工作模式, 这里我们提一下实模式和保护模式.

  1. 实模式 : 操作系统刚启动就算实模式, 这种模式下, 寻址是通过 : 物理地址 = 段值 * 16 + 偏移实现的 (由于8086时代寄存器和数据总线都是16位, 而地址总线是20位),由于段值和偏移均为16位(段值通常是存在DS、ES、FS、GS、SS这些段寄存器中的), 所以最终寻址能力只有1M.

  2. 保护模式 : 从80386开始, 寄存器和地址总线均升级到了32位, 此时CPU获得了最大4GB的寻址能力. 但是这时候采用了一种新的策略, 段值不再是地址的一部分, 它成为了一个索引, 这里我们引入GDT(global descriptor table)的概念. 在GDT中, 内存被分割成了很多个段, 表中的每一项分别代表一个段, 分别记录了每一个段的起始位置, 界限(端的具体大小), 属性(操作权限)等等. 这张表具体长什么样, 网上图片很多自己可以去搜索. 这里我用C语言中的结构体来表示.

typedef struct
{
	uint16_t limit_low;     // 段界限   15~0
	uint16_t base_low;      // 段基地址 15~0
	uint8_t  base_middle;   // 段基地址 23~16
	uint8_t  access;        // 段存在位、描述符特权级、描述符类型、描述符子类别
	uint8_t  granularity; 	// 其他标志、段界限 19~16
	uint8_t  base_high;     // 段基地址 31~24
} __attribute__((packed)) gdt_entry_t;

这里__attribute__ ((packed))的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。由此我们也可以发现这个结构体正好是8个字节.
那么这个时候. 这里指的注意的是, 段寄存器不像通用寄存器已经升级为了32位, 仍然是16位, 之前说到它保存的不再是地址的一部分而是一个索引, 现在这个值有个特别的名字叫做选择子, 具体结构是 : 前两位是RPL(request privilege level), 代表的是用什么权限去访问, 第三位是TI(table indicator), 0表示查找GDT, 1表示查找LDT. 剩下的13位才表示的是表中的索引.

但是这时候其实还有一个概念没有说, 就是CPU如何找到这个GDT呢? 这里靠的就是GDTR(global descriptor table register), 这也是80386的一个改进, 增加了48位的寄存器GDTR. 他的结构更加简单, 前16位表示GDT的界限(表的具体大小), 后32位表示表的位置. 这里你会发现前16表示的表最大也只能是64k, 这与选择子中使用13位所能索引的最大位置8k * 8(每一项8字节) = 64k保持了一致.

从这里可以看到保护模式的好处就在于 -----> 寻址能力增大以及增加了权限机制保证了安全性.

切换到保护模式的工作

现在可以明确一下下一步要做的工作了, 即切换到保护模式. 那么切换到保护模式之前我们必须要保证 :

  1. 完成GDT初始化, 对其中各个表项进行正确赋值
  2. 加载GDTR, 确保其值正确.
  3. 打开A20
  4. 置cr0的PE位
  5. 跳转进入保护模式.

首先是GDT初始化, C语言版本 :

#define GDT_LENGTH 5

// 全局描述符表定义
gdt_entry_t gdt_entries[GDT_LENGTH];

// GDTR
gdt_ptr_t gdt_ptr;

// 全局描述符表构造函数,根据下标构造
static void gdt_set_gate(int32_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t gran);

// 声明内核栈地址
extern uint32_t stack;

// 初始化全局描述符表
void init_gdt()
{
	// 全局描述符表界限 e.g. 从 0 开始,所以总长要 - 1
	gdt_ptr.limit = sizeof(gdt_entry_t) * GDT_LENGTH - 1;
	gdt_ptr.base = (uint32_t)&gdt_entries;

	// 采用 Intel 平坦模型
	gdt_set_gate(0, 0, 0, 0, 0);             	// 按照 Intel 文档要求,第一个描述符必须全 0
	gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF); 	// 指令段
	gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF); 	// 数据段
	gdt_set_gate(3, 0, 0xFFFFFFFF, 0xFA, 0xCF); 	// 用户模式代码段
	gdt_set_gate(4, 0, 0xFFFFFFFF, 0xF2, 0xCF); 	// 用户模式数据段

	// 加载全局描述符表地址到 GPTR 寄存器
	gdt_flush((uint32_t)&gdt_ptr);
}

这里gdt_set_gate用于设置GDT中的表项, 代码如下 :

static void gdt_set_gate(int32_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t gran)
{
	gdt_entries[num].base_low     = (base & 0xFFFF);
	gdt_entries[num].base_middle  = (base >> 16) & 0xFF;
	gdt_entries[num].base_high    = (base >> 24) & 0xFF;

	gdt_entries[num].limit_low    = (limit & 0xFFFF);
	gdt_entries[num].granularity  = (limit >> 16) & 0x0F;

	gdt_entries[num].granularity |= gran & 0xF0;
	gdt_entries[num].access       = access;
}

同时最后一行的作用是加载GDTR, 目前只看前两行即可 :

[GLOBAL gdt_flush]

gdt_flush:
	mov eax, [esp+4]  ; 参数存入 eax 寄存器
	lgdt [eax]        ; 加载到 GDTR [修改原先GRUB设置]
;@---------     ignore -------------------------------
	mov ax, 0x10      ; 加载我们的数据段描述符
	mov ds, ax        ; 更新所有可以更新的段寄存器
	mov es, ax
	mov fs, ax
	mov gs, ax
	mov ss, ax
	jmp 0x08:.flush   ; 远跳转,0x08是我们的代码段描述符
			  ; 远跳目的是清空流水线并串行化处理器
.flush:
	ret

下面是另外一种 :

%include	"pm.inc"	; 常量, 宏, 以及一些说明

    org	07c00h
    jmp	LABEL_BEGIN

[SECTION .gdt]
; GDT
;                              段基址,       段界限     , 属性
LABEL_GDT:	   Descriptor       0,                0, 0           ; 空描述符
LABEL_DESC_CODE32: Descriptor       0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段
;@ 0B8000 - 0BFFFF 该段地址映射文本模式的显存, 这是固定的.
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    ;@eax 归0
    ;@接下来两行模拟从实模式寻址, 并将地址存在eax当中.
	mov	ax, cs                
	shl	eax, 4
	add	eax, LABEL_SEG_CODE32

    ;@将描述符表中第二项的基地址改为32位代码段的入口
	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 作准备
    ;@接下来两行模拟从实模式寻址, 并将地址存在eax当中.
	xor	eax, eax
	mov	ax, ds
	shl	eax, 4
	add	eax, LABEL_GDT		; eax <- gdt 基地址
    ;@将GDT指针的入口改为GDT真实的入口, 上面初始化为0了.
	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]

在开头的.gdt段中, 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 字节
;

步骤中的前两点已经给出了很清楚的解释, 接下来解释一下什么叫打开A20. 因为早期实模式下寻址范围最多1M, 那么如果如果试图访问超过1M的地址时, 实际系统会回卷(wrap)到零开始重新寻址, 那么后来寻址能力已经突破到4G了, 这样显然是不行的, 必须禁用这种回卷的机制. 但是如果直接禁用的话又不能保证系统的向下兼容性, 于是乎便出现了A20地址线的开关, 默认是关闭的, 此时系统保持向下兼容性, 如果打开, 则不再回卷, 寻址能力增加到4G, 这就是所谓的打开A20. 具体的打开方式有很多, 这是其中一种(我也不懂, 反正照这些就行了).

接下来的一点, 真正决定CPU工作模式的, 是控制寄存器CR0(在80386此类寄存器共有5个), 这里我们只需要将CR0的PE位(其实就是0号位), 从0修改为1, CPU就将从默认的实模式改为保护模式.

至于为什么是jmp dword + 地址 而不是jmp + 地址, 书中给出的解释是不加的话编译出来的代码仍然是16位的(注意此时仍然处于[BITS 16]的作用域中), 这样的话如果后面的地址很大, 超过16位会被截断(因为我们是调到32位的代码中), 所以必须要这么写. 至此从实模式到保护模式的过程就圆满结束了.

posted on 2016-11-28 21:13  内脏坏了  阅读(290)  评论(0编辑  收藏  举报