Most tutorials use gcc cross compilation, which requieres compiling it. The clang compiler can be used as a cross compiler without the extra step (it produces architecture independent code in llvm which can target multiple architectures).
Use the multiboot standard so it can be booted by using standard tools such as grub2. To test if a file is compliant with multiboot:
grub-file --is-x86-multiboot file
or, for multiboot2:
grub-file --is-x86-multiboot2 file
A multiboot compliant image has a header containing three consecutive 32 bit values: a magic number, flags and a checksum. This values have to be 4 bytes aligned in the fist 8k in the file.
clang as the crosscompiler to target another platform. A platform is something like:
<processor, os, binary format, ~other things~>
such as
<x86, none, elf, ...>
The assembler and linker are invoked using the clang command.
The compiler will create an elf binary.
A bit of assembler to define some sections in the kernel image and call the kernel entry point. First some constants:
.set ALIGN, 1<<0 /* alignment */
.set MEMINFO, 1<<1
.set FLAGS, ALIGN | MEMINFO
.set MAGIC, 0x1badb002
.set CHECKSUM, -(MAGIC + FLAGS) /* checksum */
Now the multiboot section:
.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM
Later, use the linker to put the sections together to get it in the first 8k of the image.
The kernel needs a stack if it’s written in c (calling functions, etc.). Use the .bss section. Remember that in x86 the stack grows from higher memory addresses to lower.
.section .bss
.align 16 # apparently, the stack on x86 must be 16-byte aligned in the System V ABI
stack_bottom: # stack "begins"
.skip 16384 # 16 KiB of stack
stack_top: # stack "ends"
Define the .text section containing the executable code. Call the kernel entry point.
.section .text
.global _start
.type _start, @function
_start:
mov $stack_top, %esp
call kernel_main
cli
1: hlt
jmp 1b
The kernel needs to implement a kernel_main function, which will be called in the previous code.
Assuming the system uses bios, the following kernel just prints kernel running in the video memory.
#define VIDEO_MEM 0xb8000
void print_msg(void) {
char *video = (char*) VIDEO_MEM;
const char *msg = "kernel running";
for(int i = 0; msg[i]; i++) {
video[2*i] = msg[i];
video[2*i + 1] = 0x07;
}
}
void clear_screen() {
int i;
char *video = (char*) VIDEO_MEM;
for(i = 0; i < 25*80; i++) {
video[2*i] = 0;
}
}
void kernel_main(void) {
clear_screen();
print_msg();
}
A linker script defines the resulting binary. For example, the sections to include, their memory address, the entry symbol, etc.
ENTRY(_start)
SECTIONS
{
. = 0x100000; // load at 1M
.text : { // code
*(multiboot) // section for multiboot (within the 8k)
*(.text)
}
.data ALIGN(4096) : {
*(.data)
}
.rodata ALIGN(4096) : {
*(.rodata) // read-only data
}
.bss ALIGN(4096) : {
*(.bss) // variables, stack
}
}
To build the project:
#!/bin/sh
# kernel.c
clang --target=i386-pc-none-elf -ffreestanding -o kernel.o -c kernel.c
# bootloader
clang --target=i386-pc-none-elf -o boot.o -c boot.S
# link
clang --target=i386-pc-none-elf -ffreestanding -nostdlib -Wl,--no-dynamic-linker -Wl,-Tlinker.ld boot.o kernel.o -o kernel.bin
The easiest way to test a kernel is to use a virtual machine. In particular, qemu can boot a kernel directly using multiboot, without the need to create a bootable media (an .iso for example):
qemu-system-i386 -kernel kernel.bin
osdev for everything, in particular:
nasm assembler, as an alternative to gasNot in osdev:
clang x86-Toy-OS