Assembler (Jazyk symbolických adres)

(8. část)

Externí (external) Assembler

Vkládaný (inline) Assembler má podobu bloku instrukcí v programu psaném v nějakém vyšším jazyku (např. C/C++). Externí (external) Assembler je programovací jazyk, ve kterém se píší části programů nebo celé programy nezávisle na jiném vyšším jazyku. Zdrojový kód v externím Assembleru se píše do samostatných souborů nejčastěji s příponou .asm.

Jelikož je externí Assembler samostatný programovací jazyk, musí k němu samozřejmě existovat nějaký překladač. Pro vývojové prostředí M$ Visual C++ existuje překladač Macro Assembler (MASM) (nemusí být standardní součástí M$ Visual Studia, je dodáván v M$ Windows Driver Development Kit (DDK)). Do projektu potom můžeme jednoduše přidat soubor ASM (obyčejný textový soubor s příponou .asm) a do něj psát zdrojový kód externího Assembleru. Pro překlad tohoto souboru může být potřeba explicitně nastavit překladač, kterým se má soubor přeložit, pokud není vývojovým prostředím standardně nastaven. V nastavení projektu (Project Settings nebo Solution Explorer, Properties) je potřeba u souboru externího Assembleru na záložce vlastního překladu (Custom Build, General) nastavit příkazy překladu (Commands nebo Command Line) na ml /c /Cx /Fo$(OutDir)\$(InputName).obj $(InputPath) a výstupy (Outputs) na $(OutDir)\$(InputName).obj.

Struktura zdrojového souboru externího Assembleru

Syntaxe tohoto souboru je téměř shodná se syntaxí vkládaného Assembleru, instrukce se píší každá na samostatný řádek, komentáře za středník. Velikost písmen se opět nerozlišuje.

Do zdrojového souboru externího Assembleru se nepíší jenom instrukce (a navěští, instrukční prefixy a ostatní věci známé z vkládaného Assembleru), ale také příkazy pro překladač, tzv. direktivy. Pomocí direktiv se také vymezují části zdrojového textu pro data a kód (instrukce). Struktura souboru je dána použitým překladačem, u různých překladačů jsou v ní drobné odlišnosti. Nicméně jakousi základní kostru zdrojového souboru lze použít pro většinu překladačů (včetně MASM):

.386 ; procesor
.model small, C ; paměťový model, jazyk

.const

; deklarace a inicializace konstant

.data

; deklarace a inicializace proměnných

.data?

; deklarace neinicializovaných proměnných

.code

; kód (instrukce) programu

end

Direktiva procesoru, která musí být (aspoň pro MASM) uvedena jako první, udává, že následný kód v Assembleru (instrukce) je předpokládán a výsledný přeložený binární kód je určen pro tento procesor a novější (nemusí tedy fungovat na starších procesorech, např. kvůli novější instrukci v kódu).

Direktiva .model určuje tzv. paměťový model. V 16-bitovém prostředí se rozlišuje několik paměťových modelů podle velikosti programu a paměti, kterou potřebuje pro data. Podle toho, jestli jsou kód a data (plus zásobník) programu v jednom segmentu, obojí ve svém vlastním segmentu nebo některé ve více segmentech paměti, rozlišujeme paměťové modely tiny, small, medium, compact, large a huge. V externím Assembleru lze použít všechny kromě modelu huge, blíže viz literatura. Ve 32-bitovém prostředí lze ale jednoduše adresovat celou paměť a segmentace není potřeba (transparentně pro program ji řeší OS). Situace zde odpovídá modelu small ze 16-bitového prostředí, někdy zde také nazývaného flat (celá paměť), proto je tento model v 32-bitovém prostředí jediný používaný (a možný).

Uvedením jazyka (za paměťovým modelem) specifikujeme, že hodláme přeložený binární kód spojit (slinkovat) s kódem přeloženým z onoho jazyka, nejčastěji jazyka C. Je to z důvodu stejného překladu názvů funkcí, procedur (a jejich parametrů) aj. tak, aby byly binární kódy spojitelné. S jazykem C++ Assembler spolupracovat neumí, ale funkce, procedury aj. tohoto jazyka lze exportovat ve formě jazyka C pomocí extern "C".

Ostatní direktivy, .const, .data, .data? a .code uvozují části textu pro konstanty, inicializovaná a neinicializovaná data a kód (instrukce) programu. Část pro neinicializovaná data se u některých překladačů označuje .bss a část pro kód .text. Povinná je jen část pro kód .code (.text). Podle velikosti těchto částí a hlavně potom podle uvedeného paměťového modelu se nastavují segmentové registry.

Podobně jako u vyšších prog. jazyků (např. C) lze do zdrojového kódu vložit kód z jiného souboru (include), do kódu externího Assembleru lze vložit kód z jiného souboru také, a to pomocí direktivy INCLUDE: INCLUDE soubor.asm vloží na místo uvedení direktivy obsah souboru soubor.asm.

Konstanty a proměnné

Deklarace konstant a proměnných lze uvádět pouze v částech uvozených direktivami .const, .data nebo .data?, tedy ne v části pro kód uvozenou .code.

Konstanty se deklarují v části uvozené direktivou .const. První způsob definice konstanty je podobný jako #define v jazyce C, tedy se chová jako jednoduché makro a hodnotou může být libovolný text (tedy nejen číselná hodnota, ale i např. alias pro jméno jiné konstanty, proměnné, návěští nebo i instrukce, atd.). Používá se k tomu klíčové slovo EQU (pro číselné konstanty lze použít i rovnítko =): jméno_konstanty EQU hodnota (jméno_číselné_konstanty = číslo). Takto nadefinovanou konstantu (pomocí EQU) již nelze změnit (číselné konstanty definované pomocí = měnit lze)! Na druhou stranu ale lze pro definici konstant použít i dříve nadefinované konstanty, např. číselné konstanty ve výrazech, kde dochází k vyhodnocení konstanty a celého výrazu již při deklaraci (při překladu)! Vyhodnocení výrazu lze zabránit jeho uvedním mezi znaky < a > (hodnotou konstanty pak bude samotný výraz jako text). Vše si ukážeme na příkladech:

A = 6 ; číselná konstanta A = 6
B EQU A - 2 ; číselná konstana B = 4 (6 - 2), dochází k vyhodnocení výrazu
C EQU <A - 2> ; obecná (textová) konstanta C = A - 2 (hodnotou je text A - 2, vyhodnocení výrazu je potlačeno)

Druhý způsob deklarace konstant je stejný jako deklarace proměnných, viz následující odstavec.

Deklarace proměnných se uvádějí v částech uvozených direktivami .data nebo .data? a mají tvar: jméno_proměnné velikost hodnota, hodnota, .... Jelikož hodnot může být u jednoho jména uvedeno více, představuje toto jméno vlastně pouze označení adresy (navěští) těchto hodnot a proměnnou v tomto smyslu si lze představit jako pole hodnot označené jménem proměnné. Velikost může být slabika (BYTE) - DB, slovo (WORD) - DW, dvojslovo (DWORD) - DD nebo i dvě dvojslova (čtyři slova, QWORD) - DQ. Při inicializaci proměnné (paměti) ji uvedeme v části .data a hodnotou je číslo, v případě slabiky (DB) to může i znak (v apostrofech ') nebo řetězec (více znaků v uvozovkách ", v paměti pouze tyto znaky, bez 0 na konci!). U dvojslova (DD) může být jako hodnota uvedena i jiná proměnná nebo návěští, v tomto případě je hodnotou proměnné adresa uvedené proměnné nebo návěští v paměti. Místo přímého uvedení číselné hodnoty můžeme zadat i matematický výraz, který lze již při překladu vypočítat na hodnotu, podobně jako u konstant. Pokud proměnnou nechceme inicializovat, uvedeme ji v části .data? a jako hodnotu zadáme ? (otazník), přitom více otazníků znamená více neinicializovaných hodnot. V kódu se potom na konstantu nebo proměnnou (paměť) odkazujeme jednoduše uvedením jejího jména spolu s klíčovým slovem OFFSET: OFFSET jméno.

Příklad deklarace konstant a proměnných:
.const

pocet = 5
konstanty DB 1, 10, 100 ; 3 konstantní slabiky označené konstanty

.data

thevar DD 200h ; proměnná thevar (místo paměti označené thevar) velikosti dvojslova (4B)
retezec DB "Chyba programu!", 0 ; řetězec i s 0 na konci (jako v jazyce C)
znaky DB 'A', 'B', 'C'
kolikrat DQ pocet + 100 ; kolikrat = 105
thevar_addr DD thevar ; hodnotou proměnné thevar_addr je adresa proměnné thevar

.data?

i DW ?, ? ; 2 neinicializovaná slova označená i

Pole hodnot můžeme vytvořit i jednodušeji (zvláště u rozsáhlejších polí) pomocí konstrukce kolik_položek_s_hodnotami DUP (hodnoty) uvedené místo hodnot proměnných. S takto vytvořenou proměnnou pracují direktivy LENGTH a SIZE, ale podrobnosti už necháme zájemcům. Příklady:

prazde_pole DB 10 DUP (?) ; neinicializované pole 10-ti slabik
pole_dvojic DW 5 DUP (0, 1) ; pole s hodnotami 0, 1, 0, 1, .... (5x 0, 1)
Otázka: Je možné aplikovat DUP rekurzivně, např. pro vytváření vícerozměrných polí? Př. pole3d DB 3 DUP (4 DUP (5 DUP (?))). Zkuste.

Na závěr části o konstantách a proměnných uvedeme, že v externím Assembleru je možné deklarovat i složitější (netriviální) datové typy známé z vyšších prog. jazyků, jako výčtový typ (ENUM), struktura (STRUC, UNION), záznam (RECORD) ale to už přesahuje rámec textu.

Podprogramy

V části kódu programu .code lze blok instrukcí označit jako podprogram (funkci nebo proceduru) a pak tento blok volat z jiných částí. Syntaxe podprogramu:

jméno_podprogramu PROC jméno_parametru:typ, jméno_dalšího_parametru:typ, ...
 ...
 instrukce (tělo podprogramu)
 ...
jméno_podprogramu ENDP

Jméno podprogramu uvedené před klíčovými slovy PROC a ENDP je samozřemě stejné. Typ parametrů (velikost) může být BYTE, WORD nebo DWORD pro slabiku, slovo nebo dvojslovo.

Příklad funkce:
pyth PROC a:DWORD, b:DWORD
  PUSH EBX
  MOV EAX, a
  MOV EBX, b
  IMUL EAX, a
  IMUL EBX, b
  ADD EAX, EBX
  POP EBX
  RET           ; NUTNÉ!!!
pyth ENDP 

Volání podprogramu (ukládání parametrů do zásobníku, volání, návratová hodnota) je úplně stejné jako u vkládaného Assembleru. U bloku vkládaného Assembleru si překladač vyššího jazyka uloží všechny námi v bloku použité registry na zásobník, nebo jednoduše použije dvojici instrukcí PUSHAD/POPAD. U podprogramu v externím Assembleru si musíme stav registrů uložit sami! Místo instrukcí k tomu můžeme použít direktivu USES uvedenou za klíčovým slovem PROC před parametry, např. USES EAX EBX. Pozor! Instrukci RET musíme na konci podprogramu (jako jeho poslední instrukci) vždy uvést!!

Lokální proměnné (místo na zásobníku) si můžeme vytvořit známým postupem uvedeným v dřívější části věnované podprogramům. Jednodušší je ale použít direktivu LOCAL: LOCAL jméno:typ, další_jméno_typ, .... Tato deklarace musí být uvedena hned na začátku podprogramu!

Veřejné (public) a externí (extrn) deklarace

V jazyce C (C++) jsou standardně všechna jména funkcí, proměnných, atd. veřejná (public), čili jsou přístupná i z ostatních zdrojových a přeložených binárních souborů. V (externím) Assembleru je tomu naopak. Standardně jsou všechna jména lokální (jako static v C), čili zvenčí nepřístupná. Aby bylo možné volat funkce nebo používat proměnné napsané ve zdrojovém souboru v Assembleru, musíme jejich jména zveřejnit deklarací PUBLIC. Zveřejnění všech jmen se většinou uvádí buď hned na začátku kódu programu anebo těsně před deklarací funkce nebo proměnné.

Externí jména (tj. jména funkcí, proměnných, atd. definovaných v jiném zdrojovém nebo binárním souboru, která chceme v Assembleru použít) deklarujeme pomocí klíčového slova EXTRN (jen jedno E!). Za jméno se po dvojtečce uvádí typ (velikost) proměnné (BYTE, WORD, DWORD, QWORD) nebo klíčové slovo PROC pro externí deklaraci podprogramu.

Příklady veřejných a externích deklarací:
PUBLIC pyth    ; zveřejnění funkce pyth
PUBLIC thevar  ; zveřejnění proměnné thevar typu DWORD
PUBLIC retezec ; zveřejnění řetězce retezec

EXTRN promenna:WORD ; externí deklarace proměnné promenna typu WORD
EXTRN func:PROC     ; externí deklarace funkce func

Makra

Problematika maker v externím Assembleru je velmi složitá, zde bude předveden jen základní úvod, zájemci si další vyhledají sami. Princip maker je úplně stejný jako v ostatních jazycích: definují kus kódu, který může mít parametry a který se při překladu dosadí (na textové úrovni) na místo použití makra (a přeloží). Jinak řečeno je to pojmenovaný parametrizovaný blok kódu, který je uveden pouze na jednom místě a lze jej dosadit na více místech kódu (pomocí jeho jména). Syntaxe makra je podobná syntaxi podprogramu:

jméno_makra MACRO jméno_parametru, jméno_dalšího_parametru, ...
 ...
 instrukce (tělo makra)
 ...
ENDM

Makro lze definovat kdekoliv v kódu programu, tedy nejen v části uvozené direktivou .code, ale i v částech za direktivami .data nebo .const. Použít jej lze samozřejmě až za definicí. Použití makra (makra se nevolají jako podprogramy!) se provede jednoduše zapsáním jeho jména s parametry: jméno_makra skutečný_parametr, další_skutečný_parametr, ..., jakoby to byla instrukce. Při dosazení kódu makra se parametry uvedené v jeho definici nahradí skutečnými parametry uvedenými za jménem v zápisu použití makra. Parametrem makra může být jméno jiného makra, tímto způsobem lze vytvářet opravdu krkolomné kódy! Odladění takového kódu je ale (na rozdíl od ladění maker ve vyšších prog. jazycích, např. C) jednoduché, protože překladač generuje debugovací informace i pro rozvinutá makra.

Více informací o makrech v externím Assembleru (např. zrušení platnosti, ukončení rozvíjení při použití, komentáře v makrech) již čtenář nalezne v literatuře. Zde uvedeme jen příklad jednoduchého makra:

; definice makra
prohod MACRO a, b, x
  ;; makro nefunguje, pokud lib. 2 parametry jsou odkazy do paměti
  PUSH x
  MOV x, a
  MOV a, b
  MOV b, x
  POP x
ENDM

; použití makra
MOV AX, 10
prohod AX, WORD PTR promenna, BX
... další instrukce ...

Kromě maker umožňuje externí Assembler také podmíněný překlad známý z jiných jazyků. K tomu jsou určeny direktivy podmíněného překladu, jako např. IF, ELSE, IFDEF, IFB. Podrobnosti už necháme zájemcům.

Celý program v externím Assembleru

V předchozích odstavcích bylo popsáno, jak v externím Assembleru zapsat data (konstanty, proměnné) a podprogramy (funkce, procedury), využívané z jazyka C (C++). Ve vývojovém prostředí M$ Visual C++ můžeme mít většinu programu (téměř celý) zapsánu ve zdrojových souborech externího Assembleru, ale základ programu je stále v alespoň jednom zdrojovém souboru jazyka C (C++). Minimálním základem programu jazyka C je funkce main. Tato základní funkce (funkce celého programu) je ale také jenom funkce, můžeme ji tedy přepsat do externího Assembleru jako ostatní. Takto bychom mohli skončit s prázdným zdrojových souborem jazyka C. Zbývá jej tedy z projektu vyřadit a dostat tak program zapsaný celý jen v externím Assembleru. To ale bohužel nelze. Aby bylo možné vytvořit ve vývojovém prostředí M$ Visual C++ fungující program, musí být v projektu alespoň jeden zdrojový soubor jazyka C++, třeba i prázdný. Ve většině případů ale tento soubor prázdný nebude, zůstanou v něm direktivy include, protože se v programu využívá spousta funkcí ze (standardních) sdílených knihoven, např. i funkce pro výpis printf. Výsledek je tedy takový, že můžeme mít celý program v externím Assembleru, ale v projektu zůstane minimálně jeden zdrojový soubor jazyka C (C++), který však může být i prázdný.

Př. Přepište funkce fronty implementované ve vkládaném Assembleru v pátém cvičení do externího Assembleru v samostatném zdrojovém souboru. Základ programu, tj. celou funkci main, implementujte v externím Assembleru. Místo typu Q_T (long) použijte v Assembleru přímo DWORD. Deklaraci struktury Q_item zrušte a v deklaracích pomocných funkcí Q_item_new a Q_item_delete, které zůstanou v jazyce C++, použijte beztypový ukazatel void *. Ukazatele head a tail budou v externím Assembleru (jako DWORD). Ve výsledku zbudou ve zdrojovém souboru jazyka C++ pouze zmíněné dvě pomocné funkce a direktiva include, vše ostatní bude v externím Assembleru.



Jan Outrata
outrataj@phoenix.inf.upol.cz