Limine Bare Bones

From OSDev Wiki
Jump to: navigation, search

WAIT! Have you read Getting Started, Beginner Mistakes, and some of the related OS theory?

Difficulty level
Difficulty 1.png
Kernel Designs
Other Concepts

The Limine Boot Protocol is the native boot protocol provided by the Limine bootloader. Like the stivale protocols it supersedes, it is designed to overcome shortcomings of common boot protocols used by hobbyist OS developers, such as Multiboot, and stivale itself.

It provides cutting edge features such as 5-level paging support, 64-bit Long Mode support, and direct higher half kernel loading.

The Limine boot protocol is firmware and architecture agnostic. The Limine bootloader supports x86-64, IA-32, aarch64, and riscv64.

This article will demonstrate how to write a small x86-64 higher half Limine-compliant kernel in C, and boot it using the Limine bootloader.

It is also very recommended to check out this template project as it provides example buildable code to go along with this guide.



For this example, we will create these 2 files and place them in the same directory:

  • kernel.c
  • linker.ld

As one may notice, there is no "entry point" assembly stub, as one is not necessary with the Limine protocol when using a language which can make use of a standard SysV x86 calling convention.

Furthermore, we will download the header file limine.h which defines structures and constants that we will use to interact with the bootloader from here, and place it in the same directory as the other files.

Obviously, this is just a bare bones example, and one should always refer to the Limine protocol specification for more details and information.


This is the kernel "main".

#include <stdint.h>
#include <stddef.h>
#include <limine.h>
// The Limine requests can be placed anywhere, but it is important that
// the compiler does not optimise them away, so, usually, they should
// be made volatile or equivalent.
static volatile struct limine_framebuffer_request framebuffer_request = {
    .revision = 0
// GCC and Clang reserve the right to generate calls to the following
// 4 functions even if they are not directly called.
// Implement them as the C specification mandates.
// DO NOT remove or rename these functions, or stuff will eventually break!
// They CAN be moved to a different .c file.
void *memcpy(void *dest, const void *src, size_t n) {
    uint8_t *pdest = (uint8_t *)dest;
    const uint8_t *psrc = (const uint8_t *)src;
    for (size_t i = 0; i < n; i++) {
        pdest[i] = psrc[i];
    return dest;
void *memset(void *s, int c, size_t n) {
    uint8_t *p = (uint8_t *)s;
    for (size_t i = 0; i < n; i++) {
        p[i] = (uint8_t)c;
    return s;
void *memmove(void *dest, const void *src, size_t n) {
    uint8_t *pdest = (uint8_t *)dest;
    const uint8_t *psrc = (const uint8_t *)src;
    if (src > dest) {
        for (size_t i = 0; i < n; i++) {
            pdest[i] = psrc[i];
    } else if (src < dest) {
        for (size_t i = n; i > 0; i--) {
            pdest[i-1] = psrc[i-1];
    return dest;
int memcmp(const void *s1, const void *s2, size_t n) {
    const uint8_t *p1 = (const uint8_t *)s1;
    const uint8_t *p2 = (const uint8_t *)s2;
    for (size_t i = 0; i < n; i++) {
        if (p1[i] != p2[i]) {
            return p1[i] < p2[i] ? -1 : 1;
    return 0;
// Halt and catch fire function.
static void hcf(void) {
    asm ("cli");
    for (;;) {
        asm ("hlt");
// The following will be our kernel's entry point.
// If renaming _start() to something else, make sure to change the
// linker script accordingly.
void _start(void) {
    // Ensure we got a framebuffer.
    if (framebuffer_request.response == NULL
     || framebuffer_request.response->framebuffer_count < 1) {
    // Fetch the first framebuffer.
    struct limine_framebuffer *framebuffer = framebuffer_request.response->framebuffers[0];
    // Note: we assume the framebuffer model is RGB with 32-bit pixels.
    for (size_t i = 0; i < 100; i++) {
        uint32_t *fb_ptr = framebuffer->address;
        fb_ptr[i * (framebuffer->pitch / 4) + i] = 0xffffff;
    // We're done, just hang...


This is going to be our linker script describing where our sections will end up in memory.

/* Tell the linker that we want an x86_64 ELF64 output file */
/* We want the symbol _start to be our entry point */
/* Define the program headers we want so the bootloader gives us the right */
/* MMU permissions */
    text    PT_LOAD    FLAGS((1 << 0) | (1 << 2)) ; /* Execute + Read */
    rodata  PT_LOAD    FLAGS((1 << 2)) ;            /* Read only */
    data    PT_LOAD    FLAGS((1 << 1) | (1 << 2)) ; /* Write + Read */
    dynamic PT_DYNAMIC FLAGS((1 << 1) | (1 << 2)) ; /* Dynamic PHDR for relocations */
    /* We wanna be placed in the topmost 2GiB of the address space, for optimisations */
    /* and because that is what the Limine spec mandates. */
    /* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
    /* that is the beginning of the region. */
    . = 0xffffffff80000000;
    .text : {
        *(.text .text.*)
    } :text
    /* Move to the next memory page for .rodata */
    .rodata : {
        *(.rodata .rodata.*)
    } :rodata
    /* Move to the next memory page for .data */
    .data : {
        *(.data .data.*)
    } :data
    /* Dynamic section for relocations, both in its own PHDR and inside data PHDR */
    .dynamic : {
    } :data :dynamic
    /* NOTE: .bss needs to be the last thing mapped to :data, otherwise lots of */
    /* unnecessary zeros will be written to the binary. */
    /* If you need, for example, .init_array and .fini_array, those should be placed */
    /* above this. */
    .bss : {
        *(.bss .bss.*)
    } :data
    /* Discard .note.* and .eh_frame since they may cause issues on some hosts. */
    /DISCARD/ : {
        *(.note .note.*)

Building the kernel and creating an image


In order to build our kernel, we are going to use a Makefile. Since we're going to use GNU make specific features, we call this file GNUmakefile instead, so only GNU make will process it.

# Nuke built-in rules and variables.
override MAKEFLAGS += -rR
# This is the name that our final kernel executable will have.
# Change as needed.
override KERNEL := myos.elf
# Convenience macro to reliably declare user overridable variables.
define DEFAULT_VAR =
    ifeq ($(origin $1),default)
        override $(1) := $(2)
    ifeq ($(origin $1),undefined)
        override $(1) := $(2)
# It is suggested to use a custom built cross toolchain to build a kernel.
# We are using the standard "cc" here, it may work by using
# the host system's toolchain, but this is not guaranteed.
override DEFAULT_CC := cc
$(eval $(call DEFAULT_VAR,CC,$(DEFAULT_CC)))
# Same thing for "ld" (the linker).
override DEFAULT_LD := ld
$(eval $(call DEFAULT_VAR,LD,$(DEFAULT_LD)))
# User controllable C flags.
override DEFAULT_CFLAGS := -g -O2 -pipe
# User controllable C preprocessor flags. We set none by default.
# User controllable nasm flags.
override DEFAULT_NASMFLAGS := -F dwarf -g
# User controllable linker flags. We set none by default.
# Internal C flags that should not be changed by the user.
override CFLAGS += \
    -Wall \
    -Wextra \
    -std=gnu11 \
    -ffreestanding \
    -fno-stack-protector \
    -fno-stack-check \
    -fno-lto \
    -fPIE \
    -m64 \
    -march=x86-64 \
    -mno-80387 \
    -mno-mmx \
    -mno-sse \
    -mno-sse2 \
# Internal C preprocessor flags that should not be changed by the user.
override CPPFLAGS := \
    -I. \
    $(CPPFLAGS) \
    -MMD \
# Internal linker flags that should not be changed by the user.
override LDFLAGS += \
    -m elf_x86_64 \
    -nostdlib \
    -static \
    -pie \
    --no-dynamic-linker \
    -z text \
    -z max-page-size=0x1000 \
    -T linker.ld
# Internal nasm flags that should not be changed by the user.
override NASMFLAGS += \
    -Wall \
    -f elf64
# Use "find" to glob all *.c, *.S, and *.asm files in the tree and obtain the
# object and header dependency file names.
override CFILES := $(shell find -L . -type f -name '*.c' | grep -v 'limine/')
override ASFILES := $(shell find -L . -type f -name '*.S' | grep -v 'limine/')
override NASMFILES := $(shell find -L . -type f -name '*.asm' | grep -v 'limine/')
override OBJ := $(CFILES:.c=.c.o) $(ASFILES:.S=.S.o) $(NASMFILES:.asm=.asm.o)
override HEADER_DEPS := $(CFILES:.c=.c.d) $(ASFILES:.S=.S.d)
# Default target.
.PHONY: all
all: $(KERNEL)
# Link rules for the final kernel executable.
$(KERNEL): GNUmakefile linker.ld $(OBJ)
	$(LD) $(OBJ) $(LDFLAGS) -o $@
# Include header dependencies.
-include $(HEADER_DEPS)
# Compilation rules for *.c files.
%.c.o: %.c GNUmakefile
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
# Compilation rules for *.S files.
%.S.o: %.S GNUmakefile
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
# Compilation rules for *.asm (nasm) files.
%.asm.o: %.asm GNUmakefile
	nasm $(NASMFLAGS) $< -o $@
# Remove object files and the final executable.
.PHONY: clean
	rm -rf $(KERNEL) $(OBJ) $(HEADER_DEPS)


This file is parsed by Limine and it describes boot entries and other bootloader configuration variables. Further information here.

# Timeout in seconds that Limine will use before automatically booting.
# The entry name that will be displayed in the boot menu.
:myOS (KASLR on)
    # We use the Limine boot protocol.
    # Path to the kernel to boot. boot:/// represents the partition on which limine.cfg is located.
# Same thing, but without KASLR.
:myOS (KASLR off)
    # Disable KASLR (it is enabled by default for relocatable kernels)

Compiling the kernel

We can now build our example kernel by running make. This command, if successful, should generate a file called myos.elf (or the chosen kernel name). This is our Limine protocol-compliant kernel executable.

Creating the image

We can now create either an ISO or a hard disk/USB drive image with our kernel on it. Limine can boot on both BIOS and UEFI if the image is set up to do so, which is what we are going to do.

Creating an ISO

In this example we are going to create a CD-ROM ISO capable of booting on both UEFI and legacy BIOS systems.

For this to work, we will need the xorriso utility.

These are shell commands. They can also be compiled into a script or Makefile.

# Download the latest Limine binary release.
git clone --branch=v5.x-branch-binary --depth=1
# Build limine utility.
make -C limine
# Create a directory which will be our ISO root.
mkdir -p iso_root
# Copy the relevant files over.
cp -v myos.elf limine.cfg limine/limine-bios.sys \
      limine/limine-bios-cd.bin limine/limine-uefi-cd.bin iso_root/
# Create the EFI boot tree and copy Limine's EFI executables over.
mkdir -p iso_root/EFI/BOOT
cp -v limine/BOOTX64.EFI iso_root/EFI/BOOT/
cp -v limine/BOOTIA32.EFI iso_root/EFI/BOOT/
# Create the bootable ISO.
xorriso -as mkisofs -b limine-bios-cd.bin \
        -no-emul-boot -boot-load-size 4 -boot-info-table \
        --efi-boot limine-uefi-cd.bin \
        -efi-boot-part --efi-boot-image --protective-msdos-label \
        iso_root -o image.iso
# Install Limine stage 1 and 2 for legacy BIOS boot.
./limine/limine bios-install image.iso

Creating a hard disk/USB drive image

In this example, we'll create a GPT partition table using sgdisk, containing a single FAT partition, also known as the ESP in EFI terminology, which will store our kernel, configs, and bootloader.

This example is more involved and is made up of more steps than creating an ISO image.

These are shell commands. They can also be compiled into a script or Makefile.

# Create an empty zeroed-out 64MiB image file.
dd if=/dev/zero bs=1M count=0 seek=64 of=image.hdd
# Create a GPT partition table.
sgdisk image.hdd -n 1:2048 -t 1:ef00
# Download the latest Limine binary release.
git clone --branch=v5.x-branch-binary --depth=1
# Build limine utility.
make -C limine
# Install the Limine BIOS stages onto the image.
./limine/limine bios-install image.hdd
# Format the image as fat32.
mformat -i image.hdd@@1M
# Make /EFI and /EFI/BOOT an MSDOS subdirectory.
mmd -i image.hdd@@1M ::/EFI ::/EFI/BOOT
# Copy over the relevant files
mcopy -i image.hdd@@1M myos.elf limine.cfg limine/limine-bios.sys ::/
mcopy -i image.hdd@@1M limine/BOOTX64.EFI ::/EFI/BOOT
mcopy -i image.hdd@@1M limine/BOOTIA32.EFI ::/EFI/BOOT


If everything above has been completed successfully, you should now have a bootable ISO or hard drive/USB image containing your 64-bit higher half Limine protocol-compliant kernel and Limine to boot it. Once the kernel is successfully booted, you should see a line printed on screen from the top left corner.

See Also


External Links

Personal tools