   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler pod Linuksem?

Czesc 1 - Podstawy, czyli czym to sie je

   Wyobrazcie sobie, jakby to bylo moc programowac maszyne bezposrednio -
   "rozmawiac" z procesorem bez posrednictwa struktur wysokiego poziomu,
   na przyklad takich jak spotykamy w jezyku C. Bezposrednie operowanie
   na procesorze umozliwia przeciez pelna kontrole jego dzialan! Bez
   zbednych instrukcji i innych "smieci" spowalniajacych nasze programy.

   Czy juz czujecie chec pisania najkrotszych i najszybszych programow na
   swiecie?
   Programow, ktorych czasem w ogole NIE MOZNA napisac w innych jezykach?
   Brzmi wspaniale, prawda?

   Tylko pomyslcie o tym, co powiedzieliby znajomi, gdybyscie sie im
   pochwalili. Widzicie juz te ich zdumione miny?

   Mila perspektywa, prawda? No, ale dosc juz gadania. Zabierajmy sie do
   rzeczy!

   Zacznijmy od krotkiego wprowadzenia:
     _________________________________________________________________

Niedziesietne systemy liczenia.

    1. Dwojkowy (binarny)
       Najprostszy dla komputera, gdzie cos jest albo wlaczone, albo
       wylaczone. System ten operuje na liczbach zwanych bitami (bit =
       binary digit = cyfra dwojkowa). Bit przyjmuje jedna z dwoch
       wartosci: 0 lub 1.
       Na bajt sklada sie 8 bitow. Jednym bajtem mozna przedstawic wiec
       2^8=256 mozliwosci.
       Przeliczenie liczby zapisanej w systemie dwojkowym na dziesietny
       jest proste. Podobnie jak w systemie dziesietnym, kazda cyfre
       mnozymy przez odpowiednia potege podstawy (podstawa wynosi 2 w
       systemie dwojkowym, 10 w systemie dziesietnym).
       Oto przyklad (daszek ^ oznacza potegowanie):
       1010 1001 dwojkowo =
       1*(2^7) + 0*(2^6) + 1*(2^5) + 0*(2^4) + 1*(2^3) + 0*(2^2) +
       0*(2^1) + 1*(2^0) =
       128 + 32 + 8 + 1 =
       169 dziesietnie (lub "dec", od "decimal").
       Dzialanie odwrotne tez nie jest trudne: nasza liczbe dzielimy
       ciagle (do chwili uzyskania ilorazu rownego 0) przez 2, po czym
       zapisujemy reszty z dzielenia wspak:
       (przeskocz konwersje liczby dziesietnej na dwojkowa)
        169     |
         84     | 1
         42     | 0
         21     | 0
         10     | 1
          5     | 0
          2     | 1
          1     | 0
          0     | 1
       Wspak dostajemy: 1010 1001, czyli wyjsciowa liczbe.
    2. Szesnastkowy (heksadecymalny, w skrocie hex)
       Jako ze system dwojkowy ma mniej cyfr niz dziesietny, do
       przedstawienia wzglednie malych liczb trzeba uzyc duzo zer i
       jedynek. Jako ze bajt ma 8 bitow, podzielono go na dwie rowne,
       czterobitowe czesci. Teraz bajt mozna juz reprezentowac dwoma
       znakami, a nie osmioma. Na kazdy taki znak sklada sie 2^4=16
       mozliwosci. Stad wziela sie nazwa "szesnastkowy".
       Powstal jednak problem: cyfr jest tylko 10, a trzeba miec 16. Co
       zrobic?
       Postanowiono liczbom 10-15 przyporzadkowac odpowiednio znaki A-F.
       Na przyklad
       Liczba 255 dziesietnie = 1111 1111 binarnie = FF szesnastkowo
       (1111 bin = 15 dec = F hex)
       Liczba 150 dziesietnie = 1001 0110 binarnie = 96 szesnastkowo.
       Nalezy zauwazyc scisly zwiazek miedzy systemem dwojkowym i
       szesnastkowym: 1 cyfra szesnastkowa to 4 bity, co umozliwia
       blyskawiczne przeliczanie miedzy obydwoma systemami: wystarczy
       "tlumaczyc" po 4 bity (1 cyfre hex) na raz i zrobione.
       Przeliczenie liczby zapisanej w systemie szesnastkowym na
       dziesietny jest rownie proste, jak tlumaczenie z dwojkowego na
       dziesietny. Kazda cyfre mnozymy przez odpowiednia potege podstawy
       (podstawa wynosi 16 w systemie szesnastkowym).
       Oto przyklad:
       10A szesnastkowo =
       1*16^2 + 0*16^1 + A*16^0 =
       256 + 0 + 10 =
       266 dziesietnie.
       Dzialanie odwrotne tez nie jest trudne: nasza liczbe dzielimy
       ciagle (do chwili uzyskania ilorazu rownego 0) przez 16, po czym
       zapisujemy reszty z dzielenia wspak:
       (przeskocz konwersje liczby dziesietnej na szesnastkowa)
         266    |
         16     | 10
          1     | 0
          0     | 1
       Wspak dostajemy kolejno: 1, 0, 10, czyli 10A, czyli wyjsciowa
       liczbe.
       Podczas pisania programow, liczby w systemie szesnastkowym oznacza
       sie przez dodanie na koncu litery h (lub z przodu 0x), a liczby w
       systemie dwojkowym - przez dodanie litery b na koncu.
       Tak wiec, 101 oznacza dziesietna liczbe o wartosci 101, 101b
       oznacza liczbe 101 w systemie dwojkowym (czyli 5 w systemie
       dziesietnym), a 101h lub 0x101 oznacza liczbe 101 w systemie
       szesnastkowym (czyli 257 dziesietnie).
     _________________________________________________________________

Jezyk asembler i rejestry procesora

   Co to jest asembler?
   Asembler jest to jezyk programowania, nalezacy do jezykow niskiego
   poziomu. Znaczy to tyle, ze jednej komendzie asemblera odpowiada
   dokladnie jeden rozkaz procesora. Asembler operuje na rejestrach
   procesora.

   A co to jest rejestr procesora?
   Rejestr procesora to zespol ukladow elektronicznych, mogacy
   przechowywac informacje (taka wlasna pamiec wewnetrzna procesora).
   Zaraz podam Wam podstawowe rejestry, na ktorych bedziemy operowac.
   Wiem, ze ich liczba moze przerazic, ale od razu mowie, abyscie NIE
   uczyli sie tego wszystkiego na pamiec! Najlepiej zrobicie, czytajac
   ponizsza liste tylko 2 razy, a potem wracali do niej, gdy jakikolwiek
   rejestr pojawi sie w programach, ktore bede pozniej prezentowal w
   ramach tego kursu.
   Oto lista interesujacych nas rejestrow:
    1. ogolnego uzytku:
          + akumulator:
            RAX (64 bity) = EAX (mlodsze 32 bity) + starsze 32 bity,
            EAX (32 bity) = AX (mlodsze 16 bitow) + starsze 16 bitow,
            AX = AH (starsze 8 bitow) + AL (mlodsze 8 bitow)
            Rejestr ten sluzy do wykonywania dzialan matematycznych, ale
            czesto w tym rejestrze bedziemy mowic systemowi operacyjnemu,
            co od niego chcemy.
          + bazowy:
            RBX (64 bity) = EBX (mlodsze 32 bity) + starsze 32 bity,
            EBX (32 bity) = BX (mlodsze 16 bitow) + starsze 16 bitow,
            BX = BH (starsze 8 bitow) + BL (mlodsze 8 bitow)
            Ten rejestr jest uzywany na przyklad przy dostepie do tablic.
          + licznik:
            RCX (64 bity) = ECX (mlodsze 32 bity) + starsze 32 bity,
            ECX (32 bity) = CX (mlodsze 16 bitow) + starsze 16 bitow,
            CX = CH (starsze 8 bitow) + CL (mlodsze 8 bitow)
            Tego rejestru uzywamy na przyklad do okreslania ilosci
            powtorzen petli.
          + rejestr danych:
            RDX (64 bity) = EDX (mlodsze 32 bity) + starsze 32 bity,
            EDX (32 bity) = DX (mlodsze 16 bitow) + starsze 16 bitow,
            DX = DH (starsze 8 bitow) + DL (mlodsze 8 bitow)
            W tym rejestrze na przyklad przechowujemy adresy roznych
            zmiennych.
          + rejestry dostepne tylko w trybie 64-bitowym:
               o 8 rejestrow 8-bitowych: R8B, R9B, ..., R15B (lub R8L,
                 R9L, ..., R15L)
               o 8 rejestrow 16-bitowych: R8W, R9W, ..., R15W
               o 8 rejestrow 32-bitowych: R8D, R9D, ..., R15D
               o 8 rejestrow 64-bitowych: R8, R9, ..., R15
          + rejestry indeksowe:
               o indeks zrodlowy:
                 RSI (64 bity) = ESI (mlodsze 32 bity) + starsze 32 bity,
                 ESI (32 bity) = SI (mlodsze 16 bitow) + starsze 16
                 bitow,
                 SI (16 bitow) = SIL (mlodsze 8 bitow) + starsze 8 bitow
                 (tylko tryb 64-bit)
               o indeks docelowy:
                 RDI (64 bity) = EDI (mlodsze 32 bity) + starsze 32 bity,
                 EDI (32 bity) = DI (mlodsze 16 bitow) + starsze 16
                 bitow,
                 DI (16 bitow) = DIL (mlodsze 8 bitow) + starsze 8 bitow
                 (tylko tryb 64-bit)
            Rejestry indeksowe najczesciej sluza do operacji na dlugich
            lancuchach danych, w tym napisach i tablicach.
          + rejestry wskaznikowe:
               o wskaznik bazowy:
                 RBP (64 bity) = EBP (mlodsze 32 bity) + starsze 32 bity,
                 EBP (32 bity) = BP (mlodsze 16 bitow) + starsze 16
                 bitow.
                 BP (16 bitow) = BPL (mlodsze 8 bitow) + starsze 8 bitow
                 (tylko tryb 64-bit)
                 Najczesciej sluzy do dostepu do zmiennych lokalnych
                 danej funkcji.
               o wskaznik stosu:
                 RSP (64 bity) = ESP (mlodsze 32 bity) + starsze 32 bity,
                 ESP (32 bity) = SP (mlodsze 16 bitow) + starsze 16
                 bitow.
                 SP (16 bitow) = SPL (mlodsze 8 bitow) + starsze 8 bitow
                 (tylko tryb 64-bit)
                 Sluzy do dostepu do stosu (o tym nieco pozniej).
               o wskaznik instrukcji:
                 RIP (64 bity) = EIP (mlodsze 32 bity) + starsze 32 bity,
                 EIP (32 bity) = IP (mlodsze 16 bitow) + starsze 16
                 bitow.
                 Mowi procesorowi, skad ma pobierac instrukcje do
                 wykonywania.
    2. rejestry segmentowe (wszystkie 16-bitowe) - tych najlepiej nie
       dotykac w Linuksie:
          + segment kodu CS - mowi procesorowi, gdzie znajduja sie dla
            niego instrukcje.
          + segment danych DS - ten najczesciej pokazuje na miejsce,
            gdzie trzymamy nasze zmienne.
          + segment stosu SS - dzieki niemu wiemy, w ktorym segmencie
            jest nasz stos. O tym, czym w ogole jest stos, powiem w
            nastepnej czesci.
          + segment dodatkowy ES - zazwyczaj pokazuje na to samo, co DS -
            na nasze zmienne.
          + FS i GS - nie maja specjalnego przeznaczenia. Sa tu na
            wypadek, gdyby zabraklo nam innych rejestrow segmentowych.
    3. rejestr stanu procesora: FLAGI (16-bitowe), E-FLAGI (32-bitowe)
       lub R-FLAGI (64-bitowe).
       Sluza one przede wszystkim do badania wyniku ostatniego
       przeksztalcenia (na przyklad czy nie wystapilo przepelnienie, czy
       wynik jest zerem, itp.). Najwazniejsze flagi to CF (carry flag -
       flaga przeniesienia), OF (overflow flag - flaga przepelnienia), SF
       (sign flag - flaga znaku), ZF (zero flag - flaga zera), IF
       (interrupt flag - flaga przerwan), PF (parity flag - flaga
       parzystosci), DF (direction flag - flaga kierunku).

   Uzycie litery R przed symbolem rejestru, na przyklad RCX, oznacza
   rejestr 64-bitowy, dostepny tylko na procesorach 64-bitowych.

   Uzycie litery E przed symbolem rejestru, na przyklad EAX, oznacza
   rejestr 32-bitowy, dostepny tylko na procesorach rodziny 80386 lub
   lepszych. Nie dotyczy to rejestru ES.

   Napisy

     RAX = EAX+starsze 32 bity; EAX=AX+ starsze 16 bitow; AX=AH+AL

   oznaczaja:
   (przeskocz rozwiniecie rejestru RAX)
                                RAX (64 bity)
                                     |                 EAX (32b)
    00000000000000000000000000000000 | 0000000000000000 | 00000000 | 00000000
                   32b               |        16b       |        AX(16b)
                                     |                  |  AH(8b)  |   AL(8b)

   Napisy

     RSI = ESI+starsze 32 bity; ESI = SI + starsze 16 bitow; SI =
     SIL+starsze 8 bitow

   oznaczaja:
   (przeskocz rozwiniecie rejestru RSI)
                                RSI (64 bity)
                                     |               ESI (32b)
    00000000000000000000000000000000 | 0000000000000000 | 00000000 | 00000000
                   32b               |       16b        |        SI(16b)
                                     |                  |   8b     |  SIL(8b)

   Jedna wazna uwaga - miedzy nazwami rejestrow moze pojawic sie
   dwukropek w dwoch roznych znaczeniach:
     * zapis "DX : AX" (lub 2 dowolne zwykle rejestry) bedzie oznaczac
       liczbe, ktorej starsza czesc znajduje sie w rejestrze po lewej
       stronie (DX), a mlodsza - w tym z prawej (AX). Wartosc liczby
       wynosi DX*65536 + AX.
     * zapis "CS : SI" (rejestr segmentowy + dowolny zwykly) bedzie
       najczesciej oznaczac wskaznik do jakiegos obiektu w pamieci (o
       pamieci opowiem nastepnym razem). Rejestr segmentowy zawiera
       oczywiscie segment, w ktorym znajduje sie ow obiekt, a rejestr
       zwykly - offset (przesuniecie, adres w tym segmencie) tegoz
       obiektu.

   Na razie nie musicie sie przejmowac tymi dwukropkami. Mowie to tylko
   dlatego, zebyscie nie byli zaskoczeni, gdyz w przyszlosci sie pojawia.

   Programista moze odnosic sie bezposrednio do wszystkich wymienionych
   rejestrow, z wyjatkiem *IP oraz flag procesora (z wyjatkami).

   Jak widac po ich rozmiarach, do rejestrow 8-bitowych mozna wpisac
   liczbe z przedzialu 0-255 (lub od -128 do 127, gdy najwyzszy, siodmy
   bit sluzy nam jako bit oznaczajacy znak liczby), w 16-bitowych
   zmieszcza sie liczby 0-65535 (od -32768 do 32767), a w 32-bitowych -
   liczby od 0 do 4.294.967.295 (od -2.147.483.648 do 2.147.483.647).

   Dobrym, choc trudnym w odbiorze zrodlem informacji sa: Intel
   Architecture Software Developer's Manual (IASDM) dostepny ZA DARMO ze
   stron Intela oraz DARMOWE podreczniki AMD64 Architecture Programmer's
   Manual firmy AMD
     _________________________________________________________________

Pisanie i kompilowanie (asemblowanie) swoich programow

   Jak pisac programy w asemblerze?
   Nalezy zaopatrzyc sie w:
     * Edytor tekstu, mogacy zapisywac pliki tekstowe (bez formatowania),
       na przyklad VIM, LPE, Emacs/XEmacs, Joe, Pico, Jed, Kate, KWrite.
     * Kompilator jezyka asembler (patrz dalej)
     * Odpowiedni program laczacy ("konsolidator", ang. linker), chyba ze
       kompilator ma juz taki wbudowany, jak na przyklad FASM

   Wtedy wystarczy napisac w edytorze tekstu plik zawierajacy komendy
   procesora (o tym pozniej), zapisac go z rozszerzeniem .asm lub .s (GNU
   as), po czym uzyc kompilatora, aby przetworzyc program na kod
   rozumiany przez procesor.

   Jakiego kompilatora uzyc?
   Istnieje wiele kompilatorow jezyka asembler pod Linuksa. Do
   najpopularniejszych naleza Netwide Asembler Project (NASM), Flat
   Asembler (FASM), High-Level Asembler (HLA) i Gnu As.

   Mozna je sciagnac z internetu:
   (przeskocz adresy stron kompilatorow)
     * NASM: sf.net/projects/nasm
     * FASM: flatasembler.net
     * HLA: webster.cs.ucr.edu
     * Gnu Asembler znajduje sie w pakiecie binutils (powinien byc w
       kazdej dystrybucji).

   Po skompilowaniu pliku z kodem zrodlowym nalezy uzyc programu
   laczacego - bedziemy uzywac standardowego LD (tego, ktorego uzywaja
   inne kompilatory), gdyz rowniez powinien sie znajdowac w kazdej
   dystrybucji Linuksa.

   Mamy wiec juz wszystko, co potrzeba. Zaczynamy pisac. Bede tutaj
   uzywal skladni NASMa i FASMa (gdyz kompiluja one programy w skladni
   Intela, ktora jest bardziej przejrzysta, mimo iz moze sie wydawac
   "odwrotna").
   Najpierw programy na systemy 32-bitowe:
   (przeskocz pierwszy 32-bitowy program w skladni NASM)
        ; wersja NASM na system 32-bitowy

        section .text                   ; poczatek sekcji kodu.
        global _start           ; linker ld chce miec ten symbol globalny

        _start:                         ; punkt startu programu

                mov     eax, 4          ; numer funkcji systemowej:
                                        ; sys_write - zapisz do pliku
                mov     ebx, 1          ; numer pliku, do ktorego piszemy.
                                        ; 1 = standardowe wyjscie = ekran
                mov     ecx, tekst      ; ECX = adres (offset) tekstu
                mov     edx, dlugosc    ; EDX = dlugosc tekstu
                int     80h             ; wywolujemy funkcje systemowa
                mov     eax, 1          ; numer funkcji systemowej
                                        ; (sys_exit - wyjdz z programu)
                int     80h             ; wywolujemy funkcje systemowa

        section .data                   ; poczatek sekcji danych.

        tekst   db      "Czesc", 0ah    ; nasz napis, ktory wyswietlimy
        dlugosc equ     $ - tekst       ; dlugosc napisu
     _________________________________________________________________

   Teraz wersja dla FASMa:
   (przeskocz pierwszy 32-bitowy program w skladni FASM)
        ; wersja FASM na system 32-bitowy

        format ELF executable           ; typ pliku
        entry _start                    ; punkt startu programu

        segment readable executable     ; poczatek sekcji kodu

        _start:                         ; punkt startu programu

                mov     eax, 4          ; numer funkcji systemowej:
                                        ; sys_write - zapisz do pliku
                mov     ebx, 1          ; numer pliku, do ktorego piszemy.
                                        ; 1 = standardowe wyjscie = ekran
                mov     ecx, tekst      ; ECX = adres (offset) tekstu
                mov     edx, [dlugosc]  ; EDX = dlugosc tekstu
                int     80h             ; wywolujemy funkcje systemowa
                mov     eax, 1          ; numer funkcji systemowej
                                        ; (sys_exit - wyjdz z programu)
                int     80h             ; wywolujemy funkcje systemowa

        segment readable writeable      ; poczatek sekcji danych.
        tekst   db      "Czesc", 0ah    ; nasz napis, ktory wyswietlimy
        dlugosc dd      $ - tekst       ; dlugosc napisu
     _________________________________________________________________

   Teraz program 64-bitowy (x86-64) dla NASMa:
   (przeskocz pierwszy 64-bitowy program w skladni NASM)
        ; wersja NASM na system 64-bitowy (x86-64)

        section .text                   ; poczatek sekcji kodu.
        global _start           ; linker ld chce miec ten symbol globalny

        _start:                         ; punkt startu programu

                mov     rax, 1          ; numer funkcji systemowej:
                                        ; sys_write - zapisz do pliku
                mov     rdi, 1          ; numer pliku, do ktorego piszemy.
                                        ; 1 = standardowe wyjscie = ekran
                mov     rsi, tekst      ; RSI = adres (offset) tekstu
                mov     rdx, dlugosc    ; RDX = dlugosc tekstu
                syscall                 ; wywolujemy funkcje systemowa
                mov     rax, 60         ; numer funkcji systemowej
                                        ; (sys_exit - wyjdz z programu)
                syscall                 ; wywolujemy funkcje systemowa

        section .data                   ; poczatek sekcji danych.

        tekst   db      "Czesc", 0ah    ; nasz napis, ktory wyswietlimy
        dlugosc equ     $ - tekst       ; dlugosc napisu
     _________________________________________________________________

   I w koncu program 64-bitowy dla FASMa:
   (przeskocz pierwszy 64-bitowy program w skladni FASM)
        ; wersja FASM na system 64-bitowy  (x86-64)

        format ELF64 executable         ; typ pliku
        entry _start                    ; punkt startu programu

        segment readable executable     ; poczatek sekcji kodu

        _start:                         ; punkt startu programu
                mov     rax, 1          ; numer funkcji systemowej:
                                        ; sys_write - zapisz do pliku
                mov     rdi, 1          ; numer pliku, do ktorego piszemy.
                                        ; 1 = standardowe wyjscie = ekran
                mov     rsi, tekst      ; RSI = adres (offset) tekstu
                mov     rdx, [dlugosc]  ; RDX = dlugosc tekstu
                syscall                 ; wywolujemy funkcje systemowa
                mov     rax, 60         ; numer funkcji systemowej
                                        ; (sys_exit - wyjdz z programu)
                syscall                 ; wywolujemy funkcje systemowa

        segment readable writeable      ; poczatek sekcji danych.
        tekst   db      "Czesc", 0ah    ; nasz napis, ktory wyswietlimy
        dlugosc dq      $ - tekst       ; dlugosc napisu w trybie 64-bitowym

   Bez paniki! Teraz omowimy dokladnie, co kazda linia robi.
     * linie lub napisy zaczynajace sie srednikiem.
       Traktowane sa jako komentarze i sa calkowicie ignorowane przy
       kompilacji. Rozmiar skompilowanego programu wynikowego nie zalezy
       od ilosci komentarzy. Dlatego najlepiej wstawiac tyle komentarzy,
       aby inni (rowniez my) mogli pozniej zrozumiec nasz kod.
     * (FASM) format ELF executable / format ELF64 executable
       Okresla format (typ) pliku wyjsciowego: wykonywalny plik ELF
       (format uzywany w Linuksie). FASM nie potrzebuje programow
       laczacych, aby utworzyc program. Format ELF64 jest uzywany
       oczywiscie pod systemem 64-bitowym.
     * (FASM) entry _start
       Okresla, gdzie program sie zaczyna. Po uruchomieniu programu
       procesor zaczyna wykonywac komendy zaczynajace sie pod podana
       tutaj etykieta (_start) znajdujaca sie w sekcji kodu.
     * (FASM) segment readable executable
       Okresla nowy segment programu - segment kodu, ktoremu ustawiamy
       odpowiednie atrybuty: do odczytu i do wykonywania. Innym atrybutem
       jest writeable (do zapisu), ktory powinien byc uzywany tylko do
       sekcji danych. Mimo, iz FASM zaakceptuje atrybut writeable dla
       sekcji kodu, nie powinnismy go tam umieszczac. Zapisanie
       czegokolwiek do sekcji kodu moze skonczyc sie bledem naruszenia
       ochrony pamieci (segmentation fault). Mozna jednak w tym segmencie
       umieszczac dane. Ale nalezy to robic tak, aby nie staly sie one
       czescia programu, zwykle wpisuje sie je za ostatnia komenda
       konczaca program. Procesor przeciez nie wie, co jest pod danym
       adresem i z mila checia potraktuje to cos jako instrukcje, co moze
       prowadzic do przykrych konsekwencji. Swoje dane umieszczajcie tak,
       aby w zaden sposob strumien wykonywanych instrukcji nie wszedl na
       nie.
       Dane bedziemy wiec zazwyczaj umieszczac w oddzielnej sekcji.
     * (NASM) section .text
       Wskazuje poczatek segmentu, gdzie znajduje sie kod programu. Mozna
       jednak w tym segmencie umieszczac dane. Ale nalezy to robic tak,
       aby nie staly sie one czescia programu, zwykle wpisuje sie je za
       ostatnia komenda konczaca program. Procesor przeciez nie wie, co
       jest pod danym adresem i z mila checia potraktuje to cos jako
       instrukcje, co moze prowadzic do przykrych konsekwencji. Swoje
       dane umieszczajcie tak, aby w zaden sposob strumien wykonywanych
       instrukcji nie wszedl na nie. Zapisanie czegokolwiek do sekcji
       kodu moze skonczyc sie bledem naruszenia ochrony pamieci
       (segmentation fault), dlatego dane bedziemy zazwyczaj umieszczac w
       oddzielnej sekcji.
     * (NASM) global _start
       Sprawiamy, ze nazwa "_start" bedzie widziana poza tym programem
       (konkretnie to przez linker ld, ktory skompilowana wersje programu
       przerobi na wersje wykonywalna).
     * _start: (z dwukropkiem)
       Etykieta okreslajaca poczatek programu.
     * mov eax, 4 / mov rax, 1
       Do rejestru EAX (32-bitowy) lub RAX (64-bitowy) wstaw (MOV = move,
       przesun) wartosc 4 (1 na systemach x86-64). Jest to numer funkcji
       systemu Linux, ktora chcemy uruchomic. Jesli chcemy skorzystac z
       funkcji systemowych, to zawsze EAX zawiera numer takiej funkcji.
       Numery funkcji roznia sie na roznych architekturach procesorow.
       Poczytajcie moj spis funkcji systemowych.
       Komenda MOV ma 3 wazne ograniczenia:
         1. nie mozna skopiowac jedna komenda MOV komorki pamieci do
            innej komorki pamieci, takie cos:
                mov     [a], [b]
            (gdzie a i b - dwie zmienne w pamieci) jest zabronione.
            O tym, co oznaczaja nawiasy kwadratowe, czyli o adresowaniu
            zmiennych w pamieci - nastepnym razem.
         2. nie mozna skopiowac jedna komenda MOV jednego rejestru
            segmentowego (cs,ds,es,ss,fs,gs) do innego rejestru
            segmentowego, czyli taka operacja
                mov     es, ds
            jest zabroniona. W ogole najlepiej unikac jakichkolwiek
            operacji na rejestrach segmentowych.
         3. Nie mozna do rejestru segmentowego bezposrednio wpisac jakies
            wartosci. Czyli nie mozna
                mov     ds, 0
            Ale mozna:
                mov     bx, 0
                mov     ds, bx
     * mov ebx, 1 / mov rdi, 1
       Do rejestru EBX (32-bitowy) lub RDI (64-bitowy) wstaw 1. Dlaczego
       akurat 1? Zaraz sie przekonamy.
     * mov ecx,tekst / mov rsi, tekst
       Do rejestru ECX (32-bitowy) lub RSI (64-bitowy) wstaw offset
       (adres) etykiety "tekst". Mozna obliczac adresy nie tylko danych,
       ale etykiet znajdujacych sie w kodzie programu.
     * mov edx, dlugosc / mov edx, [dlugosc] / mov rdx, dlugosc / mov
       rdx, [dlugosc]
       Do rejestru EDX (32-bitowy) lub RDX (64-bitowy) wstaw dlugosc
       naszego tekstu. W pierwszym przypadku jest to stala, w drugim
       wartosc pobieramy ze zmiennej.
     * int 80h / syscall
       "Int" = interrupt = przerwanie. Nie jest to jednak znane na
       przyklad z kart dzwiekowych przerwanie typu IRQ. Wywolujac
       przerwanie 80h (128 dziesietnie) lub instrukcje syscall (w trybie
       64-bitowym) uruchamiamy jedna z funkcji Linuksa. Ktora? O tym
       zazwyczaj mowi rejestr akumulatora.
       W tym przypadku EAX = 4 (lub RAX = 1 w trybie 64-bitowym) i jest
       to funkcja zapisu do pliku - sys_write. Funkcja ta przyjmuje 3
       argumenty:
          + W rejestrze EBX (lub RDI, w trybie 64-bitowym) podajemy numer
            (deskryptor) pliku, do ktorego chcemy pisac. U nas EBX (lub
            RDI) = 1 i jest to standardowe wyjscie (zazwyczaj ekran).
          + W rejestrze ECX (lub RSI, w trybie 64-bitowym) podajemy adres
            danych, ktore chcemy zapisac do pliku.
          + W rejestrze EDX (lub RDX, w trybie 64-bitowym) podajemy, ile
            bajtow chcemy zapisac.
       Funkcje systemowe 32-bitowego Linuksa przyjmuja co najwyzej 6
       argumentow, kolejno w rejestrach: EBX, ECX, EDX, ESI, EDI, EBP. W
       EAX oczywiscie jest numer funkcji, tak jak tutaj 4.
       Funkcje systemowe 64-bitowego Linuksa przyjmuja takze co najwyzej
       6 argumentow, kolejno w rejestrach: RDI, RSI, RDX, R10, R8, R9. W
       RAX oczywiscie jest numer funkcji, tak jak tutaj 1. Rejestry RCX i
       R11 sa zamazywane.
     * mov eax, 1 / mov rax, 60
       Do EAX lub RAX wpisujemy numer kolejnej funkcji systemowej -
       sys_exit, ktora spowoduje zamkniecie naszego programu.
     * int 80h / syscall
       Przerwanie systemowe - uruchomienie funkcji wyjscia z programu.
       Numer bledu (taki DOS-owski errorlevel) zwykle umieszczamy w
       EBX/RDI, czego tutaj jednak nie zrobilismy.
     * (NASM) section .data / (FASM) segment readable writeable
       Okresla poczatek sekcji danych. Dane musza byc w osobnej czesci
       programu, bo inaczej nie mozna do nich zapisywac (a na jadrze 2.6
       nawet odczytac).
     * tekst db "Czesc",0ah
       Definicja napisu i znaku przejscia do nowej linii. O tym, jak
       deklarowac zmienne powiem nastepnym razem.
     * dlugosc equ $ - tekst / dlugosc dd $ - tekst / dlugosc dq $ -
       tekst
       Definiujemy stala, ktora przyjmuje wartosc: "adres biezacy" -
       "adres poczatku napisu", czyli dlugosc napisu. W pierwszym
       przypadku jest to stala kompilacji, w drugim i trzecim - zmienna,
       ktora bedzie umieszczona w programie.

   Programik nazywamy "hello.asm" i kompilujemy poleceniem (FASM):
        fasm hello.asm hello

   lub, dla NASMa:
        nasm -f elf hello.asm
        ld -o hello hello.o

   lub, dla NASMa na systemie 64-bitowym:
        nasm -f elf64 hello.asm
        ld -o hello hello.o

   Wyjasnienie opcji:
     * -f elf powoduje, ze plik bedzie skompilowany na 32-bitowy plik
       obiektowy typu ELF (Executable-Linkable Format, typowy dla
       wiekszosci Linuksow). Aby kompilowac programy pod systemem
       64-bitowym, nalezy uzyc formatu elf64
     * -f elf64 powoduje, ze plik bedzie skompilowany na 64-bitowy plik
       obiektowy typu ELF
     * -o nazwa spowoduje nazwanie programu wynikowego.

   Na systemach 64-bitowych mozna tez uruchamiac programy 32-bitowe, ale
   w takim przypadku trzeba je odpowiednio skompilowac i zlinkowac. Dla
   FASMa komenda sie nie zmienia, gdyz wszystkie informacje sa wewnatrz
   pliku. Zmienia sie za to kompilacja na NASMa:
        nasm -f elf hello.asm
        ld -melf_i386 -o hello hello.o

   Kompilacja, nawet programu w asemblerze (zwana czasem asemblacja), ma
   kilka etapow:
     * pre-processing - w tej fazie preprocesor przetwarza dyrektywy
       takie jak definicje stalych, dolaczanie innych plikow do kodu,
       rozwiniecia makr i inne, zanim poda program kompilatorowi do
       kompilacji
     * kompilacja - na tym etapie tworzony jest binarny kod programu
       wynikowego. Podprogram kompilatora sprawdza skladnie instrukcji,
       zmienia je na ich binarne odpowiedniki, przetwarza zmienne na ich
       binarne odpowiedniki, sprawdza, czy wszystkie wykorzystane symbole
       (na przyklad u nas zmienna tekst) sa zadeklarowane, sprawdza, czy
       skoki mieszcza sie w granicach i wykonuje inne niezbedne
       czynnosci, w tym optymalizacje. Pozostawia jednak adresy symboli
       nieuzupelnione.
     * linkowanie (konsolidowanie) - na tym etapie nastepuje sprawdzenie,
       czy wszystkie symbole pozostawione przez kompilator do
       uzupelnienia sa dostepne w samym programie lub innych plikach
       podanych linkerowi do polaczenia. Jesli wszystkie symbole sa
       obecne, nastepuje wstawianie ich adresow do programu wynikowego i
       wygenerowanie samego programu.

   Jesli do programu nie dolaczamy innych juz skompilowanych plikow ani
   bibliotek, to niektore kompilatory nie wymagaja osobnego linkera i
   moga same sobie poradzic z wygenerowaniem programu wyjsciowego. Widac
   to na przyklad w wywolaniach FASMa powyzej.

   Jesli linker LD wyswietla ostrzezenie o wykonywalnym stosie (np.
   missing .note.GNU-stack section implies executable stack), wystarczy
   do kodu programu dopisac na koncu
        section .note.GNU-stack noalloc noexec nowrite progbits

   po czym ponownie skompilowac i zlinkowac.

   Teraz uruchamiamy "./hello" i cieszymy sie swoim dzielem.
   W dalszej czesci kursu bede przedstawial programy czesto tylko
   32-bitowe i czesto tylko dla jednego kompilatora. Ta czesc bedzie
   sluzyc Wam pomoca, jesli chcielibyscie pisac programy pod systemy
   64-bitowe lub pod inny kompilator.

   Milego eksperymentowania.

     Na swiecie jest 10 rodzajow ludzi:
     ci, ktorzy rozumieja liczby binarne i ci, ktorzy nie.

   Informacja dla uzytkownikow *BSD (klawisz dostepu 3)
   Kolejna czesc kursu (klawisz dostepu 4)
   Spis tresci off-line (klawisz dostepu 1)
   Spis tresci on-line (klawisz dostepu 2)
   Ulatwienia dla niepelnosprawnych (klawisz dostepu 0)
     _________________________________________________________________

Cwiczenia

    1. Poeksperymentujcie sobie, wstawiajac rozne znaki do napisu. Na
       przyklad, znaki o kodach ASCII 10 (Line Feed), 13 (Carriage
       Return), 7 (Bell). Na przyklad:
        info    db      "Czesc.", 00, 01, 02, 07, 10, 13, 10, 13
