💻 C und Assembler mit Raspberry Pi

Unser erstes Programm in C (PI4)

Einführung

In diesem Kurs tauchst du tief in die Grundlagen der Systemprogrammierung auf dem Raspberry Pi 4 ein. Wir starten von Grund auf – ganz ohne fertiges Betriebssystem im Bare-Metal-Modus.

Schritt für Schritt arbeiten wir uns von den ersten Hardware-Grundlagen empor: Wir beginnen mit einfachen Aufgaben wie dem Ansteuern einer LED, bringen erste Zeichen auf den Bildschirm und bauen schließlich unser eigenes Terminal auf.

Diese Fundamente bilden das Sprungbrett für unser großes Ziel: Die Entwicklung eines eigenen, schlanken Betriebssystems. Im Laufe des Kurses lernst du, wie dein System den Arbeitsspeicher verwaltet, USB-Geräte wie Maus und Tastatur ansteuert sowie mit Dateisystemen umgeht. Darüber hinaus bauen wir eine eigene Konsole zur Befehlseingabe auf und schaffen die Mechanismen, um externen Code direkt in deinem eigenen Betriebssystem auszuführen.

Egal ob Hardware-Liebhaber oder angehender Systementwickler – mache dich bereit für eine spannende Reise unter die Haube moderner Computerarchitektur!


Ziel unseres ersten Programms

Unser erstes Programm wird eine einfache Endlosschleife enthalten. Dies dient als Basis für weitere Experimente und Versuche. Ich werde erklären, wie ein solches Programm erstellt, kompiliert und ausgeführt wird.

Erstellung des Sourcecodes

Leider kommen wir bei Bare-Metal-Programmierung in C nicht um Assembler herum. Auch wenn das Assemblerprogramm zunächst nicht viel tut, wird es später beispielsweise für die Interrupt-Programmierung benötigt. Erstelle zunächst das Verzeichnis "src", in der wir unseren Sourcecode ablegen werden.

Assembler-Code (boot.S)

Öffne ein Textprogramm und schreibe folgendes erstes Assemblerprogramm:

//
// The first program for RPI4
// 20.02.2025 www.satyria.de
//

.section .init      // Ensure the linker places this at the beginning of the kernel image
.globl _start       // Generates a global label
_start:             // The label _start (entry address)

  mov sp, #0x80000  // Create a stack of 512KB (524288 bytes)
  b main            // Branch to "main"

Speichere die Datei im Standardverzeichnis unter Windows: C:\msys64\home\xxx\DeinProjekt\src (wobei xxx in der Regel Dein Benutzername ist). Unter Linux kann die Datei im Home-Verzeichnis gespeichert werden. Gebe der Datei den Namen boot.S. Die Endung .S kennzeichnet die Datei als Assembler-Sourcecode.

C-Code (main.c)

Nun erstellen wir eine main.c Datei für unser erstes Programm:

//
// main.c
// The first program for RPI4
// 20.02.2025 www.satyria.de
//

int main (void)
{
  while (1){}
}

Hier wird eine einfache Endlosschleife erzeugt.

Kompilieren des Programms mit Make

Um unser Programm zu kompilieren, muss zunächst die Programmierumgebung eingerichtet werden. Verwende hierzu die entsprechenden Anleitungen. Diese funktionieren auch für den Raspberry Pi 4:

Programmierumgebung erstellen (Konsole)

Programmierumgebung erstellen (64-Bit)

Erstelle dann ein Makefile, welches im Root-Verzeichnis des Projektes abgelegt wird:

CSRCS   := $(wildcard src/*.c)
CPPSRCS := $(wildcard src/*.cpp)
ASRCS   := $(wildcard src/*.S)

# Objektdateien in getrennte Build-Unterordner umleiten!
COBJS   := $(CSRCS:src/%.c=build/c/%.o)
CPPOBJS := $(CPPSRCS:src/%.cpp=build/cpp/%.o)
AOBJS   := $(ASRCS:src/%.S=build/asm/%.o)

AllOBJS := $(COBJS) $(CPPOBJS) $(AOBJS)
LOADADDR = 0x80000

# Befehl zum Erstellen von Ordnern (je nach Betriebssystem)
ifeq ($(MSYSTEM),)
    MKDIR = if not exist $(subst /,\,$1) mkdir $(subst /,\,$1)
else
    MKDIR = mkdir -p $1
endif

GCCFLAGS = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -O0 -ffreestanding \
           -nostartfiles -nostdlib -nostdinc -g -I ./include

AFLAGS   = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -I ./include -O0 -g

CFLAGS   = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -fsigned-char -ffreestanding -g \
           -I ./include -O0 -fno-exceptions 

CPPFLAGS = -fno-exceptions -fno-rtti -nostdinc++ -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -fsigned-char \
           -ffreestanding -g -I ./include -O0 -mstrict-align -std=c++14 -Wno-aligned-new

all: new clean kernel8.img

# --- Kompilier-Regeln mit automatischer Ordner-Erstellung ---

build/asm/%.o: src/%.S
	@$(call MKDIR,build/asm)
	@echo "as $@"
	@aarch64-none-elf-gcc $(AFLAGS) -c $< -o $@

build/c/%.o: src/%.c
	@$(call MKDIR,build/c)
	@echo "gcc $@"
	@aarch64-none-elf-gcc $(CFLAGS) -c $< -o $@

build/cpp/%.o: src/%.cpp
	@$(call MKDIR,build/cpp)
	@echo "g++ $@"
	@aarch64-none-elf-g++ $(CPPFLAGS) -c $< -o $@

kernel8.img: $(AllOBJS)
	@echo "============================================================================="
	@echo "Linking..."
	@aarch64-none-elf-ld -o kernel8.elf -Map kernel8.map -nostdlib \
		--section-start=.init=$(LOADADDR) --no-warn-rwx-segments \
		-g -T linker.ld $(AllOBJS)
	aarch64-none-elf-objcopy -O binary kernel8.elf kernel8.img

clean:
ifeq ($(MSYSTEM),)
# --- Native Windows Umgebung (CMD / PowerShell) ---
	@if exist kernel8.elf del /q /f kernel8.elf
	@if exist kernel8.img del /q /f kernel8.img
	@if exist kernel8.map del /q /f kernel8.map
	@if exist build rmdir /s /q build
else
# --- MSYS2 / Unix-ähnliche Umgebung ---
	/bin/rm -rf kernel8.elf kernel8.map build *.img > /dev/null 2>&1 || true
endif

new:
ifeq ($(MSYSTEM),)
	@cls
else
	/bin/clear
endif

Speichere diese Datei unter dem Namen Makefile.

Wenn Du mehr über den Inhalt erfahren möchtest, schaue unter "Arbeiten mit Make und Linker-Script" nach.

Linker-Script (linker.ld)

Erstelle das folgende Linker-Script, welches wir unter Arbeiten mit Make und Linker-Script beschrieben haben. Dieses wird, wie das Makefile in das Root-Verzeichnis abgelegt:

/* linker.ld - Für Raspberry Pi 4 AArch64 */
ENTRY(_start)

MEMORY {
    ram : ORIGIN = 0x80000, LENGTH = 128M
}

SECTIONS {
    . = 0x80000;

    .init : {
        *(.init)
    } > ram

    .text : {
        *(.text*)
        _etext = .;
    } > ram

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

    .init_array : {
        __init_start = .;
        KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*)))
        KEEP(*(.init_array))
        __init_end = .;
    } > ram

    .ARM.exidx : {
        __exidx_start = .;
        *(.ARM.exidx*)
        __exidx_end = .;
    } > ram

    .eh_frame : {
        *(.eh_frame*)
    } > ram

    .data : {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > ram

.bss (NOLOAD) : {
        . = ALIGN(8);
        __bss_start = .;
        *(.bss*)
        *(COMMON)
        . = ALIGN(8);
        __bss_end = .;
    } > ram

    _end = .;
    __coherent_start = 0x500000;
    __heap_start     = 0x900000;
    __heap_end       = 0x900000 + 0x200000;
	 
    __MaxMemory = 0xFFFFFFFF-__heap_end;
}

Speichere diese Datei unter dem Namen linker.ld.

Kompilieren und Ausführen

Bevor wir kompilieren, prüfe deine Verzeichnisstruktur:

C:.
|   linker.ld
|   makefile
|       
\---src
        boot.S
        main.c

Nun können wir unser erstes Programm kompilieren:

make

Damit unser Kernel funktionsfähig ist, muss der Kernel auf eine SD-Karte, die in FAT32 formatiert ist, kopiert werden. Zusätzlich benötigt der Raspberry Pi 4 folgende Dateien auf der SD-Karte:

  • bootcode.bin
  • start.elf
  • config.txt
  • kernel8.img (Dein kompiliertes Programm)

Den Source-Code kann hier als ZIP-Datei herunter geladen werden.