我的博客:startcraft
保护模式
保护模式是从80386时代引入的,之前的8086只有实模式,实模式有1MB的寻址空间(2 ^ 20),而保护模式有4GB的寻址空间,且支持分页等操作
保护模式对内存段的访问做了限制,对于内存段的性质和允许的操作给出了定义,所以每一个段有一个段描述符,占8个字节,段描述符有很多,只能放在内存里,其中最重要的全局段描述符表(GDT),GDT的相关信息存在cpu的寄存器GDTR中
GDTR有48位,其中0 - 15位表示GDT的边界,也就是GDT段的大小,16 - 47位这32位存的是GDT的基地址,所以GDT段的大小最多为65536字节,也就是可以存8192个段描述符
也就是我们通过GDTR寄存器获取GDT的位置和大小,然后GDT中存储了所有段描述符
cup加电后工作在实模式,而进入保护模式后就需要GDT和GDTR,所以我们需要在进入保护模式之前就设置好GDT和GDTR,但是实模式只有1MB的寻址空间,所以一开始的GDT只能放在这里,进入保护模式后还可以更改
从实模式进入保护模式只需要修改CR0寄存器的第0位,为0表示实模式,为1表示保护模式
具体的分段策略
分段是cpu为了兼容以前的程序所保留的,所以我们一般绕过分段直接用分页技术,而绕过分段的方法就是把4GB的空间分成一段,整个虚拟地址的其实位置是0,长度为4G,这样偏移地址和段机制处理后的地址是同一个数
当然并不是给所有的段相同的描述符,数据段和代码段是不同的描述符
段描述符的结构
参考: https://blog.csdn.net/longintchar/article/details/50489889
看这个,就不搬运了
代码实现
include/gdt.h
#ifndef INCLUDE_GDT_H_
#define INCLUDE_GDT_H_
#include "types.h"
// 全局描述符类型
typedef
struct gdt_entry_t {
uint16_t limit_low; // 段界限 15 ~ 0
uint16_t base_low; // 段基地址 15 ~ 0
uint8_t base_middle; // 段基地址 23 ~ 16
uint8_t access; // 段存在位、描述符特权级、描述符类型、描述符子类别
uint8_t granularity; // 其他标志、段界限 19 ~ 16
uint8_t base_high; // 段基地址 31 ~ 24
} __attribute__((packed)) gdt_entry_t;
// GDTR
typedef
struct gdt_ptr_t {
uint16_t limit; // 全局描述符表限长
uint32_t base; // 全局描述符表 32 位基地址
} __attribute__((packed)) gdt_ptr_t;
// 初始化全局描述符表
void init_gdt();
// GDT 加载到 GDTR 的函数汇编实现[]
extern void gdt_flush(uint32_t);
#endif // INCLUDE_GDT_H_
gdt/gdt.c
#include "gdt.h"
#include "string.h"
// 全局描述符表长度
#define GDT_LENGTH 5
// 全局描述符表定义
gdt_entry_t gdt_entries[GDT_LENGTH];
// GDTR
gdt_ptr_t gdt_ptr;
static void gdt_set_gate(int32_t num, uint32_t base,
uint32_t limit, uint8_t access, uint8_t gran);
// 声明内核栈地址
extern uint32_t stack;
// 初始化全局描述符表
void init_gdt()
{
// 全局描述符表界限 e.g. 从 0 开始,所以总长要 - 1
gdt_ptr.limit = sizeof(gdt_entry_t) * GDT_LENGTH - 1;
gdt_ptr.base = (uint32_t)&gdt_entries;
// 采用 Intel 平坦模型
gdt_set_gate(0, 0, 0, 0, 0); // 按照 Intel 文档要求,第一个描述符必须全 0
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF); // 指令段
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF); // 数据段
gdt_set_gate(3, 0, 0xFFFFFFFF, 0xFA, 0xCF); // 用户模式代码段
gdt_set_gate(4, 0, 0xFFFFFFFF, 0xF2, 0xCF); // 用户模式数据段
// 加载全局描述符表地址到 GPTR 寄存器
gdt_flush((uint32_t)&gdt_ptr);
}
// 全局描述符表构造函数,根据下标构造
// 参数分别是数组下标、基地址、限长、访问标志,其它访问标志
static void gdt_set_gate(int32_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t gran)
{
gdt_entries[num].base_low = (base & 0xFFFF);
gdt_entries[num].base_middle = (base >> 16) & 0xFF;
gdt_entries[num].base_high = (base >> 24) & 0xFF;
gdt_entries[num].limit_low = (limit & 0xFFFF);
gdt_entries[num].granularity = (limit >> 16) & 0x0F;
gdt_entries[num].granularity |= gran & 0xF0;
gdt_entries[num].access = access;
}
一点一点来看
首先定义了GDT的长度,这个不是GDT的大小,而是GDT数组的长度,因为GDT就可以看作是一个数组,其中存放的是一个个8字节的段描述符
然后定义了GDT数组gdt_entries和GDTR gdt_ptr
然后是初始化GDT的函数,首先设定该gdt的大小也就是数组长度*元素大小 -1,然后gdt的基地址,也就是数组的起始地址
后面是构造全局段描述符表,首先规定第一个段是全0,段的基地址拆成低16位,中8位和高8位,然后段限长是20位的,其中低16位存在limit_low中,还有4位存在granularity的低4位中,剩下4位是访问标志
设置的描述符,除了后面的标志不同前面的都是基地址为0,限长为4GB
然后是把GDT的描述信息写入GDTR
gdt/gdt_s.s
[GLOBAL gdt_flush]
gdt_flush:
mov eax, [esp + 4] ; 参数存入 eax 寄存器
lgdt [eax] ; 加载到 GDTR 修改原先[设置GRUB]
mov ax, 0x10 ; 加载我们的数据段描述符
mov ds, ax ; 更新所有可以更新的段寄存器
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
jmp 0x08: .flush ; 远跳转, 0x08 是我们的代码段描述符
; 远跳目的是清空流水线并串行化处理器
.flush:
ret
最后更改init / entry.c测试一下
#include "gdt.h"
#include "console.h"
#include "debug.h"
int kern_entry()
{
init_debug();
init_gdt();
console_clear();
printk_color(rc_black, rc_green, "Hello, OS kernel!\n");
return 0;
}
如果正常输出就ok了
现在的目录结构