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

14. Odkazy na Internetu

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

  1. ASM Flags
    http://www.cavestory.org/guides/csasm/guide/asm_flags.html
  2. Status Register
    https://en.wikipedia.org/wiki/Status_register
  3. Intel x86 JUMP quick reference
    http://unixwiz.net/techtips/x86-jumps.html
  4. Linux assemblers: A comparison of GAS and NASM
    http://www.ibm.com/developerworks/library/l-gas-nasm/index.html
  5. Programovani v assembleru na OS Linux
    http://www.cs.vsb.cz/grygarek/asm/asmlinux.html
  6. Is it worthwhile to learn x86 assembly language today?
    https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1
  7. Why Learn Assembly Language?
    http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language
  8. Is Assembly still relevant?
    http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant
  9. Why Learning Assembly Language Is Still a Good Idea
    http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html
  10. Assembly language today
    http://beust.com/weblog/2004/06/23/assembly-language-today/
  11. Assembler: Význam assembleru dnes
    http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz
  12. Assembler pod Linuxem
    http://phoenix.inf.upol.cz/linux/prog/asm.html
  13. AT&T Syntax versus Intel Syntax
    https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html
  14. Linux Assembly website
    http://asm.sourceforge.net/
  15. Using Assembly Language in Linux
    http://asm.sourceforge.net/articles/linasm.html
  16. vasm
    http://sun.hasenbraten.de/vasm/
  17. vasm – dokumentace
    http://sun.hasenbraten.de/vasm/release/vasm.html
  18. The Yasm Modular Assembler Project
    http://yasm.tortall.net/
  19. 680×0:AsmOne
    http://www.amigacoding.com/index.php/680×0:AsmOne
  20. ASM-One Macro Assembler
    http://en.wikipedia.org/wiki/ASM-One_Macro_Assembler
  21. ASM-One pages
    http://www.theflamearrows.info/documents/asmone.html
  22. Základní informace o ASM-One
    http://www.theflamearrows.info/documents/asminfo.html
  23. Linux Syscall Reference
    http://syscalls.kernelgrok.com/
  24. Programming from the Ground Up Book – Summary
    http://savannah.nongnu.org/projects/pgubook/
  25. IBM System 360/370 Compiler and Historical Documentation
    http://www.edelweb.fr/Simula/
  26. IBM 700/7000 series
    http://en.wikipedia.org/wiki/IBM_700/7000_series
  27. IBM System/360
    http://en.wikipedia.org/wiki/IBM_System/360
  28. IBM System/370
    http://en.wikipedia.org/wiki/IBM_System/370
  29. Mainframe family tree and chronology
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_FT1.html
  30. 704 Data Processing System
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP704.html
  31. 705 Data Processing System
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP705.html
  32. The IBM 704
    http://www.columbia.edu/acis/history/704.html
  33. IBM Mainframe album
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_album.html
  34. Osmibitové muzeum
    http://osmi.tarbik.com/
  35. Tesla PMI-80
    http://osmi.tarbik.com/cssr/pmi80.html
  36. PMI-80
    http://en.wikipedia.org/wiki/PMI-80
  37. PMI-80
    http://www.old-computers.com/museum/computer.asp?st=1&c=1016
  38. The 6502 overflow flag explained mathematically
    http://www.righto.com/2012/12/the-6502-overflow-flag-explained.html