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:
Bootloader: Custom (written in NASM)
Mode Switching: Bootloader starts in 16-bit real mode, then switches to 32-bit protected mode before calling the kernel
Kernel: kernel.c compiled with i686-elf-gcc Linker: i686-elf-ld
Toolchain: Cross-compiler on macOS (M2 Mac)
Disk Image: MBR boot sector
Testing with: qemu-system-i386 -hda os-image.bin
Bootloader (boot.asm) -
Loads kernel.bin at 0x10000
Switches to protected mode
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
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


. = 0x1000;seems wrong since you loaded at0x10000. 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_startif forced to the beginning of the binary file that is producedSIfor the retry count andBXfor settingESmakes a lot of sense. Finally, it worked, and I was able to figure out the issue. You are awesome! @MichaelPetchmov.mov Sreg, r/m16doesn't list any restrictions on the source operand.