ASM -=-=- To be or not to be? It's simple: 2b or (not 2b) = ff Jako skoro každý překladač C i GCC má možnost vkládání inline assembleru. V GCC to je ale řešeno o dost jinak než v ostatních a většina lidí se toho děsí a ptá se, proč to u GNU neudělali normálně. Nevědí ale, že to bylo takto vymyšleno pro jejich dobro. Řešení v GCC má totiž několik výhod. Na první pohled každého překvapí změněná syntax. Nejjednodušší použití ASM vypadá asi takto: GCC BC asm("cli"); asm { cli } Proč to bylo takto uděláno? No výhoda je jednoduchá - nemate to programy, co znají C, ale neznají GCC. Pokud vidí zápis z GCC, řeknou si, že to je volání funkce a předání stringu, zatímco u verze z BC se několikráte podívají na asm a pak dojdou k záveřu, že tam chybí středník, to samé se opakuje u cli... Dalším rozdílem je to, že GCC používá AT&T syntax assembleru(o té jsem psal posledně - jsou tam například operátory obráceně). To samo osobě nepřináší žádnou výhodu ale ani nevýhodu. GCC funguje tak, že celý řetězec v asm prostě pošle dál assembleru a tak vůbec nic o assembleru vědět nemusí. To má tu výhodu, že například můžete použít MMX instrukce (pokud je umí assembler -- a ten je umí) a nemusíte kvůli tomu shánět novou verzi GCC, která by proto měla nějakou podporu této podivné instrukční sady. Navíc jsou programátoři, (jako já) kteří považují AT&T syntax za normální a nechápou jak mohli intelové tu jejich tak zkazit. Nejdůležitější rozdíl ale je v optimalizaci. Pokud překladač uvidí jednoduchý zápis: asm { mov ex,16 mov cx,si int 17 } už si nemůže být ničím jist - interrupt mohl klidně změnit všechny registry, globální proměné a ještě přerovnat zásobník. Prostě jeho představa o světě se zhroutí a nezbývá mu, než aby všechny snahy o optimalizaci vzdal. Ale ani u jednodušších příkladů si nemůže být jist. Nemůže znát celou instrukční sadu (protože se stále objevují nové procesory a klony s novými instrukcemi - viz MMX) a tak jednodušše nemůže nic pořádného o takovém kusu assembleru předpokládat. A to ani o samotné instrukci cli v minulém prípadě. Představte si, že píšete program, který často zapína a výpíná interrupty, uděláte si tedy inline funkce pro cli a sti: static inline cli(void) { asm {cli}} static inline sti(void) { asm {sti}} Tyto funkce voláte z nejrůznějších interních smyček (což je celkem běžné u ovladačů), chudák překladač musí být zmaten a vyprodukovat strašný kód. GCC je na tom lépe. Pokud u asm explicitně neřeknete, že něco mění, předpokládá se, že nemění nic. To jde tak daleko, že u funkcí: static inline cli(void) { asm("cli");} static inline sti(void) { asm("sti");} dojde někdy dokonce k závěru, že když taková funkce nic nemění, je nejlepší ji vůbec nevolat, začne chytračit a volání vyoptimalizuje pryč (nebo alespoň strčí před smyčku, aby se to neprovádělo zbytečně často). Tomu se dá zamezit pomocí volatile. V ansi C je definováno, že když uvedete flag volatile u proměné, je nutné předpokládat, že má nějaky speciální význam (například je hlídána a měněna z časovače) a tak není možné na ní dělat některé optimalizace (jako předpokládat jakou bude mít hodnotu, ukládat každou chvíli jinam, vyhodit ji úplně apod.) U asm toto funguje podobně. Zápis: static inline cli(void) { asm volatile ("cli");} static inline sti(void) { asm volatile ("sti");} už všechno bude fungovat tak, jak má a kód bude optimalizován jako kdyby tam žádné cli nebo sti nebylo. Ale pořád to není ono - optimalizace jde používat jen u některých hodně hloupých funkcí jako je cli, které nic nemění (ani registry) ani nic nečtou (protože optimizer může usoudit, že se mu hodí váš assembler provédst jindy a funkci může celou přeházet. proto ani nemůžete předpokládat, že už všechno co jste napsali před asm voláním je už hotové. A proto má asm další rošíření. Za samotným stringem můžete napsat : a zadat jaké má funkce vstupy, jaké výstupy a co modifikuje. To dá optimizeru pěkný obrázek o tom, co vlastně váš program dělá a je možné dělat další optimalizace. Vstupní a výstupní parametry jsou v assembleru potom přístupné jako %0 %1... (vstupy napřed, výstupy potom.) při kompilaci GCC potom projde na % kombinace a nahradí je pravým umístěním proměné. Aby ale nedocházelo ke kolizím z očíslovanými registry, je nutné u asm ze vstupy a výstupy psát dvě %% u registrů, tedy %%eax místo %eax. Například: asm volatile ("outb %1, %0" : : "d" (port), "a" (data)); říká, že assembler má dva parametry (port a data), a ty nemění. Protože funkce má vedlejší účínek, který lze težko definovat, je nutné použít volatile. první dvojtečka říká, že assembler nemá žádné výstupy, další dvojtečka odděluje vstupy (to je port a data) a tak je tam "d"(port), tato magická kombinace se skládá ze dvou částí - třídy ("d") a parametru(port) a říká, že proměná port má být uložena v edx. Druhý parametr oddělený , má třídu "a" tedy eax. GCC podporuje mnoho tříd pro uložení dat. Základní jsou: g - cokoliv (konstanta, registr, paměť) r - libovolná hodnota v registru m - hodnota musí být v paměti i - hodnota musí být "immediate" tedy konstanta známá při kompilaci a - eax, d-edx, c-ecx, d-edx D - edi, S-esi q - 'a', 'b', 'c' nebo 'd' f - floating point registr t - prvni fp registr (top of stack) s - druhy fp registr (second) Ale existují i další exotičtější, kdo to myslí s psaním asm konstrukcí vážně by měl prostudovat manuál (info system). Například 'N' znamená konstantu mezi 0-255, 'M' mezi 0-3, 'O' je adresa, ke které jde přičítat offset ('M' jako taková může být vypočtená adresa a compiler tam pak dosadí, celý výpočet). Je možně uvédst víc tříd naráz ("SD" znamená, že parametr má být v edi nebo esi) Tento formát vychází ze způsobu, jakým GCC uchovává RTL instrukce a machine description (popis architektury). Funkce out fungující i pro konstantí port je tedy: asm volatile ("outb %1, %0" : : "Nd" (port), "a" (data)); A ušetříte tím jeden registr a instrukci pro nastavování eax. Teď to vpodstatě říká, že instrukci outb jde používat buď pro konstantní port, nebo pro hodnotu uloženou v dx a pro data uložená v ax, což je podle mé chabé paměti přesně to, jak se out chová. U % parametrů je také možné přetypovávat, pokud není zaručeno, že parametry jsou stejného typu. Třeba jde použít %b0 pokud to má být byte a tak assembler nebude řvát ani když parametr nebude char. Jsou podporovány následující typy: k - cele slovo (eax) b - byte (al) h - horní byta (ah) w - word (ax) Pokud potřebuje kód například registr ax nastavený na 1, je mnohem lepší uvédst ax mezi vstupy (a jako parametr napsat 1), než začínat kód příkazem movw 1,%ax, protože GCC tak může nějak jinak zařídit nastavení ax na 1 a ušetřit tak instrukci. Za první dvojtečku se píše výstup - to je parametr, který musí být lvalue (tedy to, co je možné psát před =). GCC počítá s tím, že jeho hodnota se pouze zapisuje ale nečte. U třídy je nutné psát '=': asm volatile ("inb %1, %0" : "=a" (rv) : "Nd" (port)); Toto načte z portu port hodnotu do proměné rv. Pokud chcete vstupně výstupní proměné, můžete použít následující konstrukci: asm ("incl %0": "=g" (i): "0" (i)); Toto provede i++ pro proměnou i uloženou kdekoliv. Podobnou řádku najdete i v souboru i386.md (machine description pro 386). "0" říká, že tento parametr musí být uložen na stejném místě jako parametr číslo 0 (výstupní i). Pokud to tam použijete "g", gcc nebude mít pocit, že to první i nějak souvisí s tím druhým a bude na ně nahlížet jako na dvě různé proměné a pro každou z nich může třeba zvolit jiný registr, podle toho jak se to ve zbytku kódu hodí. Navíc gcc může výstupní parametry umístit na stejné místo jako vstupní, protože předpokládá, že vstupy se napřed načtou, pak se provede nějaké zpracování a potom se uloží do výstupů. Pokud váš kód mixuje vstupy a výstupy, je nutné k výstupní třídě přidat "&" jako v následujícím getpixelu: asm ( "movw %w1, %%fs .byte 0x64 movb (%2, %3), %b0" : "=&q" (result) /* výstup in al, bl, cl, nebo dl */ : "rm" (seg), /* segment selector v reg, nepo paměti */ "r" (pos), /* začátek řádky*/ "r" (x) /* pozice na řádce*/ ); Poslední důležitá věc je to, že občas takové assemblerové programy potřebují registry. Jedna z cest je na začátku popnout modifikované registry a na konci pushnout. Není to ale nejlepší a GCC nabízí jinou cestu. Za poslední : můžete napsat soupis registrů, které jste modifikovali, cc pro změnu flagů. Pokud kód modifikuje a čte paměť nejakým podivným způsobem (než jsou jenom změny proměných), je nutne napsat i "memory". To zařídí, aby se všechny proměné uložily do paměti, než se kód provede a potom se předpokládalo, že se mohly změnit. Navíc je u asm statementů modifikujících paměť (například ekvivalent pro memcpy) často nutné používat volatile, protože paměť není vedena ani mezi vstupy ani mezi výstupy a tak optimizer nevidí důvod, proč by takovoý kód nemohl přemísťovat, vyhodit ze smyčky apod. Například následující kód funguje jako memcpy a kopíruje n bytů ze src do dest (toto je ale jen ukázkový příklad a cesta přes rep movsb je VELMI pomalá): asm volatile ( "cld rep movsb" : bez výstupních proměných :"c" (n),"S" (src),"D" (dest) do cx počet, do si zdroj, di cíl :"cx","si","di","memory","cc"); modifikované registry, paměť a flagy To je asi kompletní syntax. Možná vám není úplně jasné k čemu je takto obecná syntax nutná. Je to právě kvůli inlinování funkcí. Pokud píšete kus assembleru do svého kódu, je situace mnohem jednodušší - ušijete ho na míru dané situaci. Ale když děláte inline funkci, je lepší dát optimizeru větší volnost. Nakonec jedno velké varování. NAUČTE SE POŘÁDNĚ TUTO SYNTAX, než začnete programovat. Je zdrojem častých chyb. Zapomenete na nějakou drobnost - třeba uvédst volatile a ono to fungovat může a nemusí. Také se může dost dobře stát, že to funguje ale jen 999 z tisíce pokusů, nebo tak, že to je nakonec pomalejší, než kdybyste to napsali v C. Nejčastější chyby jsou: - použijete 'g' a předpokládáte, že to je registr (tato buga je například v ctrl387.c ve zdrojáku DJGPP) - použijete globální návěští, které pak koliduje. Skoky se dělají zásadně: 1: ... jne 1b tedy aby assembler věděl, že se odkazujete na nejbližší návěští jménem 1 dozadu (nebo 1f pro dopředu) - zapomenete uvédst modifikované registry - zapomenete uvédst, že to modifikuje flagy (to je na mnoha místech zdrojáků Linuxu) - neuvedete "memory" (najdete tamtéž) - neuvedete volatile, kde je třeba - použijete 'r' nebo 'g' ale předpokládáte, že to nebude v nějakém registru (třeba eax) - uvedete příliš omezující podmínky, které pak zbytečně zpomalují kód. Pokud chcete, aby gcc kompilovalo vaše funkce bez řečí i v -pedantic módu, je nutné nepsat stringy na několik řádek a každou ukončit pomocí "\n" a novou začít pomocí ". Vypadá to potom strašně, ale co se dá dělat. Staré C neumělo víceřádkové stringy. Také je možné používat __asm__ místo asm a volatile místo __volatile__ Nakonec jenom několik chybých a neefektivních funkcí, které jsem při psaní tohoto článku náhodou objevil v různých zdrojácích. Nalezení nedostatků ponechám čtenáři jako jednoduché cvičení. Jak vidíte i velcí mistři se občas utnou (a občas jim to i projde). extern inline void * memmove(void * dest,const void * src, size_t n) { register void *tmp = (void *)dest; if (dest<src) __asm__ __volatile__ ( "cld\n\t" "rep\n\t" "movsb" : /* no output */ :"c" (n),"S" (src),"D" (tmp) :"cx","si","di"); else __asm__ __volatile__ ( "std\n\t" "rep\n\t" "movsb\n\t" "cld\n\t" : /* no output */ :"c" (n), "S" (n-1+(const char *)src), "D" (n-1+(char *)tmp) :"cx","si","di","memory"); return dest; } -- linux kernel, linux/include/asm/string-486.h extern __inline__ void outportb (unsigned short _port, unsigned char _data) { __asm__ __volatile__ ("outb %1, %0" : : "d" (_port), "a" (_data)); } -- djgpp, include/inline/pc.h int i = 0; __asm__(" pushl %%eax\n movl %0, %%eax\n addl 1, %%eax\n movl %%eax, %0\n popl %%eax" : : "g" (i) ); /* i++; */ -- tutoriál k assembleru djasm.html Smutné je, že takových příkladů je všude habaděj a téměř každá asm konstrukce, na kterou se kouknu je špatně. Já jsem napočítal minimálně 6 nedostatků v těchto příkladech a co vy? výheň