在计算机加电之后,bios检查硬件,并且把第一个扇区中的bootloader代码加载到0000: 07c00h处,开始执行bootloader代码.bootloader主要做两件事情:
- 从实模式进入保护模式
- 从硬盘(或者其他)中读取OS kernel到内存的固定位置处,然后跳转到OS中执行.
这里先讨论如何从实模式进入保护模式
参考链接:
- 学堂在线 - 清华大学OS课程
floppya: 1_44=freedos.img, status=inserted
floppyb: 1_44=pm.img, status=inserted
boot: a
# 相当于有两个软盘,软盘a作为bootloader,引导操作系统启动,软盘b里面放置我们自己
# 的对操作系统的操作(相当于操作系统内核)
format b:
命令格式化B:盘nasm pmtest1.asm -o pmtest1.com
sudo mkdir /mnt/floppy
sudo mount -o loop pm.img /mnt/floppy
sudo cp pmtest1.com /mnt/floppy
sudo umount /mnt/floppy
B:\pmtest1.com
即可执行参考:GDT,LDT,GDTR,LDTR 详解,包你理解透彻
GDT就是一个装有段描述符的大数组.在GDT中每一个段描述符占用8字节,段描述符最主要的包含两个信息:
在实模式下的寻址方式为:
在保护模式下,段寄存器实际存放的是选择子,选择子实际上就是GDT表的index.所以在保护模式下,段寄存器就相当于一个指针.
在下图中,逻辑地址由一个16位的段寄存器和32位EIP寄存器指出.根据段寄存器中的值(选择子,index),在GDT中找到段描述符(描述了段的基址和界限),线性地址=基址+EIP.需要注意的是:在没有页机制的情况下,线性地址和物理地址是等价的.
一个栗子
例如给出逻辑地址:21h:12345678h转换为线性地址
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
这3句是在计算物理地址,mov word [LABEL_DESC_CODE32+2], ax
是把ax的内容放到byte2和byte3处,后面的类似. ; 初始化 32 位代码段描述符
xor eax, eax
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
[SECTION .gdt]
; 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 结束
; 宏 ------------------------------------------------------------------------------------------------------
;
; 描述符
; usage: Descriptor Base, Limit, Attr
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1
dw %1 & 0FFFFh ; 段基址1
db (%1 >> 16) & 0FFh ; 段基址2
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0FFh ; 段基址3
%endmacro ; 共 8 字节
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
解释:段界限表达的是段内的最大偏移,而不是段的最大长度。 这个偏移从0开始计数,有点类似于C数组中的index.一个简单的例子,假如一个段有如下内存:
var1 db 0x01 ; 偏移0
var2 db 0x02 ; 偏移1
那么段界限应该是1还是2呢?答案是1,最大偏移是1。
访问段中数据使用:段基址 + 偏移(所以段界限说明的是这个最大偏移)
参考:段界限为什么要减1
这一章主要研究如何从保护模式跳回实模式
下面的代码展示了如何从保护模式进入实模式.
我们可以看到,前面那个jmp并没有直接跳转到实模式,而是选择跳转到一个保护模式下的16位代码段,在16位保护模式代码段下的jmp才真正跳入到实模式。
那么,你也许会问,为什么要这么麻烦,我们在实模式下不是直接从16位代码段跳到32位代码段的保护模式下了么,那为什么从保护模式就不能直接跳回去呢?
弄明白这个问题很重要,下面我们慢慢详解。
我们还是先来搞清楚一个概念:段描述符高速缓冲寄存器。
在实模式下,段寄存器含有段值,为访问存储器形成物理地址时,处理器引用相应的某个段寄存器并将其值乘以16,形成20位的段基地址。在保护模式下,段寄存器含有段选择子,如上所述,为了访问存储器形成线性地址时,处理器要使用选择子所指定的描述符中的基地址等信息。为了避免在每次存储器访问时,都要访问描述符表而获得对应的段描述符,从80286开始每个段寄存器都配有一个高速缓冲寄存器,称之为段描述符高速缓冲寄存器或描述符投影寄存器,对程序员而言它是不可见的。每当把一个选择子装入到某个段寄存器时,处理器自动从描述符表中取出相应的描述符,把描述符中的信息保存到对应的高速缓冲寄存器中。此后对该段访问时,处理器都使用对应高速缓冲寄存器中的描述符信息,而不用再从描述符表中取描述符。
这些高速缓冲寄存器在实方式下仍发挥作用,只是内容上与保护模式下有所不同。如上表所示,其中“Y”表示“是”; “N”表示“否”;“B”表示字节;“U”表示向上扩展,“W”表示以字方式操作堆栈。段基地址仍是 32位,其值是相应段寄存器值(段值)乘以16,在把段值装载到段寄存器时刷新。由于其值是16位段值乘上16,所以在实模式下基地址实际上有效位只有 20位。每个段的32位段界限都固定为0FFFFH,段属性的许多位也是固定的。所谓固定是指在实方式下不可设置这些属性值,只能继续沿用保护方式下所设置的值。因此,在准备结束保护模式回到实模式之前,要通过加载一个合适的描述符选择子到有关段寄存器,以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性。GDT中的描述符Normal就是这样一个描述符,在返回实模式之前把对应选择子Normal加载到DS和ES就是此目的。
CS段的段描述符高速缓冲寄存器的D位属性值为1表示代码当前运行在保护模式,D位属性值为0表示代码当前运行在实模式。而这个D位属性值在实模式下是固定的,也就是不容许改变的,只能在保护模式下改变。我们从上一篇日志看到,进入保护模式的标志是cr0的PE位置1,此时CPU就运行在了保护模式下,因此可以直接加载32位代码段的描述符,同时改变CS段描述符高速缓冲寄存器的D位属性值,这就是为什么可以直接从16位实模式代码段跳转到32位保护模式代码段的原因,因为跳转后的保护模式可以改变D位属性值。但是反过来就出问题了,由于实模式下D位属性值不容许改变,只能在保护模式下的时候就提前将D位属性值置0。那怎么置呢?我们想到一个方法就是先跳转到16位保护模式代码段,这样就可以置D位属性值为0,然后再由此跳入实模式。至此,这个问题我们算是弄明白了~
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回实模式:
mov ax, SelectorNormal
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
;ES 附加段寄存器
;CS 代码段寄存器
;SS 堆栈段寄存器
;DS 数据段寄存器
;FS 附加段寄存器
;GS 附加段寄存器
mov eax, cr0
and al, 11111110b
mov cr0, eax
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]
再来看看下面这一段代码,这个jmp实现了从保护模式跳转到实模式.首先,这个jmp是一个段间跳转,占用5字节(jmp1字节,段地址2字节(:前面的部分),偏移量2字节(:后面的部分)).指令图示
在编译之后,指令的"Segment"部分就是0000,但是在运行过程中,存放此指令的内存单元被mov [LABEL_GO_BACK_TO_REAL+3], ax
修改了,即此时"Segment"部分放置的是ax寄存器中的值.
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值