6.添加全局段描述符表
6.1保护模式
保护模式需要对内存段的性质和允许的操作给出定义,以实 现对特定内存段的访问检测和数据保护
80386中原先的 AX,BX,CX,DX,SI,DI,SP,BP从16位扩展(Extend)到了32位,并改名EAX,EBX, ECX,EDX,ESI,EDI,ESP,EBP,E就是Extend的意思。
8036保护模式下的分段
8036 虽然采用分段的方式进行寻址,但是 只是分了一个段
即段基址为0x00000000, 短长为0xFFFFFFFF(4GB)
32位的保护模式下,对一个内存段的描述需要8个字节 — 段描述符 (8字节 = 64位)
段描述符又分为:数据段描述符,指令段描述符和系统段描述符三种
描述符表:在内存中存放在描述符的集合
intel 直接设置了一个48位的全局描述符表寄存器(GDTR)来保存描述符表的信息
16位来表示表的长度,那么2的16次方就是65536字节,除以每一个描述符的8字 节,那么最多能创建8192个描述符
现代操作系统不在使用分段,而是直接使用分页技术
分页只是保护模式下的一种内存管理策略
6.2分段策略 - gdt.h
GRUB 在载入内核时候的一些状态
CS 指向基地址为 0x00000000,限长为4G – 1的代码段描述符。
DS,SS,ES,FS 和 GS 指向基地址为0x00000000,限长为4G–1的数据段描述符。
在内核中实现GDT 全局描述符表
include/gdt.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #ifndef INCLUDE_GDT_H_ #define INCLUDE_GDT_H_
#include "types.h"
typedef struct gdt_entry_t { uint16_t limit_low; uint16_t base_low; uint8_t base_middle; uint8_t access; uint8_t granularity; uint8_t base_high; } __attribute__((packed)) gdt_entry_t;
typedef struct gdt_ptr_t { uint16_t limit; uint32_t base; } __attribute__((packed)) gdt_ptr_t;
void init_gdt();
extern void gdt_flush(uint32_t);
#endif
|
__attribute__((packed))
是一个由GCC提供的特殊属性,用于指示编译器如何在内存中布局结构体或联合体的成员。使用这个属性可以确保编译器生成的数据结构不进行任何自动的内存对齐,而是严格按照成员声明的顺序将它们紧密地打包在一起。
函数实现
gdt/gdt.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| #include "gdt.h" #include "string.h"
#define GDT_LENGTH 5
gdt_entry_t gdt_entries[GDT_LENGTH];
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() { gdt_ptr.limit = sizeof(gdt_entry_t) * GDT_LENGTH - 1; gdt_ptr.base = (uint32_t)&gdt_entries;
gdt_set_gate(0, 0, 0, 0, 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);
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_LENGTH 5 — 为什么是5
在典型的x86保护模式操作系统中,这些段通常包括:
- 空描述符(Null Descriptor):
- 按照Intel的规定,GDT的第一个条目必须是一个空描述符。这不是用于实际的内存段,但它有助于捕捉到无效的段选择器访问,因为任何尝试访问索引为0的描述符的操作都会导致异常。
- 内核代码段(Kernel Code Segment):
- 用于操作系统内核代码的执行。这通常是特权级0的代码段,只能由内核访问。
- 内核数据段(Kernel Data Segment):
- 用户代码段(User Code Segment):
- 用于用户模式程序的代码执行。这是特权级3的代码段,允许用户级应用程序执行。
- 用户数据段(User Data Segment):
- 用于用户模式程序的数据。这同样是特权级3的数据段。
定义这5个条目是为了确保操作系统能够正确区分内核模式和用户模式,同时处理用户程序和内核之间的权限切换。这种分隔是现代操作系统安全性和稳定性的关键,因为它防止了用户程序直接访问内核资源和其他敏感数据。
gdt/gdt_s.s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [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
|
- 获取参数并加载GDT
mov eax, [esp+4]
: 这条指令从栈中获取函数参数。在C语言调用汇编时,参数通常通过栈传递。[esp+4]
表示取栈指针 esp
向上偏移4个字节的位置的值(因为 esp
指向的是返回地址),这里保存的是传递给 gdt_flush
函数的参数,即新GDT的地址。
lgdt [eax]
: lgdt
是加载全局描述符表寄存器(GDTR)的指令。[eax]
表示使用 eax
寄存器中的地址来加载GDTR。这里的地址指向一个 gdt_ptr
结构,其中包含了GDT的长度和基址。
- 更新段寄存器
mov ax, 0x10
: 将16进制值 0x10
移动到 ax
寄存器。这个值是数据段的选择子(selector),在GDT中的偏移量。这通常指向GDT中的第二个条目(首个条目是空描述符),这里定义为内核数据段。
mov ds, ax
、mov es, ax
、mov fs, ax
、mov gs, ax
、mov ss, ax
: 这些指令将数据段选择子加载到所有数据段寄存器(ds
, es
, fs
, gs
, ss
)。这样确保所有的段寄存器都指向正确的段描述符,对应新的GDT设置。
- 远跳转以更新代码段寄存器和清空流水线
jmp 0x08:.flush
: 执行一个远跳转到同一代码段中的.flush
标签。0x08
是代码段的选择子(在GDT中的位置),这里通常指向GDT的第一个代码段描述符。远跳转不仅跳转到指定的代码段,还强制CPU清空预取队列和流水线,并且实际上更新了CPU的内部结构,使代码段寄存器(cs
)与新的GDT同步。
.flush:
: 这是远跳转的目标位置,紧跟着的 ret
指令会返回到调用 gdt_flush
的代码中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| // 在调用 gdt_flush 函数之前,调用者(可能是C语言的初始化代码)将参数(gdt_ptr 的地址)压入栈中,并调用 gdt_flush 函数。栈的结构大致如下: +-----------------+ <- ESP (栈指针) | 参数:&gdt_ptr | +-----------------+ | 返回地址 | +-----------------+ | 调用者的局部变量 | +-----------------+ | ... |
// 当 gdt_flush 函数被调用时,CPU自动将返回地址(即调用 gdt_flush 之后的下一条指令的地址)压入栈中。此时,ESP 指向栈中的返回地址。 +-----------------+ <- ESP (新的栈指针位置) | 返回地址 | +-----------------+ | 参数:&gdt_ptr | +-----------------+ | 调用者的局部变量 | +-----------------+ | ... |
|
修改 entry.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include "types.h" #include "console.h" #include "debug.h" #include "gdt.h"
int kern_entry(){ init_debug(); init_gdt(); console_clear(); printk_color(rc_blue, rc_red, "Hello OS!!!\n"); return 0; }
|