💻 C und Assembler mit Raspberry Pi

Speicherverwaltung Teil 1 (RPI 4, C)

Bisher haben wir feste Adressen wie 0x500000 oft fest definiert (z. B. per #define in der config.h). Problematisch wird dies, sobald unser Kernel die vorgegebene Größe überschreitet oder neue globale BSS-Variablen hinzukommen. Es kann hierbei sehr schnell zu nicht mehr kontrolliertem Systemverhalten kommen. Da wir eine Methode finden müssen, den Speicher zu verwalten, brauchen wir einen Weg, dies zu verhindern.

Ein Weg ist unser Linker-Script, denn alleine dieses weiß, wie der Speicher in unserem System verteilt ist. Wir verwenden es und lassen den Speicher einfach dynamisch hintereinander gelagert wachsen.

Das dynamische Linker-Skript (linker.ld)

Im ersten Teil des Scripts lassen wir unser System bestehen, wie es ist. In diesem Teil wird definiert, wie der Kernel aufgebaut wird. Hier ist die einzige feste Adresse, die wir benutzen müssen, die Adresse des Kernel-Starts: 0x80000.

/* 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 = .;

Anschließend lassen wir uns durch das Linker-Script die Adressen des Speichers errechnen:

    /* -------------------------------------------------------------------- */
    /* DYNAMISCHE SPEICHERAUFTEILUNG                                        */
    /* -------------------------------------------------------------------- */

    /* 1. Coherent Region (DMA / Uncached Memory für spätere USB/Mailbox-Treiber) */
    . = ALIGN(65536);             /* 64KB Alignment für spätere 64KB MMU-Pages */
    __coherent_start = .;
    . = . + 0x400000;             /* Reserviere 4 MB */
    __coherent_end = .;

    /* 2. Heap Allocator (Dynamischer Nutzspeicher für malloc / palloc) */
    . = ALIGN(65536);             /* 64KB Alignment für palloc-Anforderungen */
    __heap_start = .;
    . = . + 0x1000000;            /* Reserviere 16 MB */
    __heap_end = .;

    /* 3. Systemspeicher / Freier RAM */
    . = ALIGN(65536);
    __memory_start = .;
    __memory_end = ORIGIN(ram) + LENGTH(ram);

Da wir das Script eigentlich nicht mehr anfassen wollen, definieren wir bereits jetzt schon unseren Coherent-Bereich. Dieser Bereich ist später für die USB-Programmierung wichtig, da es der Bereich sein wird, in den die Hardware direkt schreiben kann. Dies wird nachher per MMU geregelt.

Für diesen Teil des Tutorials ist der "Heap Allocator" und natürlich unser "Systemspeicher" wichtig.

Hier wird . = ALIGN(65536); verwendet. Dies ist für den ARM64-Prozessor extrem wichtig, da er hier auf 64-KB-Blöcke zugreift.

Verwendung von Linker-Symbolen in C

Da diese Symbole erst beim Linken entstehen, müssen wir diese Symbole zunächst dem C-Compiler mitteilen, damit er weiß, was für Werte das sind. Der Linker verwendet diese Symbole als Adressen:

// Deklaration der Linker-Symbole
extern char __heap_start;
extern char __heap_end;
extern char __memory_start;
extern char __memory_end;

Im Code können wir diese dann wie folgt verwenden:

uintptr heap_start_addr = (uintptr)&__heap_start;
uintptr heap_end_addr   = (uintptr)&__heap_end;

Aufbau unserer Speicherverwaltung

Wir verwenden folgendes Schema:

[ Bereich 1: Header-Speicher (__heap_start bis __heap_end) ]
+--------------------------------------------------------+
| HeapBlock #1  | HeapBlock #2  | HeapBlock #3  | ...    | <- Hier liegen NUR die
| (status, size,| (status, size,| (status, size,|        |    Strukturen/Metadaten
|  position...) |  position...) |  position...) |        |    (Array / Kette)
+--------------------------------------------------------+

[ Bereich 2: Echter Nutzspeicher (__memory_start bis __memory_end) ]
+--------------------------------------------------------+
| Puffer 1 (z. B. 128 Bytes für malloc)                  |
+--------------------------------------------------------+
| Page 1 (z. B. exakt 64 KB ausgerichtet für palloc)     | <- 100% sauberer RAM,
+--------------------------------------------------------+    keine Header dazwischen!
| Page 2 (z. B. 64 KB für MMU-Tabelle)                   |
+--------------------------------------------------------+

Initiierung der Heap-Kartei

Bisher hat der Kernel keinerlei Informationen zum Speicher. Dies müssen wir ihm vorher mitteilen. Dazu erstellen wir einen ersten HeapBlock, der den gesamten Speicher als frei definiert.

typedef struct HeapBlock
{
    boolean status;           // FALSE = frei, TRUE = belegt
    u64 size;                 // Größe des verbleibenden Nutzspeichers
    u64 position;             // Startadresse des Nutzspeichers
    struct HeapBlock* next;   // Nächster Block (NULL = keiner)
} HeapBlock;

void init_heap(void)
{
    uintptr heap_ctrl_start = (uintptr)&__heap_start;
    uintptr data_ram_start  = (uintptr)&__memory_start;
    uintptr data_ram_end    = (uintptr)&__memory_end;

    // Der erste Verwaltungs-Knoten liegt im CONTROL-Bereich (__heap_start)
    HeapBlock* first_header = (HeapBlock*)heap_ctrl_start;

    first_header->status   = FALSE;                          // Frei
    first_header->position = data_ram_start;                 // Zeigt auf den Start von __memory_start
    first_header->size     = data_ram_end - data_ram_start;  // Der gesamte RAM-Bereich ist frei
    first_header->next     = NULL;
}

Speicher reservieren mit alloc()

Die einzelnen Heap-Blöcke sollen den Speicher verwalten. Sie werden bei jeder Speicheranforderung durchsucht, um zu prüfen, ob die Menge an Speicher verfügbar ist.

void* alloc(u64 size, u64 align)
{
    uintptr heap_ctrl_start = (uintptr)&__heap_start;
    uintptr heap_ctrl_end   = (uintptr)&__heap_end;
    uintptr data_ram_end    = (uintptr)&__memory_end;

    if (align < 8) {
        align = 8;
    }

    HeapBlock* block = (HeapBlock*)heap_ctrl_start;

    EnterCritical2();

    while (block)
    {
        if (!block->status)
        {
            u64 pos = block->position;
            u64 aligned_pos = (pos + align - 1) & ~(align - 1);
            u64 padding = aligned_pos - pos;

            if (block->size >= size + padding)
            {
                block->status = TRUE;

                if (!block->next)
                {
                    block->size = size + padding;
                    block->position = aligned_pos;

                    HeapBlock* next_header = block + 1;

                    if ((uintptr)(next_header + 1) < heap_ctrl_end)
                    {
                        block->next = next_header;
                        next_header->status   = FALSE;
                        next_header->next     = NULL;
                        next_header->position = aligned_pos + size;

                        if (data_ram_end > next_header->position) {
                            next_header->size = data_ram_end - next_header->position;
                        } else {
                            next_header->size = 0;
                        }
                    }

                    LeaveCritical();
                    return (void*)aligned_pos;
                }
                else
                {
                    block->position = aligned_pos;
                    LeaveCritical();
                    return (void*)aligned_pos;
                }
            }
        }
        block = block->next;
    }

    LeaveCritical();
    return NULL;
}

Speicher wieder freigeben (free())

void free(void* ptr)
{
    uintptr heap_start_addr = (uintptr)&__heap_start;
    HeapBlock* block = (HeapBlock*)heap_start_addr;

    EnterCritical2();
    while (block)
    {
        if ((void*)block->position == ptr)
        {
            block->status = FALSE;  // Als frei markieren
            LeaveCritical();
            return;
        }
        block = block->next;
    }
    LeaveCritical();
}

Memory Allocation (malloc())

void *malloc (size_t nSize)
{
    void *MemPos = alloc(nSize, 0x40); // 64-Byte Cache-Line Alignment
    if (MemPos == 0)
    {
        printf("malloc: Not enough memory available or heap is full\n");
        Stop();
    }
    return MemPos;
}

Spezial-Funktion palloc()

void *palloc (void)
{
    // Fordert 64 KB mit 64 KB Alignment an
    void *MemPos = alloc(PAGE_SIZE, PAGE_SIZE);
    if (MemPos == 0)
    {
        printf("palloc: Not enough memory available or heap is full\n");
        Stop();
    }
    return MemPos;
}

Den Code kannst du hier herunterladen: https://www.satyria.de/arm/sources/RPI4/C/speicher1.zip

Fazit

Nun sind wir der Herrscher über unseren Speicher und können beliebig darüber verfügen.