第四章 保护模式
- 实模式的缺陷
- 操作系统和用户程序处于同一个特权级
- 逻辑地址和物理地址相同,用户程序所引用的地址都指向真实的物理地址
- 用户程序可以自由修改段基址,可以访问所有内存
- 访问超过64KB的内存区域需要切换段基址
- 一次只能运行一个程序
- 共20条地址线,最大可用内存为1MB
- 32位的CPU可以兼容运行16位的实模式
- CPU的发展要以兼容为主,向下兼容是发展的基本要求
- 保护模式下16位寄存器被拓展成32位
- 全局描述符表中每个表项是段描述符,大小为64KB,用来描述内存段的约束条件和属性信息
- 1985年推出了首款32位处理器80386,地址总线和寄存器都是32位的,任意一个段都可以访问4GB空间
- 实模式下寄存器的使用是固定的
- bits伪指令可以告诉编译器代码编译的机器码位数
- 进入保护模式的三个步骤:
- 打开A20
- 加载gdt
- 将cr0的pe位置1
- ***段描述符是
规定某段内存属性
的数据结构,大小为连续的8字节。
- 段界限(两段):+1后表示规定段粒度的数量
- 数据段和代码段,地址越来越高,此时的段界限用来表示段内偏移的最大值。
- 栈段,地址越来越低, 此时的段界限用来表示段内偏移的最小值
- 段基址(三段):程序段在内存中的起始地址
- TYPE字段:共4位,与S位配合使用来指定本描述符类型
- 系统段下,表示各种硬件级别的门调用
- 非系统段下,由X分为代码段/数据段
- A访问位,初始创建为0,cpu访问过后置1
- C/E表示一致性,C为1表示转移后特权级顺从转移前特权级,E为1表示向下拓展
- R/W表示,可读性/可写性
- X表示可执行性,1为可执行的代码段,0为不可执行的数据段
- S位:0表示系统段,1表示数据段
- DPL:表示描述符的4个特权级,0、1、2、3,数字越小,权限越大
- P位:如果段存在于内存中, P 为1 ,否则P 为0 ,抛出异常,转入异常处理完成后置1
- AVL:相对于用户来说是否可用
- L位:1表示64位代码段,0表示32位代码段
- G位:0表示段界限粒度大小为1字节,1表示段界限粒度大小为4KB
- 保护模式下有分页功能,可以按页(4KB)的单位来将内存换入换出
- 全局描述符表GDT:相当于一个描述符的数组,数组中的每个元素都是8字节的描述符,存储在内存中。“全局”表示多个程序均可在里面定义自己的段描述符
- GDTR:专用寄存器用于存储GDT的内存起始地址
- GDT大小为16位二进制,其表示的范围是,每个描述符大小是8字节,所以GDT中最多可容纳描述符数量是$2{16}/2{3} = 8192 $
- 16位段寄存器CS、DS、ES、FS、GS、SS
- 在实模式下,存储段基址,即内存段的起始地址
- 在保护模式下,存储段选择子selector
- 16位段选择子的组成
- 第0-1位表示RPL,有四个特权级
- 第2位表示TI,0表示选择子在GDT中索引描述符,1表示在LDT中索引描述符
- 第3-15位表示描述符的索引值,即段描述符在GDT中的下标
- 保护模式下使用32位的寄存器和地址线,不需要段基址*16再与段内偏移地址相加,而是使用
用选择子索引的段描述符中的段基址再加上段内偏移地址即为要访问的内存地址
- GDT中的第0个段描述符不可访问,因为未初始化的选择子值为0,避免错误访问到第0个描述符
- 局部描述符LDT,与任务一一对应,每个任务的私有内存段都应该放到自己的段描述符表中
- 地址回绕:逻辑地址超出了物理地址范围,让超出的逻辑地址自动回绕到0地址,继续从0地址开始映射
- A20Gate用来控制第21(A20)的有效性
- 如果A20Gate被打开,当访问到0x100000~0x10FFEF之间的地址时,CPU将真正访问这个内存
- 如果A20Gate被禁止,当访问到0x100000~0x10FFEF之间的地址时,CPU将采用地址回绕
- 打开A20,即突破20条地址线去访问更大的内存空间,只有关闭地址回绕才能实现
// 打开A20方式:将0x92的第1位置1
in al,0x92
or al,0000_0010B
out 0x92,al
- CRx寄存器系列可以用于控制和展示CPU,CR0寄存器的第0位是PE位,1表示再保护模式下运行,0表示在实模式下运行
mov eax,cr0 // cr0写入eax
or eax,0x00000001 // or运算将eax的第0位置置为1
mov cr0,eax // eax写回cr0,即PE位为1
- 保护模式下的源程序:
; 主引导程序MBR
; SECTION是伪指令,cpu不运行,只是方便程序员规划程序分段使用
; `vstart=0x7c00`表示在程序编译时将起始地址编译为0x7c00
; SS存放栈顶的段地址,SP存放栈顶的偏移地址。在任何时刻 ,SS:SP都是指向栈顶元素
; CS存放内存中代码段入口的段基址,CS:IP表示下一条要运行的指令内存地址
; 初始化部分
%include "boot.inc"
SECTION MBR vstart=0x7c00 ; =前后不能有空格
mov ax,cs ; 由于BIOS是通过`jmp 0:Ox7c00`转到MBR的,故cs此时为0
mov ds,ax ; 段寄存器不能使用立即数进行赋值,可以使用通用寄存器ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
; 使用10号中断的0x06功能号,进行窗口上卷清屏,避免BIOS检测信息影响显示
mov ax,0600h ; ah存放将要调用的中断子功能号
mov bx,0700h
mov cx,0 ; (CL,CH)=窗口左上角的(X,Y)位置
mov dx,184fh ; (DL,DH)=窗口右下角的(X,Y)位置(80,25)
int 10h ; 调用中断
; 输出背景色是绿色,前景色是红色,并且跳动的字符串为“1 MBR”
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
; 硬盘扇区的写入
mov eax,LOADER_START_SECTOR ; 起始扇区lba地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,4 ; 待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR
; 功能:读取硬盘的n个扇区,在16位模式下
rd_disk_m_16:
mov esi,eax ; 备份eax
mov di,cx ; 备份cx
; 读写硬盘
; 第一步:设置要读取的扇区数
mov dx,0x1f2 ; 存储端口号
mov al,cl
out dx,al ; 读取的扇区数(out指令用于向端口写数据)
mov eax,esi ; 恢复ax
; 第二步:将LBA地址存入端口0x1f3 ~ 0x1f6
; LBA地址的0~7位写入端口0x1f3
mov dx,0x1f3
out dx,al
; LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
; LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f ; lab第24~27位
or al,0xe0 ; 设置7-4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
; 第三步:向0x1f7端口写入读命令0x20
mov dx,0x1f7
mov al,0x20
out dx,al
; 第四步:检测硬盘状态
.not_ready: ; 同一端口写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ; 第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ; 若未准备好,继续等待
; 第5步:从0x1f0端口读数据
; di为要读取的扇区数,一个扇区有512字节,每读取一个字,共需di*512/2次
mov ax,di
mov dx,256
mul dx
mov cx,ax
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret
; $表示本行指令所在的地址,$$表示本section的起始地址,$-$$表示执行代码行到段首的偏移量
times 510-($-$$) db 0 ; 将剩余字节用0进行填充
db 0x55,0xaa ; 最后两个字节填充MBR的标识
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
;-------------- gdt描述符属性 -------------
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ 1_0000000000000000000000b
DESC_L equ 0_000000000000000000000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0_00000000000000000000b ; cpu不用此位,暂置为0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b
DESC_P equ 1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_CODE equ 1_000000000000b
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0_000000000000b
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.
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
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
; loader.s
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR ; 指定虚拟起始地址0x900
LOADER_STACK_TOP equ LOADER_BASE_ADDR ;栈底也定位 0x900
jmp loader_start ;跳转到下面 loader_start 处执行
;构建gdt及其内部的描述符
; dd用于定义双字变量的伪指令,一个字是两个字节,即四个字节
; GDT的第0个描述符,共8个字节,第0个描述符无用初始化为0
GDT_BASE: dd 0x00000000
dd 0x00000000
; 第1个描述符的低 4 位,看名称是代码段的段描述符,高四位为
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
;数据段和栈段的描述符
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
; 显存段描述符
VIDEO_DESC: dd 0x80000007
dd DESC_VIDEO_HIGH4 ; 此时dpl已改为0
GDT_SIZE equ $ - GDT_BASE ;GDT的大小
GDT_LIMIT equ GDT_SIZE - 1 ; GDT的大小减一就是 GDT 的界限,就是填在GDTR中的低16位
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的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT ;GDTR的低16位
dd GDT_BASE
; GDTR的高 32 位,GDT的基地址。;gdt_ptr相当于指针。因为gdt_ptr是存放这48位的地址。
loadermsg db '2 loader in real.'
loader_start:
mov sp,LOADER_BASE_ADDR
mov bp,loadermsg
mov cx,17
mov ax,0x1301
mov bx,0x001f
mov dx,0x1800
int 0x10
;---------------------------------------- 准备进入保护模式 ------------------------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1
;----------------- 打开A20 ----------------
in al,0x92 ; in是端口读取指令。从0x92号端口读取到al中
or al,0000_0010B
out 0x92,al
;----------------- 加载GDT ----------------
lgdt [gdt_ptr] ;注意!这里用的是 [] ,就是取地址为gdt_ptr的内存中的数据,就是那48位数据
;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响
[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 $
#!/bin/bash
rm -rf ./hd.img
bin/bximage -hd -mode="flat" -size=60 -q hd.img
echo "disk creat success!!"
nasm -I include/ -o mbr.bin mbr.s
dd if=mbr.bin of=hd.img bs=512 count=1 conv=notrunc
echo "disk write success!!"
nasm -I include/ -o loader.bin loader.s
dd if=loader.bin of=hd.img bs=512 count=4 seek=2 conv=notrunc
bin/bochs -f bochsrc
# 注意:其中的count=4
花了两天时间,总是出现boot not device错误,和无法加载boot.inc,可能是boot.inc头部注释问题,还有loader的dd写入扇区个数应该为4
of=hd.img bs=512 count=1 cnotallow=notrunc
echo “disk write success!!”
nasm -I include/ -o loader.bin loader.s
dd if=loader.bin of=hd.img bs=512 count=4 seek=2 cnotallow=notrunc
bin/bochs -f bochsrc
注意:其中的count=4