I’m working on a two-stage bootloader written in NASM, and I’m using QEMU as my emulator. Stage 1 successfully loads Stage 2, and the A20 line setup works fine with all expected messages being printed.
However, once the GDT loading… message is printed in Stage 2, the system halts unexpectedly, and no further messages appear on the terminal, particularly the success or failure message related to the GDT setup. I suspect something is going wrong either during or after the GDT setup.
Terminal Output:
Booting from Hard Disk...
A20 loading...
A20 line enable -> successfully
GDT loading...
I expected to see either the message:
GDT load -> successfully
or
GDT load -> failed.
Project Overview:
- Assembler: NASM
- Emulator: QEMU
- Bootloader: Two-stage bootloader (Stage 1 loads Stage 2).
- Makefile: I’m using a makefile to assemble the bootloader stages and create the bootloader image.
Here’s the relevant code structure:
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 := $(PROJECT_ROOT)/bootloader.img
QEMU_IMG := qemu-img
DD := dd
output: clean $(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 conv=notrunc status=progress
$(DD) if=$(STAGE2_BIN) of=$(IMG) bs=512 seek=1 conv=notrunc status=progress
clean:
@echo "Cleaning up..."
rm -f "$(STAGE1_BIN)" "$(STAGE2_BIN)" "$(IMG)"
stage1.asm
stage1.asm:
[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 start execution.
main: ; Main routine of the bootloader begins here.
; -------------------------
; Setup segment registers
; -------------------------
cli ; Clear interrupts to ensure no interrupts occur while setting up segments.
xor ax, ax ; Set AX to 0x0 (which is 0x0 >> 4).
; Explanation: We are using segment:offset addressing in real mode.
; Physical address = Segment * 16 + Offset
; So, the segment 0x0 * 16 = 0x0 (physical address).
mov ds, ax ; Set Data Segment (DS) to 0x0. DS points to the bootloader code/data in memory.
mov es, ax ; Set Extra Segment (ES) to 0x0. ES is also set to point to our code/data.
; -------------------------
; Setup stack
; -------------------------
mov ss, ax ; Set Stack Segment (SS) to 0 (base of memory).
mov sp, 0xFFFE ; Set the Stack Pointer (SP) to the highest address within the current 64KB segment (0x0000:0xFFFE).
; In real mode, the stack grows downward from 0xFFFE and 0xFFFE should be set to an even offset like 0xFFFE, not an odd one.
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 1 sector from the disk (this corresponds to the size of a sector, which is 512 bytes).
mov ch, 00h ; Set Cylinder number to 0 (since both Stage 1 and Stage 2 are on Cylinder 0).
mov cl, 02h ; Set Sector number to 2 (Stage 1 is in Sector 1, so Stage 2 starts at Sector 2).
mov dh, 00h ; Set Head number to 0 (assuming we are using Head 0 for now).
mov dl, 80h ; Use the first hard drive (usually 0x80 for the primary hard disk).
mov bx, 0x8000 ; Set BX to 0x8000, the offset address where Stage 2 will be loaded.
; Stage 2 will be loaded into memory using segment:offset addressing.
; ES = 0x0, BX = 0x8000, so the physical address = ES * 16 + BX.
; Formula: 0x0 * 16 + 0x8000 = 0x0 + 0x8000 = 0x8000 (the physical address where Stage 2 is loaded).
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 0x0000:0x8000 ; Jump to the loaded Stage 2 at address 0x0000:0x8000 (this is where Stage 2 resides).
; Here, 0x0000 is the segment, and 0x8000 is the offset.
; Physical address = 0x0000 * 16 + 0x8000 = 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 message will occur --> IO write(0x01f0): current command is 20h displayed on bochs emulator.
TIMES 510-($-$$) DB 0 ; Pad the bootloader to ensure it is exactly 512 bytes, with zeros filling the remaining space.
DW 0xAA55
stage2.asm
[BITS 16]
[org 0x8000]
jmp a20_enter
; -------------------------
; Messages
; -------------------------
section .data
msg_a20_loading db 'A20 loading... ', 0x0D, 0x0A, 0
msg_a20_error db 'A20 line enable -> failed ', 0x0D, 0x0A, 0
msg_a20_enable db 'A20 line enable -> successfully ', 0x0D, 0x0A, 0
msg_gdt_loading db 'GDT loading... ', 0x0D, 0x0A, 0
msg_gdt_success db 'GDT load -> successfully ', 0x0D, 0x0A, 0
msg_tss_loading db 'TSS loading... ', 0x0D, 0x0A, 0
msg_tss_success db 'TSS load -> successfully ', 0x0D, 0x0A, 0
; -------------------------
; BSS Section for TSS
; -------------------------
section .bss
tss resb 104 ; Reserve 104 bytes for the TSS (Task State Segment)
; -------------------------
; Initialize the TSS
; -------------------------
section .text
init_tss:
; Set ESP0 (stack pointer for privilege level 0)
mov eax, 0x9FBFF ; Example stack pointer for ring 0
mov [tss + 4], eax ; Set ESP0 in the TSS
; Set SS0 (stack segment for privilege level 0)
mov ax, 0x10 ; Kernel data segment selector (0x10)
mov [tss + 8], ax ; Set SS0 in the TSS
ret ; Return to the main flow
; -------------------------
; Print Message: A20 Loading
; -------------------------
a20_enter:
mov ax, 0x0000 ; Set data segment to 0 for memory access
mov ds, ax
mov ah, 0x0E ; BIOS teletype function (interrupt 0x10, AH=0x0E) for printing characters
mov bh, 0x00 ; Display page number
mov si, msg_a20_loading ; Load pointer to "A20 loading..." message into SI
a20_print_loading:
lodsb ; Load next byte from [SI] into AL
cmp al, 0 ; Check if null terminator (end of string)
je a20_status_check ; Jump to A20 status check if end of message
int 0x10 ; BIOS interrupt to print character in AL
jmp a20_print_loading ; Repeat for next character
; -------------------------
; A20 Status Check
; -------------------------
a20_status_check:
pushf ; Save flags
push ds ; Save data segment
push es ; Save extra segment
push di ; Save destination index
push si ; Save source index
cli ; Disable interrupts during A20 check
xor ax, ax ; Set AX to 0
mov es, ax ; Set extra segment (ES) to 0
mov di, 0x0500 ; Set DI to test memory location 0x0500 in low memory
not ax ; Set AX to 0xFFFF (inverted 0)
mov ds, ax ; Set data segment (DS) to 0xFFFF
mov si, 0x0510 ; Set SI to memory location 0x0510 (also in low memory)
; Save original values from memory locations
mov al, byte [es:di] ; Load byte at ES:DI into AL
push ax ; Save value from ES:DI
mov al, byte [ds:si] ; Load byte at DS:SI into AL
push ax ; Save value from DS:SI
; Modify memory locations for the test
mov byte [es:di], 0x00 ; Set ES:DI to 0x00
mov byte [ds:si], 0xFF ; Set DS:SI to 0xFF
; Check if memory write is visible at ES:DI
cmp byte [es:di], 0xFF ; Compare ES:DI with 0xFF
; Restore original memory values
pop ax ; Restore value from stack to AX
mov byte [ds:si], al ; Restore original byte at DS:SI
pop ax ; Restore value from stack to AX
mov byte [es:di], al ; Restore original byte at ES:DI
; Determine A20 status based on comparison result
mov ax, 0 ; Assume A20 is off
je status_a20_off ; Jump if A20 is off
mov ax, 1 ; A20 is on
jmp status_a20_on ; Jump to A20 enabled handling
; -------------------------
; A20 Enable via BIOS Interrupt 15h
; -------------------------
status_a20_off:
mov ax, 0x2401 ; Request to enable A20 via BIOS (INT 15h)
int 0x15 ; BIOS interrupt call
jc a20_error ; If carry flag is set (error), jump to error handler
jmp a20_status_check ; Otherwise, recheck A20 status after enabling
; -------------------------
; A20 Enable Failure Handling
; -------------------------
a20_error:
mov ax, 0x0000 ; Reset segment register for message output
mov ds, ax
mov ah, 0x0E ; Set up for character output
mov bh, 0x00 ; Display page number
mov si, msg_a20_error ; Load error message into SI
print_error_loop:
lodsb ; Load next byte from message
cmp al, 0 ; Check for null terminator
je end_error_msg_a20 ; If end of string, stop printing
int 0x10 ; Print character in AL
jmp print_error_loop ; Loop to print the next character
end_error_msg_a20:
cli ; Disable interrupts
hlt ; Halt the system
; -------------------------
; A20 Enable Success Handling
; -------------------------
status_a20_on:
mov ax, 0x0000 ; Reset data segment for message output
mov ds, ax
mov ah, 0x0E ; Set up for character output
mov bh, 0x00 ; Display page number
mov si, msg_a20_enable ; Load success message into SI
print_enable_loop:
lodsb ; Load next byte from message
cmp al, 0 ; Check for null terminator
je restore_registers_a20 ; If end of string, restore registers
int 0x10 ; Print character in AL
jmp print_enable_loop ; Loop to print the next character
; -------------------------
; Restore Registers and Continue
; -------------------------
restore_registers_a20:
pop si ; Restore source index
pop di ; Restore destination index
pop es ; Restore extra segment
pop ds ; Restore data segment
popf ; Restore flags
sti ; Re-enable interrupts
; -------------------------
; GDT And TSS Loading Message
; -------------------------
mov ax, 0x0000 ; Reset data segment for message output
mov ds, ax
mov ah, 0x0E ; Set up for character output
mov bh, 0x00 ; Display page number
mov si, msg_gdt_loading ; Load GDT loading message into SI
gdt_print_loading:
lodsb ; Load next byte from message
cmp al, 0 ; Check for null terminator
je tss_loading_msg ; If end of string, jump to GDT setup
int 0x10 ; Print character in AL
jmp gdt_print_loading ; Loop to print the next character
tss_loading_msg:
mov ax, 0x0000 ;
mov ds, ax
mov ah, 0x0E ;
mov bh, 0x00 ;
mov si, msg_tss_loading ;
tss_print_loading:
lodsb ;
cmp al, 0 ;
je gdt_setup ;
int 0x10 ;
jmp tss_print_loading ;
; -------------------------
; GDT and TSS Setup and Loading
; -------------------------
gdt_setup:
cli ; Disable interrupts during GDT setup
lgdt [gdt_descriptor] ; Load GDT descriptor into GDTR
push ds
push es
call init_tss ; Initialize the TSS
pop es
pop ds
mov ax, 0x28 ;
ltr ax ; <--- Bug is here!!!!!
; Display success message after loading GDT
mov ax, 0x0000 ; Reset data segment for message output
mov ds, ax
mov ah, 0x0E ; Set up for character output
mov bh, 0x00 ; Display page number
mov si, msg_gdt_success ; Load GDT success message into SIs
gdt_print_success:
lodsb ; Load next byte from message
cmp al, 0 ; Check for null terminator
je tss_succsess_msg ; If end of string, proceed to protected mode
int 0x10 ; Print character in AL
jmp gdt_print_success ; Loop to print the next character
tss_succsess_msg: ;
mov ax, 0x0000 ;
mov ds, ax
mov ah, 0x0E ;
mov bh, 0x00 ;
mov si, msg_tss_success ;
tss_print_success:
lodsb ;
cmp al, 0 ;
je enter_protected_mode ;
int 0x10 ;
jmp tss_print_success ;
; -------------------------
; Enter Protected Mode
; -------------------------
enter_protected_mode:
cli ; Disable interrupts before entering protected mode
mov eax, cr0
or eax, 1 ; Set protected mode bit in CR0
mov cr0, eax
; Far jump to update CS with GDT code segment (0x08)
jmp 0x08:update_segments
[BITS 32]
update_segments:
; Set up segment registers for protected mode
mov ax, 0x10 ; Load GDT data segment selector (0x10)
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
jmp kernel ; Jump to kernel code
; -------------------------
; Kernel Code (currently halting)
; -------------------------
kernel:
hlt ; Halt the CPU (placeholder for actual kernel code)
; -------------------------
; GDT Descriptor and TSS
; -------------------------
section .data
gdt_start:
; Null Descriptor
dd 0x0
dd 0x0
; Kernel Code Segment (DPL = 0)
dw 0xFFFF ; Limit
dw 0x0000 ; Base (lower 16 bits)
db 0x00 ; Base (next 8 bits)
db 10011010b ; Access byte
db 11001111b ; Flags and limit (upper 4 bits)
db 0x00 ; Base (upper 8 bits)
; Kernel Data Segment (DPL = 0)
dw 0xFFFF ; Limit
dw 0x0000 ; Base (lower 16 bits)
db 0x00 ; Base (next 8 bits)
db 10010010b ; Access byte
db 11001111b ; Flags and limit (upper 4 bits)
db 0x00 ; Base (upper 8 bits)
; User Code Segment (DPL = 3)
dw 0xFFFF ; Limit
dw 0x0000 ; Base (lower 16 bits)
db 0x00 ; Base (next 8 bits)
db 11111010b ; Access byte (DPL = 3)
db 11001111b ; Flags and limit (upper 4 bits)
db 0x00 ; Base (upper 8 bits)
; User Data Segment (DPL = 3)
dw 0xFFFF ; Limit
dw 0x0000 ; Base (lower 16 bits)
db 0x00 ; Base (next 8 bits)
db 11110010b ; Access byte (DPL = 3)
db 11001111b ; Flags and limit (upper 4 bits)
db 0x00 ; Base (upper 8 bits)
; TSS Descriptor
tss_base_addr: equ 0x10000 ; Base address for TSS
dw 0x0067 ; Limit
dw tss_base_addr ; Base (lower 16 bits)
db (tss_base_addr >> 16) ; Base (next 8 bits)
db 10001001b ; Access byte
db 00000000b ; Flags and limit (upper 4 bits)
db (tss_base_addr >> 24) ; Base (upper 8 bits)
gdt_end:
; GDT Descriptor (contains size and location of GDT)
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; Size of the GDT (in bytes, minus 1)
dd gdt_start
The A20-related messages print correctly, so the A20 setup seems to be working as expected.
The GDT loading… message is printed, which means the flow reaches the GDT setup.
However, after setting up the GDT (with lgdt and ltr instructions), no success or failure message prints, and the system seems to halt.
I expected to see either:
GDT load -> successfully
or.
GDT load -> failed
.
I’m unsure if the issue lies within the GDT setup itself or if something else is preventing the success/failure messages from being displayed after the GDT is loaded.
4