AKO wyklad 2011 niestacjonarne Nieznany

background image

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

background image

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.

background image

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

background image

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

background image

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"

background image

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

background image

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

background image

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

background image

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.

background image

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

background image

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.

background image

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.


background image

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.

background image

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.

background image

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,

background image

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

background image

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

background image

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

background image

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.



background image

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

background image

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ć

background image

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.

background image

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.

background image

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.

background image

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ć":

background image

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

background image

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).





background image

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]

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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ąć).





background image

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:

background image

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

=

+ −

(

)


background image

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.

background image

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

background image

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:

background image

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

background image

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.

background image

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

background image

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

background image

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).

background image

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

background image

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

background image

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.

background image

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

background image

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ę

background image

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.

background image

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.

background image

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.

background image

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.

background image

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.

background image

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ą

background image

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.

background image

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.

background image

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.

background image

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.





background image

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.

background image

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)

background image

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

background image

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.

background image

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ń


background image

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

background image

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

background image

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

background image

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

background image

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).

background image

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.

background image

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

background image

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

background image

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

background image

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.




background image

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ę

background image

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

background image

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

background image

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

background image

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;

background image

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

background image

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

background image

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.

background image

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.

background image

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.

background image

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ń.

background image

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



background image

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

background image

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

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

background image

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]

background image

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;

background image

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.

background image

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

background image

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

background image

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



Wyszukiwarka

Podobne podstrony:
AKO lab niestacjonarne 2011 cz2 Nieznany (2)
AMB ME 2011 wyklad01 id 58945 Nieznany (2)
AKO Wyklad 12 11 11 id 53978 Nieznany (2)
AMB ME 2011 wyklad04 id 58946 Nieznany (2)
2011 01 09 WIL Wyklad 15id 2752 Nieznany (2)
2011 03 24 WIL Wyklad 25id 2752 Nieznany
Dynamika Budowli wyklad 2 2011 Nieznany
2011 01 09 WIL Wyklad 17id 2752 Nieznany
2011 02 25 WIL Wyklad 21id 2752 Nieznany (2)
FINANSE WYKLAD 3 29 10 2011 id Nieznany
2011 notatki do wykladu sem Iid Nieznany (2)
2011 02 21 WIL Wyklad 20id 2752 Nieznany (2)
2011 02 21 WIL Wyklad 19id 2752 Nieznany
2011 01 07 WIL Wyklad 14id 2751 Nieznany (2)
AMB ME 2011 wyklad01 id 58945 Nieznany (2)
perswazja wykład2 2011 Zasady skutecznej perswazji Petty & Cacioppo

więcej podobnych podstron