---- 整理自狄泰软件唐佐林老师课程
文章目录
- 1. 从计算机的历史谈起
- 2. 绝对的权利带来的问题
- 3. CPU历史的里程碑 - 8086
- 3.1 深入解析 [段地址 : 偏移地址]
- 3.1.1 示例
- 3.1.2 问题
- 3.2 8086时期应用程序中的问题
- 3.3 思考
- 4. 80286的登场
- 4.1 80286的兼容性
- 4.2 初识保护模式
- 4.2.1 描述符(Descriptor)
- 4.2.2 描述符表(Descriptor Table)
- 4.2.3 选择子(Selector)
- 4.3 进入保护模式的方式
- 4.4 80286的光荣退场
- 5. 80386的登场(计算机新时期的标志)
- 5.1 新时期的内存使用方式
- 5.2 问题:x86指的究竟是什么处理器?
- 6. 保护模式
- 6.1 段属性定义
- 6.2 选择子属性定义
- 6.3 保护模式中的段定义
- 6.4 汇编小贴士
- 6.5 编程实验
- 6.6 问题
- 6.7 一个值得注意的细节
- 6.8 注意
- 7. 深入保护模式
- 7.1 定义显存段
- 7.1.1 显存的概念和意义
- 7.1.2 显卡的工作模式:文本模式&图形模式
- 7.1.3 编程实验:保护模式下的显存操作,打印字符
- 7.2 小目标
- 7.3 编程实验:打印字符串
- 7.4 小结
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 ]
- [ FFFF : FFFF ]
- 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 进入保护模式的方式
- 定义描述符表
- 打开A20地址线
- 加载描述符表
- 通知CPU进入保护模式
4.4 80286的光荣退场
- 历史意义
- 引入了保护模式,为现代操作系统和应用程序奠定了基础
- 奇葩设计
- 段寄存器为24位,通用寄存器为16位(不伦不类)
- 理论上,段寄存器中的数值可以直接作为段基址
- 16位通用寄存器最多访问64K的内存
- 为了访问16M的内存,必须不停切换段基址
- 段寄存器为24位,通用寄存器为16位(不伦不类)
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_32 | 0x4000 | 保护模式下32位段 |
DA_DR | 0x90 | 只读数据段 |
DA_DRW | 0x92 | 可读写数据段 |
DA_DRWA | 0x93 | 已访问可读写数据段 |
DA_C | 0x98 | 只执行代码段 |
DA_CR | 0x9A | 可执行可读代码段 |
DA_CCO | 0x9C | 只执行一致代码段 |
DA_CCOR | 0x9E | 可执行可读一致代码段 |
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) 代表的是 段内偏移地址
- 实模式下需要配合段寄存器中的值计算标签的物理地址
- 流水线技术:
- 处理器为了提高效率将当前指令和后续指令预取到流水线
- 因此,可能同时预取的指令中既有16位代码又有32位代码
- 为了避免将32位代码用16位的方式运行,需要刷新流水线
- 无条件跳转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 小目标
- 在保护模式下,打印指定内存中的字符串
- 定义全局堆栈段(.gs),用于保护模式下的函数调用
- 定义全局数据段(.data),用于定义只读数据段(D.T.OS!)
- 利用显存段的操作定义字符串打印函数(PrintString)
- 打印函数(PrintString)的设计
7.3 编程实验:打印字符串
【参看链接】:10-11-12 - 实模式到保护模式 / 12 / 01
data.img插入到vmware中运行结果如下:
7.4 小结
- 实模式下可以使用32位寄存器和32位地址
- 显存是显卡内部的存储单元,本质上与普通内存无差别
- 显卡有两种工作模式:文本模式&图形模式
- 文本模式下操作显存单元中的数据能够立即反映到显示器