文章目录
第一章——系统启动,打开电源后发生的故事
一、计算机的工作机理
指令存储在内存中,让然后取址——执行。
二、第一阶段
计算机加电后,PC被初始化成了地址0xFFFF0,它指向了内存的只读存储器(ROM)的一段区域,这段区域被称作BIOS
这段区域中防止了对于硬件测试的基本代码,如果测试全部通过,那么就使用BIOS的输入功能将启动磁盘的启动扇区的内容读入到内存0x7c00地址处,并设置寄存器CS,IP
接下来将执行启动扇区(0号柱面、0号磁头、1号扇区)中的程序
1.启动扇区上存放了什么程序呢?
(1)一号扇区
存放了bootsect.s程序代码bootsect代码的主要用处就是将程序搬运到0x90000地址处,并且读入setup.s程序
(a)搬运代码
下面的代码是将程序从0x7c00地址处,搬运到0x90000地址处
BOOTSEG=0x7c00
INITSEG=0x9000
entry _start
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
这一段代码是什么意思呢?
首先BOOTSEG和INITSEG可以理解为c/c++中的常量,是确定不变的。BOOTSEG表示程序的源地址,INITSEG表示程序的目标地址。
在下面的程序中,BOOTSEG和INITSEG由ax分别赋值给了ds,es。
ds表示了程序的源地址的段地址,es表示了程序的目标地址
而显然的,只靠段地址不能表示出物理地址。
所以,我们要知道ds:si表示源物理地址,es:di表示程序的目标物理地址。si是源变址寄存器,di是目的变址寄存器。
到此,程序的转移源头和目标都确定了,就像导航一样,给出了我的位置和目标位置,就出现了到达目标位置的路径了,接下来就是搬运操作。
这里要说一下cx,cx可以理解为程序的计数器,就像c/c++的for循环的限制条件一样,一旦cx为0了,意味着循环结束。
rep指令意思是重复字符串操作,并且它只能作用于一条指令。(loop可以作用于一段指令)
movw就是搬运字符的操作了,展开是mov word,也就是按字搬运,一个字一个字的搬运。到这里就可以解释一下为什么把256赋值给cx了,因为movw是按字搬运,而一个扇区有512个字节,也就是256个字(在16位实模式下一个字是两个字节)
读到这里,有的人可能会疑惑,什么是实模式呢?后面的setup会提到。
程序执行到了这里,搬运操作就完成了,内存0x7c00处就就没有代码了,然后我们就需要跳转到搬运的位置才能继续操作,也就是jmpi语句jmpi是段间跳转指令,也就是远跳,go赋给IP地址,INITSEG赋给CS地址
好了,这部分的程序转移已经结束了,为什么要转移到0x9000+go地址的位置呢,因为是为了给操作系统的代码留出足够空间。
至此,操作地址变成了0x9000+go
0x9000+go的地址指向了一个go标签,接下来要执行go标签中的语句了
(b)载入setup.s
SETUPLEN=4
go:mov ax,#INITSEG
mov ds,ax
mov es,ax
mov ss,ax
mov sp,#0xFF00
load_setup:
mov dx,#0x0000
mov cx,#0x0002
mov bx,#0x0200
mov ax,#0x0200+SETUPLEN
int 0x13
jnc ok_load_setup
mov dx,0x0000
mov ax,0x0000
int 0x13
j load_setup
首先对要用到的寄存器进行初始化操作
我们看ss和sp这是什么意思呢,由代码可以看到,ss=0x9000,sp=0xFF00,组合ss是栈的地址,而sp是栈顶指针的地址,也就是压栈的时候数值压到0x9FF00的地址处,FF00H转化为十进制非常大,是65280,即偏移量为65280个字节,sp的地址其实就是选取了一个距离要操作的地方的一个比较远的位置,防止对操作位置的数据进行修改。
后面就是要读入setup.s了
dh=0x00——》表示要读的磁盘扇区所在的磁头号为0
dl=0x00——》表示要读入扇区所在的驱动器号为0
es:bx=0x90200——》表示从磁盘读入的内容要放到的内存的起始地址
这里的地址为什么这么弄呢?
因为bootsect.s的大小刚好是512个字节(一个扇区的大小就是512个字节),而偏移量200在十进制中就是512,所以这个地址将setup.s的代码放置在bootsect.s的后面
ah=0x02——》是BIOS中断的功能号,表示要从磁盘读入内容
al=0x0004——》表示要读入4个扇区(setup.s占四个扇区)
cl=0x02——》表示要读的磁盘从二号扇区开始,ch=0x00表示要读的磁盘扇区所在的柱面号为0
int 0x13结合ah=0x02表示开始读入指定的磁盘内容(由ch,cl,dh,dl共同决定)
jnc 表示什么意思呢?
jnc是当加法没有仅为,减法没有错位的时候转移(cf=0),这里大概就是当软盘/硬盘出现问题的时候才不会执行,不清楚具体的原理(有没有大佬给我讲一下qwq)
jnc不执行跳转的话就到下面,dx=0x0000,ax=0x0000,这里是重置磁盘,int 0x13的功能号ah=0x00表示充置磁盘的操作。
到了这里载入setup.s的语句执行完毕
(c)载入system
什么时候才可以载入system呢?
显然应该在setup.s载入完成以后再载入system。因为system的转移需要用到setup.s,可以说setup.s是system的先决条件
~1.取磁盘参数
先不要急,我们不能直接载入system,因为system比一个磁道的内容要大得多,我们要先获取一些硬盘参数来计算需要读取的磁道数量。
ok_load_setup:
mov dl,#0x00
mov ah,#0x08
int 0x13
mov ch,#0x00
and cl,#0x3F
mov sectors,cx
sectors: .word 0 //这种函数要放在全部的代码最后
dl的作用在上文了解过了,这里不再赘述
ah=0x08——》0x08功能号调用int 0x13中断的作用是读取驱动器和磁盘的参数
cl——》每个磁道的扇区个数存放在cl的低六位当中,这里的and是与运算,and cl,0x3F表示保留cl的低六位然后我们将cl的内容转存到sectors标号下的字里面。
.word 0就是将字初始化为0
磁盘参数获取完毕
~2.设置操作系统的存放位置
磁盘参数读取完毕了,我们知道了一个磁道会有几个扇区以后,就可以开始读入操作系统主体了
SYSSEG=0x1000
mov ax,#SYSSEG
mov es,ax
xor bx,bx
设置好了操作系统的读入地址为es:bx——》0x10000处
为什么选在这里呢?
因为操作系统的大小为524288个字节,换算为16进制就是80000
bootsect.s转移位置以后留出的空间刚刚好够80000
~3.读取操作系统的主体循环
操作系统的读入位置确定好了以后就要正式开始读入操作系统了,因为操作系统占据了很多磁道,而我们能够确定一个磁道拥有多少个扇区,所以我们可以利用循环,一个一个磁道来读取。
SYSSIZE=0x3000
ENDSIZE=SYSSIZE+SYSSEG
rp_read:cmp ax,#ENDSEG
ja end_read
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok_read
je ok_read
xor ax,ax
sub ax-bx
shr ax,#9
ok_read:call read_track
sread: .word 1+SETUPLEN //bootsect.s和setup.s占据的五个扇区
head: .byte 0
track: .word 0
SYSSIZE=0x3000将操作系统大小设定为了12288个字节
ENDSIZE标记出了操作系统读入结束的位置,ax在设置操作系统读入位置的时候设置为了0x1000,在rp_read循环开始时ENDSIZE与ax进行比较,如果ax超过了结束标记,说明操作系统读入完成,这时候退出循环
sub ax,sread指出了当前磁道剩余要读入的扇区数量,将这个数量存入cx中,因为ax会更改(总感觉cx有点像工具人)
shl cx,#9将cx的值左移九位,相当于乘了512,扇区数量*512得出的是当前磁道的剩余字节数
接下来,我们将cx加到bx中,判断bx是否溢出,这个判断十分重要,因为es:bx组成的是读入的地址,如果会溢出的话,那么有些数据es:bx不知道往哪里存放,就出事了。
jnc和je都是如果没有溢出就读取磁道
如果溢出就执行下面的语句,将ax清零,然后用0去减bx,得到的结果相当于64kb-bx(这里我也不清楚为什么可以这样),存入ax中,
这里得到的ax是字节数,要向右移9位,也就是除以512,得到这次要读入的扇区数(确保bx不会溢出)
head——》磁头号
track——》磁道号
~4.循环完成一次以后,就要读取这一个磁道的内容了
读取磁道内容就要用到BIOS的int 0x13中断,也要用到ah=0x20的功能号,所以接下来的代码就是要将,磁道号,磁头号,起始扇区号录,驱动器号,录入寄存器当中准备
ch——》放置磁道号
cl——》放置起始扇区号
dl——》放置驱动器号
dh——》放置磁头号
read_track:
push ax
push bx
push cx
push dx
mov dx,track
mov cx,sread
inc cx
mov ch,dl
mov dl,#0
mov ah,#2
int 0x13
pop dx
pop cx
pop bx
pop ax
ret
结合之前所学的内容,我们大致可以总结得到,cx可以存储读取磁盘部分的信息,dx存储读取磁盘所需硬件的信息
然后这些将寄存器元素压入栈的操作,是为了保护局部变量,mov,inc等操作改变的不是被压入栈的寄存器的值。这是很重要的,因为后边的操作还需要用到这些信息,必须确保不会被改变,然后pop操作将存储的寄存器数据重新弹出到寄存器内,注意这里是和压栈时相反的顺序,因为栈的特性。
ret返回调用函数的地址,并执行下一语句
~为下次读磁盘做出判断
这里需要做什么判断呢,首先就是判断当前磁道有没有读完;如果读完了,再判断当前磁道是上磁道还是下磁道,如果是上磁道,就继续读取下磁道;如果当前磁道是下磁道,就转入下一个磁道,并将磁头置为0(上面的磁头号)。
mov cx,ax
add ax,sread
compare ax,sectors
jne goon_read
mov ax,#1
sub ax,head
jne nexthead_read
inc track
nexthead_read: mov head,ax
xor ax,ax
add ax,sread的目的是,得到当前读取的扇区总数,然后和一个磁道的扇区总数比较,如果大于或等于,说明一个磁道读完,则转向下一个磁道,后面的语句判断磁道到底应该怎么转。
最后的xor ax,ax将ax清零 ,因为能走到这里,说明一个磁道已经读完,那么就应该将已读取的磁道数也就是ax中寄存的值清零,重新开始
~继续读入
执行到这里的路径有三条:
(1)当前的磁道还没有读完
(2)不改变磁道的情况下使用下面的磁道
(3)开始读入下一个磁道
SETUPSEG=0x9020
goon_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
jump rp_read
end_read:
jmpi 0,SETUPSEG
.org 510
.word 0xAA55
这里的add ax,#0x1000 mov es,ax就是将es移位64kb大小,因为es左移四位以后是从0x10000变成了0x20000,增加了10000,转为十进制就是2的16次方个字节。也就是64kb
最后的.word 0xAA55是块结束的签名