配合视频学习体验更佳!https://www.bilibili.com/video/BV12h411w7F4/?vd_source=701807c4f8684b13e922d0a8b116af31
代码仓库:https://github.com/xukanshan/the_truth_of_operationg_system.git
Intel8086CPU由于自身设计存在诸多缺点,最致命的有两条:1、仅能寻址1MB内存空间;2、用户程序可以通过自由修改段基址来访问所有内存空间而引出的安全问题。所以后来的CPU自然就要解决以上的两个问题,CPU厂商为了凸显出自己新的CPU的安全性,将新开发出的CPU命名为工作在保护模式下——也就是提供了一种保护机制让程序不能随意访问所有内存空间,同时CPU的寻址范围也达到了4GB。而8086的那种工作模式由于保护模式的出现而被命名为实模式。这种具有新的工作模式的CPU被叫做IA32(工作在32位环境下)体系架构CPU,这是从80836CPU开始的一种架构。
但由于8086CPU在当时取得了非凡的市场成功,所以后来的IA32体系架构CPU必须兼容8086的那种工作模式,所以IA32体系架构CPU也必须可以运行在实模式下。8086只能运行实模式,它使用[段基址:偏移]这种寻址方式,所以IA32体系架构CPU为了兼容8086上开发的程序,也得用[段基址:偏移]这种模式,但是又同时为了能够寻址更大的地址空间以及获得安全性检查,所以就采用了将段寄存器提供的值(16位)不再作为段基址,而是作为一个选择子去GDT表中找到对应的表项,然后从这个表项中得到段基址(32位)与进行安全性检查。
由于IA32体系架构的CPU有两种主要工作模式,而BIOS加载MBR,MBR加载Loader的时候工作在实模式下,此后为了获得更大的地址空间,就必须从实模式切换到保护模式。模式的切换就意味着寻址方式的切换,所以在由实模式切换到保护模式的时候,就必须要在内存中初始化GDT表,也就是初始化GDT表中的表项(也叫段描述符)。其结构如下,每一个具体字段的含义见书P151。
GDT表中每个表项(8B)实质上就是一段内存区域的身份证,一个段描述符只能只能来描述一个内存段。操作系统的代码段、数据段、栈段自然也就各需一个段描述符。需要注意的是,GDT表第0个段描述符不可用。因为选择子忘记设置的话,就会是0(就像我们的MBR代码一上来就将段寄存全部初始化为0),就会访问这个段描述符,而如果这个段描述符有内容的话,就会将段基址定位到其他我们并不想要的地方去,所以干脆直接让GDT表第0个段描述符不可用,未设置的选择子访问这个段描述符CPU就会产生异常并阻止。
现在我们开始初始化GDT表,在初始化之前,我们先需要在boot.inc中定义一系列段描述符的模块化字段宏,这样方便我们可以结构化凑出段描述符,进而方便定义GDT表。我们在boot.inc中也同时定义了模块化的选择子字段,可以方便定义选择子。选择子结构如下图:
定义了模块化段描述符字段宏和模块化选择子字段宏的boot.inc (myos/include/boot/boot.inc)
;------------- loader和kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
;-------------- 模块化的gdt描述符字段宏-------------
DESC_G_4K equ 1_00000000000000000000000b ;设置段界限的单位为4KB
DESC_D_32 equ 1_0000000000000000000000b ;设置代码段/数据段的有效地址(段内偏移地址)及操作数大小为32位,而非16位
DESC_L equ 0_000000000000000000000b ;64位代码段标记位,我们现在是在编写32位操作系统,此处标记为0便可。
DESC_AVL equ 0_00000000000000000000b ;此标志位是为了给操作系统或其他软件设计的一个自定义位,
;可以将这个位用于任何自定义的需求。
;比如,操作系统可以用这个位来标记这个段是否正在被使用,或者用于其他特定的需求。
;这取决于开发者如何使用这个位。但从硬件的角度来看,AVL位没有任何特定的功能或意义,它的使用完全由软件决定。
DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;定义代码段要用的段描述符高32位中16~19段界限为全1
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;定义数据段要用的段描述符高32位中16~19段界限为全1
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b ;定义我们要操作显存时对应的段描述符的高32位中16~19段界限为全0
DESC_P equ 1_000000000000000b ;定义了段描述符中的P标志位,表示该段描述符指向的段是否在内存中
DESC_DPL_0 equ 00_0000000000000b ;定义DPL为0的字段
DESC_DPL_1 equ 01_0000000000000b ;定义DPL为1的字段
DESC_DPL_2 equ 10_0000000000000b ;定义DPL为2的字段
DESC_DPL_3 equ 11_0000000000000b ;定义DPL为3的字段
DESC_S_CODE equ 1_000000000000b ;无论代码段,还是数据段,对于cpu来说都是非系统段,所以将S位置为1,见书p153图
DESC_S_DATA equ DESC_S_CODE ;无论代码段,还是数据段,对于cpu来说都是非系统段,所以将S位置为1,见书p153图
DESC_S_sys equ 0_000000000000b ;将段描述符的S位置为0,表示系统段
DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
;定义代码段,数据段,显存段的高32位
DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b
;-------------- 模块化的选择子字段宏 ---------------
RPL0 equ 00b ;定义选择字的RPL为0
RPL1 equ 01b ;定义选择子的RPL为1
RPL2 equ 10b ;定义选择字的RPL为2
RPL3 equ 11b ;定义选择子的RPL为3
TI_GDT equ 000b ;定义段选择子请求的段描述符是在GDT中
TI_LDT equ 100b ;定义段选择子请求的段描述符是在LDT中
接下来修改loader.S,在其中进行GDT表的初试化
p161代码loader.S剖析:
1、代码功能
启动保护模式后利用初始化的GDT表中的显存段描述符来对显存寻址后操作以显示字符
2、实现原理
IA32体系架构为了兼容8086,所以支持实模式,同时用保护模式提供更大的CPU寻址能力与访问安全性检查。刚开启电脑时,工作在实模式下。后面为了使用更好的性能而进入保护模式后,段基址寄存器的值不再作为实模式下的段基址,而是作为一个选择子,去GDT表中查询对应的段描述符以获得段基址与进行安全性检查,配合偏移来获得真实物理地址。
3、代码逻辑(核心)
A、初始化GDT表(就是定义要用到的段描述符)
B、利用BIOS中断打印字符串
C、打开保护模式,加载GDT表的基址进入GDTR寄存器
D、刷新流水线
E、加载显存段选择子,在保护模式下操作显存来显示字符
4、怎么写代码?
A、%include “boot.inc”;指定vstart=0x900;jmp到loader的可执行代码中去(因为loader中定义的GDT段描述符数据就直接会放在loader开头,而MBR最后会跳转到本程序开头来,数据不能被执行,我们要跳过它们)
B、利用已经在boot.inc中写好的模块化段描述符字段来拼凑出我们需要的代码段、数据段、显存段的段描述符。同时GDT表的第0项是全0,还要预留一些段描述符;然后定义显存段的选择子
C、查询BIOS中断打印字符串"2 loader in real"
D、开启保护模式:1、打开A20地址线(原因是因为8086存在高端内存,这部分内存只在逻辑中存在,物理中并没有对应。由于8086只有20根地址线,所以访问高端内存会自动丢掉最高位,所以并没有问题,当时很多程序员就利用这个特性偷懒。但是当后续CPU多了地址线后,之前8086偷懒程序员写的程序就会真的访问对于8086来说的高端内存。所以为了兼容他们的程序,就用A20地址线来控制是否能够访问更多的内存。用in与out指令就能与A20交互);2、加载GDT表的首地址进入GDTR;3、将CR0寄存器的pe位置为1,意为打开保护模式(mov指令即可)
E、通过远跳转来刷新流水线
F、加载显存段选择子,切换32位编译,然后向显存段内存入数据来显示字符’P’
5、代码实现如下: (myos/boot/loader.S)
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start ;loader一进来是一大堆GDT段描述符数据,无法执行,所以要跳过
GDT_BASE: ;构建gdt及其内部的描述符
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC:
dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC:
dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl已改为0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 此处预留60个描述符的空间
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上
gdt_ptr dw GDT_LIMIT ;定义加载进入GDTR的数据,前2字节是gdt界限,后4字节是gdt起始地址,
dd GDT_BASE
loadermsg db '2 loader in real.'
loader_start:
;------------------------------------------------------------
;INT 0x10 功能号:0x13 功能描述:打印字符串
;------------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX=字符串长度
;(DH、DL)=坐标(行、列)
;ES:BP=字符串地址
;AL=显示输出方式
; 0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
; 1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
; 2——字符串中含显示字符和显示属性。显示后,光标位置不变
; 3——字符串中含显示字符和显示属性。显示后,光标位置改变
;无返回值
mov sp,LOADER_BASE_ADDR
mov bp,loadermsg ; ES:BP = 字符串地址
mov cx,17 ; CX = 字符串长度
mov ax,0x1301 ; AH = 13, AL = 01h
mov bx,0x001f ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
mov dx,0x1800 ;
int 0x10 ; 10h 号中断
;----------------- 准备进入保护模式 ------------------------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1
;----------------- 打开A20 ----------------
in al, 0x92
or al, 0000_0010B
out 0x92,al
;----------------- 加载GDT ----------------
lgdt [gdt_ptr]
;----------------- cr0第0位置1 ----------------
mov eax,cr0
or eax,0x00000001
mov cr0,eax
;jmp dword SELECTOR_CODE:p_mode_start
jmp SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_STACK_TOP
mov ax,SELECTOR_VIDEO
mov gs,ax
mov byte [gs:160], 'P'
jmp $
6、其他代码详解查看书p162
编译后,我们发现loader的大小已经超过我们的之前写的MBR设定加载的512B,所以我们修改MBR相关字段(修改与硬盘打交道7步骤中的第1步读取扇区数放入cx中的值,为了后面方便我们都改成4)
myos/boot/mbr.S
mov cx,4 ; 待读入的扇区数
编译,然后写入磁盘中运行(注意写入loader的dd命令的count参数现在是4)