本节将更新哈工大《操作系统》课程第二个 Lab 实验 操作系统的引导。按照实验书要求,介绍了非常详细的实验操作流程,并提供了超级无敌详细的代码注释。文末附完整 bootsect.s
和 setup.s
标准答案代码以及超详细注释。
实验目的:
- 熟悉 hit-oslab 实验环境;
- 建立对操作系统引导过程的深入认识;
- 掌握操作系统的基本开发过程;
- 能对操作系统代码进行简单的控制,揭开操作系统的神秘面纱。
实验任务:
1、bootsect.s 能在屏幕上打印一段提示信息“XXX is booting…”,其中 XXX 是你给自己的操作系统起的名字,例如 LZJos、Sunix 等
2、bootsect.s 能完成 setup.s 的载入,并跳转到 setup.s 开始地址执行。而 setup.s 向屏幕输出一行"Now we are in SETUP"。
3、setup.s 能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
实验工具准备:
文件名 | 介绍 |
---|---|
hit-操作系统实验指导书.pdf | 哈工大OS实验指导书 |
Linux内核完全注释(修正版v3.0).pdf | 赵博士对Linux v0.11 OS进行了详细全面的注释和说明 |
file1615.pdf | BIOS 涉及的中断数据手册 |
hit-oslab-linux-20110823.tar.gz | hit-oslab 实验环境 |
gcc-3.4-ubuntu.tar.gz | Linux v0.11 所使用的编译器 |
Bochs 汇编级调试指令 | bochs 基本调试指令大全 |
最全ASCII码对照表0-255 | 屏幕输出字符对照的 ASCII 码 |
x86_64 常用寄存器大全 | x86_64 常用寄存器大全 |
一、bootsect.s 的屏幕输出功能
需要实现:bootsect.s 能在屏幕上打印一段提示信息“XXX is booting…”,其中 XXX 是你给自己的操作系统起的名字,例如 LZJos、Sunix 等
参考赵博士《Linux 内核 0.11 完全注释(修正版 V3.0)》第六章,其中非常详细的解释了 Linux_v0.11 的启动引导过程。
就比如下面这张图,形象生动地概括了OS引动过程:
- 首先,80x86 架构CPU进入实模式(16位),从
0xfff0
开始自动执行代码,在地址0处初始化中断向量。 - 从系统软盘上将第一扇区
bootsect.s
程序读入物理地址0x07c00
处(共512 bytes),并赋给bootsect
控制权。 - 接下来
bootsect.s
将自身移动到0x90000
处,为了腾出内存空间,以便给操作系统内核使用。 - 将 setup.s 加载到内存
0x90200
处,即bootsect.s
512 bytes 之后,并赋给setup.s
控制权。 - 将
system
模块加载到0x10000
处。(==不能移动到0x00000处原因:==因为在执行是 setup.s 模块时,需要利用 ROM BIOS 的中断调用来获取机器的参数,不能把中断向量表、BIOS数据区给覆盖掉) - 将
system
模块移动到0x00000
处,进入保护模式(32位),并赋给system
控制权。
1、编写 bootsect.s
需要注意的是,我们要从头开始编写 bootsect.s,而不是在 Linux0.11 的源码上进行修改,这样子无法深刻理解 bootsect 引导过程。
解压 hit-oslab-linux-20110823.tar.gz
,并命名为 os_lab_Lab1
,在此源码基础上我们进一步完成实验。进入 os_lab_Lab1/linux-0.11/boot/
文件夹下的 bootsect.s
文件,我们清空文件内容,重头开始编写。
- 获取光标位置
entry _start
_start:
! 输出一些信息! 读入光标位置mov ah, #0x03xor bh, bh ! 显示页码设置int 0x10
其中,中断
int 10
、ah = 03H
功能以及参数bh
可以在文件 file1615.pdf 查到,如下图所示。
- 输出显示字符串。同理可以查表发现,0x10中断对应的
ah = 13
功能为向屏幕输出显示字符串,并且各个参数的功能也都有详细介绍,具体可以参照代码及注释。
BOOTSEG = 0x07c0; ! bootsect 程序开始的地址! 输出一些信息! 设置开机启动字符串mov cx, #26 ! 字符串长度(20字符+3个回车+3个换行)mov bx, #0x0007 ! 设置 显示页号 和 字符属性(BL表示显示颜色 01:蓝色,03:青色,07:普通白色)mov ax, #BOOTSEG ! 字符串 段地址mov es, ax ! 不能将立即数直接赋给段寄存器,ax 充当临时寄存器mov bp, #msg1 ! 字符串 偏移地址mov ax, #0x1301 ! ah = 13H -> 显示, al = 01H -> 设置文本模式int 0x10 ! 显示服务中断! 无限循环
inf_loop:jmp inf_loop! 显示字符串
msg1:.byte 13,10 ! 回车 + 换行.ascii "Joker is booting ...".byte 13,10,13,10.org 510 ! 将当前位置设置为内存地址 510
! 设置引导扇区标记.word 0xAA55 ! 指定引导扇区的最后两个字节,bootsect必须以它结尾,才能引导
需要注意的是:在结尾处我们需要使用
.org
指令 ,设置最后两个字节为bootsect
程序标识。
可能有不少读者不太了解 org
指令 ,在此处给出Unix操作系统指令大全:115个最常用的Linux命令行大全 - 知乎 (zhihu.com),大家遇到不懂的指令可以在该文中查到。
2、编译和运行
cd ~/my_space/OS_HIT/oslab_Lab1/linux-0.11/boot
as86 -0 -a -o bootsect.o bootsect.s // 用as86编译器,编译生成目标文件 bootsect.o
ld86 -0 -s -o bootsect bootsect.o // 用ld86链接器,将目标文件连接成可执行文件
直接在
linux-0.11
文件夹下用make all
也可以,因为Makefile里面定义了这两条语句了
编译后,通过 ls -l
指令可以查看生成文件信息。
需要留意的文件是 bootsect 的文件大小是 544 字节,而引导程序必须要正好占用一个磁盘扇区,即 512 个字节。造成多了 32 个字节的原因是 ld86 产生的是 Minix 可执行文件格式,这样的可执行文件处理文本段、数据段等部分以外,还包括一个 Minix 可执行文件头部。
- 因此,我们需要去掉这 32 个字节后,将生成的文件拷贝到 linux-0.11 目录下,并一定要命名为“Image”。
dd bs=1 if=bootsect of=Image skip=32 // 去掉前 32 个 bytes
cp ./Image ../Image // 移动到 linux-0.11 目录下
../../run // 运行
至此,实验任务一 就完成了,实现效果如下所示。
二、bootsect.s 导入 setup.s
需要实现:bootsect.s 能完成 setup.s 的载入,并跳转到 setup.s 开始地址执行。而 setup.s 向屏幕输出一行"Now we are in SETUP"。
1、编写 setup.s 屏幕输出
我们首先来编写 setup.s 向屏幕输出功能,同理需要重头开始编写 setup.s,与 bootsect.s
类似。
entry _start
_start:! 读入光标位置mov ah, #0x03xor bh, bhint 0x10! 设置开机启动字符串mov cx, #25 ! 字符串长度(19字符+3个回车+3个换行)mov bx, #0x0007 ! 设置 显示页号 和 字符属性mov ax, cs ! 使用cs值获取 段地址mov es, ax ! ax 充当临时寄存器mov bp, #msg2 ! 字符串 偏移地址mov ax, #0x1301 ! ah = 13H -> 显示, al = 01H -> 设置文本模式int 0x10! 无限循环
inf_loop:jmp inf_loop! 显示字符串
msg2:.byte 13,10 ! 回车 + 换行.ascii "Now we are in SETUP".byte 13,10,13,10
2、在 bootsect.s 中载入 setup.s
在 bootsects 中载入 setup.s ,需要用到中断 int 0x13
的 02H
号功能——从磁盘中载入数据到内存。
具体代码如下所示:(除了新增代码,我们还需要去掉在 bootsect.s
添加的无限循环)
SETUPSEG = 0x07e0; ! setup 程序开始的地址(没有移动到 0x9000 处)
SETUPLEN = 4; ! setup 占用扇区数量! 加载 setup.s 程序
load_setup:mov dx,#0x0000 ! 设置驱动器和磁头(drive 0, head 0)mov cx,#0x0002 ! 设置扇区号和磁道(sector 2, track 0)mov bx,#0x0200 ! 偏移地址为 512 bytesmov ax,#0x0200+SETUPLEN ! ah=02H:从磁盘读数据到内存;al=04H,读入四个扇区int 0x13 ! 低级磁盘服务中断jnc ok_load_setup ! 加载成功,跳转(无进位时跳转)!加载错误mov dx,#0x0000mov ax,#0x0000 ! 复位软驱int 0x13jmp load_setup ! 再次尝试ok_load_setup:jmpi 0,SETUPSEG ! 赋予 setup.s 控制权! 无限循环
! inf_loop:
! jmp inf_loop
3、编译和运行
为了加快编译速度,避免一个一个手动编译,我们将借助 makefile
来实现编译。我们将用到 linux-0.11/tools/build.c
文件来实现。注意:在使用 make BootImage
之前,我们需要修改一下 build.c
代码,因为我们还没有编写内核文件 kernel
,所以会出现报错。
- 注释掉
tool/build.c
中的部分代码:
// if ((id=open(argv[3],O_RDONLY,0))<0)
// die("Unable to open 'system'");
// // if (read(id,buf,GCC_HEADER) != GCC_HEADER)
// // die("Unable to read header of 'system'");
// // if (((long *) buf)[5] != 0)
// // die("Non-GCC header of 'system'");
// for (i=0 ; (c=read(id,buf,sizeof buf))>0 ; i+=c )
// if (write(1,buf,c)!=c)
// die("Write call failed");
// close(id);
// fprintf(stderr,"System is %d bytes.\n",i);
// if (i > SYS_SIZE*16)
// die("System is too big");return(0);
- 即可实现编译和运行
cd linux-0.11
make BootImage
../run
至此,实验任务二 就完成了,实现效果如下所示。
三、setup.s 获取硬件参数并输出显示
需要实现:setup.s 能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
1、获取硬件参数
获取硬件参数的代码大致都一样,我们将主要介绍 获取内存大小 和 获取磁盘参数表,其他参数获取参考文末代码。
- 要获取内存大小,我们需要用到中断
int 0x15
,调用ah = 0x88
功能实现。具体代码如下:
BOOTSEG = 0x07c0; ! bootsect 读入段地址
INITSEG = 0x9000; ! 初始数据段存放的位置
SETUPSEG = 0x07e0; ! setup 程序开始的地址(没有移动到 0x90200 处)! 设置段地址,将硬件参数取出来放在内存 0x90000mov ax, #INITSEGmov ds, ax ! 数据段地址 ds = 0x9000! 读光标位置,存入 dxmov ah, #0x03 ! AH = 3 -> 读光标位置xor bh, bh ! 显示页数 = 0int 0x10! 将光标位置写入 0x90000.mov [0], dx! 读入内存大小位置mov ah, #0x88 ! AH = 0x88 -> 读入内存大小int 0x15 ! 内存大存入 AXmov [2], ax ! 存入 9000 后面两个偏移
- 获取磁盘参数表,。在 PC 机中 BIOS 设定的中断向量表中 int 0x41 的中断向量位置
4*0x41 = 0x0000:0x0104
存放着第一个硬盘的基本参数表。第二个硬盘的基本参数表入口地址存于 int 0x46 中断向量位置处。每个硬盘参数表有 16 个字节大小。
! 获取磁盘参数表,从 0x41 处拷贝 16 个字节mov ax, #0x0000 ! 不允许直接将立即数加载到段寄存器,需要使用通用寄存器axmov ds, ax ! 数据段地址 ds = 0x0000lds si,[4*0x41] ! 取中断向量 41 的值,即 hd0 参数表的地址 ds:simov ax,INITSEGmov es,axmov di,#0x0080 ! 传输的目的地址: es:di = 9000:0080mov cx,#0x10 ! 重复执行次数 = 16rep ! 重复执行movsb, DS:SI -> ES:DImovsb ! 每执行一次 movsb 指令,源地址和目的地址的偏移都会自动递增
2、显示获得的参数
为了以 16进制 的形式显示获取的参数,我们需要编写显示函数。(ASCII码请参考:最全ASCII码对照表0-255)
以十六进制方式显示比较简单。这是因为十六进制与二进制有很好的对应关系(每 4 位二进制数和 1 位十六进制数存在一一对应关系),显示时只需将原二进制数每 4 位划成一组,按组求对应的 ASCII 码送显示器即可。分为两种情况
- 数字 0~9 : 对应 ASCII 码为 0x30~0x39,故显示时加上 0x30
- 数字 a~f :对应ASCII码为 0x41~0x46,故显示时在原先加 0x30 的基础上,还要添加 0x07
- 相关显示函数如下所示。将临时寄存器
ax
中存放的16位二进制数,通过4位十六进制的ASCII字符进行显示。其中使用循环左移rol
不断获取高位数据 -> 并通过and
操作保留第四位数据 -> 再统一加上 0x30 之后判断是否大于数字9,若大于再加上 0x07。
! 以 16 进制 打印寄存器 ax 中的16位数
print_hex:mov cx, #4 ! 循环次数smov dx,ax ! 将ax所指的值放入dx中,ax作为参数传递寄存器
print_digit:rol dx,#4 ! 将 DX 寄存器中的值向左循环移位 4 位,相当于将 最高的4位 移到 最低的4位。后面取出最低四位,打印的时候实现高位在前mov ax,#0x0e0f ! ah = 0eh -> 显示字符, al = 字符:半字节(4个比特)掩码。and al,dl ! 取 dl 的低4比特值。add al,#0x30 ! 给al数字加上十六进制 0x30, 0~9 + 0x30 = "0~9"cmp al,#0x3a ! 判断 al 是否大于 字符 9jl outp ! al < 数字"9"后面的字符,说明是 0~9,则跳转add al,#0x07 ! al > "9", 是a~f,要多加7h
outp:int 0x10 ! 打印字符存储在 al 中loop print_digit ! cx = 4,循环四次ret ! 函数返回指令,表示函数执行结束,返回到调用该函数的位置! 打印显示回车换行
print_nl:mov ax, #0x0e0dint 0x10mov al, #0x0aint 0x10ret
回车和换行的区别
回车(13):表示光标移动到当前行的开头,但不改变行号
换行(10):表示光标移动到下一行,但不会移动到行首
- 调用显示函数来实现硬件参数显示:
! 开始显示参数
! 前面修改了ds数据段寄存器,这里将其设置为0x9000mov ax,#INITSEG ! 设置数据段地址,后续显示硬件参数使用mov ds,ax ! 0x9000mov ax,#SETUPSEG ! 设置附加段地址,后续显示字符串使用mov es,ax ! 0x07e0! 显示 光标位置! 获取光标位置mov ah,#0x03 xor bh,bhint 0x10! 显示字符串mov bp, #cur ! 串 偏移地址mov cx, #11 ! 串 长度mov bx, #0x0003 ! 页号 = 0 + 颜色设置(03:青色,07:普通白色)mov ax, #0x1301 ! 显示字符串 + char...int 0x10! 显示数值mov ax, [0] ! 打印函数参数存入 axcall print_hex ! 调用打印寄存器函数call print_nl ! 打印回车换行! 显示 内存大小! 获取光标位置mov ah,#0x03 xor bh,bhint 0x10! 显示字符串提示mov bp, #memmov cx, #12mov bx, #0x0007 mov ax, #0x1301 ! 显示字符串 + char...int 0x10! 显示数值mov ax, [2]call print_hex! 显示 KBmov ah,#0x03 ! read cursor posxor bh,bhint 0x10mov cx,#6mov bx,#0x0007 ! page 0, attribute c mov bp,#cylmov ax,#0x1301 ! write string, move cursorint 0x10! 无限循环
inf_loop:jmp inf_loop! 显示字符串
msg2:.byte 13,10 ! 回车 + 换行.ascii "Now we are in SETUP".byte 13,10,13,10! 光标位置
cur:.ascii "Cursor POS:"! 内存大小
mem:.ascii "Memory SIZE:"! 提示信息
cyl:.ascii "KB".byte 13,10,13,10
至此,实验任务三 就完成了,实现效果如下所示。
至此,Lab2 实验介绍完毕,文末将附上 bootsect.s
和 setup.s
完整代码及详细注释。
实验 Lab2 代码与 Linux-0.11 代码的区别在于:
- Lab2 无需将 bootsect.s 从 0x07c00 移动到 0x90000,故 setup.s 存放在其后 0x07e00 段位置处,也没有移动到 0x90200
四、完整代码汇总
bootsect.s
! 声明了几个全局符号,用来标识程序的代码段、数据段和未初始化数据段的起始和结束位置
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text ! 接下来的指令是 代码段
begtext: ! 标识代码段的起始位置
.data ! 接下来的指令是 数据段
begdata: ! 标识数据段的起始位置
.bss ! 接下来的指令是 未初始化数据段
begbss: ! 标识未初始化数据段的起始位置
.text ! 接下来的指令是 代码段BOOTSEG = 0x07c0; ! bootsect 程序开始的地址
SETUPSEG = 0x07e0; ! setup 程序开始的地址(没有移动到 0x9000 处)
SETUPLEN = 4; ! setup 占用扇区数量! 指定程序的入口点 为 _start
entry _start
_start:! 从go标号处开始执行,并设置代码段 cs = BOOTSEGjmpi go,BOOTSEG
! 设置 ds=es=cs
go: mov ax,csmov ds,ax mov es,ax! 输出一些信息! 读入光标位置mov ah, #0x03xor bh, bh ! 显示页码设置int 0x10! 设置开机启动字符串mov cx, #26 ! 字符串长度(20字符+3个回车+3个换行)mov bx, #0x0007 ! 设置 显示页号 和 字符属性mov ax, #BOOTSEG ! 字符串 段地址mov es, ax ! 不能将立即数直接赋给段寄存器,ax 充当临时寄存器mov bp, #msg1 ! 字符串 偏移地址mov ax, #0x1301 ! ah = 13H -> 显示, al = 01H -> 设置文本模式int 0x10 ! 显示服务中断! 加载 setup.s 程序
load_setup:mov dx,#0x0000 ! 设置驱动器和磁头(drive 0, head 0)mov cx,#0x0002 ! 设置扇区号和磁道(sector 2, track 0)mov bx,#0x0200 ! 偏移地址为 512 bytesmov ax,#0x0200+SETUPLEN ! ah=02H:从磁盘读数据到内存;al=04H,读入四个扇区int 0x13 ! 低级磁盘服务中断jnc ok_load_setup ! 加载成功,跳转!加载错误mov dx,#0x0000mov ax,#0x0000 ! 复位软驱int 0x13jmp load_setup ! 再次尝试! 加载成功后,开始执行setup代码
ok_load_setup:jmpi 0,SETUPSEG! 显示字符串
msg1:.byte 13,10 ! 回车 + 换行.ascii "Joker is booting ...".byte 13,10,13,10! 将当前位置设置为内存地址 510 字节处
.org 510
! 设置引导扇区标记.word 0xAA55 ! 指定引导扇区的最后两个字节,bootsect必须以它结尾,才能引导.text
endtext: ! 标识代码段的结束位置
.data
enddata: ! 标识数据段的结束位置
.bss
endbss: ! 标识未初始化数据段的结束位置
setup.s
BOOTSEG = 0x07c0; ! bootsect 读入段地址
INITSEG = 0x9000; ! 初始数据段存放的位置
SETUPSEG = 0x07e0; ! setup 程序开始的地址(没有移动到 0x9000 处)! 指定程序的入口点 为 _start
entry _start
_start:! 从go标号处开始执行,并设置代码段 cs = SETUPSEGjmpi go,SETUPSEG
! 设置 ds=es=cs
go: mov ax,csmov ds,ax mov es,ax! 输出一些信息! 读入光标位置mov ah, #0x03xor bh, bhint 0x10! 设置开机启动字符串mov cx, #25 ! 字符串长度(20字符+3个回车+3个换行)mov bx, #0x0007 ! 设置 显示页号 和 字符属性mov ax, cs ! 使用cs值获取 段地址mov es, ax ! ax 充当临时寄存器mov bp, #msg2 ! 字符串 偏移地址mov ax, #0x1301 ! ah = 13H -> 显示, al = 01H -> 设置文本模式int 0x10! 设置数据段ds,将硬件参数取出来放在内存 0x90000mov ax, #INITSEGmov ds, ax ! 数据段地址 ds = 0x9000! 读光标位置,存入数据段mov ah, #0x03 ! AH = 3 -> 读光标位置xor bh, bh ! 显示页数 = 0int 0x10! 将光标位置写入 0x90000.mov [0], dx! 读入内存大小位置mov ah, #0x88 ! AH = 0x88 -> 读入内存大小int 0x15mov [2], ax ! 存入 9000 后面两个偏移! 获取磁盘参数表,从 0x41 处拷贝 16 个字节mov ax, #0x0000 ! 不允许直接将立即数加载到段寄存器,需要使用通用寄存器axmov ds, ax ! 数据段地址 ds = 0x0000lds si,[4*0x41] ! 取中断向量 41 的值,即 hd0 参数表的地址 ds:simov ax,INITSEGmov es,axmov di,#0x0080 ! 传输的目的地址: es:di = 9000:0080mov cx,#0x10 ! 重复执行次数 = 16rep ! 重复执行movsb, DS:SI -> ES:DImovsb ! 每执行一次 movsb 指令,源地址和目的地址的偏移都会自动递增! 开始显示参数
! 前面修改了ds数据段寄存器,这里将其设置为0x9000mov ax,#INITSEG ! 设置数据段地址,后续显示硬件参数使用mov ds,ax ! 0x9000mov ax,#SETUPSEG ! 设置附加段地址,后续显示字符串使用mov es,ax ! 0x07e0! 显示 光标位置! 获取光标位置mov ah,#0x03 xor bh,bhint 0x10! 显示字符串mov bp, #cur ! 串 偏移地址mov cx, #11 ! 串 长度mov bx, #0x0003 ! 页号 = 0 + 颜色设置(03:青色,07:普通白色)mov ax, #0x1301 ! 显示字符串 + char...int 0x10! 显示数值mov ax, [0] ! 打印函数参数存入 axcall print_hex ! 调用打印寄存器函数call print_nl ! 打印回车换行! 显示 内存大小! 获取光标位置mov ah,#0x03 xor bh,bhint 0x10! 显示字符串提示mov bp, #memmov cx, #12mov bx, #0x0007 mov ax, #0x1301 ! 显示字符串 + char...int 0x10! 显示数值mov ax, [2]call print_hex! 显示 KBmov ah,#0x03 ! read cursor posxor bh,bhint 0x10mov cx,#6mov bx,#0x0007 ! page 0, attribute c mov bp,#cylmov ax,#0x1301 ! write string, move cursorint 0x10! 无限循环
inf_loop:jmp inf_loop! 以 16 进制 打印寄存器 ax 中的16位数
print_hex:mov cx, #4 ! 循环次数smov dx,ax ! 将ax所指的值放入dx中,ax作为参数传递寄存器
print_digit:rol dx,#4 ! 将 DX 寄存器中的值向左循环移位 4 位,相当于将 高4位 移到 低4位mov ax,#0xe0f ! ah = 0eh -> 显示字符, al = 字符:半字节(4个比特)掩码。and al,dl ! 取 dl 的低4比特值。add al,#0x30 ! 给al数字加上十六进制 0x30, 0~9 + 0x30 = "0~9"cmp al,#0x3a ! 判断 al 是否大于 字符 9jl outp ! al < 数字"9"后面的字符,说明是 0~9,则跳转add al,#0x07 ! al > "9", 是a~f,要多加7h
outp:int 0x10 ! 打印字符存储在 al 中loop print_digit ! cx = 4,循环四次ret ! 函数返回指令,表示函数执行结束,返回到调用该函数的位置! 打印回车换行
print_nl:mov ax, #0x0e0dint 0x10mov al, #0x0aint 0x10ret! 显示字符串
msg2:.byte 13,10 ! 回车 + 换行.ascii "Now we are in SETUP".byte 13,10,13,10! 光标位置
cur:.ascii "Cursor POS:"! 内存大小
mem:.ascii "Memory SIZE:"! 提示信息
cyl:.ascii "KB".byte 13,10,13,10