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

2020-07-26 18:58:13   最后更新: 2020-07-28 09:45:45   访问数量:46




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

本文,我们就来实现内核最为初步的工作:

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

 

首先,我们需要创建堆栈空间,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 的工作主要分两个步骤:

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

 

相对于堆栈切换,这部分的工作略微多了一些,而此时,我们已经可以通过将 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

 

 

开辟内存空间存储 kernel GDT

首先,我们需要在拷贝前开辟一段空间来存储新的 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;

 

 

拷贝 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 48) { blank[i] = '\n'; blank[i + 1] = '\0'; break; } else { blank[i] = ' '; } } for (i = 0; i < 80; ++i) { disp_str(blank); } disp_pos = 0; }

 

 

加载新的 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

 

 

长跳转,进入新的 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

 

 






技术帖      操作系统      os      堆栈      system      内核      gdt      oranges      elf      kernel     


京ICP备15018585号