Učení assemblerového jazyka
Assemblerový jazyk je lidsky čitelný ekvivalent nejnižší softwarové úrovně počítačového programování – strojového kódu.
Počítač sice chápe všechny programy jako čísla, kde různá čísla dávají počítači pokyny k různým operacím, ale to je pro člověka (natož pro autora) příliš nudné. Proto lidé programují pomocí assembleru, který má téměř 1:1 shodu se strojovým kódem.
Učte se programovací jazyk C. Pokud znáte jazyk C (nebo C++), bude pro vás assembler jednodušší.
C# a Java jsou dobré jazyky, ale existuje jen málo nástrojů, které by vám ukázaly, jak vypadá kód C# a Javy v assembleru. (Ve skutečnosti mají Java a C# svůj vlastní jazyk assembleru v bajtovém kódu, který je poněkud odlišný a vzdálený skutečným hardwarovým architekturám instrukčních sad procesorů.“
Z kódu jazyka C (a C++) můžeme použít překladače, které nám ukáží, jak vypadá jazyk assembleru. Jazyk C je nízkoúrovňový jazyk, a pokud víte, jak se jednotlivé konstrukce v jazyce C překládají do jazyka assembleru, budete znát i jazyk assembleru! Příklady konstrukcí jazyka C, které chceme znát pro jazyk assembleru, jsou následující:
konstrukce jazyka C | ekvivalent jazyka assembleru |
---|---|
lokální proměnná | registr CPU nebo paměťové místo zásobníku |
globální proměnná | přístup ke globální nebo const paměťové umístění |
odkaz na pole | vzor sčítání a přístupu do paměti |
goto | podmíněná instrukce větvení |
příkaz if | vzor v if-goto: podmíněný test & instrukce větvení |
while cyklus | vzor v if-goto |
přiřazení | modifikace registru nebo paměti |
aritmetické | aritmetické instrukce |
parametry předávání | modifikace registru nebo zásobníkové paměti |
příjem parametru | přístup k registru nebo zásobníkové paměti |
funkce volání | volání instrukce |
vstup do funkce | vzor prologu |
výstup z funkce | vzor epilogu |
vrácená hodnota | úprava registru a epilog |
Jak se assembler liší od ostatních programovacích jazyků
V assembleru, se setkáte s holými prostředky základního hardwaru procesoru. (Dále věci, které můžeme v jazyce C napsat na jeden řádek, zaberou v assembleru několik řádků.)
-
Budete znát věci jako registry procesoru a zásobník volání – tyto prostředky je třeba spravovat, aby program fungoval (a v jiných jazycích jsou tyto prostředky spravovány automaticky za nás.) Znalost těchto prostředků je kritická pro volání a vracení funkcí. (Registry procesoru jsou ve srovnání s pamětí skrovné, jsou však superrychlé a také konzistentní ve svém výkonu)
-
V assembleru neexistují strukturované příkazy – všechny konstrukce toku řízení se tedy musí programovat ve stylu „If-Goto“ (v jiných jazycích překladač překládá strukturované příkazy na If-Goto). Styl if-goto používá štítky jako cíle. (I když jazyk C podporuje použití goto a štítků, programátoři většinou dávají přednost strukturovaným příkazům, takže se s goto a štítky v jazyce C často nesetkáme)
Jazyk assembleru vs. strojový kód
Jazyk assembleru je určen pro lidi, kteří jej mohou číst i psát.
Štítky
V jazyce assembleru používáme spoustu štítků. Použití štítků se ve strojovém kódu převádí na čísla a štítky ve strojovém kódu mizí. Díky štítkům (oproti přímému použití čísel) je možné provádět malé změny programů – bez nich by i malá změna byla vlastně obrovskou změnou.
Procesor potřebuje vědět, kde se nachází operand, možná datový operand nebo třeba místo kódu. Například u míst kódu bude procesor obvykle chtít vědět, jak daleko jsou tyto položky od místa kódu, které právě vykonává – tomu se říká pc-relativní adresování. Chceme-li tedy provést příkaz if-then, můžeme procesoru říci: za určitých podmínek přeskoč dopředu N instrukcí, kde N představuje velikost nebo počet instrukcí, které mají být přeskočeny (nebo ne), tedy N je velikost části then – to by bylo ve strojovém kódu. Pokud bychom do then-part přidali instrukci, počet instrukcí k přeskočení se zvýší (a pokud instrukci z then-part odstraníme, sníží se).
Použití značek automaticky zohledňuje změny (např. přidání, odebrání instrukcí) – instrukce větvení, která jako cíl používá štítek, akceptuje změny v počtu instrukcí, které mají být podmíněně přeskočeny.
Pseudoinstrukce
Většina instrukcí v assembleru se překládá 1:1 do instrukcí strojového kódu. Občas však musíme assembleru sdělit něco, co se přímo nepřekládá do strojového kódu. Typickým příkladem je sdělení assembleru, že následující instrukce jsou kód (obvykle prostřednictvím direktivy .text
) vs. data (obvykle prostřednictvím direktivy .data
). Stejně jako běžné instrukce jsou tyto instrukce obvykle zapsány na vlastním řádku, nicméně spíše informují assembler, než aby přímo generovaly instrukce strojového kódu.
Psaní jazyka Assembly
V jazyce C píšeme příkazy; tyto příkazy běží postupně za sebou, standardně sekvenčně, dokud nějaká konstrukce toku řízení (smyčka, if) tok nezmění. V assembleru platí totéž.
V jazyce C mají příkazy vliv na proměnné – můžeme uvažovat, že jednotlivé příkazy spolu komunikují prostřednictvím proměnných. Totéž platí v jazyce assembler: instrukce spolu komunikují prostřednictvím vlivu, který mají na proměnné, ačkoli s větším počtem instrukcí (nutně, protože instrukce jsou jednodušší) bude také nutně více proměnných, mnohé z nich jsou velmi krátkodobé dočasné, které slouží ke komunikaci od jedné instrukce k další.
Basic Assembly Langauge Programming
Střídáme výběr registrů a výběr instrukcí. Protože instrukce jazyka assembler jsou poměrně jednoduché, často potřebujeme použít několik instrukcí, ano, propojených přes hodnoty jako proměnné v registrech cpu. Když se tak stane, budeme potřebovat vybrat registr pro uložení mezivýsledků.
Výběr registru
Pro výběr registru potřebujeme mentální model toho, které registry jsou obsazené a které jsou volné. S touto znalostí můžeme vybrat volný registr, který použijeme pro uložení dočasného (krátkodobého) výsledku. Tento registr zůstane obsazený až do našeho posledního použití, pak se vrátí do pozice volného – jakmile je volný, může být znovu použit k něčemu úplně jinému. (V jistém smyslu procesor vlastně neví, že toto neustálé přerozdělování provádíme, protože nezná záměr programu.)
Výběr instrukce
Jakmile jsme stanovili prostředky, které se mají použít, můžeme vybrat konkrétní instrukci, kterou potřebujeme. Často nenajdeme instrukci, která by dělala přesně to, co chceme, takže budeme muset udělat práci se dvěma nebo více instrukcemi, což znamená, ano, další výběr registrů pro dočasnost.
Například MIPS a RISC V jsou podobné architektury, které poskytují instrukci compare & branch, ale mohou porovnávat pouze dva registry cpu. Pokud tedy chceme zjistit, zda je bajt v řetězci určitým znakem (například novým řádkem), musíme tuto hodnotu nového řádku (číselnou konstantu) zajistit (načíst) v registru cpu dříve, než můžeme použít instrukci pro větvení compare &.
Volání funkcí & Vracení
Program se skládá možná z tisíců funkcí! Tyto funkce sice musí sdílet paměť, ale paměť je obrovská, a tak spolu mohou vycházet. U jednoho procesoru je však omezený a pevně daný počet registrů (například 32 nebo 16) a všechny funkce musí tyto registry sdílet!!!
Aby to fungovalo, definují návrháři softwaru konvence – které si lze představit jako pravidla – pro sdílení těchto omezených pevných zdrojů. Dále jsou tyto konvence nezbytné k tomu, aby jedna funkce (volající) mohla volat jinou funkci (volající). Tato pravidla se označují jako konvence volání, které pro volajícího/obsluhovaného určují, kam umístit/najít argumenty a návratové hodnoty. Konvence volání je součástí aplikačního binárního rozhraní, které obecně říká, jak má funkce ve strojovém kódu komunikovat s okolním světem (jinými funkcemi). Volací konvence je definována pro softwarové použití (hardware nezajímá).
Tyto dokumenty nám říkají, co máme dělat při předávání parametrů, používání registrů, alokaci/dealokaci zásobníku a vracení hodnot. Tyto dokumenty zveřejňuje poskytovatel instrukční sady nebo poskytovatel překladače a jsou dobře známé, aby kód z různých míst mohl správně spolupracovat. Zveřejněné konvence volání je třeba brát jako dané, i když o nich hardware neví.
Bez takové dokumentace bychom nevěděli, jak tyto věci dělat (předávání parametrů atd..). Nemusíme používat standardní konvence volání, ale je nepraktické vymýšlet vlastní pro cokoli jiného než pro jednoduchý program na hraní spuštěný na simulátoru. Rolování našich vlastních volacích konvencí znamená žádnou interoperabilitu s jiným kódem, kterou potřebujeme v každém realistickém programu, jako je provádění systémových a knihovních volání.
Hardware nezná softwarové konvence použití a ani se o ně nezajímá. Prostě jen slepě vykonává instrukci za instrukcí (opravdu rychle). Například pro hardware je většina registrů rovnocenná, i když jsou registry rozděleny nebo mají přiřazeno konkrétní použití podle softwarových konvencí.
Vyhrazené registry
Některé registry procesoru jsou vyhrazeny pro konkrétní účely. Například většina dnešních procesorů má nějakou formu ukazatele zásobníku. U MIPS a RISC V je ukazatel zásobníku obyčejný registr neodlišený od ostatních, kromě definice softwarových konvencí – výběr toho kterého registru je libovolný, ale je skutečně zvolen a tento výběr je zveřejněn, takže všichni víme, o který registr se jedná. Na x86 je naopak ukazatel zásobníku podporován vyhrazenými instrukcemi push
& pop
(stejně jako instrukce call a return), takže pro registr ukazatele zásobníku existuje pouze jedna logická volba.
Rozdělení registrů
Aby bylo možné rozdělit ostatní registry mezi tisíce funkcí, jsou podle softwarové konvence rozděleny obvykle do čtyř sad:
- Registry pro předávání parametrů
- Registry pro příjem návratových hodnot
- Registry, které jsou „volatilní“
- Registry, které jsou „nevolatilní“
Parametrové & Návratové registry
Parametrové registry se používají pro předávání parametrů! Díky dodržování volací konvence volající ví, kde má hledat své argumenty, a volající ví, kam má umístit hodnoty argumentů. Registry parametrů se používají pro předávání parametrů podle pravidel stanovených ve volací konvenci, a pokud je předáno více parametrů, než dovoluje konvence pro procesor, zbytek parametrů se předá pomocí paměti zásobníku.
Registry návratových hodnot uchovávají návratové hodnoty funkcí, takže když funkce skončí, je jejím úkolem před vrácením toku řízení volajícímu umístit její návratovou hodnotu do příslušného registru. Díky dodržení konvence volání volající i volající vědí, kde najdou návratovou hodnotu funkce.
Volatile & Non-Volatile Registers
Volatile registry může používat libovolná funkce bez ohledu na předchozí použití nebo hodnoty v tomto registru. Jsou vhodné pro dočasné registry potřebné k propojení sekvence instrukcí a lze je také použít pro lokální proměnné. Protože však tyto registry mohou být volně použity jakoukoli funkcí, nemůžeme počítat s tím, že si udrží své hodnoty přes volání funkce – podle (softwarové konvence) definice! Po návratu funkce tedy její volající nemůže říci, jaké hodnoty by se v těchto registrech nacházely – každá funkce je může volně používat, ovšem s vědomím, že se nemůžeme spolehnout na to, že jejich hodnoty zůstanou zachovány i po návratu volané funkce. Tyto registry se tedy používají pro proměnné a dočasné proměnné, které nepotřebují překlenout volání funkce.
Pro proměnné, které potřebují překlenout volání funkce, existují nevolatilní registry (a také zásobníková paměť). Tyto registry jsou zachovány napříč voláním funkce (softwarem, který dodržuje konvenci volání). Pokud si tedy funkce přeje mít proměnnou v rychlém registru procesoru, ale tato proměnná je živá napříč voláním (nastavená před voláním a použitá po volání), můžeme zvolit nevolatilní registr s požadavkem (definovaným konvencí volání), aby se předchozí hodnota registru obnovila po návratu k volajícímu. To vyžaduje uložení aktuální hodnoty nevolatilního registru (do paměti zásobníku) před jeho opětovným použitím a její obnovení (pomocí předchozí hodnoty z paměti zásobníku) po návratu.