内核的雏形(上) — 创建属于 kernel 的堆栈与 GDT

1. 引言

经过 20 多篇文章的一步步走来,我们已经从开机启动的 BIOS 执行跳转进入到自己编写的起始扇区,又从起始扇区跳转进入到 loader,时至今日,我们终于进入到内核了,海阔凭鱼跃,天高任鸟飞,我们已经打开了操作系统真正的核心组件 — 内核,那么,就让我们赶紧扩充内核,让他成为一个真正的操作系统吧。
本文,我们就来实现内核最为初步的工作:

  1. 从 loader 切换堆栈到内核
  2. 切换 GDT 到内核
  3. 添加中断处理

2. 切换堆栈

首先,我们需要创建堆栈空间,nasm 中,resb 伪指令用来生成未经初始化的一段空间。

[SECTION .bss]  
StackSpace resb 2 * 1024 * 1024
StackTop:

[section .text]
global _start

_start:

mov esp, StackTop ; 堆栈在 bss 段中

这里我们创建了一个堆栈段,StackTop 标签指向栈顶。
接下来,我们将 StackTop 赋值给 esp 就完成了堆栈的切换。

3. 初始化 EFLAGS

进入内核,我们希望一切都从头开始,包括最为重要的标志位寄存器是必须要进行初始化的,此时,我们先暂时初始化为 0 :

push    0  
popfd

4. 切换 GDT

切换 GDT 的工作主要分两个步骤:

  1. 通过 sgdt 指令获取当前 gdtr 寄存器存储的 loader 的 GDT 存储空间首地址与界限
  2. 创建属于 kernel 的新的 GDT 存储空间
  3. 将 loader 的 GDT 拷贝到新的 GDT 存储空间中
  4. 通过 lgdt 指令将 kernel 的 GDT 存储空间首地址与界限载入到 gdtr 寄存器中

相对于堆栈切换,这部分的工作略微多了一些,而此时,我们已经可以通过将 C 语言代码编译为 ELF 文件来供 kernel 调用了,接下来我们就用 C 语言来实现这部分功能。

4.1. 内存拷贝函数

首先,我们用汇编实现一下供 C 语言调用的 memcpy 函数,我们此前的文章中曾经写过这个函数:
实战操作系统 loader 编写(下) — 进军内核

[SECTION .text]  

global memcpy

; ------------------------------------------------------------------------
; void* memcpy(void* es:pDest, void* ds:pSrc, int iSize);
; ------------------------------------------------------------------------
memcpy:
push ebp
mov ebp, esp

push esi
push edi
push ecx

mov edi, [ebp + 8] ; Destination
mov esi, [ebp + 12] ; Source
mov ecx, [ebp + 16] ; Counter
.1:
cmp ecx, 0 ; 判断计数器
jz .2 ; 计数器为零时跳出

; 逐字节移动
mov al, [ds:esi]
inc esi

mov byte [es:edi], al
inc edi

dec ecx ; 计数器减一
jmp .1 ; 循环
.2:
mov eax, [ebp + 8] ; 返回值

pop ecx
pop edi
pop esi
mov esp, ebp
pop ebp

ret

4.2. 开辟内存空间存储 kernel GDT

首先,我们需要在拷贝前开辟一段空间来存储新的 GDT,那么,开辟多大的空间呢,这里我们就需要声明一个段描述符的结构。

 

4.3. 拷贝 GDT 到内核

接下来,我们就要将 loader 中的 GDT 拷贝到 kernel 了。

unsigned char gdt_ptr[6];    /* 0~15:Limit 16~47:Base */  
DESCRIPTOR gdt[GDT_SIZE];

void copy_gdt()
{
clear_screen();
disp_str("----- welcome to the kernel by techlog.cn -----\0");
disp_str("\n----- start to copy gdt ... -----\0");

/* gdt_ptr[6] 共 6 个字节:0~15:Limit 16~47:Base。用作 sgdt/lgdt 的参数。*/
unsigned short* p_gdt_limit = (unsigned short*)(&gdt_ptr[0]);
unsigned int* p_gdt_base = (unsigned int*)(&gdt_ptr[2]);

/* 将 LOADER 中的 GDT 复制到新的 GDT 中 */
memcpy(&gdt, (void*)(*p_gdt_base), *p_gdt_limit + 1);

*p_gdt_limit = GDT_SIZE * sizeof(DESCRIPTOR) - 1;
*p_gdt_base = (unsigned int)&gdt;

disp_str("\n----- finish to copy gdt -----\0");
}

void clear_screen() {
char blank[50], i;
for (i = 0; i < 50; ++i) {
if (i <mark> 48) {
blank[i] = '\n';
blank[i + 1] = '\0';
break;
} else {
blank[i] = ' ';
}
}
for (i = 0; i < 80; ++i) {
disp_str(blank);
}
disp_pos = 0;
}

4.4. 加载新的 GDT

接下来,我们要在 kernel.asm 中调用 copy_gdt 并且通过 lgdt 指令加载新的 gdt 起始地址与界限到 gdtr。

extern    gdt_ptr  

sgdt [gdt_ptr] ; cstart() 中将会用到 gdt_ptr
call copy_gdt ; 在此函数中改变了gdt_ptr,让它指向新的GDT
lgdt [gdt_ptr] ; 使用新的GDT

4.5. 长跳转,进入新的 GDT

程序执行中,段选择子被加载到 cs 寄存器中,除非进行长跳转,否则 cs 寄存器的值是不会发生变化的。
我们虽然通过上面的指令实现了 gdtr 寄存器的更新,但我们紧接着必须通过长跳转把新的段选择子更新到 cs 寄存器中:

SELECTOR_KERNEL_CS    equ    8  

jmp SELECTOR_KERNEL_CS:csinit
csinit: ; 长跳转,让 GDT 切换生效

这里我们创建了一个段选择子,他的值为 8,表示他是 GDT 中的首个段,且选择子属性位为 0,即 GDT、Ring0 段选择子。

5. 运行 kernel

运行 kernel,我们就可以看到下图了:

6. 微信公众号

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤。

7. 完整代码

本项目已开源:https://github.com/zeyu203/techlogOS。

内核的雏形(上) — 创建属于 kernel 的堆栈与 GDT》来自互联网,仅为收藏学习,如侵权请联系删除。本文URL:http://www.bookhoes.com/197.html