在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
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
###############################################################
# 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 $