Jump to navigation Jump to search
This page is a work in progress.
This page may thus be incomplete. Its content may be changed in the near future.

This article describes how to make a sample ELF kernel with FASM and Tiny C Compiler (aka TCC). It is also possible to use NASM (Bare_Bones_with_NASM). TCC is a small and fast C compiler, which produces x86, x86_64 or ARM code, and generates PE or ELF executables. TCC is heading toward full ISOC99 compliance, and can compile itself, like FASM.

TCC includes also a linker and an assembler (only x86). But this assembler is limited: no 16/64-bit support, instructions up to MMX are supported.

Note: The Windows version of TCC doesn't produce ELF executables, but only object files. You need to recompile TCC without PE support, if you want to use this tutorial on Windows. You can skip this step if you aren't using Windows.

Building TCC with ELF support


1. Download TCC sources and 32-bit TCC and (if you have a 64-bit OS) 64-bit TCC.

2. Extract the source folder tcc-0.9.26.

3. Save the 32-bit tcc files to a folder called "win32" in the location containing tcc-0.9.26. If you have a 64-bit OS, also create a win64 folder, and save the 64-bit files to a folder called "win64" in the same location.

4. Open Notepad or another text editor, and enter this:

@echo off

echo > config.h #define TCC_VERSION "%VERSION%"

set targetP=I386
set B=32
goto begin

set targetP=X86_64
set B=64
goto begin

set targetF=PE
set CC=..\win%B%\tcc.exe -O0 -s -fno-strict-aliasing
set P=%B%

if %targetF%==ELF set P=%B%-elf
set target=-DTCC_TARGET_%targetF% -DTCC_TARGET_%targetP%

%CC% %target% win%P%\tools\tiny_impdef.c -o win%P%\tiny_impdef.exe
%CC% %target% win%P%\tools\tiny_libmaker.c -o win%P%\tiny_libmaker.exe

if not exist win%P%\libtcc\nul mkdir win%P%\libtcc
copy libtcc.h win%P%\libtcc\libtcc.h
%CC% %target% -shared -DLIBTCC_AS_DLL -DONE_SOURCE libtcc.c -o win%P%\libtcc.dll
win%P%\tiny_impdef win%P%\libtcc.dll -o win%P%\libtcc\libtcc.def

%CC% %target% tcc.c -o win%P%\tcc.exe -ltcc -Lwin%P%\libtcc

copy include\*.h win%P%\include

win%B%\tcc %target% -c lib\libtcc1.c -o win%P%\libtcc1.o
win%B%\tcc %target% -c win%P%\lib\crt1.c -o win%P%\crt1.o
win%B%\tcc %target% -c win%P%\lib\wincrt1.c -o win%P%\wincrt1.o
win%B%\tcc %target% -c win%P%\lib\dllcrt1.c -o win%P%\dllcrt1.o
win%B%\tcc %target% -c win%P%\lib\dllmain.c -o win%P%\dllmain.o
win%B%\tcc %target% -c win%P%\lib\chkstk.S -o win%P%\chkstk.o
goto lib%B%

win%B%\tcc %target% -c lib\alloca86.S -o win%P%\alloca86.o
win%B%\tcc %target% -c lib\alloca86-bt.S -o win%P%\alloca86-bt.o
win%B%\tcc %target% -c lib\bcheck.c -o win%P%\bcheck.o
win%P%\tiny_libmaker win%P%\lib\libtcc1.a win%P%\libtcc1.o win%P%\alloca86.o win%P%\alloca86-bt.o win%P%\crt1.o win%P%\wincrt1.o win%P%\dllcrt1.o win%P%\dllmain.o win%P%\chkstk.o win%P%\bcheck.o
@goto the_end

win%P%\tcc %target% -c lib\alloca86_64.S -o win%P%\alloca86_64.o
win%P%\tiny_libmaker win%P%\lib\libtcc1.a win%P%\libtcc1.o win%P%\alloca86_64.o win%P%\crt1.o win%P%\wincrt1.o win%P%\dllcrt1.o win%P%\dllmain.o win%P%\chkstk.o

del win%P%\*.o

@if %targetF%==PE (
	@set targetF=ELF
	@goto start

@if %B%==64 goto finished
@if _%PROCESSOR_ARCHITEW6432%_==_AMD64_ goto x86_64
@if _%PROCESSOR_ARCHITECTURE%_==_AMD64_ goto x86_64


5. Save to tcc-0.9.26 with any name you want, but the extension MUST be .bat. If you use notepad, you will have to change the type from "Text Documents" to "All Files".

6. Run the script, make sure everything compiled correctly (note that there may be warnings about assignments from incompatible pointer types and bound checking not supporting malloc in a certain environment, but these are okay), and inside win32-elf you should have a working ELF compiler. You should also have win32 and win64 for 32-bit and 64-bit PE compilers, and a 64-bit ELF compiler in win64-elf. Note: The 64-bit compilers won't be compiled on a non-64 bit OS. If there are errors compiling TCC, just change the "@echo off" to "@echo on" and run the script again to see where things went wrong.


Your distribution may already provide a package for tcc. If not, download the sources from and go on from there. You know how to build a program from scratch, right?

A small kernel example

This little example builds a small kernel in ELF format, which can be booted by Grub.


;  Tutorial: A small kernel with Fasm & TCC
;  By Tommy.
        format elf

; Equates
MULTIBOOT_PAGE_ALIGN	    equ (1 shl 0)
MULTIBOOT_MEMORY_INFO	    equ (1 shl 1)
MULTIBOOT_AOUT_KLUDGE	    equ (1 shl 16)

        section '.text' executable
; Multiboot header

; Kernel entry point.
        public  _start
        extrn   kmain
        ; Call the main kernel function.
        call    kmain
        jmp     @b


/*  Tutorial: A small kernel with Fasm & TCC
 *  By Tommy.

 * Main kernel function.
kmain (void)
    *((unsigned char *) 0xB8000) = 'H';
    *((unsigned char *) 0xB8001) = 0x1F;
    *((unsigned char *) 0xB8002) = 'E';
    *((unsigned char *) 0xB8003) = 0x1F;
    *((unsigned char *) 0xB8004) = 'L';
    *((unsigned char *) 0xB8005) = 0x1F;
    *((unsigned char *) 0xB8006) = 'L';
    *((unsigned char *) 0xB8007) = 0x1F;
    *((unsigned char *) 0xB8008) = 'O';
    *((unsigned char *) 0xB8009) = 0x1F;

Compiling and linking

Assemble start32.asm with:

fasm start32.asm

Compile kernel.c with:

tcc -c kernel.c

Then link the whole thing with:

tcc -nostdlib -Wl,-Ttext,0x100000 start32.o kernel.o -o kernel-i386.elf

If you would prefer it in binary form, for example, if you're using your own bootloader that doesn't support ELF, link it with this:

tcc -nostdlib -Wl,-Ttext,0x100000 -Wl,--oformat,binary -static start32.o kernel.o -o kernel-i386.bin

That's all!

Inline Assembly

TCC supports inline GAS syntax assembly like GCC:

__asm__ __volatile__("hlt");

You can use this to your benefit for many things, such as debugging in Bochs:

#define breakpoint() __asm__ __volatile__("xchg %bx, %bx");

void bochs_print (char *string) {
	char *c = string;
	while (*c != '\0') {
		outb(0xE9, *c); // may be outportb

Then adding this to your bochsrc.bxrc file in a text editor:

port_e9_hack: enabled=1
magic_break: enabled=1

And from boch's install location, executing bochsdbg.exe instead of bochs.exe.

GDT and struct warning

According to Fabrice Bellard, the creator of TCC, and (painfully) tested to be true: "In TCC 'packed' is supported only for structure fields or variable declarations, not for a whole structure. So a solution for you is to add it to each field of the packed structure." So if you use structs to store your GDT entries or GDTR, beware, you will encounter issues loading your GDT if you don't specify the packing of structures correctly. When creating structures, use something like this:

// We use the attribute 'packed' to tell TCC not to change any of the alignment in the structure.
struct some_struct {
   unsigned char a __attribute__((packed));
   unsigned char b __attribute__((packed));
} __attribute__((packed));
// This last attribute can be kept, it won't interfere with the compilation or output, so it may be
// useful to retain compatilbity with GCC, as long as the above attributes don't interfere with GCC.

Instead of this:

// We use the attribute 'packed' to tell GCC not to change any of the alignment in the structure.
struct some_struct {
   unsigned char a;
   unsigned char b;
} __attribute__((packed));

Inline Function Warning

TCC doesn't support function inlining, because the 'inline' keyword is ignored, so if a function needs to be inlined, you must use defines instead.


TCC doesn't include stdint.h, but all typedefs required are provided in stddef.h. To use stdint.h place the following code in your kernels include path as stdint.h. This will make your code compatible with both gcc and tcc.

/* stdint.h */

#ifdef __TINYC__
/* tcc */
#include <stddef.h>
/* assume gcc */
#include_next <stdint.h>