- 改写
/boot/bootsect.s
能在屏幕上打印诸如"XXX is booting"的提示信息 - 改写
/boot/setup.s
完成以下功能:- bootsect.s能完成setup.s的载入,并跳转到setup.s开始地址执行。而setup.s向屏幕输出一行"Now we are in SETUP"。
- setup.s能够获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕
- setup.s不再加载Linux内核,保持上述信息显示在屏幕即可。(我的实现仍然加载了Linux内核!)
- 有时,继承传统意味着蹩手蹩脚。x86计算机为了向下兼容,导致启动过程比较复杂。请找出x86计算机启动过程中,被硬件强制,软件必须遵守的两个”多此一举“(多找几个也无妨),说说它们为什么多此一举,并设计更简洁的替代方案。
再来一张head.s从跳转到物理内存0x00000000执行开始到转向main函数执行过程中内存的使用情况。
如上图所示,内存地址0x00000-0x9FFFF的空间范围是640KB,这片地址对应到了RAM,也就是插在主板上的内存条。你可能小声嘀咕:为什么是对应到了RAM,难道不是直接访问我的物理内存RAM吗?难道我的内存条不是全部的内存?还可以访问到别处吗?
在CPU眼里,插在主板上的物理内存确实不是它眼里“全部的内存”。
地址总线的宽度决定了可以访问的内存空间的大小,如8086的地址总线是20位,其地址范围是1MB,32位CPU的地址总线是32位,其地址范围是4GB。但以上的地址范围是指地址总线可以寻址的内存空间的大小,但这里的内存空间并不等同于物理内存。归根结底的原因是因为:在计算机中,并不是只有插在主板上的内存条需要通过地址总线访问,还有一些外设同样需要通过地址总线来访问,这类设备还很多。由于这个原因,只好在地址总线上提起预留出来一些地址空间给这些外设用,比如这片连续的地址给显存,这片连续的地址给硬盘控制器等。留够了以后,地址总线上其余的可用地址再指向RAM,也就是插在主板上的内存条,我们眼中的物理内存。这也就是说,我们平时的内存条并不是全部被用到了,毕竟要预留一些地址用来访问外设。这就是安装了4GB内存,电脑只显示3.8GB左右可用的原因。下图是我安装了8GB内存的Ubuntu虚拟机实际可用的内存大小:
BIOS是存放在ROM中的,是计算机上电后执行的第一段程序。对于8086而言,此ROM被映射到8086的1MB内存空间的顶部64KB,即地址0xF0000-0xFFFFF处,可以参照上面全景图左侧的BIOS部分。只要访问此处的地址便是访问了ROM中的BIOS,这个映射是由硬件完成的。而BIOS程序主要用于计算机开机时执行系统各部分自检,建立起操作系统需要使用的各种配置表,例如中断向量表,硬盘参数表。并且把处理器和系统其余部分初始化到一个已知的状态,而且还为DOS等操作系统提供硬件设备接口服务。但是由于BIOS提供的这些服务不具备可重入性(即其中程序不可以并发执行),并且从访问效率方面考虑,因此除了在初始化时会利用BIOS提供一些系统参数外,Linux操作系统在运行时并不使用BIOS中的功能。
目前的计算机通常都配置至少4G内存,有的甚至达到64G内存。但为了与原来的PC机在软件上兼容,系统1MB以下物理内存使用分配上仍然保持与原来的PC机基本一致,只是原来系统ROM中的基本输入输出系统BIOS一直处于CPU能寻址的内存最高端位置处,而BIOS原来所在的位置将在计算机开机初始化时被用作BIOS的影子(Shadow)区域,即BIOS代码仍然会被复制到这个区域中。
对于可以寻址4GB甚至以上内存的PC机,当上电开机或者按了复位按钮时,CPU的代码段寄存器CS会被自动设置为0xF000,则其段基址被设置为0xFFFFF000,段长度被设置为64KB,IP被设置为0xFFF0,因此此时CPU代码指针指向0xFFFFFFF0处,即4GB空间最后一个64KB的最后16个字节处。如下图所示,这里正是系统ROM BIOS存放的位置。并且BIOS会在这里存放一条跳转指令JMP
,跳转到BIOS代码中64KB范围内的某一条指令开始执行。由于目前PC/AT微机中BIOS容量大多有1MB~2MB,并存储在闪存(Flash ROM)中,因此为了能够执行或访问BIOS中超过64KB范围并且又远离0~1MB地址空间中的其他BIOS代码或数据,BIOS程序会首先使用一种成为32位大模式(Big Mode)的技术把数据段寄存器的访问位置设为4GB(而非原来的64KB),这样就可以在0到4GB范围内执行和操作数据。此后,BIOS在执行一系列硬件检测和初始化操作之后,就会把与原来PC机兼容的64KBBIOS代码和数据复制到内存低端1MB末端的64KB空间中,然后跳转到这个地方并且让CPU进入真正的实地址模式工作,如下图所示。最后BIOS就会从硬盘或其他设备上把操作系统引导程序加载到内存0x07C00处,并跳转到这个地方继续执行引导程序。
下面是Linux 0.11镜像在具有16MB内存的bochs虚拟机上最开始启动时的寄存器值:
如上面的全景图所示,bootsect.s完成的工作首先是将引导扇区从内存地址0x07C00移动到0x90000处,然后读取共占四个扇区的setup模块,显示加载系统的信息,然后将system模块读入到内存地址0x10000处。
实验结果见上图
可以看出,其中"Qiunix"为亮蓝色,其他字符为常规的白色字体。
其主要用到了BIOS的int 0x10
中断的AH=0x1301
的功能号,具体功能见[int 0x10显示服务表格](#int 0x10显示服务)
其中,由于AH=0x1301
功能号的参数BL用于指定显示属性,而该提示信息由于颜色不同,需要分段显示,先打印"Qiunix"的亮蓝色信息,在显示常规白色的其他信息。AH=0x1301
功能号的功能是:写入字符串到显示器并实现光标自动跟随移动,按照这个理解,写入"Qiunix"之前需要读取光标位置,通过DH和DL保存当前光标所在的行和列作为要写入字符串的起始行号和列号。写入完"Qiunix"之后,光标位置是会自动更新的,但DH和DL保存的光标位置还是之前的,再次写入剩余的其他信息之前,需要更新DH和DL,给定写入之后当前的光标所在行和列,即显示字符串的起始行号和列号,所以需要再次读取光标位置。
考虑到需要两次读取光标位置,将其变为一个可调用的函数,减少重复代码。由于函数调用依赖于栈进行参数传递和返回地址保存,所以在进行函数调用之前,必须先设置好栈,即设置合理的堆栈段寄存器ss和堆栈指针寄存器sp。幸运的是,在调用读取光标函数之前,原来的bootsect.s代码已经设置好了栈!其他的,经过测试,貌似函数定义必须放在bootsect.s文件尾部,放在执行代码中间会报错。
bootsect.s中给出了从磁盘加载setup模块以及system模块到内存的代码,其中牵涉到磁盘的相关知识。在这里做一些总结。
- 磁头(head) 磁头是硬盘中对盘片进行读写工作的工具,每个盘面都有自己的磁头,如果盘面的双面都记录信息,那么双面都应该有磁头。
- 磁道(track) 磁盘表面被划分为很多个同心圆,这些同心圆就是磁道。(当磁盘旋转时,磁头若保持在一个位置上,则每个磁头都会在磁盘表面划出一个圆形轨迹,这些圆形轨迹就叫做磁道。)但打开硬盘,用户不能看到这些,实际上磁道是被磁头磁化的同心圆。磁道之间是有间隔的,因为磁化单元距离太近会产生干扰。
- 柱面(cylinder) 硬盘通常由重叠的一组盘片构成,每个盘片都被划分为数目相等的磁道,并从外缘的“0”开始编号,具有相同编号的磁道形成一个圆柱,称之为磁盘的柱面。柱面数就是磁盘上的磁道数。柱面是磁盘分区的最小单位。
- 扇区(sector) 磁盘上的每个磁道被等分为若干个弧段,这些弧段便是磁盘的扇区。每个扇区的大小为512个字节,磁盘驱动器在向磁盘读取和写入数据时以扇区为单位。扇区是硬盘数据存储的最小单位。
- 硬盘容量
$一个硬盘的容量 = 柱面数(磁道数) * 磁头数 * 扇区数 * 每个扇区的大小(通常为512字节)$
如上面的全景图所示,setup.s首先读取包括光标位置,扩展内存(超过1MB大小以外的物理内存)大小,显示模式,硬盘参数等系统参数,并将这些参数写入到空间地址0x90000~0x90200这512个字节的空间中,然后将system模块从地址0x10000处移动到地址0x00000处,覆盖掉了BIOS用到的数据和中断向量表,这也说明了此后Linux系统确实不再使用BIOS提供的服务了。然后加载已经提前设置好的全局描述符表GDT到GDTR寄存器,中断描述符表IDT到IDTR寄存器,开启A20地址线,重新设置两个中断控制芯片,最后设置CPU的控制寄存器CR0(也称机器状态字),从而进入32位保护模式,并跳转到物理内存0x00000地址处执行,这里是system模块最前面的head.s程序。
和[打印"Qiunix is loading..."的提示信息](#2.1 打印"Qiunix is loading..."的提示信息)一样,注意提前设置好堆栈,具体代码可以参考bootsect.s中的原来自带的堆栈设置。
setup.s一开始就读取光标所在位置保存到内存空间0x90000~0x90001这两个字节处,从bochs的启动过程可以看出,除了我们自己添加的提示信息之外,setup.s之后还会有系统自己写入到显示器的其他信息,而这些信息的显示位置就是从内存空间0x90000~0x90001这两个字节保存的光标位置开始写入的,所以我们要在setup.s读取完系统参数后,打印这里面的某些系统参数,必须再打印之后再次更新保存在0x9000~0x90001这两个字节处的光标位置,将光标位置设置为打印完这些系统参数之后的光标位置。这点很重要,否则,如果像我这样在setup.s中仍然加载Linux内核,那么内核运行后系统输出的其他信息就会覆盖掉我们在setup.s中打印的关于系统参数的信息。
setup.s中用到的int 0x10
中断的AH=0x1301
功能号是将由ES:BP指定地址的字符串写入到显示器,setup.s中的代码和数据被加载到内存地址0x90200开始处,而打印信息中的字符串都是setup.s中的数据,所以自然在SETUPSEG
段。而打印信息的系统参数数据保存在内存地址0x90000~0x90200处,属于INITSEG
段,而取内存中的数据一般要用到数据段寄存器DS,所以需要将附加段寄存器ES设置为SETUPSEG
,将数据段寄存器DS设置为INITSEG
。具体段寄存器搭配的常规用法见5.3.1 段寄存器使用约定。
head.s程序在被编译生成目标文件后会与内核其他程序一起被链接成system模块,位于system模块的最前面开始部分,这也就是为什么称其为头部(head)程序的原因。system模块将被放置在磁盘上setup模块之后开始的扇区中,即从磁盘上第6个扇区开始放置。一般情况下Linux 0.11内核的system模块大约有120KB左右,因此在磁盘上大约占240个扇区。
如head.s执行中的内存使用情况图所示,head.s首先重新设置中断描述符表idt,共256项,并使各个表项均指向一个只报错误的哑中断子程序ignore_int,加载idt表到idtr寄存器。然后设置全局描述符表gdt,实际上新设置的GDT表最开始的3个表项与原来在setup.s程序中设置的GDT表除了在段限长上有些区别以外(原为8MB,现为16MB),其他内容完全一样。不同的是,setup.s中设置的GDT表是临时的,只设置了3个表项,而head.s中设置的GDT表在后面一直使用,这里除了设置最开始的3个表项外,还把其他表项全部请零,加载gdt表到gdtr寄存器。接着检测A20地址线是否真的开启,以及测试PC机是否含有数字协处理器芯片(80287,80387或其他兼容芯片),并在控制寄存器CR0中设置相应的标志位。接着将main函数地址压栈,并设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处(也是本程序所处的物理内存位置,因此这段程序将被覆盖掉),紧随后面放置共可寻址16MB内存的4个页表,并分别设置它们的表项,设置好页目录表和4个页表后,开启分页机制。最后,利用返回指令将之前预先放置在栈顶的/init/main.c
程序的入口地址弹出,去运行*main()*程序。
与分段机制把内存划分为长度不一的段不同,分页机制在固定大小的内存块(称为页面)上进行操作。分页机制可以简单理解为把线性地址空间和物理地址空间都划分为页面,并在两个空间的页面上做相应映射。线性地址空间中的任何页面可以被映射到物理地址空间的任何页面上。页面的大小为固定的4KB,并且对齐于4KB地址边界处,这就把4GB的线性地址空间划分成$2^20$个页面。由于$2^20$个页面的基地址都对齐在4K边界上,因此页面基地址的低12位肯定是0。这意味着线性地址的低12位可作为页内偏移量直接作为物理地址的低12位。
由于页表大小固定为4KB,所以4GB线性地址空间总共可以划分成$2^20$个页表。每个页表项占4个字节,存储这些页表将需要4MB的内存。为了减少内存占用,80x86使用两级页表。线性地址的最高10位([31:22])是页目录表的索引值,选出的页目录项含有第二级页表的基地址;线性地址的中间10位([21:12])是第二级页表的索引值,选出的页表项含有实际物理地址的高20位;该高20位的物理地址与低12位的线性地址进行组合得到一个完整的32位物理地址。
AH | 功能 | 调用参数 | 返回参数 |
---|---|---|---|
0x03 | 读光标位置和大小 | BH=页号(0-based) | CH=光标起始位置,CL=光标结束位置,DH=光标行号(0-based),DL=光标列号(0-based) |
0x0e | 显示字符(光标前移) | AL=字符 BL=前景色 | None |
0x0f | 获取当前显示模式 | None | AL=当前的显示模式,AH=屏幕宽度,以字符列,BH=当前页号(0-based) |
0x1300 | 显示字符串光标留在起始位置 | ES:BP=字符串地址,CX=字符串长度,BH=页号,BL=显示属性,DH,DL=显示字符串的起始行号和列号 | None |
0x1301 | 显示字符串光标跟随移动 | ES:BP=字符串地址,CX=字符串长度,BH=页号,BL=显示属性,DH,DL=显示字符串的起始行号和列号 | None |
AH(功能号) | 功能 | 调用参数 | 返回参数 |
---|---|---|---|
0x02 | 读软盘或硬盘上的若干物理扇区到内存的ES:BX处 | ES=段地址 BX=偏移地址 DL=驱动器号,软盘为0/1,首个硬盘或U盘为80H CH=柱面号,起始编号为0 DH=磁头号,起始编号为0 CL=起始扇区号,起始编号为1 AL=扇区数 | CF=1, when occurs error, and places error code in AH |
0x08 | 读取驱动器参数 | DL=驱动器号,软盘为0/1,首个硬盘或U盘为80H | DL=驱动器数量,DH=最大的磁头数,CH+CL[7:6]=最大的柱面数,CL[5:0]=最大的扇区数 |
0x15 | 检测指定的硬盘或软盘是否存在及其类型 | DL=0x00~0x03分别指定第1~4个软盘,DL=0x80~0x81分别指定第1~2个硬盘 | AH=0x00没有这个盘,AH=0x01是软盘,没有change-line支持,AH=0x02是软盘(或其他可移动设备),AH=0x03是硬盘,CF=1,when occurs error, and places error code in AH |
AH(功能号) | 功能 | 调用参数 | 返回参数 |
---|---|---|---|
0x88 | 获取扩展内存容量 | None | AX=1MB以上的内存的大小,以KB为单位 |
Repeat Prefixes通常是与movs
,scas
,cmps
等串指令搭配使用的,它们有:
F2: REPNE
F3: REP / REPE
(F2, F3为repeat prefixes对应的二进制代码)
Reapte Prefixes作为一个串操作指令的前缀,它重复执行其后的串操作指令。每一次重复都先判断(E)CX是否为0,如为0就结束重复,否则(E)CX的值减1,然后再重复其后的串操作指令。所以当(E)CX的值为0时,就不再执行其后的操作指令。
它类似于LOOP
指令,但LOOP
指令是先把(E)CX的值减1,然后再判断是否为0。
举例:
CLD
MOV ECX, 3
REP MOVSB
运行的结果是把DS:(E)SI的3个字节(byte)移动到ES:(E)DI去。 Repeat Prefixes的结束条件:
Repeat Prefix | 结束条件1 | 结束条件2 |
---|---|---|
REP | ECX=0 | None |
REPE | ECX=0 | ZF=0 |
REPNE | ECX=0 | ZF=1 |
从上表可以看出,repe
和repne
的结束必须同时满足两个结束条件,而rep
只管ECX等不等于0。
80x86寄存器 CPU共有一般分为5类:
- 数据寄存器
- 段寄存器
- 控制寄存器
- 指针寄存器
- 段基址寄存器
具体分类如下:
80x86 registers
|——通用寄存器
| |——数据寄存器
| | |——EAX
| | | |——AX -> |AH|AL|
| | |——EBX
| | | |——BX -> |BH|BL|
| | |——ECX
| | | |——CX -> |CH|CL|
| | |——EDX
| | |——DX -> |DH|DL|
| |
| |——指针寄存器
| | |——堆栈指针寄存器
| | | |——ESP
| | | | |——SP
| | |——基址指针寄存器
| | |——EBP
| | |__BP
| |
| |——变址寄存器
| |——源变址寄存器
| | |——ESI
| | | |——SI
| |——目的变址寄存器
| |——EDI
| |——DI
|
|——控制寄存器
| |——指令指针寄存器
| | |——EIP
| | | |——IP
| |——标志寄存器
| | |——EFLAGS
| | | |——FLAGS
| |——控制寄存器0
| | |——CR0
| |——控制寄存器1
| | |——CR1
| |——控制寄存器2
| | |——CR2
| |——控制寄存器3
| |——CR3
|
|——段寄存器
| |——代码段寄存器
| | |——CS
| |——数据段寄存器
| | |——DS
| |——堆栈段寄存器
| | |——SS
| |——附加段寄存器
| |——ES
| |——FS
| |——GS
|
|——段基址寄存器
| |——全局描述符表寄存器
| | |——GDTR
| |——中断描述符表寄存器
| | |——IDTR
| |——局部描述符表寄存器
| | |——LDTR
| |——任务状态寄存器
| |——TR
访问存储区类型 | 缺省段寄存器 | 可指定段寄存器 | 段内偏移地址来源 |
---|---|---|---|
取指令码 | CS | 无 | IP |
堆栈操作 | SS | 无 | SP |
BP用作基地址寄存器 | SS | CS DS ES | 依寻址方式寻找有效地址 |
串操作源地址 | DS | CS DS ES | SI |
串操作目的地址 | ES | 无 | DI |
一般数据存取 | DS | CS DS ES | 依寻址方式寻找有效地址 |
31 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
+-------------------------------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |V |R |0 |N | IO |O |D |I |T |S |Z |0 |A |0 |P |1 |C |
| |M |F | |T | PL |F |F |F |F |F |F | |F | |F | |F |
+-------------------------------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
- CF(Carry Flag):进位标志位
- PF(Parity Flag):奇偶标志位
- AF(Assistant Flag):辅助进位标志位
- ZF(Zero Flag):零标志位
- SF(Signal Flag):符号标志位
- IF(Interrupt Flag):中断允许标志位,由
CLI
,STI
两条指令来控制;设置IF位使CPU可识别外部(可屏蔽)中断请求,复位IF位则禁止中断;IF位对不可屏蔽外部中断和故障中断的识别没有任何作用 - DF(Direction Flag):方向标志位,由
CLD
,STD
两条指令控制;复位DF标志位,字符串操作指令中SI
,DI
值自增;设置DF位字符串操作指令中SI
,DI
值自减 - OF(Overflow Flag):溢出标志位
- IOPL(I/O Privilege Level):I/O特权级字段,宽度2位
- NT(Nested Task):控制中断返回指令
IRET
47 16 15 0
+--------------------------------------------------+---------------------------+
GDTR | 32位线性基地址 | 16位表长度 |
+--------------------------------------------------+---------------------------+
加载GDTR寄存器指令
! gdt_48
! 47 16 15 0
!+------------------+-----------+
!| 32位线性基地址 | gdt表长度 | gdt_48
!+------------------+-----------+
ldtr gdt_48
gdtr
用于加载全局描述符表寄存器GDTR。它的操作数(gdt_48)有6个字节。前2字节(字节0-1)是描述表的字节长度值;后4字节(字节2-5)是描述符表的32位线性基地址。
IDRT同GDTR
不再采用segment<<4+offset
的实模式段式寻址模式。
段寄存器是选择子
,结构如下
15 3 2 1 0
+----------------------------+--+--+--+
| 描述符索引 |TI| RPL | 段寄存器/选择子
+----------------------------+--+--+--+
- RPL(Requestor's Privilege Level),请求者特权级
- TI(Table Indicator),表指示器,用于指定选择符所引用的描述符表。TI=0,指定GDT表;TI=1,指定当前的LDT表
- 描述符索引用于选择指定描述符表中的其中一个。处理器将该索引值乘上8,并加上描述符表的基地址即可访问表中指定的段描述符。
段描述符放在描述符表中。每一个段描述符占8个字节。
下面是代码段描述符的字节分布:
63 54 53 52 51 50 48 47 46 44 43 40 39 32
+-------------+--+--+--+--+--------+--+----+--+--------+----------------+
| BaseAddress |G |B |0 |A |Segment |P | D |S | TYPE | BaseAddress |
| 31...24 | | | |V | Limit | | P | | | 23...16 |
| | | | |L | 19...16| | L |1 | 1|C|R|A| |
+-------------+--+--+--+--+--------+--+----+--+--------+----------------+
31 17 16 0
+----------------------------------+------------------------------------+
| BaseAddress | Segment |
| 15...0 | Limit |
| | 15...0 |
+----------------------------------+------------------------------------+
下面是中断门描述符的字节分布:
63 48 47 46 44 43 40 39 37 36 32
+----------------------------------+--+----+--+--------+-+-+-+----------+
| |P | D |S | | | |
| Procedure Entry Address | | P | | TYPE |0 0 0| Reserved |
| 31...16 | | L |0 | 1|1|1|0| | |
+-------------+--+--+--+--+--------+--+----+--+--------+-+-+-+----------+
31 17 16 0
+----------------------------------+------------------------------------+
| | |
| Segment Selector | Procedure Entry Address |
| | 15...0 |
+----------------------------------+------------------------------------+
虚拟基址经过段式寻址方式转化为线性地址,转换过程如下:
进入保护模式前需要设置好使用的段描述符表,包括全局描述符表GDT
和中断描述符表IDT
lidt
加载中断描述符表寄存器IDT
! idt_48
!47 32|31 16|15 0
! -32bit linear addr- | len of idt
lidt idt_48
lidt用于加载中断描述符表寄存器(IDT)。它的操作数(idt_48)有6字节。 前2字节(字节0-1)是描述符表的字节长度值;后4字节(字节2-5)是描述符表的32位线性基地址。
lgdt
加载全局描述符表寄存器GDT
lgdt gdt_48
指令格式同lidt
- lmsw(load machine status word) 切换到保护模式
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax
加载机器状态字,也就是控制寄存器CR0,其比特位0置1将导致CPU切换到保护模式,并且运行在特权级0中
在Intel公司的手册上建议80386或以上CPU应该使用指令mov cr0, ax
切换到保护模式。lmsw
指令
仅用于兼容以前的286CPU。
- 跳转到内存地址0处的
system
模块
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
寻址方式如段式寻址过程图所示
页目录表和页表的表项格式如下所示:
31 12 11 9 8 7 6 5 4 3 2 1 0
+-----------------------------------------------+-+-+-+-+-+-+-+-+-+-+-+-+
| | A | | | | |U|R| |
| Page Frame Address | V |0 0|D|A|0 0|/|/|P|
| | L | | | | |S|w| |
+-----------------------------------------------+-----+-+-+-+-+-+-+-+-+-+
下图给出二级页表的查找过程。其中CR3寄存器指定页目录表的基地址。线性地址的高10位([31:22])用于索引这个页目录表,以获得指向相关第二级页表的指针。线性地址的中间10位([21:12])用于索引二级页表,以获得物理地址的高20位。线性地址的低12位直接作为物理地址低12位,从而组成一个完整的32位物理地址。
计算机上电,BIOS初始化中断向量表之后,会将启动设备的第一个扇区(即引导扇区)读入内存地址0x07c00(31kb)
处,并跳转到此处执行,由此系统的控制权由BIOS转交给bootsect.s。而为了方便加载内核模块,bootsect.s首先将自己移动到0x90000(576kb)
处。这样的移动是多此一举。
计算机上电后,BIOS会在物理地址0处开始初始化中断向量表,其中有256个中断向量,每个中断向量占用4个字节,共1KB,在物理内存地址0x00000-0x003fff
处,这些中断向量供BIOS中断使用。这就要求,如果操作系统的引导程序在加载操作系统时使用了BIOS中断来获取或显示一些信息时,内存中这最开始的1KB数据不能被覆盖。而操作系统的内核代码最好起始于物理内存开始处,这样内核空间的代码地址等于实际的物理地址,便于对内核代码和数据进行操作,这就需要将内核代码加载到内存0x00000
处。如此就产生了矛盾。所以bootsect.s
在载入内核模块时,先将其加载到0x10000
处,之后setup.s
利用BIOS中断读取完硬件参数,再有setup.s
将内核模块从0x10000-0x8ffff
处搬运到0x00000-0x7ffff
处。这样先加载内核模块到其他地方再移到到内存起始位置是多此一举。