16.1 A-profile és M-profile különbségei: miért más a Cortex-M

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.

16.1.1 Motiváció: ugyanaz a ciklus, két teljesen más világ

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.

16.1.2 Definíció: A-profile, M-profile és a bare-metal futási modell

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.

16.1.3 Mi tűnik el az AArch64 modellhez képest?

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.

16.1.4 Miért tipikus a Cortex-M mikrokontroller és a beágyazott felhasználás?

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.

16.1.5 Mi marad meg az I. rész tudásából?

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.

16.1.5.1 Átvezetés: az AAPCS32 hívási szerződése

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.

16.1.5.2 Hívás és visszatérés: BL, BX és a Thumb függvényjelölés

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.

16.1.5.3 Páros példa: ugyanaz a C-függvény A64-ben és Thumb-2-ben

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áshoz

A 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);
#endif

A64 (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,"",%progbits

Thumb-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_u32

A 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  4

A 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.elf

Kimenet 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 2322^{32} összeadást is próbálja: 1+2+3+(232−1)+2⋅231+7+𝟶𝚡𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾≡𝟶𝚡𝟷𝟸𝟹𝟺𝟻𝟼𝟾𝟺1 + 2 + 3 + (2^{32} - 1) + 2 \cdot 2^{31} + 7 + \texttt{0x12345678} \equiv \texttt{0x12345684}.

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.elf

Kimenet 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

16.1.6 Kidolgozott példa: karakter kiírása N alkalommal

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/a061

A 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-a64

Vá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:

  1. baud-osztó: a BAUDDIV regiszter (+0x10) értéke legalább 16;
  2. engedélyezett adó: a CTRL regiszter (+0x08) TXEN bitje (bit0) 1;
  3. szabad adópuffer: minden írás előtt a 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, X

A 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.

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

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ó.

16.1.8 Feladatok

  1. (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.

  2. (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?

  1. (átviteli, középszint — cél: fejezetek összekapcsolása) Válassza ki az alábbiak közül, mi vihető át szó szerint az I. részből a Cortex-M programozásba: (a) a számlálós ciklus szerkezete, (b) a write syscall szerződése, (c) a bitmanipulációs műveletek elve, (d) az AAPCS64 regiszterszerződés. Indokoljon.

16.1.9 Megoldások

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.

16.1.10 Rövid összefoglaló

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.

16.1.11 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 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:

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.