16.7 Firmware build és QEMU-betöltés munkafolyamat

16.7.1 Motiváció

A startup kód és a linker script elkészültével a firmware még mindig csak forrásfájlok és egy leírás halmaza. Ahhoz, hogy a kód ténylegesen fusson, végig kell menni egy olyan láncon, amely a C- és assembly-forrásokból gépi kódot, abból egy betölthető image-et, majd abból egy futó rendszert csinál. Bare-metal környezetben ezt a láncot nem egy operációs rendszer és nem egy disztribúciós csomagkezelő végzi el helyettünk: minden lépést kézzel, explicit módon kell megadni. Ez az alfejezet a teljes gcc-arm-none-eabi + qemu-system-arm munkafolyamatot építi fel, a fordítási kapcsolóktól a Makefile-ig és a GDB-vel összekötött debug-indításig. A cél, hogy egyetlen paranccsal reprodukálható legyen a firmware építése és futtatása, és hogy a hallgató pontosan lássa, hol keletkezik a flash-kép, mit tölt be a QEMU, és hogyan lehet megállítani a processzort a reset után.

16.7.2 Definíció és működés

Definíció (bare-metal build-folyamat): olyan eszközlánc-munkafolyamat, amely forrásfájlokból (*.c, *.s) tárgykódot, abból linkeléssel egy ELF futtatható fájlt, abból pedig objcopy-jal egy nyers bináris flash-képet állít elő, majd ezt egy célplatformra (itt QEMU mps2-an385) betölti és elindítja. A folyamat minden lépése determinisztikus és parancssorból vezérelhető.

A cross-toolchain neve arm-none-eabi-: arm a célarchitektúra, none a hiányzó operációs rendszer, eabi az Application Binary Interface. A host x86_64-es Ubuntu 24.04 LTS-en ez a csomag gcc-arm-none-eabi néven érhető el. A használt kapcsolók szerepét a következő táblázat foglalja össze.

Kapcsoló Szerep Megjegyzés
-mcpu=cortex-m3 cél CPU-profil Thumb-2 utasításkészlet, nincs NEON/VFP
-mthumb Thumb-2 kódgenerálás Armv7-M-en kötelező
-ffreestanding nincs hosted C-környezet nincs main-különlegesség, nincs implicit libc
-nostdlib libc és startup fájlok kihagyása saját Reset_Handler és main van
-nostartfiles csak a startup fájlok kihagyása a -nostdlib ezt is tartalmazza
-T lab-mps2.ld linker script memóriatérkép és szekciók
-Wl,-Map=... linker map szimbólumok és szekcióméretek
-O0 / -Os optimalizálás debug vs. méret

A linkelés bemenete a linker script (a065), amely megadja a FLASH és RAM tartományokat, valamint elhelyezi a .text, .data és .bss szekciókat. A -nostdlib következménye, hogy bizonyos fordító által generált segédfüggvények (például 64 bites osztás, lebegőpontos műveletek) nem lesznek automatikusan elérhetők; ilyenkor a libgcc-t explicit módon, -lgcc formában hozzá kell linkelni. A mostani példa nem igényel ilyet, de a kapcsoló ismerete később fontos.

A linkelés eredménye egy ELF fájl. Ez tartalmazza a szekciókat, a szimbólumtáblát és a program headereket. A QEMU -kernel kapcsolója ARMv7-M machine esetén a armv7m_load_kernel útvonalon tölti be a képet: .elf bemenetnél a program headerek fizikai (betöltési, LMA) címeit használja, .bin bemenetnél a fájlt egyszerűen a 0x00000000-tól másolja be. Mivel a .data LMA-ja a flashben, VMA-ja a RAM-ban van, mindkét út ugyanazt a memóriatartalmat adja: a .data kezdőértékei a betöltési képben vannak, és a startup másolja át őket. Ebben az alfejezetben a futtatás és a debug ugyanazt az .elf-et tölti be, így a kettő pontosan ugyanazt a képet látja; az .elf a GDB-nek a szimbólumokat is adja. A .bin a valódi eszköz flash-programozójának szóló termék, és QEMU-ban is futtatható.

A flash-kép előállítása objcopy-jal történik:

arm-none-eabi-objcopy -O binary lab-firmware.elf lab-firmware.bin

A -O binary a LOAD típusú szekciókat folytonos binárisba írja, a VMA-k helyett az LMA-k sorrendjében. Fontos, hogy a bináris első bájtjai a vektortábla első szavai legyenek: a kezdeti stack pointer és a Reset_Handler címe (Thumb módban a legalacsonyabb bit 1).

A QEMU indítási minta:

qemu-system-arm -machine mps2-an385 -cpu cortex-m3 \
    -kernel lab-firmware.elf -semihosting -nographic

A -machine mps2-an385 az ARM MPS2+ AN385 referenciakártyát modellezi, amelynek dokumentált memóriatérképén a flash 0x00000000-tól, a SRAM 0x20000000-tól indul. A pontos tartományhosszakat és a perifériacímeket a QEMU adott verziójának machine-leírása rögzíti; ezeket a linker scripttel össze kell hangolni, és a laborgyakorlat elején érdemes a QEMU forrásában vagy a -machine help kimenetében ellenőrizni. A -semihosting engedélyezi az ARM semihosting debug-csatornát, amelyen a firmware a host konzoljára írhat — ez itt az „első kimenet” ellenőrzésének eszköze, nem valódi periféria. A -nographic a soros konzolt a standard bemenetre/kimenetre köti. A példa firmware a kiírás után a semihosting SYS_EXIT hívásával maga állítja le a QEMU-t (kilépési kód 0 = boot ok, 1 = boot FAIL); ha egy firmware végtelen ciklusban marad, kilépni Ctrl-A, majd X kombinációval lehet.

A debug-indítás ugyanezt a képet, ugyanazzal a semihosting-beállítással állítja meg a reset után. A -semihosting itt is kötelező: a firmware bkpt 0xab-ot hajt végre, és semihosting nélkül a QEMU ezt nem kiírásként kezeli, hanem debugkivételként, így a boot ok megfigyelés debug alatt elveszne:

qemu-system-arm -machine mps2-an385 -cpu cortex-m3 \
    -kernel lab-firmware.elf -semihosting -S -gdb tcp::3333 -nographic

A -S hatására a CPU nem hajt végre utasítást, amíg a debugger nem engedi el. A -gdb tcp::3333 egy GDB szervert nyit a 3333-as TCP-porton. A csatlakozás gdb-multiarch-csal történik, mert a host x86-os GDB nem feltétlenül tartalmaz ARM-támogatást:

gdb-multiarch lab-firmware.elf \
    -ex "target remote :3333" \
    -ex "break main" \
    -ex "continue"

A debug session alatt a monitor info registers, info registers sp pc, x/4xw 0x20000000 parancsokkal ellenőrizhető a reset utáni állapot, a main elérése és a .bss nullázottsága. Egy további continue után a QEMU-t futtató terminálban ugyanúgy megjelenik a boot ok, mint a run célnál, és a QEMU a SYS_EXIT hívásnál kilép.

16.7.3 Kidolgozott példa

Munkakönyvtár: ~/arm-labor/cm/a067/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).

mkdir -p ~/arm-labor/cm/a067
cd ~/arm-labor/cm/a067

A példa három saját fájlból és két átvett fájlból áll. A linker script és a startup kód a II. rész kanonikus párja, változatlanul: a lab-mps2.ld a Linker script alapjai, a lab-startup.s a Startup kód alfejezetből jön (vektortábla: .vectors, belépési pont: Reset_Handler, szóhatárra igazított és ASSERT-tel ellenőrzött .data/.bss határok).

cp ~/arm-labor/cm/a065/lab-mps2.ld ~/arm-labor/cm/a066/lab-startup.s .

A main() semihosting-kiírással igazolja a .data másolását és a .bss nullázását, majd a SYS_EXIT hívással leállítja a QEMU-t. Az ellenőrzött objektumok között szándékosan vannak 1, 3 és 5 bájtosak is, hogy a .data és a .bss tartalma ne legyen eleve 4 bájt többszöröse:

/* lab-main.c — boot-ellenőrzés semihostinggal */
#include <stdint.h>

volatile uint32_t data_counter = 0x11223344u;   /* .data */
volatile uint32_t bss_counter;                  /* .bss  */

/* Ellenpróba: 1, 3 és 5 bájtos objektumok, hogy a .data és a .bss
   tartalma ne legyen eleve 4 bájt többszöröse. */
volatile uint8_t d1    = 0xA1;
volatile uint8_t d3[3] = { 0xB1, 0xB2, 0xB3 };
volatile uint8_t d5[5] = { 0xC1, 0xC2, 0xC3, 0xC4, 0xC5 };
volatile uint8_t z1, z3[3], z5[5];

static void sh_call(uint32_t op, const void *arg)
{
    register uint32_t r0 asm("r0") = op;
    register const void *r1 asm("r1") = arg;
    asm volatile ("bkpt 0xab" : "+r"(r0) : "r"(r1) : "memory");
}

static int boot_ok(void)
{
    if (data_counter != 0x11223344u || bss_counter != 0u) return 0;
    if (d1 != 0xA1 || z1 != 0) return 0;
    for (int i = 0; i < 3; i++) if (d3[i] != 0xB1 + i || z3[i] != 0) return 0;
    for (int i = 0; i < 5; i++) if (d5[i] != 0xC1 + i || z5[i] != 0) return 0;
    return 1;
}

int main(void)
{
    int ok = boot_ok();
    sh_call(0x04u, ok ? "boot ok\n" : "boot FAIL\n");      /* SYS_WRITE0 */
    sh_call(0x18u, (const void *)(ok ? 0x20026u : 0x20023u));
    /* SYS_EXIT: 0x20026 = ADP_Stopped_ApplicationExit (kilépési kód 0),
       0x20023 = ADP_Stopped_RunTimeErrorUnknown (QEMU: kilépési kód 1). */
    for (;;) { }
}
# lab-Makefile
CROSS   := arm-none-eabi-
CC      := $(CROSS)gcc
OBJCOPY := $(CROSS)objcopy
SIZE    := $(CROSS)size
OBJDUMP := $(CROSS)objdump

CPU     := -mcpu=cortex-m3 -mthumb
CFLAGS  := $(CPU) -ffreestanding -nostdlib -nostartfiles -O0 -g3 -Wall
LDFLAGS := $(CPU) -nostdlib -T lab-mps2.ld -Wl,-Map=lab-firmware.map -Wl,--gc-sections
QEMU    := qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -semihosting -nographic

OBJS := lab-startup.o lab-main.o

all: lab-firmware.elf lab-firmware.bin

lab-startup.o: lab-startup.s
    $(CC) $(CFLAGS) -c $< -o $@

lab-main.o: lab-main.c
    $(CC) $(CFLAGS) -c $< -o $@

lab-firmware.elf: $(OBJS) lab-mps2.ld
    $(CC) $(LDFLAGS) $(OBJS) -o $@

lab-firmware.bin: lab-firmware.elf
    $(OBJCOPY) -O binary $< $@

# run és debug ugyanazt a képet és ugyanazt a semihosting-beállítást használja
run: lab-firmware.elf
    $(QEMU) -kernel lab-firmware.elf

debug: lab-firmware.elf
    $(QEMU) -kernel lab-firmware.elf -S -gdb tcp::3333

size: lab-firmware.elf
    $(SIZE) $<

disasm: lab-firmware.elf
    $(OBJDUMP) -d $< > lab-firmware.dis

clean:
    rm -f lab-startup.o lab-main.o lab-firmware.elf lab-firmware.bin \
          lab-firmware.map lab-firmware.dis

A build és futtatás:

make -f lab-Makefile
make -f lab-Makefile run; echo "kilépési állapot: $?"

Kimenet a helyettesítő eszközlánccal (Clang + ld.lld, ugyanezzel a három forrással) és a Unicorn-alapú tools/cm-run.py Cortex-M3 emulátorral mérve, amely a hardverhez hasonlóan a vektortáblából indul, és a semihosting SYS_WRITE0/SYS_EXIT hívást kezeli:

boot ok
kilépési állapot: 0

Ha a .data másolás vagy a .bss nullázás hibás, a kimenet boot FAIL lesz, és a kilépési állapot 1; ez a beépített ellenőrzési feltétel. A debug cél ugyanezt a képet ugyanazzal a semihosting-beállítással indítja. GDB-vel a break main után info registers sp pc mutatja, hogy a stack pointer az _estack közelében van (a main prológusa már foglalhatott vermet), a PC pedig a main címén áll. Egy további continue után ugyanaz a boot ok jelenik meg, mint a run célnál.

16.7.4 Tipikus hibák és ellenpróbák

16.7.5 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Magyarázd el, miért ad a QEMU -kernel kapcsolója ugyanazt a memóriatartalmat .elf és .bin bemenettel, és miért használja ez az alfejezet mégis az .elf-et a futtatáshoz és a debughoz is. Térj ki az LMA/VMA különbségre és a szimbólumtábla szerepére.
  2. (implementációs, középszint — cél: rutinépítés) Egészítsd ki a lab-Makefile-t egy map céllal, amely a lab-firmware.map-ből kiírja a .data, .bss és .text szekciók kezdő- és végcímét, valamint egy verify céllal, amely ellenőrzi, hogy a lab-firmware.bin létezik és nem üres.
  3. (hibakeresési, középszint — cél: fejezetek összekapcsolása: Startup kód: .data másolása, .bss törlése) Távolítsd el a startup kódból a .data másoló ciklust, építsd újra a firmware-t, és GDB-vel (break main, majd x/1xw &data_counter) állapítsd meg, milyen értéket vesz fel a data_counter. Hasonlítsd össze a boot FAIL tünettel, és indokold, miért nem a linker script a hiba forrása.
  4. (mérési, alapszint — cél: rutinépítés) Mérd meg a make -f lab-Makefile size kimenetét -O0 és -Os mellett. Rögzítsd a .text, .data és .bss méreteket, és indokold, melyik szekció mérete változik és miért. Az értékeket jelöld mértnek, és tedd hozzá, hogy QEMU-emulációban a méret nem függ az emulációtól, csak a fordító kimenetétől.

16.7.6 Megoldások

1. megoldás — Segítség. A QEMU -kernel a betöltendő TARTALMAT konkrét fizikai címekre tölti — melyik címet használja az ELF-ből, és mit tartalmaz a .bin a 0x00000000-tól? A GDB viszont forráskódhoz és nevekhez köti a töréspontokat — melyik fájlformátum hordozza ezt?

1. megoldás — Teljes megoldás. A .bin a flash valós tartalmát tükrözi: az objcopy -O binary az LMA-k sorrendjében ír, így a .data inicializáló értékei ott vannak, ahol a valós eszköz flashje tartalmazná őket. Az .elf program headerei ugyanezeket a betöltési (fizikai) címeket hordozzák, és a QEMU ezekre tölti a szegmenseket, tehát a memóriatartalom ugyanaz. Ezért mindkét út helyes; a startup mindkét esetben a betöltési képből másol a RAM-ba. Az .elf emellett szimbólumtáblát és debuginformációt is tartalmaz, amire a GDB-nek szüksége van. Ha a futtatás és a debug ugyanazt az .elf-et tölti, kizárt, hogy a kettő két különböző build képét lássa; a .bin a valódi eszköz programozójának szól.

2. megoldás — Segítség. A linkermap soraiban a szekciónevek (.text, .data, .bss) sortörés elején jelennek meg — milyen egyszerű szöveges eszközzel szűrhetők ki ezek a sorok?

2. megoldás — Teljes megoldás. A map cél például grep -E '^\.(text|data|bss)' lab-firmware.map formában listázza a szekciókat; a verify cél test -s lab-firmware.bin feltétellel ellenőriz. A szekcióhatárokat a linkermap adja, így a hallgató összevetheti a linker scripttel.

3. megoldás — Segítség. Az a066 tárgyalja, mi történik, ha a .data másoló ciklus kimarad — a RAM-beli változó ekkor a FLASH-beli kezdőértékét kapja-e meg, vagy valami mást?

3. megoldás — Teljes megoldás. A .data másolás elhagyásával a data_counter a RAM-ban marad, értéke nem determinisztikus (jellemzően 0, mert a QEMU induláskor nullázza a SRAM-ot, de erre nem szabad építeni). A boot_ok() feltétele hamis lesz, a kimenet boot FAIL, a kilépési állapot 1. A linker script nem hibás: a _sidata/_sdata/_edata szimbólumok továbbra is definiáltak, csak a startup nem használja őket.

4. megoldás — Segítség. Az -O0 és -Os elsősorban a KÓDOT (utasítás-összevonás, méretoptimalizálás) érinti — ez melyik szekció méretét befolyásolja, és melyiket nem, mert azt a változók száma/típusa határozza meg, nem a fordítói szint?

4. megoldás — Teljes megoldás. -O0-nál a .text nagyobb, mert nincs utasítás-összevonás; -Os-nél kisebb. A .data és .bss mérete jellemzően nem változik, mert azokat a változók száma és típusa határozza meg. Az értékeket a size fejlécében a text, data, bss, dec oszlopok adják.

16.7.7 Rövid összafoglaló

A bare-metal build-folyamat forrásból ELF-en át nyers flash-képig vezet: a gcc-arm-none-eabi -mcpu=cortex-m3 -mthumb -ffreestanding -nostdlib kapcsolókkal állítja elő a tárgykódot, a linker script a memóriatérkép szerint helyezi el a szekciókat, az objcopy -O binary pedig a betölthető image-et adja. A QEMU -kernel az ELF szegmenseit a betöltési címükre tölti (.bin-t a 0x00000000-ra), a -S -gdb tcp::3333 pedig a reset utáni megállást és GDB-csatlakozást teszi lehetővé. A run és a debug ugyanazt a képet és ugyanazt a semihosting-beállítást használja, így ugyanaz a boot ok megfigyelés mindkettőben elérhető. A Makefile ezt a láncot egyetlen paranccsá szervezi.

Továbbhasznált definíciók: - bare-metal build-folyamat: forrás → tárgykód → ELF → objcopy bináris → QEMU-betöltés. - flash-kép (bináris image): az LMA-k sorrendjében előállított nyers gépi kód, amely a valós flash-tartalmat reprezentálja. - semihosting: ARM debug-csatorna (bkpt 0xab), amellyel a firmware a host konzoljára írhat; nem valódi periféria.

16.7.8 Források és ellenőrzési hiányok

Az alábbiak dokumentációs kiindulópontok (forrásállapot: nem megnyitott, lásd az Olvasói útmutató jelöléseit):

Ellenőrzési hiányok: a QEMU mps2-an385 pontos flash/SRAM címtartománya, a -semihosting kapcsoló támogatottsága az adott QEMU-verzióban, valamint az arm-none-eabi-gcc Ubuntu 24.04-es csomagverziója a laborban ellenőrizendő. A boot ok kimenetet és a 0-s kilépési állapotot a helyettesítő eszközlánccal (Clang + ld.lld) épített lab-firmware.elf-en, a Unicorn-alapú tools/cm-run.py emulátorban mértük; a qemu-system-arm alatti futtatás és a GDB-lépések ebben a munkafolyamatban nem futottak, a hallgató laborja validálja őket.