10-11-12 - 实模式到保护模式

---- 整理自狄泰软件唐佐林老师课程

查看所有文章链接:(更新中)深入浅出操作系统 - 目录

1. 从计算机的历史谈起

  • 远古时期的程序开发:直接操作物理内存
  • CPU指令的操作数 直接使用 实地址(实际内存地址)
  • 程序员拥有绝对的权利(利用CPU指哪打哪)

2. 绝对的权利带来的问题

  • 难以重定位
    程序每次都需要同样地址的内存执行
  • 给多道程序设计带来障碍
    不管内存多大,但凡一个字节被其他程序占用都无法执行

3. CPU历史的里程碑 - 8086

  • 地址线宽度为20位,可访问1MB内存空间
    (2^20=1M个地址,1个地址存储8bit数据,所以是1MB内存)
  • 引入 [段地址 : 偏移地址](段地址 << 4 + 偏移地址) 的内存访问方式
    • 8086的段寄存器和通用寄存器为16位
    • 单个寄存器寻址最多访问64KB的内存空间
    • 需要两个寄存器配合,完成所有内存空间的访问

3.1 深入解析 [段地址 : 偏移地址]

  • 硬件所做的工作
    • 段地址左移4位,构成20位的基地址(起始地址)
    • [段地址 : 偏移地址]
      = 段地址 << 4 + 偏移地址
      = 基地址 + 偏移地址 = 实地址
  • 对于开发者的意义
    • 更有效的划分内存的功能(数据段、代码段、等)
    • 当出现程序地址冲突时,通过修改段地址解决冲突

3.1.1 示例

mov ax, [0x1234]
==> 实地址:( ds << 4 ) + 0x1234
mov ax, [es:0x1234]
==> 实地址:( es << 4 ) + 0x1234

3.1.2 问题

  • [段地址 : 偏移地址] 能访问的最大地址为 0xFFFF:0xFFFF,即10FFEF;超过了1MB的空间,CPU如何处理?
  • 8086 的高端地址区(High Memory Area)
    • [ FFFF : FFFF ]
      ⇒ 0xFFFF0 + 0xFFFF
      ⇒ 0xFFFF0 + ( 0xF + 0xFFF0 )
      ⇒ ( 0xFFFF0 + 0xF ) + 0xFFF0
      ⇒ ( 0xFFFFF ) + 0xFFF0 = 0x10FFEF
      0xFFF0,即 HMA:[ 0x100000, 0x10FFEF ]
  • 8086 的处理方式
    • 由于8086只有20位地址线,因此最高位被丢弃(溢出)
    • 0xFFFF : 0xFFFF ⇒ 0x10FFEF
    • 1 0000 1111 1111 1110 1111 ==> 回卷 ==> 0xFFEF

3.2 8086时期应用程序中的问题

  • 1MB 内存完全不够用(内存在任何时期都不够用)
  • 开发者在程序中大量使用 内存回卷技术(HMA地址被使用)
  • 应用程序之间没有界限,相互之间随意干扰
    • A程序可以随意访问B程序中的数据
    • C程序可以修改系统调度程序的指令

3.3 思考

  • 8086程序中问题的本质是什么?如何解决?

4. 80286的登场

  • 8086已经有那么多应用程序了,所以必须要兼容再兼容
  • 加大内存容量,增加地址线数量(24位,16MB)
  • [段地址 : 偏移地址] 的方式可以强化一下
    • 为每个段提供更多属性(如:范围、特权级、等)
    • 为每个段的定义提供固定方式

4.1 80286的兼容性

  • 默认情况下完全兼容8086的运行方式(实模式)
    • 默认可直接访问1MB的内存空间
    • 通过特殊的方式访问1MB+的内存空间
  • 这个特殊的方式指的是什么?
    • 处理器需要特定的设置步骤才能进入保护模式,默认为实模式。
    • 80286之后的工作模式:
实模式保护模式
兼容8086的工作模式新的工作模式
实地址 =( 段寄存器 << 4 )+ 偏移地址内存地址 = 段起始地址 + 偏移地址
任意内存随意访问每个段增加各种属性描述,保证安全性

4.2 初识保护模式

  • 每一段的内存拥有一个属性定义(描述符 Descriptor
  • 所有段的属性定义构成一张表(描述符表 Descriptor Table
  • 段寄存器保存的是属性定义在表中的索引(选择子 Selector

4.2.1 描述符(Descriptor)

在这里插入图片描述

  • 段基址:段的起始地址(这段从哪开始)
  • 段界限:段内偏移地址的最大值(这个段有多大)
  • 段属性:-

4.2.2 描述符表(Descriptor Table)

在这里插入图片描述

  • 存在一个特殊的寄存器存放描述符表的地址。

4.2.3 选择子(Selector)

  • 段寄存器中保存的再也不是段基址了,里面保存的内容叫做 选择子

    简单来说:选择子就是段描述符在段描述符表中的索引下标。把全局描述符表当成数组,选择子就像数组下标一样。

    在这里插入图片描述

  • 说明:

    • RPL:请求者特权级标识,通过 特权级 判断是否可以访问对应段
    • TI:表示当前选择子所属的描述符表
      • 0 代表 GDT(全局段描述符表)
      • 1 代表 LDT(局部段描述符表)

4.3 进入保护模式的方式

  1. 定义描述符表
  2. 打开A20地址线
  3. 加载描述符表
  4. 通知CPU进入保护模式

4.4 80286的光荣退场

  • 历史意义
    • 引入了保护模式,为现代操作系统和应用程序奠定了基础
  • 奇葩设计
    • 段寄存器为24位,通用寄存器为16位(不伦不类)
      • 理论上,段寄存器中的数值可以直接作为段基址
      • 16位通用寄存器最多访问64K的内存
      • 为了访问16M的内存,必须不停切换段基址

5. 80386的登场(计算机新时期的标志)

  • 32位 地址总线(可支持4G的内存空间)
  • 段寄存器和通用寄存器都为32位
    • 任何一个寄存器都能访问到内存的任意角落
      • 开启了 平坦内存模式 的新时代
      • 段基址为0,使用通用寄存器访问4G内存空间

5.1 新时期的内存使用方式

  • 实模式
    • 兼容8086的内存使用方式
  • 分段模式
    • 通过 [段地址 : 偏移地址] 的方式将内存从功能上分段(数据段、代码段)
  • 平坦模式
    • 所有内存就是一个段 [0 : 32位偏移地址]

5.2 问题:x86指的究竟是什么处理器?

  • 8086第1代
  • 80286第2代
  • 80386第3代

6. 保护模式

6.1 段属性定义

标识符意义
DA_320x4000保护模式下32位段
DA_DR0x90只读数据段
DA_DRW0x92可读写数据段
DA_DRWA0x93已访问可读写数据段
DA_C0x98只执行代码段
DA_CR0x9A可执行可读代码段
DA_CCO0x9C只执行一致代码段
DA_CCOR0x9E可执行可读一致代码段

6.2 选择子属性定义

段描述符表中的第几项。

在这里插入图片描述

注:

  • RPL:请求者特权级标识,通过 特权级 判断是否可以访问对应段
  • 这里的TI取值为0、1,因为是第2位,所以为1时得到图中的4。

6.3 保护模式中的段定义

注:宏定义的语法

%macro   macro_name    number_of_params 
<macro body> 
%endmacro

在这里插入图片描述

6.4 汇编小贴士

  • section关键字用于 “逻辑的” 定义一段代码集合
  • section定义的代码段不同于 [段地址 : 偏移地址] 代码段
    • section定义的代码段 仅限于源码(文本形式)中的代码段(代码节)
    • [ 段地址 : 偏移地址 ] 的代码段指 内存中的代码段(源码经编译并加载到内存中执行)

在这里插入图片描述

  • [bits 16]
    用于表示编译器将代码按照16位方式进行编译

  • [bits 32]
    用于指示编译器将代码按照32位方式进行编译

  • 注意事项:

    • 段描述符表中的第0个描述符不使用(仅用于占位)
    • 代码中必须 显式的 指明16位代码段和32位代码段
    • 必须使用 jmp指令 从16位代码段跳转到32位代码段

6.5 编程实验

  • 需要在16位实模式中对GDT中的数据进行初始化
  • 代码中需要为GDT定义一个标识数据结构(GdtPtr)
  • 需要使用jmp指令从16位代码跳转到32位代码

【参看链接】:10-11-12 - 实模式到保护模式 / 11

反编译loader:ndisasm -o 0x9000 loader > loader.txt
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.6 问题

  • 为什么不直接使用标签定义描述符中的段基地址?
  • 为什么16位代码段到32位代码段必须无条件跳转?
  • 需要掌握的重点:
    • NASM将汇编文件当成一个 独立的代码段 编译
    • 汇编代码中的 标签(Label) 代表的是 段内偏移地址
    • 实模式下需要配合段寄存器中的值计算标签的物理地址
  • 流水线技术:
    1. 处理器为了提高效率将当前指令和后续指令预取到流水线
    2. 因此,可能同时预取的指令中既有16位代码又有32位代码
    3. 为了避免将32位代码用16位的方式运行,需要刷新流水线
    4. 无条件跳转jmp能 强制刷新流水线

6.7 一个值得注意的细节

	...
	; 4. enter protect mode
	mov  eax, cr0
	or eax, 0x01
	mov cr0, eax
	; 5. jump to 32 bits code
	jmp dword Code32Selector : 0

[section .s32]
[bits 32]
CODE32_SEGMENT:
    mov eax, 0
    jmp CODE32_SEGMENT
	...

jmp dword Code32Selector : 0 ==> 为什么需要dword?

  • 不一般的 jmp(s16 ==> s32)
    • 在16位代码中,所有的立即数默认为16位
    • 从16位代码段跳转到32位代码段时,必须做强制转换;否则,段内偏移地址可能被截断
      在这里插入图片描述

6.8 注意

在这里插入图片描述

7. 深入保护模式

7.1 定义显存段

  • 为了显示数据,必须存在两大硬件:显卡 + 显示器
    • 显卡:为显示器提供需要显示的数据,控制显示器的模式和状态
    • 显示器:将目标数据以可见的方式呈现在屏幕上

7.1.1 显存的概念和意义

  • 显卡拥有自己内部的数据存储器,简称 显存
    • 显存在本质上和普通内存无差别,用于存储目标数据
    • 操作显存中的数据将导致显示器上的内容改变

7.1.2 显卡的工作模式:文本模式&图形模式

  • 在不同的模式下,显卡对显存内容的解释是不同的

  • 可以使用专属指令或 int 0x10 中断改变显卡工作模式

    • 在文本模式下:(这里只介绍文本模式)
      • 显存的地址范围映射为:[ 0xB8000, 0xBFFFF ]
      • 一屏幕可以显示25行,每行80个字符(25 * 80)
      • 显卡的文本模式原理:
        在这里插入图片描述
  • 文本模式下显示字符:

在这里插入图片描述
在这里插入图片描述

注:fs、gs是80386起增加的两个辅助段寄存器,在这之前只有一个辅助段寄存器ES,增加这两个寄存器是为了减轻ES寄存器的负担,并能更好地配合适用于通用寄存器组的基址和变址寄存器。这两个是通用的段寄存器,语法上同其它的段寄存器一样,不能直接用立即数给它赋值。
  FS、GS 是从 80386 开始增加的,没有全称,取名就是按字母序排在 CS、DS、ES 之后的。而 CS、DS、ES、SS 是有全称的:CS (Code Segment) 代码段、DS (Data Segment) 数据段、ES (Extra Segment) 附加段、SS (Stack Segment) 栈段。

7.1.3 编程实验:保护模式下的显存操作,打印字符

【参看链接】:10-11-12 - 实模式到保护模式 / 12 / 00

在这里插入图片描述

7.2 小目标

  1. 在保护模式下,打印指定内存中的字符串
    • 定义全局堆栈段(.gs),用于保护模式下的函数调用
    • 定义全局数据段(.data),用于定义只读数据段(D.T.OS!)
    • 利用显存段的操作定义字符串打印函数(PrintString)
  2. 打印函数(PrintString)的设计
    在这里插入图片描述

7.3 编程实验:打印字符串

【参看链接】:10-11-12 - 实模式到保护模式 / 12 / 01

在这里插入图片描述

data.img插入到vmware中运行结果如下:

在这里插入图片描述

7.4 小结

  • 实模式下可以使用32位寄存器和32位地址
  • 显存是显卡内部的存储单元,本质上与普通内存无差别
  • 显卡有两种工作模式:文本模式&图形模式
  • 文本模式下操作显存单元中的数据能够立即反映到显示器
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: x86模式保护模式的转换需要编写一些配套的代码,以下是一些主要的步骤和对应的代码现: 1. 关闭中断:在进入保护模式之前,需要先关闭中断。可以使用汇编指令“cli”来现: ```assembly cli ``` 2. 设置全局描述符表(GDT):保护模式中使用GDT来管理内存和其他资源。需要定义一个GDT并将其加载到CPU中。可以使用以下代码进行初始化: ```assembly gdt_start: dd 0x0 dd 0x0 gdt_code: dw 0xFFFF dw 0x0 db 0x0 db 10011010b db 11001111b db 0x0 gdt_data: dw 0xFFFF dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: gdt_descriptor: dw gdt_end - gdt_start - 1 dd gdt_start ;加载GDT到CPU lgdt [gdt_descriptor] ``` 3. 切换到保护模式:使用汇编指令“jmp”进行转移,并设置CR0控制寄存器的PE位为1,即可切换到保护模式: ```assembly jmp gdt_code:protect_mode align 4 protect_mode: mov eax, cr0 or eax, 0x1 mov cr0, eax ``` 4. 设置段选择符:在保护模式下,需要使用新的段选择符。可以通过以下代码对段选择符进行初始化: ```assembly mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax ``` 5. 启用中断:在保护模式下,可以使用汇编指令“sti”开启中断: ```assembly sti ``` 通过以上配套的代码,可以完成从x86模式保护模式的转换。这些代码包括关闭中断、设置GDT、加载GDT、切换到保护模式、设置段选择符和启用中断等关键步骤,确保计算机可以在保护模式下正常运行。 ### 回答2: x86模式保护模式的转换需要一些特定的代码来完成。下面是一个简单的例子: ```assembly ; 进入保护模式的代码 org 0x7C00 ; 设定源代码的起始地址 ; 设置初始的段寄存器 xor ax, ax ; 将 ax 寄存器初始化为 0 mov ds, ax ; 将数据段寄存器 (ds) 设置为 0 ; 切换到保护模式 cli ; 清空中断标志寄存器 (IF) mov ax, 0x1000 ; 设置代码段选择器为 0x1000 mov ds, ax ; 将数据段寄存器 (ds) 设置为 0x1000 mov ss, ax ; 将堆栈段寄存器 (ss) 设置为 0x1000 mov sp, 0xFFFF ; 设定堆栈指针 (sp) 到堆栈的顶部 sti ; 设置中断标志寄存器 (IF) ; 切换到保护模式后的代码 [其他代码] ``` 这段代码主要包含了进入保护模式的过程。首先,通过将 ax 寄存器初始化为零,并将 ds 寄存器设置为零来确保初始的段寄存器的正确设定。然后,通过cli指令清空中断标志寄存器 (IF)。接下来,通过将代码段选择器设置为 0x1000,再将 ds 和 ss 寄存器都设置为 0x1000,来设定合适的段寄存器。然后,通过设定堆栈指针 sp 为 0xFFFF 来设置堆栈的顶部。最后,通过sti指令来设置中断标志寄存器 (IF)。 当切换到保护模式后,接下来的代码将在保护模式下执行。在这段代码后可以添加一些其他的指令,用于在保护模式下完成所需的任务。 ### 回答3: x86处理器可以在模式保护模式下运行。模式是x86处理器最初的运行模式,它是用于向后兼容早期的x86处理器设计的模式保护模式是一种更高级的模式,它提供了更多的内存管理、特权级别和安全功能。 要从x86模式切换到保护模式,需要编写配套的代码。以下是一个简化的示例代码: 1. 关闭中断:使用汇编指令`CLI`(Clear Interrupt flag)关闭中断。 2. 设置GDT(全局描述符表):在保护模式下,每个段需要由描述符来定义。首先需要初始化一个GDT,其中包含代码段、数据段和其他需要的段。使用`LGDT`(Load Global Descriptor Table)指令将GDT的地址加载到处理器的GDTR(Global Descriptor Table Register)寄存器中。 3. 切换到保护模式:将CR0(Control Register 0)寄存器的第0位(PE位)设置为1,这将使处理器进入保护模式。使用`MOV`指令将带有标志位的CR0的值加载到`EAX`寄存器中,然后使用`OR`指令将`0x1`布尔值与`EAX`寄存器的值相或,并将结果放回CR0寄存器。 4. 刷新段寄存器:由于段寄存器中的段选择器已经发生了变化,因此需要执行一个`JMP`指令来重新加载CS(Code Segment)和其他段寄存器。 5. 打开中断:使用`STI`(Set Interrupt flag)指令打开中断。 这些是切换x86模式保护模式的基本步骤。当这些步骤完成后,处理器将以保护模式运行,并可以利用更多的内存、特权级别和安全功能。需要注意的是,上述代码是一个简化版本,际的代码可能更加复杂,包含错误处理和其他必要的配置。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

uuxiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值