操作系统的引导
# 1. 操作系统的引导
# 实验介绍
此次实验的基本内容是:
- 阅读《Linux内核完全注释》的第6章,对计算机和Linux 0.11的引导过程进行初步的了解;
- 按照下面的要求改写0.11的引导程序bootsect.s
- 有兴趣同学可以做做进入保护模式前的设置程序setup.s。
在实验报告中回答如下问题:
- 有时,继承传统意味着别手蹩脚。x86计算机为了向下兼容,导致启动过程比较复杂。请找出x86计算机启动过程中,被硬件强制,软件必须遵守的两个“多此一举”的步骤(多找几个也无妨),说说它们为什么多此一举,并设计更简洁的替代方案。 评分标准
- bootsect显示正确,30%
- bootsect正确读入setup,10%
- setup获取硬件参数正确,20%
- setup正确显示硬件参数,20%
- 实验报告,20%
# 改写bootsect.s
bootsect.s能在屏幕上打印一段提示信息“XXX is booting...”,其中XXX是你给自己的操作系统起的名字,例如LZJos、Sunix等(可以上论坛上秀秀谁的OS名字最帅,也可以显示一个特色logo,以表示自己操作系统的与众不同。)
# 简单版v1.0
简单的话,我们直接改版原本的bootsect.s 的字符串和字符串个数即可,步骤如下:
cd ~/oslab/linux-0.11/boot
vim bootsect.s
修改msg1的内容,同时修改cx的值
修改246的字符串为 "POS is runing"
第98行 mov cx,#23
然后保存并退出vim
重新编译
as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o
dd bs=1 if=bootsect of=Image skip=32
我们可以写个shell脚本,这样以后每次编译的时候,就不用写上述命令了,我这里新建一个lab1shell.sh,里面放上面3条命令
生成内核
cd ~/oslab/linux-0.11
make all
可以看到Image的时间已经是被更新为你编译的时候的时间
$ ll
-rw-rw-r-- 1 peterjxl peterjxl 128161 10月 28 07:54 Image
运行Bochs
cd ~/oslab
./run
实验截图
# 不太简单版v2.0
接下来我们重写bootsect.s ,只完成关键的功能即可
首先完成屏幕输出功能:屏幕输出的功能主要是用10号中断
! 首先读入光标位置
mov ah, #0x03
xor bh, bh
int 0x10
输入参数:bh是页号,这里置0
返回参数:CH=光标起始位置,CL=光标结束位置,DH=光标行号(0-based),DL=光标列号(0-based)
然后定义字符串:
msg1: .byte 13, 10
.ascii "Hello OS World, my name is JXL"
.byte 13, 10, 13, 10
.org 510
boot_flag: .word 0xAA55
.byte 13,10就是定义个回车和换行,第4行就是两个回车和换行
.org 510 伪指令,表示在它之后的指令从地址510开始存放。遇到.org,编译器会把其后的指令代码放到org伪指令指定的偏移地址。如org指定的地址和之前的指令地址有空洞,则用0填充。
boot_flag: .word 0xAA55:是启动盘具有有效引导扇区的标志。仅供BI0S中的程序加载引导扇区时识别使用。它必须位于引导扇区的最后两个字节中。
然后是显示字符串
! 显示字符串
mov cx, #36
mov bx, #0x0007
mov ax, #0x7c00
mov es, ax
mov bp, #msg1
mov ax, #0x1301
int 0x10
调用参数:cx是字符串长度,BH=页号,BL=显示属性,,es:bp是字符串的地址,ax是功能号 显示字符串光标跟随移动
显示完字符串后,我们不再往下执行(例如载入setup.s 和把自己挪到0x9000处),我们这里可以写个简单的死循环
inf_loop:
jmp inf_loop
完整代码如下:
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#36
mov bx,#0x0007
mov bp,#msg1
mov ax,#0x07c0
mov es,ax
mov ax,#0x1301
int 0x10
inf_loop:
jmp inf_loop
msg1: .byte 13,10
.ascii "Hello OS world, my name is LZJ"
.byte 13,10,13,10
.org 510
boot_flag: .word 0xAA55
我们重新编译和运行,实验结果如下
运行后,就不会继续往下有输出了,因为是死循环。
# 2. 改写setup.s
改写setup.s主要完成如下功能:
- bootsect.s能完成setup.s的载入,并跳转到setup.s开始地址执行。而setup.s向屏幕输出一行"Now we are in SETUP"。
- setup.s能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
- setup.s不再加载Linux内核,保持上述信息显示在屏幕上即可。
# 加载setup.s版 v1.0
同样是显示字符串,因此可以直接复用 bootsect.s的2.0版本,改改显示的字符串即可。我们可以直接删掉setup.s,然后cp一份bootsect.s
$ cat bootsect.s
$ cp bootsect.s setup.s
然后有4个地方要修改:
完整代码:
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#25
mov bx,#0x0007
mov bp,#msg2
mov ax,cs
mov es,ax
mov ax,#0x1301
int 0x10
inf_loop:
jmp inf_loop
msg2: .byte 13,10
.ascii "Now we are in SETUP"
.byte 13,10,13,10
.org 510
boot_flag: .word 0xAA55
修改bootsect.s 加载setup模块 v3.0,我们可以参考原版的bootsect.s的模块,我们只需要将其copy到我们自己的bootsect.s即可……
load_setup:
! 设置驱动器和磁头(drive 0, head 0): 软盘 0 磁头
mov dx,#0x0000
! 设置扇区号和磁道(sector 2, track 0): 0 磁头、0 磁道、2 扇区
mov cx,#0x0002
! 设置读入的内存地址:BOOTSEG+address = 512,偏移512字节
mov bx,#0x0200
! 设置读入的扇区个数(service 2, nr of sectors),
! SETUPLEN是读入的扇区个数,Linux 0.11 设置的是 4,
! 我们不需要那么多,我们设置为 2(因此还需要添加变量 SETUPLEN=2)
mov ax,#0x0200+SETUPLEN
! 应用 0x13 号 BIOS 中断读入 2 个 setup.s扇区
int 0x13
! 读入成功,跳转到 ok_load_setup: ok - continue
jnc ok_load_setup
! 软驱、软盘有问题才会执行到这里。我们的镜像文件比它们可靠多了
mov dx,#0x0000
! 否则复位软驱 reset the diskette
mov ax,#0x0000
int 0x13
! 重新循环,再次尝试读取
jmp load_setup
ok_load_setup:
! 接下来要干什么?当然是跳到 setup 执行。
! 要注意:我们没有将 bootsect 移到 0x9000,因此跳转后的段地址应该是 0x7ce0
! 即我们要设置 SETUPSEG=0x07e0
完整的bootsect.s代码如下:
SETUPLEN=2
SETUPSEG=0x07e0
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#36
mov bx,#0x0007
mov bp,#msg1
mov ax,#0x07c0
mov es,ax
mov ax,#0x1301
int 0x10
load_setup:
mov dx, #0x0000 ; 设置驱动器和磁头(drive 0, head 0): 软盘 0 磁头
mov cx, #0x0002 ; 设置扇区号和磁道(sector 2, track 0): 0 磁头、0 磁道、2 扇区
mov bx, #0x0200 ; 设置读入的内存地址:BOOTSEG+address = 512,偏移512字节
mov ax, #0x0200 + SETUPLEN; 设置读入的扇区个数(service 2, nr of sectors),SETUPLEN是读入的扇区个数,Linux 0.11 设置的是 4,我们不需要那么多,我们设置为 2(因此还需要添加变量 SETUPLEN=2)
int 0x13 ; 应用 0x13 号 BIOS 中断读入 2 个 setup.s扇区
jnc ok_load_setup ; 读入成功,跳转到 ok_load_setup: ok - continue
mov dx, #0x0000 ; 软驱、软盘有问题才会执行到这里。我们的镜像文件比它们可靠多了
mov ax, #0x0000 ; 复位软驱 reset the diskette
int 0x13 ; 继续尝试加载setup.s模块
jmp load_setup
; 接下来要干什么?当然是跳到 setup 执行。要注意:我们没有将 bootsect 移到 0x9000,因此跳转后的段地址应该是 0x7ce0, 即我们要设置 SETUPSEG=0x07e0
ok_load_setup:
jmpi 0, SETUPSEG
msg1: .byte 13,10
.ascii "Hello OS world, my name is JXL"
.byte 13,10,13,10
.org 510
boot_flag: .word 0xAA55
然后我们再次编译,setup.s和bootsect.s都要编译和链接,效率较低,类似我只之前讲到的写个shell脚本来编译,这次我们不用自己写,用Makefile里自带的即可
$ cd ~/oslab/linux-0.11/
$ make BootImage
as86 -0 -a -o boot/bootsect.o boot/bootsect.s
ld86 -0 -s -o boot/bootsect boot/bootsect.o
as86 -0 -a -o boot/setup.o boot/setup.s
ld86 -0 -s -o boot/setup boot/setup.o
tools/build boot/bootsect boot/setup none > Image
Root device is (3, 1)
Boot sector 512 bytes.
Setup is 512 bytes.
Unable to open 'system'
make: *** [Makefile:54:BootImage] 错误 1
有 Error!这是因为 make 根据 Makefile 的指引执行了 tools/build.c,它是为生成整个内核的镜像文件而设计的,没考虑我们只需要 bootsect.s 和 setup.s 的情况。它在向我们要 “系统” 的核心代码。为完成实验,接下来给它打个小补丁。
我们可以简单的看看 linux-0.11/Makefile的内容:
all: Image
Image: boot/bootsect boot/setup tools/system tools/build
cp -f tools/system system.tmp
strip system.tmp
objcopy -O binary -R .note -R .comment system.tmp tools/kernel
tools/build boot/bootsect boot/setup tools/kernel $(ROOT_DEV) > Image
rm system.tmp
rm tools/kernel -f
sync
BootImage: boot/bootsect boot/setup tools/build
tools/build boot/bootsect boot/setup none $(ROOT_DEV) > Image
sync
build.c 从命令行参数得到 bootsect、setup 和 system 内核的文件名,将三者做简单的整理后一起写入 Image。其中 system 是第三个参数(argv[3])。当 “make all” 的时候,这个参数传过来的是正确的文件名,build.c 会打开它,将内容写入 Image。
而 “make BootImage” 时,传过来的是字符串 "none"。所以,改造 build.c 的思路就是当 argv[3] 是"none"的时候,只写 bootsect 和 setup,忽略所有与 system 有关的工作,或者在该写 system 的位置都写上 “0”。
修改工作主要集中在 build.c 的尾部,可以参考下面的方式,将圈起来的部分注释掉(也可以删掉)。

当按照前一节所讲的编译方法编译成功后再 run,实验结果如下图
$ cd ~/oslab/linux-0.11
$ make BootImage
$ ../run
# 获取硬件参数版 v2.0
在理论课里我们讲到过,setup.s 将获得硬件参数放在内存的 0x90000 处。
用 ah=#0x03 调用 0x10 中断可以读出光标的位置,用 ah=#0x88 调用 0x15 中断可以读出内存的大小。有些硬件参数的获取要稍微复杂一些,如磁盘参数表。在 PC 机中 BIOS 设定的中断向量表中 int 0x41 的中断向量位置(4*0x41 = 0x0000:0x0104)存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。第二个硬盘的基本参数表入口地址存于 int 0x46 中断向量位置处。每个硬盘参数表有 16 个字节大小。下表给出了硬盘基本参数表的内容:
表 1 磁盘基本参数表
| 位移 | 大小 | 说明 |
|---|---|---|
| 0x00 | 字 | 柱面数 |
| 0x02 | 字节 | 磁头数 |
| ... | ... | ... |
| 0x0E | 字节 | 每磁道扇区数 |
| 0x0F | 字节 | 保留 |
所以获得磁盘参数的方法就是复制数据。
下面是将硬件参数取出来放在内存 0x90000 的关键代码。
mov ax,#INITSEG
! 设置 ds = 0x9000
mov ds,ax
mov ah,#0x03
! 读入光标位置
xor bh,bh
! 调用 0x10 中断
int 0x10
! 将光标位置写入 0x90000.
mov [0],dx
! 读入内存大小位置
mov ah,#0x88
int 0x15
mov [2],ax
! 从 0x41 处拷贝 16 个字节(磁盘参数表)
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0004
mov cx,#0x10
! 重复16次
rep
movsb
显示获得的参数
现在已经将硬件参数(只包括光标位置、内存大小和硬盘参数,其他硬件参数取出的方法基本相同,此处略去)取出来放在了 0x90000 处,接下来的工作是将这些参数显示在屏幕上。这些参数都是一些无符号整数,所以需要做的主要工作是用汇编程序在屏幕上将这些整数显示出来。
以十六进制方式显示比较简单。这是因为十六进制与二进制有很好的对应关系(每 4 位二进制数和 1 位十六进制数存在一一对应关系),显示时只需将原二进制数每 4 位划成一组,按组求对应的 ASCII 码送显示器即可。ASCII 码与十六进制数字的对应关系为:0x30 ~ 0x39 对应数字 0 ~ 9,0x41 ~ 0x46 对应数字 a ~ f。从数字 9 到 a,其 ASCII 码间隔了 7h,这一点在转换时要特别注意。为使一个十六进制数能按高位到低位依次显示,实际编程中,需对 bx 中的数每次循环左移一组(4 位二进制),然后屏蔽掉当前高 12 位,对当前余下的 4 位(即 1 位十六进制数)求其 ASCII 码,要判断它是 0 ~ 9 还是 a ~ f,是前者则加 0x30 得对应的 ASCII 码,后者则要加 0x37 才行,最后送显示器输出。以上步骤重复 4 次,就可以完成 bx 中数以 4 位十六进制的形式显示出来。
下面是完成显示 16 进制数的汇编语言程序的关键代码,其中用到的 BIOS 中断为 INT 0x10,功能号 0x0E(显示一个字符),即 AH=0x0E,AL=要显示字符的 ASCII 码。
! 以 16 进制方式打印栈顶的16位数
print_hex:
! 4 个十六进制数字
mov cx,#4
! 将(bp)所指的值放入 dx 中,如果 bp 是指向栈顶的话
mov dx,(bp)
print_digit:
! 循环以使低 4 比特用上 !! 取 dx 的高 4 比特移到低 4 比特处。
rol dx,#4
! ah = 请求的功能值,al = 半字节(4 个比特)掩码。
mov ax,#0xe0f
! 取 dl 的低 4 比特值。
and al,dl
! 给 al 数字加上十六进制 0x30
add al,#0x30
cmp al,#0x3a
! 是一个不大于十的数字
jl outp
! 是a~f,要多加 7
add al,#0x07
outp:
int 0x10
loop print_digit
ret
! 这里用到了一个 loop 指令;
! 每次执行 loop 指令,cx 减 1,然后判断 cx 是否等于 0。
! 如果不为 0 则转移到 loop 指令后的标号处,实现循环;
! 如果为0顺序执行。
!
! 另外还有一个非常相似的指令:rep 指令,
! 每次执行 rep 指令,cx 减 1,然后判断 cx 是否等于 0。
! 如果不为 0 则继续执行 rep 指令后的串操作指令,直到 cx 为 0,实现重复。
! 打印回车换行
print_nl:
! CR
mov ax,#0xe0d
int 0x10
! LF
mov al,#0xa
int 0x10
ret
只要在适当的位置调用 print_bx 和 print_nl(注意,一定要设置好栈,才能进行函数调用)就能将获得硬件参数打印到屏幕上,完成此次实验的任务。但事情往往并不总是顺利的,前面的两个实验大多数实验者可能一次就编译调试通过了(这里要提醒大家:编写操作系统的代码一定要认真,因为要调试操作系统并不是一件很方便的事)。但在这个实验中会出现运行结果不对的情况(为什么呢?因为我们给的代码并不是 100% 好用的)。所以接下来要复习一下汇编,并阅读《Bochs 使用手册》,学学在 Bochs 中如何调试操作系统代码。
我想经过漫长而痛苦的调试后,大家一定能兴奋地得到下面的运行结果:
图 4 用可以打印硬件参数的 setup.s 进行引导的结果Memory Size 是 0x3C00KB,算一算刚好是 15MB(扩展内存),加上 1MB 正好是 16MB,看看 Bochs 配置文件 bochs/bochsrc.bxrc:
!……
megs: 16
!……
ata0-master: type=disk, mode=flat, cylinders=410, heads=16, spt=38
!……
这些都和上面打出的参数吻合,表示此次实验是成功的。
实验楼的环境中参数可能跟上面给出的不一致。大家需要根据自己环境中
bochs/bochsrc.bxrc 文件中的内容才能确定具体的输出信息。
下面是提供的参考代码,大家可以根据这个来进行编写代码:
INITSEG = 0x9000
entry _start
_start:
! Print "NOW we are in SETUP"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#25
mov bx,#0x0007
mov bp,#msg2
mov ax,cs
mov es,ax
mov ax,#0x1301
int 0x10
mov ax,cs
mov es,ax
! init ss:sp
mov ax,#INITSEG
mov ss,ax
mov sp,#0xFF00
! Get Params
mov ax,#INITSEG
mov ds,ax
mov ah,#0x03
xor bh,bh
int 0x10
mov [0],dx
mov ah,#0x88
int 0x15
mov [2],ax
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0004
mov cx,#0x10
rep
movsb
! Be Ready to Print
mov ax,cs
mov es,ax
mov ax,#INITSEG
mov ds,ax
! Cursor Position
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#18
mov bx,#0x0007
mov bp,#msg_cursor
mov ax,#0x1301
int 0x10
mov dx,[0]
call print_hex
! Memory Size
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#14
mov bx,#0x0007
mov bp,#msg_memory
mov ax,#0x1301
int 0x10
mov dx,[2]
call print_hex
! Add KB
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#2
mov bx,#0x0007
mov bp,#msg_kb
mov ax,#0x1301
int 0x10
! Cyles
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#7
mov bx,#0x0007
mov bp,#msg_cyles
mov ax,#0x1301
int 0x10
mov dx,[4]
call print_hex
! Heads
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#8
mov bx,#0x0007
mov bp,#msg_heads
mov ax,#0x1301
int 0x10
mov dx,[6]
call print_hex
! Secotrs
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#10
mov bx,#0x0007
mov bp,#msg_sectors
mov ax,#0x1301
int 0x10
mov dx,[12]
call print_hex
inf_loop:
jmp inf_loop
print_hex:
mov cx,#4
print_digit:
rol dx,#4
mov ax,#0xe0f
and al,dl
add al,#0x30
cmp al,#0x3a
jl outp
add al,#0x07
outp:
int 0x10
loop print_digit
ret
print_nl:
mov ax,#0xe0d ! CR
int 0x10
mov al,#0xa ! LF
int 0x10
ret
msg2:
.byte 13,10
.ascii "NOW we are in SETUP"
.byte 13,10,13,10
msg_cursor:
.byte 13,10
.ascii "Cursor position:"
msg_memory:
.byte 13,10
.ascii "Memory Size:"
msg_cyles:
.byte 13,10
.ascii "Cyls:"
msg_heads:
.byte 13,10
.ascii "Heads:"
msg_sectors:
.byte 13,10
.ascii "Sectors:"
msg_kb:
.ascii "KB"
.org 510
boot_flag:
.word 0xAA55
# 扩展
你可以显示不同的字符串,例如修改显示的字符串的颜色等等,或者可以实现载入system模块等等,完全可以自己有想做的功能,就做着玩~
可以参考hoverwinter/HIT-OSLab: S - 哈工大《操作系统》实验 (opens new window),这里就实现了system模块的载入,并且实验手册也写的很棒
# 附录
# int 0x10显示服务
| AH | 功能 | 调用参数 | 返回参数 |
|---|---|---|---|
| 0x03 | 读光标位置和大小 | BH=页号(0-based) | CH=光标起始位置,CL=光标结束位置,DH=光标行号(0-based),DL=光标列号(0-based) |
| 0x0e | 显示字符(光标前移) | AL=字符 BL=前景色 | None |
| 0x0f | 获取当前显示模式 | None | AL=当前的显示模式,AH=屏幕宽度,以字符列,BH=当前页号(0-based) |
| 0x1300 | 显示字符串光标留在起始位置 | ES:BP=字符串地址,CX=字符串长度,BH=页号,BL=显示属性,DH,DL=显示字符串的起始行号和列号 | None |
| 0x1301 | 显示字符串光标跟随移动 | ES:BP=字符串地址,CX=字符串长度,BH=页号,BL=显示属性,DH,DL=显示字符串的起始行号和列号 | None |