TCC
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
Windows
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
set \p VERSION = < .VERSION
echo > config.h #define TCC_VERSION "%VERSION%"
set targetP=I386
set B=32
goto begin
:x86_64
set targetP=X86_64
set B=64
goto begin
:begin
set targetF=PE
set CC=..\win%B%\tcc.exe -O0 -s -fno-strict-aliasing
set P=%B%
:start
if %targetF%==ELF set P=%B%-elf
set target=-DTCC_TARGET_%targetF% -DTCC_TARGET_%targetP%
:tools
%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
:libtcc
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
:tcc
%CC% %target% tcc.c -o win%P%\tcc.exe -ltcc -Lwin%P%\libtcc
:copy_std_includes
copy include\*.h win%P%\include
:libtcc1.a
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%
:lib32
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
:lib64
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
:the_end
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
:finished
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.
Linux
Your distribution may already provide a package for tcc. If not, download the sources from https://download.savannah.gnu.org/releases/tinycc/ 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.
start32.asm
; Tutorial: A small kernel with Fasm & TCC
; By Tommy.
format elf
use32
;
; Equates
;
MULTIBOOT_PAGE_ALIGN equ (1 shl 0)
MULTIBOOT_MEMORY_INFO equ (1 shl 1)
MULTIBOOT_AOUT_KLUDGE equ (1 shl 16)
MULTIBOOT_HEADER_MAGIC equ 0x1BADB002
MULTIBOOT_HEADER_FLAGS equ MULTIBOOT_PAGE_ALIGN or MULTIBOOT_MEMORY_INFO
MULTIBOOT_CHECKSUM equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
section '.text' executable
;
; Multiboot header
;
dd MULTIBOOT_HEADER_MAGIC
dd MULTIBOOT_HEADER_FLAGS
dd MULTIBOOT_CHECKSUM
;
; Kernel entry point.
;
public _start
extrn kmain
_start:
; Call the main kernel function.
call kmain
@@:
jmp @b
kernel.c
/* Tutorial: A small kernel with Fasm & TCC
* By Tommy.
*/
/*
* Main kernel function.
*/
void
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
c++;
}
}
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.
stdint.h
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>
#else
/* assume gcc */
#include_next <stdint.h>
#endif