Programowanie w języku Asembler x86
Podstawowe informacje na temat języka assembly
W tym rozdziale przedstawię podstawowe informacje dotyczące
języka assembly, systemów liczbowych, konwersji między nimi i podstawowych
operacji logicznych.
Język assembly jest niskopoziomowym językiem programowania
komputerów i mikroprocesorów wykazującym duże powiązanie między samym językiem
a kodem maszynowym danej architektury. Każdy język assembly jest ściśle
związany z daną architekturą, dla porównania większość języków wysokiego
poziomu działa na wielu architekturach, lecz wymagają wcześniej kompilacji lub
są interpretowane.
Jednym z najczęściej używanych języków assembly jest
assembly x86.
Język assembly używa mnemoników do odwzorowania każdej
instrukcji maszynowej. Każda instrukcja składa się, z co najmniej jednego
operandu. Przykładami operandów są etykiety, symbole i wyrażenia.
Program, który zamienia kod zapisany w języku assembly na
kod maszynowy to assembler. Oblicza
on również wyrażenia stałe zapisane w kodzie programu i zamienia wszystkie
elementy, będące symbolem np. nazwa funkcji na jednoznaczny adres w pamięci
danego elementu. Jest to jedna z kluczowych funkcji assemblera, ponieważ w
ten sposób oszczędza programiście żmudnej pracy ręcznego obliczania adresów.
Teoretycznie kod będący wynikiem działania assemblera powinien być identyczny,
co do kodu źródłowego zapisanego w języku assembly, lecz zapisany w formie
maszynowej, natomiast niektóre assemblery wprowadzają pewne optymalizacje,
przez co mogą wystąpić drobne różnice.
Programy komputerowe zazwyczaj składają się z kilku części
lub modułów, programem, który łączy pliki obiektowe (zawierające kod maszynowy)
w plik wykonywalny jest linker.
Reprezentacja liczb naturalnych
Komputer do reprezentacji danych używa bitów, czyli
jednostek danych mogących przyjmować wartość 0 lub 1. Najmniejszą dostępną
jednostką danych, na której można operować jest bajt, czyli zbiór 8 bitów. Bity
w bajcie numerujemy od prawej do lewej strony. Bit po lewej stronie ma
największą wartość i nazywamy go (MSB) most significant bit (najbardziej znaczący bit), natomiast po
prawej ma najmniejszą wartość i nazywamy go least significant bit (LSB, najmniej znaczący bit).
MSB
|
LSB
|
|||||||
1
|
1
|
1
|
0
|
0
|
1
|
1
|
0
|
Wartość
|
7
|
6
|
5
|
4
|
3
|
2
|
1
|
0
|
Numer bitu
|
W powyższym zapisie wykorzystujemy jedynie dwie wartości, 1
i 0, dlatego taki system zapisu nazywa się system dwójkowy. Podstawa każdego
systemu wynika z ilości wartości, którą można przy pomocy jego zapisać, więc
dla systemu dwójkowego podstawa wynosi 2. W informatyce popularne są również
systemy ósemkowe i szesnastkowe, natomiast ten wykorzystywany przez nas, na co
dzień jest system dziesiętnym.
Poniższa tabela pokazuje wartości używane w wyżej opisanych
systemach.
System
|
Podstawa
|
Możliwe wartości
|
Dwójkowy (binarny)
|
2
|
0, 1
|
Ósemkowy (oktalny)
|
8
|
0, 1, 2, 3, 4, 5, 6, 7
|
Dziesiątkowy (decymalny)
|
10
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
|
Szesnastkowy (heksadecymalny)
|
16
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
|
W przypadku systemu szesnastkowego, nie ma możliwości zapisu
wyłączenie w cyfrach, z racji tego, że cyfr mamy 10, a ów system potrzebuje 16
symboli do zapisu, dlatego wykorzystuje się 10 cyfr oraz 6 pierwszych liter
alfabetu łacińskiego.
Zamiana z systemu dziesiątkowego na system dwójkowy jest prosta,
wykorzystuje się do tego dzielenie przez dwa z resztą.
Dzielenie
|
Wynik
|
Reszta
|
74 : 2
|
37
|
0
|
37 : 2
|
18
|
1
|
18
|
9
|
0
|
9
|
4
|
1
|
4
|
2
|
0
|
2
|
1
|
0
|
1
|
0
|
1
|
W celu odczytania wartości binarnej, należy odczytać kolejne
wartości reszty zacząwszy od dołu. W tym przypadku będzie to 1001010. Szybkie
sprawdzenie w kalkulatorze systemowym, czy nasze obliczenia są poprawne.
Jak widać wynik jest poprawny.
A jak wygląda zamiana w odwrotną stronę, czyli z systemu
dwójkowego na system dziesiętny. Każda pozycja jedynki lub zera odpowieda
kolejnej potędze liczby dwa. Przedstawia to poniższa tabela.
2n
|
Wartość
|
20
|
1
|
21
|
2
|
22
|
4
|
23
|
8
|
24
|
16
|
25
|
32
|
26
|
64
|
27
|
128
|
W celu zamienia wcześniej obliczonej liczby binarnej należy
pomnożyć poszczególne zera i jedynki przed odpowiadające im potęgi liczby dwa i
zapisać jako sume składników.
1001010(2) = 1 * 26 + 0 *25 +
0 * 24 + 1 * 23 +
0 *22 + 1 + 21 * 0 + 20 = 1 * 64 + 0 * 32 + 0
* 16 + 1 * 8 + 0 * 4 + 1 * 2 + 0 * 1 = 64 + 8 + 2 = 74(10)
Warto zwrócić uwagę, że najmniej znaczący bit (LSB – ang.
Least significant bit) w prosty sposób informuje o parzystości liczby. Gdy
wynosi 1 liczba jest nieparzysta, a gdy 0 liczba jest parzysta.
Dodawanie i odejmowanie liczb binarnych
Dodawanie binarne przeprowadza się w takim sam sposób jak
dodawanie pisemne na liczbach dziesiętnych.
Należy zapamiętać następujące własności:
0 + 0 = 0
|
0 + 1 = 1
|
1 + 0 = 1
|
1 + 1 = 10
|
Tak wygląda dodanie 1 bajtowych (zapisane są jako 8 bitów)
liczb 32 i 50.
0
|
0
|
1
|
0
|
0
|
0
|
0
|
0
|
0
|
0
|
1
|
1
|
0
|
0
|
1
|
0
|
0
|
1
|
0
|
1
|
0
|
0
|
1
|
0
|
1010010(2)
= 82(10)
Odejmowanie binarne przeprowadza się w analogiczny sposób do
dodawania.
Należy zapamiętać następujące własności:
0 – 0 = 0
|
0 – 1 = 1
|
1 – 0 = 1
|
1 – 1 = 0
|
Tak wygląda odjęcie liczb 50 i 32.
0
|
0
|
1
|
1
|
0
|
0
|
1
|
0
|
0
|
0
|
1
|
0
|
0
|
0
|
0
|
0
|
0
|
0
|
0
|
1
|
0
|
0
|
1
|
0
|
10010(2) = 18(10)
Zamiana liczb dziesiętnych na liczby szesnastkowe i na odwrót
W celu zamiany liczby dziesiętnej na liczbę szesnastkowę
dzielimy tę liczbę przez 16 z resztą.
Dzielenie
|
Wynik
|
Reszta
|
3485 / 16
|
217
|
13
|
217 / 16
|
13
|
9
|
13 / 16
|
0
|
13
|
0
|
Należy teraz odczytać od resztę z dzielenia. Liczby większe
i równe 10 zamieniamy na odpowiadające im litery. W tym przypadku wynik to D9D.
W celu zamiany liczby szesnastkowej na liczbę dziesiętną
należy postąpić tak samo jak w przypadku zamiany liczby binarnej na liczbę
dziesiątną, tylko że kolejne miejsca cyfr odpowiadają kolejnym potęgom liczby
16.
D9D(16) = 13 * 162 + 9 * 161
+ 13 * 160 = 3328 + 144 + 13 = 3485(10)
Zamiana liczb binarnych na liczby szesnastkowe i na odwrót
Można zauważyć, że podstawie liczby szesnastkowej, czyli
liczbie 16 odpowiada liczba 24 co oznacza, że na 4 bitach można
zapisać 16 kombinacji – cyfr. Pozwala to na szybką zamiane liczby binarnej na
liczbę szesnastkową. Wystarczy taką liczbę zapisać jako grupy czterech bitów i
każdą z tych grup zamienić na jedną z szesnastu cyfr szesnastkowych.
Poniżej prezentuję zamianę liczby 532421 zapisaną w systemie
dziesiętnym na system szesnastkowy.
Grupa 4 bitowa
|
1000
|
0001
|
1111
|
1100
|
0101
|
Wartość dziesiętna
|
8
|
1
|
15
|
12
|
5
|
Wartość szesnastkowa
|
8
|
1
|
F
|
C
|
5
|
W celu zamiany liczby szesnastkowej na binarną należy
postąpić w odwrotny sposób, każdą cyfrę szesnastkową zamienić na grupę czterech
bitów.
Reprezentacja liczb całkowitych
Liczby całkowite to liczby naturalne, liczby przeciwne do
liczb naturalnych oraz zero. Ten rodzaj liczb wymusił na pionierach informatyki
wymyślenie metody zapisu liczb ujemnych. W tej części kursu przedstawię kody,
których używa się do zapisu takich liczb.
Kod znak-moduł
W tym kodzie najstarszy bit zarezerwowany jest do określenia
znaku liczby. Jeżeli jest to jedynka, liczba jest niedodatnia, jeżeli zero
liczba jest nieujemna.
Przedstawia to poniższa tabela
Bit znaku
|
Bity modułu
|
Liczba
|
0
|
000
|
0
|
0
|
001
|
1
|
0
|
010
|
2
|
0
|
011
|
3
|
0
|
100
|
4
|
0
|
101
|
5
|
0
|
110
|
6
|
0
|
111
|
7
|
1
|
000
|
0
|
1
|
001
|
-1
|
1
|
010
|
-2
|
1
|
011
|
-3
|
1
|
100
|
-4
|
1
|
101
|
-5
|
1
|
110
|
-6
|
1
|
111
|
-7
|
Kod uzupełnień do jedności
W kodzie uzupełnień do jedności liczby dodatnie zapisywane
są tak jak liczby w naturalnym kodzie dwójkowym z dodatkowym bitem znaku, który
wynosi 0, natomiast liczby ujemne są negacją tych liczb.
Tabela kodu
U1
|
Liczba
|
0000
|
0
|
0001
|
1
|
0010
|
2
|
0011
|
3
|
0100
|
4
|
0101
|
5
|
0110
|
6
|
0111
|
7
|
1111
|
0
|
1110
|
-1
|
1101
|
-2
|
1100
|
-3
|
1011
|
-4
|
1010
|
-5
|
1001
|
-6
|
1000
|
-7
|
Kod uzupełnień do dwóch
Jest to najpopularniejszy kod reprezentacji liczb
całkowitych w systemach cyfrowych. Operacje dodawania i odejmowania są wykowane
tak samo jak dla liczb binarnych bez znaku co daje mu znaczącą przewagę nad
innymi kodami, gdyż nie trzeba implementować oddzielnych jednostek procesora
odpowiedzialnych za operacje na tych liczbach.
Wartość wag pozycji w zapisie U2
Waga
|
-2n-1
|
2n-2
|
2n-3
|
…
|
22
|
21
|
20
|
Cyfra
|
Bn-1
|
Bn-2
|
Bn-3
|
…
|
B2
|
B1
|
B0
|
Tabela kodu
U2
|
Obliczenia
|
Liczba
|
0000
|
0
|
0
|
0001
|
20
|
1
|
0010
|
21
|
2
|
0011
|
21 + 20
|
3
|
0100
|
22
|
4
|
0101
|
22 + 20
|
5
|
0110
|
22 + 21
|
6
|
0111
|
22 + 21 + 20
|
7
|
1000
|
-(23)
|
-8
|
1001
|
-(23) + 20
|
-7
|
1010
|
-(23) + 21
|
-6
|
1011
|
-(23) + 21 + 20
|
-5
|
1100
|
-(23) + 22
|
-4
|
1101
|
-(23) + 22 + 20
|
-3
|
1110
|
-(23) + 22 + 21
|
-2
|
1111
|
-(23) + 22 + 21 + 20
|
-1
|
W celu szybkiej zamiany liczby na przeciwną należy zanegować
ją i dodać do niej jeden.
3 na -3
3
|
0011
|
Zapisujemy liczbę
|
Negacja
|
1100
|
Negujemy liczbę
|
-3
|
1101
|
Do zanegowanej liczby dodajemy 1
|
-7 na 7
-7
|
1001
|
Zapisujemy liczbę
|
Negacja
|
0110
|
Negujemy liczbę
|
7
|
0111
|
Do zanegowanej liczby dodajemy 1
|
Rozmiar liczb całkowitych
W systemach architektury x86 podstawową jednostką informacji
jest jeden bajt, wielkość składająca się z 8 bitów. Z tej podstawowej jednostki
tworzy się większe bedące jej wielokrotnościami
Byte
|
8
|
||||||||||||||||||
Word
|
16
|
||||||||||||||||||
Doubleword
|
32
|
||||||||||||||||||
Quadword
|
64
|
||||||||||||||||||
Double quadword
|
128
|
||||||||||||||||||
Reprezentacja znaków
Komputery przechowują dane binarne zakodowane w odpowiedni
sposób, odpowiednia interpretacja tych danych i wykorzystanych kodów pozwala na
reprezentacje znaków (liczb, liter itp).
ASCII
Jednym z pierwszych kodów powstałych w tym celu jest kod
ASCII (ang. American Standard for Information Interchange). Jest to
siedmiobitowy kod przyporządkowujący liczby z zakresu 0-127 literom alfabetu
angielskiego, znakom przestankowym, sybmol oraz poleceniom sterującym.
Kod ASCII dzieli się na 95 znaków drukowalnych oraz 33 znaki
sterujące.
Kodowanie znaków ANSI
Amerykańska instytucja ANSI ustalające normy techniczne
obowiązująca w USA stworzyła kod wykorzystujący 8 bitów do kodowania zbioru
znaków. Standard ten znany jest również jako ANSEL i obejmuje 128 znaków kodu
ASCII oraz 63 znaki dodatkowe. 14 lutego 2013 roku został administracyjne
wycofany przez ANSI.
Standard Unicode
Potrzeba reprezentacji dużej liczby znaków znajdujących się
w wielu alfabetach spowodowała stworzenie standardu Unicode.
Standard Unicode obejmuje przydział przestrzeni numeracyjnej
poszczególnym grupom znaków oraz sposoby bajtowego kodowania znaków. Jest kilka
metod kodowania, oznaczanych skrótowcami UCS (Universal Character Set) i UTF
(Unicode Transformation Format). Do najważniejszych należą:
- UTF-32/UCS-4
- UTF-16
- UTF-8
Ciąg znaków ASCII
Sekwencja jednego lub większej ilości znaków nazywana jest
ciągiem znaków. Ciąg ASCII reprezentowany jest w pamięci jako ciąg bajtów
zawierających kody ASCII. Ciągowi „ABC123” odpowiadają następujące po sobie
wartości zapisane w pamięci komputera
A
|
B
|
C
|
1
|
2
|
3
|
41h
|
42h
|
43h
|
31h
|
32h
|
33h
|
Języki programowania C i C++ korzystają z ciągów znaków
zakończonych bitem zerowym (ang. Null-terminated string).
Wyrażenia logiczne
George Bool w XIX w. opracował algebraiczne ujęcie logiki
matematycznej, które w informatyce przekładają się na operacje na wartościach 1
i 0 (prawda i fałsz). W tym kursie nie będę zagłębiał się rozlegle w ten dział
matematyki. W dzisiejszych czasach jego dogłębna znajomość potrzebna jest przy
projektowaniu urządzeń cyfrowych. Podczas programowania wystarczy znać tylko
kilka podstawowych operacji.
Poniższa tabela przedstawia wszystkie podstawowe wyrażenia
logiczne, ich nazwę oraz operatory.
Nazwa
|
Operator
|
Działanie
|
Negacja (NOT)
|
¬, ~, ‘
|
¬P
|
Koniunkcja (Iloczyn logiczny, AND)
|
∧
|
P ∧ Q
|
Alternatywa (Suma logiczna, OR)
|
∨, +
|
P ∨ Q
|
Alternatywa wykluczająca (suma modulo 2, XOR)
|
⊕, ⊻
|
P ⊕ Q
|
Implikacja (równoważnośc, XNOR)
|
→
|
P → q
|
Tablice prawdy poszczególnych funkcji
Negacja
P
|
¬P
|
1
|
0
|
0
|
1
|
Iloczyn logiczny
P
|
Q
|
P ∧ Q
|
0
|
0
|
0
|
0
|
1
|
0
|
1
|
0
|
0
|
1
|
1
|
1
|
Suma logiczna
P
|
Q
|
P ∨ Q
|
0
|
0
|
0
|
0
|
1
|
1
|
1
|
0
|
1
|
1
|
1
|
1
|
Suma modulo 2
P
|
Q
|
P ⊕ Q
|
0
|
0
|
0
|
0
|
1
|
1
|
1
|
0
|
1
|
1
|
1
|
0
|
Równoważność
P
|
Q
|
P ↔ Q
|
0
|
0
|
1
|
0
|
1
|
0
|
1
|
0
|
0
|
1
|
1
|
1
|
Języki programowania C i C++ korzystają z ciągów znaków zakończonych bitem zerowym (ang. Null-terminated string).
OdpowiedzUsuńBajtem zerowym