实现保护模式
保护模式和实模式
保护模式,是一种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的编译运行流程一样
得到下面结果: