3

I'm writing a custom OS with my own bootloader and trying to switch from an assembly-based kernel (kernel.asm) to a C-based kernel (kernel.c).

When using kernel.asm, everything works fine. But when I replace it with kernel.c, my bootloader fails to load the kernel and gives an error.

Setup Details:

  1. Bootloader: Custom (written in NASM)

  2. Mode Switching: Bootloader starts in 16-bit real mode, then switches to 32-bit protected mode before calling the kernel

  3. Kernel: kernel.c compiled with i686-elf-gcc Linker: i686-elf-ld

  4. Toolchain: Cross-compiler on macOS (M2 Mac)

  5. Disk Image: MBR boot sector

  6. Testing with: qemu-system-i386 -hda os-image.bin

Bootloader (boot.asm) -

  1. Loads kernel.bin at 0x10000

  2. Switches to protected mode

  3. Jumps to 0x10000 assuming it's the kernel entry point

Jump Code (After Switching to Protected Mode):

mov eax, 0x10000
jmp eax  ; Jump to kernel

With kernel.asm, everything works fine.

Problem When Using kernel.c

I replaced kernel.asm with kernel.c:

void kernel_main() {
    char *video_memory = (char *)0xB8000;
    *video_memory = 'X';  // Print 'X' to the screen
    while (1);
}

I also defined _start as the entry point:

__attribute__((section(".text"))) 
__attribute__((noreturn)) 
void _start() {
    kernel_main();
    while (1);  // Prevent returning
}

Linker Script (linker.ld):

OUTPUT_FORMAT(elf32-i386)
ENTRY(_start)

SECTIONS
{
    . = 0x1000;          /* Load the kernel at 0x1000 */
    .text ALIGN(4) : { *(.text) }
    .rodata ALIGN(4) : { *(.rodata) }
    .data ALIGN(4) : { *(.data) }
    .bss ALIGN(4) : { *(.bss) }
}

Build Commands:

nasm -f bin boot.asm -o boot.bin
i686-elf-gcc -ffreestanding -m32 -c kernel.c -o kernel.o
i686-elf-ld -m elf_i386 -T linker.ld -o kernel.bin --oformat binary kernel.o
cat boot.bin kernel.bin > os-image.bin
qemu-system-x86_64 -fda os-image.bin

qemu-system-x86_64 -fda os-image.bin output

boot.asm

[bits 16]                  ; 16-bit Real Mode
[org 0x7c00]               ; Bootloader loads at 0x7C00

start:
    mov al, dl
    call print_hex_byte  ; Print boot drive number
    cli                    ; Disable interrupts
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00         ; Set up stack

    ; Print "LOADING KERNEL" message
    mov si, loading_msg
    call print_string

disk_read:
    mov ah, 0x00   ; Reset disk before reading
    mov dl, 0x00   ; Drive 0
    int 0x13
    
    mov ah, 0x02   ; BIOS Read Sector
    mov al, 1      ; Read 1 sector (512 bytes)
    mov ch, 0      ; Cylinder 0
    mov dh, 0      ; Head 0
    mov cl, 2      ; Sector 2
    mov dl, 0x00   ; Drive 0 (HDD)
    mov ax, 0x1000 
    mov es, ax
    mov bx, 0x0000

    mov cx, 3      ; Retry count
read_retry:
    int 0x13
    jnc read_ok    ; If successful, jump
    
    mov si, int_cmd
    call print_string
    jnc read_ok    ; If successful, jump

    loop read_retry  ; Retry 3 times
    jmp disk_error  ; Show error if all retries fail

; 16-byte Disk Address Packet (DAP)
dap:
    db 0x10         ; Size of packet (16 bytes)
    db 0            ; Always 0
    dw 1            ; Number of sectors to read
    dw 0x0000       ; Buffer offset
    dw 0x1000       ; Buffer segment
    dq 1            ; LBA sector to read (Sector 2)

read_ok:
    mov si, kernel_loaded_msg
    call print_string
    mov esi, 0x10000
    mov ecx, 16
print_loop:
    mov al, [esi]
    call print_hex_byte
    inc esi
    loop print_loop

    ; Load Global Descriptor Table (GDT)
    cli                  ; Disable interrupts
    lgdt [gdt_descriptor] ; Load GDT

    ; Set Protected Mode bit in CR0
    mov eax, cr0
    or eax, 0x1          ; Set PE (Protection Enable) bit
    mov cr0, eax

    ; Far jump to 32-bit mode (flushes CPU pipeline)
    jmp 0x08:init_protected_mode

; ----------------- 32-bit Protected Mode -----------------
[bits 32]
init_protected_mode:
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000

    ; Print marker to VGA to confirm protected mode
    mov dword [0xB8000], 0x2F4D2F4F   ; "MO" in red
    mov dword [0xB8004], 0x2F442F45   ; "DE" in red

    ; Read first 4 bytes of 0x10000
    ;mov esi, 0x10000
    ;mov al, [esi]
    ;call print_hex_byte  ; Print first byte
    ;mov al, [esi + 1]
    ;call print_hex_byte  ; Print second byte
    ;mov al, [esi + 2]
    ;call print_hex_byte  ; Print third byte
    ;mov al, [esi + 3]
    ;call print_hex_byte  ; Print fourth byte
    ;mov esi, 0x10000
    ;mov al, [esi]
    ;call print_hex_byte

    ; If kernel is missing, loop forever
    cmp dword [esi], 0x00000000
    je halt  ; Halt if first bytes are 0 (meaning kernel is missing)

    ; Jump to kernel
    ;call dword 0x10000
    mov eax, 0x10000
    jmp eax

; ----------------- Global Descriptor Table (GDT) -----------------
gdt:
    dq 0x0000000000000000  ; Null descriptor
    dq 0x00CF9A000000FFFF  ; Code segment (32-bit)
    dq 0x00CF92000000FFFF  ; Data segment (32-bit)

gdt_descriptor:
    dw gdt_descriptor - gdt - 1  ; GDT size
    dd gdt  ; GDT address

loading_msg db "LOADING KERNEL", 0
kernel_loaded_msg db "JUMP!", 0
int_cmd db "INT-T", 0
in_protected_mode db "IN-PRT", 0

disk_error:
    mov si, error_msg
    call print_string
    jmp halt

error_msg db "ERR!", 0

halt:
    hlt                   ; Halt the CPU

; ----------------- Print String Function -----------------
print_string:
    pusha
.loop:
    lodsb         ; Load next char from SI into AL
    or al, al     ; Check if null terminator
    jz .done
    mov ah, 0x0E  ; BIOS teletype function
    int 0x10
    jmp .loop
.done:
    popa
    ret

print_hex_byte:
    push ax
    push bx
    push cx
    push dx

    mov ah, 0x0E  ; BIOS teletype output

    ; Convert upper nibble to hex
    mov bl, al
    shr bl, 4
    cmp bl, 9
    jg hex_letter
    add bl, '0'
    jmp print1
hex_letter:
    add bl, 'A' - 10
print1:
    mov al, bl
    int 0x10

    ; Convert lower nibble to hex
    mov bl, al
    and bl, 0x0F
    cmp bl, 9
    jg hex_letter2
    add bl, '0'
    jmp print2
hex_letter2:
    add bl, 'A' - 10
print2:
    mov al, bl
    int 0x10

    pop dx
    pop cx
    pop bx
    pop ax
    ret

times 510 - ($ - $$) db 0  ; Fill up 510 bytes
dw 0xAA55                  ; Boot sector signature

17
  • 2
    . = 0x1000; seems wrong since you loaded at 0x10000. Binary files don't have an entry point. If you jump to 0x10000 it will start executing whatever function was placed in the binary first. That may not have been _start. I would recommend changing .text ALIGN(4) : { *(.text) } to .text ALIGN(4) : { *(.text.start) *(.text) } and changing __attribute__((section(".text"))) to __attribute__((section(".text.start"))) so that _start if forced to the beginning of the binary file that is produced Commented Feb 22 at 9:30
  • 2
    I haven't tried your code but usually CHS is supported most of the time where LBA (extended disk reads) is not. DL=0x00 is usually the first floppy (not hard drive). First hard drive is usually 0x80. You generally can't use extended disk reads (int 0x13/ah=42h) on floppy media (or USB emulated floppy media). Commented Feb 22 at 9:34
  • 3
    You have misread the intel documentation. You can use any 16-bit general purpose register to load a segment register (DS, ES, SS, FS, and GS but not CS). The 16-bit registers that can be used to load a segment register include AX, BX, CX, DX, SI, DI, BP, and even SP. Commented Feb 22 at 10:19
  • 1
    I really appreciate your suggestion! Using SI for the retry count and BX for setting ES makes a lot of sense. Finally, it worked, and I was able to figure out the issue. You are awesome! @MichaelPetch Commented Feb 22 at 10:39
  • 2
    Just for the record, felixcloutier.com/x86/mov is the documentation for mov. mov Sreg, r/m16 doesn't list any restrictions on the source operand. Commented Feb 22 at 10:46

1 Answer 1

1

Thanks @michael-petch

The issue was likely CX for retries and AX for ES. Using SI for retries kept CX free, and BX for ES avoided overwrites. These tweaks fixed the bootloader!

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

call print_string jnc read_ok loop read_retry Because print_string ALWAYS returns with the carry flag clear (or al,al does that) the loop will NEVER try to reload a sector! It's as though the retry functionality was not there... That's one debugging message ("INT-T") that sits in the way of a correct operation.
"We also switched DL from 0x00 (floppy) to 0x80 (first hard disk), but that didn’t resolve the issue either." In a bootloader you should never hardcode the drive number. ALWAYS use whatever BIOS gave you in the DL register. Both your print_hex_byte and print_string are preserving DX, so just don't put any new value in DL.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.