经过 20 多篇文章的一步步走来,我们已经从开机启动的 BIOS 执行跳转进入到自己编写的起始扇区,又从起始扇区跳转进入到 loader,时至今日,我们终于进入到内核了,海阔凭鱼跃,天高任鸟飞,我们已经打开了操作系统真正的核心组件 — 内核,那么,就让我们赶紧扩充内核,让他成为一个真正的操作系统吧。 本文,我们就来实现内核最为初步的工作:
首先,我们需要创建堆栈空间,nasm 中,resb 伪指令用来生成未经初始化的一段空间。
[SECTION .bss]
StackSpace resb 2 * 1024 * 1024
StackTop:
[section .text]
global _start
_start:
mov esp, StackTop ; 堆栈在 bss 段中
这里我们创建了一个堆栈段,StackTop 标签指向栈顶。 接下来,我们将 StackTop 赋值给 esp 就完成了堆栈的切换。
进入内核,我们希望一切都从头开始,包括最为重要的标志位寄存器是必须要进行初始化的,此时,我们先暂时初始化为 0 :
push 0
popfd
切换 GDT 的工作主要分两个步骤:
相对于堆栈切换,这部分的工作略微多了一些,而此时,我们已经可以通过将 C 语言代码编译为 ELF 文件来供 kernel 调用了,接下来我们就用 C 语言来实现这部分功能。
首先,我们用汇编实现一下供 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
首先,我们需要在拷贝前开辟一段空间来存储新的 GDT,那么,开辟多大的空间呢,这里我们就需要声明一个段描述符的结构。
#define GDT_SIZE 128
/* 段描述符 */
typedef struct s_descriptor
{
unsigned short limit_low; /* Limit */
unsigned short base_low; /* Base */
unsigned char base_mid; /* Base */
unsigned char attr1; /* P(1) DPL(2) DT(1) TYPE(4) */
unsigned char limit_high_attr2; /* G(1) D(1) 0(1) AVL(1) LimitHigh(4) */
unsigned char base_high; /* Base */
} DESCRIPTOR;
接下来,我们就要将 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;
}
接下来,我们要在 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
程序执行中,段选择子被加载到 cs 寄存器中,除非进行长跳转,否则 cs 寄存器的值是不会发生变化的。 我们虽然通过上面的指令实现了 gdtr 寄存器的更新,但我们紧接着必须通过长跳转把新的段选择子更新到 cs 寄存器中:
SELECTOR_KERNEL_CS equ 8
jmp SELECTOR_KERNEL_CS:csinit
csinit: ; 长跳转,让 GDT 切换生效
这里我们创建了一个段选择子,他的值为 8,表示他是 GDT 中的首个段,且选择子属性位为 0,即 GDT、Ring0 段选择子。
运行 kernel,我们就可以看到下图了:
本项目已开源:https://github.com/zeyu203/techlogOS。