gccassemblyx86bootloaderosdev

Cannot call real mode C function from bootloader (NASM + GCC toolchain)


I am attempting to write my own OS kernel, and have been having some issues getting the linking to work properly between my bootloader and (what will soon be) my kernel (written in C).

I have the following code...

src/bootloader.asm

; Allows our code to be run in real mode.
BITS 16
extern kmain

section .text
global _start
_start:
        jmp Start

; Moves the cursor to row dl, col dh.
MoveCursor:
    mov ah, 2
    mov bh, 0
    int 10h
    ret

; Prints the character in al to the screen.
PrintChar:
    mov ah, 10
    mov bh, 0
    mov cx, 1
    int 10h
    ret

; Set cursor position to 0, 0.
ResetCursor:
    mov dh, 0
    mov dl, 0
    call MoveCursor
    ret

Start:
        call ResetCursor

; Clears the screen before we print the boot message.
; QEMU has a bunch of crap on the screen when booting.
Clear:
        mov al, ' '
        call PrintChar

        inc dl
        call MoveCursor

        cmp dl, 80
        jne Clear

        mov dl, 0
        inc dh
        call MoveCursor

        cmp dh, 25
        jne Clear

; Begin printing the boot message. 
Msg:    call ResetCursor
        mov si, BootMessage

NextChar:
        lodsb
        call PrintChar

        inc dl
        call MoveCursor

        cmp si, End
        jne NextChar 

call kmain

BootMessage: db "Booting..."
End:

; Zerofill up to 510 bytes
times 510 - ($ - $$)  db 0

; Boot Sector signature
dw 0AA55h

src/god.c

asm(".code16gcc");

// JASOS kernel entry point.
void kmain()
{
    asm(     "movb $0, %dl;"
             "inc %dh;"
             "movb $2, %ah;"
             "movb $0, %bh;"
             "int $0x10;"
             "movb $'a', %al;"
             "movb $10, %ah;"
             "movw $1, %cx;"
             "int $0x10;"   );

    while (1);
}

and, finally... the Makefile

bootloader: src/bootloader.asm
    nasm -f elf32 src/bootloader.asm -o build/bootloader.o

god: src/god.c
    i686-elf-gcc -c src/god.c -o build/god.o -ffreestanding

os: bootloader god
    i686-elf-ld -Ttext=0x7c00 --oformat binary build/bootloader.o build/god.o -o bin/jasos.bin

The bootloader is pretty simple at the moment. It just types out "Booting..." and (attempts to) load kmain. However, nothing happens after the string is printed.

I am still in real-mode when kmain gets called so I don't expect the failure is because of lack of access to BIOS interrupts from my inline assembly. Correct me if I'm wrong.


Solution

  • I don't recommend GCC for 16-bit code. A GCC alternative may be the separate IA16-GCC project which is a work in progress and is experimental.

    It is hard to get GCC to emit proper real-mode code because of the need for inline assembly. GCC's inline assembly is difficult to get right if you wish to avoid subtle bugs especially when optimizations are enabled. It is possible to write such code but I strongly advise against it.

    You don't have a linker script so your compiled C code was placed after the bootloader signature. The BIOS only reads one sector into memory. Your jmp kmain ends up jumping to memory where the kernel would have been had it actually been loaded into memory, but it wasn't loaded so it fails to work as expected. You need to add code to call BIOS Int 13/AH=2 to read additional disk sectors starting from Cylinder, Head, Sector (CHS) = (0,0,2) which is the sector right after the bootloader.

    Your bootloader doesn't properly set up the segment registers. Because you are using GCC, it expects CS=DS=ES=SS. Since we need to load data into memory we need to put the stack somewhere safe. The kernel will be loaded to 0x0000:0x7e00, so we can place the stack below the bootloader at 0x0000:0x7c00 where they won't conflict. You need to clear the direction flag (DF) with CLD before calling GCC as it is a requirement. Many of these issues are captured in my General Bootloader Tips. A more complex bootloader that determines the size of the kernel (stage2) and reads the appropriate number of sectors from disk can be found in my other Stackoverflow answer.

    We need a linker script to properly lay things out in memory and ensure the instruction(s) at the very beginning jumps to the real C entry point kmain. We also need to properly zero out the BSS section because GCC expects that. The linker script is used to determine the beginning and the end of the BSS section. The function zero_bss clears that memory to 0x00.

    The Makefile could be cleaned up a bit to make adding code easier in the future. I've amended the code so the object files get built in the src directory. This simplifies the make processing.

    When the real-mode code support was introduced and support added to GNU assembler it was enabled in GCC by using asm (".code16gcc");. For quite some time now GCC has supported the -m16 option that does the same thing. With -m16 you don't need to add the .code16gcc directive to the top of all the files.

    I haven't modified your inline assembly that prints a to the screen. Just because I didn't modify it, doesn't mean that it doesn't have problems. Since registers are clobbered and the compiler isn't told of that it can lead to strange bugs especially when optimizations are on. The second part of this answer shows a mechanism to use the BIOS to print characters and strings to the console with proper inline assembly.

    I recommend the compiler options -Os -mregparm=3 -fomit-frame-pointer to optimize for space.

    Makefile:

    CROSSPRE=i686-elf-
    CC=$(CROSSPRE)gcc
    LD=$(CROSSPRE)ld
    OBJCOPY=$(CROSSPRE)objcopy
    DD=dd
    NASM=nasm
    
    DIR_SRC=src
    DIR_BIN=bin
    DIR_BUILD=build
    
    KERNEL_NAME=jasos
    KERNEL_BIN=$(DIR_BIN)/$(KERNEL_NAME).bin
    KERNEL_ELF=$(DIR_BIN)/$(KERNEL_NAME).elf
    BOOTLOADER_BIN=$(DIR_BIN)/bootloader.bin
    BOOTLOADER_ASM=$(DIR_SRC)/bootloader.asm
    DISK_IMG=$(DIR_BUILD)/disk.img
    
    CFLAGS=-g -fno-PIE -static -std=gnu99 -m16 -Os -mregparm=3 \
        -fomit-frame-pointer -nostdlib -ffreestanding -Wall -Wextra
    LDFLAGS=-melf_i386
    
    # List all object files here
    OBJS=$(DIR_SRC)/god.o
    
    .PHONY: all clean
    
    all: $(DISK_IMG)
    
    $(BOOTLOADER_BIN): $(BOOTLOADER_ASM)
            $(NASM) -f bin $< -o $@
    
    %.o: %.c
            $(CC) -c $(CFLAGS) $< -o $@
    
    $(KERNEL_ELF): $(OBJS)
            $(LD) $(LDFLAGS) -Tlink.ld $^ -o $@
    
    $(KERNEL_BIN): $(KERNEL_ELF)
            $(OBJCOPY) -O binary $< $@
    
    $(DISK_IMG): $(KERNEL_BIN) $(BOOTLOADER_BIN)
            $(DD) if=/dev/zero of=$@ bs=1024 count=1440
            $(DD) if=$(BOOTLOADER_BIN) of=$@ conv=notrunc
            $(DD) if=$(KERNEL_BIN) of=$@ conv=notrunc seek=1
    
    clean:
            rm -f $(DIR_BIN)/*
            rm -f $(DIR_BUILD)/*
            rm -f $(DIR_SRC)/*.o
    

    link.ld:

    OUTPUT_FORMAT("elf32-i386");
    ENTRY(kmain);
    SECTIONS
    {
        . = 0x7E00;
    
        .text.main : SUBALIGN(0) {
            *(.text.bootstrap);
            *(.text.*);
        }
    
        .data.main : SUBALIGN(4) {
            *(.data);
            *(.rodata*);
        }
    
        .bss : SUBALIGN(4) {
            __bss_start = .;
            *(.COMMON);
            *(.bss)
        }
        . = ALIGN(4);
        __bss_end = .;
    
        __bss_sizel = ((__bss_end)-(__bss_start))>>2;
        __bss_sizeb = ((__bss_end)-(__bss_start));
    
        /DISCARD/ : {
            *(.eh_frame);
            *(.comment);
        }
    }
    

    src/god.c:

    #include <stdint.h>
    
    /* The linker script ensures .text.bootstrap code appears first.
     * The code simply jumps to our real entrypoint kmain */
    
    asm (".pushsection .text.bootstrap\n\t"
         "jmp kmain\n\t"
         ".popsection");
    
    extern uintptr_t __bss_start[];
    extern uintptr_t __bss_end[];
    
    /* Zero the BSS section */
    static inline void zero_bss()
    {
        uint32_t *memloc = __bss_start;
    
        while (memloc < __bss_end)
            *memloc++ = 0;
    }
    
    /* JASOS kernel C entrypoint */
    void kmain()
    {
        /* We need to zero out the BSS section */
        zero_bss();
    
        asm (
            "movb $0, %dl;"
            "inc %dh;"
            "movb $2, %ah;"
            "movb $0, %bh;"
            "int $0x10;"
            "movb $'a', %al;"
            "movb $10, %ah;"
            "movw $1, %cx;"
            "int $0x10;"
        );
    
        return;
    }
    

    src/bootloader.asm:

    ; Allows our code to be run in real mode.
    BITS 16
    ORG 0x7c00
    
    _start:
        xor ax, ax                 ; DS=ES=0
        mov ds, ax
        mov es, ax
        mov ss, ax                 ; SS:SP=0x0000:0x7c00
        mov sp, 0x7c00
        cld                        ; Direction flag = 0 (forward movement)
                                   ; Needed by code generated by GCC
    
        ; Read 17 sectors starting from CHS=(0,0,2) to 0x0000:0x7e00
        ; 17 * 512 = 8704 bytes (good enough to start with)
        mov bx, 0x7e00             ; ES:BX (0x0000:0x7e00) is memory right after bootloader
        mov ax, 2<<8 | 17          ; AH=2 Disk Read, AL=17 sectors to read
        mov cx, 0<<8 | 2           ; CH=Cylinder=0, CL=Sector=2
        mov dh, 0                  ; DH=Head=0
        int 0x13                   ; Do BIOS disk read
    
        jmp 0x0000:Start           ; Jump to start set CS=0
    
    ; Moves the cursor to row dl, col dh.
    MoveCursor:
        mov ah, 2
        mov bh, 0
        int 10h
        ret
    
    ; Prints the character in al to the screen.
    PrintChar:
        mov ah, 10
        mov bh, 0
        mov cx, 1
        int 10h
        ret
    
    ; Set cursor position to 0, 0.
    ResetCursor:
        mov dh, 0
        mov dl, 0
        call MoveCursor
        ret
    
    Start:
    
        call ResetCursor
    
    ; Clears the screen before we print the boot message.
    ; QEMU has a bunch of crap on the screen when booting.
    Clear:
        mov al, ' '
        call PrintChar
    
        inc dl
        call MoveCursor
    
        cmp dl, 80
        jne Clear
    
        mov dl, 0
        inc dh
        call MoveCursor
    
        cmp dh, 25
        jne Clear
    
    ; Begin printing the boot message.
    Msg:
        call ResetCursor
        mov si, BootMessage
    
    NextChar:
        lodsb
        call PrintChar
    
        inc dl
        call MoveCursor
    
        cmp si, End
        jne NextChar
    
        call dword 0x7e00          ; Because GCC generates code with stack
                                   ; related calls that are 32-bits wide we
                                   ; need to specify `DWORD`. If we don't, when
                                   ; kmain does a `RET` it won't properly return
                                   ; to the code below.
    
        ; Infinite ending loop when kmain returns
        cli
    .endloop:
        hlt
        jmp .endloop
    
    BootMessage: db "Booting..."
    End:
    
    ; Zerofill up to 510 bytes
    times 510 - ($ - $$)  db 0
    
    ; Boot Sector signature
    dw 0AA55h
    

    A 1.44MiB floppy disk image called build/disk.img is created. It can be run in QEMU with a command like:

    qemu-system-i386 -fda build/disk.img
    

    The expected output should look similar to:

    enter image description here


    Proper use of Inline Assembly to Write a String Using the BIOS

    A version of the code that uses more complex GCC extended inline assembly is presented below. This answer is not meant to be a discussion on GCC's extended inline assembly usage, but there is information online about it. It should be noted that there is a lot of bad advice, documentation, tutorials, and sample code fraught with problems written by people who may not have had a proper understanding of the subject. You have been warned!1

    Makefile:

    CROSSPRE=i686-elf-
    CC=$(CROSSPRE)gcc
    LD=$(CROSSPRE)ld
    OBJCOPY=$(CROSSPRE)objcopy
    DD=dd
    NASM=nasm
    
    DIR_SRC=src
    DIR_BIN=bin
    DIR_BUILD=build
    
    KERNEL_NAME=jasos
    KERNEL_BIN=$(DIR_BIN)/$(KERNEL_NAME).bin
    KERNEL_ELF=$(DIR_BIN)/$(KERNEL_NAME).elf
    BOOTLOADER_BIN=$(DIR_BIN)/bootloader.bin
    BOOTLOADER_ASM=$(DIR_SRC)/bootloader.asm
    DISK_IMG=$(DIR_BUILD)/disk.img
    
    CFLAGS=-g -fno-PIE -static -std=gnu99 -m16 -Os -mregparm=3 \
        -fomit-frame-pointer -nostdlib -ffreestanding -Wall -Wextra
    LDFLAGS=-melf_i386
    
    # List all object files here
    OBJS=$(DIR_SRC)/god.o $(DIR_SRC)/biostty.o
    
    .PHONY: all clean
    
    all: $(DISK_IMG)
    
    $(BOOTLOADER_BIN): $(BOOTLOADER_ASM)
            $(NASM) -f bin $< -o $@
    
    %.o: %.c
            $(CC) -c $(CFLAGS) $< -o $@
    
    $(KERNEL_ELF): $(OBJS)
            $(LD) $(LDFLAGS) -Tlink.ld $^ -o $@
    
    $(KERNEL_BIN): $(KERNEL_ELF)
            $(OBJCOPY) -O binary $< $@
    
    $(DISK_IMG): $(KERNEL_BIN) $(BOOTLOADER_BIN)
            $(DD) if=/dev/zero of=$@ bs=1024 count=1440
            $(DD) if=$(BOOTLOADER_BIN) of=$@ conv=notrunc
            $(DD) if=$(KERNEL_BIN) of=$@ conv=notrunc seek=1
    
    clean:
            rm -f $(DIR_BIN)/*
            rm -f $(DIR_BUILD)/*
            rm -f $(DIR_SRC)/*.o
    

    link.ld:

    OUTPUT_FORMAT("elf32-i386");
    ENTRY(kmain);
    SECTIONS
    {
        . = 0x7E00;
    
        .text.main : SUBALIGN(0) {
            *(.text.bootstrap);
            *(.text.*);
        }
    
        .data.main : SUBALIGN(4) {
            *(.data);
            *(.rodata*);
        }
    
        .bss : SUBALIGN(4) {
            __bss_start = .;
            *(.COMMON);
            *(.bss)
        }
        . = ALIGN(4);
        __bss_end = .;
    
        __bss_sizel = ((__bss_end)-(__bss_start))>>2;
        __bss_sizeb = ((__bss_end)-(__bss_start));
    
        /DISCARD/ : {
            *(.eh_frame);
            *(.comment);
        }
    }
    

    src/biostty.c:

    #include <stdint.h>
    #include "../include/biostty.h"
    
    void fastcall
    writetty_str (const char *str)
    {
        writetty_str_i (str);
    }
    
    void fastcall
    writetty_char (const uint8_t outchar)
    {
        writetty_char_i (outchar);
    }
    

    include/x86helper.h:

    #ifndef X86HELPER_H
    #define X86HELPER_H
    
    #include <stdint.h>
    
    #define STR_TEMP(x) #x
    #define STR(x) STR_TEMP(x)
    
    #define TRUE 1
    #define FALSE 0
    #define NULL (void *)0
    
    /* regparam(3) is a calling convention that passes first
       three parameters via registers instead of on stack.
       1st param = EAX, 2nd param = EDX, 3rd param = ECX */
    #define fastcall  __attribute__((regparm(3)))
    
    /* noreturn lets GCC know that a function that it may detect
       won't exit is intentional */
    #define noreturn      __attribute__((noreturn))
    #define always_inline __attribute__((always_inline))
    #define used          __attribute__((used))
    
    /* Define helper x86 function */
    static inline void fastcall always_inline x86_hlt(void){
        __asm__ ("hlt\n\t");
    }
    static inline void fastcall always_inline x86_cli(void){
        __asm__ ("cli\n\t");
    }
    static inline void fastcall always_inline x86_sti(void){
        __asm__ ("sti\n\t");
    }
    static inline void fastcall always_inline x86_cld(void){
        __asm__ ("cld\n\t");
    }
    
    /* Infinite loop with hlt to end bootloader code */
    static inline void noreturn fastcall haltcpu()
    {
        while(1){
            x86_hlt();
        }
    }
    
    #endif
    

    include/biostty.h:

    #ifndef BIOSTTY_H
    #define BIOSTTY_H
    
    #include <stdint.h>
    #include "../include/x86helper.h"
    
    /* Functions ending with _i are always inlined */
    
    extern fastcall void
    writetty_str (const char *str);
    
    extern fastcall void
    writetty_char (const uint8_t outchar);
    
    static inline fastcall always_inline void
    writetty_char_i (const uint8_t outchar)
    {
       __asm__ ("int $0x10\n\t"
                :
                : "a"(((uint16_t)0x0e << 8) | outchar),
                  "b"(0x0000));
    }
    
    static inline fastcall always_inline void
    writetty_str_i (const char *str)
    {
        /* write characters until we reach nul terminator in str */
        while (*str)
            writetty_char_i (*str++);
    }
    
    #endif
    

    src/god.c:

    #include <stdint.h>
    #include "../include/biostty.h"
    
    /* The linker script ensures .text.bootstrap code appears first.
     * The code simply jumps to our real entrypoint kmain */
    
    asm (".pushsection .text.bootstrap\n\t"
         "jmp kmain\n\t"
         ".popsection");
    
    extern uintptr_t __bss_start[];
    extern uintptr_t __bss_end[];
    
    /* Zero the BSS section */
    static inline void zero_bss()
    {
        uint32_t *memloc = __bss_start;
    
        while (memloc < __bss_end)
            *memloc++ = 0;
    }
    
    /* JASOS kernel C entrypoint */
    void kmain()
    {
        /* We need to zero out the BSS section */
        zero_bss();
    
        writetty_str("\n\rHello, world!\n\r");
        return;
    }
    

    The linker script and bootloader are unmodified from the first version presented in this answer.

    When run in QEMU the output should look similar to:

    enter image description here


    Footnotes: