Blog URL: www.shicoder.top
WeChat: 18223081347
Welcome to group chat: 452380935
This time we will improve the kernel, mainly including the loading of global descriptors, task scheduling, interrupts, etc.
Loading of global descriptors
Let's review, is loader
there some code for global descriptors in
prepare_protected_mode:
cli; 关闭中断
; 打开A20线
in al, 0x92
or al, 0b10 ; 第1位置1
out 0x92, al
; 加载GDT
lgdt [gdt_ptr]
; 启动保护模式
mov eax, cr0
or eax, 1 ; 第0位置1
mov cr0, eax
; 用跳转来刷新缓存,启用保护模式
jmp dword code_selector:protect_mode
复制代码
When we are preparing to enter the protected mode, we will gdt_ptr
load the pointed place into the gdt
register, so is it not necessary to enter the protected mode, that is, the kernel stage, of course not, and you think, we are loader
in, there are only 2 segment, one is the code segment, the other is the data segment, and there are a total of 8192 segments, then how to load other segments when they are used by the kernel, so we need to redefine an array in the kernel, first initialize 8192 (of course in my In this kernel, not so much is used, in fact, only 128 are initialized), then first assign the value of the register in the previous protected mode gdt
to this array, and then load the address of the array into the gdt
register, then know this step, let's get started
#define GDT_SIZE 128 // 本身有8192个,但是我们在这里用不到这么多
// 全局描述符
typedef struct descriptor_t /* 共 8 个字节 */
{
unsigned short limit_low; // 段界限 0 ~ 15 位
unsigned int base_low : 24; // 基地址 0 ~ 23 位 16M
unsigned char type : 4; // 段类型
unsigned char segment : 1; // 1 表示代码段或数据段,0 表示系统段
unsigned char DPL : 2; // Descriptor Privilege Level 描述符特权等级 0 ~ 3
unsigned char present : 1; // 存在位,1 在内存中,0 在磁盘上
unsigned char limit_high : 4; // 段界限 16 ~ 19;
unsigned char available : 1; // 该安排的都安排了,送给操作系统吧
unsigned char long_mode : 1; // 64 位扩展标志
unsigned char big : 1; // 32 位 还是 16 位;
unsigned char granularity : 1; // 粒度 4KB 或 1B
unsigned char base_high; // 基地址 24 ~ 31 位
} _packed descriptor_t;
// 段选择子
typedef struct selector_t
{
u8 RPL : 2;
u8 TI : 1;
u16 index : 13;
} selector_t;
// 全局描述符表指针
typedef struct pointer_t
{
u16 limit;
u32 base;
} _packed pointer_t;
void gdt_init();
复制代码
Let’s define the structure first, because in the kernel, we can use C language to write, so the definition here is not loader
in assembly like that. Does it feel simpler? The most important thing is a gdt_init
function, which is the step we just took. main implementation of
descriptor_t gdt[GDT_SIZE]; // 内核全局描述符表
pointer_t gdt_ptr; // 内核全局描述符表指针
// 初始化内核全局描述符表
void gdt_init()
{
DEBUGK("init gdt!!!\n");
// 在loader.asm中,已经有三个描述符了,因此GDTR寄存器有3个了
asm volatile("sgdt gdt_ptr"); // 读取GDTR寄存器到gdt_ptr指向的地方
memcpy(&gdt, (void *)gdt_ptr.base, gdt_ptr.limit + 1);
// 此时gdt这个数组前3个有值,后面125个是0
gdt_ptr.base = (u32)&gdt;
gdt_ptr.limit = sizeof(gdt) - 1;
asm volatile("lgdt gdt_ptr\n"); // 将gdt_ptr指向的值写入到GDTR寄存器 ,此时GDTR寄存器有128个全局描述符
}
复制代码
It's still quite simple, that's all, note that our kernel only has an array of 128, and 8192 is not implemented, but normally, it linux
must be 8192
Tasks and Scheduling
To put it simply, a task can be thought of as a process, then each process needs to have its own stack to store the information it needs to run. In this code writing, in order to simplify, the stack of a process occupies one page memory, and its structure is as follows
因此任务调度就是将此时的栈切换为下一个进程的栈,那么切换肯定要知道切换之后要保存哪些东西,这个是由ABI来规定的,一个进程有自己的寄存器值,ABI规定,比如进程a要切换到进程b,那么进程a要自己保存下面三个
- eax
- ecx
- edx
进程b要替进程a保存以下5个
- ebx
- esi
- edi
- ebp
- esp
知道上面的理论,我们就可以进行切换了
创建进程
我们上面说到一个进程需要一个栈,那么我们就给这个栈创建一个结构体
typedef struct task_t
{
u32 *stack; // 内核栈
} task_t;
复制代码
此时就是按照上面那个图,设置一些值
#define PAGE_SIZE 0x1000 // 4KB 表示一页 每一页里面存放进程的信息和进程的栈信息
task_t *a = (task_t *)0x1000; // 进程a的栈的初始地址,然后每个进程的栈有1页
u32 thread_a()
{
while (true)
{
printk("A");
schedule();
}
}
static void task_create(task_t *task, target_t target)
{
// 此时stack为这个进程的栈的最高地址
u32 stack = (u32)task + PAGE_SIZE;
// 进程的栈的最高地址往下一点,就是存放task_frame_t
stack -= sizeof(task_frame_t);
task_frame_t *frame = (task_frame_t *)stack;
frame->ebx = 0x11111111;
frame->esi = 0x22222222;
frame->edi = 0x33333333;
frame->ebp = 0x44444444;
frame->eip = (void *)target;
task->stack = (u32 *)stack;
}
task_create(a, thread_a);
复制代码
进程调度
其中最重要的函数schedule
中的task_switch
由于需要对寄存器进行操作,因此采用汇编实现
void schedule()
{
// 第一次进入时候,current是main进程,后续才是ababa这样一直切换
task_t *current = running_task();
task_t *next = current == a ? b : a;
task_switch(next);
}
复制代码
task_switch:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov eax, esp;
and eax, 0xfffff000; current
mov [eax], esp
;=======上面是保存切换前的环境,下面是恢复即将要切换的线程环境,其实最重要的一点就是
; esp的值,esp决定了此时在哪个进程的栈中
mov eax, [ebp + 8]; next
mov esp, [eax]
pop edi
pop esi
pop ebx
pop ebp
ret
复制代码
差不多到此时,栈的切换完成,一旦ret
,就会到进程a的代码
中断
上面可以看出我们是使用schedule
来自己进行切换,而正常情况会出现抢占式的切换,就比如自己遇到一些情况,比如打印机需要纸,就会自动切换进程,这样就要使用中断来切换,中断就是一个函数
中断向量表
由于中断就是一个函数,因此有一个表来存放这个函数的地址,到时候调用中断时候,去表里面查询调用的函数序号就知道具体调用什么函数,在实模式下,处理器要求将它们的入口点集中存放到内存中从物理地址 0x000 开始,到 0x3ff 结束,共 1 KB 的空间内,一共256个中断向量,中断向量是指向中断函数的指针。一个向量包括4个字节,前2个字节为段内偏移,后2个字节是段地址,调用方式为int num
,下面我们来试一下实模式下的中断,我们将boot.asm
改成下面,把跳转到loader
的部分先注释掉
; 将该代码放在0x7c00 因为由007内核加载器.md文件可知,MBR加载区域就是从0x7c00开始
[org 0x7c00]
;设置屏幕模式为文本模式,清除屏幕
mov ax, 3
int 0x10
;初始化段寄存器
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
;====================测试中断
mov word [0x54 * 4], interrupt
mov word [0x54 * 4 + 2],0
int 0x54
;====================
jmp $
interrupt:
mov si, string
call print
iret
string:
db ".",0
; print 函数需要三条语句
; mov ah 0x0e mov al 字符串 int 0x10
print:
mov ah, 0x0e
.next:
mov al, [si]
; si相当于是指针,不断向后移动,知道遇到booting字符串最后的0
cmp al, 0
jz .done
int 0x10
inc si
jmp .next
.done:
ret
复制代码
关键是这三行
mov word [0x54 * 4], interrupt
mov word [0x54 * 4 + 2],0
int 0x54
复制代码
In fact, when we use int num
to call an interrupt, we first register the function we want to call. As mentioned earlier, there are a total of 256, each 4 bytes, so for example, the above is int 0x54
where the interrupt
function is registered 0x54 * 4
.
You can see that the print is successful.
However, in protected mode, since we rarely use segment addresses and offsets within segments, the above method is rarely used, but this idea is still retained. Let's talk about interrupts in protected mode.
Interrupt Descriptor Table
The interrupt vector table in real mode becomes an interrupt descriptor table in protected mode, and the interrupt vector in real mode becomes an interrupt descriptor in protected mode. Let's talk about the interrupt descriptor first.
We know that the interrupt vector actually points to the address of the function, but because the space of the interrupt descriptor becomes larger, a lot of other things have been added. Let's take a look at its structure first.
typedef struct gate_t
{
u16 offset0; // 段内偏移 0 ~ 15 位
u16 selector; // 代码段选择子
u8 reserved; // 保留不用
u8 type : 4; // 任务门/中断门/陷阱门
u8 segment : 1; // segment = 0 表示系统段
u8 DPL : 2; // 使用 int 指令访问的最低权限
u8 present : 1; // 是否有效
u16 offset1; // 段内偏移 16 ~ 31 位
} _packed gate_t;
复制代码
The offset1
sum offset2
can be thought of as the address of the function pointed to, of course, it is divided into the upper 15-bit address and the lower 15-bit address
Similarly, all interrupt descriptors are aggregated into one table, which is of course the interrupt descriptor table. Similarly, there is a special register pointing to this interrupt descriptor table, which is IDT register
, there are also two instructions
lidt [idt_ptr]; 加载 idt 将idt_ptr指向的地方保存到IDT register
sidt [idt_ptr]; 保存 idt 将IDT register存放的值放在idt_ptr中
复制代码
Let's implement the interrupt descriptor table in our system.
global interrupt_handler
interrupt_handler:
xchg bx, bx
push message
call printk
add esp, 4
xchg bx, bx
iret
section .data
message:
db "default interrupt", 10, 0
复制代码
gate_t idt[IDT_SIZE];
pointer_t idt_ptr; // 本身这个变量是针对全局描述符表,因为中断描述符表的指针一样,所以公用
extern void interrupt_handler();
void interrupt_init()
{
for (size_t i = 0; i < IDT_SIZE; i++)
{
gate_t *gate = &idt[i];
gate->offset0 = (u32)interrupt_handler & 0xffff;
gate->offset1 = ((u32)interrupt_handler >> 16) & 0xffff;
gate->selector = 1 << 3; // 代码段
gate->reserved = 0; // 保留不用
gate->type = 0b1110; // 中断门
gate->segment = 0; // 系统段
gate->DPL = 0; // 内核态
gate->present = 1; // 有效
}
idt_ptr.base = (u32)idt;
idt_ptr.limit = sizeof(idt) - 1;
// BMB;
asm volatile("lidt idt_ptr\n");
}
复制代码
void kernel_init()
{
console_init();
gdt_init();
interrupt_init();
return;
}
复制代码
_start:
call kernel_init
; main.c返回
int 0x80; 调用 0x80 中断函数 系统调用,因此在初始化中,将256整个中断描述符表的每一项中断描述符都指向interrupt_handler,所以随便调用哪个都可以
jmp $
复制代码
Let's talk about its process. First of all, it is used in kernel
the main function kernel_init
function to interrupt_init
create an interrupt descriptor table with a size of 128, and initialize each interrupt descriptor. The function address pointed to by each descriptor is interrupt_handler
, This function is to print default interrupt
, and then note that when the kernel_init
function returns, it will come to the _start
function, which will be used in it int 0x80
. In fact, there is no need to specify which number at this time, because 128 interrupt descriptors are the same, int 0x69
and the same, less than 128.
The result is out
\