Search code examples
assemblynasmx86-16bootloaderosdev

Bootloader doesn't jump to stage 2 to display a message


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)"

HxD test: enter image description here Issue:

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:

  1. Set up segment registers and stack.

  2. Read the second stage from the disk into memory.

  3. 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.


Solution

  • What ORG is about

    When 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
    
    • No need for 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.
    • I prefer to have the stack in the memory below the bootloader.
    • You should always keep the stackpointer naturally aligned; in real address mode an even offset address is fine. In other words: don't use mov sp, 0xFFFF, but write mov sp, 0xFFFE.

    Comments are very important

    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
    

    The 0x00008000 physical address

    • There are many ways you can specify this address with a segmented pointer, but the more common ones tend to leave either the segment part or the offset part at zero. Either 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.
    • There's no need to reload ES that we previously set at 0. Anyway you expected it to hold 0x7C0 but were loading 0x0201 into it. Both values would have been wrong!
    • Since you've managed till here to not modify the DL register, it is best you trust BIOS for having provided the drive number ready for use, in other words "Don't include 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
    

    The second stage

    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.