Jak jsem pěstoval trpaslíky
                         -=-=-=-=-=-=-=-=-=-=-=-=-=-=-

                       Thus spake the master programmer:
      "Though a program be but three lines long, someday it will have to
                                be maintained."
                  -- Geoffrey James, "The Tao of Programming"

                        Pavel Kaňkovský, 7. května 1998

                                 Slovo na úvod

          V  dubnovém  čísle   Linuxových  novin  byla  vyhlášena   soutěž
      o nejmenší  soubor  spustitelný pod Linuxem.  Když jsem se dozvěděl,
      že program  může  dělat  úplně  cokoli,  je-li to dobře  definováno,
      zaujalo mne to a také jsem se zúčastnil.

                                Smršťování kódu

          Nejdříve  jsem si položil  otázku:  Jaký je nejmenší  možný  kód
      (bez hlaviček a podobného  smetí), který ještě pod Linuxem dělá něco
      smysluplného?  Je  zřejmé, že takový  program  napsaný v jazyce C by
      vypadal například takto:

      int main()
      {
         return 0;
      }

          to jest nedělá nic jiného, než že skončí.

          Ovšem takový  program je ve skutečnosti  obalen kódem  knihovny,
      která po návratu z funkce  main()  volá  funkci  exit(),  která  pak
      volá stejnojmené  systémové  volání, které ukončí život procesu. Což
      znamená, že program je možno "zjednodušit" takto (případné  varování
      překladače, že nezná návratovou hodnotu, budeme ignorovat):

      #include <syscall.h>
      int main()
      {
         syscall(SYS_exit, 0);
      }

          Nyní musíme  volání  funkce  (resp. makra)  syscall()  "rozložit
      na prvočinitele".  Nahlédneme do souboru /usr/include/asm/unistd.h a
      podíváme se, jak se provádí volání jádra na procesorech Intel:

      #define _syscall1(type,name,type1,arg1) \
         type name(type1 arg1) \
         { \
            long __res; \
            __asm__ volatile ("int 0x80" \
                            : "=a" (__res) \
                            : "0" (__NR_##name),"b" ((long)(arg1))); \
            if (__res >= 0) \
               return (type) __res; \
            errno = -__res; \
            return -1; \
         }

          Poněkud kryptické,  není-liž pravda? Nicméně cvičené oko odhalí,
      že je  vhodné  nastavit  registr  eax na  číslo  systémového  volání
      (v našem  případě je to číslo 1),  registr  ebx  nastavit na hodnotu
      argumentu tohoto volání (tedy nulu) a vyvolat přerušení číslo 0x80.

          Nyní  již  opusťme  (relativně)   pohostinné  končiny  jazyka  C
      a přepišme celý program do assembleru (neboli  "jazyka  symbolických
      adres" pro přátele starých pořádků):

     .text
     .globl _start
     _start:
        mov     1, %eax
        mov     0, %ebx
        int     0x80

          Takový program se už obejde bez podpory  podobných  zbytečností,
      jako  jsou  knihovny,  takže  už  nebudeme  ani  předstírat  nějakou
      funkci  main() a rovnou  definujeme  symbol  _start (a  ušetříme  si
      jeden  parametr  při  volání  linkeru,  neboť  _start je  implicitní
      název   vstupního  bodu  programu,  který  se  obyčejně   nachází  v
      /usr/lib/crt1.o).

          Náš  zdrojový  text  nazveme  například  program.s  a  přeložíme
      příkazem  cc -c  program.s.  Výsledkem  bude  následujících 12 bajtů
      strojového kódu (objdump --disassemble --show-raw-insn program.o):

      00000000 <_start>   b8 01 00 00 00  movl 0x1,%eax
      00000005 <_start+5> bb 00 00 00 00  movl 0x0,%ebx
      0000000a <_start+a> cd 80           int  0x80

          Je vidět, že značné  rezervy jsou v nastavení  obou registrů: 10
      bajtů se zdá být značným  plýtváním  prostorem.  Assemblerový expert
      by kód upravil například takto:

      .text
      .globl _start
      _start:
         xor     %eax, %eax
         mov     %eax, %ebx
         inc     %eax
         int     0x80

          což vytvoří následujících 7 bajtů kódu:

      00000000 <_start>   31 c0  xorl %eax,%eax
      00000002 <_start+2> 89 c3  movl %eax,%ebx
      00000004 <_start+4> 40     incl %eax
      00000005 <_start+5> cd 80  int  0x80

          Ale to ještě není konec! Ukazuje se (a lze to ověřit nahlédnutím
      do zdrojových  textů  jádra), že registr  eax je již  při  vstupu do
      programu  vynulován  - jako  by  docházelo k  úspěšnému  návratu  ze
      systémového  volání  execve().  Proto lze vynechat první  instrukci.
      Nutným  předpokladem  ovšem  je, že náš  kód je  skutečně  to první,
      co bude vykonáno, nikoli dynamický linker nebo něco podobného, proto
      je žádoucí při linkování používat parametr -static.

          Nyní  lze   přistoupit  k  zřejmě  již   definitivně    poslední
      optimalizaci: nastavení registru ebx spotřebovává dva bajty. Tomu se
      jen těžko lze vyhnout, má-li mít nulovou (nebo jedničkovou) hodnotu,
      nicméně  zadání  vyžaduje,  aby  měl  hodnotu  "dobře  definovanou".
      Iniciální  hodnota při spuštění tuto podmínku  bohužel - narozdíl od
      registru eax - nesplňuje.  Naštěstí je jedna vhodná  hodnota hned po
      ruce:  jedná se o počet  argumentů  programu  (známý pod  přezdívkou
      argc),  který je  jádrem  uložen na samotný  vrchol  zásobníku.  Náš
      program je tedy možné  zkrátit na tři instrukce  zvíci  pouhých čtyř
      bajtů:

      00000000 <_start>   40     incl   %eax
      00000001 <_start+1> 5b     popl   %ebx
      00000002 <_start+2> cd 80  int    0x80

          neboli ve zdrojové  formě  (která je teď pro změnu  uvedena jako
      druhá):

     .text
     .globl _start
     _start:
        inc     %eax
        pop     %ebx
        int     0x80

          což je víceméně ekvivalent tohoto céčkového programu:

      int main(int argc)
      {
         return argc;
      }

          Tento  kód již  vypadá  celkem  minimálně,  takže z něj  zkusíme
      vytvořit spustitelný soubor:

       cc -c program.s
       ld -static program.o
       ls -l a.out
      -rwxr-xr-x 1 peak users 675 May 3 22:00 a.out

          Ó jé!  Takový  malý kód a tak velký  soubor! S tím se musí  něco
      udělat.

                              Smršťování souboru

          Nyní je třeba  řešit  další  zásadní  problém:  Jak  vyrobit  co
      nejmenší spustitelný soubor?

          Linux zná několik  druhů  spustitelných  souborů:  základní jsou
      a.out, ELF a skripty (tj. soubory  začínající  znaky  \#!).  Skripty
      vynecháme, protože nevyhovují původnímu zadání.

      a.out je starší a jednodušší  formát.  Jeho kořeny  sahají  někam do
         šerého  unixového  dávnověku,  ale v dnešní  době se už prakticky
         nepoužívá. Má několik variant, které se liší způsobem načítání do
         paměti. Viz include/linux/asm/a.out.h a fs/binfmt_aout.c.
      ELF je novější a složitější  formát. Byl vyvinut pro potřeby  System
         V.  Ve  své  úplnosti  je tak  složitý,  že pro  jeho  dekódování
         existuje zvláštní knihovna (libelf), ale nás zajímá jen malá část
         - přesně ta, co zajímá  jádro.  Viz {\tt  include/linux/asm/elf.h
         a fs/binfmt_elf.c.

      Věnujme se nejprve krátce formátu a.out. Jeho hlavička vypadá takto:

      a_info      "magické číslo"
      a_text      délka kódu
      a_data      délka inicializovaných dat
      a_bss       délka neinicializovaných dat
      a_syms      velikost tabulky symbolů v bajtech
      a_entry     startovací adresa
      a_trsize    velikost relokační tabulky pro kód
      a_drsize    velikost relokační tabulky pro data

          Spustitelný  soubor je tvořen hlavičkou a za sebou následujícími
      úseky kódu, inicializovaných dat a tabulky symbolů (neinicializovaná
      data nejsou pochopitelně v souboru obsažena). Soubor nesmí obsahovat
      žádná relokační data a příslušné hodnoty v hlavičce musí být nulové.

          "Magické   číslo"  popisuje   variantu   formátu.  Nabývá  jedné
      z následujících hodnot:

      OMAGIC tzv. "impure  executable"; zbytek souboru je načten do paměti
         od adresy 0, zápis je povolen všude  (pozn. tato varianta  bývala
         používána i pro objektové soubory formátu a.out(*.o)
      NMAGIC tzv. "pure executable"  zbytek souboru je načten do paměti od
         adresy 0, ale do oblasti kódu je zakázán zápis
      ZMAGIC tzv. "demand-paged  executable"  soubor počínaje  pozicí 4096
         je namapován do paměti od adresy 0x1000, v prostoru první stránky
         není nic namapováno
      QMAGIC  jiná  varianta  "demand-paged  executable",  celý  soubor je
         namapován  do paměti  od adresy  0x1000,  narozdíl  od  předchozí
         varianty  není  na  začátku  souboru  jedna  prakticky  nevyužitá
         stránka

          Zkusíme  vytvořit  soubor ve formátu a.out  pomocí  standardních
      nástrojů. Jediný problém je v tom, že musíme linkeru dát parametr -m
      i386linux nebo -b a.out-i386-linux,  protože (pokud není provozovaná
      verze opravdu  letitá) bez těchto  parametrů by byl vytvořen  soubor
      ve formátu  ELF. Parametrem -N si vyžádáme  vytvoření  varianty {\tt
      OMAGIC} (i když stejně dobře by posloužila  varianta NMAGIC vyvolaná
      parametrem  -n). Navíc  (narozdíl od příkladu v předchozí  kapitole)
      nezapomeneme odstranit ladící informace pomocí příkazu strip (což je
      kupodivu účinnější než použití parametru -s při linkování).

       ld -static -m i386linux -N program.o
       strip a.out
       ls -l a.out
      -rwxr-xr-x 1 peak users 40 May 3 22:05 a.out

          Soubor  obsahuje 32 bajtů  hlavičky a 4 bajty kódu  zvětšené o 4
      bajty  výplně. To je docela pěkný  výsledek, ale, jak uvidíme  záhy,
      není zdaleka nejlepší možný.

          Přikročme  nyní  ke  studiu  formátu  ELF.  Ten má dvě  zajímavé
      hlavičky.  První je hlavička  souboru známá pod názvem  15Elf32_Ehdr
      (existuje i 64-bitová varianta Elf64_Ehdr):

      e_ident     identifikace souboru
      e_type      typ souboru
      e_machine   platforma
      e_version   verze formátu souboru
      e_entry     startovací adresa
      e_phoff     začátek tabulky segmentů (v souboru)
      e_shoff     začátek tabulky sekcí (v souboru)
      e_flags     různé příznaky
      e_ehsize    velikost hlavičky souboru v bajtech
      e_phentsize velikost záznamu segmentu
      e_phnum     počet záznamů v tabulce segmentů
      e_shentsize velikost záznamu sekce
      e_shnum     počet záznamů v tabulce sekcí
      e_shstrndx  index sekce obsahující jména

          a druhá je hlavička  segmentu (tedy položka v tabulce  segmentů)
      čili Elf32_Phdr:

      p_type      druh segmentu
      p_offset    počátek segmentu v souboru
      p_vaddr     virtuální adresa v paměti
      p_paddr     fyzická adresa v paměti
      p_filesz    velikost segmentu v souboru
      p_memsz     velikost segmentu v paměti
      p_flags     příznaky segmentu
      p_align     zarovnání adresy segmentu

          Spustitelný  soubor ve formátu ELF je tvořen  hlavičkou  souboru
      a "volným  prostorem", ve kterém jsou víceméně  libovolně  rozloženy
      ostatní části souboru: tabulky  segmentů a sekcí a samotné  segmenty
      a sekce.  Tyto  části  se  mohou  libovolně  překrývat  (nenaruší-li
      to integritu jejich obsahu).

          První  položkou v hlavičce  souboru je  identifikace:  16 bajtů,
      které  určují, že se jedná o soubor ve formátu  ELF a popisuje  jeho
      základní  vlastnosti  (LSB/MSB, 32/64  bitů, verzi formátu  hlavičky
      souboru).  Celkem je obsazeno  pouze 7 bajtů,  zbylých 9 by mělo mít
      nulovou hodnotu, ale nutné to není. Později toho bude využito.

          Položka e_type určuje typ souboru,  spustitelné soubory zde mají
      hodnotu 2, symbolicky  ET_EXEC. Položka e_machine určuje hardwarovou
      platformu,  pro  kterou je soubor  určen - v našem  případě to  bude
      3, symbolicky EM_I386. Položka e_version určuje verzi formátu, která
      zatím existuje pouze jediná s číslem 1.

          Důležité   položky   hlavičky   souboru   jsou  ještě   e_entry,
      která určuje  adresu, od které bude náš program  vykonáván,  e_phoff
      obsahující  polohu  tabulky  segmentů v souboru,  e_phnum  udávající
      počet segmentů a e_phentsize, která musí obsahovat velikost hlavičky
      segmentu, v bajtech, tedy číslo 32.

          Zbylé položky nejsou při načítání souboru do paměti jádrem nijak
      interpretovány, takže mohou mít prakticky libovolné hodnoty.

          Segment je ta část souboru, která má být při spouštění  programu
      jádrem načtena či namapována do paměti, tedy kód nebo data. Existují
      sice také některé druhy  segmentů  mající  speciální  význam, ale my
      budeme  blíže  zkoumat  pouze právě popsaný typ PT_LOAD,  číselně se
      jedná o typ 1.

          V hlavičce segmentu je (narozdíl od hlavičky  souboru)  důležitá
      většina  údajů s  výjimkou  p_paddr  (neboť  mapování  na  konkrétní
      fyzické  adresy není možné) a p_align  (segmenty jsou vždy zarovnány
      na  celé  stránky).  Zvláštní  pozornost   zaslouží  p_flags,  který
      popisuje,  jestli  bude možno  namapovaný  segment  číst  (PF_R, 4),
      vykonávat  (PF_X, 1), resp. zda do něj bude možno  zapisovat  (PF_W,
      2).

          Zkusme  nyní vyrobit co nejmenší  spustitelný  soubor ve formátu
      ELF. Nejprve použijeme linker a strip, jako v případě formátu a.out:

       ld -static program.o
       strip a.out
       ls -l a.out
      -rwxr-xr-x 1 peak users 364 May 3 22:23 a.out

          Výsledek je to rozhodně  lepší než  předtím,  ale  pořád to není
      ono. Přeložený soubor ve skutečnosti  obsahuje velké množství smetí,
      jak nám ukáže program objdump:

       objdump -h a.out

      a.out:     file format elf32-i386

      Sections:
      Idx Name Size     VMA      LMA      File off Algn
       0 .text 00000004 08048074 08048074 00000074 2**2
             CONTENTS, ALLOC, LOAD, READONLY, CODE
       1 .data 00000000 08049078 08049078 00000078 2**2
             CONTENTS, ALLOC, LOAD, DATA
       2 .bss  00000000 08049078 08049078 00000078 2**2
             ALLOC

          V souboru  jsou  obsaženy tři sekce, ale jediná  potřebná z nich
      je sekce .text, která  obsahuje  spustitelný kód. Pokusíme se soubor
      zmenšit  odstraněním  ostatních  sekcí.  K  tomu  použijeme  program
      objcopy:

      objcopy -R .data -R .bss a.out
      ls -l a.out
     -rwxr-xr-x 1 peak users  276 May 3 22:29 a.out

          Soubor  obsahuje  52  bajtů  hlavičky  souboru,  jednu  hlavičku
      segmentu o velikosti  32  bajtů,  pak 32  bajtů  nul a 4 bajty  kód,
      dohromady  jeden  segment  o velikosti  120  bajtů,  po  kterém  pak
      následuje 156 bajtů s tabulkou sekcí a tabulkou jmen, což jsou ovšem
      věci pro vlastní chod programu celkem  nepodstatné  (je-li  linkován
      staticky, u dynamicky linkovaných programů je to trochu jinak).

          O mnoho více již s pomocí  standardních  nástrojů nelze dokázat.
      Nejjednodušší bude asi vyrobit celý soubor ručně...

                              Bitové inženýrství

          Zkusme si tedy vyrobit  nějaký  spustitelný  soubor  sami - bajt
      po bajtu. Pro tento účel je vhodné použít nějaký  "překladač", který
      nám umožní  zapsat  binární data v nějaké  čitelné  textové  podobě.
      Já jsem si jednu takovou  jednoduchou pomůcku pro tento účel vyrobil
      a nazval jsem ji hex. Jazyk překladače hex je opravdu jednoduchý:

      - vše počínaje znakem \uv{\#} je až do konce řádku ignorováno
      - obsah souboru je interpretován po slovech oddělených bílými znaky
      - slovo  obsahující  "L" je interpretováno  jako  4-bajtová  hodnota
        (long),  slovo  obsahující  \uv{W}  jako  2-bajtová   (word),  vše
        ostatní je jednobajtová hodnota; vícebajtové hodnoty jsou ukládány
        od nejméně významného bajtu k nejvýznamnějšímu (little-endian)
      - slovo  obsahující  "x" je interpretováno v šestnáctkové  soustavě,
        slovo obsahující "o" v osmičkové soustavě, ostatní v desítkové

          Začneme opět s formátem a.out neboť je významně jednodušší.

      # a.out hlavicka
      Lx00640107 # a_info (OMAGIC)
      L4  # a_text
      L0  # a_data
      L0  # a_bss
      L0  # a_syms
      L0  # a_entry
      L0  # a_trsize
      L0  # a_drsize

      # kod
      x40 # inc %eax
      x5b # pop %ebx
      xcd # int
      x80 # 0x80

          Tento "zdrojový soubor" se po průchodu  překladačem hex změní na
      36 bajtový  soubor, který je jádro Linuxu ochotno  načíst a spustit.
      Uspořili  jsme  zatím  čtyři  bajty  výplně.  Někdo může  namítnout,
      že vhodnější volbou parametrů překladu bychom mohli dosáhnout téhož.
      Ale ještě jsme neskončili!

          Všimněme si nyní dvou věcí: za prvé,  pokud  změníme typ souboru
      na  {\tt  QMAGIC,  bude  do paměti  načten - resp.  namapován - celý
      soubor  (i když  jádro  bude  poněkud  "nervózní" z toho,  že  délka
      souboru není násobek 4096 bajtů), za druhé, pokud vynecháme koncovou
      část  hlavičky,  domyslí si na tom místě  jádro  nuly  (těžko  říci,
      zda je to chyba nebo  úmysl). Náš kód má pouze  čtyři  bajty,  takže
      by ho bylo možno vecpat do některé  položky v hlavičce a ještě ji na
      konci uříznout. Ač to zní překvapivě, možné to je: kód lze vložit do
      položky a_bss, ačkoli podle zdravého rozumu by něco takového  nemělo
      jádro vůbec  spustit  (velikost  neinicializovaných dat je větší než
      2GB). Spustí...

      # a.out hlavicka
      Lx006400cc # a_info (QMAGIC)
      L20 # a_text
      L0  # a_data

      # L0  # a_bss

      # kod (na miste a_bss!)
      x40 # inc %eax
      x5b # pop %ebx
      xcd # int
      x80 # 0x80

      L0  # a_syms
      Lx100c  # a_entry

      # ty tady nemusi byt, protoze jsou nulove
      # (poznamenejme, ze nulove byt MUSI)
      # L0  # a_trsize
      # L0  # a_drsize

          Hrůza!  Ale na  druhou  stranu má  výsledný  spustitelný  soubor
      velikost  pouhých 24 bajtů (tedy o 8 méně než je regulérní  velikost
      hlavičky). Menší  spustitelný  soubor už asi nikdo  nevyrobí.  Škoda
      jen,  že při  každém  spuštění  jádro  vypíše  executable  not  page
      aligned.

          A co formát ELF? Začneme opět konzervativně:

      # Elf32_Ehdr
      # e_ident
      x7f x45 x4c x46 # \177ELF
      x01 x01 x01 x00 # 32bit, LSB, current version
      L0
      L0
      # dalsi polozky
      W2      # e_type = ET_EXEC
      W3      # e_machine = EM_386
      L1      # e_version
      Lx1054  # e_entry
      Lx34    # e_phoff
      L0      # e_shoff
      L0      # e_flags
      Wx34    # e_ehsize
      Wx20    # e_phentsize
      W1      # e_phnum
      Wx28    # e_shentsize
      W0      # e_shnum
      W0      # e_shstrnum

      # Elf34_Phdr
      L1      # p_type = PT_LOAD
      L0      # p_offset
      Lx1000  # p_vaddr
      Lx1000  # p_paddr
      L88     # p_filesz
      L88     # p_memsz
      L5      # p_flags = PF_R | PF_X
      Lx1000  # p_align

      # kod
      x40 # inc %eax
      x5b # pop %ebx
      xcd # int
      x80 # 0x80

          Nejprve  52  bajtů  hlavičky  souboru,  pak  32  bajtů  hlavičky
      segmentu a pak čtyři bajty kódu. Dohromady 88 bajtů.

          Zkusme nyní ušetřit nějaké místo. Prvním krokem bude asi využití
      prázdného  místa v e_ident:  nejjednodušší bude do něj přesunout kód
      - tím soubor  zkrátíme o čtyři  bajty. Lepší by bylo někam přesunout
      hlavičku  segmentu,  ale ta je moc  velká,  do e_ident  se  rozhodně
      nevejde, ani kdybychom  odtud  odstranili kód, který jsme tam zrovna
      umístili...  Ovšem pozor! V hlavičce  souboru je spousta  zbytečného
      místa: umístíme-li  začátek  hlavičky  segmentu na stejné místo jako
      e_shoff, dojde k jediné významné  kolizi: druhý bajt p_vaddr bude na
      stejném  místě jako  e_phentsize, což je jednoduché  vyřešit - stačí
      vhodně  zvolit  adresu, na  kterou  program v paměti  umístíme.  Tím
      ušetříme 20 bajtů, další čtyři ještě můžeme získat useknutím položky
      e_align, kterou jádro postrádat nebude.

      # Elf32_Ehdr
      # e_ident
      x7f x45 x4c x46 # \177ELF
      x01 x01 x01 x00 # 32bit, LSB, current version

      # kod (pres zbytek e_ident)
      x40 # inc %eax
      x5b # pop %ebx
      xcd # int
      x80 # 0x80

      L0      # tady je jeste mista....
      # dalsi polozky
      W2      # e_type = ET_EXEC
      W3      # e_machine = EM_386
      L1      # e_version
      Lx200008# e_entry
      Lx20    # e_phoff
      #                         Elf34_Phdr
      L1      # e_shoff         p_type = PT_LOAD
      L0      # e_flags         p_offset
      W0      # e_ehsize        p_vaddr = 0x00200000
      Wx20    # e_phentsize (!)
      W1      # e_phnum         p_paddr = 0x00280001
      Wx28    # e_shentsize
      Lx20    # e_shnum         p_filesz
              # e_shstrnum
      Lx20    #                 p_memsz
      L5      #                 p_flags = PF_R | PF_X
              #                 p_align (vynechano)

          Celková  velikost  je  neuvěřitelných  60  bajtů!  Navíc  takový
      program funguje, jádro si na nic nestěžuje a program file ho správně
      identifikuje:

        ls -l elf_tiny
       -rwxr-xr-x 1 peak users  60 May 3 22:47 elf_tiny
        file elf_tiny
       elf_tiny: ELF 32-bit LSB executable, \
       Intel 80386, version 1,
       statically linked, stripped
        ./elf_tiny 1 2 3; echo ?
       4

          A to je vše, přátelé!  Menší  programy  jsem už opravdu  vyrobit
      nedokázal.


            výheň