linux0.11内核分析-bootsect

章节目录

第一步:BIOS启动,加载bootsect

电脑加电启动后,RAM还没有数据,由于CPU只能执行内存中的程序指令,现在还没有数据,是不是CPU很懵逼?其实不是的,在开机的一瞬间,也就是接电的一瞬间,CPU 的CS:IP 寄存器被强制初始化为0xF000:0xFFF0,因为这个时候还是实模式,CS:IP指向0xFFFF0,此地址便是BIOS 的入口地址。

图1

8086CPU启动的时候还是实模式,0xFFFF0地址范围就落在BIOS范围内了。

图2
实模式下的内存布局

接下来就是执行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/

你可能感兴趣的:(linux0.11内核分析-bootsect)