操作系统的引导
实验内容
此次实验的基本内容是:
- 阅读《Linux 内核完全注释》的第 6 章,对计算机和 Linux 0.11 的引导过程进行初步的了解;
- 按照下面的要求改写 0.11 的引导程序 bootsect.s
- 有兴趣同学可以做做进入保护模式前的设置程序 setup.s。
改写 bootsect.s
主要完成如下功能:
- bootsect.s 能在屏幕上打印一段提示信息“XXX is booting…”,其中 XXX 是你给自己的操作系统起的名字,例如 LZJos、Sunix 等(可以上论坛上秀秀谁的 OS 名字最帅,也可以显示一个特色 logo,以表示自己操作系统的与众不同。)
改写 setup.s
主要完成如下功能:
- bootsect.s 能完成 setup.s 的载入,并跳转到 setup.s 开始地址执行。而 setup.s 向屏幕输出一行"Now we are in SETUP"。
- setup.s 能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
- setup.s 不再加载 Linux 内核,保持上述信息显示在屏幕上即可。
完成 bootsect.s 的屏幕输出功能
重新编写bootsect.s
entry _start
_start:
mov ah,#0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中(dh为
行,dl为列)
xor bh,bh
int 0x10
mov cx,#36 !共显示36个字符
mov bx,#0x0007 !bh为页号 bl为字符属性
mov bp,#msg1
mov ax,#0x07c0
mov es,ax
mov ax,#0x1301 !0x10功能号ah=0x13实现显示字符串,al=01,表示使用bl中的属性值,即
normal
int 0x10
inf_loop:
jmp inf_loop
msg1:
.byte 13,10 !2个字符
.ascii "Hello OS world, my name is GYX" !30个字符
.byte 13,10,13,10 !4个字符
.org 510 !使得boot_flag在最后两个字节
boot_flag:
.word 0xAA55 ! 设置引导扇区标记 0xAA55,供BIOS中的程序加载引导扇区时识别使用,必
须位于引导扇区的最后两个字节
编译并运行bootsect.s
从终端进入 ~/oslab/linux-0.11/boot/ 目录,执行以下命令:
as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o
其中 -0 (注意:这是数字 0,不是字母 O)表示生成 8086 的 16 位目标程序, -a 表示生成与 GNU as和 ld 部分兼容的代码, -s 告诉链接器 ld86 去除最后生成的可执行文件中的符号信息
如上图所示,编译成功,可以发现 bootsect 的文件大小是 544 字节,而引导程序必须要正好占用一个磁盘扇区,即 512 个字节。造成多了 32 个字节的原因是 ld86 产生的是 Minix 可执行文件格式,因此要去掉这个文件头。执行以下命令ddbs=1if=bootsectof=Imageskip=32生成的 Image 就是去掉文件头的 bootsect。
接下来将刚刚生成的 Image 复制到 linux-0.11 目录下,再执行 oslab 目录中的 run 脚本,就得到以下结果。
bootsect.s读入setup.s
首先编写一个 setup.s,该 setup.s 可以就直接拷贝前面的 bootsect.s(还需要简单的调整),然后将其中的显示的信息改为:“Now we are in SETUP”。
编写setup.s
entry _start
_start:
! 显示字符串 "Now we are in SETUP"
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中(dh为
行,dl为列)
xor bh, bh
int 0x10
mov cx, #25 ! 显示字符串的长度
mov bx, #0x0007 !bh为页号 bl为字符属性
mov bp, #msg2
mov ax, cs
mov es, ax ! es:bp 是将要显示的字符串的地址
mov ax, #0x1301 !0x10功能号ah=0x13实现显示字符串,al=01,表示使用bl中的属性值,
即normal
int 0x10
inf_loop:
jmp inf_loop
msg2:
.byte 13,10 !2个字符
.ascii "NOW we are in SETUP" !19个字符
.byte 13,10,13,10 !4个字符
.org 510 !使得boot_flag在最后两个字节
boot_flag:
.word 0xAA55 ! 设置引导扇区标记 0xAA55,供BIOS中的程序加载引导扇区时识别使用,必
须位于引导扇区的最后两个字节中
修改bootsect.s
接下来需要编写 bootsect.s 中载入 setup.s 的关键代码。原版 bootsect.s 中下面的代码就是做这个的。因此将load_setup部分相关代码复制到bootsect.s中,完整代码如下:
SETUPLEN = 2 ! 要读取的扇区数
SETUPSEG = 0x07e0 ! setup 读入内存后的起始地址,这里 bootsect 没有将自己挪动到
0x90000 处,所以setup=0x07e00
entry _start
_start:
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中
(dh为行,dl为列)
xor bh, bh
int 0x10
mov cx, #36 ! 显示字符串的长度
mov bx, #0x0007 !bh为页号 bl为字符属性
mov bp, #msg2
mov ax, cs
mov es, ax ! es:bp 是将要显示的字符串的地址
mov ax, #0x1301 !0x10功能号ah=0x13实现显示字符串,al=01,表示使用bl中的属性值,
即normal
int 0x10
load_setup:
mov dx, #0x0000 ! dh=磁头号 dl=驱动器号
mov cx, #0x0002 ! ch=磁道号的低8位 cl=扇区号(0-5),磁道号高2位
(6-7)
mov bx, #0x0200 ! es:bx 指向数据缓冲区
mov ax, #0x0200 + SETUPLEN ! ah=int 13h 的功能号(2 表示读扇区) al=读取的扇区
数
int 0x13 ! 读setup.s
jnc ok_load_setup ! 读入成功则跳转
mov dx, #0x0000
mov ax, #0x0000 ! 软驱、硬盘有问题时,会复位软驱
int 0x13
jmp load_setup ! 重新循环,再次尝试读取
ok_load_setup:
jmpi 0, SETUPSEG ! 段间跳转指令 ip=0, cs=SETUPSEG,跳到
0x07e0:0000(setup.s程序开始的地方)执行
msg1:
.byte 13, 10 ! 2个字符
.ascii "Hello OS world, my name is GYX" !30个字符
.byte 13, 10, 13, 10 !4个字符
.org 510 !使得boot_flag在最后两个字节
boot_flag:
.word 0xAA55 ! 设置引导扇区标记 0xAA55,供BIOS中的程序加载引导扇区时识别使用,必
须位于引导扇区的最后两个字节中
编译并运行
进入 linux-0.11 目录后 ,使用 make BootImage 命令编译,如下图,发现有 Error!这是因为 make 根据 Makefile 的指引执行了 tools/build.c ,它是为生成整个内核的镜像文件而设计的,默认的 build.c会将 bootsect、setup 和 system 内核的文件名,将三者做简单的整理后一起写入 Image 中。其中system 是第三个参数(argv[3])。为完成实验,接下来给它打个小补丁,即忽略所有和system有关的内容。
执行
$ cd ~/oslab/linux-0.11
$ make BootImage
$ ../run
在setup.s中获取基本硬件参数并打印
setup.s 将获得硬件参数放在内存的 0x90000 处。原版 setup.s 中已经完成了光标位置、内存大小、显存大小、显卡参数、第一和第二硬盘参数的保存。
INITSEG = 0x9000 ! setup.s 将获得的硬件参数放在内存的 0x90000 处
entry _start
_start:
! 显示字符串 "Now we are in SETUP"
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中
(dh为行,dl为列)
xor bh, bh
int 0x10
mov cx, #25 ! 显示字符串的长度
mov bx, #0x0007 !bh为页号 bl为字符属性
mov bp, #msg2
mov ax, cs
mov es, ax ! es:bp 是将要显示的字符串的地址
mov ax, #0x1301 !0x10功能号ah=0x13实现显示字符串,al=01,表示使用bl中的属性
值,即normal
int 0x10
! 获取基本硬件参数
mov ax, #INITSEG
mov ds, ax ! ds = 0x9000
! 读取光标的位置并写入 0x90000 处
mov ah, #0x03 ! 读入光标位置
xor bh, bh
int 0x10
mov [0], dx ! 将获取的光标位置dx写入 ds:[0]=0x90000 处
! 读取内存大小并写入内存中
mov ah, #0x88 !用 ah=0x88 调用 0x15 中断可以读出内存的大小,结果返回在ax中
int 0x15
mov [2], ax ! 将内存大小ax写入 ds:[2]=0x90002 处
! 从 0x41 处拷贝 16 个字节(磁盘参数表)
! 在 PC 机中 BIOS 设定的中断向量表中 int 0x41 的中断向量位置(4*0x41 =
0x0000:0x0104)存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。
mov ax, #0x0000
mov ds, ax
lds si, [4 * 0x41] !取中断向量0x41处的值
mov ax, #INITSEG
mov es, ax
mov di, #0x0004 !传到es:di 0x9000:0x0004=0x90004的位置
mov cx, #0x10 ! 重复 16 次,因为每个硬盘参数表有 16 个字节大小。
rep
movsb
! 准备打印参数
mov ax, cs
mov es, ax
mov ax, #INITSEG
mov ds, ax !ds和ss都设置为0x90000
mov ss, ax
mov sp, #0xFF00 !sp=0xff00
! 打印光标的位置
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中(dh为
行,dl为列)
xor bh, bh
int 0x10
mov cx, #18
mov bx, #0x0007
mov bp, #msg_cursor
mov ax, #0x1301 !0x10功能号ah=0x13实现显示字符串,al=01,表示使用bl中的属性值,即
normal
int 0x10
mov [0],dx !将光标位置写入 0x90000
call print_hex ! 调用 print_hex 显示信息
! 打印内存大小
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中(dh为
行,dl为列)
xor bh, bh
int 0x10
mov cx, #14
mov bx, #0x0007
mov bp, #msg_memory
mov ax, #0x1301 !0x10功能号ah=0x13实现显示字符串,al=01,表示使用bl中的属性值,即
normal
int 0x10
mov [2],dx !将光标位置写入 0x90002
call print_hex ! 调用 print_hex 显示信息
! 添加内存单位 KB
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中(dh为
行,dl为列)
xor bh, bh
int 0x10
mov cx, #2
mov bx, #0x0007
mov bp, #msg_kb
mov ax, #0x1301 !0x10功能号ah=0x13实现显示字符串,al=01,表示使用bl中的属性值,即
normal
int 0x10
! 打印柱面数
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中(dh为
行,dl为列)
xor bh, bh
int 0x10
mov cx, #7
mov bx, #0x0007
mov bp, #msg_cyles
mov ax, #0x1301
int 0x10
mov dx, [4] !将0x90004处的值放入dx,在磁盘基本参数表中,柱面数的位移为0x0
call print_hex ! 调用 print_hex 显示信息
! 打印磁头数
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中(dh
为行,dl为列)
xor bh, bh
int 0x10
mov cx, #8
mov bx, #0x0007
mov bp, #msg_heads
mov ax, #0x1301 !0x10功能号ah=0x13实现显示字符串,al=01,表示使用bl中的属性值,即
normal
int 0x10
mov dx, [6] !!将0x90006处的值放入dx,在磁盘基本参数表中,磁头数的位移为0x2
call print_hex ! 调用 print_hex 显示信息
! 打印扇区
mov ah, #0x03 !BIOS中断0x10功能号ah=0x03读取光标的位置,并将结果返回保存在dx中(dh为
行,dl为列)
xor bh, bh
int 0x10
mov cx, #10
mov bx, #0x0002
mov bp, #msg_sectors
mov ax, #0x1301
int 0x10
mov dx, [12] !!将0x90012处的值放入dx,在磁盘基本参数表中,扇区数的位移为0x0e
call print_hex ! 调用 print_hex 显示信息
call print_nl ! 打印换行回车
inf_loop:
jmp inf_loop ! 设置一个无限循环
! 以 16 进制方式打印栈顶的 16 位数
print_hex:
mov cx, #4 ! 循环的次数,一个 dx 寄存器有 16 位,每 4 位显示一个 ASCII 字
符,因此需要循环 4 次
print_digit:
rol dx, #4 ! 循环左移,将 dx 的高 4 位移到低 4 位处
mov ax, #0xe0f ! ah=0x0e为int 0x10的子程序0x0e(显示一个字符串) al=要显示字符
的 ASCII 码
and al, dl ! 取 dl 的低 4 位,通过与运算放入 al 中
add al, #0x30 ! 数字 + 0x30 == 对应的 ASCII 码
cmp al, #0x3a ! 比较指令,仅对标志寄存器位有影响
jl outp ! jl 小于跳转
add al, #0x07 ! a~f 是 字符 + 0x37 == 对应的 ASCII 码
outp:
mov bx, #0x0007
int 0x10
loop print_digit
ret
print_nl:
mov ax, #0xe0d
int 0x10 ! 打印回车
mov al, #0xa
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_kb:
.ascii "KB"
msg_cyles:
.byte 13,10
.ascii "Cyls:"
msg_heads:
.byte 13,10
.ascii "Heads:"
msg_sectors:
.byte 13,10
.ascii "Sectors:"
.org 510 !使得boot_flag在最后两个字节
boot_flag:
.word 0xAA55 ! 设置引导扇区标记 0xAA55,供BIOS中的程序加载引导扇区时识别使用,必
须位于引导扇区的最后两个字节中
再次执行
$ cd ~/oslab/linux-0.11
$ make BootImage
$ ../run
打开 Bochs 配置文件 bochs/bochsrc.bxrc,结果符合。Memory Size 是 0x3C00KB= 15MB,加上1MB 正好是 16MB
小结
上电前
计算机上电初始化后,物理内存被设置为从地址0开始的连续区域,除了地址从0xA0000到0xFFFFF(630K到1M)和0xFFFE0000到0xFFFFFFFF(4G中最后一个64K)以外的所有内存被用作系统内存。
其中4G中最后一个64K存放的是BIOS程序,它主要用于计算机开机时执行系统各部分自检
具体执行顺序
执行bootsect.s
当bootsect.s被执行时候,bootsect会把自己移动到内存绝对地址0x90000处,并把从磁盘第二个扇区开始的4个扇区的setup模块读入到内存0x90200处,然后显示“Loding system…",随后把内核中的其他部分(system)被读入到内存地址0x10000处。跳到setup程序的开始处执行setup程序,此时将控制权给setup
执行setup.s
执行setup.s,利用BIOS中断获取基本的硬件参数之后,将这些参数保存在0x90000开始的位置,然后setup把system模块从0x10000开始的位置移动到内存的起始位置0x00000处,这样system模块的代码地址也就是实际的物理地址。随后加载IDT和GDT表,通过设置寄存器cr0进入保护模式,接着跳转到system模块的最前面的部分head.s执行
执行head.s
在执行head.s过程中重新设置了IDT和GDT表,从而使得head.s在保护模式下运行。然后利用ret返回指令将预先放在堆栈中的main.c程序的入口地址弹出来,跳到system模块中的初始化程序main.c中继续执行
执行main
main函数进行一系列初始化工作,随后进入用户态,启动完成。
遇到的问题
在打印基本硬件参数的实验中,获取到的数据如下图所示,可见数据全都是0,而且Memory size后面并没有KB,于是对照linux内核完全注释这本书重新查看了代码,发现在setup.s中的print_hex里面多了mov dx,(bp),这个语句,再看一下代码,在call print_hex的时候,es:bp指向的是msg,所以dx中存放的是msg信息,无法打印成进制数字,因此导致print_hex执行变得没有意义,从而显示的都是0.
print_hex:
! 4 个十六进制数字
mov cx,#4
! 将(bp)所指的值放入 dx 中,如果 bp 是指向栈顶的话
mov dx,(bp)
在注释掉那条代码之后,重新运行,问题解决,查看boch的配置文件,发现结果吻合