Az előző alfejezetben (Linker script alapjai:
szekciók memóriába helyezése) láttuk, hogy a .data
szekció VMA-ja (futásidejű címe) RAM-ban van, míg az
LMA-ja (betöltési címe) a flashben. Ez a szétválás nem
esztétikai: a flash memória a Cortex-M-en tipikusan csak olvasható a
futó program számára, így a C nyelvű globális változók kezdőértéke egy
másik memóriarégióba kerül, mint ahol futásidőben lakniuk kell.
A C programozó ezt soha nem látja: azt írja, hogy
int counter = 42; /* inicializált globális */
int buffer[64]; /* nulla-inicializált globális */és elvárja, hogy a main() első utasításakor a
counter értéke 42, a buffer minden eleme 0
legyen. Ez az elvárás nem az ISA-ból, hanem a C runtime
szerződéséből következik. Az ISA csak annyit mond, hogy
bizonyos bájtok bizonyos címen adott értéket tartalmaznak — azt nem,
hogy ezt ki és mikor állítja be. Ezt a feladatot a startup
kód (startup code, néha reset handler) végzi el, mielőtt
bármilyen C függvény lefutna. Ebben az alfejezetben megírjuk a
.data másoló és a .bss nullázó ciklust, és
megmutatjuk, miért nélkülözhetetlen a linker script és a startup kód
összehangolása.
Startup kód (startup code): az a legalacsonyabb
szintű, kézzel írt assembly rutin, amely a main() meghívása
előtt lefut, és a C runtime előfeltételeit megteremti. A Cortex-M-en ezt
tipikusan a reset vektorba írt címre helyezzük (a vektortábla részleteit
lásd Reset és boot-folyamat: vektortábla), így a
processzor hardveres újrainduláskor automatikusan ide kerül
vezérlés.
A startup kód legalább három dolgot végez:
.data szekcióba..bss szekció nullázása — azok a
globális és statikus változók, amelyeket C-ben nem inicializáltunk, a C
standard szerint 0 értékkel kezdenek. Ehhez nem tárolunk 0-bájtokat a
flashben; csak a területet foglaljuk le, és futásidőben nullázzuk.main() meghívása — innen veszi át a
vezérlést a C világ.Két külön rutin, két különböző cél. Az első adatot mozgat; a második adat nélkül dolgozik (csak nulláz). Mindkettő ciklushatárait a linker scriptből exportált szimbólumok adják.
Képzeljünk el egy .data szekciót, amely 1 KiB-ot foglal
el a RAM-ban. Futásidőben a program írhat bele (hiszen ez RAM). Ha a
.data LMA-ját és VMA-ját ugyanarra a flash-beli címre
tennénk, akkor a counter = 43; utasítás egy flash-címre
próbálna írni — a Cortex-M-en ez vagy hard fault, vagy (rosszabb
esetben) csendben elveszik, és a globális változó minden írás után
visszaáll a kezdőértékre. Ezért van az AT> FLASH és a
> RAM kombináció.
A startup kód tehát a linker által kigondolt LMA→VMA leképezést valósítja meg futásidőben: bájtonként (szavanként) átmásolja a tartalmat a flash-beli helyéről a RAM-beli helyére.
A linker script SECTIONS blokkjában szimbólumokat
definiálhatunk úgy, hogy egy címkéhez a szekció aktuális helyét kötjük.
A Linker script alapjai kanonikus
lab-mps2.ld-jének vonatkozó része (a teljes scriptet ott
találod, ez az alfejezet változatlanul azt használja):
.data : ALIGN(4) { /* VMA: RAM, LMA: FLASH */
_sdata = .;
*(.data*)
. = ALIGN(4); /* a vég is szóhatár: a szavas másolás nem lép túl */
_edata = .;
} > RAM AT> FLASH
_sidata = LOADADDR(.data);
.bss (NOLOAD) : ALIGN(4) {
_sbss = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
A startup kód és a linker script közös szerződése: mind az öt
határ (_sidata, _sdata, _edata,
_sbss, _ebss) 4 bájtra igazított, és
_sdata <= _edata, _sbss <= _ebss. A
linker script ezt ASSERT-tel ellenőrzi, a startup kód pedig
erre a szerződésre építve dolgozhat szavanként.
Ezek a szimbólumok nem memóriacímet tartalmazó változók: maguk a szimbólumok léteznek, és a címük az, amit képviselnek. Assemblyben ezért így hivatkozunk rájuk:
ldr r1, =_sdataA ldr Rd, =symbol pszeudo-utasítás a linker relocation
során symbol címét tölti be Rd-be. Nincs
szükség [pc, #offset] kézi számolgatásra — a linker elvégzi
helyettünk.
Fontos: a startup assembly fájlban ezek a szimbólumok
külső (extern) szimbólumok, ezért nem kell
.global direktívával deklarálni őket (a linker script
globális szimbólumai bármelyik objektumból láthatók). Ha viszont egy
assembly vagy C fájl .extern _sdata-ként hivatkozik rájuk,
az is működik — a fordítás szempontjából az a fontos, hogy ne
definiáljuk újra.
Munkakönyvtár: ~/arm-labor/cm/a066/ —
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/a066
cd ~/arm-labor/cm/a066Az alábbi lab-startup.s a II. rész kanonikus
startup kódja: a vektortáblát (.vectors), a
Reset_Handler-t és a gyenge aliasokkal bekötött
Default_Handler-t tartalmazza. A párja a Linker script alapjai lab-mps2.ld-je;
a firmware build és a későbbi laborok ezt a két
fájlt másolják át.
/* lab-startup.s — a II. rész kanonikus startup kódja
* ISA: Thumb-2 / Armv7-M; Target: qemu-system-arm -machine mps2-an385
* Szerződés a lab-mps2.ld-vel: _sidata, _sdata, _edata, _sbss, _ebss
* mind 4 bájtra igazított (a linker script ASSERT-tel ellenőrzi). */
.syntax unified
.cpu cortex-m3
.thumb
.section .vectors, "a", %progbits
.align 2
.global __vectors
__vectors:
.word _estack /* 0x00: kezdeti SP (MSP) */
.word Reset_Handler /* 0x04: Reset (bit0 = 1) */
.word NMI_Handler /* 0x08: NMI */
.word HardFault_Handler /* 0x0C: HardFault */
.word MemManage_Handler /* 0x10: MemManage */
.word BusFault_Handler /* 0x14: BusFault */
.word UsageFault_Handler /* 0x18: UsageFault */
.word 0, 0, 0, 0 /* 0x1C-0x28: fenntartott */
.word SVC_Handler /* 0x2C: SVCall */
.word DebugMon_Handler /* 0x30: DebugMonitor */
.word 0 /* 0x34: fenntartott */
.word PendSV_Handler /* 0x38: PendSV */
.word SysTick_Handler /* 0x3C: SysTick */
.text
.thumb_func
.global Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
/* 1. .data másolása FLASH (LMA) -> RAM (VMA), szavanként.
Elöltesztelő ciklus, előjel nélküli összehasonlítással: üres
szekciónál egyszer sem fut, és a végcímen pontosan megáll. */
ldr r0, =_sidata
ldr r1, =_sdata
ldr r2, =_edata
1: cmp r1, r2
bhs 2f
ldr r3, [r0], #4
str r3, [r1], #4
b 1b
/* 2. .bss nullázása, szavanként. */
2: ldr r1, =_sbss
ldr r2, =_ebss
movs r3, #0
3: cmp r1, r2
bhs 4f
str r3, [r1], #4
b 3b
/* 3. main(); ha visszatérne, végtelen ciklus. */
4: bl main
5: b 5b
.size Reset_Handler, . - Reset_Handler
.thumb_func
.global Default_Handler
.type Default_Handler, %function
Default_Handler:
b .
.size Default_Handler, . - Default_Handler
/* Gyenge aliasok: amit a firmware nem definiál, az a Default_Handlerre fut. */
.macro lab_default_handler name
.weak \name
.thumb_set \name, Default_Handler
.endm
lab_default_handler NMI_Handler
lab_default_handler HardFault_Handler
lab_default_handler MemManage_Handler
lab_default_handler BusFault_Handler
lab_default_handler UsageFault_Handler
lab_default_handler SVC_Handler
lab_default_handler DebugMon_Handler
lab_default_handler PendSV_Handler
lab_default_handler SysTick_HandlerNézzük a szerződéseket és a részleteket:
4 bájtos granularitás és a végcím. A ciklusok
ldr/str szópárokkal dolgoznak, 4 bájtos
lépésközzel. Ehhez nem kell, hogy minden C-objektum mérete 4 többszöröse
legyen: a linker script a .data és a .bss
kezdetét és végét is szóhatárra igazítja, a köztes réseket és a végét
kitölti. Egy 1, 3 vagy 5 bájtos uint8_t objektum tehát
belefér a szerződésbe; a firmware build
lab-main.c-je pontosan ilyen objektumokkal ellenőrzi a
bootot. Ha a végcím mégsem lenne szóhatár, azt a linker
ASSERT-je már linkeléskor elutasítja.
Előjel nélküli, elöltesztelő ciklus. Mindkét ciklus
a törzs előtt vizsgál (cmp +
bhs), ezért üres szekciónál egyszer sem fut. A
bhs (unsigned higher or same) címekre való összehasonlítás:
a ciklus addig fut, amíg a mutató kisebb a végcímnél. Ez
nem egyenlőségvizsgálat. Egy cmp +
bne ciklus csak akkor állna meg, ha a mutató pontosan
elérné a végcímet, és egy nem 4 többszörös hossznál átlépné azt. Az
ellenpróbát lent mérjük.
A regiszterek megválasztása. Az r0..r3
regisztereket használjuk munka-regiszterként. A startup kód itt maga a
Reset_Handler, ahol még nincs értelmes programállapot,
ezért a main() hívása előtt szabadon használhatjuk a
caller-saved regisztereket. A main() hívásakor viszont már
betartjuk az AAPCS-t: nem feltételezzük, hogy r0..r3
érintetlen marad a visszatérés után — pont ezért nem használjuk őket a
main() előtti és utáni állapot összekapcsolására.
Ellenpróba: mi történik szerződésszegésnél? A
helyettesítő eszközlánccal (Clang + ld.lld, Unicorn-alapú
Cortex-M3 emulátor) három változatot építettünk ugyanabból a
lab-main.c-ből, amelyben 1, 3 és 5 bájtos
.data- és .bss-objektumok vannak:
| Linker script | Startup ciklus | Eredmény |
|---|---|---|
kanonikus (ALIGN(4) a végeken,
ASSERT) |
cmp + bhs |
boot ok |
a .data végéről kivett ALIGN(4),
ASSERT bekapcsolva |
– | a linkelés megáll: .data bounds not word aligned |
ugyanez, ASSERT nélkül
(_edata = 0x2000000d) |
cmp + bhs |
boot ok, de a ciklus a
0x2000000d–0x2000000f kitöltő bájtokat is
írja; ez csak azért ártalmatlan, mert a .bss itt szóhatáron
kezdődik |
ugyanez, ASSERT nélkül |
cmp + bne (a korábbi kiadás mintája) |
a mutató a 0x2000000c → 0x20000010
lépéssel átlépi a 0x2000000d végcímet, és a ciklus nem áll
meg: a RAM nagy részét felülírja, majd az emulátorban a forrásmutató a
kódterület végén túl olvasva fault-ot okoz |
A tanulság: a helyes viselkedést nem egyetlen elem adja, hanem a
szerződés két oldala együtt. Az igazítást a linker script garantálja és
ellenőrzi, a ciklus pedig a tartományvégre (bhs), nem az
egyenlőségre teszteli a mutatót.
A .bss nullázás sorrendje. A
.bss nullázást a .data másolás
után végezzük, mert a két szekció a RAM-ban egymást
követi (a linker script így rendezi el őket), és bár a másolás nem
érinti a .bss-t, a sorrend a szokásos konvenció. Ha
.bss előbb lenne, sem lenne hiba — a két rutin egymástól
független.
A
qemu-system-arm -machine mps2-an385 -kernel lab-firmware.elf -nographic
parancs futtatása után, közvetlenül a main() belépése
előtt:
| Regiszter | Érték (példa) | Jelentés |
|---|---|---|
r0 |
_sidata + (_edata − _sdata) |
a forrás-mutató a .data másolás után, a betöltési kép
vége |
r1 |
_ebss = _sbss + .bss
méret |
a .bss végére mutat |
r2 |
_ebss |
a ciklus felső határa |
r3 |
0 |
a .bss nullázás után |
SP |
a linker script által definiált _estack |
a verem teteje |
Ellenőrzés: a main() első sora után állítsunk be egy
töréspontot GDB-ben (break main), és olvassuk ki a
.data és .bss tartalmát. A .data
értékének meg kell egyeznie a flash-beli LMA-val; a .bss
minden bájtja 0.
Az mps2-an385 memóriatérképét a QEMU dokumentációja adja
meg; a konkrét címtartományok (a flash-oldali régió és a RAM régió)
QEMU-verziófüggők lehetnek, ezért ezeket a linker scriptben
kell rögzíteni, és a build során a map fájllal
ellenőrizni.
1. A szimbólumok elgépelése. Ha a linker scriptben
_sdata_ vagy _sData szerepel, de az assembly
_sdata-ra hivatkozik, a linker „undefined symbol” hibát ad.
Tünet: linkelési hiba, nem futásidejű. Ellenpróba:
nm lab-firmware.elf | grep -E '_s?data' — a szimbólumnak
meg kell jelennie.
2. A .data vagy .bss vége nem
szóhatár. Ha a linker script a záró . = ALIGN(4);
nélkül definiálja az _edata/_ebss szimbólumot,
a szavas ciklus a szekció végén túl is ír. A bhs ciklus
legfeljebb 3 bájttal lépi túl a végcímet, ami a következő szekció elejét
írhatja felül; egy bne ciklus meg sem áll. Tünet: a
.bss első bájtjai felülíródnak, vagy a boot fault-tal
elszáll. Ellenpróba: a kanonikus scriptben az ASSERT ezt
linkeléskor jelzi; kézzel nm -n lab-firmware.elf — az
_edata és az _ebss értéke legyen 4-gyel
osztható.
3. A .bss nullázás kimarad. A C kód
lefut, de egy „nullázatlan” globális véletlenszerű értéket tartalmaz. Az
mps2-an385 esetében a SRAM tartalma hardver-indításkor nem
garantáltan 0 — ezért a C standard megköveteli, hogy a startup kód
nullázza. Tünet: a buffer elemei nem 0-k, vagy a
counter kezdőértéke nem az, amit a C kód deklarált.
Ellenpróba: állítsuk be a .bss régióját előzetesen
nem-nulla értékre (QEMU monitorból vagy egyéb debug interfészből), majd
futtassuk — a startup után minden 0-nak kell lennie.
4. A _sidata és _sdata
felcserélése. A copy ciklus a flash-ből RAM-ba másol, nem
fordítva. Ha tévedésből _sdata a forrás és
_sidata a cél, a ciklus a flash-beli betöltési képre próbál
írni. Valódi flash-es mikrokontrolleren ez tipikusan BusFault/HardFault
vagy csendben hatástalan írás. A QEMU mps2-an385-ön viszont
a FLASH nevű régió valójában írható SSRAM (memóriatérkép), ezért ott nincs
fault: a ciklus csendben felülírja a betöltési képet a RAM tartalmával,
és a .data változók rossz kezdőértékkel indulnak. Tünet
QEMU-ban: boot FAIL, kivétel nélkül.
5. A main() bl helyett
b. A b main nem menti a visszatérési
címet, és nem állítja be az LR-t. A main
visszatérésekor a processzor egy véletlenszerű címre ugrik. Tünet: a
main visszatérése utáni viselkedés kaotikus. Ellenpróba:
objdump -d lab-firmware.elf | grep -A20 '<Reset_Handler>' | grep main
— a main hívásnak bl-nek kell lennie.
1. (magyarázó, alapszint — cél: megértés-ellenőrzés)
Magyarázd el, miért nem oldható meg a .data inicializálás
pusztán a linker scripttel, holott a linker ismeri a .data
VMA-ját és LMA-ját is. Mi az a lépés, amit futásidőben kell megtenni, és
miért nem „elég okos” ehhez a linker?
2. (implementációs, középszint — cél: rutinépítés)
Módosítsd a startup rutint, hogy 1 bájtos granularitású
legyen (a .data és .bss méretének 4-gyel való
oszthatósága nélkül is működjön). Gondoskodj róla, hogy a ciklusok előbb
a maradék 1–3 bájtot másolják/nullázzák, majd az igazított 4 bájtos
blokkokat. Ellenőrizd, hogy a rutin továbbra is helyes, ha a méret 0,
vagy ha pontosan 4.
3. (hibakeresési/mérési, elmélyült — cél: fejezetek
összekapcsolása: Memory-mapped I/O
alapelve) A
qemu-system-arm -machine mps2-an385 célon futtasd a startup
kódot úgy, hogy a main() helyett egy olyan függvény
hívódjon, amely kiírja a .data és .bss
tartalmát UART-on (a memory-mapped UART részleteit lásd Memory-mapped I/O alapelve). Készíts egy kis
szkriptet, amely megváltoztatja a _sidata-t és a
_sdata-t felcserélő verziót, és figyeld meg, mi történik a
bl main után. Gyűjtsd össze, milyen regiszterértékek és
kivétel-típusok jelennek meg.
1. megoldás — Segítség. A linker egy FORDÍTÁSI IDEJŰ eszköz — fut-e még, amikor a programnak ténylegesen másolnia kellene a bájtokat a flashből a RAM-ba?
1. megoldás — Teljes megoldás. A linker statikus
időben dolgozik, és csak a címeket ismeri. A .data LMA-ja a
flashben van, VMA-ja a RAM-ban. A flash tartalma a programozás után fix;
a RAM tartalma futásidőben alakul. A linker nem tudja „átmásolni” a
bájtokat, mert a program nem fut, amikor a linker dolgozik. A másolást
egy futó processzornak kell elvégeznie — ez a startup kód feladata. Az
ISA csak a címzést és az utasításokat adja; a programozó (vagy a C
runtime implementációja) dönti el, hogy mit másol hova.
2. megoldás — Segítség. Gondolkodj két fázisban: előbb egy szavas (4 bájtos) ciklus addig fut, amíg még BELEFÉR egy teljes szó a maradék tartományba, utána egy bájtos ciklus takarítja el a maradékot — melyik feltétel dönti el, hogy a szavas ciklus mikor áll le?
2. megoldás — Teljes megoldás. A bájt-granularitású változat:
/* .data másolás bájt-granularitással */
ldr r0, =_sidata
ldr r1, =_sdata
ldr r2, =_edata
/* először 4-gyel osztható méretig szó, majd maradék bájt */
b data_w_check
data_w_copy:
ldr r3, [r0], #4
str r3, [r1], #4
data_w_check:
add r3, r1, #4
cmp r3, r2
bls data_w_copy /* előjel nélkül: r1 + 4 <= r2 */
data_b_copy:
cmp r1, r2
bhs data_done
ldrb r3, [r0], #1
strb r3, [r1], #1
b data_b_copy
data_done:A .bss nullázás hasonlóan: előbb str
szavakkal amíg a mutató + 4 ≤ _ebss, majd strb
a maradékra. Ez a megoldás akkor is helyes, ha a méret 0 (mindkét
elsődleges ciklus üresen fut le), vagy pontosan 4 (a bájtos rész azonnal
kilép). A lényeg: a feltételek előjel nélküliek (bls,
bhs, címekre ez a helyes), és a bájtos ciklus is
tartományvégre tesztel, így a mutató nem csúszhat túl a végen. A
kanonikus scriptben erre nincs szükség, mert az ALIGN(4) és
az ASSERT a szavas változatot is biztonságossá teszi; a
bájtos farokkezelés akkor kell, ha a linker scriptet nem mi írjuk.
3. megoldás — Segítség. Ha a forrás- és céltartomány-pointer felcserélődik, a program egy RAM-címről MÁR helyesen olvas, de a tárolás célja megváltozik — mi van a flash-címtartomány írási jogosultságával?
3. megoldás — Teljes megoldás. A
_sidata/_sdata felcserélése esetén a
ldr r3, [r0], #4 utasítás egy RAM-címről olvas (ez még
működik), majd a str r3, [r1], #4 a betöltési kép
(FLASH nevű régió) címére ír. A kimenetel a céltároló
tulajdonságán múlik, nem az utasításon. Valódi flash-es eszközön a
flash-vezérlő az ilyen írást elutasítja vagy figyelmen kívül hagyja; az
előbbi BusFault-ként, eszkalálva HardFault-ként jelenik meg. A QEMU
mps2-an385-ön a FLASH régió írható SSRAM,
ezért az írás csendben sikerül: a betöltési kép
felülíródik, a RAM-beli .data pedig nem kapja meg a
kezdőértékét, és a kimenet boot FAIL lesz kivétel nélkül.
Ennek ellenőrzésére GDB-vel érdemes a str utasítás után
megállni és a RAM- és a betöltési kép tartalmát összevetni. A gyűjtött
regiszterértékek közül a legfontosabb a PC a hiba után (a
fault handler címe) és az LR (EXC_RETURN
érték), amely a stack frame típusát jelzi.
A startup kód felelős a C runtime előfeltételeiért: a
.data szekciót a flash-beli LMA-ból a RAM-beli VMA-ba
másolja, a .bss szekciót nullázza, majd a
main()-t hívja. A ciklushatárok a linker scriptből
exportált _sidata, _sdata,
_edata, _sbss, _ebss szimbólumok.
A tipikus hibaforrások a szimbólum-elgépelés, a 4 bájtos igazítás
megsértése, a .bss kihagyása, és a másolási irány
fordítottja. A kanonikus pár (lab-mps2.ld +
lab-startup.s) szerződése: szóhatárra igazított határok,
amelyeket a linker ASSERT-tel ellenőriz, és elöltesztelő,
előjel nélküli (bhs) ciklusok, amelyek üres szekciónál nem
futnak, és a végcímen túl nem írnak.
Továbbhasznált definíciók: - startup
kód: assembly rutin, amely a main() előtt a C
runtime előfeltételeit biztosítja (.data másolás,
.bss nullázás). - LMA és VMA: betöltési és
futásidejű cím; a .data esetében a LMA flashben, a VMA
RAM-ban van. - .bss szekció:
nulla-inicializált globális/statikus változók szekciója; tartalmát
futásidőben nullázni kell.
A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.
ldr/str immediate offset) itt ellenőrizhető.
Ellenőrizve: a link él (a fejezetszám és a kiadásbetű
az Arm-oldalak közös korlátja miatt nem ellenőrzött).as kézikönyv — a .syntax unified,
.cpu, .thumb direktívák és a
ldr Rd, =symbol pszeudo-utasítás pontos szemantikája.
Tartalmilag ellenőrizve: a jelenlegi kiadás binutils
2.47.ld kézikönyv — a linker script SECTIONS,
LOADADDR, AT> szintaxis, valamint az
exportált szimbólumok viselkedése. Ugyanaz a binutils 2.47-es
kiadás.mps2-an385 machine memóriatérképe és
a betöltési konvenciók (-kernel opció). Tartalmilag
ellenőrizve: a master ág dokumentációja (közös
verziókorlát).Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is
ténylegesen fennmaradnak: a kanonikus
lab-startup.s-t és az ellenpróba-táblázat mind a négy sorát
a helyettesítő eszközlánccal (Clang + ld.lld) és a
Unicorn-alapú tools/cm-run.py Cortex-M3 emulátorral
futtattuk, nem GNU eszközökkel és nem qemu-system-arm-mal.
A fennmaradó tételek: az mps2-an385 pontos
flash-/RAM-címtartománya és a -kernel alapértelmezett
betöltési címe az aktuális QEMU-verziótól függ; ezt a hallgatónak a
saját laborkörnyezetében kell a linker script és a map fájl alapján
rögzítenie. A GNU toolchain csomagolt verziója (Ubuntu 24.04) a
reprodukálható baseline, de a konkrét ld- és
as-kiadás viselkedése apró részletekben eltérhet — érdemes
a nm, readelf -S, objdump -d
kimenetét a build során rendszeresen ellenőrizni.