boot启动程序包含三个汇编程序,主要作用是:当PC机打开电源后,CPU将进入实模式,并从地址0xFFFF0处开始执行程序,这个地址就是BOIS程序。BOIS程序将执行系统的某些检测,并在物理0地址处初始化中断向量。接着将可启动设备的第一扇区(磁盘引导扇区)读入内存绝对地址0x7C00处,并跳转到这个地址开始执行,而0x7C00就是bootsect.s的入口地址。注:启动设备通常是软盘或硬盘。在现代计算机中,启动设备几乎都是硬盘。
boot目录下只有三个汇编文件,如下图,bootsect.s和setup.s是as86汇编语法编写,是实模式下运行的16位代码程序。而head.s通过gas汇编语法编写,运行在保护模式下。之所以使用两种汇编语法是因为gas直到1994年才开始支持编译实模式下16位程序的。所以Linux2.4x版本的内核只有gas汇编语法。
如上述所言,系统启动时最先执行的就是BIOS程序,然后是bootsect.s,在内核执行bootsect.s程序时,会把bootsect.s程序移动到ox90000处,bootsect.s长0.5k,也就是长0x200。然后把setup.s程序移动到0x90200开始处,setup.s长,而内核其他部分(system模块)则被读入到从内存地址0x10000处,在系统加载期间会显示loading,然后将控制权交到setup.s。因此系统上电后的各程序加载顺序如下图所示:
系统启动时引导代码在内存中的动态位置如下图所示。
setup.s代码识别主机的某些特性以及VGA卡的类型。然后将系统从地址0x10000处移动到0x0000处,进入保护模式并转至系统的剩余部分(0x00000)。此时32位的运行方式的设置启动被完成:IDT、GDT、LDT被加载,处理器和协处理器也已经确认,分页工作也设置好;最终调用init/main.c中的main()程序。以上是head.s代码执行的步骤。至此,系统上电之后开始从0xFFFF0地址处开始执行BIOS程序,执行系统的某些检测,然后初始化中断向量,再将bootsect.s程序加载到0x7C00处。并在执行bootsect.s的同时将自己移动到0x90000处,然后将setup.s代码移动到0x90200开始处,将系统其他部分移动到0x10000开始处。执行setup.s,执行的过程当中,它会要求将系统的剩余部分移动到0x0000处,并开始执行head.s。
那么为什么setup.s不直接在0x0000处开始执行而是在0x90200处,这是因为setup.s在执行的过程当中需要用到BIOS的中断向量表,而中断向量表是存在0地址处的,大小为0x400(1k)个字节。需要等使用完BIOS中断调用才能覆盖。
而Linux系统需要运行,还需要最基本的文件系统支持,也就是跟目录。跟目录的默认块设备号也在bootsect.s中定义。
bootsect.s程序
bootsect.s用于x86上创建启动引导扇区,是引导扇区的第一个程序,而引导扇区位于硬盘的第一个扇区,大小为512字节,引导扇区包含了系统启动的必要信息,如操作系统的文件位置,文件系统类型等。系统启动时,BIOS首先读取引导扇区,并将执行权移交引导扇区上的bootsect.s。
前面说到bootsect.s执行期间会将自己移动到0x90000开始处并继续执行,而bootsect.s的主要作用是将setup.s模块加载到0x90200开始处,然后利用BIOS中断0x13取磁盘参数表中的引导盘参数。接着在屏幕上打印Loading system…。然后将setup.s后面的system模块加载到0x10000开始处。随后确定根文件系统的设备号(如果没有指定根文件设备号,则根据引导盘的每磁道扇区数判别盘的类型(是否为1.44M A盘)),并将其保存在root_dev(引导块的508地址处)。最后长跳转到setup.s程序开始处(0x90200)执行setup.s。
总的来说,bootsect.s做了四件事情:
1 将自己移动到0x90000开始处,并加载setup.s到0x90200处。
2 利用BIOS中断0x13取磁盘参数表中启动引导盘的参数 加载setup.s模块到0x90200处,加载system模块到0x10000处
3 将根文件设备号存在root_dev中。
4 长跳转到setup.s处开始执行setup.s
Linux0.11内核在1.44M磁盘所占扇区的分布情况如下图:
boot扇区占一个扇区,setup模块占四个扇区。system模块占240个扇区。而1.44M盘共有2880个扇区。剩下的没有用到的扇区用来存放一个基本的根文件系统。
86as汇编
一句86as汇编代码通常由标号(可选)、指令名、操作数组成。标号通常用冒号结尾。如begdata:。
.globl和.global用于定义随后的标识符是外部的还是全局的。
.text、.date、.bss分别用于定义当前代码段、数据段和未定义数据段。
ds和es
:ds(data segment register)数据寄存器,用于存储程序的全局变量和静态变量。es(extra segment register)用于存储其他数据段,如常量、字符串、数组等。ax和bx
:ax是累加器寄存器,一般用于保存计算的中间结果或函数返回值。bx是基址寄存器,用于存储偏移地址。cx
:计数器寄存器,用于循环控制。sub
目标操作数,原操作数:目标操作数=目标操作数-原操作数
rep,结合ax使用,ax用于循环控制,rep是重复执行并递减ax,直至ax=0。需要注意的是,rep执行循环的语句是紧跟其后的一条语句,例如在Linux源码中:
mov cx,#256
sub si,si
sub di,di
rep
movw
重复的部分只有紧跟rep后的movw。也就是将原地址的256个字复制到目标地址di中。
si和di
:si用于存储原数据地址或偏移量,di用于存储目标地址或偏移量。
cs寄存器存储代码段的基地址。
ds寄存器存储数据段的基地址。
es寄存器通常用于存储额外的数据段基地址。
ss寄存器存储栈段的基地址。
寄存器名字 | 作用 |
ax | 通用,累加器 |
bx | 通用,存储偏移地址 |
cx | 通用,计数器 |
si | 原地址 |
di | 目标地址 |
ss | 段寄存器,堆栈寄存器 |
cs | 段寄存器,代码段寄存器 |
ds | 段寄存器,数据段寄存器 |
sp | 通用寄存器,存堆栈偏移量 |
bp | 通用寄存器,存堆栈基地址 |
dx | 通用,数据寄存器,存临时数据 |
代码名称 | 代码基地址 | 代码长度 |
bootseg | 0x7c00 | |
bootsect.s(initseg) | 0x90000 | 0x200(1个扇区) |
setup.s | 0x90200 | 四个扇区 |
system模块 | 0x10000 | 240扇区 |
jnc:有条件jmp,如果CF标志寄存器为0则跳转,否则继续
gas汇编语法记录
div
// 语法:div operand
__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
"r" (hd_info[dev].sect));
operand为被除数。而除数则需要放在紧随着div指令的另一个操作数中。商保存在被除数所在的寄存器中,余数则保存在紧随着该寄存器的另一个寄存器中。
在这句代码中,block被保存在eax寄存器中,而hd_info[dev].sect则作为除数的操作数直接放在divl指令的操作数中。:“0” (block),“1” (0),表示将block值传入0个操作数,将0传入第一个操作数