用于在第二阶段引导实模式代码的旧版 BIOS 引导加载程序

Posted

技术标签:

【中文标题】用于在第二阶段引导实模式代码的旧版 BIOS 引导加载程序【英文标题】:Legacy BIOS bootloader to bootstrap real-mode code in second stage 【发布时间】:2019-07-20 12:46:31 【问题描述】:

我正在编写自己的操作系统。到目前为止,我的代码超过了 512 字节,这对于一个简单的引导扇区来说太大了。

我知道我现在必须编写一个引导加载程序来读取可能大于也可能不大于单个 512 字节扇区的任意代码。

引导加载程序需要:

用作磁盘签名为 0xaa55 的引导记录。 从内存地址 0x7E00 开始的任意长度的 LBA 1(LBA 0 是引导扇区)开始读取第二阶段(测试代码)。 使用 FAR JMP 将控制权转移到 0x0000:0x7E00。 可用作 1.44 MiB 软盘映像,用于 QEMU、BOCHS、VirtualBox 等模拟器。 可以在 USB 记忆棒上传输和使用,以便在 Bios 设置为使用软盘驱动器 (FDD) 仿真启动 USB 的情况下在真实硬件上进行测试。 注意:Some bootloaders 放在 USB 驱动器上时效果不佳。 将引导驱动器传递到 DL 中的第二阶段。 将所有段寄存器清零并将 SS:SP 设置为 0x0000:0x7C00(从引导加载程序下方向下增长)。

这也可以作为就涉及操作系统开发的 Stack Overflow 提出问题的良好起点。程序员经常难以创建Minimal, Complete, and Verifiable Example。一个通用的样板/模板将允许其他希望帮助测试代码的 Stack Overflow 用户以有限的麻烦。

我将如何构建这样一个可重用的引导加载程序?

【问题讨论】:

注意: This question is being discussed on Meta。如果您对其格式有异议,或对如何改进提出建议,请在此处权衡。此处的讨论仅限于技术问题和对问题本身的澄清。 对于另一种实现,我做了类似的事情。虽然它是在我学习 x86 的不同部分时完成的。 github.com/sherrellbc/realmode-loader 【参考方案1】:

我已经编写了这样的代码作为其他答案的一部分,但从来没有机会展示一个可以从其他 *** 问题中引用的简单测试工具。你所要求的是相当微不足道的。可以通过在 NASM 中编写引导加载程序来做到这一点,其中包括您希望测试的汇编代码的二进制映像。将使用 BIOS 函数 Int 13/ah=2 从 LBA 1(引导加载程序之后的第一个扇区)开始从磁盘读取此映像。然后控制权将通过 FAR JMP 转移到 0x0000:0x7e00。

引导加载程序代码如下所示:

bpb.inc

global bpb_disk_info

    jmp short boot_continue
    nop

bpb_disk_info:

    ; Dos 4.0 EBPB 1.44MB floppy
    OEMname:           db    "mkfs.fat"  ; mkfs.fat is what OEMname mkdosfs uses
    bytesPerSector:    dw    512
    sectPerCluster:    db    1
    reservedSectors:   dw    1
    numFAT:            db    2
    numRootDirEntries: dw    224
    numSectors:        dw    2880
    mediaType:         db    0xf0
    numFATsectors:     dw    9
    sectorsPerTrack:   dw    18
    numHeads:          dw    2
    numHiddenSectors:  dd    0
    numSectorsHuge:    dd    0
    driveNum:          db    0
    reserved:          db    0
    signature:         db    0x29
    volumeID:          dd    0x2d7e5a1a
    volumeLabel:       db    "NO NAME    "
    fileSysType:       db    "FAT12   "

boot.asm

STAGE2_ABS_ADDR  equ 0x07e00
STAGE2_RUN_SEG   equ 0x0000
STAGE2_RUN_OFS   equ STAGE2_ABS_ADDR
                                ; Run stage2 with segment of 0x0000 and offset of 0x7e00

STAGE2_LOAD_SEG  equ STAGE2_ABS_ADDR>>4
                                ; Segment to start reading Stage2 into
                                ;     right after bootloader

STAGE2_LBA_START equ 1          ; Logical Block Address(LBA) Stage2 starts on
                                ;     LBA 1 = sector after boot sector
STAGE2_LBA_END   equ STAGE2_LBA_START + NUM_STAGE2_SECTORS
                                ; Logical Block Address(LBA) Stage2 ends at
DISK_RETRIES     equ 3          ; Number of times to retry on disk error

bits 16
ORG 0x7c00

; Include a BPB (1.44MB floppy with FAT12) to be more compatible with USB floppy media
%ifdef WITH_BPB
%include "bpb.inc"
%endif

boot_continue:
    xor ax, ax                  ; DS=SS=0 for stage2 loading
    mov ds, ax
    mov ss, ax                  ; Stack at 0x0000:0x7c00
    mov sp, 0x7c00
    cld                         ; Set string instructions to use forward movement

    ; Read Stage2 1 sector at a time until stage2 is completely loaded
load_stage2:
    mov [bootDevice], dl        ; Save boot drive
    mov di, STAGE2_LOAD_SEG     ; DI = Current segment to read into
    mov si, STAGE2_LBA_START    ; SI = LBA that stage2 starts at
    jmp .chk_for_last_lba       ; Check to see if we are last sector in stage2

.read_sector_loop:
    mov bp, DISK_RETRIES        ; Set disk retry count

    call lba_to_chs             ; Convert current LBA to CHS
    mov es, di                  ; Set ES to current segment number to read into
    xor bx, bx                  ; Offset zero in segment

.retry:
    mov ax, 0x0201              ; Call function 0x02 of int 13h (read sectors)
                                ;     AL = 1 = Sectors to read
    int 0x13                    ; BIOS Disk interrupt call
    jc .disk_error              ; If CF set then disk error

.success:
    add di, 512>>4              ; Advance to next 512 byte segment (0x20*16=512)
    inc si                      ; Next LBA

.chk_for_last_lba:
    cmp si, STAGE2_LBA_END      ; Have we reached the last stage2 sector?
    jl .read_sector_loop        ;     If we haven't then read next sector

.stage2_loaded:
    mov ax, STAGE2_RUN_SEG      ; Set up the segments appropriate for Stage2 to run
    mov ds, ax
    mov es, ax

    ; FAR JMP to the Stage2 entry point at physical address 0x07e00
    xor ax, ax                  ; ES=FS=GS=0 (DS zeroed earlier)
    mov es, ax

    ; SS:SP is already at 0x0000:0x7c00, keep it that way
    ; DL still contains the boot drive number
    ; Far jump to second stage at 0x0000:0x7e00
    jmp STAGE2_RUN_SEG:STAGE2_RUN_OFS

.disk_error:
    xor ah, ah                  ; Int13h/AH=0 is drive reset
    int 0x13
    dec bp                      ; Decrease retry count
    jge .retry                  ; If retry count not exceeded then try again

error_end:
    ; Unrecoverable error; print drive error; enter infinite loop
    mov si, diskErrorMsg        ; Display disk error message
    call print_string
    cli
.error_loop:
    hlt
    jmp .error_loop

; Function: print_string
;           Display a string to the console on display page 0
;
; Inputs:   SI = Offset of address to print
; Clobbers: AX, BX, SI

print_string:
    mov ah, 0x0e                ; BIOS tty Print
    xor bx, bx                  ; Set display page to 0 (BL)
    jmp .getch
.repeat:
    int 0x10                    ; print character
.getch:
    lodsb                       ; Get character from string
    test al,al                  ; Have we reached end of string?
    jnz .repeat                 ;     if not process next character
.end:
    ret

;    Function: lba_to_chs
; Description: Translate Logical block address to CHS (Cylinder, Head, Sector).
;
;   Resources: http://www.ctyme.com/intr/rb-0607.htm
;              https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
;              https://***.com/q/45434899/3857942
;              Sector    = (LBA mod SPT) + 1
;              Head      = (LBA / SPT) mod HEADS
;              Cylinder  = (LBA / SPT) / HEADS
;
;      Inputs: SI = LBA
;     Outputs: DL = Boot Drive Number
;              DH = Head
;              CH = Cylinder (lower 8 bits of 10-bit cylinder)
;              CL = Sector/Cylinder
;                   Upper 2 bits of 10-bit Cylinders in upper 2 bits of CL
;                   Sector in lower 6 bits of CL
;
;       Notes: Output registers match expectation of Int 13h/AH=2 inputs
;
lba_to_chs:
    push ax                    ; Preserve AX
    mov ax, si                 ; Copy LBA to AX
    xor dx, dx                 ; Upper 16-bit of 32-bit value set to 0 for DIV
    div word [sectorsPerTrack] ; 32-bit by 16-bit DIV : LBA / SPT
    mov cl, dl                 ; CL = S = LBA mod SPT
    inc cl                     ; CL = S = (LBA mod SPT) + 1
    xor dx, dx                 ; Upper 16-bit of 32-bit value set to 0 for DIV
    div word [numHeads]        ; 32-bit by 16-bit DIV : (LBA / SPT) / HEADS
    mov dh, dl                 ; DH = H = (LBA / SPT) mod HEADS
    mov dl, [bootDevice]       ; boot device, not necessary to set but convenient
    mov ch, al                 ; CH = C(lower 8 bits) = (LBA / SPT) / HEADS
    shl ah, 6                  ; Store upper 2 bits of 10-bit Cylinder into
    or  cl, ah                 ;     upper 2 bits of Sector (CL)
    pop ax                     ; Restore scratch registers
    ret

; If not using a BPB (via bpb.inc) provide default Heads and SPT values
%ifndef WITH_BPB
numHeads:        dw 2          ; 1.44MB Floppy has 2 heads & 18 sector per track
sectorsPerTrack: dw 18
%endif

bootDevice:      db 0x00
diskErrorMsg:    db "Unrecoverable disk error!", 0

; Pad boot sector to 510 bytes and add 2 byte boot signature for 512 total bytes
TIMES 510-($-$$) db  0
dw 0xaa55

; Beginning of stage2. This is at 0x7E00 and will allow your stage2 to be 32.5KiB
; before running into problems. DL will be set to the drive number originally
; passed to us by the BIOS.

NUM_STAGE2_SECTORS equ (stage2_end-stage2_start+511) / 512
                                ; Number of 512 byte sectors stage2 uses.

stage2_start:
    ; Insert stage2 binary here. It is done this way since we
    ; can determine the size(and number of sectors) to load since
    ;     Size = stage2_end-stage2_start
    incbin "stage2.bin"

; End of stage2. Make sure this label is LAST in this file!
stage2_end:

; Fill out this file to produce a 1.44MB floppy image
TIMES 1024*1440-($-$$) db 0x00

要使用它,您将首先生成一个名为stage2.bin 的二进制文件。在构建stage2.bin 之后,您可以使用以下命令构建一个没有 BIOS 参数块 (BPB) 的 1.44MiB 磁盘映像:

nasm -f bin boot.asm -o disk.img

要使用 BPB 构建 1.44MiB 磁盘映像,您可以使用以下命令构建它:

nasm -DWITH_BPB -f bin boot.asm -o disk.img

stage2.bin 中的代码必须在内存中的 ORG(原点)为 0x07e00 的假设下生成。


示例用法/示例

生成到名为stage2.bin 的文件的代码示例,可以使用此测试工具加载:

testcode.asm

ORG 0x7e00

start:
    mov si, testCodeStr
    call print_string

    cli
.end_loop:
    hlt
    jmp .end_loop

testCodeStr: db "Test harness loaded and is executing code in stage2!", 0

; Function: print_string
;           Display a string to the console on display page 0
;
; Inputs:   SI = Offset of address to print
; Clobbers: AX, BX, SI

print_string:
    mov ah, 0x0e                ; BIOS tty Print
    xor bx, bx                  ; Set display page to 0 (BL)
    jmp .getch
.repeat:
    int 0x10                    ; print character
.getch:
    lodsb                       ; Get character from string
    test al,al                  ; Have we reached end of string?
    jnz .repeat                 ;     if not process next character
.end:
    ret

注意:顶部有一个ORG 0x7e00。这个很重要。要将此文件组装成stage2.bin,请使用:

nasm -f bin testcode.asm -o stage2.bin

然后使用以下命令创建 1.44MiB 磁盘映像:

nasm -f bin boot.asm -o disk.img

结果应该是大小正好为 1.44MiB 的磁盘映像,包含 stage2.bin 的副本,并具有我们的测试工具引导扇区。

文件stage2.bin 可以是任何具有二进制代码的文件,可以在 0x0000:0x7e00 处加载和启动。用于在stage2.bin 中创建代码的语言(C、汇编等)无关紧要。我在这个例子中使用了 NASM。当这个测试代码在 QEMU 中使用 qemu-system-i386 -fda disk.img 执行时,它看起来类似于:


特别说明::如果您使用 FDD 仿真从 USB 引导,使用 -DWITH_BPB 启用 BPB 非常有用。一些将 USB 作为软盘启动的 BIOS 会假设存在 BPB,并在将控制权转移到物理地址 0x07c00 处的驱动器几何图形之前覆盖该区域。

【讨论】:

【参考方案2】:

我修改了自己的引导扇区加载程序以添加新协议。它使其设置 es = ds = ss = 0 并将整个加载文件加载到地址 07E00h,跳转到 0000h:7E00h 处。但是,sp 仍然指向 7C00h 以下。

问题中的要求有很大的不同:这个加载器使用(FAT12 或 FAT16)文件系统来加载下一阶段。如果找到,它会从名为 KERNEL7E.BIN 的文件中加载。文件名和整个加载协议一样,可以通过编辑源文件或在 NASM 命令行中传递定义来调整。

由于代码大小的限制,出错时只输出单字符的错误信息:R 表示磁盘读取错误,M 表示要加载的文件太大(内存不足)。另一个限制是不使用 RPL(远程程序加载器)协议,因为它需要更多字节。

为了减轻空间压力,加载器可以内置-D_CHS=0 -D_QUERY_GEOMETRY=0(如果通过ROM-BIOS的LBA接口加载)或-D_LBA=0(如果通过CHS接口加载)。

要构建加载器,请克隆 lmacros 和 ldosboot 存储库,并将它们彼此相邻放置。对于 FAT12,加载程序将使用 NASM 从 ldosboot 目录构建:

$ nasm -I ../lmacros/ boot.asm -l boot7e12.lst -D_MAP=boot7e12.map -o boot7e12.bin -D_COMPAT_KERNEL7E

对于 FAT16 或者这种方式:

$ nasm -I ../lmacros/ boot.asm -l boot7e16.lst -D_MAP=boot7e16.map -o boot7e16.bin -D_FAT16 -D_COMPAT_KERNEL7E

这是how to install the loader 到现有的已格式化的 FAT12 或 FAT16 文件系统映像:

dd if=boot7e12.bin of=floppy.img bs=1 count=11 conv=notrunc
dd if=boot7e12.bin of=floppy.img bs=1 count=$((512 - 0x3e)) seek=$((0x3e)) skip=$((0x3e)) conv=notrunc

NASM 可以创建整个图像,而不是使用现有图像。我在https://hg.ulukai.org/ecm/bootimg写了这样一个程序,它的构建是这样的:

nasm -I ../lmacros/ -D_BOOTFILE="'../ldosboot/boot12.bin'" -D_MULTIPAYLOADFILE="'../ldebug/bin/ldebug.com','../ldebug/bin/lddebug.com'" bootimg.asm -o bootimg.img

请注意 long def 如何在单引号列表条目周围使用双引号。每个列表条目都被剥离为基本名称(在最后一个斜杠或反斜杠之后),将其内容添加到数据区域,并将目录条目添加到根目录。文件名是 ASCII 并且全部大写。

ldosboot 存储库也包含一个两扇区 FAT32 加载程序,但我还没有修改它以支持此协议。通过重定位,FAT 缓冲区应该已经位于内存的顶部。这意味着文件可以加载到 07E00h。但是,ss 将处于高段而不是零。除此不同外,还可以使用开关指定协议。构建它的命令是nasm -I ../lmacros/ boot32.asm -l boot7e32.lst -D_MAP=boot7e32.map -o boot7e32.bin -D_RELOCATE -D_MEMORY_CONTINUE=0 -D_ZERO_DS -D_ZERO_ES -D_SET_BL_UNIT=0 -D_SET_DL_UNIT=1 -D_LOAD_ADR=07E00h -D_EXEC_SEG_ADJ=-7E0h -D_EXEC_OFS=7E00h -D_OEM_NAME="'KERNEL7E'" -D_LOAD_NAME="'KERNEL7E'" -D_LOAD_EXT="'BIN'"

还有用于 DOS 的 instsect 程序(在它自己的存储库中),它使用加载程序映像构建并将它们安装到 DOS 驱动器。

【讨论】:

我只是简单地总结一下,通用样板/模板将允许其他希望帮助测试代码的 Stack Overflow 用户少量大惊小怪。 在我的问题的底部。目标并不是真正的操作系统设计,而是一种帮助人们在这个网站上展示他们的代码时潜在地创造更好minimal reproducible example的方法,因为许多做 OSDev 的人忽略了向我们展示他们的引导加载程序。 我是赞成票。我可能应该在我的问题中添加我有一个不涉及使用文件系统的规范的原因。在 *** 上,我们遇到了跨越操作系统(尤其是 Windows 和 Linux)的问题。该规范的设计方式是简单地拥有 NASM 就可以生成一个可用的软盘映像,而无需更多工具(挂载和卸载映像——在 Windows 上涉及更多)。使用mtools 在 Linux 上的用户空间中使用起来容易得多。我实际上也倾向于将人们引导到 Alexey Frunze(另一个 SO 用户)的 BOOTPROG:github.com/alexfru/BootProg @Michael Petch:当我开始开发 ldosboot 集合时,我确实也使用过 alexfru 的加载器实现。但是,我最终决定使用 Chris Giese 的。至于您的要求,我知道图像的格式化和访问并不总是那么容易。我仍然想宣传我的装载机作为替代方案。感谢您的支持! 了解,您的回答没有任何问题,这就是我非常乐意投票的原因。我也有可以读取 Fat12 等的引导加载程序。我的第一个引导加载程序是在 1980 年代后期(我正在使用 Wendin DOS 工作),当时除了在 Compuserv 等地方寻求帮助之外,没有太多示例引导加载程序可以从中汲取灵感从 except 到反汇编 DOS 的。 @Michael Petch:我添加了一个使用 NASM 构建文件系统映像的程序(脚本 ish)。我将描述添加到我的答案中。它缺乏复杂性,但对于任何只想使用 NASM 的人来说,它无疑是您解决方案的充分替代方案。

以上是关于用于在第二阶段引导实模式代码的旧版 BIOS 引导加载程序的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 BIOS 中断设置引导分区

Linux源码学习---引导程序boot

x86下SylixOS引导过程分析

引导启动程序

01 linux011 引导和保护模式的初始设置

01 linux011 引导和保护模式的初始设置