手写简单操作系统内核1操作过程2原理

在这里插入图片描述

本文目的在于自定义一个简易操作系统,通过引导程序启动后打印一行字符串,先演示操作,最后介绍原理。

1 操作过程

我的环境:
  在virtualBox上面先安装好虚拟机Ubuntu 16.04

源码:
  gitee.com/hnuwjw/os-h…

1.1 设置grub进入引导菜单

  1. 安装nasm,才能执行make

先安装nasm:

sudo apt-get install -y nasm
复制代码

然后在HelloOS目录下执行:

make -f Makefile
复制代码

就可以得到HelloOS.bin文件了
在这里插入图片描述

  1. 配置下启动项,不然无法选择HelloOS菜单

先修改文件:

/etc/default/grub
复制代码

在这里插入图片描述

如果修改的时候报错“readonly option is set XXXXX”,则使用超级管理员角色编辑文件:

sudo vim grub
复制代码

然后执行:

sudo update-grub
复制代码

1.2 增加HelloOS启动选项

修改/boot/grub/grub.cfg,增加HelloOS启动项:

menuentry 'HelloOS' {
     insmod part_msdos #GRUB加载分区模块识别分区
     insmod ext2 #GRUB加载ext文件系统模块识别ext文件系统
     set root='hd0,msdos1' #注意boot目录挂载的分区,这是我机器上的情况
     multiboot2 /boot/HelloOS.bin #GRUB以multiboot2协议加载HelloOS.bin
     boot #GRUB启动HelloOS.bin
}
复制代码

在这里插入图片描述

在这里插入图片描述

文件系统          1K-块    	  已用     	  可用      已用% 	挂载点
/dev/sda4      48752308 	8087584 	38158536    18%    	  /
复制代码

其中的“sda1”就是硬盘的第四个分区,但是GRUB的menuentry中不能写sda4,而是要写“hd0,msdos1”,这是GRUB的命名方式,hd0表示第一块硬盘。


然后把HelloOS.bin文件复制到/boot/目录下,最后重启计算机

只要我们的PC机上安装了Ubuntu Linux操作系统,GRUB就已经存在了,就不用我们从引导程序开始写了。

在这里插入图片描述
在这里插入图片描述

2 原理

2.1 Hello OS的引导流程

在这里插入图片描述

PC机BIOS固件是固化在PC机主板上的ROM芯片中的,掉电也能保存,PC机上电后的第一条指令就是BIOS固件中的,它负责检测和初始化CPU、内存及主板平台,然后加载硬盘中的第一个扇区数据,到0x7c00地址开始的内存空间,再接着跳转到0x7c00处执行指令,这里就是GRUB引导程序。

2.2 Hello OS引导汇编代码

对应entry.asm文件:

MBT_HDR_FLAGS	EQU 0x00010003
MBT_HDR_MAGIC	EQU 0x1BADB002 ;多引导协议头魔数
MBT_HDR2_MAGIC	EQU 0xe85250d6 ;第二版多引导协议头魔数
global _start ;导出_start符号
extern main ;导入外部的main函数符号

[section .start.text] ;定义.start.text代码节
[bits 32] ;汇编成32位代码
_start:
	jmp _entry
ALIGN 8
mbt_hdr:
	dd MBT_HDR_MAGIC
	dd MBT_HDR_FLAGS
	dd -(MBT_HDR_MAGIC+MBT_HDR_FLAGS)
	dd mbt_hdr
	dd _start
	dd 0
	dd 0
	dd _entry

;以上是GRUB所需要的头
ALIGN 8
mbt2_hdr:
	DD	MBT_HDR2_MAGIC
	DD	0
	DD	mbt2_hdr_end - mbt2_hdr
	DD	-(MBT_HDR2_MAGIC + 0 + (mbt2_hdr_end - mbt2_hdr))
	DW	2, 0
	DD	24
	DD	mbt2_hdr
	DD	_start
	DD	0
	DD	0
	DW	3, 0
	DD	12
	DD	_entry
	DD      0
	DW	0, 0
	DD	8
mbt2_hdr_end:
;以上是GRUB2所需要的头
;包含两个头是为了同时兼容GRUB、GRUB2

ALIGN 8

_entry:
	;关中断
	cli
	;关不可屏蔽中断
	in al, 0x70
	or al, 0x80
	out 0x70,al
	;重新加载GDT
	lgdt [GDT_PTR]
	jmp dword 0x8 :_32bits_mode

_32bits_mode:
	;下面初始化C语言可能会用到的寄存器
	mov ax, 0x10
	mov ds, ax
	mov ss, ax
	mov es, ax
	mov fs, ax
	mov gs, ax
	xor eax,eax
	xor ebx,ebx
	xor ecx,ecx
	xor edx,edx
	xor edi,edi
	xor esi,esi
	xor ebp,ebp
	xor esp,esp
	;初始化栈,C语言需要栈才能工作
	mov esp,0x9000
	;调用C语言函数main
	call main
	;让CPU停止执行指令
halt_step:
	halt
	jmp halt_step


GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
k16cd_dsc: dq 0x00009e000000ffff
k16da_dsc: dq 0x000092000000ffff
GDT_END:

GDT_PTR:
GDTLEN	dw GDT_END-GDT_START-1
GDTBASE	dd GDT_START

复制代码
  1. 代码1~40行,用汇编定义的GRUB的多引导协议头,之所以有两个引导头,是为了兼容GRUB1和GRUB2。

  2. 代码44~52行,关掉中断,设定CPU的工作模式。

  3. 代码54~73行,初始化CPU的寄存器和C语言的运行环境。

  4. 代码78~87行,从GDT_START开始是CPU工作模式所需要的数据。

2.3 主函数

#include "vgastr.h"
void main()
{
  printf("Hello OS!");
  return;
} 
复制代码

其中的printf不是应用程序库中的那个printf,而是需要我们自己实现。

2.4 控制计算机屏幕

我们要在屏幕上显示字符,就要编程操作显卡。

显卡把屏幕分成24行,每行80个字符,把这(24*80)个位置映射到以0xb8000地址开始的内存中,每两个字节对应一个字符,其中一个字节是字符的ASCII码,另一个字节为字符的颜色值。

在这里插入图片描述

void _strwrite(char* string)
{
  char* p_strdst = (char*)(0xb8000);//指向显存的开始地址
  while (*string)
  {
    *p_strdst = *string++;
    p_strdst += 2;
  }
  return;
}

void printf(char* fmt, ...)
{
  _strwrite(fmt);
  return;
}
复制代码

_strwrite函数正是将字符串里每个字符依次定入到0xb8000地址开始的显存中,而p_strdst每次加2,这也是为了跳过字符的颜色信息的空间。

2.5 编译过程

在这里插入图片描述