操作系统--实模式到保护模式
一.实模式到保护模式(上)
A.在这里需要从计算机的历史谈起
1.远古时期的程序开发:是直接操作物理内存
2.CPU指令的操作数直接使用实地址(实际的内存地址)
3.程序员拥有绝对的权力(利用cpu指哪打哪)
在当时的实模式所拥有的权力带来了许多的问题-难以定位的问题,主要因为程序每次都需要同样地址的内存执行;同时还会带来给多道程序设计带来障碍的问题,主要是因为不管内存多大,只要有一个字节被其它程序占用都无法执行
B.为了解决上述的问题就有了这款CPU历史的里程碑-8086
1.地址宽度为20位,可访问1M内存空间
2.引入[段地址:偏移地址]的内存访问方式-8086的段寄存器和通用寄存器位16位,单个寄存器寻址最多访问64K的内存空间,需要两个寄存器配合,完成所有内存空间的访问
段地址:偏移地址--这两个的使用与定义有两方面的意义
1.硬件所做的工作--段地址左移4位,构成20位的基地址(起始地址),同时实地址=基地址+偏移地址
2.对于开发者的意义--可以更有效的划分内存的功能(数据段,代码段等),同时当程序地址冲突时,通过修改段地址解决冲突
8086的详细介绍https://baike.baidu.com/item/8086/7716347?fr=aladdin
Q:由8086会引出一个问题-段地址:偏移地址能访问的最大地址位0xFFFF:0xFFFF,即10FFEF;超过了1MB的空间,CPU该如何处理?
我们知道8086的高端地址区
所以8086的处理方式-由于8086只有20位地址线,因此最高位被丢弃
所以8086时期应用程序中的问题
1.1MB内存完全不够用-内存在任何时期都不够用
2.开发者在程序中大量使用内存回卷技术-HMA地址被使用
3.应用程序之间没有界限,相互之间随意干扰-A程序可以随意访问B程序中的数据,C程序可以修改系统调度程序的指令
所以80286出现--8086已经没有那么多应用程序,所以必须兼容再兼容,加大内存容量,增加地址线数量(24位),[段地址:偏移地址]的方式可以强化一下,可以为每个段提供更多属性(如:范围,特权级等),可以为每个段的定义提供固定方式;80286在默认情况下完全兼容8086的运行方式(实模式),它默认可直接访问1MB的内存空间,但是通过特殊的方式访问1MB+的空间
C.保护模式
1.每一段内存都拥有一个属性定义(描述符)
2.所有段的属性定义构成一张表(描述符表)
3.段寄存器保存的是属性定义在表中的索引(选择子)
描述符的内存结构
描述符表
选择子的结构
进入保护模式的方式--1.定义描述符表2.打开A20地址线3.加载描述表4.通知CPU进入保护模式
小结
1.[段地址:偏移地址]的寻址方式解决了早期程序重定位难得问题
2.8086实模式下的程序无法保证安全性
3.80286中提出了保护模式,加强了内存段的安全性
4.处于兼容的考虑,80286之后的处理器都有2种工作模式
5.处理器需要特定的设置步骤才能进入保护模式,默认为实模式
二.实模式到保护模式(中)
80286的出现引入了保护模式,为现代操作系统和应用程序奠定了基础,但是在设计方面还是有缺陷的-体现在段寄存器为24位,通用寄存器为16为,理论上段寄存器中的数值可以直接作为段基址,16位通用寄存器最多访问64K的内存,为了访问16M的内存,必须不停切换段基址
A.80386(由于80286的不足,出现了改进版80386)
1.32位地址总线,可支持4G的内存空间
2.段寄存器和通用寄存器位32位
3.任何一个寄存器都能访问到内存的任意角落--开启了平坦内存模式的新时代,段基址为0,使用通用寄存器访问4G内存空间
新时期的内存使用方式有三种
1.实模式-兼容8086的内存使用方式
2.分段模式-通过[段地址:偏移地址]的方式将内存从功能上分段(数据段,代码段)
3.平坦模式-所有内存就是一个段[0:32位偏移地址]
段属性定义
选择子属性定义
保护模式中的段定义
汇编小贴士
section关键字用于"逻辑的"定义一段代码集合
section定义的代码段不同于[段地址:偏移地址]的代码段
section定义的代码段仅限于源码中的代码段
[段地址:偏移地址]的代码段指内存中的代码段
bits16-用于指示编译器将代码按照16位方式进行编译
bits32-用于指示编译器将代码按照32位方式进行编译
在这里我们需要注意的是
1.段描述表中的第0个描述符不使用
2.代码中必须显示的指明16位代码段和32位代码段
3.必须使用jmp指令从16位代码段跳转到32位代码段
保护模式的编程实验--实验的原材料需要inc.asm同时需要将loader.asm进行修改、
loader.asm修改如下
%include "inc.asm" org 0x9000 jmp CODE16_SEGMENT [section .gdt] ; GDT definition GDT_ENTRY : Descriptor 0, 0, 0 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 ; GDT end GdtLen equ $ - GDT_ENTRY GdtPtr: dw GdtLen - 1 dd 0 ; GDT Selector Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 ; end of [section .gdt] [section .s16] [bits 16] CODE16_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0x7c00 ; initialize GDT for 32 bits code segment mov eax, 0 mov ax, cs shl eax, 4 add eax, CODE32_SEGMENT mov word [CODE32_DESC + 2], ax shr eax, 16 mov byte [CODE32_DESC + 4], al mov byte [CODE32_DESC + 7], ah ; initialize GDT pointer struct mov eax, 0 mov ax, ds shl eax, 4 add eax, GDT_ENTRY mov dword [GdtPtr + 2], eax ; 1. load GDT lgdt [GdtPtr] ; 2. close interrupt cli ; 3. open A20 in al, 0x92 or al, 00000010b out 0x92, al ; 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 Code32SegLen equ $ - CODE32_SEGMENT
make以及inc.asm
; Segment Attribute DA_32 equ 0x4000 DA_DR equ 0x90 DA_DRW equ 0x92 DA_DRWA equ 0x93 DA_C equ 0x98 DA_CR equ 0x9A DA_CCO equ 0x9C DA_CCOR equ 0x9E ; Selector Attribute SA_RPL0 equ 0 SA_RPL1 equ 1 SA_RPL2 equ 2 SA_RPL3 equ 3 SA_TIG equ 0 SA_TIL equ 4 ; 描述符 ; 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 & 0xFFFF ; 段界限1 dw %1 & 0xFFFF ; 段基址1 db (%1 >> 16) & 0xFF ; 段基址2 dw ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 属性1 + 段界限2 + 属性2 db (%1 >> 24) & 0xFF ; 段基址3 %endmacro ; 共 8 字节
准备工作之后make,bochs之后看结果
发现在bochs下并没有打印结果所以需要设置断点来对该实验进行继续验证,首先对loader.asm进行反编译得到如图左边的结果,发现箭头对应处为loader.asm也就是右图箭头所对应处
可以在左边对应点地址处设置断点来对结果进行分析,结果如下
从右边的结果可以得出,进行以此跳转之后再进行赋值,为了实验,多次进行单步操作,发现得出的结果是一致的,意味着死循环了,这样我们就从实模式到了保护模式,从16位代码段进入到32位代码段进行执行。
我们在上面的代码中为什么不直接使用标签定义描述符中的段基地址?为什么 16 位代码段到 32 位代码段必须无条件跳转呢?那么在汇编中,NASM 将汇编文件当成一个独立的代码段进行编译,汇编代码中的标签(Label)代表的是段内偏移地址,实模式下需要配合段寄存器中的值计算标签的物理地址,这便是我们不直接使用标签定义描述符中的段基地址的原因了。代码跳转则是由于在汇编中存在一个流水线技术的概念。什么是流水线技术呢?处理器为了提高效率将当前指令和后续指令预取到流水线,因此,可能同时预期的指令中既有 16 位代码又有 32 位代码。为了避免将 32 位代码用 16 位代码的方式运行,需要刷新流水线,此时便需要使用无条件跳转 jmp 技术才能强制刷新流水线。
小结
1.80386处理器是计算机发展史上的里程碑
2.32位的寄存器和地址总线能够直接访问4G内存的任意角落
3.需要在16位实模式中对GDT中的数据进行初始化
4.代码中需要位GDT定义一个标识数据结构
5.需要使用jmp指令从16位代码跳转到32位代码
三.实模式到保护模式(下)
在上面的实验中,我们注意到使用了jmp dword Code32Selector :0,为什么需要dword,要知道在这里的jmp的作用(s16-s32)-在16位代码中,所有的立即数默认为16位,从16位代码段跳转到32位代码时,必须做强制转换,否则,段内偏移地址可能被截断
在这节需要深入保护模式:定义显存段,为了显示数据,必须存在两大硬件:显卡+显示器。显卡是为显示器提供需要显示的数据,控制显示器的模式和状态。而显示器是将目标数据以可见的方式呈现在屏幕上。显存的概念和意义就是显卡拥有自己内部的而数据存储器,显存在本质上和普通内存无差别,用于存储目标数据,操作显存中的数据将导致显示器上内容的改变。
显卡的工作模式有两种-文本模式与图形模式。在不同的模式下,显卡对显存内容的解释是不同的,可以使用专属指令或int 0x10中断改变显卡工作模式,在文本模式下的显存的地址范围映射位[0xB8000,0xBFFFF],一屏幕可以显示25行,每行80个字符
显卡的文本显示原理与文本模式下显示字符
对段基址和段属性进行设置之后以及打印的结果,发现会在bochs上打印出结果p
在实现完单个字符的打印之后,可以进一步实现指定内存中的字符串打印,首先需要准备的工作有定义全局堆栈段(.gs),用于保护模式下的函数调用,之后定义全局数据段(.dat),用于定义只读数据,最后利用对显存段的操作定义字符串打印函数
打印函数的设计可以如下图所示
在这里需要注意的是32位保护模式下的乘法操作是被乘数放到AX寄存器,乘数放到通用寄存器或内存单元(16位),相乘的结果放在EAX寄存器中;同时$表示当前行相对于代码起始位置处的偏移量,$$表示当前代码节的起始位置
实现过程以及实现结果,可以看到实现的结果打印出设置的字符串
小结
1.实模式下可以使用32位寄存器和32位地址
2.显存是显卡内部的存储单元,本质上与普通内存无差别
3.显卡有两种工作模式-文本模式与图形模式
4.文本模式下操作显存单元中的数据能够立即反映到显示器