写一个微型bootloader

3-许一帆
3-许一帆   编辑于 2020-04-03 20:46
阅读量: 287

写在前面

我们在做什么?

这份文档会带领大家写一个微型的x86指令集的bootloader,并且我们专注学习基于在Linux系统下x86 PC展开讨论。

什么是bootloader?

希望通过几句对于bootloader(引导加载程序)的叙述会有一个直观感觉.

bootloader是在计算机开机BIOS(Basic Input/Ouput System)运行之后加载运行的一段程序。

bootloader是在操作系统运行之前加载的一段程序。

bootloader是一段引导加载程序,用来引导操作系统内核启动运行的加载程序。

bootloader引导操作系统内核映像通过引导介质(硬盘HDD or 软盘floppy等)进入内存,之后跳转到内核入口点去启动操作系统。

现在来做一个总结。bootloader是在计算机开机自检完成后加载操作系统或其他系统软件的计算机程序,是用来引导操作系统以及各种硬件设备初始化的程序,为最终调用操作系统内核准备好正确合适的环境。

为什么需要bootloader?

我想大家可能会有一个疑问,为什么会需要bootloader呢?为什么不让BIOS直接做完这些引导加载工作来引导操作系统内核呢?我将从两方面来展开讨论。

BIOS并不是一个“聪明”的程序。BIOS(存储在ROM中)是计算机开机时运行的第一个软件(固化在固件上)主要是初始化和检测硬件设备,之后是跳转到bootloader程序所在位置。人性化来讲BIOS不聪明,假设你的计算机中有多个操作系统(Windows Linux etc.)又或者可以假设你的计算机中有很多不同版本的操作系统,如果这些加载工作都交给BIOS的话无疑会使BIOS复杂很多倍。这个时候就体现出bootloader存在的价值。在BIOS做完一些初始化硬件设备的工作及检测后,就会将控制权转交给bootloader。对于不同版本不同类型的操作系统的加载,会分别有一个bootloader去引导。

想写一个自己的操作系统。如果你是这样的有志青年,那么你当然需要自己去写一个适合加载自己操作系统的bootloader。如果是这样的话我会推荐你下x86 PC的操作系统,因为亲身体会发现有很多可用的文档供参考。

bootloader在哪?

bootloader通常位于硬盘的第一扇区,这个扇区最前面的512个子节通常被称为主引导记录MBR。

我们先来看看MBR的组成结构。

1. 第1-446字节:bootloader✩(注意)

2.第447-510字节:分区表(Partition table)

3.第511字节-512字节:主引导记录MBR签名(0x55 0xAA)

分区表

分区表是干啥的呢?分区表中记录了这个硬盘每个分区的信息。那为什么硬盘会有分区呢?这个简单理解就是为我们安装不同操作系统做准备的。

关于分区表我们必须要知道的是:分区表的长度有64字节,里面分成了四项,每一项有16字节。这16字节的第一个字节(0x80)是告诉我们这个分区是否是激活分区。下面是拓展内容:

第1个字节:如果是0x80就表示该主分区是激活分区,控制权要转交给这个分区。

主引导记录MBR签名

在包含有效的引导程序代码的磁盘上,MBR的最后两个字节应始终为0x55 0xAA。

总结

请注意我在上文提到的MBR组成结构中:第1-446字节是bootloader。这只是针对于这篇文档的目标而言的,只是写一个微型的bootloader把原理讲清楚帮助大家理解。但是相信大家可以看到仅仅440左右大小的字节数是很小的。实际上在现在的计算机中这仅仅是作为第一个阶段的bootloader程序,作用是在硬盘上查找另一个文件并加载该文件以执行实际的引导过程。

我想大家还存在一些疑惑,对于这整个过程又不太理解了。下面为大家总结一下现实生活中实际的计算机的启动过程以及引导过程。

第一步:计算机开机后读取BIOS,BIOS检查硬件设备和初始化硬件环境。之后读取加载到主引导扇区MBR的位置,移交计算机控制权。

第二步:MBR中引导程序(第一个阶段的bootloader程序)开始工作,在MBR的分区表中查找可激活的分区。找到活动分区后,MBR中的引导程序代码一般会从活动分区的第一个扇区开始加载程序,我们第二阶段的bootloader程序就在这第一个扇区。

第三步:第二阶段的bootloader程序会读取找到活动分区中的可执行引导文件。下一步就是启动操作系统的实际过程。

因为我们这篇文档只是一个引导性的帮助,希望为大家学习操作系统提供启示。所以我们只专注于实现相对简单的引导程序的基础。要注意真正的引导程序要比我所说的要负责的多,但是概念上都是有相通性的。

正文

准备工作

我们推荐在Linux下进行学习,在这里开发和学习是较为方便的。

配置:Ubuntu18.0 NASM Bochs

安装Ubuntu:Virtualbox安装Ubuntu18.04

配置Bochs(我编写):http://www.edu2act.cn/article/bochsjian-yi-jiao-cheng/

下载NASM:sudo apt-get install nasm

了解汇编语言(关注厂长所写的系列文章):回忆一下汇编的世界

10H中断:https://zh.wikipedia.org/wiki/INT_10H

可能用到的知识点

0x07C0:这个地址来自Intel的第一代个人电脑8088,之后的CPU为了保持兼容就一直使用这个地址。当时的操作系统所需内存最少是32KB。我们知道内存地址是从0x0000开始编号,32KB的内存就是0x0000-0x7FFF。

下面来分配一下:8088芯片本身需要占用0x0000-0x03FF,用来保存各种中断处理程序的存储位置,所以可用内存只剩下0x0400-0x7FFF。之后为了把尽量多的连续内存留给操作系统,MBR就放到了内存地址的尾部。主引导扇区需要占用512字节,MBR本身也可能会产生数据,所以另外留出512字节保存。

0x7FFF - 512 - 512 - 1 = 0x7C00

Register

SS:存储栈中起始地址

SP:与SS搭配使用 存储参考起始地址而言的相对地址

BP:与SS SP搭配使用 存放基本的指针

AX:常用于中断调用实现底层调用 ah是AX中的高八位 al是AX中的低八位

ah=07H:滚动窗口

al=00H:滚动行 效果等同于清空窗口

bh:在中断调用中与AX配合使用

用到的命令

nasm xxx.asm -o xxx.bin # 将汇编文件编译为二进制文件

bximage # 创建镜像文件

dd if=xxx.bin of=xxx.img bs=512 count=1 conv=notrunc  #将文件写入镜像

bochs -f bochsrc.txt  # 提前写好bochs配置文件 参照上文链接过程

 

核心代码

    bits 16      # 16位实模式

    # ax 是通用寄存器

    mov ax, 0x07C0   # MBR存入内存地址0x7C00 BIOS会在这里读取MBR

    mov ds, ax           # 0x07C0中的内容流入ds(段寄存器中)

    mov ax, 0x07E0      # 07E0H = (07C00H+200H)/10H

    mov ss, ax          # ax中存储的内容流入ss寄存器中 

    mov sp, 0x2000      # 初始化一个8K大小的栈



    call clearscreen     # 调用clearscreen过程



    push 0x0000         # 将0x0000中保存的内容放入栈中  ???why is 0x0000

    call movecursor     # 调用movecursor过程

    add sp, 2            



    push msg              # 将标签为msg的内容放入栈中

    call print              # 调用print过程

    add sp, 2



    cli

    hlt                      # cli和hlt所起作用不接受中断并且停止继续运行



    clearscreen:

        push bp            # 保存bp寄存器

        mov bp, sp        # 将sp寄存器保存的指针传给bp

        pusha              # 把通用寄存器都入栈保护起来



   # 下面涉及到了中断调用的知识,该调用提供了一些简洁的底层功能



        mov ah, 0x07        # 告诉BIOS向下滚动窗口

        mov al, 0x00        # 相当于清空整个窗口

        mov bh, 0x07        # 设置字体显示为白色

        mov cx, 0x00        # 设置屏幕左上角是(0,0)位置

        mov dh, 0x18        # 18H换算成十进制是24

        mov dl, 0x4f        # 4fH换算成十进制是79 规定好屏幕右下角在(25,80)处

        int 0x10            # 10H中断



        popa             # 将通用寄存器弹出栈

        mov sp, bp        # 将原始的sp中寄存器保存的指针还给sp

        pop bp             # 还原bp的值

        ret                  # 退出子过程



    movecursor:

        push bp

        mov bp, sp

        pusha



        mov dx, [bp+4]      # bp寄存器中的指针向高位移动4个字

        mov ah, 0x02        # 设置光标位置

        mov bh, 0x00        # 回到第0页

        int 0x10



        popa

        mov sp, bp

        pop bp

        ret



    print:

        push bp

        mov bp, sp

        pusha

        mov si, [bp+4]      # bp指针递增4个字 将所存内容传给si寄存器

        mov bh, 0x00        # 回到第0页

        mov bl, 0x00        # 图像模式来决定颜色

        mov ah, 0x0E        # 将字符打印到所选位置

        mov al, [si]        # 获取当前指针所指字符

        add si, 1           # 持续递增si寄存器所指 直到碰见空字符

        or al, 0

        je .return          # 如果字符串已完成则结束 (读取到 0)

        int 0x10            # 继续打印字符

        jmp .char           # 持续循环

    .return:

        popa

        mov sp, bp

        pop bp

        ret



    msg:    db "Oh boy do I sure love assembly!", 0  # 0是这段msg的结束标志



times 510-($-$$) db 0 #$:当前物理地址,$$:开始的物理地址 用0填充510-($-$$)个字节

dw 0xAA55   # 结束标志 上文有所叙述

总结整个过程

安装bochs

sudo apt-get install bochs

sudo apt-get install bochs-x

写一个bootloader引导加载程序

vim boot.asm  # asm是汇编语言编写的源程序文件

nasm boot.asm -o boot.bin # 编译为boot.bin二进制文件

创建镜像文件

bximage #按着步骤走 最后一步是命名

将bootloader写入镜像

dd if=boot.bin of=xxx.img bs=512 count=1 conv=notrunc

配置bochs

vim bochsrc  # 参考上述配置内容

开机

bochs -f bochsrc  # 注意这里的bochsrc 名字不固定 你的配置文件命名为啥这就是啥 后缀名也写上

c   # 最后你可能需要在命令行里输入c 才能继续运行

 

留疑

逻辑地址与物理地址

BIOS中断调用:https://en.wikipedia.org/wiki/BIOS_interrupt_call

 

推荐阅读与参考博文:

BIOS / MBR引导过程 English

计算机是如何启动的? Zh

http://joebergeron.io/posts/post_two.html

收藏 1 转发 评论 1
3-徐良

牛逼!学到了