Search Knowledge

© 2026 LIBREUNI PROJECT

Operating Systems Internals / Laboratory: OS Development

From Source to Screen: Building and Emulating

From Source to Screen: Building and Emulating

Transforming low-level assembly (boot.s) and C source files (kernel.c) into a single executable binary image requires compiling the code for a target architecture and configuring the physical layout of the output binary using a linker script.

Because a kernel boots directly on the bare metal without an underlying operating system, the compilation process must omit standard user-space libraries and runtime symbols, and specify the exact physical memory location where the binary will reside.

1. The Linker Script (linker.ld)

Standard user-space applications are compiled to rely on a dynamic loader, allowing the kernel to place the program sections anywhere in virtual memory. For a bootable kernel, memory placement must be precise. Most x86 firmware and bootloaders expect the kernel binary to be loaded at the 1 Megabyte (0x100000) boundary in physical memory, leaving lower memory ranges free for BIOS data structures and hardware mapped I/O.

A Linker Script specifies how the linker compiles input object sections into output sections, defining the precise memory layout.

For example, a typical linker script (linker.ld) defines the starting memory offset and groups the program’s segments (.text, .rodata, .data, .bss) sequentially:

/* The entry point defined in boot.s */
ENTRY(_start)

SECTIONS {
    /* Set the location counter to the 1 Megabyte boundary */
    . = 1M;

    /* First, place the Multiboot header and the code segment */
    .text BLOCK(4K) : ALIGN(4K) {
        *(.multiboot)
        *(.text)
    }

    /* Read-only data segment (constants and string literals) */
    .rodata BLOCK(4K) : ALIGN(4K) {
        *(.rodata)
    }

    /* Read-write data segment (initialized global variables) */
    .data BLOCK(4K) : ALIGN(4K) {
        *(.data)
    }

    /* Read-write data segment (uninitialized globals) and stack */
    .bss BLOCK(4K) : ALIGN(4K) {
        *(COMMON)
        *(.bss)
    }
}

2. The Build Process

To compile code for a bare-metal architecture, you use a target-independent cross-compiler. The compilation steps assemble the boot interface, compile the kernel logic, and link the final executable image.

For example, these commands illustrate the compilation sequence on a Linux host targeting x86 bare metal:

Step 1: Assemble the Boot Code

nasm -f elf32 boot.s -o boot.o

This command compiles the assembly source file into a 32-bit ELF format object file (boot.o).

Step 2: Compile the C Kernel

gcc -m32 -c kernel.c -o kernel.o -ffreestanding -O2 -Wall -Wextra

The flags serve the following purposes:

  • -m32: Generates 32-bit instructions.
  • -ffreestanding: Directs GCC not to assume standard C library functions or runtime symbols are available (e.g., omitting standard main entry requirements and custom system library linkages).
  • -O2: Applies standard optimization structures.
ld -m elf_i386 -T linker.ld boot.o kernel.o -o myos.bin

The -T flag instructs the linker (ld) to apply our custom memory layout schema (linker.ld), linking the object files into the final bootable binary myos.bin.

3. Running in QEMU

Deploying a raw kernel to physical hardware is time-consuming and risks hardware lockups. Instead, developers use a hardware emulator like QEMU (Quick Emulator) to simulate a complete computer system in software.

To run the compiled kernel in a simulated 32-bit x86 environment, execute the following emulation command:

qemu-system-i386 -kernel myos.bin

Upon execution, QEMU boots the multiboot-compliant kernel directly. The emulator initializes the screen buffer, loading the output strings into video memory.

Summary of Tools

The table below outlines the functions and outputs of each tool used in the bare-metal development pipeline:

ToolPurposeOutput
NASMAssemblerObject File (.o)
GCCC Compiler (with -ffreestanding)Object File (.o)
LDLinker (with -T linker.ld)Executable Binary (.bin)
QEMUEmulatorSimulated PC Execution

For example, to verify that each component of this toolchain is installed and accessible in the system environment, you can run their version check flags:

# Verify installation of the compilation and emulation toolchain
nasm -v
gcc --version
ld -v
qemu-system-i386 --version

Exercise: The Entry Point

Which line in the linker script tells the computer where to start executing the code when the kernel is loaded?

Defining the Entry

()

Applying these toolchain steps enables the creation of a bootable operating system core, laying the foundation for developing memory managers, process schedulers, and device drivers.

References & Further Reading

To practice or explore further, see the official documentation below for an example of toolchain configuration and architecture reference: