【操作系统】模式切换篇

news2025/1/26 15:41:46

CPU的模式

什么是CPU的模式?这和CPU的发展过程有关,最开始CPU是8位的,后来发展到16位,然后是32位,现在是64位,多少多少位指的是寄存器的位宽。CPU能使用的寄存器宽度以及CPU使用的指令等就构成了CPU的模式,比如16位模式和32位模式,注意除了寄存器,不同模式下CPU对指令的解释也是不同的,因此16位模式的程序是不能在32位模式下运行的。为了向后兼容,后来的CPU要能运行在之前CPU的模式下,比如32位CPU也能跑16位模式,这样在之前CPU上编写的程序也能在新CPU上运行。

说到CPU的模式,我们常常会看到16位实模式,32位保护模式这样的概念,单从字面上看,让人像个丈二的和尚。这两个概念其实包含了两对概念:

  • 16位和32位
  • 实模式和保护模式

16位和32位模式很好理解,它描述的是寄存器的宽度和CPU的指令。实模式和保护模式是个啥呢?要理解这两个概念,我们需要看下这两个概念的英文原文。

  • 实模式原文是real address mode(简称为real mode),全称真实地址模式。
  • 保护模式原文是protected virtual address mode,全称受保护的虚拟地址模式。

首先我们可以看到它们限定的实体都是寻址模式(address mode),也就是CPU访问内存的方式。CPU访问内存不都是二维数组的形式吗?为何还有虚实之分呢?

这里的虚和实指的是段寄存器中的值是(真实的)二维数组下标,还是(虚假的)段描述符偏移。由于虚拟寻址模式下可以控制内存是否允许被访问,所以它又多了一个限定:受保护的。这也意味着在真实寻址模式下,对任意内存的访问都是始终被允许的,是不具备段保护功能的。

保护模式下的段寻址

在切换到保护模式之前,我们还得再唠唠段寻址,上次唠了5毛钱的,这次再唠5毛钱。

首先来回顾一下段寻址的本质,所谓段寻址就是把一维数组抽象成二维数组,用二维数组下标来定位内存字节,CPU会帮我们把二维下标换算成一维下标,这一点依然没有改变。

实地址模式下段寄存器指哪打哪,CPU没有拒绝的理由,然而在保护模式下,CPU是可以拒绝非法的段寻址的,因此,保护模式保护的是段,而不是某个或某几个任意的内存字节。在保护模式下,CPU除了需要知道段的起始地址,还需要知道关于这个段的一些属性,比如段的类型,安全级别等等。这么多的信息,段寄存器肯定是放不下了,要知道即使是在32位(包括64位)模式下,段寄存器依然只有16位。

为了解决这个问题,我们需要把段的相关信息放到内存中,相比于寄存器,内存就如同大海一样。这些放在内存中的段信息称为全局描述符表,英文名Global Descriptor Table,简称GDT。全局描述符表中的每一项代表了一个段的信息,称为段描述符,英文叫Segment Descriptor,简称SD。与此同时,我们还要告诉CPU全局描述符表的起始地址和长度,这样CPU才知道GDT放在那里,这里GDT的起始地址和长度叫做GDT描述符。之后我们使用段寻址时,给到段寄存器的其实是SD相对于GDT起始地址的偏移量。32位保护模式下SD的大小为8字节,如果我们将段寄存器设置为0x8,那么CPU就会使用GDT中的第二个段描述符,如下图所示。注意GDT的第一个段描述符是null,也就是全零,设置这样一个非法的段描述符是为了防止从16位实地址模式切换到32位保护模式后忘记设置段寄存器引发的问题。

段描述符中主要包括段的起始地址,段的大小和段的属性3大部分,SD的结构如下图所示。

段描述符

段大小和段起始地址在这8个字节中并不是连续分布的,这样的设计我也想不通,像极了一开始没有预留足够的空间,后来又加上的。

注意Type字段有4个字节,X用来区分代码段还是数据段,中间两个比特在代码段和数段中的解释是不同的,在代码段中,C=0表示该段的代码不能被特权等级(DPL)比它低的段中的代码调用,也就是说只有特权等级更高或相同的段才能调用这个段中的代码,这就是保护模式的关键所在。

切换到32位保护模式:启动超级变换形态

我们不能总是让CPU在16位实地址模式下工作,在真正执行操作系统之前,我们必须将CPU切换到32位保护模式,一是因为效率更高,我们可以利用32位寄存器,二是因为32位保护模式下段寻址方式发生了变化,CPU可以拒绝非法的地址访问。

进入32位保护模式同时也意味着我们要离开BIOS了,因为BIOS是为16位指令编译的,没法在32位模式下使用。因此一旦切换到32位保护模式,包括驱动屏幕,键盘,硬盘等都需要我们自己来编写程序实现了,这绝对是大显身手的好机会,不管怎样,我们先切换过去再说。

切换到保护模式需要在内存中准备两个东西,一个是GDT,一个是GDT描述符。CPU提供了lgdt指令来设置全局段描述符,这只是准备工作,真正让CPU进入32位模式还需要将cr0寄存器的最后一位设置位1。注意,最然我们一直在说32位保护模式,但是它其实包含了两个概念,32位模式和保护模式,要分别进行设置。

注意我们还没介绍中断向量表(IDT),所以目前我们要关闭中断,因为32位模式下BIOS提供的那些中断也不能用了,因为BIOS的程序都是16位指令,无法在32位模式下运行。所以,我们还是先关闭中断吧。

;; 准备GDT
gdt_start:
gdt_null:
  dd 0, 0       ; 第一个段描述符必须是null
gdt_code:
  dw 0xffff     ; Limit(0-15位)
  dw 0x0        ; 基址(0-15位)
  db 0x0        ; 基址(16-23位)
  db 10011010b  ; 第一个标志+类型标志
  db 11001111b  ; 第二个标志+Limit(16-19位)
  db 0x0        ; 基址(24-31位)
gdt_data:
  dw 0xffff     ; Limit(0-15位)
  dw 0x0        ; 基址(0-15位)
  db 0x0        ; 基址(16-23位)
  db 10010010b  ; 第一个标志+类型标志
  db 11001111b  ; 第二个标志+Limit(16-19位) 
  db 0x0        ; 基址(24-31位)
gdt_end:

;; GDT描述符
gdt_descriptor:
  dw gdt_end - gdt_start - 1      ; GDT大小,总是真实大小减一
  dd gdt_start                    ; GDT起始地址

CODE_SEG equ gdt_code - gdt_start ; 代码段描述符偏移量
DATA_SEG equ gdt_data - gdt_start ; 数据段描述符偏移量

;; 切换保护模式
cli                    ; 关闭中断,因为我们还没有设置好保护模式下的中断向量表
lgdt [gdt_descriptor]  ; 设置GDT
mov eax, cr0
or eax, 0x1            ; 将cr0寄存器的最后一位设置为1进入32位模式
mov cr0, eax
jmp CODE_SEG:init_pm   ; 进行一个远跳转(机器码EA),清空指令流水线,
                       ; 因为CPU已经处于32位模式了,指令流水线中的16位指令需要清空掉

bits 32                ; 编译为32位指令,也可写作[bits 32]
;; 初始化保护模式
init_pm:
  mov ax, DATA_SEG     ; 在保护模式,旧的段已经没有用了,
  mov ds, ax           ; 所以,我们将段寄存器指向GDT中定义得数据段
  mov es, ax 
  mov fs, ax 
  mov gs, ax
  mov ebp , 0x90000    ; 更新栈得位置
  mov esp , ebp
  jmp $                ; for {}

上面就是切换到32位保护模式的过程了,关于GDT的设置,目前我们只做了最简单可行的配置,数据段和代码段是重叠的,作为示例,先这样吧。

进入操作系统

我们已经介绍了足够多的基础知识了,也知道了操作系统是如何启动的,我们已经在引导扇区里和BIOS玩的够久了,是时候真正进入操作系统了。

;; 启动扇区
org 0x7c00                        ; 也可以写作 [org 0x7c00]

OS_ADDR       equ   0x7e00        ; 操作系统内存地址
VIDEO_ADDR    equ   0xb8000       ; 帧缓存地址
CODE_SEG      equ   gdt_code - gdt_start   ; 代码段描述符偏移量
DATA_SEG      equ   gdt_data - gdt_start   ; 数据段描述符偏移量

;; ===================
;; 1.读盘
;; 2.准备GDT
;; 3.切换32位保护模式
;; 4.跳转到操作系统
;; ===================

mov  [boot_device], dl            ; 保存引导盘的驱动器号
;; 读盘
mov  ah, 0x2                      ; 读磁盘
mov  al, 0x10                     ; 扇区数,16×512B=8KB
mov  ch, 0                        ; 柱面,从0开始
mov  cl, 2                        ; 扇区,从1开始
mov  dh, 0                        ; 磁头,从0开始
mov  dl, [boot_device]            ; 驱动器号,0:软驱A,1:软驱B,0x80:磁盘C
mov  bx, OS_ADDR                  ; 数据地址
int  0x13                         ; 磁盘中断
jc   read_disk_err                ; 读盘失败则跳转到read_disk_err
;; 清屏
mov ah, 0x6                       ; 屏幕初始化或上卷
mov al, 0x0                       ; 上卷行数,0表示整个窗口空白
mov bh, 0x0                       ; 卷入后空出的行的写入属性
mov ch, 0x0                       ; 左上角行号
mov cl, 0x0                       ; 左上角列号
mov dh, 0x18                      ; 右下角行号
mov dl, 0x4f                      ; 右下角列号
int 0x10                          ; 触发中断
;; 切换保护模式
cli                               ; 关闭中断,因为我们还没有设置好保护模式下的中断向量表
lgdt [gdt_descriptor]             ; 设置GDT
mov  eax, cr0                     ; 将寄存器cr0的最
or   eax, 0x1                     ; 后一位设置为1后,
mov  cr0, eax                     ; CPU进入32位模式
jmp  CODE_SEG:init_pm             ; 执行一个远跳转(机器码EA),清空CPU指令流水线

read_disk_err:
    mov si, err_code_read_disk    ; 错误码写入地址
    call hex2str                  ; 将错误码转换成十六进制字符串
    mov ah, 0x13                  ; 显示字符串
    mov al, 0x1                   ; 写入模式
    mov cx, 0x14                  ; 字符串长度
    mov dh, 0x8                   ; 显示位置的行号
    mov dl, 0x0                   ; 显示位置的列号
    mov bp, err_read_disk         ; 字符串地址
    mov bh, 0                     ; 页号
    mov bl, 0x4                   ; 字符显示属性
    int 0x10                      ; 打印字符串中断
    jmp fin

;; 函数
hex2str:
    pusha
    add si, 1
    shr ax, 4
    shr al, 4
    call hex2ascii
    sub si, 1
    shr ax, 8
    call hex2ascii
    popa
    ret

hex2ascii:
    pusha
    add al, 0x30
    cmp al, 0x39
    jle .num
    add al, 0x7
    .num:
    mov [si], al
    popa
    ret

fin:
    hlt
    jmp fin

bits 32                           ; 编译为32位指令
init_pm:
    mov ax, DATA_SEG              ; 在保护模式,旧的段已经没有用了,
    mov ds, ax                    ; 所以,我们将段寄存器指向GDT中定义得数据段
    mov es, ax 
    mov fs, ax 
    mov gs, ax
    mov ebp, 0x9fb00              ; 更新栈得位置,指向空闲空间的顶部
    mov esp, ebp
 
    call OS_ADDR                  ; 跳转系统内核,也就是os_main
    .fin:
        hlt
        jmp .fin                  ; for {}


;; 数据
boot_device:         db 0
err_read_disk:       db 'read disk failed:'
err_code_read_disk:  db '??H', 0
msg_load_os:         db 'loading os...', 0

;; 准备GDT
gdt_start:
gdt_null:
    dd 0, 0                       ; 第一个段描述符必须为null
gdt_code:                         ; 代码段
    dw 0xffff                     ; Limit(0-15位)
    dw 0x0                        ; 基址(0-15位)
    db 0x0                        ; 基址(16-23位)
    db 10011010b                  ; 第一个标志+类型标志
    db 11001111b                  ; 第二个标志+Limit(16-19位)
    db 0x0                        ; 基址(24-31位)
gdt_data:                         ; 数据段
    dw 0xffff                     ; Limit(0-15位)
    dw 0x0                        ; 基址(0-15位)
    db 0x0                        ; 基址(16-23位)
    db 10010010b                  ; 第一个标志+类型标志
    db 11001111b                  ; 第二个标志+Limit(16-19位) 
    db 0x0                        ; 基址(24-31位)
gdt_end:

;; GDT描述符
gdt_descriptor:
    dw gdt_end - gdt_start - 1    ; GDT大小,总是真实大小减一
    dd gdt_start                  ; GDT起始地址

;; 填充引导扇区
db 510+$$-$ dup 0                 ; 填充0直到510字节,$$=0x7c00
dw 0xaa55                         ; 启动扇区标识

;; 系统内核
; bits 32
os_main:
    mov word [VIDEO_ADDR], 0xf<<8|'A'
    jmp $

db 512+os_main-$ dup 0

;; 填充剩余扇区,因为前面我们读了16个扇区
db 15*512 dup 0

如果你在屏幕左上角看到了’A’,那就说明我们成功进入到了操作系统了。啥,这也能成功?在这里我不得不再次提醒各位,内存是一个数组,数组只有下标和值。CPU只会傻傻的一条接着一条执行指令,程序能否正确执行,主要看能不能跳转到正确的指令,能不能找到正确的数据,而这其中的关键在于指令或数据的下标是不是正确,下标,下标,它真的很重要。

大家一定要想明白call OS_ADDR是如何跳转到os_main的,这里我们取了巧,代码在磁盘镜像中的布局和在内存中的布局是一样的,所以我们不需要使用org额外修正os_main的地址,是不是很神奇呢。

其实我们想做的事情非常简单,将操作系统的机器指令读到内存中,放在一个指定的位置,然后在完成一些基本的初始化和设定后跳转到操作系统的第一条指令所在的地址,至此操作系统登基称帝,接管一切事物。虽然说起来不难,但是这条登基之路却是充满坎坷的。如果把写操作系统比作登山,那现在我们还只是在山脚下转了转,找到了一条上山的小路。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/89297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

传统ERP管理项目有哪些问题?项目ERP系统哪个好?

8Manage FAS 是专为基于项目的公司设计的企业资源规划系统&#xff08;ERP系统&#xff09;。基于项目的公司包括建筑、工程和施工操作 (AEC)、产品要订购制造 (ETO) 和各种其他类型的专业服务公司 (PSO)。 对任何公司来说&#xff0c;无论在什么行业&#xff0c;项目对其业务…

Linux 调试之 TRACE_EVENT

文章目录前言一、TRACE_EVENT简介二、TRACE_EVENT() 结构2.1 TRACE_EVENT简介2.2 trace_sched_switch示例三、The header file参考资料前言 在Linux的整个历史中&#xff0c;人们一直希望在内核中添加静态跟踪点&#xff0c;即记录内核中特定位置的数据以供以后检索的函数。与…

[附源码]Nodejs计算机毕业设计基于大数据的超市进销存预警系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

开源大数据比对平台(dataCompare)新版本发布

开源大数据比对平台设计与实践—dataCompare 前文介绍了开源大数据比对平台设计和实践&#xff0c;最近将整体业务流程进行了完善和开发。 一、目前当前版本实现了如下功能&#xff1a; (1)低代码简单配置完成数据比对核心功能 (2)数据量级比对、数据一致性比对 二、系统功…

sentinel限流,熔断等具体流程分析

基于sentinel 1.8.6 从sentinel-dashboard来看&#xff0c;sentinel主要提供了流控&#xff0c;熔断&#xff0c;热点&#xff0c;系统规则&#xff0c;授权规则等。 针对http请求的数据监控以及规则限制的适配&#xff0c;可以参考sentinel-spring-webmvc-adapter以及sentin…

CBAM(Convolutional Block Attention Module)卷积注意力模块用法及代码实现

CBAM卷积注意力模块用法及代码实现CBAMChannel Attention模块&#xff08;CAM&#xff09;Spatial Attention模块&#xff08;SAM&#xff09;代码实现CBAM CBAM&#xff08; Convolutional Block Attention Module &#xff09;是一种轻量级注意力模块的提出于2018年。CBAM包…

185-200-spark-核心编程-Streaming

185-spark-核心编程-Streaming&#xff1a; 数据处理延迟的长短分为&#xff1a;实时数据处理&#xff08;毫秒级别&#xff09;&#xff0c;离线数据处理&#xff08;小时&#xff0c;天&#xff09; 数据处理的方式分为&#xff1a;流式数据处理&#xff08;streaming&…

ORACLE19c数据库随LINUX操作系统自动启动实现方式

1.建立目录 # su - oracle $ mkdir /home/oracle/scripts 2.建立启动脚本&#xff1a; $ cd /home/oracle/scripts $ vim startdb.sh #!/bin/bash export ORACLE_BASE/u01/app/oracle export ORACLE_HOME$ ORACLE_BASE/product/19.16.0/db_1 export ORACLE_SIDemrep export PAT…

【电脑使用】利用diskpart删除电脑的EFI分区

文章目录前言问题描述问题解决扩展&#xff1a;测量磁盘读写速度1 win10自带工具2 第三方工具前言 在Windows的磁盘管理中&#xff0c;往往会发现自己电脑的磁盘中莫名多了一些分区&#xff0c;有一些是系统分区&#xff08;一般不删&#xff09;&#xff0c;还有一些是还原分区…

m索引OFDM调制解调系统的性能仿真分析

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 随着无线通信技术的不断发展,人们对下一代移动通信系统提出了越来越高的要求。在这样的时代背景下,具有低峰均比,强频偏对抗能力和高能量效率的索引调制OFDM系统(Orthogonal Frequency Division …

【跟学C++】C++STL三大主要组件——容器/迭代器/算法(Study19)

文章目录1、前言2、简介2.1、STL是什么&#xff1f;2.2、STL能干什么&#xff1f;2.3、STL组成3、容器3.1、顺序容器3.2、排序容器(关联式容器)3.3、哈希容器3.4、容器适配器3、迭代器3.1、迭代器介绍3.2、迭代器定义方式3.3、迭代器类别3.4、辅助函数4、算法5、总结 【说明】…

【MATLAB教程案例60】使用matlab实现基于GRU网络的数据分类预测功能与仿真分析

欢迎订阅《FPGA学习入门100例教程》、《MATLAB学习入门100例教程》 目录 1.软件版本 2.GRU网络理论概述

【云原生进阶之容器】第一章Docker核心技术1.5.4节——cgroups使用

4 CGroups使用 4.1 挂载cgroup树 开始使用cgroup前需要先挂载cgroup树,下面先看看如何挂载一颗cgroup树,然后再查看其根目录下生成的文件。 #准备需要的目录 #准备需要的目录 dev@ubuntu:~$ mkdir cgroup && cd cgroup dev@ubuntu:~/cgroup$ mkdir demo#由于name=…

[论文解析] Diffusion Guided Domain Adaptation of Image Generators

project link: https://styleganfusion.github.io/ 文章目录OverviewWhat problem is addressed in the paper?What is the key to the solution?What is the main contribution?IntroductionBackgroundLatent diffusion modelClassifier-free guidanceMethodModel Structur…

pytorch深度学习实战lesson36

第三十六课 锚框 因为我们在目标检测里面需要预测边缘框&#xff0c;所以给我们的预测带来了很大的问题。我们在卷积神经网络里面做图片分类的时候&#xff0c;整个代码写起来看上去非常简单&#xff0c;就是一个 soft Max 出去就完事了。但是因为有边框的加入&#xff0c;使得…

第十二期 | 万元的正版课程仅花9.9就可买到?

顶象防御云业务安全情报中心监测发现&#xff0c;某线上教育培训类平台课件遭遇大规模盗取。被盗取的课件&#xff0c;经加工处理后&#xff0c;进行低价转售&#xff0c;严重损害了平台的合法权益。 飞速发展的在线教育和看不见的风险 随着5G、视频编解码等技术融合&#xff…

DevExpress .Net Components 22.2.3 Crack

DevExpress .Net适用于 Windows、Internet 以及您的移动世界的用户界面组件 使用适用于 WinForms、WPF、ASP.NET&#xff08;WebForms、MVC 和 Core&#xff09;、Windows 10、VCL 和 JavaScript 的 DevExpress 组件&#xff0c;打造一流的用户体验并模拟最热门的企业生产力程…

产品负责人 VS 产品经理

概述 Scrum框架创造了对新角色的需求&#xff0c;其中就包括 “产品负责人” 。这不可避免额外地导致对产品负责人和产品经理角色的误解和误用&#xff0c;对团队产生不必要的压力。 角色混淆会带来噪音和摩擦&#xff0c;削弱团队对价值、质量、速度和满意度的关注。这种混乱…

让搜狗快速收录网站的方法,批量查询网站有没有被搜狗收录

让搜狗快速收录只需做到以下8点&#xff1a; 1、网页标题要与内容相关。 2、页面少用flash&#xff0c;图片等 3、将网站链接大量推送给搜狗。 4、网页尽量采用静态网页。 5、首页的外部链接不要过多。 6、搜狗更喜欢受用户欢迎的内容的网站。 7、网站不要欺骗用户。 8、网站不…

四道编程题(涉及最大公约数最小公倍数,子序列等)

tips 1. scanf当是读取整数%d的时候&#xff0c;这时候如果它读取到\n&#xff0c;它就会停止读取。并且碰到空格的时候也会跳过。 2. getchar不需要传入参数&#xff0c;读取失败的时候会返回EOF。那getchar或者scanf到底是怎么从键盘上读取我输入的字符呢&#xff1f;在getc…