基于网上的一些博客的基础上,我对操作系统中实模式与保护模式的切换,做了一点整理,仅供参考,这篇写的是从实模式进入到保护模式。
在从实模式进入到保护模式之前,要先对GDT(全局描述符表),GDT段选择子,数据段,全局堆栈段进行定义。
对全局描述符表定义
org 0100h ;加载到偏移地址0100处
jmp LABEL_BEGIN ;跳入到16位代码段(实模式)进行各个数据段,代码段,堆栈段的初始化,最后跳入到保护模式
; 对GDT定义
;SECTION.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基地址
对段选择子定义
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
对数据段定义
;SECTION.data1
ALIGN 32 ;32位字节对齐
[BITS 32] ;32位字节
LABEL_DATA:
SPValueInRealMode dw 0
; 字符串
PMMessage: db "In Protect Mode now. ", 0 ; 在保护模式中显示 ;(后面的零作为字符串的结束标志,Line189、232的test会用到)
OffsetPMMessage equ PMMessage - $$ ; ($$ == LABEL_DATA 的基地址,相对于 LABEL_DATA 的偏移地址)
StrTest: db "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0 ;(后面的零作为字符串的结束标志)
OffsetStrTest equ StrTest - $$
DataLen equ $ - LABEL_DATA
对全局堆栈段定义
;SECTION.gs
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK - 1 ;堆栈顶
以上是为了从实模式切换到保护模式的准备,接下来才是重点。
为从实模式跳转到保护模式所做的准备工作
;SECTION.s16
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
让cs=ds=es=ss,让各个段处于相同的64KB空间,便于代码和数据处理;sp栈顶指针指向0100h段基地址。
mov [LABEL_GO_BACK_TO_REAL+3], ax
mov [SPValueInRealMode], sp ; 保存sp,然后返回实模式后,再将sp取出来
mov [label_backto_real + 3], ax, 这是在代码已经加载到了内存之后再执行的,为什么加上3,一位jmp指令占用3个字节, 而jmp后面跟着跳转地址;他的作用是在段基地址存储单元中重新设置新值去覆盖原先的段基地址。
初始化 16 位代码段描述符
mov ax, cs ;代码带赋值给ax
movzx eax, ax ;eax是32位,而这是16位代码段,所以用零扩展传送,就是屏蔽eax的高16位再赋值
shl eax, 4 ;eax逻辑左移4位
add eax, LABEL_SEG_CODE16 ;LABEL_SEG_CODE16即偏移地址,即物理地址=段地址*16+offset
mov word [LABEL_DESC_CODE16 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE16 + 4], al
mov byte [LABEL_DESC_CODE16 + 7], ah
以上代码就是在根据代码段描述符,并且初始化它。详情可见初始化代码段描述符
初始化32位代码段描述符
xor eax, eax ;异或清零,等同于mov eax,0 但比它的效率高
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
以上代码类似于上段。
初始化数据段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_DATA ; 所以 OffsetStrTest 既是字符串相对于 LABEL_DATA 的偏移地址,也是其在数据段的偏移
mov word [LABEL_DESC_DATA + 2], ax
shr eax, 16
mov byte [LABEL_DESC_DATA + 4], al
mov byte [LABEL_DESC_DATA + 7], ah
类似于上段的初始化。
初始化堆栈段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK
mov word [LABEL_DESC_STACK + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK + 4], al
mov byte [LABEL_DESC_STACK + 7], ah
类似于以上。
为加载 GDTR 作准备,填充GDT基地址的数据结构
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT
mov dword [GdtPtr + 2], eax
将GDT的地址送到eax中,再将eax送给GDTR(全局描述符寄存器)的前32位。
加载 GDTR
lgdt [GdtPtr]
关中断
cli ;禁止中断发生
打开地址线A20
in al, 92h ;从92h号端口读入一个字节
or al, 00000010b ;将从端口0x92读入数据的二进制码的第二位置1,从而实现开启A20地址。因为A20信号的第二位就是用于控制开启/禁止A20地址的
out 92h, al ;向92h号端口写入一个字节
准备切换到保护模式, PE位置1
mov eax, cr0
or eax, 1
mov cr0, eax
打开cr0控制寄存器的PE位,也就是将PE位置1,这是保护模式的开关。
进入保护模式
jmp dword SelectorCode32:0 ; 把 SelectorCode32 装入cs, 并跳转Code32Selector:0 处(双字节跳转到32位代码段第一条指令开始执行)
保护模式下,是通过段选择子:偏移量的方式寻址的;不同于在实模式下:段寄存器×16+偏移量得到32位物理地址。
进入到保护模式后,可以对屏幕进行输出:
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子
mov edi, (80 * 10 + 0) * 2 ; 屏幕第 10 行, 第 0 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P' ;输出P
mov [gs:edi], ax
; 到此停止
jmp $
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
综上所述,进入保护模式的步骤:1.准备GDT 2.用lgdt加载gdtr 3.打开A20地址线 4.跳转