Linker Scripts
A linker script tells the linker (ld) exactly how to arrange all the pieces of the kernel into a final binary. Without one, the linker would put sections wherever it felt like — and GRUB would fail to find our Multiboot header.
Why We Need a Custom Linker Script
Two reasons:
- GRUB scans only the first 8KB for the Multiboot header. If our header ends up in the wrong place, GRUB won’t recognise the binary.
- We need to control the load address. GRUB loads the kernel at 1MB (
0x00100000). The linker needs to know this.
The rule from the subject: we cannot use the .ld file of the host system. We write our own.
The Linker Script
ENTRY(_start)
SECTIONS
{
/* Kernel is loaded at 1MB by GRUB */
. = 1M;
_kernel_start = .;
/* Multiboot header MUST come first (GRUB looks in first 8KB) */
.multiboot ALIGN(4K) : {
*(.multiboot_header)
}
/* Read-only code section */
.text ALIGN(4K) : {
*(.text)
*(.text.*)
}
/* Read-only data section */
.rodata ALIGN(4K) : {
*(.rodata)
*(.rodata.*)
}
/* Read-write data section */
.data ALIGN(4K) : {
*(.data)
*(.data.*)
}
/* Uninitialised data section */
.bss ALIGN(4K) : {
*(.bss)
*(.bss.*)
*(COMMON)
}
_kernel_end = .;
/* Discard debug and comment sections */
/DISCARD/ : {
*(.comment)
*(.eh_frame)
*(.note*)
}
}
Line-by-Line Breakdown
Entry Point
ENTRY(_start)
Tells the linker “execution begins at _start”. This is the symbol defined in boot.asm.
Setting the Base Address
. = 1M;
The dot (.) is the location counter — it tracks where the linker is currently placing things. Setting it to 1M means “start placing sections at address 1MB (0x00100000)”. This matches where GRUB loads the kernel.
The Multiboot Section
.multiboot ALIGN(4K) : {
*(.multiboot_header)
}
ALIGN(4K) aligns this section to a 4KB boundary. The *(.multiboot_header) pattern means “collect all .multiboot_header sections from all input files”. Since we put our Multiboot header in section .multiboot_header in boot.asm, it lands here — first in the binary.
Text, Data, BSS
Each section uses ALIGN(4K) to ensure 4KB page alignment. This matters for paging later — memory protection works at page granularity.
.text— Executable code.rodata— Read-only data (string literals, constants).data— Initialised read-write data (global variables with values).bss— Uninitialised data (zero-initialised, includes our stack)
Kernel Boundary Symbols
_kernel_start = .;
/* ... all sections ... */
_kernel_end = .;
These create symbols that Rust code can reference:
#![allow(unused)]
fn main() {
extern "C" {
static _kernel_end: u8;
}
let kernel_end = &_kernel_end as *const u8 as u32;
}
_kernel_end is critical for the memory allocator in KFS_3 — we need to know where the kernel ends so we don’t allocate frames on top of ourselves.
Discarding Sections
/DISCARD/ : {
*(.comment)
*(.eh_frame)
*(.note*)
}
These sections are generated by compilers but are not needed in our kernel:
.comment— Compiler version info.eh_frame— Exception handling frames (we’re not using stack unwinding).note*— Various annotations
Discarding them keeps the binary smaller and avoids the linker warning about RWX segment permissions.
Linking Command
ld -m elf_i386 -n -T linker.ld -o kernel.bin boot.o kb.o exc.o libkfs.a
-m elf_i386— Target i386 ELF format-n— Disable automatic section alignment (we control this in the script)-T linker.ld— Use our linker script- The inputs: assembly
.ofiles and the Rust static library
Memory Layout After Linking
0x00100000 (_kernel_start)
│
├── [.multiboot] ← Multiboot header
├── [.text] ← Code (_start, kernel_main, handlers)
├── [.rodata] ← String literals, constants
├── [.data] ← Initialised global variables
└── [.bss] ← Stack, uninitialised variables
│
0x001XXXXX (_kernel_end)