内核引导启动程序
文件结构
知识补充
BIOS:
- 计算机启动最开始执行的BIOS程序,BIOS(Basic Input Output System).
- 是一组固化到计算机内主板上一个ROM芯片上的程序,它保存着计算机最重要的基本输入输出程序、开机后自检程序和系统自启动程序,它可从CMOS中读写系统设置的具体信息
结合boot/目录下程序讲解计算机启动流程
- PC的电源开机后,80x86结构的CPU自动进入实地址模式。
- 执行从0xFFFF0开始的程序,一般就是BIOS,再次映证BIOS是计算机启动第一个执行的程序。
- BIOS将执行系统的检测,并在物理地址0处开始初始化中断向量,这里的中断向量是BIOS中断。
- 将软盘或硬盘的第一个扇区(512字节)读入内存绝对地址为0x7C00处,并跳转到这个地方。
小结:BIOS是计算机加电后第一个执行的程序,BIOS执行完毕后会加载第一个扇区的程序到0x7C00处继续执行,这里的第一个扇区存放boot/bootsect.s,其功能下面进行讲解。
程序分析
bootsect.s(8086汇编)
- 首先理解下文件名:其名为bootsect.s,后缀名表示其为汇编程序,那么bootsect的含义呢?boot表示启动,sect是section的缩写,表示扇区,即bootsect是启动扇区的含义。
- 上面说到,BIOS执行完后会将bootsect.s加载到0x7c00处,然后开始执行bootsect.s,下面分析bootsect.s程序,不要怕,就是简单的汇编,我们日常作业写的汇编都比这难,只不过这里涉及一些硬件相关的知识和BIOS的中断调用。
功能描述:
BIOS int 0x13中断理解 简单来说:该中断将指定扇区的代码加载到内存指定位置,其本质是磁盘服务程序。
- 将自己移动到内存0x90000处,并跳转到0x90000
- 利用BIOS的int 0x13,将setup.s模块加载到0x90200处
- 利用BIOS的int 0x13(上面说过,BIOS从物理地址0处设置中断向量)取磁盘参数表中当前启动引导盘的参数
- 将system模块(链接生成的文件镜像),从磁盘加载到内存0x10000开始的地方
- 设置根文件系统
- 长跳转到setup程序,执行setup.s程序
提问:上面提到0x90000, 0x90200,0x10000,为什么非得是这三个地址呢?不能是其他吗?看下面一张程序分布图(提示:三个地址是根据每个程序的大小来定的, 0x200B == 512B == 一个扇区大小):
源码解析
由于源码篇幅比较大,我不会将所有源码贴进来,建议读者去下载本系列博客目录提供的书籍资料,以及源码压缩包,然后配合本博客一起读,笔者使用VSCode(安装x86 and x86_64 Assembly插件)阅读汇编代码。
- 数据以及标识符声明部分:
!
! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
! versions of linux
!
SYSSIZE = 0x3000 !0x3000*16=0x30000字节,注意一个十六进制位表示4个二进制位
!
! bootsect.s (C) 1991 Linus Torvalds
!
! 这里就是该程序的功能描述
! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
! iself out of the way to address 0x90000, and jumps there.
!
! It then loads 'setup' directly after itself (0x90200), and the system
! at 0x10000, using BIOS interrupts.
!
! NOTE! currently system is at most 8*65536=2^19(0.5M) bytes long. This should be no
! problem, even in the future. I want to keep it simple. This 512 kB
! kernel size should be enough, especially as this doesn't contain the
! buffer cache as in minix
!
! The loader has been made as simple as possible, and continuos
! read errors will result in a unbreakable loop. Reboot by hand. It
! loads pretty fast by getting whole sectors at a time whenever possible.
! 全局标识符
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
! 注意这里是段地址:绝对地址=段地址<<4+偏移地址
SETUPLEN = 4 ! nr of setup-sectors ! setup程序的扇区数
BOOTSEG = 0x07c0 ! original address of boot-sector ! 被BIOS加载到0x7c0处,即本程序的初始地址
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
! ROOT_DEV: 0x000 - same type of floppy as boot.
! 0x301 - first partition on first drive etc
ROOT_DEV = 0x306 ! 指定根文件系统设备是第2 个硬盘的第1 个分区。这是Linux 老式的硬盘命名
! 方式,具体值的含义如下:
! 设备号=主设备号*256 + 次设备号(也即dev_no = (major<<8) + minor )
! (主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道)
! 0x300 - /dev/hd0 - 代表整个第1 个硬盘,3<<8 == 0x300;
! 0x301 - /dev/hd1 - 第1 个盘的第1 个分区;
! …
! 0x304 - /dev/hd4 - 第1 个盘的第4 个分区;
! 0x305 - /dev/hd5 - 代表整个第2 个硬盘盘;
! 0x306 - /dev/hd6 - 第2 个盘的第1 个分区;
! …
! 0x309 - /dev/hd9 - 第2 个盘的第4 个分区;
! 从linux 内核0.95 版后已经使用与现在相同的命名方法了。
该部分声明一些数据,建议读者仔细看看,有助于后面程序的阅读。
下面会根据上面提供的功能描述来解析:
- 将本程序自身移动到0x90000处:
entry _start
_start:
mov ax,#BOOTSEG
mov ds,ax ! 源地址:0x07c0:0x0000
mov ax,#INITSEG
mov es,ax ! 目的地址:0x9000:0x0000
mov cx,#256 ! 重复256次
sub si,si
sub di,di
rep
movw ! 一次移动一个字,一个字两个字节,共256字=512字节,刚好是第一个扇区大小
jmpi go,INITSEG ! 然后跳转到0x9000段的go标号处
重新设置段寄存器:
go: mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
这里涉及一些计算:
- 为何要把堆栈放置到0xFF00处呢?
- 首先:从绝对地址0x90000开始放置本程序即bootsect.s,本程序供512字节,对应16进制表示为0x200。
- 所以setup程序装在绝对地址0x90200开始处,数据声明部分声明了setup占4个扇区,那么4x512字节 = 4 x 0x200 = 0x800, 0x90200 + 0x800 = 0x90A00。
- 由于堆栈是向低地址扩展,那么sp指针的位置还得再加一个堆栈大小,即 绝对地址0x90A00 + 堆栈大小 = 绝对地址0x9FF00。
- 那么我们就可以反推出这里设置的堆栈大小是:0x9FF00 - 0x90A00 = 0xF500,即61KB大小的堆栈。
- 这样就明白了0x9FF00的意义了。
- 下面讲解如何将setup程序移动到本程序后面,本程序开始地址是0x90000,大小是512B=0x200,所以setup程序加载到0x90200处:
! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
load_setup:
mov dx,#0x0000 ! DL(驱动器号):drive 0, DH(磁头号):head 0
mov cx,#0x0002 ! CL(开始扇区0-5位,磁道号的高两位对应CL的6-7位):sector 2, CH(磁道号的第八位):track 0
mov bx,#0x0200 ! address = 512, in INITSEG ES:BX 指向 0x9200 表示装入setup程序的开始地址
mov ax,#0x0200+SETUPLEN ! AH:service 2 表示读磁盘扇区到内存, AL:nr of sectors 表示读几个扇区
int 0x13 ! read it, BIOS的0x13调用
jnc ok_load_setup ! ok - continue ,如果出错则置CF标志位,所以这里使用jnc
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette,上面的AH=0x02表示读磁盘扇区到内存,这里的AH=0x00表示重置磁盘驱动器
int 0x13
j load_setup ! 跳回去继续执行
上面的注释已经比较清楚了,不过涉及一些额外的知识:
注意上面代码中的一个跳转指令:j load_setup,这里是无条件跳转,即如果读取失败的话,那么跳回去继续执行load_setup。如果一直出错就是死循环了,意思是出错只能手动重启了。
- 利用BIOS int 0x13中断取磁盘参数表中当前启动引导盘的参数
如果读者对CHS比较了解或者读了我的另一篇CHS寻址博客,应该知道,CHS寻址就像三维数组寻址一样,需要知道每一维有多少元素才能去寻址,所以这里这里读取的数据包括 “一个磁道(每个盘面)有多少扇区” 。
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
mov dl,#0x00 ! dl=驱动器号
mov ax,#0x0800 ! AH=8 is get drive parameters,
int 0x13 ! BIOS调用,cl中返回每磁道最大扇区数
mov ch,#0x00
seg cs ! 表示下一条语句的操作数在cs段寄存器的段中
mov sectors,cx ! 保存每磁道最大扇区数,sector变量在后面定义
mov ax,#INITSEG ! seg cs改变了es寄存器的值,这里恢复
mov es,ax
然后紧接着打印一些提示信息 BIOS int0x10系统调用功能及参数详解
! Print some inane message
mov ah,#0x03 ! read cursor pos,在DX中包含行列信息
xor bh,bh
int 0x10
mov cx,#24 ! 这中间没有改变DX的值,所以是从原来的光标处继续显示的
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1 ! Loading system ...
mov ax,#0x1301 ! write string, move cursor
int 0x10
! ok, we've written the message, now
! we want to load the system (at 0x10000)
这里建议读者去看看上面推荐的一篇BIOS int0x10调用的详解。
- 下面在0x10000处加载system模块
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
call kill_motor
下面是read_it的代码,比较复杂,笔者加了大量的注释,看了好几遍才看明白。读者最好结合本系列博客目录提供的源代码按照函数跳转顺序好好读。
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary,判断传入的参数是否正确
xor bx,bx ! bx is starting address within segment
rp_read: ! 函数功能:判断是否已经读完,如果没有读完,跳转继续读。如果读完return
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?判断是否全部加载完毕
jb ok1_read ! 如果没有加载完毕,跳转至ok1_read,如果加载完毕return
ret
ok1_read: ! 函数功能:判断将本磁道剩余内容都装入本段是否装得下,保证读入内容不会溢出本段
seg cs ! 取扇区数
mov ax,sectors
sub ax,sread ! sectors - sread 表示未读扇区数
mov cx,ax ! cx=未读扇区数目
shl cx,#9 ! 一个扇区512字节,扇区数<<9 得字节数
add cx,bx ! cx=未读字节数
jnc ok2_read ! 这里判断如果将剩余内容都装入本段,是否能够装得下,如果装得下,ok2_read。
! 如果装不下,计算能装多少
je ok2_read ! 请读者思考,这里段大小是64KB,为何?(TIPS:2^16B=64KB)
xor ax,ax
sub ax,bx !计算本段还能装入多少即 64KB-bx
shr ax,#9 ! 转换成扇区数
ok2_read: ! 函数功能:读本磁道内容到内存,读完判断本磁道是否读完
call read_track ! 读当前磁道ax个扇区数到内存,利用上面用到的ah=2,int 0x13 BIOS系统调用.
! 传入参数ax,表示须读扇区数。 bx,表示从bx地址开始装
mov cx,ax ! ax表示此次读的扇区数
add ax,sread ! 计算本磁道已读扇区数
seg cs
cmp ax,sectors
jne ok3_read ! 如果本磁道还有扇区未读
! 传入参数:ax = 本磁道已读扇区数。cx = read_track函数读入的扇区数
mov ax,#1 ! 磁头是有上盘面磁头和下盘面磁头之分的,所以这里判断下,读完上盘面磁头去读下盘面磁头
sub ax,head
jne ok4_read ! 下盘面磁头还没读,去ok4_read读
inc track ! 两个磁头都读完,磁道+1
ok4_read: ! 设置磁头号
mov head,ax
xor ax,ax
ok3_read: ! 本磁道没读完继续读,读完重置参数,继续读下一磁道
mov sread,ax ! 本磁道已读扇区数更新
shl cx,#9 ! 刚才read_track读入的扇区数
add bx,cx ! 重置bx
jnc rp_read ! 如果还没有产生进位说明本段还没有用完,跳转到rp_read继续读。
mov ax,es ! 如果本段用完了,调整寄存器参数,继续用下一段
add ax,#0x1000 ! 为什么加0x1000呢?这里是分段的知识:物理地址=段地址<<4+偏移地址,
! 本应该加0x10000,因为在地址转换时有一个左移四位,所以这里加0x1000
mov es,ax
xor bx,bx
jmp rp_read
read_track: ! 函数功能:读当前磁道ax个扇区数到内存
push ax
push bx
push cx
push dx
mov dx,track ! 获取当前磁道号
mov cx,sread ! 当前磁道已读扇区数
inc cx
mov ch,dl ! ch = 磁道号
mov dx,head ! 取磁头号
mov dh,dl ! dh保存磁头号
mov dl,#0 ! dl为驱动器号,为0表示当前驱动器
and dx,#0x0100
mov ah,#2 ! ah=2表示读磁盘到内存,读到内存中es:bx的内容,bx寄存器的内容是函数调用之前设置好的
int 0x13
jc bad_rt ! int 0x13调用失败
pop dx
pop cx
pop bx
pop ax
ret
bad_rt: mov ax,#0 ! 磁盘启动器复位,跳到read_track重试
mov dx,#0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
下面是kill_motor的代码,读者不必在这里为具体代码纠结,明白其意图是关闭马达, 系统启动之后就明确了软驱的状态是关闭。
!/*
! * This procedure turns off the floppy drive motor, so
! * that we enter the kernel in a known state, and
! * don't have to worry about it later.
! */
kill_motor:
push dx
mov dx,#0x3f2 ! 软驱控制卡的驱动端口,只写
mov al,#0 ! A驱动器,关闭FDC,禁止DMA和中断请求,关闭马达
outb ! 将内容写到相应端口里
pop dx
ret
最后是判断根文件系统的类型
! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28)(1.44M A驱动器) or
! /dev/at0 (2,8)(1.2M A驱动器), depending
! on the number of sectors that the BIOS reports currently.
! 下面程序判断根文件系统是什么类型的,然后写到root_dev中
seg cs
mov ax,root_dev
cmp ax,#0
jne root_defined
seg cs
mov bx,sectors
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15 ! 扇区数是15表示是1.2M软驱
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18 ! 扇区数是18表示是1.44M软驱
je root_defined
undef_root: ! 如果不是两种类型之一,无限循环-死机
jmp undef_root
root_defined:
seg cs
mov root_dev,ax
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
jmpi 0,SETUPSEG
判断完根文件类型后直接跳转到SETUPSEG,即setup程序所在地。
bootsect.s程序分析完毕!!!