Assembler (Jazyk symbolických adres)

(1. část)

Assembler je jazyk, ve kterém se píší přímo instrukce pro procesor. Překladem vznikne spustitelný kód (např. EXE soubor), ve kterém je Assembler ve formě strojového kódu (binární kód), který vykonává procesor. Protože tvorba programů ve dvojkové (šestnáctkové) soustavě je prakticky nemožná, zavedl se symbolický zápis instrukcí. Možná se zamýšlíte nad tím, proč Assembler používat, když jsou tu vyšší programovací jazyky, jako např. C nebo Pascal. Důvodů je několik. V žádném vyšším programovacím jazyce nelze napsat program tak, aby byl tak rychlý, jako napsaný v Assembleru. Zároveň takový program v Assembleru zabírá nejméně místa. Nespornou výhodou je také to, že máte absolutní kontrolu nad tím, co program dělá. Samozřejmě i Assembler má nějaké nevýhody: kód je velmi dlouhý, těžko se hledají chyby. Protože tvorba složitějších programů jen v Assembleru by byla zdlouhavá, program se vytváří ve vyšším programovacím jazyce, a v Assembleru se píší jen ty jeho části, které se často opakují, jejich tvorba není náročná a potřebujeme je co nejrychlejší. Tyto bloky se nejlépe programují v tzv. vloženém Assembleru.

Programátorský model mikroprocesoru

Obvod Intel 8086 je univerzální šestnáctibitový mikroprocesor. Je schopen provádět operace s šestnáctibitovými čísly. S okolím komunikuje po šestnáctibitové datové a dvacetibitové adresové sběrnici, z čehož vyplývá, že je schopen adresovat 1MB paměti. Od tohoto mikroprocesoru je odvozena spousta novějších, byly přidávány nové instrukce a tím pádem nové vlastnosti a možnosti.
Mikroprocesor 80286 je strukturou i vlastnostmi podobný 8086. Je schopen pracovat ve dvou režimech. V základním reálném téměř přesně simuluje obvod 8086, v chráněném (protected) módu je schopen adresovat paměť i nad 1MB.
Procesor 80386 už je 32-bitový, má zdvojnásobené registry a to mu umožňuje adresovat až 4GB paměti.
Procesor 80486 dokáže spracovávat až 4 instrukce zároveň (fázováním instrukcí - pipelining).
Pentium může zpracovat více instrukcí v jednom cyklu, Pentium MMX má zase zdvojené registry (64-bitové) a multimediální instrukce, další jsou Pentium Pro, Pentium II, Pentium III, Pentium IV.

My si vystačíme s 32-bitovým procesorem 80386, další nové instrukce nebudeme využívat.

Registry

Registr je speciální paměťové místo přímo v procesoru. Práce s nimi je tedy mnohem rychlejší než s pamětí. Procesor v nich uchovává hodnoty, s kterými právě pracuje.

Mikroprocesor musí být schopen pracovat i se vstupy-výstupy. Umístění jednotlivých portů určuje šestnáctibitová adresa umístěná nejčastěji v registru DX.

Vkládaný (inline) Assembler v jazyce C (C++)

Vkládaný (inline) Assembler je blok v programu psaném v jazyce C (C++). Překladače (nebo vývojová prostředí) se mohou v zápisu bloku Assembleru lišit. V OS M$ Windows budeme programovat ve vývojovém prostředí M$ Visual C++. V tomto prostředí je tento blok uvozen klíčovým slovem _asm (nebo __asm) a je uzavřen do závorek ohraničujících blok { }. Řádky programu ve vkládaném Assembleru nemusí končit středníkem v případě, že na jednom řádku není více jak jedna instrukce (při více jak jedné instrukci musíme instrukce středníkem oddělit). Komentáře se píší stejně jako v C (C++) nebo za středník. V bloku a instrukcích lze použít jména proměnných, funkcí, konstant, typů, aj. z externího C (C++) kódu. Ve vkládaném Assembleru se standardně nerozlišuje velikost písmen.

Velmi užitečný nástroj je Disassembler. Ten nám ukáže všechny instrukce programu, tak, jak je vykonává procesor. Disassembler je tak vlastně opak Assembleru (jako překladače jazyka do binárního kódu stroje), převádí binární kód stroje zpět do symbolického jazyka. V M$ Visual C++ spusťte debugování pomocí F10 a pomocí Alt+8 se zobrazí okno Disassembleru, ve kterém vidíte instrukce programu.

Př. Napište v M$ Visual C++ nějaký jednoduchý program a podívejte se, jak je zapsaný pomocí instrukcí.

Instrukce přesunů dat

Instrukce má tvar: jmeno_instrukce operand1, operand2, ....
Počet operandů bývá 0 až 3.

Každý program musí být schopen přesunů dat a to mezi registry, registry a pamětí, registry a vstupy/výstupy. Při této operaci si musíme vždy uvědomit, kolikabitové číslo přesouváme. Počet bitů je většinou specifikován jménem použitého registru. V případě, že používáme jen paměť, specifikuje počet bitů pro operaci označení:

Přesuny registr - registr, registr - paměť

Všechny přesuny tohoto typu provedeme univerzální instrukcí: Použití této instrukce demonstruje příklad:
      #include <stdio.h>

      unsigned char slabika; // v paměti rezervuj 8 bitů
      unsigned short slovo; // v paměti rezervuj 16 bitů
      unsigned long dvojslovo; // v paměti rezervuj 32 bitů

      int main()
      {
      _asm {
      mov al,100        // do registru AL dosaď 8 bitů, hodnotu 100
      mov slabika,al    // do paměti na místo ozn. slabika dosaď obsah AL
      mov bx,0x200      // do registru BX (16 bitový) dosaď 0x200
      mov slovo,bx      // do paměti na místo ozn. slovo dosaď 16 bitů BX
      mov ecx,0xABCDEF  // do registru ECX (32 bitový) dosaď 0xABCDEF
      mov dvojslovo,ecx // do paměti na místo ozn. dvojslovo dosaď 32 bitů ECX
      }
      printf("%u %X %X\n", slabika, slovo, dvojslovo); 
      return 0;
      }
    
Pozor! Instrukce MOV nikdy nemění hodnoty žádných flagů.

Př. Zkuste si tento první příklad přeložit a spustit.

Metody adresace

Místo v paměti označuje hodnota (adresa, přímo nebo v registru), která musí být zapsaná v hranatých závorkách.

Pozor! V jedné istrukci můžete pouze jedinkrát adresovat paměť. To znamená, že např. příkaz a=33; zapíšete normálně jako MOV a,33, ale naopak a=b; takto jednoduše napsat nejde. Byla by zde dvojí adresace v jedné instrukci.

Otázka: Jaké instrukce provedou a=b?

Assembler umožňuje dvě metody adresace.

Přímá adresa

Přímé adresování je takové, kde jsou adresy známé přímo při překladu, tzn. při práci se statickými nebo globálními proměnnými (jazyka C).

MOV AH, [0x1A40] - do registru AH předej 8 bitů z adresy určené číslem
Tuto metodu použijeme, jestliže předem víme adresu hledaného místa v paměti.
      static short thevar, thevar2; // proměnné thevar a thevar2 jsou v paměti za sebou
      _asm {
      mov ax,thevar            ;ax=thevar - do registru ax vlož 16-bitovou hodnotu z paměti na místě ozn. thevar 
      mov ax,thevar+2          ;ax=thevar+2=thevar2 - do registru ax vlož 16-bitovou hodnotu z paměti na místě thevar+2=thevar2
      }
    
Jméno proměnné se tedy vyhodnocuje jako přímá adresa (protože překladač zná adresu hodnoty proměnné v paměti), je to jen textové označení adresy hodnoty proměnné. Instrukce MOV ax,thevar je přepsána na MOV ax, [adresa_hodnoty_promenne_thevar]. S proměnnými jako adresami ještě můžeme provádět základní aritmetické operace.
      _asm {
      mov ax,thevar         ;ax=[adresa_hodnoty_thevar]
      mov ax,[thevar]       ;ax=[[adresa_hodnoty_thevar]]=[adresa_hodnoty_thevar]
      }
    
Pokud je thevar statická nebo globální proměnná, pak oba tyto příkazy dělají totéž, čili do AX se přiřadí hodnota proměnné thevar.

Nepřímá adresa

V C (C++) toto adresování odpovídá ukazatelům na hodnoty. Zatímco u přímého adresování známe přesnou adresu hodnoty proměnné již při překladu, u nepřímého adresování máme tuto adresu hodnoty proměnné v jiné proměnné typu ukazatel na hodnotu, čili hodnota proměnné typu ukazatel na hodnotu (např. velikosti byte) je adresa (32-bitová) této hodnoty (velikosti byte) v paměti. Obecný tvar nepřímě adresy je [mem+reg1+reg2*size] (ekvivalentní zápis je mem[reg1][reg2*size]), kde size je konstanta (může být pouze 1, 2, 4, 8), reg1, reg2 libovolné 32-bitové registry a mem je přímé zádání adresy v paměti (tj. číslo), všechny položky jsou nepovinné.
      char *thepointer; // 32-bitová proměnná typu ukazatel na 8-bitovou hodnotu

      _asm {
      mov ebx,thepointer     ;ebx=thepointer - přímá adresace
      mov esi,2              ;esi=2
      mov al,[ebx+esi]       ;al=*(thepointer+2)=thepointer[2] - nepřímá adresace
      mov ah,thepointer[esi] ;ah=[adresa_hodnoty_thepointer][esi] - ??
      }
    

Otázka: Je poslední příkaz platný? Proč?

Př. Přepište příklad demostrující instrukci MOV (první, kompletní, příklad) tak, aby proměnné byly ukazatele na původní datové typy. Potom jej přepište tak, aby proměnné byly ukazatele na tyto ukazatele. Při obou úpravách samozřejmě musíte upravit i blok Assembleru. Cílem je vyzkoušet si, zda rozumíte přímé a nepřímé adresaci.

Práce se zásobníkem

Zásobník je část paměti počítače vyhrazená k ukládání dat potřebných pro samotný chod programu, pokud již nestačí nebo nechceme použít registry procesoru. Je organizovaná tak, že data, která jsou uložena naposledy, vyjímáme jako první (princip LIFO). Na vrchol zásobníku ukazuje adresa uložená v registru ESP (SP), na dno pak adresa v EBP (BP). Přidáváním dat do zásobníku se ESP automaticky snižuje o velikost vkládaného registru nebo čísla (a naopak), čili pokud uvažujeme adresový prostor rostoucí směrem vzhůru, pak je zásobník obrácen dnem nahoru a roste směrem dolů (k menším adresám). Do zásobníku můžeme odkládat jen 32-bitová data. Pro práci se zásobníkem slouží instrukce:
      #include <stdio.h>

      unsigned short promenna;

      int main()
      {
      promenna=10;
      _asm {
      mov ax, promenna // obsah proměnné dosaď do registru AX
      mov ebx,0xBBBB     // do regisru BX dosaď číslo
      push ax          // ulož obsah AX
      push ebx          // ulož obsah BX
      mov ax,0xAAA     // přepiš obsah AX
      mov ebx,0xCCCC     // přepiš obsah BX
      pop ebx           // obnov obsah BX
      pop ax           // obnov obsah AX
      mov promenna, ax // vrať obsah AX do proměnné
      }
      printf("%u\n", promenna);
      return 0;
      }
    

Na zásobníku jsou uloženy i lokální proměnné procedur a funkcí. Jsou zde i parametry, s kterými je podprogram volaný.

U registrů, které program používal před blokem Assembleru a které používáme v tomto bloku my, už překladač nemůže předpokládat jejich původní hodnoty, protože jsme je mohli přepsat. Proto by jsme správně měli hodnoty všech používaných registrů hned na začátku bloku uložit (na zásobník - PUSH), a na konci bloku pak obnovit (POP). Pozor!. Velikosti uložených dat na zásobník musí přesně odpovídat velikost dat vyjmutých ze zásobníku, jinak se program ve většině případů zhroutí. Prakticky to znamená, že data ze zásobníku vybíráme v přesně opačném pořadí, než v jakém jsme je tam uložili. V M$ Visual C++ můžete ve vkládaném Assembleru libovolně používat registry EAX, EBX, ECX, EDX, ESI, EDI. Překladač sám přidá potřebné instrukce PUSH a POP na začátek a konec bloku.

Otázka: Jakými instrukcemi lze nahradit instrukce PUSH a POP?

Př. V modifikovaném příkladu demostrujícím instrukci MOV uschovejte (a obnovte) všechny používané registry do (ze) zásobníku.



Jan Outrata
outrataj@phoenix.inf.upol.cz