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:
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:
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.
JMP 0x0000:0x7e00
– Shawneendisasm -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 didcall kmain
in the bootloader it tried jumping to 0x7e00. You should see that instruction in the output00007C4E E8AF01 call 0x7e00
– Shawnee