第一步:BIOS启动,加载bootsect
电脑加电启动后,RAM还没有数据,由于CPU只能执行内存中的程序指令,现在还没有数据,是不是CPU很懵逼?其实不是的,在开机的一瞬间,也就是接电的一瞬间,CPU 的CS:IP 寄存器被强制初始化为0xF000:0xFFF0,因为这个时候还是实模式,CS:IP指向0xFFFF0
,此地址便是BIOS 的入口地址。
8086CPU启动的时候还是实模式,0xFFFF0
地址范围就落在BIOS范围内了。
接下来就是执行BIOS程序,我们不可能把BIOS每一步都在干啥写出来,跳过一些步骤,直接看重点,自检后,将0号磁头对应的盘面0磁道的1扇区(扇区的编号是从1开始的)加载到0x07c00
地址处,这里加载了1个扇区(512字节)的数据,这样设计的好处是BIOS只负责加载该扇区的数据,至于该扇区里面是什么,BIOS并不关心,只有这样,我们才能随心所欲的安装操作系统。
第二步:bootsect接管cpu,复制bootsect到 0x90000位置
接下在就可以执行我们的bootsect.s里面的代码了。将bootsect的全部512字节从0x07c00地址复制到 0x90000处
代码路径 boot/bootsect.s
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
把0x07c0处数据以字(2个字节)为单位复制到0x9000处
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
这里说明下为什么不直接把#BOOTSEG传递给ds,因为有历史的原因,关于CPU,不得不提Intel,Intel的处理器不允许将一个立即数传递到段寄存器,只允许用其他寄存器中转。
通用寄存器cx一般用于循环的控制,256字也就是512字节。
将si与di寄存器清0,这样ds:si = 0x07c0:0 ,es:di = 0x9000:0。
rep 循环执行一条指令,每次循环寄存器cx的值都会自动减1,直到cx = 0。
movw的功能是将ds:si指向的内存单元中的字
送入es:di中,因为这里操作数是字,所有执行movw指令后,si和di都会自增或者自减2,标志寄存器DF标志控制自增与自减
如果DF = 0,si 、di就会自增
如果DF = 1,si 、di就会自减
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
jmpi段间跳转后CS的值就是INITSEG的值( 0x9000),标号go表示的是一个偏移地址,这样CPU又继续往下执行了,要不然还在执行0x07c0段地址处的代码,
注意此时的ds的值是 0x9000,后面取数据依赖ds寄存器,这里给es设置值,因为后面加载setup时候会用到这个值。
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
数据被复制到0x90000处,不能再用原来的栈了,这里需要重新设置栈,SS:SP组合在一起指向栈顶,栈的生长方向是从高地址向低地址方向,也就是说入栈(push)时SP的值会自动减去一个字(2个字节),出栈(pop)时SP的值会自动加上一个字。
第三步: 由bootsect加载setup
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
这里通过bios 13号中断,因为bios 13号中断功能很多,这里用到了02号功能,即读取磁盘数据,想要读取磁盘数据,就需要直到哪个磁盘,哪个磁头,哪个磁道,哪个扇区,这些参数都是通过寄存器传递的。
ah=02表示读取扇区数据,注意扇区的编号是从1开始的,
al表示需要读取的扇区数量,这里#0x0200+SETUPLEN
, ah=0x02H,al = SETUPLEN,也就是al=4,需要读取4个扇区。
dl表示哪个驱动器,00H ~ 7FH:软盘;80H~0FFH:硬盘。
dh表示磁头,磁头的编号是从0开始的,这里就是0号磁头。
ch表示读取的扇区在哪个柱面,也就表示数据在哪个磁道。
cl表示从哪个扇区开始读取,因为前面加载bootsect时已经读取了一个扇区,所以这里从编号2的扇区开始读取。
读取的数据需要往内存中放,ES:BX表示这个缓冲区。
读取后输出的结果参数放在哪呢?还是放在寄存器中,
CF=0表示读取成功,AH=00H,AL=传输的扇区数,否则,AH=状态代码
jnc ok_load_setup
表示如果CF = 0,则跳转到标号ok_load_setup处执行。
如果失败就复位磁盘,重新读取。
这里还用到bios 13号中断的另一个功能,ah=0x00H,复位磁盘。
dl表示哪个驱动器,00H ~ 7FH:软盘;80H~0FFH:硬盘。
返回参数:CF=0表示操作成功,AH=00H,否则,AH=状态代码
j load_setup
跳转到标号load_setup处执行。
第四步: 读取与显示驱动器参数
这里最重要的是读取每个磁道的扇区数,因为后面加载system需要用到这个参数。
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs
mov sectors,cx
mov ax,#INITSEG
mov es,ax
这里还是用到bios 13号中断,08H功能,读取驱动器参数
参数:
ah=08H
dl表示哪个驱动器,00H ~ 7FH:软盘;80H~0FFH:硬盘。
返回参数: CF=0 操作成功
BL =驱动器类型
CH=柱面数的低8位
CL =扇区数(位 0-5 ),柱面数的高2位(位6-7)
DH=磁头数
DL=驱动器数
ES:DI=磁盘驱动器参数表
否则 CF=1 操作失败,AH=状态代码
mov ch,#0x00
cx的高位清0,这里读取的是软盘,1.44M的软盘柱面数是80,所以cl的高两位肯定是0,因为ch已经足够保存柱面数,所以cl的值就是每个柱面的扇区数。
seg cs只会影响接下来的指令,
seg cs
mov sectors,cx
就如同
mov cs:[sectors],cx
因为获取磁盘参数改掉了es的值,这里重新改回来
第五步: 由bootsect加载system
加载system需要240个扇区,加载的位置在0x10000。
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
call kill_motor
将段地址0x1000送入es中,
跳转到read_it标号处执行,call指令还会把当前的IP压栈,类似调用方法
read_it:
!es已经被初始化为0x1000
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
!bx 为段内偏移,清0,这样es:bx = 0x1000:0
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
!后面会对es进行修改,ax表示当前的段地址,比较当前的段地址是否就是ENDSEG所处在段。
cmp ax,#ENDSEG ! have we loaded all yet?
!如果不是,就跳转到ok1_read读取扇区数据
jb ok1_read
ret
ok1_read:
! 段超越,此时cs的值是0x9000
seg cs
! 这个sectors参数是我们通过读取磁盘参数得到的每个磁道的扇区数量
mov ax,sectors
! 减去已经读取扇区的数量,因为一开始已经读取5个扇区(加载bootsect的1个扇区,加载setup的4个扇区)
sub ax,sread
!ax 里面的值就是还需要读取的扇区数量
mov cx,ax
!计算需要的字节数,左移9位就是乘以512
shl cx,#9
!在 xor bx,bx这个步骤中bx已经被清0
add cx,bx
!jump not carry ,即cf=0跳转,说明读取成功后,最后一个字节没有超越es:bx所能表示的范围
jnc ok2_read
! zf=1跳转,说明读完后刚好等于当前es:bx所能表示的最大范围+1
je ok2_read
!读取后已经超过段地址
xor ax,ax
sub ax,bx
!ax记录着该段里面还能读取的最大扇区数
shr ax,#9
ok2_read:
! 读取某个柱面的多个扇区,需要读的扇区放在al中
call read_track
!ax 就是刚才读的扇区数量
mov cx,ax
! 累加当前已经读取的扇区数量
add ax,sread
seg cs
!查看这个磁道的扇区是否已经读完
cmp ax,sectors
!还没有读完则跳转到ok3_read处继续读取
jne ok3_read
!等于则说明已经读完一个一个磁道,读取下一个磁头上的数据(1号磁头
mov ax,#1
!判断当前磁头号
sub ax,head
!zf=0则跳转,如果是0磁头,再去读1磁头面上的扇区数据
jne ok4_read
!否则去读下一个磁道
inc track
ok4_read:
!保存当前磁头号
mov head,ax
!清0
xor ax,ax
ok3_read:
! 更新已经读取的扇区数,ax保存了已经读取的扇区数
mov sread,ax
!cx里面的值就是已经读取的扇区数,转换为总字节数
shl cx,#9
!更新bx的值,es:bx指向的内存存放着从软盘读取的数据
add bx,cx
!cf=0则跳转,说明还没有超越es:bx所能指向的内存范围,跳转rp_read继续读
jnc rp_read
!说明刚才读取的数据超过了当前es:bx表示的范围,更新es和bx的值
!
mov ax,es
!es加64KB,到下一个段地址
add ax,#0x1000
mov es,ax
!因为开始一个新的段地址了,bx清0
xor bx,bx
!继续读
jmp rp_read
read_track:
! ax 保存着需要读取的扇区数量
push ax
push bx
push cx
push dx
! 磁道号,初始的时候是0
mov dx,track
!已经读取的扇区数量
mov cx,sread
!加1,表示即将读取的扇区号,
inc cx
!磁道号
mov ch,dl
mov dx,head
!磁头号
mov dh,dl
!驱动器号,为0表示当前A驱动器
mov dl,#0
!保证磁头号不大于1
and dx,#0x0100
!ah=0x2H,读取磁盘扇区,al中保存这需要读取的扇区数量
mov ah,#2
int 0x13
!jump carry,表示进位则跳转,即cf=1时跳转,cf=1表示读取失败
jc bad_rt
pop dx
pop cx
pop bx
pop ax
ret
! 磁盘系统复位,跳转到read_track重新读取
bad_rt: mov ax,#0
mov dx,#0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
来看看这个test ax,#0x0fff
是在干嘛的,我找了不少网上资料,可能是我比较笨,根本看不懂在说什么,以下就是我自己的理解:
此时es的值是0x1000H,在8086CPU启动的时候还是实模式,只能访问1MB的内存地址,因为是20根地址线,2的20次方就是1MB,但是寄存器是16位的,2的16次方也就是64KB的内存地址,怎么才能访问1MB的地址呢?解决办法是用2个寄存器表示内存地址,一个寄存器左移4位 + 另一个寄存器这样就得到一个20位的地址,这就是段地址 + 偏移地址。
我们得到:物理地址 = 段基址<<4 + 段内偏移
在不允许段之间重叠的情况下,每个段的最大长度是64KB,因为偏移地址也是16位的,从0000H到FFFFH。在这种情况下,1MB的内存,最多只能划分成16个段,每段长64KB,段地址分别是0000H、1000H、2000H、3000H、...,一直到F000H。
这样一个段最大就是2的16次方,就是64KB,上面代码也说了,加载的时候,需要在64KB的边缘,也就是起始位置,就像火车车厢一样,一节一节的,总不能把一节车厢放到另一节车厢里面吧,检查es的值都是按照64KB对齐,那么段地址都是64KB的倍数,此时段寄存器es=0x1000H,test ax,#0x0fff结果为0,则ZF=1。不满足JNE跳转条件(ZF=0)
如果这个地方es不是64KB的倍数,那就死机了die: jne die
。
kill_motor:
push dx
mov dx,#0x3f2
mov al,#0
outb
pop dx
ret
关闭软驱的马达,#0x3f2软驱控制卡的驱动端口。
seg cs
!跟设备号,0x306
mov ax,root_dev
cmp ax,#0
! 不等于0说明定义了,跳转到root_defined
jne root_defined
seg cs
mov bx,sectors
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
!保存设备号
mov root_dev,ax
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
jmpi 0,SETUPSEG
软盘的主设备号是2,次设备号是 type * 4 + n,(n = 0-3)
如果sectors = 15则说明是1.2Mb的驱动器
如果sectors = 18则说明是1.44Mb的驱动器
不是上面2个的驱动器,就死机了。
到此所有程序都加载完毕,接着跳转到jmpi 0,SETUPSEG
处开始执行setup程序了。
此时CS:IP = 0x9020:0000
下一章节分析setup程序
参考:https://web.archive.org/web/20160619063203/http://www.ctyme.com/intr/rb-0621.htm#Table242
图1来源 https://blog.codinghorror.com/dude-wheres-my-4-gigabytes-of-ram/
图2来源 https://www.programmersought.com/article/90352129718/