在实模式下,程序可以随意修改1M内存的任何位置的数据,没有任何监管,如同原始社会。
进入保护模式后,使用内存需要登记,不仅仅是你从哪里开始使用,还需要登记你使用的范围、用于和目的,读?写?还是执行?
为了实现登记,保护模式提出一个新的概念,描述符。每个段都有8个字节的描述,并且所有的描述符都需要集中存储,便于管理。这样就出现了描述符表。
而最主要的描述符表就是全局描述符表(Global Descriptor Table, GDT)。这个全局描述符表是进保护模式前必须先定义好的。
描述符表可以放在内存任何空闲位置,为了便于跟踪全局描述符,处理器内部有一个48位的寄存器,这就是全局描述符表寄存器 (GDTR)。
GDTR由32位线性地址和16位的边界组成,32位地址可位于4G内存中任何位置,16位边界描述了全局描述符表的边界。
一个描述符有8个字节,因此16位边界可界定(2^16)/8=8192个描述符。
要注意一点的是这个16 位边界需要总描述符减一。当然这是规定,哈哈。
接下来,我们就要介绍描述符了。
每个描述符占8个字节,也就是2个双字。
由于兼容,从8086到80286再到80386,这个描述符会有点怪,这也是我们常常不理解的地方。当然,你记住就好,能理解工程师的苦衷那就更好了。
一个描述符如下所示。
GDTR
每个描述符中指定了32位的段起始地址(高32位中的3124和2316,低32位中的3116),以及20位的段边界(高32位中的1916,低32位中的15~0)。
其它位的描述如下。
G 位是粒度位(Granularity)。G=0时,段界限以字节为单位,段的扩展为1B1MB;G=1时,段界限以4KB为单位,段的扩展为4KB4GB。
D/B 位区分段或栈是32位还是16位的,D/B=1表示32位,D/B=0表示16位。
L 位是64位代码段标志,L=1表示64位,其他则为0即可。
AVL 软件可用的位,处理器不使用。
P 位是段存在位,P=1表示段存在于内存中,P=0表示段不存在与内存。这个位可用于硬盘虚拟内存,当内存不够用时就将很少用的内存放于硬盘中,并这个位进行标志。
DPL 位时特权描述位,表示处理器4种特权:0、1、2、3,越小级别越高。
S 位表示描述符的类型。S=0表示一个系统段,S=1表示一个代码段或者数据段。
TYPE 位指示描述符的子类型。其对于数据段,4位是X、E、W、A;对于代码段,4位是X、C、R、A。
X | E | W | A | 描述符类别 | 含义 |
---|---|---|---|---|---|
0 | 0 | 0 | x | 数据 | 只读 |
0 | 0 | 1 | x | 数据 | 读、写 |
0 | 1 | 0 | x | 数据 | 只读,向下扩展 |
0 | 1 | 1 | x | 数据 | 读、写,向下扩展 |
X | C | R | A | 描述符类别 | 含义 |
---|---|---|---|---|---|
1 | 0 | 0 | x | 代码 | 只执行 |
1 | 0 | 1 | x | 代码 | 执行、读 |
1 | 1 | 0 | x | 代码 | 只执行,依从的代码段 |
1 | 1 | 1 | x | 代码 | 执行、读,依从的代码段 |
接下来我们就可以读示例代码了。(本人比较懒,就直接使用别人写好的例子啦,出处《x86汇编--从实模式到保护模式》,这是我见过写的最详细关于编写系统内核的书。)
org 0x7c00 ; 该命令表示程序将被装在到偏移地址为0x7C00的地方
jmp start
start:
mov ax,cs
mov ss,ax ;设置堆栈段和栈指针
mov sp,0x7c00
;计算GDT所在的逻辑段地址
mov ax,[cs:gdt_base] ;低16位 gdt_base是段内偏移标志,不是一个变量
mov dx,[cs:gdt_base+0x02] ;高16位
mov bx,16
div bx ;实模式下段基址x16+偏移地址,其实就是将0x7e00除以16
mov ds,ax ;令DS指向该段以进行操作
mov bx,dx ;段内起始偏移地址
mov dword [bx+0x00],0x00 ;创建0#描述符,它是空描述符,这是处理器的
要求
mov dword [bx+0x04],0x00
;创建#1描述符,保护模式下的代码段描述符
mov dword [bx+0x08],0x7c0001ff ; 7c00为段基址15~0,01ff为段界限
符15~0 512字节
mov dword [bx+0x0c],0x00409800 ;0040 00为段基址31~24 40:0100_0000B
表示G=0以字节为段界限粒度,D/B=1表示为32位偏移或操作,
L=0表示不为64位,AVL保留位,后面的0000为段界限19~16;
98:1001_1000表示P=1段存在,DPL=00特权为0最高级,
S=1表示为代码/数据段,TYPE=8表示只执行,00表示段基址23~16
;创建#2描述符,保护模式下的数据段描述符
(文本模式下的显示缓冲区)
mov dword [bx+0x10],0x8000ffff ;64KB 数据段基址 0xb8000显存地址
mov dword [bx+0x14],0x0040920b ;92表示TYPE=2读写
;创建#3描述符,保护模式下的堆栈段描述符
mov dword [bx+0x18],0x00007a00
mov dword [bx+0x1c],0x00409600 ;96表示type=6可读可写
;初始化描述符表寄存器GDTR
mov word [cs: gdt_size],31 ;描述符表的界限(总字节数减一)
lgdt [cs: gdt_size] ;载入6个字节,先载入gdt_size也就是31,
然后是gdt_base 0x7e000 32位基址
in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20
cli ;保护模式下中断机制尚未建立,应
;禁止中断
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位,打开了保护模式标志
;以下进入保护模式... ...
jmp dword 0x0008:(flush-0x7c00) ;16位的描述符选择子:32位偏移,
为什么是0x0008,因为已经进入了保护模式,段寄存器不
再保存基址了,而是保存偏移地址的索引号,8=01_000B,
也就是索引号为1的代码段描述符
;清流水线并串行化处理器,这个比较重要,
清初了流水线可更新段寄存器
[bits 32]
flush:
mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
mov ds,cx
;以下在屏幕上显示"Protect mode OK."
mov byte [0x00],'P'
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],' '
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
mov byte [0x18],' '
mov byte [0x1a],'O'
mov byte [0x1c],'K'
;以下用简单的示例来帮助阐述32位保护模式下
的堆栈操作
mov cx,00000000000_11_000B ;加载堆栈段选择子
mov ss,cx
mov esp,0x7c00
mov ebp,esp ;保存堆栈指针
push byte '.' ;压入立即数(字节)
sub ebp,4
cmp ebp,esp ;判断压入立即数时,ESP是否减4
jnz ghalt
pop eax
mov [0x1e],al ;显示句点
ghalt:
hlt ;已经禁止中断,将不会被唤醒
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址, 这个是自定义的,
只要在实模式1M内存内即可,当然最好是在第一个扇区512之后,
(7E00H = 7C00H + 512)
times 510-($-$$) db 0
db 0x55,0xaa
实验方法:
- 使用bochs自带的bximage创建一个1.44软盘
- 使用nasm -f bin test.asm -o test.bin编译汇编
- 使用dd if=test.bin of=test.img bs=1024 count=1440 conv=notrunc写入虚拟映像,再使用虚拟机引导即可