Linux内核启动(1,0.11版本)启动BIOS与加载内核
从电源到启动BIOS
从我们按下启动电源到BIOS,按下电源–>主板会向电源组发出信号–> 接受到信号后,当主板收到电源正常启动信号后,主板会启动CPU(CPU重置所有寄存器数据,并且初始化数据),比如32位系统,实模式采用内存段来管理0-0xFFFFF这1MB内存空间。
实模式:内存访问是真实的内存地址,软件可以不受限制操作实际内存所有地址空间和IO设备)
保护模式:全部使用虚拟内存,页等机制对内存进行保护,比起实模式更为安全和可靠,同时也增加了扩展性和灵活性
计算机的运行是离不开程序的,但是CPU被设计的智能读取内存中的程序和数据,不能直接从硬盘加载程序和数据,必须先将程序和数据加载到内存,那我们怎么启动程序呢,秘诀是 0xFFFF0
,既然软件的方式无法执行BIOS,那我们就采取硬件的方式,从硬件的角度看,8086系列CPU可以分别在16位实模式和32位保护模式运行,为了兼容也为了解决最初的启动问题,Intel将所有的x86系列CPU包括最新的CPU硬件都设计为加电即进入16位实模式运行,当然,关键的是将CPU的硬件逻辑设计为加电瞬间强行将CS寄存器的值设置为 0xF000
IP寄存器的值设置为 0xFFF0
这样CS:IP就指向了 0xFFFF0
这个地址位置
CS是代码段寄存器,IP是指令指针寄存器,两者组合就是即将要执行的指令的内存地址,实模式为绝对地址,指令指针为16位,保护模式下为线性地址,指令指针为32位即EIP,
实模式下两者组合出线性地址的方式为
CS << 4 + IP
注意,这是一个纯硬件操作,如果 0xFFFF0
处没有任何可执行代码,那么CPU就此死机,但是BIOS程序的第一条指令就设计在这个位置
BIOS程序的代码量并不大,但是却非诚精深,需要对计算机硬件非常熟悉才能看明白,这显然已经超出了 Linux 的范围,所以只做简单讲解,BIOS程序被固化在计算机主板上一块非常小的ROM内,通常不同主板采用的BIOS也不同,但是其基本原理大致相同,当屏幕开始显示时,这意味着BIOS已经启动了,有一项对启动操作系统至关重要的工作,就是BIOS在内存中建立中断向量表和中断服务程序。
ROM 只读存储器,断电之后仍能保存信息,但是某些情况下因为BIOS是写在闪存上的,所以也可以更改,毕竟BIOS也是可以升级的。
BIOS程序在内存最开始的位置用1KB构建中断向量表,用256字节构建BIOS数据区,在大约57KB之后的位置,加载了8KB左右的与中断向量表相应的若干中断服务程序。此时内存的分布
内存位置 | 内容 |
---|---|
0x00000 - 0x003FF | 中断向量表 |
0x00400 - 0x004FF | BIOS数据区 |
0x00500 - 0x0E05A | 空 |
0x0E05B - 0x0FFFE | 中断服务程序 |
0x0FFFF - 0xFFFEF | 空 |
0xFE000 - 0xFFFFF | BIOS启动块 |
我们可以发现,其实0xFFFF0并不是BIOS启动块的头地址,只是BIOS的第一行程序的地址。
中断(INT),打断正在执行的程序,转而执行处理这个时间的特定程序,处理结束后,回到被打断的程序继续执行,可以理解为一种技术手段
加载操作系统内核
现在开始我们要加载操作系统内核了,但是我们发现一个问题没有,我们操作系统内核还是在硬盘或者软盘上,没有在内存里啊,这个其实也是硬件完成的,BIOS启动之后,计算机完成自检,我们把我们的软盘设置成了启动设备,所以计算机硬件和BIOS程序会联手操作,让CPU收到一个 int 0x19
中断,CPU收到这个中断后,会立即在中断向量表中找到这个中断向量,然后中断向量吧CPU指向 0x0E6F2
这个位置,这个位置就是对应的中断服务程序的入口地址,这个中断是设计好的,代码是固定的,与我们的操作系统无关,这段代码唯一的作用就是找到软盘加载第一扇区,其余的它什么都不知道,也不必知道。
中断向量表(interrupt Vector Table):实模式中断机制的重要组成部分,记录所有的中断号对应的中断服务程序内存地址
中断服务程序(interrupt Service Programme):通过中断向量表的索引对中断进行响应服务,是一些具有特殊功能的程序
按照这个规则,int 0x19
中断将软盘的0号磁头对应的盘面的0磁道1扇区的内容复制到 0x07C00
的位置,这个数据块有多大呢,512B,这个扇区内装载的就是Linux 0.11的引导程序,也就是我们要讲的 bootsect.s
程序,这是一个汇编程序。此时我们就有了我们操作系统自己的代码了,虽然只是启动代码。此程序的源文件地址位于 /boot/bootsect.s
bootsect
的功能很简单,就是把第二批和第三批程序陆续加载到内存,为了能加载到适当位置,bootsect
首先做的就是规划内存。要知道,实模式下的内存寻址最大范围是1MB,内存是非常宝贵的。
这里我们就涉及到Linux的内核源码了,接着看
SYSSIZE = 0x3000 ! 给出system模块的大小 0x3000字节 = 192KBSETUPLEN = 4 ! setup程序的扇区数
BOOTSEG = 0x07c0 ! bootsect的起始地址
INITSEG = 0x9000 ! bootsect的目标地址
SETUPSEG = 0x9020 ! setup的起始地址
SYSSEG = 0x1000 ! system的起始加载地址
ENDSEG = SYSSEG + SYSSIZE ! system的终止地址! ROOT_DEV: 0x000 - same type of floppy as boot.
! 0x301 - first partition on first drive etc 指定根文件系统设备时第2个硬盘的第一个分区
ROOT_DEV = 0x306
这里定义了一些数据,目的是规划内存
entry _start
_start:mov ax,#BOOTSEGmov ds,axmov ax,#INITSEGmov es,axmov cx,#256sub si,sisub di,direpmovwjmpi go,INITSEG
这里进入第一个函数 _start
,这个函数的目的就是想 0x07C00
处的引导程序搬到 0x90000
处,在这次复制过程中,DS和SI联合使用构成了原地址 0x07C00
,ES和DI联合使用构成了目标地址 0x90000
,而给CX赋值 256
的原因是 movw这个指令复制的是字,一个字为两个字节,256个字刚好为512字节,rep反复执行movw直到CX为0,执行完毕后执行跳转指令
ENTRY 是程序入口伪指令。在一个完整的汇编程序中至少有一个 ENTRY,编译程序在编译连接时依据程序入口进行连接
go: mov ax,csmov ds,ax ! 数据段寄存器mov es,ax ! 附加段寄存器
! put stack at 0x9ff00. mov ss,ax ! 栈寄存器 栈操作是从高地址到低地址mov sp,#0xFF00 ! arbitrary value >>512
这里使用CS初始化段寄存器和栈空间,就比较简单了,此时CS段寄存器的地址是 0x9000
,所以DS,ES,SS都被赋值为 0x9000
,这其实意味着我们可以使用栈了,也意味着我们可以使用13号中断来从磁盘搬运程序了,接着往下走
load_setup:mov dx,#0x0000 ! drive 0, head 0mov cx,#0x0002 ! sector 2, track 0mov bx,#0x0200 ! address = 512, in INITSEGmov ax,#0x0200+SETUPLEN ! service 2, nr of sectorsint 0x13 ! read it jnc ok_load_setup ! ok - continuemov dx,#0x0000 ! mov ax,#0x0000 ! reset the disketteint 0x13 j load_setup
0x13
中断是一个服务,用来对磁盘进行操作,我们介绍一下各寄存器的功能,如果要使用这个功能,需要
AH
:必须设置为0x02
AL
:扇区数目
CH
:柱面
CL
:扇区
DH
:磁头
DL
:驱动器(00H - 7FH 为软盘;80H - 0FFH 为硬盘)
ES:BX
: 缓冲区地址
既然是读取操作必然要制导读取结果,如果 CF=0
则表示成功,此时 AH=状态码
,AL=传输的扇区数
此时ES = 9000H
,而读取时BX = 0x0200
,所以读取后被放到0x90200
这个地址。如果读取成功,就会跳转到ok_load_setup
这个标签,失败重置磁盘状态(AH = 0
调用),重试直到成功。执行完int 0x13
指令后,我们看看下面的程序:
ok_load_setup:! Get disk drive parameters, specifically nr of sectors/trackmov dl,#0x00 ! DL 为驱动器 如果成功CF= 0 BL会获得1-4的数据,为磁盘大小mov ax,#0x0800 ! AH=8 is get drive parameters AH= 8 时是获取磁盘信息int 0x13 ! mov ch,#0x00 seg csmov sectors,cxmov ax,#INITSEGmov es,ax
当AH = 0x08
时,调用int 0x13
指令是获取磁盘大小信息,其中DL
为驱动器,如果成功CF = 0
,BL
会获得1-4
的数值,含义如下
BL=1
360KB
BL=2
2.2MB
BL=3
720KB
BL=4
1.44MB
与此同时,一些其他寄存器也被赋予了含义
CH
代表柱面数的低八位
CL
的高两位代表柱面数的高两位,低六位代表扇区数
DH
代表柱头数
DL
代表驱动器数
ES:DI
指向的是磁盘驱动器参数表地址
seg cs
只影响到mov sectors,cx
而不影响mov ax,#INITSEG
,如果以Masm语法写,seg cs
和mov sectors,cx
两句合起来等价于mov cs:[sectors],cx
,这里使用了间接寻址方式。seg cs
只是表明紧跟它的下一条语句将使用段超越,因为在编译后的代码中可以清楚的看出段超越本质上就是加了一个字节的指令前缀。cx
的高八位已经被置为0,所以这里是将区块的扇区数目放到了CS:sectors
这个地址中。
! Print some inane messagemov ah,#0x03 ! read cursor posxor bh,bhint 0x10 mov cx,#24mov bx,#0x0007 ! page 0, attribute 7 (normal)mov bp,#msg1mov ax,#0x1301 ! write string, move cursorint 0x10
这段代码中的 int 0x10
中断用于向屏幕中输出字符串,这一段代码将 msg1
输出到了屏幕上
msg1: ! "\nLoading system ...\n".byte 13,10.ascii "Loading system ...".byte 13,10,13,10
msg1
写的是 "Loading system ..."
,回车键的ASCII
是13,换行键的ASCII
是10,如果组合起来就是回车换行,就是C/C++
的\n
我们继续下面的代码
! ok, we've written the message, now
! we want to load the system (at 0x10000)mov ax,#SYSSEGmov es,ax ! segment of 0x010000call read_itcall kill_motor
这部分就是加载system
模块了,system
模块就是内核模块,包含库模块lib
、内存管理模块mm
、内核模块kernel
、main.c
和head.s
程序,后面将会详细介绍。read_it
就是读取函数,将模块读取到0x010000
这个地址。kill_motor
函数是关闭驱动器马达,以知道驱动器状态。
从底层技术上看,这与前面的setup程序加载没什么本质区别,比较突出的特点是,这次加载的扇区数是240个,足足是之前加载4个扇区的60倍,所需时间也是几十倍的增加,为了防止用于以为是机器故障,所以才有了之前的往屏幕上输出一个字符串的操作。
我们先看read_it
,这个函数的功能是把软盘第六扇区看事的约240个扇区的system
模块加载到内存的SYSSEG(0x10000)
处,由于长时间的操作软盘,所以对软盘设备需要更多地监控,对读盘结果需要不断地检测,因此read_it
后续的调用步骤多一点
sread: .word 1+SETUPLEN ! sectors read of current track
head: .word 0 ! current head
track: .word 0 ! current trackread_it:mov ax,estest ax,#0x0fff
die: jne die ! es must be at 64kB boundaryxor bx,bx ! bx is starting address within segment
rp_read:mov ax,escmp ax,#ENDSEG ! have we loaded all yet?jb ok1_readret
ok1_read:seg csmov ax,sectorssub ax,sreadmov cx,axshl cx,#9add cx,bxjnc ok2_readje ok2_readxor ax,axsub ax,bxshr ax,#9
ok2_read:call read_trackmov cx,axadd ax,sreadseg cscmp ax,sectorsjne ok3_readmov ax,#1sub ax,headjne ok4_readinc track
ok4_read:mov head,axxor ax,ax
ok3_read:mov sread,axshl cx,#9add bx,cxjnc rp_readmov ax,esadd ax,#0x1000mov es,axxor bx,bxjmp rp_readread_track:push axpush bxpush cxpush dxmov dx,trackmov cx,sreadinc cxmov ch,dlmov dx,headmov dh,dlmov dl,#0and dx,#0x0100mov ah,#2int 0x13jc bad_rtpop dxpop cxpop bxpop axret
bad_rt: mov ax,#0mov dx,#0int 0x13pop dxpop cxpop bxpop axjmp read_track
嗯,是不是乱的一批,其实没有太多的必要去理解这个代码,这个和硬件的关系太紧密了,我们只需要知道这里搬运了一部分代码就可以了。
此时只剩下了最后一块bootsect.s
程序:
! 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) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.seg csmov ax,root_devcmp ax,#0jne root_definedseg csmov bx,sectorsmov ax,#0x0208 ! /dev/ps0 - 1.2Mbcmp bx,#15je root_definedmov ax,#0x021c ! /dev/PS0 - 1.44Mbcmp bx,#18je root_defined
undef_root:jmp undef_root
root_defined:seg csmov root_dev,ax
root_dev
是一个变量,它指向一个字大小的宏ROOT_DEV
,它的值为0x306
。
root_dev:.word ROOT_DEV
为什么是这个值呢?如果该值为0
,根文件系统设备与引导使用同样的软驱设备;如果是0x301
,则为第一个硬盘的第一个分区上,这个被称为设备号。设备号 = 主设备号 * 256 + 次设备号
设备号 | 设备文件 | 对应的设备 |
---|---|---|
0x300 | /dev/hd0 | 系统中第一个硬盘 |
0x301 | /dev/hd1 | 系统中第一个硬盘的第一分区 |
0x302 | /dev/hd2 | 系统中第一个硬盘的第二分区 |
0x303 | /dev/hd3 | 系统中第一个硬盘的第三分区 |
0x304 | /dev/hd4 | 系统中第一个硬盘的第四分区 |
0x305 | /dev/hd5 | 系统中第二个硬盘 |
0x306 | /dev/hd6 | 系统中第二个硬盘的第一分区 |
0x307 | /dev/hd7 | 系统中第二个硬盘的第二分区 |
0x308 | /dev/hd8 | 系统中第二个硬盘的第三分区 |
0x309 | /dev/hd9 | 系统中第二个硬盘的第四分区 |
这一部分就是再次确认一下根设备号
Linux 0.11使用的文件系统管理方式要求系统必须存在一个根文件系统,其他文件系统挂接在骑上,而不是同等地为,Linux 0.11 并没有提供在设备上建立文件系统的工具所以必须在一个正在运行的系统上利用工具作出一个文件系统并加载至本机,所以Linux 0.11 的启动需要分为两部分,内核镜像和根文件系统。
于是该内核使用的是第二个硬盘的第一个分区,作为根文件系统设备。接下来两个cmp
可能看不懂,咱们给个解释:sectors
是我们之前保存的每磁道扇区数目,如果是 15 ,那么就是 1.2 MB 的驱动器;如果是 18 ,那么就是 1.44 MB 的,也就是引导驱动器的设备号。如果正常找到,将会执行jmpi 0,SETUPSEG
,该部分程序结束;否则,直接死循环。
跳转到setup
跳转到setup就简单多了,随便的一行
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:jmpi 0,SETUPSEG
总结
本文主要讲了从按下电源按钮的那一刻,到将操作系统加载到内存中主要发生了什么,这也是这一个系列的第一个部分的第一章。