一个操作系统的实现(3)


实现保护模式

保护模式和实模式

保护模式,是一种80286系列和之后的x86兼容CPU操作模式。保护模式有一些新的特色,设计用来增强多工和系统稳定度,像是 内存保护,分页系统,以及硬件支援的 虚拟内存。

保护模式与实模式相对应。在80286以前,CPU只有实时模式,地址总线有20位,而内存地址是16位,也就是最多能够访问2^20=1M的内存空间。在80286及以后,内存地址改为16位或32位,至少可以访问到2^32=4G的内存空间。但为了保证后续的CPU能够运行旧的CPU,只能保持向下兼容。因此,80286及以后的CPU首先进入实模式,然后通过切换机制再进入到保护模式。

保护模式与实模式相比,主要是两个差别:一是提供了段间的保护机制,防止程序间胡乱访问地址带来的问题,二是访问的内存空间变大,见前面的描述。

实模式下内存寻址

  • 段首地址*16+偏移量=物理地址(段寄存器左移四位+offset)

保护模式下寻址

  • 段寄存器中存放段选择子Selector
  • GDTR(全局描述符表寄存器)中存放段描述符首地址
  • 通过选择子与GDTR中首地址,找到对应的段描述符
  • 段描述符中有段的物理首地址,就得到段在内存中的首地址
  • 加上偏移量,就得到这个段中存放的数据的真正物理地址

实现从实模式到保护模式的转换

关键代码:

; ==========================================
; pmtest1.asm
; 编译方法:nasm pmtest1.asm -o pmtest1.bin
; ==========================================
%include "pm.inc" ; 常量, 宏, 以及一些说明
org 07c00h
	jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
;                              段基址,       段界限     , 属性
LABEL_GDT:    Descriptor       0,                0, 0           ; 空描述符
LABEL_DESC_CODE32: Descriptor       0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段
LABEL_DESC_VIDEO:  Descriptor 0B8000h,           0ffffh, DA_DRW      ; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
	mov ax, cs
	mov ds, ax
	mov es, ax
	mov ss, ax
	mov sp, 0100h
	; 初始化 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
	; 为加载 GDTR 作准备
	xor eax, eax
	mov ax, ds
	shl eax, 4
	add eax, LABEL_GDT ; eax <- gdt 基地址
	mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
	; 加载 GDTR
	 lgdt [GdtPtr]
	; 关中断
	cli
	; 打开地址线A20
	in al, 92h
	or al, 00000010b
	out 92h, al
	; 准备切换到保护模式
	mov eax, cr0
	or eax, 1
	mov cr0, eax
	; 真正进入保护模式
	jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
	; 并跳转到 Code32Selector:0  处
; END of [SECTION .s16]
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
	mov ax, SelectorVideo
	mov gs, ax ; 视频段选择子(目的)
	mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
	mov ah, 0Ch ; 0000: 黑底    1100: 红字
	mov al, 'P'
	mov [gs:edi], ax
	; 到此停止
	jmp $
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]

编译

nasm pmtest1.asm -o pmtest1.bin

复制之前的a.img和.bochsrc过来
并将生成的pmtest.bin二进制文件写入软盘

sudo dd if=pmtest1.bin of=a.img bs=512 count=1 conv=notrunc

运行bochs

代码分析:

  • 宏定义Descriptor可看做是表示描述符的结构体,由3个参数构成,这些Descriptor组成全局描述符表GDT
  • org 0100h 大于512bytes时制作成.com文件,通过freedos来加载.com文件,得到运行的效果
  • LABEL_GDT: Descriptor 0, 0, 0  ;描述符的三个参数分别表示段物理首地址,段界限,段属性,下面定义的三个描述符LABEL_GDT,CODE32和VIDEO构成描述符表
  • GdtPtr dw GdtLen - 1 GdtPtr结构共48位,低16位为段界限,高32位为0,以后会重置,高32位的访问通过GdtPtr+2进行,高32位存放GDT的物理地址        
  • SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT ;定义段选择子,即当前描述符相对全局描述符表的偏移量
  • LABEL_BEGIN:程序的入口处
    mov ax,csds,es,ss与cs代码段基地址都一样
    xor eax,eax自己与自己异或,将eax清零
    mov ax,cseax是32位寄存器,ax是eax的低16位
    shl eax,4 左移四位,相当于乘以16,实模式下计算物理地址
    add eax,LABEL_SEG_CODE32加上段相对代码段的偏移地址,eax中得到32位的CODE32段的物理地址
    mov word [LABEL_DESC_CODE32 + 2],ax用实际物理地址填充LABEL_DESC_CODE32段描述符的段物理首地址字段,其中低16位分别放在2,3字节,高16位中的低8位放在第4字节,高16位中的高8位放在第7字节.
    add eax,LABEL_GDT计算出实际物理地址
    mov dword [GdtPtr + 2],eax 将实际物理地址直接放到GdtPtr+2中(和实际GDTR寄存器结构相同)
    lgdt  [GdtPtr]通过lgdt指令将GdtPtr中内容加载到GDTR寄存器
    cli 关中断(实模式和保护模式下中断处理不同,故关中断防止出错)
    in al,92h 打开A20地址线
    mov eax,cr0将cr0寄存器的第0位(PE位,决定CPU运行于实模式还是保护模式)置为1
    jmp dword SelectorCode32:0跳入code32执行,加dword告诉编译器这句代码要编译成32位代码

进入保护模式的主要步骤总结:

  • 准备GDT(包括设置各个段描述符内容)
  • 用lgdt加载GDTR
  • 关中断,打开A20地址线
  • 置cr0的PE位
  • 跳转,进入保护模式

下载FreeDos,使用Dos执行pmtest1

下载地址:

http://bochs.sourceforge.io
https://bochs.sourceforge.io/diskimages.html
解压:tar vxzf   FreeDos.img.tar.gz
进入文件夹:cd freedos-img 
生成软盘印象:dd if=pmtest1.bin of=pm.img bs=512 count=1 conv=notrunc
修改当前.bochsrc文件,添加:
floppya: 1_44="freedos.img", status=inserted
floppyb: 1_44="pm.img", status=inserted
boot: a

启动bochs,格式化B盘

将代码pmtest1.asm的第8行中的07c00h改为0100h,并重新编译

nasm pmtest1.asm -o pmtest1.com

将pmtest1.com复制到虚拟软驱pm.img中

sudo mount -o loop pm.img /mnt/floppy/
sudo cp pmtest1.com /mnt/floppy/
sudo umount /mnt/floppy

出现的问题:挂载点/mnt/floppy不存在
解决:不存在的话,那就在/mnt目录下创建一个floppy
指令:mkdir /mnt/floppy

启动freedos,在B盘符下输入pmtest1.com
使用Dos执行pmtest1

从保护模式返回实模式

关键代码:GDT、数据段和堆栈段

%include "pm.inc" ; 常量, 宏, 以及一些说明
org 0100h
jmp LABEL_BEGIN
[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 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorTest equ LABEL_DESC_TEST - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .data1]  ; 数据段
ALIGN 32
[BITS 32]
LABEL_DATA:
SPValueInRealMode dw 0
; 字符串
PMMessage: db "In Protect Mode now. ^-^", 0 ; 在保护模式中显示
OffsetPMMessage equ PMMessage - $$
StrTest: db "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
OffsetStrTest equ StrTest - $$
DataLen equ $ - LABEL_DATA
; END of [SECTION .data1]
; 全局堆栈段
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK - 1
; END of [SECTION .gs]

其余代码不再展示

整体代码分析:

  • esi,edi区别:在串操作指令这类指令里(movs/cmps/stos/lods),esi和edi的使用是固定的,比如movs是由ds:[esi]复制到es:[edi]处,即指定源和目的.
  • CLD指令功能:将标志寄存器Flag的方向标志位DF清零,在字串操作中使变址寄存器SI或DI的地址指针自动增加
  • lodsb|lodsw|lodsd :用于将ds:[esi]指向的存储单元数据装入al|ax|eax中,stosb|stosw等则相反
  • test只是做与操作,结果只改变标志位寄存器
  • div SRC 无符号除法,16位被除数在ax,8位除数为源操作数,结果的8位商在AL中,8位余数在AH中.
  • mul SRC 如果SRC是字节操作数,则把AL中的无符号数与SRC相乘得到16位结果送入AX中,如果SRC是字操作数,则把AX中无符号数与SRC相乘得到32位结果送入DX和AX中,DX存高16位,AX存低16位.
  • 汇编换行实现—((视频段偏移/160)&0xff+1)*160
  • ja 根据jmp结果进行判断,若前者大于后者则跳转 jump if above
  • loop label指令,根据cx寄存器计数循环次数
  • movzx  dst,src  将源操作数取出,置于目的操作数,目的操作数其余位用0填充.
  • jmp 0:LABEL_REAL_ENTRY能实现jmp cs_real_mode:LABEL_REAL_ENTRY的效果:这条语句是编译后是属于代码段的,存在与内存中,而在程序真正运行时,保护模式下不能更改代码段内容,但是在实模式下是可以改变代码段内容的,在程序开头的实模式初始化时,有mov [LABEL_GO_BACK_TO_REAL+3],ax,这句代码就更改了原来的jmp 0:LABEL_REAL_ENTRY语句了.+3的原因和长跳转指令的机器码有关,这个机器码的第四五字节表示段基址。
  • Normal描述符:在实模式下不能改变段属性,我们预先设置一个Normal描述符,设置其属性,在返回实模式之前,将对应选择子SelectorNormal加载到ds,es,ss,使得返回实模式后段属性符合实模式要求.

和上面执行流程一样

GDT描述符、LDT描述符、段选择子

全局描述符表GDT(Global Descriptor Table)

在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里

段选择子(Selector)

由GDTR访问全局描述符表是通过“段选择子”(实模式下的段寄存器)来完成的。为了访问一个段,一个Pentium程序必须把这个段的选择子装入机器的6个段寄存器的某一个中。

段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。

局部描述符表LDT(Local Descriptor Table)

局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。

Pmtest3.asm和2的编译运行流程一样

得到下面结果:


文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录