Wpis ten traktuję jako wstęp do czegoś większego, bo od jakiegoś czasu przymierzam się do napisania swojej pierwszej aplikacji z wykorzystaniem języka Assmebler. Żeby dodać całości trochę smaku aplikacją tą będzie gra napisana dla nieśmiertelnego Commodore 64. Zanim jednak wezmę się za pisanie właściwego kodu, chcę rozłożyć wszystko na czynniki pierwsze, zaczynając od tego jak wygląda komunikacja z urządzeniem posiadającym na swoim pokładzie dowolny procesor.

Jako programiści generujemy każdego dnia pewne ilości kodu, który sprawia, że maszyny naginają się do naszej woli (a przynajmniej nam się tak wydaje 😝). Zapewne większość z Was korzysta na co dzień z języków programowania wysokiego poziomu, takiego jak choćby Java, czy Swift. Języki pokroju C lub C++, określane często mianem niskopoziomowych, również działają na wysokim poziomie abstrakcji względem takiego Assemblera.

 

 

Jest to oczywiście jak najbardziej zrozumiałe, ponieważ pisanie dzisiejszych aplikacji z wykorzystaniem Assemblera byłoby delikatnie mówiąc kłopotliwe. Jednak mnie od samego początku interesowało jak to wszystko wygląda na najniższym możliwym poziomie. Nie lubię jednak uczyć się nowych rzeczy skupiając się tylko na teorii, dlatego po dłuższym namyśle postanowiłem, że napiszę jakąś prostą grę na Commodore 64 właśnie z wykorzystaniem assemblera. Będzie to pewnie klon starej produkcji, która utkwiła mi w pamięci, ale nie udało mi się zapamiętać jej nazwy. W przyszłości pojawi się pewnie kilka wpisów, w których przedstawię efekty swoich prac.

Zacząć jednak chciałem do samych podstaw, czyli jak za pomocą dwóch liczb – zera i jedynki – można sterować zachowaniem niemal każdego dostępnego dzisiaj urządzenia. Zaczynajmy 😎

 

 

Magiczne obwody

 

 

W dużym uproszczeniu budowa komputera (czyli dzisiaj właściwie każdego urządzenie) oparta jest na układach elektronicznych, które w dowolnym momencie swojego działania mogą znajdować się tylko w dwóch stanach – wyłączony (off) oraz włączony (on). Stany te reprezentowane są przez różne napięcia elektryczne. Tryb „off” reprezentowany jest zwykle przez 0 V (wolt), a tryb „on” przez 3.5 V. W rzeczywistości wszystko jest uzależnione od wykorzystanej technologii i przyjęte rozwiązania mogą się różnić. Tuja macie link do bardzo fajnego wyjaśnienia, które znalazłem na portalu Quora.

Jednak dla naszych rozważań najważniejszy jest fakt, że komputery operują w oparciu o zera i jedynki. Jest to tak zwany język maszynowy, czyli język zrozumiały dla komputera. Bardzo trudno byłoby jednak pisać za jego pomocą jakiekolwiek aplikacje. Wyobraźcie sobie sytuację, w której zamiast zamiast zapisu typu – var name = „Malwina” – piszecie coś w rodzaju 0100101001001001001010 (sekwencja przypadkowa 😅). Na nasze szczęście kompilatory bardzo dobrze radzą sobie z tłumaczeniem języków wysokiego poziomu na język maszynowy.

 

 

Są na to sposoby

 

 

W celu ułatwienia wszystkim życia zestawy instrukcji składających się z zer i jedynek opisywane są za pomocą systemów liczbowych, które są bardziej czytelne z punktu widzenia człowieka. Liczby reprezentowane za pomocą tych systemów ostatecznie są konwertowane właśnie na język maszynowy.

Zdaje sobie sprawę jak banalnie będzie brzmiało to co za chwilę przeczytacie, ale wyjaśnienia te będą przydatne na późniejszym etapie.

Najbardziej znanym i powszechnym systemem zapisu liczb jest system dziesiętny 😜. Jego nazwa wywodzi się z faktu, że podstawę stanowi dokładnie dziesięć cyfr – od 0 do 9. Kiedy pojawi się konieczność przedstawienia liczby większej od 9 to ponownie zaczynamy wykorzystywać liczy z podstawowego zakresu, tak jak w przypadku liczby 16.

Kiedy rozłożymy sobie system dziesiętny na czynniki pierwsze to zauważymy, że wzrost wartości danej liczby następuje od lewej to prawej – liczba po prawej stronie zawsze będzie reprezentowała niższą wartość. Każda z wartości składających się na liczbę w systemie dziesiętnym jest równa wartości podstawy (liczby 10) podniesionej do odpowiedniej potęgi i pomnożonej przez właściwą liczbę. Pierwsza liczba po prawej podnoszona jest do potęgi zerowej, liczba na lewo od niej do potęgi pierwszej, następna do drugiej i tak dalej. Lepiej prezentuje to poniższa tabelka:

 

Podstawa do potęgi

10 ^ 3

10 ^ 2

10 ^ 1

10 ^ 0

Wartość

1000

100 10

1

Przykładowa liczba

5

7 2

8

 

Na powyższym przykładzie możemy zobaczyć, że liczba 5728 składa się tak na prawdę z następujących elementów:

5 * 1000 + 7 * 100 + 2 * 10 + 1 * 8 = 5728.

 

 

System szesnastkowy

 

 

Dzięki zrozumieniu jak zapis konkretnych wartości działa w systemie dziesiętnym, łatwiej będzie nam opanować inne formy zapisu. Na przykład system szesnastkowy (heksadecymalny):

 

Podstawa do potęgi

16 ^ 3

16 ^ 2 16 ^ 1

16 ^ 0

Wartość

4096

256 16

1

Przykładowa liczba

0

1 F

A

 

W systemie tym dostępnych mamy szesnaście znaków. Skoro w przypadku liczb możemy korzystać jedynie z zakresu od 0 do 9, to koniecznym jest posiłkowanie się znakami literowymi. W tym przypadku będziemy korzystali ze znaków od A do F, co w połączeniu ze znakami liczbowymi daje nam właśnie szesnaście możliwości. Powyżej 9 zaczynamy używać znaku A, aż dojdziemy do F. Liczbę 01FA (system szesnastkowy) z powyższego przykładu możemy rozpisać w następujący sposób:

0 * 4096 + 1 * 256 + F (16 w systemie dziesiętnym) * 16 + A (10 w systemie dziesiętnym) * 8 = 592.

System szesnastkowy jest najczęściej stosowanym zapisem w przypadku konieczności podania adresu np. w pamięci rejestru procesora. W Commodore 64 zakres adresacji pamięci wynosi od 0 do 65535 w systemie dziesiętnym, co w systemie szesnastkowym można zapisać jako 0 – FFFF.

 

 

System binarny

 

 

W systemie binarnym mamy do wykorzystania tylko dwa znaki – jedynkę oraz zero:

 

Podstawa do potęgi 2 ^ 3 2 ^ 2 2 ^ 1 2 ^ 0
Wartość 8 4 2 1
Przykładowa liczba 0 1 0 1

 

Liczbę 0101 możemy z łatwością przeliczyć na wartość dziesiętną:

0 * 8 + 1 * 4 + 0 * 2 + 1 * 1 = 5

 

 

Do czego to się przydaje?

 

 

Tak jak wspomniałem na początku, z komputerami komunikujemy się za pomocą zer i jedynek, albo inaczej rzecz ujmując za pomocą dwóch stanów. Dzięki systemowi dziesiętnemu oraz szesnastkowemu możemy znacznie ułatwić sobie życie, zapisując na przykład złożony adres w pamięci komputera za pomocą tylko kilku znaków – 34BD zamiast długiego ciągu 0 i 1 – 0011 0100 1011 1101.

 

 

Zera i jedynki określane są mianem bitów (liczb binarnych), które wykorzystywane są do określania właściwego stanu. Pomimo ich pozornego ograniczenia, za ich pomocą można również przedstawiać liczby większe od 1. W tym celu bity umieszczane są w większych grupach. Ogólnie przyjętym standardem jest tworzenie bajtów, czyli zestawu 8 bitów. Maksymalna wartość, którą może przechowywać jeden bajt to 255. Jak to jest obliczane możecie sprawdzić w poniższej tabelce:

 

Podstawa do potęgi 2 ^ 7 2 ^ 6 2 ^ 5 2 ^ 4 2 ^ 3 2 ^ 2 2 ^ 1 2 ^ 0
Wartość 128 64 32 16 8 4 2 1
Przykładowa liczba 1 1 1 1 1 1 1 1

 

Tak to wygląda po rozpisaniu:

1 * 128 + 1 * 64 + 1 * 32 + 1 * 16 + 1 * 8 + 1 * 4 + 1 * 2 + 1 * 1 = 256

W celu reprezentacji większych liczb bajty mogą być grupowane np. w słowa maszynowe (ang. word), które składają się z 2 bajtów i mogą przechowywać maksymalną wartość równą 65535. Istnieją również takie formy jak słowo maszynowe poczwórne (qword lub quadword), którego nazwa mówi sama za siebie 😎.

Możemy również za pomocą liczb binarnych zapisywać liczby ujemne. W takim wypadku ostatni bit wykorzystywany jest jako flaga określająca jaki znak zostanie przypisany danej liczbie. W przypadku liczby ujemnej zakres wartości mieści się od  -128 do 128. Wynika to właśnie z faktu, że ostatni, ósmy bit zostaje zabrany, przez co ostania wartość (128) nie jest wliczana do puli.

Najbardziej chyba znaną formą zapisu w systemie dziesiętnym, którą często przeliczamy na system binarny, jest dobrze wszystkim znana maska podsieci. Tak będzie wyglądał przelicznik przykładowej maski:

255.255.255.0 (system dziesiętny) = 11111111 11111111 11111111 00000000 (system binarny)

Możemy również skorzystać z systemu szesnastkowego w celu ułatwienia sobie zapisu adres wskazującego pewien obszar w pamięci komputera:

01AE A9F0 (system szesnastkowy) = 0000 0001 1010 1110 1010 1001 1111 0000 (system binarny)

 

 

Słowo na drogę

 

I to właściwie na tyle. Powyższe przykłady powinny Wam pomóc trochę lepiej zrozumieć jak wygląda komunikacja programisty (i nie tylko) z komputerem. Z pozoru rzecz wydaje się banalna i raczej mało przydatna w codziennym użyciu. Jednak kiedy będziemy chcieli skorzystać np. z Assemblera to taka powtórka z podstaw może okazać się przydatna. Do usłyszenia.

 


 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *