从实模式迈向保护模式

一、摘要

在8086 CPU出现时,只有16bit的字长,且寻址模式只能是物理内存寻址。到了80286和80386,已经有32bit字长,且CPU支持虚拟地址寻址,且能做到内存访问权限控制。所以这两种情况分别称为实模式和保护模式。但是为了兼容8086,还需要保留实模式运行,然后再向保护模式转移。

二、实模式和保护模式

1、实模式下缺陷
(1)可以随意访问修改物理内存,没有权限限制,不安全。
(2)20根地址总线只能访问1MB内存。
(3)一次只能运行一个程序,不能实现并发(非并行)。
2、保护模式下优点
(1)通过虚拟内存映射访问物理内存,突破内存访问大小限制。
(2)开启页表,实现并发。
(3)CPU扩展为32位,地址总线可访问4GB内存。
(4)描述符,限制内存段访问权限。
实模式到保护模式的转变,本质上:
(1)从用物理地址,到使用虚拟地址。这样才会有页表,平坦模式等这些体现保护模式优势的操作。
(2)到80286后,CPU增加对描述符的支持,例如GDTR,选择子。CPU对内存做权限检测,让访问内存有权限限制。
3、寄存器寻址方式
(1)32位CPU下,通用寄存器、指令指针寄存器、标志寄存器都32位,段寄存器16位。
(2)实模式:基址寄存器只有bx,bp,变址寄存器只有si,di。
保护模式:基址寄存器所有32位通用寄存器,变址寄存器为除esp外的所有通用寄存器。
4、push压栈
(1)在实模式环境下:
当压入 8 位立即数时,由于实模式下默认操作数是 16 位, CPU 会将其扩展为 16 位后再将其入枝, sp-2 。
当压入 16 位立即数时, CPU 会将其直接入枝, sp-2 。
当压入 32 位立即数时, CPU 会将其直接入枝, sp-4 。
(2)在保护模式下
当压入 位立即数时,由于保护模式下默认操作数是 32 位,CPU 将其扩展为 32 位后入拢, esp 指针减4
当压入16位立即数时,CPU直接压入2字节,esp指针减2。
当压入32位立即数时,CPU直接压入4字节,esp指针减4。
当压入段寄存器时,压入4字节,esp指针减4。

三、保护模式概述

1、全局描述符表保存在内存中,存有段描述符,选择子索引到段描述符。段基址在段描述符中,给出的选择子索引到段描述符后, CPU自动从段描述符中索引取出段基址。然后再加上段内偏移地址,便凑成“段基址:段内偏移地址”的形式。且段基址不需要再乘16。
2、段寄存器保存选择子,可以索引到段描述符。这个选择子相当保存数组下标。
3、CPU更新:8086——>80286——>80386
4、段描述符缓冲寄存器:由于段描述符存在内存中,访问较慢,且需要经过拼凑成完整数据才能给CPU使用,故将拼凑的数据保存到段描述符缓冲寄存器,以减少CPU访问时间。而实模式下,也可以将左移四位后的段基址保存到该寄存器。段描述符缓冲寄存器是在CPU重新访问一个段时更新。
5、平坦模式:
(1)32 / 64位处理器有完整的数据 / 地址总线,不需要分段就可以访问全部物理内存。所以可以做到段基址不变,只需要段内偏移即可访问所有内存。段基址为0。
(2)一个任务一个段,且段大小限定为4GB虚拟地址空间。
(3)分段机制作为X86处理器的固有机制,是无法绕过的。处理器总是按照[段基址 + 段内偏移]的方式生成线性地址。只是段基址可以不变。
(4)平坦模型一定是在开启分页机制的情况下使用的,因为平坦模型的基础就是分页模型带来的4GB虚拟地址空间,而平坦模型就是在这个空间中平坦。
(5)系统中仍然需要构造段描述符,但是每个段的虚拟基地址均为0,段界限均为0xFFFFF,段粒度均为4KB,也就是说每个段描述符都指向完整的4GB虚拟地址空间。
(6)在程序加载时,将程序一次性加载到线性地址空间中,同时创建页目录和页表,建立虚拟地址空间到物理内存的映射。
6、GDTR寄存器,存储GDT内存起始地址和GDT界限。48位。由指令lgdt加载。
lgdt 的指令格式是: lgdt 48位内存数据。
7、选择子,保存在16段寄存器中。包含请求特权级(RPL),描述符类型(GDT or LDT),段描述符索引

四、段描述符表

1、为方便理解,将全局描述符表看成数组,段描述符表看成数组元素,GDTR寄存器保存着数组首地址,段寄存器(选择子)保存着数组下标和对应下标段描述符访问权限。
2、段描述符表格式,连续的64位(8字节)。为方便描述和查看,人为分成高32位,低32位。

(1)高3124、高70、低31~16共32位拼成段基址,表示段描述符首地址。
(2)高16-19,低0~15共20位拼成段界限,这是一个数值,然后以
段界限边界值=(段界限+1)*单位-1
公式表示该段描述符指向的段能占用的内存大小。具体还要结合高23位G位,看单位是4KB还是1字节。
(3)G=0,单位为1字节。G=1,单位为4KB。
(4)D/B=0,指令中的有效地址和操作数是 16 位,栈段使用sp 寄存器。D/B=1,指令中的有效地址和操作数是 32位,栈段使用esp 寄存器。
(5)L=1,64位代码段。L=0,32位代码段。
(6)AVL=0,设0。当段内存存在时,且访问了段描述符,这CPU会将值置为1。
(7)P=1,段存在于内存中。P=0,段不存在于内存中。
(8)DPL,有两位,定义该段描述符归属特权级。有四种特权级(0,1,2,3),Linux用到0,3特权级,分别系统态(0),用户态(3)
(9)S=1,非系统段,就是代码段、数据段这些。S=0,系统段,就是表示”门“结构,如中断门、任务门。
(10)S=1时,TYPE共4位,定义代码段或数据段,已经执行、读写权限。S=0时,TYPE定义门结构相关属性

3、GDT中第0个段描述符不可用。
4、CPU检测过程
(1)保证选择子正确,索引值小于等于描述符表段描述符个数。
描述符表基地址 + 选择子中索引值*8 + 7 <= 描述符表基地址 + 描述符表界限值。
先检查TI位值,是在GDP或LDT中取描述符表基地址和段界限值,然后用上面公式计算。
(2)检查选择子并索引到段描述符后,检查段描述符TYPE位,检查段寄存器的用途和段类型是否匹配。
(3)通过P位检查内存段是否存在。
(4)CPU 每访问一个代码指令或数据,都要确认该指令或数据长度不能超过其所在内存段的范围 。

四、开启保护模式

1、配置描述符表
2、设置A20。为兼容8086 CPU,默认超过20根地址总线的内存访问会被地址回绕。要想使用20根地址总线以上的地址总线,需要设置A20。设置指令如下:
in al, 0x92
or al, 0000_0010B
out 0x92, al
3、置CR0寄存器第0位为1,第0位命名为PE(Protection Enable)。设置指令如下:
mov eax,cr0
or eax,0x00000001
mov cr0,eax

五、bug调试

1、问题:boot.asm不能正常运行
2、思路
(1)对比bin和img文件,查看是否对得上。
(2)对比查看0x500后内存,查看是否正常从硬盘加载数据到内存。
(3)发现内存内容不对,并且第一个扇区加载正常,第二扇区对不上。猜测是加载数据时有问题。
(4)经调试代码,发现犯了个低级错误。少些一行代码:mov cx,ax。导致loop只循环256次。
mov ax, di
mov cx,256
mul cx ;一个扇区512字节,每次读2字节,故一个扇区需要读取256次。既是axcx。mul的隐式表达式为:dxax=ax*cx,溢出会存到dx,故这里dx会赋值0x00,所以mov dx,0x1f0需要写在mul后面,否则会被修改。
mov cx,ax
mov dx,0x1f0
;循环读取硬盘数据到内存
.read_data:
in ax, dx
mov [bx], ax
add bx, 2
loop .read_data

六、代码

1、code tree
在这里插入图片描述
2、bochs

###############################################################
# Configuration file for Bochs
###############################################################

# how much memory the emulated machine will have
megs: 32

magic_break: enabled=1
display_library: x, options="gui_debug"

# filename of ROM images
romimage: file=/usr/local/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/local/share/bochs/VGABIOS-lgpl-latest

# what disk images will be used
#floppya: 1_44=./hd30M.img, status=inserted
ata0-master: type=disk, path="./hd30M.img", mode=flat

# choose the boot disk.
boot: disk

# where do we send log messages?
# log: bochsout.txt

# disable the mouse
mouse: enabled=0

# enable key mapping, using US layout as default.
#keyboard_mapping: enabled=1, map=/usr/share/bochs/keymaps/x11-pc-us.map

3、makefile

.PHONY:build image clean

mbr_src=mbr.asm
boot_src=boot.asm
mbr=mbr.bin
boot=boot.bin
img=./hd30M.img

all:build image

build:
	@-rm -rf *.bin
	nasm -I include/ -o $(mbr) $(mbr_src)
	nasm -I include/ -o $(boot) $(boot_src)

## boot.img,30MB大小,前512字节是MBR

image:
	@-rm -rf $(img)
	bximage -hd=30 -func=create -imgmode=flat -sectsize=512 -q $(img)
	dd if=$(mbr) of=$(img) bs=512 count=1 conv=notrunc
	dd if=$(boot) of=$(img) bs=512 seek=2 count=2 conv=notrunc

bochs:
	bochs -qf bochsrc

clean:
	rm -rf *.img *.bin

4、mbr.asm

;引入外部文件,这个文件定义常量
%include "boot.inc"
;代码以此地址作为偏移地址的基地址
[ORG 0x7c00]

;指定代码段和16bit模式
[SECTION .text]
[BITS 16]

_start:
	mov ax,cs           ;寄存器清0
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00       ;设置栈寄存器sp,栈地址从上往下存储,恰好0x500~0x7bff是可用区域
	mov ax,0xb800       ;设置gs段寄存器地址,gs段寄存器不能直接赋值,只能通过先传给通用寄存器,再传gs寄存器。
	mov gs,ax

	;call print          ;跳转打印

	mov eax,2           ;读取起始扇区地址
	mov bx,LOADER_BASE_ADDR       ;写入的地址
	mov cx,2			;待读入扇区数
	call rd_disk        ;将boot加载到内存

	call print          ;打印"MBR"
    xchg bx, bx
	jmp LOADER_BASE_ADDR     ; 跳转到0x500

print:
	;int 0x10 相当函数,参数从ax,bx,cx,dx这些寄存器取。清掉BIOS的输出。
	mov	ax,	0x0600  ;ah=0x06,表示功能号,功能为上卷清屏.al=0x0,表示上卷行数,为0则表示全部
	mov	bx,	0x0700  ;上卷属性,黑底
	mov	cx,	0       ;左上角坐标(0, 0)
	mov	dx,	0x184f  ;右下角坐标(24, 79), 0x18=24,0x4f=79
	int	0x10        ;0x10中断

	;写入1MB内存中,文本显示区域0xb800-0xbffff,显卡会自动在该内存映射到显示器。显示字符需要两个字节,一个保存字符,一个保存字符属性
	mov byte [gs:0x00],'M'
	mov byte [gs:0x01],0x0F  ;表示黑色背景,4表示前景色为白色
	mov byte [gs:0x02],'B'
	mov byte [gs:0x03],0x0F
	mov byte [gs:0x04],'R'
	mov byte [gs:0x05],0x0F
	ret

rd_disk:
	mov esi,eax     ;备份esi
	mov di,cx       ;备份di

	;设置读取扇区数
	mov dx,0x1f2
	mov al,cl
	out dx,al
	mov eax,esi

	;0x1f3 8bit iba地址低八位 0-7
	inc dx
	out dx,al

	;0x1f4 8bit iba地址中八位 8-15
	inc dx
	mov cl,8        ;shr移多个位可以用cl存储移位数
	shr eax,cl
	out dx,al

	;0x1f5 8bit iba地址高八位 16-23
	inc dx
	shr eax,cl
	out dx,al

    ; 0x1f6 8bit
    ; 0-3 位iba地址的24-27
    ; 4 0表示主盘 1表示从盘
    ; 5、7位固定为1
    ; 6 0表示CHS模式,1表示LAB模式
    inc dx
    mov al, ch
    or al, 0xe0
    out dx, al

    ; 0x1f7 8bit  命令或状态端口
    inc dx
    mov al, 0x20
    out dx, al

    ;循环检查硬盘状态
.read_check:
    in al,dx
    and al,0x88  ; 取硬盘状态的第3、7位
    cmp al,0x08  ; 硬盘数据准备好了且不忙了
    jnz .read_check

    ; 读数据
    mov ax, di
    mov cx,256
    mul cx       ;一个扇区512字节,每次读2字节,故一个扇区需要读取256次。既是ax*cx。mul的隐式表达式为:dx*ax=ax*cx,溢出会存到dx,故这里dx会赋值0x00,所以mov dx,0x1f0需要写在mul后面,否则会被修改。
    mov cx,ax
    mov dx,0x1f0

    ;循环读取硬盘数据到内存
.read_data:
    in ax, dx
    mov [bx], ax
    add bx, 2
    loop .read_data

    ret

    times 510-($-$$) db 0    ;一个扇区512字节,减去0xaa55为510字节。($$-$)为上面代码所占用字节数,510-($-$$)为当前行到魔术符字节数。time 字节数 dd 0 : 填充当前字节开始,到字节数为0
    dw 0xaa55                ;BIOS程序识别MBR魔术符

5、boot.asm

;引入外部文件,这个文件定义常量
%include "boot.inc"

[ORG  0x500]


[SECTION .gdt]
SEG_BASE equ 0
SEG_LIMIT equ 0xfffff

CODE_SELECTOR equ (1 << 3)
DATA_SELECTOR equ (2 << 3)

;第一个段描述符,不可访问,设为0
gdt_base:
    dd 0, 0

;0x00_1_1_0_0_0xf_1_00_1_1000_0x00_0x0000_0xffff
gdt_code:
    dw SEG_LIMIT & 0xffff                           ;段界限低16位
    dw SEG_BASE & 0xffff                            ;段基址低16位
    db SEG_BASE >> 16 & 0xff                        ;段基址中8位
    db 0b1_00_1_1000                                ;P_DPL_S_TYPE
    db 0b1_1_0_0_0000 | (SEG_LIMIT >> 16 & 0xf)     ;G_DB_L_AVL_LIMIT:LIMIT段极限高4位
    db SEG_BASE >> 24 & 0xf                         ;段基址高8位

;0x00_1_1_0_0_0xf_1_00_1_0010_0x00_0x0000_0xffff
gdt_data:
    dw SEG_LIMIT & 0xffff                           ;段界限低16位
    dw SEG_BASE & 0xffff                            ;段基址低16位
    db SEG_BASE >> 16 & 0xff                        ;段基址中8位
    db 0b1_00_1_0010                                ;P_DPL_S_TYPE
    db 0b1_1_00_0000 | (SEG_LIMIT >> 16 & 0xf)      ;G_DB_AVL_LIMIT:LIMIT段极限高4位
    db SEG_BASE >> 24 & 0xf                         ;段基址高8位
gdt_display:
    dd 0x80000007                                   ;文本显示内存地址为0xb8000~0xbffff,所以文本显示段基址为0xb8000,段界限为(0xbffff-0xb8000)/4k=0x7
	db 0x0b
    db 0b1_00_1_0010                                ;P_DPL_S_TYPE
    db 0b1_1_00_0000 | (SEG_LIMIT >> 24 & 0xf)      ;G_DB_AVL_LIMIT
    db SEG_BASE >> 24 & 0xf                         ;段基址高8位

    times 60 dq 0					 ; 此处预留60个描述符的位置
    SELECTOR_CODE equ (0x0001<<3) + 000b + 00b         ; 相当于(gdt_code - gdt_base)/8 + TI_GDT + RPL0
    SELECTOR_DATA equ (0x0002<<3) + 000b + 00b	 ; 同上
    SELECTOR_DISPLAY equ (0x0003<<3) + 000b + 00b	 ; 同上

;计算出全局描述符界限和内存起始地址
gdt_ptr:
    dw $ - gdt_base - 1
    dd gdt_base

[SECTION .text]
[BITS 16]
global boot_start

loadermsg db 'boot in real.'

boot_start:

    mov	sp, LOADER_BASE_ADDR
    mov	bp, loadermsg           ; ES:BP = 字符串地址
    mov	cx, 12			 ; CX = 字符串长度
    mov	ax, 0x1301		 ; AH = 13,  AL = 01h
    mov	bx, 0x001f		 ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
    mov	dx, 0x1800		 ;
    int	0x10                    ; 10h 号中断

	;开启A20总线
    in al,0x92
    or al,0000_0010B
    out 0x92,al

	lgdt [gdt_ptr]

	;cr0寄存器PE位置1,开启保护模式
    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

	;跳转到保护模式地址,刷新掉16位实模式下流水线和缓存。
	jmp  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_BASE_ADDR
   mov ax, SELECTOR_DISPLAY
   mov gs, ax

    ; 测试是否成功进入保护模式.如果没有,写入内存是失败的
    xchg bx, bx
    mov byte [0x100000], 0xaa

   jmp $

6、效果
从实模式迈向保护模式_第1张图片

你可能感兴趣的:(操作系统,linux,系统架构)