I'm developing a bootloader for a 32-bit kernel, and I've encountered an issue with the transition from stage 1 to stage 2 of the bootloader. The code doesn't seem to jump to stage 2 as expected. I'm using NASM for assembly and QEMU for emulation. Here is the code for both stages and the Makefile used to build the bootloader.
Stage 1 Code:
[BITS 16] ; We are working in 16-bit Real Mode
[org 0x7c00] ; The origin (starting address) of the bootloader in memory, which is 0x7C00 as loaded by the BIOS.
; This is the physical address where the bootloader is loaded into memory.
start: ; Start of execution, this label marks the entry point of the code.
jmp main ; Jump to the 'main' label to skip over data (if present), ensuring the code runs properly.
main: ; Main routine of the bootloader begins here.
; -------------------------
; Setup segment registers
; -------------------------
cli ; Clear interrupts to ensure no interrupts occur while setting up segments.
mov ax, 0x7C0 ; Set AX to 0x7C0 (which is 0x7C00 >> 4).
; Explanation: We are using segment:offset addressing in real mode.
; Physical address = Segment * 16 + Offset
; So, the segment 0x7C0 * 16 = 0x7C00 (physical address).
; This is the base segment for our code loaded by BIOS at the physical address 0x7C00.
mov ds, ax ; Set Data Segment (DS) to 0x7C0. DS points to the bootloader code/data in memory.
mov es, ax ; Set Extra Segment (ES) to 0x7C0. ES is also set to point to our code/data.
mov fs, ax ; Set FS to 0x7C0.
mov gs, ax ; Set GS to 0x7C0.
; -------------------------
; Setup stack
; -------------------------
xor ax, ax ; Set AX to 0 (clear register).
mov ss, ax ; Set Stack Segment (SS) to 0 (base of memory).
mov sp, 0xFFFF ; Set the Stack Pointer (SP) to the top of memory. The stack grows downwards from 0xFFFF.
; Although SS is set to 0x0000 here, the actual physical address for the stack
; will be 0x0000:0xFFFF = 0xFFFF (top of the 64KB memory block).
sti ; Re-enable interrupts after segment and stack setup is complete.
; -------------------------
; Load Stage 2 bootloader from disk
; -------------------------
mov ah, 02h ; BIOS Interrupt 13h, Function 02h: Read sectors from the disk.
mov al, 01h ; Read 63 sectors (this should correspond to the size of Stage 2). Ensure this number does not exceed the size of Stage 2 to avoid reading unnecessary code.
mov ch, 00h ; Set Cylinder number to 1 (since Stage 1 is at Cylinder 0, Stage 2 starts at Cylinder 1).
mov cl, 02h ; Set Sector number to 1 (the first sector on the cylinder to read from).
mov dh, 00h ; Set Head number to 0 (assuming we are using Head 0 for now).
mov dl, 0x80
mov es, ax ; Set ES to the address where Stage 2 should be loaded (0x7C0).
mov bx, 0x8000 ; Set BX to 0x8000, the memory address where Stage 2 will be loaded.
; Stage 2 will be loaded into the physical address 0x8000:0000 (0x08000 physical address).
int 13h ; Call BIOS interrupt 13h to read the specified sectors into memory.
jc disk_read_error ; If carry flag is set (indicating an error), jump to the error handler.
pass: ; If the disk read was successful (carry flag is cleared), continue from here.
jmp 0x0800:0x0000 ; Jump to the loaded Stage 2 at address 0x0800:0x0000 (this is where Stage 2 resides).
; Here, 0x0800 is the segment, and 0x0000 is the offset.
; Physical address = 0x0800 * 16 + 0x0000 = 0x8000, where Stage 2 is loaded.
disk_read_error:
int 18h ; If the disk read fails, call INT 18h to attempt a boot from a different device (like network boot).
; This error massage will occur --> IO write(0x01f0): current command is 20h
TIMES 510-($-$$) DB 0 ; Pad the bootloader to ensure it is exactly 512 bytes, with zeros filling the remaining space.
DW 0xAA55 ; The boot signature (magic number) required for the BIOS to recognize this as a bootable sector.
Stage 2 Code:
[BITS 16]
[ORG 0x8000]
start:
mov si, message
call print_string
cli
hlt
print_string:
mov ah, 0x0E
.next_char:
lodsb
cmp al, 0
je done
int 0x10
jmp .next_char
done:
ret
message db "Hello from Stage 2! :)", 0
TIMES 510-($-$$) db 0
DW 0xAA55
Makefile:
PROJECT_ROOT := $(CURDIR)
STAGE1_SRC := $(PROJECT_ROOT)\Boot\Stage1\stage1.asm
STAGE2_SRC := $(PROJECT_ROOT)\Boot\Stage2\stage2.asm
STAGE1_BIN := $(PROJECT_ROOT)\Boot\Stage1\stage1.bin
STAGE2_BIN := $(PROJECT_ROOT)\Boot\Stage2\stage2.bin
IMG := C:\Users\ilanv\OS_32Bit\Code\bootloader.img
QEMU_IMG := "C:/QEMU/qemu-img.exe"
DD := "C:/dd/dd.exe"
output: $(STAGE1_BIN) $(STAGE2_BIN) $(IMG)
$(STAGE1_BIN): $(STAGE1_SRC)
@echo "Assembling Stage 1..."
nasm -f bin "$(STAGE1_SRC)" -o "$(STAGE1_BIN)"
$(STAGE2_BIN): $(STAGE2_SRC)
@echo "Assembling Stage 2..."
nasm -f bin "$(STAGE2_SRC)" -o "$(STAGE2_BIN)"
$(IMG): $(STAGE1_BIN) $(STAGE2_BIN)
@echo "Creating bootloader.img..."
$(QEMU_IMG) create -f raw "$(IMG)" 10M
$(DD) if=$(STAGE1_BIN) of=$(IMG) bs=512 count=1
$(DD) if=$(STAGE2_BIN) of=$(IMG) bs=512 seek=1
clean:
@echo "Cleaning up..."
if exist "$(STAGE1_BIN)" del "$(STAGE1_BIN)"
if exist "$(STAGE2_BIN)" del "$(STAGE2_BIN)"
if exist "$(IMG)" del "$(IMG)"
When running the bootloader in QEMU, it does not jump to Stage 2 as expected. The message "Hello from Stage 2! :)" does not appear. I've double-checked the segment and offset values, and verified the .img
file using HxD, and everything seems correct.
Any suggestions on what might be causing this problem? Are there any common issues or mistakes that could prevent the jump to Stage 2?
I wrote and assembled a simple bootloader with two stages. The first stage is supposed to:
Set up segment registers and stack.
Read the second stage from the disk into memory.
Jump to the address where the second stage is loaded.
The second stage is a simple program that displays the message "Hello from Stage 2! :)" using BIOS interrupt 0x10 for text output.
I used NASM to assemble the code and QEMU for emulation. The bootloader and stages are combined into a bootable image using a Makefile, which creates an image file with the first stage at the beginning and the second stage immediately following it.
ORG
is aboutWhen you write [org 0x7C00]
you are making a promise to the assembler (NASM in this case) that your code will reside in memory at the specified offset address (0x7C00 in this case). If eg. you were to include (in the below snippet) the instruction mov bx, Start
, you would see that NASM encodes this instruction like it was mov bx, 0x7C00
.
Since BIOS loads our bootloader in memory at linear address 0x00007C00, there can be only one setting for the segment registers that is correct. Zero is correct for an ORG 0x7C00
.
Start:
xor ax, ax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov sp, 0x7C00
cli
/sti
as long as you load SS and SP together and in that order, and you know for a fact that the processor supports FS and GS.mov sp, 0xFFFF
, but write mov sp, 0xFFFE
.The part where you "Load Stage 2 bootloader from disk" has a lot of comments that are incorrect.
mov al, 01h ; Read 63 sectors mov ch, 00h ; Set Cylinder number to 1 mov cl, 02h ; Set Sector number to 1
0x0000:0x8000
(segment zero) or 0x0800:0x0000
(offset zero). Because you have included an [ORG 0x8000]
in your stage 2, the more sensible choice would leave the segment at zero, so write jmp 0x0000:0x8000
.mov dl, 0x80
". mov dh, 0 ; Head 0
mov cx, 0x0002 ; Cylinder 0, Sector 2
mov bx, 0x8000 ; Buffer ES:BX = 0000h:8000h
mov ax, 0x0201 ; Function 2, Sectors 1
int 13h
jc disk_read_error
jmp 0x0000:0x8000
Your second stage can build on everything that was setup in the first stage. Except perhaps that it could be wise to include a one-time explicit cld
instruction since you are working with the lodsb
string primitive and you want to rely on auto-incrementing SI.
The second stage is loaded by you, therefore no 0xAA55 signature is required. You can safely end with TIMES 512-($-$$) db 0
.