微型操作系统实现笔记(一)

一. 前言

在跟踪并学习了一遍Linux内核源码框架之后,又经过了一系列相关书籍的阅读,最终决定沉下心来从零开始写一个属于自己的微型操作系统,大致思路如下:

  • 写一个简单的可以启动的操作系统,显示Hello World
  • 实现第一个进程
  • 实现基本的内存管理
  • 实现多进程及进程调度
  • 增加设备注册、设备驱动
  • 增加虚拟文件系统
  • 增加网络架构
  • 封装内核态和用户态
  • 添加交互,如shell等

那么现在就从零开始,一步一步搭建属于自己的操作系统吧。路可能很漫长,但是只要坚持就一定会有收获。本讲内容为如何写出一个可以启动的操作系统。

二. 实现

在前文研究操作系统的时候提到过,电源启动后会由CS:IP组成指令指向BIOS,随后会跳转至bootloader加载操作系统。由此,我们需要以下几部分内容:

  • 一个可运行的程序文件(操作系统)
  • 对bootloader的修改

2.1 程序文件

首先看看程序文件,首先我们当然需要一个汇编文件来做初始化以及系统的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
## 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

以上的汇编代码分为 4 个部分:

  1. 代码 1~40 行,用汇编定义的 GRUB 的多引导协议头,其实就是一定格式的数据,我们的 Hello OS 是用 GRUB 引导的,当然要遵循 GRUB 的多引导协议标准,让 GRUB 能识别我们的OS。之所以有两个引导头,是为了兼容 GRUB1 和 GRUB2。
  2. 代码 44~52 行,关掉中断,设定 CPU 的工作模式。
  3. 代码 54~73 行,初始化 CPU 的寄存器和 C 语言的运行环境。
  4. 代码 78~87 行,GDT_START 开始的,是 CPU 工作模式所需要的数据。

最后调用main函数作为入口函数启动,函数如下:

1
2
3
4
5
6
7
8
// main.c
#include "vgastr.h"

void main()
{
printf("Hello world, welcome to TY's OS!");
return;
}

其中对应的头文件意义在于定义printf函数。我们通常使用的printf来自于运行时库,而此处自建操作系统是没有库函数用的,需要自己写一个能够打印并显示出来的函数。自制的 printf 函数直接调用了 _strwrite 函数,而 _strwrite 函数正是将字符串里每个字符依次定入到 0xb8000 地址开始的显存中,而 p_strdst 每次加 2,这也是为了跳过字符的颜色信息的空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//vgstr.h
void _strwrite(char* string);
void printf(char* fmt, ...);

//vgstr.c
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;
}

在前文hello world的研究中,我们有提及通过lds可以自定义链接文件和虚拟内存分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ENTRY(_start)
OUTPUT_ARCH(i386)
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
. = 0x200000;
__begin_start_text = .;
.start.text : ALIGN(4) { *(.start.text) }
__end_start_text = .;

__begin_text = .;
.text : ALIGN(4) { *(.text) }
__end_text = .;

__begin_data = .;
.data : ALIGN(4) { *(.data) }
__end_data = .;

__begin_rodata = .;
.rodata : ALIGN(4) { *(.rodata) *(.rodata.*) }
__end_rodata = .;

__begin_kstrtab = .;
.kstrtab : ALIGN(4) { *(.kstrtab) }
__end_kstrtab = .;

__begin_bss = .;
.bss : ALIGN(4) { *(.bss) }
__end_bss = .;
}

最后再编写一个makefile文件来自动编译。注意这里在Makefile中增加了update_imageqemu,用于通过qemu直接启动而不需要修改操作系统文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
MAKEFLAGS = -sR
MKDIR = mkdir
RMDIR = rmdir
CP = cp
CD = cd
DD = dd
RM = rm

ASM = nasm
CC = gcc
LD = ld
OBJCOPY = objcopy

ASMBFLAGS = -f elf -w-orphan-labels
CFLAGS = -c -Os -std=c99 -m32 -Wall -Wshadow -W -Wconversion -Wno-sign-conversion -fno-stack-protector -fomit-frame-pointer -fno-builtin -fno-common -ffreestanding -Wno-unused-parameter -Wunused-variable
LDFLAGS = -s -static -T main.lds -n -Map NanoOS.map
OJCYFLAGS = -S -O binary

OS_OBJS :=
OS_OBJS += entry.o main.o vgastr.o
OS_ELF = NanoOS.elf
OS_BIN = NanoOS.bin

.PHONY : build clean all link bin

all: clean build link bin

clean:
$(RM) -f *.o *.bin *.elf

build: $(OS_OBJS)

link: $(OS_ELF)
$(OS_ELF): $(OS_OBJS)
$(LD) $(LDFLAGS) -o $@ $(OS_OBJS)
bin: $(OS_BIN)
$(OS_BIN): $(OS_ELF)
$(OBJCOPY) $(OJCYFLAGS) $< $@

%.o : %.asm
$(ASM) $(ASMBFLAGS) -o $@ $<
%.o : %.c
$(CC) $(CFLAGS) -o $@ $<

update_image:
sudo mount floppy.img /mnt/kernel
sudo cp HelloOS.bin /mnt/kernel/hx_kernel
sleep 1
sudo umount /mnt/kernel

umount_image:
sudo umount /mnt/kernel

qemu:
qemu-system-i386 -fda floppy.img -boot a
#add '-nographic' option if using server of linux distro, such as fedora-server,or "gtk initialization failed" error will occur.

2.2 bootloader的修改

实际上,在makefile里我们已经做了一个qemu的镜像操作,利用qemu可以不用修改bootloader,直接在Linux或者windows上启动虚拟机,操作如下

1
2
make && make umount_image
make qemu

如果还是希望能够在Linux/Windows上实际生效一个新的操作系统,那就需要用到下面的知识修改BootLoader添加新的操作系统配置项了。

首先需要修改的是grub.cfg配置文件,该文件实际存储了各个操作系统配置,会加载并显示在屏幕供选择。

1
2
3
4
5
6
7
menuentry 'HelloOS' { 
insmod part_msdos #GRUB加载分区模块识别分区
insmod ext2 #GRUB加载ext文件系统模块识别ext文件系统
set root='hd0,msdos1' #注意boot目录挂载的分区
multiboot2 /boot/NanoOS.bin #GRUB以multiboot2协议加载NanoOS.bin
boot #GRUB启动
}

接着修改/etc/default/grub,修改步骤如下

a. 注释掉HIDDEN所在的2行

b. 将GRUB_TIMEOUT设置为30(使用默认值10其实也可以)

c. 将GRUB_CMDLINE_LINUX_DEFAUL设置为text

执行sudo update-grub更新

然后将bin文件拷贝至/boot目录下,重启系统即可。

坚持原创,坚持分享,谢谢鼓励和支持