Hlavní navigace

Když céčko nestačí: GAS

25. 2. 2003
Doba čtení: 6 minut

Sdílet

Potřebujeme-li v kritické sekci programu napsaném v jazyce C nebo C++ maximální výkon, jako poslední možnost se jeví přepsat tuto kritickou část do assembleru, tedy přímo do jazyka symbolických instrukcí dané platformy. V následujícím článku se podíváme na to, jaké máme na Linuxu možnosti, použijeme-li assembler vkládaný přímo do kódu jazyka C překladače gcc, tedy GNU Assembler.

Než se odhodnáte k tomuto odvážnému kroku, je nutno zvážit, zdali je opravdu nutné assembler použít. Jistě, s trochou šikovnosti můžete skutečně urychlit vykonávaný kód, zejména tam, kde se zpracovávají větší množství dat (audio, video atd). Je dobré se na to ale podívat z jiné stránky.

Programování v assembleru je značně nebezpečné a pracné. Zatímco kompilátor jazyka C odhalí při překladu spoustu chyb, jazyk symbolických instrukcí spolyká všechno, co je syntakticky správně. Odhalování následných chyb je pak noční můrou všech programátorů. Další značnou nevýhodou je nepřenositelnost kódu, a to i v případě, že zůstává cílová platforma (např. Intel) stejná. Pokud budete chtít program přeložit na jiné platformě, počítejte s tím, že budete někdy muset vynaložit nemalé úsilí, aby se program úspěšně přeložil. Navíc, optimalizační algoritmy jsou dnes na tak vysoké úrovni, že assembler v normálním případě takřka potřebovat nebudete.

Jelikož platí, že budete muset přepisovat do assembleru jen malé části aplikace, je proto nejvhodnější použít inline assembler. Je to takový assembler, který se mixuje přímo s jazykem C a kompilátor mu rozumí a správně jej vloží do generovaného kódu. Evidentně tedy záleží na kompilátoru jazyka C, který chcete použít. Nejpoužívanějším kompilátorem v Linuxu je překladač jazyka C z balíku GCC (GNU Compiler Collection) s příhodným názvem gcc, který podporuje assembler GAS (GNU Assembler).

Programátoři zvyklí na programování v systémech DOS/Windows pravděpodobně zažijí malý šok (já sám jsem začínal v DOSu). Hned první ranou bylo pro mě zjištění, že GAS používá syntaxi AT&T, která se od intelovské syntaxe značně liší. Druhé zjištění však bylo ještě horší. Vkládání assembleru do kódu C/C++ je implementováno dosti nemotorně, pravděpodobně z důvodů přenositelnosti.

Svět UNIXu tu byl dávno před firmami Intel nebo Microsoft. Tehdejší UNIXy se provozovaly zřejmě hodně na procesorech Motorola, které podporovaly právě tuto syntaxi, vyvinutou v laboratořích AT&T. Od intelovské se liší zejména v těchto věcech:

  • parametry všech instrukcí jsou obráceně (zdroj, cíl)
  • registry musí být uvozeny pomocí znaku % (procento)
  • každé konstantě nebo číslu musí předcházet znak $ (dolar)
  • většina instrukcí končí znaky b, w, l, které udávají šířku parametru (byte, word, longword)
  • syntaxe adresování paměti je zcela odlišná (kulaté závorky místo složených)

Samozřejmě, že rozdílů je více, ale tyto jsou ty nejmarkantnějsí. Na ostatní narazíte méně často. Současné verze GASu již umožňují zapnout Intel syntaxi, ovšem vzhledem k tomu, že intelovská syntaxe je v GASu mizerně dokumentována, a s přihlédnutím na to, že veškeré tutoriály, návody či dokonce zdrojové kódy programů (či jádra) jsou v AT&T syntaxi, doporučuji raději tu od Intelu v GASu nepoužívat. Zvyk je zvyk a bývá to leckdy velmi těžké se přizpůsobit.

Další překážkou však může být způsob, jakým se tento assembler vkládá do zdrojového kódu C. Všechno se totiž děje nepříjemným způsobem – pomocí řetězcových literálů. Co řádek, to symbolická instrukce, takže je oddělujete pomocí \n. Pokud budete používat normální odřádkování, kompilátor vás bude varovat, že se odřádkování v řetězci nemá používat. Můžete alternativně použít středník, pokud programujete pro Intel. Programátoři zvyklí na tasm nebo masm si musejí navíc dávat pozor na to, že musejí vrátit stav registrů na konci bloku tak, jak byl na začátku, a odkazy na proměnné v C musejí explicitně zadávat ručně. Překladač gcc navíc kontroluje syntaxi minimálně (resp. vůbec, gas minimálně), jakékoliv překlepy jsou ztrestány množstvím ilegálního kódu a nesmyslných chybových hlášení. Jejich případné dohledávání je hotové peklo. Takové nepřepnutí na konci bloku zpět na AT&T syntaxi je opravdovou lahůdkou.

Samotná syntaxe inline assembleru v  gcc je následující:
asm ("assembler kód"
: mapování výstupních proměnných
:
mapování vstupních proměnných
: seznam modifikovaných registrů, případně paměti);

Za klíčovým slovem asm (resp. __asm__) následuje vlastní kód v GAS assembleru. Pokud hodláte použít proměnné z Céčka (to dělat nemusíte, poslední tři „parametry“ jsou totiž nepovinné), musíte je vyjmenovat v další části příkazu a říci optimalizátoru, jak je umístit. Nakonec je nutno dodat seznam registrů, které hodláte měnit, aby kompilátor zajistil jejich odložení na zásobníku. Ještě bych dodal, že na vstupní/výstupní proměnné se odkážete pomocí %1...%n a znaky procenta, které uvozují registry, musíte zdvojit.

Dalo by se říci, že programování v inline GASu se doporučuje zkušenějším jedincům. Pokud tedy s assemblerem začínáte nebo přecházíte-li z Windows, doporučuji spíše assemblery basm nebo nasm, o kterých bude řeč později. Následuje malá ochutnávka, co od inline assembleru GAS můžete očekávat.

Tabulka č. 389
#include <stdio.h>

int att_faktorial(int n) {
int result;

asm ("
    movl $1, %%eax      /* a = 1*/
    movl %1, %%ecx      /* c = n */
    cmp $0, %%ecx       /* if (c == 0) */
    jne aiter           /*   goto iter */
    incl %%ecx          /* c++ */
    aiter:              /* do { */
    imull %%ecx, %%eax  /*   c *= a */
    loop aiter          /* } while (c >= 0) */
    movl %%eax, %0"     /* result = a */
    : "=g" (result)
    : "g" (n)
    : "%eax", "%ecx", "memory"
  );

return result;
}

int intel_faktorial(int n) {
int result;

asm volatile ("
    .intel_syntax noprefix
    .arch i386
    xor eax, eax
    inc eax         /* eax = 1*/
    mov ecx, %1     /* ecx = n */
    cmp ecx, 0      /* if (ecx == 0) */
    jne iiter       /*   goto iter */
    inc ecx         /* ecx++ */
    iiter:          /* do { */
    imul eax, ecx   /*   ecx *= eax */
    loop iiter      /* } until (ecx >= 0) */
    mov %0, eax     /* result = eax */
    .att_syntax"    /* NUTNE PREPNOUT ZPET! */
    : "=&r" (result)
    : "r" (n)
    : "%eax", "%ecx", "memory"
  );

return result;
}

int main(int argc, char* argv[])
{
  printf("Faktorial 6 je %d\n",
    att_faktorial(6));
  printf("Faktorial 6 je %d\n",
    intel_faktorial(6));
return 0;
}
faktorial.c: faktoriál v inline assembleru GAS s AT&T i Intel sntaxí

Pokusím se v krátkosti přiblížit, jak probíhá předávání proměnných z jazyka C do assembleru. Překladač zamění všechny výskyty %0 (jež nahradí proměnnou result) a %1 (nahrazuje proměnnou n). Musíte mu ale sdělit, jak to má udělat. Ve většině případů vás to moc zajímat nebude, použijete tedy modifikátor "g", který kompilátoru říká, udělej to nejrychleji, jak umíš. V tomto případě může kompilátor zástupný znak nahradit třeba adresou v paměti. Můžete také použít "r", čímž naznačujete, že to musí být registr, ale je vám jedno, který, což se hodí tehdy, pokud budete používat zástupný znak ( %1) ve smyčce. Pomocí "&" zajistíte, aby se registry pro jednotlivé proměnné lišily. Konečně pomocí "a", "b", "c", "d" můžete registr specifikovat konkrétně. Pro kompletní pochopení doporučuji přeložit s parametrem - S takto: gcc -S faktorial.c. Překladač vytvoří soubor faktorial.s , který obsahuje kompletní výpis celého programu. Sekce, které programátor vytvořil inline, jsou označeny.

Bývá zvykem kompilátoru sdělit, ať se blok v assembleru nepokouší optimalizovat. To se hodí tehdy, jsme-li si jisti, že náš kód je nejrychlejší možný. Právě k tomu slouží specifikátor volatile. Zbytek programu již nepotřebuje vysvětlení, zajistí totiž výpis faktoriálu na konzoli. Při experimentování doporučuji hojně používat parametr -S a následně si prohlížet vygenerovaný assembler.

GAS samozřejmě umí zpracovávat soubory i externě. Mívají obyčejně příponu .s, a jak sem nastínil výše, můžete si takový výpis nechat vygenerovat překladačem gcc pomocí parametru -S. Veliká výhoda je, že takto vygenerovaný kód lze pomocí GASu znovu přeložit, takže můžete směle experimentovat. Nutno podotknout, že v čistém assembleru voláte přímo služby operačního systému, jejichž čísla musíte zkrátka znát nebo použít sadu maker.

CS24_early

GAS obsahuje vlastní preprocesor gasp, který umožňuje definovat makra, a tak usnadňuje psaní programů v čistém assembleru. Syntaxe je ale dost šílená. Mám za to, že byla vytvořena spíše pro stroje než pro lidi a vůbec bych se nedivil, kdyby GCC gasp přímo využíval při kompilaci kódu v jazyce C.

www.manualy.sk
ibm.com

Byl pro vás článek přínosný?