**该系列文章为bilibili李述铜老师手写基于x86 32位的操作系统课程笔记,如有疑问欢迎评论沟通**
前言:
CPU上电复位后默认进入实模式,这种模式下没有保护机制,但是提供了BIOS服务 。这种模式的限制:
1. 只能访问1MB内存,内核寄存器最大为16位宽
2. 所有的操作数最大为16位宽
3. 没有任何保护机制
4. 没有特权级支持
5. 没有分页机制和虚拟内存的支持
保护模式为后面增加的功能,这种模式为操作系统及应用程序的运行添加了很多支持,特性为:
1. 寄存器位宽扩展到32位,最大可访问4GB内存
2. 所有的操作数最大为32位宽,出入栈也为32位
3. 提供4种特权级。操作系统可以运行在最高特权级;应用程序可运行在最低特权级
4. 支持虚拟内存,可以开启分页机制
一、实模式切换至保护模式
1. 切换流程
从实模式切换至保护模式,需要遵循的流程(此处引用课程截图):
因为CPU执行指令是以流水线的形式进行执行,因此开启保护模式后,流水线上可能还会存在16位的指令未被执行,但是保护模式下执行的都是32位的指令,因此需要清空原来的流水线以取消掉原16位的指令。
2. 实现切换
实现切换的所有相关的函数均封装在comm文件夹下的cpu_instr.h文件中
①禁用中断函数
//关中断函数
static inline void cli(void){
__asm__ __volatile__("cli");
}
//开中断函数
static inline void sti(void){
__asm__ __volatile__("sti");
}
②打开A20地址线(A20地址线打开方式及为什么要打开可以参考此篇文章:关于A20地址线的简单介绍https://blog.csdn.net/weixin_47852408/article/details/135827145?spm=1001.2014.3001.5502) :
//读端口函数
static inline uint8_t inb(uint16_t port){
//返回参数
uint8_t rv;
//内联汇编形式读取端口
__asm__ __volatile__(
"inb %[p], %[v]"
:[v]"=a"(rv) : [p]"d"(port)
);
return rv;
/*注意的是:
inb al, dx是intel的汇编语法,而下面的
inb %[p], %[v]是GCC的汇编语法
两者表示同一指令,但是写法不一样。源,目的操作数位置正好相反
*/
}
//写端口函数
static inline void outb(uint16_t port, uint8_t data){
/*指令格式为
outb al(数据), dx(端口)
*/
__asm__ __volatile__(
"outb %[v], %[p]"
::[v]"a"(data), [p]"d"(port)
);
/*注意的是:
outb al, dx是intel的汇编语法,而下面的
outb %[v], %[p]是GCC的汇编语法
两者表示同一指令,但是写法不一样。源,目的操作数位置正好相反
*/
}
//2.开A20地址线
uint8_t v = inb(0x92);
outb(0x92, v | 0x2);
③加载GDT表(后续的课程会对GDT表进行详解):
//gdt表的配置信息,现在先照写,后续课程会更新为什么这么写
uint16_t gdt_table[][4] = {
{0, 0, 0, 0},
{0xFFFF, 0x0000, 0x9a00, 0x00cf},
{0xFFFF, 0x0000, 0x9200, 0x00cf},
};
//加载GDT表函数,后续的课程会进一步讲解
static inline void lgdt(uint32_t start, uint32_t size){
//gdt表的结构
struct{
uint16_t limit;
uint16_t start15_0;
uint16_t start31_16;
}gdt;
gdt.start31_16 = start >> 16;
gdt.start15_0 = start & 0xFFFF;
gdt.limit = size - 1;
__asm__ __volatile__(
"lgdt %[g]"
::[g]"m"(gdt)
);
}
④开始保护模式使能位
//读取cr0
static inline uint16_t read_cr0(void){
uint32_t cr0;
__asm__ __volatile__(
"mov %%cr0, %[v]"
::[v]"r"(cr0)
);
return cr0;
}
//写入cr0
static inline void write_cr0(uint32_t v){
__asm__ __volatile__(
"mov %[v], %%cr0"
::[v]"r"(v)
);
}
//4.开启保护模式的使能位
uint32_t cr0 = read_cr0();
write_cr0(cr0 | (1<<0));
⑤远跳转
实现远跳转首先要编写跳转到的函数,该函数写于汇编文件中,用于清空流水线进入保护模式,其中load_kernel函数为loader_32.c文件中的函数:
.code32 # 声明生成32位的代码
.text # 表示下面的为代码
.global protect_mode_entry # 将函数声明为全局的
.extern load_kernel # 跳转到C代码环境去
protect_mode_entry:
# 远跳转过来之后清空流水线
mov $16, %ax # 为什么设置为16之后的课程会解释
mov %ax, %ds
mov %ax, %ss
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
jmp $8, $load_kernel # 为什么为8,之后的课程会解释
jmp .
然后在之前的c环境中跳转到该汇编代码:
//远跳转(后续的课程会对跳转函数进行讲解)
static inline void far_jump(uint32_t selector, uint32_t offset){
uint32_t addr[] = {offset, selector};
__asm__ __volatile(
"ljmpl *(%[a])"
::[a]"r"(addr)
);
}
//5.远跳转,清空流水线
far_jump(8, (uint32_t)protect_mode_entry);
最终的切换保护模式的函数:
//进入保护模式函数
static void enter_protected_mode(void){
//1.关中断
cli();
//2.开A20地址线
uint8_t v = inb(0x92);
outb(0x92, v | 0x2);
//3.加载GDT表
lgdt((uint32_t)gdt_table, sizeof(gdt_table));
//4.开启保护模式的使能位
uint32_t cr0 = read_cr0();
write_cr0(cr0 | (1<<0));
//5.远跳转,清空流水线
far_jump(8, (uint32_t)protect_mode_entry);
}
二、保护模式读取磁盘
在保护模式下运行的代码为32位的代码即loader_32.c文件
在保护模式下,BIOS功能无法使用,读取磁盘需要使用LBA模式,对于LBA模式可以参考该文章:LBA模式简单介绍-CSDN博客文章浏览阅读13次。LBA读磁盘https://blog.csdn.net/weixin_47852408/article/details/135830924?spm=1001.2014.3001.5501
#define SECTOR_SIZE 512//磁盘扇区大小宏定义
#define SYS_KERNEL_LOAD_ADDR (1024*1024) //LBA模式读取磁盘缓存存放开始位置
//读寄存器数据函数
static inline uint16_t inw(uint16_t port){
//返回参数
uint16_t rv;
// in ax, dx
__asm__ __volatile__(
"in %[p], %[v]"
:[v]"=a"(rv) : [p]"d"(port)
);
return rv;
}
//使用LBA48位模式读磁盘函数,参数分别为 读取位置,读多少个,读取到的数据缓存存放位置
static void read_disk(uint32_t sector, uint32_t sector_count, uint8_t *buf){
/*设置LBA模式的参数
*/
outb(0x1F6, 0xE0);//选择读取硬盘
outb(0x1F2, (uint8_t)(sector_count >> 8));
/*
下面的0x1Fx分别表示LBA参数的从低到高的位数
*/
outb(0x1F3, (uint8_t) (sector >> 24));
outb(0x1F4, 0);
outb(0x1F5, 0);
outb(0x1F2, (uint8_t)sector_count);
outb(0x1F3,(uint8_t)(sector));
outb(0x1F4, (uint8_t)(sector >> 8));
outb(0x1F5, (uint8_t)(sector >> 16));
outb(0x1F7, 0x24);
uint16_t * data_buf = (uint16_t *)buf;
while(sector_count--){
while((inb(0x1F7) & 0x88) != 0x8){}//通过状态寄存器可知当第三位为1时表示就绪,可以写数据或者读数据
//开始读一个扇区数据,并将数据写入缓存
for(int i = 0; i < SECTOR_SIZE /2 ;i++){
*data_buf++ = inw(0x1F0);//从0x1F0读
}
}
}
项目gitee地址:https://gitee.com/jim-jenny/x86-32OS