16.2 Thumb-2 ISA áttekintés

16.2.1 Motiváció: miért kell ismerni a Thumb-2 kódolást?

Képzeljünk el egy Cortex-M3 alapú motorvezérlőt, ahol a megszakításkezelő rutin (ISR) minden egyes ciklusa számít. A rendszer egy 72 MHz-es órajelen fut, és a szabályozási hurok 10 µs-onként lefut. Ha az ISR kódja néhány tíz bájttal hosszabb a szükségesnél, az flash-memória megtakarítás mellett a végrehajtási idő is nő. A Cortex-M3 utasításkészlete, a Thumb-2, változó hosszúságú kódolást használ: a gyakori műveletek 16 biten, a bonyolultabbak 32 biten érhetők el. A kódméret és a sebesség közötti egyensúly megértéséhez elengedhetetlen, hogy tisztában legyünk azzal, melyik utasítás melyik kódolási formában érhető el, milyen regisztereket ér el, és hogyan viselkedik az állapotregiszter. Ez az alfejezet az I. részben megismert A64 ISA után áttér a Thumb-2-re, és megalapozza a Cortex-M3 assembly-programozást.

16.2.2 Definíció: a Thumb-2 ISA és a Cortex-M3 regiszterkészlete

A Thumb-2 egy olyan utasításkészlet-architektúra (ISA), amely 16 bites és 32 bites utasításkódolást egyaránt tartalmaz. Ez az Armv7-M architektúra kötelező utasításkészlete; a Cortex-M3 processzor kizárólag Thumb-2 módban működik, ARM (A32) mód nem érhető el rajta. A 16 bites utasítások az eredetileg ARMv4T-ben bevezetett Thumb utasításkészlet továbbfejlesztett változatai, amelyek korlátozottabbak: jellemzően csak az R0–R7 regisztereket érik el, és szűkebb immediate-tartományt kódolnak. A 32 bites utasítások – amelyeket gyakran „Thumb-2” néven emlegetünk szűkebb értelemben – már a teljes R0–R15 regiszterkészletet elérik, nagyobb immediate-eket és összetettebb címzési módokat támogatnak.

A Cortex-M3 regiszterkészlete 16 darab 32 bites regiszterből áll. Ezek szerepe a következő:

Regiszter Név Szerep
R0–R12 általános célú szabadon használható adattárolásra; R0–R7 „low”, R8–R12 „high” regiszter
R13 SP (Stack Pointer) veremmutató; a verem tetejére mutat
R14 LR (Link Register) visszatérési cím tárolása BL utasítás esetén
R15 PC (Program Counter) a következő végrehajtandó utasítás címe

Három különböző „szélességet” érdemes szétválasztani, mert a mindennapi szóhasználat összemossa őket:

Fogalom Cortex-M3 (Armv7-M) Megjegyzés
regiszterszélesség minden általános célú regiszter 32 bites nincs Xn/Wn-szerű nézetpár
eredmény- és adatszélesség jellemzően 32 bit, de van 64 bites eredmény is: az UMULL/SMULL (32 × 32 → 64 bit) és az UMLAL/SMLAL két regiszterbe írja az eredményt, az LDRD/STRD egy duplaszót (8 bájt) mozgat két regiszter között 64 bites összeadás ADDS + ADC lánccal készül
utasításhossz 16 vagy 32 bit (2 vagy 4 bájt) független a regiszter- és az adatszélességtől

A regiszterek tehát egységesen 32 bitesek, és ez alapvető eltérés az AArch64-hez képest, ahol az Xn regiszter 64 bites, a Wn pedig ugyanazon regiszter 32 bites nézete. A 32 bites regiszterekből viszont nem következik, hogy a hardver semmilyen 64 bites műveletet nem ismer: a hosszú szorzás és a duplaszavas memóriaelérés két regiszter együttes használatával, de egyetlen utasítással történik.

A PC (R15) olvasásakor Thumb állapotban a kapott érték az aktuális utasítás címe + 4, függetlenül attól, hogy az aktuális utasítás 16 vagy 32 bites. (Ez tehát nem általánosan a „következő utasítás + 4”: 16 bites utasításnál a következő utasítás címe +2, 32 bitesnél +4.) Néhány használatnál további igazítás is számít. A PC-relatív betöltés (LDR Rt, [PC, #imm], illetve a LDR Rt, =érték literáljai) és az ADR az Align(PC, 4) értékből indul, vagyis az aktuális utasítás címe + 4 lefelé 4 többszörösére kerekítve. Ezt a részletet a későbbi, elágazásokat és literálokat tárgyaló alfejezetekben hasznos lesz szem előtt tartani. A Thumb bit (az EPSR egyik bitje) mindig 1, és a BX/BLX utasítások kezelik a váltást, de mivel ARM mód nincs, ez csak elméleti jelentőségű.

16.2.3 A 16 és 32 bites kódolás mechanizmusa

A Thumb-2 utasítások változó hosszúságúak. A processzor a memóriából mindig 16 bites félszavakat (halfword) olvas. Az első félszó felső 5 bitje dönti el, hogy az utasítás 16 vagy 32 bites-e. Ha a felső 5 bit értéke 11101, 11110 vagy 11111, akkor az utasítás 32 bites, és a következő félszó is az utasításhoz tartozik. Minden más esetben az utasítás 16 bites. Ez a séma teszi lehetővé a tömör kódolást: a gyakori, egyszerű műveletek 16 biten, a ritkább, összetettebbek 32 biten érhetők el. Az alábbi táblázat néhány tipikus utasítást mutat be mindkét csoportból. Két írásmódot következetesen megkülönböztetünk:

A táblázat minden sora tényleges assembler-kimenetből származik (a forrás és az ellenőrzés módja a Források szakaszban):

Utasítás Hossz Bájtsor (memória) Félszavak (objdump) Megjegyzés
MOVS r0, #1 16 bit 01 20 2001 R0–R7, 8 bites immediate
ADDS r2, r0, r1 16 bit 42 18 1842 R0–R7, flagfrissítés
CMP r2, #30 16 bit 1E 2A 2a1e R0–R7, 8 bites immediate
ADD.W r0, r0, r1, LSL #2 32 bit 00 EB 81 00 eb00 0081 eltolt regiszteroperandus
MOVW r3, #0x5678 32 bit 45 F2 78 63 f245 6378 16 bites immediate az alsó félbe, a felső fél 0
MOVT r3, #0x1234 32 bit C1 F2 34 23 f2c1 2334 16 bites immediate a felső félbe, az alsó marad

A 0x12345678 konstans egyetlen utasításba nem kódolható. A MOV.W csak „módosított immediate”-et fogad el: egy 8 bites értéket eltolva, vagy bizonyos ismétlődő bájtmintákat (0x00XY00XY, 0xXYXYXYXY stb.). Az assembler a MOV.W r3, #0x12345678 sort ezért elutasítja. A teljes 32 bites konstanst a MOVW + MOVT pár tölti be. A korábbi kiadás itt egy 4F F0 78 56 bájtsort közölt; ez valójában a MOV.W r6, #0x3e000000 utasítás.

A 16 bites utasítások előnye a kisebb kódméret, hátrányuk a korlátozott regiszter- és immediate-elérés. A 32 bites utasítások nagyobb rugalmasságot adnak, de négy bájtot foglalnak. A fordítóprogram és az assembly-programozó feladata eldönteni, mikor melyiket használja. A MOVS r0, #1 16 biten elfér, de a MOV r0, #0x1234 már 32 bites kódolást igényel (az assembler MOVW-t választ).

16.2.4 Az APSR állapotregiszter és az NZCV-jelzők

Az APSR (Application Program Status Register) egy 32 bites regiszter, amelynek néhány bitje van definiálva. A legfontosabbak az NZCV-jelzők: N (negative), Z (zero), C (carry) és V (overflow). Ezek megegyeznek az AArch64-ben megismert szemantikával, de elérésük módja eltér. Az APSR-t az MRS és MSR utasításokkal lehet olvasni, illetve írni, közvetlenül nem címezhető.

Bit Név Jelentés
31 N negatív eredmény
30 Z nulla eredmény
29 C átvitel (carry) vagy kölcsön (borrow)
28 V előjeles túlcsordulás

A Thumb-2 utasítások többsége csak akkor frissíti a jelzőket, ha az utasítás neve S utótagot kap. Például az ADDS frissíti az NZCV-t, az ADD nem. A CMP utasítás mindig frissíti a jelzőket, mert az összehasonlítás lényege a flagbeállítás. A 16 bites Thumb utasításoknál gyakran az S utótag implicit: az ADDS r2, r0, r1 16 bites formája mindig frissít, míg a 32 bites ADD.W alapértelmezésben nem frissít, csak ADDS.W esetén. Ez a látszólagos következetlenség a történelmi fejlődés eredménye, és a programozónak tudatosan kell kezelnie.

16.2.5 Kidolgozott példa: 16 és 32 bites utasítások keverése

Munkakönyvtár: ~/arm-labor/cm/a062/ — 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/a062
cd ~/arm-labor/cm/a062

Az alábbi assembly rutin bemutatja a Thumb-2 kódolás kevert használatát. A példa összeszerelhető az arm-none-eabi-as assemblerrel, és az objdump segítségével ellenőrizhető a kódolás hossza.

    .syntax unified
    .thumb
    .global lab_thumb2_mix
    .text
lab_thumb2_mix:
    MOVS    r0, #10          @ 16 bites: R0 = 10
    MOVS    r1, #20          @ 16 bites: R1 = 20
    ADDS    r2, r0, r1       @ 16 bites: R2 = 30, NZCV frissül
    CMP     r2, #30          @ 16 bites: Z = 1
    BEQ     lab_ok           @ 16 bites: elágazás, ha Z = 1
    MOVW    r3, #0x5678      @ 32 bites: az alsó fél, a felső fél nulla
    MOVT    r3, #0x1234      @ 32 bites: a felső fél beállítása
lab_ok:
    BX      lr               @ 16 bites: visszatérés

Assemble és disassemble:

arm-none-eabi-as -mcpu=cortex-m3 -mthumb -o lab-thumb2-mix.o lab-thumb2-mix.s
arm-none-eabi-objdump -d lab-thumb2-mix.o

Kimenet a helyettesítő eszközlánccal (a Clang integrált assemblere, -target thumb-freestanding-eabi -mcpu=cortex-m3, és a könyv tools/thumb-dump.py capstone-alapú disassemblere). A második oszlop az objdump-féle félszóalak, a szögletes zárójelben a memóriabeli bájtsor áll. A GNU objdump ugyanezeket a kódokat adja, csak az immediate-eket tízes számrendszerben írja (#10, #22136 @ 0x5678), a szűk elágazást pedig beq.n alakban:

00000000 <lab_thumb2_mix>:
       0:   200a        [0a 20      ]   movs    r0, #0xa
       2:   2114        [14 21      ]   movs    r1, #0x14
       4:   1842        [42 18      ]   adds    r2, r0, r1
       6:   2a1e        [1e 2a      ]   cmp r2, #0x1e
       8:   d003        [03 d0      ]   beq #0x12
       a:   f245 6378   [45 f2 78 63]   movw    r3, #0x5678
       e:   f2c1 2334   [c1 f2 34 23]   movt    r3, #0x1234

00000012 <lab_ok>:
      12:   4770        [70 47      ]   bx  lr

A disassembly lista jól mutatja a 2 bájtos (16 bites) és 4 bájtos (32 bites) utasításokat. A 0x12345678 konstans egyetlen MOV.W utasításban nem kódolható; a MOVW az alsó, a MOVT a felső 16 bitet állítja be. Mindkettő 4 bájtos. A beq.n utasítás 16 bites, a .n utótag a szűk kódolásra utal. A BX lr szintén 16 bites.

Ellenőrzési feltétel: Az objdump kimenetében a movw és movt 4–4 bájtot foglaljon, a többi utasítás 2 bájtot. A beq.n célja a 0x12 eltolású lab_ok. A megadott bemeneti konstansokkal a branch átugorja a két konstansbetöltést: visszatéréskor R0=10, R1=20, R2=30, Z=1; R3 értékét ez az útvonal nem változtatja meg. (A rutint emulátorban lefuttatva, R3 = 0xdead kezdőértékkel, pontosan ez az állapot adódott, C = 1-gyel együtt, mert a CMP 30, 30 kivonás nem kér kölcsönt.)

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

Hiba 1: High regiszter használata 16 bites utasításban.
Ha a programozó MOVS r8, #1 utasítást ír, az assembler hibát jelez, mert a 16 bites MOVS csak R0–R7-et ér el. Ellenpróba: használjunk MOV.W r8, #1 utasítást, vagy töltsük be egy low regiszterbe és mozgassuk át.
Tünet → hipotézis → adatgyűjtés: „Assembler error: invalid register” → valószínűleg high regiszter 16 bites utasításban → nézzük meg az utasításkészlet-kézikönyvben az elérhető regisztereket.

Hiba 2: Immediate túlcsordulás.
A MOVS r0, #0x100 utasítás 16 biten nem kódolható, mert a 16 bites MOVS immediate csak 8 bites. Az assembler 32 bites MOV.W-t javasol, vagy hibát jelez. Ellenpróba: MOV.W r0, #0x100 használata.
Tünet → hipotézis → adatgyűjtés: „Immediate out of range” → a 16 bites kódolás korlátja → ellenőrizzük az utasítás 32 bites változatát.

Hiba 3: PC-relatív számítás figyelmen kívül hagyása. Az ADR r0, cimke és a PC-relatív LDR az Align(PC, 4) értékből számol, ahol a PC az ADR saját címe + 4. Ha a ADR egy 4-gyel nem osztható (csak 2-vel osztható) címen áll, a kiinduló érték az utasítás címe + 2. A kézi eltolásszámításnál mindkét lépést figyelembe kell venni. Ellenpróba: disassemble, és ellenőrizzük a betöltött címet.

16.2.7 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Ismertesse a Cortex-M3 regisztereit (R0–R15), és térjen ki az SP, LR, PC konvencionális szerepére! Miért nincs a Cortex-M3-nak 64 bites regiszterpárja, mint az AArch64 Xn/Wn nézete?

  2. (implementációs, középszint — cél: rutinépítés) Írjon Thumb-2 assembly rutint, amely két 32 bites számot összead, és az eredményt az R0 regiszterbe teszi! A carry flaget (C) egy külön regiszterbe (R1) mentse el 0 vagy 1 értékkel! Használjon ADDS és ADC utasításokat! A rutin végén BX lr-rel térjen vissza.

  3. (hibakeresési/mérési, középszint — cél: rutinépítés) Adott az alábbi objdump kimenet. Azonosítsa, mely utasítások 16 és melyek 32 bitesek! Számítsa ki a teljes kódméretet bájtban! Melyik utasítás töltene be nagyobb immediate-et, és miért?

   0:   2305            movs    r3, #5
   2:   240a            movs    r4, #10
   4:   191b            adds    r3, r3, r4
   6:   f04f 5568       mov.w   r5, #0x3a000000
   a:   429d            cmp     r5, r3
   c:   d101            bne.n   12
   e:   2000            movs    r0, #0
  10:   e000            b.n     14
  12:   2001            movs    r0, #1
  14:   4770            bx      lr

16.2.8 Megoldások

1. megoldás — Segítség. Nézd meg a fejezet eleji regisztertáblázatot, és gondold át: egy nézetpárhoz (mint Xn/Wn) két ELTÉRŐ szélességű hardveres tárolóra lenne szükség ugyanahhoz a fizikai regiszterhez — van-e ilyen a Cortex-M3-ban?

1. megoldás — Teljes megoldás. - R0–R12: általános célú, szabadon használható. R0–R7 „low”, R8–R12 „high”.
- R13 (SP): veremmutató, a verem tetejére mutat.
- R14 (LR): visszatérési cím tárolása BL esetén.
- R15 (PC): programszámláló. Thumb utasításból olvasva az aktuális utasítás címe + 4; a PC-relatív betöltés és az ADR ennek 4-re lefelé igazított értékét használja. - Nincs 64 bites regiszter, mert az Armv7-M csak 32 bites általános célú regisztereket definiál. Az AArch64-ben az Xn a 64 bites, Wn a 32 bites nézet ugyanazon regiszternek, de itt minden regiszter fixen 32 bites. - Indoklás: nézetpárra azért nincs szükség, mert nincs 64 bites regiszter, amelynek egy része lehetne. Ez nem jelenti, hogy a 64 bites műveletek teljesen hiányoznának: az UMULL/SMULL 64 bites szorzatot ír két regiszterbe, az LDRD/STRD duplaszót mozgat, csak az eredmény itt mindig regiszterpárban keletkezik.

2. megoldás — Segítség. Az ADDS beállítja a C jelzőt, az ADC pedig egy harmadik operandusként hozzáadja azt egy másik összeadáshoz — hogyan használható ez arra, hogy a C értékét (0 vagy 1) egy sima regiszterbe “csapold ki”?

2. megoldás — Teljes megoldás.

    .syntax unified
    .thumb
    .global lab_add_carry
    .text
lab_add_carry:
    ADDS    r0, r0, r1      @ összeadás, flagfrissítés
    MOVS    r1, #0          @ R1 = 0
    ADC     r1, r1, #0      @ R1 = R1 + 0 + C
    BX      lr

3. megoldás — Segítség. Az objdump bal oszlopában a bájt-cím ugrása (pl. 0→2, 2→4 vs. 4→a) közvetlenül elárulja, melyik utasítás hány bájtos — nem kell a mnemonicokat fejből tudni hozzá.

3. megoldás — Teljes megoldás.
- 16 bites utasítások: movs r3, #5, movs r4, #10, adds r3, r3, r4, cmp r5, r3, bne.n, movs r0, #0, b.n, movs r0, #1, bx lr. - 32 bites utasítás: mov.w r5, #0x3a000000, kódja f04f 5568 (bájtsor: 4f f0 68 55). - Teljes kódméret: 9 db 16 bites (18 bájt) + 1 db 32 bites (4 bájt) = 22 bájt; ezt a címoszlop is mutatja (0x14 + 2 = 0x16 = 22). - A mov.w azért 32 bites, mert a 0x3a000000 immediate nem fér el 16 biten; a 16 bites MOVS csak 8 bites immediate-et kódol. A 32 bites alak a 0x3a000000-t módosított immediate-ként ábrázolja. A második félszó 5568 mezői: imm3 = 101, Rd = 0101 (R5), imm8 = 0x68; az első félszó i bitje 0. Az imm12 = 0:101:01101000 = 0x568 felső bitjei nem nullák, ezért elforgatott alakról van szó: az alapérték 1:1101000 = 0xE8, a forgatás imm12[11:7] = 01010 = 10 bit, és 0xE8 ROR 10 = 0x3A000000. Ugyanez a dekódolás a korábbi kiadás f04f 4561 sorára 0xE1 ROR 8 = 0xE1000000-t adna, ezért az a sor nem a 0x3a000000-t töltötte volna be.
- A bne.n és b.n szűk elágazások, 16 bitesek.

16.2.9 Rövid összefoglaló

A Thumb-2 ISA 16 és 32 bites utasításokat kever, lehetővé téve a tömör kódméretet és a rugalmas regiszterelérést. A Cortex-M3 16 darab 32 bites regiszterrel rendelkezik: R0–R12 általános, R13 SP, R14 LR, R15 PC. Nincs Xn/Wn nézetpár, mert nincsenek 64 bites regiszterek; 64 bites eredményt adó utasítások (UMULL, SMULL, LDRD/STRD) viszont vannak, regiszterpárral. A regiszterszélesség, az eredményszélesség és az utasításhossz három külön fogalom. A PC olvasáskor az aktuális utasítás címe + 4. Az APSR tartalmazza az NZCV-jelzőket, amelyeket az S utótagú utasítások és a CMP frissítenek. A 16 bites utasítások jellemzően R0–R7-et érnek el, a 32 bitesek a teljes regiszterkészletet. A kódolás hosszát az első félszó felső 5 bitje dönti el.

Továbbhasznált definíciók: - Thumb-2: 16 és 32 bites utasításokat egyaránt tartalmazó ISA, az Armv7-M kötelező utasításkészlete. - APSR: alkalmazásprogram állapotregisztere, amely az NZCV-jelzőket tárolja. - low/high regiszter: R0–R7 (low) és R8–R12 (high); a 16 bites utasítások jellemzően csak a low regisztereket érik el.

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

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.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: a QEMU mps2-an385 machine pontos Thumb-2 utasítástámogatása és az arm-none-eabi-as verziófüggő viselkedése ebben a folyamatban nem került ellenőrzésre. A kódolási táblázat, a kidolgozott példa listája, a 3. feladat listája és a 2. megoldás kódja a helyettesítő eszközlánccal készült: a Clang integrált assemblere (zig cc -target thumb-freestanding-eabi -mcpu=cortex_m3), a disassembly pedig a capstone-alapú tools/thumb-dump.py. A 16 és 32 bites kódolás azonos ISA-specifikációt követ, ezért a bájtoknak GNU as-sel is egyezniük kell; a GNU objdump kiírási formája (decimális immediate-ek, .n/.w utótagok) eltér. A lektor GNU eszközökkel végzett ellenőrzése a 42 18, 00 EB 81 00 és a f04f 4561 = mov.w r5, #0xe1000000 értéket ugyanígy adta. A lab_thumb2_mix és a lab_add_carry rutint Unicorn-emulátorban is lefuttattuk; a közölt regiszter- és jelzőállapot ebből származik.