Seriál s: Malý úvod do assembleru (2 b)

Zadáno v čísle 23.4.

Zadání

Assembler – jazyk procesoru. Pojďte se se mnou na chvíli podívat do hlubin počítače.

Už po přečtení nadpisu slyším: ,,Proč bychom se měli zabývat assemblerem?“ Mezi běžné argumenty lidí, kteří se s ním nesetkali, nebo setkali jen zběžně, patří: Assembler je zastaralý. Už nepotřebujeme assembler, máme vysokoúrovňové programovací jazyky. A na nízkoúrovňové programování stačí C nebo jazyk podobného typu. Psát v assembleru je časově neefektivní, náchylné k chybám, těžko čitelné a zbytečně složité. Zkusím tyto námitky jednu po druhé rozebrat.

  • Assembler je zastaralý. Toto tvrzení je nepřesné. Prvně, assemblerů je mnoho – dalo by se říci, že každá skupina procesorů s podobnou architekturou má svůj. Takže – některé assemblery nepochybně zastaralé jsou. Ale naproti tomu existují takové, které se používají stále. Příkladem budiž assembler pro mikrokontroléry AVR.

  • Máme vysokoúrovňové programovací jazyky, případně C jakožto nízkoúrovňový jazyk. To je nepochybně pravda, jenže assembler se využívá na jiné typy úloh. Oproti běžným programovacím jazykům má minimálně dvě výhody:

    1. I v dnešní době, kdy mnozí z vás nosí v kapse počítač s výkonem o několik řádům větším, než měly počítače ještě před desítkou let, můžeme narazit na potřebu optimalizovat spotřebu paměti nebo rychlost provádění kritické části kódu za hranice toho, co dokáže překladač vyššího programovacího jazyku. Třeba pro mikrokontroléry s malou pamětí, které mají své místo v ovladači kávovaru, čtečce platebních karet, krokoměru a podobně.

    2. Některé operace specifické pro konkrétní hardware, může být problém vyjádřit v univerzálních programovacích jazycích včetně C, které se snaží od hardwarové vrstvy abstrahovat. V takových případech se může kontrola nad každou jednotlivou instrukcí programu nebo jeho části hodit.

  • Neefektivní pro psaní kódu. Nepochybně. Po assembleru většinou sáhneme až ve chvíli, kdy nemáme jinou možnost. Nebo když se někdo nudí. (Potkali jste už někdo nějaký jazyk ze soutěže zvané codegolf? Ještě stále vám assembler připadá složitý?)

  • Náchylný k chybám. Ano, assembler nenabízí komplexnější řídící struktury, často neumožňuje mít vyhrazené oddělené proměnné, jak bychom podle jejich významu v kódu chtěli, a vůbec má svou sadu drobných zákeřností. Asi tak jako každý jiný jazyk.

  • Těžko čitelný. Ano.

  • Zbytečně složitý. Jak se to vezme. Assembler je ve své podstatě velmi jednoduchý. O to složitější je z něj něco smysluplného vytvořit.

Myslím, že i v dnešní době má smysl se na assembler podívat. A to ne jen jako na historickou kuriozitu. Je pravda, že kdo nechce, nejspíše se takto blízko železu nemusí ani přiblížit. Ale na druhou stranu může být i pro vysokoúrovňového programátora užitečné o strojovém kódu něco málo vědět, kdyby už jenom proto, aby se přesvědčil, že v hlubinách počítače nesedí velmi chytrý skřítek (zploštělý, aby se vám vlezl do kapsy).

Ukázali jsme si, proč má smysl se assemblerem zabývat, ale zaznívají jiné hlasy, konkrétně: ,,Ale já neumím programovat.“ Nevadí. V assembleru totiž běžné programovací techniky mají jen omezené využití. Sice budete v nevýhodě, ale aspoň se můžete cítit jako průkopníci, kteří nic než assembler neměli. Oni na začátku neměli ani ten assembler.

Assembler je poměrně široké téma. Kde tedy začít?

Kdo se podívá na internet, narazí na mnoho manuálů. Proč si tedy lámat hlavu zrovna s tím mým? Většina jich totiž řeší, jak psát v assembleru. Já zkusím trochu jiný úhel: jak pochopit assembler. Pro tyto účely budu často odbíhat, trochu zjednodušovat a co nejvíce vysvětlovat. Možná trochu polopaticky, ale budu počítat s tím, že někteří z vás neumí programovat, a z vlastní zkušenosti vím, že začátky s assemblerem nebývají jednoduché.

Inu začněme. Pojďme se společně zamyslet nad zcela fiktivním assemblerem, který nazvu PAPIRAS – papírový assembler. Vezměte si prosím papír a tužku, budeme si kreslit a počítat. PAPIRAS se bude podobat AVR-assembleru, který si však upravím pro své potřeby. Rád bych teď rozptýlil vaše obavy ze skřítka v počítači, ale to by se do tohoto miniseriálu nevešlo. Budu se tedy k procesoru chovat jako k černé skříňce – nebudu řešit, jak je zadrátovaný uvnitř. Nebudu řešit logické obvody. Mějme tedy abstraktní procesor.

to 0.4height 2.5ex depth 1explus 1fil#plus 1fil Program Counter (PC) A B C D E F G H

Obrázek 1: Vnitřní paměť procesoru tvořená devíti registry, každý o velikosti 8 bitů.

Co si pod tím pojmem představit? Je to stroj s pamětí, který vykonává instrukce, které mu dodáme. K instrukcím se dostaneme vzápětí, první je potřeba se seznámit s vnitřní pamětí procesoru. Ta reprezentuje okamžitý stav procesoru, a spolu se zrovna prováděnou instrukcí plně určuje, co konkrétně se bude v následujícím okamžiku dít. Buňky této paměti nazýváme registry, v našem případě jich bude devět (Obr. 1).1

Každý z našich registrů je buňka velikosti 8 bitů, tedy čísla v rozsahu 0–255. Desítková soustava je sice čitelnější pro lidi, ale nás budou zajímat konkrétní bity, takže se přesuneme do soustav pro procesor přirozenějších. Pokud tedy dále v textu uvidíte číslo ve tvaru 0xAB, tak se nelekněte – předpona 0x značí šestnáctkovou soustavu, předpona 0b soustavu dvojkovou. Registry A až H jsou registry pro běžné použití, registr PC je speciální – většina instrukcí k němu nemá přístup.

Kromě registrů, které reprezentují vnitřní stav procesoru, potřebujeme ještě vnější paměť, ze které budeme číst instrukce programu. Tu si můžeme představit jako očíslovanou nekonečnou pásku, viz Obrázek 1. Jedno každé políčko pásky má taktéž velikost 8 bitů.

to $\vcenter {\halign {\vrule height 3ex depth 1ex#& & \hbox to 6ex{\hss }}}$_#膔$$0123456789


1) Existuje alternativní architektura, založená na zásobníku – akumulátoru. Ta je však mimo rozsah našeho článku.

4.5 Návrh instrukční sady (2b)

Navrhněte 8bitové instrukce4, s jejichž pomocí budete schopni realizovat dále uvedené operace, a napište, jak bude tato realizace vypadat.5 Co budeme po naší sadě chtít?

  • Zachovat operace NOP a INC Rd.

  • Umět nastavit registry na libovolnou zadanou hodnotu z celého jejich rozsahu. Není nutné, aby to byla operace na jednu instrukci.

    Jednou z variant by mohlo být zavést instrukci SHL Rr, která posune bity registru Rr doleva o jeden bit.6 Potom by uložení hodnoty 255 do registru C vypadalo takto:
    ## C (v C je 0b#)INC1SHL10INC11SHL110…
    Po několika dalších opakování získáme v registru C hodnotu 0b11111111, jak jsme chtěli. Ale bude nás to stát 15 instrukcí, což není právě lichotivé, nehledě na to, že každé číslo budeme do paměti cpát jinak dlouho. Navíc má uvedený příklad jeden zásadní nedostatek – bude fungovat jen krátce po zapnutí procesoru dokud je v registru nezměněná počáteční nulová hodnota. Zkuste vymyslet lepší řešení.

  • Dále chceme umět operace:
    # #ADD Rd, Rr součet Rd a Rr zapiš do Rd, operace přetéká;SUB Rd, Rr $\code {Rd}- \code {Rr}\rightarrow \code {Rd}$, operace podtéká;NOT Rd bitovou negaci Rd zapiš zpět do Rd;AND Rd, Rr $\code {Rd}\codeop {AND} \code {Rr}\rightarrow \code {Rd}$, bitový logický součin registrů;OR Rd, Rr $\code {Rd}\codeop {OR} \code {Rr}\rightarrow \code {Rd}$, bitový logický součet registrů;SHL Rd bitový posun doleva;SHR Rd bitový posun doprava;STOP ukončí chod procesoru.

Analogicky k přetečení: $0 - 1 = 255$. Bitová negace: zapište si jednotlivé bity vedle sebe a znegujete každý jednotlivý bit. Z čísla 0b00000011 tak vznikne číslo 0b11111100. Bitové operace si dobře zapamatujte, budeme je v assembleru používat v budoucnu velmi často – schválně, k čemu by se nám mohly hodit? Bitový and/or: tentokrát si sepište pod sebe bity dvou registrů a na každou odpovídající dvojici aplikujte logický součin respektive součet. Tedy $\code {0b11001100} \codeop {AND} \code {0b10101010} = \code {0b10001000}$.

Fantazii se meze nekladou, odměnou za zajímavá a elegantní řešení mohou být další bonusové body. Pokud se vám zdá, že vám nějaká operace chybí, klidně si ji dodefinujte, budete-li mít dost bitů nazbyt. Těm z vás, kteří již programovat umí, budou nepochybně chybět řídící struktury – větvení a skoky. K těm se dostaneme příště.

Pro teď bych rád, abyste se zamysleli nad tím, jak co nejefektivněji využít 8 bitů ke kódování instrukcí. Za míru efektivity můžete vzít například počet kódů instrukcí, které si musíme zarezervovat pro danou operaci vůči počtu kroků, které bude provedení operace trvat.

Až sadu budete mít, předveďte její funkčnost spočítáním následujícího výrazu a uložením výsledku do registru D:

\[ 255 - 10 + ((5 - 3) \codeop {OR} 64) \rightarrow \code {D}\, . \]

K využití operací vám poskytnu následující příklad. Chceme spočítat výraz $1 + 2 - (16 \gg 3)$ a výsledek uložit do registru B. Symbol $\gg 3$ značí bitový posun doprava o tři bity. #; # ‘;’ bude v PAPIRASU značit komentář pro zbytek řádku.LDI B, 1 Načte hodnotu 1 do B. LDI C, 2 Načteme do registru C hodnotu 2. ADD B, C V PAPIRASU si příště zadefinujeme ADD, který sečte dva registry a výsledek uloží do registru B. Vy nejspíše nebudete mít dost bitů a budete muset najít nějakou alternativu. LDI C, 16 Přepíšeme registr C hodnotou 16, předchozí hodnotu už nepotřebujeme. SHR C, 3 Za předpokladu, že funkce SHR posunuje o zadaný počet bitů. Opět nejspíše narazíte na bitová omezení. Jedna z variant by byla SHR C, SHR C, SHR C, pokud posunujeme vždy jen o jedna. SUB B, C SUB B, C je instrukce B = B - C a jsme hotovi.STOP konec

Tímto končím první díl úvodu do assembleru. Pokud vám dnešní část přišla složitá, nezoufejte, příště to bude jednodušší – budeme se méně zabývat jednotlivými bity a více samotnými instrukcemi. Myslím si však, že dnešní část je důležitá k pochopení některých vlastností, které assemblery mají, a to i v dnešní době-100 64bitových mnohojádrových procesorů.

Na co se těšit příště: Větší registry, více paměti. Nové instrukce – zejména skoky a větvení. Počítání instrukcí, generování Fibonacciho čísel. A nejen to.

Tomáš Bartoněk


1) Hle, počátek číslování (indexování) od nuly.

2) Proč je zrovna prázdná instrukce instrukcí NOP? Protože nechceme, aby procesor, který nedostal instrukce, začal dělat něco nepředvídatelného.

3) V instrukčních sadách se v takovýchto případech používají souvislé bloky bitů – je to přehlednější a lépe se to ,,drátuje“.

4) Instrukce je číselná hodnota v rozsahu 0–255, která spolu s okamžitým obsahem registrů jednoznačně určuje, co procesor následně udělá (například jak upraví hodnoty svých registrů). Pak tuto instrukci ,,zapomene“ a načte novou podle adresy v registru PC.

5) K provedení jedné operace může být potřeba jedna i více 8bitových instrukcí. Tato ,,realizace“ operace se může lišit například v závislosti na tom, na kterých konkrétních registrech má celá operace proběhnout.

6) Bitový posun doleva: když si napíšete bity vedle sebe, zahodíte ten nejvíce vlevo (tedy ten, který odpovídá nejvyšší mocnině dvojky v binárním zápisu čísla) a doprava dopíšete nulu, získáte bitový posun doleva. Bitový posun doprava je analogicky obráceným směrem. (Výsledkem je hodnota vynásobená, respektive podělená dvěma.)