这里实现引导区和内核区分离以及保护模式功能。
前面实现制作虚拟软盘,引导扇区写入代码来实现helloworld,系统启动来加载软盘的引导扇区即第一个扇区,由于第一个扇区只有512Byte,而操作系统内核必定大于512B,因此一般都将引导扇区作为内核加载器来使用,将操作系统代码放置于软盘的后续扇区中,当系统启动时,首先加载引导扇区,由引导扇区实现软盘后续数据的读取加载到内存中,最后跳转至内核代码起始处,即实现引导区和内核区的分离。
首先修改一下引导区代码,实现部分寄存器初始化,以及将内核代码从软盘中加载到内存0x8000位置开始处
这里假定内核代码位于软盘1柱面2扇区,并且内核代码大小小于一个扇区
org 0x7c00
LOAD_ADDR EQU 0x8000 ;内核代码起始处
jmp entry
db 0x90
DB "OSKERNEL"
DW 512
DB 1
DW 1
DB 2
DW 224
DW 2880
DB 0xf0
DW 9
DW 18
DW 2
DD 0
DD 2880
DB 0,0,0x29
DD 0xFFFFFFF
DB "MYFIRSTOS "
DB "FAT12 "
RESB 18
entry:
mov ax, 0
mov ss, ax
mov ds, ax
mov es, ax
readFloppy:
mov ch, 1 ;磁道号cylinder
mov dh, 0 ;磁头号head
mov cl, 2 ;扇区号sector
mov bx, LOAD_ADDR ;数据存储缓冲区即将内核从该位置开始加载到内存中
mov ah, 0x02 ;读扇区
mov al, 1 ;连续读取扇区数量 这里先假定内核代码只有一个扇区大小
mov dl, 0 ;驱动器编号
INT 0x13 ;调用BIOS中断
jc fin
jmp LOAD_ADDR
fin:
HLT
jmp fin
TIMES 0x1FE-($-$$) DB 0x00
DB 0x55, 0xAA
增加内核代码汇编部分kernel.asm
org 0x8000
entry:
mov ax, 0
mov ss, ax
mov ds, ax
mov es, ax
mov si, msg
putloop:
mov al, [si]
add si, 1
cmp al, 0
je fin
mov ah, 0x0e
mov bx, 15
INT 0x10
jmp putloop
fin:
HLT
jmp fin
msg:
DB "This is Hello World from Kernal"
修改makeFloppy.c 实现将引导区和内核代码写入软盘相应位置
#include
#include
#include "floppy.h"
#include
int main(int argc, char *argv[]){
printf("boot %s \n", argv[1]);
FILE* boot = fopen(argv[1], "r");
printf("kernel %s \n", argv[2]);
FILE* kernel = fopen(argv[2], "r");
printf("img %s \n", argv[3]);
FILE* img = initFloppy(argv[3]);
if(boot == NULL || kernel == NULL){
printf("The boot or kernel file not found");
exit(0);
}
//写引导扇区cylinder0 sector1
char buf[512];
memset(buf, 0, 512);
fread(buf, 512, 1, boot);
writeFloppy(0, 0, 1, img, buf);
//写内核 cylinder1 sector2
memset(buf, 0, 512);
fread(buf, 512, 1, kernel);
writeFloppy(1, 0, 2, img, buf);
fclose(boot);
fclose(kernel);
}
修改执行脚本run.sh
#!/bin/bash
nasm boot.asm
echo "nasm boot.asm"
nasm kernel.asm
echo "nasm kernel.asm"
gcc floppy.c makeFloppy.c -o makeFloppy
echo "gcc floppy.c makeFloppy.c -o makeFloppy"
./makeFloppy boot kernel system.img
echo "./makeFloppy boot kernel system.img"
利用virtualBox来加载system.img结果如下所示:
在这一部分,启用处理器的保护模式,利用显存增强功能从显示特定字符串。显示代码如下:
boot.asm
org 0x7c00
LOAD_ADDR EQU 0x9000 ;内核代码起始处
jmp entry
db 0x90
DB "OSKERNEL"
DW 512
DB 1
DW 1
DB 2
DW 224
DW 2880
DB 0xf0
DW 9
DW 18
DW 2
DD 0
DD 2880
DB 0,0,0x29
DD 0xFFFFFFF
DB "MYFIRSTOS "
DB "FAT12 "
RESB 18
entry:
mov ax, 0
mov ss, ax
mov ds, ax
mov es, ax
readFloppy:
mov ch, 1 ;磁道号cylinder
mov dh, 0 ;磁头号head
mov cl, 2 ;扇区号sector
mov bx, LOAD_ADDR ;数据存储缓冲区即将内核从该位置开始加载到内存中
mov ah, 0x02 ;读扇区
mov al, 1 ;连续读取扇区数量 这里先假定内核代码只有一个扇区大小
mov dl, 0 ;驱动器编号
INT 0x13 ;调用BIOS中断
jc fin
jmp LOAD_ADDR
fin:
HLT
jmp fin
TIMES 0x1FE-($-$$) DB 0x00
DB 0x55, 0xAA
kernel.asm
;全局描述符结构 8字节
; byte7 byte6 byte5 byte4 byte3 byte2 byte1 byte0
; byte6低四位和 byte1 byte0 表示段偏移上限
; byte7 byte4 byte3 byte2 表示段基址
;定义全局描述符数据结构
;3 表示有3个参数分别用 %1、%2、%3引用参数
;%1:段基址 %2:段偏移上限 %3:段属性
%macro GDescriptor 3
dw %2 & 0xffff ;设置段偏移上限0,1字节
dw %1 & 0xffff ;设置段基址2,3字节
db (%1>>16) & 0xff ;设置段基址4字节
dw ((%2>>8) & 0x0f00) | (%3 & 0xf0ff) ;设置段偏移上限6字节低四位
db (%1>>24) & 0xff ;设置段基址7字节
%endmacro
DA_32 EQU 0x4000 ;32位段属性
DA_CODE EQU 0x98 ;执行代码段属性
DA_RW EQU 0x92 ;读写代码段属性值
org 0x9000 ;内核代码在内存中起始加载处
jmp entry
[SECTION .gdt]
;全局描述符 段基址 段偏移上线 段属性
LABLE_GDT: GDescriptor 0, 0, 0
LABLE_DESC_CODE: GDescriptor 0, SegCodeLen-1, DA_CODE + DA_32
LABLE_DESC_VIDEO: GDescriptor 0xb8000, 0xffff, DA_RW ;显存内存地址从0xB8000开始
;GDT表大小
GdtLen EQU $ - LABLE_GDT
;GDT表偏移上限和基地址
GdtPtr dw GdtLen-1
dd 0
;cpu寻址方式
;实模式 段寄存器×16+偏移地址
;保护模式下 段寄存器中存储的是GDT描述表各个描述符的偏移
SelectorCode32 EQU LABLE_DESC_CODE - LABLE_GDT
SelectorVideo EQU LABLE_DESC_VIDEO - LABLE_GDT
[SECTION .s16]
[BITS 16]
entry:
mov ax, cs
mov ss, ax
mov ds, ax
mov es, ax
mov sp, 0x100
;设置LABLE_DESC_CODE描述符段基址
mov eax, 0
mov ax, cs
shl eax, 4
add eax, SEG_CODE32
mov word [LABLE_DESC_CODE+2], ax
shr eax, 16
mov [LABLE_DESC_CODE+4], al
mov [LABLE_DESC_CODE+7], ah
mov eax, 0
mov ax, ds
shl eax, 4
add eax, LABLE_GDT
mov dword [GdtPtr+2], eax
;设置GDTR寄存器
lgdt [GdtPtr]
cli ;关中断
;打开A20
in al, 0x92
or al, 0x02
out 0x92, al
;进入保护模式CR0寄存器最低位PE设为1
mov eax, cr0
or eax, 1
mov cr0, eax
jmp dword SelectorCode32:0
[SECTION .s32]
[BITS 32]
SEG_CODE32:
;将显存段基址放入gs
mov ax, SelectorVideo
mov gs, ax ;gs 寄存器是80386新增的辅助段寄存器
;在屏幕中间显示字符串,屏幕为每行80个字符,共25行。低字节为ascii字符编码,高字节为字符显示属性
mov ax, (80*11+20)
mov ecx, 2
mul ecx
mov edi, eax ;edi=显存位置(80*11+20)*2
mov ah, 0ch ;设置字符颜色
mov si, msg
putloop:
mov al, [si]
cmp al, 0
je fin
;向显存位置写数据ax 其中ax=ah+al
;ah=0ch设置字符颜色
;al=[si] 要显示的字符串的ASC||值
mov [gs:edi], ax
add edi, 2
inc si
jmp putloop
fin:
HLT
jmp fin
msg:
DB "Protected Mode", 0
SEG_CODE32_END: nop
;32位模式代码长度
SegCodeLen EQU SEG_CODE32_END-SEG_CODE32
最终结果显示如下:
接下来要解决的问题有 什么是保护模式,程序如何进入保护模式,以及如何使用显存显示功能。
80386处理器有四种运行模式:实模式、保护模式、SMM模式和虚拟8086模式。要想理解保护模式,首先得理解下80386的寄存器以及各个寄存器的作用
80386的寄存器可以分为8组:通用寄存器,段寄存器,指令指针寄存器,标志寄存器,系统地址寄存器,控制寄存器,调试寄存器,测试寄存器,它们的宽度都是32位。
EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP这些寄存器的低16位就是8086的 AX/BX/CX/DX/SI/DI/SP/BP
EAX:累加器
EBX:基址寄存器
ECX:计数器
EDX:数据寄存器
ESI:源地址指针寄存器
EDI:目的地址指针寄存器
EBP:基址指针寄存器
ESP:堆栈指针寄存器
也称 Segment Selector,段选择符,段选择子除了8086的4个段外(CS,DS,ES,SS),80386还增加了两个段FS,GS,这些段寄存器都是16位的,用于不同属性内存段的寻址,它们的含义如下:
CS:代码段(Code Segment)
DS:数据段(Data Segment)
ES:附加数据段(Extra Segment)
SS:堆栈段(Stack Segment)
FS:附加段
GS 附加段
EIP的低16位就是8086的IP,它存储的是下一条要执行指令的内存地址,在分段地址转换中,表示指令的段内偏移地址。
EFLAGS,和8086的16位标志寄存器相比,增加了4个控制位
CF(Carry Flag):进位标志位;
PF(Parity Flag):奇偶标志位;
AF(Assistant Flag):辅助进位标志位;
ZF(Zero Flag):零标志位;
SF(Singal Flag):符号标志位;
IF(Interrupt Flag):中断允许标志位,由CLI,STI两条指令来控制;设置IF位使CPU可识别外部(可屏蔽)中断请求,复位IF位则禁止中断,IF位对不可屏蔽外部中断和故障中断的识别没有任何作用;
DF(Direction Flag):向量标志位,由CLD,STD两条指令来控制;
OF(Overflow Flag):溢出标志位;
IOPL(I/O Privilege Level):I/O特权级字段,它的宽度为2位,它指定了I/O指令的特权级。如果当前的特权级别在数值上小于或等于IOPL,那么I/O指令可执行。否则,将发生一个保护性故障中断;
NT(Nested Task):控制中断返回指令IRET,它宽度为1位。若NT=0,则用堆栈中保存的值恢复EFLAGS,CS和EIP从而实现中断返回;若NT=1,则通过任务切换实现中断返回。在ucore中,设置NT为0。
个人计算机早期的8086处理器采用的一种简单运行模式,当时微软的MS-DOS操作系统主要就是运行在8086的实模式下。80386加电启动后处于实模式运行状态,在这种状态下软件可访问的物理内存空间不能超过1MB,且无法发挥Intel 80386以上级别的32位CPU的4GB内存管理能力。实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的。
实模式寻址方式
* 16位寄存器
* 20位地址线,可访问1MB内存
* 通过CS/DS寄存器左移4位+IP寄存器的值生成20位访问地址
这里有两个问题值得注意: 1. CS<<4 + IP理论上来讲,最大可以表示的数值是0xFFFF0 + 0xFFFF = 0x10FFEF,即大约1M+64KB-16Bytes,然而由于地址线只有20根(A0~A19),这个地址最前面的1无法被表示,当CS=0xFFFF时,实际访问的地址0x10FFEF就变成了0xFFEF。即地址由卷回了0地址到64KB-16Bytes处。
保护模式的一个主要目标是确保应用程序无法对操作系统进行破坏。实际上,80386就是通过在实模式下初始化控制寄存器(如GDTR,LDTR,IDTR与TR等管理寄存器)以及页表,然后再通过设置CR0寄存器使其中的保护模式使能位置位,从而进入到80386的保护模式。当80386工作在保护模式下的时候,其所有的32根地址线都可供寻址,物理寻址空间高达4GB。在保护模式下,支持内存分页机制,提供了对虚拟内存的良好支持。保护模式下80386支持多任务,还支持优先级机制,不同的程序可以运行在不同的特权级上。特权级一共分0~3四个级别,操作系统运行在最高的特权级0上,应用程序则运行在比较低的级别上;配合良好的检查机制后,既可以在任务间实现数据的安全共享也可以很好地隔离各个任务。
保护模式寻址方式
段基地址+段内偏移地址 其中段基地址有4字节来计算,即32位,可以实现4GB的内存
保护模式下内存分段管理,全局描述符用来表示段信息,全局描述符一共有8字节组成,对于每一个段内存来说,需要表明该段的基地址,段偏移上限以及段内存的读写属性。
实模式下用户写的代码中CS与IP寄存器组合成的地址即为实际的物理内存地址,保护模式下CS寄存器中保存的内容为段描述符表中的索引,具体寻址过程如下所示:
DOS系统在启动用户应用程序时先在内存中设置两个表,一个叫全局描述符表,GDT,一个叫局部描述符表LDT。表中的每个条目实际代表一段内存地址,包含这段内存的启始地址和段的长度,即base和limit。一个表的多个条目即代码这个用户程序可以访问的多块内存。
DOS系统把这两个表的内存启始地址写到LDTR和GDTR两个寄存器里。并在CS、DS寄存器中设置好一个初始的索引。
应用程序在访问内存时,CS:IP组合称为逻辑地址,CPU拿CS中的3~15位所组成的数据作为索引去LDT或GDT中找到一个描述符,拿这个描述符的base + IP做为实际的物理内存地址去访存。LDT和GDT只有DOS操作系统才能修改,因而应用程序只能访问DOS为它设置好的内存范围,而不能随意访问全部物理内存。应用程序内存不够用时,需要调用一些系统调用,让DOS分配一段内存,把将这段内存的base, limit做成一个描述符加入到GDT或LDT中。应用程序如果胡乱修改CS,造成无法索引到一个有效的段描述符就会发一个“段错误, segmentation fault”。
如图所示,在80286中,CS/DS/ES/FS寄存器的意义发生了变化。它的3~15位是描述符表的索引。TI用来表示索引的是哪一张表,是GDT还是LDT。RPL称为请求权限级别。RPL对应描述符中的DPL,只要RPL的级别高于(值小于)DPL时,才有权限访问这段内存。
控制寄存器CR0中的位0用PE标记,控制分段管理机制的操作,所以把它们称为保护控制位。 PE控制分段管理机制。 PE=0,处理器运行于实模式; PE=1,处理器运行于保护方式。在系统刚上电时,处理器被复位成PE=0和PG=0(即实模式状态),以允许引导代码在启用分段和分页机制之前能够初始化这些寄存器和数据结构。
在80386及之后的处理器里增加了分页管理机器,是否启用分页由CR0的位31标记。
切换到保护模式的代码如下:
; switch to protection mode
switch_proMode:
mov eax, cr0
or eax, 1 ; set CR0's PE bit
mov cr0, eax
1. 初始化GDT全局描述符表
2.设置GDTR寄存器寄存器
3.关中断
4.打开A20
5.置位CR0最低位PE=1进入保护模式
其中A20的具体解释可以参考
https://zhuanlan.zhihu.com/p/27401519
https://blog.csdn.net/u014106644/article/details/97002252
gs寄存器主要用来指向显存,当将信息写入gs指向的内存后,信息会显示到屏幕上。用于显示字符的显存,内存地址从0XB800h开始,从该地址开始,每两个字节用来在屏幕上显示一个字符,这两个字节中,第一个字节的信息用来表示字符的颜色,第二个字节用来存储要显示的字符的ASCII值,屏幕一行能显示80个字符。
设置显存GDT描述符
LABLE_DESC_VIDEO: GDescriptor 0xb8000, 0xffff, DA_RW ;显存内存地址从0xB8000开始
将指定位置的字符串写入到显存区域中
SEG_CODE32:
;将显存段基址放入gs
mov ax, SelectorVideo
mov gs, ax ;gs 寄存器是80386新增的辅助段寄存器
;在屏幕中间显示字符串,屏幕为每行80个字符,共25行。低字节为ascii字符编码,高字节为字符显示属性
mov ax, (80*11+20)
mov ecx, 2
mul ecx
mov edi, eax ;edi=显存位置(80*11+20)*2
mov ah, 0ch ;设置字符颜色
mov si, msg
putloop:
mov al, [si]
cmp al, 0
je fin
;向显存位置写数据ax 其中ax=ah+al
;ah=0ch设置字符颜色
;al=[si] 要显示的字符串的ASC||值
mov [gs:edi], ax
add edi, 2
inc si
jmp putloop
参考资料
https://zhuanlan.zhihu.com/p/27401519 80286与保护模式
https://blog.csdn.net/Zllvincent/article/details/83549084 实模式进入保护模式
https://www.jianshu.com/p/22c83b3e238c 由实模式进入保护模式之32位寻址
https://blog.csdn.net/yang_yulei/article/details/22613327 CPU的实模式与保护模式(简介)