I am developing a custom bootloader for my 64-bit OS in x86_64 NASM on macOS environment using QEMU. The bootloader is a 2-stage, and is designed to load operating systems from disks. I am experiencing issues where the bootloader fails to properly jump to the kernel entry point after loading it into memory.
Current Behavior
- The bootloader successfully reads and loads Stage 2 into memory (“Stage 2 loaded successfully!”).
- It then attempts to jump to the kernel located at the higher-half address
0xFFFFFFFF80000000
- The bootloader just freezes when it tries to load the kernel
Relevant Code:
first_stage.asm
:
; First stage bootloader
[BITS 16]
[ORG 0x7C00]
start:
; Set up segments and stack
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ; Set stack just below the bootloader
; Store boot drive number
mov [bootDrive], dl
; Load Stage 2 (3 sectors, starting from sector 2)
mov ah, 0x02 ; BIOS read sector function
mov al, 3 ; Number of sectors to read
mov ch, 0 ; Cylinder number
mov cl, 2 ; Start from sector 2
mov dh, 0 ; Head number
mov bx, 0x7E00 ; Load Stage 2 right after the bootloader
int 0x13 ; BIOS interrupt to read disk
jc disk_error ; Jump if error
; Print success message
mov si, successMsg
call print_string
; Pass boot drive number to second stage and jump
mov dl, [bootDrive] ; Load boot drive number
jmp 0x0000:0x7E00
disk_error:
mov si, diskErrorMsg
call print_string
mov ah, 0x0E ; Print the error code in AH
int 0x10
cli
hlt
print_string:
lodsb
or al, al
jz done
mov ah, 0x0E
int 0x10
jmp print_string
done:
ret
diskErrorMsg db 'Disk Error!', 13, 10, 0
successMsg db 'Stage 2 loaded successfully!', 13, 10, 0
bootDrive db 0
times 510 - ($ - $$) db 0
dw 0xAA55 ; Boot signature
second_stage.asm
:
; Second stage bootloader
; Second stage bootloader
[BITS 16]
[ORG 0x7E00]
start:
; Set up segments
xor ax, ax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ax, 0x9000 ; Use segment 0x9000 (0x90000 physical address)
mov ss, ax
mov sp, 0xFFFF ; Set stack pointer to the top of the segment
; Print initial success message
mov si, stage2Msg
call print_string_16
; Enable A20 line for mem access beyond 1MB
call enable_A20
; Load the kernel
call load_kernel
; Load GDT and switch to protected mode
call enable_protected_mode
enable_A20:
in al, 0x92
or al, 2
out 0x92, al
ret
load_kernel:
mov ah, 0x02 ; BIOS read sector function
mov al, 9 ; Number of sectors to read (adjust as needed)
mov ch, 0 ; Cylinder number
mov cl, 5 ; Start from sector 5
mov dh, 0 ; Head number
; DL already contains the boot drive number from the first stage
mov bx, 0x8000 ; Load starting at 0x8000
int 0x13 ; BIOS interrupt to read disk
jc disk_error ; Jump if error
ret
enable_protected_mode:
cli ; Disable interrupts
lgdt [gdt_descriptor]
; Enable protected mode
mov eax, cr0
or eax, 1 ; Set PE (Protection Enable) bit
mov cr0, eax
; Enable PAE
mov eax, cr4
or eax, 1 << 5 ; Set PAE bit
mov cr4, eax
; Enter protected mode
jmp 0x08:protected_mode_entry
[BITS 32]
protected_mode_entry:
mov ax, 0x10 ; Data segment selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Set up stack for protected mode
mov esp, 0x90000
; Debug print
mov esi, protected_mode_msg
call print_string_pm
; Set up paging for long mode
call setup_paging
; Enable long mode
call enable_long_mode
; Load 64-bit GDT
lgdt [gdt64_descriptor]
; Jump to long mode entry
jmp 0x08:long_mode_entry
setup_paging:
; Set up 4-level paging
mov edi, 0x1000
mov cr3, edi
xor eax, eax
mov ecx, 4096
rep stosd
mov edi, cr3
mov dword [edi], 0x2003 ; PML4[0] -> PDPT
add edi, 0x1000
mov dword [edi], 0x3003 ; PDPT[0] -> PD
add edi, 0x1000
mov dword [edi], 0x4003 ; PD[0] -> PT
add edi, 0x1000
mov ebx, 0x00000003 ; Entry flags
mov ecx, 512 ; Number of entries
.set_entry:
mov dword [edi], ebx ; Set entry
add ebx, 0x1000 ; Next page
add edi, 8 ; Next entry
loop .set_entry
ret
enable_long_mode:
; Enable long mode in IA32_EFER MSR
mov ecx, 0xC0000080
rdmsr
or eax, 1 << 8 ; Set Long Mode Enable (LME) bit
wrmsr
ret
[BITS 64]
long_mode_entry:
; Set up segment registers for long mode
mov ax, 0x10 ; Data segment selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Set up stack for long mode
mov rsp, 0x90000
; Print long mode message
mov rsi, long_mode_msg
call print_string_lm
mov rax, 0xFFFFFFFF80000000 ; Kernel address
jmp rax
[BITS 16]
print_string_16:
lodsb
or al, al
jz .done
mov ah, 0x0E ; BIOS teletype function
int 0x10
jmp print_string_16
.done:
ret
[BITS 32]
print_string_pm:
push eax
push ebx
.loop:
lodsb
or al, al
jz .done
mov ah, 0x0F ; Text attribute for character
mov ebx, 0xB8000 ; VGA video memory address
mov [ebx], ax ; Write character to VGA memory
add ebx, 2 ; Move to next character
jmp .loop
.done:
pop ebx
pop eax
ret
[BITS 64]
print_string_lm:
push rax
push rbx
.loop:
lodsb
or al, al
jz .done
mov ah, 0x0F ; Text attribute for character
mov rbx, 0xB8000 ; VGA video memory address
mov [rbx], ax ; Write character to VGA memory
add rbx, 2 ; Move to next character
jmp .loop
.done:
pop rbx
pop rax
ret
[BITS 16]
disk_error:
mov si, diskErrorMsg
call print_string_16
cli
hlt
; Data
stage2Msg db 'Entered Stage 2', 13, 10, 0
protected_mode_msg db 'Entered Protected Mode', 0
long_mode_msg db 'Entered Long Mode', 0
diskErrorMsg db 'Disk Error While Reading Kernel!', 13, 10, 0
; GDT for Protected Mode
gdt_start:
dq 0x0000000000000000 ; Null descriptor
dq 0x00CF9A000000FFFF ; 32-bit code descriptor
dq 0x00CF92000000FFFF ; 32-bit data descriptor
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
; GDT for Long Mode
gdt64_start:
dq 0x0000000000000000 ; Null descriptor
dq 0x00AF9A000000FFFF ; 64-bit code descriptor
dq 0x00AF92000000FFFF ; 64-bit data descriptor
gdt64_end:
gdt64_descriptor:
dw gdt64_end - gdt64_start - 1
dq gdt64_start
times 1536 - ($ - $$) db 0 ; Pad to 3 sectors
linker.ld
:
ENTRY(kernel_start)
KERNEL_VMA = 0xFFFFFFFF80000000;
KERNEL_LMA = 0x8000;
SECTIONS
{
. = KERNEL_VMA;
.text ALIGN(4K) : AT(ADDR(.text) - KERNEL_VMA + KERNEL_LMA)
{
*(.text*)
}
.rodata ALIGN(4K) : AT(ADDR(.rodata) - KERNEL_VMA + KERNEL_LMA)
{
*(.rodata*)
}
.data ALIGN(4K) : AT(ADDR(.data) - KERNEL_VMA + KERNEL_LMA)
{
*(.data*)
}
.bss ALIGN(4K) : AT(ADDR(.bss) - KERNEL_VMA + KERNEL_LMA)
{
*(COMMON)
*(.bss*)
}
/DISCARD/ :
{
*(.eh_frame)
*(.comment)
}
}
Makefile
:
ARCH ?= x86_64
ASM=nasm
CC=$(ARCH)-elf-gcc
LD=$(ARCH)-elf-ld
OBJCOPY=x86_64-elf-objcopy
STRIP=x86_64-elf-strip
NASMFLAGS=-f bin
NASMFLAGS_ELF64=-f elf64
CFLAGS=-ffreestanding -nostdlib -mno-red-zone -Wall -Wextra -g -O2 -mcmodel=kernel
LDFLAGS=-T linker.ld
BUILD_DIR=bin
BOOT_DIR=boot/$(ARCH)
KERNEL_DIR=kernel
ARCH_DIR=kernel/arch/
KERNEL_BIN=$(BUILD_DIR)/kernel-$(ARCH).bin
KERNEL_ELF=$(BUILD_DIR)/kernel-$(ARCH).elf
BOOTLOADER_IMG=$(BUILD_DIR)/bootloader-$(ARCH).img
KERNEL_C=$(KERNEL_DIR)/kernel.c
KERNEL_ENTRY=$(ARCH_DIR)/entry-$(ARCH).asm
FIRST_STAGE=$(BOOT_DIR)/first_stage.asm
SECOND_STAGE=$(BOOT_DIR)/second_stage.asm
$(BUILD_DIR):
@mkdir -p $(BUILD_DIR)
$(BUILD_DIR)/first_stage-$(ARCH).bin: $(FIRST_STAGE) | $(BUILD_DIR)
$(ASM) $(NASMFLAGS) $< -o $@
$(BUILD_DIR)/second_stage-$(ARCH).bin: $(SECOND_STAGE) | $(BUILD_DIR)
$(ASM) $(NASMFLAGS) $< -o $@
$(BUILD_DIR)/entry-$(ARCH).o: $(KERNEL_ENTRY) | $(BUILD_DIR)
$(ASM) $(NASMFLAGS_ELF64) $< -o $@
$(BUILD_DIR)/kernel.o: $(KERNEL_C) | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(KERNEL_ELF): $(BUILD_DIR)/entry-$(ARCH).o $(BUILD_DIR)/kernel.o | $(BUILD_DIR)
$(LD) $(LDFLAGS) -o $@ $^
$(KERNEL_BIN): $(KERNEL_ELF) | $(BUILD_DIR)
$(OBJCOPY) -O binary $< $@
$(BOOTLOADER_IMG): $(BUILD_DIR)/first_stage-$(ARCH).bin $(BUILD_DIR)/second_stage-$(ARCH).bin $(KERNEL_BIN)
dd if=/dev/zero of=$(BOOTLOADER_IMG) bs=512 count=131072
dd if=$(BUILD_DIR)/first_stage-$(ARCH).bin of=$(BOOTLOADER_IMG) bs=512 seek=0 conv=notrunc
dd if=$(BUILD_DIR)/second_stage-$(ARCH).bin of=$(BOOTLOADER_IMG) bs=512 seek=2 conv=notrunc
dd if=$(KERNEL_BIN) of=$(BOOTLOADER_IMG) bs=512 seek=34 conv=notrunc
run: $(BOOTLOADER_IMG)
qemu-system-x86_64 -drive file=bin/bootloader-$(ARCH).img,format=raw -monitor stdio
clean:
rm -rf $(BUILD_DIR)/*.o $(BUILD_DIR)/*.bin $(BUILD_DIR)/*.img $(KERNEL_ELF)
all: $(BOOTLOADER_IMG)
rerun: clean all run
Question: What did I do wrong?
PS: The kernel is small enough to fit within the allocated sectors (190 bytes), and it is allocated correctly in the expected address. The first stage boot is at 0x7C00
.
I tried debugging using print statements in VGA however there was nothing on the emulator console. There were also warnings during assemble as well:
boot/x86_64/second_stage.asm:172: warning: signed dword immediate exceeds bounds [-w+number-overflow]
boot/x86_64/second_stage.asm:172: warning: dword data exceeds bounds [-w+number-overflow]
Edit 1: It seems that load_kernel
does not return after being called and BIOS interrupt caused issue(?).
Edit 2: The registers appeared to be right and correctly jumped into 64-bit. However, the kernel is not loaded correctly in the memory but no error was thrown (memory inaccessible at 0xFFFFFFFF80000000
.
typeAl_ is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
20