Files
prog8/codeGenM68k/docs/linking.md
T

4.1 KiB

Linking the kernel

The kernel is linked using vlink, producing an ELF32 binary (kernel.elf) that qemu loads directly via its -kernel option.

Toolchain

Tool Role
vasmm68k_mot Assembles kernel.S → kernel.o
vlink Links kernel.o → kernel.elf using link.ld

The Makefile invocation:

vlink -b elf32m68k -n -T link.ld -o kernel.elf kernel.o

Command-line flags

-b elf32m68k — output format

Tells vlink to produce a 32-bit big-endian m68k ELF binary.

vlink supports many output formats; without this flag it would default to something else. Note that vlink ignores the OUTPUT_FORMAT() directive inside linker scripts, so the format must be specified on the command line.

-n — NMAGIC / no page alignment

Disables page-boundary alignment of ELF segments.

Without -n, vlink aligns the LOAD segment to a full page (0x2000 = 8 KB). The resulting ELF has a large gap of padding between the ELF header and the actual code:

Type   Offset   VirtAddr   PhysAddr   Align
LOAD   0x002000 0x00010000 0x00010000 0x2000

qemu's ELF loader gets confused by this layout and either fails to load the kernel or jumps to the wrong address.

With -n, segment alignment is reduced to the minimum (0x10) and the code starts right after the ELF header:

Type   Offset   VirtAddr   PhysAddr   Align
LOAD   0x000080 0x00010000 0x00010000 0x10

qemu loads this cleanly — the code is at file offset 0x80, maps to virtual address 0x10000, and execution starts at _start as expected.

-n is the vlink equivalent of GNU ld's -N / --omagic option.

-T link.ld — linker script

Passes the linker script (see below).

The linker script (link.ld)

The compiler resolves the linker script for a target in this order:

  1. link.ld in the target's library directory (the library setting). This is how a custom target, defined outside of the compiler, supplies its own memory layout.
  2. /prog8lib/<targetname>/link.ld, for targets built into the compiler.
  3. /prog8lib/qemu68k/link.ld as the fallback, which is the script shown below.

Only the ELF output type links at all. The AMIGAHUNK output type (used by the built-in Amiga targets) assembles straight to a hunk executable and ignores the linker script.

ENTRY(_start)
SECTIONS
{
  . = 0x10000;
  .text : { *(.text) }
  .data : { *(.data) }
  .bss  : { *(.bss)  *(COMMON) }
  _end_of_code = .;
}

Line by line

Line What it does
ENTRY(_start) Sets the ELF entry point to _start — the first instruction in kernel.S. qemu uses this to know where to jump after loading the kernel.
SECTIONS { … } Opens the section layout block — the core of any linker script. Everything inside describes how to arrange the output binary.
. = 0x10000; Sets the load address to 0x10000 (64 KB). This is where qemu places the kernel in memory and begins execution. The . is the location counter — it advances automatically as sections are placed.
.text : { *(.text) } Collects all .text (code) sections from every input object file into one output .text section.
.data : { *(.data) } Same for initialised data.
.bss : { *(.bss) *(COMMON) } Same for uninitialised (zero-filled) data. COMMON catches any unresolved global variables that the assembler emits as common symbols.
_end_of_code = .; Defines the symbol _end_of_code at the address immediately after the last section. kernel.S references this symbol to find where the bootinfo records begin in memory (qemu appends them just past the kernel image).

Why a linker script is required

vlink has no command-line flag equivalent to . = 0x10000 (setting the load address). Attempting -Ttext=0x10000 causes a fatal error because vlink treats the = as part of a filename. The correct syntax -Ttext 0x10000 (space-separated) does set the address, but then _end_of_code is undefined because there is no linker script to define it.

The linker script is therefore the minimum required: it sets the load address and defines _end_of_code.