Blog-URL: www.shicoder.top
WeChat: 18223081347
Willkommen im Gruppenchat: 452380935
Dieses Mal werden wir den Kernel verbessern, hauptsächlich einschließlich des Ladens globaler Deskriptoren, Task-Scheduling, Interrupts usw.
Laden von globalen Deskriptoren
Lassen Sie uns überprüfen, ob loader
es einen Code für globale Deskriptoren gibt
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
复制代码
gdt_ptr
Wenn wir uns darauf vorbereiten, in den geschützten Modus zu wechseln, laden wir die angegebene Stelle in das gdt
Register. Ist es also nicht erforderlich, in den geschützten Modus zu wechseln, dh in die Kernel-Phase, natürlich nicht, und Sie denken, wir befinden uns loader
darin, Es gibt nur 2 Segmente, eines ist das Codesegment, das andere ist das Datensegment, und es gibt insgesamt 8192 Segmente. Wie werden dann andere Segmente geladen, wenn sie vom Kernel verwendet werden, also müssen wir ein Array neu definieren den Kernel initialisieren, zuerst 8192 initialisieren (natürlich wird in diesem Kernel nicht so viel verwendet, tatsächlich werden nur 128 initialisiert), dann gdt
diesem Array zuerst den Wert des Registers im vorherigen geschützten Modus zuweisen und dann laden die Adresse des Arrays in das gdt
Register, dann kennen Sie diesen Schritt, fangen wir an
#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();
复制代码
Lassen Sie uns zuerst die Struktur definieren, denn im Kernel können wir die C-Sprache zum Schreiben verwenden, also ist die Definition hier nicht so loader
in Assembler. Fühlt es sich einfacher an? Das Wichtigste ist eine gdt_init
Funktion, und das ist der Schritt, den wir gerade gemacht haben Hauptimplementierung von
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个全局描述符
}
复制代码
Es ist immer noch ziemlich einfach, das ist alles, beachten Sie, dass unser Kernel nur ein Array von 128 hat und 8192 nicht implementiert ist, aber normalerweise linux
muss es 8192 sein
Aufgaben und Terminplanung
Um es einfach auszudrücken, kann man sich eine Task als einen Prozess vorstellen, dann muss jeder Prozess seinen eigenen Stack haben, um die Informationen zu speichern, die er zum Ausführen benötigt.Bei diesem Codeschreiben belegt der Stack eines Prozesses zur Vereinfachung einen Stack Seitenspeicher, und seine Struktur ist wie folgt
因此任务调度就是将此时的栈切换为下一个进程的栈,那么切换肯定要知道切换之后要保存哪些东西,这个是由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
复制代码
Tatsächlich registrieren wir, wenn int num
wir einen Interrupt aufrufen, zuerst die Funktion, die wir aufrufen möchten.Wie bereits erwähnt, gibt es insgesamt 256, jeweils 4 Bytes, also wird zum Beispiel oben int 0x54
die interrupt
Funktion registriert 0x54 * 4
.
Sie können sehen, dass der Druck erfolgreich war.
Da wir im geschützten Modus jedoch selten Segmentadressen und Offsets innerhalb von Segmenten verwenden, wird die obige Methode selten verwendet, aber diese Idee wird immer noch beibehalten.Lassen Sie uns über Interrupts im geschützten Modus sprechen.
Interrupt-Deskriptor-Tabelle
Die Interrupt-Vektortabelle im Real-Modus wird zu einer Interrupt-Deskriptor-Tabelle im geschützten Modus, und der Interrupt-Vektor im Real-Modus wird zu einem Interrupt-Deskriptor im geschützten Modus Lassen Sie uns zuerst über den Interrupt-Deskriptor sprechen.
Wir wissen, dass der Interrupt-Vektor tatsächlich auf die Adresse der Funktion zeigt, aber weil der Platz des Interrupt-Deskriptors größer wird, sind viele andere Dinge hinzugefügt worden.Lassen Sie uns zuerst einen Blick auf seine Struktur werfen.
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;
复制代码
Die offset1
Summe offset2
kann man sich als Adresse der Funktion vorstellen, auf die gezeigt wird, natürlich ist sie in eine hohe 15-Bit-Adresse und eine niedrige 15-Bit-Adresse unterteilt
In ähnlicher Weise werden alle Interrupt-Deskriptoren in einer Tabelle zusammengefasst, die natürlich die Interrupt-Deskriptor-Tabelle ist.In ähnlicher Weise gibt es ein spezielles Register, das auf diese Interrupt-Deskriptor-Tabellezeigt, nämlich IDT register
, es gibt auch zwei Befehle
lidt [idt_ptr]; 加载 idt 将idt_ptr指向的地方保存到IDT register
sidt [idt_ptr]; 保存 idt 将IDT register存放的值放在idt_ptr中
复制代码
Lassen Sie uns die Interrupt-Deskriptortabelle in unserem System implementieren.
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 $
复制代码
Lassen Sie uns über seinen Prozess sprechen.Erstens wird es in kernel
der Hauptfunktion function kernel_init
verwendet, interrupt_init
um eine Interrupt-Deskriptor-Tabellemit einer Größe von 128 zu erstellen und jeden Interrupt-Deskriptor zu initialisieren.Die Funktionsadresse, auf die jeder Deskriptor zeigt, ist interrupt_handler
, This function is to print default interrupt
, und beachten Sie dann, dass, wenn die kernel_init
Funktion zurückkehrt, es zu der _start
Funktion kommt, die darin verwendet int 0x80
wird.Tatsächlich ist es nicht nötig, zu diesem Zeitpunkt anzugeben, welche Nummer, da 128 Interrupt-Deskriptoren gleich sind, int 0x69
und die gleich, weniger als 128.
Das Ergebnis ist raus
\