Obsah
1. Zápis funkcí obsahujících instrukce Thumb a Thumb-2 v MicroPythonu (2)
2. Anatomie funkcí s dekorátorem @micropython.asm_thumb
3. Strojové instrukce v prázdné funkci
5. Instrukce PUSH a POP v instrukční sadě Thumb
6. Strojové kódy funkcí popsaných minule
7. Instrukce LDR a přístup k prvkům polí
8. Instrukce nepodmíněného skoku a pseudoinstrukce label
9. Překlad instrukce nepodmíněného skoku na zadané návěští
11. Stavové registry na mikroprocesorech s architekturou ARM
12. Příznakové a stavové bity na mikroprocesorech s architekturou ARM
13. Podmínky specifikované u instrukcí skoku (condition codes)
14. Instrukce podmíněného skoku a jednoduchá počítaná programová smyčka
15. Nastavení příznakových bitů u aritmetických instrukcí
16. Zjednodušená programová smyčka
18. Doposud popsané instrukce a způsob jejich zápisu v MicroPythonu
19. Repositář s demonstračními příklady
1. Zápis funkcí obsahujících instrukce Thumb a Thumb-2 v MicroPythonu (2)
Na úvodní článek o využití instrukcí z instrukční sady Thumb a Thumb-2 v MicroPythonu dnes navážeme. Nejdříve si řekneme, jakým způsobem je možné prozkoumat funkce označené dekorátorem @micropython.asm_thumb, dále si popíšeme specifika zápisu některých instrukcí a nakonec (to ale až v navazujícím článku) provedeme benchmarky pro zjištění, zda a v jakých situacích může být propojení Pythonu s (de facto) assemblerem výhodné a kdy je to naopak pouze ztráta času vývojáře a tudíž předběžná optimalizace (premature optimization).
Připomeňme si nejdříve, jaké vlastnosti musí funkce s instrukcemi Thumb a/nebo Thumb-2 splňovat:
- Funkce musí být označena dekorátorem @micropython.asm_thumb
- Funkce může být bez parametrů nebo může akceptovat maximálně čtyři celočíselné argumenty
- Tyto argumenty musí být pojmenovány r0 až r3 (což jsou jména pracovních registrů ARMu)
- Návratová hodnota je vždy jedna a je předána v registru r0 (nepoužívá se příkaz return)
- Tyto funkce mohou obsahovat pouze symbolicky zapsané instrukce Thumb a Thumb-2 (nelze je tedy kombinovat s Pythonním kódem)
2. Anatomie funkcí s dekorátorem @micropython.asm_thumb
V úvodní části dnešního článku nás bude zajímat, jak vlastně interně vypadají funkce, které jsou označeny dekorátorem @micropython.asm_thumb. Víme již, že v těle těchto funkcí mohou být zapsány pouze strojové instrukce a nikoli příkazy Pythonu. Lze tedy předpokládat, že celá funkce bude přeložena do strojového kódu. Ovšem na začátku a konci sekvence instrukcí velmi pravděpodobně budou nějaké instrukce dodané přímo MicroPythonem. Minimálně na konci funkce je totiž nutné zajistit návrat do funkce volající. A právě pro zjištění, jak vlastně vypadá tělo funkce přeložené do strojového kódu slouží následující kód, který vznikl úpravou skriptu zmíněného v této diskuzi https://github.com/orgs/micropython/discussions/12257 (ovšem jeho autor je ještě někdo další):
import machine import array def inspect(f, nbytes=16): baddr = bytes(array.array("O", [f])) addr = int.from_bytes(baddr, "little") print("function object at: 0x%08x" % addr) print("number of args: %u" % machine.mem32[addr + 4]) code_addr = machine.mem32[addr + 8] print("machine code at: 0x%08x" % code_addr) print("-- code --") for i in range(nbytes): print(f"{machine.mem8[code_addr + i]:02x}", end=" ") print("\n----------")
Tento kód, kterému se předá reference na funkci, nejdříve získá binární obraz hlavičky funkce, z něhož zjistí dvě důležité informace – počet předávaných argumentů (ten je pevný) a taktéž adresu, na níž jsou uloženy strojové instrukce tvořící tělo funkce. Hodnoty z takto získaného bloku jsou vypsány v hexadecimálním formátu. Délka těla funkce (počet bajtů) není zjišťován (pravděpodobně bude součástí hlavičky), ale je vypsán nbytes bajtů. Délku těla funkce tedy budeme v dalších kapitolách nejprve odhadovat a teprve poté si funkci inspect vylepšíme, aby konec detekovala.
3. Strojové instrukce v prázdné funkci
Prozkoumejme nyní, jaké strojové instrukce bude obsahovat (zdánlivě) zcela prázdná funkce překládaná do strojového kódu. Taková funkce může být zapsána následovně:
@micropython.asm_thumb def no_op(): pass
Taková funkce je po zápisu do MicroPythonu ihned přeložena, takže její tělo můžeme velmi snadno prozkoumat. Použijeme přitom pomocnou funkci inspect představenou v rámci předchozí kapitoly:
>>> inspect(no_op, 20)
Výsledek by měl vypadat takto (samozřejmě se budou lišit obě vypsané adresy):
function object at: 0x20014080 number of args: 0 machine code at: 0x20014240 -- code -- f2 b5 f2 bd 00 00 00 00 00 00 00 00 00 00 00 00 de 00 07 00 ----------
Výše uvedenou sekvenci bajtů (f2, b5, …) překopírujeme do online disassembleru, jenž je dostupný na adrese https://armconverter.com/?disasm. V pravé části stránky se zobrazí výsledek disassemblingu pro různé instrukční sady. Zkusme si tedy překopírovat výše uvedenou sekvenci bajtů a následně v sekci Thumb uvidíme:
push {r1, r4, r5, r6, r7, lr} movs r0, r0 movs r0, r0 movs r0, r0 movs r0, r0 movs r0, r0 movs r0, r0 lsls r6, r3, #3 movs r7, r0 pop {r1, r4, r5, r6, r7, pc}
Z tohoto kódu je patrné (resp. bude patrné), že význam mají pouze první dvě instrukce. Zbylé bajty již do strojového kódu naší funkce nepatří:
push {r1, r4, r5, r6, r7, lr} pop {r1, r4, r5, r6, r7, pc}
Prázdná funkce tedy začíná instrukcí push a končí instrukcí pop.
4. Instrukce PUSH a POP
V mnoha ohledech jsou instrukce PUSH a POP vůbec nejsložitějšími instrukcemi na architektuře ARM. Je tomu tak z toho důvodu, že jediná instrukce dokáže uložit na zásobník (nebo naopak ze zásobníku přečíst) prakticky libovolnou kombinaci pracovních registrů r0 až r15 (s několika omezeními pro registry SP, PC a LR). Z tohoto důvodu je v původní 32bitové instrukční sadě ARM v samotném kódu instrukce rezervováno bitové pole s šestnácti bity: viz též již minule uvedený obrázek, konkrétně jeho šestý řádek. Instrukce poté pro každý registr, jehož bit je nastaven na jedničku, provede operaci PUSH či POP a nakonec příslušně upraví obsah registru SP.
5. Instrukce PUSH a POP v instrukční sadě Thumb
Původní sémantika instrukcí PUSH a POP sice zůstala zachována i v instrukční sadě Thumb, ovšem došlo k určitým omezením, protože, podobně jako všechny další instrukce z instrukční sady Thumb, musí být instrukce zakódovány v šestnácti bitech (tudíž se do instrukčního slova nemůže vejít 16bitové pole).
V případě instrukce PUSH lze pracovat se spodními osmi registry R0 až R7 a taktéž s registrem LR (link register), jenž obsahuje návratovou adresu:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 0 | 1 | 1 | 0 | 1 | 0 | LR| bitové pole registrů | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
To například znamená, že můžeme zakódovat tyto operace:
Kód instrukce (hex) | Provedená operace |
---|---|
f2 b4 | push {r1, r4, r5, r6, r7} |
f2 b5 | push {r1, r4, r5, r6, r7, lr} |
ff b5 | push {r0, r1, r2, r3, r4, r5, r6, r7, lr} |
01 b4 | push {r0} |
00 b5 | push {lr} |
Instrukce POP má nepatrně odlišné kódování a namísto registru LR lze obnovit registr PC. Bitové pole pro spodním osm pracovních registrů však zůstalo zachováno i zde:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 0 | 1 | 1 | 1 | 1 | 0 | PC| bitové pole registrů | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Opět se podívejme, jaké konkrétní operace můžeme provést:
Kód instrukce (hex) | Provedená operace |
---|---|
00 bd | pop {pc} |
f2 bc | pop {r1, r4, r5, r6, r7} |
f2 bd | pop {r1, r4, r5, r6, r7, pc} |
ff bc | pop {r0, r1, r2, r3, r4, r5, r6, r7} |
ff bd | pop {r0, r1, r2, r3, r4, r5, r6, r7, pc} |
A opět platí, že 00 bc (tedy neobnovení žádného registru) je neplatnou operací.
Vraťme se tedy k původním instrukcím, které byly nalezeny v přeloženém nativním kódu:
push {r1, r4, r5, r6, r7, lr} pop {r1, r4, r5, r6, r7, pc}
První z těchto instrukcí uloží obsahy pěti specifikovaných pracovních registrů na zásobník. A taktéž na něj uloží obsah registru LR (link register), jenž obsahuje návratovou adresu. A druhá instrukce, která bude provedena jako poslední, obnoví všech pět zmíněných pracovních registrů a poté do registru PC (program counter) vloží adresu, která byla původně uložena v registru LR – tedy provede automatický návrat z funkce do volajícího kódu!
Pokud budeme chtít tyto instrukce zapsat v MicroPythonu, je nutné jména registrů vložit do množin:
push({r1}) push({r1, r2, r7}) push({r0, r1, r2, r3, r4, r5, r6, r7}) pop({r1}) pop({r1, r2, r7}) pop({r0, r1, r2, r3, r4, r5, r6, r7})
Na pořadí prvků v množinách nezáleží; není tedy možné například prohození obsahu dvou registrů přes zásobník tímto trikem:
push({r1, r2}) pop({r2, r1})
6. Strojové kódy funkcí popsaných minule
Pro zajímavost se podívejme na způsob překladu funkcí, které jsme si popsali v úvodním článku. Začneme funkcí, která vrací hodnotu 42 (což se děje přes obsah pracovního registru R0, jak již dobře víme). Zdrojový kód funkce vypadá takto:
@micropython.asm_thumb def return_constant(): mov(r0, 42)
Strojový kód získaný překladem provedeným MicroPythonem:
>>> inspect(return_constant, 20) function object at: 0x20014a20 number of args: 0 machine code at: 0x20014be0 -- code -- f2 b5 2a 20 f2 bd 00 00 00 00 00 00 00 00 00 00 de 00 07 00 ----------
Ve strojovém kódu nalezneme pouze šest bajtů se třemi instrukcemi. První a poslední z nich již dobře známe, takže přibyla pouze prostřední instrukce naplňující pracovní registr r0 konstantou:
push {r1, r4, r5, r6, r7, lr} movs r0, #0x2a pop {r1, r4, r5, r6, r7, pc}
Mimochodem, tato instrukce zakódována v této dvojici bajtů:
2a 20
První bajt obsahuje konstantu 42 a druhý kód instrukce (uložení little endian):
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | konstanta | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Což přesně odpovídá formátu, který jsme si uvedli minule:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 1 |operace| Rd/Rn | konstanta | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
V tomto případě je operace zakódována jako 00 (mov) a Rd má index 000 neboli R0.
Dále prozkoumejme funkci, která sečte své čtyři argumenty. Připomeňme si, že tato funkce vypadá takto:
@micropython.asm_thumb def add_four(r0, r1, r2, r3): add(r0, r0, r1) add(r0, r0, r2) add(r0, r0, r3)
Způsob jejího překladu do strojového kódu:
>>> inspect(add_four, 20) function object at: 0x20014fc0 number of args: 4 machine code at: 0x20015180 -- code -- f2 b5 40 18 80 18 c0 18 f2 bd 00 00 00 00 00 00 de 00 07 00 ----------
Druhá instrukce je zakódována bajty 40 18, což znamená:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 1 | 0 | A | Rm=R1 | Rs=R0 | Rd=R0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Třetí instrukce je zakódována bajty 80 18:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 1 | 0 | A | Rm=R2 | Rs=R0 | Rd=R0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
A konečně čtvrtá instrukce je zakódována bajty c0 18:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 1 | 0 | A | Rm=R3 | Rs=R0 | Rd=R0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
To zcela přesně odpovídá disassemblované sekvenci instrukcí:
push {r1, r4, r5, r6, r7, lr} adds r0, r0, r1 adds r0, r0, r2 adds r0, r0, r3 pop {r1, r4, r5, r6, r7, pc}
7. Instrukce LDR a přístup k prvkům polí
Minule jsme si taktéž ukázali využití instrukce LDR (load register) určené pro naplnění obsahu pracovního registru libovolnou 32bitovou hodnotou (což nám standardní instrukce MOV neumožňuje). Připomeňme si, že jsme si museli vypomoci polem s jediným 32bitovým prvkem, jehož adresa byla předána do funkce s instrukcemi Thumb:
from array import array control = array('I', [100000]) @micropython.asm_thumb def return_big_constant(r0): ldr(r0, [r0, 0]) return_big_constant(control)
Vygenerovaný strojový kód získáme opět pomocí inspect:
>>> inspect(return_big_constant, 20) function object at: 0x20015670 number of args: 1 machine code at: 0x200158c0 -- code -- f2 b5 00 68 f2 bd 00 00 00 00 00 00 00 00 00 00 de 00 07 00 ----------
Tento kód neobsahuje žádné záhady, pouze instrukci LDR, která akceptuje adresu v registru R0 a 32bitové slovo na této adrese uloží taktéž do registru R0:
push {r1, r4, r5, r6, r7, lr} ldr r0, [r0] pop {r1, r4, r5, r6, r7, pc}
8. Instrukce nepodmíněného skoku a pseudoinstrukce label
Většina strojových instrukcí se zpracovává sekvenčně tak, jak jsou zapsány za sebou. Výjimkou je instrukce IT (ta však například na Cortex-M0+ chybí), a dále instrukce přímo či nepřímo manipulující s obsahem registru PC (Program Counter neboli programový čítač). Jednu z těchto instrukcí už jsme viděli – jedná se o instrukci POP v případě, že má nastaven příznakový bit „obnov registr PC“. Ovšem kromě toho lze programový čítač modifikovat i instrukcemi skoku, resp. rozeskoku, protože na architektuře ARM jsou tyto instrukce nazývány branch a nikoli jump.
Nejjednodušší je v tomto ohledu instrukce pojmenovaná jediným písmenem: B. Jedná se o nepodmíněný skok na zadanou adresu. V instrukčním slovu této instrukce je uložen jedenáctibitový offset, takže nepodmíněné skoky jsou v tomto ohledu pouze „lokální“; konkrétně v rozsahu 2kB. To však většinou nevadí, protože se stejně neprovádí skoky mimo právě prováděnou proceduru (potom by se jednalo o instrukci BL neboli branch and link.
Formát instrukce B již známe:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 1 | 1 | 0 | 0 | 11bitový offset | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Cíl skoku musí být pojmenovaný. V klasických assemblerech se pro tento účel používají takzvaná návěští (label), z nichž se v MicroPythonu stala pseudofunkce pojmenovaná taktéž label. Způsob jejího použití je ukázán v navazující kapitole.
9. Překlad instrukce nepodmíněného skoku na zadané návěští
Kombinaci instrukce B s návěštím si ukážeme na velmi jednoduchém demonstračním příkladu, který obsahuje instrukci skoku, jenž přeskočí druhou instrukci MOV (jakoby byla umístěna do větve if False:). Tento příklad je samozřejmě umělý, ovšem jedná se o tu nejjednodušší možnost, jak návěští a skok použít:
@micropython.asm_thumb def branch(): mov(r0, 42) b(cil_skoku) mov(r0, 99) label(cil_skoku)
V případě, že tuto funkci zavoláme ve smyčce REPL MicroPythonu, měla by se vypsat hodnota 42 vrácená v pracovním registru R0. To tedy znamená, že instrukce mov r0, 99 se nespustila:
>>> branch() 42
Zajímavé bude se podívat na způsob překladu výše uvedené sekvence instrukcí do strojového kódu:
>>> inspect(branch) function object at: 0x20008730 number of args: 0 machine code at: 0x200088c0 -- code -- f2 b5 2a 20 00 e0 63 20 f2 bd 00 00 00 00 00 00 ----------
Disassembler v této sekvenci bajtů korektně našel pětici instrukcí. Povšimněte si hodnoty u instrukce B. Tato hodnota značí absolutní adresu (první instrukce je na adrese 0, druhá na adrese 2, atd. atd.). Tato hodnota byla dopočítána disassemblerem:
push {r1, r4, r5, r6, r7, lr} movs r0, #0x2a b #8 movs r0, #0x63 pop {r1, r4, r5, r6, r7, pc}
Ve skutečnosti je totiž instrukce skoku reprezentována těmito dvěma bajty:
00 e0
Což můžeme snadno rozkódovat tak, že zapsaný offset je nulový. Je tomu tak proto, že PC již při výpočtu cílové adresy obsahuje hodnotu zvýšenou o 4:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 1 | 1 | 0 | 0 | 11bitový offset | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Abychom si ukázali roli offsetu, zkusme nyní přeskočit nikoli jednu, ale tři instrukce:
@micropython.asm_thumb def branch(): mov(r0, 42) b(cil_skoku) mov(r0, 99) mov(r0, 0) mov(r0, 1) label(cil_skoku)
Překlad do strojového kódu vypadá následovně:
>>> inspect(branch) function object at: 0x20008ff0 number of args: 0 machine code at: 0x200091b0 -- code -- f2 b5 2a 20 02 e0 63 20 00 20 01 20 f2 bd 00 00 ----------
Zpětný překlad disassemblerem ukazuje, že skok bude proveden na adresu 0×c (opět se tedy počítá s tím, že kód začíná na nule):
push {r1, r4, r5, r6, r7, lr} movs r0, #0x2a b #0xc movs r0, #0x63 movs r0, #0 movs r0, #1 pop {r1, r4, r5, r6, r7, pc}
Ovšem samotná instrukce skoku je zakódována do následující dvojice bajtů:
02 e0
První bajt v tomto případě obsahuje offset (=2), a to z toho důvodu, že všechny instrukce Thumb jsou 16bitové a nemá tedy smysl počítat s lichými adresami (dolní bit offsetu tudíž bude vždy nulový a proto ho není zapotřebí nikde ukládat):
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 1 | 1 | 0 | 0 | 11bitový offset | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
10. Skok zpět
Jenže výpočet offsetu nás přivádí k dalšímu problému, a to konkrétně ke skoku zpět – na nižší adresy. To je samozřejmě potřebná instrukce používaná pro implementaci programových smyček atd. Vyzkoušejme si takový zpětný skok naprogramovat:
@micropython.asm_thumb def branch(): mov(r0, 42) b(cil_skoku) label(skok_zpet) mov(r0, 99) b(skok_zpet) mov(r0, 1) label(cil_skoku)
Opět si necháme provést zpětný překlad (disassembling) na základě bajtů se strojovým kódem:
>>> inspect(branch) function object at: 0x200095c0 number of args: 0 machine code at: 0x20009780 -- code -- f2 b5 2a 20 02 e0 63 20 fd e7 01 20 f2 bd 00 00 ----------
Výsledek disassemblingu ukazuje, že cíle skoku jsou v pořádku, a to i u druhé instrukce B provádějící skok zpět, tj. na nižší adresy:
push {r1, r4, r5, r6, r7, lr} movs r0, #0x2a b #0xc movs r0, #0x63 b #6 movs r0, #1 pop {r1, r4, r5, r6, r7, pc}
Samotná instrukce druhého skoku je zakódována následovně:
fd e7
Nyní je offset binárně reprezentován hodnotou 11111111101. Jedná se o hodnotu v dvojkovém doplňku a tudíž lze snadno spočítat, že desítkový offset reprezentovaný tímto binárním číslem je –3:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 1 | 1 | 0 | 0 | 11bitový offset | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Skok zpět na samou instrukci skoku by měl mít offset –2, takže offset=-3 značí skok na instrukci uvedenou těsně před instrukcí skoku.
11. Stavové registry na mikroprocesorech s architekturou ARM
Kromě patnácti 32bitových pracovních registrů a programového čítače obsahují mikroprocesory s architekturou ARM i registry, v nichž se uchovávají různé příznaky. V uživatelském režimu se pracuje s příznaky uloženými v registru nazvaném CPSR (Current Program Status Register) a pro každý další režim existuje navíc zvláštní registr nazvaný SPSR (Saved Program Status Register), v němž jsou uchovány původní příznaky ze CPSR. Podobně jako všechny pracovní registry, mají i registry CPSR a SPSR shodnou šířku 32 bitů, což má svoje výhody. Mimo jiné i to, že šířka 32 bitů ponechala konstruktérům procesorů ARM mnoho prostoru pro uložení různých důležitých informací do registrů CPSR/SPSR, takže se nemuseli uchylovat k nepříliš promyšleným technikám známým například z platformy x86, kde se původně šestnáctibitový registr FLAGS (8086) postupně změnil na 32bitový registr EFLAGS (80386), vedle něho vznikl registr MSW (80286) rozšířený na CR0 atd.
12. Příznakové a stavové bity na mikroprocesorech s architekturou ARM
Ve výše zmíněných stavových registrech CPSR/SPSR mikroprocesorů ARM jsou uloženy především příznakové bity nastavované aritmeticko-logickou jednotkou při provádění základních aritmetických instrukcí či bitových operací, dále pak bity určující, jakou instrukční sadu mikroprocesor v daný okamžik zpracovává (ARM, Thumb, Jazelle – na Cortex-M se nemění), příznak pořadí zpracovávání bajtů (little/big endian) a taktéž příznaky používané u SIMD operací. Zdaleka ne všechny mikroprocesory ARM však skutečně pracují se všemi bity, což je logické, protože například příznak Q je používán jen u mikroprocesorů podporujících aritmetiku se saturací, příznak J u čipů s podporou technologie Jazelle atd. Pojďme si tedy jednotlivé příznakové i stavové bity vypsat:
Příznak | Význam zkratky | Poznámka |
---|---|---|
N | negative | výsledek ALU operace je záporný |
V | overflow | přetečení (znaménková aritmetika, signed) |
Z | zero | výsledek je nulový |
C | carry | přenos (bezznaménková aritmetika, unsigned) |
Q | sticky overflow | aritmetika se saturací, od ARMv5e výše |
I | interrupt | zákaz IRQ (přerušení) |
F | fast interrupt | zákaz FIRQ (rychlého přerušení) |
T | thumb | příznak zpracování instrukční sady Thumb (jen u procesorů se znakem „T“ v názvu) |
J | jazelle | příznak zpracování instrukční sady Jazelle (jen u procesorů se znakem „J“ v názvu) |
E | endianness | pořadí bajtů při práci s RAM (big/little endian) |
GE | 4 bity | použito u SIMD operací (pouze některé čipy) |
IF | 5 bitů | použito u instrukcí Thumb2 (pouze některé čipy) |
M | 5 bitů | režim práce mikroprocesoru (user, IRQ, FIRQ, …) |
13. Podmínky specifikované u instrukcí skoku (condition codes)
U instrukce podmíněného skoku B(podmínka) lze uvést 14 různých podmínek (15 pokud počítáme i Any/Always).
První sada podmínkových kódů se používá pro provedení či naopak neprovedení instrukce na základě hodnoty jednoho z příznakových bitů zero, overflow či negative. Poslední podmínkový kód z této skupiny má název AL (Any/Always) a značí, že se instrukce provede v každém případě (ovšem u instrukční sady Thumb nelze použít):
Kód | Přípona | Význam | Testovaná podmínka |
---|---|---|---|
0000 | EQ | Z set | rovnost (či nulový výsledek) |
0001 | NE | Z clear | nerovnost (či nenulový výsledek) |
0100 | MI | N set | výsledek je záporný |
0101 | PL | N clear | výsledek je kladný či 0 |
0110 | VS | V set | nastalo přetečení |
0111 | VC | V clear | nenastalo přetečení |
1110 | AL | Any/Always | většinou se nezapisuje, implicitní podmínka |
Další čtyři podmínkové kódy se většinou používají při porovnávání dvou hodnot bez znaménka (unsigned). V těchto případech se testují stavy příznakových bitů carry a zero, přesněji řečeno kombinací těchto bitů:
Kód | Přípona | Význam | Testovaná podmínka |
---|---|---|---|
0010 | CS/HS | C set | >= |
0011 | CC/LO | C clear | < |
1000 | HI | C set and Z clear | > |
1001 | LS | C clear or Z set | <= |
Poslední čtyři podmínkové kódy se používají pro porovnávání hodnot se znaménkem (signed). V těchto případech se namísto příznakových bitů carry a zero testují kombinace bitů negative, overflow a zero:
Kód | Přípona | Význam | Testovaná podmínka |
---|---|---|---|
1010 | GE | N and V the same | >= |
1011 | LT | N and V differ | < |
1100 | GT | Z clear, N == V | > |
1101 | LE | Z set, N != V | <= |
14. Instrukce podmíněného skoku a jednoduchá počítaná programová smyčka
Již v předchozím článku jsme si řekli, že pro skoky s podmínkou je v instrukčním slovu v sadě Thumb rezervován jen osmibitový offset, protože celkem čtyři bity zabírají kódy podmínky popsané v předchozí kapitole a několik bitů bylo nutné rezervovat pro operační kód instrukce. Samotné instrukční slovo vypadá následovně:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 1 | 1 | 0 | 1 | podmínka | 8bitový offset | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Podmíněné skoky jsou tedy lokální, což ovšem dává smysl, protože jsou používány pro lokální rozvětveni či pro realizaci programových smyček všech typů.
Pojďme si nyní naprogramovat počítanou programovou smyčku. Počitadlem bude pracovní registr R1 a v samotné smyčce budeme hodnotu počitadla postupně snižovat o jedničku. Jakmile se dosáhne nuly, bude programová smyčka ukončena. A abychom viděli, že smyčka skutečně proběhla, budeme v ní postupně zvyšovat hodnotu uloženou v pracovním registru R0 (a víme již, že hodnota v tomto registru bude návratovou hodnotou z funkce).
Realizace v assembleru:
mov r0, #0 mov r1, #100 @ pocatecni hodnota pocitadla loop: add r0, r0, #1 @ telo smycky sub r1, r1, #1 @ zmenseni pocitadla cmp r2, #0 @ otestovani, zda jsme jiz nedosahli nuly bne loop @ pokud jsme se nedostali k nule, skok na zacatek smycky
Realizace téhož algoritmu v MicroPythonu:
@micropython.asm_thumb def loop(): mov(r0, 0) mov(r1, 100) # počáteční hodnota počitadla label(loop) # označení začátku programové smyčky add(r0, r0, 4) # tělo smyčky sub(r1, r1, 1) # snížení hodnoty počitadla cmp(r1, 0) # test na nulu bne(loop) # skoku v případě že se nedosáhlo nuly
V případě, že tuto funkci spustíme, měla by se zobrazit návratová hodnota 400, protože se stokrát zvýšila hodnota registru R0 o konstantu 4:
>>> loop() 400
Samozřejmě nezapomeneme na prozkoumání strojového kódu, který vznikl překladem výše uvedené funkce:
>>> inspect(loop) function object at: 0x2000ace0 number of args: 0 machine code at: 0x2000af10 -- code -- f2 b5 00 20 64 21 00 1d 49 1e 00 29 fb d1 f2 bd ----------
Online disassembler nám prozradí způsob překladu:
push {r1, r4, r5, r6, r7, lr} movs r0, #0 movs r1, #0x64 adds r0, r0, #4 subs r1, r1, #1 cmp r1, #0 bne #6 pop {r1, r4, r5, r6, r7, pc}
15. Nastavení příznakových bitů u aritmetických instrukcí
Ve skutečnosti je ovšem výše uvedená programová smyčka realizována neefektivním způsobem. Je tomu tak z toho důvodu, že už samotná instrukce SUBS nastavuje příznakové bity. My jsme sice při zápisu funkce loop použili pseudofunkci nazvanou sub, ta ovšem byla ve skutečnosti přeložena do instrukce SUBS (ono S na konci signalizuje nastavení příznaků).
U původní instrukční sady ARM32 bylo možné zvolit, zda mají aritmetické instrukce nastavovat stavové bity (používal se pro to takzvaný S-bit), ovšem u instrukční sady Thumb je chování instrukcí plně určeno jak typem instrukce, tak i typem operandů. U instrukcí součtu vypadají dostupné varianty následovně:
Instrukce | Nastavované příznaky | Prováděná operace |
---|---|---|
ADDS Rd, Rn, #<imm> | N Z C V | Rd := Rn + imm |
ADDS Rd, Rn, Rm | N Z C V | Rd := Rn + Rm |
ADD Rd, Rd, Rm | Rd := Rd + Rm | |
ADDS Rd, Rd, #<imm> | N Z C V | Rd := Rd + imm |
ADCS Rd, Rd, Rm | N Z C V | Rd := Rd + Rm + C-bit |
ADD SP, SP, #<imm> | SP := SP + imm | |
ADD Rd, SP, #<imm> | Rd := SP + imm | |
ADR Rd, <label> | Rd := label |
Podobně je tomu i u instrukcí rozdílu, jak je to ostatně patrné z následující tabulky:
Instrukce | Nastavované příznaky | Prováděná operace |
---|---|---|
SUBS Rd, Rn, Rm | N Z C V | Rd := Rn – Rm |
SUBS Rd, Rn, #<imm> | N Z C V | Rd := Rn – imm |
SUBS Rd, Rd, #<imm> | N Z C V | Rd := Rd – imm |
SBCS Rd, Rd, Rm | N Z C V | Rd := Rd – Rm – NOT C-bit |
SUB SP, SP, #<imm> | SP := SP – imm |
16. Zjednodušená programová smyčka
To tedy mj. znamená, že instrukci CMP (porovnání) můžeme z naší programové smyčky zcela vynechat, protože samotné snížení počitadla nastaví příslušné příznaky (v našem konkrétním případě příznak nulovosti).
Realizace v assembleru:
mov r0, #0 mov r1, #100 @ pocatecni hodnota pocitadla loop: add r0, r0, #1 @ telo smycky subs r1, r1, #1 @ zmenseni pocitadla + nastaveni priznaku bne loop @ pokud jsme se nedostali k nule, skok na zacatek smycky
Realizace téhož algoritmu v MicroPythonu:
@micropython.asm_thumb def loop(): mov(r0, 0) mov(r1, 100) # počáteční hodnota počitadla label(loop) # označení začátku programové smyčky add(r0, r0, 4) # tělo smyčky sub(r1, r1, 1) # snížení hodnoty počitadla + nastavení příznaků bne(loop) # skok v případě že se nedosáhlo nuly
17. Vnořené programové smyčky
Nic nám pochopitelně nebrání v naprogramování dvojice vnořených počítaných smyček. Každá z těchto smyček použije jako svoje počitadlo odlišný pracovní registr (máme jich prozatím dostatek). Konkrétně pro vnější smyčku využijeme registr R1 a pro smyčku vnitřní registr R2:
@micropython.asm_thumb def loop(): mov(r0, 0) mov(r1, 100) # počáteční hodnota počitadla vnější smyčky label(outer_loop) # označení začátku vnější programové smyčky mov(r2, 100) # počáteční hodnota počitadla vnitřní smyčky label(inner_loop) # označení začátku vnitřní programové smyčky add(r0, r0, 1) # tělo smyčky sub(r2, r2, 1) # snížení hodnoty počitadla + nastavení příznaků bne(inner_loop) # opakování vnitřní smyčky sub(r1, r1, 1) # snížení hodnoty počitadla + nastavení příznaků bne(outer_loop) # opakování vnější smyčky
Výsledkem této funkce by měla být hodnota 10000, protože hodnota v pracovním registru R0 je zvýšena celkem 100×100=10000×:
>>> loop() 10000
Prozkoumejme nyní tělo funkce přeložené do strojového kódu. Tentokrát už musíme explicitně zvětšit počet zobrazených bajtů, zde konkrétně na 20, protože v těle funkce jsme použili osm instrukcí (16 bajtů) a další dvě instrukce jsou přidány na začátek (PUSH) i na konec (POP) funkce:
>>> inspect(loop, nbytes=20) function object at: 0x2000bde0 number of args: 0 machine code at: 0x2000bfa0 -- code -- f2 b5 00 20 64 21 64 22 40 1c 52 1e fc d1 49 1e f9 d1 f2 bd ----------
Výsledek disassemblingu vypadá následovně:
push {r1, r4, r5, r6, r7, lr} movs r0, #0 movs r1, #0x64 movs r2, #0x64 adds r0, r0, #1 subs r2, r2, #1 bne #8 subs r1, r1, #1 bne #6 pop {r1, r4, r5, r6, r7, pc}
18. Doposud popsané instrukce a způsob jejich zápisu v MicroPythonu
Prozatím jsme se seznámili se zápisem následujících instrukcí v MicroPythonu:
Instrukce | Zápis v MicroPythonu | Stručný popis |
---|---|---|
mov r0, #0 | mov(r0, 0) | uložení krátké konstanty do pracovního registru |
movw r0, #1000 | movw(r0, 1000) | uložení 16bitového slova do pracovního registru |
ldr r0, [r0, 0] či ldr r0, =adresa | ldr(r0, [r0, 0]) | přečtení 32bitového slova ze zadané adresy |
adds r1, r1, #1 | add(r0, r0, 1) | součet registru s konstantou, zápis do obecně jiného registru |
subs r1, r1, #1 | sub(r1, r1, 1) | rozdíl, zde konkrétně snížení hodnoty registru R1 o jedničku s nastavením příznaků |
cmp R1, #0 | cmp(r1, 0) | porovnání obsahu zvoleného pracovního registru s krátkou konstantou a nastavení příznaků |
b cil_skoku | b(cil_skoku) | nepodmíněný skok na zadanou adresu (omezený rozsah, typicky pro jednu funkci/subrutinu) |
bne cil_skoku či b.ne cil_skoku | bne(cil_skoku) | podmíněný skok za podmínky, že příznak Z (zero) je nastaven |
push {registry} | push({registry}) | uložení vybraných pracovních registrů na zásobník |
pop {registry} | push({registry}) | obnovení vybraných pracovních registrů ze zásobníku |
19. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro MicroPython běžící na čipech s architekturou Cortex-M0+, popř. Cortex-M3/M4 (a otestovaných na RP2040) byly uloženy do repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs:
20. Odkazy na Internetu
- Online ARM converter
https://armconverter.com/?disasm - Fast Filters for the Pyboard
https://github.com/peterhinch/micropython-filters - How to load 32 bit constant from assembler with @micropython.asm_thumb
https://forum.micropython.org/viewtopic.php?f=21&t=12931&sid=25de8871fa9cfcf8cafb6318f9d8ba3a - Pi pico, micropython.asm_thumb: ADR Rd, <label> and LDR Rd, <label> not implemented?
https://github.com/orgs/micropython/discussions/12257 - MicroPython documentation
https://docs.micropython.org/en/latest/index.html - Inline assembler for Thumb2 architectures
https://docs.micropython.org/en/latest/reference/asm_thumb2_index.html - Inline assembler in MicroPython
https://docs.micropython.org/en/latest/pyboard/tutorial/assembler.html#pyboard-tutorial-assembler - MCU market turns to 32-bits and ARM
http://www.eetimes.com/document.asp?doc_id=1280803 - Cortex-M0 Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0.php - Cortex-M0+ Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0plus.php - ARM Processors in a Mixed Signal World
http://www.eeweb.com/blog/arm/arm-processors-in-a-mixed-signal-world - RISCové mikroprocesory s komprimovanými instrukčními sadami
https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami/ - RISCové mikroprocesory s komprimovanými instrukčními sadami (2)
https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami-2/ - ARM Architecture (Wikipedia)
https://en.wikipedia.org/wiki/ARM_architecture - Cortex-M0 (Wikipedia)
https://en.wikipedia.org/wiki/ARM_Cortex-M0 - Cortex-M0+ (Wikipedia)
https://en.wikipedia.org/wiki/ARM_Cortex-M#Cortex-M0.2B - Improving ARM Code Density and Performance
New Thumb Extensions to the ARM Architecture Richard Phelan - The ARM Processor Architecture
http://www.arm.com/products/processors/technologies/instruction-set-architectures.php - Thumb-2 instruction set
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344c/Beiiegaf.html - Introduction to ARM thumb
http://www.eetimes.com/discussion/other/4024632/Introduction-to-ARM-thumb - ARM, Thumb, and ThumbEE instruction sets
http://www.keil.com/support/man/docs/armasm/armasm_CEGBEIJB.htm - An Introduction to ARM Assembly Language
http://dev.emcelettronica.com/introduction-to-arm-assembly-language - Processors – ARM
http://www.arm.com/products/processors/index.php - The ARM Instruction Set
http://simplemachines.it/doc/arm_inst.pdf - The Thumb instruction set
http://apt.cs.manchester.ac.uk/ftp/pub/apt/peve/PEVE05/Slides/05_Thumb.pdf - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Assembler pod Linuxem
http://phoenix.inf.upol.cz/linux/prog/asm.html - AT&T Syntax versus Intel Syntax
https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html - Linux Assembly website
http://asm.sourceforge.net/