Wyobraźcie sobie, jakby to było móc programować maszynę bezpośrednio, czyli rozmawiać
z
procesorem bez pośrednictwa struktur wysokiego poziomu, na przykład takich jak spotykamy w języku C.
Bezpośrednie operowanie na procesorze umożliwia przecież pełną kontrolę jego działań! Bez
zbędnych instrukcji i innych śmieci
spowalniających nasze programy.
Czy już czujecie chęć pisania najkrótszych i najszybszych programów na świecie?
Programów, których czasem w ogóle NIE MOŻNA napisać w innych językach? Brzmi wspaniale, prawda?
Tylko pomyślcie o tym, co powiedzieliby znajomi, gdybyście się im pochwalili. Widzicie już te ich zdumione miny?
Miła perspektywa, prawda? No, ale dość już gadania. Zabierajmy się do rzeczy!
Najprostszy dla komputera, gdzie coś jest albo włączone, albo wyłączone. System ten operuje na
liczbach zwanych bitami (bit = binary digit
= cyfra dwójkowa).
Bit przyjmuje jedną z dwóch wartości: 0 lub 1.
Na bajt składa się 8 bitów. Jednym bajtem można przedstawić więc
2^8=256 możliwości.
Przeliczenie liczby zapisanej w systemie dwójkowym na dziesiętny jest proste. Podobnie jak w systemie dziesiętnym, każdą cyfrę mnożymy przez odpowiednią potęgę podstawy (podstawa wynosi 2 w systemie dwójkowym, 10 w systemie dziesiętnym).
Oto przykład (niech daszek ^ oznacza potęgowanie):
1010 1001 dwójkowo =
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 dziesiętnie (lub dec
, od decimal
).
169 | 84 | 1 42 | 0 21 | 0 10 | 1 5 | 0 2 | 1 1 | 0 0 | 1Wspak dostajemy: 1010 1001, czyli wyjściową liczbę.
Jako że system dwójkowy ma mniej cyfr niż dziesiętny, do przedstawienia względnie małych
liczb trzeba użyć dużo zer i jedynek. Jako że bajt ma 8 bitów, podzielono go na dwie równe,
czterobitowe części. Teraz bajt można już reprezentować dwoma znakami, a nie ośmioma. Na każdy
taki znak składa się 2^4=16 możliwości. Stąd wzięła się nazwa szesnastkowy
.
Powstał jednak problem: cyfr jest tylko 10, a trzeba mieć 16. Co zrobić?
Postanowiono liczbom 10-15 przyporządkować odpowiednio znaki A-F.
Na przykład
Liczba 255 dziesiętnie = 1111 1111 binarnie = FF szesnastkowo (1111 bin = 15 dec = F hex)
Liczba 150 dziesiętnie = 1001 0110 binarnie = 96 szesnastkowo.
Należy zauważyć ścisły związek między systemem dwójkowym i szesnastkowym:
1 cyfra szesnastkowa to 4 bity, co umożliwia błyskawiczne przeliczanie między obydwoma
systemami: wystarczy tłumaczyć
po 4 bity (1 cyfrę hex) na raz i zrobione.
Przeliczenie liczby zapisanej w systemie szesnastkowym na dziesiętny jest równie proste, jak tłumaczenie z dwójkowego na dziesiętny. Każdą cyfrę mnożymy przez odpowiednią potęgę podstawy (podstawa wynosi 16 w systemie szesnastkowym).
Oto przykład:
10A szesnastkowo =
1*16^2 + 0*16^1 + A*16^0 =
256 + 0 + 10 =
266 dziesiętnie.
266 | 16 | 10 1 | 0 0 | 1Wspak dostajemy kolejno: 1, 0 i 10, czyli 10A, czyli wyjściową liczbę.
Co to w ogóle jest asembler?
Asembler jest to język programowania, należący do języków niskiego poziomu. Znaczy to tyle, że jednej komendzie asemblera odpowiada dokładnie jeden rozkaz procesora. Asembler operuje na rejestrach procesora.
A co to jest rejestr procesora?
Rejestr procesora to zespół układów elektronicznych, mogący przechowywać informacje (taka własna pamięć wewnętrzna procesora).
Zaraz podam Wam podstawowe rejestry, na których
będziemy operować. Wiem, że ich liczba może przerazić, ale od razu mówię, abyście
NIE uczyli
się tego wszystkiego na pamięć! Najlepiej zrobicie, czytając poniższą listę tylko 2 razy, a
potem wracali do niej, gdy jakikolwiek rejestr pojawi się w programach, które
będę później prezentował w ramach tego kursu.
Oto lista interesujących nas rejestrów:
Użycie litery R
przed symbolem rejestru, na przykład RCX, oznacza rejestr 64-bitowy,
dostępny tylko na procesorach 64-bitowych.
Użycie litery E
przed symbolem rejestru, na przykład EAX, oznacza rejestr 32-bitowy,
dostępny tylko na procesorach rodziny 80386 lub lepszych.
Nie dotyczy to rejestru ES.
RAX = EAX+starsze 32 bity; EAX=AX + starsze 16 bitów; AX=AH+ALoznaczają takie zależności między tymi rejestrami:
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 bitów; SI = SIL+starsze 8 bitówoznaczają:
RSI (64 bity) | ESI (32b) 00000000000000000000000000000000 | 0000000000000000 | 00000000 | 00000000 32b | 16b | SI(16b) | | 8b | SIL(8b)
Tak, w DOSie można używać rejestrów 32-bitowych (o ile posiada się 80386 lub nowszy). Można nawet 64-bitowych, jeśli tylko posiada się właściwy procesor.
Jedna ważna uwaga - między nazwami rejestrów może pojawić się dwukropek w dwóch różnych znaczeniach:
DX : AX(lub 2 dowolne zwykłe rejestry) będzie oznaczać liczbę, której starsza część znajduje się w rejestrze po lewej stronie (DX), a młodsza - w tym z prawej (AX). Wartość liczby wynosi DX*65536 + AX.
CS : SI(rejestr segmentowy + dowolny zwykły) będzie najczęściej oznaczać wskaźnik do jakiegoś obiektu w pamięci (o pamięci opowiem następnym razem). Rejestr segmentowy zawiera oczywiście segment, w którym znajduje się ów obiekt, a rejestr zwykły - offset (przesunięcie, adres w tym segmencie) tegoż obiektu.
Na razie nie musicie się przejmować tymi dwukropkami. Mówię to tylko dlatego, żebyście nie byli zaskoczeni, gdyż w przyszłości się pojawią.
Programista może odnosić się bezpośrednio do wszystkich wymienionych rejestrów, z wyjątkiem *IP oraz flag procesora (z wyjątkami).
Jak widać po ich rozmiarach, do rejestrów 8-bitowych można wpisać liczbę z przedziału 0-255 (lub od -128 do 127, gdy najwyższy, siódmy bit służy nam jako bit oznaczający znak liczby), w 16-bitowych zmieszczą się 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, choć trudnym w odbiorze źródłem informacji są: Intel Architecture Software Developer's Manual (IASDM) dostępny ZA DARMO ze stron Intela oraz DARMOWE podręczniki AMD64 Architecture Programmer's Manual firmy AMDJak pisać programy w asemblerze?
Należy zaopatrzyć się w:
Wtedy wystarczy napisać w edytorze tekstu plik zawierający komendy procesora (o tym później), zapisać go z rozszerzeniem .ASM, po czym użyć kompilatora, aby przetworzyć program na kod rozumiany przez procesor.
Jakiego kompilatora użyć?
Istnieje wiele kompilatorów języka asembler. Do najpopularniejszych należą Turbo asembler firmy Borland, Microsoft Macro asembler (MASM), Netwide asembler Project (NASM), A86/A386, NBASM, FASM, HLA.
Można je ściągnąć z internetu:
(przeskocz adresy stron kompilatorów)
Po skompilowaniu pliku z kodem źródłowym należy użyć programu łączącego, dostępnego zwykle z odpowiednim kompilatorem (na przykład tlink z tasm, link z masm).
Mamy więc już wszystko, co potrzeba. Zaczynamy pisać. Będę tutaj używał składni Turbo asemblera zgodnego z MASMem oraz FASMa i NASMa.
; wersja TASM .model tiny .code org 100h start: mov ah, 9 mov dx, offset info int 21h mov ah, 0 int 16h mov ax, 4C00h int 21h info db "Czesc.$" end start
; wersja NASM ; nie ma ".model" ani ".code" ; tu można wstawić: ; section .text ; aby dać znać NASMowi, że to będzie w sekcji kodu. ; Nie jest to jednak wymagane, bo to jest sekcja domyślna. org 100h start: ; nawet tego NASM nie wymaga mov ah, 9 mov dx, info ; nie ma słowa "offset" int 21h mov ah, 0 int 16h mov ax, 4C00h int 21h info db "Czesc.$" ; nie ma "end start"
; wersja FASM format binary ; nie ma ".model" ani ".code" org 100h start: ; nawet tego FASM nie wymaga mov ah, 9 mov dx, info ; nie ma słowa "offset" int 21h mov ah, 0 int 16h mov ax, 4C00h int 21h info db "Czesc.$" ; nie ma "end start"
Bez paniki! Teraz omówimy dokładnie, co każda linia robi.
Traktowane są jako komentarze i są całkowicie ignorowane przy kompilacji. Rozmiar skompilowanego programu wynikowego nie zależy od ilości komentarzy. Dlatego najlepiej wstawiać tyle komentarzy, aby inni (również my) mogli później zrozumieć nasz kod.
.model tiny
(pamiętajcie o kropce) lub
format binary
(w FASMie)
.code
(też z kropką)
Wskazuje początek segmentu, gdzie znajduje się kod programu. Można jednak w tym segmencie
umieszczać dane, ale należy to robić tak, aby nie stały się one częścią programu. Zwykle
wpisuje się je za ostatnią komendą kończącą program. Procesor przecież nie wie, co jest pod
danym adresem i z miłą chęcią potraktuje to coś jako instrukcję, co może prowadzić do
przykrych konsekwencji. Swoje dane umieszczajcie tak, aby w żaden sposób strumień
wykonywanych instrukcji nie wszedł na nie.
Są też inne dyrektywy: .data
, deklarująca początek segmentu z danymi oraz
.stack
,
deklarująca segment stosu (o tym później), której nie można używać w programach typu
.com
, gdzie stos jest automatycznie ustawiany.
org 100h
(bez kropki)
Ta linia mówi kompilatorowi, że nasz kod będzie (dopiero po uruchomieniu!) znajdował się
pod adresem 100h (256 dziesiętnie) w swoim segmencie. To jest typowe dla programów .com.
DOS, uruchamiając taki program, szuka wolnego segmentu i kod programu umieszcza dopiero
pod adresem (czasami zwanym offsetem - przesunięciem) 100h.
Co jest więc wcześniej? Wiele ciekawych informacji, z których chyba najczęściej używaną jest
linia poleceń programu (parametry uruchomienia, na przykład różne opcje itd.).
Dyrektywa org
podana na początku kodu NIE wpływa na rozmiar programu, ułatwia kompilatorowi
określenie adresów różnych etykiet (w tym danych) znajdujących się w programie.
Jeśli chcemy tworzyć programy typu .com, należy zawsze podać org 100h
i opcję /t dla
Turbo Linkera.
start:
(z dwukropkiem) i end start
(bez dwukropka)Mówią kompilatorowi, gdzie są odpowiednio: początek i koniec programu.
mov ah,9
Do 8-bitowego rejestru AH (górnej części 16-bitowego AX) wstaw (MOV = move, przesuń) wartość 9. Po co i czemu akurat 9? Zaraz zobaczymy.
Najpierw powiem o czymś innym: komenda MOV
ma ważne ograniczenia:
MOV
komórki pamięci do innej komórki pamięci, czyli
takie coś:
mov [a], [b](gdzie a i b - dwie zmienne w pamięci) jest zabronione.
MOV
jednego rejestru segmentowego (cs, ds, es, ss, fs, gs)
do innego rejestru segmentowego, czyli działanie
mov es, dsjest zabronione.
mov ds, 0ale można:
mov bx, 0 mov ds, bx
mov dx,offset info
Do rejestru danych (DX, 16-bitowy) wstaw offset (adres względem
początku segmentu) etykiety info
. Można obliczać adresy nie tylko danych, ale etykiet
znajdujących się w kodzie programu.
int 21h
INT = interrupt = przerwanie. Nie jest to jednak znane na przykład z kart dźwiękowych przerwanie typu IRQ. Wywołując przerwanie 21h (33 dziesiętnie) uruchamiamy jedną z funkcji DOSa. Którą? O tym zazwyczaj mówi rejestr AX. W spisie przerwań Ralfa Brown'a (RBIL) patrzymy:
(przeskocz opis int 21h, ah=9) INT 21 - DOS 1+ - WRITE STRING TO STANDARD OUTPUT
AH = 09h
DS:DX -> $-terminated string
Już widzimy, czemu do AH poszła wartość 9. Chcieliśmy uruchomić
funkcję, która wypisuje na
na ekran ciąg znaków zakończony znakiem dolara. Adres tego ciągu musi się znajdować w parze
rejestrów: DS wskazuje segment, w którym znajduje się ten ciąg, a DX - jego adres w tym
segmencie. Dlatego było mov dx,offset info
.
Zaraz, zaraz! Ale przecież my nic nie robiliśmy z DS, a dane znajdują się przecież w segmencie
kodu! I to działa?
Oczywiście! Programy typu .com są małe. Tak małe, że mieszczą się w jednym segmencie
pamięci.
Dlatego przy ich uruchamianiu DOS ustawia nam CS=DS=ES=SS. Nie musimy się więc o to martwić.
Opis podstawowych funkcji kilku przerwań znajdziecie na jednej z moich podstron,
poświeconej najczęściej stosowanym funkcjom przerwań,
gdzie znajdziecie także instrukcje budowania RBIL.
mov ah,0
Do rejestru AH wpisz 0. Czemu? Zaraz zobaczymy. Ale najpierw wspomnę o czymś innym. Otóż,
mov rejestr, 0
nie jest najlepszym sposobem na wyzerowanie danego rejestru. Szybsze lub krótsze są dwa inne:
xor rej1, rej1 ; 1 xor 1 = 0 oraz 0 xor 0 = 0.
; Stąd "coś XOR to_samo_coś"
; zawsze daje 0.
sub rej1, rej1 ; sub=subtract=odejmij.
; rej1 - rej1 = 0
Ja zwykle używam XOR.
int 16h
Kolejne przerwanie, więc znowu do listy Ralfa Brown'a:
(przeskocz opis int 16h, ah=0) INT 16 - KEYBOARD - GET KEYSTROKE
AH = 00h
Return: AH = BIOS scan code
AL = ASCII character
Ta funkcja pobiera znak z klawiatury i zwraca go w rejestrze AL. Jeśli nie naciśnięto nic, poczeka, aż użytkownik naciśnie.
mov ax,4c00h
Do rejestru AX wpisujemy wartość 4c00 szesnastkowo.
int 21h
Znów przerwanie DOSa, funkcja 4ch. Patrzymy do RBIL:
(przeskocz opis int 21h, ah=4ch) INT 21 - DOS 2+ - "EXIT" - TERMINATE WITH RETURN CODE
AH = 4Ch
AL = return code
Return: never returns
Jak widzimy, ta funkcja powoduje wyjście z powrotem do DOSa, z numerem błędu (errorlevel) w AL równym 0. Przyjmuje się, że 0 oznacza, iż program zakończył się bez błędów. Jak widać po rozmiarze rejestru AL (8 bitów), program może wyjść z 2^8=256 różnymi numerami błędu.
info db "Czesc.$"
Etykietą info
opisujemy kilka bajtów, w tym przypadku zapisanych jako ciąg znaków.
A po co znak dolara $? Jak sobie przypomnimy, funkcja 9. przerwania DOSa wypisuje ciąg znaków zakończony
właśnie na znak dolara $. Gdyby tego znaczka nie było, DOS wypisywałby różne śmieci z pamięci, aż trafiłby na przypadkowy
znak dolara $ nie wiadomo gdzie. O deklarowaniu zmiennych będzie w następnej części.
end start
Koniec programu.
Programik kompilujemy poleceniem:
tasm naszplik.asm tlink naszplik.obj /t
(opcja /t - utworzy plik typu .com).
Jeśli otrzymujecie komunikaty o braku pamięci, możecie wypróbować następujący sposób:
tasmx naszplik.asm tlink naszplik.obj /t /3
Dla NASMa kompilacja wygląda tak:
nasm -o naszplik.com -f bin naszplik.asm
(-o = nazwa pliku wyjściowego
-f = format pliku. Bin = binarny = na przykład COM lub SYS).
A dla FASMa:
fasm naszplik.asm naszplik.com
Kompilacja, nawet programu w asemblerze (zwana czasem asemblacją), ma kilka etapów:
info
) są zadeklarowane, sprawdza, czy skoki mieszczą się w granicach i wykonuje
inne niezbędne czynności, w tym optymalizację. Pozostawia jednak adresy symboli
nieuzupełnione.Jeśli do programu nie dołączamy innych już skompilowanych plików ani bibliotek, to niektóre kompilatory nie wymagają osobnego linkera i mogą same sobie poradzić z wygenerowaniem programu wyjściowego. Widać to na przykład w wywołaniach NASMa i FASMa powyżej.
Teraz uruchamiamy naszplik.com i cieszymy się swoim dziełem.
Miłego eksperymentowania.
Na świecie jest 10 rodzajów ludzi:
ci, którzy rozumieją liczby binarne i ci, którzy nie.
info db "Czesc.", 00, 01, 02, 07, 10, 13, 10, 13, "$"