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

1

5

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.

Ozan answered 14/9, 2019 at 1:29 Comment(11)
It is because using LD without a specialized linker script for this kind of thing placed the code for the kernel after the boot loader signature 0xaa55. The problem of course is that the BIOS only reads the first sector into memory and nothing after that. So your C code isn't in memory.Shawnee
I recommend to transfer to protected mode first and then call your functions. I never got 16-bit mode C working in my project.Woodcock
Are you intending to write a 16-bit OS or a 32-bit OS? The bootloader has to be 16-bit but would help to know what you are ultimately working towards.Shawnee
@JL2210 thank you for the recommendation. I will eventually do that, but just wanted to see if I could get the bootloader to even call my code.Ozan
@MichaelPetch - the plan is for 32-bit (maybe eventually x86_64). Thank you for the advice on the linker script. I will certainly look into that and post back what I come up with. However, could you elaborate more on why my current linking process doesn't work / what's missing?Ozan
It is easier to not link the bootloader and the C code together. Your bootloader would read extra sectors from disk into memory after the bootloader at 0x7e00 that contains the kernel and then jumps to the start with a JMP 0x0000:0x7e00Shawnee
As I said your linker is placing the C code after the firsts ector of the disk image. But the BIOS hasn't loaded anythig but the first sector into memory so you jump into memory that hasn't had code/data loaded into it.Shawnee
do this. ndisasm -b16 -0x7c00 bin/jasos.bin . Everything after the boot signature 0xaa55 (@ 0x7dfd) starting at 0x7e00 (your compiled C code) in that output hasn't been read into memory. When you did call kmain in the bootloader it tried jumping to 0x7e00. You should see that instruction in the output 00007C4E E8AF01 call 0x7e00Shawnee
As @JL2210 suggested, I think you should switch to protected mode be before doing all these. I also don't think linking your kernel to a bootloader is a good idea. You should load your kernel to a known address in memory from the disk and jump to it.Otti
@preciousbetine It is quite possible to link a bootloader and the kernel together. It is much more involved and requires more linker script magic. You can even write much of the boot sector in GCC although the code is quite bloated.Shawnee
@MichaelPetch I have not had a chance to give it a try, but I will comment back when I do. Thank you for being so helpful, and I will let you know as soon as I can!Ozan
S
8

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:

  • 1One of the top Google hits for "Writing a bootloader in C" is a Code Project tutorial. It is highly rated, and got top monthly article at one point. Unfortunately like many tutorials that involve inline assembly, they teach a lot of bad habits and get things wrong. They were lucky to have their code work with the compiler they did use. Many people attempt to use those bad ideas to write real-mode kernels with GCC and fail miserably. I single the Code Project tutorial out because it has been the basis for many questions on Stackoverflow in the past. Like many other tutorials it really can't be trusted at all. One exception is the article Real mode in C with gcc : writing a bootloader.

    I have provided the second code sample as a Minimal Complete Verifiable Example to show what proper GCC inline assembly looks like to print a character and to print a string. There are very few articles out there that show how to do this properly using GCC. The second example shows the difference between writing assembly code inside a C function and writing a C function with low level inline assembly for required things like BIOS calls etc. If you are going to use GCC to wrap entire assembly code functions then it is much easier and less problematic to write the functions in assembly to begin with. That defeats the purpose of using C.

Shawnee answered 14/9, 2019 at 5:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.