Zápis funkcí obsahujících instrukce Thumb a Thumb-2 v MicroPythonu (2)

30. 1. 2024
Doba čtení: 29 minut

Sdílet

Ilustrační snímek Autor: Depositphotos
Ilustrační snímek
Ve druhé části článku o využití instrukcí z instrukční sady Thumb a Thumb-2 v MicroPythonu si řekneme, jaký prozkoumat strojový kód funkcí označených dekorátorem @micropython.asm_thumb.

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

4. Instrukce PUSH a POP

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í

10. Skok zpě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

17. Vnořené programové smyčky

18. Doposud popsané instrukce a způsob jejich zápisu v MicroPythonu

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

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:

  1. Funkce musí být označena dekorátorem @micropython.asm_thumb
  2. Funkce může být bez parametrů nebo může akceptovat maximálně čtyři celočíselné argumenty
  3. Tyto argumenty musí být pojmenovány r0r3 (což jsou jména pracovních registrů ARMu)
  4. Návratová hodnota je vždy jedna a je předána v registru r0 (nepoužívá se příkaz return)
  5. Tyto funkce mohou obsahovat pouze symbolicky zapsané instrukce Thumb a Thumb-2 (nelze je tedy kombinovat s Pythonním kódem)
Poznámka: v žádném případě se nejedná o samoúčelná omezení, protože jak uvidíme dále, je interní formát těchto funkcí odlišný od běžných Pythonovských funkcí.

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/mi­cropython/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.

Poznámka: interní struktura přeložených funkcí není nikde přesně specifikována a může se v dalších verzích MicroPythonu změnit!

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ů r0r15 (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.

Poznámka: to není tak jednoduchá operace, jak by se mohlo na první pohled zdát, protože mikroprocesor musí reagovat na přerušení atd.

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}
Poznámka: ovšem minimálně jeden pracovní registr je nutné vybrat, protože operační kód 00 b4 nemá význam.

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!

Poznámka: obsah registrů R2 a R3 je tedy ztracen a registr R0 se neobnovuje proto, že bude obsahovat návratovou hodnotu z funkce.

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})
Poznámka: to plně odpovídá sémantice obou instrukcí, v nichž je pořadí ukládaných či obnovovaných registrů taktéž pevně dané.

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.

Poznámka: tyto výpočty pochopitelně nemusíme dělat ručně; právě z tohoto důvodu máme k dispozici pseudofunkci label.

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.

Poznámka: na některých mikrořadičích Cortex-M se používá jen jediný režim a tedy i jediný registr PSR.

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
Poznámka: suffix „S“ tedy znamená, že instrukce nastavuje všechny čtyři příznakové bity.

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
Poznámka: teoreticky by sice bylo možné využít i instrukce CBZ či CBNZ (tedy porovnání s nulou a skok v případě nulovosti nebo naopak nenulovosti), ovšem tyto instrukce nejsou na čipech Cortex-M0+ dostupné.

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

bitcoin_skoleni

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:

# Demonstrační příklad Stručný popis příkladu Cesta
1 return_constant.py návratová hodnota z funkce s Thumb instrukcemi https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/return_constant.py
2 return_big_constant1.py pokus o vrácení příliš velké konstanty (nekorektní) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/return_big_constant1.py
3 return_big_constant2.py pokus o vrácení příliš velké konstanty (nekorektní na Cortex-M0+) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/return_big_constant2.py
4 return_big_constant3.py vrácení 32bitové konstanty https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/return_big_constant3.py
       
5 inc1.py předání argumentu do funkce s Thumb instrukcemi https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/inc1.py
6 inc2.py pokus o předání argumentu v parametru špatného jména https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/inc2.py
       
7 add.py součet dvou předaných argumentů https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/add.py
8 add_four.py součet čtyř předaných argumentů (nekorektní) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/add_four.py
9 add_five.py součet pěti předaných argumentů https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/add_five.py
       
10 inspect_function.py získání strojového kódu přeložených funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/inspect_function.py
11 no_op.py funkce bez příkazů, která se má přeložit do strojového kódu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/no_op.py
12 branch1.py využití instrukce nepodmíněného skoku https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/branch1.py
13 branch2.py skok dopředu o několik instrukcí https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/branch2.py
14 branch3.py skok vzad https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/branch3.py
15 loop1.py programová smyčka s počitadlem a podmíněným skokem https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop1.py
16 loop2.py zjednodušená varianta programové smyčky https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop2.py
17 loop3.py vnořené programové smyčky https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop3.py

20. Odkazy na Internetu

  1. Online ARM converter
    https://armconverter.com/?disasm
  2. Fast Filters for the Pyboard
    https://github.com/peterhin­ch/micropython-filters
  3. How to load 32 bit constant from assembler with @micropython.asm_thumb
    https://forum.micropython­.org/viewtopic.php?f=21&t=12931&sid=25­de8871fa9cfcf8cafb6318f9d8ba3a
  4. Pi pico, micropython.asm_thumb: ADR Rd, <label> and LDR Rd, <label> not implemented?
    https://github.com/orgs/mi­cropython/discussions/12257
  5. MicroPython documentation
    https://docs.micropython.or­g/en/latest/index.html
  6. Inline assembler for Thumb2 architectures
    https://docs.micropython.or­g/en/latest/reference/asm_thum­b2_index.html
  7. Inline assembler in MicroPython
    https://docs.micropython.or­g/en/latest/pyboard/tutori­al/assembler.html#pyboard-tutorial-assembler
  8. MCU market turns to 32-bits and ARM
    http://www.eetimes.com/do­cument.asp?doc_id=1280803
  9. Cortex-M0 Processor (ARM Holdings)
    http://www.arm.com/produc­ts/processors/cortex-m/cortex-m0.php
  10. Cortex-M0+ Processor (ARM Holdings)
    http://www.arm.com/produc­ts/processors/cortex-m/cortex-m0plus.php
  11. ARM Processors in a Mixed Signal World
    http://www.eeweb.com/blog/arm/arm-processors-in-a-mixed-signal-world
  12. RISCové mikroprocesory s komprimovanými instrukčními sadami
    https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami/
  13. RISCové mikroprocesory s komprimovanými instrukčními sadami (2)
    https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami-2/
  14. ARM Architecture (Wikipedia)
    https://en.wikipedia.org/wi­ki/ARM_architecture
  15. Cortex-M0 (Wikipedia)
    https://en.wikipedia.org/wi­ki/ARM_Cortex-M0
  16. Cortex-M0+ (Wikipedia)
    https://en.wikipedia.org/wi­ki/ARM_Cortex-M#Cortex-M0.2B
  17. Improving ARM Code Density and Performance
    New Thumb Extensions to the ARM Architecture Richard Phelan
  18. The ARM Processor Architecture
    http://www.arm.com/produc­ts/processors/technologies/in­struction-set-architectures.php
  19. Thumb-2 instruction set
    http://infocenter.arm.com/hel­p/index.jsp?topic=/com.ar­m.doc.ddi0344c/Beiiegaf.html
  20. Introduction to ARM thumb
    http://www.eetimes.com/dis­cussion/other/4024632/Intro­duction-to-ARM-thumb
  21. ARM, Thumb, and ThumbEE instruction sets
    http://www.keil.com/suppor­t/man/docs/armasm/armasm_CEG­BEIJB.htm
  22. An Introduction to ARM Assembly Language
    http://dev.emcelettronica­.com/introduction-to-arm-assembly-language
  23. Processors – ARM
    http://www.arm.com/produc­ts/processors/index.php
  24. The ARM Instruction Set
    http://simplemachines.it/doc/ar­m_inst.pdf
  25. The Thumb instruction set
    http://apt.cs.manchester.ac­.uk/ftp/pub/apt/peve/PEVE05/Sli­des/05_Thumb.pdf
  26. Why Learn Assembly Language?
    http://www.codeproject.com/Ar­ticles/89460/Why-Learn-Assembly-Language
  27. Is Assembly still relevant?
    http://programmers.stackex­change.com/questions/95836/is-assembly-still-relevant
  28. Why Learning Assembly Language Is Still a Good Idea
    http://www.onlamp.com/pub/a/on­lamp/2004/05/06/writegreat­code.html
  29. Assembly language today
    http://beust.com/weblog/2004/06/23/as­sembly-language-today/
  30. Assembler: Význam assembleru dnes
    http://www.builder.cz/rubri­ky/assembler/vyznam-assembleru-dnes-155960cz
  31. Assembler pod Linuxem
    http://phoenix.inf.upol.cz/li­nux/prog/asm.html
  32. AT&T Syntax versus Intel Syntax
    https://www.sourceware.or­g/binutils/docs-2.12/as.info/i386-Syntax.html
  33. Linux Assembly website
    http://asm.sourceforge.net/
ikonka

Zajímá vás toto téma? Chcete se o něm dozvědět víc?

Objednejte si upozornění na nově vydané články do vašeho mailu. Žádný článek vám tak neuteče.

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.