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.
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.binA -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 -nographicA -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 -nographicA -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.
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/a067A 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.disA 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.
run egy régi .bin-t tölt, a GDB pedig az új
.elf szimbólumaival dolgozik, így a töréspontok rossz címre
kerülnek. Ellenpróba: a run és a debug cél
ugyanazt az .elf-et töltse; ha .bin-t is
használunk, a Makefile függőségként mindig az aktuális
.elf-ből állítsa elő.-nostdlib, vagy fordítva, hiányzó
-lgcc: az első esetben a linker libc-szimbólumokat
keres; a második ritkább, de 64 bites műveleteknél
undefined reference to __aeabi_ldivmod tünetként
jelentkezik.KEEP(*(.vectors)) hiányzik, vagy a
--gc-sections kigyomlálja. Tünet: a QEMU a reset után
azonnal faultol, vagy a PC 0 körül marad. Ellenpróba:
arm-none-eabi-objdump -s -j .vectors lab-firmware.elf,
illetve a vektortábla-ellenőrző program. A
kanonikus lab-mps2.ld ASSERT-je a rossz helyre
került táblát már linkeléskor jelzi.bkpt 0xab debugkivételt vagy
HardFaultot okoz, vagy a QEMU csendben marad. Ellenpróba: a
-semihosting a run és a debug
parancsban is szerepeljen (a fenti Makefile ezért közös
QEMU változót használ), illetve GDB-vel a PC és az
xPSR vizsgálata a bkpt után.gdb helyett gdb-multiarch (vagy
arm-none-eabi-gdb), különben a target remote
nem ismeri fel a cél architektúrát.-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.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..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.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.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.
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.
Az alábbiak dokumentációs kiindulópontok (forrásállapot: nem megnyitott, lásd az Olvasói útmutató jelöléseit):
BKPT utasítás elsődleges
forrása.objcopy kézikönyv — a -O binary
és a szekciók LMA szerinti kiírásának leírása.-mcpu, -mthumb,
-ffreestanding, -nostdlib kapcsolók
jelentése.-kernel, -S, -gdb,
-semihosting és az mps2-an385 machine
viselkedése.SYS_WRITE0 (0x04)
műveletkód és a BKPT 0xAB konvenció.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.