bootsect.s 分析—— Linux-0.11 学习笔记(一)

bootsect.s分析—— Linux-0.11学习笔记(一)

为了节省篇幅,完整的代码就不贴了。感兴趣的朋友可以去下载,下载地址是:
http://oldlinux.org/Linux.old/

本文,我打算详解bootsect.s。如有纰缪,还请各位看官斧正。关于如何讲好代码,我暂时没有找到什么好的展示方法。姑且贴一段、注释一段、讲一段吧。为了不使代码片太长,我删去了一些原来的注释。

一些符号常量


SYSSIZE = 0x3000  ;system模块的长度


.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

SETUPLEN = 4                ! setup模块的长度,4个扇区
BOOTSEG  = 0x07c0           ! original address of boot-sector
INITSEG  = 0x9000           ! bootsect把自身搬运到0x90000
SETUPSEG = 0x9020           ! setup模块被加载到 0x90200
SYSSEG   = 0x1000           ! system模块被加载到0x10000
ENDSEG   = SYSSEG + SYSSIZE ! where to stop loading, 0x1000 + 0x3000 = 0x4000, 停止加载的段地址

ROOT_DEV = 0x306            !第2个硬盘的第1个分区

ROOT_DEV = 0x306 ,这里的0x306表示第2个硬盘的第1个分区,当年Linus是在第2个硬盘的第1个分区上安装了Linux-0.11操作系统。

老式Linux设备号的命名规则

设备号 = 主设备号 * 256 + 次设备号

或者说:

dev_no = (major << 8) + minor

这里的主设备号是事先定义好的(1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道)。譬如对于硬盘,主设备号为3,因此3*256+0=0x300即为系统中第一个硬盘的设备号。更多的例子如下表:

设备号 设备文件 对应的设备
0x300 /dev/hd0 系统中第一个硬盘
0x301 /dev/hd1 系统中第一个硬盘的第一分区
0x302 /dev/hd2 系统中第一个硬盘的第二分区
0x303 /dev/hd3 系统中第一个硬盘的第三分区
0x304 /dev/hd4 系统中第一个硬盘的第四分区
0x305 /dev/hd5 系统中第二个硬盘
0x306 /dev/hd6 系统中第二个硬盘的第一分区
0x307 /dev/hd7 系统中第二个硬盘的第二分区
0x308 /dev/hd8 系统中第二个硬盘的第三分区
0x309 /dev/hd9 系统中第二个硬盘的第四分区

bootsect 把自己搬运到 0x90000,并跳转

entry _start
_start:
    mov ax,#BOOTSEG 
    mov ds,ax      !ds = 0x07c0
    mov ax,#INITSEG
    mov es,ax      !ex = 0x9000
    mov cx,#256    !搬运256次
    sub si,si      !si = 0
    sub di,di      !di = 0
                   !ds:si=0x07c0:0x0, es:di=0x9000:0x0
    rep
    movw           !每次搬运2个字节
    jmpi go,INITSEG   !跳转到 0x9000:go

以上代码表示把ds:si处(物理地址0x7c00)的内容搬运到es:di(物理地址0x90000),一共搬运512字节,即主引导扇区把自己移动到了0x90000处。

对于movw指令,可以参考我的博文。
http://blog.csdn.net/longintchar/article/details/50949923

我的疑问是,Linus为什么没有清除DF标志呢?是不是设置DF=0会更严谨呢?

jmpi go,INITSEG段间跳转,INITSEG是段地址,go是偏移地址。这句话执行完,CPU就一下子跑到了0x9000:go处执行了。(下图中左边的蓝色箭头,点击图片可放大)

bootsect.s 分析—— Linux-0.11 学习笔记(一)_第1张图片

跳转后继续执行下面的指令,设置ds,es,ss和sp.

go: mov ax,cs
    mov ds,ax
    mov es,ax     !ds=es=cs=0x9000
    mov ss,ax
    mov sp,#0xFF00  
                  !es:sp = 0x9000:0xff00 ,栈的设置  

加载 setup 模块到 0x90200

load_setup:
    mov dx,#0x0000      ! 驱动器号(DL)0,磁头号(DH)0
    mov cx,#0x0002      ! 起始扇区号2, 磁道号0
    mov bx,#0x0200      ! 偏移地址0x200
    mov ax,#0x0200+SETUPLEN ! 功能号AH=0x02,AL=要读的扇区数目=SETUPLEN=4 
    int 0x13            ! read it
    jnc ok_load_setup   ! ok - continue
    mov dx,#0x0000      !需要复位的驱动器号=DL=0
    mov ax,#0x0000      !功能号AH=0
    int 0x13            ! 复位磁盘
    j   load_setup

以上代码利用INT 13H, AH=02H把setup模块从磁盘(2~5扇区)加载到0x90200后面。
注意:柱面号和磁头号都从0开始,扇区号从1开始。

INT 13H AH=02H:读扇区

此功能从磁盘上把一个或更多的扇区内容读进内存。这是一个低级功能,在一个操作中读取的全部扇区必须在同一条磁道上。

参数 说明
入口参数
AH =02H ,指明调用读扇区功能。
AL 要读的扇区数目,不允许使用读磁道末端以外的数值,也不允许使该寄存器为0。
DL 需要进行读操作的驱动器号,0表示软盘,80H表示硬盘。
DH 所读磁盘的磁头号。
CH 磁道号的低8位数(磁道号共10位)。
CL 低5位放入所读起始扇区号,位7-6表示磁道号的高2位。
ES:BX 读出数据的缓冲区地址。
返回参数
CF =0,操作成功;=1,操作失败。
AH 错误返回码。
AL 实际读到的扇区数。

INT 13H AH=00H:磁盘控制器复位

此功能用于复位磁盘(软盘和硬盘)。当磁盘I/O功能调用出现错误时,需要调用此功能。

参数 说明
入口参数
AH =00H,指明调用复位磁盘功能。
DL 需要复位的驱动器号。软盘:00H-7FH;硬盘:80H-FFH
返回参数
CF =0,操作成功;=1,则操作失败
AH 错误返回码。

获得磁盘驱动器参数(主要是每磁道的扇区数量)

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track

    mov dl,#0x00    !驱动器号为0,说明是软盘
    mov ax,#0x0800  ! AH=8 is get drive parameters
    int 0x13
    mov ch,#0x00    !这里用不上软盘的最大磁道号,可以使CH=0
    seg cs          !把段超越前缀设置为cs,只影响下一条语句
    mov sectors,cx  
    !保存每磁道最大扇区数。对于软盘,最大磁道号不会超过256,所以CH足以表示,CL[7:6]为0
    !以上两句可以写为  mov cs:[sectors], cx
    mov ax,#INITSEG
    mov es,ax       !因为上面ES的值被修改,所以令ES=0x9000

INT 13H AH=08H:读取驱动器参数

参数 说明
入口参数
AH =08H,读取驱动器参数
DL 驱动器号(如果是硬盘则[7]=1)
返回参数
CF 0-操作成功;1-操作失败
AH 错误返回码
BL 驱动器类型
CH 最大磁道号的[7:0]
CL[7:6] 最大磁道号的[9:8]
CL[5:0] 每磁道最大扇区数
DH 最大磁头数
DL 驱动器数量
ES:DI 指向软驱磁盘参数表

打印 “Loading system …”

    mov ah,#0x03    !读光标的位置
    xor bh,bh       !bh=页号
    int 0x10

我们主要是用行号(DH中)和列号(DL中)。

INT 10H AH=03H:获取光标位置和形状

参数 说明
入口参数
AH =03H,读光标的位置
BH 页号
返回参数
CH 行扫描开始
CL 行扫描结束
DH 行号
DL 列号

INT 10H AH=13H:在Teletype模式下显示字符串

参数 说明
入口参数
AH =13H,在Teletype模式下显示字符串
BH 页码
BL 属性(若 AL=00H 或 01H)
CX 要显示的字符串的长度
DH、DL 坐标(行、列)
ES:BP 指向要显示的字符串
AL 显示输出方式
返回参数

对于显示输出方式,解释如下:

取值 说明 字符串格式
0 字符串中只含显示字符,显示属性在BL中;显示后,光标位置不变 char1,char2,……,charN
1 字符串中只含显示字符,显示属性在BL中;显示后,光标位置跟随字符串改变 char1,char2,……,charN
2 字符串中含有显示字符和显示属性;显示后,光标位置不变 char1,attri1,char2,attri2,……,charN,attriN
3 字符串中含有显示字符和显示属性;显示后,光标位置跟随字符串改变 char1,attri1,char2,attri2,……,charN,attriN
    mov cx,#24          ! 24个字符
    mov bx,#0x0007      ! page 0, attribute 7 (normal)
    mov bp,#msg1
    mov ax,#0x1301      ! write string, move cursor
    int 0x10
msg1:
    .byte 13,10
    .ascii "Loading system ..."
    .byte 13,10,13,10

13是回车,10是换行。它们的区别如下表。

回车和换行

中文名称 英文名称 字母简写 ASCII码 来源
回车 carriage return CR 0x0D=13D “车”指的是纸车,它带着纸向左移动。在开始打第一个字之前,要把纸车拉到最右边,使弹簧收紧。随着打字的进行,弹簧把纸车推向左边。把纸车拉到最右边,叫做“回车”。
换行 line feed LF 0x0A=10D 换行的概念是,打字机左边有个”把手”,扳动一下把手,纸就会上移一行。

加载 system 到 0x10000

! we want to load the system (at 0x10000)

    mov ax,#SYSSEG  ! SYSSEG=0x1000
    mov es,ax       ! segment of 0x010000
    call    read_it
    call    kill_motor

3~5行,把system模块加载到0x10000。

第6行,关闭驱动器马达。

过程read_it

这个过程的功能是把还未读取的扇区加载到es:0x0000处。注意:es必须是0x1000的整数倍,否则会陷入死循环。每读64KB,都会使es的值增加0x1000,当es=0x4000的时候,停止读取。

sread:  .word 1+SETUPLEN !当前磁道已经读取的扇区数, 前面的1表示引导扇区bootsect.s
head:   .word 0          ! current head,当前磁头号
track:  .word 0          ! current track,当前磁道号

read_it:
    mov ax,es
    test ax,#0x0fff     !使ax与0xfff按位与,测试es是否为0x1000的整数倍
die:    jne die         !结果不为0(说明es不是0x1000的整数倍)则陷入死循环
    xor bx,bx           ! bx(作为段内偏移地址)清零
rp_read:
    mov ax,es
    cmp ax,#ENDSEG      ! 实际上求(ax-ENDSEG)
    jb ok1_read         ! 当CF=1(axret                 ! 当ax>=ENDSEG时返回(我认为不会出现大于的情况)
ok1_read:
    seg cs
    mov ax,sectors      ! 这两句相当于 mov ax, cs:[sectors]; 获得每磁道扇区数
    sub ax,sread        ! ax = ax - sread, 得出本磁道未读扇区数
    mov cx,ax
    shl cx,#9           ! cx乘以512,求出字节数
    add cx,bx           ! 以上3行相当于 cx = ax * 512 + bx
                        ! 假设再读ax个扇区,cx就是段内共读入的字节数
    jnc ok2_read        ! 若cx < 0x10000(CF=0,没有进位)则跳转到ok2_read
    je ok2_read         ! 若cx = 0(ZF=1),说明刚好读入64KB,则跳转到ok2_read
    xor ax,ax            ! ax = 0x0000
    sub ax,bx            ! 求bx对0x10000的补数,结果在ax中
    shr ax,#9            ! 除以512,得到扇区数,AL作为参数,传给read_track
ok2_read:               
    call read_track  !调用read_track过程,用AL传参,读取AL个扇区到ES:BX
    mov cx,ax        !cx是该次操作已经读取的扇区数
    add ax,sread     !ax是当前磁道已经读取的扇区数
    seg cs
    cmp ax,sectors   
    jne ok3_read     !如果当前磁道还有扇区未读,跳转到ok3_read
    mov ax,#1        !说明当前磁道的扇区都已读完
    sub ax,head      !ax = 1 - 磁头号
    jne ok4_read     !不为0则跳转到 ok4_read,说明磁头号为0
    inc track        !说明磁头号为1,磁道号增加1
ok4_read:
    mov head,ax  !更新磁头号(如果是37行跳转过来,则 head=1;否则 head=0)
    xor ax,ax    !ax=0, 因为更换了磁道,所以当前磁道已读扇区数置0
ok3_read:
    mov sread,ax      !更新当前磁道已经读取的扇区数
    shl cx,#9
    add bx,cx         !更新偏移地址
    jnc rp_read       !没有进位,则跳转到rp_read
    mov ax,es         !有进位,说明BX达到了64KB边界
    add ax,#0x1000    
    mov es,ax         !es增加0x1000
    xor bx,bx         !bx = 0
    jmp rp_read       !继续读取

以上汇编代码看起来实在是费劲。为了便于理解,写成C语言伪代码如下:

void read_it(es)//参数是es
{ 
    if((es & 0xFFF) != 0) //es 必须是0x1000的倍数,否则进入死循环
        while(1);  //dead loop

    bx = 0;
    while(es < ENDSEG){
        // 1. 看看要读多少个扇区,用ax表示
        // 2. sread:本磁道已经读取的扇区数 
        ax = SECTORS - sread;
        if((ax * 512 + bx) > 0x10000){
            ax = (0x10000 - bx) / 512;
        }

        read_track(ax); //调用读扇区过程,al:要读的扇区数,es:bx->缓冲区
        cx = ax; //该次操作读取的扇区数   
        ax += sread; //ax是本磁道已读取的扇区总数

        if(ax==SECTORS){
            //本磁道的扇区全部读完
            if(head == 1){ //0和1磁头都已经读完,更新磁道
                ++track;
                head = 0; //从0磁头开始
            }
            else{
                head = 1; //切换到1磁头          
            }       
            ax = 0; //本磁道已读扇区数置为0
        }

        sread = ax; //更新本磁道已读扇区数
        bx += cx * 512; 更新偏移地址bx

        if(bx == 0x10000)
        {
            //如果偏移地址到达0x10000,则更新es,并使bx=0
            es += 0x1000;
            bx = 0;
        }
    }
    return;
}

过程read_track

读取AL个扇区到ES:BX。此过程的入口参数是:

AL-要读的扇区数目

ES:BX-缓冲区地址

read_track:
    push ax
    push bx
    push cx
    push dx
    mov dx,track  !当前磁道号
    mov cx,sread  !已经读取的扇区数
    inc cx        !CL是起始扇区号
    mov ch,dl     !CH是磁道号----
    mov dx,head   !当前磁头号
    mov dh,dl     !DH是磁头号
    mov dl,#0      !DL是驱动器号,0表示软盘
    and dx,#0x0100 !DH是磁头号,不是0就是1
    mov ah,#2      !功能号2,读扇区
    int 0x13
    jc bad_rt       !CF=1,表示出错,复位磁盘
    pop dx
    pop cx
    pop bx
    pop ax
    ret
bad_rt: mov ax,#0   !AH=0,磁盘复位功能
    mov dx,#0       !DL=0,驱动器号
    int 0x13
    pop dx
    pop cx
    pop bx
    pop ax
    jmp read_track  !重新读取

过程kill_motor

kill_motor:
    push dx
    mov dx,#0x3f2 !软盘控制器的端口-数字输出寄存器端口,只写
    mov al,#0     !驱动器A,关闭FDC,禁止DMA和中断请求,关闭马达
    outb          !将al的值写入端口dx
    pop dx
    ret

DOR(数字输出寄存器)

DOR是一个8位寄存器,他控制驱动器马达的开启、驱动器选择、启动/复位FDC以及允许/禁止DMA及中断请求。

Name Description
7 MOT_EN3 Driver D motor:1-start;0-stop
6 MOT_EN2 Driver C motor:1-start;0-stop
5 MOT_EN1 Driver B motor:1-start;0-stop
4 MOT_EN0 Driver A motor:1-start;0-stop
3 DMA_INT DMA and IRQs; 1 enable; 0-disable
2 RESET 0= enter reset mode;1= normal operation
1 and 0 DRV_SEL1, DRV_SEL0 “Select” drive number for next access

确认根文件系统设备号

    seg cs
    mov ax,root_dev     !ax = ROOT_DEV
    cmp ax,#0   
    jne root_defined    !如果 ROOT_DEV 不等于0则跳转到 root_defined
    seg cs
    mov bx,sectors       ! 取每磁道扇区数
    mov ax,#0x0208       ! /dev/ps0 - 1.2Mb
    cmp bx,#15           ! 判断每磁道扇区数是否等于15
    je  root_defined     ! 说明是1.2MB的软盘
    mov ax,#0x021c       ! /dev/PS0 - 1.44Mb
    cmp bx,#18           ! 判断每磁道扇区数是否等于18
    je  root_defined     ! 说明是1.44MB的软盘
undef_root:
    jmp undef_root       ! 死循环
root_defined:
    seg cs
    mov root_dev,ax      ! 将检查过的设备号保存到 root_dev 中

在Linux中软驱的主设备号是2,次设备号 = type * 4 + nr.

其中,nr等于0~3时分别对应软驱A、B、C、D;type是软驱的类型,比如2表示1.2MB,7表示1.44MB等。

因为是可引导的驱动器,所以肯定是A驱。对于1.2MB,设备号 = 2 << 8 + 2 * 4 + 0 = 0x208;对于1.44MB,设备号 = 2 << 8 + 7 * 4 + 0 = 0x21C.

.org 508
root_dev:
    .word ROOT_DEV !这里存放根文件系统所在设备号(init/main.c中会用)

ROOT_DEV到底有何用,怎么用,这里先存疑,后面再探究。

跳转到 setup 去执行

jmpi    0,SETUPSEG   !到此本程序就结束了。

段间跳转,跳转到0x9020:0x0000(setup.s程序开始处)去执行。

代码分析到这里,就差不多明白了。虽然是一个引导扇区,编译后只有512字节,可是涉及的知识点还真不少。真是太佩服Linus了,一个大学生就能写出这样的代码,实属出众。


参考资料

[1]《Linux内核完全剖析》(赵炯,2006)
[2] https://github.com/Wangzhike/HIT-Linux-0.11/blob/master/1-boot/OS-booting.md
[3] https://wiki.osdev.org/Floppy_Disk_Controller#DOR_bitflag_definitions

你可能感兴趣的:(Linux-0.11)