Előfeltétel a II. részhez: a Cortex-M eszközlánc
(gcc-arm-none-eabi, binutils-arm-none-eabi,
qemu-system-arm) telepítése és smoke tesztje a Labor-alapkörnyezet alfejezet „A II. rész
eszközlánca” szakaszában. Az I. rész aarch64-linux-gnu-
eszközlánca nem a Cortex-M eszközlánca, ezért a II. rész példái előtt a
~/arm-labor/cm/a006/ könyvtárban a smoke tesztnek sikeresen
le kell futnia (Cortex-M smoke: OK, kilépési állapot
0). A II. rész minden laborja a
~/arm-labor/cm/ ágon, alfejezetenként külön könyvtárban
dolgozik.
Az I. részben megszoktuk, hogy a program _start-ból
indul, a kimenetet write rendszerhívással kérjük, a címeink
virtuálisak, és a kernel gondoskodik a lapozásról, a pufferelésről, a
hibajelzésről. A Cortex-M-en ugyanaz a számlálós ciklus fut le, de a
write helyett egyetlen STRB utasítás ír egy
periféria regiszterébe, és nincs kernel, amely elkapná a hibát. Ez az
alfejezet rendszerezi, mi tűnik el az AArch64 Linux-userspace modellből,
mi marad meg, és miért nem lehet a két oldalt mechanikusan egymásra
másolni. A cél nem a teljes Cortex-M firmware megírása, hanem annak
megértése, hogy a „gép” fogalma profilonként változik: a regiszterek és
a cikluslogika átvihető, a futtatási szerződés nem.
A-profile (Application profile): olyan architektúraprofil, amelyet összetett operációs rendszerek alatti alkalmazásokhoz terveztek. Az ARMv8-A AArch64 végrehajtási állapota ide tartozik; jellemzője az MMU, a többszintű kivételkezelés (exception level, EL0–EL3), a virtuális memória és a gazdag rendszerhívási felület.
M-profile (Microcontroller profile):
mikrokontrolleres beágyazott feladatokra szabott profil. A könyv II.
része az Armv7-M architektúrát és a Cortex-M3 referenciamagot használja.
Thumb-2 utasításkészlet, NVIC (Nested Vectored Interrupt Controller),
opcionális MPU, nincs MMU. Az architektúra nem zárja ki
az operációs rendszert: Cortex-M-en gyakori RTOS-ek (FreeRTOS, Zephyr,
RTX stb.) is futnak, saját ütemezővel és SVC-alapú
szolgáltatáshívással. Az, hogy a könyv II. részének laborjaiban nincs
operációs rendszer, a választott bare-metal tanulási
modell sajátossága, nem M-profile architektúra-követelmény.
Bare-metal: operációs rendszer nélküli
programfuttatás, amelyben a program saját maga biztosítja az indulási
környezetet: a verem beállítását, az inicializált adatok másolását, a
.bss törlését, majd a fő rutin meghívását. Reset után a
hardver a vektortábla első bejegyzéséből (a kezdeti
veremmutató értékéből) tölti be az SP-t, majd a
második bejegyzésben található resetkezelő címére
ugrik; ez utóbbi a tényleges belépési pont (lásd Reset és boot-folyamat: vektortábla).
Thread mód és Handler mód: az M-profile egyszerűsített privilege-modellje. A Thread módban fut a normál program, amely lehet privileged vagy unprivileged. A Handler módban futnak a kivételkezelők és megszakításkezelők; ez mindig privileged. Nincs EL0–EL3 hierarchia: a hardver csak azt különbözteti meg, hogy éppen kivételkezelés történik-e.
Memory-mapped I/O: olyan perifériakezelési modell,
amelyben a periféria regiszterei a normál memóriacím-térben jelennek
meg. A program STR/LDR utasításokkal éri el
őket; nincs külön IN/OUT utasítás, mint egyes
más architektúrákon. A Cortex-M-en a perifériák fix fizikai címeken
vannak, és a programozott cím közvetlenül a buszra kerül.
A profilváltás négy dolgot szüntet meg vagy változtat meg alapvetően. Az első három architektúraszintű különbség — az M-profile maga zárja ki vagy alakítja át ezeket; a negyedik, mint alább kiderül, nem architektúra-korlát, hanem a könyv laborjának választása.
1. Nincs MMU. Az AArch64 Linux alatt a program virtuális címeket használ, amelyeket az MMU lapozási táblákon keresztül képez le fizikai címekre. A Cortex-M-en nincs lapozás: a programban szereplő cím az a fizikai cím, amely a memóriabuszra kerül. Ez egyszerűbbé teszi a címszámítást, de elveszik a virtuális memória nyújtotta izoláció, a laphiba-alapú memóriakezelés és a klasszikus ASLR. Az M-profile opcionális MPU-ja (Memory Protection Unit) tud védelmet adni régiókra, de ez nem lapozás, és nem hoz létre virtuális címtérképet. Ez architektúraszintű tény: az M-profile sosem ad MMU-t.
2. Nincs operációs rendszer-szintű
syscall-szerződés. Az AArch64-en a write
rendszerhívás az SVC #0 utasítással kerül a kernelbe, amely
ellenőrzi az argumentumokat, a fájlleírót, a puffert, majd a megfelelő
illesztőprogramot hívja. Az M-profile architektúrájában az
SVC utasítás és a hozzá tartozó SVCall kivétel (a
vektortábla 11. bejegyzése, lásd a079)
létezik — ezt használják például az RTOS-ek
privilégium-váltáshoz —, de ebben a bare-metal laborban
nincs operációs rendszer, amely kiszolgálná: nincs X8-hoz
hasonló egységes syscall-szám-konvenció, nincs kernel-oldali pufferelés
vagy blokkolás. A kimenet ezért egy perifériaregiszter közvetlen írása,
például a UART adatregiszterébe. A hiba detektálása sem automatikus: ha
a periféria nem tudja fogadni az adatot, azt a programnak kell
ellenőriznie egy állapotregiszterből.
3. Nincs exception level-hierarchia. Az AArch64-en az alkalmazás EL0-ban, a kernel EL1-ben fut; a kivételkezelés szintjei szigorúan elválnak. Az M-profile-on csak Thread mód és Handler mód van. A Handler mód mindig privileged, a Thread mód lehet unprivileged, de nincs négyfokozatú skála. Ez egyszerűbb, de kevesebb izolációs lehetőséget ad — és ez is architektúraszintű tény, nem labor-választás.
4. Nincs operációs rendszer — ebben a laborban. Ez, az előző hárommal szemben, nem M-profile architektúra-korlát: Cortex-M-en kereskedelmi és nyílt forráskódú RTOS-ek (FreeRTOS, Zephyr, RTX stb.) is futnak, saját ütemezővel és szál- vagy folyamatmodellel. A jelen laborban nincs ütemező, nincs folyamat, nincs fájlrendszer, nincs dinamikus linker — ez a könyv II. részének pedagógiai döntése, a bare-metal modell egyszerűsége miatt. A program a reset vektorból indul, és jellemzően soha nem „tér vissza”: vagy végtelen ciklusban fut, vagy alacsony fogyasztású módba kerül. A megszakítások jelentik a valódi párhuzamosságot, nem a folyamatok vagy szálak.
A Cortex-M-et alacsony fogyasztásra, kis chipterületre és determinisztikus válaszidőre tervezték. Egy tipikus beágyazott rendszer egyetlen vezérlési feladatot lát el: motorvezérlés, szenzorolvasás, kommunikációs protokoll kezelése. Az MMU hiánya nem hátrány, hanem költségcsökkentő egyszerűsítés: nincs szükség lapozási táblákra, TLB-re, és a címfordítás elmarad. A megszakítás-latencia kiszámíthatóbb, mert nincs kernel, amely előbb ütemezne. A perifériák közvetlenül címezhetők, így a válaszidő néhány tíz ciklus nagyságrendű lehet. Ezért a Cortex-M a valós idejű, erőforrás-korlátos rendszerek tipikus választása.
A jó hír, hogy az I. rész jelentős része átvihető. A
load/store architektúra elve ugyanaz: a memóriát
LDR/STR típusú utasítások érik el, a műveletek
regisztereken folynak. A ciklusszervezés, a számláló
dekrementálása és a feltételkód-alapú elágazás szemantikailag azonos. A
bitmanipuláció (maszkolás, shift, logikai műveletek)
nagy része megmarad, még ha a mnemonicok és az immediate-kódolási
korlátok el is térnek. A stack használat és a hívási
konvenció is megmarad, de az M-profile saját AAPCS-változattal
rendelkezik, és két veremmutatót használ (MSP, PSP). Az
utasítások szemantikája gyakran átvihető, a
kódolás és a regiszternevek azonban nem.
Ez az utolsó mondat a II. rész kulcsa: a C-driverkódok mögött mindig egy assembly-szintű hívási szerződés áll, és ez Cortex-M-en nem az AAPCS64, hanem az AAPCS32. Mielőtt a vektortáblával, a startup kóddal és a perifériákkal foglalkoznánk, ezt a szerződést és a Thumb hívási mechanikáját rendezzük el, majd ugyanazt a C-függvényt mindkét ISA-n megírjuk.
Az alábbi táblázat az I. részben megismert AAPCS64-et (Az AAPCS64 regiszterszerződés) állítja párba a Cortex-M-en érvényes AAPCS32-vel (Armv7-M, Thumb-2):
| Szerep | AAPCS64 (AArch64) | AAPCS32 (Cortex-M, Thumb-2) |
|---|---|---|
| egész/mutató argumentumok | X0–X7, a többi a veremben |
R0–R3, a többi a veremben |
| 64 bites argumentum | egy Xn |
páros–páratlan regiszterpár (R0:R1 vagy
R2:R3), a veremben 8 bájtra igazítva |
| visszatérési érték | X0 (128 bites: X0:X1) |
R0; 64 bites: R0:R1 |
| hívó által mentett (scratch) | X0–X17 |
R0–R3, R12 (IP: a linker
által beszúrt veneer is használhatja) |
| hívott által mentett | X19–X28, D8–D15
alsó fele |
R4–R11 (platformfüggő részletekkel, lásd
lent) |
| link register | X30 |
R14 (LR) |
| veremmutató | SP, 16 bájtra igazítva |
SP (Cortex-M-en MSP vagy
PSP); nyilvános interfészen 8 bájtra
igazítva, közben legalább 4-re |
| keretmutató | X29 |
nem kötelező; a GCC Thumb-kódban R7-et használja
keretmutatónak, ARM-állapotban R11-et |
| platformregiszter | X18 |
R9 |
Az R4–R11 platformfüggő
részletei. Az AAPCS32 szerint mind a nyolc regiszter
callee-saved, de kettőnek a platform külön szerepet adhat. Az
R9 a platformregiszter: egy platform
kijelölheti statikus bázisnak (SB, pozíciófüggetlen adat) vagy szálhelyi
tárolónak. Ilyenkor a függvények nem használhatják szabadon. Ha a
platform semmihez nem rendeli, közönséges callee-saved regiszter. Az
R7 a GCC Thumb-kódjában keretmutató, ha a keretmutató be
van kapcsolva (-fno-omit-frame-pointer); egy
assembly-függvény ilyenkor is használhatja, ha visszaállítja, de a
hibakereső veremvisszakövetése rá épül. Bare-metal Cortex-M3
laborjainkban egyik sincs lefoglalva, de egy RTOS vagy más fordító ezt
másként döntheti el: a szerződés részleteit mindig a platform
dokumentációja rögzíti.
A kivételbelépés és a hívási szerződés. A Cortex-M
kivételbelépéskor a hardver pontosan a hívó által mentett halmazt menti
a verembe (R0–R3, R12,
LR, plusz PC és xPSR). Ezért
írható a megszakításkezelő közönséges C-függvényként (ISR írása): az R4–R11
megőrzése a fordító dolga, ugyanúgy, mint bármely más függvényben.
A 8 bájtos igazítás. Nyilvános interfészen, vagyis
minden olyan hívási ponton, ahol a hívott fél más fordítási egységből
vagy külső kódból származhat, az SP 8-cal osztható kell
legyen. Ez a 64 bites adatok (LDRD/STRD,
double, uint64_t) miatt kell. Egy függvényen
belül az SP átmenetileg csak 4-gyel osztható is lehet. A
gyakorlati következmény: a prológus páros számú
regisztert mentsen (például push {r4, lr}; öt regiszter
helyett, mint a push {r4-r7, lr}, inkább hat:
push {r3-r7, lr}), különben a következő bl már
igazítatlan veremmel hív.
BL címke: a hívás az
LR-be a visszatérési címet írja, bit0 =
1-gyel, mert a hívó Thumb-állapotban van; a visszatérés erre a
címre ugrik.BLX Rm: regiszteres (közvetett) hívás,
például függvénymutatón keresztül. Az Rm bit0-jának 1-nek
kell lennie; a Cortex-M nem ismer ARM-állapotot, ezért egy páros cím
INVSTATE UsageFaultot okoz.BX LR. Ha a függvény más
függvényt is hív, és ezért az LR-t mentenie kell, a
prológus push {..., lr}, az epilógus pedig
pop {..., pc}: a mentett cím közvetlenül a
PC-be töltődik. Levélfüggvény az LR-t nem
menti..thumb_func (a .type név, %function mellett)
jelzi, hogy a szimbólum Thumb-függvény. Ettől lesz a szimbólum értéke
páratlan, és ezt a páratlan értéket kapja minden, ami a címét veszi:
függvénymutató, BLX-cél, vektortábla-bejegyzés (vektortábla). A jelölés hiányában a BL
még működik (a linker közvetlen ágat fordít), de a cím alapú hívás páros
címet kap, és hibát okoz.Az AArch64-hez képest a mechanika tehát ugyanaz (a BL az
LR-be ír, a visszatérés az LR-en át megy), két különbséggel: a
visszatérési cím bit0-ja állapotjelző, és a nem levélfüggvény
pop {pc}-vel is visszatérhet.
Munkakönyvtár: ~/arm-labor/cm/a061/. A
példa egyetlen C-szerződést valósít meg mindkét ISA-n, és
ugyanaz a teszthajtó ellenőrzi mindkettőt. A teszthajtó
a függvény eredménye mellett a hívási szerződést is méri: A64-en az a029 lab_abi_call teszthívójával,
Thumb-2-n annak itt bevezetett párjával, a
lab_abi_call32-vel. A Thumb-futtatáshoz a Linker script alapjai és a Startup kód kanonikus párja kell. Ha azok még
nincsenek meg, a Thumb-oldali futtatás az alfejezetek után végezhető el;
a fordítás és a disassembly addig is ellenőrizhető.
mkdir -p ~/arm-labor/cm/a061 && cd ~/arm-labor/cm/a061
cp ~/arm-labor/a64/a029/lab-abi-call.h ~/arm-labor/a64/a029/lab-abi-call.S .
cp ~/arm-labor/cm/a065/lab-mps2.ld ~/arm-labor/cm/a066/lab-startup.s . # a Thumb-futtatáshozA közös szerződés:
/* lab-sum.h — a közös C-szerződés mindkét ISA-ra */
#ifndef LAB_SUM_H
#define LAB_SUM_H
#include <stdint.h>
/* p[0..n-1] összege modulo 2^32; n == 0 esetén 0. */
uint32_t lab_sum_u32(const uint32_t *p, uint32_t n);
#endifA64 (AAPCS64: x0 = p, w1 =
n, eredmény w0):
// lab-sum-a64.s — A64, AAPCS64: x0 = p, w1 = n; visszatérés w0-ban
.text
.global lab_sum_u32
.type lab_sum_u32, %function
lab_sum_u32:
mov w2, #0 // összeg: caller-saved w2, nincs mentés
cbz w1, 2f
1: ldr w3, [x0], #4 // post-index: p++
add w2, w2, w3
subs w1, w1, #1
b.ne 1b
2: mov w0, w2
ret // visszatérés az X30-ban kapott címre
.size lab_sum_u32, . - lab_sum_u32
.section .note.GNU-stack,"",%progbitsThumb-2 (AAPCS32: r0 = p, r1 =
n, eredmény r0). A változat szándékosan
callee-saved regisztereket (r4, r5) használ,
hogy a mentés és a páros push látható legyen:
@ lab-sum-t32.s — Thumb-2, AAPCS32: r0 = p, r1 = n; visszatérés r0-ban
.syntax unified
.thumb
.text
.global lab_sum_u32
.thumb_func @ a szimbólum Thumb-függvény: címének bit0-ja 1
.type lab_sum_u32, %function
lab_sum_u32:
@ callee-saved regiszterek; páros darabszám: SP 8-ra igazított marad
push {r4, r5}
movs r4, #0
cbz r1, 2f
1: ldr r5, [r0], #4 @ post-index: p++
adds r4, r4, r5
subs r1, r1, #1
bne 1b
2: mov r0, r4
pop {r4, r5}
bx lr @ visszatérés: LR bit0 = 1, Thumb marad
.size lab_sum_u32, . - lab_sum_u32A Thumb-2 ABI-teszthívó az R4–R11-be ismert
mintát tölt, a hívás előtti SP-t egy statikus horgonyba
(lab_abi32_sp) menti, BLX-szel meghívja a
függvényt, majd ellenőrzi a mintákat és az SP-t. Az A64-es
párhoz hasonlóan (a029) saját keretét csak a
horgonyból állítja vissza (mov sp, r3), így a hívott által
elrontott SP-ről sem a &result-ot, sem a
mentett regisztereket és az LR-t nem olvassa. A horgony
miatt nem reentráns: egymásba ágyazott hívásnál a belső
hívás nem indítja el a függvényt, hanem LAB_ABI32_BUSY
hibabitet ad. Ő maga 10 regisztert ment (40 bájt), így a hívás pontján
az SP 8-ra igazított marad:
/* lab-abi-call32.h — AAPCS32 ABI-teszthívó Thumb-2-re (a lab_abi_call párja). */
#ifndef LAB_ABI_CALL32_H
#define LAB_ABI_CALL32_H
#include <stdint.h>
typedef void (*lab_abi_fn32)(void);
/* hibamaszk: bit 0..7 = R4..R11 sérült, bit 8 = SP nem a hívás előtti */
#define LAB_ABI32_R4 (1u << 0)
#define LAB_ABI32_SP (1u << 8)
#define LAB_ABI32_BUSY (1u << 9) /* újrabelépés: fn nem hívódott meg */
/* fn(a0, a1) hívása úgy, hogy előtte R4–R11 ismert mintát kap; visszatérés
után ellenőrzi a mintákat és az SP-t. *result = a hívott R0-ja.
NEM reentráns: a hívás előtti SP-t egyetlen statikus horgony őrzi;
egymásba ágyazott hívásnál a belső LAB_ABI32_BUSY-t ad, *result = 0. */
uint32_t lab_abi_call32(lab_abi_fn32 fn, uint32_t a0, uint32_t a1, uint32_t *result);
#endif@ lab-abi-call32.s — AAPCS32 megőrzési teszthívó (Thumb-2, Armv7-M)
@ r0 = fn, r1 = a0, r2 = a1, r3 = &result; visszatérés r0 = hibamaszk
.syntax unified
.thumb
.text
.global lab_abi_call32
.thumb_func
.type lab_abi_call32, %function
lab_abi_call32:
@ 10 regiszter = 40 bájt: SP 8-ra igazított marad; r3 = &result
push {r3-r11, lr}
mov r12, r0 @ fn (R12: scratch, a hívó szabadon használja)
ldr r4, =lab_abi32_sp @ horgony: 0 = szabad, különben egy hívás már fut
ldr r5, [r4]
cmp r5, #0
bne 3f @ újrabelépés: fn nem hívható
mov r5, sp
str r5, [r4] @ a hívás előtti SP
ldr r4, =0x44444444 @ minták a callee-saved regiszterekbe
ldr r5, =0x55555555
ldr r6, =0x66666666
ldr r7, =0x77777777
ldr r8, =0x88888888
ldr r9, =0x99999999
ldr r10, =0xAAAAAAAA
ldr r11, =0xBBBBBBBB
mov r0, r1 @ a0
mov r1, r2 @ a1
blx r12 @ BLX: LR = visszatérési cím | 1
movs r2, #0 @ hibamaszk
ldr r1, =0x44444444
cmp r4, r1
it ne
orrne r2, r2, #(1 << 0)
ldr r1, =0x55555555
cmp r5, r1
it ne
orrne r2, r2, #(1 << 1)
ldr r1, =0x66666666
cmp r6, r1
it ne
orrne r2, r2, #(1 << 2)
ldr r1, =0x77777777
cmp r7, r1
it ne
orrne r2, r2, #(1 << 3)
ldr r1, =0x88888888
cmp r8, r1
it ne
orrne r2, r2, #(1 << 4)
ldr r1, =0x99999999
cmp r9, r1
it ne
orrne r2, r2, #(1 << 5)
ldr r1, =0xAAAAAAAA
cmp r10, r1
it ne
orrne r2, r2, #(1 << 6)
ldr r1, =0xBBBBBBBB
cmp r11, r1
it ne
orrne r2, r2, #(1 << 7)
ldr r1, =lab_abi32_sp
ldr r3, [r1] @ a hívás előtti SP a horgonyból
mov r12, sp
cmp r12, r3
it ne
orrne r2, r2, #(1 << 8)
@ helyreállítás csak a horgonyból: a sérült SP-t nem használjuk
mov sp, r3
movs r3, #0
str r3, [r1] @ a horgony újra szabad
b 4f
3: mov r2, #(1 << 9) @ LAB_ABI32_BUSY; a horgonyt nem írjuk felül
movs r0, #0
4: ldr r3, [sp] @ a mentett &result
str r0, [r3] @ *result = a hívott R0-ja
mov r0, r2
pop {r3-r11, pc} @ visszatérés: a mentett LR közvetlenül a PC-be
.size lab_abi_call32, . - lab_abi_call32
.bss
.align 2
lab_abi32_sp:
.space 4A közös teszthajtó. Csak a platformréteg (kiírás, ABI-teszthívó, kilépés) különbözik, a tesztesetek és az ellenőrzés logikája azonos:
/* lab-sum-test.c — közös teszthajtó: ugyanaz a logika A64-en és Thumb-2-n.
* Csak a platformréteg (kiírás, ABI-teszthívó, kilépés) különbözik. */
#include <stdint.h>
#include "lab-sum.h"
/* ---------- platformréteg ---------- */
#if defined(__aarch64__)
#include <stdio.h>
#include "lab-abi-call.h"
static void out(const char *s) { fputs(s, stdout); }
static uint32_t call_checked(const uint32_t *p, uint32_t n, uint32_t *mask)
{
uint64_t x0 = 0;
*mask = (uint32_t)lab_abi_call((lab_abi_fn)lab_sum_u32, (uintptr_t)p, n, 0, 0, 0, 0, &x0);
return (uint32_t)x0; /* uint32_t visszatérés: csak W0 értelmes */
}
static int finish(int fails) { return fails != 0; }
#else
#include "lab-abi-call32.h"
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 void out(const char *s) { sh_call(0x04u, s); } /* SYS_WRITE0 */
static uint32_t call_checked(const uint32_t *p, uint32_t n, uint32_t *mask)
{
uint32_t r0 = 0;
*mask = lab_abi_call32((lab_abi_fn32)lab_sum_u32, (uint32_t)(uintptr_t)p, n, &r0);
return r0;
}
static int finish(int fails)
{
sh_call(0x18u, (const void *)(fails ? 0x20023u : 0x20026u)); /* SYS_EXIT */
return fails != 0;
}
#endif
/* ---------- közös tesztlogika ---------- */
static void out_hex(uint32_t v)
{
char b[11] = "0x";
for (int i = 0; i < 8; i++) b[2 + i] = "0123456789abcdef"[(v >> (28 - 4 * i)) & 0xFu];
b[10] = '\0';
out(b);
}
static const uint32_t data[8] = { 1, 2, 3, 0xFFFFFFFFu,
0x80000000u, 0x80000000u, 7, 0x12345678u };
int main(void)
{
static const uint32_t ns[] = { 0, 1, 3, 4, 8 };
int fails = 0;
for (unsigned c = 0; c < sizeof ns / sizeof ns[0]; c++) {
uint32_t n = ns[c], ref = 0, mask;
for (uint32_t i = 0; i < n; i++) ref += data[i]; /* C-referencia, mod 2^32 */
uint32_t got = call_checked(data, n, &mask);
int ok = got == ref && mask == 0;
fails += !ok;
out("n="); out_hex(n); out(" sum="); out_hex(got);
out(" abi_mask="); out_hex(mask); out(ok ? " OK\n" : " HIBA\n");
}
out(fails ? "SUMTEST: FAIL\n" : "SUMTEST: PASS\n");
return finish(fails);
}# A64 (qemu-user)
aarch64-linux-gnu-gcc -O1 -static -o lab-sum-a64 lab-sum-test.c lab-sum-a64.s lab-abi-call.S
qemu-aarch64 ./lab-sum-a64
# Thumb-2 (Cortex-M3, qemu-system-arm, semihosting)
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -ffreestanding -nostdlib -O1 -T lab-mps2.ld \
-o lab-sum-t32.elf lab-startup.s lab-sum-t32.s lab-abi-call32.s lab-sum-test.c
qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -nographic -semihosting \
-kernel lab-sum-t32.elfKimenet mindkét platformon, a helyettesítő eszközlánccal (Clang +
musl, illetve Clang + ld.lld) fordítva, az A64-oldalon a
tools/lab-run.py, a Thumb-oldalon a
tools/cm-run.py emulátorral futtatva. A két kimenet
karakterre azonos:
n=0x00000000 sum=0x00000000 abi_mask=0x00000000 OK
n=0x00000001 sum=0x00000001 abi_mask=0x00000000 OK
n=0x00000003 sum=0x00000006 abi_mask=0x00000000 OK
n=0x00000004 sum=0x00000005 abi_mask=0x00000000 OK
n=0x00000008 sum=0x12345684 abi_mask=0x00000000 OK
SUMTEST: PASS
A 8 elemes eset a modulo összeadást is próbálja: .
Ellenpróba. Ha a Thumb-változatból kihagyjuk a
push {r4, r5} / pop {r4, r5} párt, az összegek
ugyanúgy helyesek, de az abi_mask 0x00000003
lesz (sérült az R4 és az R5; az
n = 0 esetben csak az R4, ezért ott
0x00000001), és a teszt SUMTEST: FAIL-lel
zárul. Az eredmény helyessége tehát nem igazolja a hívási szerződést;
azt külön kell mérni, mindkét ISA-n.
A teszthívó saját negatív tesztje. Magát a
lab_abi_call32-t is ellenpróbázni kell, mert a
LAB_ABI32_SP bit csak akkor ér valamit, ha a teszthívó a
hibát túléli. Két hívott szabályosan,
BX LR-rel tér vissza, de az SP-t 4, illetve 8
bájttal eltolva hagyja. A teszthívót egy assembly hívó
(lab_abi32_outer) hívja meg, amely előtte saját mintát tölt
az R4–R11-be, és utána ellenőrzi, hogy a
mintái és az SP-je megmaradt-e. Az eredményszó két oldalán
őrszó áll, így az is kiderül, ha a teszthívó idegen címre írna. Elvárás:
mindkét esetben pontosan LAB_ABI32_SP a maszk, a 31. bit (a
hívó állapota sérült) nem áll be, az eredmény 3, és az őrszavak
épek.
@ lab-abi32-neg.s — a lab_abi_call32 negatív tesztjei (Thumb-2, Armv7-M)
.syntax unified
.thumb
.text
@ HIBÁS hívottak: szabályosan, BX LR-rel térnek vissza, de eltolt SP-vel.
.global lab_t32_bad_sp4
.thumb_func
.type lab_t32_bad_sp4, %function
lab_t32_bad_sp4:
sub sp, sp, #4
adds r0, r0, r1
bx lr
.size lab_t32_bad_sp4, . - lab_t32_bad_sp4
.global lab_t32_bad_sp8
.thumb_func
.type lab_t32_bad_sp8, %function
lab_t32_bad_sp8:
sub sp, sp, #8
adds r0, r0, r1
bx lr
.size lab_t32_bad_sp8, . - lab_t32_bad_sp8
@ uint32_t lab_abi32_outer(lab_abi_fn32 fn, uint32_t *result)
@ A teszthívó saját hívója: R4–R11-be mintát tölt, meghívja a
@ lab_abi_call32(fn, 1, 2, result)-ot, majd ellenőrzi, hogy a mintái és az
@ SP-je megmaradt. Visszatérés: a teszthívó maszkja, bit 31 = a hívó állapota sérült.
.global lab_abi32_outer
.thumb_func
.type lab_abi32_outer, %function
lab_abi32_outer:
push {r4-r11, lr} @ 9 regiszter
sub sp, sp, #4 @ + 4 bájt: SP 8-ra igazított
mov r3, r1 @ result
mov r12, r0 @ fn
ldr r4, =0x14141414
ldr r5, =0x15151515
ldr r6, =0x16161616
ldr r7, =0x17171717
ldr r8, =0x18181818
ldr r9, =0x19191919
ldr r10, =0x1A1A1A1A
ldr r11, =0x1B1B1B1B
mov r1, sp
str r1, [sp] @ a saját SP-nk, a keretünk aljára
mov r0, r12
movs r1, #1
movs r2, #2
bl lab_abi_call32
ldr r1, [sp]
mov r2, sp
cmp r1, r2 @ a mi SP-nk változatlan?
bne 9f
ldr r1, =0x14141414
cmp r4, r1
bne 9f
ldr r1, =0x15151515
cmp r5, r1
bne 9f
ldr r1, =0x16161616
cmp r6, r1
bne 9f
ldr r1, =0x17171717
cmp r7, r1
bne 9f
ldr r1, =0x18181818
cmp r8, r1
bne 9f
ldr r1, =0x19191919
cmp r9, r1
bne 9f
ldr r1, =0x1A1A1A1A
cmp r10, r1
bne 9f
ldr r1, =0x1B1B1B1B
cmp r11, r1
beq 1f
9: orr r0, r0, #0x80000000 @ a hívó állapota sérült
1: add sp, sp, #4
pop {r4-r11, pc}
.size lab_abi32_outer, . - lab_abi32_outer/* lab-abi32-neg-test.c — a lab_abi_call32 negatív tesztje: eltolt SP-vel visszatérő
* hívottak. */
#include <stdint.h>
#include "lab-abi-call32.h"
extern void lab_t32_bad_sp4(void), lab_t32_bad_sp8(void);
uint32_t lab_abi32_outer(lab_abi_fn32 fn, uint32_t *result);
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 void out(const char *s) { sh_call(0x04u, s); } /* SYS_WRITE0 */
static void out_hex(uint32_t v)
{
char b[11] = "0x";
for (int i = 0; i < 8; i++) b[2 + i] = "0123456789abcdef"[(v >> (28 - 4 * i)) & 0xFu];
b[10] = '\0';
out(b);
}
/* result körül őrszavak: a teszthívó csak a középső szót írhatja. */
static int expect(const char *name, lab_abi_fn32 fn)
{
volatile uint32_t box[3] = { 0xC0DEC0DEu, 0, 0xC0DEC0DEu };
uint32_t mask = lab_abi32_outer(fn, (uint32_t *)&box[1]);
int ok = mask == LAB_ABI32_SP && box[1] == 3
&& box[0] == 0xC0DEC0DEu && box[2] == 0xC0DEC0DEu;
out(name); out(" mask="); out_hex(mask); out(" result="); out_hex(box[1]);
out(ok ? " OK\n" : " HIBA\n");
return !ok;
}
int main(void)
{
int fails = expect("bad_sp4", lab_t32_bad_sp4);
fails += expect("bad_sp8", lab_t32_bad_sp8);
out(fails ? "ABI32-NEG: FAIL\n" : "ABI32-NEG: PASS\n");
sh_call(0x18u, (const void *)(fails ? 0x20023u : 0x20026u)); /* SYS_EXIT */
return fails;
}arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -ffreestanding -nostdlib -O1 -T lab-mps2.ld \
-o lab-abi32-neg.elf lab-startup.s lab-abi32-neg-test.c lab-abi32-neg.s lab-abi-call32.s
qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -nographic -semihosting \
-kernel lab-abi32-neg.elfKimenet a helyettesítő eszközlánccal (Clang + ld.lld,
tools/cm-run.py):
bad_sp4 mask=0x00000100 result=0x00000003 OK
bad_sp8 mask=0x00000100 result=0x00000003 OK
ABI32-NEG: PASS
Ez azért működik, mert a teszthívó az összehasonlítás után
először a horgonyból tölti vissza az SP-t
(mov sp, r3), és csak utána olvassa a vermet
(ldr r3, [sp], pop {r3-r11, pc}). A
helyreállítás tehát sem a hívott által elrontott SP-től,
sem egy esetleg sérült callee-saved regisztertől nem függ. Egy korábbi
változat az ellenőrzés után is a sérült SP-ről hajtotta
végre ezt a két olvasást. Ugyanezen a teszten a pop a hívó
R11-mintáját töltötte a PC-be
(0x1b1b1b1a), és a futás fault-tal leállt: éppen azt a
hibát nem élte túl, amelyre külön bitje van.
A két megvalósítás összevetése:
| Szempont | A64 | Thumb-2 |
|---|---|---|
| argumentumok | x0, w1 |
r0, r1 |
| munkaregiszterek | w2, w3 (caller-saved, nincs mentés) |
r4, r5 (callee-saved, ezért
push/pop) |
| post-index betöltés | ldr w3, [x0], #4 |
ldr r5, [r0], #4 |
| visszatérés | ret (X30) |
bx lr (LR bit0 = 1) |
| függvényjelölés | .type …, %function |
.thumb_func + .type …, %function |
Munkakönyvtár: ~/arm-labor/cm/a061/ —
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/a061
cd ~/arm-labor/cm/a061A feladat: írjuk ki az 'X' karaktert tízszer egymás
után. Az AArch64-oldalon a Hello world tisztán
assemblyben write-syscall mintáját és a Ciklusszervezés számlálós ciklusát használjuk újra.
A Cortex-M-oldalon ugyanazt a ciklus- és számlálólogikát tartjuk meg,
csak a kimeneti lépést cseréljük le.
AArch64 változat (A64 ISA, A-profile):
.section .rodata
ch: .ascii "X"
.text
.global _start
_start:
mov x19, #10 // N = 10
adr x20, ch // buffer címe
loop:
mov x0, #1 // fd = stdout
mov x1, x20 // buffer
mov x2, #1 // hossz = 1 bájt
mov x8, #64 // write syscall száma
svc #0 // rendszerhívás
subs x19, x19, #1 // számláló csökkentése
b.ne loop // ha nem nulla, ismétlés
mov x0, #0
mov x8, #93 // exit
svc #0
Build és futtatás:
aarch64-linux-gnu-as -o lab-a061-a64.o lab-a061-a64.s
aarch64-linux-gnu-ld -o lab-a061-a64 lab-a061-a64.o
qemu-aarch64 ./lab-a061-a64Várt kimenet: XXXXXXXXXX, kilépési kód 0. A
write visszatérési értékét itt nem ellenőrizzük; ez a példa
egyszerűsítése.
Cortex-M3 változat (Thumb-2, Armv7-M ISA, M-profile):
.syntax unified
.thumb
.equ UART0_DATA, 0x40004000 @ QEMU mps2-an385 CMSDK UART0 adatregisztere
.text
.global main
main:
ldr r1, =UART0_DATA @ r1 = UART0 adatregiszter címe
movs r2, #0x58 @ 'X' ASCII kódja
movs r0, #10 @ N = 10
loop:
strb r2, [r1] @ egy bájt kiírása a UART adatregiszterbe
subs r0, r0, #1 @ számláló csökkentése
bne loop @ ha nem nulla, ismétlés
done:
b done @ nincs OS, amely visszatérjen; végtelen ciklus
Ez a töredék csak előkészített UART mellett értelmezhető, és nem lesz belőle önálló firmware pusztán attól, hogy startup kódot adunk mellé. A CMSDK APB UART az adatregiszterbe írt bájtot csak akkor küldi ki, ha három előfeltétel teljesül, és a töredék egyiket sem teljesíti:
BAUDDIV regiszter
(+0x10) értéke legalább 16;CTRL regiszter
(+0x08) TXEN bitje (bit0) 1;STATE regiszter (+0x04) TXFULL
bitje (bit0) 0; tele pufferbe írt bájt elveszik.Letiltott adóval a valódi UART semmit nem küld ki. A QEMU modelljétől
sem várható, hogy ilyenkor kiírja a karaktert; a pontos viselkedés a
használt QEMU-verzióban ellenőrizendő, de a programnak nem szabad rá
építenie. A töredék tehát egy olyan állapotot feltételez, amelyet valaki
(bootloader, korábbi kód) már beállított. Az önállóan induló változat
ugyanaz a ciklus, előtte a két beállítással, és minden írás előtt a
TXFULL figyelésével:
/* lab-a061-uart.s — 'X' tízszer, előkészített UART0-val (CMSDK APB UART) */
.syntax unified
.cpu cortex-m3
.thumb
.equ UART0_BASE, 0x40004000 @ QEMU mps2-an385: CMSDK UART0
.equ UART_DATA, 0x00 @ adatregiszter
.equ UART_STATE, 0x04 @ bit0: TXFULL (az adópuffer tele)
.equ UART_CTRL, 0x08 @ bit0: TXEN (adó engedélyezése)
.equ UART_BAUDDIV, 0x10 @ baud-osztó, legalább 16
.text
.thumb_func
.global main
.type main, %function
main:
ldr r1, =UART0_BASE
movs r3, #16 @ 1. baud-osztó (a CMSDK minimuma)
str r3, [r1, #UART_BAUDDIV]
movs r3, #1 @ 2. adó engedélyezése
str r3, [r1, #UART_CTRL]
movs r2, #0x58 @ 'X' ASCII kódja
movs r0, #10 @ N = 10
loop:
1: ldr r3, [r1, #UART_STATE] @ 3. várakozás, amíg az adópuffer tele
lsls r3, r3, #31 @ bit0 -> N jelző
bmi 1b
str r2, [r1, #UART_DATA] @ egy karakter kiírása
subs r0, r0, #1 @ számláló csökkentése
bne loop @ ha nem nulla, ismétlés
done:
b done @ nincs OS, amelyhez visszatérhetne
.size main, . - main
A vektortáblát, a linker scriptet és a main-t hívó
startup kódot a Reset és boot-folyamat:
vektortábla, a Linker script alapjai és a Startup kód alfejezetek építik fel. Ha azok
elkészültek, a két kanonikus fájllal ez a változat már önálló
firmware:
cp ~/arm-labor/cm/a065/lab-mps2.ld ~/arm-labor/cm/a066/lab-startup.s .
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -nostdlib -T lab-mps2.ld \
-o lab-a061.elf lab-startup.s lab-a061-uart.s
qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -nographic \
-kernel lab-a061.elf # kilépés: Ctrl-A, XA helyettesítő eszközlánccal (Clang + ld.lld) épített
lab-a061.elf a Unicorn-alapú tools/cm-run.py
emulátorban a XXXXXXXXXX kimenetet adta. Az emulátor
STATE regisztere mindig 0, ezért a várakozó ciklust ott nem
lehetett kipróbálni. A UART0_DATA címe és a regiszterek
eltolása a QEMU mps2-an385 dokumentált memóriatérképéből,
illetve a CMSDK UART leírásából származik. A bitmezők részletes
kezelése, a vételi oldal és a hibakezelés a Memory-mapped perifériák és GPIO, illetve az
Egyszerű UART driver témája; itt a kimeneti
store-utasítás és a minimális előkészítés szemléltetése a cél.
Összehasonlítás: mi azonos és mi új?
| Elem | AArch64 (A64 ISA) | Cortex-M3 (Thumb-2) | Átvihető? |
|---|---|---|---|
| Számláló dekrementálása | SUBS x19, x19, #1 |
SUBS r0, r0, #1 |
Igen, a szemantika azonos |
| Feltételes elágazás | B.NE loop |
BNE loop |
Igen, a feltételkód-logika azonos |
| Ciklusinvariáns | „a hátralévő iterációk száma” | ugyanaz | Igen |
| Kimeneti mechanizmus | SVC #0, X8 = 64 |
STRB fix fizikai címre |
Nem, teljesen új |
| Címkezelés | virtuális cím, MMU | fizikai cím, nincs MMU | Nem |
| Kernel szerepe | pufferelés, hibakezelés, blokkolás | nincs kernel | Nem |
| Privilegizáltság | EL0/EL1 | Thread/Handler mód | Nem |
A táblázat két réteget mutat. Az első réteg a
vezérlési logika: a számláló, a dekrementálás, a feltételkód és az
elágazás szó szerint azonos elven működik. A második
réteg a kimeneti mechanizmus: az AArch64-en a
write egy rendszerhívás, amelynek van syscall-száma,
argumentumszerződése és visszatérési értéke; a Cortex-M-en a
STRB egyetlen memóriahozzáférés, amelynek nincs
visszatérési értéke, nincs pufferelése, és a cím fix. A „kimenet”
fogalma rendszerhívásból hardverregiszter-írássá alakul.
1. SUB helyett SUBS. Ha a
Cortex-M kódban subs helyett sub-ot írunk, a
feltételkódok nem frissülnek, és a BNE a ciklusban
utoljára lefutott flagíró utasítás Z értéke alapján
dönt. A végén tesztelő ciklus törzse ettől függetlenül legalább egyszer
lefut: ha Z = 1, pontosan egyszer, ha Z = 0, végtelen ciklus lesz
belőle. Hogy melyik eset áll fenn, azt nem a „véletlen”, hanem a ciklus
utasításai döntik el (lásd a 3. megoldást). Ellenpróba:
objdump -d-vel ellenőrizzük, hogy a SUBS
kódolása szerepel-e.
2. Immediate-kódolási korlát. A Thumb-2
MOVS csak 8 bites immediátot fogad el. A 32 bites
0x40004000 cím közvetlenül nem kódolható, ezért kell az
LDR r1, =UART0_DATA pszeudoutasítás, amely literálpoolból
tölt. Ellenpróba: objdump -d mutatja a literálbetöltést és
a pool tartalmát.
3. STR helyett STRB. A
UART adatregisztere 8 bites; ha 32 bites STR-et használunk,
a periféria viselkedése nem garantált. A strb a helyes
választás.
4. A UART FIFO tele lehet. A valódi drivernek a state regiszterből kell ellenőriznie, hogy a TX puffer fogad-e adatot. Itt ezt nem tesszük; a teljes kezelés a Egyszerű UART driver témája.
5. A QEMU kimenetének irányítása. A
qemu-system-arm -machine mps2-an385 a UART kimenetét a host
soros konzoljára irányítja; ennek beállítása a Firmware build és QEMU-betöltés munkafolyamat
alfejezetben következik.
6. Az AArch64 write visszatérési
értéke. A példa nem ellenőrzi az X0-t; részleges
írás előfordulhat. A hibakezelés az Egyszerű
instrumentáció és a GDB assembly szinten
eszközeivel vizsgálható.
(magyarázó, alapszint — cél: megértés-ellenőrzés) Sorolja fel, melyik négy jellemző tűnik el az AArch64 Linux-userspace modellből a Cortex-M3-ra lépve. Mindegyikhez írjon egy mondatot arról, hogy ez mit jelent a gyakorlatban a program számára.
(implementációs, alapszint — cél: rutinépítés)
Módosítsa a Cortex-M3 kódot úgy, hogy a kiírt karakter 'A'
legyen, és a ciklus 20-szor fusson le. Nevezze meg, melyik utasítás
változik, és miért.
3. (hibakeresési, középszint — cél: rutinépítés) A
lab-a061-uart.s-ben cserélje a subs r0, r0, #1
utasítást sub r0, r0, #1-re. Utasításonként keresse meg,
melyik az utolsó NZCV-író utasítás a bne előtt, és ebből
vezesse le, hány karakter jelenik meg. Hogyan igazolhatja
objdump-pal, hogy valóban SUB szerepel a
kódban, és milyen kódolást választott az assembler?
write syscall szerződése, (c) a
bitmanipulációs műveletek elve, (d) az AAPCS64 regiszterszerződés.
Indokoljon.1. megoldás — Segítség. Nézze át a fejezet “Mi tűnik el az AArch64 modellhez képest?” négy pontját — melyik architektúraszintű tény, és melyik a labor saját választása?
1. megoldás — Teljes megoldás. A négy jellemző: (i)
MMU — nincs virtuális cím, a programozott cím fizikai, ez
architektúraszintű M-profile tény; (ii) operációs rendszer-szintű
syscall-szerződés — az SVC utasítás és az SVCall kivétel
architekturálisan létezik M-profilon, de ebben a laborban nincs kernel,
amely kiszolgálná, ezért nincs X8-hoz hasonló
syscall-szám-konvenció, a kimenet közvetlen regiszterírás; (iii)
exception level-hierarchia — csak Thread és Handler mód van, nincs
EL0–EL3, ez is architektúraszintű tény; (iv) operációs rendszer — ez a
labor választása, nem architektúra-korlát: Cortex-M-en RTOS is futhatna,
de a jelen laborban nincs ütemező, folyamat, fájlrendszer. A
gyakorlatban ez azt jelenti, hogy a programnak magának kell kezelnie az
indulást, a perifériák állapotát és a hibákat.
2. megoldás — Segítség. Két konstans vár cserére: az ASCII karakterkód és az ismétlésszám — a cikluslogikát (léptetés, feltétel) kell-e egyáltalán hozzányúlni?
2. megoldás — Teljes megoldás. A
movs r2, #0x58 helyére movs r2, #0x41 kerül
('A' ASCII kódja), a movs r0, #10 helyére
pedig movs r0, #20. A strb, a
subs és a bne változatlan marad, mert a
cikluslogika független a konkrét karaktertől és a számláló
kezdőértékétől. Ez pontosan azt mutatja, hogy a vezérlési szerkezet
átvihető.
3. megoldás — Segítség. A BNE a Z flag
állapotától függ. Ha a SUB nem frissíti, akkor melyik
utasítás írta utoljára a flageket, mielőtt a vezérlés a
bne-hez ért? Nézd meg, milyen feltétellel jut el a vezérlés
a várakozó ciklusból a str-ig.
3. megoldás — Teljes megoldás. A SUB
nem frissíti a feltételkódokat. A bne tehát a ciklusban
utoljára lefutott flagíró utasítás Z értékét látja. A
lab-a061-uart.s egy iterációjában a bne előtt
ezek futnak le:
| Utasítás | Ír NZCV-t? | Hatás |
|---|---|---|
ldr r3, [r1, #UART_STATE] |
nem | |
lsls r3, r3, #31 |
igen (N, Z, C) | a STATE bit0-ja (TXFULL) kerül a 31.
bitre; Z = 1, ha az eredmény 0 |
bmi 1b |
nem | csak akkor esik át, ha N = 0, azaz TXFULL = 0 |
str r2, [r1, #UART_DATA] |
nem | |
sub r0, r0, #1 |
nem | |
bne loop |
olvassa | Z a lsls-től |
A str-ig csak TXFULL = 0 mellett jut el a
vezérlés. Ekkor a lsls eredménye 0, tehát Z = 1. A
bne ezért soha nem ugrik, a ciklustörzs
pontosan egyszer fut le: egyetlen X
jelenik meg, r0 = 9 marad, és a program a done
ciklusba kerül. A viselkedés determinisztikus, nem a ciklus előtti
„véletlen” Z-értéktől függ.
Az objdump -d a sub r0, r0, #1-et 32 bites
sub.w r0, r0, #1 alakban mutatja (Thumb-2 T3 kódolás, S =
0, f1a0 0001). A subs r0, r0, #1 IT-blokkon
kívül 16 bites, flagbeállító utasítás (1e40). A kódolás
hossza tehát már a listában is elárulja a különbséget.
A flagelőzmény hatása egy periféria nélküli, minimális ciklusban
tisztán levezethető. Itt a ciklus előtti cmp az utolsó
flagíró, és a törzs add-ja (S nélkül) sem ír flaget:
movs r0, #3 @ számláló
movs r1, #0 @ lefutott törzsek száma
cmp r0, r0 @ Z = 1 (változat: cmp r0, #0 -> Z = 0)
1: add r1, r1, #1 @ add.w: flaget nem ír
sub r0, r0, #1 @ sub.w: flaget nem ír
bne 1b @ Z a cmp-től
Előre beállított Z = 1 mellett a bne nem ugrik: a törzs
egyszer fut le (r1 = 1, r0 = 2). Z = 0 mellett
a bne mindig ugrik, a számláló túlcsordul, és a ciklus
végtelen. A helyettesítő eszközlánccal (Clang,
tools/cm-run.py és Unicorn) mindhárom esetet lefuttattuk: a
minimális ciklus Z = 1-nél egyszer futott le, Z = 0-nál 1000 utasítás
után is futott, a sub-os lab-a061-uart.s pedig
pontosan egy X-et írt. Ez a hiba jól mutatja, hogy a
feltételkódok explicit állapotot hordoznak, és a ciklus helyes működése
azon múlik, melyik utasítás írta őket utoljára.
4. megoldás — Segítség. Melyik kettő tisztán algoritmikus-logikai mintázat, amely nem függ az operációs rendszertől vagy az ABI-tól — és melyik kettő kifejezetten az AArch64 Linux-userspace konkrét szerződése?
4. megoldás — Teljes megoldás. Átvihető: (a) a
számlálós ciklus szerkezete és (c) a bitmanipulációs műveletek elve. Nem
vihető át: (b) a write syscall szerződése, mert
M-profile-on nincs syscall, és (d) az AAPCS64 regiszterszerződés, mert
az M-profile saját, 32 bites AAPCS-változatot használ. Az indoklás: az
(a) és (c) tisztán algoritmikus-logikai mintázat, amely nem függ a
privilegizáltsági szinttől vagy a memóriamodelltől; a (b) és (d) viszont
az operációs rendszer és az AArch64 ABI konkrét szerződései.
Az A-profile és M-profile közötti váltás nem utasításkészlet-csere,
hanem futtatási modellváltás. Az MMU és az exception level-hierarchia
architektúraszinten eltűnik; az operációs rendszer és a hozzá kötött
syscall-szerződés hiánya ezzel szemben a könyv választott bare-metal
laborjának, nem az M-profile architektúrának a következménye —
Cortex-M-en RTOS is futtatható, SVC-alapú
szolgáltatáshívással. Helyette fizikai címek, Thread/Handler mód,
közvetlen perifériaírás és reset vektoros indulás jelenik meg. A
vezérlési logika – számláló, dekrementálás, feltételkód, elágazás –
átvihető; a kimeneti mechanizmus és a címkezelés nem. A Cortex-M azért
tipikus mikrokontroller-választás, mert kiszámítható, kis erőforrású és
közvetlen hardverelérést ad.
Továbbhasznált definíciók: - M-profile:
mikrokontrolleres architektúraprofil, Armv7-M, Thumb-2, NVIC, opcionális
MPU, MMU nélkül. - Bare-metal: operációs rendszer
nélküli futtatás, saját indulási környezettel. - Memory-mapped
I/O: perifériaregiszterek a normál memóriacím-térben,
LDR/STR hozzáféréssel.
Az alábbiak dokumentációs kiindulópontok (forrásállapot: nem megnyitott, lásd az Olvasói útmutató jelöléseit):
LDR = pszeudoutasítás,
szekciók és direktívák.mps2-an385 machine és a CMSDK perifériák dokumentált
memóriatérképe.Ellenőrzési hiányok: a lab_abi_call32
ellenpróbáit (a fenti, 4 és 8 bájttal elállított SP-vel
visszatérő negatív teszt; továbbá helyes hívott, egymásba ágyazott hívás
→ LAB_ABI32_BUSY, majd újra helyes hívott a horgony
felszabadulásának igazolására) a helyettesítő eszközlánccal (Clang +
ld.lld, tools/cm-run.py) futtattuk, nem
arm-none-eabi-gcc/qemu-system-arm alatt. A
0x40004000 UART0-cím és a CMSDK UART adatregiszter offsetje
a QEMU mps2-an385 dokumentált memóriatérképéből származó
jelölt adat; ezt a laborban, az adott QEMU-verzióval kell ellenőrizni. A
UART FIFO állapotkezelése, a TX-megszakítás és a teljes driver a Memory-mapped perifériák és GPIO és az Egyszerű UART driver alfejezetekben következik. A
QEMU mps2-an385 UART-kimenetének host konzolra irányítása a
Firmware build és QEMU-betöltés munkafolyamat
témája; ennek pontos paraméterezése QEMU-verziófüggő lehet.
Kísérőcsomag, helyettesítő futtató és közös ellenőrzési
táblázat. A könyv „helyettesítő eszközlánccal mérve” jelölt
eredményei, laborforrásai és próbái egy verziózott kísérőcsomagban
érhetők el: dsk-arm-assembly-tools-1.0.tar.gz, SHA-256:
54b86bca4b2100ab5e0fb0edb8f72c7347fdf9a1fba049e6595c83878735c17b,
letöltés: github.com/gellistvan/dsk-arm-assembly-tools,
v1.0 kiadás. A csomag tartalma:
lab-run.py (AArch64 Linux user-mode) és
cm-run.py (Cortex-M3), mindkettő --version
kapcsolóval;zig cc 0.17.0;cm-run.py támogatási mátrixa
(README.md): UART0 RX/TX, Timer0 megszakítással, Timer1
időalapként, NVIC, SysTick szoftveres pendingként, WFI,
semihosting; GPIO és DWT nélkül; nem cikluspontos;MANIFEST.sha256, és a csomag kiadásakor futtatott
próbák naplói (logs/).Egy üres munkakönyvtárban a csomag további fájlkeresés nélkül
futtatható: sha256sum -c MANIFEST.sha256, majd
bash tests/run.sh substitute (helyettesítő út), illetve
bash tests/run.sh gnu-qemu (a könyv fő útja:
aarch64-linux-gnu-*, arm-none-eabi-gcc,
qemu-aarch64,
qemu-system-arm -machine mps2-an385). Mindkét mód
ugyanazokból a fájlokból épít, próbánként
PASS/FAIL sort ír, és naplót készít, amelynek
elején a csomagverzió, a manifest-ellenőrzés és az eszközverziók állnak.
A helyettesítő eredmények kiegészítő bizonyítékok a
saját hatókörükben. Nem helyettesítik a fő utat, és nem igazolják
automatikusan a QEMU-parancsokat, a QEMU NVIC-jét vagy timerét. A II.
rész ellenőrzési állapota egy helyen, a v1.0 csomaghoz kötve:
| Labor | Helyettesítő úton
(logs/substitute-1.0-20261020.log) |
Fő úton (GNU + QEMU mps2-an385) |
Ciklusmérés (DWT) |
|---|---|---|---|
a061 páros példa,
lab_abi_call32 negatív teszt |
PASS (SUMTEST,
ABI32-NEG) |
a v1.0 csomagban gnu-qemu napló még nincs | — |
| a064–a067 vektortábla, linker script, startup, firmware build | PASS: minden labor-ELF név szerinti
vektorellenőrzése |
ugyanígy | — |
| a069 NVIC-regiszterteszt | futott (--prio-bits 3), a csomag tesztkészletén
kívül |
a QEMU 8 prioritásbitje miatt ott 0xff a
visszaolvasás |
— |
| a070, a071, a076, a077 IRQ-laborok | PASS: Timer0 IRQ8 boot, LATENCY: SKIP,
SysTick-preempció és mutánsa, UART ready + echo,
tick sorok |
ugyanígy | SKIP (a QEMU 8.2.2-n is) |
a079–a080
záróprojekt, lab-m19-systest.py |
PASS (--no-timing): boot, echo, 50 bájtos
burst, tick, reset 0 és 3 s késéssel; a hat hibás protokoll-DUT
FAIL; T1 és K5 SKIP |
ugyanígy | SKIP |
A fő út kiadási elfogadásához a csomag
tests/run.sh gnu-qemu futtatása kell, a kijelölt
QEMU-verzióval és a GNU eszközlánccal; a
logs/gnu-qemu-*.log napló kerül a csomagba. A nem
támogatott időmérés ott is SKIP marad. A későbbi
alfejezetek „Ellenőrzési hiányok” pontjai erre a táblázatra épülnek.