I am working on writing my own operating system. So far, my code exceeds 512 bytes, which is too large to fit in a simple boot sector.
I understand that I now have to write a bootloader that reads arbitrary code that may or may not be greater than a single 512-byte sector.
The bootloader would need to:
- Function as a boot record with disk signature 0xaa55.
- Read a second stage (the test code) start from LBA 1 (LBA 0 is boot sector) of arbitrary length starting at memory address 0x7E00.
- Transfer control to it using a FAR JMP to 0x0000:0x7E00.
- Be usable as a 1.44 MiB floppy disk image for use in emulators like QEMU, BOCHS, VirtualBox etc.
- Can be transferred and used on a USB stick to test on real hardware with the BIOS set to boot USB using Floppy Disk Drive (FDD) emulation. Note: Some bootloaders do not work well when placed on USB drives.
- Pass the boot drive to the second stage in DL.
- Zero out all the segment registers and set SS:SP to 0x0000:0x7C00 (grows down from just under the bootloader).
This would also serve as a good starting point for asking questions on Stack Overflow that involve OS development. Programmers often struggle to create a Minimal, Complete, and Verifiable Example. A common boilerplate/template would allow other Stack Overflow users wishing to help to test the code with a limited amount of fuss.
How would I go about building such a reusable bootloader?
I have written such code as part of other answers but never had an opportunity to present a simple test harness that could be referenced from other Stackoverflow questions. What you are asking for is rather trivial. One can do this by writing a bootloader in NASM that includes a binary image of the assembled code you wish to test. This image would be read from disk starting at LBA 1 (first sector after the bootloader) using BIOS function Int 13/ah=2. Control would then be transferred to it via a FAR JMP to 0x0000:0x7e00.
The bootloader code would look like this:
%define WITH_BPB 1 global bpb_disk_info jmp boot_start TIMES 3-($-$$) DB 0x90 ; Support 2 or 3 byte encoded JMPs before BPB. 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 "
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 comaptible with USB floppy media %include "bpb.inc" boot_start: xor ax, ax ; DS=SS=ES=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 mov fs, ax mov gs, 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). ; Works for all valid FAT12 compatible disk geometries. ; ; Resources: http://www.ctyme.com/intr/rb-0607.htm ; https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion ; https://stackoverflow.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 ; Uncomment these lines if not using a BPB (via bpb.inc) %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 ;BOOT_SECTION_SIZE equ $-$$ ; Pad boot sector to 510 bytes and add 2 byte boot signature for 512 total bytes ;section .bootsig start=BOOT_SECTION_SIZE align=1 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
To use this you would first generate a binary file called
stage2.bin. Then you would build the 1.44MiB disk image with this command:
nasm -f bin boot.asm -o disk.img
The code in
stage2.bin would have to be generated with the assumption that the ORG (origin point) is 0x07e00 in memory.
An example of code generated to a file called
stage2.bin that can be loaded with this test harness:
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
Note: there is an
ORG 0x7e00 at the top. This is important. To assemble this file into
nasm -f bin testcode.asm -o stage2.bin
Then create the 1.44MiB disk image with:
nasm -f bin boot.asm -o disk.img
The result should be a disk image exactly 1.44MiB in size, contains a copy of
stage2.bin and has our test harness boot sector.
stage2.bin can be anything that has binary code written to be loaded and started at 0x0000:0x7e00. The language (C, assembly etc) used to create the code in
stage2.bin doesn't matter. I use NASM for this example. When this test code is executed in QEMU using
qemu-system-i386 -fda disk.img it would look similar to this:
Special Note: : If you don't want to use
bpb.inc and don't intend to boot from USB using FDD emulation you can comment out or remove this line in