Skoro czytasz tą stronę drogi czytelniku zapewne jesteś
zainteresowany poznaniem tak wspaniałej rzeczy jaką jest język
asemblera. A może zastanawiasz się w jakim celu w dzisiejszych
czasach - czasach języków coraz to "wyżej" poziomowych.
Pewnie dręczy cię pytanie - w jakim celu uczyć się języka
niskopoziomowego jakim jest asembler. Po co męczyć się godzinami
pisząc setki - jak nie tysiące linijek strasznie ciężkiego do
zrozumienia (ale tylko pozornie) kodu - kiedy w zasadzie wszystko
czego zapragniesz można "wyklikać". Poprzez serię tych
artykułów postaram się przedstawić wam zalety (jak i wady)
asemblera, sposoby jego wykorzystania - zwłaszcza w ukierunkowaniu
na programowanie gier komputerowych. Postaram się sprawić abyście
pokochali programowanie w języku asemblera tak jak ja je pokochałem
;). Tak więc zapraszam do podążania za mną do krainy gdzie każda
jedynka czy zero ma znaczenie ;).
Po co nam asembler? - lub - w jakim celu programujemy w
asemblerze? - to pytanie pewnie dręczy was tak jak mnie kiedyś
dręczyło. Więc - odpowiedź na to pytania jest banalnie prosta:
Asembler jest nam potrzebny z trzech prostych powodów:
-
Szybkość
- Szybkość
- Szybkość
Programy powstałe
w asemblerze nie tylko są bardzo szybkie (zawierają tylko kod
niezbędny do wykonania programu - czego nie uzyskamy programując w
językach wysokiego poziomu), ale także bardzo małe (brak zbędnego
kodu = mniejszy rozmiar samego pliku programu). Ze względu na te
powyższe elementy asembler jest przydatny w wielu przypadkach - ale
do tego stopniowo sami dojdziemy ;)
Nudne - ale trzeba przez to przejść aby nie było jakiś
niepotrzebnych pytań ani problemów. Na początek należy wspomnieć
o odmianach języka asemblera (tak, tak - nie jest on jeden ;)). My
zajmiemy się językiem asemblera dla architektury x86
- czyli dla procesorów firmy Intel - Pentium, Pentium Pro, Pentium
MMX, Pentium II, Pentium III, Pentium 4, Core 2, etc. - i tutaj
wyprzedzając pytania - zdecydowana większość kodu który pojawi
się w tym kursie powinna bezproblemowo działać pod procesorami AMD
(K5,K6,K7,K8,K10) jak i również na niektórych procesorach innych
producentów. Tutaj odsyłam zainteresowanych do poczytania więcej
na temat architektury x86 - nie ma sensu abym się tutaj teraz na ten
temat rozpisywał. Później pojawią się pewne elementy które
niestety nie będą działać na procesorach firm innych niż Intel -
ale o tym wspomnę w odpowiednim momencie.
Dobór
kompilatora (asemblera) - tutaj są dwa najpopularniejsze nurty
(chociaż tak naprawdę jest ich trochę więcej):
NASM
- używający składni Intel'owskiej
GASM - GNU
Assembler - używający składni AT&T (tej będę używał w tym
kursie)
System operacyjny - tez bardzo ważna kwestia.
Programowanie w języku asemblera pod Windowsem/Dosem rożni się od
programowania pod Linux'em. Ja będę używał systemu Gentoo
Linux 2008.0 AMD64 (architektura 64-bitowa) jednak kody
które pojawią się w tym kursie powinny działać bez większych
problemów pod dowolną dystrybucją linux'a. Windowsowców zachęcam
do zainstalowania jakiejkolwiek dystrybucji Linux chociażby jako
wirtualną maszynę (ja używam Sun xVM VirtualBox).
Będziemy później programować w języku asemblera pod
Windowsem/Dosem ale do poznania podstaw potrzebny będzie
Linux.
Instalacja GASM - wybraliśmy (w zasadzie to ja
wybrałem ;P) system operacyjny jak i kompilator (asembler).
Instalacja kompilatora pod Gentoo będzie wręcz banalnie
prosta - wystarczy w konsoli wpisać
emerge
binutils
i wszystko co nam potrzebne (asembler i linker) powinien się
sam ściągnąć, skompilować i zainstalować. Powinniśmy uzyskać
w konsoli dostęp do dwóch ważnych komend - as (kompilacja) i ld
(linkowanie).
W następnej części spróbujemy napisać nasz pierwszy
program, poznamy rejestry oraz porównamy składnię Intela i AT&T
- zrobimy to w prostym celu - będziemy mogli sobie tłumaczyć kod z
składni Intel owskiej na AT&T oraz odwrotnie. Dzięki temu
będziemy mogli korzystać z kompilatorów używających składni
Intel-owskiej jak i również zrozumieć wiele kodów napisanych w
składni Intel owskiej. W kolejnych częściach poznamy podstawy
programowania w języku asemblera - napiszemy parę ciekawych
programów, następnie zaczniemy korzystać z koprocesora i zrobimy
parę operacji na liczbach zmiennoprzecinkowych. Poznamy rozszerzenia
instrukcji procesora takie jak MMX, SSE, SSE2, SSE3
(prawdopodobnie również SSE4) - dla AMD-owców
postaram się trochę omówić rozszerzenie 3DNow!,
następnie powykorzystujemy (:P) trochę funkcje biblioteczne języka
C/C++ jak i będziemy pisać w języku asemblera pojedyncze funkcje
które później wykorzystywać będziemy w programach pisanych w C.
Na koniec najprawdopodobniej napiszemy jakąś grę (po to tu
jesteśmy w końcu) :P
Jeśli macie jakieś pytania
zapraszam na nasze forum ;) Do zobaczenia wkrótce.
Skoro czytasz ten tekst drogi czytelniku oznacza to że w dalszym ciągu chcesz nauczyć się programować w języku asemblera i pierwsza część kursu Cię do tego nie zniechęciła ;). Jednak zanim zaczniemy cokolwiek na poważnie pisać w języku asemblera musimy przebrnąć przez część teoretyczną. Jeśli to przetrwacie - to przetrwacie już wszystko ;). A tak na poważnie to teraz pora na poznanie konstrukcji samego procesora. Będzie to niezbędne jeśli chcemy dobrze programować w języku asemblera. Pod każdą architekturę język asemblera wygląda inaczej (co jest logiczne) - my zajmiemy się (jak już było wspomniane) programowaniem pod architekturę x86. Większość kodów które napiszemy w dalszej części tego kursu powinna działać zarówno pod procesorami firmy Intel jak i pod procesorami firmy AMD - chociaż jak później się okaże niektóre niestety pod AMD działać nie będą. Do rzeczy.
Ponieważ procesory 64-bitowe są już jakiś czas na rynku, zapewne większość z was taki posiada. Dlatego głupio było by nie wspomnieć o tymże procesorze (wykracza to trochę poza tematykę bo to już architektura x86_64 lub AMD64... jak zwał, tak zwał). Jednak wszystkie kody w tym kursie będą pisane pod procesory 32-bitowe (na końcu ewentualnie możemy napisać coś pod 64 bity).
Rejestry procesora są to niewielkie komórki pamięci umieszczone w samym procesorze - przez co bardzo szybko dla procesora dostępne. Przechowuje się w nich wyniki tymczasowych obliczeń, adresy etc. Procesor architektury x86 zawiera następujące rejestry (dla architektury x86_64 rejestry te są większe - 64 bitowe - jak pokazane poniżej):
Uwaga: rejestry RAX,RBX,RCX,RDX,RBP,RSI,RDI,RSP,RIP występują tylko w procesorach 64-bitowych
8 rejestrów ogólnego użytku:
1. Akumulator (rejestr służący głównie do wykonywania działań matematycznych):
RAX (64 bity) = EAX (młodsze 32 bity) + starsze 32 bity
EAX (32 bity) = AX (młodsze 16 bitów) + starsze 16 bitów
AX (16 bitów) = AL (młodsze 8 bitów) + AH (starsze 8 bitów)
RAX (64 bity) |
|||||||
|
EAX (32 bity) |
||||||
|
|
|
AX (16 bitów) |
||||
|
|
|
|
|
|
AH (8 bitów) |
AL (8 bitów) |
2. Rejestr bazowy (wskaźnik do danych w pamięci np. tablicy):
RBX (64 bity) = EBX (młodsze 32 bity) + starsze 32 bity
EBX (32 bity) = BX (młodsze 16 bitów) + starsze 16 bitów
BX (16 bitów) = BL (młodsze 8 bitów) + BH (starsze 8 bitów)
RBX (64 bity) |
|||||||
|
EBX (32 bity) |
||||||
|
|
|
BX (16 bitów) |
||||
|
|
|
|
|
|
BH (8 bitów) |
BL (8 bitów) |
3. Licznik (rejestr przydatny np. w pętlach):
RCX (64 bity) = ECX (młodsze 32 bity) + starsze 32 bity
ECX (32 bity) = CX (młodsze 16 bitów) + starsze 16 bitów
CX (16 bitów) = CL (młodsze 8 bitów) + CH (starsze 8 bitów)
RCX (64 bity) |
|||||||
|
ECX (32 bity) |
||||||
|
|
|
CX (16 bitów) |
||||
|
|
|
|
|
|
CH (8 bitów) |
CL (8 bitów) |
4. Rejestr danych (przechowywanie różnych adresów etc.)
RDX (64 bity) = EDX (młodsze 32 bity) + starsze 32 bity
EDX (32 bity) = DX (młodsze 16 bitów) + starsze 16 bitów
DX (16 bitów) = DL (młodsze 8 bitów) + DH (starsze 8 bitów)
RDX (64 bity) |
|||||||
|
EDX (32 bity) |
||||||
|
|
|
DX (16 bitów) |
||||
|
|
|
|
|
|
DH (8 bitów) |
DL (8 bitów) |
5. Rejestr indeksowy (indeks źródłowy)
RSI (64 bity) = ESI (młodsze 32 bity) + starsze 32 bity
ESI (32 bity) = SI (młodsze 16 bitów) + starsze 16 bitów
RSI (64 bity) |
|||
|
ESI (32 bity) |
||
|
|
|
SI (16 bitów) |
6. Rejestr indeksowy (indeks docelowy)
RDI (64 bity) = EDI (młodsze 32 bity) + starsze 32 bity
EDI (32 bity) = DI (młodsze 16 bitów) + starsze 16 bitów
RDI (64 bity) |
|||
|
EDI (32 bity) |
||
|
|
|
DI (16 bitów) |
7. Wskaźnik bazowy
RBP (64 bity) = EBP (młodsze 32 bity) + starsze 32 bity
EBP (32 bity) = BP (młodsze 16 bitów) + starsze 16 bitów
RBP (64 bity) |
|||
|
EBP (32 bity) |
||
|
|
|
BP (16 bitów) |
8. Wskaźnik stosu
RSP (64 bity) = ESP (młodsze 32 bity) + starsze 32 bity
ESP (32 bity) = SP (młodsze 16 bitów) + starsze 16 bitów
RSP (64 bity) |
|||
|
ESP (32 bity) |
||
|
|
|
SP (16 bitów) |
Rejestr FLAGS (flagi procesora):
1. CF (carry flag) - flaga przeniesienia
Przyjmuje wartość 1 jeśli w wyniku ostatnio wykonanej operacji nastąpiło przeniesienie z bitu najstarszego na zewnątrz lub nastąpiła pożyczka z zewnątrz do bitu najstarszego. Jeśli nic z tych rzeczy nie nastąpiło flaga jest zerowana.
2. PF (parity flag) - flaga parzystości
Przyjmuje wartość 1 jeśli w wyniku ostatnio wykonanej operacji liczba bitów o wartości 1 w młodszym bajcie jest parzysta. W przeciwnym wypadku flaga jest zerowana.
3. AF (auxiliary flag) - flaga przeniesienia pomocniczego
Jak CF tylko w przypadku pożyczki z bitu 4 na 3 lub z 3 na 4. W przeciwnym wypadku flaga jest zerowana.
4. ZF (zero flag) - flaga zera
Przyjmuje wartość 1 jeśli wynik ostatnio wykonanej operacji jest równy 0. W przeciwnym wypadku flaga jest zerowana.
5. SF (sign flag) - flaga znaku
Wartość flagi jest zgodna z bitem znaku (najbardziej znaczącym) w otrzymanym w wyniku ostatniej operacji wyniku.
6. TF (trap flag) - flaga pracy krokowej
Wartość równa 1 wywołuje wprowadzenie procesora w tryb pracy single step modus (umożliwiającą po każdym wykonanym rozkazie wywołanie przerwania). Wartość 0 oznacza normalną pracę procesora.
7. IF (interrupt flag) - flaga przerwania
Wartość równa 1 powoduje obsługiwanie przez procesor systemu przerwań. W przeciwnym wypadku przerwania są ignorowane.
8. DF (direction flag) - flaga kierunku
Wartość równa 1 powoduje przetwarzanie łańcuchów przy rosnących adresach. W przeciwnym wypadku - przy malejących
9. OF (overflow flag) - flaga przepełnienia
Przyjmuje wartość 1 w przypadku wystąpienia przepełnienia (wystąpiło przeniesienie na bit znaku, lub pożyczka z tegoż) i jednocześnie CF=0. W przeciwnym wypadku flaga jest zerowana.
Rejestr EIP:
1. Wskaźnik instrukcji (mówi procesorowi skąd ten ma pobierać kolejne instrukcje)
RIP (64 bity) = EIP (młodsze 32 bity) + starsze 32 bity
EIP (32 bity) = IP (młodsze 16 bitów) + starsze 16 bitów
Oprócz tego mamy jeszcze:
8 rejestrów koprocesora (80-bitowych) - O koprocesorze napiszę w którejś kolejnej części tego kursu. Na razie wystarczy wiedzieć że są ;)
8 rejestrów MMX (64-bitowych, mapowanych na rejestrach koprocesora) - też wspomniemy o nich później
8 rejestrów XMM (128-bitowych) - j/w
Poznaliśmy właśnie rejestry procesora architektury x86 (oraz wiemy o istnieniu ich rozszerzonej wersji w x86_64). Będziemy z tej wiedzy cały czas korzystać więc radzę ją dosyć dobrze opanować. W następnej części napiszemy swój pierwszy program w języku asemblera ;)