这篇文章的目的,在将
linuxkernel
的
boot
部份做一个介绍,因为笔者觉得很少有这样的文章介绍一个作业系统最最开始的一步
--
把
kernel
本身载入至内存中,同时进行一些机器相关
(machinedependent)
的初始化工作,由于
linux
刚好使用的是大家最熟悉的
386
,
486
系列
PC
,所以在说明其程序流程时,也刚好可以对其相关的
PC
硬体架构做探讨,可以说是一举两得,不过,我必须假设读者对于组合语言及
PC
最基础的架构,如寄存器,分段,分页,中断服务等有大概的认识。
读者可在
linuxsourcecode
的
/boot
子目录下找到几个以
.S
作为副档名的组合语言档,本文要说明的即是其中的
bootsect.S
及
setup.S
两个档案,及尽量简单的说明其所牵涉的相关硬体部份。
bootsect.S
这个程序是
linuxkernel
的第一个程序,包括了
linux
自己的
bootstrap
程序,但是在说明这个程序前,必须先说明一般
IBMPC
开机时的动作
(
此处的开机是指
"
打开
PC
的电源
"):
一般
PC
在电源一开时,是由内存中地址
FFFF:0000
开始执行
(
这个地址一定在
ROMBIOS
中,
ROMBIOS
一般是在
FEOOOh
到
FFFFFh
中
)
,而此处的内容则是一个
jump
指令,
jump
到另一个位于
ROMBIOS
中的位置,开始执行一系列的动作,包括了检查
RAM
,
keyboard
,显示器,软硬磁盘等等,这些动作是由系统测试码
(systemtestcode)
来执行的,随着制作
BIOS
厂商的不同而会有些许差异,但都是大同小异,读者可自行观察自家机器开机时,萤幕上所显示的检查讯息。
紧接着系统测试码之后,控制权会转移给
ROM
中的启动程序
(ROMbootstraproutine)
,这个程序会将磁盘上的零道零扇区读入内存中
(
这就是一般所谓的
bootsector
,如果你曾接触过电脑病毒,就大概听过它的大名
)
,至于被读到内存的哪里呢
?--
绝对位置
07C0:0000(
即
07C00h
处
)
,这是
IBM
系列
PC
的特性。而位在
linux
开机磁盘的
bootsector
上的正是
linux
的
bootsect
程序,也就是说,
bootsect
是第一个被读入内存中并执行的程序。现在,我们可以开始来看看到底
bootsect
做了什么。
第一步
首先,
bootsect
将它
"
自己
"
从被
ROMBIOS
载入的绝对地址
0x7C00
处搬到
0x90000
处,然后利用一个
jmpi(jumpindirectly)
的指令,跳到新位置的
jmpi
的下一行去执行,关键的
assemblycode
如下
:
.
(
搬移
bootsect
本身
)
.
.
jmpigo,INITSEC
go:
.
.
.
表示将跳到
CS
为
0x9000
,
IP
为
offset"go"
的位置
(CS:IP=0x9000:offsetgo)
,其中
INITSEC=0x9000
定义于程序开头的部份,而
go
这个
label
则恰好是下一行指令所在的位置。
第二步
接着,将其它
segmentregisters
包括
DS
,
ES
,
SS
都指向
0x9000
这个位置,与
CS
看齐。另外将
SP
及
DX
指向一任意位移地址
(offset)
,这个地址等一下会用来存放磁盘参数表
(diskpara-metertable)
提到磁盘参数表,就必须提到
BIOS
中断
1Eh
。先简单的介绍一下
BIOS
的中断服务
:80x86
将内存最低的
256*4byte
保留给
256
个中断向量
(
每个
interruptvector
大小为
4byte
,所以一共有
256*4=1024byte)
,而其中的第
1Eh
个向量指向
"
磁盘参数表
"
,这个表会告诉电脑如何去读取磁盘机,而我们所要做的事是搬移磁盘参数表到刚才所设定的任意地址。
接着,改变搬移来的参数表的参数,以符合我们的需要。再将中断向量
1Eh
指向我们所修改过的磁盘参数表,然后呼叫
BIOSinterrupt
的
int13h(function0
,即
AH=0)
重置磁盘控制卡及磁盘驱动器,之后磁盘机就会照我们的意思动作了。如果你曾
trace
过
DOS
的
kernel
,你会发现,上述的动作在
DOS
中也有类似的对应流程。
现在让我们来看看关键的程序码
:.
.
.
push#0
popfs
movbx,#0x78
.
(
使
GS:SI=FS:BX
,指向磁盘参数表,
再将
GS:SI
所指地址的内容搬移
6
个
word
至
ES:DI
所指的地址
)
.
.
此段程序是将
FS:BX
调整成
0000:0078
,接着再将
GS:SI
的内容设成与
FS:BX
相同,此处
0x78h
即为
int1Eh
的起始位置
(7*16+8=120,(1*16+14)*4=120)
。调整
ES:DI
为刚才所设定的任意地址,从
GS:SI
搬移
6
个
word(
即
12byte)
到
ES:DI
所指的位置,显然磁盘参数表的长度就是
6
个
word
,
(
不过事实上,磁盘参数表的确实长度是
11
个
byte)
。关于磁盘参数表,有兴趣的读者可自行参阅讲述
BIOSinterruptservices
的技术手册,会有详细的说明。
读者可以用
debug
自行观察自家机器上
DOS
的磁盘参数表的起始位置
(
即
int1Eh
的内容
)
。以下是笔者机器的情形
(
笔者使用的作业系统是
MSDOS6.2):
C:>debug
-d0000:0000
0000:00008A101601F4067000-1600CB04F4067000......p.......p.
0000:0010F40670000301790E-43EB00F0EBEA00F0..p...y.C.......
0000:002004108E340C118E34-5700CB046F00CB04...4...4W...o...
0000:00308700CB0408079433-B700CB04F4067000.......3......p.
0000:00400C01790E4DF800F0-41F800F0BA165F06..y.M...A....._.
0000:005039E700F01B01790E-70118E341201790E9.....y.p..4..y.
0000:006000E000F085175F06-6EFE00F0EE067000......_.n.....p.
0000:007053FF00F0A4F000F0-220500003E4600C0S......."...>F..
^^^^^^^^
由上图中可知,在
DOS
中磁盘参数表的起始位置
(int1Eh
的内容
)
为
0000:0522
。接着观察
DOS
中位置
0000:0522
开始的
11
个
byte
,也就是磁盘参数表的内容
C:>debug
-d0000:0520l10
0000:05204D53DF022502121B-FF54F60F08000000MS..%....T......
^^^^^^^^^^^^^^^^^^^^^^
此
11byte
即为磁盘参数表的内容
(
分别是
byte00h
到
0Ah)
在程序中我们所更动的是第五个
byte(byte04h)
,改为
18h(
在上图例子中为
12h)
,这个
byte
的功能是定义磁轨上一个磁区的资料笔数。关键的程序码如下
:
.
movb4(di),*18
.
第叁步
接着利用
BIOS
中断服务
int13h
的第
0
号功能,重置磁盘控制器,使得刚才的设定发挥功能。
.
.
xorah,ah
xordl,dl
int0x13
.
.
第四步
完成重置磁盘控制器之后,
bootsect
就从磁盘上读入紧邻着
bootsect
的
setup
程序,也就是以后将会介绍的
setup.S
,此读入动作是利用
BIOS
中断服务
int13h
的第
2
号功能。
setup
的
image
将会读入至程序所指定的内存绝对地址
0x90200
处,也就是在内存中紧邻着
bootsect
所在的位置。待
setup
的
image
读入内存后,利用
BIOS
中断服务
int13h
的第
8
号功能读取目前磁盘机的参数。
第五步
再来,就要读入真正
linux
的
kernel
了,也就是你可以在
linux
的根目录下看到的
"vmlinuz"
。在读入前,将会先呼叫
BIOS
中断服务
int10h
的第
3
号功能,读取游标位置,之后再呼叫
BIOS
中断服务
int10h
的第
13h
号功能,在萤幕上输出字符串
"Loading"
,这个字符串在
bootlinux
时都会首先被看到,相信大家应该觉得很眼熟吧。
linux
的
kernel
将会被读入至内存绝对地址
0x10000
处,键关的程序码如下
:
.
.
movax,#SYSSEG
moves,ax
callread_it
callkill_motor
.
.
其中
SYSSEG
于程序开头时定义为
0x1000
,先将
ES
内容设为
0x1000
,接着在
read_it
这个子程序便以
ES
为目的地的节地址,将
kernel
读入内存中,至于
read_it
子程序的详细内容笔者并不想一一介绍,不过聪明的读者们应该已经猜到,
read_it
一定又利用了
BIOSint13h
与磁盘有关的
I/O
中断服务了。
至于
kill_motor
子程序,它的功能在于停止软盘机的马达
(
各位聪明的读者会不会觉得这个子程序的名称取得颇为传神呢
?)
,其程序码如下
:
.
.
kill_motor:
pushdx
movdx,#0x3f2
xoral,al
outb
popdx
ret
.
.
首先利用
DX
指定要输出的
port
,而
03f2
这个
port
则是代表了软盘控制器
(floppydiskcontroller)
的所在,再利用
outb
将资料送出,而我们送出的资料,当然就是归零过的
AL
了。如此一来,软盘的马达就停止了。
第六步
接下来做的事是检查
rootdevice
,之后就仿照一开始的方法,利用
indirectjump
跳至刚刚已读入的
setup
部份,程序码如下
:
.
.
jmpi0,SETYPSEG
其中
SETUPSEG
已在先前定义为
0x9020
,所以
CS:IP
会设定为
9020:0000
,即跳到绝对地址为
0x90200
,也就是
setup
的起点。而
bootsect
也大功告成了。
到此为止,内存的内容应该如下图所示
:
比较
把大家所熟知的
MSDOS
与
linux
的开机部份做个粗浅的比较,
MSDOS
由位于磁盘上
bootsector
的
boot
程序负责把
IO.SYS
载入内存中,而
IO.SYS
则负有把
DOS
的
kernel--MSDOS.SYS
载入内存的重责大任。而
linux
则是由位于
bootsector
的
bootsect
程序负责把
setup
及
linux
的
kernel
载入内存中,再将控制权交给
setup
。
至于
setup.S
,就留到下一次再来讨论了。