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

转载于:https://www.cnblogs.com/nzhl/p/6111418.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《操作系统设计与实现PDF》是一本关于操作系统设计和实现的书籍。在这本书中,作者主要介绍了操作系统的基本概念、原理和设计技术。 首先,本书从操作系统的基本原理开始介绍,包括进程管理、内存管理、文件系统等内容。作者通过简单易懂的方式解释了这些原理的概念和作用,帮助读者建立对操作系统的基本认识。 其次,本书重点关注操作系统的设计技术。作者详细介绍了操作系统的体系结构、内核设计以及设备驱动程序的开发等方面的内容。通过深入讲解这些技术,读者可以了解到操作系统的内部工作原理和设计思路。 此外,本书还涵盖了操作系统实现技术。作者介绍了一些常用的操作系统实现方法和工具,如编译器、调试器和虚拟化技术等。通过学习这些实现技术,读者可以了解如何将操作系统理论转化为实际可用的软件。 总体而言,《操作系统设计与实现PDF》是一本很好的操作系统学习资料。它不仅涵盖了操作系统的基本原理和设计技术,还介绍了操作系统实现方法和工具。无论是想深入了解操作系统原理还是学习如何实现一个操作系统,读者都可以从这本书中获得很多有用的知识。 ### 回答2: 《操作系统设计与实现pdf》是一本介绍操作系统设计和实现的电子书籍。在这本书中,作者详细介绍了操作系统的基本原理、设计思想和实践方法。 首先,书中讲解了操作系统的作用和重要性。操作系统是计算机系统中最核心的组成部分之一,它负责管理计算机硬件资源,为用户提供友好的界面和丰富的功能。了解操作系统的设计与实现对于理解计算机系统的工作原理和优化系统性能非常重要。 其次,书中详细阐述了操作系统的组成和功能。操作系统由内核、进程管理、内存管理、文件系统、设备管理等模块组成。作者深入浅出地介绍了每个模块的设计思想和实现方法,帮助读者理解操作系统的整体架构和各个模块的工作原理。 此外,书中还涉及了操作系统的调度算法、并发控制、死锁处理和内存管理等重要问题。这些问题是操作系统设计和实现中常见的挑战和难点,作者从理论到实践层面都进行了全面而深入的讲解,帮助读者理解和解决这些问题。 最后,书中还介绍了一些实际的操作系统案例和应用。通过分析和比较不同的操作系统,读者可以更好地理解操作系统的设计和实现方法,并且根据实际需求选择和优化操作系统。 总之,《操作系统设计与实现pdf》是一本系统全面介绍操作系统设计与实现的优秀电子书籍。通过阅读这本书,读者可以理解操作系统的基本原理,掌握操作系统的设计和实现方法,提高系统性能和优化计算机资源的利用效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值