Hlavní navigace

Specifické vlastnosti procesorů AArch64: podmíněné a nepodmíněné skoky, adresování dat

3. 3. 2022
Doba čtení: 40 minut

Sdílet

 Autor: Raspberry Pi Foundation
Ve druhém článku o specifických vlastnostech instrukční sady procesorů AArch64 si popíšeme především použití podmíněných i nepodmíněných skoků a taktéž způsoby adresování dat, například při provádění blokových operací.

Obsah

1. Specifické vlastnosti procesorů AArch64: podmíněné a nepodmíněné skoky, adresování dat

2. Adresace u instrukce nepodmíněného skoku

3. Adresace u instrukce podmíněného skoku

4. Druhá varianta zápisu programové smyčky bez explicitního testování nulové hodnoty počitadla

5. Test na ukončení smyčky na začátku každé iterace

6. Instrukce CBZ a CBNZ

7. Úprava předchozího příkladu – použití instrukce CBNZ

8. Podpora pro zpracování polí: automatická změna adresy uložené v registru v instrukcích typu LOAD a STORE

9. Úprava předchozích příkladů s využitím automatické změny adresy

10. Přesuny bloků dat

11. Příklad realizace přesunu bloku dat

12. Zjednodušení zápisu kódu v assembleru s využitím maker

13. Vytvoření jednoduchého makra bez parametrů

14. Vytvoření makra s parametry

15. Výpis zdrojového kódu po expanzi maker a překladu

16. Makro pro přesun bloku dat po bajtech

17. Použití návěští v makrech

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

19. Předchozí články o architektuře AArch64

20. Odkazy na Internetu

1. Specifické vlastnosti procesorů AArch64: podmíněné a nepodmíněné skoky

Na úvodní článek o specifických vlastnostech procesorů s architekturou AArch64 dnes navážeme. Věnovat se budeme zejména problematice skoků, a to jak skoků podmíněných, tak i nepodmíněných. Právě skoky jsou totiž instrukcemi, které jsou v určitém ohledu velmi kritické z hlediska výpočetního výkonu – provedení skoku totiž může (ale ne vždy musí) „přerušit“ pipeline RISCových procesorů, takže již nemusí docházet k dokončení instrukce v každém strojovém cyklu (u skalárních procesorů). Proto je důležité tyto instrukce navrhnout a implementovat správně.

Připomeňme si, jaké instrukce skoku jsou podporovány:

# Instrukce Stručný popis
1 B skok na adresu vypočtenou z offsetu vůči PC v rozsahu ±128 MB
     
2 BL branch and link, stejné jako předchozí instrukce, ovšem původní hodnota PC se uloží do X30
3 BR skok na adresu uloženou v registru s hintem, že se nejedná o výskok z podprogramu
4 RET jako BR, ovšem s hintem, že se jedná o výskok z podprogramu
5 BRL kombinace BR + BL, tj. skok na adresu uloženou v registru + původní PC do X30
     
6 B.EQ podmíněný skok BEQ, rovnost při porovnání či nulový výsledek poslední ALU operace
7 B.NE podmíněný skok BNE, nerovnost při porovnání či nenulový výsledek poslední ALU operace
8 B.MI podmíněný skok BMI, výsledek je záporný
9 B.PL podmíněný skok BPL, výsledek je kladný či 0
10 B.VS podmíněný skok BVS, nastalo přetečení
11 B.VC podmíněný skok BVC, nenastalo přetečení
12 B.CS podmíněný skok BCS, C == 1
13 B.CC podmíněný skok BCC, C == 0
14 B.HI podmíněný skok BHI, C == 1 & Z == 0
15 B.LS podmíněný skok BLS, C == 0 | Z == 1
16 B.GE podmíněný skok BGE, N == V
17 B.LT podmíněný skok BLT, N ≠ V
18 B.GT podmíněný skok BGT, Z = 0, N = V
19 B.LE podmíněný skok BLE, Z = 1, N ≠ V
     
20 CBZ Compare and Branch if Zero, bude použit v demonstračních příkladech
21 CBNZ Compare and Branch if Not Zero
     
22 TBZ Test and Branch if Zero
23 TBNZ Test and Branch if Not Zero

2. Adresace u instrukce nepodmíněného skoku

Nejprve se budeme zabývat tím nejobyčejnějším skokem, což je instrukce s mnemotechnickým názvem B. Skok je prováděn na určitou adresu:

b cíl_skoku

Víme již, že všechny instrukce jsou uloženy ve čtyřech bajtech, do nichž se musí kromě samotného kódu instrukce vejít i adresa cíle skoku. Konkrétně je instrukce b zakódována následujícím způsobem:

  1. Nejvyšších šest bitů obsahuje konstantu 000101
  2. Následuje 26bitová adresa umožňující relativní skok v rozsahu ±128 MB (cíl skoku je vždy dělitelný čtyřmi, proto se spodní dva bity nemusí ukládat)

Následující příklad ukazuje použití instrukce B pro realizaci nekonečné smyčky:

# asmsyntax=as
 
# Nekonečná smyčka
# v assembleru GNU as.
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit=93
sys_write=64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
 
 
#-----------------------------------------------------------------------------
.section .data
 
message:
        .string "Diamons are forever!\n"
end_string:
 
#-----------------------------------------------------------------------------
.section .bss
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start          // tento symbol ma byt dostupny i linkeru
 
_start:
 
loop:
        mov  x8, #sys_write     // cislo sycallu pro funkci "write"
        mov  x0, #1             // standardni vystup
        ldr  x1, =message       // adresa retezce, ktery se ma vytisknout
        mov  x2, #(end_string-message)   // pocet znaku, ktere se maji vytisknout
        svc  0                  // volani Linuxoveho kernelu
 
        b    loop               // dokolecka dokola
Poznámka: vůbec není nutné řešit „korektní“ ukončení aplikace, protože žádná instrukce za B se neprovede.

Tento program bude přeložen následujícím způsobem:

   0:   d2800808        mov     x8, #0x40                       // #64
   4:   d2800020        mov     x0, #0x1                        // #1
   8:   58000101        ldr     x1, 28 <_start+0x28>
   c:   d28002c2        mov     x2, #0x16                       // #22
  10:   d4000001        svc     #0x0
  14:   17fffffb        b       0 <_start>
  18:   d2800ba8        mov     x8, #0x5d                       // #93
  1c:   d2800000        mov     x0, #0x0                        // #0
  20:   d4000001        svc     #0x0

Podívejme se na instrukci b, která je přeložena do:

  14:   17fffffb

Neboli binárně:

0001 0111 1111 1111 1111 1111 1111 1011

Víme již, že nejvyšších šest bitů je konstanta (samotný kód instrukce):

000101

Následuje adresa 1111.....1011, tj. hodnota zapsaná ve dvojkovém doplňku. Jedná se o zápornou hodnotu, takže vypočteme její jedničkový a následně dvojkový doplněk:

orig       11 1111 1111 1111 1111 1111 1011
1 doplněk  00 0000 0000 0000 0000 0000 0100
2 doplněk  00 0000 0000 0000 0000 0000 0101

Jedná se tedy o skok zpět o 0101=5×4=20 bajtů počítaných od instrukce skoku. To přesně odpovídá skutečnosti, protože instrukce skoku je uložena na adrese 0×14=20, takže skok je proveden na instrukci uložené na adrese 0 (v rámci kódového segmentu):

   0:   d2800808        mov     x8, #0x40                       // #64
   4:   d2800020        mov     x0, #0x1                        // #1
   8:   58000101        ldr     x1, 28 <_start+0x28>
   c:   d28002c2        mov     x2, #0x16                       // #22
  10:   d4000001        svc     #0x0
  14:   17fffffb        b       0 <_start>

3. Adresace u instrukce podmíněného skoku

Instrukce podmíněného skoku je zakódována odlišně, než je tomu u skoku nepodmíněného. Zmenšil se rozsah adres, na které lze skočit (což v praxi prakticky nijak nevadí – skoky se typicky provádí v rámci funkce/subrutiny) a do instrukčního slova byla přidána čtyřbitová podmínka:

  1. Nejvyšších osm bitů obsahuje konstantu 01010100
  2. Následuje 19bitová adresa umožňující relativní skok v rozsahu ±1 MB (cíl skoku je vždy dělitelný čtyřmi, proto se spodní dva bity nemusí ukládat)
  3. Další bit je nulový
  4. Zbývající čtyři bity obsahují kód podmínky

„Rozkódování“ podmínky, která se testuje, lze zapsat tímto pseudokódem, kde je použit příznakový registr:

switch cond(3:1)  // horní tři bity podmínky
    case 000: result = Z==1
    case 001: result = C==1
    case 010: result = N==1
    case 011: result = V==1
    case 100: result = C==1 and Z==0
    case 101: result = N==V
    case 110: result = N==V and Z==0
    case 111: result = True
 
if cond(0) ==1 and cond != 1111 then
    result = not result
end

V dalším demonstračním příkladu je ukázáno použití instrukce BNE, kterou lze zapsat i jako B.NE:

# asmsyntax=as
 
# Testovaci program naprogramovany v assembleru GNU as
# - pocitana programova smycka
# - uprava pro mikroprocesory s architekturou AArch64
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit   = 93
sys_write  = 64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
# Dalsi konstanty pouzite v programu - standardni streamy
std_input  = 0
std_output = 1
 
# pocet opakovani znaku
rep_count  = 40
 
 
 
#-----------------------------------------------------------------------------
.section .data
 
 
 
#-----------------------------------------------------------------------------
.section .bss
        .lcomm buffer, rep_count     // rezervace bufferu pro vystup
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start               // tento symbol ma byt dostupny i linkeru
 
_start:
        ldr   x1, =buffer            // zapis se bude provadet do tohoto bufferu
        mov   x2, #rep_count         // pocet opakovani znaku
        mov   w3, #'*'               // zapisovany znak
loop:
        strb  w3, [x1]               // zapis znaku do bufferu
        add   x1, x1, #1             // uprava ukazatele do bufferu
        sub   x2, x2, #1             // zmenseni pocitadla
        cmp   x2, #0                 // otestovani, zda jsme jiz nedosahli nuly
        bne   loop                   // pokud jsme se nedostali k nule, skok na zacatek smycky
 
        mov   x8, #sys_write         // cislo syscallu pro funkci "write"
        mov   x0, #std_output        // standardni vystup
        ldr   x1, =buffer            // adresa retezce, ktery se ma vytisknout
        mov   x2, #rep_count         // pocet znaku, ktere se maji vytisknout
        svc   0                      // volani Linuxoveho kernelu
 
        mov   x8, #sys_exit          // cislo sycallu pro funkci "exit"
        mov   x0, #0                 // exit code = 0
        svc   0                      // volani Linuxoveho kernelu

Překlad tohoto demonstračního příkladu provedeme nám již známým způsobem (zde se jednotlivé architektury od sebe prakticky neodlišují):

$ as loop1-aarch64-v1.s -o loop1-aarch64-v1.o
$ ld -s loop1-aarch64-v1.o

Zpětný překlad lze získat klasickým „disassemblingem“:

$ objdump -f -d -t -h a.out

Výstup z disassembleru bude vypadat následovně:

a.out:     file format elf64-littleaarch64
architecture: aarch64, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x00000000004000b0
 
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000048  00000000004000b0  00000000004000b0  000000b0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .bss          00000028  00000000004100f8  00000000004100f8  000000f8  2**3
                  ALLOC
SYMBOL TABLE:
no symbols
 
 
 
Disassembly of section .text:
 
00000000004000b0 <.text>:
  4000b0:       58000201        ldr     x1, 0x4000f0
  4000b4:       d2800502        mov     x2, #0x28                       // #40
  4000b8:       52800543        mov     w3, #0x2a                       // #42
  4000bc:       39000023        strb    w3, [x1]
  4000c0:       91000421        add     x1, x1, #0x1
  4000c4:       d1000442        sub     x2, x2, #0x1
  4000c8:       f100005f        cmp     x2, #0x0
  4000cc:       54ffff81        b.ne    0x4000bc  // b.any
  4000d0:       d2800808        mov     x8, #0x40                       // #64
  4000d4:       d2800020        mov     x0, #0x1                        // #1
  4000d8:       580000c1        ldr     x1, 0x4000f0
  4000dc:       d2800502        mov     x2, #0x28                       // #40
  4000e0:       d4000001        svc     #0x0
  4000e4:       d2800ba8        mov     x8, #0x5d                       // #93
  4000e8:       d2800000        mov     x0, #0x0                        // #0
  4000ec:       d4000001        svc     #0x0
  4000f0:       004100f8        .inst   0x004100f8 ; undefined
  4000f4:       00000000        udf     #0

Povšimněte si, že se ve zpětném překladu používá „ARMovský“ zápis instrukce podmíněného skoku: B.NE namísto alternativního zápisu BNE, který jsme použili ve zdrojovém kódu:

  4000cc:       54ffff81        b.ne    0x4000bc  // b.any

Instrukce B.NE je přeložena do:

54ffff81

Neboli binárně:

0101 0100 1111 1111 1111 1111 1000 0001

Víme již, že nejvyšších osm bitů je konstanta (samotný kód instrukce):

0101 0100

Následuje adresa 1111.....100, tj. hodnota zapsaná ve dvojkovém doplňku. Jedná se o zápornou hodnotu, takže vypočteme její jedničkový a následně dvojkový doplněk:

orig       1111 1111 1111 1111 100
1 doplněk  0000 0000 0000 0000 011
2 doplněk  0000 0000 0000 0000 100

Jedná se tedy o skok zpět o 0100=4×4=16 bajtů počítaných od instrukce skoku.

4. Druhá varianta zápisu programové smyčky bez explicitního testování nulové hodnoty počitadla

Alternativní způsob zápisu programové smyčky spočívá v odstranění explicitního testování nulové hodnoty počitadla instrukcí CMP. Můžeme totiž využít toho, že se příznak Z (zero) může nastavit automaticky již při dekrementaci hodnoty počitadla. Na procesorech ARM je v tomto případě nutné namísto instrukce SUB použít instrukci SUBS, kde poslední znak „S“ znamená „set (flags)“. Celá programová smyčka se nám zkrátí o jednu instrukci, což může v praxi znamenat poměrně znatelné urychlení (samozřejmě nikoli v našem jednoduchém příkladu, ale například při výpočtech nad velkými poli se tato optimalizace již může projevit):

loop:
        strb  w3, [x1]               // zapis znaku do bufferu
        add   x1, x1, #1             // uprava ukazatele do bufferu
        subs  x2, x2, #1             // zmenseni pocitadla a soucasne nastaveni priznaku
        bne   loop                   // pokud jsme se nedostali k nule, skok na zacatek smycky

Následuje úplný zdrojový kód druhého demonstračního příkladu, v němž je tato úprava provedena:

# asmsyntax=as
 
# Testovaci program naprogramovany v assembleru GNU as
# - pocitana programova smycka
# - uprava pro mikroprocesory s architekturou AArch64
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit   = 93
sys_write  = 64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
# Dalsi konstanty pouzite v programu - standardni streamy
std_input  = 0
std_output = 1
 
# pocet opakovani znaku
rep_count  = 40
 
 
 
#-----------------------------------------------------------------------------
.section .data
 
 
 
#-----------------------------------------------------------------------------
.section .bss
        .lcomm buffer, rep_count     // rezervace bufferu pro vystup
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start               // tento symbol ma byt dostupny i linkeru
 
_start:
        ldr   x1, =buffer            // zapis se bude provadet do tohoto bufferu
        mov   x2, #rep_count         // pocet opakovani znaku
        mov   w3, #'*'               // zapisovany znak
loop:
        strb  w3, [x1]               // zapis znaku do bufferu
        add   x1, x1, #1             // uprava ukazatele do bufferu
        subs  x2, x2, #1             // zmenseni pocitadla a soucasne nastaveni priznaku
        bne   loop                   // pokud jsme se nedostali k nule, skok na zacatek smycky
 
        mov   x8, #sys_write         // cislo syscallu pro funkci "write"
        mov   x0, #std_output        // standardni vystup
        ldr   x1, =buffer            // adresa retezce, ktery se ma vytisknout
        mov   x2, #rep_count         // pocet znaku, ktere se maji vytisknout
        svc   0                      // volani Linuxoveho kernelu
 
        mov   x8, #sys_exit          // cislo sycallu pro funkci "exit"
        mov   x0, #0                 // exit code = 0
        svc   0                      // volani Linuxoveho kernelu

5. Test na ukončení smyčky na začátku každé iterace

Další možná úprava programové smyčky spočívá v testu ukončení iterací na jejím začátku. To je opět téma, kterému se budeme muset věnovat mnohem podrobněji, takže si dnes pouze ukažme, jakým způsobem je možné tuto programovou smyčku implementovat na architektuře AArch64. Samotná programová smyčka končí nepodmíněným skokem na její začátek; instrukce nepodmíněného skoku se jmenuje B („branch“):

loop:
        sub   x2, x2, #1             // zmenseni pocitadla
        cmp   x2, #0                 // otestovani, zda jsme jiz nedosahli nuly
        beq   konec                  // pokud jsme se dostali k nule, konec smycky
        strb  w3, [x1]               // zapis znaku do bufferu
        add   x1, x1, #1             // uprava ukazatele do bufferu
        b     loop                   // nepodmineny skok na zacatek smycky
konec:
Poznámka: sémantika je ovšem odlišná – přenos dat nemusí v tomto případě proběhnout ani jednou.

Samozřejmě je opět možné vynechat instrukci CMP a nastavit příznak zero přímo při dekrementaci počitadla:

loop:
        subs  x2, x2, #1             // zmenseni pocitadla a nastaveni priznaku
        beq   konec                  // pokud jsme se dostali k nule, konec smycky
        strb  w3, [x1]               // zapis znaku do bufferu
        add   x1, x1, #1             // uprava ukazatele do bufferu
        b     loop                   // nepodmineny skok na zacatek smycky
konec:

Další části programu mohou zůstat nezměněny.

6. Instrukce CBZ a CBNZ

Instrukční sada procesorů s architekturou AArch64 obsahuje i některé instrukce, které byly dříve úspěšně otestovány v „komprimovaných“ instrukčních sadách Thumb a Thumb-2. Do této oblasti spadají i nové typy podmíněných skoků. Ty se totiž v mnoha případech ukazují být kritickou částí kódu, protože zejména podmíněné skoky mohou přerušit jinak plynulý tok zpracovávaných instrukcí, takže se z ideálního stavu, kdy RISCové jádro díky existenci pipeline dokončí v každém cyklu jednu instrukci (v případě superskalárních čipů Cortex-A i více instrukcí) můžeme dostat do stavu, kdy podmíněný skok způsobí nutnost přerušit již zpracovávané instrukce a začít znovu (samozřejmě s latencí).

Při analýze reálných aplikací si tvůrci instrukční sady Thumb-2 všimli si, že se v programech velmi často vyskytuje sekvence instrukcí, které nejdřív porovnají obsah vybraného pracovního registru s nulou a posléze provedou podmíněný skok na základě toho, zda je onen pracovní registr skutečně nulový nebo naopak nenulový. Poměrně velké frekvenci této sekvence instrukcí se nelze ani divit, protože podobným způsobem mohou být ve vysokoúrovňových programovacích jazycích (sem počítám v kontextu článku i céčko) implementovány například testy na hodnotu NULL, počítané smyčky, smyčky typu do-while v nichž je pravdivostní hodnota vyjádřena celým číslem, práce s ASCIIZ řetězci atd. Aby bylo možné zmenšit velikost binárního kódu programu a současně ho i urychlit, byly do instrukční sady Thumb-2 přidány dvě nové instrukce, které nejprve provedou porovnání pracovního registru s nulou a poté provedou skok, pokud je registr nulový či naopak není nulový. Součástí instrukčního slova je přitom i krátký offset umožňující provést skok do vzdálenosti PC-1MB až PC+1MB (což by v rámci jednoho podprogramu mělo naprosto bez problémů postačovat).

První z těchto instrukcí provede skok, pokud je vybraný pracovní registr nulový:

CBZ Rn, offset   ; compare and branch if zero

Tato instrukce je ekvivalentní delší sekvenci:

CMP Rn, #0
BEQ label

Druhá instrukce provádí skok v přesně opačném případě, tj. tehdy, když má registr nenulovou hodnotu:

CBNZ Rn, offset   ; compare and branch if non zero

Ekvivalentní zápis by tedy vypadal následovně:

CMP Rn, #0
BNE label
Poznámka: cíl skoku je vypočten z offsetu uloženého v devatenácti bitech. Hodnota offsetu je vynásobena čtyřmi, takže skutečně dostáváme 21bitový rozsah relativních adres, tedy &plusm;1MB.

7. Úprava předchozího příkladu – použití instrukce CBNZ

Vzhledem k tomu, že se instrukce CBZ a CBNZ mohou použít i u 64bitové architektury AArch64, upravíme si předchozí demonstrační příklad takovým způsobem, aby se v něm tyto instrukce využily. To znamená, že se namísto sekvence instrukcí:

        mov   x2, #rep_count         // pocet opakovani programove smycky
loop:
        ...
        ...
        ...
        sub   x2, x2, #1             // zmenseni pocitadla
        cmp   x2, #0                 // otestovani, zda jsme jiz nedosahli nuly
        bne   loop                   // pokud jsme se nedostali k nule, skok na zacatek smycky

Použije nová sekvence:

        mov   x2, #rep_count         // pocet opakovani programove smycky
loop:
        ...
        ...
        ...
        sub   x2, x2, #1             // zmenseni pocitadla
        cbnz  x2, loop               // pokud jsme se nedostali k nule, skok na zacatek smycky

Vidíme, že se nám podařilo smyčku zkrátit o jednu instrukci, takže jsme se vlastně dostali do stejné situace, jako při použití dvojice SUBS + podmíněný skok (jinými slovy – zde nám instrukce CBNZ vlastně příliš nepomohla):

        mov   x2, #rep_count         // pocet opakovani programove smycky
loop:
        ...
        ...
        ...
        subs  x2, x2, #1             // zmenseni pocitadla a soucasne nastaveni priznaku
        bne   loop                   // pokud jsme se nedostali k nule, skok na zacatek smycky

Podívejme se nyní na způsob zařazení instrukce CBNZ do celého programu, který po svém spuštění vygeneruje řetězec se čtyřiceti hvězdičkami, který následně vytiskne na standardní výstup:

# asmsyntax=as
 
# Testovaci program naprogramovany v assembleru GNU as
# - pocitana programova smycka realizovana instrukci CBNZ
# - uprava pro mikroprocesory s architekturou AArch64
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit   = 93
sys_write  = 64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
# Dalsi konstanty pouzite v programu - standardni streamy
std_input  = 0
std_output = 1
 
# pocet opakovani znaku
rep_count  = 40
 
 
 
#-----------------------------------------------------------------------------
.section .data
 
 
 
#-----------------------------------------------------------------------------
.section .bss
        .lcomm buffer, rep_count     // rezervace bufferu pro vystup
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start               // tento symbol ma byt dostupny i linkeru
 
_start:
        ldr   x1, =buffer            // zapis se bude provadet do tohoto bufferu
        mov   x2, #rep_count         // pocet opakovani znaku
        mov   w3, #'*'               // zapisovany znak
loop:
        strb  w3, [x1]               // zapis znaku do bufferu
        add   x1, x1, #1             // uprava ukazatele do bufferu
        sub   x2, x2, #1             // zmenseni pocitadla
        cbnz  x2, loop               // pokud jsme se nedostali k nule, skok na zacatek smycky
 
        mov   x8, #sys_write         // cislo syscallu pro funkci "write"
        mov   x0, #std_output        // standardni vystup
        ldr   x1, =buffer            // adresa retezce, ktery se ma vytisknout
        mov   x2, #rep_count         // pocet znaku, ktere se maji vytisknout
        svc   0                      // volani Linuxoveho kernelu
 
        mov   x8, #sys_exit          // cislo sycallu pro funkci "exit"
        mov   x0, #0                 // exit code = 0
        svc   0                      // volani Linuxoveho kernelu

8. Podpora pro zpracování polí: automatická změna adresy uložené v registru v instrukcích typu LOAD a STORE

Při práci s poli nebo s řetězci je typicky nutné zpracovat jeden prvek pole (čtení, zápis, otestování atd.) a posléze se přesunout na prvek další, a to zvýšením adresy uložené v pracovním registru (popř. snížením adresy, pokud z nějakého důvodu je nutné polem procházet opačným směrem):

loop:
        strb  w3, [x1]               // zapis znaku do bufferu
        add   x1, x1, #1             // uprava ukazatele do bufferu
        sub   x2, x2, #1             // zmenseni pocitadla
        cbnz  x2, loop               // pokud jsme se nedostali k nule, skok na zacatek smycky

Kombinace LDR/STR následovaná ADD/SUB adresovacího registru je tak častá, že instrukce LDR/STR umožňují provést zvýšení nebo snížení adresy přímo. Způsob zápisu je patný z následujícího úryvku kódu:

loop:
        strb  w3, [x1], 1            // zapis znaku do bufferu s post-inkrementaci adresy
        sub   x2, x2, #1             // zmenseni pocitadla
        cbnz  x2, loop               // pokud jsme se nedostali k nule, skok na zacatek smycky

Je asi zřejmé, že velikost použité konstanty (tedy například onen jeden bajt) je omezena tím, že instrukce LDR/STR musí nést informace o dvou pracovních registrech, samotný kód instrukce atd. Kódování vypadá následovně:

  1. Pět bitů pro uložení indexu registru, který je ukládán (resp. jeho osm bitů)
  2. Pět bitů pro uložení registru s adresou
  3. 9 bitů pro uložení konstanty pro operace pre-inkrementu/dekrementu popř. post-inkrementu/dekrementu
Poznámka: konstanta je v tomto případě uložena i se znaménkem, což v případě potřeby umožňuje i snižování adresy.

9. Úprava předchozích příkladů s využitím automatické změny adresy

Automatické zvýšení či naopak snížení adresy uložené v registru použitém v instrukcích LDR a STR je v praxi velmi užitečné a umožňuje nejenom zkrátit programový kód, ale navíc ho i urychlit tím, že se obě prováděné operace mohou částečně překrývat. Zkrácení programového kódu je patrné v dalším demonstračním příkladu:

# asmsyntax=as
 
# Testovaci program naprogramovany v assembleru GNU as
# - pocitana programova smycka realizovana instrukci CBNZ
# - uprava pro mikroprocesory s architekturou AArch64
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit   = 93
sys_write  = 64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
# Dalsi konstanty pouzite v programu - standardni streamy
std_input  = 0
std_output = 1
 
# pocet opakovani znaku
rep_count  = 40
 
 
 
#-----------------------------------------------------------------------------
.section .data
 
 
 
#-----------------------------------------------------------------------------
.section .bss
        .lcomm buffer, rep_count     // rezervace bufferu pro vystup
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start               // tento symbol ma byt dostupny i linkeru
 
_start:
        ldr   x1, =buffer            // zapis se bude provadet do tohoto bufferu
        mov   x2, #rep_count         // pocet opakovani znaku
        mov   w3, #'*'               // zapisovany znak
loop:
        strb  w3, [x1], 1            // zapis znaku do bufferu s post-inkrementaci adresy
        sub   x2, x2, #1             // zmenseni pocitadla
        cbnz  x2, loop               // pokud jsme se nedostali k nule, skok na zacatek smycky
 
        mov   x8, #sys_write         // cislo syscallu pro funkci "write"
        mov   x0, #std_output        // standardni vystup
        ldr   x1, =buffer            // adresa retezce, ktery se ma vytisknout
        mov   x2, #rep_count         // pocet znaku, ktere se maji vytisknout
        svc   0                      // volani Linuxoveho kernelu
 
        mov   x8, #sys_exit          // cislo sycallu pro funkci "exit"
        mov   x0, #0                 // exit code = 0
        svc   0                      // volani Linuxoveho kernelu

10. Přesuny bloků dat

V prakticky všech aplikacích naprogramovaných ve vysokoúrovňových programovacích jazycích se setkáme s kódem, který vyžaduje přenosy bloků dat (resp. přesněji řečeno kopii dat, protože typicky zdrojová data nejsou smazána). Může se jednat například o předávání větších parametrů do funkcí (čehož si nemusí vývojář všimnout):

// ConfigStruct is a structure holding the whole service configuration
type ConfigStruct struct {
        Storage StorageConfiguration `mapstructure:"storage" toml:"storage"`
        S3      S3Configuration      `mapstructure:"s3" tomp:"s3"`
        Logging LoggingConfiguration `mapstructure:"logging" toml:"logging"`
}
 
type LoggingConfiguration struct {
        ...
        ...
        ...
}
 
// GetLoggingConfiguration returns logging configuration
func GetLoggingConfiguration(config ConfigStruct) LoggingConfiguration {
        return config.Logging
}
Poznámka: v tomto konkrétním případě je vývojář vlastně „nucen“ kopírovat celé struktury, protože jazyk neumožňuje předávání „ukazatele na neměnnou strukturu“.

Nebo se s přenosy dat setkáme i u spojování řetězců apod.:

s1 := "foobar"
s2 := getNameFromREST("adresa")
return s1 + s2

Interně musí být podprogramy určené pro kopírování dat optimalizovány a u některých architektur mikroprocesorů se dokonce setkáme s tím, že je podpora realizována již na úrovni instrukční sady – některé mikroprocesory s architekturou CISC tedy umožňují vlastní kopii dat provádět jedinou instrukcí, která je však řízena mikroprogramovým řadičem a její rychlost provedení bude závislá mj. i na délce kopírovaného bloku (popř. i na jeho zarovnání).

Abychom si přiblížili, jakým způsobem je možné operaci přenosu dat (block move) realizovat na mikroprocesorech s architekturou AArch64, ukážeme si (alespoň prozatím) základní a prakticky neoptimalizovanou variantu, která bloky přenáší velmi neefektivním způsobem – po jednotlivých bajtech. To má jedinou výhodu – nemusíme řešit zarovnání zdrojového a cílového bloku. Ovšem převažují nevýhody, protože i nepatrnou úpravou by bylo možné zajistit prakticky osminásobné rychlosti přenosu. Touto problematikou se budeme podrobněji zabývat příště – a přesně tuto problematiku musí implementovat všechny překladače.

Nejjednodušší forma sekvence instrukcí pro přenos dat spočívá v tom, že využijeme několik pracovních registrů, a to v následujících rolích:

  1. Registr s adresou bajtu, který se má načíst
  2. Registr s adresou, kam se má přenášený bajt uložit
  3. Registr sloužící jako mezipaměť: bude obsahovat právě přenášený bajt
  4. Registr fungující jako počitadlo smyčky

Můžeme například použít registry x1x4:

  1. x1 adresa bajtu, který se má načíst
  2. x2 adresa, kam se má přenášený bajt uložit
  3. x3/w3 mezipaměť: bude obsahovat právě přenášený bajt
  4. x4 počitadlo smyčky

Inicializace registrů v assembleru může vypadat takto:

        ldr   x1, =source       // adresa bloku pro čtení
        ldr   x2, =target       // adresa bloku pro zápis
        mov   x4, #rep_count    // počet přenášených bajtů
Poznámka: registr x3 resp. w3 je použit pouze uvnitř smyčky, takže ho není zapotřebí nijak inicializovat.

A jedna z možností realizace této smyčky (opět podotýkám, že neoptimalizované), může vypadat následovně:

loop:
        ldrb  w3, [x1], 1       // čtení bajtu + zvýšení adresy v registru x1
        strb  w3, [x2], 1       // zápis bajtu + zvýšení adresy v registru x2
        sub   x4, x4, #1        // zmenšeni počitadla
        cbnz  x4, loop          // pokud jsme se nedostali k nule, skok na začátek smyčky

Povšimněte si, že v každé iteraci se přenese pouze jediný bajt, protože instrukce LDRB a STRB využívají jen spodních osm bitů 32bitového registru w3.

11. Příklad realizace přesunu bloku dat

V následujícím příkladu jsou vytvořeny dvě paměťové oblasti o shodné délce:

hello_lbl:
        .string "Hello World!\n"
 
buffer:
        .string "************\n"

Program nejprve vypíše obsah oblasti buffer (tedy sadu hvězdiček a řídicí znak pro odřádkování). Následně přenese řetězec „Hello World“ do tohoto bufferu (což je náš blokový přenos dat) a opět vypíše jeho obsah:

# asmsyntax=as
 
# Presun bloku dat.
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit=93
sys_write=64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
# pocet bajtu
rep_count  = 13
 
 
#-----------------------------------------------------------------------------
.section .data
 
hello_lbl:
        .string "Hello World!\n"
 
buffer:
        .string "************\n"
 
#-----------------------------------------------------------------------------
.section .bss
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start          // tento symbol ma byt dostupny i linkeru
 
_start:
        mov  x8, #sys_write     // cislo sycallu pro funkci "write"
        mov  x0, #1             // standardni vystup
        ldr  x1, =buffer        // adresa retezce, ktery se ma vytisknout
        mov  x2, #rep_count     // pocet znaku, ktere se maji vytisknout
        svc  0                  // volani Linuxoveho kernelu
 
        ldr   x1, =hello_lbl    // adresa bloku pro cteni
        ldr   x2, =buffer       // adresa bloku pro zapis
        mov   x4, #rep_count    // pocet bajtu
loop:
        ldrb  w3, [x1], 1       // cteni bajtu
        strb  w3, [x2], 1       // zapis bajtu
        sub   x4, x4, #1        // zmenseni pocitadla
        cbnz  x4, loop          // pokud jsme se nedostali k nule, skok na zacatek smycky
 
        mov  x8, #sys_write     // cislo sycallu pro funkci "write"
        mov  x0, #1             // standardni vystup
        ldr  x1, =buffer        // adresa retezce, ktery se ma vytisknout
        mov  x2, #rep_count     // pocet znaku, ktere se maji vytisknout
        svc  0                  // volani Linuxoveho kernelu
 
        mov  x8, #sys_exit      // cislo sycallu pro funkci "exit"
        mov  x0, #0             // exit code = 0
        svc  0                  // volani Linuxoveho kernelu

12. Zjednodušení zápisu kódu v assembleru s využitím maker

Nástroje typu „assembler“ je možné podle principu jejich práce rozdělit do několika kategorií. Do první kategorie spadají assemblery interaktivní, které uživateli nabízejí poměrně komfortní vývojové prostředí, v němž je v případě potřeby možné zapisovat jednotlivé instrukce, spouštět programy, krokovat je, vypisovat obsahy pracovních registrů mikroprocesoru, prohlížet si obsah operační paměti, zásobníku atd. Velkou výhodou byla nezávislost těchto assemblerů na rychlém externím paměťovém médiu, proto jsme se s nimi mohli setkat například na osmibitových domácích mikropočítačích či dnes na různých zařízeních typu IoT (i když zde úlohu pouhého interaktivního assembleru mnohdy přebírá interaktivní debugger). Druhý typ assemblerů je široce používán dodnes – jedná se vlastně o běžné překladače, kterým se na vstupu předloží zdrojový kód a po překladu se výsledný nativní kód taktéž uloží na paměťové médium (odkud ho lze přímo spustit, což se dělo například v operačním systému DOS, popř. ho ještě před spuštěním slinkovat, což je případ Linuxu a dalších moderních operačních systémů).

Obrázek 1: Vývojové prostředí Atari Macro Assembleru (výpis obsahu pracovních registrů mikroprocesoru). Jedná se o jeden z interaktivních assemblerů.

Assemblery spadající do druhé kategorie jsou mnohdy vybaveny více či méně dokonalým systémem maker; odtud ostatně pochází i jejich často používané označení macroassembler. Makra, která se většinou aplikují na zdrojový kód v první fázi překladu, je možné použít pro různé činnosti, ať již se jedná o zjednodušení zápisu kódu či o jeho zkrácení a zpřehlednění. Existují například sady poměrně složitých maker, která do assembleru přidávají některé konstrukce známé z vyšších programovacích jazyků – rozvětvení, programové smyčky, deklaraci objektů atd. GNU Assembler, podobně jako prakticky všechny další moderní assemblery, práci s makry podporují, i když se způsob zápisu maker i jejich základní vlastnosti od sebe odlišují. Z tohoto důvodu se v navazujících kapitolách budeme věnovat (prozatím) pouze makrům v GNU Assembleru. Ukážeme si i řešení některých častých problémů, které mohou při deklaraci maker nastat, například práci s návěštími (labels) apod.

Obrázek 2: Takto vypadá úryvek programu napsaný v assembleru mikroprocesoru MOS 6502.

13. Vytvoření jednoduchého makra bez parametrů

Makra v assembleru, tedy i v námi používaném GNU Assembleru, provádí textové substituce, což mj. znamená, že expanze maker je vykonána v první fázi překladu. V GNU Assembleru deklarace makra začíná direktivou .macro a končí direktivou .endm (obě zmíněné direktivy se zapisují včetně teček na začátku). Za direktivou .macro musí následovat jméno makra a popř. i jeho parametry. Na dalších řádcích je pak vlastní text makra. Použití makra je ještě jednodušší než jeho deklarace – kdekoli se prostě uvede jméno makra s případnými parametry. Jakmile GNU Assembler zjistí, že se ve zdrojovém kódu nachází jméno makra, provede jeho expanzi, takže se vlastně případné instrukce, ze kterých se text makra skládá, přímo vloží do kódu na místo volání makra.

Podívejme se nyní na velmi jednoduchý demonstrační příklad, kterým je makro bez parametrů. Toto makro jsme nazvali exit a v jeho těle se zavolá syscall (funkce jádra) sloužící k ukončení procesu:

# Deklarace makra pro ukonceni aplikace
.macro exit
        mov  x8, #sys_exit      // cislo sycallu pro funkci "exit"
        mov  x0, #0             // exit code = 0
        svc  0                  // volani Linuxoveho kernelu
.endm

Povšimněte si, že v těle makra se může nacházet libovolný text, včetně komentářů, symbolů deklarovaných mimo makro (sys_exit) atd. Volání makra nazvaného exit vypadá takto:

#-----------------------------------------------------------------------------
.section .text
        .global _start               // tento symbol ma byt dostupny i linkeru
 
_start:
        ...
        libovolné instrukce či volání jiných maker
        ...
        exit                         // ukonceni aplikace
Poznámka: ve skutečnosti se nejedná o volání makra ve smyslu volání subrutiny (podprogramu), ale skutečně pouze o textovou expanzi prováděnou v době překladu programu ze zdrojového kódu do kódu objektového.

Šablonu z úvodního článku tedy můžeme upravit do podoby:

# asmsyntax=as
 
# Sablona pro zdrojovy kod Linuxoveho programu naprogramovaneho
# v assembleru GNU AS pro architekturu AArch64.
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit=93
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
 
# Deklarace makra pro ukonceni aplikace
.macro exit
        mov  x8, #sys_exit      // cislo sycallu pro funkci "exit"
        mov  x0, #0             // exit code = 0
        svc  0                  // volani Linuxoveho kernelu
.endm
 
 
#-----------------------------------------------------------------------------
.section .data
 
 
 
#-----------------------------------------------------------------------------
.section .bss
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start          // tento symbol ma byt dostupny i z linkeru
 
_start:
        exit

14. Vytvoření makra s parametry

Makra mohou v případě potřeby (a ta je poměrně častá) akceptovat i parametry, ovšem práce s nimi může být zpočátku poněkud neobvyklá. Jména parametrů se zadávají již v hlavičce makra přímo za jeho názvem, tedy následujícím způsobem:

.macro jméno_makra parametr1, parametr2, ...

Důležitější ovšem je, že přímo v těle makra se před jméno parametru musí vložit znak zpětného lomítka, jinak nedojde k náhradě názvu parametru jeho skutečným obsahem (to má svůj význam, protože nemůže nastat situace, že by se nějaký text nahrazoval parametrem makra omylem). Podívejme se na praktický příklad – konkrétně na makro určené pro výpis zprávy na standardní výstup. Tomuto makru se předává adresa řetězce a jeho délka. Uvnitř těla makra se před parametry skutečně zapisuje zpětné lomítko:

# Deklarace makra pro vytisteni zpravy na standardni vystup
.macro writeMessage message,messageLength
        mov  x8, #sys_write       // cislo sycallu pro funkci "write"
        mov  x0, #1               // standardni vystup
        ldr  x1, =\message        // adresa retezce, ktery se ma vytisknout
        mov  x2, #\messageLength  // pocet znaku, ktere se maji vytisknout
        svc  0                    // volani Linuxoveho kernelu
.endm

Příklad volání tohoto makra:

writeMessage buffer, rep_count
Poznámka: to už vypadá podobně, jako by se program zapisoval ve vyšším programovacím jazyce :-).

Aplikaci typu „Hello, world!“, s níž jsme se opět seznámili minule, lze tedy přepsat do podoby využívající makra:

# asmsyntax=as
 
# Jednoducha aplikace typu "Hello world!" naprogramovana
# v assembleru GNU as.
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit=93
sys_write=64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
 
# Deklarace makra pro ukonceni aplikace
.macro exit
        mov  x8, #sys_exit      // cislo sycallu pro funkci "exit"
        mov  x0, #0             // exit code = 0
        svc  0                  // volani Linuxoveho kernelu
.endm
 
 
# Deklarace makra pro vytisteni zpravy na standardni vystup
.macro writeMessage message,messageLength
        mov  x8, #sys_write       // cislo sycallu pro funkci "write"
        mov  x0, #1               // standardni vystup
        ldr  x1, =\message        // adresa retezce, ktery se ma vytisknout
        mov  x2, #\messageLength  // pocet znaku, ktere se maji vytisknout
        svc  0                    // volani Linuxoveho kernelu
.endm
 
 
#-----------------------------------------------------------------------------
.section .data
 
hello_lbl:
        .string "Hello World!\n"
 
#-----------------------------------------------------------------------------
.section .bss
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start          // tento symbol ma byt dostupny i linkeru
 
_start:
        writeMessage hello_lbl, 13
        exit

15. Výpis zdrojového kódu po expanzi maker a překladu

Při zápisu maker či při jejich volání může dojít k situaci, kdy se makro neexpanduje podle našich předpokladů a je nutné zjistit, kde přesně nastal problém. GNU Assembler sice neexpanduje makra samostatným preprocesorem (jak je tomu v céčku a jeho preprocesoru nazvaném cpp), ovšem obsahuje možnost nechat si vygenerovat výpis původního zdrojového kódu kombinovaného s přeloženým objektovým kódem, přesněji řečeno s objektovým kódem zapsaným v hexadecimálním tvaru. Jedná se o mnohdy velmi užitečnou vlastnost, kterou nalezneme u mnoha assemblerů, a to i u některých starších nástrojů. Takový výpis se na historických mainframech bez obrazovky většinou posílal přímo na tiskárnu, takže obsahoval i vepsané chyby nalezené překladačem. A právě v tomto výpisu se mohou objevit expandovaná makra. Podívejme se, co se stane, pokud při překladu použijeme volbu -alm (resp. volbu -a s dalšími příznaky l a m) kombinovanou s volbou -g:

$ as -alm -g hello_world_3.s -o hello_world_3.o

Na standardní výstup by se měl vypsat následující výpis:

AARCH64 GAS  hello_world_3.s                    page 1
 
 
   1                    # asmsyntax=as
   2
   3                    # Jednoducha aplikace typu "Hello world!" naprogramovana
   4                    # v assembleru GNU as.
   5                    #
   6                    # Autor: Pavel Tisnovsky
   7
   8
   9
  10                    # Linux kernel system call table
  11                    sys_exit=93
  12                    sys_write=64
  13
  14                    # List of syscalls for AArch64:
  15                    # https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
  16
  17
  18                    # Deklarace makra pro ukonceni aplikace
  19                    .macro exit
  20                            mov  x8, #sys_exit      // cislo sycallu pro funkci "exit"
  21                            mov  x0, #0             // exit code = 0
  22                            svc  0                  // volani Linuxoveho kernelu
  23                    .endm
  24
  25
  26                    # Deklarace makra pro vytisteni zpravy na standardni vystup
  27                    .macro writeMessage message,messageLength
  28                            mov  x8, #sys_write       // cislo sycallu pro funkci "write"
  29                            mov  x0, #1               // standardni vystup
  30                            ldr  x1, =\message        // adresa retezce, ktery se ma vytisknout
  31                            mov  x2, #\messageLength  // pocet znaku, ktere se maji vytisknout
  32                            svc  0                    // volani Linuxoveho kernelu
  33                    .endm
  34
  35
  36                    #-----------------------------------------------------------------------------
  37                    .section .data
  38
  39                    hello_lbl:
  40 0000 48656C6C              .string "Hello World!\n"
  40      6F20576F
  40      726C6421
  40      0A00
  41
  42                    #-----------------------------------------------------------------------------
  43                    .section .bss
  44
  45
  46
  47                    #-----------------------------------------------------------------------------
  48                    .section .text
  49                            .global _start          // tento symbol ma byt dostupny i linkeru
  50
  51                    _start:
  52                            writeMessage hello_lbl, 13
  52 0000 080880D2      >  mov x8,#sys_write
  52 0004 200080D2      >  mov x0,#1
 
 
AARCH64 GAS  hello_world_3.s                  page 2
 
 
  52 0008 C1000058      >  ldr x1,=hello_lbl
  52 000c A20180D2      >  mov x2,#13
  52 0010 010000D4      >  svc 0
  53                            exit
  53 0014 A80B80D2      >  mov x8,#sys_exit
  53 0018 000080D2      >  mov x0,#0
  53 001c 010000D4      >  svc 0
  53      00000000
  53      00000000
Poznámka: vidíme, že se skutečně jedná o formátovaný výstup určený pro tisk, ovšem podstatné je, že došlo jak k expanzi maker, tak i k umístění značek > do těch částí kódu, kde byla makra umístěna.

16. Makro pro přesun bloku dat po bajtech

Na základě předchozích příkladů se můžeme pokusit o vytvoření makra určeného pro přesun bloku dat (prozatím po bajtech). Toto makro by mělo akceptovat trojici parametrů – počáteční adresu zdrojového bloku, adresu, kam se má blok přesunout a počet přesouvaných bajtů. První (a již na tomto místě nutno říci, že nekorektní) varianta tohoto makra by mohla vypadat následovně:

# Deklarace makra pro presun bloku
.macro moveBlock from, to, length
        ldr   x1, =\from        // adresa bloku pro cteni
        ldr   x2, =\to          // adresa bloku pro zapis
        mov   x4, #\length      // pocet bajtu
loop:
        ldrb  w3, [x1], 1       // cteni bajtu
        strb  w3, [x2], 1       // zapis bajtu
        sub   x4, x4, #1        // zmenseni pocitadla
        cbnz  x4, loop          // pokud jsme se nedostali k nule, skok na zacatek smycky
.endm

Celý příklad můžeme upravit takovým způsobem, že se sekvence instrukcí v hlavním programu vlastně celá nahradí makry, což (zdánlivě) začíná připomínat vysokoúrovňové programovací jazyky (které se ostatně vyvíjely právě od makroassemblerů):

writeMessage buffer, rep_count
 
moveBlock hello_lbl, buffer, rep_count
 
writeMessage buffer, rep_count
 
exit

Úplný zdrojový kód tohoto demonstračního příkladu:

# asmsyntax=as
 
# Presun bloku dat.
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit=93
sys_write=64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
# pocet bajtu
rep_count  = 13
 
 
# Deklarace makra pro ukonceni aplikace
.macro exit
        mov  x8, #sys_exit      // cislo sycallu pro funkci "exit"
        mov  x0, #0             // exit code = 0
        svc  0                  // volani Linuxoveho kernelu
.endm
 
 
 
# Deklarace makra pro vytisteni zpravy na standardni vystup
.macro writeMessage message,messageLength
        mov  x8, #sys_write       // cislo sycallu pro funkci "write"
        mov  x0, #1               // standardni vystup
        ldr  x1, =\message        // adresa retezce, ktery se ma vytisknout
        mov  x2, #\messageLength  // pocet znaku, ktere se maji vytisknout
        svc  0                    // volani Linuxoveho kernelu
.endm
 
 
# Deklarace makra pro presun bloku
.macro moveBlock from,to,length
        ldr   x1, =\from        // adresa bloku pro cteni
        ldr   x2, =\to          // adresa bloku pro zapis
        mov   x4, #\length      // pocet bajtu
loop:
        ldrb  w3, [x1], 1       // cteni bajtu
        strb  w3, [x2], 1       // zapis bajtu
        sub   x4, x4, #1        // zmenseni pocitadla
        cbnz  x4, loop          // pokud jsme se nedostali k nule, skok na zacatek smycky
.endm
 
 
 
#-----------------------------------------------------------------------------
.section .data
 
hello_lbl:
        .string "Hello World!\n"
 
buffer:
        .string "************\n"
 
#-----------------------------------------------------------------------------
.section .bss
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start          // tento symbol ma byt dostupny i linkeru
 
_start:
        writeMessage buffer, rep_count
 
        moveBlock hello_lbl, buffer, rep_count
 
        writeMessage buffer, rep_count
 
        exit

17. Použití návěští v makrech

V tělech mnoha maker se používají instrukce skoku, což mj. znamená, že se mnohdy nevyhneme použití návěští. Což je ostatně přesně náš případ. Podívejme se, kde vlastně vězí celý problém:

# Deklarace makra pro presun bloku
.macro moveBlock from, to, length
        ldr   x1, =\from        // adresa bloku pro cteni
        ldr   x2, =\to          // adresa bloku pro zapis
        mov   x4, #\length      // pocet bajtu
loop:
        ldrb  w3, [x1], 1       // cteni bajtu
        strb  w3, [x2], 1       // zapis bajtu
        sub   x4, x4, #1        // zmenseni pocitadla
        cbnz  x4, loop          // pokud jsme se nedostali k nule, skok na zacatek smycky
.endm

Toto makro sice bude (alespoň zdánlivě) fungovat, ale pouze v tom případě, že bude použito jen jedinkrát. Pokud makro použijeme dvakrát (či samozřejmě vícekrát), vytvoří se v překládaném kódu větší množství návěští se shodným názvem loop, což samozřejmě povede k chybě při překladu. Ostatně můžeme si to vyzkoušet v dalším demonstračním příkladu, kde přesouváme stejný blok dvakrát za sebou (zmíněná část kódu je dvakrát podtržena):

# asmsyntax=as
 
# Presun bloku dat.
#
# Autor: Pavel Tisnovsky
 
 
 
# Linux kernel system call table
sys_exit=93
sys_write=64
 
# List of syscalls for AArch64:
# https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
 
# pocet bajtu
rep_count  = 13
 
 
# Deklarace makra pro ukonceni aplikace
.macro exit
        mov  x8, #sys_exit      // cislo sycallu pro funkci "exit"
        mov  x0, #0             // exit code = 0
        svc  0                  // volani Linuxoveho kernelu
.endm
 
 
 
# Deklarace makra pro vytisteni zpravy na standardni vystup
.macro writeMessage message,messageLength
        mov  x8, #sys_write       // cislo sycallu pro funkci "write"
        mov  x0, #1               // standardni vystup
        ldr  x1, =\message        // adresa retezce, ktery se ma vytisknout
        mov  x2, #\messageLength  // pocet znaku, ktere se maji vytisknout
        svc  0                    // volani Linuxoveho kernelu
.endm
 
 
# Deklarace makra pro presun bloku
.macro moveBlock from,to,length
        ldr   x1, =\from        // adresa bloku pro cteni
        ldr   x2, =\to          // adresa bloku pro zapis
        mov   x4, #\length      // pocet bajtu
loop:
        ldrb  w3, [x1], 1       // cteni bajtu
        strb  w3, [x2], 1       // zapis bajtu
        sub   x4, x4, #1        // zmenseni pocitadla
        cbnz  x4, loop          // pokud jsme se nedostali k nule, skok na zacatek smycky
.endm
 
 
 
#-----------------------------------------------------------------------------
.section .data
 
hello_lbl:
        .string "Hello World!\n"
 
buffer:
        .string "************\n"
 
#-----------------------------------------------------------------------------
.section .bss
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start          // tento symbol ma byt dostupny i linkeru
 
_start:
        writeMessage buffer, rep_count
 
        moveBlock hello_lbl, buffer, rep_count
        moveBlock hello_lbl, buffer, rep_count
 
        writeMessage buffer, rep_count
 
        exit

Takový program ovšem nelze přeložit:

$ as move3.s
 
move3.s: Assembler messages:
move3.s:75: Error: symbol `loop' is already defined

Pro zajímavost se podívejme, jak vypadá listing překládaného programu při výskytu takové chyby (ukážeme si pouze relevantní část):

$ as -alm move3.s
 
  74                            moveBlock hello_lbl, buffer, rep_count
  74 ???? 21030058      >  ldr x1,=hello_lbl
  74 ???? C2020058      >  ldr x2,=buffer
  74 ???? A40180D2      >  mov x4,#rep_count
  74                    > loop:
  74 ???? 23144038      >  ldrb w3,[x1],1
  74 ???? 43140038      >  strb w3,[x2],1
  74 ???? 840400D1      >  sub x4,x4,#1
  74 ???? A4FFFFB5      >  cbnz x4,loop
  75                            moveBlock hello_lbl, buffer, rep_count
  75 ???? 41020058      >  ldr x1,=hello_lbl
  75 ???? E2010058      >  ldr x2,=buffer
  75 ???? A40180D2      >  mov x4,#rep_count
  75                    > loop:
  75 ???? 23144038      >  ldrb w3,[x1],1
  75 ???? 43140038      >  strb w3,[x2],1
  75 ???? 840400D1      >  sub x4,x4,#1
  75 ???? C4FEFFB5      >  cbnz x4,loop
Poznámka: vidíme, že se assembleru nepodařilo zjistit všechny adresy (poslední krok překladu), takže na určitá místa musel pouze doplnit otazníky.

Vzhledem k tomu, že se při práci s makry velmi často setkáme s nutností vytvořit symboly (například právě návěští) s unikátními jmény, obsahuje GNU Assembler velmi jednoduše použitelný nástroj, který je možné v makrech využít. Jedná se o počitadlo použití maker – při každém použití makra se toto počitadlo automaticky zvýší o jedničku. Pokud tedy hodnotu počitadla spojíme s prefixem návěští, budeme mít jistotu, že se vždycky vytvoří unikátní jméno – nové použití makra zvýší počitadlo o jedničku. Počitadlo je v makrech představováno znakem @, před nějž musíme zapsat zpětné lomítko, ostatně jako i v případě parametrů atd. Upravená verze makra pro výpis zprávy může vypadat následovně:

# Deklarace makra pro presun bloku
.macro moveBlock from, to, length
        ldr   x1, =\from        // adresa bloku pro cteni
        ldr   x2, =\to          // adresa bloku pro zapis
        mov   x4, #\length      // pocet bajtu
loop\@:
        ldrb  w3, [x1], 1       // cteni bajtu
        strb  w3, [x2], 1       // zapis bajtu
        sub   x4, x4, #1        // zmenseni pocitadla
        cbnz  x4, loop\@        // pokud jsme se nedostali k nule, skok na zacatek smycky
.endm
Poznámka: symbol @ je platný pouze uvnitř maker.

Na následujícím výpisu vidíme činnost počitadla symbolu loop\@ v praxi:

skoleni

  74                            moveBlock hello_lbl, buffer, rep_count
  74 0014 21030058      >  ldr x1,=hello_lbl
  74 0018 C2020058      >  ldr x2,=buffer
  74 001c A40180D2      >  mov x4,#rep_count
  74                    > loop1:
  74 0020 23144038      >  ldrb w3,[x1],1
  74 0024 43140038      >  strb w3,[x2],1
  74 0028 840400D1      >  sub x4,x4,#1
  74 002c A4FFFFB5      >  cbnz x4,loop1
  75                            moveBlock hello_lbl, buffer, rep_count
  75 0030 41020058      >  ldr x1,=hello_lbl
  75 0034 E2010058      >  ldr x2,=buffer
  75 0038 A40180D2      >  mov x4,#rep_count
  75                    > loop2:
  75 003c 23144038      >  ldrb w3,[x1],1
  75 0040 43140038      >  strb w3,[x2],1
  75 0044 840400D1      >  sub x4,x4,#1
  75 0048 A4FFFFB5      >  cbnz x4,loop2

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

Všechny dnes popisované demonstrační příklady byly společně s podpůrným souborem Makefile určeným pro jejich překlad či naopak pro disassembling, uloženy do GIT repositáře dostupného na adrese https://github.com/tisnik/pre­sentations/. Všechny příklady jsou určeny pro GNU Assembler a používají výchozí syntaxi procesorů Aarch64. Následuje tabulka s odkazy na zdrojové kódy příkladů i na již zmíněné podpůrné skripty:

Příklad Popis Zdrojový kód
template.s šablona pro programy psané v assembleru https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/template.s
template2.s šablona pro programy psané v assembleru, založeno na makrech https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/template.s
hello_world1.s základní podoba programu typu „Hello, world!“ https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/hello_wor­ld1.s
hello_world2.s přepis tištěného řetězce https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/hello_wor­ld2.s
hello_world3.s refaktoring, použití maker https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/hello_wor­ld3.s
aritmetic1.s základní aritmetické operace https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/aritmetic1­.s
infinite_loop.s nekonečná programová smyčka realizovaná instrukcí b https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/infinite_lo­op.s
loop1-aarch64-v1.s základní varianta počítané programové smyčky https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/loop1-aarch64-v1.s
loop1-aarch64-v2.s optimalizace – odstranění instrukce CMP https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/loop1-aarch64-v2.s
loop1-aarch64-v3.s optimalizace – použití instrukce CBNZ https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/loop1-aarch64-v3.s
loop1-aarch64-v4.s automatické zvýšení adresy (ukazatele) https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/loop1-aarch64-v4.s
move1.s přesun bloku dat https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/move1.s
move2.s přepis s využitím maker https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/move2.s
move3.s problém s vícenásobným použitím makra https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/move3.s
move4.s vyřešení předchozího problému https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/move4.s
     
Makefile soubor pro překlad všech příkladů nástrojem make https://github.com/tisnik/pre­sentations/blob/master/as­sembler/aarch64/Makefile

19. Předchozí články o architektuře AArch64

S architekturou AArch64 jsme se již na stránkách Roota setkali, a to konkrétně v následující pětici článků, z nichž dnes vycházíme:

  1. 64bitové mikroprocesory s architekturou AArch64
    https://www.root.cz/clanky/64bitove-mikroprocesory-s-architekturou-aarch64/
  2. Instrukční sada AArch64
    https://www.root.cz/clanky/instrukcni-sada-aarch64/
  3. Instrukční sada AArch64 (2.část)
    https://www.root.cz/clanky/instrukcni-sada-aarch64–2-cast/
  4. Tvorba a ladění programů v assembleru mikroprocesorů AArch64
    https://www.root.cz/clanky/tvorba-a-ladeni-programu-v-assembleru-mikroprocesoru-aarch64/
  5. Instrukční sada AArch64: technologie NEON
    https://www.root.cz/clanky/instrukcni-sada-aarch64-technologie-neon/

20. Odkazy na Internetu

  1. Arm Architecture Reference Manual for A-profile architecture
    https://developer.arm.com/do­cumentation/ddi0487/latest
  2. The GNU Assembler – macros
    http://tigcc.ticalc.org/doc/gnu­asm.html#SEC109
  3. GNU Binutils
    https://sourceware.org/binutils/
  4. Documentation for binutils 2.38
    https://sourceware.org/binutils/docs-2.38/
  5. AArch64 Instruction Set Architecture
    https://developer.arm.com/ar­chitectures/learn-the-architecture/aarch64-instruction-set-architecture/instruction-sets-in-the-arm-architecture
  6. Arm Armv8-A A32/T32 Instruction Set Architecture
    https://developer.arm.com/do­cumentation/ddi0597/2021–12/?lang=en
  7. Comparison of ARMv8-A cores
    https://en.wikipedia.org/wi­ki/Comparison_of_ARMv8-A_cores
  8. Cortex-A32 Processor
    https://www.arm.com/produc­ts/processors/cortex-a/cortex-a32-processor.php
  9. Cortex-A35 Processor
    https://www.arm.com/produc­ts/processors/cortex-a/cortex-a35-processor.php
  10. Cortex-A53 Processor
    https://www.arm.com/produc­ts/processors/cortex-a/cortex-a53-processor.php
  11. Cortex-A57 Processor
    https://www.arm.com/produc­ts/processors/cortex-a/cortex-a57-processor.php
  12. Cortex-A72 Processor
    https://www.arm.com/produc­ts/processors/cortex-a/cortex-a72-processor.php
  13. Cortex-A73 Processor
    https://www.arm.com/produc­ts/processors/cortex-a/cortex-a73-processor.php
  14. Apple A7 (SoC založen na CPU Cyclone)
    https://en.wikipedia.org/wi­ki/Apple_A7
  15. System cally pro AArch64 na Linuxu
    https://github.com/torval­ds/linux/blob/master/inclu­de/uapi/asm-generic/unistd.h
  16. Architectures/AArch64 (FedoraProject.org)
    https://fedoraproject.org/wi­ki/Architectures/AArch64
  17. SIG pro AArch64 (CentOS)
    https://wiki.centos.org/Spe­cialInterestGroup/AltArch/A­Arch64
  18. The ARMv8 instruction sets
    http://infocenter.arm.com/hel­p/index.jsp?topic=/com.ar­m.doc.den0024a/ch05s01.html
  19. A64 Instruction Set
    https://developer.arm.com/pro­ducts/architecture/instruc­tion-sets/a64-instruction-set
  20. Switching between the instruction sets
    http://infocenter.arm.com/hel­p/index.jsp?topic=/com.ar­m.doc.den0024a/ch05s01.html
  21. The A64 instruction set
    http://infocenter.arm.com/hel­p/index.jsp?topic=/com.ar­m.doc.den0024a/ch05s01.html
  22. Introduction to ARMv8 64-bit Architecture
    https://quequero.org/2014/04/in­troduction-to-arm-architecture/
  23. MCU market turns to 32-bits and ARM
    http://www.eetimes.com/do­cument.asp?doc_id=1280803
  24. Cortex-M0 Processor (ARM Holdings)
    http://www.arm.com/produc­ts/processors/cortex-m/cortex-m0.php
  25. Cortex-M0+ Processor (ARM Holdings)
    http://www.arm.com/produc­ts/processors/cortex-m/cortex-m0plus.php
  26. ARM Processors in a Mixed Signal World
    http://www.eeweb.com/blog/arm/arm-processors-in-a-mixed-signal-world
  27. ARM Architecture (Wikipedia)
    https://en.wikipedia.org/wi­ki/ARM_architecture
  28. DSP for Cortex-M
    https://developer.arm.com/techno­logies/dsp/dsp-for-cortex-m
  29. Cortex-M processors in DSP applications? Why not?!
    https://community.arm.com/pro­cessors/b/blog/posts/cortex-m-processors-in-dsp-applications-why-not
  30. White Paper – DSP capabilities of Cortex-M4 and Cortex-M7
    https://community.arm.com/pro­cessors/b/blog/posts/white-paper-dsp-capabilities-of-cortex-m4-and-cortex-m7
  31. Q (number format)
    https://en.wikipedia.org/wi­ki/Q_%28number_format%29
  32. TriCore Architecture & Core
    http://www.infineon.com/cms/en/pro­duct/microcontroller/32-bit-tricore-tm-microcontroller/tricore-tm-architecture-and-core/channel.html?channel=ff80808112ab681d0112­ab6b73d40837
  33. TriCoreTM V1.6 Instruction Set: 32-bit Unified Processor Core
    http://www.infineon.com/dgdl/tc_v131_in­structionset_v138.pdf?file­Id=db3a304412b407950112b409b6dd0352
  34. TriCore v2.2 C Compiler, Assembler, Linker Reference Manual
    http://tasking.com/suppor­t/tricore/tc_reference_gu­ide_v2.2.pdf
  35. Infineon TriCore (Wikipedia)
    https://en.wikipedia.org/wi­ki/Infineon_TriCore
  36. C166®S V2 Architecture & Core
    http://www.infineon.com/cms/en/pro­duct/microcontroller/16-bit-c166-microcontroller/c166-s-v2-architecture-and-core/channel.html?channel=db3a304312bef5660112­c3011c7d01ae
  37. Comparing four 32-bit soft processor cores
    http://www.eetimes.com/au­thor.asp?section_id=14&doc_id=1286116
  38. RISC-V Instruction Set
    http://riscv.org/download­.html#spec_compressed_isa
  39. RISC-V Spike (ISA Simulator)
    http://riscv.org/download.html#isa-sim
  40. RISC-V (Wikipedia)
    https://en.wikipedia.org/wiki/RISC-V
  41. David Patterson (Wikipedia)
    https://en.wikipedia.org/wi­ki/David_Patterson_(compu­ter_scientist)
  42. OpenRISC (oficiální stránky projektu)
    http://openrisc.io/
  43. OpenRISC architecture
    http://openrisc.io/architecture.html
  44. Emulátor OpenRISC CPU v JavaScriptu
    http://s-macke.github.io/jor1k/demos/main.html
  45. OpenRISC (Wikipedia)
    https://en.wikipedia.org/wi­ki/OpenRISC
  46. OpenRISC – instrukce
    http://sourceware.org/cgen/gen-doc/openrisc-insn.html
  47. OpenRISC – slajdy z přednášky o projektu
    https://iis.ee.ethz.ch/~gmichi/a­socd/lecturenotes/Lecture6­.pdf
  48. Berkeley RISC
    http://en.wikipedia.org/wi­ki/Berkeley_RISC
  49. Great moments in microprocessor history
    http://www.ibm.com/develo­perworks/library/pa-microhist.html
  50. Microprogram-Based Processors
    http://research.microsoft.com/en-us/um/people/gbell/Computer_Struc­tures_Principles_and_Exam­ples/csp0167.htm
  51. Great Microprocessors of the Past and Present
    http://www.cpushack.com/CPU/cpu1.html
  52. A Brief History of Microprogramming
    http://www.cs.clemson.edu/~mar­k/uprog.html
  53. What is RISC?
    http://www-cs-faculty.stanford.edu/~ero­berts/courses/soco/projec­ts/2000–01/risc/whatis/
  54. RISC vs. CISC
    http://www-cs-faculty.stanford.edu/~ero­berts/courses/soco/projec­ts/2000–01/risc/risccisc/
  55. RISC and CISC definitions:
    http://www.cpushack.com/CPU/cpu­AppendA.html
  56. FPGA
    https://cs.wikipedia.org/wi­ki/Programovateln%C3%A9_hra­dlov%C3%A9_pole
  57. The Evolution of RISC
    http://www.ibm.com/develo­perworks/library/pa-microhist.html#sidebar1
  58. disasm.pro
    https://disasm.pro/
  59. Exploring AArch64 assembler – Chapter 5
    https://thinkingeek.com/2016/11/13/ex­ploring-aarch64-assembler-chapter-5/

Autor článku

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.