Limine Bare Bones: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
(Use base revision 1)
m (Wording fix.)
(22 intermediate revisions by 4 users not shown)
Line 3: Line 3:
{{Template:Kernel designs}}
{{Template:Kernel designs}}


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.
The Limine Boot Protocol is the native boot protocol provided by the [[Limine]] bootloader. It is designed to overcome shortcomings of common boot protocols used by hobbyist OS developers, such as [[Multiboot]].


It provides cutting edge features such as 5-level paging support, 64-bit [[Long Mode]] support, and direct higher half kernel loading.
It provides cutting edge features such as 5-level paging support, 64-bit [[Long Mode]] support, and direct higher half kernel loading.
Line 9: Line 9:
The Limine boot protocol is firmware and architecture agnostic. The Limine bootloader supports x86-64, IA-32, aarch64, and riscv64.
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.
This article will demonstrate how to write a small Limine-compliant x86-64 kernel in (GNU) [[C]], and boot it using the [[Limine]] bootloader.


It is also very recommended to check out [https://github.com/limine-bootloader/limine-c-template this template project] as it provides example buildable code to go along with this guide.
Additionally, it is highly recommended to check out [https://github.com/limine-bootloader/limine-c-template this template project] as it provides example buildable code to go along with this guide.


===Overview===
==Overview==


For this example, we will create these 2 files to create the basic directory tree of our project:
For this example, we will create these 2 files to create the basic directory tree of our project:
* src/kernel.c
* src/main.c
* linker.ld
* linker.ld


Line 25: Line 25:
Obviously, this is just a bare bones example, and one should always refer to the [https://github.com/limine-bootloader/limine/blob/trunk/PROTOCOL.md Limine protocol specification] for more details and information.
Obviously, this is just a bare bones example, and one should always refer to the [https://github.com/limine-bootloader/limine/blob/trunk/PROTOCOL.md Limine protocol specification] for more details and information.


===src/kernel.c===
===src/main.c===


This is the kernel "main".
This is the kernel "main".


<source lang="c">
<syntaxhighlight lang="c">
#include <stdint.h>
#include <stdint.h>
#include <stddef.h>
#include <stddef.h>
Line 35: Line 35:
#include <limine.h>
#include <limine.h>


// Set the base revision to 1, this is recommended as this is the latest
// Set the base revision to 2, this is recommended as this is the latest
// base revision described by the Limine boot protocol specification.
// base revision described by the Limine boot protocol specification.
// See specification for further info.
// See specification for further info.


__attribute__((used, section(".requests")))
LIMINE_BASE_REVISION(1)
static volatile LIMINE_BASE_REVISION(2);


// The Limine requests can be placed anywhere, but it is important that
// The Limine requests can be placed anywhere, but it is important that
// the compiler does not optimise them away, so, in C, they should
// the compiler does not optimise them away, so, usually, they should
// be made volatile or equivalent, _and_ they should be accessed at least
// NOT be made "static".
// once or marked as used with the "used" attribute as done here.


__attribute__((used, section(".requests")))
struct limine_framebuffer_request framebuffer_request = {
static volatile struct limine_framebuffer_request framebuffer_request = {
.id = LIMINE_FRAMEBUFFER_REQUEST,
.id = LIMINE_FRAMEBUFFER_REQUEST,
.revision = 0
.revision = 0
};
};

// Finally, define the start and end markers for the Limine requests.
// These can also be moved anywhere, to any .c file, as seen fit.

__attribute__((used, section(".requests_start_marker")))
static volatile LIMINE_REQUESTS_START_MARKER;

__attribute__((used, section(".requests_end_marker")))
static volatile LIMINE_REQUESTS_END_MARKER;


// GCC and Clang reserve the right to generate calls to the following
// GCC and Clang reserve the right to generate calls to the following
Line 135: Line 147:
// Note: we assume the framebuffer model is RGB with 32-bit pixels.
// Note: we assume the framebuffer model is RGB with 32-bit pixels.
for (size_t i = 0; i < 100; i++) {
for (size_t i = 0; i < 100; i++) {
uint32_t *fb_ptr = framebuffer->address;
volatile uint32_t *fb_ptr = framebuffer->address;
fb_ptr[i * (framebuffer->pitch / 4) + i] = 0xffffff;
fb_ptr[i * (framebuffer->pitch / 4) + i] = 0xffffff;
}
}
Line 143: Line 155:
}
}


</syntaxhighlight>
</source>


===linker.ld===
===linker.ld===
Line 149: Line 161:
This is going to be our linker script describing where our sections will end up in memory.
This is going to be our linker script describing where our sections will end up in memory.


<source lang="c">
<syntaxhighlight lang="c">
/* Tell the linker that we want an x86_64 ELF64 output file */
/* Tell the linker that we want an x86_64 ELF64 output file */
OUTPUT_FORMAT(elf64-x86-64)
OUTPUT_FORMAT(elf64-x86-64)
Line 161: Line 173:
PHDRS
PHDRS
{
{
text PT_LOAD FLAGS((1 << 0) | (1 << 2)) ; /* Execute + Read */
text PT_LOAD FLAGS(0x05); /* Execute + Read */
rodata PT_LOAD FLAGS((1 << 2)) ; /* Read only */
rodata PT_LOAD FLAGS(0x04); /* Read only */
data PT_LOAD FLAGS((1 << 1) | (1 << 2)) ; /* Write + Read */
data PT_LOAD FLAGS(0x06); /* Write + Read */
dynamic PT_DYNAMIC FLAGS((1 << 1) | (1 << 2)) ; /* Dynamic PHDR for relocations */
dynamic PT_DYNAMIC FLAGS(0x06); /* Dynamic PHDR for relocations */
}
}


Line 180: Line 192:


/* Move to the next memory page for .rodata */
/* Move to the next memory page for .rodata */
. += CONSTANT(MAXPAGESIZE);
. = ALIGN(CONSTANT(MAXPAGESIZE));


.rodata : {
.rodata : {
Line 187: Line 199:


/* Move to the next memory page for .data */
/* Move to the next memory page for .data */
. += CONSTANT(MAXPAGESIZE);
. = ALIGN(CONSTANT(MAXPAGESIZE));


.data : {
.data : {
*(.data .data.*)
*(.data .data.*)

/* Place the sections that contain the Limine requests as part of the .data */
/* output section. */
KEEP(*(.requests_start_marker))
KEEP(*(.requests))
KEEP(*(.requests_end_marker))
} :data
} :data


Line 207: Line 225:
} :data
} :data


/* Discard .note.* and .eh_frame since they may cause issues on some hosts. */
/* Discard .note.* and .eh_frame* since they may cause issues on some hosts. */
/* Also discard the program interpreter section since we do not need one. This is */
/* more or less equivalent to the --no-dynamic-linker linker flag, except that it */
/* works with ld.gold. */
/DISCARD/ : {
/DISCARD/ : {
*(.eh_frame)
*(.eh_frame*)
*(.note .note.*)
*(.note .note.*)
*(.interp)
}
}
}
}


</syntaxhighlight>
</source>


==Building the kernel and creating an image==
==Building the kernel and creating an image==
Line 224: Line 246:
GNU make will process it.
GNU make will process it.


<source lang="make">
<syntaxhighlight lang="make">
# Nuke built-in rules and variables.
# Nuke built-in rules and variables.
override MAKEFLAGS += -rR
override MAKEFLAGS += -rR
Line 245: Line 267:
# We are using the standard "cc" here, it may work by using
# We are using the standard "cc" here, it may work by using
# the host system's toolchain, but this is not guaranteed.
# the host system's toolchain, but this is not guaranteed.
override DEFAULT_CC := cc
override DEFAULT_KCC := cc
$(eval $(call DEFAULT_VAR,CC,$(DEFAULT_CC)))
$(eval $(call DEFAULT_VAR,KCC,$(DEFAULT_KCC)))


# Same thing for "ld" (the linker).
# Same thing for "ld" (the linker).
override DEFAULT_LD := ld
override DEFAULT_KLD := ld
$(eval $(call DEFAULT_VAR,LD,$(DEFAULT_LD)))
$(eval $(call DEFAULT_VAR,KLD,$(DEFAULT_KLD)))


# User controllable C flags.
# User controllable C flags.
override DEFAULT_CFLAGS := -g -O2 -pipe
override DEFAULT_KCFLAGS := -g -O2 -pipe
$(eval $(call DEFAULT_VAR,CFLAGS,$(DEFAULT_CFLAGS)))
$(eval $(call DEFAULT_VAR,KCFLAGS,$(DEFAULT_KCFLAGS)))


# User controllable C preprocessor flags. We set none by default.
# User controllable C preprocessor flags. We set none by default.
override DEFAULT_CPPFLAGS :=
override DEFAULT_KCPPFLAGS :=
$(eval $(call DEFAULT_VAR,CPPFLAGS,$(DEFAULT_CPPFLAGS)))
$(eval $(call DEFAULT_VAR,KCPPFLAGS,$(DEFAULT_KCPPFLAGS)))


# User controllable nasm flags.
# User controllable nasm flags.
override DEFAULT_NASMFLAGS := -F dwarf -g
override DEFAULT_KNASMFLAGS := -F dwarf -g
$(eval $(call DEFAULT_VAR,NASMFLAGS,$(DEFAULT_NASMFLAGS)))
$(eval $(call DEFAULT_VAR,KNASMFLAGS,$(DEFAULT_KNASMFLAGS)))


# User controllable linker flags. We set none by default.
# User controllable linker flags. We set none by default.
override DEFAULT_LDFLAGS :=
override DEFAULT_KLDFLAGS :=
$(eval $(call DEFAULT_VAR,LDFLAGS,$(DEFAULT_LDFLAGS)))
$(eval $(call DEFAULT_VAR,KLDFLAGS,$(DEFAULT_KLDFLAGS)))


# Internal C flags that should not be changed by the user.
# Internal C flags that should not be changed by the user.
override CFLAGS += \
override KCFLAGS += \
-Wall \
-Wall \
-Wextra \
-Wextra \
Line 287: Line 309:


# Internal C preprocessor flags that should not be changed by the user.
# Internal C preprocessor flags that should not be changed by the user.
override CPPFLAGS := \
override KCPPFLAGS := \
-I src \
-I src \
$(CPPFLAGS) \
$(KCPPFLAGS) \
-MMD \
-MMD \
-MP
-MP


# Internal linker flags that should not be changed by the user.
# Internal linker flags that should not be changed by the user.
override LDFLAGS += \
override KLDFLAGS += \
-m elf_x86_64 \
-m elf_x86_64 \
-nostdlib \
-nostdlib \
-static \
-pie \
-pie \
--no-dynamic-linker \
-z text \
-z text \
-z max-page-size=0x1000 \
-z max-page-size=0x1000 \
Line 305: Line 325:


# Internal nasm flags that should not be changed by the user.
# Internal nasm flags that should not be changed by the user.
override NASMFLAGS += \
override KNASMFLAGS += \
-Wall \
-Wall \
-f elf64
-f elf64
Line 322: Line 342:


# Link rules for the final kernel executable.
# Link rules for the final kernel executable.
# The magic printf/dd command is used to force the final ELF file type to ET_DYN.
# GNU binutils, for silly reasons, forces the ELF type to ET_EXEC even for
# relocatable PIEs, if the base load address is non-0.
# See https://sourceware.org/bugzilla/show_bug.cgi?id=31795 for more information.
bin/$(KERNEL): GNUmakefile linker.ld $(OBJ)
bin/$(KERNEL): GNUmakefile linker.ld $(OBJ)
mkdir -p "$$(dirname $@)"
mkdir -p "$$(dirname $@)"
$(LD) $(OBJ) $(LDFLAGS) -o $@
$(KLD) $(OBJ) $(KLDFLAGS) -o $@
printf '\003' | dd of=$@ bs=1 count=1 seek=16 conv=notrunc


# Include header dependencies.
# Include header dependencies.
Line 332: Line 357:
obj/%.c.o: src/%.c GNUmakefile
obj/%.c.o: src/%.c GNUmakefile
mkdir -p "$$(dirname $@)"
mkdir -p "$$(dirname $@)"
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
$(KCC) $(KCFLAGS) $(KCPPFLAGS) -c $< -o $@


# Compilation rules for *.S files.
# Compilation rules for *.S files.
obj/%.S.o: src/%.S GNUmakefile
obj/%.S.o: src/%.S GNUmakefile
mkdir -p "$$(dirname $@)"
mkdir -p "$$(dirname $@)"
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
$(KCC) $(KCFLAGS) $(KCPPFLAGS) -c $< -o $@


# Compilation rules for *.asm (nasm) files.
# Compilation rules for *.asm (nasm) files.
obj/%.asm.o: src/%.asm GNUmakefile
obj/%.asm.o: src/%.asm GNUmakefile
mkdir -p "$$(dirname $@)"
mkdir -p "$$(dirname $@)"
nasm $(NASMFLAGS) $< -o $@
nasm $(KNASMFLAGS) $< -o $@


# Remove object files and the final executable.
# Remove object files and the final executable.
Line 348: Line 373:
clean:
clean:
rm -rf bin obj
rm -rf bin obj
</syntaxhighlight>
</source>


===limine.cfg===
===limine.cfg===
Line 354: Line 379:
This file is parsed by Limine and it describes boot entries and other bootloader configuration variables. Further information [https://github.com/limine-bootloader/limine/blob/trunk/CONFIG.md here].
This file is parsed by Limine and it describes boot entries and other bootloader configuration variables. Further information [https://github.com/limine-bootloader/limine/blob/trunk/CONFIG.md here].


<source lang="ini">
<syntaxhighlight lang="ini">
# Timeout in seconds that Limine will use before automatically booting.
# Timeout in seconds that Limine will use before automatically booting.
TIMEOUT=5
TIMEOUT=5


# The entry name that will be displayed in the boot menu.
# The entry name that will be displayed in the boot menu.
:myOS (KASLR on)
:myOS
# We use the Limine boot protocol.
# We use the Limine boot protocol.
PROTOCOL=limine
PROTOCOL=limine

# Disable KASLR (it is enabled by default for relocatable kernels)
KASLR=no


# Path to the kernel to boot. boot:/// represents the partition on which limine.cfg is located.
# Path to the kernel to boot. boot:/// represents the partition on which limine.cfg is located.
KERNEL_PATH=boot:///myos
KERNEL_PATH=boot:///boot/myos


# Same thing, but without KASLR.
# Same thing, but with KASLR.
:myOS (KASLR off)
:myOS (with KASLR)
PROTOCOL=limine
PROTOCOL=limine


KERNEL_PATH=boot:///boot/myos
# Disable KASLR (it is enabled by default for relocatable kernels)
</syntaxhighlight>
KASLR=no

KERNEL_PATH=boot:///myos
</source>


===Compiling the kernel===
===Compiling the kernel===
Line 384: Line 409:
''If you are not using macOS, you can skip this section.''
''If you are not using macOS, you can skip this section.''


The macOS Xcode toolchain uses Mach-O binaries, and not the ELF binaries supported by limine. The solution is to obtain a cross compiler from [https://brew.sh homebrew]. After installing homebrew, install the '''x86_64-elf-gcc''' package, then replace '''DEFAULT_CC''' with '''x86_64-elf-gcc''' and '''DEFAULT_LD''' with '''x86_64-elf-ld'''.
The macOS Xcode toolchain uses Mach-O binaries, and not the ELF binaries required for this Limine-compliant kernel. A solution is to build a [[GCC Cross-Compiler]], or to obtain one from [https://brew.sh homebrew] by installing the '''x86_64-elf-gcc''' package. After one of these is done, build using '''make CC=x86_64-elf-gcc LD=x86_64-elf-ld'''.


===Creating the image===
===Creating the image===
Line 398: Line 423:
These are shell commands. They can also be compiled into a script or Makefile.
These are shell commands. They can also be compiled into a script or Makefile.


<source lang="bash">
<syntaxhighlight lang="bash">
# Download the latest Limine binary release.
# Download the latest Limine binary release for the 7.x branch.
git clone https://github.com/limine-bootloader/limine.git --branch=v5.x-branch-binary --depth=1
git clone https://github.com/limine-bootloader/limine.git --branch=v7.x-binary --depth=1


# Build limine utility.
# Build "limine" utility.
make -C limine
make -C limine


Line 409: Line 434:


# Copy the relevant files over.
# Copy the relevant files over.
mkdir -p iso_root/boot
cp -v bin/myos limine.cfg limine/limine-bios.sys \
limine/limine-bios-cd.bin limine/limine-uefi-cd.bin iso_root/
cp -v bin/myos iso_root/boot/
mkdir -p iso_root/boot/limine
cp -v limine.cfg limine/limine-bios.sys limine/limine-bios-cd.bin \
limine/limine-uefi-cd.bin iso_root/boot/limine/


# Create the EFI boot tree and copy Limine's EFI executables over.
# Create the EFI boot tree and copy Limine's EFI executables over.
Line 418: Line 446:


# Create the bootable ISO.
# Create the bootable ISO.
xorriso -as mkisofs -b limine-bios-cd.bin \
xorriso -as mkisofs -b boot/limine/limine-bios-cd.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-no-emul-boot -boot-load-size 4 -boot-info-table \
--efi-boot limine-uefi-cd.bin \
--efi-boot boot/limine/limine-uefi-cd.bin \
-efi-boot-part --efi-boot-image --protective-msdos-label \
-efi-boot-part --efi-boot-image --protective-msdos-label \
iso_root -o image.iso
iso_root -o image.iso
Line 426: Line 454:
# Install Limine stage 1 and 2 for legacy BIOS boot.
# Install Limine stage 1 and 2 for legacy BIOS boot.
./limine/limine bios-install image.iso
./limine/limine bios-install image.iso
</syntaxhighlight>
</source>


====Creating a hard disk/USB drive image====
====Creating a hard disk/USB drive image====
Line 436: Line 464:
These are shell commands. They can also be compiled into a script or Makefile.
These are shell commands. They can also be compiled into a script or Makefile.


<source lang="bash">
<syntaxhighlight lang="bash">
# Create an empty zeroed-out 64MiB image file.
# Create an empty zeroed-out 64MiB image file.
dd if=/dev/zero bs=1M count=0 seek=64 of=image.hdd
dd if=/dev/zero bs=1M count=0 seek=64 of=image.hdd
Line 443: Line 471:
sgdisk image.hdd -n 1:2048 -t 1:ef00
sgdisk image.hdd -n 1:2048 -t 1:ef00


# Download the latest Limine binary release.
# Download the latest Limine binary release for the 7.x branch.
git clone https://github.com/limine-bootloader/limine.git --branch=v5.x-branch-binary --depth=1
git clone https://github.com/limine-bootloader/limine.git --branch=v7.x-binary --depth=1


# Build limine utility.
# Build "limine" utility.
make -C limine
make -C limine


Line 455: Line 483:
mformat -i image.hdd@@1M
mformat -i image.hdd@@1M


# Make relevant subdirectories.
# Make /EFI and /EFI/BOOT an MSDOS subdirectory.
mmd -i image.hdd@@1M ::/EFI ::/EFI/BOOT
mmd -i image.hdd@@1M ::/EFI ::/EFI/BOOT ::/boot ::/boot/limine


# Copy over the relevant files
# Copy over the relevant files.
mcopy -i image.hdd@@1M bin/myos limine.cfg limine/limine-bios.sys ::/
mcopy -i image.hdd@@1M bin/myos ::/boot
mcopy -i image.hdd@@1M limine.cfg limine/limine-bios.sys ::/boot/limine
mcopy -i image.hdd@@1M limine/BOOTX64.EFI ::/EFI/BOOT
mcopy -i image.hdd@@1M limine/BOOTX64.EFI ::/EFI/BOOT
mcopy -i image.hdd@@1M limine/BOOTIA32.EFI ::/EFI/BOOT
mcopy -i image.hdd@@1M limine/BOOTIA32.EFI ::/EFI/BOOT
</syntaxhighlight>
</source>


==Conclusions==
==Conclusions==

Revision as of 16:36, 28 June 2024

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

Beginner
Kernel Designs
Models
Other Concepts

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

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 Limine-compliant x86-64 kernel in (GNU) C, and boot it using the Limine bootloader.

Additionally, it is highly recommended to check out this template project as it provides example buildable code to go along with this guide.

Overview

For this example, we will create these 2 files to create the basic directory tree of our project:

  • src/main.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 src directory.

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

src/main.c

This is the kernel "main".

#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <limine.h>

// Set the base revision to 2, this is recommended as this is the latest
// base revision described by the Limine boot protocol specification.
// See specification for further info.

__attribute__((used, section(".requests")))
static volatile LIMINE_BASE_REVISION(2);

// 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, _and_ they should be accessed at least
// once or marked as used with the "used" attribute as done here.

__attribute__((used, section(".requests")))
static volatile struct limine_framebuffer_request framebuffer_request = {
    .id = LIMINE_FRAMEBUFFER_REQUEST,
    .revision = 0
};

// Finally, define the start and end markers for the Limine requests.
// These can also be moved anywhere, to any .c file, as seen fit.

__attribute__((used, section(".requests_start_marker")))
static volatile LIMINE_REQUESTS_START_MARKER;

__attribute__((used, section(".requests_end_marker")))
static volatile LIMINE_REQUESTS_END_MARKER;

// 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 the bootloader actually understands our base revision (see spec).
    if (LIMINE_BASE_REVISION_SUPPORTED == false) {
        hcf();
    }

    // Ensure we got a framebuffer.
    if (framebuffer_request.response == NULL
     || framebuffer_request.response->framebuffer_count < 1) {
        hcf();
    }

    // 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++) {
        volatile uint32_t *fb_ptr = framebuffer->address;
        fb_ptr[i * (framebuffer->pitch / 4) + i] = 0xffffff;
    }

    // We're done, just hang...
    hcf();
}

linker.ld

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 */
OUTPUT_FORMAT(elf64-x86-64)
OUTPUT_ARCH(i386:x86-64)

/* We want the symbol _start to be our entry point */
ENTRY(_start)

/* Define the program headers we want so the bootloader gives us the right */
/* MMU permissions */
PHDRS
{
    text     PT_LOAD    FLAGS(0x05); /* Execute + Read */
    rodata   PT_LOAD    FLAGS(0x04); /* Read only */
    data     PT_LOAD    FLAGS(0x06); /* Write + Read */
    dynamic  PT_DYNAMIC FLAGS(0x06); /* Dynamic PHDR for relocations */
}

SECTIONS
{
    /* 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 */
    . = ALIGN(CONSTANT(MAXPAGESIZE));

    .rodata : {
        *(.rodata .rodata.*)
    } :rodata

    /* Move to the next memory page for .data */
    . = ALIGN(CONSTANT(MAXPAGESIZE));

    .data : {
        *(.data .data.*)

        /* Place the sections that contain the Limine requests as part of the .data */
        /* output section. */
        KEEP(*(.requests_start_marker))
        KEEP(*(.requests))
        KEEP(*(.requests_end_marker))
    } :data

    /* Dynamic section for relocations, both in its own PHDR and inside data PHDR */
    .dynamic : {
        *(.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.*)
        *(COMMON)
    } :data

    /* Discard .note.* and .eh_frame* since they may cause issues on some hosts. */
    /* Also discard the program interpreter section since we do not need one. This is */
    /* more or less equivalent to the --no-dynamic-linker linker flag, except that it */
    /* works with ld.gold. */
    /DISCARD/ : {
        *(.eh_frame*)
        *(.note .note.*)
        *(.interp)
    }
}

Building the kernel and creating an image

GNUmakefile

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

# Convenience macro to reliably declare user overridable variables.
define DEFAULT_VAR =
    ifeq ($(origin $1),default)
        override $(1) := $(2)
    endif
    ifeq ($(origin $1),undefined)
        override $(1) := $(2)
    endif
endef

# 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_KCC := cc
$(eval $(call DEFAULT_VAR,KCC,$(DEFAULT_KCC)))

# Same thing for "ld" (the linker).
override DEFAULT_KLD := ld
$(eval $(call DEFAULT_VAR,KLD,$(DEFAULT_KLD)))

# User controllable C flags.
override DEFAULT_KCFLAGS := -g -O2 -pipe
$(eval $(call DEFAULT_VAR,KCFLAGS,$(DEFAULT_KCFLAGS)))

# User controllable C preprocessor flags. We set none by default.
override DEFAULT_KCPPFLAGS :=
$(eval $(call DEFAULT_VAR,KCPPFLAGS,$(DEFAULT_KCPPFLAGS)))

# User controllable nasm flags.
override DEFAULT_KNASMFLAGS := -F dwarf -g
$(eval $(call DEFAULT_VAR,KNASMFLAGS,$(DEFAULT_KNASMFLAGS)))

# User controllable linker flags. We set none by default.
override DEFAULT_KLDFLAGS :=
$(eval $(call DEFAULT_VAR,KLDFLAGS,$(DEFAULT_KLDFLAGS)))

# Internal C flags that should not be changed by the user.
override KCFLAGS += \
    -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 \
    -mno-red-zone

# Internal C preprocessor flags that should not be changed by the user.
override KCPPFLAGS := \
    -I src \
    $(KCPPFLAGS) \
    -MMD \
    -MP

# Internal linker flags that should not be changed by the user.
override KLDFLAGS += \
    -m elf_x86_64 \
    -nostdlib \
    -pie \
    -z text \
    -z max-page-size=0x1000 \
    -T linker.ld

# Internal nasm flags that should not be changed by the user.
override KNASMFLAGS += \
    -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 cd src && find -L * -type f -name '*.c')
override ASFILES := $(shell cd src && find -L * -type f -name '*.S')
override NASMFILES := $(shell cd src && find -L * -type f -name '*.asm')
override OBJ := $(addprefix obj/,$(CFILES:.c=.c.o) $(ASFILES:.S=.S.o) $(NASMFILES:.asm=.asm.o))
override HEADER_DEPS := $(addprefix obj/,$(CFILES:.c=.c.d) $(ASFILES:.S=.S.d))

# Default target.
.PHONY: all
all: bin/$(KERNEL)

# Link rules for the final kernel executable.
# The magic printf/dd command is used to force the final ELF file type to ET_DYN.
# GNU binutils, for silly reasons, forces the ELF type to ET_EXEC even for
# relocatable PIEs, if the base load address is non-0.
# See https://sourceware.org/bugzilla/show_bug.cgi?id=31795 for more information.
bin/$(KERNEL): GNUmakefile linker.ld $(OBJ)
	mkdir -p "$$(dirname $@)"
	$(KLD) $(OBJ) $(KLDFLAGS) -o $@
	printf '\003' | dd of=$@ bs=1 count=1 seek=16 conv=notrunc

# Include header dependencies.
-include $(HEADER_DEPS)

# Compilation rules for *.c files.
obj/%.c.o: src/%.c GNUmakefile
	mkdir -p "$$(dirname $@)"
	$(KCC) $(KCFLAGS) $(KCPPFLAGS) -c $< -o $@

# Compilation rules for *.S files.
obj/%.S.o: src/%.S GNUmakefile
	mkdir -p "$$(dirname $@)"
	$(KCC) $(KCFLAGS) $(KCPPFLAGS) -c $< -o $@

# Compilation rules for *.asm (nasm) files.
obj/%.asm.o: src/%.asm GNUmakefile
	mkdir -p "$$(dirname $@)"
	nasm $(KNASMFLAGS) $< -o $@

# Remove object files and the final executable.
.PHONY: clean
clean:
	rm -rf bin obj

limine.cfg

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.
TIMEOUT=5

# The entry name that will be displayed in the boot menu.
:myOS
    # We use the Limine boot protocol.
    PROTOCOL=limine

    # Disable KASLR (it is enabled by default for relocatable kernels)
    KASLR=no

    # Path to the kernel to boot. boot:/// represents the partition on which limine.cfg is located.
    KERNEL_PATH=boot:///boot/myos

# Same thing, but with KASLR.
:myOS (with KASLR)
    PROTOCOL=limine

    KERNEL_PATH=boot:///boot/myos

Compiling the kernel

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

Compiling the kernel on macOS

If you are not using macOS, you can skip this section.

The macOS Xcode toolchain uses Mach-O binaries, and not the ELF binaries required for this Limine-compliant kernel. A solution is to build a GCC Cross-Compiler, or to obtain one from homebrew by installing the x86_64-elf-gcc package. After one of these is done, build using make CC=x86_64-elf-gcc LD=x86_64-elf-ld.

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 for the 7.x branch.
git clone https://github.com/limine-bootloader/limine.git --branch=v7.x-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.
mkdir -p iso_root/boot
cp -v bin/myos iso_root/boot/
mkdir -p iso_root/boot/limine
cp -v limine.cfg limine/limine-bios.sys limine/limine-bios-cd.bin \
      limine/limine-uefi-cd.bin iso_root/boot/limine/

# 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 boot/limine/limine-bios-cd.bin \
        -no-emul-boot -boot-load-size 4 -boot-info-table \
        --efi-boot boot/limine/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 for the 7.x branch.
git clone https://github.com/limine-bootloader/limine.git --branch=v7.x-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 relevant subdirectories.
mmd -i image.hdd@@1M ::/EFI ::/EFI/BOOT ::/boot ::/boot/limine

# Copy over the relevant files.
mcopy -i image.hdd@@1M bin/myos ::/boot
mcopy -i image.hdd@@1M limine.cfg limine/limine-bios.sys ::/boot/limine
mcopy -i image.hdd@@1M limine/BOOTX64.EFI ::/EFI/BOOT
mcopy -i image.hdd@@1M limine/BOOTIA32.EFI ::/EFI/BOOT

Conclusions

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

Articles

External Links