【实现一个简单的操作系统】01-在虚拟机中运行简单内核
【实现一个简单的操作系统】02-实现保护模式内存寻址GDT
【实现一个简单的操作系统】03-实现保护模式下的中断处理IDT

前言

本系列是实现一个简单的操作系统中的第 1 篇博客,如果您感兴趣,建议您阅读上述其他博客。本篇博客将介绍如何写一个入门内核以及如何在虚拟机中启动内核。

1 操作系统的启动

接通电源后,内存中固化了一段代码(这段代码无需从存储器中读取)。这段代码的执行流程如下:

① 设置段地址cs=0xFFFF(0x表示16进制),偏移地址ip=0x0000
② 系统当前处于实模式,寻址方式为段地址左移四位加上偏移地址,此时pc=0xFFFF0,对应着 ROM BIOS 映射区(BIOS,Basic Input Output System,基本输入输出系统)
③ 检查RAM,键盘,显示器,磁盘等等是否正常工作
④ 将0磁0扇区(共512字节)读入0x7C00地址处(这就是引导扇区)
⑤ 设置段地址cs=0x07C0,ip=0x0000,pc=cs<<4+ip=0x7C00,系统接下来从0x7C00开始执行,即引导扇区的地址

1.2 具体实现

当我们自己实现一个简单操作系统时,暂时不需要执行上述复杂的流程,只需要让内核正确加载,成功运行即可。
代码目录:kernel.cpp, loader.s, Makefile, linker.ld

1.2.1 GRUB 引导代码编写

GRUB 是一个系统启动引导管理器,是在计算机启动后运行的第一个程序,他是用来负责加载、传输控制到操作系统的内核,一旦把内核挂载,系统引导管理器的任务就算完成退出。接下来由内核来控制完成系统引导的其它部分,比如系统的初始化及启动过程。GRUB 是一个来自自由软件基金会项目的多操作系统启动程序,它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。GRUB 可用于选择操作系统分区上的不同内核,也可用于向这些内核传递启动参数。

如果希望使用 GRUB 引导操作系统,需要满足以下两个条件:
① 需要有一个 Multiboot Header,这个 Multiboot Header 必须在内核镜像的前 8192 个字节内,并且是首地址是 4 字节对其的。
② 内核的加载地址在 1MB 以上的内存中,这个要求是 GRUB 附加的,并非多重引导规范的规定。

Multiboot Header 定义如下:

偏移量类型域名备注
0uint32_tmagic必需
4uint32_tflags必需
8uint32_tchecksum必需
12uint32_theader_addr如果flags[16]被置位
16uint32_tload_addr如果flags[16]被置位
20uint32_tload_end_addr如果flags[16]被置位
24uint32_tbss_end_addr如果flags[16]被置位
28uint32_tentry_addr如果flags[16]被置位
32uint32_tmode_type如果flags[2]被置位
36uint32_twidth如果flags[2]被置位
40uint32_theight如果flags[2]被置位
44uint32_tdepth如果flags[2]被置位

其中 magic, flags, checksum 是前三个必须保留的 header 域名,只要在汇编代码中定义即可。GRUB 将会在引导操作系统时检查该 header 以正常引导操作系统。

; 设置必要的 Multiboot Header
.set MAGIC, 0x1badb002
.set FLAGS, (1 << 0 | 1 << 1)
.set CHECKSUM, -(MAGIC + FLAGS)

; 定义 multiboot段
.section .multiboot
    .long MAGIC
    .long FLAGS
    .long CHECKSUM

loader.s
注:关于 ARM 架构下常用 GNU 汇编程序伪指令的使用方式可看文末链接。

; 设置必要的 Multiboot Header
.set MAGIC, 0x1badb002
.set FLAGS, (1 << 0 | 1 << 1)
.set CHECKSUM, -(MAGIC + FLAGS)

; 定义 multiboot段
.section .multiboot
    .long MAGIC
    .long FLAGS
    .long CHECKSUM

; 定义代码段
.section .text
.extern kernelMain          ; 声明外部函数,在kernel.cpp实现,见下文
.extern callConstructors    ; 声明外部函数,在kernel.cpp实现,见下文
.global loader

loader:
    mov $kernel_stack, %esp
    call callConstructors
    push %eax
    push %ebx
    call kernelMain

_stop:
    cli
    hlt
    jmp _stop

.section .bss
.space 2*1024*1024
kernel_stack:               ; 栈从高地址向低地址使用

1.2.2 kernel代码编写

kernel 代码只完成一个简单功能,可以在屏幕显示 Hello OS!。之前在loader.s文件里声明了外部函数,kernelMain 和 callConstructors,kernel.cpp 中需要实现。

<kernelMain函数>

extern "C" void kernelMain(void *multiboot_structure, int32_t magic_number){
    printf("Hello OS!");

    while(1); // 内核程序需要保证持续运行,死循环保证持内核持续运行
}

由于自己实现操作系统,无法使用库函数(自己实现的操作系统没有库函数),需要自己实现,printf 函数。

<printf函数>
需要在操作系统的显示内存中显示字符,就需要知道操作系统的显示内存地址,显示内存地址为 0xb8000,往该内存写内容就可显示字符。显示一个字符需要两个字节,第一个字节决定该字符的颜色,第二个字节决定该字符的内容。

void printf(const char *str){
    static short *VideoMemory = (short*)0xb8000;
    static char x = 0;
    for(int i = 0; str[i] != '\0'; ++i){
        VideoMemory[x] = (VideoMemory[x] & 0xFF00 | str[i]);
        ++x;
    }
}

<callConstructors函数>
callConstructors 负责一些程序初始化操作,比如初始化全局变量。

typedef void (*constructor)();

// 在链接文件中,start_ctors 和 end_ctors实际上时两个地址
extern constructor start_ctors;
extern constructor end_ctors;

// 进行初始化操作
extern "C" void callConstructors(){
    for(constructor *i = &start_ctors; i != &end_ctors; ++i){
        (*i)();
    }
}

其中的 start_ctors 和 end_ctors (函数指针)定义在链接文件 linker.ld 中,可以认为是要执行的一些函数的起始地址和终止地址。

综上所述,kernel.cpp代码如下:
kernel.cpp

void printf(const char *str){
    static short *VideoMemory = (short*)0xb8000;
    static char x = 0;
    for(int i = 0; str[i] != '\0'; ++i){
        VideoMemory[x] = (VideoMemory[x] & 0xFF00 | str[i]);
        ++x;
    }
}

typedef void (*constructor)();

// 在链接文件中,start_ctors 和 end_ctors实际上时两个地址
extern constructor start_ctors;
extern constructor end_ctors;

// 进行初始化操作
extern "C" void callConstructors(){
    for(constructor *i = &start_ctors; i != &end_ctors; ++i){
        (*i)();
    }
}

extern "C" void kernelMain(void *multiboot_structure, int magic_number){
    printf("Hello OS!");

    while(1); // 内核程序需要保证持续运行,死循环保证持内核持续运行
}


1.2.3 链接脚本的编写

链接脚本的编写可参考文末链接,有助于对链接脚本有初步认识,不需要完全掌握。

linker.ld

ENTRY(loader)

OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)

SECTIONS {
    . = 0x100000;

    .text : {
        *(.multiboot)
        *(.text*)
        *(.rodata)
    }

    .data : {
        start_ctors = .;
        KEEP(*(.init_array));
        KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*)));
        end_ctors = .;

        *(.data)
    }

    .bss : {
        *(.bss)
    }

    /DISCARD/ : {
        *(fini_array*)
        *(.comment)
    }
}
1.2.4 Makefile编写

Makefile 可以帮助我们快速编译文件,相关命令可参考文末链接。
Makefile

GPPPARAMS = -m32 -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-rtti -fno-exceptions -fno-leading-underscore
ASMPARAMS = --32
LDPARAMS = -melf_i386

objects = kernel.o loader.o

%.o : %.cpp
	g++ $(GPPPARAMS) -o $@ -c $<

%.o : %.s
	as $(ASMPARAMS) -o $@ $<

mykernel.bin : linker.ld $(objects)
	ld $(LDPARAMS) -T $< -o $@ $(objects)

mykernel.iso : mykernel.bin
	mkdir iso
	mkdir iso/boot
	mkdir iso/boot/grub
	cp $< iso/boot/
	echo 'set timeout=2' >> iso/boot/grub/grub.cfg
	echo 'set default=0' >> iso/boot/grub/grub.cfg
	echo '' >> iso/boot/grub/grub.cfg
	echo 'menuentry "Mini OS" {' >> iso/boot/grub/grub.cfg
	echo '	multiboot /boot/mykernel.bin' >> iso/boot/grub/grub.cfg
	echo '	boot' >> iso/boot/grub/grub.cfg
	echo '}' >> iso/boot/grub/grub.cfg
	grub-mkrescue --output=$@ iso
	rm -rf iso

run : mykernel.iso
	virtualboxvm --startvm "Tuitorial" &

.phony : clean
clean:
	rm -rf $(objects) mykernel.bin mykernel.iso

1.3 运行操作系统

① 编译得到 mykernel.iso 文件

make mykernel.iso

② 使用 VMWare 创建虚拟机

在这里插入图片描述

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

③ 运行操作系统
在这里插入图片描述

参考资料

[1] 【操作系统】操作系统是如何启动的?看这一篇就够了【小白也能看得懂的详解操作系统之启动】
[2] 操作系统启动过程——启动引导+硬件自检+系统引导+系统加载+系统登录
[3] 详解grub(一)
[4] 用 GRUB 引导自己的操作系统
[5] ARM架构下常用GNU汇编程序伪指令介绍 Assembler Directive
[6] 用 GRUB 引导自己的操作系统的条件
[7] 链接脚本语法简介
[8] Makefile基本使用方法

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐