1
POLITECHNIKA GDAŃSKA
WYDZIAŁ ELEKTRONIKI, TELEKOMUNIKACJI i INFORMATYKI
KATEDRA ARCHITEKTURY SYSTEMÓW KOMPUTEROWYCH
Architektura komputerów
Materiały pomocnicze do wykładu
dla studentów kierunku Informatyka
(studia niestacjonarne I stopnia)
Opracował dr inż. Andrzej Jędruch
Gdańsk 2011
2
Wprowadzenie
Technika
komputerowa
jest
rezultatem
wieloletniego
rozwoju.
Podstawowe rozwiązania techniczne leżące u podstaw współczesnych
komputerów zostały opracowane w latach czterdziestych i pięćdziesiątych
ubiegłego stulecia. Zbudowano wówczas pierwsze komputery, skonstruowano
języki programowania i ich translatory. Początkowo uważano komputer przede
wszystkim za narzędzie obliczeniowe, które miało wspomagać rozwiązanie
trudnych problemów występujących w naukach technicznych i przyrodniczych.
Szybko jednak okazało się, że komputery mogą pełnić jeszcze wiele innych
funkcji, wśród których na czoło wysunęły się przetwarzanie danych
ekonomiczno-finansowych, gromadzenie i udostępnianie danych (bazy danych),
sterowanie procesami przemysłowymi i wiele innych. Zakres zastosowań
komputerów ciągle się rozszerza o nowe dziedziny działalności ludzi i
społeczeństw.
Budowę szybkich urządzeń obliczeniowych podjęto w USA, w Anglii i w
Niemczech w czasie II wojny światowej. Nie były to jeszcze komputery w
pełnym tego słowa znaczeniu, ale w trakcie ich konstruowania rozwiązano
szereg problemów o kluczowym znaczeniu, które nadal są istotne. Przełomowe
znaczenie miało jednak opracowanie koncepcji urządzenia obliczeniowego z
programem wbudowanym. Koncepcja ta opublikowana (r. 1945) przez
matematyka amerykańskiego Johna von Neumanna i współpracowników
określiła zasady budowy i działania komputerów stosowane do dnia
dzisiejszego.
Rozwój sprzętu komputerowego
W XIX wieku budowano już arytmometry mechaniczne, używane m.in.
przez geodetów. Jednak problemy obliczeniowe występujące w trakcie
projektowania skomplikowanych urządzeń wymagały stosowania narzędzi
znacznie silniejszych, pozwalających wykonywać bardzo złożone obliczenia w
niedługim czasie. Okazało się jednak, że elementy mechaniczne nie pozwalają
na zwiększenie szybkości obliczeniowej. Zwrócono wówczas uwagę na
elementy elektroniczne, wprawdzie niedoskonałe i kosztowne z punktu widzenia
obecnej techniki, ale na owe czasy dość szybkie. Podstawowym elementem
elektronicznym była wówczas lampa elektronowa, stosowana głównie w
odbiornikach radiowych. Zauważono wówczas, że podobnie jak obecnie,
elementy elektroniczne w urządzeniach obliczeniowych pracują pewnie i
stabilnie jako elementy dwustanowe, tzn. w każdej chwili mogą się znajdować
w jednym z dwóch stanów, określanych jako włączony/wyłączony czy
zapalony/zgaszony.
3
W konsekwencji sposób przedstawiania liczb musiał być dostosowany do
własności stosowanych elementów elektronicznych. Przetwarzane liczby mogły
być zapisywane wyłącznie za pomocą dwóch cyfr: 0 i 1. Trzeba było więc
skierować uwagę na system dwójkowy, wcześniej mający tylko znaczenie
teoretyczne. W roku 1943 zbudowano w Wielkiej Brytanii elektroniczne
urządzenie obliczeniowe (kalkulator) Colossus I, w którym stosowana była
arytmetyka dwójkowa.
Po drugiej wojnie światowej w USA i w Wielkiej Brytanii prowadzone
były zaawansowane prace nad budową komputerów. W roku 1949 w
Uniwersytecie Manchester uruchomiono komputer Mark 1, którego bardzo
skromna lista rozkazów była jednak zadziwiająco podobna do rozwiązań
współczesnych.
W tym czasie komputery były urządzeniami eksperymentalnymi,
budowanymi w laboratoriach uniwersyteckich i wojskowych. Dopiero w
połowie
lat
pięćdziesiątych
rozpoczęto
wytwarzanie
komputerów
przeznaczonych do sprzedaży. Oczywiście, ze względu na cenę sięgającą
dziesiątek milionów dolarów, klientami mogły być tylko wielkie koncerny czy
instytucje wojskowe.
Wysoka cena komputerów wynikała ze stosowania lamp elektronowych,
które były dość kosztowne, zużywały sporo energii, a przy tym ulegały częstym
uszkodzeniom. Lampy elektronowe stosowane były w komputerach do około
roku 1960. Komputery tej klasy przyjęto nazywać komputerami pierwszej
generacji
.
Kluczowe znaczenie dla dalszego rozwoju komputerów miało
wynalezienie tranzystora (r. 1948). Niedługo po tym opracowano, i stale
ulepszano technologie wytwarzania tranzystorów, w wyniku czego tranzystor
stał się podstawowym elementem ówczesnych komputerów. Tranzystor zużywał
mniej energii, był bardziej niezawodny i tańszy. Istotne znaczenie miało też
zbudowanie (r. 1951) pamięci rdzeniowej, w której wykorzystywano zjawiska
magnetyczne. Wynalazki te pozwoliły na budowę komputerów szybszych,
bardziej niezawodnych i tańszych. Komputery tranzystorowe, produkowane od
roku 1956, przyjęto nazywać komputerami drugiej generacji.
Dalszy postęp wiąże się z wprowadzeniem układów scalonych (około
roku 1964) — układy scalone stanowią złożone zespoły tranzystorów i
elementów pomocniczych, zamknięte w niewielkiej obudowie. Początkowo
liczba tranzystorów w pojedynczym układzie nie przekraczała kilkuset. Układy
takie nazywano układami małej skali integracji. Stopniowo zwiększano liczbę
tranzystorów, wprowadzając układy średniej skali integracji. Komputery, w
których stosowano omawiane układy scalone zaliczane są do komputerów
trzeciej generacji
.
Około roku 1974 wprowadzono technologię wielkiej skali integracji,
oznaczanej skrótem VLSI. W ramach tej technologii rozpoczęto produkcję
całych procesorów, zamkniętych w jednej obudowie, o wymiarach zbliżonych
4
do pudełka z zapałkami. Dwadzieścia lat wcześniej procesor zajmował
kilkadziesiąt metrów kwadratowych! Była to już kolejna, czwarta generacja
komputerów
. Kształt komputerów piątej generacji nie jest jeszcze w pełni znany.
Liczba tranzystorów w procesorach scalonych (r. 2009) przekroczyła miliard.
Komputery w Polsce
Uruchomienie w 1958 roku w Warszawie pierwszego polskiego
komputera XYZ stanowiło znaczne osiągnięcie, biorąc pod uwagę całkowicie
nowe, dotychczas zupełnie nieznane zagadnienia. Komputery tej klasy w
krajach zachodnich zaczęto produkować kilka lat wcześniej. Począwszy od roku
1960
rozpoczęto
produkcję
komputera
pierwszej
generacji
ZAM 2
(lampowego), który w roku 1964 został zastąpiony przez komputer
tranzystorowy (II generacja) ZAM 21/41. W latach siedemdziesiątych produkcja
komputerów ulokowana była w Zakładach Elektronicznych ELWRO we
Wrocławiu. Produkowano wówczas komputery III generacji serii ODRA 1300.
Jednocześnie w Warszawie podejmowano próby budowy minikomputerów
("Momik"), które poprzedziły komputery osobiste.
W latach osiemdziesiątych rozpoczęto we Wrocławiu produkcję
komputerów R-32 wzorowanych na komputerach IBM 360. Niedługo potem,
wobec rozpowszechnienia się komputerów osobistych produkcja została
zaniechana.
Komputery osobiste
Komputery osobiste produkowano już w końcu lat siedemdziesiątych.
Przeznaczone były jednak do mniej odpowiedzialnych zadań i traktowano je
jako rodzaj ciekawostki technicznej.
W końcu lat siedemdziesiątych mikroprocesory scalone wytwarzało kilka
firm, spośród których najbardziej znane były firmy Intel i Motorola. Procesor
8086 opracowany w firmie Intel w roku 1978 stał podstawowym elementem
konstrukcyjnym komputera osobistego IBM PC, opracowanego w roku 1981.
Produkowano też nieco uproszczoną wersję tego procesora oznaczoną 8088.
Procesory 8086/88 stały się punktem wyjścia rozwoju całej rodziny procesorów,
na szczycie której znajduje się obecnie procesor Intel Core i7 czy AMD Phenom
II, które nadal wykonują wszystkie funkcje swojego poprzednika z 1978r.
Obecnie procesory należące do tej rodziny określa się jako procesory zgodne z
architekturą x86, a ich 64-bitowe wersje jako zgodne z architekturą x86-64.
Z kolei procesory firmy Motorola zostały zastosowane w komputerach
osobistych firmy Apple. Komputery te są nadal dostępne na rynku, ale
5
podstawowe znaczenie mają komputery wykorzystujące procesory zgodne z
architekturą x86.
Istotną przyczyną sukcesu komputera IBM PC (r. 1981) było przyjęcie
przez firmę IBM zupełnie nowych reguł dotyczących podzespołów
komputerowych. Płyta główna komputera została wyposażona w kilka gniazd
rozszerzeniowych, w których można było instalować dodatkowe wyposażenie
komputera. Dokumentacja sposobu przyłączenia tych podzespołów została
opublikowana, co zachęciło wiele innych firm do podjęcia produkcji
wyposażenia dodatkowego (m.in. kart graficznych). W rezultacie użytkownicy
mieli do wyboru wiele różnych produktów, o różnych własnościach i cenach. To
z kolei spowodowało stopniowe obniżanie cen podzespołów i całego komputera.
Od co najmniej dwudziestu pięciu lat rodzina komputerów osobistych IBM PC,
wytwarzana obecnie przez tysiące producentów na całym świecie, stanowi
dominujący czynnik współczesnej informatyki.
Rozwój oprogramowania
W początkowym okresie rozwoju techniki komputerowej zbudowanie
nawet prostego programu wymagało dokładnej znajomości operacji
wykonywanych przez komputer. Z tego powodu podjęto prace zmierzające do
umożliwienia programowania komputera poprzez podawanie mu algorytmów
obliczeń w sposób zbliżony do zwykłej notacji matematycznej. W ten sposób
opracowano w roku 1954 język programowania Fortran, który wprowadzał
radykalnie ułatwienia w programowaniu komputerów. Do chwili obecnej
opracowano setki i tysiące języków programowania, przeznaczonych dla
różnych zastosowań. Wśród języków ogólnego przeznaczenia najbardziej znane
są języki C/C++ i Pascal.
Jednocześnie rozwijały się systemy operacyjne. Ze względu na bardzo
wysoki koszt komputerów w latach pięćdziesiątych ubiegłego stulecia,
wynoszący zwykle kilkadziesiąt milionów dolarów, starano się wykorzystywać
komputery w sposób maksymalnie efektywny. Potrzebne były do tego celu
systemy operacyjne, które umożliwiały pracę wielozadaniową, minimalizującą
przestoje komputera. Rozwinięto wówczas także koncepcje sterowania pracą
urządzeń zewnętrznych, ochrony programów, zarządzania pamięcią i wiele
innych. Znaczna część ówczesnych osiągnięć jest nadal stosowana, także w
systemach operacyjnych komputerów osobistych.
Daty z historii informatyki
1834 Babbage — projekt urządzenia "Analytical Engine"
1854 Boole: "Laws of thought"
6
1930 Laboratorium firmy Bell: kalkulator elektromechaniczny
1941 Kalkulator elektromechaniczny (K. Zuse), mnożenie 3 s
1942 – 1946 kalkulatory elektroniczne
1944 MARK I (H. Aiken), Harvard University
1945 ENIAC
1945 Koncepcje J. von Neumanna
1948 Opracowanie tranzystora
1949 Rozwój oprogramowania: biblioteki podprogramów, asembler
1951 Komputer EDVAC (von Neumann) — program przechowywany w
pamięci
1954 Język programowania FORTRAN
1954 IBM 650 — pierwszy komputer produkowany masowo
1955 Pierwszy komputer tranzystorowy
1958 Język Algol
1958 Komputery tzw. drugiej generacji (tranzystorowe)
1958 Komputer Atlas z pamięcią wirtualną
1959 Komputer PDP–1
1962 Systemy z podziałem czasu
1968 Komputery tzw. trzeciej generacji (układy scalone)
1969 System Unix
lata 70:
minikomputery
1971 Procesor Intel 4004
1972 Język C
1977 Komputery osobiste: Apple, Commodore
1981 Komputer IBM PC (16 KB RAM)
1983 Początki Internetu
1984 Turbo-Pascal
1985 Język C++
1990 System Windows 3.0
1990 Pierwsza strona WWW
1991 Pierwsza wersja Linuksa
1994 Procesor Pentium
1995 Język Java
1999 Procesor Athlon AMD
2001 System Windows XP
2008 Procesory: Phenom AMD, Core i7 Intel
2009 System Windows 7
7
Komputery w Polsce
1958 Komputer XYZ (Zakład Aparatów Matematycznych PAN)
1960 Komputer ZAM 2
1960 Język programowania SAKO
1964 Komputery ZAM 41
1972 Komputery ODRA (Elwro Wrocław)
1975 Komputer Momik
1976 Komputery RIAD
Model komputera von Neumanna
Urządzenia do wykonywania skomplikowanych obliczeń, budowane w
latach czterdziestych ubiegłego stulecia, były zaprojektowane do wykonywania
ś
ciśle określonych, z góry zadanych obliczeń. Dane i wyniki pośrednie
przechowywane były w pamięci, a opis wykonywanych czynności był
reprezentowany przez ustalone połączenia na tablicy rozdzielczej urządzenia.
Zmiana sposobu wykonywania obliczeń wymagała ponownego, bardzo
kłopotliwego, zestawienia połączeń. W istocie, z punktu widzenia współczesnej
techniki, układ połączeń wykonany na tablicy rozdzielczej stanowił program,
podobny do tego jaki obecnie możemy przechowywać w pamięci.
Przełomowe znaczenie dla dalszego rozwoju techniki komputerowej
miała koncepcja von Neumanna i współpracowników. Von Neumann
zaproponował ażeby program obliczeń, czyli zestaw czynności potrzebnych do
rozwiązania zadania, przechowywać również w pamięci komputera, tak samo
jak przechowywane są dane do obliczeń. W ten sposób ukształtowała się
koncepcja komputera z programem wbudowanym, znana w literaturze
technicznej jako architektura von Neumanna. Mimo upływu wielu lat prawie
wszystkie współczesne komputery ogólnego przeznaczenia stanowią realizację
tego modelu.
Zasadniczą i centralną część każdego komputera stanowi procesor — jego
własności decydują o pracy całego komputera. Procesor steruje podstawowymi
operacjami komputera, wykonuje operacje arytmetyczne i logiczne, przesyła i
odbiera sygnały, adresy i dane z jednego podzespołu komputera do drugiego.
Procesor pobiera kolejne instrukcje programu i dane z pamięci głównej
(operacyjnej) komputera, przetwarza je i ewentualnie odsyła wyniki do pamięci.
Komunikacja ze światem zewnętrznym realizowana jest za pomocą urządzeń
wejścia/wyjścia.
Podzespoły urządzeń komputerowych łączone są za pomocą wielu
przewodów, które stanowią drogi dla danych, sygnałów sterujących i rozkazów
komputera — przewody te nazywane są magistralami. Magistralę tworzy pęk
linii (przewodów) i zestaw elementów przełączalnych umożliwiających
przekazywanie informacji z jednego rejestru do innego. W każdej chwili
8
możliwe jest przekazywanie informacji tylko między jednym wskazanym
rejestrem nadającym i jednym wskazanym rejestrem odbierającym — inne
rejestry są w tym czasie odłączone od magistrali.
Głównym zadaniem procesora jest wykonywanie programów, które
przechowywane są w pamięci operacyjnej. Program składa się z ciągu poleceń,
zakodowanych w sposób zrozumiały dla procesora — realizacja programu przez
procesor polega na kolejnym pobieraniu z pamięci operacyjnej tych poleceń
(instrukcji) i ich wykonywaniu.
Jak już wspomnieliśmy, do budowy współczesnych komputerów używane
są elementy elektroniczne — inne rodzaje elementów (np. mechaniczne) są
znacznie wolniejsze (o kilka rzędów). Ponieważ elementy elektroniczne pracują
pewnie i stabilnie jako elementy dwustanowe, informacje przechowywane i
przetwarzane przez komputer mają postać ciągów zerojedynkowych.
Procesor składa się z wielu różnych podzespołów wykonawczych, które
wykonują określone działania (np. sumowanie liczb) — podzespoły te na
rysunku reprezentowane są przez jednostkę arytmetyczno–logiczną (ang.
arithmetic logic unit). Podzespoły wykonawcze podejmują działania wskutek
sygnałów otrzymywanych z jednostki sterującej.
We współczesnych procesorach wykonanie nawet najprostszej operacji
dodawania wymaga wysłania sygnałów (w odpowiedniej kolejności) do co
najmniej kilkunastu podzespołów procesora (wchodzących w skład jednostki
arytmetyczno-logicznej). Tak więc algorytm wykonywania obliczeń powinien
być zakodowany w formie ciągu poleceń, które przechowywane są w pamięci i
sukcesywnie odczytywane przez procesor. Po otrzymaniu kolejnego polecenia
procesor wysyła sygnał elektryczny do odpowiedniego podzespołu.
Procesor
Pamięć
Urządzenia
wejścia/wyjścia
Jednostka
arytm. – logiczna
Jednostka
sterująca
Rozkazy
Dane
9
Taki sposób kodowania algorytmów wymaga dokładnej znajomości zasad
funkcjonowania poszczególnych podzespołów procesora, może się zmieniać w
kolejnych modelach tego samego procesora, a przy tym jest bardzo rozwlekły i
kłopotliwy. Ażeby uprościć programowanie, przyjęto pewien podstawowy zbiór
operacji (dla konkretnego typu procesora lub rodziny procesorów) i każdej
operacji przypisano ustalony kod w postaci ciągu zero-jedynkowego. Do zbioru
operacji podstawowych należą zazwyczaj cztery działania arytmetyczne,
operacje logiczne na bitach (negacja, suma logiczna, iloczyn logiczny), operacje
przesyłania, operacje porównywania i wiele innych. Zazwyczaj liczba
zdefiniowanych operacji zawiera się w granicach od kilkudziesięciu do kilkuset.
Operacje zdefiniowane w zbiorze podstawowym nazywane są rozkazami
lub instrukcjami procesora. Każdy rozkaz ma przypisany ustalony kod zero-
jedynkowy, a podstawowy zbiór operacji procesora jest zwykle nazywany listą
rozkazów procesora
.
W takim ujęciu algorytm obliczeń przedstawiany jest za pomocą operacji
ze zbioru podstawowego. Algorytm zakodowany jest w postaci sekwencji
ciągów zero-jedynkowych zdefiniowanych w podstawowym zbiorze operacji —
tak zakodowany algorytm nazywać będziemy programem w języku
maszynowym
.
Program przechowywany jest w pamięci, a wykonywanie programu
polega na przesyłaniu kolejnych ciągów zero-jedynkowych z pamięci głównej
do układu sterowania procesora. Zadaniem układu sterowania, po odczytaniu
takiego ciągu, jest wygenerowanie odpowiedniej sekwencji sygnałów
kierowanych do poszczególnych podzespołów, tak by w rezultacie wykonać
wymaganą operację (np. dodawanie).
Program ten musi bezpośrednio dostępny, tak by niezwłocznie po
zakończeniu jednej operacji można było zacząć następną. Oznacza to, że
program musi być przechowywany w pamięci ściśle współdziałającej z
procesorem. Zatem pamięć współpracująca z procesorem musi być dostatecznie
szybka, tak by oczekiwanie na odczytanie potrzebnych informacji nie
powodowało przestojów w pracy procesora. Niestety, współczesne konstrukcje
pamięci nie nadążają za coraz szybszymi procesorami, co może powodować
przestoje w pracy procesora. Jednak konstruktorom procesorów udało istotnie
ograniczyć niedogodności wynikające ze zbyt wolnej pamięci — zagadnienia te
omawiane będą w dalszej części opracowania.
Omówiona koncepcja programu przechowywanego w pamięci stanowi
kluczowy element modelu von Neumanna. Program używający tych rozkazów
nazywany jest programem w języku maszynowym. Można więc powiedzieć, że
moduł sterowania procesora przekształca każdy rozkaz (instrukcję) języka
maszynowego w odpowiednią sekwencję sygnałów koniecznych do wykonania
danego rozkazu. Język maszynowy jest kłopotliwy w użyciu nawet dla
specjalistów — znacznie wygodniejszy jest spokrewniony z nim język
asemblera, który będzie omawiany dalej.
10
Pamięć główna (operacyjna)
Informacje przechowywane w pamięci komputera mają postać ciągów
złożonych z zer i jedynek. Zatem elementarna komórka pamięci musi być
zdolna do przechowywania jednej dwu możliwych wartości: 0 lub 1. Taką
komórkę nazywać będziemy bitem. Omawiane informacje zapisane w pamięci
muszą być oczywiście dostępne na każde żądanie procesora. Konieczne jest
więc ponumerowanie wszystkich bitów, tak by procesor mógł jednoznacznie
wskazać położenie w pamięci potrzebnego ciągu zer i jedynek. Takie
numerowanie byłoby jednak niepraktyczne, ponieważ procesor żąda zazwyczaj
przekazania mu całego ciągu zer i jedynek, a nie pojedynczego zera lub jedynki.
Celowe jest więc grupowanie bitów w zespoły.
Spróbujmy rozpatrzyć jak duże powinny te zespoły bitów. Z punktu
widzenia konstrukcji układów cyfrowych liczba bitów w zespole powinna być
potęgą dwójki, czyli: 2, 4, 8, 16, 32, 64, itd. Jeśli ograniczymy uwagę do liczb
naturalnych, to największa liczba, która da się zapisać za pomocą k cyfr
binarnych określona jest wzorem 2
k
− 1. Maksymalne wartości liczb dla różnych
wartości k podane w poniższej tablicy.
Liczba
cyfr k
Maksymalna wartość liczby w systemie
dwójkowym
2
2
2
− 1 = 3
4
2
4
− 1 = 15
8
2
8
− 1 = 255
16
2
16
−1 = 65 535
32
2
32
−1 = 4 294 967 295
64
2
64
−1 = 18 446 744 073 709 551 615
Zespoły 2- i 4-bitowe nie mają praktycznego znaczenia, ponieważ można
w nich przechowywać tylko liczby z bardzo wąskiego zakresu 0 ÷ 3 lub 0 ÷ 15.
Zespoły takie nie nadają się także do przechowywania kodów znaków
alfanumerycznych (liter i cyfr). Zauważmy bowiem, że zakodowanie małych i
wielkich liter alfabetu łacińskiego (26 + 26), cyfr (0 ÷ 9) i znaków
przestankowych wymaga użycia prawie 100 różnych ciągów zer i jedynek,
podczas gdy na 4 bitach można zakodować tylko 16 różnych kombinacji zer i
jedynek
Zespół 8-bitowy nadaje się dobrze do przechowywania kodów znaków —
istnieje 256 różnych ciągów 8-bitowych, które mogą reprezentować małe i
11
wielkie litery, cyfry, znaki przestankowe, itp. Zakres liczb, które można
przechowywać w zespole 8-bitowym jest dość ograniczony (0 ÷ 255), ale w
pewnych zastosowaniach wystarczający. Tak więc praktyczne znaczenie ma
dopiero zespół 8 bitów, który w literaturze nazywany jest bajtem.
Z kolei, tam gdzie programy wykonują działania na danych liczbowych,
zazwyczaj wystarczające będą liczby zapisane na 32 cyfrach dwójkowych, czyli
liczby 32-bitowe
— wartości takich liczb mogą nieco przekraczać 4 miliardy
(dokładnie: mogą dochodzić do 4 294 967 295). Takie liczby mogą być
oczywiście zapisane w czterech kolejnych bajtach. Dochodzimy więc do
wniosku, że elementarną komórką pamięci powinien być bajt. Mówimy, że taka
pamięć ma organizację bajtową. Taka organizacja pamięci występuje w
większości współczesnych komputerów.
Pamięć główna (operacyjna) w komputerze składa z dużej liczby komórek
(kilka miliardów). Poszczególne komórki mogą zawierać dane, na których
wykonywane są obliczenia, jak również mogą zawierać rozkazy (instrukcje) dla
procesora. W trakcie pracy procesor komunikuje się z pamięcią operacyjną,
wykonując operacje zapisu i odczytu danych, a także pobierając kolejne rozkazy
do wykonania.
W celu precyzyjnego zorganizowania operacji odczytu i zapisu w
pamięci, elementarne komórki pamięci (bajty) powinny zostać ponumerowane.
Zazwyczaj numeracja zaczyna się od zera. W informatyce numer komórki
pamięci nazywany jest jej adresem fizycznym. Adres fizyczny przekazywany jest
przez procesor (lub inne urządzenie) do podzespołów pamięci w celu wskazania
położenia bajtu, który ma zostać odczytany lub zapisany. Zbiór wszystkich
adresów fizycznych dla danego typu procesora nazywa się fizyczną przestrzenią
adresową
.
W wielu współczesnych procesorach adresy fizyczne są 32-bitowe, co
określa
od
razu
maksymalny
rozmiar
zainstalowanej
pamięci:
2
32
= 4 294 967 296 bajtów (4 GB). W procesorach 8086/88, które stosowane
były w pierwszych komputerach IBM PC, adresy są 20-bitowe, skąd wynika, że
maksymalny rozmiar zainstalowanej pamięci wynosił 2
20
= 1 048 576 bajtów (1
MB).
Poniższy rysunek pokazuje adresy występujące w pamięci o pojemności
4 GB.
12
Procesor wykonuje często działania na zespołach bajtów: zespół dwóch
bajtów (16 bitów) nazywany jest słowem, zespół czterech bajtów (32 bity)
nazywany jest podwójnym słowem, zaś zespół ośmiu bajtów (64 bity) —
poczwórnym słowem
. W miarę potrzeby tworzy się także większe zespoły
bajtów.
Producenci procesorów ustalają konwencję numeracji bitów w bajtach i
słowach — numeracja przyjęta w architekturze procesorów Intel 32 pokazana
jest na rysunku.
13
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
8
9
11
12
13
14
15
10
bajt
słowo (ang. word)
31 30
0
podwójne słowo (ang. double word)
47 46
0
63 62
0
79 78
0
Pamięć fizyczna i wirtualna
Rozkazy (instrukcje) programu odczytujące dane z pamięci operacyjnej
(czy też zapisujące wyniki) zawierają informacje o położeniu danej w pamięci,
czyli zawierają adres danej. W wielu procesorach adres ten ma postać adresu
fizycznego
, czyli wskazuje jednoznacznie komórkę pamięci, gdzie znajduje się
potrzebna dana. W trakcie operacji odczytu adres fizyczny kierowany do
układów pamięci poprzez linie adresowe, a ślad za tym układy pamięci
odczytują i odsyłają do procesora potrzebną daną.
Taki nieskomplikowany sposób adresowania okazał się dość
niepraktyczny, utrudniając efektywne wykorzystanie pamięci, szczególnie w
systemach wielozadaniowych (np. MS Windows, Linux). W rezultacie
wieloletniego rozwoju architektury procesorów i systemów operacyjnych
wyłoniła się koncepcja pamięci wirtualnej, będącej pewną iluzją pamięci
rzeczywistej (fizycznej). Programista, tworząc nowy program przyjmuje, że ma
do dyspozycji pewien obszar pamięci, którego rozmiar w przypadku
architektury x86 (np. procesor Intel Core i5) może dochodzić do 4 GB. Jednak
rozmiar pamięci rzeczywiście zainstalowanej w komputerze może być mniejszy
i w typowych komputerach zawiera się w przedziale między 2 GB MB i 8 GB.
14
Odpowiednie układy procesora, sterowane przez system operacyjny, dokonują
transformacji adresów, którymi posługuje się programista na adresy w
istniejącej pamięci fizycznej, zwykle wspomaganej przez pamięć dyskową.
Pamięć operacyjna komputera w kształcie widzianym przez programistę
nosi nazwę pamięci wirtualnej, a zbiór wszystkich możliwych adresów w
pamięci wirtualnej nosi nazwę wirtualnej przestrzeni adresowej. Czasami
używany jest termin pamięć logiczna w znaczeniu pamięci wirtualnej.
Analogiczne znaczenie, jak w przypadku pamięci fizycznej, ma termin adres
wirtualny
(logiczny).
Transformacja adresów z przestrzeni wirtualnej na adresy fizyczne
(rzeczywiście
istniejących
komórek
pamięci)
jest
technicznie
dość
skomplikowana i nie może przy tym nadmiernie przedłużać wykonywania
rozkazu. Problemy te zostały jednak skutecznie rozwiązane, a związane z tym
wydłużenie czasu wykonywania programu zwykle nie przekracza kilku procent.
Jednocześnie programista może sobie wyobrażać, że pamięć wirtualna jest
rzeczywiście istniejącą pamięcią — taki właśnie punkt widzenia przyjęto w
początkowej części niniejszego opracowania. Stopniowo, w dalszej części
spróbujemy wyjaśnić zasady działania pamięci wirtualnej i mechanizmy
transformacji adresów.
Adresowanie pamięci
Przypuśćmy, że w pewnym programie (np. w języku C) zdefiniowano
dwie zmienne 32-bitowe: a, b. W trakcie wykonywania programu zmienne te
zajmować będą dwa (zazwyczaj przyległe) obszary 4-bajtowe. Położenie tych
zmiennych w pamięci wirtualnej określane jest poprzez podanie położenia bajtu
o najniższym adresie w obszarze 4-bajtowym — ilustruje to rysunek. Adres tego
bajtu nazywany jest także przesunięciem lub offsetem zmiennej. Innymi słowy
offset
, jest odległością zmiennej, liczoną w bajtach, od początku obszaru
pamięci (wirtualnej). Zatem adres zawarty w rozkazie (instrukcji) nie zawiera
adresu fizycznego danej, czyli nie wskazuje bezpośrednio jej położenia w
pamięci fizycznej, lecz jedynie odległość zmiennej od początku pamięci
wirtualnej.
15
offset a
a
b
offset b
Architektura x86
W komputerze IBM PC (r. 1981) zastosowano procesor 8088 firmy Intel.
W coraz to nowszych konstrukcjach komputerów miejsce tego procesora
zajmowały kolejno procesory 80286, 80386, 80486 (ściśle: i486), Pentium, Core
Duo, Core i7 pojawiające się zazwyczaj co 4 lata. Każdy z nich charakteryzuje
się coraz większą szybkością i złożonością.
Stopniowo, wytwarzanie procesorów kompatybilnych z wymienionymi
podejmowały także inne firmy, spośród których najbardziej znana jest firma
AMD. Zarówno procesory firmy Intel, jak i AMD (np. Athlon) realizują prawie
identyczny zestaw operacji, tak że z punktu widzenia oprogramowania nie
potrzeba ich odróżniać. Występują natomiast znaczne różnice w organizacji
wewnętrznej procesorów, co ma istotny wpływ na wydajność procesora.
Omawiane procesory klasyfikowane są jako procesy zgodne z
architekturą x86. Charakterystyczną cechą tych procesorów jest kompatybilność
wsteczna, co oznacza że każdy nowy model procesora realizuje funkcje swoich
poprzedników, m.in. programy dla komputera IBM PC opracowane na początku
lat osiemdziesiątych mogą być wykonywane także w komputerze wyposażonym
w procesor AMD Athlon.
Projekt procesora 8086/88, opracowany w końcu lat siedemdziesiątych,
przewidywał, że procesor ten współpracować będzie z pamięcią główną
(operacyjną) zawierającą co najwyżej 2
20
= 1 048 576 bajtów. Po kilku latach
okazało się, że taki rozmiar pamięci jest już niewystarczający i zachodzi
konieczność zastąpienia dotychczas używanego procesora przez inny,
16
umożliwiający współpracę z znacznie większą pamięcią. Jednak wprowadzenie
całkowicie nowego typu procesora mogłoby nie zostać zaakceptowane przez
użytkowników komputerów, których dotychczasowe oprogramowanie stałoby
się bezużyteczne — w tej sytuacji postanowiono skonstruować procesor
posiadający możliwość pracy w dwóch trybach, przy czym przełączenie między
trybami wykonywane jest w sposób programowy:
•
w trybie "starym", który nazywany jest trybem rzeczywistym (ang. real
mode), procesor zachowuje się podobnie do swojego poprzednika 8086/88;
•
w trybie "nowym", określany jako tryb chroniony (ang. protected mode)
procesor stosuje inne techniki adresowania pamięci, co pozwala zainstalować
w komputerze pamięć główną o rozmiarze do 4 GB (gigabajtów), a
nowszych procesorach do 64 GB i więcej.
Tryb chroniony w ograniczonym zakresie został wprowadzony w procesorze
80286, i szerzej rozwinięty w procesorach 386, 486 i w kolejnych wersjach
omawianej rodziny procesorów. Podstawowa lista rozkazów jest stopniowo
rozszerzana o nowe rozkazy i sposoby adresowania, wśród których najczęściej
wymienia się operacje grupy SSE, specjalnie zaprojektowane do szybkiego
przetwarzania danych w operacjach multimedialnych, jak również stopniowe
przechodzenie na przetwarzanie adresów i danych 64-bitowych w miejsce
stosowanych 32-bitowych.
W ciągu ostatnich kilku lat w architekturze procesorów pojawiły się nowe
elementy, spośród których najważniejsze znaczenie mają:
•
wprowadzenie architektury 64-bitowej,
•
wprowadzenie przetwarzania wielowątkowego,
•
rozpoczęcie produkcji procesorów wielordzeniowych.
Wymienione elementy zostaną omówione w dalszej części opracowania.
Rejestry ogólnego przeznaczenia
W trakcie wykonywania obliczeń często wyniki pewnych operacji stają
się danymi dla kolejnych operacji — w takim przypadku nie warto odsyłać
wyników do pamięci operacyjnej, a lepiej przechować te wyniki w komórkach
pamięci wewnątrz procesora. Komórki pamięci wewnątrz procesora zbudowane
są w postaci rejestrów (ogólnego przeznaczenia), w których mogą być
przechowywane dane i wyniki pośrednie. Z punktu widzenia procesora dostęp
do danych w pamięci głównej wymaga zawsze pewnego czasu (mierzonego w
dziesiątkach nanosekund), natomiast dostęp do danych zawartych w rejestrach
jest praktycznie natychmiastowy. Niestety, w większości procesorów jest
zaledwie kilka rejestrów ogólnego przeznaczenia, tak że nie mogą one
zastępować pamięci głównej.
Na przełomie lat siedemdziesiątych i osiemdziesiątych ubiegłego stulecia
rozwinięto nowe koncepcje budowy procesorów znane jako architektura RISC
17
(ang, reduced instruction set computer — komputery o zredukowanej liczbie
instrukcji). W procesorach tego typu zwiększono liczbę rejestrów do kilkuset, co
oczywiście usprawniło wykonywanie programów. Jednak do chwili obecnej
obok procesorów RISC wytwarzane są nadal procesory o architekturze
konwencjonalnej (CISC). Procesor Intel Core i7, aczkolwiek należy do
architektury CISC, to jednak zawiera znaczną liczbę różnych mechanizmów
zaczerpniętych z koncepcji RISC.
W rodzinie procesorów x86 początkowo
wszystkie rejestry ogólnego przeznaczenia były 16-
bitowe i oznaczone AX, BX, CX, DX, SI, DI, BP, SP.
Wszystkie te rejestry w procesorze 386 i wyższych
zostały rozszerzone do 32 bitów i oznaczone
dodatkową literą E na początku, np. EAX, EBX, ECX,
itd. W ostatnich latach rozwinięto nową architekturę
wprowadzając rejestry 64-bitowe, np. RAX, RBX,
RCX, itd. — nowa architektura oznaczana jest
symbolem x86-64, używane są też oznaczenia Intel 64
(firma Intel) lub AMD64 (firma AMD).
W architekturze x86 rejestry 16-bitowe są nadal
dostępne, np. młodsza część rejestru EAX nazywa AX,
a młodsza część rejestru EBX nazywa się BX. Ponadto
w kilku rejestrach wyodrębniono mniejsze rejestry 8-
bitowe, oznaczone AL, AH, BL, BH, itd. Omawiane
rejestry pokazane są na poniższym rysunku.
RAX
RDX
RBP
RSI
RDI
RSP
R8
R9
R10
R11
R12
R13
RCX
RBX
R14
R15
0
63
31
EAX
EBX
ECX
EDX
EBP
ESI
EDI
ESP
18
Wykonywanie programu przez procesor
Podstawowym zadaniem procesora jest wykonywanie programów, które
przechowywane są w pamięci głównej (operacyjnej). Program składa się z ciągu
elementarnych poleceń, zakodowanych w sposób zrozumiały dla procesora.
Poszczególne polecenia nazywane są rozkazami lub instrukcjami. Rozkazy
(instrukcje) wykonują zazwyczaj proste operacje jak działania arytmetyczne
(dodawanie, odejmowanie, mnożenie, dzielenie), operacje na pojedynczych
bitach, przesłania z pamięci do rejestrów i odwrotnie, i wiele innych. Rozkazy
zapisane są w postaci ustalonych ciągów zer i jedynek — każdej czynności
odpowiada inny ciąg zer i jedynek. Postać tych ciągów jest określana na etapie
projektowania procesora i jest dostępna w dokumentacji technicznej.
I tak na przykład przekazanie procesorowi Intel Core i7 instrukcji w
formie bajtu 01000010 spowoduje zwiększenie liczby umieszczonej w rejestrze
EDX o 1, natomiast przekazanie bajtu 01001010 — zmniejszenie tej liczby o 1.
Często polecenia przekazywane procesorowi składają się z kilku bajtów, np.
bajty 10000000 11000111 00100101 są traktowane przez procesor jako
polecenie dodania liczby 37 do liczby znajdującej się w rejestrze BH.
Ze względu na to, że posługiwanie się w procesie kodowania programu
wartościami zero-jedynkowymi byłoby bardzo kłopotliwe, wprowadzono skróty
literowe (tzw. mnemoniki) dla poszczególnych rozkazów procesora. I tak
podane rozkazy 01000010 i 01001010 zastępuje się mnemonikami INC EDX
(ang. increment — zwiększenie) i DEC EDX (ang. decrement —
zmniejszenie), zaś rozkaz o kodzie 10000000 11000111 00100101 zapisywany
jest w postaci ADD BH, 37 (ang. addition – dodawanie). Oczywiście,
mnemoniki są niezrozumiałe dla procesora i przed wprowadzeniem programu
do pamięci muszą być zamienione na kody zero-jedynkowe — programy
dokonujące takiej konwersji nazywane są asemblerami. W dalszej części
19
opracowania podane są szczegółowe informacje dotyczące posługiwania się
mnemonikami.
Tak więc rozmaite czynności, które może wykonywać procesor, zostały
zakodowane w formie ustalonych kombinacji zer i jedynek, składających się na
jeden lub kilka bajtów. Zakodowany ciąg bajtów umieszcza się w pamięci
operacyjnej komputera, a następnie poleca się procesorowi odczytywać z
pamięci i wykonywać kolejne rozkazy (instrukcje). W rezultacie procesor
wykonana szereg operacji, w wyniku których uzyskamy wyniki końcowe
programu.
Rozpatrzmy teraz dokładniej zasady pobierania rozkazów (instrukcji) z
pamięci. Poszczególne rozkazy przekazywane do procesora mają postać jednego
lub kilku bajtów o ustalonej zawartości. Przystępując do wykonywania
kolejnego rozkazu procesor musi znać jego położenie w pamięci, innymi słowy
musi znać adres komórki pamięci głównej (operacyjnej), gdzie znajduje się
rozkaz. Często rozkaz składa się z kilku bajtów, zajmujących kolejne komórki
pamięci. Jednak do pobrania wystarczy znajomość adresu tylko pierwszego
bajtu rozkazu.
W prawie wszystkich współczesnych procesorach znajduje się rejestr,
nazywany wskaźnikiem instrukcji lub licznikiem rozkazów, który określa
położenie kolejnego rozkazu, który ma wykonać procesor. Zatem procesor, po
zakończeniu wykonywania rozkazu, odczytuje liczbę zawartą we wskaźniku
instrukcji i traktuje ją jako położenie w pamięci kolejnego rozkazu, który ma
wykonać. Innymi słowy odczytana liczba jest adresem pamięci, pod którym
znajduje się rozkaz. W tej sytuacji procesor wysyła do pamięci wyznaczony
adres z jednoczesnym żądaniem odczytania jednego lub kilku bajtów pamięci
znajdujących się pod wskazanym adresem. W ślad za tym pamięć operacyjna
odczytuje wskazane bajty i odsyła je do procesora. Procesor traktuje otrzymane
bajty jako kolejny rozkaz, który ma wykonać.
Po wykonaniu rozkazu (instrukcji) procesor powinien pobrać kolejny
rozkaz, znajdujący w następnych bajtach pamięci, przylegających do aktualnie
wykonywanego rozkazu. Wymaga to zwiększenia zawartości wskaźnika
instrukcji, tak by wskazywał położenie następnego rozkazu. Nietrudno
zauważyć, że wystarczy tylko zwiększyć zawartość wskaźnika instrukcji o
liczbę bajtów aktualnie wykonywanego rozkazu. Tak też postępują prawie
wszystkie procesory.
Wskaźnik instrukcji pełni więc bardzo ważną rolę w procesorze,
każdorazowo wskazując mu miejsce w pamięci operacyjnej, gdzie znajduje się
kolejny rozkaz do wykonania. W architekturze x86 wskaźnik instrukcji jest
rejestrem 32-bitowym oznaczonym symbolem EIP.
20
31
0
EIP
Rozpatrzmy przykład podany na poniższym rysunku. W pamięci
komputera
znajduje
się
wiele
rozkazów,
a
wśród
nich
rozkaz
11111110 11000011, który zajmuje dwa bajty pamięci o adresach 7204 i 7205.
Wykonanie tego rozkazu przez procesor powoduje zwiększenie rejestru BL o 1,
a zapis rozkazu w postaci asemblerowej ma postać INC BL. Przypuśćmy, że w
pewnej chwili procesor zakończył wykonywanie jakiegoś rozkazu, a w rejestrze
EIP znajduje się liczba 7204.
Procesor przystępuje do wykonywania kolejnego rozkazu. W tym celu
odczytuje liczbę zapisaną w rejestrze EIP (tj. 7204) — liczba ta wskazuje adres
komórki pamięci, w której znajduje się rozkaz przewidziany do wykonania.
Procesor wysyła więc do układów pamięci żądanie odczytania bajtu o adresie
7204. Po chwili układy pamięci odsyłają do procesora odczytany bajt 11111110.
Procesor porównuje odczytany bajt z wzorcami bitowymi różnych rozkazów
(które przechowywane są wewnątrz procesora) i stwierdza, że na podstawie
otrzymanych 8 bitów nie jest w stanie określić czynności wykonywanych przez
rozkaz — potrzebny jest drugi bajt. W tej sytuacji procesor ponownie zwraca się
do układów pamięci z żądaniem odczytania kolejnego bajtu (o adresie 7205).
Po wykonaniu drugiego odczytu procesor dysponuje już bajtami
11111110 11000011 i porównuje je z wzorcami bitowymi. Okazuje się
odczytane bajty są identyczne z wzorcem opisującym operację zwiększenia
zawartości rejestru BL o 1. Wobec tego procesor wykonuje dodawanie i zaraz
potem przygotowuje się wykonania kolejnego rozkazu. W tym celu procesor
zwiększa zawartość wskaźnika instrukcji EIP o liczbę bajtów zajmowanych
21
przez aktualnie wykonywany rozkaz (w analizowanym przykładzie o 2), tak
nowa zawartość wskaźnika instrukcji EIP określała położenie w kolejnego
rozkazu, przylegającego w pamięci do aktualnie wykonywanego.
Pobranie rozkazu
Dekodowanie
kodu rozkazowego
Obliczenie adresu
efektywnego
Obliczenie adresu
fizycznego
Wykonanie
rozkazu
Wyznaczenie
położenia
następnego
rozkazu
Czynności wykonywane przez procesor w trakcie pobierania i
wykonywania poszczególnych rozkazów powtarzane są cyklicznie, a cały
proces nosi nazwę cyklu rozkazowego.
We współczesnych procesorach proces pobierania rozkazów z pamięci
wykonywany jest zazwyczaj z wyprzedzeniem, tj. procesor pobiera z pamięci
kilkanaście kolejnych rozkazów, które stopniowo wykonuje. Nie zmienia to
jednak podstawowych koncepcji cyklu rozkazowego. Do zagadnień tych
powrócimy w dalszej części opracowania.
Rozkazy sterujące i niesterujące
Omawiany wyżej schemat pobierania rozkazów ma jednak zasadniczą
wadę. Rozkazy mogą być pobierane z pamięci w kolejności ich rozmieszczenia.
Często jednak sposób wykonywania obliczeń musi być zmieniony w zależności
od uzyskanych wyników w trakcie obliczeń. Przykładowo, dalszy sposób
rozwiązywania równania kwadratowego zależy od wartości wyróżnika
trójmianu (delty). W omawianym wyżej schemacie nie można zmieniać
22
kolejności wykonywania rozkazów, a więc procesor działający ściśle wg tego
schematu nie mógłby nawet zostać użyty do rozwiązania równania
kwadratowego.
Przekładając ten problem na poziom instrukcji procesora można
stwierdzić, że w przypadku ujemnego wyróżnika (delty) należy zmienić
naturalny porządek ("po kolei") wykonywania rozkazów (instrukcji) i
spowodować, by procesor pominął ("przeskoczył") dalsze obliczenia. Można to
łatwo zrealizować, jeśli do wskaźnika instrukcji zostanie dodana odpowiednio
duża liczba (np. dodanie liczby 143 oznacza, że procesor pominie wykonywanie
instrukcji zawartych w kolejnych 143 bajtach pamięci operacyjnej). Oczywiście,
takie pominięcie znacznej liczby instrukcji powinno nastąpić tylko w przypadku,
gdy obliczony wyróżnik (delta) był ujemny.
Można więc zauważyć, że potrzebne są specjalne instrukcje, które w
zależności od własności uzyskanego wyniku (np. czy jest ujemny) zmienią
zawartość wskaźnika instrukcji, dodając lub odejmując jakąś liczbę, albo też
zmienią zawartość wskaźnika instrukcji w konwencjonalny sposób — rozkazy
takie nazywane są rozkazami sterującymi (skokowymi).
Rozkazy sterujące warunkowe
na ogół nie wykonują żadnych obliczeń, ale
tylko sprawdzają, czy uzyskane wyniki mają oczekiwane własności. W
zależności od rezultatu sprawdzenia wykonywanie programu może być
kontynuowane przy zachowaniu naturalnego porządku rozkazów albo też
porządek ten może być zignorowany poprzez przejście do wykonywania
rozkazu znajdującego się w odległym miejscu pamięci operacyjnej. Istnieją też
rozkazy sterujące, zwane bezwarunkowymi, których jedynym zadaniem jest
zmiana porządku wykonywania rozkazów (nie wykonują one żadnego
sprawdzenia).
Działanie rozkazów sterujących warunkowych jest ściśle związane ze
znacznikami
procesora. Znaczniki są rejestrami jednobitowymi, które są
ustawiane w stan 1 lub zerowane w zależności od wyniku aktualnie
wykonywanej operacji dodawania lub odejmowania (a także bitowych operacji
logicznych). Między innymi, dość często używany jest znacznik ZF (ang. zero
flag), który ustawiany jest w stan 1, jeśli wynik dodawania lub odejmowania
wynosi zero, i zerowany w przeciwnym przypadku. Inny znacznik CF (ang.
carry flag) jest znacznikiem przeniesienia, który jest ustawiany w stan 1, jeśli w
trakcie dodawania występuje przeniesienie wychodzące poza rejestr (albo
pożyczka w przypadku odejmowania).
Poszczególne znaczniki procesora tworzą razem 32-bitowy rejestr
znaczników, oznaczony jako EFLAGS (w architekturze 64-bitowej występuje
rejestr RFLAGS). Na poniższym rysunku pokazano fragment rejestru EFLAGS
zawierający znaczniki ZF i CF. Inne bity rejestru znaczników opisane są w
dalszej części niniejszego opracowania.
23
ZF
5
7
6
5
Rejestr znaczników
0
CF
Rozpatrzmy dla przykładu rozkaz skoku warunkowego, który oznaczany
jest skrótem literowym (mnemonikiem) jne. Rozkaz ten używany jest do
sprawdzenia czy wynik operacji jest różny od zera.
01110101
Zakres skoku
Ś
ciśle: rozkaz sprawdza stan znacznika ZF procesora, i jeśli znacznik ten
zawiera liczbę 0, to z punktu widzenia rozkazu warunek jest spełniony.
Wówczas wskaźnik instrukcji EIP zostaje zwiększony o liczbę bajtów, którą
zajmuje omawiany rozkaz (tu: 2) oraz o wartość podaną w drugim bajcie
(wartość na powyższym rysunku jest oznaczona jako Zakres skoku).
Zatem, jeśli warunek jest spełniony to procesor „przeskoczy” pewną
liczbę rozkazów i dalej zacznie wykonywać program. W szczególności skok
może być wykonany do tyłu, a więc procesor zacznie wykonywać rozkazy,
które przypuszczalnie już przed chwilą wykonywał. Jeśli zaś warunek nie będzie
spełniony, to procesor będzie wykonywał rozkazy po kolei, w naturalnym
porządku.
Wprowadzenie do programowania w asemblerze
Wykonanie programu przez procesor wymaga uprzedniego załadowania
do pamięci danych i rozkazów, zakodowanych w formie ciągów
zerojedynkowych, zrozumiałych przez procesor. Współczesne kompilatory
języków programowania generują takie ciągi w sposób automatyczny na
podstawie kodu źródłowego programu. Niekiedy jednak celowe jest precyzyjne
zakodowanie programu lub fragmentu programu za pomocą pojedynczych
rozkazów procesora. Dokumentacja techniczna procesora zawiera zazwyczaj
tablice ciągów zerojedynkowych przypisanych poszczególnym operacjom
(rozkazom procesora). Jednak kodowanie na poziomie zer i jedynek, aczkolwiek
możliwe, byłoby bardzo żmudne i podatne na pomyłki.
Z tego powodu opracowano programy, nazywane asemblerami, które na
podstawie skrótu literowego (tzw. mnemonika) opisującego czynności rozkazu
dokonują zamiany tego skrótu na odpowiedni ciąg zer i jedynek. Asemblery
udostępniają wiele innych udogodnień, jak np. możliwość zapisu liczb w
systemach o podstawie 2, 8, 10, 16 czy też automatyczną zamianę tekstów
znakowych na ciągi bajtów zawierające kody ASCII poszczególnych liter.
24
Zatem asemblery umożliwiają programowanie na poziomie pojedynczych
rozkazów procesora, uwalniając jednocześnie programistę od żmudnych
czynności binarnego kodowania i adresowania rozkazów. Języki te, zazwyczaj
odrębne dla każdej rodziny procesorów, oferują szereg rozmaitych opcji,
czyniąc programowanie maksymalnie elastycznym i wygodnym. Dla
procesorów architektury x86 dostępnych jest wiele asemblerów, a najbardziej
znany jest asembler MASM firmy Microsoft, którego najnowsza wersja
oznaczona jest numerem 10.0 (plik ml.exe). Używany jest też darmowy
asembler NASM (dostępny także w wersji dla systemu Linux).
W początkowym okresie rozwoju informatyki asemblery stanowiły często
podstawowy język programowania, na bazie którego tworzono nawet złożone
systemy informatyczne. Obecnie asembler stosowany jest przede wszystkim do
tworzenia modułów oprogramowania, działających jako interfejsy programowe.
Należy tu wymienić moduły służące do bezpośredniego sterowania urządzeń i
podzespołów komputera. W asemblerze koduje się też te fragmenty
oprogramowania, które w decydujący sposób określają szybkość działania
programu. Wymienione zastosowania wskazują, że moduły napisane w
asemblerze występują zazwyczaj w połączeniu z modułami napisanymi w
innych językach programowania.
Asembler możemy też uważać jako narzędzie, za pomocą którego można
zbadać podstawowe mechanizmy wykonywania programów przez procesor na
poziomie rejestrowym. Taki właśnie punkt widzenia przyjęto w niniejszym
opracowaniu.
Przypuśćmy, że w pamięci głównej (operacyjnej) komputera, począwszy
od adresu wirtualnego 72308H, znajduje się tablica zawierająca pięć liczb 16-
bitowych całkowitych bez znaku — tablica ta stanowi część obszaru danych
programu. Litera H występująca po cyfrach liczby oznacza, że wartość liczby
została podana w kodzie szesnastkowym (heksadecymalnym). Spróbujmy
napisać fragment programu, który przeprowadzi sumowanie liczb zawartej w tej
tablicy.
25
72309H
7230EH
7230DH
7230CH
7230BH
7230AH
72308H
72307H
72310H
72311H
7230FH
00000111
00000001
00000001
00000001
00000001
00000000
00001101
00000000
11111011
11110001
Adres
72312H
pierwszy
element tablicy
drugi
element tablicy
trzeci
element tablicy
czwarty
element tablicy
piąty
element tablicy
Dla uproszczenia problemu przyjmiemy, że w trakcie sumowania
wszystkie wyniki pośrednie dadzą się przedstawić w postaci liczby binarnej co
najwyżej 16-bitowej — innymi słowy w trakcie sumowania na pewno nie
wystąpi przepełnienie (nadmiar). Operacje sumowania zapiszemy najpierw w
postaci symbolicznej: najpierw do 16-bitowego rejestru AX zostaje załadowana
wartość pierwszego elementu tablicy, i następnie do rejestru AX dodawane są
wartości kolejnych elementów.
AX ← [72308H]
AX ← AX + [7230AH]
AX ← AX + [7230CH]
AX ← AX + [7230EH]
AX ← AX + [72310H]
Zapis [72308H] oznacza zawartość komórki pamięci znajdującej się w obszarze
danych o adresie podanym w nawiasach kwadratowych. Litera H oznacza, że
liczba podana jest w zapisie szesnastkowym.
Na poziomie rozkazów procesora, operacja przesłania zawartości komórki
pamięci do rejestru realizowana przez rozkaz oznaczony skrótem literowym
(mnemonikiem) MOV. Rozkaz ten ma dwa argumenty: pierwszy argument
określa cel, czyli "dokąd przesłać", drugi zaś określa źródło, czyli "skąd
przesłać
" lub "co przesłać":
26
MOV
dokąd
przesłać
, skąd (lub co)
przesłać
W omawianym dalej fragmencie programu mnemonik operacji przesłania
zapisywany jest małymi literami (mov), podczas w opisach używa się zwykle
wielkich liter (MOV) — obie formy są równoważne.
Rozkaz (instrukcja) przesłania MOV jest jednym z najprostszych w grupie
rozkazów niesterujących — jego zadaniem jest skopiowanie zawartości podanej
komórki pamięci lub rejestru do innego rejestru. W programach napisanych w
asemblerze dla procesorów architektury Intel 32 rozkaz przesłania MOV ma
dwa argumenty rozdzielone przecinkami. W wielu rozkazach drugim
argumentem może być liczba, która ma zostać przesłana do pierwszego
argumentu — tego rodzaju rozkazy określa się jako przesłania z argumentami
bezpośrednimi
, np.
MOV ECX, 7305
Przypomnijmy, że rozkazy (instrukcje) niesterujące nie zmieniają naturalnego
porządku wykonywania rozkazów, tzn. że po wykonaniu takiego rozkazu
procesor rozpoczyna wykonywanie kolejnego rozkazu, przylegającego w
pamięci do rozkazu właśnie zakończonego.
Rozkazy niesterujące wykonują podstawowe operacje jak przesłania,
działania arytmetyczne na liczbach (dodawanie, odejmowanie, mnożenie,
dzielenie), operacje logiczne na bitach (suma logiczna, iloczyn logiczny),
operacje przesunięcia bitów w lewo i w prawo, i wiele innych. Argumenty
rozkazów wykonujących operacje dodawania ADD i odejmowania SUB
zapisuje się podobnie jak argumenty rozkazu MOV
ADD
dodajna , dodajnik
SUB
odjemna , odjemnik
wynik wpisywany jest do
pierwszy argument
obiektu wskazanego przez
Podane tu rozkazy dodawania i odejmowania mogą być stosowane zarówno do
liczb bez znaku, jak i liczb ze znakiem (zob. temat Kodowanie liczb
całkowitych
). W identyczny sposób podaje się argumenty dla innych rozkazów
wykonujących operacje dwuargumentowe, np. XOR. Ogólnie rozkaz taki
27
wykonuje operację na dwóch wartościach wskazanych przez pierwszy i drugi
operand, a wynik wpisywany jest do pierwszego operandu. Zatem rozkaz
„operacja”
cel, źródło
wykonuje działanie
cel ← cel „operacja” źródło
Operandy cel i źródło mogą wskazywać na rejestry lub lokacje pamięci, jednak
tylko jeden operand może wskazywać lokację pamięci. Wyjątkowo spotyka się
asemblery (np. asembler w wersji AT&T), w których wynik operacji wpisywany
jest do drugiego operandu (przesłania zapisywane są w postaci skąd, dokąd).
Nieco inaczej zapisuje się rozkaz mnożenia MUL (dla liczb bez znaku). W
przypadku tego rozkazu konstruktorzy procesora przyjęli, że mnożna znajduje
się zawsze w ustalonym rejestrze: w AL – jeśli mnożone są liczby 8-bitowe, w
AX – jeśli mnożone są liczby 16-bitowe, w EAX – jeśli mnożone są liczby 32-
bitowe. Z tego powodu podaje się tylko jeden argument — mnożnik. Rozmiar
mnożnika (8, 16 lub 32 bity) określa jednocześnie rozmiar mnożnej.
MUL
mnożnik
Wynik mnożenia wpisywany jest zawsze do ustalonych rejestrów: w przypadku
mnożenia dwóch liczb 8-bitowych, 16-bitowy wynik mnożenia wpisywany jest
do rejestru AX, analogicznie przy mnożeniu liczb 16-bitowych wynik
wpisywany jest do rejestrów DX:AX, a dla liczb 32-bitowych do EDX:EAX.
Inne rozkazy dodawania, odejmowania, mnożenia i dzielenia rozpatrzymy
później.
Powracając do przykładu sumowania, wymagane operacje możemy teraz
zapisać w postaci równoważnej sekwencji rozkazów procesora
mov
ax, ds:[72308H]
add
ax, ds:[7230AH]
add
ax, ds:[7230CH]
add
ax, ds:[7230EH]
add
ax, ds:[72310H]
Występujący tutaj dodatkowy symbol ds: oznacza, że pobierane dane znajdują
się w obszarze danych programu (ang. data segment).
28
Tryby adresowania
Podany w poprzedniej części sposób sumowania elementów tablicy jest
bardzo niewygodny, zwłaszcza jeśli ilość sumowanych liczb jest duża.
Powtarzające się obliczenia wygodnie jest realizować w postaci pętli, ale
wymaga to korekcji adresu instrukcji dodawania — w każdym obiegu pętli
adres lokacji pamięci wskazujący dodawaną liczbę powinien być zwiększany
o 2. Przedstawione problemy rozwiązuje się poprzez stosowanie odpowiednich
trybów adresowania — adres lokacji pamięci, na której wykonywane jest
działanie określony jest nie tylko poprzez pole adresowe rozkazu, ale zależy
również od zawartości jednego lub dwóch wskazanych rejestrów. W
architekturze x86 dostępne są różne tryby adresowania:
•
mogą być używane dowolne 32-bitowe rejestry ogólnego przeznaczenia:
EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP;
•
drugi rejestr indeksowy może być skojarzony z tzw. współczynnikiem skali,
który podawany jest w postaci ∗1, ∗2, ∗4, ∗8 — podana liczba wskazuje
przez ile zostanie pomnożona zawartość drugiego rejestru indeksowego
podczas obliczania adresu.
Omawiane mechanizmy adresowania z użyciem rejestrów 32-bitowych ilustruje
poniższy rysunek.
zawartość pola adresowego instrukcji
Zawartość 32-bitowego rejestru
+
+
Adres efektywny
(pole adresowe może być pominięte)
(EAX, EBX, ECX, . . . )
ogólnego przeznaczenia
Zawartość 32-bitowego rejestru
(z wyjątkiem ESP)
ogólnego przeznaczenia
x1
x2
x4
x8
(wirtualny)
Adres efektywny w trybie 32-bitowym obliczany jest modulo 2
32
, tzn. bierze się
pod uwagę 32 najmłodsze bity uzyskanej sumy. Przykładowo, adres efektywny
poniższego rozkazu
sub eax, ds:[123H] [edx] [ecx ∗ 4]
29
zostanie jako obliczony jako suma:
•
liczby 123H,
•
zawartości rejestru EDX,
•
zawartości rejestru ECX pomnożonej przez 4.
W literaturze zawartość pierwszego rejestru nazywana jest adresem bazowym, a
drugiego adresem indeksowym.
Niekiedy pole adresowe instrukcji jest całkowicie pominięte, a wartość
adresu określona jest wyłącznie poprzez wskazane rejestry indeksowe; takie
rozwiązanie jest:
•
niezbędne, jeśli adres lokacji pamięci zostaje obliczony dopiero w trakcie
wykonywania programu (nie jest znany ani w trakcie kodowania programu
przez programistę ani też podczas translacji) — dotyczy to często kodu
generowanego przez kompilatory języków wysokiego poziomu;
•
szczególnie korzystne w przypadku wielokrotnego odwoływania się do tej
samej lokacji pamięci — ponieważ pole adresowe nie występuje, więc
instrukcja może być zapisana na mniejszej liczbie bajtów (zwykle 2 bajty).
Adresowanie z użyciem rejestru EBP działa trochę inaczej. Rejestr ten został
bowiem zaprojektowany do wspomagania operacji przekazywania parametrów
do procedur za pośrednictwem stosu — z tego względu użycie ww. rejestrów
jako indeksów powoduje, że operacja zostanie wykonana na danych zawartych
w obszarze stosu.
Posługując się trybami adresowania, omawiany wcześniej fragment
programu obliczający sumę liczb można zakodować w formie pętli rozkazowej.
W kolejnych obiegach pętli adres rozkazu dodawania ADD powinien zwiększać
się o 2 — można to łatwo zrealizować poprzez uzależnienie adresu rozkazu od
zawartości rejestru indeksowego EBX. Ponieważ w kolejnych obiegach pętli
rejestr EBX będzie zawierał liczby 0, 2, 4, ..., więc kolejne adresy efektywne
rozkazu ADD, które stanowią sumę pola adresowego (tu: 72308H) i zawartości
rejestru EBX, będą wynosiły:
72308H, 7230AH, 7230CH, 7230EH, 72310H
Tak więc w każdym obiegu pętli do zawartości rejestru AX dodawane będą
kolejne elementy tablicy liczb.
mov
ecx, 5 ; licznik obiegów pętli
mov
ax, 0
; początkowa wartość sumy
mov
ebx, 0 ; początkowa zawartość rejestru
; indeksowego
ptl_suma:
add
ax, ds:[72308H][ebx] ; dodanie kolejnego
; elementu tablicy
add
bx, 2
; zwiększenie indeksu
30
loop
ptl_suma ; sterowanie pętlą
Rozkaz loop stanowi typowy sposób sterowania pętlą: powoduje on odjęcie 1
od zawartości rejestru ECX, i jeśli wynik odejmowania jest różny od zera, to
sterowanie przenoszone do rozkazu poprzedzonego podaną etykietą, a
przeciwnym razie następuje przejście do następnego rozkazu; ponieważ
początkowa zawartość rejestru ECX wynosiła 5, więc rozkazy wchodzące w
skład pętli zostaną wykonane 5 razy.
Rozpatrując rozkaz loop jako rozkaz sterujący (skokowy) można
powiedzieć, że warunek testowany przez rozkaz jest spełniony, jeśli po odjęciu
1 zawartość rejestru ECX jest różna od zera — wówczas następuje skok, który
polega na dodaniu do rejestru EIP liczby umieszczonej w polu adresu rozkazu
LOOP i zwiększeniu EIP o 2 (liczba bajtów rozkazu LOOP). Jeśli warunek nie
jest spełniony, to EIP zostaje zwiększony o 2.
Podstawowe formaty liczb dwójkowych w komputerze
Jak już wielokrotnie stwierdziliśmy, wszystkie operacje w komputerze
wykonywane są na danych zakodowanych w formie ciągów zer i jedynek.
Dotyczy to również liczb, które mogą być kodowane w różnych formatach. Na
razie jednak skupimy uwagę wyłącznie na liczbach całkowitych. W
architekturze Intel 32 wyróżnia się liczby całkowite bez znaku i liczby całkowite
ze znakiem.
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
8
9
11
12
13
14
15
10
2
3
2
2
2
1
2
0
8
2
7
2
6
2
5
2
4
2
11
2
10
2
9
2
2
14
2
13
2
12
2
3
2
2
2
1
2
0
2
6
2
5
2
4
2
7
2
15
m = 8
m = 16
Przyjęty format kodowania liczb bez znaku ilustruje rysunek. Liczba
może zapisana na 8, 16 lub na 32 bitach. Poszczególnym bitom przypisane są
wagi kolejno od prawej: 2
0
, 2
1
, 2
2
, itd. Wartość liczby jest równa sumie
iloczynów poszczególnych bitów przez odpowiadające im wagi. Określa to
poniższe wyrażenie
∑
−
=
⋅
=
1
0
2
m
i
i
i
x
w
31
gdzie m oznacza liczbę bitów rejestru lub komórki pamięci, zaś xi oznacza
zawartość i-tego bitu.
Rozpatrzmy prosty przykład. Na poniższym rysunku przedstawiono liczbę
47305 w formacie 16-bitowej liczby całkowitej bez znaku. Łatwo sprawdzić, że
215 + 213 + 212 + 211 + 27 + 26 + 23 + 20 = 32768 + 8192 + 4096 + 2048 +
128 + 64 + 8 + 1 = 47305.
0
1
2
3
4
5
6
7
8
9
11
12
13
14
15
10
2
3
2
2
2
1
2
0
8
2
7
2
6
2
5
2
4
2
11
2
10
2
9
2
2
14
2
13
2
12
2
15
1
1
1
1
1
1
1
1
0
0
0
0
0
0
0
0
Spróbujmy określić zakresy dopuszczalnych wartości dla liczb 8, 16, 32 i
64-bitowych. Oczywiście, we wszystkich tych formatach wartość najmniejszej
liczby wynosi 0. Wartość największej liczby zależy od liczby bitów. I tak w
formacie 8-bitowym, jeśli wszystkie bity mają wartość 1, to wartość liczby
wynosi
27 + 26 + 25 + 24 + 23 + 22 + 21 + 20 = 128 + 64 + 32 + 16 + 8 + 4 + 2 +1 =
255 = 2
8
– 1
Tak więc, posługując się 8-bitowymi liczbami binarnymi całkowitymi bez znaku
trzeba pamiętać, że mogą one być użyte do przedstawiania liczb z przedziału <0,
255>. Analogicznie można wyznaczyć zakresy dla formatów 16, 32 i 64-
bitowych:
liczby 8-bitowe:
<0, 255> (lub <0, 2
8
– 1>
liczby 16-bitowe
<0, 65535> (lub <0, 2
16
– 1>
liczby 32-bitowe
<0, 4 294 967 295> (lub <0, 2
32
– 1>
liczby 64-bitowe
<0, 18 446 744 073 709 551 615>
(lub <0, 2
64
– 1>
Ponieważ nie każdy wie jak przeczytać liczbę 18 446 744 073 709 551 615,
więc podajemy ją słownie (1 trylion = 10
18
):
osiemnaście trylionów
czterysta czterdzieści sześć biliardów
siedemset czterdzieści cztery biliony
siedemdziesiąt trzy miliardy
siedemset dziewięć milionów
pięćset pięćdziesiąt jeden tysięcy
sześćset piętnaście
32
Drugim podstawowym formatem liczb całkowitych w architekturze
Intel 32 są liczby ze znakiem. W tym przypadku skrajny bit z lewej strony
reprezentuje znak liczby: jeśli bit ten zawiera 0, to liczba jest dodatnia lub równa
0, jeśli zaś bit zawiera 1, to liczba jest ujemna. Pozostałe bity określają wartość
liczby. We współczesnych procesorach stosowane dwa sposoby kodowania
wartości liczby całkowitej ze znakiem. Omówimy najpierw prostszy sposób,
znany jako znak-moduł — trzeba od razu zaznaczyć, że w architekturze x86 ten
sposób kodowania używany jest tylko w arytmetyce zmiennoprzecinkowej. W
zwykłych obliczeniach podstawowe znaczenie ma opisany dalej kod U2.
W systemie znak-moduł stosuje się naturalny schemat kodowania, w
którym bity znaczące liczby określają wartość bezwzględną liczby, czyli jej
moduł
. Obliczając więc wartość liczby należy najpierw zsumować odpowiednie
wagi, tak jak pokazaliśmy dla liczb bez znaku, a następnie umieścić znak minus
przed liczbą, jeśli bit znaku zawiera 1. Sposób kodowania liczb 8- i 16-bitowych
ilustruje poniższy rysunek.
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
8
9
11
12
13
14
15
10
bit znaku
2
3
2
2
2
1
2
0
8
2
7
2
6
2
5
2
4
2
11
2
10
2
9
2
2
14
2
13
2
12
2
3
2
2
2
1
2
0
2
6
2
5
2
4
m = 8
m = 16
W analogiczny sposób przedstawia się liczby 32-bitowe, 64-bitowe, itd. W
omawianym systemie kodowania wartość liczby określa formuła
gdzie m oznacza liczbę bitów rejestru lub komórki pamięci, zaś s stanowi
wartość bitu znaku. Zakresy wartości liczb kodowanych w systemie znak-moduł
podano poniżej.
liczby 8-bitowe
<−127, +127>
liczby 16-bitowe <−32767, +32767>
liczby 32-bitowe <−2 147 483 647, +2 147 483 647>
liczby 64-bitowe <−9 223 372 036 854 775 807, +9 223 372 036 854 775 807>
∑
−
=
⋅
⋅
−
=
2
0
2
)
1
(
m
i
i
i
s
x
w
33
Rozpatrzymy teraz drugi sposób kodowania liczb ze znakiem, znany jako
kodowanie U2. Sposób ten jest powszechnie stosowany we współczesnych
komputerach, ponieważ znacznie usprawnia wykonywania dodawania i
odejmowania. Także rozkazy udostępniane w procesorach zgodnych z
architekturą x86 wykonują działania na takich liczbach. Kodowanie liczb w
systemie U2 można opisują poniższe reguły:
1. liczby dodatnie kodowane są dokładnie tak samo jak w systemie znak-moduł;
2. liczby ujemne koduje się w postaci sumy: u + 1 + dana_liczba (ujemna),
gdzie u oznacza największą liczbę dodatnią (bez znaku), która da się zapisać
na ustalonej liczbie bitów dla danego formatu.
Przykładowo, dla formatu 8-bitowego u = 255, u + 1 = 256. W tym przypadku
reprezentacja liczby –128 będzie miała postać 256 + (–128) = 128, czyli
10000000.
Powyższy sposób jest jednak niepraktyczny i zazwyczaj korzysta z innej
reguły: aby zmienić znak liczby kodowanej w systemie U2 wystarczy
zanegować wszystkie bity i do uzyskanej wartości dodać 1. Przykładowo, liczba
1 w formacie 8-bitowym ma postać 00000001, a po zanegowaniu wszystkich
bitów 11111110. Jeśli do tej liczby dodamy 1, to otrzymamy 11111111. Zatem
8-bitowa reprezentacja liczby –1 w kodzie U2 ma postać 11111111.
Formalnie, wartość liczby w kodzie U2 określa poniższa formuła:
gdzie m oznacza liczbę bitów rejestru lub komórki pamięci. Zakresy wartości
liczb kodowanych w systemie U2 zestawiono w tabeli.
liczby 8-bitowe
<−128, +127>
liczby 16-bitowe <−32768, +32767>
liczby 32-bitowe <−2 147 483 648, +2 147 483 647>
liczby 64-bitowe <−9 223 372 036 854 775 808, +9 223 372 036 854 775 807>
W komputerach stosowanych jest jeszcze wiele innych sposobów
kodowania liczb. Między innymi stosowane są liczby zmiennoprzecinkowe,
które nadają się szczególnie dobrze do obliczeń naukowo-technicznych.
Niekiedy liczby koduje się w systemie dwójkowym-dziesiętnym (BCD). W
dalszej części niektóre z tych sposobów kodowania omówimy dokładniej.
∑
−
=
−
−
⋅
+
⋅
−
=
2
0
1
1
2
2
m
i
i
i
m
m
x
x
w
34
Interpretacja jako liczby:
bez znaku
ze znakiem
(znak-moduł)
ze znakiem
(U2)
0000 0000
0
+0
0
0000 0001
1
1
1
— — — — — — — — —
0111 1110
126
126
126
0111 1111
127
127
127
1000 0000
128
−
0
−
128
1000 0001
129
−
1
−
127
1000 0010
130
−
2
−
126
— — — — — — — — —
1111 1110
254
−
126
−
2
1111 1111
255
−
127
−
1
Powyżej pokazana jest tablica z przykładowymi ciągami 8-bitowymi, i ich
interpretacje w różnych systemach kodowania.
Teraz spróbujemy przeanalizować przykład obliczania wartości
wyrażenia arytmetycznego
(
) (
)
b
a b
−
⋅
+
+
1
7
Założymy, że 32-bitowe zmienne a i b przyjmują wartości całkowite nieujemne
i wcześniej zostały wpisane do rejestrów ESI i EDI. Zatem najpierw trzeba
obliczyć wartości wyrażeń w nawiasach, a potem pomnożyć uzyskane wartości.
Realizuje to poniższy ciąg rozkazów
ADD
ESI, EDI
; dodawanie ESI ← ESI + EDI
ADD
ESI, 7
; dodawanie ESI ← ESI + 7
MOV
EAX, ESI
; przesłanie EAX ← ESI
SUB
EDI, 1
; odejmowanie EDI ← EDI − 1
MUL
ESI
; mnożenie EDX:EAX ← EAX ∗ ESI
Najpierw obliczana jest wartość wyrażenia w prawym nawiasie, a następnie w
lewym. Po pomnożeniu 64-bitowy wynik wpisywany jest do rejestrów
EDX:EAX. Zazwyczaj uzyskany wynik jest też liczbą 32-bitową, wobec czego
cały wynik mieści się w rejestrze EAX (zawartość EDX można pominąć).
35
Dodawanie i odejmowanie liczb binarnych
W architekturze Intel 32 liczby całkowite ze znakiem kodowane są w
kodzie U2. Upraszcza to bardzo znacznie układy sumatora w procesorze, a
zarazem pozwala zastosować te same rozkazy do dodawania i odejmowania.
Główną zaletą stosowania kodu U2 jest możliwość "mechanicznego" dodawania
liczb ze znakiem.
W zilustrowania omawianej techniki dodawania weźmy pod uwagę dwie
8-bitowe liczby binarne 00111001 i 10100010. Jeśli podane liczby
interpretować jako liczby bez znaku, to ich wartości dziesiętne wynoszą,
odpowiednio, 57 i 162. Spróbujmy teraz przeprowadzić dodawanie liczb
binarnych.
0 0 1 0 0 0 0 0
przeniesienia
0 0 1 1 1 0 0 1
1 0 1 0 0 0 1 0
——————— dodawanie
1 1 0 1 1 0 1 1
W wyniku dodawania uzyskaliśmy liczbę 11011011 — ponieważ jest to liczba
bez znaku, więc łatwo można obliczyć jej wartość dziesiętną 219. Zatem
uzyskaliśmy poprawny wynik.
Przyjmijmy teraz, że liczby binarne 00111001 i 10100010 zostały
zakodowane jako liczby ze znakiem w systemie U2. W tym przypadku ich
wartości dziesiętne wynoszą, odpowiednio, 57 i −94. Dodawanie liczb w kodzie
U2 przeprowadza się tak samo jak dodawanie liczb bez znaku, czyli:
0 0 1 1 1 0 0 1
1 0 1 0 0 0 1 0
———————
dodawanie
1 1 0 1 1 0 1 1
Zauważmy, że w zwykłym dodawaniu liczb ze znakiem musimy zawsze
sprawdzić znaki dodawanych liczb: jeśli znaki są jednakowe, to wykonujemy
dodawanie, jeśli znaki są różne, to wykonujemy odejmowanie. W przypadku
kodowania w systemie U2, pomija się sprawdzanie znaków i zawsze wykonuje
dodawanie.
W wyniku dodawania uzyskaliśmy liczbę 11011011, która zakodowana
jest w systemie U2, a jej wartość dziesiętna wynosi −37. A więc również w tym
przypadku uzyskaliśmy poprawny wynik. Wartość dziesiętną liczby binarnej w
kodzie U2 można obliczyć (posługując się wcześniej opisanym schematem) w
poniższy sposób:
36
1. jeśli liczba jest dodatnia, to wartość dziesiętną obliczamy dokładnie tak jak
dla liczb bez znaku (zob. przykład pokazany wcześniej);
2. jeśli liczba jest ujemna, to negujemy jej wszystkie bity (tj. jedynki
zamieniamy na zera, a zera na jedynki) i do uzyskanej wartości dodajemy 1;
uzyskaną wartość zamieniamy na dziesiętną tak jak opisano w p. 1, przy
przed liczbą dopisujemy znak minus (−).
Przykładowo, jeśli liczba w kodzie U2 ma postać 11011011, to w tym
przypadku najstarszy bit ma wartość, a więc jest to liczba ujemna. Obliczenie
pokazane jest poniżej.
1 1 0 1 1 0 1 1 liczba ujemna w kodzie U2
0 0 1 0 0 1 0 0 liczba po zanegowaniu bitów
0 0 0 0 0 0 0 1 dodajemy 1
———————
0 0 1 0 0 1 0 1 wynik sumowania
Czyli otrzymaliśmy:
−
(2
5
+ 2
2
+ 2
0
) = − (32 + 4 + 1) = −37
Wyniki uzyskane w trakcie sumowania liczb binarnych 00111001 i 10100010:
1. przy założeniu, że obie liczby zostały zakodowane jako liczby bez znaku;
2. przy założeniu, że obie liczby zostały zakodowane jako liczby ze znakiem w
kodzie U2
zestawiono w poniższej tabeli.
Ciąg bitów
Interpretacja jako liczby:
bez znaku
ze znakiem
(U2)
0 0 1 1 1 0 0 1
57
57
1 0 1 0 0 0 1 0
162
−
94
Wynik dodawania
1 1 0 1 1 0 1 1
219
−
37
Analogiczne zasady dotyczą odejmowania. Zazwyczaj operacja
odejmowania liczb binarnych realizowane jest przez procesor według podanej
niżej formuły — najpierw zmienia się znak odjemnika, a następnie wykonuje
dodawanie.
a
b
a
b
−
=
+ −
(
)
37
Identyfikacja nadmiaru — rejestr znaczników procesora
Spróbujmy wykonać analogiczne obliczenia, ale tym razem dla innych
wartości liczb binarnych 00111001 i 11111111, które podobnie jak
poprzednio będziemy interpretować jako liczby bez znaku i jako liczby ze
znakiem w kodzie U2.
Ciąg bitów
Interpretacja jako liczby:
bez znaku
ze znakiem
(U2)
0 0 1 1 1 0 0 1
57
57
1 1 1 1 1 1 1 1
255
−
1
Wynik dodawania
0 0 1 1 1 0 0 0
56 błąd !!!
56
Podany przykład wskazuje, że takie „mechaniczne” dodawanie niekiedy
powoduje uzyskanie błędnych rezultatów. W podanym przykładzie wynik jest
poprawny tylko wówczas, jeśli dodawane liczby traktujemy jako liczby ze
znakiem w kodzie U2. Jeśli założymy, że liczby są kodowane jako liczby bez
znaku, to wynik 56 jest błędny.
Jak stwierdzić czy uzyskany wynik poprawny? W tym celu wykonany
dodawanie liczb binarnych.
1 1 1 1 1 1 1 1
przeniesienia
0 0 1 1 1 0 0 1
1 1 1 1 1 1 1 1
——————— dodawanie
CF ← 1 0 0 1 1 1 0 0 0
Zauważmy, że w wyniku sumowania najstarszej pozycji (1 + 0 + 1) powstało
przeniesienie, które nie mieści się w ramach formatu 8-bitowego, i należałoby je
zapisać na niedostępnym 9-tym bicie wyniku.
Takie przeniesienie wpisywane jest do bitu CF w rejestrze znaczników
procesora. Rejestr znaczników zawiera zestaw bitów, które opisują stan
procesora — niekiedy nazywany jest rejestrem stanu procesora. Poprzez
wpisanie wartości do odpowiednich znaczników można w pewnym stopniu
zmienić reguły obliczania adresów czy zasady współpracy z urządzeniami
zewnętrznymi komputera. Inne znaczniki opisują wynik operacji (np. czy
uzyskany wynik jest liczbą ujemną). Znaczniki te, zebrane razem, tworzą
32-bitowy rejestr znaczników o strukturze podanej na poniższym rysunku.
38
Niektóre znaczniki dostępne są tylko dla systemu operacyjnego, inne mogą być
ustawiane przez programy użytkowników. Rola poszczególnych znaczników
staje się w pełni jasna dopiero w trakcie rozpatrywania poszczególnych operacji
procesora. Warto jednak krótko opisać najczęściej używane znaczniki.
CF
(ang. carry) znacznik przeniesienia — do znacznika tego wpisywane
jest przeniesienie (pożyczka) z najbardziej znaczącego bitu; znacznik
ten można także interpretować jako znacznik nadmiaru w operacjach
na liczbach bez znaku;
ZF
(ang. zero) znacznik zera — znacznik ten ustawiany jest w stan 1,
gdy wynik operacji arytmetycznej lub logicznej jest równy 0 i
zerowany w przypadku przeciwnym;
SF
(ang. sign) znacznik reprezentujący znak wyniku obliczenia;
IF
(ang. interrupt enable) zezwolenie na przerwanie — znacznik ten
włącza lub wyłącza system przerwań; jeśli znacznik IF zawiera 0, to
przerwania sprzętowe są ignorowane aż do chwili, gdy IF zawierać
będzie 1; zawartość znacznika IF nie wpływa na wykonywanie
przerwań programowych;
DF
(ang. direction) znacznik kierunku — stan tego znacznika wpływa na
sposób wykonywania operacji na łańcuchach znaków;
OF
(ang. overflow) znacznik nadmiaru (używany w operacjach na
liczbach ze znakiem).
Powracając do przykładu sumowania liczb, możemy powiedzieć, że
wpisanie 1 do znacznika CF w wyniku dodawania liczb bez znaku oznacza, że
wystąpił nadmiar i obliczona suma jest błędna. Taką samą rolę pełni znacznik
OF w trakcie sumowania liczb ze znakiem w kodzie U2.
Rozkazy sterujące
W każdym prawie algorytmie realizowanym w komputerze występują
pewne struktury decyzyjne, czyli takie fragmenty programu, w których dalsza
kolejność wykonywania rozkazów zależy od wartości wyników pośrednich,
które nie są znane w trakcie tworzenia programu. Decyzje te na poziomie
rozkazów procesora polegają na sprawdzeniu pewnych własności wyników
pośrednich: czy wynik ostatniej operacji jest równy zero, czy jest ujemny, czy
jest liczbą parzystą, czy w trakcie operacji arytmetycznej wystąpił nadmiar, itp.
Wszystkie te informacje reprezentowane są przez zawartości odpowiednich
39
bitów rejestru znaczników (który był omawiany w poprzedniej części). Rola
rozkazów sterujących (nazywanych też skokami) sprowadza się do zbadania
stanu odpowiedniego bitu w rejestrze znaczników, i jeśli bit ma oczekiwaną
wartość, to naturalny porządek wykonywania programu zostaje zmieniony,
natomiast jeżeli bit ma inną wartość, to rozkazy wykonywane są w naturalnej
kolejności ("po kolei").
Obok rozkazów sterujących, które testują pewne warunki, dostępne są
także rozkazy sterujące, które nie sprawdzają żadnego warunku, przyjmując, że
warunek jest zawsze spełniony. Rozkazy takie bezwarunkowo zmieniają
naturalny porządek wykonywania rozkazów, stąd nazywane są skokami
bezwarunkowymi
, lub ściślej rozkazami sterującymi bezwarunkowymi.
Rozkazy sterujące warunkowe używane są do realizacji rozgałęzień w
programu w zależności od spełnienia lub nie jakiegoś warunku. W procesorach
zgodnych z architekturą x86 testowanie czy pewien warunek jest spełniony (np.
czy liczba w rejestrze DX jest ujemna) wymaga zastosowania na ogół dwóch
rozkazów. Pierwszy z tych rozkazów wykonuje pewną operację arytmetyczną
lub logiczną, przy czym wybrane własności uzyskanego wyniku wpisywane są
rejestru znaczników. Przykładowo, jeśli wynik operacji wynosi 0, to znacznik
zera ZF (w rejestrze znaczników) przyjmuje wartość 1.
Drugi z omawianych rozkazów jest rozkazem sterującym, który testuje
wybrany bit rejestru znaczników. Niektóre rozkazy sterujące testują wartości
pewnych wyrażeń logicznych zależnych od stanu kilku bitów rejestru
znaczników. Przykładowo, rozkaz JA (nazywany zwykle: skocz, gdy większy)
przyjmuje, że warunek jest spełniony, gdy jednocześnie CF = 0 i ZF = 0.
W praktyce programowania rozkazy sterujące występują zazwyczaj
bezpośrednio po rozkazach porównania. Przykładowo, jeśli chcemy sprawdzić
czy liczba w rejestrze EDX jest większa lub równa od liczby w rejestrze EDI, to
porównanie to realizują poniższe rozkazy
CMP
EDX, EAX ; porównywanie zawartości rejestrów EDX i EAX
JAE
wieksza_w_EDX
; warunek spełniony — skok do innego
; miejsca w programie
MOV
ECX, 12
; warunek nie spełniony — rozkazy
; wykonywanie są dalej w naturalnej
; kolejności
— — — — — — — — — — — — — — — — —
— — — — — — — — — — — — — — — — —
wieksza_w_EDX:
40
Prawie wszystkie rozkazy sterujące wyznaczają zawartość wskaźnika instrukcji
EIP wg poniższej zależności:
1. gdy warunek jest spełniony:
EIP ← EIP + <liczba bajtów aktualnie wykonywanej instrukcji> +
+ <zawartość pola 'zakres skoku'>
2. gdy warunek nie jest spełniony
EIP ← EIP + <liczba bajtów aktualnie wykonywanej instrukcji>
Porównywanie liczb całkowitych bez znaku
Rozpatrzmy dokładniej problem porównywania liczb bez znaku.
Przyjmijmy, że porównywane liczby znajdują się w rejestrach CX i DX. W
zależności od wyniku porównywania sterowanie w programie powinno być
przekazane do etykiety:
ety_rowne
gdy obie liczby są jednakowe,
ety_mniejsze
gdy liczba zawarta w rejestrze CX jest mniejsza od liczby w
rejestrze DX,
ety_wieksze
gdy liczba zawarta w rejestrze CX jest większa od liczby w
rejestrze DX.
W celu porównania tych liczb wykonuje się odejmowanie zawartości
rejestrów CX − DX. Jeśli wynik odejmowania będzie równy zero, to znaczy że
liczby są równe. Jeśli w wyniku odejmowania pojawi się żądanie pożyczki,
reprezentowane przez ustawienie znacznika CF, to znaczy, że liczba w rejestrze
DX jest większa od liczby w rejestrze CX. Wreszcie, jeśli nie pojawi się żądanie
pożyczki i wynik jest różny od zera, to liczba zawarta w rejestrze CX jest
większa od liczby w rejestrze DX. Zatem analiza stanu znaczników ZF i CF po
wykonaniu odejmowania pozwala stwierdzić która z porównywanych liczb
większa i czy liczby są równe. Mamy bowiem:
♦
gdy ZF = 1, to liczby są równe;
♦
gdy CF = 1, to liczba w rejestrze DX jest większa od liczby w rejestrze CX;
♦
gdy CF = 0 i ZF = 0, to liczba w rejestrze DX jest mniejsza od liczby w
rejestrze CX.
Zauważmy, że w ostatnim przypadku sprawdzenie tylko znacznika CF jest
niewystarczające: znacznik CF przyjmuje wartość 0 także w przypadku gdy
liczby są równe.
Odejmowanie zawartości rejestrów wykonuje się za pomocą rozkazu
SUB. Ale w rozpatrywanym zadaniu wynik odejmowania nie jest potrzebny,
potrzebne są natomiast pewne własności tego wyniku. Ponieważ w praktyce
41
programowania operacje porównywania występują bardzo często, więc na liście
rozkazów procesora wprowadzono nieco zmieniony rozkaz odejmowania
oznaczony mnemonikiem CMP. Rozkaz ten wykonuje odejmowanie, ustawia
odpowiednie bity w rejestrze znaczników, ale nigdzie nie wpisuje wyniku
odejmowania. Działania rozkazu CMP (ang. compare) dokładnie odpowiadają
wymaganiom związanym z porównywaniem liczb. Omawiane tu porównanie
można zrealizować za pomocą sekwencji podanych niżej rozkazów.
CMP
CX, DX
JE
rowne
; warunek spełniony, gdy ZF = 1
JA
wieksze
; warunek spełniony, gdy CF =0 i ZF = 0
mniejsze:
Typowe rozkazy sterujące warunkowe kodowane są dwóch bajtach.
Pierwszy bajt zawiera kod rozkazu, drugi bajt zawiera liczbę, która dodawana
jest do rejestru EIP, jeśli testowany warunek jest spełniony. W takim przypadku
możliwe byłoby tylko zwiększanie zawartości rejestru EIP o liczbę, która może
się zawierać w przedziale 0 ÷ 255. Przyjęto więc dodatkowe założenie, że liczba
8-bitowa podana w drugim bajcie instrukcji jest rozszerzana do 32 bitów
poprzez powielenie najstarszego bitu. W rezultacie poprzez ustalenie
odpowiedniej wartości drugiego bajtu możliwe jest zwiększenie wskaźnika
instrukcji EIP o co najwyżej 127, lub zmniejszenie o co najwyżej 128.
Niekiedy trzeba jednak zwiększyć wskaźnik instrukcji EIP o więcej niż
127. W takim przypadku asembler wybiera inny kod rozkazu sterującego, w
którym właściwy kod rozkazu zapisywany jest na dwóch bajtach, a dalsze cztery
bajty określają wartość, która zostanie dodana do rejestru EIP, jeśli warunek jest
spełniony.
Do porównywania liczb bez znaku i liczb ze znakiem używa się nieco
innych rozkazów sterujących. Mnemoniki tych rozkazów zestawiono w
poniższej tablicy.
Rodzaj porównywanych liczb
liczby bez znaku
liczby ze
znakiem
skocz, gdy większy
ja (jnbe)
jg (jnle)
skocz, gdy mniejszy
jb (jnae, jc)
jl (jnge)
skocz, gdy równe
je (jz)
je (jz)
skocz, gdy nierówne
jne (jnz)
jne (jnz)
skocz, gdy większy lub równy
jae (jnb, jnc)
jge (jnl)
skocz, gdy mniejszy lub równy
jbe (jna)
jle (jng)
W nawiasach podano mnemoniki rozkazów o tych samych kodach — w
zależności konkretnego porównania można bardziej odpowiedni mnemonik, np.
42
rozkaz JAE używamy do sprawdzania czy pierwszy operand rozkazu cmp
(liczby bez znaku) jest większy lub równy od drugiego; jeśli chcemy zbadać
pierwszy operand jest niemniejszy od drugiego, to używamy rozkazu JNB —
rozkazy JAE i JNB są identyczne i są tłumaczone na ten sam kod.
Kodowanie tekstów – kod ASCII
Początkowo komputery używane były do obliczeń numerycznych.
Okazało się jednak, że doskonale nadają się także do edycji i przetwarzania
tekstów. Wyłoniła się więc konieczność ustalenia w jakiej formie mają być
przechowywane w komputerze znaki używane w tekstach. Ponieważ w
komunikacji dalekopisowej (telegraficznej) ustalono wcześniej standardy
kodowania znaków używanych w tekstach, więc sięgnięto najpierw do tych
standardów. W wyniku różnych zmian i ulepszeń około roku 1968 w USA
ustalił się sposób kodowania znaków znany jako kod ASCII (ang. American
Standard Code for Information Interchange). Początkowo w kodzie ASCII
każdemu znakowi przyporządkowano unikatowy 7-bitowy ciąg zer i jedynek,
zaś ósmy bit służył do celów kontrolnych. Wkrótce zrezygnowano z bitu
kontrolnego, co pozwoliło na rozszerzenie podstawowego kodu ASCII o nowe
znaki, używane w alfabetach narodowych (głównie krajów Europy Zachodniej).
Ponieważ posługiwanie się kodami złożonymi z zer i jedynek jest
kłopotliwe, w programach komputerowych kody ASCII poszczególnych znaków
zapisuje się w postaci liczb dziesiętnych lub szesnastkowych. Znaki o kodach od
0 do 127 przyjęto nazywać podstawowym zestawem ASCII, zaś znaki o kodach
128 do 255 rozszerzonym kodem ASCII. Przykładowe kody ASCII niektórych
znaków podano w tablicy.
a
0110 0001
61H
b
0110 0010
62H
c
0110 0011
63H
d
0110 0100
64H
e
0110 0101
65H
f
0110 0110
66H
— — — — —
y
0111 1001
79H
z
0111 1010
7AH
A
0100 0001
41H
B
0100 0010
42H
C
0100 0011
43H
D
0100 0100
44H
E
0100 0101
45H
43
F
0100 0110
46H
— — — — — —
Y
0101 1001
59H
Z
0101 1010
5AH
!
0010 0001
21H
"
0010 0010
22H
#
0010 0011
23H
$
0010 0100
24H
— — — — —
{
0111 1011
7BH
|
0111 1100
7CH
0
0011 0000
30H
1
0011 0001
31H
2
0011 0010
32H
3
0011 0011
33H
— — — — — —
8
0011 1000
38H
9
0011 1001
39H
Kody od 0 do 31 oraz kod 127 zostały przeznaczone do sterowania
komunikacją dalekopisową. Niektóre z nich pozostały w informatyce, chociaż
zatraciły swoje pierwotne znaczenie, inne zaś są nieużywane. Do tej grupy
należy m.in. znak powrotu karetki (CR) o kodzie 0DH (dziesiętnie 13). W
komunikacji dalekopisowej kod ten powodował przesunięcie wałka z papierem
na skrajną lewą pozycję. W komputerze jest często interpretowany jako kod
powodujący przesunięcie kursora do lewej krawędzi ekranu. Bardzo często
używany jest także znak nowej linii (LF) o kodzie 0AH (dziesiętnie 10).
Problem znaków narodowych
Z chwilą szerszego rozpowszechnienia się komputerów osobistych w
wielu krajach wyłonił się problem kodowania znaków narodowych.
Podstawowy kod ASCII zawiera bowiem jedynie znaki alfabetu łacińskiego (26
małych i 26 wielkich liter). Rozszerzenie kodu ASCII pozwoliło stosunkowo
łatwo odwzorować znaki narodowe wielu alfabetów krajów Europy Zachodniej.
Podobne działania podjęto także w odniesieniu do alfabetu języka polskiego. Z
jednej polscy producenci oprogramowania stosowali kilkanaście sposobów
kodowania, z których najbardziej znany był kod Mazovia. Jednocześnie firma
Microsoft wprowadziła standard kodowania znany jako Latin 2, a po
44
wprowadzeniu systemu Windows zastąpiła go standardem Windows 1250.
Dodatkowo jeszcze organizacja ISO (ang. International Organization for
Standardization) wprowadziła własny standard (zgodny z polską normą) znany
jako ISO 8859-2, który jest obecnie często stosowany w Internecie. Podana
niżej tablica zawiera kody litery ą w różnych standardach kodowania.
Znak
Mazovia
Latin 2
Windows
1250
ISO 8859-
2
Unicode
ą
86H
A5H
B9H
B1H
0105H
Ą
8FH
A4H
A5H
A1H
0104H
Uniwersalny zestaw znaków
Kodowanie znaków za pomocą ośmiu bitów ogranicza liczbę różnych
kodów do 256. Z pewnością nie wystarczy to do kodowania liter alfabetów
europejskich, nie mówiąc już o alfabetach krajów dalekiego wschodu. Z tego
względu od wielu prowadzone są prace na stworzeniem kodów obejmujących
alfabety i inne znaki używane na całym świecie.
Prace nad standaryzacją zestawu znaków używanych w alfabetach
narodowych podjęto na początku lat dziewięćdziesiątych. Prace prowadzone są
niezależnie
przez
organizację
ISO
(International
Organization
for
Standardization) i konsorcjum koncernów informatycznych Unicode. Instytucje
te przyjęły jednak wspólne reguły kodowania znaków, aczkolwiek wydają
odrębne dokumenty różniące się w specjalistycznych zagadnieniach. Nie
wnikając w te różnice rozpatrzymy problematykę uniwersalnego zestawu
znaków, oznaczając go dalej określeniem Unicode.
Unicode zawiera znaki potrzebne do reprezentacji tekstów praktycznie we
wszystkich znanych językach. Obejmuje nie tylko znaki alfabetu łacińskiego,
greki, cyrylicy, arabskiego, ale także znaki chińskie, japońskie i wiele innych.
Co więcej, niektóre kraje (np. Japonia, Korea) przyjęły standard Unicode jako
standard narodowy, ewentualnie z pewnymi uzupełnieniami.
Formalnie rzecz biorąc standard Unicode definiuje zestaw znaków 31-
bitowych. Jak dotychczas używany jest 16-bitowy podzbiór obejmujący 65534
znaki. Przypuszcza się, że kody nigdy nie wyjdą poza 21 bitów, co pozwala na
reprezentację ponad miliona znaków. Ciekawostką może być to, że standard
przypisuje każdemu znakowi nie tylko kod liczbowy, ale także oficjalną nazwę.
Przykładowo, wielka litera A, ma przypisany kod liczbowy, który zapisywany
jest w postaci 0041H, a oficjalna nazwa brzmi "Latin capital letter A".
Znaki o kodach 0000H do 007FH są identyczne ze znakami kodu ASCII
(standard ISO 646 IRV). Z kolei znaki z przedziału 0080H do 00FFH są
identyczne ze znakami kodu ISO 8859-1 (kod Latin-1).
45
W standardzie Unicode poszczególnym znakom przypisano wartości
liczbowe, ale uczyniono to bez wskazywania w jakiej postaci mają być one
przechowywane w pamięci komputera. Ponieważ pamięci współczesnych
komputerów mają organizację bajtową, zachodzi konieczność przedstawiania
znaku Unicode w postaci dwóch, a przyszłości trzech lub czterech bajtów. Jak
wspomnieliśmy wyżej, aktualnie używany jest podzbiór 16-bitowy, co oznacza
ż
e do przedstawienia jednego znaku potrzebne są dwa bajty — taki sposób
reprezentacji kodu znaków oznaczono symbolem UTF-16. Skrót UTF został
utworzony z angielskiego określenia Unicode Transformation Format.
Zauważmy jednak, że w standardzie ISO-8859-2, w którym mogą być
kodowane teksty w języku polskim, każdy znak zajmuje 8 bitów, czyli jeden
bajt. Oznacza to, że pliki tekstowe w formacie UTF-16 będą dwukrotnie dłuższe
w porównaniu do plików kodowanych w sposób tradycyjny (np. ISO-8859-2),
co przedłuża czas ich przesyłania przez Internet.
Omawiane trudności w znacznym stopniu eliminuje sposób kodowania
oznaczony symbolem UTF-8. Skrót UTF został utworzony angielskiego
określenia "Unicode Transformatiom Format". Przy kodowaniu UTF-8
obowiązują następujące reguły:
1. Znaki o kodach 0000H do 007FH (czyli znaki kodu ASCII) są
kodowane jako pojedyncze bajty o wartościach z przedziału 00H do
7FH. Oznacza to, że pliki zawierające wyłącznie 7-bitowe kody ASCII
mają taką samą postać zarówno w kodzie ASCII jak i w UTF-8.
2. Wszystkie znaki o kodach większych od 007FH są kodowane jako
sekwencja kilku bajtów, z których każdy ma ustawiony najstarszy bit na
1. Pierwszy bajt w sekwencji kilku bajtów jest zawsze liczbą z
przedziału C0H do FDH i określa ile bajtów następuje po nim.
Wszystkie pozostałe bajty zawierają liczby z przedziału 80H do BFH.
Takie kodowanie pozwala, w przypadku utraty jednego z bajtów, na
łatwe
zidentyfikowanie
kolejnej
sekwencji
bajtów
(ang.
resynchronization).
Podana niżej tablica określa sposób kodowania UTF-8 dla różnych wartości
kodów znaków. Bity oznaczone xxx zawierają reprezentację binarną kodu
znaku. Dla każdego może być użyta tylko jedna, najkrótsza sekwencja bajtów.
Warto zwrócić uwagę, że liczba jedynek z lewej strony pierwszego bajtu jest
równa liczbie bajtów reprezentacji UTF-8.
Zakresy kodów
Reprezentacja w postaci UTF-8
od
do
0 (0000H)
127 (007FH)
0xxxxxxx
128 (0080H)
2047 (07FFH)
110xxxxx 10xxxxxx
2048 (0800H)
65535 (FFFFH)
1110xxxx 10xxxxxx 10xxxxxx
46
Przykładowo, znak "copyright" (litera C w kółku) ma przypisany kod
00A0H = 0000 0000 1010 1001. W reprezentacji UTF-8 kod ten należy do
zakresu <0080H, 07FFH> i jest przedstawiany w postaci 11 bitów. Wobec tego
w liczbie 0000 0000 1010 1001 pomijamy początkowe 5 bitów (zaznaczone
kursywą) i kodujemy dalej liczbę 000 1010 1001. Z podanej tabeli wynika, że
pierwsze pięć bitów wpiszemy do pierwszego bajtu, a następne sześć bitów – do
drugiego bajtu. Tak więc otrzymamy:
pierwszy bajt:
110 000 10
drugi bajt: 10 10 1001
czyli C2H i A9H. Podobnie, znak o kodzie 2260H ("różny") kodowany jest w
postaci trzech bajtów: E2H, 89H A0H.
Organizacja stosu
Stos jest strukturą danych, która stanowi odpowiednik, np. stosu książek.
Kolejne wartości zapisywane na stos ładowane są zawsze na jego wierzchołek.
Również wartości odczytywane są zawsze z wierzchołka stosu, przy odczytanie
wartości należy rozumieć jako usunięcie jej ze stosu. W literaturze technicznej
tak zorganizowana struktura danych nazywana jest kolejką LIFO, co stanowi
skrót od ang. "Last In, First Out". Oznacza to, że obiekt który wszedł jako
ostatni, jako pierwszy zostanie usunięty.
W komputerach z procesorem zgodnym z architekturą x86 stos
umieszczany jest w pamięci operacyjnej. Położenie wierzchołka stosu wskazuje
rejestr ESP. Zdefiniowano dwa podstawowe rozkazy wykonujące operacje na
stosie:
push — zapisanie danej na stosie
pop — odczytanie danej ze stosu.
W trybie 32-bitowym na stosie zapisywane są wartości 32-bitowe, czyli 4-
bajtowe. Wskaźnik stosu ESP wskazuje zawsze położenie najmłodszego bajtu
spośród czterech tworzących zapisaną wartość.
Rozkaz push przed zapisaniem danej na stosie powoduje zmniejszenie
rejestru ESP o 4, natomiast rozkaz pop po odczytaniu danej zwiększa rejestr
ESP o 4. Stos używany jest często do przechowywania zawartości rejestrów,
np. rozkazy
push esi
push edi
powodują zapisanie na stos kolejno zawartości rejestrów ESI i EDI. W dalszej
części programu można odtworzyć oryginalne zawartości rejestrów poprzez
odczytanie ich ze stosu
47
pop edi
pop esi
Rzadziej używane są rozkazy PUSHF i POPF. Pierwszy z nich powoduje
zapisanie na stosie zawartości rejestru znaczników FLAGS, drugi zaś przenosi
zawartość wierzchołka stosu do rejestru znaczników FLAGS.
Na stosie zapisywana jest także zawartość wskaźnika rozkazu EIP przy
wywoływaniu podprogramów, co jest opisane w następnym podrozdziale.
Tworzenie i wywoływanie podprogramów
W praktyce programowania spotykamy się często z sytuacjami, gdy
identyczne czynności wykonywane są w wielu miejscach programu. W takich
przypadkach tworzymy odpowiedni podprogram (w języku wysokiego poziomu
nazywany często procedurą lub funkcją), który może być wywoływany w
różnych miejscach programu. Poniżej rozpatrzymy szczegółowo mechanizmy
wywoływania i powrotu z podprogramów na poziomie rozkazów procesora.
Wywołanie ciągu rozkazów tworzącego podprogram wymaga wykonania
nie tylko skoku, ale przekazania także informacji dokąd należy wrócić po
wykonaniu tego ciągu. Innymi słowy, trzeba podać liczbę, która ma zostać
wpisana do wskaźnika instrukcji EIP po zakończeniu wykonywania sekwencji
rozkazów tworzącej podprogram.
Wywołanie podprogramu realizuje się za pomocą rozszerzonego rozkazu
skoku — konieczne jest bowiem zapamiętanie adresu powrotu, zwanego
śladem
, tj. miejsca, do którego ma powrócić sterowanie po zakończeniu
wykonywania podprogramu. W architekturze Intel 32 ww. czynności wykonuje
rozkaz CALL — występuje on również w wersji z adresowaniem bezpośrednim
i pośrednim. Adres powrotu zapisuje się na stosie. Spotyka się inne typy
procesorów, w których ślad zapisywany jest w rejestrach.
48
Podprogram
Skok do
podprogramu
Powrót z
podprogramu
Przykładowo, jeśli przyjmiemy, że rozkaz CALL zajmuje pięć bajtów
począwszy od adresu 7A34H, to kolejny rozkaz po CALL znajduje się w
pamięci począwszy od offsetu 7A39H. Ślad zapisany na stosie powinien
wskazywać położenie rozkazu, który znajduje się bezpośrednio za rozkazem
CALL, który wywołał podprogram. Tak więc rozkaz CALL powinien zapisać na
stosie liczbę
7A39H = położenie rozkazu CALL + liczba bajtów rozkazu CALL
49
Ś
lad zapisany na stosie wskazuje miejsce w programie, dokąd należy przekazać
sterowanie po wykonaniu podprogramu. Innymi słowy: w chwili zakończenia
wykonywania podprogramu zawartość wierzchołka stosu powinna zostać
przepisana do rejestru EIP — czynności te realizuje rozkaz RET.
W asemblerze podprogram rozpoczyna dyrektywa PROC a kończy
dyrektywa ENDP, np.
czytaj
PROC
— — — — — —
— — — — — —
czytaj
ENDP
Operacje bitowe
Obok rozkazów wykonujących operacje arytmetyczne, w których
zawartość rejestru lub komórki pamięci traktowana jest jako liczba, istnieje
obszerna grupa rozkazów, które traktują zawartość komórki pamięci lub rejestru
jako ciąg niezależnych bitów. Rozkazy mogą wykonywać operacje na
wskazanych pojedynczych bitach, mogą przesuwać ciąg bitów na sąsiednie
pozycje w lewo lub w prawo, mogą zamieniać wszystkie bity ciągu na
przeciwne (negacja), i wreszcie mogą wykonywać operacje logiczne (sumy,
iloczynu, sumy modulo dwa) na odpowiadających bitach dwóch ciągów.
Operacje na pojedynczych bitach
Lista rozkazów procesorów zgodnych z architekturą x86 m.in. zawiera
cztery rozkazy wykonujące działania na pojedynczych bitach. Położenie bitu
określane jest przez dwa operandy: pierwszy wskazuje rejestr lub komórkę
50
pamięci zawierającą przetwarzany bit, drugi określa numer bitu. Pierwszy
operand może określać obiekty 16- lub 32-bitowe. Drugi argument może być
podany w postaci liczby będącej numerem bitu, albo w postaci nazwy rejestru,
w którym umieszczony jest ten numer. Wszystkie podane niżej rozkazy, przed
wykonaniem operacji, przepisują zawartość wskazanego bitu do znacznika CF.
BT
bit nie ulega zmianie (tylko kopiowanie do CF)
BTS
wpisanie 1 do bitu
BTR
wpisanie 0 do bitu
BTC
zanegowanie zawartości bitu
Przykłady:
btc
ax, cx
bt
edi, 29
Ze względu na rozmaite zastosowania znacznika CF, zdefiniowano rozkazy
bezargumentowe:
CLC
zerowanie (wpisanie 0) znacznika CF
STC
ustawianie (wpisanie 1) znacznika CF
CMC
negowanie zawartości znacznika CF
W zastosowaniach związanych ze sterowaniem pojawia się czasami
problem wyznaczenia numeru bitu, na którym znajduje się wartość 1. Można to
łatwo zrealizować za pomocą niżej podanych rozkazów: rozkaz BSF poszukuje
bitu o wartości 1 posuwając się od lewej do prawej, natomiast rozkaz BSR — od
prawej do lewej.
BSF
poszukiwanie bitu jedynkowego (w prawo)
BSR
poszukiwanie bitu jedynkowego (w lewo)
W podanych rozkazach przeglądany jest obiekt określony przez drugi operand,
zaś wynik wpisywany jest do pierwszego operandu.
Przesunięcia
W praktyce programowania posługujemy się dość często rozkazami
przesunięć, które przesuwają wszystkie bity w rejestrze lub w komórce pamięci
w prawo lub w lewo. Istnieją różne odmiany tych rozkazów, związane z
interpretacją bitu znaku, czy też powodujące, że bity opuszczające rejestr są
wprowadzane ponownie. Podane dalej rysunki wyjaśniają te przesunięcia.
51
Przesunięcie logiczne w lewo i prawo
CF
7 6 5 4 3 2 1 0
0
CF
7 6 5 4 3 2 1 0
0
Przesunięcie logiczne w lewo
Przesunięcie logiczne w prawo
Przesunięcie logiczne
polega na przesunięciu wszystkich bitów na pozycje
sąsiednie z lewej lub z prawej (w zależności od kierunku przesunięcia). Bity
wychodzące wprowadzane są do znacznika CF, zaś na wolne pozycje
wprowadzane są zera. Poniższy przykład ilustruje przesunięcie rejestru DH o 1
pozycję w lewo i o 1 pozycję w prawo.
CF
Rejestr DH przed wykonaniem
instrukcji shl dh, 1
1
0
1
1
1
0
0
1
CF
Rejestr DH po wykonaniu
instrukcji shl dh, 1
0
1
1
1
0
0
1
0
1
CF
Rejestr DH przed wykonaniem
instrukcji shr dh, 1
1
0
1
1
1
0
0
1
CF
Rejestr DH po wykonaniu
instrukcji shr dh, 1
0
1
0
1
1
1
0
0
1
Przesunięcie logiczne w lewo
Przesunięcie logiczne w prawo
Przesunięcie cykliczne (obrót) w lewo i prawo
Przesunięcie cykliczne (obrót) w lewo
Przesunięcie cykliczne (obrót) w prawo
7 6 5 4 3 2 1 0
6 5 4 3 2 1 0
CF
CF
Przesunięcie cykliczne
(nazywane też obrotem) polega na przesunięciu
wszystkich bitów na pozycje sąsiednie z lewej lub z prawej (w zależności od
kierunku przesunięcia). Bity wychodzące wprowadzane są na wolne pozycje z
drugiej strony. Dodatkowo bity wychodzące wpisywane są do znacznika CF.
Poniższy przykład ilustruje przesunięcie cykliczne rejestru DH o 1 pozycję w
lewo i o 1 pozycję w prawo.
52
CF
Rejestr DH przed wykonaniem
instrukcji ror dh, 1
1
0
1
1
1
0
0
1
CF
Rejestr DH po wykonaniu
instrukcji ror dh, 1
1
1
0
1
1
1
0
0
1
CF
Rejestr DH przed wykonaniem
instrukcji rol dh, 1
1
0
1
1
1
0
0
1
CF
Rejestr DH po wykonaniu
instrukcji rol dh, 1
0
1
1
1
0
0
1
1
1
Przesunięcie cykliczne (obrót) w lewo i prawo przez CF
Przesunięcie cykliczne (obrót) przez CF w lewo
Przesunięcie cykliczne (obrót) przez CF w prawo
7 6 5 4 3 2 1 0
6 5 4 3 2 1 0
CF
CF
Przesunięcie cykliczne przez CF
(nazywane też obrotem przez CF) polega na
przesunięciu wszystkich bitów na pozycje sąsiednie z lewej lub z prawej (w
zależności od kierunku przesunięcia). Zawartość znacznika CF wpisywana jest
na wolną pozycję, a bity wychodzące wprowadzane są do znacznika CF.
Poniższy przykład ilustruje przesunięcie cykliczne przez CF rejestru DH o 1
pozycję w lewo i o 1 pozycję w prawo.
CF
Rejestr DH przed wykonaniem
instrukcji rcl dh, 1
1
0
1
1
1
0
0
1
CF
Rejestr DH po wykonaniu
instrukcji rcl dh, 1
0
1
1
1
0
0
1
0
0
1
CF
Rejestr DH przed wykonaniem
instrukcji rcr dh, 1
1
0
1
1
1
0
0
1
0
CF
Rejestr DH po wykonaniu
instrukcji rcr dh, 1
0
1
0
1
1
1
0
0
1
Negacja ciągu bitów
Rozkaz NOT zmienia wszystkie bity w rejestrze lub w komórce pamięci na
wartości przeciwne. Poniższy przykład ilustruje wykonywanie rozkazu negacji
bitowej.
53
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
negacja
0 1 1 1 0 1 0 1
1 0 0 0 1 0 1 0
rejestr AH
zawartość AH
po wykonaniu
rozkazu not ah
bitowa
Operacje logiczne sumy, iloczynu i sumy modulo dwa
Lista rozkazów typowych procesorów zawiera grupę rozkazu
wykonujących operacje na argumentach traktowanych jako ciąg niezależnych
bitów. Przykładowo, zawartość rejestru CX może być traktowana jako ciąg 16
oddzielnych bitów. Jeśli weźmiemy pod uwagę dwa ciągi bitów o jednakowej
długości, umieszczone na przykład w rejestrach CX i DX, to na bitach
traktowanych osobno można wykonywać różne operacje logiczne.
x
y
suma
logiczna
iloczyn
logiczny
suma
modulo
dwa
0
0
0
0
0
0
1
1
0
1
1
0
1
0
1
1
1
1
1
0
Rozkazy
AND,
TEST,
OR,
XOR
wykonują
operacje
na
odpowiadających sobie bitach obu operandów — rezultat wpisywany jest do
operandu docelowego, i jednocześnie ustawiane są znaczniki ZF, SF, PF
(znaczniki CF i OF są zerowane); rozkaz TEST wyznacza iloczyn logiczny
odpowiadających sobie bitów obu operandów (tak jak rozkaz AND), według
tego iloczynu ustawia znaczniki, ale obliczony iloczyn logiczny nie zostaje
nigdzie wpisany.
Podany niżej rysunek zawiera przykład ilustrujący sposób obliczania
sumy logicznej, iloczynu logicznego i sumy modulo dwa.
54
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
suma logiczna
1 0 1 0 0 1 1 1
0 1 1 1 0 1 0 1
1 1 1 1 0 1 1 1
rejestr AH
rejestr BL
rozkazu or ah, bl
zawartość AH
po wykonaniu
bitowa
0
1
2
3
4
5
6
7
iloczyn logiczny
0 0 1 0 0 1 0 1
rozkazu and ah, bl
zawartość AH
po wykonaniu
bitowy
0
1
2
3
4
5
6
7
symetryczna
1 1 0 1 0 0 1 0
rozkazu xor ah, bl
zawartość AH
po wykonaniu
bitowa różnica
Bardzo często rozkaz XOR używany jest do zerowania rejestrów. Poniżej
podano dwa sposoby zerowania rejestru EDI.
mov edi, 0
xor edi, edi
Wyodrębnianie pól bitowych
Jednym z charakterystycznych zastosowań rozkazów wykonujących
operacje na bitach jest wyodrębnianie pól bitowych. Przykładowo, w rejestrze
16-bitowym można zakodować datę wg następującego schematu.
55
Zauważmy, że 7 ostatnich bitów zawiera rok pomniejszony o 1980, co pozwala
ma zapisywanie dat od roku 1980 do 2107.
Zarówno zapisanie daty, jak i jej odczytanie wymaga wykonania operacji
na bitach. Przyjmijmy, że data w podanym formacie została umieszczona w
rejestrze SI. W pierwszej kolejności wyznaczymy zawartość pola dzień. Można
to zrealizować za pomocą poniższych rozkazów.
mov bx, si
shr bx, 11
Po wykonaniu tych rozkazów w rejestrze BX zostanie umieszczony ciąg bitów
0000 0000 0001 0100, co oznacza, że pole dzień zawiera liczbę 20. Z kolei pole
miesiąc można wyznaczyć poprzez wykonanie poniższych rozkazów.
mov cx, si
shr cx, 7
andj cx, 0FFFH
Po wykonaniu tych rozkazów w rejestrze CX zostanie umieszczony ciąg bitów
0000 0000 0000 0010, co oznacza, że pole miesiąc zawiera liczbę 2. Z kolei
pole rok można wyznaczyć poprzez wykonanie poniższych rozkazów.
mov dx, si
and cx, 007FH
add cx, 1980
Tak więc umieszczona tu data to 20.02.2001.
Zasady komunikacji z urządzeniami zewnętrznymi
W dotychczasowych rozważaniach skupiliśmy uwagę na różnych
aspektach wykonywania rozkazu (rozkazów) przez procesor. Obecnie zajmiemy
się sposobami komunikacji z urządzeniami zewnętrznymi — stanowią one okno
na świat, drogę wymiany informacji z otoczeniem. Rozpatrzymy również pewne
specjalne sytuacje, w których procesor sygnalizuje przeszkody w dalszym
wykonywaniu programu, określane jako wyjątki procesora.
Rozmaite rodzaje urządzeń zewnętrznych komputera wymagają
doprowadzenia określonych sygnałów, specyficznych dla danego urządzenia,
np. monitor ekranowy wymaga przekazywania, obok informacji o treści
wyświetlanego obrazu, także impulsów synchronizujących, które sygnalizują
rozpoczęcie kreślenia nowej linii i nowego obrazu. W tej sytuacji niezbędne jest
zainstalowanie układów pośredniczących, które dopasowują standardy
sygnałowe procesora i płyty głównej do specyficznych wymagań
poszczególnych urządzeń — takie układy pośredniczące nazywane są często
układami wejścia/wyjścia
. Zwykle układy wejścia/wyjścia umieszczane są na
kartach rozszerzeniowych lub na płycie głównej komputera.
W ten sposób sterowanie pracą urządzeń jest realizowane za pomocą
podzespołów tworzących układy wejścia/wyjścia. Podzespoły te umożliwiają
56
testowanie stanu (gotowości) urządzenia, wysyłanie poleceń do urządzenia oraz
wysyłanie i przyjmowanie danych. Od strony procesora ww. komunikacja
odbywa się zazwyczaj poprzez zapis i odczyt rejestrów zainstalowanych w
układach wejścia/wyjścia.
Układy
EIDE
RS232C
Karta
graficzna
Centronics
Karta
dźwiękowa
SCSI
Dysk twardy
CD ROM
Napęd ZIP
Urządzenia
Modem
Mysz
Monitor ekran.
Drukarka
Mikrofon
Głośniki
Dysk twardy
Skaner
Streamer
wejścia/wyjścia
Stosowane są dwie metody dostępu do zawartości rejestrów układów
wejścia/wyjścia: poprzez współadresowany obszar pamięci i poprzez odrębną
przestrzeń adresową portów wejścia–wyjścia
. W pierwszym przypadku
poszczególne rejestry sterownika urządzenia (np. karty graficznej) są dostępne
poprzez ustalone adresy komórek pamięci operacyjnej. W rezultacie zapis i
odczyt tych rejestrów wykonywany jest za pomocą tych samych rozkazów (np.
MOV), które używane do zapisu i odczytu komórek pamięci. W drugim
przypadku mamy do czynienia z odrębną przestrzenią adresową, w której
dostępne są rejestry poszczególnych urządzeń. Ta przestrzeń nazywana jest
przestrzenią adresową wejścia-wyjścia
lub przestrzenią adresową portów.
Niekiedy używa się też określenia izolowane wejście-wyjście.
57
W komputerach PC spotykamy oba rodzaje komunikacji, przy czym
komunikacja poprzez współadresowalny obszar pamięci stosowana jest głównie
do przesyłania danych do/z urządzeń, zaś komunikacja poprzez porty wejścia–
wyjścia służy przede wszystkim do sterowania pracą urządzeń. Operacje w tej
przestrzeni wykonywane są przez specjalnie zaprojektowane rozkazy — w
procesorach zgodnych z architekturą Intel 32 podstawowe znaczenie mają
rozkazy IN i OUT, natomiast ich odmiany, np. INSB, OUTSB, itd. używane są
rzadziej. Przykładowo, używane są m.in. rozkazy:
in al, 60H —
przesłanie zawartości portu 60H do rejestru AL
(w PC: odczyt numeru naciśniętego klawisza);
out 64H, al —
przesłanie zawartości rejestru AL do portu 64H.
Współczesne systemy operacyjne powszechnego użytku, z pewną pomocą
procesora, nie zezwalają zwykłym programom na bezpośredni dostęp do
rejestrów urządzeń komputera. Zazwyczaj sterowanie urządzeniami jest dość
skomplikowane i wysłanie niewłaściwych kodów sterujących mogłoby
doprowadzić do uszkodzenia lub przedwczesnego zużycia urządzenia. Mogłyby
też występować kolizje w zakresie dostępu do urządzeń, jeśli w komputerze
uruchomionych jest kilka programów.
Praktycznie oznacza to, że rozkazy IN i OUT nie mogą być stosowane w
zwykłych programach, a ewentualne ich użycie powoduje wygenerowanie
wyjątku procesora i najczęściej zakończenie wykonywania programu. Tak samo
niedozwolony jest bezpośredni zapis do (dalej omawianej) pamięci ekranu.
Omawiane operacje mogą być jednak bez ograniczeń wykonywane przez system
operacyjny, ponieważ pracuje on na wysokim poziomie uprzywilejowania, w
którym dostępna jest pełna lista rozkazów procesora.
Zwykłe programy mogą odwoływać się do urządzeń komputera wyłącznie
za pośrednictwem systemu operacyjnego. Odwołania te mają postać zgodną z
interfejsem API podanym dla danego systemu operacyjnego — zagadnienia te
omawiane są w dalszej części opracowania.
Współadresowalne układy wejścia/wyjścia
Typowym
przykładem
wykorzystania
techniki
układów
współadresowalnych jest pamięć ekranu w komputerach PC. W trybie
tekstowym sterownika graficznego znaki wyświetlane na ekranie stanowią
odwzorowanie zawartości obszaru pamięci od adresu fizycznego B8000H.
Pamięć ta należy do przestrzeni adresowej procesora, ale zainstalowana jest na
karcie sterownika graficznego.
58
000B8000H
Adresy
fizyczne
Przestrzeń adresowa
pamięci
RAM
RAM
pamięć ekranu
41H
45H
42H
000B8000H
000B8001H
000B8002H
000B8005H
000B8004H
000B8003H
000B8009H
000B8008H
000B8007H
000B8006H
07H
07H
07H
Ekran (tryb tekstowy)
A B E
Bajt atrybutu
R G B
R G B
M
I
kolor znaku
kolor tła
Każdy znak wyświetlany na ekranie jest opisywany przez dwa bajty w
pamięci ekranu: bajt o adresie parzystym zawiera kod ASCII znaku, natomiast
następny bajt zawiera opis sposobu wyświetlania, nazywany atrybutem znaku.
Kolejne bajty omawianego obszaru odwzorowywane są w znaki na ekranie
począwszy od pierwszego wiersza od lewej do prawej, potem drugiego wiersza,
itd. tak jak przy czytaniu zwykłego tekstu.
Pamięć ekranu w trybie graficznym
Współczesne sterowniki (karty) graficzne oferują zazwyczaj wiele trybów
wyświetlania, różniących się rozdzielczością, liczbą kolorów i innymi
parametrami — wszystkie sterowniki realizują nadal funkcje zwykłego
sterownika VGA. Sterownik VGA oferuje między innymi dość prosty tryb
graficzny oznaczony numerem 13H, w którym raster ma wymiary 320 * 200
pikseli, przy 256 kolorach.
W trybie 13H pamięć ekranu, zawierająca 64000 bajtów (320 * 200),
umieszczona jest począwszy od adresu fizycznego A0000H. Kolejne bajty w
tym obszarze opisują kolory pikseli wg standardowej palety VGA (paletę można
zmienić), np. 10 oznacza kolor jasnozielony. Podany niżej fragment programu
powoduje wyświetlenie jasnozielonej linii pionowej w środku ekranu.
59
mov
ecx, 200
; liczba linii na ekranie
mov
ebx, 160
; adres początkowy
ptl_lin:
mov
byte PTR [ebx], 10 ; kolor jasnozielony
add
ebx, 320
loop
ptl_lin
Przestrzeń adresowa portów
W dalszym ciągu zajmiemy się sterowaniem poprzez porty. Do zapisu i
odczytu informacji w przestrzeni adresowej portów stosuje się rozkazy IN i
OUT oraz ich rozszerzenia.
Procesor
Przestrzeń adresowa portów
in
out
Poniższa tablica zawiera przedziały adresacji w przestrzeni portów wejścia-
wyjścia dla typowych urządzeń zewnętrznych.
Adres
Nazwa układu
000H - 01FH
Sterownik DMA nr 1
020H - 03FH
Sterownik przerwań 8259A (master)
040H - 05FH
Generatory programowalne
060H - 06FH
Sterownik klawiatury
070H - 07FH
Zegar czasu rzeczywistego
Zasady sterowania poszczególnych urządzeń mogłyby stanowić treść
książki (zob. np. Metzger, Anatomia PC), toteż skupimy się tu na kilku prostych
przykładach.
60
Przykład zmiany palety w trybie graficznym
W omawianym wcześniej trybie graficznym 13H (VGA) używana jest
standardowa paleta, w której kod 10 oznacza kolor jasnozielony. Podany niżej
fragment programu dokonuje zmiany palety, w taki sposób, że kod 10 oznaczać
będzie kolor żółty. Zmiana palety dokonywana jest poprzez wpisanie kodu
koloru do portu 3C8H, a następnie przesłanie składowych: R (czerwony), G
(zielony), B (niebieski) do portu 3C9H. Poszczególne składowe mogą
przyjmować wartości z przedziału <0, 63>.
mov
dx, 3C8H
mov
al, 10
; kod koloru
out
dx, al
mov
dx, 3C9H
mov
al, 63
; składowa czerwona (R)
out
dx, al
mov
al, 63
; składowa zielona (G)
out
dx, al
mov
al, 0
; składowa niebieska (B)
out
dx, al
Sterowanie pracą urządzeń zewnętrznych komputera
Komunikacja z urządzeniami realizowana poprzez odczyt i zapis
rejestrów urządzeń dostępnych na poziomie rozkazu programu. Zlecenie by
urządzenie wykonało pewną operację wymaga podjęcia następujących działań:
1. sprawdzenie stanu urządzenia;
2. wysłanie odpowiednich poleceń do urządzenia, o ile znajduje się ono w
stanie gotowości;
3. przesłania (lub odczytania) danych;
4. sprawdzenie czy urządzenie wykonało polecenie:
a. metoda przeglądania (odpytywania),
b. metoda przerwaniowa
Wiele urządzeń pracujących w otoczeniu procesora nie wymaga ciągłego
nadzoru. Zazwyczaj ich obsługa ogranicza się do ich zainicjowania i
późniejszego odebrania wyniku operacji. Dobrym przykładem jest transmisja
szeregowa w standardzie RS232C. Co jakiś czas w buforze odbiornika pojawia
się nowy znak. Procesor powinien go odczytać w możliwie krótkim czasie. Jeśli
tego nie uczyni, to następny znak, który zostanie odebrany, zamaże poprzedni i
tym samym zostanie on utracony. Są dwie możliwe metody realizacji takiego
odbioru: metoda przeglądania (odpytywania) i metoda przerwaniowa.
61
Metoda przeglądania polega na cyklicznym sprawdzaniu, czy nadszedł
nowy znak. Jej podstawową wadą jest wymóg wielokrotnego wykonywania
określonej sekwencji rozkazu testowych w ustalonych odstępach czasu. Odstęp
między kolejnymi testami jest uzależniony od szybkości transmisji i mocy
obliczeniowej procesora.
Ogólnie, metoda przeglądania polega na wielokrotnym odczytywaniu
stanu urządzenia, aż do chwili gdy odczytany stan wskazywać będzie na
zakończenie operacji. Metoda przeglądania jest nieefektywna i jałowo pochłania
czas pracy procesora. Trzeba też brać pod uwagę możliwość, że oczekiwane
zdarzenie może wystąpić po bardzo długim czasie lub w ogóle nie wystąpić.
Jeśli nawet sprawdzenie urządzenia wykonywane jest w pewnych
odstępach czasu, to występują przerwy w obsłudze urządzenia, które zakłócają
płynność jego pracy — urządzenie musi czekać na obsługę, co nie zawsze jest
dopuszczalne (np. nieodczytany bajt zostaje zamazany przez kolejny przyjęty).
Z kolei zwiększenie częstotliwości sprawdzania zwiększa straty czasu procesora
— zazwyczaj dobór optymalnej częstotliwości sprawdzania jest trudny.
Najbardziej efektywna jest metoda przerwaniowa — urządzenie
sygnalizuje zakończenie operacji (albo niezdolność do dalszego jej
wykonywania) za pomocą sygnału przerwania skierowanego do procesora.
Sygnał przerwania powoduje przerwanie wykonywania aktualnego programu i
przejście do wykonania podprogramu obsługi urządzenia, właściwego dla
przyjętego przerwania.
W odniesieniu do omawianego przykładu transmisji szeregowej oznacza
to, że układ transmisji szeregowej, po odebraniu nowego znaku generuje
przerwanie. Wówczas procesor przerywa wykonywanie głównego programu i
przechodzi do rozkazów obsługi transmisji. W ramach obsługi odczyta znak z
bufora, prześle do obszaru docelowego i powróci do wykonywania przerwanego
programu.
Odpowiednie środki sprzętowe i programowe powinny zapewnić
możliwość wznowienia wykonywania przerwanego programu po zakończeniu
podprogramu obsługi urządzenia. Można powiedzieć, że przerwanie powinno
być niewidoczne dla aktualnie wykonywanego programu, powodując jedynie
jego chwilowe zatrzymanie. Metoda przerwaniowa jest zazwyczaj trudniejsza
do zaprogramowania, ale jest znacznie bardziej efektywna.
Przerwania sprzętowe
Rozważmy najpierw ogólne zasady obsługi przerwań. Oprócz
sekwencyjnego wykonywania głównego programu, procesor musi być
przygotowany do obsługi przerwań, które pojawiają się asynchronicznie.
Zazwyczaj procesor podejmuje obsługę przerwania po zakończeniu aktualnie
wykonywanego rozkazu. Następnie zapisuje położenie w pamięci (adres)
62
kolejnego rozkazu do wykonania, który został by wykonany, gdyby nie nadeszło
przerwanie. Zazwyczaj położenie to zapisywane jest na stosie.
Obsługa przerwania polega na wykonaniu specjalnego podprogramu,
który sprawdza stan urządzenia i wysyła do niego odpowiednie polecenia.
Oczywiście dla każdego urządzenia musi istnieć odrębny podprogram,
dostosowany do jego specyfiki.
Po zakończeniu obsługi przerwania musi nastąpić wznowienie
wykonywania programu głównego. Obsługa przerwanie nie może mieć żadnego
wpływu na wykonywanie programu głównego, w szczególności nie mogą
nastąpić jakiekolwiek zmiany zawartości rejestrów i znaczników. Ponieważ
rejestry i znaczniki będą używane w trakcie obsługi przerwania, trzeba je więc
od razu zapamiętać i odtworzyć bezpośrednio przez zakończeniem obsługi.
Działania te wykonywane są zazwyczaj programowo, z częściowym
wspomaganiem sprzętowym. Przykładowo, w architekturze Intel 32
automatycznie zapamiętywany jest tylko rejestr znaczników, inne rejestry muszą
być zapamiętane przez program obsługi.
Omówimy teraz technikę obsługi przerwań sprzętowych stosowaną w
architekturze Intel 32. Warunkiem przyjęcia przerwania sprzętowego
(generowanego przez urządzenie zewnętrzne) jest stan znacznika IF = 1.
Znacznik IF (ang. interrupt flag) w rejestrze znaczników (bit nr 9) określa
zezwolenie na przyjmowanie przerwań: procesor może przyjmować przerwania
tylko wówczas, gdy IF = 1. Znacznik IF jest automatycznie zerowany w chwili
przyjęcia przerwania.
Możliwe jest zablokowanie przyjmowania przerwań poprzez wyzerowanie
znacznika IF. W programie, do zmiany stanu znacznika IF można zastosować
rozkazy CLI (IF ← 0) lub STI (IF ← 1).
W procesorach Pentium po wystąpieniu przerwania sprzętowego,
bezpośrednio przed uruchomieniem programu obsługi przerwania na stosie
zapisywany jest ślad, który umożliwia powrót do przerwanego programu.
Struktura śladu jest identyczna jak w przypadku rozkazu INT.
Obsługa przerwań jest ściśle związana z tablicą
deskryptorów przerwań
. Tablica ta zawiera 256 adresów, które
wskazują różne podprogramy systemowe, w tym podprogramy
obsługi przerwań sprzętowych.
IF
9
rozkaz CLI wpisuje 0 do IF
rozkaz STI wpisuje 1 do IF
EIP
EFLAGS
CS
63
Po zapisaniu śladu na stosie procesor odszukuje w tablicy deskryptorów
przerwań adres podprogramu obsługi przerwania i rozpoczyna wykonywać
podprogram. Numer deskryptora, w którym zawarty jest adres podprogramu
obsługi zależy w ustalony sposób od numeru linii IRQ, poprzez którą nadszedł
sygnał przerwania. Ustalenie to zależy od reguły przyjętej w systemie
operacyjnym, np. w systemie Linux linia przerwania IRQ 0 jest skojarzona z
deskryptorem nr 32, linia IRQ 1 z deskryptorem 33, linia IRQ 2 z deskryptorem
34 itd. Dalsze szczegóły podane w następnym podrozdziale.
Podprogram obsługi przerwania kończy rozkaz IRET, która powoduje
wznowienie wykonywania przerwanego programu poprzez odtworzenie
rejestrów EIP, CS i EFLAGS, na podstawie śladu zapamiętanego na stosie.
Sterownik przerwań
Zazwyczaj każde urządzenie dołączone do komputera jest w stanie
generować sygnały przerwań. Wymaga to odpowiedniego zorganizowania
systemu przerwań, tak poszczególne przerwania były przyjmowane wg ustalonej
hierarchii. Na ogół procesor nie jest przygotowany do bezpośredniej obsługi
przerwań, zwłaszcza jeśli jest zainstalowanych dużo urządzeń. Stosowane są
różne systemy obsługi przerwań; niekiedy zainstalowana jest wspólna linia
przerwań dla wszystkich urządzeń — po nadejściu przerwania procesor
sprawdza stany poszczególnych urządzeń identyfikując urządzenie, które
wysłało przerwanie (metoda odpytywania). W innych systemach linia przerwań
przechodzi przez wszystkie zainstalowane urządzenia ustawione wg
priorytetów.
W komputerach PC system przerwań obsługiwany jest przez układ APIC.
Zastąpił on używane dawniej dwa układy typu 8259. Sygnały przerwań z
poszczególnych urządzenia kierowane są do układu APIC poprzez linie
oznaczone symbolami IRQ 0 – IRQ 23.
64
Procesor 2
Local APIC 2
Procesor 3
Local APIC 3
Procesor 1
Local APIC 1
Układ 8259
I/O APIC
Sygnały
przerwań
65
IRQ
Urządzenie
N
r
d
e
s
k
ry
p
IR
Q
Urządzenie
N
r
d
e
s
k
ry
p
0
zegar systemowy,
przerwanie wysyłane
przez układ 8254 (w
systemie DOS około
18 razy/s)
32
8
zegar czasu
rzeczywistego,
przerwanie
generowane
ustalonym czasie
(budzenie)
40
1
klawiatura,
przerwanie wysyłane
po naciśnięciu lub
zwolnieniu klawisza
33
9
41
2
połączone z drugim
układem 8259
10
42
3
łącze szeregowe
COM2
35
11
43
4
łącze szeregowe
COM1
36
12
44
5
łącze równoległe
LPT2
37
13 koprocesor
arytmetyczny
45
6
sterownik dyskietek
38
14 sterownik dysku
twardego
46
7
łącze równoległe
LPT1
39
15
47
Z każdą linią IRQ (ang. interrupt request) skojarzony jest wektor
przerwania w tablicy deskryptorów przerwań. Skojarzenie to wykonywane
poprzez odpowiednie zaprogramowanie układu APIC — wykonuje to system
operacyjny podczas inicjalizacji. Typowe przyporządkowanie stosowane w
systemie Linux podane jest poniższej tabeli.
Zatem, nadejście sygnału, np. IRQ 1 powoduje przerwanie i uruchomienie
podprogramu obsługi przerwania, którego adres znajduje się w deskryptorze 33
(w przypadku systemu Linux).
Zegar czasu rzeczywistego RTC
Zegar czasu rzeczywistego RTC (ang. real time clock) stanowi odrębny
podzespół komputera, który udostępnia aktualną datę i czas. Układ wykonany
66
jest w technologii CMOS, co zapewnia mały pobór energii — w czasie gdy
komputer nie pracuje, zegar CMOS RTC zasilany jest małej baterii. Odczyt i
zapis zawartości zegara RTC dokonywany jest poprzez porty 70H i 71H.
Przerwanie z zegara czasu rzeczywistego (IRQ 8) występuje tylko
wyjątkowo, jeśli została w zegarze RTC została zaprogramowana operacja
"budzenia" o ustalonej godzinie.
Przerwania niemaskowalne
Omówione wyżej przerwania mogą być blokowane poprzez wyzerowanie
znacznika IF, wobec czego zaliczane są do klasy przerwań maskowalnych.
Procesor Pentium może też przyjmować przerwania niemaskowalne, które nie
mogą być blokowane. Przerwania niemaskowalne (ang. NMI — non-maskable
interrupt) stosuje do sygnalizacji zdarzeń wymagających natychmiastowej
obsługi niezależnie od stanu systemu. W komputerach PC przerwanie
niemaskowalne generowane jest w przypadku zidentyfikowania błędu pamięci
RAM.
Układy DMA
Przesyłanie
danych
z
urządzenia zewnętrznego do
pamięci RAM jak również w
drugą
stronę
może
być
zazwyczaj
realizowane
za
pomocą procesora. Jednakże w
przypadku
znacznych
ilości
danych
taka
transmisja
absorbuje
czas
procesora,
opóźniając realizacją innych,
ważnych zadań.
Procesor
Pamięć
RAM
Układy
wejścia/wyjścia
Sterownik
DMA
67
W takich przypadkach wskazane jest wykorzystanie techniki
bezpośredniego dostępu do pamięci DMA (ang. direct memory access). Moduł
DMA otrzymuje od procesora następujące informacje:
•
rodzaj operacji (odczyt, zapis),
•
adres urządzenia wejścia-wyjścia,
•
adres obszaru pamięci RAM przewidzianego do odczytania lub
zapisania,
•
liczba bajtów, które mają być odczytane lub zapisane.
Na podstawie otrzymanych danych moduł DMA uruchamia przesyłanie danych,
natomiast procesor kontynuuje inne prace. Po zakończeniu przesyłania moduł
DMA wysyła sygnał przerwania do procesora — dzięki temu procesor jest
angażowany tylko na początku i końcu operacji przesyłania.
Wyjątki procesora
W trakcie wykonywania programu przez procesor występują sytuacje
uniemożliwiające dalsze wykonywanie programu, np. niezidentyfikowany kod
rozkazu, próba zmiany zawartości lokacji poza dozwolonym adresem, itd.
Wystąpienie takich sytuacji powoduje wygenerowanie wyjątku przez procesor.
Działania podejmowane przez procesor w chwili wystąpienia wyjątku są prawie
identyczne jak w przypadku wystąpienia przerwania. O ile jednak przerwania
powstają wskutek zdarzeń zewnętrznych w stosunku do procesora, to wyjątki
związane są z wykonywaniem rozkazów przez procesor.
Adresy podprogramów obsługujących wyjątki zawarte są w tablicy
deskryptorów przerwań na pozycjach 0 ÷ 31, przy czym aktualnie nie wszystkie
deskryptory są używane. Tak jak w przypadku obsługi przerwania, wyjątek
procesora powoduje zapamiętanie śladu na stosie i rozpoczęcie wykonywania
podprogramu właściwego dla określonego wyjątku. Zazwyczaj programy
obsługi wyjątków stanowią integralną część systemu operacyjnego. W starszych
systemach istniała możliwość zainstalowania własnego programu obsługi
wyjątku w miejsce standardowego.
Wystąpienie nadmiaru przy dzieleniu (rozkazy DIV lub IDIV) powoduje
wygenerowanie wyjątku, który skojarzony jest z deskryptorem nr 0 w tablicy
deskryptorów przerwań. W rezultacie rozpocznie się wykonywanie
podprogramu, którego adres znajduje się w deskryptorze nr 0.
Wyjątek nr 13 — błąd ochrony (ang. general protection error) —
generowany w przypadku próby naruszenia niedostępnych zasobów, np. gdy
zwykły program próbuje odczytać nieprzydzielony mu obszar pamięci lub
wykonać rozkaz CLI (który zeruje znacznik IF). Niektóre wyjątki nie mają
charakteru błędów, ale używane do sygnalizowania pewnych sytuacji, w których
procesor nie może dalej wykonywać programu — sterowanie przekazywane jest
wówczas do systemu operacyjnego, który dokonuje odpowiednich zmian w
68
pamięci i wznawia wykonywanie programu. Przykładem takiego wyjątku jest
błąd stronicowania (nr 14) generowany, jeśli odwołanie dotyczy strony aktualnie
nieobecnej w pamięci operacyjnej. Po odczytaniu przesłaniu potrzebnej strony z
dysku do pamięci operacyjnej program jest wznawiany.
Specyfika obliczeń naukowo–technicznych
Omawiany wcześniej sposób kodowania liczb mieszanych może być
kłopotliwy w przypadku, gdy w obliczenie wykonywane jest na liczbach bardzo
dużych i bardzo małych, np. obliczenie stałej czasowej obwodu RC:
R = 4.7 M , C = 68 pF
RC
Ω
=
⋅
⋅
⋅
=
⋅
−
−
4 7 10 68 10
319 6 10
6
12
6
.
.
Wartości R i C w postaci binarnej mają postać
R=01000111 10110111 01100000
C=0.00000000 00000000 00000000 00000000 01001100 . . . . .
Kodowanie z zadowalającą dokładnością obu tych liczb wymagałoby
wprowadzenia 24 bitów dla części całkowitej i 40 bitów dla części ułamkowej,
co w konsekwencji wymagałoby zdefiniowania formatu 8-bajtowego. Łatwo
zauważyć, że reprezentacja binarna wartości 68 pF zawierała by 57 bitów
zerowych z lewej strony, a reprezentacja wartości 4.7 MΩ zawierała by 40
bitów zerowych z prawej strony.
Z tego względu obliczenia na liczbach niecałkowitych wykonywane są
zazwyczaj w arytmetyce zmiennoprzecinkowej (zmiennopozycyjnej). W
architekturze Intel 32 zdefiniowana jest znaczna liczba rozkazów wykonujących
działania na liczbach zmiennoprzecinkowych przetwarzanych przez koprocesor
arytmetyczny.
Liczby zmiennoprzecinkowe
Liczby
zmiennoprzecinkowe
,
nazywane
też
zmiennopozycyjnymi
,
kodowane są w postaci pary liczb określanych jako mantysa i wykładnik.
mantysa
wykładnik
69
W przypadku ogólnym wartość liczby zmiennoprzecinkowej (różnej od zera)
określa wyrażenie (podane wyrażenie w realizacjach komputerowych ma
zwykle nieco inną postać):
wykladnik
2
mantysa⋅
Pole wykładnika można interpretować jako liczbę pozycji, o którą trzeba
przesunąć w lewo lub w prawo umowną kropkę rozdzielającą część całkowitą i
ułamkową mantysy.
Zazwyczaj wprowadza się warunek normalizacji mantysy (dla liczb ≠ 0):
1
2
≤
<
mantysa
Liczba 0 traktowana jest jako wartość specjalna i reprezentowana jest przez kod
zawierający same zera w polu wykładnika i mantysy.
Obliczenia wykonywane na komputerach różnych typów powinny dawać
jednakowe rezultaty. W komputerach starszych typów, ze względu na różne
formaty liczb zmiennoprzecinkowych i inne reguły zaokrąglania, postulat ten
nie zawsze był spełniony. Z tych powodów przyjęto normę IEEE 754, która
została opracowana z myślą aby ułatwić przenoszenie programów z jednego
procesora do drugiego — określa ona specyficzne metody i procedury służące
temu, aby arytmetyka zmiennoprzecinkowa dawała jednolite i przewidywalne
wyniki, niezależnie od platformy sprzętowej. Norma ta jest stosowana
praktycznie we wszystkich we wszystkich współczesnych procesorach i
koprocesorach arytmetycznych.
Norma
IEEE
754
określa
też
standardowe
formaty
liczb
zmiennoprzecinkowych. Podstawowym formatem liczb jest format 64-bitowy.
Pokazano, że uzyskiwanie dokładnych wyników 64-bitowych wymaga
wykonywania niektórych obliczeń na liczbach 80-bitowych.
Formaty liczb zmiennoprzecinkowych
Koprocesor
arytmetyczny
wykonuje
działania
na
liczbach
zmiennoprzecinkowych 80-bitowych w formacie pośrednim (nazywanym też
chwilowym, ang. temporary real, extended precision).
70
S wykładnik
mantysa (64-bitowa)
15 bitów
bit znaku:
S = 0 — liczba dodatnia
S = 1 — liczba ujemna
umowna kropka rozdzielająca część
całkowitą i ułamkową mantysy
(w formacie 80-bitowym część
całkowita mantysy występuje
w postaci jawnej)
2
–1
2
0
2
–2
2
–3
2
–4
. . . . . . . .
. . . . . . . .
Liczba 0 kodowana jest jako tzw. wartość wyjątkowa (zob. dalszy opis): pole
mantysy i pole wykładnika zawiera same zera.
Oprócz formatu 80-bitowego pośredniego koprocesor akceptuje także
inne formaty zmiennoprzecinkowe, całkowite i BCD. Obliczenia wykonywane
są najczęściej na liczbach zmiennoprzecinkowych w formacie 64-bitowym,
które określane są jako liczby zmiennoprzecinkowe długie (ang. double
precision). Stosowany jest także format 32-bitowy — liczby zapisane w tym
formacie określane są jako liczby zmiennoprzecinkowe krótkie (ang. single
precision).
Warunek normalizacji mantysy wymaga, by jej wartość (dla liczb ≠ 0)
zawierała się w przedziale (−2, −1> lub <1, 2). Oznacza to, że bit części
całkowitej mantysy (dla liczb ≠ 0) będzie zawsze zawierał 1 — zatem można
pominąć bit części całkowitej mantysy, zwiększając o 1 liczbę bitów części
ułamkowej. Takie kodowanie stosowane jest w formatach 64- i 32-bitowych —
mówimy wówczas, że część całkowita mantysy występuje w postaci niejawnej.
71
W celu uniknięcia konieczności wprowadzenia znaku wykładnika stosuje
się przesunięcie wartości wykładnika o:
16383 = 3FFFH dla formatu 80-bitowego, czyli
1023 = 3FFH dla formatu 64-bitowego, czyli
127 = 7FH dla formatu 32-bitowego, czyli
Ponadto koprocesor akceptuje 3 formaty liczb całkowitych (16-bitowy, 32-
bitowy i 64-bitowy) oraz 80-bitowy format BCD, jednak obliczenia wewnątrz
koprocesora prowadzone są zawsze w formacie zmiennoprzecinkowym 80-
bitowym.
Kompilatory języka C kodują wartości typu double jako liczby
zmiennoprzecinkowe
64-bitowe,
a
wartości
float
jako
liczby
zmiennoprzecinkowe 32-bitowe.
Przykład kodowania liczby zmiennoprzecinkowej
Kodowanie liczby 12.25 w formacie 32-bitowym
Liczbę 12.25 przedstawiamy w postaci iloczynu
m
k
⋅
2
. Wykładnik potęgi k musi
być tak dobrany, by spełniony był warunek normalizacji mantysy 1
2
≤
<
| |
m
,
czyli
Łatwo zauważyć, że warunek normalizacji jest spełniony, gdy k = 3. Zatem
12 25
12 25
2
2
1 53125 2
1 10001
2
1 10001
2
3
3
3
2
130 127
2
10000010
127
2
.
.
.
( .
)
( .
)
(
)
=
⋅
=
⋅
=
⋅
=
=
⋅
−
−
Ponieważ część całkowita mantysy nie jest kodowana, więc w polu mantysy
zostanie wpisana liczba
(0.10001)
2
, zaś w polu wykładnika (po przesunięciu
o 127) liczba
(10000010)
2
. Ostatecznie otrzymamy
liczba
mantysa
wykladnik
=
×
−
2
16383
liczba
mantysa
wykladnik
=
+
×
−
(
)
1
2
1023
liczba
mantysa
wykladnik
=
+
×
−
(
)
1
2
127
1
12 25
2
2
≤
<
.
k
72
Zasady wykonywania obliczeń przez koprocesor arytmetyczny
Koprocesor arytmetyczny stanowi odrębny procesor, współdziałający z
procesorem głównym, umieszczony w tej samej obudowie. Wcześniejsze wersje
koprocesorów (387, 287, 8087) konstruowane były w postaci oddzielnych
układów scalonych.
Liczby, na których wykonywane są obliczenia, składowane są w 8
rejestrach 80-bitowych tworzących stos (który nie ma nic wspólnego ze stosem
obsługiwanym przez główny procesor). Rozkazy koprocesora adresują rejestry
stosu nie bezpośrednio, ale względem wierzchołka stosu. W kodzie
asemblerowym rejestr znajdujący się na wierzchołku stosu oznaczany jest
ST(0) lub ST, a dalsze ST(1), ST(2),..., ST(7). Ze względu na specyficzny
sposób adresowania koprocesor arytmetyczny zaliczany jest do procesorów o
architekturze stosowej
.
Rejestry stosu
Pola
rejestru
stanu
R7
R6
R5
R4
R3
R2
R1
R0
(rejestry 80-bitowe)
pola 2-
bitowe
Lista rozkazów koprocesora arytmetycznego zawiera rozkazy wykonujące
działania na liczbach zmiennoprzecinkowych, w tym rozkazy przesłania,
bit
mantysa
8 bitów
23 bity
wykładnik
10000010 10001000000000000000000
znaku
0
73
działania arytmetyczne, obliczanie pierwiastka kwadratowego, funkcji
trygonometrycznych (sin, cos, tg, arc tg), wykładniczych i logarytmicznych.
Przykładowo, do obliczenia wartości funkcji tangens używa się rozkazu
FPTAN, który w wyniku podaje dwie liczby — ich iloraz stanowi wartość
funkcji; w celu obliczenia tg α bezpośrednio po rozkazie FPTAN należy
wykonać rozkaz FDIV (bez operandów).
FPTAN
tg α = p/q
ST(0)
π
/ 3 ≈ 1.047
q 1
ST(0)
ST(1)
inna liczba
p 1.73 ≈
3
ST(1)
inna liczba
ST(2)
Koprocesor arytmetyczny charakteryzuje się dość specyficznymi
własnościami, które mogą wydawać się nie do końca jasne (np. możliwość
dzielenia przez zero). Własności te wynikają z obserwacji, że złożone obliczenia
numeryczne trwają czasami wiele godzin czy nawet dni. Wystąpienie nadmiaru
lub niedomiaru nie powinno powodować załamania programu (praktyka
wskazuje, że w złożonych obliczeniach wyniki pośrednie z nadmiarem czy
niedomiarem często mają niewielki wpływ na wynik końcowy). W tym celu
zdefiniowano wartości specjalne.
W koprocesorze arytmetycznym przyjęto, że wszystkie liczby, których
pole wykładnika zawiera same zera lub same jedynki traktowane są jako
wartości specjalne. W zależności od ustawienia bitów w rejestrze sterującym
koprocesora, wystąpienie wartości specjalnej może powodować przerwanie
(wyjątek koprocesora), albo też obliczenia mogą być kontynuowane.
Przykładowo, wartość rezystancji R dla podanego układu można wyznaczyć z
zależności:
3
2
1
R
1
R
1
R
1
1
R
+
+
=
Zamaskowanie wyjątku "dzielenie przez zero" pozwala na poprawne
obliczenie rezystancji R podanego układu, także w przypadku, gdy wartość
rezystancji R1 lub R2 lub R3 wynosi 0.
R
1
R
2
R
3
R
74
Przetwarzanie potokowe w procesorach
Wydajność komputera zależy przed wszystkim od szybkości procesora.
Rozwój elektroniki, opracowanie nowych technologii elektronicznych
umożliwiających wykonywanie operacji w krótszym czasie, pozwala na budowę
coraz szybszych i doskonalszych procesorów. Jednak kluczowe znaczenie ma
sposób wykonywania rozkazów.
Można przyjąć, że wykonanie pojedynczego rozkazu przez procesor
obejmuje poniższe czynności:
•
pobranie rozkazu (ang. instruction fetch), stanowi etap, w którym rozkaz
pobierany jest z pamięci głównej albo z pamięci podręcznej;
•
dekodowanie rozkazu (ang. instruction decode) — rozkaz jest dekodowany,
przy czym identyfikuje się argumenty źródłowe, które przepisywane są do
rejestrów pomocniczych wejściowych procesora (niedostępnych na poziomie
programowania);
•
wykonanie rozkazu (ang. execution) — procesor wykonuje operacje na
argumentach zapisanych w rejestrach pomocniczych, a uzyskane wyniki
zapisuje do rejestrów pomocniczych wyjściowych;
•
zapisanie wyników (ang. write-back) stanowi etap, w którym zawartości
rejestrów pomocniczych wyjściowych zostają skopiowane do zwykłych
rejestrów procesora lub do lokacji pamięci.
Liczba etapów realizacji rozkazu zależy od konstrukcji procesora, wymienione
cztery etapy mają podstawowe znaczenie.
Przejście do kolejnego etapu następuje po zakończeniu realizacji
poprzedniego. Nasuwa się więc koncepcja, ażeby wykonywanie wymienionych
etapów było realizowane przez odrębne podzespoły procesora. Przy takim
rozwiązaniu każdy podzespół mógłby, natychmiast po zakończeniu
przetwarzania jednego rozkazu, przejść do przetwarzania następnego rozkazu.
Taki sposób wykonywania rozkazów nazywany jest przetwarzaniem
potokowym
.
Przetwarzanie potokowe stanowi pewną technikę wykonywania rozkazów
etapami, co umożliwia przyspieszenie pracy procesora. Przetwarzanie potokowe
rozkazów jest podobne do użycia linii montażowej w zakładzie produkcyjnym
— możliwa jest jednoczesna praca nad wyrobami w różnych stadiach produkcji.
W potoku na jednym końcu przyjmowane są nowe elementy wejściowe, zanim
jeszcze elementy poprzednio przyjęte ukażą się na wyjściu.
75
Cykl
Etapy
Pobranie
rozkazu
Dekodowanie
rozkazu
Wykonanie
rozkazu
Zapisanie
wyników
rozkazu
1
Rozkaz 1
2
Rozkaz 2
Rozkaz 1
3
Rozkaz 3
Rozkaz 2
Rozkaz 1
4
Rozkaz 4
Rozkaz 3
Rozkaz 2
Rozkaz 1
5
Rozkaz 5
Rozkaz 4
Rozkaz 3
Rozkaz 2
Przetwarzanie potokowe jest stosowane w różnych typach procesorów,
zarówno o architekturze CISC, jak i w procesorach o zredukowanej liczbie
rozkazów (RISC). Jednak pełne wykorzystanie możliwości przetwarzania
potokowego wymaga przygotowania odpowiedniego kodu przez kompilatory,
które nie zawsze uwzględniają specyfikę takiego przetwarzania.
Można przyjąć, że każdy etap zajmuje jeden cykl zegarowy. Procesor
przyjmuje nowy rozkaz do potoku po każdym cyklu zegara, po czym rozkaz
przechodzi kolejno przez poszczególne etapy. Taka realizacja nie skraca czasu
wykonywania rozkazu, ale zwiększa całkowitą przepustowość, powodując
zakończenie jednego rozkazu po każdym cyklu zegara.
W procesorach klasy CISC dekodowanie rozkazu jest bardziej złożone i z
tego względu przedstawiane jest w trzech etapach:
•
właściwe dekodowanie rozkazu — obejmuje określenie kodu operacji i
specyfikatorów argumentów;
•
obliczanie argumentów — obejmuje obliczanie adresu efektywnego każdego
argumentu źródłowego (z ewentualnym wykorzystaniem rejestrów
modyfikacji adresowych, adresowania pośredniego itp.);
•
pobieranie argumentów — pobranie argumentów z pamięci lub z rejestrów i
przepisanie do rejestrów pomocniczych wejściowych.
Po zapełnieniu potoku, po każdym cyklu zegarowym zostaje zakończony
jeden rozkaz — dla podanego przykładu współczynnik przyspieszenia wynosi 4.
Warto dodać, że poszczególne rozkazy wykonywane są zazwyczaj w ciągu kilku
cykli zegarowych, natomiast producenci procesorów podają liczbę cykli
potrzebnych dla wykonania poszczególnych rozkazów przy założeniu, że rozkaz
stanowi jeden z wielu kolejno wykonywanych rozkazów. Zatem wartości tej nie
należy traktować jako czasu wykonywania rozkazu, ale raczej jako miarę
76
wydajności procesora (gotowe samochody w fabryce pojawiają się co dwie
minuty, ale montaż pojedynczego samochodu trwa wiele godzin).
Istnieją różne przyczyny, które powodują zmniejszenie współczynnika
przyspieszenia:
•
realizacja niektórych etapów może powodować konflikty dostępu do
pamięci;
•
jeśli czasy trwania poszczególnych etapów mogą być niejednakowe, to na
różnych etapach wystąpi pewne oczekiwanie;
•
w programie występują skoki (rozgałęzienia) warunkowe, które mogą
zmienić kolejność wykonywania instrukcji, a tym samym unieważnić kilka
pobranych rozkazów — muszą one być usunięte z potoku, a potok musi być
zapełniony nowym strumieniem rozkazów;
•
wystąpienie przerwania sprzętowego lub wyjątku procesora stanowi
zdarzenie nieprzewidywalne i również pogarsza przetwarzanie potokowe;
•
niektóre rozkazy wymagają dodatkowych cykli (np. do ładowania danych);
•
czasami rozkazy muszą oczekiwać z powodu zależności od nie
zakończonych poprzednich rozkazów — system musi zawierać rozwiązania
zapobiegające tego rodzaju konfliktom.
Znaczna część ww. przyczyn opóźnień jest stopniowo eliminowana poprzez
opracowanie ulepszonych metod przetwarzania, np. udaje się, z dużym
prawdopodobieństwem, przewidywać kierunek rozgałęzienia dla skoków
warunkowych.
Kodowanie programów bez użycia skoków
Współczesne procesory umożliwiają równoległe wykonywanie kilku
instrukcji (ang. ILP — instruction level parallelism) w jednym cyklu
zegarowym, ale dwa istotne czynniki ograniczają równoległość wykonywania:
•
źle przewidziane rozgałęzienia (skoki);
•
relatywnie wysokie opóźnienie związane z ładowaniem danych z pamięci.
W ostatnich latach skupiono uwagę na sposobach kodowania, w których nie
używa się (lub ogranicza) rozkazów skoku warunkowego. Problem ten
wyjaśnimy na przykładzie. Przypuśćmy, że w rejestrach ESI i EDI znajdują się
dwie 32-bitowe liczby całkowite bez znaku, a zadanie polega na wyświetleniu
na ekranie większej z tych liczb. Konwencjonalne rozwiązanie mogłoby
wyglądać tak:
mov
eax, esi
cmp
esi, edi
jae
dalej
; rozkaz skoku warunkowego
mov
eax, edi
77
dalej:
call
wyswietl32 ; wyświetlanie zawartości EAX
Poszukiwaną wartość można wyznaczyć także w inny sposób.
Przyjmijmy, że 32-bitowa zmienna b może przyjmować wartości 00...000
(false) albo 11...111 (true). Algorytm obliczeń, w którym nie używa się
skoków, można zapisać w postaci:
b = ESI > EDI
wynik = ESI & b + EDI & (~ b)
print (wynik)
gdzie symbole & i ~ oznaczają:
&
bitowy iloczyn logiczny
~
negacja bitowa
Podany algorytm implementuje poniższa sekwencja rozkazów:
mov
edx, 0
; zmienna b
cmp
esi, edi
setg
dl
; wpisanie 1 do DL, jeśli ESI > EDI
neg
edx
; zmiana znaku liczby kodowanej w U2
; jeśli liczba w ESI była większa od liczby w EDI, to w EDX
; (zmienna b) będą same jedynki, w przeciwnym razie same zera
and
esi, edx
not
edx
and
edi, edx
or
esi, edi
mov
eax, esi
call
wyswietl32 ; wyświetlanie zawartości EAX
Przedstawiona tu metoda programowania, nazywana metodą predykatową (ang.
predication), jest wspomagana sprzętowo w najnowszych typach procesorów.
Pamięć podręczna
W typowych komputerach pamięć główna (operacyjna) konstruowana jest
w postaci pamięci dynamicznej, w której informacje przechowywane są w
postaci ładunków w mikrokondensatorach. Pamięci tego typu są względnie
tanie, zajmują mało miejsca, ale dla współczesnych procesorów są zbyt wolne.
Wytwarzane są także pamięci statyczne, które są szybsze od dynamicznych, ale
pobierają więcej energii, są znacznie droższe i charakteryzują się niższym
78
stopniem scalenia — trudno jest więc zbudować pamięć główną (operacyjną)
komputera wyłącznie z pamięci statycznych.
Przedstawiony problem ten rozwiązuje się poprzez zainstalowanie
stosunkowo niedużej pamięci statycznej, pełniącej rolę bufora między
procesorem a pamięcią główną. Pamięć taka nosi nazwę pamięci podręcznej
(ang. cache memory).
Funkcjonowanie pamięci podręcznej opiera się na zasadzie lokalności:
programy mają tendencję do ponownego używania danych i rozkazów, które
były niedawno używane. Rozkazy i dane używane w krótkim odstępie czasu są
zwykle położone w pamięci blisko siebie.
Pamięć podręczna zawiera pewną liczbę obszarów (nazywanych też
wierszami lub liniami), które służą do przechowywania bloków z pamięci
głównej. Typowy blok zawiera 4 ÷ 16 bajtów. W trakcie wykonywania
rozkazów procesor szuka najpierw rozkazów i danych w pamięci podręcznej:
•
jeśli potrzebna informacja zostanie znaleziona, co jest określane jako
trafienie (ang. cache hit)
, to jest przesyłana do procesora;
•
jeśli potrzebnej informacji nie ma w pamięci podręcznej (chybienie, ang.
cache miss
), to jest ona pobierana z pamięci głównej, przy czym
jednocześnie wpisywana jest do pamięci podręcznej w postaci całego bloku;
Załadowanie całego bloku do pamięci jest wskazane, ponieważ zgodnie z zasadą
lokalności istnieje duże prawdopodobieństwo, że potrzebne będą kolejne dane.
Adres pamięci
etykieta
słowo
etykieta
blok
Pamięć podręczna
0000003
blok 3
0000003
Stosowane są różne organizacje pamięci podręcznej, a wśród nich
organizacja oparta na adresowaniu asocjacyjnym. W tym przypadku każdy blok
(4 ÷ 16 bajtów) pamięci podręcznej zawiera pole etykiety, nazywanej też
numerem bloku. Przykładowo, jeśli stosowane są adresy 32-bitowe i bloki 16-
bajtowe, to pole etykiety ma długość 28 bitów.
W podanym przykładzie, w celu odnalezienia potrzebnej informacji, 28
najstarszych bitów adresu lokacji pamięci generowanego przez procesor jest
79
porównywanych (jednocześnie) z etykietami zapamiętanymi w pamięci
podręcznej (pole etykiety). Jeśli wystąpi trafienie, to potrzebne dane są
pobierane z pamięci podręcznej, jeśli zaś wystąpi chybienie, to dane pobierane
są z pamięci RAM, przy czym odpowiedni blok zapisywany jest w pamięci
podręcznej. Równoległe przeszukiwanie jest realizowane przy użyciu dość
skomplikowanych (a więc i kosztownych) układów elektronicznych
wbudowanych w pamięć podręczną.
Bardziej
skomplikowana
jest
organizacja
pamięci
podręcznej
odwzorowywanej bezpośrednio
(ang. direct-mapped cache) — w tym przypadku
nie występuje konieczność jednoczesnego porównywania wielu etykiet, chociaż
efektywność może być mniejsza. W omawianej organizacji 32-bitowy adres
pamięci jest dzielony na trzy pola:
•
pole etykiety (16 bitów),
•
pole obszaru (12 bitów)
•
pole słowa (4 bity).
Adres 32-bitowy generowany przez procesor
etykieta (16 bitów)
nr linii
(12 bitów)
adres
wewn.
bloku
Pamięć podręczna
etykieta
blok
000
001
002
FFF
Przypuśćmy, że pamięć podręczna jest na razie całkowicie pusta i
zachodzi konieczność skopiowania do niej bloku 16-bajtowego z pamięci RAM
o adresie A4447650H. Zatem w rozpatrywanym przykładzie pole etykiety
zawiera liczbę A444, pole obszaru 765H, a pole słowa 0. Wówczas blok ten
zostanie wpisany do wiersza pamięci podręcznej o indeksie 765H i jednocześnie
do pola etykiety tego wiersza zostanie wpisana wartość A444H.
Jeśli w trakcie dalszych działań zajdzie konieczność odczytu bajtu o
adresie A4447652H, to zostaną podjęte niżej podane działania:
•
z wiersza pamięci podręcznej o indeksie 765H zostanie odczytana zawartość
pola etykiety i porównana z polem etykiety adresu 32-bitowego;
80
•
jeśli porównywane wartości są identyczne, to zostanie odczytany
indeksowany wiersz, w którym na pozycji 2 znajduje się potrzebny bajt;
•
jeśli porównywane wartości nie są jednakowe, to bajt trzeba odczytać z
pamięci RAM.
Istnieje wiele adresów 32-bitowych, które mają identyczne 12-bitowe pole
obszaru i różne wartości pola etykiety — w pamięci podręcznej będzie zapisany
tylko jeden blok 16-bajtowy. Ograniczenie to stanowi główny problem
występujący przy pamięciach podręcznych z odwzorowaniem bezpośrednim.
Pamięć podręczna może być używana do przechowywania rozkazów i
danych. Spotyka się rozwiązania, w których dla rozkazów i danych używa się
odrębnych pamięci, jak też może występować jedna pamięć wspólna. Istotnym
problemem jest zapewnienie spójności zawartości pamięci operacyjnej
(głównej) i pamięci podręcznej. Problem ten nie występuje jeśli pamięć
operacyjna używana jest tylko do przechowywania rozkazów. Stosowane są
dwie podstawowe metody:
•
metoda zapis przez (ang. write-through) wykonuje zapis do pamięci głównej
po każdej operacji zapisu w pamięci podręcznej;
•
metoda zapis z opóźnieniem (ang. write-back), polega na tym, że zamiast
natychmiastowego zapisu bloku do pamięci głównej, zmienia się tylko bit
stanu, oznaczający, że wiersz bufora został zmodyfikowany; zmodyfikowany
blok jest kopiowany do pamięci głównej, dopiero, gdy trzeba go zastąpić
innym; w przypadku używania transmisji DMA zapewnienie spójności tą
metodą może być problemem.
Hierarchia pamięci
Zauważmy, że pamięci statyczne i dynamiczne nie rozwiązują wszystkich
problemów związanych z przechowywaniem informacji w komputerze, choćby
z tego powodu, że są pamięciami ulotnymi, w których wszystkie zapisane
informacje są tracone po wyłączeniu zasilania. Niezbędna jest więc pamięć
masowa
, nieulotna, w której przechowywany jest system operacyjny, programy i
dane, przy czym wymagania dotyczące czasów dostępu nie są zbyt ostre.
Wśród pamięci masowych najważniejsze znaczenie mają dyski twarde:
czas dostępu do informacji wynosi ok. 10 ms, a cena poniżej 200 zł / TB.
Szczególnie ważną cechą pamięci dyskowej jest możliwość wielokrotnego
zapisu i odczytu, przy czym zapisane informacje nie ulegają skasowaniu po
wyłączeniu zasilania. Wyłania się więc pewien schemat współdziałania różnych
typów pamięci i procesora, znany jako hierarchia pamięci i przedstawiany w
formie diagramu. Na szczycie tego diagramu (poziom L0) umieszczone są
rejestry procesora, które zawierają dane najłatwiej dostępne do przetwarzania.
Kolejne niższe poziomy zawierają informacje coraz trudniej dostępne (z punktu
81
widzenia czasu oczekiwania) dla procesora, ale jednocześnie charakteryzujące
się coraz większym rozmiarem i niższym kosztem przechowywania informacji.
rejestry
procesora
pamięć
podręczna
zintegrowana z
procesorem (SRAM)
pamięć główna (operacyjna)
(DRAM)
pamięć masowa (ang. secondary
storage) (dyski lokalne)
pamięć masowa
(rozproszone systemy plików, serwery sieciowe)
pamięć podręczna
niezintegrowana z
procesorem (SRAM)
Pamięć L1 przechowuje
informacje uzyskane z
pamięci L2
Rejestry procesora przechowuja
informacje uzyskane z pamieci L1
L0
(off-chip) L2:
L3:
L4:
L5:
Mniejsza,
szybsza i
droższa
Większa,
wolniejsza
i tańsza
(on-chip) L1:
Pamięć L2 przechowuje
informacje uzyskane z
pamięci głównej
Pamięć główna
przechowuje
informacje uzyskane
z pamięci masowej
Pamięć masowa przechowuje
informacje uzyskane z dysków
w serwerach sieciowych
Realizacja pamięci wirtualnej za pomocą stronicowania
Mówiąc o pamięci wirtualnej mamy na myśli symulowanie dużej pamięci
operacyjnej za pomocą stosunkowo niedużej pamięci RAM i pamięci dyskowej.
Implementacja ta polega na przechowywaniu zawartości pamięci symulowanej
częściowo w pamięci RAM i częściowo w pamięci dyskowej.
Wygodnie jest podzielić całą pamięć na bloki o jednakowych rozmiarach,
najczęściej 4 KB, a czasami 2 lub 4 MB — taki blok nazywany jest stroną. Jeśli
strona, do której następuje odwołanie, aktualnie nie znajduje się w pamięci
operacyjnej, to generowany jest wyjątek (przerwanie), który obsługiwany jest
przez system operacyjny, który dokonuje wymiany stron. Wówczas inna,
aktualnie nieużywana strona kopiowana jest na dysk, a na jej miejsce
wprowadzana jest żądana strona. W systemie Windows strony zapisywane i
odczytywane z dysku gromadzone są w pliku wymiany.
Ponieważ stosowana jest transformacja adresów, więc wymiana może
dotyczyć jakiekolwiek strony w pamięci RAM (zwykle wybiera się stronę od
82
dawna nieużywaną) położoną w dowolnym miejscu pamięci o adresie
początkowym podzielnym przez 4096. Dotychczasowa zawartość strony w
pamięci RAM jest zapisywana na dysk, a na jej miejsce wprowadzana jest
aktualnie potrzebna strona, tymczasowo przechowywana na dysku.
W niektórych przypadkach program może się odwoływać do lokacji
pamięci o adresach nie istniejących w zainstalowanej pamięci RAM. Odwołania
te muszą zostać skierowane do odpowiednich, rzeczywistych komórek pamięci.
Stosowanie pamięci wirtualnej powoduje pewne zmniejszenie prędkości
wykonywania programu wskutek konieczności wymiany stron między pamięcią
operacyjną a pamięcią dyskową. Im mniejsza jest zainstalowana pamięć RAM,
tym wymiana stron wykonywana częściej.
Realizacja przedstawionych koncepcji nie byłaby możliwa, gdyby
procesor nie dysponował mechanizmem transformacji adresów. Jeśli bowiem
trzeba skopiować z dysku brakującą stronę, to jest ona zapisywana do
dowolnego, niezajętego obszaru pamięci o zupełnie innym adresie
początkowym. Trzeba więc spowodować, ażeby omawiany obszar pamięci był
traktowany przez rozkazy programu jako obszar o innym adresie niż jest w
rzeczywistości. W związku tym adresy zawarte w rozkazach są każdorazowo
transformowane, tak by wskazywały dane tam gdzie się one rzeczywiście
znajdują. Mechanizm transformacji adresów jest dość skomplikowany, ale
praktycznie nie powoduje zwiększenia czasu wykonywania rozkazów.
Komputery CISC i RISC
Przez wiele lat wzrost wydajności procesorów starano się uzyskać
poprzez zwiększanie wielkości i złożoności list rozkazów, jak również poprzez
instalowanie bloków funkcjonalnych, wspomagających procesor (np. pamięć
podręczna). Badania kompilatorów różnych języków programowania pokazały
jednak, że tylko niewielki podzbiór rozkazów procesora jest używany przez
kompilatory. Przykładowo, kompilatory języka C firmy Sun i GNU nie używały
71% instrukcji procesora Motorola 68020. Dalsze badania innych procesorów
pokazały, że najczęściej używane są rozkazy przesyłania danych (46%), skoki
(w tym wywołania i powroty z podprogramu) (27%), rozkazy arytmetyczne
(14%), porównania (10%) i rozkazy logiczne (2%).
Pozornie, jeśli lista rozkazów procesora zawiera rozkazy zawierające
złożone operacje, to stanowi dużą wygodę dla autorów kompilatora, a
wygenerowany kod jest krótszy. Jednak doświadczenia praktyczne pokazują, że
złożone rozkazy maszynowe są często trudne do wykorzystania, ponieważ
kompilator musi zidentyfikować te fragmenty kodu, które pasują dokładnie do
czynności rozkazu — powoduje znaczny wzrost złożoności kompilatora
(zwłaszcza procedur optymalizacyjnych). Ogólnie: zauważono, że kompilatory
wykazują tendencję do faworyzowania prostych rozkazów.
83
Również doświadczenia praktyczne nie potwierdziły przypuszczenia, że
programy wykorzystujące złożone instrukcje będą krótsze — zwykle zawierają
mniej instrukcji, ale złożone instrukcje kodowane są za pomocą większej liczby
bajtów, tak że rozmiary programu nie ulegają istotnemu zmniejszeniu.
W powstałej sytuacji zaproponowano ograniczenie listy rozkazów,
uproszczenie kodowania, co pozwoliłoby na szybsze ich wykonywanie — w
rezultacie podjętych prac ukształtowała się architektura procesorów o
zredukowanych listach rozkazów, znanych jako RISC (ang. Reduced Instruction
Set Computer). Jednocześnie, istniejące procesory, o rozbudowanych listach
rozkazów zaliczono do typu CISC (ang. Complex Instruction Set Computer).
Jako charakterystyczne cechy architektury CISC wymienia się zazwyczaj:
•
lista rozkazów zawiera 100 ÷ 250 pozycji, wśród których występują rozkazy
realizujące złożone funkcje;
•
dostępna jest duża liczba trybów adresowania 5 ÷ 20;
•
czasy wykonania poszczególnych rozkazów, w zależności od stopnia
skomplikowania, zmieniają się w dość szerokich granicach;
•
realizacja rozkazów oparta jest na technice mikroprogramowania.
Z kolei dla procesorów RISC charakterystyczne są poniższe cechy:
•
stosunkowo niewiele trybów adresowania;
•
formaty rozkazów stałej długości, łatwe do zdekodowania;
•
dostęp do pamięci operacyjnej umożliwiają tylko dwa rozkazy: load, store;
•
stosunkowo obszerny zbiór rejestrów ogólnego przeznaczenia;
•
rozkazy wykonują działania na argumentach zapisanych w rejestrach (a nie w
pamięci operacyjnej);
•
sterowanie wykonywaniem rozkazów realizowane jest układowo (nie
mikroprogramowo);
•
intensywne wykorzystanie przetwarzania potokowego (występuje też w
innych, nowoczesnych procesorach); także kompilatory generują kod
uwzględniający wymagania przetwarzania potokowego.
Sterowanie mikroprogramowe i układowe
W ujęciu skrótowym, wykonywanie rozkazu przez procesor rozpoczyna
się pobrania rozkazu z pamięci, po czym identyfikowany jest jego kod — na tej
podstawie, zgodnie rozpoznanym kodem rozkazu, jednostka sterująca w
procesorze wysyła sekwencję sygnałów do różnych modułów procesora,
kierując odpowiednio przepływem i przetwarzaniem danych, tak by w rezultacie
wykonać wymaganą operację (np. dodawanie). Istnieją dwa podstawowe
sposoby konstrukcji jednostki sterującej procesora:
•
jednostka sterująca mikroprogramowalna,
•
jednostka sterująca układowa.
84
Koncepcję sterowania mikroprogramowanego można określić jako
sterowanie za pomocą wewnętrznego procesora, wbudowanego w główny
procesor. Wewnętrzny mikroprocesor zawiera własny wskaźnik instrukcji
(licznik rozkazów) i wykonuje mikroprogram zapisany w pamięci ROM (lub w
tablicy logicznej PLA – ang. programmed logic array). Mikroprogram składa się
z szeregu mikrorozkazów, a każdy mikrorozkaz zawiera sekwencję bitów, która
reprezentuje mikrooperację sterującą przemieszczaniem informacji między
różnymi podzespołami i rejestrami procesora. Wśród mikrorozkazów istnieją
także skoki warunkowe i bezwarunkowe, zmieniające kolejność, w jakiej
wykonywane są mikrorozkazy.
W tym kontekście rozkazy zwykłego programu wykonywanego przez
procesor nazywane są makrorozkazami. Termin makrorozkazy używany jest
także w innym znaczeniu w językach programowania i opisuje instrukcje
zastępowane w treści programu przez teksty makrodefinicji. Ponieważ wiele
mikroprogramów wymaga jednakowych sekwencji mikrorozkazów, używane są
także mikroprocedury.
Sterowanie mikroprogramowe umożliwia stosunkowe łatwe tworzenie
nowych wersji procesorów o bardziej rozbudowanej liście rozkazów; ponadto
konstrukcje ze sterowaniem mikroprogramowym pozwalają na względnie łatwe
usuwanie błędów projektowych na etapie prototypowym.
Sterowanie układowe
stanowi złożony układ cyfrowy zawierający bramki,
przerzutniki i inne podzespoły. Istotnym elementem takiego sterowania jest
licznik sekwencji
, który jest zwiększany o 1 w kolejnych fazach wykonania
rozkazu. Na wejście układu sterowania wprowadzany jest także (unikatowy dla
każdego rozkazu) sygnał z dekodera rozkazów. Po załadowaniu kolejnego
rozkazu do rejestru rozkazów zostaje uruchomiony licznik sekwencji — dla
kolejnych stanów licznika układ logiczny generuje odpowiednie sygnały
sterujące, które przesyłane są do podzespołów procesora.
Sterowanie układowe pozwala zazwyczaj na nieco szybsze wykonywanie
rozkazów. Na poziomie projektowania, sterowanie układowe jest mniej
elastyczne niż mikroprogramowe i projekty nie mogą łatwo modyfikowane.
Sterowanie układowe nie może być stosowane ze złożonymi formatami
rozkazów.
85
Układ
sterowania
(układ
logiczny)
Licznik
sekwencji
(generator
taktowania)
Zegar
T
1
T
2
T
n
Dekoder
Rejestr rozkazu
Znaczniki
stanu
Sygnały
sterujące
I
1
I
k
Systemy z pamięcią wspólną i z pamięcią rozproszoną
W systemach komputerowych dużej mocy z pamięcią wspólną procesory
komunikują się poprzez sieć połączeń, czytając i zapisując dane zawarte w
pamięci wspólnej. Najczęściej stosowaną metodę komunikacji stanowi
magistrala z podziałem czasu. Stosując tę metodę, procesor musi najpierw
sprawdzić czy magistrala jest dostępna — oznacza to, że kiedy jeden z
procesorów używa magistrali, pozostałe muszą czekać, aczkolwiek mogą
używać pamięć podręczną. Wspólna magistrala jest więc źródłem konfliktów —
niezbędne jest więc wprowadzenie jakiejś formy arbitrażu w przypadku kilku
żą
dań.
86
P1
P2
Pn
Procesory P1, P2, ..., Pn
pamięć
podręczna
pamięć
podręczna
pamięć
podręczna
magistrala
Pamięć
główna
W systemach z pamięcią rozproszoną każdy procesor ma pamięć lokalną,
a procesory nie współdzielą zmiennych — zamiast tego procesory wymieniają
dane, przesyłając między sobą komunikaty za pośrednictwem specyficznej sieci
komunikacyjnej. Każdy węzeł zawiera procesor, pamięć lokalną i kilka łączy
komunikacyjnych do wymiany komunikatów. Znaczne przyspieszenie można
uzyskać poprzez wprowadzenie specjalnych procesorów przesyłających
komunikaty
Pamięć lokalna
Procesor
Układy we/wy
Pamięć lokalna
Procesor
Układy we/wy
Pamięć lokalna
Procesor
Układy we/wy
System przesyłania komunikatów
Węzeł 2
Węzeł 1
Węzeł 3
87
Systemy komputerowe dużej mocy
W niektórych dziedzinach techniki i nauk przyrodniczych występują
złożone problemy matematyczne, których rozwiązanie wymaga użycia wielkich
mocy obliczeniowych, niemożliwych do uzyskania za pomocą nawet
najszybszych komputerów osobistych. Komputery o dużej mocy obliczeniowej
konstruowane są od wielu lat. Konstrukcje w latach siedemdziesiątych i
osiemdziesiątych ubiegłego stulecia, znane jako superkomputery, tworzone były
bardzo dużym nakładem kosztów, w postaci specjalnie projektowanych
procesorów o wielkiej wydajności.
Współcześnie stosuje się tańsze rozwiązania, w których komputery dużej
mocy obliczeniowej buduje się poprzez złożenie dużej liczby komputerów
powszechnego użytku, ale bez klawiatury, monitora czy myszki — tego rodzaju
systemy nazywane są klastrami obliczeniowymi. Komputery wchodzące w skład
klastra, nazywane węzłami lub „nodami” połączone są szybką siecią
umożliwiającą im współdziałanie, tak że cały klaster pracuje tak ja by był
jednym komputerem o wielu procesorach. Węzły klastra działają pod kontrolą
niezależnych systemów operacyjnych, a procesy realizowane są w oddzielnych
pamięciach fizycznych.
Procesy obliczeniowe w klastrach współdziałają ze sobą przy pomocy
wymiany komunikatów. Wymiana danych i koordynacja obliczeń odbywa się za
pomocą dedykowanej sieci komputerowej, która oznaczana jest często skrótem
SAN (ang. System Area Network). W większości przypadków sieci SAN
tworzone są w oparciu o typowe technologie i topologie sieci lokalnych, np.
Gigabit Ethernet
.
Podane technologie nie są jednak dobrze dopasowane do specyfiki
obliczeń rozproszonych i wprowadzają wiele ograniczeń. Najbardziej znanym
problemem jest mała przepustowość sieci dla małych ramek i duże,
niedeterministyczne czasy opóźnień przy przesyłaniu krótkich komunikatów.
Ograniczenia uwidaczniają się szczególnie wyraźnie przy prowadzeniu obliczeń
drobnoziarnistych, z dużą liczbą komunikatów synchronizujących.
W celu eliminacji tego typu problemów, w zaawansowanych klastrach
obliczeniowych stosuje się topologie połączeń zaczerpnięte z superkomputerów,
m.in. sieć lokalna zastępowana jest skomplikowaną strukturą połączeń między
komputerami.
W Centrum Informatycznym Trójmiejskiej Akademickiej Sieci
Komputerowej (CI TASK), które mieści się w gmachu głównym Politechniki
Gdańskiej, zainstalowanych jest kilkanaście komputerów dużej mocy.
Komputery te wykorzystywane są głównie obliczeń z zakresu mechaniki i fizyki
ciała stałego, chemii teoretycznej oraz do obliczeń numerycznych i wizualizacji
problemów inżynierskich. Przykładowo, między innymi zainstalowany jest
klaster obliczeniowy Galera o wydajności 50 TFLOPS (liczba rozkazów
zmiennoprzecinkowych wykonywana w ciągu sekundy, wyrażona w bilionach
88
(10
12
)). Klaster ten zbudowany jest z serwerów zawierających po dwie płyty
główne: każda płyta zawiera po dwa procesory czterordzeniowe Xeon, 8 GB
pamięci operacyjnej, dwa porty Gigabit Ethernet, port InfiniBand, zainstalowany
jest także dysk twardy SATA o pojemności 160 GB.
Przetwarzanie równoległe w architekturze x86
Z chwilą rozwinięcia metod grafiki komputerowej i cyfrowego
przetwarzania dźwięku, obliczenia na liczbach zmiennoprzecinkowych znalazły
nowe,
szerokie
zastosowania.
Możliwości
przetwarzania
liczb
zmiennoprzecinkowych oferowane przez koprocesor arytmetyczny okazały się
zbyt ubogie i nie dostosowane do potrzeb przetwarzania grafiki i dźwięku.
Główny
problem
polega
na
konieczności
szybkiego
wykonywania
powtarzających się operacji na wielkiej liczbie danych, przy czym wystarczająca
jest umiarkowana dokładność obliczeń.
Wychodząc naprzeciw tym potrzebom główni producenci procesorów dla
komputerów osobistych (Intel, AMD) od kilkunastu lat wprowadzają nowe
rozkazy dostosowane do przetwarzania danych multimedialnych. Główną cechą
tych rozkazów jest jednoczesne wykonywanie działań na dwóch, czterech,
ośmiu, … zestawach danych. Przykładowo, pojedynczy rozkaz może
jednocześnie obliczyć pierwiastki z czterech liczb.
Omawiane rozkazy zostały oznaczone przez firmę Intel skrótem SSE —
oznaczenie SSE stanowi skrót od Streaming SIMD Extension. Wprowadzono
także podobną grupę rozkazów oznaczoną symbolem MMX. Ponieważ rozkazy
MMX korzystają z rejestrów koprocesora arytmetycznego i mogą utrudniać jego
wykorzystanie, grupa rozkazów MMX stopniowo wychodzi z użycia.
Typowe rozkazy grupy SSE wykonują równoległe operacje na czterech
32-bitowych liczbach zmiennoprzecinkowych — można powiedzieć, że
działania
wykonywane
są
na
czteroelementowych
wektorach
liczb
zmiennoprzecinkowych
. Wykonywane obliczenia są zgodne ze standardem IEEE
754. Dostępne są też rozkazy wykonujące działania na liczbach
stałoprzecinkowych (wprowadzone w wersji SSE2).
Dla SSE w trybie 32-bitowym dostępnych jest 8 rejestrów oznaczonych
symbolami XMM0 ÷ XMM7. Każdy rejestr ma 128 bitów i może zawierać:
4 liczby zmiennoprzecinkowe 32-bitowe (zob. rysunek), lub
0
64
32
96
31
63
95
127
2 liczby zmiennoprzecinkowe 64-bitowe, lub
16 liczb stałoprzecinkowych 8-bitowych, lub
8 liczb stałoprzecinkowych 16-bitowych, lub
89
4 liczby stałoprzecinkowe 32-bitowe.
W trybie 64-bitowym dostępnych jest 16 rejestrów oznaczonych symbolami
XMM0 ÷ XMM15. Dodatkowo, za pomocą rejestru sterującego MXCSR można
wpływać na sposób wykonywania obliczeń (np. rodzaj zaokrąglenia wyników).
Zazwyczaj ta sama operacja wykonywana jest na każdej parze
odpowiadających sobie elementów obu operandów. Zawartości podanych
operandów można traktować jako wektory złożone z 2, 4, 8 lub 16 elementów,
które mogą być liczbami stałoprzecinkowymi lub zmiennoprzecinkowymi (w
tym przypadku wektor zawiera 2 lub 4 elementy). W tym sensie rozkazy SSE
mogą traktowane jako rozkazy wykonujące działania na wektorach.
Zestaw rozkazów SSE jest ciągle rozszerzany (SSE2, SSE3, SSE4, SSE5).
Kilka rozkazów wykonuje działania identyczne jak ich konwencjonalne
odpowiedniki — do grupy tej należą rozkazy wykonujące bitowe operacje
logiczne: PAND, POR, PXOR. Podobnie działają też rozkazy przesunięć, np.
PSLLW
. W SSE4 wprowadzono m.in. rozkaz obliczający sumę kontrolną CRC–
32 i rozkazy ułatwiające kompresję wideo.
Ze względu na umiarkowane wymagania dotyczące dokładności obliczeń,
niektóre rozkazy (np. RCPPS) nie wykonują obliczeń, ale wartości wynikowe
odczytują z tablicy — indeks potrzebnego elementu tablicy stanowi
przetwarzana liczba.
Dostępne są operacje "poziome", które wykonują działania na elementach
zawartych
w
tym
samym
wektorze.
W
przypadku
rozkazów
dwuargumentowych, podobnie jak przypadku zwykłych rozkazów dodawania
lub odejmowania, wyniki wpisywane są do obiektu (np. rejestru XMM)
wskazywanego przez pierwszy argument.
Wśród rozkazów grupy SSE nie występują rozkazy ładowania stałych.
Potrzebne stałe trzeba umieścić w pamięci i miarę potrzeby ładować do
rejestrów XMM. Prosty sposób zerowania rejestru polega na użyciu rozkazu
PXOR
, który wyznacza sumę modulo dwa dla odpowiadających sobie bitów obu
operandów, np. pxor xmm5, xmm5. Wypełnienie całego rejestru bitami o
wartości 1 można wykonać za pomocą rozkazu porównania PCMPEQB, np.
pcmpeqb xmm7, xmm7
.
Dla wygody programowania zdefiniowano 128-bitowy typ danych
oznaczony symbolem XMMWORD. Typ ten może być stosowany do definiowania
zmiennych statycznych, jak również do określania rozmiaru operandu, np.
odcinki XMMWORD ?
— — — — — — — — — — — —
; przesłanie słowa 128-bitowego do rejestru XMM0
movdqa xmm0, xmmword PTR [ebx]
90
Analogiczny typ 64-bitowy MMWORD zdefiniowano dla operacji MMX (które
jednak wychodzą z użycia).
Niektóre rozkazy wykonują działania zgodnie z regułami tzw. arytmetyki
nasycenia (ang. saturation): nawet jeśli wynik operacji przekracza dopuszczalny
zakres, to wynikiem jest największa albo najmniejsza liczba, która może być
przedstawiona w danym formacie. Także inne rozkazy wykonują dość
specyficzne operacje, które znajdują zastosowanie w przetwarzaniu dźwięków i
obrazów.
Operacje porównania wykonywane są oddzielnie dla każdej pary
elementów obu wektorów. Wyniki porównania wpisywane są do odpowiednich
elementów wektora wynikowego, przy czym jeśli testowany warunek był
spełniony, to do elementu wynikowego wpisywane są bity o wartości 1, a w
przeciwnym razie bity o wartości 0. Poniższy przykład ilustruje porównywanie
dwóch wektorów 16-elementowych zawartych w rejestrach xmm3 i xmm7 za
pomocą rozkazu PCMPEQB. Rozkaz ten zeruje odpowiedni bajt wynikowy, jeśli
porównywane bajty są niejednakowe, albo wpisuje same jedynki jeśli bajty są
identyczne.
Przy omawianej organizacji obliczeń konstruowanie rozgałęzień w programach
za pomocą zwykłych rozkazów skoków warunkowych byłoby kłopotliwe i
czasochłonne. Z tego powodu instrukcje wektorowe typu if ... then ... else
konstruuje się w specyficzny sposób, nie używając rozkazów skoku, ale stosując
w zamian bitowe operacje logiczne. Zagadnienia te omawiane były wcześniej.
Rozkazy grupy SSE mogą wykonywać działania na danych:
•
upakowanych (ang. packed instructions) — zestaw danych obejmuje cztery
liczby; instrukcje działające na danych spakowanych mają przyrostek ps;
91
0
64
32
96
31
63
95
127
0
64
32
96
31
63
95
127
op
op
op
op
a3
a0
a1
a2
0
64
32
96
31
63
95
127
b3
b0
b1
b2
a3 op b3
a2 op b2
a1 op b1
a0 op b0
•
skalarnych (ang. scalar instructions) — zestaw danych zawiera jedną liczbę,
umieszczoną na najmniej znaczących bitach; pozostałe trzy pola nie ulegają
zmianie; instrukcje działające na danych skalarnych mają przyrostek ss;
0
64
32
96
31
63
95
127
0
64
32
96
31
63
95
127
op
a3
a0
a1
a2
0
64
32
96
31
63
95
127
b3
b0
b1
b2
a3
a2
a1
a0 op b0
Instrukcje grupy SSE znajdują się fazie ciągłego rozwoju: najnowsza
grupa tej klasy instrukcji, zaprojektowana przez firmę Intel, oznaczana jest
skrótem AVX (ang. Advanced Vector Extension). Grupa AVX może być
traktowana jako rozszerzenie dotychczas używanych instrukcji grup MMX i
SSE. Rozkazy grupy AVX jeszcze bardziej usprawniają przetwarzanie danych,
szczególnie w przypadku wykonywania tych samych operacji na danych o
dużych rozmiarach.
Rozkazy grupy AVX wykonują działania na danych wektorowych, które
przechowywane są w rejestrach 256-bitowych, oznaczonych symbolami
YMM0…YMM15 — przewidywane jest dalsze rozszerzenie tych rejestrów do
512 i 1024 bitów. Nowym charakterystycznym elementem omawianej grupy
rozkazów są także instrukcje (rozkazy), w których podane są trzy operandy.
Typowe rozkazy procesora wymagają podania dwóch operandów A i B, z
których pierwszy określa także rejestr lub komórkę pamięci, do której zostanie
wpisany wynik operacji. Zatem wynik operacji dodawania A + B zostanie
wpisany do A, powodując jednocześnie skasowanie poprzedniej zawartości A.
W grupie AVX ta operacja dodawania W = A + B wymaga podania trzech
operandów: W, A, B, przy czym po wykonaniu operacji operandy A i B
pozostają niezmienione.
92
Jeszcze innym, ważnym elementem grupy AVX są rozkazy akumulujące
wynik mnożenia (ang. multiply-accumulate, w skrócie MAC). Rozkazy te
używane są w implementacji algorytmów cyfrowego przetwarzania sygnałów i
wykonują szybkie operacje typu w = a + b * c na wektorach liczb
zmiennoprzecinkowych. W tym przypadku rozkaz ma cztery operandy.
Warto wspomnieć o nowych instrukcjach wspomagających szyfrowanie w
standardzie AES.
Procesory wielordzeniowe i wielowątkowe
Przez wiele lat, ze względu na wysokie koszty sprzętu, złożone systemy
komputerowe, o wysokiej wydajności, konstruowane były indywidualnie z
przeznaczaniem do z góry określonych zastosowań. W tego rodzaju systemach,
ze względu na wagę rozwiązywanych problemów, koszty sprzętu były
problemem drugorzędnym. W konstrukcjach komputerów powszechnego użytku
również celem jest zwiększenie wydajności, ale koszty takich działań muszą być
umiarkowane, zapewniając przy tym wyraźną poprawę. W okresie ostatnich
dwudziestu lat można zauważyć stosowanie kilku charakterystycznych
sposobów zwiększenia wydajności procesorów:
♦
zwiększenie częstotliwości zegara — wymaga rozwiązywania trudnych
problemów technologii elektronicznej, m.in. wzrastających zniekształceń
sygnałów, które mogą trudne do zrekonstruowania po stronie odbiorczej, czy
też problemów skutecznego odprowadzania ciepła;
♦
zwiększenie stopnia zrównoleglenia wykonywanie rozkazów tego samego
procesu w tym samym czasie
(ILP – ang. instruction level parallelism)
technikami superskalarnymi, poprzez rozbudowę zasobów procesora, np.
rozmiaru pamięci podręcznej, liczby jednostek wykonawczych — wszystko
to jednak komplikuje procesor, i w rezultacie podwyższa cenę;
♦
zwiększenie stopnia zrównoleglenia operacji na poziomie wątków (TLP —
ang. thread level paralellism), realizowane różnymi sposobami — techniki te
intensywnie rozwijane są w ostatnich kilku latach.
Zwiększenie liczby jednostek wykonawczych w procesorze przynosi
korzyści tylko wówczas, jeśli na poziomie sprzętu, w wykonywanej sekwencji
rozkazów można zidentyfikować wystarczająco dużo operacji, które
potencjalnie mogą być wykonywane równolegle. Jednak dotychczasowe
doświadczenia pokazują, że na ogół nie udaje się optymalnie wykorzystać
wszystkich jednostek wykonawczych; sytuację pogarszają jeszcze błędy w
przewidywaniu skoków i związane z tym straty polegające na konieczności
usunięcia zawartości potoku.
Obserwacje wykorzystania podzespołów wykonawczych w procesorze
pokazały, że moduły te wykorzystywane średnio tylko przez 35% czasu pracy
93
procesora. W tej sytuacji pojawiło się dążenie do bardziej efektywnego
wykorzystania zasobów pojedynczego procesora, czego wyrazem było
opracowanie koncepcji procesorów wielowątkowych, spośród których
najbardziej rozpowszechniła się wielowątkowość jednoczesna SMT (ang.
simultaneous multithreading), stosowana przez firmę Intel w procesorach
wykorzystujących technologię HT (hyperthreading).
Należy tu podkreślić, że termin wielowątkowość w odniesieniu do
architektury procesorów ma inne znaczenie niż wielowątkowość w rozumieniu
systemów operacyjnych.
Koncepcja wielowątkowości jednoczesnej SMT polega na powieleniu
niektórych modułów procesora w taki sposób, że procesor jest zdolny do
jednoczesnego pobierania dwóch (lub więcej) strumieni rozkazów, w
szczególności w procesorze istnieją dwa zestawy rejestrów ogólnego
przeznaczenia (EAX, EBX, ECX, ...) i dwa wskaźniki instrukcji EIP. Rozkazy
pobierane z pamięci przez dwa moduły wykonawcze są następnie kierowane do
realizacji przez jednostki wykonawcze wspólne dla obu strumieni rozkazów,
przy zastosowaniu techniki przetwarzania potokowego.
Wprowadzenie obsługi drugiego strumienia rozkazów wymaga
stosunkowo niewielkich nakładów — liczba tranzystorów w procesorze wzrasta
jedynie o 5%. Jednak ze względu na zwiększone obciążenie jednostek
wykonawczych, częściej będą występowały kolizje w zakresie dostępu do tych
podzespołów, co powoduje przestoje w trakcie wykonywania rozkazów.
Z punktu widzenia systemu operacyjnego, procesor dwuwątkowy jest
traktowany tak jak gdyby w komputerze zainstalowane były dwa oddzielne
procesory — w celu uściślenia opisu mówimy, że w komputerze zainstalowane
są dwa procesory logiczne.
Naturalnym sposobem równoległego wykonywania kilku programów
(czyli procesów w sensie terminologii systemów operacyjnych) jest
zastosowanie odrębnych procesorów — konfiguracja taka jest bardziej wydajna
niż omawiane poprzednio wykorzystanie dwóch procesorów logicznych.
Technika ta rozpowszechniła się od kilku lat przede wszystkim ze
względu rozpoczęcie wytwarzania układów dwóch, czterech lub więcej
procesorów umieszczonych w pojedynczej obudowie, znanych jako procesory
wielordzeniowe
. Procesory wielordzeniowe (CMP – ang. chip multiprocessing)
używają wspólnej lub rozdzielonej pamięci podręcznej.
Z punktu widzenia systemu operacyjnego różnice między procesorami
wielowątkowymi i wielordzeniowymi mogą być słabo widoczne. Poniższy
rysunek przedstawia fragment okna menedżera zadań systemu Windows 7 w
komputerze wyposażonym w procesor Core i7. Procesory Intel Core i7
(laboratorium EA 508) mają 4 rdzenie, z których każdy posiada zdolność
wykonywania dwóch wątków — w rezultacie procesor taki może realizować
jednocześnie 8 procesów
94
Na tym poziomie używa się czasami terminów wieloprocesorowość fizyczna,
jeśli programy (procesy) wykonywane są przez oddzielne procesory i
wieloprocesorowość wirtualna
, jeśli programy (procesy) wykonywane są przez
procesory logiczne korzystające z pojedynczego procesora fizycznego.
Współcześnie dostępne są cztery podstawowe struktury platform
sprzętowych, pozwalających na pracę równoległą.
Komputer dwuprocesorowy
lub komputer z procesorem
dwurdzeniowym
Obsługa
przerwań
Stan
procesora
Jednostki
wykonawcze
Obsługa
przerwań
Stan
procesora
Jednostki
wykonawcze
Komputer
jednoprocesorowy
Obsługa
przerwań
Stan
procesora
Jednostki
wykonawcze
Komputer z procesorem
wielowątkowym (HT)
Obsługa
przerwań
Stan
procesora
Obsługa
przerwań
Stan
procesora
Wspólne jednostki wykonawcze