EuroAssembler Index Manual Download Source Macros


Sitemap Links Forum Tests Projects

EuroAssembler tutoriál


Úvod

English version of this tutorial

Předpokládané znalosti ↓

Jak počítač pracuje ↓

Procesorové módy a režimy ↓

Datové typy ↓

Strojové instrukce ↓

Instrukce pro kopii dat ↓

Aritmetické a logické instrukce ↓

Instrukce pracující se zásobníkem ↓

Skokové instrukce ↓

Posuvy a rotace ↓

Řetězcové instrukce ↓

První program ↓

BIOS 16 bitů ↓

DOS 16 bitů ↓

Linux 32 bitů ↓

Linux 64 bitů ↓

Windows 32 bitů ↓

Windows 64 bitů ↓

Zkusíme to s makry ↓

Definice a expanze maker ↓

Literál namísto symbolu ↓

Načtení informace od uživatele ↓

Z příkazového řádku ↓

Ze standardního vstupu ↓

Hledání chyb ↓

Hledání chyb kontrolními výpisy ↓

Hledání chyb makrem Debug ↓

Hledání chyb ladicí aplikací ↓


Možná máte zájem naučit se programovat v asembleru, ale nevíte, kde začít. Slyšeli jste, že se v tomto jazyce píší bootsektory, ovladače zařízení, kompilátory, operační systémy, a tak zkoušíte něco takového napsat a pak zahlcujete odborná fóra dotazy, kde přiznáváte, že jste v asembleru začátečník a něco vám nefunguje. Než se pustíte do podobných specializovaných úkolů, je třeba získat základní orientaci a praxi v používání nástrojů jako je asembler, disasembler, prohlížeč či analyzátor souborů apod. To se nejlépe provede jejich častým používáním, nejprve na jednoduchých příkladech typu "Hello world", kalkulačka, kreslení ASCII grafikou, později i na méně triviálních aplikacích. Nemusíte se bát, že je psaní v asembleru oproti jiným jazykům pomalejší; samotným psaním na klávesnici strávíme mnohonásobně méně času, než přemýšlením nad logikou programu, a to platí ve všech jazycích.

Předpokládejme, že jste někde zahlédli pár asemblerových instrukcí, které byste chtěli vyzkoušet, ale teď nastává hlavní problém práce s novým programovacím jazykem: kam tyto instrukce napsat a jak zařídit, aby je váš počítač provedl. Zde se dozvíte jak.

Předpokládané znalosti

Tento tutoriál je určen zájemcům o programování v asembleru pro osobní počítače architektury x64_32 (Intel, AMD) a budeme psát programy pro operační systémy MS Windows, Linux, DOS.

Předpokládejme, že máte aspoň základní znalosti angličtiny, umíte ovládat počítač, instalovat a spouštět programy z příkazového řádku, umíte editovat soubory prostým textovým editorem (nano, joe, Notepad apod.), znáte hexadecimální zápis čísel a umíte základní operace s hexadecimálními čísly, například umíte spočítat příklady typu

1234h 89ABCDh 4567h +5678h -234567h -89ABh --------- --------- -------- 68ACh 666666h ~FBBBCh

zpaměti, na papíře nebo aspoň s pomocí programátorské kalkulačky.

Jak počítač pracuje

Z pohledu programátora aplikací se počítač skládá z

  1. centrální procesorové jednotky (CPU) obsahující paměťové registry
  2. operační paměti (memory)
  3. zařízení (devices).
┌──────────┐ data ┌───────────┐ data ┌────────┐ │ │<══════════>│ CPU │<══════════>│ │ │ devices │ port │ │ address │ memory │ │ │<══════════>│ registers │<══════════>│ │ └──────────┘ └───────────┘ └────────┘

Nejmenší jednotkou informace je jeden bit, jehož fyzickou realizaci v CPU si můžeme představit jako klopný obvod se dvěma stabilními stavy, který může být překlopen tak, aby na výstupu poskytoval napětí, kterému jsme přiřadili hodnotu buď logická 1 nebo logická 0 (a nic mezi tím). Klopný obvod si pamatuje svůj stav a může ho na požádání změnit (zapsat nebo přečíst).
Spojení více klopných obvodů tak, aby se jejich bity daly zapisovat/číst najednou, se říká registr. Ten může být v osobních počítačích osmibitový, šestnáctibitový, třiadvacetibitový atd. až 512bitový. Zápis obsahu registru pomocí nul a jedniček by byl nepřehledný, místo toho se obvykle obsah registru zapisuje pomocí dvou hexadecimálních cifer na každý jeho bajt, tedy na 8 bitů.

Registry jsou umístěny na čipu CPU. Na rozdíl od pamětí jsou registry velmi rychlé, ale jejich počet a velikost jsou omezeny. Budeme pracovat hlavně s obecnými registry (General Purpose Registers, GPR). K registrům programátor přistupuje pomocí jejich jména (nikoli adresy), přičemž u většiny GPR lze kromě celého registru jmenovat také jejich podmnožiny. Dolní polovina 64bitového registru RAX se nazývá EAX, dolní čtvrtina AX, dolní osmina AL. Kompletní přehled registrů je v manuálu. Někde používáme označení rAX, rBX atd. Malé písmeno r zde vyjadřuje, že rAX může reprezentovat registry RAX, EAX nebo AX, v závislosti na aktuálním módu procesoru.

RAX ┌───────────────────────────────────────┐ │ │ EAX │ │ │ │ │ AX │ │ │ │ │ │ │ │ AH │ AL │ └────┴────┴────┴────┴────┴────┴────┴────┘

Obdobně jako RAX lze rozdělit i ostatní obecné registry RBX, RCX, RDX, RBP, RSI, RDI, RSP, R8..R15.

Obecné registry jsou v některých instrukcích specializovány nebo pevně přiřazeny, čemuž částečně odpovídá i mnemotechnika jejich názvu.

Ostatní registry, jako ST0..ST7 (matematický koprocesor), MM0..MM7 (multimediální), YMM0..YMM15 (SIMD registry) jsou ortogonální, tzn. nespecializované a vzájemně zaměnitelné.

Kromě registrů existují na čipu CPU klopné obvody zvané příznaky (flags), které se nastavují automaticky po určitých operacích, zejména aritmetických. Budou nás zajímat hlavně tyto:

Ostatní flagy, jako Parity, Auxilliary, Trap, Interrupt se v běžných aplikacích obvykle neužívají. Na příznaky můžeme nahlížet jako na samostatné klopné obvody s pamětí jednoho bitu. Pouze pro účely jejich zápisu na zásobník se sdružují do virtuálního registru, se kterým lze manipulovat instrukcemi PUSHF, POPF a obnovovat tak všechny flagy najednou.

K registrům lze započítat také ukazatel instrukcí rIP, který při vykonávání instrukce ukazuje za ni na adresu následující instrukce. Pouze u skokových instrukcí skoků se adresa v rIP nahrazuje cílovou adresou, na niž se má skákat.

Carry Flag je vyjímečný tím, že jej můžeme nastavovat na 1 instrukcí STC, nulovat instrukcí CLC nebo měnit jeho hodnotu na opačnou pomocí CMC. Podobně se ještě dají nastavovat a nulovat Direction Flag pomocí STD a CLD, a Interrupt Flag pomocí STI a CLI. Ostatní příznaky takto explicitně měnit nelze, ale například Zero Flag můžeme nastavit na 1 vynulováním některého registru pomocí SUB reg, reg.

CPU je spojen s pamětí pomocí datové a adresní sběrnice (soustava vodičů). Když CPU potřebuje něco načíst nebo zapsat, nastaví adresu na adresní sběrnici a zapsaná data přečte nebo zapíše na datovou sběrnici.

Obdobným způsobem funguje čtení a zápis do zařízení. K zařízením patří klávesnice, monitor, myš, síťová karta a další obdobné periférie. Na rozdíl od paměti se datovým kombinacím sloužícím k jejich výběru neříká adresa, ale port, např. klávesnice má pevně určený port 64h, tiskárna mívá port 378h apod. Přehled portů osobního počítače nalezneme např. v TechHelp.

Obecně se dá o fungování počítače z pohledu programátora v asembleru říci, že

procesor načte z paměti nebo ze zařízení nějakou informaci do registru, provede s ní nějakou manipulaci a pak ji někam zapíše.

Tou manipulací může být aritmetická nebo logická operace, změna bitů, nastavení na nějakou hodnotu apod. Kroky, které má procesor provést, jsou určeny pomocí strojových instrukcí. Ty mají proměnnou délku 1 až 15 bajtů a jsou uloženy v operační paměti, jedna za druhou. CPU je postupně z paměti načítá a provádí.

Každá instrukce má mnemotechnickou zkratku (určenou výrobcem procesoru), za kterou následují operandy, jež určují odkud a kam se má informace zapsat. Úlohou programu zvaného asembler je převést mnemotechnické zkratky a operandy na hexadecimální kód strojových instrukcí a uložit je do souboru tak, aby byly spustitelné pomocí operačního systému.

Typická instrukce má dva operandy – vstupní a výstupní – a v intelské syntaxi se zapisují v pořadí instrukce výstup, vstup. Například instrukce ADD EAX,ECX přikazuje procesoru, aby k obsahu registru EAX (výstupní) přičetl obsah vstupního registru ECX. Obsah registru ECX zůstává nezměněn. Obsah obecných registrů se přitom považuje za celé číslo s pevnou desetinnou čárkou.

Operační kód před provedením instrukce ADD EAX,ECX EAX ECX ┌──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ │01│C8│ │12│34│56│78│ │56│78│9A│BC│ └──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴──┘ AH AL CH CL po provedení instrukce ADD EAX,ECX EAX ECX ┌──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ │01│C8│ │68│AC│F1│34│ │56│78│9A│BC│ └──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴──┘ AH AL CH CL

Uvedený příklad pracuje s dvaatřicetibitovými registry EAX a ECX, ale stejný postup bychom uplatnili i pro sčítání registrů s šířkou 8, 16, 32 nebo 64 bitů, například ADD AH,CL, ADD AH,CH, ADD RAX,RCX. Zlomkové registry, které nemají jméno, jako např. horní polovina EAX, třetí osmina RCX ap. takto přímo sčítat nelze, ale mohli bychom pomocí instrukcí rotace dočasně posunout obsah požadovaného zlomkového registru do pojmenované části, provést sčítání pomocí ADD AL,CL a pak případně opačnou rotací vrátit sečtený registr zpět.

Vedle intelské syntaxe ještě existuje syntaxe vyvinutá ve firmě AT&T, ve které je přehozen vstup a výstup. Tou se zde ale nebudeme zabývat, neboť téměř všechny asemblery i výrobci procesorů používají syntaxi Intel.

Při sledování výukových materiálů si nelze nevšimnout mnoha nekonzistencí v popisování dat:

Obsah registru ECX ve výše uvedeném příkladu ADD EAX,ECX je zapsán hexadecimálně jako 56 78 9A BC a začíná tedy jeho nejvíce významným bajtem 56. To je zdánlivě v rozporu s ukládáním 32bitového slova do paměti počínaje nejméně významným bajtem, avšak slovo v registru si představujeme jinak než slovo v paměti. Pokud bychom registr ECX uložili do paměti třeba na adresu 0 (což by se provedlo instrukcí MOV [0],ECX) a pak zobrazili obsah paměti debugerem nebo podobným nástrojem, viděli bychom

00000000: BC 9A 78 56

Z toho je patrné, že záleží na tom, zda na obsah paměťového místa nahlížíme jako na vícebajtové slovo, nebo jako na sérii bajtů.

Procesorové módy a režimy

Osobní počítače běží v dnešní době prakticky výhradně v chráněném módu (protected mode). Operační systém v tomto módu chrání především sám sebe, aby uživatel nemohl z nepozornosti anebo záměrně narušit paměť systému nebo jiných uživatelů, kteří by mohli současně na počítači pracovat. Při pokusu o čtení nebo zápis do paměti, která nebyla uživateli přidělena, se přístup neprovede. Obdobně je omezen i přístup ke vstupně-výstupním portům. Instrukce pro přímý zápis a čtení portů IN a OUT jsou v chráněném režimu privilegovány a jejich používání si pro sebe vyhrazuje operační systém.

V počítačovém pravěku ještě osobní počítače (tehdy pouze 16bitové) běžely v reálném módu operačního systému DOS, kdy měl uživatel pro sebe celou operační paměť i všechny porty. Na dnešních PC je tento režim dostupný už jen pomocí emulátoru, jako je DOSBox, tedy oproti nativnímu prostředí Linuxu nebo Windows poněkud nepohodlně. Přesto se kupodivu šestnáctibitový režim preferuje v asemblerových kursech, snad z (mylného) přesvědčení, že 16bitový režim je pro začátečníky snazší než 32bitový nebo 64bitový.

Je li počítač přepnut do 16bitového reálného módu, ať už v rámci emulace, nebo nabootováním počítače do DOSu, budeme mít k dispozici pouze 16bitové registry AX, BX, CX, DX, BP, SP, SI, DI a při adresaci paměti musíme brát do úvahy segmentové registry CS, DS, SS, ES. Paměť můžeme oslovovat buď přímým zápisem adresy, např. MOV AX,[1234h], nebo adresu nejprve nahrát do registru a ten pak použít k adresaci:

MOV BX,1234h MOV AX,[BX]

V reálném módu můžeme k adresaci použít nanejvýš jeden bázový registr BX nebo BP a nanejvýš jeden indexový registr SI nebo DI, např. MOV AX,[BX+SI+1234h]. Při použití BP bude jako implicitní segmentový registr použit SS (není-li výslovně předepsáno jinak), jinak se použije defaultní datový registr DS.

K adrese vypočtené jako součet obsahu registrů BX, SI a přímé hodnoty 1234h se ještě přičítá obsah 16bitového segmentového registru vynásobený 16, než se tato lineární adresa použije k přístupu do paměti.

V chráněném 32bitovém módu je adresace paměti podstatně jednodušší. K dispozici máme 32bitové registry EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI a k adresaci paměti můžeme použít libovolnou kombinaci až dvou těchto registrů, například MOV EAX,[ECX+EDX+12346789h]. Je-li v adresaci použit ESP nebo EBP, segmentovým registrem se stává namísto defaultního DS registr SS. To je ale obvykle jedno, protože DS, SS i ES obsahují v chráněném módu tutéž adresu a segmentovými registry se nemusíme vůbec zabývat. Je tedy vidět, že 32bitový režim je pro programátora mnohem snazší než 16bitový.

Obdobná pravidla adresace platí i v 64bitovém módu, navíc zde můžeme kromě RAX, RBX, RCX, RDX, RBP, RSP, RSI, RDI využívat i další obecné registry R8..R15. Segmentové registry CS, DS, SS, ES se zde vůbec nepoužívají.

Datové typy

Datový typ určuje, jak máme na datovou položkou nahlížet - zda ji brát jako celočíselnou hodnotu, číslo s plovoucí desetinnou tečkou, textový řetězec, bitovou mapu či jinou strukturu. Jejich přehled je v manuálu. Na rozdíl od vyšších programovacích jazyků nejsou datové typy asemblerem nijak hlídány. Základní datový typ je určen šířkou datové položky (např. 8, 16, 32, 64 bitů), ale nic nám nebrání třeba 64bitové číslo s plovoucí desetinnou tečkou interpretovat jako binární číslo nebo jako řetězec znaků (a dobrat se samozřejmě chybného výsledku). Obsah položky (paměťového místa) můžeme zobrazovat jako text, převést na číslo ve formátu člověkem očekávaného, můžeme je přehrávat jako zvuk, zobrazovat jako fotografii, cokoli.

Datový typ je v asembleru definován operacemi, které budeme s položkou provádět.

Strojové instrukce

Co můžeme ve formátu zápisu strojových instrukcí mnemotechická_zkratka výstup, vstup používat jako výstup, vstup? Jsou čtyři možnosti.

  1. Jméno registru, do nebo ze kterého se má výsledek načíst, např. EBX
  2. Adresa paměti, odkud se má operand načíst nebo kam se má výstup uložit. Adresa bývá zapsaná v hranatých závorkách, např. [1234h], často ale místo číselného zápisu používáme jeho symbolické vyjádření. Paměťový operand může být v instrukci pouze jeden (buď vstup, nebo výstup, případně žádný).
  3. Přímá číselná hodnota, např. 1234h. Ta se dá použít pouze jako vstupní, nemůžeme do ní zapisovat.
  4. Některé instrukce mají svou vstupní hodnotu určenu už svým názvem (např. FLDPI, XLAT) nebo používají implicitní pevně určené registry (např. PUSHA, STOSB, CQO), takže svůj vstup ani výstup nespecifikují.

Instrukcím se říká strojové, protože jsou zabudovány do stroje (procesoru). V asembleru ještě můžeme vidět pseudoinstrukce, které vypadají podobně, ale jsou to direktivy pro asembler, nikoli pro procesor.

Nejnovější procesory architektury x86-64 mohou mít přes dva tisíce různých strojových instrukcí. Dobrá zpráva je, že k běžnému programování vystačíme jen s pár desítkami, které si teď popíšeme.

Autoritativní popis každé instrukce je nejlépe hledat na webu výrobce CPU, např. Intel. Jedná se ale většinou o rozsáhlé materiály ve formátu PDF, se kterými se špatně pracuje a odkazuje, takže se asi budete raději dívat na jejich podobu převedenou do webového formátu HTML.

Instrukce pro kopii dat

Asi nejuniverzálnější instrukcí je MOV, zkratka od MOVE, tedy přesuň, přestěhuj. Což je poněkud nešťastně zvolený název, lepší by bylo COPY, neboť se jedná o kopírování dat z jednoho místa na druhé, a ne o jejich přesouvání. Informace ve vstupním registru nebo paměťovém místu zůstává zachována. Ostatně v počítači ani není možnost informaci z registru nebo z paměti odstranit, vždy tam nějaká zůstane, přinejmenším samé nuly.

Kromě kopírování mezi 8,16,32,64bitovými registry a paměťovými místy je stejná mnemonika (MOV) využita i k přesouvání mezi obecnými (GPR) a dalšími registry, jako jsou segmentové, kontrolní, ladicí, MMX nebo SIMD.

Pomocí MOV z paměti do registru načítáme obsah paměťového místa (bajtu, slova, dvojslova) do registru, což se vyjadřuje uzavřením paměťového místa do hranatých závorek: MOV ECX,[1234h], MOV ECX,[EBX] apod. Při vynechání závorek bychom namísto obsahu uloženého v paměti načítali adresu paměťového místa. MOV ECX,1234h by tedy naplnilo registr ECX číslem 1234h. MOV ECX,EBX by pouze zkopírovalo obsah registru EBX do registru ECX. Neuvědomění si rozdílu mezi adresou (offsetem) paměťového místa a jeho obsahem je častým zdrojem chyb.

Další velmi často používanou instrukcí je LEA. Zatímco MOV registr, [memory] načítal obsah paměti a MOV registr, memory načítal adresu paměti, LEA registr, [memory] načítá adresu, přestože je druhý operand vždy v hranatých závorkách. Instrukce LEA je navíc o jeden bajt delší než MOV, proč ji tedy používat? Například tam. kde nás zajímá výpočet adresy a nechceme znát její obsah (který ani nemusí v paměti existovat). Ve 32bitovém a 64bitovém módu můžeme využít adresaci paměti se dvěma obecnými registry (jeden je bázový a druhý indexový), přičemž indexový registr může být navíc škálován, tedy interně vynásoben dvěma, čtyřmi nebo osmi. To dovoluje používat k jednodušším výpočtům adresní aritmetiku:
LEA EDX,[ESI+ECX] naplní EDX součtem obsahu registrů ESI a ECX.
LEA EAX,[8*EAX] naplní EAX osminásobkem původního obsahu EAX.
LEA EBX,[EDX+4*EDX] naplní EBX pětinásobkem obsahu EDX.

Další oblastí využití LEA je získávání adresy v 64bitovém režimu, kde na rozdíl od MOV používá relativní adresaci místo absolutní a umožňuje tak oslovovat paměť v rozsahu plusminus 2 GB od instrukce LEA.

K instrukcím kopírování dat ještě patří XCHG, tedy vzájemná výměna informací mezi dvěma registry nebo mezi registrem a paměťovým místem.

Je dobré si pamatovat, že MOV, LEA, XCHG nechávají příznaky (flags) beze změny.

Jedinou instrukcí MOV můžeme načíst z paměti do registru 1 až 8 bajtů, případně až 64 bajtů pomocí VMOV*. Procesor přístupy do paměti sdružuje, takže když načítáme nebo zapisujeme jen jeden bajt, např. pomocí MOV AL,[RBX], MOV [RBX],AL, ve skutečnosti CPU načte najednou třeba 16 sousedních bajtů, z nichž pak vybere ten jeden požadovaný a ostatní bajty nechá v původním stavu. Nejmenší granularita přístupu do paměti je jeden bajt, programátor se ale sdružováním v procesoru nemusí zabývat.

Co kdybychom potřebovali změnit jen část bajtu, třeba jen jeden bit? O to už se musí postarat programátor sám: načíst celý bajt, změnit v něm pouze požadovaný bit (a ostatní ponechat tak jak jsou) a pak ten bajt zapsat zpět do paměti. K manipulaci s jednotlivými bity můžeme použít specializované instrukce BTS (nastavení bitu na 1), BTR (nulování) nebo BTC (změna na opačný). Anebo můžeme použít bitově-logické (bitwise) operace OR k nastavování bitů, AND k nulování bitů nebo XOR k překlopení bitů do opačného stavu.

Aritmetické a logické instrukce

Aritmetické instrukce počítají s celými čísly v binárním vyjádření. Obsah osmibitového registru může být v rozmezí 00h až FFh, tedy dekadicky 0 až 255. Takové interpretaci říkáme čísla bez znaménka (unsigned). Kromě 8bitového registru můžeme celá čísla uložit také do 16, 32 nebo 64bitového registru. Pokud k osmibitovému registru obsahujícímu např. číslo 78h=120 přičteme jiný registr s obsahem 9Ah=154, třeba instrukcí ADD AL,BL, součet činí 112h=274. To je o 12h víc než je kapacita osmibitového registru (FFh), do registru AL se proto uloží hodnota jen 12h a procesor nastaví Carry Flag na 1 jako signalizaci přetečení. Tato jednička přetečená z AL tedy nepřechází do vyššího registru AH, jako by tomu bylo při 16bitovém sčítání ADD AX,BX. Můžeme ji ale započíst k dalšímu součtu, pokud bychom jej namísto ADD provedli instrukcí ADC, (Add with Carry). U ní se k součtu ještě navíc přičte jednotka, pokud byl Carry Flag nastaven: ADC AH,0. V praxi se to využívá pro sčítání a odčítání delších čísel, než odpovídá kapacitě GPR.

Následující příklad ilustruje sčítání dvou velkých čísel v 16bitových registrech, když 32bitové registry nebyly k dispozici, jako tomu bylo v DOSu. Mějme například sečíst v 16bitovém režimu dvě 32bitová čísla 89ABCDEFh a 55556666h. Čísla rozdělíme do dvojic registrů DX:AX a BX:CX. Dvojtečka mezi názvy registrů zde představuje spojení dvou 16bitových registrů do jednoho virtuálního 32bitového.

MOV DX,89ABhh MOV AX,0CDEFh MOV BX,5555h MOV CX,6666h CF DX AX BX CX ┌─┐ ┌──┬──┐ ┌──┬──┐ ┌──┬──┐ ┌──┬──┐ │?│ │89│AB│ │CD│EF│ │55│55│ │66│66│ └─┘ └──┴──┘ └──┴──┘ └──┴──┘ └──┴──┘ ADD AX,CX CF DX AX BX CX ┌─┐ ┌──┬──┐ ┌──┬──┐ ┌──┬──┐ ┌──┬──┐ │1│ │89│AB│ │34│55│ │55│55│ │66│66│ └─┘ └──┴──┘ └──┴──┘ └──┴──┘ └──┴──┘ ADC DX,BX CF DX AX BX CX ┌─┐ ┌──┬──┐ ┌──┬──┐ ┌──┬──┐ ┌──┬──┐ │0│ │DF│01│ │34│55│ │55│55│ │66│66│ └─┘ └──┴──┘ └──┴──┘ └──┴──┘ └──┴──┘

Výsledek ve dvojici registrů DX:AX je tedy DF013455h.

K dalším aritmetickým operacím vedle ADD a ADC patří ještě SUB a SBB (Subtrack with Borrow). SBB se liší od prostého odčítání (SUB) tím, že je-li nastaven Carry Flag, odečte navíc ještě jedničku.

Podobnou instrukcí jako SUB je CMP, která ale nic neodčítá (obsah registrů se nemění), pouze nastavuje příznaky podle výsledku hypotetického odčítání.

Logické instrukce OR, AND a XOR provádějí stejnojmenné logické operace s operandy šířky 8, 16, 32, 64 bitů systémem každý s každým, tedy nultý bit výstupního operandu s nultým bitem vstupního, první bit s prvním bitem, druhý s druhým atd.

Celá čísla v osmibitovém registru 0..255 jsme považovali za bezznaménková. To ale není jediná možná interpretace; můžeme vyhradit nejvýznamnější bit pro znaménko a považovat tak číslo za znaménkové (signed). Pak budou hodnoty 01h..7Fh odpovídat kladným číslům 1..127 a hodnoty FFh..80h záporným -1..-128, Nula zůstává nulou. Číselný rozsah se tedy u osmibitového registru změnil na -128..+127, u širších registrů bude samozřejmě ještě mnohem větší. Krása binární aritmetiky spočívá v tom, že znaménková i bezznaménková čísla sčítají a odčítají stejně, pomocí stejných instrukcí ADD a SUB. Těmto instrukcím nezáleží na tom, zda jsme jim předložili znaménková či bezznaménková čísla, výsledek aritmetické operace můžeme interpretovat tak i onak.

Pokud operujeme se znaménkovými čísly, přetečení (vybočení z povoleného číselného rozsahu) se namísto Carry Flag signalizuje pomocí Overflow Flag.

Instrukce NEG převádí pozitivní binární číslo na jeho negativní hodnotu a naopak. To se provede tak, že změní všechny bity na opačné a k takto změněnému výsledku přičte jedničku. V osmibitovém registru instrukce NEG AL změní hodnotu AL z 02h na FEh, z 01h na FFh, z 00h na 00h, z FFh na 01h atd. Obdobná instrukce je NOT, ta se liší od NEG tím, že k invertovaným bitům žádnou jedničku nepřičítá, hodí se tedy spíše k logickým operacím.

Užitečné aritmetické operace jsou INC a DEC, které zvyšují a snižují obsah registru nebo paměťového místa o jednotku. U těchto dvou instrukcí se musíme zapamatovat, že mění aritmetické příznaky s výjmkou CF.

Carry Flag zůstává provedením INC nebo DEC nezměněn.

K aritmetickým instrukcím ještě patří násobení a dělení. U nich ale neplatí, že by s kladnými i zápornými čísly počítaly stejně. Pokud chceme násobit nebo dělit binární čísla se znaménkem, musíme je buď převést na kladná (pomocí NEG) a vypočtenou hodnotu pak případně opět převést na zápornou, anebo namísto MUL a DIV použít jejich znaménkové varianty IMUL a IDIV. U násobení a dělení neplatí, že bychom mohli použít kterékoli registry. Výsledek násobení dvou 64bitových čísel může pro uložení výsledku vyžadovat až 128 bitů, proto se pro uložení součinu používá dvojice registrů rDX:rAX, která je pevně dána. U 32bitového násobení se výsledek ukládá do dvojice EDX:EAX, u 16bitového do DX:AX, pouze u osmibitového násobení je výjimka a výsledek násobení AL vstupní osmibitovou hodnotou jde do registru AX (DX zůstává nezměněn). K přetečení nemůže už z principu dojít, avšak souběžné nastavení Carry Flag a Overflow Flag na jednotky signalizuje, že výsledek je velký a přetekl do horní z dvojice výstupních registrů (do DX, EDX nebo RDX).

Při celočíselném dělení se použije opačný postup: dělenec se umístí do dvojice registrů rDX:rAX, dělitelem může být jiný registr nebo paměťové místo odpovídající šířky. Zde však k přetečení dojít může, pokud bude dělitel menší než číslo v horní polovině vstupního registrového páru (DX, EDX,RDX), výsledek by se nevešel do dolní poloviny (AX, EAX, RAX) a nebyl by tedy definován (tomu se říká dělení nulou). Architektura x86 neví, jaké číslo by v takovém případě měla uložit do výstupního registru a vyvolá tedy programovou výjimku (přerušení), což může způsobit havarijní ukončení našeho programu. Před dělením je tedy třeba horní polovinu vstupní dvojice registru vynulovat (v případě DIV) nebo naopak nastavit na samé jedničky při dělení záporných čísel pomocí IDIV. K tomu nejlépe poslouží u bezznaménkového dělení nulování rDX pomocí SUB rDX,rDX a při znaménkovém dělení použití krátké instrukce CWD, CDQ nebo CQO.

Instrukce pracující se zásobníkem

Zásobník je souvislá oblast rezervovaná z celkového množství operační paměti a prohlášená za zásobník. Pro adresaci v zásobníku se používá obecný registr rSP (stack pointer). Zásobník nejčastěji slouží k dočasnému uložení a následné obnově obsahu obecných registrů instrukcemi PUSH a POP. Při zavedení programu do paměti se operační systém postará, aby rezervoval dostatečné množství paměti pro zásobník a nastaví jeho ukazatel ESP nebo RSP na jeho počátek, což ale není nejnižší adresa, nýbrž naopak nejvyšší. Při ukládání na zásobník pomocí PUSH adresy postupně klesají a naopak při vybírání ze zásobníku pomocí POP adresy stoupají.

Předmětem PUSH mohou být obecné registry v šířce 16, 32 nebo 64 bitů, nebo paměťové proměnné o stejné šířce, dále segmentové registry a také přímá číselná hodnota, kterou procesor znaménkově rozšíří na šířku operandu. Procesor nejprve sníží ukazatel zásobníku rSP o 2, 4 nebo 8 bajtů a do takto vzniklého místa uloží operand. Registr rSP tedy adresuje právě uloženou položku.

Operace POP funguje obráceně: přesune obsah 2, 4 nebo 8 bajtů adresovaných registrem rSP do operandu a pak zvýší rSP o 2, 4 nebo 8 bajtů.

Používání registru rSP k adresaci zásobníku je implicitní; v instrukcích PUSH a POP se uvádí pouze vstupní nebo výstupní operand. Některé asemblery dovolují psát více než jeden operand do jedné instrukce, což je ale interně implementováno jako řada samostatných instrukcí PUSH nebo POP. Zápis více operandů slouží hlavně k úspoře řádků zdrojového programu.

Ukládání a obnova ze zásobníku probíhá z principu metodou LIFO, tedy Last In, First Out. Registr uložený na zásobník jako poslední pomocí PUSH se pak následnou instrukcí POP obnoví jako první; musíme je tedy obnovovat v opačném pořadí, než jsme je ukládali. Příklad:

PUSH EAX,EBX,ECX ; Ulož tři registry na zásobník. ; Tady jsou instrukce měnící EAX, EBX,ECX. Obsah ESP je o 3*4 bajty menší než byl před instrukcí PUSH. POP ECX,EBX,EAX ; Obnov registry ze zásobníku do původního stavu. ESP je zpět, paměť pod ním už má nedefinovaný obsah.

V 16 a 32bitovém režimu můžeme namísto ukládání a následné obnově většího počtu registrů použít instrukce PUSHA a POPA, které najednou ukládají a obnovují všech 8 GPR v pořadí eAX, eCX, eDX, eBX, eSP, eBP, eSI, eDI. Ukládat všech osm registrů je sice mnohdy zbytečné a pomalejší, ale zase šetříme velikost kódu, neboť PUSHA i POPA zabírají jen jeden bajt. V 64bitovém režimu PUSHA není k dispozici a musíme tedy registry ukládat individuálně.

Skokové instrukce

Pokud si z výuky programovacích jazyků pamatujete poučky o zákazu skoků a o škodlivosti příkazu GOTO, v asembleru na to můžete zapomenout. Všechny programové konstrukty jako IF, ELSE, WHILE, SWITCH, REPEAT UNTIL atd. se zde provádějí pomocí podmíněných skoků, kdy se nejprve vyhodnotí nějaká podmínka, např. instrukcemi CMP nebo TEST, a pak se provede (nebo neprovede) skok na jiné místo v kódu pomocí instrukce podmíněného skoku Jcc. Tento skok má řadu variant lišících se podmínkou cc v mnemotechnickém názvu instrukce. Například JA (Jump if Above) nejprve zkoumá, zda platí současně CF=0 a ZF=0 a skočí na cílovou adresu (návěstí) pouze pokud jsou obě tyto podmínky splněny, jinak instrukci ignoruje a pokračuje tou následující pod ní.

Pojmy Above, Below se používají, pokud jsme porovnávali čísla bez znaménka, např. dvě adresy pomocí CMP. Pojmy Greater, Less naopak po porovnávání znaménkových celých čísel.

Rozdílů mezi skoky short a near si nemusíme všímat, je starostí asembleru, aby použil ten správný.

K podmíněným skokům patří ještě instrukce LOOPcc, která nejprve sníží o 1 obsah registru rCX a pokud je rCX nenulový, provede skok na návěstí uvedené v operandu instrukce, jinak pokračuje pod instrukcí LOOP. Pokud by byl rCX nulový už před instrukcí LOOP, nejprve se sníží na hodnotu CX=65535 nebo dokonce ECX=4294967295, tedy nenulové, a smyčka se pak bude právě tolikrát opakovat. Což jsme asi nechtěli, proto se registr rCX před instrukcí LOOP testuje pomocí JCXZ nebo JECXZ a smyčka se v případě jeho nulovosti přeskočí:

JECXZ Skip Label: ; Instrukce prováděné ECX-krát. LOOP Label Skip:

Vedle podmíněných skoků můžeme také skákat na jiné místo programu nepodmíněně, tedy pokaždé když se v toku instrukcí objeví JMP. Tato instrukce nahradí registr rIP (který normálně obsahuje adresu příští instrukce) adresou, na kterou se skáče.

S nepodmíněnými skoky souvisí dvojice instrukcí CALL a RET. Podobně jako JMP, také instrukce CALL nahrazuje rIP adresou skoku, ale navíc ještě předtím uloží obsah rIP na zásobník, podobně jako bychom hypoteticky provedli neexistující operaci PUSH rIP. Instrukce RET provede hypotetickou operaci POP rIP, což je ekvivalentní skoku na návratovou adresu, která byla uložena na zásobníku instrukcí CALL.

Zásobníkové instrukce umožňují rozdělit tok aplikace na kratší podprogramy (procedury) a program tak přehledně strukturovat. Každý podprogram či programové makro můžeme považovat za černou skříňku, zdokumentovat její vstup, výstup a funkci, a pak na detaily její implementace zapomenout.

CALL BlackBox BlackBox: PROC ; Černá skříňka (na jiném místě programu). ; Pokračování v hlavním programu. PUSHAD ; Ulož registry ; Instrukce černé skříňky. POPAD ; Obnov původní obsah registrů. RET ; Návrat do hlavního programu pod CALL BlackBox. ENDPROC

Strojové instrukci CALL se podobá INT vyvolávající softwarové přerušení (interrupt). Jako operand se zadává číslo 0..255, které v reálném módu udává pořadové číslo dvojslova v tabulce přerušení (IDT) udávajícího adresu rutiny zpracovávající dané přerušení. Například INT 21h se podívá na adresu 21h*4 a dvě 16bitová slova z této adresy zavede do registrů IP a CS. Na této adrese by měl být podprogram provádějící funkci očekávanou od INT 21h; ten se pak ukončí instrukcí IRET. Rozdíl mezi instrukcemi CALL/RET a INT/IRET je v tom, že INT před uložením návratové adresy na zásobník tam navíc uloží i příznaky a IRET je pak obnoví zpět.

Posuvy a rotace

Následujících osm instrukcí posuvů a rotací umožňují manipulovat s obsahem 8, 16, 32, 64bitového registru nebo paměťového místa. Počet posuvů se zadává ve druhém oprandu jako bezprostřední číslo nebo jako obsah pevně určeného registru CL. O tento počet bitů se obsah posouvá či rotuje buď směrem vlevo, tedy od nejméně významného bitu LSb k nejvýznamnějšímu MSb, anebo naopak vpravo, tedy od MSb k LSb.

RCL RCR ┌──┐ ┌───────────┐ ┌───────────┐ ┌──┐ │CF│<-│MSb <-- LSb│<-CF CF->│MSb --> LSb│->│CF│ └──┘ └───────────┘ └───────────┘ └──┘ ROL ROR ┌──┐ ┌───────────┐ ┌───────────┐ ┌──┐ │CF│<-│MSb <-- LSb│<-MSb LSb->│MSb --> LSb│->│CF│ └──┘ └───────────┘ └───────────┘ └──┘ SAL SAR ┌──┐ ┌───────────┐ ┌───────────┐ ┌──┐ │CF│<-│MSb <-- LSb│<-0 MSb->│MSb --> LSb│->│CF│ └──┘ └───────────┘ └───────────┘ └──┘ SHL SHR ┌──┐ ┌───────────┐ ┌───────────┐ ┌──┐ │CF│<-│MSb <-- LSb│<-0 0->│MSb --> LSb│->│CF│ └──┘ └───────────┘ └───────────┘ └──┘

Při instrukcích rotace přes carry (RCL, RCR) se k bitům registru ještě přidává bit Carry Flag jakožto devátý (17., 33., 65.) bit.

Aritmetické instrukce SAL, SAR slouží k rychlému násobení a dělení znaménkových čísel pomocí mocnin dvou, např. SAL AX,4 je ekvivalentní násobení obsahu AX šestnácti (24), SAR EBX,3 vydělí obsah EBX osmi atd. Proto taky u aritmetického posuvu vpravo (SAR) nejvyšší znaménkový bit MSb kopíruje při každém kroku svou původní hodnotu.

Logické posuvy (SHL, SHR) se hodí k logickým operacím. Instrukce SHL a SAL se chovají identicky. Dva příklady:

STC MOV EAX,12345678h CF EAX ┌─┐ ┌──┬──┬──┬──┐ │1│ │12│34│56│78│ └─┘ └──┴──┴──┴──┘ RCL EAX,4 CF EAX ┌─┐ ┌──┬──┬──┬──┐ │1│ │23│45│67│88│ └─┘ └──┴──┴──┴──┘ MOV ECX,89ABCDEFh ECX CF ┌──┬──┬──┬──┐ ┌─┐ │89│AB│CD│EF│ │?│ └──┴──┴──┴──┘ └─┘ SHR ECX,2 ECX CF ┌──┬──┬──┬──┐ ┌─┐ │22│6A│F3│7B│ │1│ └──┴──┴──┴──┘ └─┘

Řetězcové instrukce

Jak víme, instrukce mohou přesouvat data mezi registry navzájem a mezi registry a pamětí, ale nikoli z jednoho místa paměti do druhého. To není tak zcela pravda, existuje instrukce MOVS, která to dělá za cenu obejití standardního způsobu zakódování adresy (ModRM+SIB). Místo toho očekává, že adresa vstupu bude uložena do registru rSI a adresa výstupu do rDI. Množství přenesených dat závisí na příponě instrukce, tedy MOVSB, MOVSW, MOVSD, MOVSQ pro přenesení jednoho bajtu, 16bitového slova (WORD), 32bitového slova (DWORD) nebo 64bitového slova (QWORD). Počet přenesených slov jednou instrukcí může být i větší, pokud použijeme opakovací prefix REP. Ten specifikuje, že počet opakování přenosu jednoho elementu se má opakovat tolikrát, jaký je obsah rCX, přičemž po každém přenosu se rCX zmenší o 1 a adresy v registrech rSI a rDI se změní o velikost přenášeného slova, tedy o 1, 2, 4 nebo 8 bajtů. To, zda se adresy rSI a rDI budou o velikost slova zvětšovat nebo snižovat, závisí na Direction Flag.

Dva příklady použití MOVS ve 32bitovém módu, kdy jako indexové registry rSI, rDI jsou použity ESI,EDI:

CLD MOV ESI,02h MOV ECX,3 MOV EDI,07h ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬───── │01│23│456789│AB│CD│EF│01│23│45│67│89│.. └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───── 00 01 02 03 04 05 06 07 08 09 0A 0B 0C .. ^ ^ DF=0 ESI=02h EDI=07h ECX=3 REP MOVSB ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬───── │01│23│45│67│89│AB│CD│456789│45│67│89│.. └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───── 00 01 02 03 04 05 06 07 08 09 0A 0B 0C .. ^ ^ DF=0 ESI=05h EDI=0Ah ECX=0 STD MOV ECX,2 ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬───── │01│23│45│6789ABCD│45│67│89│45│67│89│.. └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───── 00 01 02 03 04 05 06 07 08 09 0A 0B 0C .. ^ ^ DF=1 ESI=05h EDI=0Ah ECX=2 REP MOVSW ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬───── │01│23│45│67│89│AB│CD│45│6789ABCD│89│.. └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───── 00 01 02 03 04 05 06 07 08 09 0A 0B ... ^ ^ ESI EDI ECX=0

Instrukce REP MOVSB v předchozím příkladu nejprve přenesla ECX=3 bajty z adresy v ESI=02h na adresu EDI=07h. V pokračování příkladu jsme změnili směr přenosu pomocí STD doleva, změnili šířku elementu z jednoho bajtu (MOVSB) na jedno 16bitové slovo (MOVSW) a předepsali přenos pouze ECX=2 těchto elementů z adresy ESI=05h na adresu EDI=0Ah. Ke změně obsahu obou adresovacích registrů rSI, rDI tedy dochází až po přenosu jednoho elementu. Není-li před instrukcí MOVS zadán prefix REP, provede se přenos právě jednoho elementu (a oba registry se zvětší nebo zmenší o velikost elementu), jinak se zkoumá obsah rCX a dokud je nenulový, přenos elementu včetně změny rSI, rDI se opakuje a rCX se pak sníží o jednotku. Obsah rCX v REP MOVS tedy určuje počet přenášených elementů. Pokud bylo rCX=0, REP MOVS se neprovede ani jednou a obsah registrů rCX, rSI, rDI se nemění.

Zajímavější situace nastává, pokud se přenášená pole částečně překrývají. V následujícím příkladu se překrývá zdrojový řetězec (adresa ESI=02h, délka ECX=5) s cílovým (adresa EDI=07h, délka ECX=5). V každém z pěti kopírovacích kroků se nejprve načte a přenese jeden element (v našem případě bajt), a to i v případě, že tento vstupní element byl před chvilkou přenesen. Pokud je ESI<EDI a DF=0, anebo pokud je ESI>EDI a DF=1, řetězec se nekopíruje, ale pouze se jeho část mezi počátečními adresami obou registrů rozmnoží po celé délce výstupního řetězce. Aby došlo k posunutí zdrojového řetězce dopředu nebo dozadu, muselo by platit ESI<EDI a DF=1, anebo ESI>EDI a DF=0.

CLD MOV ESI,02h MOV ECX,5 MOV EDI,05h ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬───── │01│23│456789ABCD│EF│01│23│45│67│89│.. └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───── 00 01 02 03 04 05 06 07 08 09 0A 0B 0C .. ^ ^ DF=0 ESI=02h EDI=07h ECX=3 REP MOVSB ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬───── │01│23│45│67│89│4567894567│45│67│89│.. └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───── 00 01 02 03 04 05 06 07 08 09 0A 0B 0C .. ^ ^ DF=0 ESI=07h EDI=0Ah ECX=0 STD MOV ECX,3 ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬───── │01│23│45│678945678945│67│45│67│89│.. └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───── 00 01 02 03 04 05 06 07 08 09 0A 0B 0C .. ^ ^ DF=1 ESI=07h EDI=0Ah ECX=3 REP MOVSW ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬───── │01│23│45│67│89│45│678945678945│89│.. └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───── 00 01 02 03 04 05 06 07 08 09 0A 0B ... ^ ^ ESI=01h EDI=04h ECX=0

Další užitečnou řetězcovou instrukcí je STOS, která ukládá obsah registru AL, AX, EAX nebo RAX do paměti na adresu uloženou v registru rDI, a po uložení obsah rDI o velikost elementu zvětší nebo zmenší, což záleží na Direction Flag. Také před touto instrukcí se často používá prefix REP a umožňuje tak vynulovat velké úseky operační paměti.

CLD MOV EDI,MemBlock ; Adresa počátečního bajtu nulovaného bloku paměti o velikosti 64 KB. MOV ECX,64K / 4 ; Počet opakování = velikost bloku děleno velikostí DWORD. SUB EAX,EAX ; Vynuluj registr EAX (DWORD), který se bude ukládat do bloku. REP MOVSD ; Vynuluj celý MemBlock.

Opačným způsobem než STOS funguje LODS, která načítá paměť z adresy dané registrem ESI do AL, AX, EAX nebo RAX, a po načtení zvětší nebo zmenší rSI. U této instrukce nemá smysl používat prefix REP, neboť načítané hodnoty by přepisovaly obsah rAX ještě dříve, než bychom jej stačili nějak zpracovat. LODSB se používá se spolupráci se STOSB, pokud chceme kopírovat řetězec bajt po bajtu a zároveň reagovat na kopírované znaky. Příklad jednoduché konverze nulou zakončeného řetězce z malých písmen a..z na velká A..Z:

CLD MOV ESI,MixedCase ; Načti adresy obou řetězců. MOV EDI,UpperCase ; Délka je neznámá, ale je zakončen nulovým znakem. Next: LODSB ; Načti jeden znak vstupního řetězce z [ESI] do AL a zvětši ESI o 1. CMP AL,0 ; Testuj, zda už jsme na konci vstupního řetězce. JE End ; Pokud ano, skoč na návěstí End. CMP AL,'a' ; Kontrola, zda se jedná o znak v rozmezí 'a'..'z'. JB Store ; Pokud je menší než 'a', neměň ho a skoč na návěstí Store. CMP AL,'z' ; Kontrola, zda se jedná o znak v rozmezí 'a'..'z'. JA Store ; Pokud je větší než 'z', neměň ho a skoč na návěstí Store. AND AL,11011111b ; Nuluj 5. bit znaku v AL, což jej převede na velké písmeno 'A'..'Z'. Store:STOSB ; Ulož znak z AL na adresu [EDI] a zvětši EDI o 1. JMP Next ; Skoč zpět pro další znak vstupního řetězce. End: STOSB ; Hotovo, ukonči nulou z registru AL i výstupní řetězec.

Řetězcová instrukce SCAS slouží k vyhledání pozice hodnoty v registru AL, AX, EAX nebo RAX ve vstupním řetězci adresovaném pomocí rDI. Porovnání nastaví příznaky a pak zvětší nebo zmenší rDI o šířku porovnávaného registru (1, 2, 4 nebo 8) v závislosti na Direction Flag. Instrukce se používá s prefixem REPNE (opakovat, pokud se nerovná), počet opakování je přitom určen obsahem rCX. Provádění instrukce se ukončí buď nalezením znaku v řetězci (nastaví se Zero Flag jakožto příznak shody), anebo vyčerpáním délky řetězce v registru rCX. Pokud operace skončila kvůli vynulování rCX, můžeme ještě podle stavu příznaku ZF zjistit, zda byla hodnota nalezena v posledním elementu vstupního řetězce.

CLD MOV EDI,String ; Adresa prohledávaného řetězce. MOV ECX,SizeOfString ; Počet znaků v řetězci. MOV AL,0 ; Budeme hledat nulový bajt. REPNE SCASB ; EDI nyní ukazuje za nalezený nulový bajt, pokud byl nalezen. JNE Error ; Skoč, pokud řetězec neobsahuje nulový bajt, ale přitom ECX=0.

Poněkud menší smysl má skenování opakované s prefixem REPE (opakovat, dokud se rovná). To bychom využili, pokud registr EDI ukazuje například na řetězec samých nulových znaků a chceme zjistit pozici prvního nenulového:

CLD MOV EDI,String ; Adresa prohledávaného řetězce. MOV ECX,SizeOfString ; Počet znaků v řetězci. MOV AL,0 ; Budeme hledat nenulový bajt. REPE SCASB ; EDI nyní ukazuje za nalezený prvý nenulový bajt, pokud byl nalezen. JE Error ; Skoč, pokud řetězec obsahuje pouze nulové bajty.

Poslední užitečnou řetězcovou instrukcí je CMPS, která porovnává element v paměti (BYTE, WORD, DWORD nebo QWORD) adresovaný pomocí rSI s elementem stejné šířky adresovaným pomocí rDI. Nastaví aritmetické příznaky a pak zvětší nebo zmenší rSI a rDI o velikost elementu. Rovněž tuto instrukci můžeme opakovat pomocí prefixu REPE. Následující příklad ukazuje vyhledání nějakého slova nebo výrazu (Needle) v delším řetězci znaků (Haystack). Algoritmus nejprve vyhledá pomocí SCASB první znak Needle a pak pomocí CMPSB porovná následné znaky hledaného slova. Pokud tam nejsou, pokračuje v hledání prvního znaku a celý postup opakuje.

CLD MOV ESI,Needle ; Adresa hledaného výrazu. MOV EDI,Haystack ; Adresa bloku, v němž budeme hledat Needle. MOV ECX,HaystackSize ; Velikost tohoto bloku. LODSB ; Načti první znak Needle. Next:REPNE SCASB ; Vyhledej první znak v Haystack počínaje EDI. JNE NotFound ; Needle není obsaženo v Haystack, skončili jsme. PUSH ECX,ESI,EDI ; Ulož rozpracovaný stav na zásobník. MOV ECX,NeedleSize-1 ; ESI ukazuje na druhý znak Needle. REPE CMPSB ; Porovnej s Needle počínaje druhým znakem. POP EDI,ESI,ECX ; Obnov stav ze zásobníku, zatím ignoruj příznaky. JE Found ; Pokud je ZF=1, tak REPE CMPSB nalezlo shodu, máme hotovo. JMP Next ; Jinak pokračuj v hledání prvního znaku, který je stále v AL.

Linux i Windows vyžadují, aby Direction Flag byl před voláním jeho funkcí vynulován, a zároveň zaručuje, že při návratu je rovněž DF=0. Pokud bychom potřebovali používat řetězcovou instrukci s nastaveným Direction Flag, je třeba jej opět vynulovat pomocí CLD, nejlépe hned po jejím provedení. Pak ani nemusíme tento příznak nulovat na začátku uvedených příkladů.

Pamatujme, že řetězcové instrukce inkrementují registry až po provedení, takže např. REPNE SCASB se po nalezení bajtu zastaví o jeden element později a nalezený bajt je tedy na adrese EDI-1.

Instrukce SCAS a CMPS nastavují příznaky, ostatní řetězcové instrukce je ponechávají beze změny.

První program

Po seznámení se základními strojovými instrukcemi se konečně dostáváme k jejich vyzkoušení. Instrukce je třeba zapsat do zdrojového souboru jako prostý text v osmibitovém kódování (nikoli UTF-16) a bez interních příznaků uvozujících tučné či skloněné písmo, nadpisy apod. Soubor pak předložíme programu zvanému asembler, který jej převede do jiného souboru ve spustitelném tvaru. Tento tutorial doporučuje použít EuroAssembler, který je výhodný tím, že nemusíme specifikovat žádné parametry na příkazovém řádku, pouze název zdrojového souboru. Výstupem může být přímo spustitelný soubor pro DOS, Linux nebo Windows, není tedy potřeba linker. Nebudeme využívat ani žádné knihovny třetích stran, pouze API (Application Programming Interface) operačního systému.

Aby bylo programování zábavné, měl by každý program něco zajímavého vykonávat, přinejmenším něco vypsat na monitor. Obvykle se začíná vypsáním fráze "Hello, world!". Monitor, klávesnice, myš patří k zařízením (devices), která však jsou ve správě operačního systému a ten nám nedovolí použít instrukce IN a OUT k zápisu do zařízení. Možná by to šlo v reálném DOSu, ale beztak bychom se těžko potýkali s porty rozmanitých videoadaptérů, jaké se v době DOS používaly. K vypsání řetězce znaků na monitor budeme muset použít služby operačního systému.

Ukážeme si, jak vypsat "Hello, world!" v 16bitovém systému DOS, pak v 32bitovém Linuxu, 64bitovém Linuxu, 32bitovém MS Windows a 64bitovém MS Windows.

DOS 16 bitů

Služby DOS se vyvolávají strojovou instrukci INT 21h a jsou popsány např. v seznamu DOS Fn Index nebo INT 21h. Po rozkliknutí seznamu funkcí INT 21h vidíme dva řádky zabývající se standardním výstupem, tedy zápisem na konzolu: Int 21/AH=02h - DOS 1+ - WRITE CHARACTER TO STANDARD OUTPUT a Int 21/AH=09h - DOS 1+ - WRITE STRING TO STANDARD OUTPUT. Chceme vypisovat celý řetězec, ne jen jednotlivý znak (character), takže se podíváme na tu druhou funkci, která má dva vstupní parametry:

AH = 09h DS:DX -> '$'-terminated string

Vyvolání této funkce tedy vyžaduje následující instrukce:

MOV AH,09h MOV DX,HelloWorldAddress INT 21h

Zbývá vyřešit maličkost: jak do dvojice DS:DX zapsat adresu řetězce "Hello, world!", prozatímně symbolizovanou slovem HelloWorldAddress?

V DOSu budeme preferovat programy ve spustitelném formátu COM. Ten se celý odehrává v rámci jednoho bloku paměti o velikosti 64 KB, což pro naše drobné úlohy bohatě stačí a má velikou výhodu, že všechny čtyři segmentové registry jsou již DOSem nastaveny na potřebný obsah, takže se jimi nemusíme zabývat. DOS při spouštění našeho programu jej zavede do paměti a před jeho začátek uloží datovou strukturu o velikosti 256 bajtů zvanou PSP. První instrukce našeho programu následuje hned za ní, tedy na adrese 256. Program nemůže začínat na adrese 256 definicí řetězce "Hello, world!", protože by procesor zkoušel znaky řetězce vykonávat jako programový kód a nejspíš by ohlásil chybu nebo zatuhnul. Definici řetězce tedy odsuneme až na konec programu, asembleru je to jedno. Po instrukci INT 21h bychom měli náš program regulerně ukončit, jinak by sice vypsal pozdrav, ale pak by se marně snažil vykonávat bajty řetězce jako strojové instrukce a nejspíš přitom zamrzl. Prohlédnutím seznamu funkcí DOS 21h najdeme hned dvě obsahující text "TERMINATE": Int 21/AH=00h - DOS 1+ - TERMINATE PROGRAM a Int 21/AH=4Ch - DOS 2+ - EXIT - TERMINATE WITH RETURN CODE. V programu formátu COM ale existuje ještě další a mnohem jednodušší způsob regulerního ukončení: prostou instrukcí RET. Ta načte z zásobníku nulové slovo a tím způsobí, že se program vrací na adresu CS:0, kde je začátek PSP a tam je uložena strojová instrukce INT 0x20 pro ukončení programu v DOS. Náš program tedy bude vypadat takto:

MOV AH,09h MOV DX,HelloWorldAddress INT 21h RET HelloWorldAddress DB "Hello, world!"

Textový řetězec byl v programu definován pomocí pseudoinstrukce DB (definuj bajty) a zapsán v uvozovkách. Assembler použije adresu následující za instrukcí RET, na ni uloží 13 bajtů řetězce a této adrese přiřadí symbolický název HelloWorldAddress.
Máme tedy zdrojový kód, zapíšeme ho do souboru nazvaného např. hello.asm a uložíme. Zkusíme jej přeložit do spustitelného tvaru tím, že zadáme v konzoli povel euroasm hello.asm (uvozovky kolem jména souboru se mohou vynechat, pokud neobsahuje mezery):

... I0180 Assembling source file "hello.asm". I0270 Assembling source "hello". I0310 Assembling source pass 1. I0310 Assembling source pass 2. I0330 Assembling source pass 3 - final. I0760 16bit TINY BIN file "hello.bin" created from source, size=29. I0750 Source "hello" (4 lines) assembled in 3 passes with errorlevel 0. I0860 Listing file "hello.asm.lst" created, size=881. I0980 Memory allocation 448 KB. 21 statements assembled in 1 s. I0990 EuroAssembler terminated with errorlevel 0.

Z výstupních zpráv generovaných EuroAssemblerem v průběhu překladu si povšimněme řádku I0760 16bit TINY BIN file "hello.bin" created from source, size=29. Program formátu COM by měl mít příponu .com. To je tím, že jsme asembleru nesdělili, že má překládat do formátu COM, takže použil defaultní příponu .bin, která mimo jiné nevytváří PSP, takže náš program by beztak nefungoval.

Jak říci EuroAssembleru, že má vygenerovat COM? K tomu slouží dvojice pseudoinstrukcí PROGRAM a ENDPROGRAM a operandy FORMAT=, WIDTH=, MODEL=, SUBSYSTEM= (a mnoho dalších). Musíme tedy náš program zabalit mezi PROGRAM a ENDPROGRAM. Jako povinné návěstí povelu PROGRAM uvedeme název programu (bez přípony). Nazvěme jej třeba HelloDos. Stejné jméno se uvádí také u ENDPROGRAM, ovšem nikoli v poli návěstí, ale jako operand, jak je obvyklé u blokových pseudoinstrukcí EuroAssembleru. Jméno programu nemusí nutně odpovídat názvu zdrojového souboru, jako je tomu u jiných asemblerů. Mohli bychom totiž v jednom souboru definovat více než jeden blok PROGRAM/ENDPROGRAM a tím vyrábět více různých spustitelných programů najednou. Ale to zatím nebudeme zkoušet.

Kromě názvu programu musíme v parametrech pseudoinstrukce PROGRAM specifikovat ještě formát a šířku výsledného souboru. EuroAssembler si ale šířku u formátu COM odvodí sám jako WIDTH=16, takže bychom tento operand mohli vynechat. Obdobně bychom mohli vynechat i operand ENTRY=256, neboť vstupní bod programu je u COM programů vždy pevně určen na této adrese.

HelloDos PROGRAM FORMAT=COM, WIDTH=16, ENTRY=256 MOV AH,09h MOV DX,HelloWorldAddress INT 21h RET HelloWorldAddress DB "Hello, world!" ENDPROGRAM HelloDos

Po jeho spuštění pomocí euroasm hello.asm dostáváme:

... I0180 Assembling source file "hello.asm". I0270 Assembling source "hello". I0310 Assembling source pass 1. I0330 Assembling source pass 2 - final. I0470 Assembling program "HelloDos". "hello.asm"{1} I0510 Assembling program pass 1. "hello.asm"{1} I0510 Assembling program pass 2. "hello.asm"{1} I0530 Assembling program pass 3 - final. "hello.asm"{1} I0660 16bit TINY COM file "HelloDos.com" created, size=29. "hello.asm"{7} I0650 Program "HelloDos" assembled in 3 passes with errorlevel 0. "hello.asm"{7} I0750 Source "hello" (6 lines) assembled in 2 passes with errorlevel 0. I0860 Listing file "hello.asm.lst" created, size=1039. I0980 Memory allocation 448 KB. 32 statements assembled in 1 s. I0990 EuroAssembler terminated with errorlevel 0.

Důležitá je poslední řádka, která informuje o errorlevel 0, tedy bez chyb. Jinak bychom je nejprve museli ve zdrojovém textu najít a odstranit. Podobně řádka I0660 16bit TINY COM file "HelloDos.com" created, size=29. potvrzuje, že byl vygenerován program ve formátu COM a jakou má velikost. Po téměř každé zprávě EuroAssembler ještě připojuje pozici ve zdrojovém souboru, které se zpráva týká. Například "hello.asm"{7} přiřazuje zprávu k sedmému řádku zdrojového souboru, což je ENDPROGRAM HelloDos.

Ještě si všimněme zprávy I0860 Listing file "hello.asm.lst" created, size=1039.. EuroAssembler bez ptaní vytváří soubor s listingem přeloženého souboru. Je to opět prostý textový soubor, který si proto můžeme prohlédnout například Notepadem nebo podobným prohlížečem:

| |HelloDos PROGRAM FORMAT=COM, WIDTH=16, ENTRY=256 |[CODE] ::::Section changed. |0000:B409 | MOV AH,09h |0002:BA[0000] | MOV DX,HelloWorldAddress |0005:CD21 | INT 21h |0007:C3 | RET |[DATA] ::::Section changed. |0000:48656C6C6F2C20776F72~|HelloWorldAddress DB "Hello, world!" | | ENDPROGRAM HelloDos | **** ListMap "HelloDos.com",model=TINY,groups=1,segments=3,entry=256,stack=[COMGROUP]:0000FFFEh | [COMGROUP],FA=00000000h,RVA=00000000h,size=0000011Dh=285,group [PSP] [CODE] [DATA] | [PSP],FA=00000000h,RVA=00000000h,size=00000100h=256,width=0,align=0,purpose=PHDR | [CODE],FA=00000000h,RVA=00000100h,size=00000008h=8,width=16,align=0010h,purpose=CODE | [DATA],FA=00000010h,RVA=00000110h,size=0000000Dh=13,width=16,align=0010h,purpose=DATA | **** ListGlobals "HelloDos.com",Global=0,Public=0,Extern=0,eXport=0,Import=0

Vidíme, že listing obsahuje kopii zdrojového kódu odsunutou vpravo a do levé části se vložil sloupec ohraničený znaky | obsahující čtyřmístnou hexadecimální adresu ukončenou dvojtečkou, a po ní strojový kód instrukce. Na konci programu ještě přibyla mapa ukazující, jak byly jeho sekce slinkovány do výsledného souboru (**** ListMap) a seznam globálních symbolů (**** ListGlobals), v našem případě prázdný.

Formát listingu produkovaného EuroAssemblerem odpovídá platnému zdrojovému kódu, neboť levý sloupec ohraničený znaky | se jako strojová poznámka ignoruje. Pokud bychom tedy překlad znovu spustili povelem euroasm hello.asm.lst, opět bychom získali soubor HelloDos.com.

Překlad programu skončil s errorlevel 0. Můžeme tedy otevřít emulátor DOSu (DosBox), přejít do adresáře, v němž máme HelloDos.com a zkusit ho spustit zapsáním HelloDos nebo HelloDos.com. Nejspíš v okně DOSu uvidíte text Hello, wordl! následovaný změtí nesmyslných znaků. To je způsobeno přehlédnutím detailu v popisu služby DS:DX -> '$'-terminated string - vypisovaný řetězec musí být zakončen znakem dolaru. Zároveň s dolarem můžeme na konec řetězce připsat ještě dvojici Carriage Return a Line Feed, tedy 13 a 10, které způsobí odřádkování:

HelloWorldAddress DB "Hello, world!",13,10,'$'

Po opravě řádku a novém přeložení by již mělo všechno fungovat podle očekávání.

BIOS 16 bitů

Po zapnutí počítače CPU startuje v reálném módu a provádí program pevně "zadrátovaný" v paměti na základní desce. V té době ještě není z disku zaveden DOS ani jiný operační systém, ale funguje rozhraní BIOS, případně UEFI, které umí několik základních funkcí počítače: vypisovat na monitor znaky a řetězce, číst klávesnici, načíst z disku program pro zavedení OS. Tyto funkce se dají vyvolat instrukcemi INT, za zmínku stojí zejména INT 10h pro práci s videoadaptérem a tedy i monitorem. Úplný přehled funkcí vyvolávaných přerušením INT je na Interrupt Jump Table.

V předchozím příkladu bylo k vypsání řetězce použito rozhraní DOSu. Pokud bychom potřebovali zapisovat na obrazovku hned po zapnutí počítače ještě dříve, než je DOS nebo jiný operační systém zaveden, museli bychom použít rozhraní BIOS napevno uložené ve firmware základní desky. Takto se chová například program boot sektoru, který zavede 512 bajtů dlouhý obsah jednoho diskového sektoru do paměti a předá mu řízení. Zkusíme vypsat Hello, world! pomocí služby BIOS. Ty podobně jako služby DOSu zachovávají obsah všech registrů kromě těch, které vracejí výsledek. My použijeme službu TELETYPE OUTPUT, která očekává vypisovaný znak v AL, dále AH=0Eh jako identifikátor služby a BH=0 jako číslo interní stránky videoadaptéru.

EuroAssembler sice dovoluje generovat boot sektor přímo, volbou PROGRAM FORMAT=BOOT, avšak zavádění boot sektoru je komplikované a nepohodlné. Z praktických důvodů budeme opět generovat osvědčený formát COM:

HelloBio PROGRAM FORMAT=COM, WIDTH=16, ENTRY=Start: Start: MOV AX,CS ; Naplň segmentový registr DS stejnou hodnotou, jakou má CS. MOV DS,AX ; Nelze k tomu použít MOV DS,CS, neboť taková instrukce není podporována. MOV SI,HelloWorldAddress MOV CX,HelloWorldSize CLD ; Pro jistotu vynulujeme Direction Flag. SUB BX,BX ; Použijeme základní (nultou) stránku videoadaptéru. MOV AH,0Eh ; Číslo funkce BIOS INT 10h. Next: LODSB ; Načtení jednoho znaku řetězce do AL, zvětšení SI. INT 10h ; Vyvolání funkce BIOS pro vypsání znaku. LOOP Next ; Skok pro další znak CX-krát. JMP $ ; Program ukončíme nekonečným skákáním na sebe. Bez OS to jinak nejde. HelloWorldAddress DB "Hello, world!",10 HelloWorldSize EQU $ - HelloWorldAddress ENDPROGRAM HelloBio

Po spuštění programu HelloBio.com v emulátoru DosBOX opět dostáváme očekávaný výstup Hello, world!. Vzhledem k zacyklení programu na konci ale pak budeme muset DosBOX havarijně ukončit.

Linux 32 bitů

Spustitelný formát pro Linux se v EuroAssembleru nazývá ELFX, generované programy v tomto formátu dostávají souborovou příponu .x, které se ale můžeme zbavit přejmenováním pomocí mv HelloL32.x HelloL32 a mít tak spustitelný program bez přípony, jak je v Linuxu zvykem. Kromě parametrů pseudoinstrukce PROGRAM FORMAT=ELFX a WIDTH=32 ještě musíme povinně určit vstupní bod programu, tedy první prováděnou instrukci. Vstupní bod (ENTRY=) označíme symbolicky návěstím například Start: nebo Main: apod.

Pokud programy tohoto tutoriálu spouštíme pod MS Windows, ke spuštění linuxové varianty bychom museli mít ve Windows nainstalován emulátor WSL.

Pro vypsání textu v Linuxu opět musíme chtě nechtě využít aplikační rozhraní jeho kernelu. V případě 32bitového systému se funkce kernelu vyvolává instrukcí INT 80h a její parametry se zadávají do registrů EBX, ECX, EDX, ESI, EDI, EBP, přičemž identifikátor volané funkce se zadává v EAX. Naplníme samozřejmě pouze ty vstupní registry, které funkce jádra vyžaduje, v případě funkce zápisu (sys_write má tři parametry) to jsou registry EBX, ECX, EDX. Volání kernelu vrací výsledek funkce v registru EAX, ostatní registry se vracejí nezměněny.
Pro vypsání řetězce "Hello, world!" budeme tedy potřebovat funkci sys_write zapisující na standardní výstup. Podle dokumentace Linux Syscall Reference (32 bit) má tato funkce identifikátor EAX=0x04. V prvním parametru zadávaném v registru EBX se určuje fd, což je file descriptor neboli file handle. Pro standardní výstup je to číslo 1, pro výstup chyb by to bylo 2. Do dalších registrů zadáme druhý a třetí parametr, což je adresa a velikost vypisovaného řetězce. Velikost bychom mohli zadat číslem (v našem příkladě MOV EDX,13), rozumnější je ale zadat ji nepřímo, jako rozdíl adresy $ a adresy HelloWorldAddress. Pokud bychom později prodloužili nebo zkrátili řetězec HelloWordAddress, jeho velikost se nastaví samočinně. Symbol dolaru $ označuje aktuální adresu instrukce, v našem případě instrukce EQU. Jelikož zadáváme délku explicitně, není třeba řetězec zakončovat nulovým znakem. Zkusíme tedy zadat:

HelloL32 PROGRAM FORMAT=ELFX, WIDTH=32, ENTRY=Start: Start: MOV EAX,4 ; Funkce sys_write. MOV EBX,1 ; File descriptor pro standardní výstup. MOV ECX,HelloWorldAddress MOV EDX,HelloWorldSize INT 80h ; Vypiš řetězec. RET HelloWorldAddress DB "Hello, world!" HelloWorldSize EQU $ - HelloWorldAddress ENDPROGRAM HelloL32

Program v souboru hello.asm opět přeložíme pomocí euroasm hello.asm. Pokud vše prošlo dobře a EuroAssembler ohlásil

I0660 32bit FLAT ELFX file "HelloL32.x" created, size=717. "hello.asm"{11} I0650 Program "HelloL32" assembled in 3 passes with errorlevel 0. "hello.asm"{11}

můžeme jej zkusit spustit v nativním Linuxu nebo ve WSL povelem ./HelloL32.x. V mém případě se na konzoli vypsalo:

Hello, world!Segmentation fault (core dumped).

Program tedy funguje, ale po vypsání řetězce zhavaroval s chybou Segmentation fault. Chyba byla v ukončení programu - instrukce RET v Linuxu nestačí, musíme opět použít funkci kernelu sys_exit s číslem 1. Jako první parametr jí zadáme nulu, ostatní parametry nejsou použity. A zároveň na konec řetězce přidáme znak Line Feed (10) pro odřádkování po skončení výpisu. Carriage Return se v Linuxu obvykle nepoužívá.

HelloL32 PROGRAM FORMAT=ELFX, WIDTH=32, ENTRY=Start: Start: MOV EAX,4 ; Funkce sys_write. MOV EBX,1 ; File descriptor pro standardní výstup. MOV ECX,HelloWorldAddress MOV EDX,HelloWorldSize INT 80h ; Vypiš řetězec. MOV EAX,1 ; Funkce sys_exit. MOV EBX,0 ; Errorlevel při ukončení programu. INT 80h ; Ukonči program. HelloWorldAddress DB "Hello, world!",10 HelloWorldSize EQU $ - HelloWorldAddress ENDPROGRAM HelloL32

Po nahrazení RET instrukce pro ukončení programu již program pracuje podle očekávání.

Linux 64 bitů

Program pro 64bitový Linux vypadá obdobně, liší se parametr WIDTH=64, který musíme v pseudoinstrukci PROGRAM zadávat, neboť její default je WIDTH=32. Další věc, která odlišuje 32bitový a 64bitový Linux je volání jádra (funkcí kernelu) nikoli pomocí INT 80h, nýbrž instrukcí SYSCALL. Liší se také číselné identifikátory funkcí kernelu předávané v RAX a pořadí registrů pro přenos parametrů: namísto EBX, ECX, EDX, ESI, EDI, EBP je zadáváme v registrech RDI, RSI, RDX, R10, R8, R9. Jinak ale funkce zůstávají stejné. Přehled volání jádra je např. v Linux System Call Table for x86 64.

HelloL64 PROGRAM FORMAT=ELFX, WIDTH=64, ENTRY=Start: Start: MOV RAX,1 ; Funkce sys_write. MOV RDI,1 ; File descriptor pro standardní výstup. MOV RSI,HelloWorldAddress MOV RDX,HelloWorldSize SYSCALL ; Vypiš řetězec. MOV RAX,60; Funkce sys_exit. MOV RDI,0 ; Errorlevel při ukončení programu. SYSCALL ; Ukonči program. HelloWorldAddress DB "Hello, world!",10 HelloWorldSize EQU $ - HelloWorldAddress ENDPROGRAM HelloL64

Při pokusu o přeložení dostáváme řadu varování

W2340 This instruction requires option "EUROASM CPU=X64".

I těmito chybami by program fungoval, ale raději mu dáme požadovanou volbu přidáním řádku EUROASM CPU=X64 před pseudoinstrukci HelloL64 PROGRAM, aby zbytečně neupozorňoval, že používáme 64bitové registry.
Po novém přeložení už bychom měli dostat errorlevel 0 a můžeme náš 64bitový program vyzkoušet:

$ ./HelloL64.x Hello, world! $

Windows 32 bitů

Volání funkcí v MS Windows používá aplikační rozhraní Win32 definované v dynamicky linkovaných knihovnách. Většina základních funkcí je k dispozici v knihovně "kernel32.dll".

Budeme opět psát na standardní výstup. Oproti Linuxu zde ale nejsou pevně stanovené identifikátory souborů fd, jako byly 0 pro standardní vstup, 1 pro standardní výstup a 2 pro chybový výstup. Místo toho se nejprve musíme operačního systému zeptat, jaký identifikátor (file handle) je dnes používán pro standardní výstup. K tomu slouží funkce Win32 nazvaná GetStdHandle, které jako parametr zadáme identifikátor standardního výstupu, což je číslo -11. Hodnota vracená funkcí v registru EAX je pak již file handle použitelný ve funkci WriteFile.

Zbývá vyřešit, jak funkce Win32 vyvolat a jak jim předat parametry (adresu a velikost řetězce). 32bitový Windows používá volací konvenci standard call, kde se nejprve uloží parametry na zásobník (PUSH) v pořadí od posledního k prvnímu, a pak se zavolá (CALL) importovaná funkce. O odstranění uložených parametrů ze zásobníku se postaral programátor dané funkce. Obvykle to dělá tak, že místo obyčejného návratu pomocí RET použije RET n*4, kde n je počet parametrů na zásobníku. Instrukce RET n*4 funguje jako obyčejný návrat z funkce (RET), ale navíc pak zvětší ESP o n*4 bajty.

Jak zařídit, aby instrukce CALL poznala, že voláme dynamicky linkovanou funkci? Buď do programu začleníme importní knihovnu winapi.lib, anebo jméno funkce definujeme pseudoinstrukcí IMPORT. Tato pseudoinstrukce má parametr LIB=, kterým definujeme soubor (knihovnu), jež funkci obsahuje. Knihovna je v popisu každé funkce na webu Microsoftu definována v odstavci Requirements. Pokud je název knihovny "kernel32.dll", nemusíme ji parametrem LIB= uvádět. Zkusíme tedy napsat spustitelný program pro Windows, který bude mít formát PE (Portable Executable):

HelloW32 PROGRAM FORMAT=PE, WIDTH=32, ENTRY=Start: IMPORT GetStdHandle, WriteFile, ExitProcess ; Použijeme 3 importované funkce z "kernel32.dll". Start: PUSH -11 ; 1. parametr: Identifikátor pro standardní výstup. CALL GetStdHandle ; Funkce vrací file handle v EAX. PUSH 0 ; 5. parametr (lpOverlapped): nepoužijeme. PUSH lpNumberOfBytesWritten ; 4. parametr: adresa dvojslova pro výsledek (skutečně zapsaný počet bajtů). PUSH HelloWorldSize ; 3. parametr: počet zapisovaných bajtů. PUSH HelloWorldAddress ; 2. parametr: adresa řetězce. PUSH EAX ; 1. parametr: file handle. CALL WriteFile ; Funkce s 5 parametry zapíše řetězec na standardní výstup. PUSH 0 ; 1. parametr: errorlevel. CALL ExitProcess ; Ukončení programu. Funkce nic nevrací. HelloWorldAddress DB "Hello, world!",10 HelloWorldSize EQU $ - HelloWorldAddress lpNumberOfBytesWritten DD 0 ; Sem Windows zapíše, kolik znaků WriteFile zapsala. ENDPROGRAM HelloW32

Přeložíme jej pomocí euroasm hello.asm a výsledek by měl být

... I0180 Assembling source file "hello.asm". I0270 Assembling source "hello". I0310 Assembling source pass 1. I0330 Assembling source pass 2 - final. I0470 Assembling program "HelloW32". "hello.asm"{1} I0510 Assembling program pass 1. "hello.asm"{1} I0510 Assembling program pass 2. "hello.asm"{1} I0530 Assembling program pass 3 - final. "hello.asm"{1} I0660 32bit FLAT PE file "HelloW32.exe" created, size=2588. "hello.asm"{16} I0650 Program "HelloW32" assembled in 3 passes with errorlevel 0. "hello.asm"{16} I0750 Source "HelloW32" (15 lines) assembled in 2 passes with errorlevel 0. I0860 Listing file "HelloW32.asm.lst" created, size=2514. I0980 Memory allocation 512 KB. 68 statements assembled in 1 s. I0990 EuroAssembler terminated with errorlevel 0.

Po spuštění programu ve Windows povelem HelloW32.exe nebo jen HelloW32 bychom měli dostat pozdrav Hello, world!.

Windows 64 bitů

MS Windows v 64bitovém režimu používá tytéž Win32 funkce a tytéž knihovny "kernel32.dll" jako 32bitový Windows, podstatně se ale liší volací konvence: namísto StdCall se používá FastCall. V této konvenci se pro první čtyři parametry používají RCX, RDX, R8, R9. Vyžaduje-li funkce více než čtyři parametry, ukládají se na zásobník opět v opačném pořadí (od posledního do pátého). Pak se na zásobníku rezervuje místo (tzv. shadow space) pro čtyři parametry předávané v registrech, a to i tehdy, má-li funkce méně než čtyři parametry. Před zavoláním externí funkce (před instrukcí CALL) musí být navíc zásobník (registr RSP) zaokrouhlen na celý násobek 16 bajtů. V případě, že jsou funkci předávány parametry typu plovoucí desetinné čárky, namísto příslušného RCX, RDX, R8, R9 se použije dolní polovina XMM0, XMM1, XMM2, XMM3. Volaná funkce po skončení neodstraňuje parametry ze zásobníku, o to se musí postarat volající.

Vybaveni těmito znalostmi se pokusíme vypsat řetězec v 64bitových Windows:

EUROASM CPU=X64 HelloW64 PROGRAM FORMAT=PE, WIDTH=64, ENTRY=Start: IMPORT GetStdHandle, WriteFile, ExitProcess Start: TEST SPL,08h ; Kontrola zarovnání zásobníku na 16. JZ Round ; Skok přes PUSH RAX, pokud byl zásobník zaokrouhlený na 16. PUSH RAX ; Jinak proveď instrukci pro snížení RSP o 8 bajtů. Round: SUB RSP,4*8 ; Rezervuj shadow space pro 4 registry. MOV RCX,-11 ; 1. parametr: identifikátor pro standardní výstup. CALL GetStdHandle ; Funkce vrací file handle v RAX. ADD RSP,4*8 ; Návrat ukazatele zásobníku na stav Round. PUSH RAX ; Instrukce pro snížení RSP o 8 bajtů kvůli zaokrouhlení. PUSH 0 ; 5. parametr (lpOverlapped): nepoužijeme. SUB RSP,4*8 ; Rezervuj shadow space pro 4 registry. MOV R9,lpNumberOfBytesWritten ; 4. parametr: dvojslovo pro výsledek (skutečně zapsaný počet bajtů). MOV R8,HelloWorldSize ; 3. parametr: počet zapisovaných bajtů. MOV RDX,HelloWorldAddress ; 2. parametr: adresa řetězce. MOV RCX,RAX ; 1. parametr: file handle. CALL WriteFile ; Funkce zapíše řetězec na standardní výstup. ADD RSP,6*8 ; Návrat ukazatele zásobníku na stav Round. SUB RSP,4*8 ; Rezervuj shadow space pro 4 registry (zbytečné). MOV RCX,0 ; 1. parametr - errorlevel. CALL ExitProcess ; Funkce nic nevrací. HelloWorldAddress DB "Hello, world!",10 HelloWorldSize EQU $ - HelloWorldAddress lpNumberOfBytesWritten DQ 0 ; Sem Windows zapíše, kolik znaků WriteFile zapsala. ENDPROGRAM HelloW64

Zkusíme to s makry

Jak je vidět zejména v posledním případě pro 64bitový Windows, vyvolání tak prosté funkce, jako je vypsání pozdravu, vyžaduje napsání poměrně velkého počtu strojových instrukcí. Pokusíme se s tím něco udělat. Klíčem k redukci programátorské práce je použití makroinstrukcí. Každá makroinstrukce neboli makro může nahradit celou řadu strojových instrukcí, přitom makro může akceptovat vstupní parametry a upravovat tak svou činnost podle potřeby programátora.

Syntaxe jazyka makroinstrukcí a jeho nuance jsou vlastnosti použitého asembleru, prakticky každý asembler je řeší po svém. Zde se zaměříme na psaní makroinstrukcí v jazyce EuroAssembler. Jeho aparát používá pro výrazy používané při psaní maker znak procenta %. Pseudoinstrukce začínající procentem se týkají maker nebo pomocných proměnných asembleru. Zatímco běžné paměťové proměnné zapsané pomocí pseudoinstrukce D, například OrdinaryVar DD 1234h definují místo v paměti nazvané OrdinaryVar a obsahující DWORD s hodnotou 1234h, proměnná %OrdinaryVar představuje něco zcela jiného: proměnnou samotného EuroAssembleru. Její místo není v přeloženém programovém kódu nebo v jeho datech, ale existuje jen během práce EuroAssembleru v jeho paměti. Její obsah může být nastaven pseudoinstrukcí %SET a může jím být jakýkoli text, aritmetický výraz, řetězec, číslo apod. Kdykoli se pak ve zdrojovém textu objeví %OrdinaryVar, bude tímto textem nahrazeno.

MyAddress %SET "Hello, world!" MySize %SET 13 HelloWorldAddress DB %MyAddress ; Totéž jako HelloWorldAddress DB "Hello, world" HelloWorldSize EQU %MySize ; Totéž jako HelloWorldSize EQU 13

Makroinstrukce jsou definovány dvojicí blokových pseudoinstrukcí %MACRO a %ENDMACRO. Identifikátor v poli návěstí %MACRO se stává názvem makroinstrukce. Strojové instrukce uvnitř bloku %MACRO/%ENDMACRO jsou tělem makra. Kdykoli ve zdrojovém kódu napíšeme název makroinstrukce, nahradí se všemi strojovými instrukcemi z jeho definice. Příklad makra pro 64bitový Linux:

; Definice makra WriteString: WriteString %MACRO StringAddress, StringSize MOV RAX,1 ; Funkce sys_write v 64bitovém Linuxu. MOV RDI,1 ; File descriptor pro standardní výstup. MOV RSI, %1 ; První argument makra. MOV RDX, %2 ; Druhý argument makra. SYSCALL %ENDMACRO WriteString ; Použití (expanze) makra WriteString: WriteString HelloWorldAddress, HelloWorldSize ; Zde by měl vypsat řetězec "Hello, world!". HelloWorldAddress DB "Hello, world!",10 HelloWorldSize EQU $ - HelloWorldAddress

Nadefinovali jsme makro nazvané WriteString se dvěma parametry: adresou řetězce a jeho délkou. Uvedení názvu makra v programu pak způsobí jeho rozvinutí do pěti strojových instrukcí, přičemž první a druhý parametr jsou k dispozici jako proměnné %1 a %2. Místo číselného označení proměnných parametrů (%1, %2) bychom také mohli použít formální názvy parametrů tak, že názvu parametru v definici makra (StringAddress, StringSize) předřadíme v těle makra znak procenta:

; Definice makra WriteString: WriteString %MACRO StringAddress, StringSize MOV RAX,1 ; Funkce sys_write v 64bitovém Linuxu. MOV RDI,1 ; File descriptor pro standardní výstup. MOV RSI, %StringAddress ; První argument makra. MOV RDX, %StringSize ; Druhý argument makra. SYSCALL %ENDMACRO WriteString

Makro můžeme dále vylepšit tak, že file descriptor pro standardní výstup předávaný v RDI nebudeme zadávat natvrdo jako číslo 1, ale zadáme jej jako parametr. A abychom nemuseli tento parametr uvádět v případě, že použijeme jeho obvyklou hodnotu 1, zadáme jej jako klíčový parametr, tedy s rovnítkem a s defaultní hodnotou 1:

WriteString %MACRO StringAddress, StringSize, fd=1 MOV RAX,1 ; Funkce sys_write v 64bitovém Linuxu. MOV RDI,%fd ; File descriptor. MOV RSI, %StringAddress ; První argument.makra. MOV RDX, %StringSize ; Druhý argument.makra. SYSCALL %ENDMACRO WriteString WriteString HelloWorldAddress, HelloWorldSize ; Zde by měl vypsat řetězec "Hello, world!" na standardní výstup, neboť jsme nezměnili třetí parametr fd=1.

Stejné makro nyní můžeme použít i pro psaní na chybový výstup místo standardního; stačí přidat parametr fd=2.

Literál namísto symbolu

EuroAssembler dovoluje definovat paměťové proměnné pomocí literálů, tj. bezprostředně zadaných hodnot. Namísto definování řetězce pseudoinstrukcí DB a vymýšlení jeho symbolického jména jej definujeme až při jeho použití v instrukci:

ClassicVar DB "Memory variable defined classically.",10 WriteString ClassicVar, 36 ; Použita klasická definice řetězce. WriteString =B "Memory variable defined as literal.",35 ; Použita definice literálem.

Literál se definuje rovnítkem, za nímž následuje určení typu (BYTE, WORD, DWORD, QWORD nebo jen B, W, D, Q) a pak jeho hodnota. Výhodou literálu oproti symbolu je, že mu nemusíme vymýšlet název proměnné a hned vidíme jeho hodnotu v instrukci, kde byl použit.

S použitím makrojazyka a literálů nyní můžeme pro vypsání řetězce na standardní výstup napsat vlastní makra. Nazveme je třeba StdOutput. Tato práce již byla vykonána a makra jsou uvedena v knihovnách dodávaných spolu s EuroAssemblerem, konkrétně v makroknihovnách dosapi.htm pro DOS 16 bitů, linapi.htm pro Linux 32 bitů, linabi.htm pro Linux 64 bitů, winapi.htm pro Windows 32 bitů, winabi.htm pro Windows 64 bitů.

S použitím makra StdOutput a s použitím literálů se naše zkušební prográmky podstatně zjednoduší, stačí includovat patřičnou knihovnu podle cílové platformy. Inkludování pomocí pseudoinstrukce INCLUDE způsobí to, že jmenovaná knihovna (což je jen další zdrojový soubor) nahradí řádek s povelem INCLUDE "knihovna.htm". Knihovny s rozhraním API (16 a 32 bit) nebo ABI (64bit) obsahují definice maker StdOuput, TerminateProgram a několik dalších.

Pro zjednodušení zapíšeme programy pro DOS, Linux i Windows do jednoho zdrojového souboru hello.asm. Vzhledem k tomu, že se makra pro zápis na standardní výstup a pro ukončení programu jmenují ve všech knihovnách stejně (StdOutput a TerminateProgram), měli bychom před nadefinováním dalšího programu říci EuroAssembleru pomocí %DROPMACRO, aby zapomenul jejich definice z předchozí knihovny.

EUROASM CPU=X64 HelloDos PROGRAM FORMAT=COM, WIDTH=16 ; Verze pro DOS. INCLUDE dosapi.htm StdOutput =B "Hello, world!" TerminateProgram ENDPROGRAM HelloDos %DROPMACRO * HelloL32 PROGRAM FORMAT=ELFX, WIDTH=32, ENTRY=Start ; Verze pro Linux 32 bit. INCLUDE linapi.htm Start: StdOutput =B "Hello, world!" TerminateProgram ENDPROGRAM HelloL32 %DROPMACRO * HelloL64 PROGRAM FORMAT=ELFX, WIDTH=64, ENTRY=Start ; Verze pro Linux 64 bit. INCLUDE linabi.htm Start: StdOutput =B "Hello, world!" TerminateProgram ENDPROGRAM HelloL64 %DROPMACRO * HelloW32 PROGRAM FORMAT=PE, WIDTH=32, ENTRY=Start ; Verze pro Windows 32 bit. INCLUDE winapi.htm Start: StdOutput =B "Hello, world!" TerminateProgram ENDPROGRAM HelloW32 %DROPMACRO * HelloW64 PROGRAM FORMAT=PE, WIDTH=64, ENTRY=Start ; Verze pro Windows 64 bit. INCLUDE winabi.htm Start: StdOutput =B "Hello, world!" TerminateProgram ENDPROGRAM HelloW64

Po přeložení výše uvedeného souboru známým povelem euroasm hello.asm bychom měli dostat pět programů s názvy HelloDos.com, HelloL32.x, HelloL64.x, HelloW32.exe, HelloW64.exe, které hned můžeme s pomocí emulátorů (DosBox, WSL, wine) vyzkoušet.

Definice a expanze maker

Blok instrukcí mezi %MACRO a %ENDMACRO představuje definici makra. Samotná definice ještě nic zajímavého nedělá, pouze zabírá místo v zdrojovém kódu. Teprve až se pokusíme makroinstrukci použít v programu, dojde k její expanzi, tedy k nahrazení názvu makra instrukcemi z jeho těla (a možná taky k projevení chyb, pokud jsme nějakou při psaní makra spáchali).

Definici makra můžeme v EuroAssembleru napsat na začátku bloku PROGRAM/ENDPROGRAM, nebo před tímto blokem, případně i v separátním includovaném souboru (knihovně), avšak vždy předtím, než bude makro poprvé použito (expandováno). Makroinstrukce (a také proměnné EuroAssembleru začínající znakem procenta) totiž na rozdíl od běžných symbolů procházejí hranicemi bloku PROGRAM/ENDPROGRAM, jsou viditelné v celém zdrojovém kódu počínaje jejich definicí. V tom se liší od symbolů, které musí být v bloku PROGRAM/ENDPROGRAM unikátní, nesmějí se opakovat.

Makra a %proměnné tedy mohou být redefinovány i v rámci téhož zdrojového souboru. Redefinování makra je ale poněkud neobvyklé, proto na ně EuroAssembler reaguje varovnou zprávou W2512 Overwriting previously defined macro. Pokud tedy chceme makro přepsat jinou definicí makra se stejným názvem, je lepší ho nejprve nechat zapomenout pomocí pseudoinstrukce %DROPMACRO.

Načtení informace od uživatele

Předat text uživateli našeho programu bylo snadné, v předchozích příkladech jsme použili službu operačního systému obvykle nazvanou write nebo podobně, možná zabalenou do makra StdOutput. Teď se podíváme na opačný případ, kdy chceme něco získat od uživatele. Jednou z možností je načtení argumentů z příkazového řádku, kterým jsme náš program spustili.

Z příkazového řádku

Pokud se náš program jmenuje například MyCalc a v konsole jsme napsali MyCalc.exe 2 + 3, operační systém nám poskytne řetězec obsahující tutéž informaci MyCalc.exe 2 + 3. Tedy název spuštěného programu (nultý argument) a pak přesnou kopii následujících znaků včetně mezer či jiných znaků oddělujících argumenty. Kde je tento řetězec uložen?

V DOSu leží ve struktuře PSP počínaje bajtem 81h. Předchozí bajt na adrese 80h obsahuje délku řetězce.

Ve Windows dostaneme ukazatel na obdobný řetězec pomocí funkce API GetCommandLine. Pokud bychom chtěli mít každý argument zvlášť, musíme řetězec načítat např. instrukcí LODSB a reagovat na oddělovací znaky (této činnosti se říká parsing).

V Linuxu je to trochu jinak: řetězec už je rozparsován na jméno programu a na jeho mezerou oddělené argumenty, přičemž všechny tyto položky jsou zakončeny nulovým znakem a ukazatele na ně jsou uloženy na zásobníku. Schématicky je to znázorněno na obrázku u makra GetArg. Adresy zásobníkových položek na tomto obrázku rostou směrem nahoru. Šířka každé položky je 8 bajtů v 64bitovém programu nebo 4 bajty v 32bitovém programu. Instrukcí MOV RCX,[RSP] na začátku programu bychom tedy do RCX načetli celkový počet argumentů. Adresu prvního argumentu získáme pomocí MOV RSI,[RSP+2*8], adresu druhého pomocí MOV RSI,[RSP+3*8] atd.

Anebo použijeme již hotové makro GetArg které dodá jednotlivé argumenty již rozparsované bez ohledu na operační systém. Stačí jen použít patřičnou makroknihovnu pro DOS, Linux nebo Windows v potřebné šířce 16, 32 nebo 64 bitů. Knihovny maker se načítají pseudoinstrukcí INCLUDE, které zadáme jako parametr jméno souboru dosspi.htm, linapi.htm, linabi.htm, winapi.htm nebo winabi.htm. Makro GetArg se jmenuje ve všech těchto knihovnách stejně a pro požadovaný argument vrátí ukazatel na něj v rSI a jeho délku v rCX. Případně vrátí CarryFlag, pokud dotyčný argument nebyl na příkazovém řádku dodán.

Zkusíme naprogramovat primitivní čtyřúkonovou kalkulačku. Po zadání dvou celých čísel rozdělených znaménkem aritmetické operace by naše kalkulačka měla vrátit správný výsledek. Přípustné operace budou určeny znaménkem +, -, *, / pro sčítání, odčítání, násobení, dělení.

Jako cílovou platformu vybereme 32bitové Windows, odpovídající knihovnou bude tedy winapi.htm. Zapíšeme-li na příkazový řádek CalcW32 3 + 4, chceme dostat výsledek 7. Soubor se zdrojovým kódem nazvěme třeba calc.asm.Zapíšeme do něj:

CalcW32 PROGRAM FORMAT=PE, WIDTH=32, ENTRY=Start:, IconFile= INCLUDE winapi.htm Start: GetArg 1 ; Použij makro k načtení 1. argumentu našeho programu. JC Error: ; Pokud argument chybí, skoč na návěstí Error. ; ESI ukazuje na první argument (číslici 3), v ECX je jeho délka (1 bajt). ; Nemáme dost registrů pro trvalé zapamatování všech argumentů, ; takže pro uložení obou číselných argumentů a znaménka operace založíme ; tři prázdné řetězcové proměnné Arg1, Arg2, Arg3 v délce jednoho bajtu. Arg1: D BYTE ; Definuj paměťovou proměnnou typu BYTE. Její název je Arg1. Arg2: D BYTE Arg3: D BYTE ; Jelikož jsme úspěšně načetli první argument (CF=0, neskákalo se na návěstí Error:), ; uložíme argument do řetězcové proměnné a načteme další. MOV EDI,Arg1 REP MOVSB ; Přesuň řetězec z adresy ESI na adresu EDI v délce ECX bajtů. GetArg 2 JC Error: MOV EDI,Arg2 REP MOVSB ; Ulož znak požadované operace (v tomto případě +). GetArg 3 JC Error: MOV EDI,Arg3 REP MOVSB ; Ulož třetí argument (číslici 4). ; Argumenty jsou zadány. Zkusme je pro kontrolu vypsat: StdOutput Arg1, Arg2, Arg3, Size=1 ; Kontrolní výpis argumentů. CMP [Arg2],'+' ; Byla zadána operace sčítání? JNE Error: ; Jinou jsme náš program zatím nenaučili. StdOutput =B "=" ; Vypíšeme rovnítko, specifikované jako literál. MOV AL,[Arg1] ; První argument do registru AL. ADD AL,[Arg3] ; Přičti k němu třetí argument. ; Teď bychom měli vypsat součet z registru AL. ; Avšak makro StdOutput vypisuje obsah paměti, nikoli registru. ; Musíme proto AL nejprve uložit, třeba do Result. Result: D BYTE MOV [Result],AL StdOutput Result, Size=1 ; Výpis výsledného součtu. TerminateProgram ; Makro z knihovny winapi.htm pro ukončení. Error: StdOutput Help Help: DB "Calculate number1 operation number2.", 13, 10 DB "Example: %^PROGRAM 3 + 4", 13, 10, 0 ENDPROGRAM

Mělo by vás zarazit definování proměnných Arg1, Arg2, Arg3 uprostřed toku instrukcí. To je špatná programátorská technika, která ale v EuroAssembleru funguje díky defaultně povolenému parametru EUROASM AUTOSEGMENT=ON. Autosegmentace rozlišuje, zda byla na řádku uvedena strojová instrukce nebo definice dat a podle toho rozděluje výstup přeložených instrukcí do samostatných sekcí pro programový kód, inicializovaná data a neinicializovaná data. Tyto sekce mají tradiční názvy [.text], [.data], [.bss]. Pokud bychom si prohlédli listing calc.asm.lst, automatickou změnu sekcí tam uvidíme.
Spoléhat na autosegmentaci ale u delších programů není příliš moudré, raději už při psaní programu zařaďme datové položky Arg* až za programový kód.

Dále si povšimněme řádku nápovědy DB "Example: %^PROGRAM 3 + 4", 13, 10, 0. Výraz %^PROGRAM zde nepředstavuje uživatelsky definovanou proměnnou díky znaku ^ (caret) následujícím po znaku procenta. Takovéto proměnné jsou systémové, jejich hodnota je nastavena samotným EuroAssemblerem, v tomto konkrétním případě na jméno programu (CalcW32). Systémových proměnných má EuroAssembler spoustu, každý parametr pseudoinstrukcí EUROASM a PROGRAM má jednu, viz karta nápovědy.

Program se přeložil bez chyby, ale po spuštění vrací nesprávný výsledek:

C:\> CalcW32 3 + 4 3+4=g C:\>

Díky řádku StdOutput Arg1, Arg2, Arg3, Size=1 ; Kontrolní výpis argumentů. jsme si nechali vypsat, s jakými argumenty byl CalcW32 spuštěn. To je dobrá taktika pro hledání chyb (debugging). Vidíme, že všechny tři argumenty byly zadány správně.

Zkušenější programátor by v programu CalcW32 našel několik chyb. Například není určeno, kde má program pokračovat po vypsání nápovědy na řádku s návěstím Error. Chceme ho poslat na ukončení programu. Přidejme tedy k pseudoinstrukci TerminateProgram návěstí End:, kam necháme program skočit po vypsání nápovědy. Další chybou, která způsobuje nesprávný výsledek 3+4=g je nerozlišování binárního čísla a ASCII kódu jednotlivých číslic, které nám GetArg vrací. Při zadání číslice 3 jako argumentu makro GetArg vrací její ASCII kód, což je hexadecimálně 33. Obdobně namísto číslice 4 dostáváme 34 a protože jsme sčítali ASCII kódy, výsledkem je 33h + 34h = 67h, což je ASCII kód písmene g a nikoli číslice 7. Před použitím instrukce ADD pro sčítání musíme převést ASCII kódy na prostá binární čísla. To se provede snadno, odečtením 30h neboli '0' od ASCII kódu číslice. Po binárním sečtení pak převedeme součet přičtením 30h a tento ASCII kód již můžeme nechat vypsat jako výsledek pomocí StdOutput. Upravíme tedy program CalcW32 takto:

CalcW32 PROGRAM FORMAT=PE, WIDTH=32, ENTRY=Start: INCLUDE winapi.htm [.text] ; Takto se v EuroAssembleru označuje, že budou následovat strojové instrukce. Start: GetArg 1 ; Použij makro k načtení 1. argumentu našeho programu. JC Error: ; Pokud argument chybí, skoč na návěstí Error. ; ESI ukazuje na první argument (číslici 3), v ECX je jeho délka (1 bajt). MOV EDI,Arg1 REP MOVSB ; Přesuň řetězec z adresy ESI na adresu EDI v délce ECX bajtů. GetArg 2 JC Error: MOV EDI,Arg2 REP MOVSB ; Ulož znak požadované operace (v tomto případě +). GetArg 3 JC Error: MOV EDI,Arg3 REP MOVSB ; Ulož třetí argument (číslici 4). ; Argumenty jsou zadány. Zkusme je pro kontrolu vypsat: StdOutput Arg1, Arg2, Arg3, Size=1 ; Kontrolní výpis argumentů. CMP [Arg2],'+' ; Byla zadána operace sčítání? JNE Error: ; Jinou jsme náš program zatím nenaučili. StdOutput =B "=" ; Vypíšeme rovnítko, specifikované jako literál. MOV AL,[Arg1] ; První argument do registru AL. SUB AL,'0' ; Převod ASCII na binární hodnotu. MOV BL,[Arg3] ; Třetí argument do registru BL. SUB BL,'0' ; Převod ASCII na binární hodnotu. ADD AL,BL ; Binární součet svou celých čísel. ADD AL,'0' ; Převod binárního čísla na ASCII. MOV [Result],AL ; Uložení ASCII výsledku do Arg4. StdOutput Result, Size=1 ; Výpis výsledného součtu. End: TerminateProgram ; Makro z knihovny winapi.htm pro ukončení. Error: StdOutput Help JMP End: [.data] ; Tady skončila sekce programového kódu a začínají data. Help: DB "Calculate number1 operation number2.", 13, 10 DB "Example: %^PROGRAM 3 + 4", 13, 10, 0 Arg1: D BYTE ; Definuj paměťovou proměnnou typu BYTE. Její název je Arg1. Arg2: D BYTE Arg3: D BYTE Result: D BYTE ENDPROGRAM
C:\> CalcW32 3 + 4 3+4=7 C:\> CalcW32 3 + 7 3+7=: C:\>

Program už funguje dobře, ovšem pouze pro sčítání jednociferných čísel a musíme dodržet mezery mezi argumenty. Při zadání bez mezer CalcW32 3+4 program vypisuje nápovědu, tzn. něco se mu nelíbí. Asi vzal celý řetězec 3+4 jako jeden argument a při pokusu o načtení neexistujícího druhého a třetího už GetArg vrací chybu. Budeme muset vyřešit vstup a výstup binárních čísel v ASCII (znakové) podobě pro delší čísla než pouze jednociferná. Operační systémy bohužel nenabízejí žádnou funkci převodu ASCII čísel na binární a zpět, vše musíme naprogramovat sami. Máme-li jako vstupní řetězec prvního čísla třeba znaky "123", očekáváme výsledek v binární podobě jako číslo 123, tedy hexadecimálně 7Bh v 8bitovém registru neboli 0000007Bh v 32bitovém. Jak převést řetězec "123" na binární číslo? Každý postupně načtený znak v rozsahu "0" až "9" nejprve převedeme na číslo v rozsahu 0 až 9 odečtením "0" neboli odečtením 30h. Pak jsou dva možné přístupy, jak takto získaná čísla postupně skládat do výsledku:

  1. Začneme odzadu (zprava) a každou číslici nejprve vynásobíme její váhou. U trojmístého čísla "123" je váha trojky 1, váha dvojky 10 a váha jedničky 100. Více číslic není, takže stačí tato tři zvážená čísla sečíst a dostáváme 3*1 + 2*10 + 1*100 = 123.
  2. Začneme zleva první číslicí (v našem příkladu je to "1"), kterou převedeme na číslo odečtením 30h a vložíme do akumulačního registru. Bude tam tedy binární číslo 1. Pak načteme další znak a pokud je to číslice (ano, je to "2"), nejprve vynásobíme akumulátor deseti a pak do něj přičteme odpovídající číslo 2. V akumulátoru tedy bude 1*10 + 2 = 12. Opakujeme tento krok, dokud jsou ve vstupním řetězci číslice. Po třetím kroku bude v akumulátoru 12*10 + 3 = 123. Ve čtvrtém kroku už se nejedná o číslici, takže končíme s výsledkem 123.

Druhá metoda vypadá jednodušeji, neboť nemusíme počítat narůstající váhy 1, 10, 100 atd., místo toho vystačíme s opakovaným násobením deseti. Nazveme tento algoritmus ASCIItoInteger. Asi jej budeme používat častěji v různých programech, proto jej zapouzdříme do procedury pomocí pseudoinstrukcí PROC a ENDPROC. Tu pak budeme volat pomocí CALL ASCIItoInteger kdykoli bude potřeba převést číslo ze znaků ASCII na celočíselnou binární hodnotu.

ASCIItoInteger PROC ; Definování procedury. SUB EAX,EAX ; Akumulátor výsledku. SUB EBX,EBX ; Registr pro převod čísla. SUB ECX,ECX ; Délka vstupního čísla. MOV EDI,10 ; Násobitel. Next: MOV BL,[ESI+ECX] ; Načtení ECX-tého znaku ze vstupního řetězce ESI. SUB BL,'0' ; Převod na binární hodnotu, pokud to byla číslice. JB End: ; Skok, pokud to nebyla číslice. CMP BL,9 ; Kontrola na horní mez číslice. JA End: ; Skok, pokud to nebyla číslice. INC ECX ; Započtení znaku do registru určujícího délku vstupního čísla. MUL EDI ; Vynásobení akumulátoru deseti. JC Over: ; Skok při přetečení 32 bitů. ADD EAX,EBX ; Přičtení poslední číslice do akumulátoru. JMP Next: ; Zpracování dalšího znaku. End: CLC ; Návrat s vynulovaným příznakem CF. Binární číslo je v EAX. Over: RET ; Návrat s nastaveným příznakem CF. Výsledek není definován. ENDPROC ASCIItoInteger ; Konec procedury.

Ještě zbývá doplnit proceduru popisem, co přesně dělá, jaké hodnoty očekává na vstupu a co poskytuje na výstupu.

; Popis: Procedura ASCIItoInteger převádí řetězec ASCII číslic na binární celé číslo. ; Vstup: ESI obsahuje ukazatel na první znak převáděného čísla v paměti. ; Procedura načítá znaky dokud obsahují číslice nebo dokud nedojde k přetečení. ; Výstup:Carry Flag je nulový, EAX obsahuje převedené číslo v binárním tvaru. ; ECX obsahuje počet zpracovaných ASCII znaků ze vstupního řetězce. ; Chyba: Carry Flag je nastaven, pokud by vstupní číslo přesáhlo 32 bitů. EAX je nedefinován. ; ECX obsahuje počet zpracovaných ASCII znaků ze vstupního řetězce. ; Mění: EBX,EDX,EDI.

Vyrobili jsme tak něco jako černou skříňku, u které již nemusíme přemýšlet nad jejími instrukcemi, ale zabýváme se pouze jejím popisem jako celkem. Později můžeme celou proceduru umístit do samostatného souboru a vytvořit tím knihovnu, kterou pak zařadíme do všech programů vyžadujících převody řetězců na čísla. Knihovnu můžeme nazvat třeba libcvt32.asm.

V příkladu jednoduché kalkulačky ještě musíme vyřešit opačný převod binárního čísla na sérii ASCII znaků reprezentující toto číslo ve formátu, který můžeme tisknout pomocí StdOutput. Opět jsou zde dvě možnosti:

  1. Vstupní číslo opakovaně dělíme deseti a zbytky po celočíselném dělení skládáme do výstupu odzadu.
  2. Vstupní číslo v rozsahu 0..4_294_967_295 dělíme váhou nejvýznamnější číslice (1_000_000_000) a podíl se zanedbáním zbytku v rozsahu '0'..'9' zapisujeme do výstupu zleva. Pak postup opakujeme se zbytkem, který dělíme váhou druhé nejvýznamnější číslice (100_000_000) atd, až dojdeme k váze 1.

Druhý přístup je komplikovanější nutností udržovat dělitele 1_000_000_000, 100_000_000, 10_000_000 atd., použijeme první metodu. Opět to naprogramujeme jako samostatnou proceduru, neboť se jedná o často používanou funkci.

; Popis: Procedura IntegerToASCII převádí binární číslo z registru EAX na řetězec ASCII číslic. ; Vstup: EAX obsahuje vstupní binární bezznaménkové číslo v rozsahu 0..4294967295. ; ESI obsahuje ukazatel na výstupní pole o velikosti 10 bajtů. ; Výstup:ESI je zvětšen o 0..9 bajtů a ukazuje na první platnou číslici výsledku. ; Chyba: K chybě nemůže dojít. ; Mění: EAX,ECX,EDX,EDI. IntegerToASCII PROC ; Definování procedury. MOV ECX,10 ; Počet míst ve výsledku. MOV EDI,10 ; Dělitel. Next1:SUB EDX,EDX ; Vyšších 32 bitů vstupního čísla je před dělením EDX:EAX nutno vynulovat. DIV EDI ; EAX je teď podíl, EDX zbytek 0..9. ADD DL,'0' ; Převod DL na číslici '0'..'9'. DEC ECX ; Ukládání čísel zezadu. JS End1: ; Skok při ukončení všech deseti číslic. MOV [ESI+ECX],DL ; Uložení číslice na konec výsledku. JMP Next1: ; Zpracování další číslice. End1: ; Pole výsledku nyní pro vstupní číslo 123 obsahuje "0000000123". ; Napozicujeme ESI na první nenulovou číslici výsledku. LEA EDI,[ESI+10] ; Zarážka, za níž už se nebude ESI číst. Next2:CMP ESI,EDI ; Konec výstupního čísla? JAE End2: ; Skok pokud ano, ESI bude ukazovat na poslední '0'. LODSB ; Načtení číslice výsledku z ESI, inkrementace ESI. CMP AL,'0' ; Je to začátek platných cifer? JE Next2: ; Skok, pokud nikoli. End2: DEC ESI ; Návrat ESI na první platnou číslici. RET ; Návrat z procedury. ENDPROC IntegerToASCII

Vraťme se k našemu kalkulátoru. Nevýhodou načítání vstupu z příkazového řádku je použitelnost pouze pro jeden příklad; pro zadání dalšího výpočtu se musí program spustit znovu. Naučíme se proto číst vstupní znaky zadané z klávesnice, abychom mohli zadávat různé početní příklady opakovaně.

Přímé čtení klávesnice a myši pomocí instrukcí IN a OUT už v době USB periférií a chráněných operačních systémů není aktuální. Nejnižší úroveň práce s klávesnicí v DOSu a jeho emulacích poskytuje rozhraní INT 16h, konkrétně funkce CHECK FOR KEYSTROKE a pokud tato zjistí, že byla stisknuta klávesa, pak ještě GET KEYSTROKE, která vrací ASCII znak v AL.

None: MOV AH,01h ; CHECK FOR KEYSTROKE INT 16h ; Dotaz na klávesnici. JZ None: ; Pokud nic nebylo stisknuto, čekej dále. MOV AH,00h ; GET KEYSTROKE INT 16h ; Získej stisknutý znak v registru AL.

Ze standardního vstupu

Ve Windows, Linuxu, ale i v DOSu je vhodnější funkce pro čtení ze standardního vstupu, která je přesměrovatelná a po řádcích vrací text zadaný z klávesnice. Do programu používajícícho čtení standardního vstupu může být přesměrován obsah jiného textového souboru, například v DOSu takto: type answers.txt | program.exe.

Ke čtení použijeme v DOSU funkci READ FROM FILE OR DEVICE, v Linuxu sys_read, ve Windows ReadFile. Tyto funkce nevracejí ASCII znaky po každém stisku klávesy, ale implementují řádkový editor. To znamená, že se napsaný text dá přepisovat nebo mazat pomocí klávesy Backspace a do programu se vrátí až když stiskneme klávesu Enter. Vracený text bude v Linuxovém programu zakončen znakem Line Feed (0Ah), v DOSu a ve Windows dvojicí znaků Carriage Return a Line Feed (0Dh 0Ah). Kromě toho tyto funkce informují o počtu skutečně načtených znaků, včetně oněch zakončujících 0Dh,0Ah.

Zkusíme náš nepříliš funkční kalkulátor ve zdrojovém souboru calc.asm vylepšit použitím čtení ze standardního vstupu. Předpokládejme, že již máme textový soubor nazvaný libcvt32.asm, do něhož jsme uložili obě výše uvedené konverzní procedury ASCIItoInteger a IntegerToASCII. Tento soubor vložíme do calc.asm pomocí INCLUDE, takže se zdrojový kód kalkulátoru hezky zmenší.

Pro změnu napišme program pro 32bitový Linux:

CalcL32 PROGRAM FORMAT=ELFX, WIDTH=32, ENTRY=Start: INCLUDE libcvt32.asm, linapi.htm ; Použij funkce z těchto knihoven. [.text] ; Takto se v EuroAssembleru označuje, že budou následovat strojové instrukce. Start: StdOutput Prompt ; Představení programu, výzva k zadání. MOV EAX,3 ; Linux32 funkce kernelu č.3 - čtení ze souboru nebo ze zařízení. MOV EBX,0 ; 1. parametr je file descriptor standardního vstupu. MOV ECX,Buffer ; 2. parametr je adresa buferu, kam načteme celé zadání příkladu, např. řetězec "3 + 7". MOV EDX,SIZE# Buffer ; Atribut SIZE# vrací velikost Buffer v bajtech. Taky bychom mohli použít MOV EDX,80. INT 80h ; Vyvolání funkce kernelu Linuxu. ; Buffer je nyní naplněn EAX znaky zadání. Měl by obsahovat dvě ASCII čísla oddělená aritmetickým operátorem. MOV ESI,Buffer Next1: LODSB ; Načtení dalšího znaku. CMP AL,0 ; Konec vstupního řetězce? JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next1: DEC ESI ; Vrácení ukazatele na platný znak. CALL ASCIItoInteger ; Procedura z knihovny libcvt32.asm. JC Error: MOV [Arg1],EAX ; Uložení prvního čísla. ADD ESI,ECX ; Umísti ESI za první načtené číslo. Next2: LODSB ; Načtení dalšího znaku CMP AL,0 ; Konec vstupního řetězce? JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next2: MOV [Arg2],AL ; Uložení operátoru. Next3: LODSB ; Načtení dalšího znaku CMP AL,0 JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next3: DEC ESI ; Vrácení ukazatele na platný znak. CALL ASCIItoInteger ; Procedura z knihovny libcvt32.asm. JC Error: MOV [Arg3],EAX ; Uložení druhého čísla. CMP [Arg2],'+' JE Addition: CMP [Arg2],'-' JE Subtraction: CMP [Arg2],'*' JE Multiplication: CMP [Arg2],'/' JE Division: Error: StdOutput Help ; Pokud byl zadán neplatný operátor, vypíše se nápověda. JMP End: Addition: MOV EAX,[Arg1] ADD EAX,[Arg3] MOV ESI,Result CALL IntegerToASCII StdOutput ESI JMP End: Subtraction: MOV EAX,[Arg1] SUB EAX,[Arg3] CALL IntegerToASCII StdOutput ESI JMP End: Multiplication: MOV EAX,[Arg1] MUL [Arg3] CALL IntegerToASCII StdOutput ESI JMP End: Division: MOV EAX,[Arg1] SUB EDX,EDX DIV [Arg3] CALL IntegerToASCII StdOutput ESI JMP End: End: TerminateProgram ; Makro pro ukončení programu. [.data] ; Tady skončila sekce programového kódu a začínají data. Arg1 DD 0 Arg2 DD 0 Arg3 DD 0 Result DB 10 * B DB 10,0 Buffer DB 80 * B Prompt: DB "%^PROGRAM: Enter two integer numbers separated with arithmetic operator + - * /.",13, 10, 0 Help: DB "Calculate number1 operator number2.", 13, 10 DB "Example: 3 + 4", 13, 10, 0 ENDPROGRAM

Při pokusu o přeložení předchozího programu EuroAssembler hlásí tyto chyby:

E6610 Symbol "Next1" was already defined at "libcvt32.asm"{38}. "calc.asm"{19} E6610 Symbol "Next2" was already defined at "libcvt32.asm"{48}. "calc.asm"{25} E6610 Symbol "End" was already defined at "libcvt32.asm"{16}. "linapi.htm"{763} "calc.asm"{69}

K chybě došlo proto, že jsme v procedurách i v hlavním programu vícekrát použili návěstí nazvaná Next1, Next2, End. V každém programu může být určité návěstí použito pouze jednou. Měli bychom vymyslet unikátnější jména, ale existuje ještě lepší řešení, jak se zbavit duplicit: použít lokální jména symbolů. Lokální jména začínají tečkou a EuroAssembler si je pak ve skutečnosti pamatuje spojená se jménem procedury nebo programu, v němž bylo návestí definováno. Takže End v proceduře IntegerToASCII se po přejmenování na .End uloží jako IntegerToASCII.End a nebude pak kolidovat s Endem v programu CalcL32.

Jak je to vlastně s tečkami před názvem symbolu a s dvojtečkami za ním? Tečka na začátku označuje symbol jako lokální, ve skutečnosti je jméno symbolu modifikováno připojení názvu jmenného prostoru (programu, procedury, struktury) před lokální jméno. Viz také odstavec jmenného prostoru v manuálu.

Dvojtečka může, ale nemusí být připojena za název symbolu a zdůrazňuje tím, že to je symbol a nikoli třeba jméno struktury, registru, instrukce. Na rozdíl od většiny jiných asemblerů může být dvojtečka v €ASM připojena za jméno symbolu nejen při jeho definici v poli návěstí, ale také kdykoli je symbol zmiňován, například MOV RSI, Symbol:. A pokud je dvojtečka dvojitá, zároveň tím označuje globální viditelnost symbolu, nemusíme ji pak explicitně stanovovat pomocí GLOBAL Symbol, případně pomocí PUBLIC nebo EXTERN.

Po opravě symbolů Next1, Next2, End1, End2 v knihovně libcvt32.asm na lokální přidáním tečky před jejich jméno by už měl jít program přeložit bez chyb a můžeme jej vyzkoušet s různými i delšími čísly a operacemi.

Vidíme, že načítání argumentů ze standardního vstupu funguje lépe než jejich odebírání z příkazového řádku programu. Stejně ale bylo třeba program ./CalcL32.x spouštět po každém příkladu znova, neboť jsme zatím nenaprogramovali přechod na nové zadání po každém úspěšném výpočtu. To snadno napravíme, stačí u návěstí End místo TerminateProgram zařadit skok na začátek, tedy End: JMP Start:. Anebo ještě lépe: nahradíme všechny skoky na End: skokem na Start:.

Další změnou bude náhrada volání kernelu Linux makrem StdInput z knihovny linapi.htm, která dělá v podstatě totéž a je snadno nahraditelné stejnojmenným makrem z knihoven pro jiné operační systémy.

Poslední věc k opravě předchozího zdrojového kódu jsou redundance ve výpočtu aritmetických operací Addition, Subtraction, Multiplication, Division: načtení prvního čísla MOV EAX,[Arg1] může být provedeno pouze jednou a pak použito pro všechny čtyři možné operace.
Instrukce

CALL IntegerToASCII StdOutput ESI JMP End:

se opakují, takže u druhé, třetí a čtvrté aritmetické operace je můžeme nahradit skokem na tu první. Zdrojový kód programu v souboru calc.asm se těmito zásahy zkrátí:

CalcL32 PROGRAM FORMAT=ELFX, WIDTH=32, ENTRY=Start: INCLUDE libcvt32.asm, linapi.htm ; Použij funkce z těchto knihoven. [.text] ; Takto se v EuroAssembleru označuje, že budou následovat strojové instrukce. Start: StdOutput Prompt ; Představení programu, výzva k zadání. StdInput Buffer ; Načtení zadání do proměnné Buffer. MOV ESI,Buffer Next1: LODSB ; Načtení dalšího znaku. CMP AL,0 ; Konec vstupního řetězce? JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next1: DEC ESI ; Vrácení ukazatele na platný znak. CALL ASCIItoInteger ; Procedura z knihovny libcvt32.asm. JC Error: MOV [Arg1],EAX ; Uložení prvního čísla. ADD ESI,ECX ; Umísti ESI za první načtené číslo. Next2: LODSB ; Načtení dalšího znaku CMP AL,0 ; Konec vstupního řetězce? JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next2: MOV [Arg2],AL ; Uložení operátoru. Next3: LODSB ; Načtení dalšího znaku CMP AL,0 JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next3: DEC ESI ; Vrácení ukazatele na platný znak. CALL ASCIItoInteger ; Procedura z knihovny libcvt32.asm. JC Error: MOV [Arg3],EAX ; Uložení druhého čísla. MOV EAX,[Arg1] ; Načteme první argument do EAX a pak se podíváme na operátor. CMP [Arg2],'+' JE Addition: CMP [Arg2],'-' JE Subtraction: CMP [Arg2],'*' JE Multiplication: CMP [Arg2],'/' JE Division: Error: StdOutput Help ; Pokud byl zadán neplatný operátor, vypíše se nápověda. TerminateProgram ; a program se ukončí. Addition: ADD EAX,[Arg3] Print: MOV ESI,Result CALL IntegerToASCII StdOutput ESI JMP Start: Subtraction: SUB EAX,[Arg3] JMP Print: Multiplication: MUL [Arg3] JMP Print: Division: SUB EDX,EDX DIV [Arg3] JMP Print [.data] ; Tady skončila sekce programového kódu a začínají data. Arg1 DD 0 Arg2 DD 0 Arg3 DD 0 Result DB 10 * B DB 10,0 Buffer DB 80 * B Prompt: DB 13,10,"%^PROGRAM: Enter two integer numbers separated with arithmetic operator + - * /.",13, 10, 0 Help: DB "Calculate number1 operator number2.", 13, 10 DB "Example: 3 + 4", 13, 10, 0 ENDPROGRAM

Máme tedy fungující kalkulátor calc.asm pro 32bitový Linux. Portace pro Windows 32 bitů je snadná: místo CalcL32 PROGRAM FORMAT=ELFX, WIDTH=32, ENTRY=Start: použijeme CalcW32 PROGRAM FORMAT=PE, WIDTH=32, ENTRY=Start: a místo INCLUDE libcvt32.asm, linapi.htm použijeme INCLUDE libcvt32.asm, winapi.htm. To je vše, ostatní instrukce se nemění a makra StdOutput, StdInput a TerminateProgram mění pouze svoje tělo, ale nikoli název.

O něco málo složitější to bude s portací programu pro 64 bitů. Zatímco v 32bitovém módu se ke práci s daty i s adresami používaly 32bitové registry, v 64bitovém módu se šířka adres zvětšuje na 64 bitů, zatímco defaultní šířka dat zůstává 32 bitů (i když se dá také zvětšit na 64 bitů použitím registrů začínajících na R). Dále je třeba si pamatovat, že zápis do 32bitového registru zároveň nuluje horních 32 bitů odpovídajícího 64bitového registru. Tedy SUB EAX,EAX nuluje nejen registr EAX, ale také horní (významnější) polovinu registru RAX.

Naše knihovna pro konverzi celých čísel na ASCII a zpět bude po portaci na 64 bitů vypadat mírně odlišně. Nazvěmě ji libcvt64.asm:

; Popis: Procedura ASCIItoInteger převádí řetězec ASCII číslic na binární celé číslo. ; Vstup: RSI obsahuje ukazatel na první znak převáděného čísla v paměti. ; Procedura načítá znaky dokud obsahují číslice nebo dokud nedojde k přetečení. ; Výstup:Carry Flag je nulový, EAX obsahuje převedené číslo v binárním tvaru. ; RCX obsahuje počet zpracovaných ASCII znaků ze vstupního řetězce. ; Chyba: Carry Flag je nastaven, pokud by vstupní číslo přesáhlo 32 bitů. EAX je nedefinován. ; RCX obsahuje počet zpracovaných ASCII znaků ze vstupního řetězce. ; Mění: RBX,RDX,RDI. ASCIItoInteger PROC ; Definování procedury. SUB EAX,EAX ; Akumulátor výsledku. SUB EBX,EBX ; Registr pro převod čísla. SUB ECX,ECX ; Délka vstupního čísla. MOV EDI,10 ; Násobitel. .Next: MOV BL,[RSI+RCX] ; Načtení RCX-tého znaku ze vstupního řetězce RSI. SUB BL,'0' ; Převod na binární hodnotu, pokud to byla číslice. JB .End: ; Skok, pokud to nebyla číslice. CMP BL,9 ; Kontrola na horní mez číslice. JA .End: ; Skok, pokud to nebyla číslice. INC ECX ; Započtení znaku do registru určujícího délku vstupního čísla. MUL EDI ; Vynásobení akumulátoru deseti. JC .Over: ; Skok při přetečení 32 bitů. ADD EAX,EBX ; Přičtení poslední číslice do akumulátoru. JMP .Next: ; Zpracování dalšího znaku. .End: CLC ; Návrat s vynulovaným příznakem CF. Binární číslo je v EAX. .Over: RET ; Návrat s nastaveným příznakem CF. Výsledek není definován. ENDPROC ASCIItoInteger ; Konec procedury. ; Popis: Procedura IntegerToASCII převádí binární číslo z registru EAX na řetězec ASCII číslic. ; Vstup: EAX obsahuje vstupní binární bezznaménkové číslo v rozsahu 0..4294967295. ; RSI obsahuje ukazatel na výstupní pole o velikosti 10 bajtů. ; Výstup:RSI je zvětšen o 0..9 bajtů a ukazuje na první platnou číslici výsledku. ; Chyba: K chybě nemůže dojít. ; Mění: RAX,RCX,RDX,RDI. IntegerToASCII PROC ; Definování procedury. MOV ECX,10 ; Počet míst ve výsledku. MOV EDI,10 ; Dělitel. .Next1:SUB EDX,EDX ; Vyšších 32 bitů vstupního čísla je před dělením EDX:EAX nutno vynulovat. DIV EDI ; EAX je teď podíl, EDX zbytek 0..9. ADD DL,'0' ; Převod DL na číslici '0'..'9'. DEC ECX ; Ukládání čísel zezadu. JS .End1: ; Skok při ukončení všech deseti číslic. MOV [RSI+RCX],DL ; Uložení číslice na konec výsledku. JMP .Next1: ; Zpracování další číslice. .End1: ; Pole výsledku nyní pro vstupní číslo 123 obsahuje "0000000123". ; Napozicujeme ESI na první nenulovou číslici výsledku. LEA RDI,[RSI+10] ; Zarážka, za níž už se nebude ESI číst. .Next2:CMP RSI,RDI ; Konec výstupního čísla? JAE .End2: ; Skok pokud ano, ESI bude ukazovat na poslední '0'. LODSB ; Načtení číslice výsledku z ESI, inkrementace ESI. CMP AL,'0' ; Je to začátek platných cifer? JE .Next2: ; Skok, pokud nikoli. .End2: DEC RSI ; Návrat ESI na první platnou číslici. RET ; Návrat z procedury. ENDPROC IntegerToASCII

Kód kalkulátoru pro 64bitový operační systém se změní jen nepatrně, neboť zůstaneme omezeni na 32bitová čísla a práce s neadresními registry zůstane stejná. Namísto 32bitových knihoven libcvt32.asm, linapi.htm, winapi.htm budeme načítat knihovny libcvt64.asm, linabi.htm, winabi.htm. Namísto MOV ESI,Buffer bude lepší použít LEA RSI,[Buffer]. Ostatní instrukce mohou zůstat stejné jako ve 32bitové variantě:

EUROASM CPU=x64 CalcL64 PROGRAM FORMAT=ELFX, WIDTH=64, ENTRY=Start: INCLUDE libcvt64.asm, linabi.htm ; Použij funkce z těchto knihoven. [.text] ; Takto se v EuroAssembleru označuje, že budou následovat strojové instrukce. Start: StdOutput Prompt ; Představení programu, výzva k zadání. StdInput Buffer ; Načtení zadání do proměnné Buffer. LEA RSI,[Buffer] Next1: LODSB ; Načtení dalšího znaku. CMP AL,0 ; Konec vstupního řetězce? JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next1: DEC RSI ; Vrácení ukazatele na platný znak. CALL ASCIItoInteger ; Procedura z knihovny libcvt64.asm. JC Error: MOV [Arg1],EAX ; Uložení prvního čísla. ADD RSI,RCX ; Umísti ESI za první načtené číslo. Next2: LODSB ; Načtení dalšího znaku CMP AL,0 ; Konec vstupního řetězce? JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next2: MOV [Arg2],AL ; Uložení operátoru. Next3: LODSB ; Načtení dalšího znaku CMP AL,0 JE Error: CMP AL,' ' ; Oddělující mezery je třeba přeskočit. JE Next3: DEC RSI ; Vrácení ukazatele na platný znak. CALL ASCIItoInteger ; Procedura z knihovny libcvt64asm. JC Error: MOV [Arg3],EAX ; Uložení druhého čísla. MOV EAX,[Arg1] ; Načteme první argument do EAX a pak se podíváme na operátor. CMP [Arg2],'+' JE Addition: CMP [Arg2],'-' JE Subtraction: CMP [Arg2],'*' JE Multiplication: CMP [Arg2],'/' JE Division: Error: StdOutput Help ; Pokud byl zadán neplatný operátor, vypíše se nápověda. TerminateProgram ; a program se ukončí. Addition: ADD EAX,[Arg3] Print: LEA RSI,[Result] CALL IntegerToASCII StdOutput RSI JMP Start: Subtraction: SUB EAX,[Arg3] JMP Print: Multiplication: MUL [Arg3] JMP Print: Division: DIV [Arg3] JMP Print [.data] ; Tady skončila sekce programového kódu a začínají data. Arg1 DD 0 Arg2 DD 0 Arg3 DD 0 Result DB 10 * B DB 10,0 Buffer DB 80 * B Prompt: DB 13,10,"%^PROGRAM: Enter two integer numbers separated with arithmetic operator + - * /.",13, 10, 0 Help: DB "Calculate number1 operator number2.", 13, 10 DB "Example: 3 + 4", 13, 10, 0 ENDPROGRAM

Portace z 64bitového Linuxu na Windows je opět snadná: nahradíme knihovnu linabi.htm knihovnou winabi.htm, název a formát programu změníme z CalcL64 PROGRAM FORMAT=ELFX na CalcW64 PROGRAM FORMAT=PE a to je vše. Získáme tím kalkulátor pro 64bitový Windows.

Hledání chyb

Hledání a odstraňování chyb programu — ladění — je podstatnou náplní práce programátora, v asembleru to platí dvojnásob. Ukážeme si několik metod, jak chyby hledat.

Hledání chyb kontrolními výpisy

V předchozích příkladech jsme pro kontrolu použili vypsání zadaných argumentů makrem StdOutput. To je docela dobrá metoda jak se ujistit, že procesor probíhá těmi zákoutími našeho programu, kde ho očekáváme. Na prvním místě dejme výpis StdOutput hned na začátek, kam ukazuje vstupní bod programu ENTRY=. Tím se ujistíme, že jsme nechali náš program načíst správnou knihovnu a že výpis na standardní výstup funguje. Text vypisovaný pomocí StdOutput nemusí být nijak sofistikovaný, stačí aby sdělil, že program dospěl do určitého bodu. Například StdOutput ="Jsem na řádku 123",Eol=Yes.

Hledání chyb makrem Debug

Na mnoha místech programu by bylo užitečné kromě informace, kde zrovna jsme, vidět také obsah určitého registru nebo paměťového místa. Mohli bychom sice obsah kteréhokoli registru převést do dekadického nebo hexadecimálního ASCII tvaru, uložit jej do nějaké dočasné paměťové proměnné a pak ji vypsat pomocí StdOutput, ale to je nepohodlné řešení náchylné k chybám. Lepší bude použít specializované makro, které dělá totéž pro všechny registry a nezanechává v laděném programu žádné stopy (kromě zvětšení jeho délky).

K ladění je k dispozici knihovna maclib/debug.htm a obsahuje jediné makro Debug, nezávislé na operačním systému a na šířce laděného programu (16, 32 nebo 64 bitů). Nezávislost na OS je vykoupena tím, že makro Debug informaci o registrech nevypisuje samo, ale posílá naformátovaný výpis do procedury s defaultním názvem DebugOutput volané z makra jako tzv. callback. Tuto proceduru musíme v laděném programu dočasně použít, naštěstí to není nic složitého. DebugOutput má za úkol vypsat na standardní výstup řetězec adresovaný registrem rSI v délce rCX bajtů. Můžeme zde k výpisu použít dobře známé makro StdOutput.

Příklady procedury DebugOutput pro různé šířky programu:

DebugOutput PROC ; 16bitový program. StdOutput SI,Size=CX RET ENDPROC DebugOutput DebugOutput PROC ; 32bitový program. StdOutput ESI,Size=ECX RET ENDPROC DebugOutput DebugOutput PROC ; 64bitový program. StdOutput RSI,Size=RCX RET ENDPROC DebugOutput

Postup pro ladění pomocí makra Debug:

Výpis stavu počítače po každém použití makra Debug vypadá v 32bitovém programu nějak takto:

#### Debug at "filename.ext"{1234) CS=0000 DS=0000 ES=0000 SS=0000 GPR: FL=0000_0000 NT=0 PL=0 OF=0 DF=0 IF=0 TF=0 SF=0 ZF=0 AF=0 PF=0 CF=0 EAX=0000_0000 ECX=0000_0000 EDX=0000_0000 EBX=0000_0000 FS=0000 GS=0000 ESP=0000_0000 EBP=0000_0000 ESI=0000_0000 EDI=0000_0000 EIP=0000_0000

Makro na začátku vypsalo svou pozici ve zdrojovém kódu "filename.ext"{1234), tedy název souboru a ve složených závorkách číslo řádku, a pak obsah všech GPR. Vložení makra do programu jej nijak neovlivňuje, všechny registry i příznaky zůstávají zachovány.

Debug umí vypsat také obsah jednoho paměťového místa, jehož adresu a velikost specifikujeme parametrem makra. Například Debug ESI, Size=32 vypíše kromě obsahu GPR také obsah paměti na adrese ESI v délce 32 bajty. Parametr Size= je v rozsahu 1..256, defaultní hodnota je 16. U chráněných OS je třeba dbát, aby specifikovaná paměť skutečně existovala (byla programu alokována), jinak může dojít k havarijnímu ukončení.

Hledání chyb ladicí aplikací

Mnohem pohodlnější přístup k ladění poskytují specializované ladicí aplikace — debuggery.
Pro Linux je k dispozici konzolová aplikace gdb, případně její grafická nadstavba nemiver nebo ddd.

Nejvíce jsem si oblíbil tyto:

Pro DOS Turbo Debugger dodávaný firmou Borland spolu s Turbo Assemblerem nebo Turbo Pascalem.
Pro 32bitový Windows OllyDbg,
pro 64bitový Windows x64dbg. Na těchto celoobrazovkových aplikacích je sympatické, že jejich autoři od sebe opsali jednotné základní ovládání pomocí funkčních kláves F4, F7, F8 apod. Nejčastěji budeme využívat základní obrazovku CPU, která, rozdělena na čtyři části, zobrazuje strojové instrukce, obsah registrů, obsah paměti a zásobník. Krokování programu (F7) s případným přeskakováním detailního procházení procedur (F8) umožňuje po každém kroku sledovat, jak se změnily registry, paměť programu a zásobník.

Debuger ukazuje v levé horní čtvrtině okna CPU adresy a disasemblovaný strojový kód. Adresy odpovídají tomu, jaké jim byly přiděleny linkerem, vidíme je jako hexadecimální čísla. Bylo by užitečné vidět na jejich místě raději návěstí použité ve zdrojovém programu. Toto propojení číselné adresy se symboly se ale bohužel neděje automaticky, ani když je ve spustitelném souboru přítomna tabulka symbolů s jejich adresami. Chceme-li znát adresy, jaké EuroAssembler přidělil symbolům, podívejme se do listingu. Pokud jsme to ponechali povoleno volbou PROGRAM LISTGLOBALS=ON, na konci listingu calc.asm.lst uvidíme u globálních symbolů jejich virutální adresu (VA=). Pokud bychom potřebovali znát adresu dalších, neglobálních symbolů, musíme jim přidělit globální viditelnost. To by šlo přidáním explicitní pseudoinstrukce GLOBAL Division, Error, Multiplication atd., ale ještě jednodušší bude na konec názvu každého návěstí přidat dvě dvojtečky. Po novém přeložení už v listingu jejich adresy uvidíme:

... | **** ListGlobals "CalcL64.x",Global=0,Public=12,Extern=0,eXport=0,Import=0 | Division,[.text]:00000139h,VA=00401629h,scope='P' | Error,[.text]:000000EBh,VA=004015DBh,scope='P' | LinABI@RT,[.text]:000001C9h,VA=004016B9h,scope='P' | Multiplication,[.text]:00000131h,VA=00401621h,scope='P' | Next1,[.text]:0000007Dh,VA=0040156Dh,scope='P' | Next2,[.text]:00000099h,VA=00401589h,scope='P' | Next3,[.text]:000000A8h,VA=00401598h,scope='P' | Print,[.text]:0000010Eh,VA=004015FEh,scope='P' | Start,[.text]:00000050h,VA=00401540h,scope='P' | StdInputLin64@RT,[.text]:00000192h,VA=00401682h,scope='P' | StdOutputLin64@RT,[.text]:00000141h,VA=00401631h,scope='P' | Subtraction,[.text]:00000129h,VA=00401619h,scope='P'
OllyDebugger pro 32bitový Windows dokonce uměl načítat adresy z tabulky symbolů v objektovém souboru COFF, pomocí klávesové zkratky Ctrl-O. My sice překládáme zdrojový kód přímo do spustitelného souboru, bez mezistupně v podobě COFF souboru, ale mohli bychom dočasně změnit PROGRAM FORMAT=PE na PROGRAM FORMAT=COFF a objektový formát COFF pak vygenerovat a použít pro načtení adres symbolů OllyDebuggerem.

▲Zpět na začátek▲