【实现一个简单的操作系统】01-在虚拟机中运行简单内核
接通电源后,内存中固化了一段代码(这段代码无需从存储器中读取)。这段代码的执行流程如下:① 设置段地址cs=0xFFFF(0x表示16进制),偏移地址ip=0x0000② 系统当前处于实模式,寻址方式为段地址左移四位加上偏移地址,此时pc=0xFFFF0,对应着 ROM BIOS 映射区(BIOS,Basic Input Output System,基本输入输出系统)③ 检查RAM,键盘,显示器,
【实现一个简单的操作系统】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
定义如下:
偏移量 | 类型 | 域名 | 备注 |
---|---|---|---|
0 | uint32_t | magic | 必需 |
4 | uint32_t | flags | 必需 |
8 | uint32_t | checksum | 必需 |
12 | uint32_t | header_addr | 如果flags[16]被置位 |
16 | uint32_t | load_addr | 如果flags[16]被置位 |
20 | uint32_t | load_end_addr | 如果flags[16]被置位 |
24 | uint32_t | bss_end_addr | 如果flags[16]被置位 |
28 | uint32_t | entry_addr | 如果flags[16]被置位 |
32 | uint32_t | mode_type | 如果flags[2]被置位 |
36 | uint32_t | width | 如果flags[2]被置位 |
40 | uint32_t | height | 如果flags[2]被置位 |
44 | uint32_t | depth | 如果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基本使用方法
更多推荐
所有评论(0)