当前位置: 首页 > news >正文

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 - 0x004FFBIOS数据区
0x00500 - 0x0E05A
0x0E05B - 0x0FFFE中断服务程序
0x0FFFF - 0xFFFEF
0xFE000 - 0xFFFFFBIOS启动块

我们可以发现,其实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 = 0BL会获得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 csmov 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、内核模块kernelmain.chead.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

总结

本文主要讲了从按下电源按钮的那一刻,到将操作系统加载到内存中主要发生了什么,这也是这一个系列的第一个部分的第一章。

http://www.lryc.cn/news/3250.html

相关文章:

  • python制作贪吃蛇小游戏,畅玩无限制
  • MySQL-InnoDB数据页结构浅析
  • Java、JSP职工人事管理系统设计与实现
  • 数据结构与算法这么难,为什么我们还要学习?
  • 剑指 Offer 52. 两个链表的第一个公共节点
  • 可以写进简历的软件测试电商项目,不进来get一下?
  • 蓝桥杯-算法-印章问题
  • 戴尔游匣G16电脑U盘安装系统操作教程分享
  • 2023数学建模美赛赛题思路分析 2023美赛 美国大学生数学建模数模
  • vue3与vue2的对比
  • 史上最全软件测试工程师常见的面试题总结(百度、oppo、中软国际、华为)备战金三银四
  • “深度学习”学习日记。卷积神经网络--用CNN的实现MINIST识别任务
  • JavaWeb--JDBC练习
  • 【LeetCode】2335. 装满杯子需要的最短总时长
  • Android 12.0 通过驱动实现禁用usb鼠标和usb键盘功能
  • C++入门——内存管理
  • MySQL-InnoDB行格式浅析
  • AXI 总线协议学习笔记(4)
  • C++复习笔记6
  • 指针的步长及意义(C语言基础)
  • SpringMVC:统一异常处理(11)
  • SpringBoot的配置与使用
  • 【Python】tkinter messagebox练习笔记
  • 2022年12月电子学会Python等级考试试卷(五级)答案解析
  • 计算机网络自定向下 -- 浅谈可靠性之rdt协议
  • 制造业升级转型:制造业上市公司-智能制造词频统计数据集
  • HTML 开发工具整理
  • 介绍ACE C++网络通信框架
  • 【Mac OS】JDK 多版本切换配置
  • RabbitMQ-Exchanges交换机