zoukankan      html  css  js  c++  java
  • 操作系统的实现(1)

    实模和保护模式的概念

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

    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位的代码中), 所以必须要这么写. 至此从实模式到保护模式的过程就圆满结束了.

  • 相关阅读:
    java内存模型
    类、对象和接口
    Python--数据存储:pickle模块的使用讲解
    Python--常用的内置函数
    Python--迭代器和生成器的定义和案例
    Python--作业2--对员工信息文件,实现增删改查操作
    Python--文本基本操作
    Python--字符串基本操作
    Python--字典基本操作
    Python--作业1--购物车程序
  • 原文地址:https://www.cnblogs.com/nzhl/p/6111418.html
Copyright © 2011-2022 走看看