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
7. Úprava předchozího příkladu – použití instrukce CBNZ
9. Úprava předchozích příkladů s využitím automatické změny adresy
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
18. Repositář s demonstračními příklady
19. Předchozí články o architektuře AArch64
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:
- Nejvyšších šest bitů obsahuje konstantu 000101
- 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
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:
- Nejvyšších osm bitů obsahuje konstantu 01010100
- 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)
- Další bit je nulový
- 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:
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
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ě:
- Pět bitů pro uložení indexu registru, který je ukládán (resp. jeho osm bitů)
- Pět bitů pro uložení registru s adresou
- 9 bitů pro uložení konstanty pro operace pre-inkrementu/dekrementu popř. post-inkrementu/dekrementu
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 }
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:
- Registr s adresou bajtu, který se má načíst
- Registr s adresou, kam se má přenášený bajt uložit
- Registr sloužící jako mezipaměť: bude obsahovat právě přenášený bajt
- Registr fungující jako počitadlo smyčky
Můžeme například použít registry x1 až x4:
- x1 adresa bajtu, který se má načíst
- x2 adresa, kam se má přenášený bajt uložit
- x3/w3 mezipaměť: bude obsahovat právě přenášený bajt
- 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ů
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
Š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
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
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
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
Na následujícím výpisu vidíme činnost počitadla symbolu loop\@ v praxi:
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/presentations/. 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:
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:
- 64bitové mikroprocesory s architekturou AArch64
https://www.root.cz/clanky/64bitove-mikroprocesory-s-architekturou-aarch64/ - Instrukční sada AArch64
https://www.root.cz/clanky/instrukcni-sada-aarch64/ - Instrukční sada AArch64 (2.část)
https://www.root.cz/clanky/instrukcni-sada-aarch64–2-cast/ - Tvorba a ladění programů v assembleru mikroprocesorů AArch64
https://www.root.cz/clanky/tvorba-a-ladeni-programu-v-assembleru-mikroprocesoru-aarch64/ - Instrukční sada AArch64: technologie NEON
https://www.root.cz/clanky/instrukcni-sada-aarch64-technologie-neon/
20. Odkazy na Internetu
- Arm Architecture Reference Manual for A-profile architecture
https://developer.arm.com/documentation/ddi0487/latest - The GNU Assembler – macros
http://tigcc.ticalc.org/doc/gnuasm.html#SEC109 - GNU Binutils
https://sourceware.org/binutils/ - Documentation for binutils 2.38
https://sourceware.org/binutils/docs-2.38/ - AArch64 Instruction Set Architecture
https://developer.arm.com/architectures/learn-the-architecture/aarch64-instruction-set-architecture/instruction-sets-in-the-arm-architecture - Arm Armv8-A A32/T32 Instruction Set Architecture
https://developer.arm.com/documentation/ddi0597/2021–12/?lang=en - Comparison of ARMv8-A cores
https://en.wikipedia.org/wiki/Comparison_of_ARMv8-A_cores - Cortex-A32 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a32-processor.php - Cortex-A35 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a35-processor.php - Cortex-A53 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a53-processor.php - Cortex-A57 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a57-processor.php - Cortex-A72 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a72-processor.php - Cortex-A73 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a73-processor.php - Apple A7 (SoC založen na CPU Cyclone)
https://en.wikipedia.org/wiki/Apple_A7 - System cally pro AArch64 na Linuxu
https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h - Architectures/AArch64 (FedoraProject.org)
https://fedoraproject.org/wiki/Architectures/AArch64 - SIG pro AArch64 (CentOS)
https://wiki.centos.org/SpecialInterestGroup/AltArch/AArch64 - The ARMv8 instruction sets
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/ch05s01.html - A64 Instruction Set
https://developer.arm.com/products/architecture/instruction-sets/a64-instruction-set - Switching between the instruction sets
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/ch05s01.html - The A64 instruction set
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/ch05s01.html - Introduction to ARMv8 64-bit Architecture
https://quequero.org/2014/04/introduction-to-arm-architecture/ - MCU market turns to 32-bits and ARM
http://www.eetimes.com/document.asp?doc_id=1280803 - Cortex-M0 Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0.php - Cortex-M0+ Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0plus.php - ARM Processors in a Mixed Signal World
http://www.eeweb.com/blog/arm/arm-processors-in-a-mixed-signal-world - ARM Architecture (Wikipedia)
https://en.wikipedia.org/wiki/ARM_architecture - DSP for Cortex-M
https://developer.arm.com/technologies/dsp/dsp-for-cortex-m - Cortex-M processors in DSP applications? Why not?!
https://community.arm.com/processors/b/blog/posts/cortex-m-processors-in-dsp-applications-why-not - White Paper – DSP capabilities of Cortex-M4 and Cortex-M7
https://community.arm.com/processors/b/blog/posts/white-paper-dsp-capabilities-of-cortex-m4-and-cortex-m7 - Q (number format)
https://en.wikipedia.org/wiki/Q_%28number_format%29 - TriCore Architecture & Core
http://www.infineon.com/cms/en/product/microcontroller/32-bit-tricore-tm-microcontroller/tricore-tm-architecture-and-core/channel.html?channel=ff80808112ab681d0112ab6b73d40837 - TriCoreTM V1.6 Instruction Set: 32-bit Unified Processor Core
http://www.infineon.com/dgdl/tc_v131_instructionset_v138.pdf?fileId=db3a304412b407950112b409b6dd0352 - TriCore v2.2 C Compiler, Assembler, Linker Reference Manual
http://tasking.com/support/tricore/tc_reference_guide_v2.2.pdf - Infineon TriCore (Wikipedia)
https://en.wikipedia.org/wiki/Infineon_TriCore - C166®S V2 Architecture & Core
http://www.infineon.com/cms/en/product/microcontroller/16-bit-c166-microcontroller/c166-s-v2-architecture-and-core/channel.html?channel=db3a304312bef5660112c3011c7d01ae - Comparing four 32-bit soft processor cores
http://www.eetimes.com/author.asp?section_id=14&doc_id=1286116 - RISC-V Instruction Set
http://riscv.org/download.html#spec_compressed_isa - RISC-V Spike (ISA Simulator)
http://riscv.org/download.html#isa-sim - RISC-V (Wikipedia)
https://en.wikipedia.org/wiki/RISC-V - David Patterson (Wikipedia)
https://en.wikipedia.org/wiki/David_Patterson_(computer_scientist) - OpenRISC (oficiální stránky projektu)
http://openrisc.io/ - OpenRISC architecture
http://openrisc.io/architecture.html - Emulátor OpenRISC CPU v JavaScriptu
http://s-macke.github.io/jor1k/demos/main.html - OpenRISC (Wikipedia)
https://en.wikipedia.org/wiki/OpenRISC - OpenRISC – instrukce
http://sourceware.org/cgen/gen-doc/openrisc-insn.html - OpenRISC – slajdy z přednášky o projektu
https://iis.ee.ethz.ch/~gmichi/asocd/lecturenotes/Lecture6.pdf - Berkeley RISC
http://en.wikipedia.org/wiki/Berkeley_RISC - Great moments in microprocessor history
http://www.ibm.com/developerworks/library/pa-microhist.html - Microprogram-Based Processors
http://research.microsoft.com/en-us/um/people/gbell/Computer_Structures_Principles_and_Examples/csp0167.htm - Great Microprocessors of the Past and Present
http://www.cpushack.com/CPU/cpu1.html - A Brief History of Microprogramming
http://www.cs.clemson.edu/~mark/uprog.html - What is RISC?
http://www-cs-faculty.stanford.edu/~eroberts/courses/soco/projects/2000–01/risc/whatis/ - RISC vs. CISC
http://www-cs-faculty.stanford.edu/~eroberts/courses/soco/projects/2000–01/risc/risccisc/ - RISC and CISC definitions:
http://www.cpushack.com/CPU/cpuAppendA.html - FPGA
https://cs.wikipedia.org/wiki/Programovateln%C3%A9_hradlov%C3%A9_pole - The Evolution of RISC
http://www.ibm.com/developerworks/library/pa-microhist.html#sidebar1 - disasm.pro
https://disasm.pro/ - Exploring AArch64 assembler – Chapter 5
https://thinkingeek.com/2016/11/13/exploring-aarch64-assembler-chapter-5/