Příznakové bity a podmíněné skoky, se kterými jsme se setkali už ve čtvrté části tohoto seriálu, se samozřejmě nepoužívají pouze na platformách i386 a x86_64. Podobný systém můžeme nalézt i u mikroprocesorů s architekturou ARM, v nichž je dokonce možné použít podmínky u prakticky všech instrukcí. Právě s tímto zajímavým konceptem se dnes podrobněji seznámíme.
Obsah
1. Použití assembleru v Linuxu: podmínky, rozvětvení a programové smyčky na procesorech ARM
2. Malé zopakování z minula – příznakové bity a instrukce podmíněného skoku na i386 a x86_64
3. Stavové registry na mikroprocesorech s architekturou ARM
4. Příznakové a stavové bity na mikroprocesorech s architekturou ARM
5. Podmínky specifikované u instrukcí (condition codes)
6. Instrukce podmíněného i nepodmíněného skoku v režimu ARM
7. Nastavení příznakových bitů u aritmetických instrukcí
8. První praktický příklad – jednoduchá počítaná programová smyčka
9. Druhý praktický příklad – nastavení příznakových bitů při odečítání jedničky instrukcí sub
10. Třetí praktický příklad – počítaná programová smyčka s testem provedeným na začátku
11. Čtvrtý praktický příklad – optimalizace předchozího příkladu instrukcí subs
12. Repositář s demonstračními příklady
13. Optimalizace kódu (počtu instrukcí) při výpočtu největšího společného dělitele
1. Použití assembleru v Linuxu: podmínky, rozvětvení a programové smyčky na procesorech ARM
V předchozí části seriálu o použití assembleru v operačním systému Linux jsme se seznámili s takzvanými příznakovými bity, které se v assembleru používají společně s instrukcemi určenými pro podmíněné skoky k implementaci rozvětvení a taktéž pro implementaci různých typů programových smyček. Ukázali jsme si, jak je tento koncept využitý u mikroprocesorů s 32bitovou architekturou i386 i se 64bitovou architekturou x86_64. Příznakové bity se ovšem používají i u dalších mikroprocesorových architektur, včetně populárních čipů typu ARM (určitou výjimku z tohoto trendu představují čipy s architekturami MIPS, RISC-V, DEC Alpha a taktéž některé VLIW architektury). Právě architekturou ARM a konceptem příznakových bitů kombinovaných s takzvanými podmínkovými kódy se budeme zabývat v dnešním článku. Navíc si přepíšeme dva demonstrační příklady ukázané minule do formy vhodné právě pro mikroprocesory s architekturou ARM. Prozatím se pro jednoduchost budeme zabývat „klasickou“ 32bitovou RISCovou instrukční sadou těchto mikroprocesorů, nikoli sadou Thumb či Thumb-2.
2. Malé zopakování z minula – příznakové bity a instrukce podmíněného skoku na i386 a x86_64
Pro úplnost a i pro vzájemné porovnání různých přístupů k dané problematice si jen velmi stručně připomeňme, jakým způsobem je koncept příznakových bitů a podmíněných skoků realizován na mikroprocesorech s architekturou i386 a x86_64. Všechny příznakové bity jsou uloženy v registru EFLAGS (32bitové prostředí) popř. RFLAGS (64bitové prostředí), přičemž mezi základní příznaky používané v praxi velmi často patří především příznaky nazvané Carry flag, Sign flag, Zero flag a Overflow flag, tj. příznak přenosu, příznak záporného výsledku, příznak nulovosti a příznak přetečení. Význam těchto příznakových bitů se shrnut v následující tabulce:
Příznak | Význam zkratky | Poznámka |
---|---|---|
ZF | zero flag | výsledek předchozí operace je nulový |
CF | carry flag | přenos (bezznaménková aritmetika, unsigned) |
SF | sign flag | výsledek je záporný (nastaven nejvyšší bit bajtu či slova) |
OF | overflow flag | přenos/přetečení ve znaménkové aritmetice (signed) |
Tyto příznakové bity jsou nastavovány aritmetickými instrukcemi i instrukcemi určenými pro porovnání operandů. Dále mohou být otestovány a použity v instrukcích podmíněných skoků. Pro jednoduchost na chvíli zapomeňme na práci s čísly se znaménkem (těm bude věnován samostatný článek, protože se jedná o relativně rozsáhlou problematiku), takže se počet podmíněných skoků prozatím redukuje na pouhých šest instrukcí:
Mnemotechnická zkratka instrukce | Význam instrukce podmíněného skoku |
---|---|
JC | podmíněný skok provedený za předpokladu, že je nastaven příznak přenosu (Carry flag) |
JNC | podmíněný skok provedený za předpokladu, že je vynulován příznak přenosu (Carry flag) |
JZ | podmíněný skok provedený za předpokladu, že je nastaven příznak nulovosti (Zero flag) |
JNZ | podmíněný skok provedený za předpokladu, že je vynulován příznak nulovosti (Zero flag) |
JS | podmíněný skok provedený za předpokladu, že je nastaven příznak záporného výsledku (Sign flag) |
JNS | podmíněný skok provedený za předpokladu, že je vynulován příznak záporného výsledku (Sign flag) |
Pro některé z výše uvedených instrukcí určených pro provedení podmíněných skoků existují i takzvané jmenné aliasy, což jsou ve skutečnosti zcela totožné instrukce, které ovšem mají jiné jméno. S těmito aliasy je dobré se seznámit, protože je nalezneme například při výpisu disassembleru či při práci s některými debuggery (používá je například i GNU Debugger a nástroj objdump, což jsme si ověřili minule):
Instrukce | Alias |
---|---|
JZ | JE |
JNZ | JNE |
JC | JB, JNAE |
JNC | JNB, JAE |
JS | nemá alias |
JNS | nemá alias |
Poznámka: povšimněte si, že každá instrukce skoku testuje pouze jediný příznakový bit (jeho nulovost či naopak nenulovost). Ve skutečnosti však existují i takové skokové instrukce, v nichž se testuje kombinace několika příznakových bitů či jejich negací. S těmito instrukcemi a především s jejich významem pro praxi se seznámíme příště.
3. Stavové registry na mikroprocesorech s architekturou ARM
Nyní se již konečně můžeme zabývat populární architekturou ARM. Kromě patnácti 32bitových pracovních registrů a programového čítače obsahují mikroprocesory s touto architekturou i registry, v nichž se uchovávají různé příznaky. V uživatelském režimu se pracuje s příznaky uloženými v registru nazvaném CPSR (Current Program Status Register) a pro každý další režim existuje navíc zvláštní registr nazvaný SPSR (Saved Program Status Register), v němž jsou uchovány původní příznaky ze CPSR. Podobně jako všechny pracovní registry, mají i registry CPSR a SPSR shodnou šířku 32 bitů, což má svoje výhody. Mimo jiné i to, že šířka 32 bitů ponechala konstruktérům procesorů ARM mnoho prostoru pro uložení různých důležitých informací do registrů CPSR/SPSR, takže se nemuseli uchylovat k nepříliš promyšleným technikám známým například z platformy x86, kde se původně šestnáctibitový registr FLAGS (8086) postupně změnil na 32bitový registr EFLAGS (80386), vedle něho vznikl registr MSW (80286) rozšířený na CR0 atd.
4. Příznakové a stavové bity na mikroprocesorech s architekturou ARM
Ve výše zmíněných stavových registrech CPSR/SPSR mikroprocesorů ARM jsou uloženy především příznakové bity nastavované aritmeticko-logickou jednotkou při provádění základních aritmetických instrukcí či bitových operací, dále pak bity určující, jakou instrukční sadu mikroprocesor v daný okamžik zpracovává (ARM, Thumb, Jazelle), příznak pořadí zpracovávání bajtů (little/big endian) a taktéž příznaky používané u SIMD operací. Zdaleka ne všechny mikroprocesory ARM však skutečně pracují se všemi bity, což je logické, protože například příznak Q je používán jen u mikroprocesorů podporujících aritmetiku se saturací, příznak J u čipů s podporou technologie Jazelle atd. Pojďme si tedy jednotlivé příznakové i stavové bity vypsat. Povšimněte si, že především první čtyři bity mají prakticky shodný název i stejný význam, jako je tomu u již popsané architektury i386 a x86_64 (rozdíl je jen v pojmenování příznaku sign flag a negative flag, význam je však shodný):
Příznak | Význam zkratky | Poznámka |
---|---|---|
N | negative | výsledek ALU operace je záporný |
V | overflow | přetečení (znaménková aritmetika, signed) |
Z | zero | výsledek je nulový |
C | carry | přenos (bezznaménková aritmetika, unsigned) |
Q | sticky overflow | aritmetika se saturací, od ARMv5e výše |
I | interrupt | zákaz IRQ (přerušení) |
F | fast interrupt | zákaz FIRQ (rychlého přerušení) |
T | thumb | příznak zpracování instrukční sady Thumb (jen u procesorů se znakem "T" v názvu) |
J | jazelle | příznak zpracování instrukční sady Jazelle (jen u procesorů se znakem "J" v názvu) |
E | endianness | pořadí bajtů při práci s RAM (big/little endian) |
GE | 4 bity | použito u SIMD operací (pouze některé čipy) |
IF | 5 bitů | použito u instrukcí Thumb2 (pouze některé čipy) |
M | 5 bitů | režim práce mikroprocesoru (user, IRQ, FIRQ, ...) |
Poznámka: v tabulce zobrazené výše nejsou jednotlivé bity uvedeny v takovém pořadí, v jakém se nachází ve stavovém registru; sdruženy jsou podle své funkce.
V dalším textu nás budou zajímat opět pouze tři příznakové bity: Z, C a N, podobně jako tomu bylo ve čtvrté části tohoto seriálu.
5. Podmínky specifikované u instrukcí (condition codes)
U klasické RISCové instrukční sady ARM se v nejvyšších čtyřech bitech každé instrukce nachází takzvaný kód podmínky. Konstruktéři těchto mikroprocesorů totiž (alespoň částečně) vyřešili problematiku podmíněných skoků tím, že umožnili vykonat každou instrukci pouze v tom případě, že je splněna podmínka, jejích kód je zapsán právě v oněch čtyřech nejvyšších bitech instrukce. A o jakou problematiku podmíněných skoků se vlastně jedná? Podmíněné skoky představují pro klasickou RISCovou pipeline obtížný úkol: důvodem existence instrukční pipeline je to, aby se v každém taktu v ideálním případě dokončila jedna instrukce. U skoků, zvláště těch podmíněných, se však již před rozhodnutím, zda se skok provede či nikoli, začnou zpracovávat další instrukce umístěné za skokem, což však znamená, že se v případě provedení skoku tyto instrukce ve skutečnosti nemají vykonat. Konstruktéři RISCových a posléze i CISCových mikroprocesorů tedy hledali různé způsoby řešení této problematiky, ať se již jedná o spekulativní provádění instrukcí (příliš mnoho tranzistorů) či o prediktory skoků (ne vždy jsou úspěšné).
Díky tomu, že u mikroprocesorů ARM lze podmínku vykonání zadat u každé instrukce, je možné, aby se celkový počet podmíněných skoků v programu minimalizoval. Zejména se to týká skoků používaných pro implementaci programové konstrukce if-then-else, kde se v jednotlivých větvích nachází jen malé množství instrukcí. Aby však mělo použití podmínkových kódů smysl, musela se změnit ještě jedna vlastnost procesorů ARM: jejich aritmeticko-logická jednotka totiž změní stavové bity carry, zero, overflow a negative pouze v tom případě, že je to explicitně v instrukčním kódu zapsáno (výjimku tvoří porovnávací instrukce). Touto vlastností se budeme zabývat až v následujícím textu.
První sada podmínkových kódů se používá pro provedení či naopak neprovedení instrukce na základě hodnoty jednoho z příznakových bitů zero, overflow či negative. Poslední podmínkový kód z této skupiny má název AL (Any/Always) a značí, že se instrukce provede v každém případě. Tento podmínkový kód se tudíž většinou v assembleru nezapisuje, protože je (celkem pochopitelně) považován za implicitní:
Kód | Přípona | Význam | Testovaná podmínka |
---|---|---|---|
0000 | EQ | Z set | rovnost (či nulový výsledek) |
0001 | NE | Z clear | nerovnost (či nenulový výsledek) |
0100 | MI | N set | výsledek je záporný |
0101 | PL | N clear | výsledek je kladný či 0 |
0110 | VS | V set | nastalo přetečení |
0111 | VC | V clear | nenastalo přetečení |
1110 | AL | Any/Always | většinou se nezapisuje, implicitní podmínka |
Další čtyři podmínkové kódy se většinou používají při porovnávání dvou hodnot bez znaménka (unsigned). V těchto případech se testují stavy příznakových bitů carry a zero, přesněji řečeno kombinací těchto bitů:
Kód | Přípona | Význam | Testovaná podmínka |
---|---|---|---|
0010 | CS/HS | C set | >= |
0011 | CC/LO | C clear | < |
1000 | HI | C set and Z clear | > |
1001 | LS | C clear or Z set | <= |
Poslední čtyři podmínkové kódy se používají pro porovnávání hodnot se znaménkem (signed). V těchto případech se namísto příznakových bitů carry a zero testují kombinace bitů negative, overflow a zero:
Kód | Přípona | Význam | Testovaná podmínka |
---|---|---|---|
1010 | GE | N and V the same | >= |
1011 | LT | N and V differ | < |
1100 | GT | Z clear, N == V | > |
1101 | LE | Z set, N != V | <= |
6. Instrukce podmíněného i nepodmíněného skoku v režimu ARM
Podobně, jako je tomu i u dalších instrukčních sad (ISA), najdeme i v instrukčním souboru mikroprocesorů ARM několik instrukcí určených pro provedení skoku. Tyto instrukce lze (poněkud zjednodušeně řečeno) považovat za instrukce, které do registru PC/R15 vloží novou konstantu, popř. navíc uloží původní obsah registru PC/14 do registru LR/R14 (v tomto případě je však situace komplikovanější, protože obsah registru PC se již kvůli posunu instrukce v pipeline zvýšil o hodnotu 8; podrobnosti si řekneme v dalším textu).
Základní instrukcí skoku je instrukce pojmenovaná jednoduše B, což je zkratka od slova branch. 32bitové slovo této instrukce je rozděleno na tři části. V nejvyšších čtyřech bitech se nachází kód podmínky, což v důsledku znamená, že jediná instrukce B může nahradit všechny formy podmíněných skoků (přesněji řečeno čtrnáct typů podmíněných skoků a jeden skok nepodmíněný). Za těmito čtyřmi bity následuje taktéž čtyřbitový operační kód 1010 a ve zbylých 24 bitech je pak uložena konstanta, z níž se vypočítá offset skoku:
31 27 23 0 +------+------+--------------------------+ | cond | 1010 | offset skoku | +------+------+--------------------------+
Vzhledem k tomu, že všechny instrukce jsou v operační paměti zarovnány na adresu dělitelnou čtyřmi, je před provedením skoku 24bitová konstanta obsažená v instrukci posunuta o dva bity doleva. Výsledkem je 26bitová konstanta (mající dva nejnižší bity nulové), která je při provádění skoku přičtena k aktuální hodnotě registru PC. Jednoduše lze spočítat, že díky použití 26bitové konstanty lze provést podmíněný či nepodmíněný skok v rozsahu +-32 MB. Důležité je, že se při překladu programu z assembleru musí při výpočtu offsetu odečíst od aktuální hodnoty registru PC hodnota osm, a to z toho důvodu, že se skok provádí až ve fázi execute, tj. ve chvíli, kdy se již v instrukční pipeline nachází další dvě instrukce: jedna ve fázi decode (ta byla přečtena z adresy původní PC+4) a druhá teprve ve fázi fetch a registr PC již obsahuje adresu této nejpozději načtené instrukce (původní PC+8). Pozdější varianty mikroprocesorů ARM sice již mají pipeline s jiným počtem řezů, ovšem toto chování zůstalo kvůli zpětné kompatibilitě zachováno.
7. Nastavení příznakových bitů u aritmetických instrukcí
Mezi základní aritmetické instrukce patří samozřejmě instrukce součtu a rozdílu. U instrukcí rozdílu je zajímavé, že existují ve dvou variantách podle toho, zda se odečítá první operand od druhého nebo naopak. Motivace je zřejmá – pro oba operandy existují odlišná pravidla:
# | Instrukce | Význam |
---|---|---|
1 | ADD | operand1+operand2 |
2 | ADC | operand1+operand2+carry |
3 | SUB | operand1-operand2 |
4 | SBC | operand1-operand2+carry-1 |
5 | RSB | operand2-operand1 |
6 | RSC | operand2-operand1+carry-1 |
Tyto instrukce navíc ještě ve svém slově obsahují takzvaný S-bit určující, zda má instrukce nastavit příznaky ALU (N, V, Z, C) na základě výsledku operace. Jediné instrukce, u nichž je tento bit nastaven stále, jsou instrukce provádějící porovnání bez uložení výsledku operace (popsané ihned v následujícím odstavci):
# | Instrukce | Význam |
---|---|---|
1 | ADDS | operand1+operand2 a současně nastavení příznakových bitů |
2 | ADCS | operand1+operand2+carry a současně nastavení příznakových bitů |
3 | SUBS | operand1-operand2 a současně nastavení příznakových bitů |
4 | SBCS | operand1-operand2+carry-1 a současně nastavení příznakových bitů |
5 | RSBS | operand2-operand1 a současně nastavení příznakových bitů |
6 | RSCS | operand2-operand1+carry-1 a současně nastavení příznakových bitů |
Další skupinou instrukcí jsou instrukce provádějící nějakou aritmetickou či logickou operaci. Ovšem výsledek této operace se nikam neuloží, pouze se nastaví příznakové bity (navíc se tyto bity nastaví vždy, není zde možnost volby bitu S):
# | Instrukce | Význam |
---|---|---|
1 | CMP | operand1-operand2 |
2 | CMN | operand1+operand2 (compare negative) |
3 | TST | operand1 and operand2 |
4 | TEQ | operand1 xor operand2 |
8. První praktický příklad – jednoduchá počítaná programová smyčka
Konečně se dostáváme k demonstračním příkladům. V prvním demonstračním příkladu je ukázána implementace programové smyčky, v níž se nejprve naplní řetězec čtyřiceti znaky hvězdičky a následně se tento řetězec vytiskne na standardní výstup. Popišme si nyní podrobněji některé zajímavé instrukce. Pro načtení adresy bufferu do registru R1 se použije zápis:
ldr r1, =buffer @ zapis se bude provadet do tohoto bufferu
Samotný zápis do bufferu (tedy do paměti) zajišťuje instrukce strb, tedy „store byte“. Adresa je uložena v registru R1, dolních osm bitů registru R3 obsahuje kód znaku:
strb r3, [r1] @ zapis znaku do bufferu
Samotná programová smyčka s počitadlem reprezentovaným registrem R2 vypadá takto:
loop: strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu sub r2, r2, #1 @ zmenseni pocitadla cmp r2, #0 @ otestovani, zda jsme jiz nedosahli nuly bne loop @ pokud jsme se nedostali k nule, skok na zacatek smycky
Povšimněte si použití instrukce sub pro snížení hodnoty počitadla, po níž následuje test cmp na kontrolu, zda počitadlo dosáhlo nulové hodnoty.
Následuje výpis celého zdrojového kódu tohoto demonstračního příkladu:
# asmsyntax=as # Testovaci program naprogramovany v assembleru GNU as # - pocitana programova smycka # - uprava pro mikroprocesory s architekturou ARM # # Autor: Pavel Tisnovsky # Linux kernel system call table sys_exit = 1 sys_write = 4 # 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 r1, =buffer @ zapis se bude provadet do tohoto bufferu mov r2, $rep_count @ pocet opakovani znaku mov r3, #'*' @ zapisovany znak loop: strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu sub r2, r2, #1 @ zmenseni pocitadla cmp r2, #0 @ otestovani, zda jsme jiz nedosahli nuly bne loop @ pokud jsme se nedostali k nule, skok na zacatek smycky mov r7, $sys_write @ cislo syscallu pro funkci "write" mov r0, $std_output @ standardni vystup ldr r1, =buffer @ adresa retezce, ktery se ma vytisknout mov r2, $rep_count @ pocet znaku, ktere se maji vytisknout svc 0 @ volani Linuxoveho kernelu mov r7, $sys_exit @ cislo sycallu pro funkci "exit" mov r0, #0 @ exit code = 0 svc 0 @ volani Linuxoveho kernelu
Překlad a slinkování se provede příkazy:
as loop1-arm-v1.s -o loop1-arm-v1.o ld -s loop1-arm-v1.o
Výpis obsahu vytvořeného spustitelného souboru:
a.out: file format elf32-littlearm architecture: armv4, flags 0x00000102: EXEC_P, D_PAGED start address 0x00008074 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000044 00008074 00008074 00000074 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .bss 00000028 000100b8 000100b8 000000b8 2**3 ALLOC 2 .ARM.attributes 00000014 00000000 00000000 000000b8 2**0 CONTENTS, READONLY SYMBOL TABLE: no symbols Disassembly of section .text: 00008074 <.text>: 8074: e59f1038 ldr r1, [pc, #56] ; 0x80b4 8078: e3a02028 mov r2, #40 ; 0x28 807c: e3a0302a mov r3, #42 ; 0x2a 8080: e5c13000 strb r3, [r1] 8084: e2811001 add r1, r1, #1 8088: e2422001 sub r2, r2, #1 808c: e3520000 cmp r2, #0 8090: 1afffffa bne 0x8080 8094: e3a07004 mov r7, #4 8098: e3a00001 mov r0, #1 809c: e59f1010 ldr r1, [pc, #16] ; 0x80b4 80a0: e3a02028 mov r2, #40 ; 0x28 80a4: ef000000 svc 0x00000000 80a8: e3a07001 mov r7, #1 80ac: e3a00000 mov r0, #0 80b0: ef000000 svc 0x00000000 80b4: 000100b8 strheq r0, [r1], -r8
9. Druhý praktický příklad – nastavení příznakových bitů při odečítání jedničky instrukcí sub
Programová smyčka implementovaná v předchozím příkladu není příliš dokonalá, protože se zbytečně provádí dvě instrukce pro snížení počitadla a test na nulu namísto použití instrukce jediné:
loop: strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu sub r2, r2, #1 @ zmenseni pocitadla cmp r2, #0 @ otestovani, zda jsme jiz nedosahli nuly bne loop @ pokud jsme se nedostali k nule, skok na zacatek smycky
Z předchozího textu již víme, že když se namísto instrukce sub použije instrukce subs, nastaví se při snížení hodnoty počitadla i příznakové bity, zejména dnes důležitý příznak nulovosti. Smyčku lze tedy přepsat takto:
loop: strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu subs r2, r2, #1 @ zmenseni pocitadla a soucasne nastaveni priznaku bne loop @ pokud jsme se nedostali k nule, skok na zacatek smycky
Nejenom že ušetříme čtyři bajty v objektovém kódu, ale každá iterace bude minimálně o jeden takt rychlejší. Následuje výpis celého zdrojového kódu druhého demonstračního příkladu, který se vlastně příliš neliší od příkladu předchozího (až na odlišnou implementaci testu na ukončení smyčky):
# asmsyntax=as # Testovaci program naprogramovany v assembleru GNU as # - pocitana programova smycka # - uprava pro mikroprocesory s architekturou ARM # # Autor: Pavel Tisnovsky # Linux kernel system call table sys_exit = 1 sys_write = 4 # 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 r1, =buffer @ zapis se bude provadet do tohoto bufferu mov r2, $rep_count @ pocet opakovani znaku mov r3, #'*' @ zapisovany znak loop: strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu subs r2, r2, #1 @ zmenseni pocitadla a soucasne nastaveni priznaku bne loop @ pokud jsme se nedostali k nule, skok na zacatek smycky mov r7, $sys_write @ cislo syscallu pro funkci "write" mov r0, $std_output @ standardni vystup ldr r1, =buffer @ adresa retezce, ktery se ma vytisknout mov r2, $rep_count @ pocet znaku, ktere se maji vytisknout svc 0 @ volani Linuxoveho kernelu mov r7, $sys_exit @ cislo sycallu pro funkci "exit" mov r0, #0 @ exit code = 0 svc 0 @ volani Linuxoveho kernelu
Z disassemblovaného výstupu je patrné, že se skutečně podařilo program zkrátit o čtyři bajty (to samozřejmě není mnoho, ale u větších aplikací se toto zmenšení už pěkně nasčítá):
a.out: file format elf32-littlearm architecture: armv4, flags 0x00000102: EXEC_P, D_PAGED start address 0x00008074 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000040 00008074 00008074 00000074 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .bss 00000028 000100b8 000100b8 000000b8 2**3 ALLOC 2 .ARM.attributes 00000014 00000000 00000000 000000b4 2**0 CONTENTS, READONLY SYMBOL TABLE: no symbols Disassembly of section .text: 00008074 <.text>: 8074: e59f1034 ldr r1, [pc, #52] ; 0x80b0 8078: e3a02028 mov r2, #40 ; 0x28 807c: e3a0302a mov r3, #42 ; 0x2a 8080: e5c13000 strb r3, [r1] 8084: e2811001 add r1, r1, #1 8088: e2522001 subs r2, r2, #1 808c: 1afffffb bne 0x8080 8090: e3a07004 mov r7, #4 8094: e3a00001 mov r0, #1 8098: e59f1010 ldr r1, [pc, #16] ; 0x80b0 809c: e3a02028 mov r2, #40 ; 0x28 80a0: ef000000 svc 0x00000000 80a4: e3a07001 mov r7, #1 80a8: e3a00000 mov r0, #0 80ac: ef000000 svc 0x00000000 80b0: 000100b8 strheq r0, [r1], -r8
10. Třetí praktický příklad – počítaná programová smyčka s testem provedeným na začátku
Ve třetím demonstračním příkladu je ukázána počítaná programová smyčka, v níž se test na ukončení provádí na jejím začátku ihned po odečtení jedničky od počitadla. Tento příklad je vlastně v mnoha ohledech totožný s příkladem předchozím, ovšem vzhledem k tomu, že změna stavu počitadla (snížení jeho hodnoty o jedničku) a následný test je proveden na začátku smyčky, je nutné při inicializaci počitadla do něj vložit hodnotu 41 a nikoli 40, jinak by se vytisklo pouze 39 hvězdiček následovaných ASCII znakem s kódem nula:
mov r2, $rep_count+1 @ pocet opakovani znaku
Samotná programová smyčka je implementována takto (instrukce b na konci představuje nepodmíněný skok na začátek smyčky):
loop: sub r2, r2, #1 @ zmenseni pocitadla cmp r2, #0 @ otestovani, zda jsme jiz nedosahli nuly beq konec @ pokud jsme se dostali k nule, konec smycky strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu b loop @ nepodmineny skok na zacatek smycky konec:
Podívejme se na úplný zdrojový kód příkladu:
# asmsyntax=as # Testovaci program naprogramovany v assembleru GNU as # - pocitana programova smycka s testem na zacatku # - uprava pro mikroprocesory s architekturou ARM # # Autor: Pavel Tisnovsky # Linux kernel system call table sys_exit = 1 sys_write = 4 # 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 r1, =buffer @ zapis se bude provadet do tohoto bufferu mov r2, $rep_count+1 @ pocet opakovani znaku mov r3, #'*' @ zapisovany znak loop: sub r2, r2, #1 @ zmenseni pocitadla cmp r2, #0 @ otestovani, zda jsme jiz nedosahli nuly beq konec @ pokud jsme se dostali k nule, konec smycky strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu b loop @ nepodmineny skok na zacatek smycky konec: mov r7, $sys_write @ cislo syscallu pro funkci "write" mov r0, $std_output @ standardni vystup ldr r1, =buffer @ adresa retezce, ktery se ma vytisknout mov r2, $rep_count @ pocet znaku, ktere se maji vytisknout svc 0 @ volani Linuxoveho kernelu mov r7, $sys_exit @ cislo sycallu pro funkci "exit" mov r0, #0 @ exit code = 0 svc 0 @ volani Linuxoveho kernelu
Zpětný překlad objektového kódu vypadá následovně:
a.out: file format elf32-littlearm architecture: armv4, flags 0x00000102: EXEC_P, D_PAGED start address 0x00008074 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000048 00008074 00008074 00000074 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .bss 00000028 000100c0 000100c0 000000c0 2**3 ALLOC 2 .ARM.attributes 00000014 00000000 00000000 000000bc 2**0 CONTENTS, READONLY SYMBOL TABLE: no symbols Disassembly of section .text: 00008074 <.text>: 8074: e59f103c ldr r1, [pc, #60] ; 0x80b8 8078: e3a02029 mov r2, #41 ; 0x29 807c: e3a0302a mov r3, #42 ; 0x2a 8080: e2422001 sub r2, r2, #1 8084: e3520000 cmp r2, #0 8088: 0a000002 beq 0x8098 808c: e5c13000 strb r3, [r1] 8090: e2811001 add r1, r1, #1 8094: eafffff9 b 0x8080 8098: e3a07004 mov r7, #4 809c: e3a00001 mov r0, #1 80a0: e59f1010 ldr r1, [pc, #16] ; 0x80b8 80a4: e3a02028 mov r2, #40 ; 0x28 80a8: ef000000 svc 0x00000000 80ac: e3a07001 mov r7, #1 80b0: e3a00000 mov r0, #0 80b4: ef000000 svc 0x00000000 80b8: 000100c0 andeq r0, r1, r0, asr #1
11. Čtvrtý praktický příklad – optimalizace předchozího příkladu instrukcí subs
Čtvrtý příklad vznikl zjednodušením příkladu třetího. Opět se to týká programové smyčky a využití instrukce subs namísto instrukce sub. Původní verze smyčky vypadala následovně:
loop: sub r2, r2, #1 @ zmenseni pocitadla cmp r2, #0 @ otestovani, zda jsme jiz nedosahli nuly beq konec @ pokud jsme se dostali k nule, konec smycky strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu b loop @ nepodmineny skok na zacatek smycky
Dvojici instrukcí sub+cmp lze nahradit instrukcí subs nastavující příznaky:
loop: subs r2, r2, #1 @ zmenseni pocitadla a nastaveni priznaku beq konec @ pokud jsme se dostali k nule, konec smycky strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu b loop @ nepodmineny skok na zacatek smycky konec:
Opět se podívejme na úplný zdrojový kód příkladu:
# asmsyntax=as # Testovaci program naprogramovany v assembleru GNU as # - pocitana programova smycka s testem na zacatku # - uprava pro mikroprocesory s architekturou ARM # # Autor: Pavel Tisnovsky # Linux kernel system call table sys_exit = 1 sys_write = 4 # 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 r1, =buffer @ zapis se bude provadet do tohoto bufferu mov r2, $rep_count+1 @ pocet opakovani znaku mov r3, #'*' @ zapisovany znak loop: subs r2, r2, #1 @ zmenseni pocitadla a nastaveni priznaku beq konec @ pokud jsme se dostali k nule, konec smycky strb r3, [r1] @ zapis znaku do bufferu add r1, r1, #1 @ uprava ukazatele do bufferu b loop @ nepodmineny skok na zacatek smycky konec: mov r7, $sys_write @ cislo syscallu pro funkci "write" mov r0, $std_output @ standardni vystup ldr r1, =buffer @ adresa retezce, ktery se ma vytisknout mov r2, $rep_count @ pocet znaku, ktere se maji vytisknout svc 0 @ volani Linuxoveho kernelu mov r7, $sys_exit @ cislo sycallu pro funkci "exit" mov r0, #0 @ exit code = 0 svc 0 @ volani Linuxoveho kernelu
Zpětný překlad objektového kódu vypadá následovně:
a.out: file format elf32-littlearm architecture: armv4, flags 0x00000102: EXEC_P, D_PAGED start address 0x00008074 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000044 00008074 00008074 00000074 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .bss 00000028 000100b8 000100b8 000000b8 2**3 ALLOC 2 .ARM.attributes 00000014 00000000 00000000 000000b8 2**0 CONTENTS, READONLY SYMBOL TABLE: no symbols Disassembly of section .text: 00008074 <.text>: 8074: e59f1038 ldr r1, [pc, #56] ; 0x80b4 8078: e3a02029 mov r2, #41 ; 0x29 807c: e3a0302a mov r3, #42 ; 0x2a 8080: e2522001 subs r2, r2, #1 8084: 0a000002 beq 0x8094 8088: e5c13000 strb r3, [r1] 808c: e2811001 add r1, r1, #1 8090: eafffffa b 0x8080 8094: e3a07004 mov r7, #4 8098: e3a00001 mov r0, #1 809c: e59f1010 ldr r1, [pc, #16] ; 0x80b4 80a0: e3a02028 mov r2, #40 ; 0x28 80a4: ef000000 svc 0x00000000 80a8: e3a07001 mov r7, #1 80ac: e3a00000 mov r0, #0 80b0: ef000000 svc 0x00000000 80b4: 000100b8 strheq r0, [r1], -r8
12. Repositář s demonstračními příklady
Všechny čtyři dnes popisované demonstrační příklady byly společně s podpůrnými skripty určenými pro překlad či naopak pro disassembling uloženy do GIT repositáře dostupného na adrese https://github.com/tisnik/presentations/. Všechny dnešní příklady jsou určené pro GNU Assembler. Následuje tabulka s odkazy na zdrojové kódy příkladů i na již zmíněné podpůrné skripty:
První demonstrační příklad
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | loop1-arm-v1.s | program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/loop1-arm-v1.s |
2 | assemble_arm-v1 | skript pro překlad na procesorech ARM | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/assemble_arm-v1 |
3 | disassemble-arm | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/disassemble-arm |
Druhý demonstrační příklad
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | loop1-arm-v2.s | program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/loop1-arm-v2.s |
2 | assemble_arm-v2 | skript pro překlad na procesorech ARM | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/assemble_arm-v2 |
3 | disassemble-arm | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/disassemble-arm |
Třetí demonstrační příklad
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | loop2-arm-v1.s | program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/loop2-arm-v1.s |
2 | assemble_arm-v1 | skript pro překlad na procesorech ARM | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/assemble_arm-v1 |
3 | disassemble-arm | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/disassemble-arm |
Čtvrtý demonstrační příklad
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | loop2-arm-v2.s | program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/loop2-arm-v2.s |
2 | assemble_arm-v2 | skript pro překlad na procesorech ARM | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/assemble_arm-v2 |
3 | disassemble-arm | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/disassemble-arm |
13. Optimalizace kódu (počtu instrukcí) při výpočtu největšího společného dělitele
Pojďme si nyní ukázat, jak je možné použít podmínkové kódy v praxi. Následující demonstrační příklad byl získán přímo z materiálů dodávaných k mikroprocesorům ARM, takže je jisté, že byl vybrán s ohledem na to, aby dobře ilustroval použití podmínkových kódů a markantní rozdíl v délce programu i délce jeho trvání (v praxi jsou tyto rozdíly poněkud menší). Příklad představuje céčkovskou funkci určenou pro výpočet největšího společného dělitele dvou 32bitových hodnot. Algoritmus pro výpočet největšího společného dělitele je možné v céčku zapsat následovně:
int gcd(int a, int b) { while (a != b) do { if (a > b) { a = a - b; } else { b = b - a; } } return a; }
Pokud by tento algoritmus byl přeložen do assembleru s využitím klasicky pojatých podmíněných skoků, mohl by výsledek vypadat následovně: instrukce CMP porovná dva pracovní registry obsahující obě hodnoty, pro něž se počítá největší společný dělitel. Na základě tohoto porovnání se vykoná buď „větev if“ nebo „větev else“, což ovšem znamená, že se v každé iteraci musí vykonat dvě instrukce podmíněných skoků (BEQ, BLT) a jedna instrukce skoku nepodmíněného (B):
gcd CMP r0, r1 ; porovnání registrů r0 a r1 BEQ end ; pokud r0 == r1, konec smyčky BLT less ; skok když r0 je menší než r1 SUB r0, r0, r1 ; tělo "if" B gcd ; další iterace less SUB r1, r1, r0 ; tělo "else" B gcd ; další iterace end
Naproti tomu optimalizující překladač může výše uvedený program přeložit pouze do čtyř instrukcí, přičemž obě prostřední instrukce jsou vykonány pouze při splnění zadané podmínky (viz suffix uvedený u jména instrukce):
gcd CMP r0, r1 ; porovnání registrů r0 a r1 SUBGT r0, r0, r1 ; rozdíl jen v případě, že r0 byl větší než r1 SUBLT r1, r1, r0 ; rozdíl jen v případě, že r1 byl větší než r0 BNE gcd ; pokud r0 != r1, skok na začátek smyčky
Vzhledem k tomu, že všechny instrukce mají konstantní šířku 32 bitů, odpovídá zkrácení programu v assembleru (počet ušetřených řádků) přímo úměrně i zkrácení výsledného strojového programu a současně i jeho rychlejšímu běhu, protože provedení podmíněného skoku vede k pozastavení instrukční pipeline na tři takty:
Program | Instrukcí | Bajtů |
---|---|---|
A | 7 | 28 |
B | 4 | 16 |
14. Odkazy na Internetu
- ASM Flags
http://www.cavestory.org/guides/csasm/guide/asm_flags.html - Status Register
https://en.wikipedia.org/wiki/Status_register - Intel x86 JUMP quick reference
http://unixwiz.net/techtips/x86-jumps.html - Linux assemblers: A comparison of GAS and NASM
http://www.ibm.com/developerworks/library/l-gas-nasm/index.html - Programovani v assembleru na OS Linux
http://www.cs.vsb.cz/grygarek/asm/asmlinux.html - Is it worthwhile to learn x86 assembly language today?
https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1 - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Assembler pod Linuxem
http://phoenix.inf.upol.cz/linux/prog/asm.html - AT&T Syntax versus Intel Syntax
https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html - Linux Assembly website
http://asm.sourceforge.net/ - Using Assembly Language in Linux
http://asm.sourceforge.net/articles/linasm.html - vasm
http://sun.hasenbraten.de/vasm/ - vasm – dokumentace
http://sun.hasenbraten.de/vasm/release/vasm.html - The Yasm Modular Assembler Project
http://yasm.tortall.net/ - 680x0:AsmOne
http://www.amigacoding.com/index.php/680x0:AsmOne - ASM-One Macro Assembler
http://en.wikipedia.org/wiki/ASM-One_Macro_Assembler - ASM-One pages
http://www.theflamearrows.info/documents/asmone.html - Základní informace o ASM-One
http://www.theflamearrows.info/documents/asminfo.html - Linux Syscall Reference
http://syscalls.kernelgrok.com/ - Programming from the Ground Up Book - Summary
http://savannah.nongnu.org/projects/pgubook/ - IBM System 360/370 Compiler and Historical Documentation
http://www.edelweb.fr/Simula/ - IBM 700/7000 series
http://en.wikipedia.org/wiki/IBM_700/7000_series - IBM System/360
http://en.wikipedia.org/wiki/IBM_System/360 - IBM System/370
http://en.wikipedia.org/wiki/IBM_System/370 - Mainframe family tree and chronology
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_FT1.html - 704 Data Processing System
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP704.html - 705 Data Processing System
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP705.html - The IBM 704
http://www.columbia.edu/acis/history/704.html - IBM Mainframe album
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_album.html - Osmibitové muzeum
http://osmi.tarbik.com/ - Tesla PMI-80
http://osmi.tarbik.com/cssr/pmi80.html - PMI-80
http://en.wikipedia.org/wiki/PMI-80 - PMI-80
http://www.old-computers.com/museum/computer.asp?st=1&c=1016 - The 6502 overflow flag explained mathematically
http://www.righto.com/2012/12/the-6502-overflow-flag-explained.html
25. 7. 2016 at 19:45
Skvělé, opravdu skvělé! Děkuji 🙂
29. 7. 2016 at 11:17
Zajimave je, ze podle TIOBE je assembler znovu na desatem miste popularity jazyku:
http://www.tiobe.com/tiobe-index/
A to by clovek rekl, ze klesne nekam pres prvni padesatku, asi to opravdu zpusobuje IOT
5. 8. 2016 at 01:40
Boží! Ono to ještě existuje? Myslím jako „žije“. Můj nejmilovanější jazyk .. 🙂