实验:Hello OS

401 阅读4分钟

引导流程

通过借助 Ubuntu Linux 操作系统的 GRUB 引导程序来引导 Hello OS 操作系统。引导过程如下图所示:

代码组成

汇编代码

C 作为通用的高级语言,不能直接操作特定的硬件,而且 C 语言的函数调用、函数传参,都需要用栈。它是一块内存空间,由 CPU 特定的栈寄存器指向,所以我们要先用汇编代码处理好这些 C 语言的工作环境

;彭东 @ 2021.01.09
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所需要的头
;包含两个头是为了同时兼容GRUBGRUB2

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

主函数代码

上面的汇编代码调用了 main 函数,而在其代码中并没有看到其函数体,而是从外部引入了一个符号。这个函数是用 C 语言写在 main.c 文件中最终它们分别由 nasmGCC 编译成可链接模块,由 LD 链接器链接在一起,形成可执行的程序文件:

//彭东 @ 2021.01.09
#include "vgastr.h"
void main()
{
  printf("Hello OS!");
  return;
} 

需要注意的是:这不是应用程序的 main 函数,而是 Hello OS 的 main 函数。printf 也需要我们自己实现。

输出函数代码

计算机屏幕显示往往是显卡的输出,我们要在屏幕上显示字符,就要编程操作显卡。显卡都支持一种叫 VESA 的标准,这种标准下有两种工作模式:字符模式和图形模式。显卡们为了兼容这种标准,不得不自己提供一种叫 VGABIOS 的固件程序。

显卡的字符模式的工作细节如下:

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

//彭东 @ 2021.01.09
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 OS 的编译工作来说特别简单,因为总共才三个代码文件,最多四条命令就可以完成。一个成熟的商业操作系统更是多达几万个代码模块文件,几千万行的代码量,是这世间最复杂的软件工程之一。所以需要一个牛逼的工具来控制这个巨大的编译过程。

make 工具

make 历史悠久,小巧方便,也是很多成熟操作系统编译所使用的构建工 。只要某个文件有变化,就要重新构建的项目,都可以用Make构建。

make 读取一个叫“makefile”的文件,也是一种文本文件,这个文件中写好了构建软件的规则,它能根据这些规则自动化构建软件

Makefile 格式

Makefile文件由一系列规则(rules)构成。每条规则的形式如下。

<target> : <prerequisites> 
[tab]  <commands>

第一行冒号前面的部分,叫做"目标"(target),冒号后面的部分叫做"前置条件"(prerequisites);第二行必须由一个tab键起首,后面跟着"命令"(commands)。 "目标"是必需的,不可省略; "前置条件"和"命令" 都是可选的,但是两者之中必须至少存在一个

一个有关 makefile 的例子:

CC = gcc #定义一个宏CC 等于gcc
CFLAGS = -c #定义一个宏 CFLAGS 等于-c
OBJS_FILE = file.o file1.o file2.o file3.o file4.o #定义一个宏
.PHONY : all everything #定义两个伪目标all、everything
all:everything #伪目标all依赖于伪目标everything
everything :$(OBJS_FILE) #伪目标everything依赖于OBJS_FILE,
#而OBJS_FILE是宏会被替换成file.o file1.o file2.o file3.o file4.o
%.o : %.c
   $(CC) $(CFLAGS) -o $@ $<
  1. make 规定“#”后面为注释,make 处理 makefile 时会自动丢弃。
  2. makefile 中可以定义,宏最终会在宏出现的地方替换成相应的字符串。
  1. .PHONY 在 makefile 中表示定义伪目标。它不代表一个真正的文件名,在执行 make 时可以指定这个目标来执行其所在规则定义的命令。伪目标可以依赖于另一个伪目标或者文件。
  2. everything 下面并没有相关的执行命令,但是下面有个通用规则:“%.o : %.c”。其中的“%”表示通配符,表示所有以“.o”结尾的文件依赖于所有以“.c”结尾的文件。
  1. 针对这些依赖关系,分别会执行:$(CC) $(CFLAGS) -o $@ $< 命令,当然最终会转换为:gcc –c –o xxxx.o xxxx.c,这里的“xxxx”表示一个具体的文件名。
  2. $@ 和 $< 都是自动变量,$@ 指代当前目标,就是Make命令当前构建的那个目标。比如,make foo的 $@ 就指代foo。$< 指代第一个前置条件。比如,规则为 t: p1 p2,那么 $< 就指代p1。

编译过程

安装 HelloOS

经过上述流程,我们就会得到 Hello OS.bin 文件,然后通过安装使得 GRUB 能够找到它,这样才能在计算机启动时加载它。

GRUB 在启动时会加载一个 grub.cfg 的文本文件,根据其中的内容执行相应的操作,其中一部分内容就是启动项。GRUB 首先会显示启动项到屏幕,然后让我们选择启动项,最后 GRUB 根据启动项对应的信息,加载 OS 文件到内存。

编写 HelloOS 的启动项信息到 /boot/grub/grub.cfg。

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

实验中遇到的问题

1、在 VMware 中安装 Ubuntu 系统的时候由于分辨率的问题导致安装按钮隐藏,不能执行。

先试用,试用时点开终端, 运行sudo apt-get install open-vm-tools, 安装完成后,再点VM菜单中的进入全屏模式就可以全屏运行, 在全屏运行的情况下再安装就不会有问题了。

2、Ubuntu 系统网络无连接。

没有打开 VMware 相关服务。

3、将 HelloOS.bin 文件复制到 /boot/ 下,重启并无 HelloOS 选项。

需要在增加 HelloOS 启动项之前设置 grub 进入菜单。修改 /etc/default/grub 实现。

GRUB_DEFAULT=0
#GRUB_TIMEOUT_STYLE=hidden #HelloOS
#GRUB_TIMEOUT=0 #HelloOS
GRUB_TIMEOUT=30 #HelloOS
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
#GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" #HelloOS
GRUB_CMDLINE_LINUX_DEFAULT="text" #HelloOS
GRUB_CMDLINE_LINUX="locale=zh_CN"

修改完之后通过 sudo update-grub 来更新 grub 配置。