Orange's一个操作系统的实现:保护模式

概述

在计算机加电之后,bios检查硬件,并且把第一个扇区中的bootloader代码加载到0000: 07c00h处,开始执行bootloader代码.bootloader主要做两件事情:
- 从实模式进入保护模式
- 从硬盘(或者其他)中读取OS kernel到内存的固定位置处,然后跳转到OS中执行.

这里先讨论如何从实模式进入保护模式
参考链接:
- 学堂在线 - 清华大学OS课程

先让代码跑起来

  1. 先下在freedos,并将其中的a.img复制到代码在的工作目录,改名为freedos.img.下载链接:freedos
  2. 用bximg生成软盘映像,pm.img
  3. 修改bochsrc,添加下面三行
floppya: 1_44=freedos.img, status=inserted
floppyb: 1_44=pm.img, status=inserted
boot: a
#  相当于有两个软盘,软盘a作为bootloader,引导操作系统启动,软盘b里面放置我们自己
# 的对操作系统的操作(相当于操作系统内核)
  1. 启动Bochs,待FreeDos其中之后,使用format b:命令格式化B:盘
  2. 把代码编译成.com文件,nasm pmtest1.asm -o pmtest1.com
  3. 把pmtest1.com复制到虚拟软盘pm.img上:一种拷贝方法,值得学习
sudo mkdir /mnt/floppy
sudo mount -o loop pm.img /mnt/floppy
sudo cp pmtest1.com /mnt/floppy
sudo umount /mnt/floppy
  1. 现在我们的.com文件已经拷贝到软盘pm.img中了,在freedos中使用B:\pmtest1.com即可执行

GDT(Global Descriptor Table)

参考:GDT,LDT,GDTR,LDTR 详解,包你理解透彻

GDT就是一个装有段描述符的大数组.在GDT中每一个段描述符占用8字节,段描述符最主要的包含两个信息:

  • 段的起始地址(段基址)
  • 段的大小(段界限)
    代码段和数据段描述符如下所示:
    Orange's一个操作系统的实现:保护模式_第1张图片
    GDT表的基地址存放在GDTR寄存器中.GDTR是一个48位寄存器,其低16位保存GDT表的长度,高32位保存表的基址.所以GDT表的最大长度是1M,因为每一个描述符是8字节,所以一共可以存放213个段描述符,但是注意:第一个描述符为空,不使用

实模式下的寻址方式为:
Orange's一个操作系统的实现:保护模式_第2张图片
保护模式下,段寄存器实际存放的是选择子,选择子实际上就是GDT表的index.所以在保护模式下,段寄存器就相当于一个指针.Orange's一个操作系统的实现:保护模式_第3张图片
在下图中,逻辑地址由一个16位的段寄存器和32位EIP寄存器指出.根据段寄存器中的值(选择子,index),在GDT中找到段描述符(描述了段的基址和界限),线性地址=基址+EIP.需要注意的是:在没有页机制的情况下,线性地址和物理地址是等价的
Orange's一个操作系统的实现:保护模式_第4张图片
Orange's一个操作系统的实现:保护模式_第5张图片

选择子(Selector)

选择子的结构如图:
Orange's一个操作系统的实现:保护模式_第6张图片
包括3个部分:

  • 描述符索引(index):表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符。
  • TI:段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT(Local Descriptor Table)选择。
  • 请求特权级(RPL):代表选择子的特权级,共有4个特权级(0级、1级、2级、3级).0代表最高级(操作系统在0级),3代表最低级(应用程序在3级).任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。

一个栗子
例如给出逻辑地址:21h:12345678h转换为线性地址

  • 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1
  • OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

段描述符属性

Orange's一个操作系统的实现:保护模式_第7张图片

  • P位:存在位,P=1表示段在内存中存在;P=0表示段在内存中不存在.
  • DPL描述特权级.特权级可以是0,1,2,3.数字越小特权级越大
  • S位指明描述符是数据段/代码段描述符(S=1)还是系统段/门描述符(S=0)
  • TYPE
    Orange's一个操作系统的实现:保护模式_第8张图片
  • G位段界限粒度位.G=0时段界限粒度为字节;G=1时段界限粒度为4KB
  • D/B位
    Orange's一个操作系统的实现:保护模式_第9张图片
  • AVL位保留位,可以被系统软件使用

代码理解

  1. 问题1:为什么还要初始化32位代码段描述符??????为什么gdt里面的段基地址都是0????
    解释:这段代码是把32位代码段的物理地址加载到段描述符的"段基址"位置处(见上文段描述符图示).mov ax, cs shl eax, 4 add eax, LABEL_SEG_CODE32这3句是在计算物理地址,mov word [LABEL_DESC_CODE32+2], ax是把ax的内容放到byte2和byte3处,后面的类似.
    所以这也可以解释为什么在gdt表中,段基址写成0(LABEL_DESC_TEST和LABEL_DESC_VIDEO除外,因为这两个已经明确知道段基址了),因为这个0只是占位子而已,后面的初始化工作,才是真正的设置成正确的段基址.所以,要分清楚编译和运行是不一样的
    ; 初始化 32 位代码段描述符
    xor	eax, eax
    mov	ax, cs
    shl	eax, 4
    add	eax, LABEL_SEG_CODE32
    mov	word [LABEL_DESC_CODE32 + 2], ax
    shr	eax, 16
    mov	byte [LABEL_DESC_CODE32 + 4], al
    mov	byte [LABEL_DESC_CODE32 + 7], ah
[SECTION .gdt]
; GDT
;                            段基址,        段界限 , 属性
LABEL_GDT:         Descriptor    0,              0, 0         ; 空描述符
LABEL_DESC_NORMAL: Descriptor    0,         0ffffh, DA_DRW    ; Normal 描述符
LABEL_DESC_CODE32: Descriptor    0, SegCode32Len-1, DA_C+DA_32; 非一致代码段, 32
LABEL_DESC_CODE16: Descriptor    0,         0ffffh, DA_C      ; 非一致代码段, 16
LABEL_DESC_DATA:   Descriptor    0,      DataLen-1, DA_DRW    ; Data
LABEL_DESC_STACK:  Descriptor    0,     TopOfStack, DA_DRWA+DA_32; Stack, 32 位
LABEL_DESC_TEST:   Descriptor 0500000h,     0ffffh, DA_DRW
LABEL_DESC_VIDEO:  Descriptor  0B8000h,     0ffffh, DA_DRW    ; 显存首地址
; GDT 结束
  1. 问题2:计算描述符的宏为什么这样写?
; 宏 ------------------------------------------------------------------------------------------------------ 
; 
; 描述符 
; 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 & 0FFFFh                ; 段界限1 
    dw    %1 & 0FFFFh                ; 段基址1 
    db    (%1 >> 16) & 0FFh            ; 段基址2 
    dw    ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)    ; 属性1 + 段界限2 + 属性2 
    db    (%1 >> 24) & 0FFh            ; 段基址3 
%endmacro ; 共 8 字节 
  1. 问题3:为什么界限=长度-1??
GdtLen		equ	$ - LABEL_GDT	; GDT长度
GdtPtr		dw	GdtLen - 1	; GDT界限

解释:段界限表达的是段内的最大偏移,而不是段的最大长度。 这个偏移从0开始计数,有点类似于C数组中的index.一个简单的例子,假如一个段有如下内存:
var1 db 0x01 ; 偏移0
var2 db 0x02 ; 偏移1
那么段界限应该是1还是2呢?答案是1,最大偏移是1。
访问段中数据使用:段基址 + 偏移(所以段界限说明的是这个最大偏移)
参考:段界限为什么要减1

保护模式进阶

这一章主要研究如何从保护模式跳回实模式

下面的代码展示了如何从保护模式进入实模式
我们可以看到,前面那个jmp并没有直接跳转到实模式,而是选择跳转到一个保护模式下的16位代码段,在16位保护模式代码段下的jmp才真正跳入到实模式。

那么,你也许会问,为什么要这么麻烦,我们在实模式下不是直接从16位代码段跳到32位代码段的保护模式下了么,那为什么从保护模式就不能直接跳回去呢?

弄明白这个问题很重要,下面我们慢慢详解。

我们还是先来搞清楚一个概念:段描述符高速缓冲寄存器

在实模式下,段寄存器含有段值,为访问存储器形成物理地址时,处理器引用相应的某个段寄存器并将其值乘以16,形成20位的段基地址。在保护模式下,段寄存器含有段选择子,如上所述,为了访问存储器形成线性地址时,处理器要使用选择子所指定的描述符中的基地址等信息。为了避免在每次存储器访问时,都要访问描述符表而获得对应的段描述符,从80286开始每个段寄存器都配有一个高速缓冲寄存器,称之为段描述符高速缓冲寄存器或描述符投影寄存器,对程序员而言它是不可见的。每当把一个选择子装入到某个段寄存器时,处理器自动从描述符表中取出相应的描述符,把描述符中的信息保存到对应的高速缓冲寄存器中。此后对该段访问时,处理器都使用对应高速缓冲寄存器中的描述符信息,而不用再从描述符表中取描述符。

这些高速缓冲寄存器在实方式下仍发挥作用,只是内容上与保护模式下有所不同。如上表所示,其中“Y”表示“是”; “N”表示“否”;“B”表示字节;“U”表示向上扩展,“W”表示以字方式操作堆栈。段基地址仍是 32位,其值是相应段寄存器值(段值)乘以16,在把段值装载到段寄存器时刷新。由于其值是16位段值乘上16,所以在实模式下基地址实际上有效位只有 20位。每个段的32位段界限都固定为0FFFFH,段属性的许多位也是固定的。所谓固定是指在实方式下不可设置这些属性值,只能继续沿用保护方式下所设置的值。因此,在准备结束保护模式回到实模式之前,要通过加载一个合适的描述符选择子到有关段寄存器,以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性。GDT中的描述符Normal就是这样一个描述符,在返回实模式之前把对应选择子Normal加载到DS和ES就是此目的。

CS段的段描述符高速缓冲寄存器的D位属性值为1表示代码当前运行在保护模式,D位属性值为0表示代码当前运行在实模式。而这个D位属性值在实模式下是固定的,也就是不容许改变的,只能在保护模式下改变。我们从上一篇日志看到,进入保护模式的标志是cr0的PE位置1,此时CPU就运行在了保护模式下,因此可以直接加载32位代码段的描述符,同时改变CS段描述符高速缓冲寄存器的D位属性值,这就是为什么可以直接从16位实模式代码段跳转到32位保护模式代码段的原因,因为跳转后的保护模式可以改变D位属性值。但是反过来就出问题了,由于实模式下D位属性值不容许改变,只能在保护模式下的时候就提前将D位属性值置0。那怎么置呢?我们想到一个方法就是先跳转到16位保护模式代码段,这样就可以置D位属性值为0,然后再由此跳入实模式。至此,这个问题我们算是弄明白了~

; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN	32
[BITS	16]
LABEL_SEG_CODE16:
	; 跳回实模式:
	mov	ax, SelectorNormal
	mov	ds, ax
	mov	es, ax
	mov	fs, ax
	mov	gs, ax
	mov	ss, ax

;ES 附加段寄存器
;CS 代码段寄存器
;SS 堆栈段寄存器
;DS 数据段寄存器
;FS 附加段寄存器
;GS 附加段寄存器

	mov	eax, cr0
	and	al, 11111110b
	mov	cr0, eax

LABEL_GO_BACK_TO_REAL:
	jmp	0:LABEL_REAL_ENTRY	; 段地址会在程序开始处被设置成正确的值

Code16Len	equ	$ - LABEL_SEG_CODE16
; END of [SECTION .s16code]

再来看看下面这一段代码,这个jmp实现了从保护模式跳转到实模式.首先,这个jmp是一个段间跳转,占用5字节(jmp1字节,段地址2字节(:前面的部分),偏移量2字节(:后面的部分)).指令图示
Orange's一个操作系统的实现:保护模式_第10张图片
编译之后,指令的"Segment"部分就是0000,但是在运行过程中,存放此指令的内存单元被mov [LABEL_GO_BACK_TO_REAL+3], ax修改了,即此时"Segment"部分放置的是ax寄存器中的值.

LABEL_GO_BACK_TO_REAL:
	jmp	0:LABEL_REAL_ENTRY	; 段地址会在程序开始处被设置成正确的值

你可能感兴趣的:(操作系统)