本章包含了三个汇编文件,本部分主要介绍bootsect.s.
目录
3.1 概述
3.2 总体功能
3.3 bootsect.s 程序
3.3.1 功能描述
3.3.2 代码片段分析
3.3.3 整个代码流程
3.3.4 附相关源代码中文注释版
本章主要描述 boot/目录中的三个汇编代码文件,见列表 3-1 所示。正如在前一章中提到的,这三个文件虽然都是汇编程序,但却使用了两种语法格式。bootsect.s 和 setup.s 采用近似于Intel 的汇编语言语法,需要使用Intel 8086 汇编编译器和连接器 as86 和 ld86,而 head.s 则使用 GNU 的汇编程序格式,并且运行在保护模式下,需要用GNU 的 as 进行编译。这是一种 AT&T 语法的汇编语言程序。使用两种编译器的主要原因是由于对于Intel x86处理器系列来讲,GNU的编译器仅支持i386及以后出的CPU。不支持生成运行在实模式下的程序。
阅读这些代码除了你需要知道一些一般8086汇编语言的知识以外,还要对采用 Intel 80X86 微处理器的PC机的体系结构以及 80386 32位保护模式下的编程原理有些了解。
这里先总的说明一下 Linux 操作系统启动部分的主要执行流程。当 PC 的电源打开后,80x86 结构的 CPU 将自动进入实模式,并从地址 0xFFFF0 开始自动执行程序代码,这个地址通常是ROM-BIOS 中的地址。PC 机的 BIOS 将执行某些系统的检测,并在物理地址 0 处开始初始化中断向量。此后,它将可启动设备的第一个扇区(磁盘引导扇区,512 字节)读入内存绝对地址 0x7C00 处,并跳转到这个地方。启动设备通常是软驱或是硬盘。这里的叙述是非常简单的,但这已经足够理解内核初始化的工作过程了。
Linux 的前面部分是8086 汇编语言编写的(boot/bootsect.s),它将由BIOS读入到内存绝对地址0x7C00(31KB)处,当它被执行时就会把自己移到绝对地址 0x90000(576KB)处,并把启动设备中后2kB 字节代码(boot/setup.s)读入到内存 0x90200 处,而内核的其它部分(system 模块)则被读入到从地址0x10000开始处,因为当时 system模块的长度不会超过0x80000字节大小(即 512KB),所以它不会覆盖在0x90000处开始的bootsect 和setup 模块。后面 setup 程序将会把 system 模块移动到内存起始处,这样system 模块中代码的地址也即等于实际的物理地址,便于对内核代码和数据的操作。图 3-1 清晰地显示出Linux系统启动时这几个程序或模块在内存中的动态位置。其中,每一竖条框代表某一时刻内存中各程序的映像位置图。在系统加载期间将显示信息"Loading..."。然后控制权将传递给 boot/setup.s中的代码,这是另一个实模式汇编语言程序。
启动部分识别主机的某些特性以及vga卡的类型。如果需要,它会要求用户为控制台选择显示模式。然后将整个系统从地址 0x10000 移至 0x0000 处,进入保护模式并跳转至系统的余下部分(在 0x0000 处)。此时所有32位运行方式的设置启动被完成: IDT、GDT 以及 LDT 被加载,处理器和协处理器也已确认,分页工作也设置好了;最后调用init/main.c中的main()程序。上述操作的源代码是在 boot/head.S 中的,这可能是整个内核中有诀窍的代码了。注意如果在前述任何一步中出了错,计算机就会死锁。在操作系统还没有完全运转之前是处理不了出错的。 为什么不把系统模块直接加载到物理地址 0x0000 开始处而要在 setup 程序中再进行移动呢?这是因 为在 setup 程序代码开始部分还需要利用 ROM BIOS 中的中断调用来获取机器的一些参数(例如显示卡 模式、硬盘参数表等)。当 BIOS 初始化时会在物理内存开始处放置一个大小为0x400 字节(1Kb)的中断向量表,因此需要在使用完 BIOS 的中断调用后才能将这个区域覆盖掉。
bootsect.s 代码是磁盘引导块程序,驻留在磁盘的第一个扇区中(引导扇区,0磁道(柱面),0 磁头,第 1 个扇区)。在 PC 机加电 ROM BIOS 自检后,引导扇区由 BIOS 加载到内存 0x7C00 处,然后将自己移动到内存0x90000 处。该程序的主要作用是首先将setup 模块(由setup.s编译成)从磁盘加载到内存, 紧接着 bootsect 的后面位置(0x90200),然后利用 BIOS 中断 0x13 取磁盘参数表中当前启动引导盘的参数,接着在屏幕上显示“Loading system...”字符串。再者将 system 模块从磁盘上加载到内存 0x10000 开始的地方。随后确定根文件系统的设备号,若没有指定,则根据所保存的引导盘的每磁道扇区数判别出盘的类型和种类(是1.44M A盘吗?)并保存其设备号于root_dev(引导块的0x508地址处),后长跳转到 setup 程序开始处(0x90200)执行 setup 程序。
bootsec.s代码移动到ox9000中:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movsw
关于cx的值,由于是要移动一个扇区的数据,一个扇区的大小是512B,这里的cx=256,但是movsw表示移动两个B,所以256*2=512B。
关于load_setup:
mov dx,#0x0000
mov cx,#0x0002
mov bx,#0x0200
mov ax,#0200+SETUPLEN
int ox13
仔细看int 0x13这个中断的各个参数,这里比较重要的是es:bx 将指向setup模块的在内存中的位置。在移动bootsec.s的代码最后有一个jmpi go,INITSEG这个指令,此时的cs=0x900不再是0x7c00了,es=cs,于是es:bx指向了0x900:0200处,从而实现了装载setup模块的功能。
关于ok_load_setup:
seg cs
mov sectors,cx
这里主要是来谈谈这两条指令。从代码的第241行中可以看出sectors是个标量指向一个word长的地址。seg cs表明了sectors的段地址是cs,而不是ds。而且seg cs 的作用范围只有下一行,不会延生到其他地方。
最后介绍这篇文章中最重要的read_it,她主要的功能是读磁盘上 system 模块,用es作为输入参数:
read_it:
mov ax,es
test ax,#0x0fff
die:
jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9
ok2_read:
call read_track
mov cx,ax
add ax,sread
seg cs
cmp ax,sectors
jne ok3_read
mov ax,#1
sub ax,head
jne ok4_read
inc track
ok4_read:
mov head,ax
xor ax,ax
ok3_read:
mov sread,ax
shl cx,#9
add bx,cx
jnc rp_read
mov ax,es
add ax,#0x1000
mov es,ax
xor bx,bx
jmp rp_read
read_track:
push ax
push bx
push cx
push dx
mov dx,track
mov cx,sread
inc cx
mov ch,dl
mov dx,head
mov dh,dl
mov dl,#0
and dx,#0x0100
mov ah,#2
int 0x13
jc bad_rt
pop dx
pop cx
pop bx
pop ax
ret
bad_rt:
mov ax,#0
mov dx,#0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
1.ok1_read:
主要的功能是确定了ax的值,其实严格的说是确定了al的值,因为al的值是代表了要读取的扇区的值,关于最后的几行代码
xor ax,ax
sub ax,bx
shr ax,#9
这几行代码是以后执行的,到时后bx的值不再是0,而是代表了一个段内读取了的数据,sub是不带进位的0-bx正好是64k-段内已读的数据(不得不佩服linus的基本功),从而得到了段内剩余的空间,从而可以求出还可以读取最大的扇区数。
2.ok2_read:
调用了read_track(),read_track()的功能其实可以看成read_track(ax),根据ax,主要是al来确定一个磁道内从哪个扇区开始读数据,从而读取一个磁道的数据;然后ax=read_track(ax)(伪代码而已),ax表示读取的扇区的值,然后再加上已读的扇区数后比较是否等于每个磁道的扇区数,如果不等,则调用ok3_read(),否则读取下一个磁头1,(软盘有两个磁头,0和1,这和硬盘不一样,硬盘磁头比较多)
3.ok4_read:
ok_read4的主要的功能是重新赋值磁头和ax即扇区的起始地址。
4.ok3_read:
主要是确定了bx的值和es的值。
mov sread,ax
shl cx,#9
add bx,cx
jnc rp_read
mov ax,es
add ax,#0x1000
mov es,ax
xor bx,bx
jmp rp_read
这里add bx,cx 是确定了bx的值,即段内已经读取的数据大小,如果bx没有溢出即超过0xffff,则跳转到开始的循环,否则段基址es+=0x1000,再重新循环。
由于软盘只有两个磁头0和1,且只有18个扇区,每个扇区的大小是512B从而代码开始处的流程应该是如下的:
1.在ok1_read中从0磁头读取的数目最多(18-6)*512B,不会溢出,则会进入到ok2_read()中;如果bx不等于0,且数值比较接近0xffff时会溢出,则确定该段内剩余的地址可以读取最大的扇区数;
2.到ok2_read中,从0磁头读取从能够ax开始的整个磁道剩余的扇区数,cx=读取的扇区数,在加上已读的扇区数确定是否全部读完,若是则下一步,否则到4;
3.到ok4_read,磁头变成了1,(只有两个磁头,如果原磁头是1,则增加磁道track),ax=0。
4.到ok3_read中,此时ok3_read的操作是针对下一次循环的,根据这一次的循环设置下一次循环的一些变量,bx=段内已经读取的数据的大小,如果超出64k则增加段基址,由于add是无进位的(这里又体现了linus的高明了);
5.重新返回到1,执行循环,知道es的值>ENDSEG;
源代码中先是ok4_read了,而后是ok3_read,这是因为如果在源代码中把ok3_read放在ok4_read之前那么代码会多加几个jmp命令。
链接:https://pan.baidu.com/s/1P-rM8bDpnSmGN4yUF7vZXA
提取码:ecv6