体育网站开发的目的合肥seo排名扣费
更好的阅读体验,请点击 YinKai 's Blog | 实现一个最简单的内核。
这篇文章带大家实现一个最简单的操作系统内核—— Hello OS。
PC 机的引导流程
我们这里将借助 Ubuntu Linux 操纵系统上的 GRUB 引导程序来引导我们的 Hello OS。
首先我们得了解一下,Hello OS 的引导流程:
简单解释一下,PC 机 BIOS 固件是固化在 PC 机主板上的 ROM 芯片中的,掉电也能保存,PC 机上电后的第一条指令就是 BIOS 固件中的,它负责检测和初始化 CPU、内存及主板平台,然后加载引导设备(大概率是硬盘)中的第一个扇区数据,到 0x7c00 地址开始的内存空间,再接着跳转到 0x7c00 处执行指令,在我们这里的情况下就是 GRUB 引导程序。
Hello OS 引导汇编代码
我们的 Hello OS 总有 6 个文件,下面一一讲解。
Hello OS 的主函数
main.c
#include "vgastr.h"
void main()
{printf("Hello OS! I am YinKai");return;
}
entry.asm
; 多引导协议头(GRUB)
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 _entryALIGN 8
; GRUB 所需的多引导协议头
mbt_hdr:dd MBT_HDR_MAGICdd MBT_HDR_FLAGSdd -(MBT_HDR_MAGIC+MBT_HDR_FLAGS)dd mbt_hdrdd _startdd 0dd 0dd _entryALIGN 8
; GRUB2 所需的多引导协议头
mbt2_hdr:DD MBT_HDR2_MAGICDD 0DD mbt2_hdr_end - mbt2_hdrDD -(MBT_HDR2_MAGIC + 0 + (mbt2_hdr_end - mbt2_hdr))DW 2, 0DD 24DD mbt2_hdrDD _startDD 0DD 0DW 3, 0DD 12DD _entryDD 0DW 0, 0DD 8mbt2_hdr_end:ALIGN 8
_entry:; 关中断cli; 关不可屏蔽中断in al, 0x70or al, 0x80out 0x70, al; 重新加载GDTlgdt [GDT_PTR]jmp dword 0x8 :_32bits_mode_32bits_mode:; 初始化C语言可能会用到的寄存器mov ax, 0x10mov ds, axmov ss, axmov es, axmov fs, axmov gs, axxor eax,eaxxor ebx,ebxxor ecx,ecxxor edx,edxxor edi,edixor esi,esixor ebp,ebpxor esp,esp; 初始化栈,C语言需要栈才能工作mov esp,0x9000; 调用C语言函数maincall main; 让CPU停止执行指令
halt_step:haltjmp halt_step; GDT 全局描述符表
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
这是一个引导加载程序,它是计算机启动过程中的第一个软件,它的主要任务是在计算机启动时,通过 GRUB 或 GRUB2 多引导协议头,初始化系统环境,设置 GDT,然后调用 C 语言的 main
函数。
控制计算机屏幕
首先我们得知道显卡的字符模式的工作细节。
它把屏幕分成 24 行,每行 80 个字符,把这(24*80)个位置映射到以 0xb8000 地址开始的内存中,每两个字节对应一个字符,其中一个字节是字符的 ASCII 码,另一个字节为字符的颜色值。如下图所示:
了解原理之后,我们来自己实现 printf 函数:
vgastr.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;
}
代码很简单,我们在 printf 把传入的字符串作为参数,传给 _strwrite 函数,然后把字符串中的每个字符依次写入 0xb8000 地址开始的显存中。p_strdst 每次加 2 ,是为了跳过表示颜色值的字符,直接指向下一个字符的 ASCII 值。
为了编译器能够正确识别我们的函数,我们还需要另写一个文件,保证函数调用的正确性。
vgastr.h
void _strwrite(char* string);
void printf(char* fmt, ...);
链接
hello.lds
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 = .;
}
这段代码是一个链接脚本,用于告诉链接器如何将各个目标文件组合成最终的可执行文件。
编译
我们这里使用 make 工具进行系统编译,将每个代码模块编译最后链接成可执行的二进制文件。
Makefile
MAKEFLAGS = -sR
MKDIR = mkdir
RMDIR = rmdir
CP = cp
CD = cd
DD = dd
RM = rmASM = nasm
CC = gcc
LD = ld
OBJCOPY = objcopyASMBFLAGS = -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 hello.lds -n -Map HelloOS.map
OJCYFLAGS = -S -O binaryHELLOOS_OBJS :=
HELLOOS_OBJS += entry.o main.o vgastr.o
HELLOOS_ELF = HelloOS.elf
HELLOOS_BIN = HelloOS.bin.PHONY : build clean all link binall: clean build link binclean:$(RM) -f *.o *.bin *.elfbuild: $(HELLOOS_OBJS)link: $(HELLOOS_ELF)
$(HELLOOS_ELF): $(HELLOOS_OBJS)$(LD) $(LDFLAGS) -o $@ $(HELLOOS_OBJS)
bin: $(HELLOOS_BIN)
$(HELLOOS_BIN): $(HELLOOS_ELF)$(OBJCOPY) $(OJCYFLAGS) $< $@%.o : %.asm$(ASM) $(ASMBFLAGS) -o $@ $<
%.o : %.c$(CC) $(CFLAGS) -o $@ $<
安装 Hello OS
不同的系统,可能操作不同,我这里用的是 ubuntu。
安装编译环境
- 安装汇编编译器
sudo apt-get install nasm
- 安装gcc(该命令会安装包括gcc在内的所有软件)
sudo apt install build-essential
修改启动项等待时间
修改启动项等待时间,以供我们选择启动项文件
sudo vim /etc/default/grub
,打开文件,修改为 10 s
使用 sudo update-grub
更新我们的修改。
:::warning
每次使用这个命令之后,我们追加的启动项(后面会说到)就会被清除,需要重新添加。
:::
构建 HelloOS.bin 文件
在自己的家目录下创建一个 HelloOS 文件夹,放入我们依赖的 6 个文件,代码及文件命名见上。
使用 make 构建
在 HelloOS 目录下,使用 make 命令,即可获得 HelloOS.bin 文件,并将该文件移动到 /boot/
目录下。(如果原本就有要将其删除,再放入。)
追加 GRUB 启动项
使用 df /boot
获取文件系统名,以及文件系统的挂载点,我的如下:
文件系统 1K-块 已用 可用 已用% 挂载点
/dev/sda5 19947120 9921616 8986912 53% /
写 grub 的引导文件,将下面的启动项代码插入到 /boot/grub/grub.cfg
文件末尾
menuentry 'HelloOS' {insmod part_msdos #GRUB加载分区模块识别分区insmod ext2 #GRUB加载ext文件系统模块识别ext文件系统set root='hd0,msdos5' #注意boot目录挂载的分区,这是我机器上的情况multiboot2 /boot/HelloOS.bin #GRUB以multiboot2协议加载HelloOS.binboot #GRUB启动HelloOS.bin
}
:::warning
① 这里的 hd0,msdos?
需要根据 (4)中的 /dev/sda?
对应起来;
② 如果挂载点是 / 就需要在文件中写 /boot/HelloOS.bin
;如果挂载点是 /boot
,则直接写 /HelloOS.bin
即可
③ 如果该文件不可修改,可以用 root 权限修改该文件为可写文件。
:::
最后使用 reboot
命令,即可重启系统,看到我们的 Hello OS 选项:
选择后,即可看到我们在主函数 main.c 中写的字符串啦~
小结
Hello OS 启动的流程主要包括以下步骤:
- 计算机上电: 当计算机上电时,主板上的 BIOS 固件开始执行。
- BIOS 初始化: BIOS 负责检测和初始化计算机硬件,包括处理器、内存等。
- 加载引导扇区: BIOS 根据启动设备配置加载引导扇区,通常是硬盘上的 MBR。
- 引导加载程序执行: 引导加载程序(如 GRUB)被加载,负责加载操作系统内核。
- GRUB 加载 Hello OS: GRUB 通过配置文件加载 Hello OS 的二进制文件(HelloOS.bin)。
- Hello OS 入口点: Hello OS 的入口点在
entry.asm
中,负责初始化系统环境。 - 切换到 32 位保护模式:
_start
调用_32bits_mode
将处理器切换到 32 位保护模式。 - C 语言的 main 函数:
main.c
包含操作系统的主要逻辑,调用了输出字符串的函数。 - 屏幕输出:
vgastr.c
中的_strwrite
和printf
负责向屏幕输出字符串。 - 系统初始化完成: Hello OS 在初始化完成后,等待主要逻辑执行完毕。
- CPU 停止执行指令: 使用
halt
指令让 CPU 停止执行,操作系统启动过程结束。 - 系统运行或重新启动: 如果需要,可以继续执行其他操作系统功能或重新启动计算机。