自制操作系统之进入loader

3 篇文章 0 订阅
1 篇文章 0 订阅

计算机电源打开时先进行加电自检,然后寻找启动盘,若启动盘第一扇区的最后连个字节为0xaa55 ,则BIOS认为该扇区为引导扇区,然后BIOS会把该扇区加载到0x0000:0x7c00处,然后跳到该处执行。然而第一扇区仅仅只有512个字节,这往往不够用(我自己尝试写boot时一开始就超出512字节,使得我不得不痛苦的反复阅读精简代码),所以通常这512字节(boot)把loader加载到内存跳到loader,然后loader把kernel加载到内存跳到kernel,当然也可以直接在boot直接把kernel加载到内存。

一、在开始之前,我们先把工具准备好。(在linux系统下,本人使用的是ubuntu)

  • 编译工具:nasm,这里直接在终端用指令安装即可

sudo apt-get install nasm

  • 虚拟机:qemu,也直接用指令安装。另附上本人的另一个介绍qemu简单使用的博客

sudo apt-get install qemu

  • 虚拟机:bochs,这个主要用来调试,如果不进行调试可以不用。可以直接指令安装

sudo apt-get install vgabios bochs bochs-x bximage

直接指令安装的bochs可能没有安装调试功能(我自己没有试过,有兴趣的可以试试),如果需要调试可以通过源码安装,先到官网下载源码包,选.tar.gz下载。安装过程如下,另附上本人介绍bochs简单使用的博客

  • tar vxzf bochs-xxx.tar.gz
  • cd bochs-xxx
  • ./configure --enable-debugger --enable-disasm
  • make
  • make install

二、接下来就上代码了,先看boot.asm

;boot.asm
    org  07c00h            ; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00 处并开始执行

;============================================================================================
BaseOfStack        equ    07c00h    ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)

;============================================================================================

BaseOfLoader            equ    09000h    ; LOADER.BIN 被加载到的位置 ----  段地址
OffsetOfLoader            equ    0100h    ; LOADER.BIN 被加载到的位置 ---- 偏移地址

RootDirSectors            equ    14        ; 根目录占用空间
StartOfRootDir            equ    19        ; Root Directory 的第一个扇区号
StartOfFAT1                equ    1        ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt
DeltaSectorNo            equ    17        ; DeltaSectorNo = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) - 2

; 文件的开始Sector号 = DirEntry中的开始Sector号 + 根目录占用Sector数目 + DeltaSectorNo

;============================================================================================

    jmp short Start                        ; Start to boot.
    nop                                          ; 这个 nop 不可少

    ; 下面是 FAT12 磁盘的头
    BS_OEMName        DB 'ForrestY'        ; OEM String, 必须 8 个字节
    BPB_BytsPerSec    DW 512                 ; 每扇区字节数
    BPB_SecPerClus    DB 1                      ; 每簇多少扇区
    BPB_RsvdSecCnt    DW 1                    ; Boot 记录占用多少扇区
    BPB_NumFATs        DB 2                      ; 共有多少 FAT 表
    BPB_RootEntCnt    DW 224                  ; 根目录文件数最大值, 14 个扇区
    BPB_TotSec16    DW 2880                    ; 逻辑扇区总数, 1.44M
    BPB_Media        DB 0xF0                      ; 媒体描述符
    BPB_FATSz16        DW 9                      ; 每FAT扇区数
    BPB_SecPerTrk    DW 18                      ; 每磁道扇区数
    BPB_NumHeads    DW 2                       ; 磁头数(面数)
    BPB_HiddSec        DD 0                        ; 隐藏扇区数
    BPB_TotSec32    DD 0                           ; 如果 TotSec16 是 0, 则由这个值记录扇区数
    BS_DrvNum        DB 0                           ; 中断 13 的驱动器号
    BS_Reserved1    DB 0                           ; 未使用
    BS_BootSig        DB 29h                        ; 扩展引导标记 (29h)
    BS_VolID        DD 0                                ; 卷序列号
    BS_VolLab        DB 'abytec     '               ; 卷标, 必须 11 个字节
    BS_FileSysType    DB 'FAT12   '             ; 文件系统类型, 必须 8个字节  

Start:    
    mov    ax, cs
    mov    ds, ax
    mov    es, ax
    mov    ss, ax
    mov    sp, BaseOfStack        ; 初始化段寄存器和栈

    ; 清屏
    mov    ax, 0600h            ; AH = 6,  AL = 0h
    mov    bx, 0700h            ; 黑底白字(BL = 07h)
    mov    cx, 0                     ; 左上角: (0, 0)
    mov    dx, 0184fh            ; 右下角: (80, 50)
    int    10h                          ; int 10h
    
    mov dh, [cursor]
    mov si, Message0        ; "Booting"
    call    DispStr            ; 显示字符串
    
    xor    ah, ah                ; 
    xor    dl, dl                  ;  软驱复位
    int    13h                    ; 
    
    ; 下面在 A 盘的根目录寻找 LOADER.BIN

    mov si, LoaderFileName            ; ds:si -> "LOADER  BIN"                                            
    
    mov ax, BaseOfLoader
    mov es, ax                      ; ┓ es <- BaseOfLoader
    mov bx, OffsetOfLoader          ; ┣ bx <- OffsetOfLoader  于是, es:bx = BaseOfLoader:OffsetOfLoader
                                    ; ┛ 扇区被读到的位置
    mov cx, RootDirSectors
    mov word [Sector], StartOfRootDir
.readSector:
    push cx

    mov di, OffsetOfLoader          ; es:di -> BaseOfLoader:0100 = BaseOfLoader*10h+100h
                                    ; 比较字符串时用到

    mov    ax, [Sector]                ; ax <- Root Directory 中的某 Sector 号
    call    ReadSector

    mov cx, 16                        ; 512/32 一个扇区可保存的条目, 一个条目 32 字节
.compareStr:
    call CmpStr                        ; 比较字符串, 相同时 al 返回 1
    cmp al, 1
    jz FoundLoader                    ; 找到 loader 文件
    add di, 20h                        ; 下一个根目录区条目
    loop .compareStr

    inc word [Sector]                ; 下一个扇区
    pop cx
    loop .readSector

NoFoundLoader:
    mov dh, [cursor]                        ; 没有发现Loader
    mov si, Message2                        ; "No Loader"
    call DispStr
    jmp $                                    ; 死循环

FoundLoader:
    add di, 1ah                                ; 定位到条目对应的开始簇号
    mov ax, word [es:di]                    ; 取簇号
    push ax                                    ; 保存簇号
    add ax, RootDirSectors
    add ax, DeltaSectorNo                    ; 数据扇区号
    
.loading:
    call ReadSector                            ; 读扇区, es:bx=读取目的位置, ax=扇区号

    pop ax                                    ; 取出簇号

    call GetNextCluster                        ; 获取文件下一部分的簇号, ax <- cluster

    cmp ax, 0fffh                            ; 簇号为 fffh 文件结束, 加载 Loader 完成
    jz Loaded

    push ax                                    ; 保存簇号(Cluster)
    add ax, RootDirSectors
    add ax, DeltaSectorNo                    ; 数据扇区号

    add bx, [BPB_BytsPerSec]                ; 下一扇区要读到的内存偏移

    jmp .loading

Loaded:
    mov    dh, [cursor]            ; "Ready"
    mov si, Message1
    call    DispStr                ; 显示字符串

; *****************************************************************************************************
    jmp    BaseOfLoader:OffsetOfLoader    ; 这一句正式跳转到已加载到内
                                    ; 存中的 LOADER.BIN 的开始处,
                                    ; 开始执行 LOADER.BIN 的代码。
                                    ; Boot Sector 的使命到此结束。
; *****************************************************************************************************

;============================================================================
;变量
;----------------------------------------------------------------------------
Sector            dw    0                ; 要读取的扇区号
bOdd            db    0                 ; 奇数还是偶数
cursor            db    0                ; 光标所在行
ReadFailedCount    db    0       ; 磁盘读取失败次数, 在循环中递减

;============================================================================
;字符串
;----------------------------------------------------------------------------
LoaderFileName    db    "LOADER  BIN", 0    ; LOADER.BIN 之文件名

Message0:        db    "Booting", 0        ; Booting时显示该字符串
Message1:        db    "Ready", 0            ; Loader加载完成时显示该字符串
Message2:        db    "No Loader", 0        ; 找不到Loader时显示该字符串
Message3:        db    "Error",0            ; 出现其他错误(软盘读取错误)时显示该字符串
;============================================================================


;----------------------------------------------------------------------------
; 函数名: DispStr
;----------------------------------------------------------------------------
; para:
;    dh    显示的行
;    si    字符串的偏移地址(使用默认段寄存器 ds)
; desc:
;    显示一个字符串, 在 dl 列,dh行显示字符串
DispStr:
    mov ax, 80
    mul dh
    mov bl, 0            ;光标位置
    
    mov bx, 2
    mul bx                ; ax 显示位置
    mov di, ax
    mov ax, 0b800h
    mov es, ax
    mov ah, 0ch            ;黑地红字
    mov bh, 0            ;第0页
.showChar:
    mov al, [ds:si]
    cmp al, 0
    jz .dispStrFin        ; 遇到 0 , 字符串结束标志
    mov [es:di], ax
    
    inc si
    inc di
    inc di
    inc dl                ; 下一个

    jmp .showChar
.dispStrFin:
    inc dh
    mov ah, 02h
    int    10h                ; int 10h, 在 dh 行 dl 列显示光标
    inc byte [cursor]    ; 下一行

    ret

;----------------------------------------------------------------------------
; 函数名: CmpStr
;----------------------------------------------------------------------------
; para:
;    si    字符串源偏移
;    di    字符串目的偏移
; desc: ds:si 和 es:di 指向的前 11 个字符比较
;    相同 al 返回 1 , 否则 al 返回 0

CmpStr:
    push cx
    push di
    push si

    cld
    mov dl, 0
    mov cx,11
.compareChar:
    mov al, byte [ds:si]                    ; ds:si -> al
    cmp al, byte [es:di]
    jnz .different
    inc si
    inc di
    loop .compareChar
    mov dl, 1
    mov al, dl
.different:
    cmp dl, 1
    jz .CmpStrFin
    mov al, 0
.CmpStrFin:

    pop si
    pop di
    pop cx
    ret
;----------------------------------------------------------------------------
; 函数名: ReadSector
;----------------------------------------------------------------------------
; para:
;    ax    要读的扇区号
;    es    目的段地址
;    bx    目的偏移地址
; desc:
;    从第 ax 个 Sector 开始, 将 1 个 Sector 读入 es:bx 中
ReadSector:
    ; -----------------------------------------------------------------------
    ; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
    ; -----------------------------------------------------------------------
    ; 设扇区号为 x
    ;                          ┌ 柱面号 = y >> 1
    ;       x           ┌ 商 y ┤
    ; -------------- => ┤      └ 磁头号 = y & 1
    ;  每磁道扇区数     │
    ;                   └ 余 z => 起始扇区号 = z + 1
    push cx
    push bx

    mov    bl, [BPB_SecPerTrk]    ; bl: 每磁道扇区数
    div    bl                    ; y 在 al 中, z 在 ah 中
    inc    ah                    ; z ++ : 起始扇区号
    mov    cl, ah                ; cl <- 起始扇区号
    mov    dh, al                ; dh <- y
    and dh, 1                ; y & 1 : 磁头号
    shr    al, 1                ; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
    mov    ch, al                ; ch <- 柱面号

    ; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^
    mov    dl, [BS_DrvNum]        ; 驱动器号 (0 表示 A 盘)
    pop bx
    mov byte [ReadFailedCount], 5
.GoOnReading:
    cmp byte [ReadFailedCount], 0
    jz FloppyDamage
    dec byte [ReadFailedCount]
    mov    ah, 2                ; 读
    mov    al, 1                ; 读 1 个扇区
    int    13h
    jc    .GoOnReading        ; 如果读取错误 CF 会被置为 1, 读取失败时继续读取, 直到 5 次

    pop    cx
    ret

FloppyDamage:
    mov dh, [cursor]
    mov si, Message3        ; "Error"
    call DispStr
    jmp $

;----------------------------------------------------------------------------
; 函数名: GetNextCluster
;----------------------------------------------------------------------------
; para:
;    ax    当前簇号
; desc:
;    找到簇号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
;    需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
GetNextCluster:
    push    es
    push    bx

    push    ax                ; 暂时保存簇号

    mov    ax, BaseOfLoader    ; `.
    sub    ax, 0100h            ; | 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT
    mov    es, ax                ; /

    pop    ax                    ; 当前簇号
    mov    byte [bOdd], 0
    mov    bx, 3
    mul    bx                    ; dx:ax = ax * 3
    mov    bx, 2
    div    bx                    ; dx:ax / 2  ==>  ax <- 商(当前簇对应在 FAT 中的偏移量), dx <- 余数
    
    cmp    dx, 0
    jz    .even_1
    mov    byte [bOdd], 1

.even_1:                    ; 偶数
                            ; 现在 ax 中是 FATEntry 在 FAT 中的偏移量,下面来
                            ; 计算 FATEntry 在 FAT 的哪个扇区中(FAT占用不止一个扇区)
    xor    dx, dx            
    mov    bx, [BPB_BytsPerSec]
    div    bx                     ; dx:ax / BPB_BytsPerSec
                               ; ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号)
                               ; dx <- 余数 (FATEntry 在扇区内的偏移)。
    push    dx

    mov    bx, 0                 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00
    add    ax, StartOfFAT1     ; 此句之后的 ax 就是 FATEntry 所在的扇区号

    call    ReadSector         ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界
                            ; 发生错误, 因为一个 FATEntry 可能跨越两个扇区
    add bx, [BPB_BytsPerSec]
    inc ax
    call ReadSector

    pop    dx
    sub bx, [BPB_BytsPerSec]
    add    bx, dx                ; 对应 FAT 在内存中的偏移

    mov    ax, [es:bx]
    cmp    byte [bOdd], 1
    jnz    .even_2                ; 如果是偶数
    shr    ax, 4                ; 奇数
.even_2:
    and    ax, 0FFFh            ; 取低 12 位

    
    pop    bx
    pop    es
    ret
;----------------------------------------------------------------------------

times     510-($-$$)    db    0    ; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw     0xaa55                    ; 结束标志

可以看到,最后两字节被置为0xaa55。为与dos兼容,开头定义了一段数据,这是把软盘格式化为FAT12格式。这段程序先在软盘的更目录区查找loader.bin文件,找到后把它加载的内存(BaseOfLoader:OffsetOfLoader),然后把控制权交给loader。至此,boot的使命就结束了,接下来loader就会把kernel加载到内存。

在这段代码中,软盘的结构是按FAT12格式来组织的,所以使得代码更复杂了一些。事实上,我们完全不顾这些(不考虑兼容性的话),把格式化为FAT12的那一段删掉,自己组织软盘的存储方式,把loader从软盘加载到内存。

三、为了测试boot能否成功加载loader并转到loader,写一个loader.asm试一试吧。

;loader.asm

org    0100h

    mov    ax, 0B800h
    mov    gs, ax
    mov    ah, 0Fh                ; 0000: 黑底    1111: 白字
    mov    al, 'L'
    mov    [gs:((80 * 12 + 39) * 2)], ax    ; 屏幕第 12 行, 第 39 列。

    jmp    $                ; 到此停住

这段代码仅仅在屏幕中间显示一个'L',然后进入死循环,所以如果能看到屏幕中间出现'L'就说明成功了。

四、接下来就编译测试一下,注意loader须命名为loader.bin(大小写不敏感)

  • nasm boot.asm -o boot.bin
  • nasm loader.asm -o loader.bin

然后我们需要一个软盘映像(abytec.img)并把文件写到软盘里,软盘映像的制作方法会在前面提到的bochs的简单使用这篇博客里提到

  • dd if=boot/boot.bin of=abytec.img bs=512 count=1 conv=notrunc

把loader.bin放入软盘,注意进行上一步操作之后才能进行这一步,否则会挂载失败。(boot.bin含有格式化软盘的部分)

  • sudo mount -o loop abytec.img /mnt/floppy/
  • sudo cp loader.bin /mnt/floppy/
  • sudo umount /mnt/floppy/

在虚拟机里测试

  • qemu-system-i386 -m 8 -k en-us -rtc base=localtime -soundhw sb16,adlib -device cirrus-vga -fda abytec.img -boot order=a

OK,大功告成!

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值