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ň