从零编写linux0.11 第一章 简单的bootloader

编程环境:Ubuntu Kylin 16.04

代码仓库:https://gitee.com/AprilSloan/linux0.11-project

工程结构:每一个目录对应于一章的内容,如chapter_01对应于第一章。每一章包含多个小节,如chapter_01/1st代表第一章第一节(简单的启动盘),所有的shell命令都默认当前目录为小节的目录。

linux0.11源码下载(不能直接编译,需进行修改)

原本linux0.11是用gas汇编编写bootloader,我认为大家对gas汇编不太熟悉,所以用Intel汇编编写bootloader。部分汇编知识和计算机知识并不会详细讲述,毕竟这是博客不是写书,还请大家见谅。

1.简单的启动盘

来写一个简单的启动盘吧。启动盘有两个最基本的要求,一是大小必须为512字节,二是最后两个字节必须是0x55和0xaa,不然这不会被识别为启动盘。

以下为bootsect.s的内容。

start:
	jmp start		; 死循环

	times 0x1fe - ($ - $$) db 0	; 填写0,直到0x1fe
	dw 0xaa55		; 启动盘标识

第1、2行是死循环,第4行是让jmp指令后到0x1fe的空间全部填充为0。第五行是让地址0x1fe和0x1ff分别为0x55和0xaa。

利用如下指令编译汇编文件。(没有安装nasm的话,用sudo apt install nasm安装nasm)

nasm boot/bootsect.s -o boot/bootsect.bin

此时,bootsect.bin就可以用来仿真调试了,但为了规范,我们还是将bootsect.bin写入软盘中,再用软盘启动。所以,我们要创建一个软盘。这里使用bximage创建软盘,安装bochs仿真器的过程中就会安装bximage,大家可以看看我的另一篇博客安装bochs:Linux下bochs的安装与使用,里面也有使用bochs的方法。

bximage创建软盘

接下来要将bootsect.bin写入kernel.img中,使用如下命令:

dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc

现在就可以用kernel.img进行仿真调试了。但还别急,要使用bochs仿真还需要配置文件固定仿真使用的cpu、内存大小、软盘启动设置等等,这个文件在仓库中有,我命名为bochsrc,内容如下。

plugin_ctrl: unmapped=1, biosdev=1, speaker=1, extfpuirq=1, parallel=1, serial=1, iodebug=1
config_interface: textconfig
#使用GUI调试
display_library: x, options="gui_debug"
cpu: model=core2_penryn_t9600, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"
cpu: cpuid_limit_winnt=0
cpuid: x86_64=1, mmx=1, sep=1, simd=sse4_2, apic=xapic, aes=1, movbe=1, xsave=1
cpuid: family=6, model=0x1a, stepping=5
romimage: file=$BXSHARE/BIOS-bochs-latest 
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest
vga: extension=vbe, update_freq=5
keyboard: type=mf, serial_delay=250, paste_delay=100000, user_shortcut=none
mouse: type=ps2, enabled=0, toggle=ctrl+mbutton
pci: enabled=1, chipset=i440fx
clock: sync=none, time0=local, rtc_sync=0
private_colormap: enabled=0

#软盘启动的配置,kernel.img为软盘名
floppya: type=1_44, 1_44=kernel.img, status=inserted, write_protected=0

ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata1: enabled=0, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata2: enabled=0, ioaddr1=0x1e8, ioaddr2=0x3e0, irq=11
ata3: enabled=0, ioaddr1=0x168, ioaddr2=0x360, irq=9

# 硬盘启动配置,kernel.img为硬盘名,根据硬盘大小不同需改变后三个参数的值
#ata0-master: type=disk, mode=flat, path=kernel.img, cylinders=130, heads=16, spt=63
#ata0-slave: type=none
#ata0-master: type=none
#ata1-slave: type=none

# 软盘启动/硬盘启动
boot: floppy
# boot: disk

floppy_bootsig_check: disabled=0
log: -
logprefix: %t%e%d
panic: action=ask
error: action=report
info: action=report
debug: action=ignore
debugger_log: -
com1: enabled=1, mode=null
com2: enabled=0
com3: enabled=0
com4: enabled=0
parport1: enabled=1, file=none
parport2: enabled=0
speaker: enabled=1, mode=system
magic_break: enabled=0
print_timestamps: enabled=0
port_e9_hack: enabled=0
megs: 2048

好了,开始仿真调试吧!

bochs -qf bochsrc

输入以上命令会出现两个界面,如下所示:

bochs gui调试界面
bochs终端界面

第一个界面会显示操作系统的汇编代码,当前的寄存器数值,可以查看内存内容,输入指令控制程序运行。第二个程序是操作系统运行时用于显示的界面。

在第一个界面中分别输入以下两个指令:

b 0x7c00
c

第一个指令用于在0x7c00地址打断点,第二个指令让程序继续运行,程序会在断点处停止。界面会变成如下所示:

0x7c00地址的界面

这是我们编写的代码。我们可以看到,程序被加载到了0x7c00地址处,那为什么是在这个地址呢?这是约定俗成的,电脑开机,会将磁盘的前512字节内容拷贝到0x7c00地址,如果这512字节不是以0x55和0xaa结束,会报错说找不到启动盘。

这一节的内容就到此为止了,下一节让我来打印Hello World吧。

2.打印Hello World

学习编程必不可少的是什么?是语法或数据结构吗?不,是Hello World!

我们对bootsect.s进行修改。

start:
	mov ax, 0x07c0
	mov es, ax
	mov dx, 0		; 光标位置为(0,0)
    mov cx, 16		; 写16个字符
    mov bx, 0x0007	; 页面0,颜色模式7
    mov bp, msg		; 字符串地址
    mov ax, 0x1301	; 写字符串,光标随之移动
    int 0x10		; 进入BIOS中断

	jmp $			; 死循环

msg:				; 要打印的字符串
    db 13, 10
    db "Hello World!"
    db 13, 10

	times 0x1fe - ($ - $$) db 0	; 填写0,直到0x1fe
	dw 0xaa55		; 启动盘标识

这段程序打印Hello World!后进入死循环。虽然我把程序执行结果告诉你了,但你还是想知道第2-8行是什么意思对不对?推荐下载BIOS接口技术参考手册,这是我的资源,不收C币,免费下载。查阅手册,找到int 0x10下的如下内容:

int_0x10_ah_0x13

虽然是英文的,但也不难。汇编程序中改变了ax,bx,cx,dx,es,bp等寄存器的值,改变这些寄存器的目的都在上图中指出。

最后得到的结果如图所示。

打印Hello World的结果

有没有觉得每次在终端里敲命令很繁琐?就不能一条命令就编译内核然后启动仿真器吗?当然可以!现在轮到脚本和Makefile出场了。

使用bximage制作软盘虽然只需要敲几下键盘就可以了,但本着能偷懒就偷懒的精神,我用脚本(在Makefile中出错了)实现了制作软盘的步骤,命名为mkimg.sh。

#!/bin/bash
RED_COLOR='\E[41m'
BLUE_COLOR='\E[44m'
RESET='\E[0m'
echo -e "${BLUE_COLOR}=== env check ===${RESET}"
if [ ! -e bochsrc ];then
    echo -e "${RED_COLOR}=== no bochsrc ===${RESET}"
    exit 1
fi

if [ ! -e /usr/local/bin/bochs ];then
    echo "${RED_COLOR}=== no bochs ===${RESET}"
    exit 1
fi

if [ ! -e /usr/local/bin/bximage ];then
    echo "${RED_COLOR}=== no bximage ===${RESET}"
    exit 1
fi

if [ -e kernel.img ]; then
    rm kernel.img
fi

echo -e "1\nfd\n\nkernel.img\n" | bximage

我在这里面添加了不少提示性的信息,最后一句才是精华,好好品味吧。

Makefile要完成所有的工作,之后,一条make指令就可以开始仿真调试了。

default: all

all: Image

Image: mkimg boot/bootsect.bin
	dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
	bochs -qf bochsrc

boot/bootsect.bin: boot/bootsect.s
	nasm boot/bootsect.s -o boot/bootsect.bin

mkimg:
	./mkimg.sh

clean:
	rm -rf boot/*.bin kernel.img

这个Makefile并不难理解(每次有人说**不难理解,我都想打人,我终究活成了自己讨厌的摸样:-),不做过多赘述。

这节内容到此结束,下节会详细讲讲启动盘的任务。

3.boot和loader

bootloader的作用是什么?它主要完成加载内核以及系统初始化,把加载内核的工作称为boot,把系统初始化称为loader。bootsect.s完成boot部分,setup.s完成loader部分。

系统一开始只会将bootsect.s的内容,而setup.s的内容就需要让bootsect.s加载到内存中。我们首先将bootsect.s的内容从0x7c00移动到0x90000,再将setup.s的内容加载到0x90200。前面0x00000~0x90000的空间之后会用来存放内核。这里我们假设setup.s的内容会占用4个扇区。

下面会小小地改动bootsect.s的内容。

SETUPLEN    equ 4
BOOTSEG     equ 0x07c0
INITSEG     equ 0x9000
SETUPSEG    equ 0x9020

start:
    mov ax, BOOTSEG
    mov ds, ax
    mov ax, INITSEG
    mov es, ax
    mov cx, 256
    sub si, si
    sub di, di
    rep
    movsw			; 将bootsect.s从0x7c00移动到0x90000
    jmp INITSEG:go
go: mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0xff00

load_setup:
    mov dx, 0x00
    mov cx, 0x02
    mov bx, 0x0200
    mov ax, 0x0200 + SETUPLEN
    int 0x13		; 加载setup.s到0x90200
    jnc ok_load_setup
    mov dx, 0x00
    mov ax, 0x00
    int 0x13
    jmp load_setup	; 加载失败则复位软盘,重新加载

ok_load_setup:
    mov ah, 0x03
    xor bh, bh
    int 0x10		; 获取光标位置

    mov cx, 24
    mov bx, 0x0007
    mov bp, msg
    mov ax, 0x1301
    int 0x10		; 打印字符串

    jmp SETUPSEG:0	; 跳转到setup.s的内容

msg:
    db 13, 10
    db "Loading system ..."
    db 13, 10, 13, 10

    times 0x1fe - ($ - $$) db 0 ; 填写0,直到0x1fe
    dw 0xaa55       ; 启动盘标识

这个内容不算多吧。。。前4行定义了4个宏定义,方便以后修改或维护。第7~15行将移动bootsect.s的内容,rep movsw会将[ds:si]地址的两个字节移动到[es:di]处,每次移动si和di都加1,一共移动cx次。然后是跳转到移动后的地址。

跳转后要重新设置段寄存器和sp,这些寄存器在之后都会用到,sp的值只要远大于512即可。

做完这些就开始加载setup.s的内容,setup.s的内容保存在第2~5扇区内容。查BIOS接口技术参考手册的int 0x13可以知道:

int_0x13_ah_2

dh=0,dl=0,ch=0,cl=2代表第2扇区,al=4代表读取4扇区,将内容读到es:bx(0x9000:0x200)地址处。读取成功,CF=0,跳转到ok_load_setup;读取失败,CF=1,复位软盘,重新读取扇区。

下面是读取光标位置到dx寄存器中。在第2节中,打印Hello World的地方原本有字符,不大好看。读取光标位置后打印字符串会打印在空白地区,更好看一点。

打印字符串的程序之前已经说过,就不多说了。打印Loading system …之后就会跳转到setup.s的内容中。

这次把setup.s写简单一点,还是一个死循环。

start:
    jmp start

弄完bootsect.s和setup.s后,就开始更改Makefile。

default: all

all: Image

Image: mkimg boot/bootsect.bin boot/setup.bin
	dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
	dd if=boot/setup.bin of=kernel.img bs=512 count=4 seek=1 conv=notrunc
	bochs -qf bochsrc

boot/bootsect.bin: boot/bootsect.s
	nasm boot/bootsect.s -o boot/bootsect.bin

boot/setup.bin: boot/setup.s
	nasm boot/setup.s -o boot/setup.bin

mkimg:
	./mkimg.sh

clean:
	rm -rf boot/*.bin kernel.img

dd指令的使用可以在linux中使用dd --help进行查看,其余就没什么好讲的了。

那就开始仿真调试吧。

make完运行了jmp INITSEG:go指令之后,地址发生了变化。

jmp_0x90018

我们可以一直使用n命令一步一步运行。

Loading_system

这次打印的字符串明显要更好一些。

Refresh

咦?说好的setup.s的内容呢?别慌,点击上面的Refresh键。

Refresh之后

这次可以看到死循环了。

这一节的内容结束了,下一节会开始完善setup.s,bootsect.s的内容会在开始写内核的时候再完善。

4.完善setup.s

setup.s要本要完成移动内核到指定位置的任务,但现在还没有写内核,就只完成初始化的功能就可以了。

目前,CPU还处于16位模式,能够使用的寄存器也都是16位的,而我们接下来要进入32位保护模式,setup.s会为进入32位做准备。(我们都知道32位比16位好,那这是为什么呢?)

要进入保护模式需要做什么呢?

  • 初始化GDT描述符,加载gdtr
  • 打开A20地址线
  • 设置cr0寄存器的PE位为1,使之运行于保护模式

最后还需要跳转到保护模式的地址,但由于没写内核代码,这步将由死循环代替(老惯例了)。

以下是修改后的setup.s的代码:

INITSEG     equ 0x9000
SYSSEG      equ 0x1000
SETUPSEG    equ 0x9020
	
	cli		; 保护模式下中断机制尚未建立,应禁止中断
	
start:    
    mov ax, SETUPSEG
    mov ds, ax
    lgdt    [gdt_48]

    mov al, 2
    out 0x92, al

    mov ax, 0x0001
    lmsw    ax
    jmp $

gdt:
    dw  0, 0, 0, 0
    dw  0x07ff, 0x0000, 0x9a00, 0x00c0
    dw  0x07ff, 0x0000, 0x9200, 0x00c0

gdt_48:
    dw  0x800
    dw  512 + gdt, 0x9

gdt包含操作系统内存分段管理的相关知识,在setup.s中只是临时设置gdt,在内核中会重新设置gdt。gdt的结构如下所示。如果对gdt不感兴趣就跳过下面的图表吧:-)

gdt结构图

请添加图片描述

gdt各字段解释

gdt各字段意义
基地址段在内存中的起始地址
段界限段长度=(段界限+1)*段界限单位(段界限单位与下方的G字段有关)
G粒度位,用于解释段界限的含义。G=0,段界限以字节为单位,段的扩展范围为1B到1MB(描述符的界限值为20位);G=1,段界限以4KB为单位,段的扩展范围为4KB到4GB
D/B默认的操作数大小。主要是为了能够在32位处理器上兼容运行16位保护模式的程序。D/B=0,表示指令中的偏移地址或者操作数是16位的;D/B=1,表示指令中的偏移地址或者操作数是32位的。
L64位代码段标志,保留此位给64位处理器使用,目前将此位置0即可。
AVL软件可利用位。80386对该位未作规定,且与80386兼容的处理器都不会对该位的使用做任何规定。
P段存在位,用于表示描述符所对应的段是否存在。P=0,表示段不在内存中。P=1,表示段在内存中。
DPL表示描述符的特权级。处理器支持的特权级别有4种,分别是0,1,2,3,其中0是最高特权级别。
S用于指定描述符的类型。S=0,表示这是一个系统段;S=1,表示这是一个代码段或数据段(堆栈段也是特殊的数据段)。
TYPE由于介绍过长,放在下面解释

gdt中TYPE字段的介绍

数据段代码段
XEWA含义XCRA含义
000X只读100X只执行
001X读和写101X读和可执行
010X只读,向下扩展110X只执行,依从的代码段
011X读写,向下扩展111X可执行,读,依从的代码段

上表中第2行各字母的含义

字母含义
X表示是否可执行。X=0,表示不可执行,数据段总是不可执行的;X=1,表示可执行,代码段总是可执行的。
E段的扩展方向。E=0,向上扩展,即向高地址方向扩展,是普通的数据段;E=1,向下扩展,即向低地址方向扩展,通常是堆栈段。
W段的写属性。W=0,不允许写入,此时写入的话会引发异常中断;W=1,允许写入。
A已访问位。用于表示它指向的段最近是否被访问过。在描述符创建的时候应该清0。之后每当该段被访问时,处理器将该位置1。
C段特权级依从。C=0,表示非依从的代码段,可以与特权级相同的代码段调用,或者通过门调用;C=1,表示运行从低特权级的程序转移到该段执行。
R段是否允许读出。R=0,表示代码段不能读出,此时读出会引发处理器异常中断;R=1,表示代码段可以读出。

通过上面的图表,我们可以知道代码第18-20行都干了什么。每个gdt占8个字节,所以每一行就是一个gdt。第一个gdt内容必须是0,对应于第18行代码。第二个gdt代表系统代码段,这8个字符的意思是:该段在内存的起始地址是0,段界限为0x7FF,段界限以4KB为单位,段长度为8MB,该段指令中的偏移地址或者操作数是32位的,段在内存中,特权级为0,这是一个代码段或数据段,该段可读可执行。这么介绍确实有些繁琐,不过应该挺易懂的。第三个gdt代表系统数据段,相关内容还请各位自行观看。

第8行代码是加载gdt的地址及长度到gdtr中。LGDT指令是将源操作数中的值加载到全局描述符表格寄存器(GDTR)。源操作数指定6字节内存位置,它包括了GDT的基址和界限。如果操作数大小属性是32位,则将16位限制(操作数的2个低位字节)与32位基址(操作数的4个高位字节)加载到寄存器。第24行的基址也可表示为0x90200+gdt或0x9020:gdt。

第10-11行是为了开启A20,这样就可以访问1MB以上的内容地址了。关于A20的更多知识请自行百度。开启A20的方法有几种,这里使用的是最简单的方法,与linux0.11的代码并不相同。

第13-14行是为了设置cr0寄存器的PE位为1(PE位位于cr0寄存器的bit0)。这段代码与如下代码的意思相同。

	mov ax, cr0
	or	ax, 1
	mov cr0, ax

下面来运行一下代码吧。

0x90200

这好像没什么可以展示的啊。在运行了lgdt和lmsw指令之后可以看看gdtr和cr0寄存器的变化。

进入保护模式的准备已经做好了,下面就可以写内核,但是写完内核就需要更新bootsect.s和setup.s的内容,好复杂啊。。。

这章内容有点多,感觉应该写得更细一点,但写多了又感觉会变得很臃肿,就这样了,下章的内容也不少啊。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值