background image

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

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

 

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

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

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

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

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

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

 

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

  − 1 = 3  

2

4

  − 1 = 15 

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, 

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ą
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 = 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 

0000 0001 

 —    —    —    —    —    —    —    —    — 
0111 1110 

126 

126 

126 

0111 1111 

127 

127 

127 

1000 0000 

128 

128 

1000 0001 

129 

127 

1000 0010 

130 

126 

 —    —    —    —    —    —    —    —    — 
1111 1110 

254 

126 

1111 1111 

255 

127 

 
 

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 

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. 
 

0110 0001 

61H 

0110 0010 

62H 

0110 0011 

63H 

0110 0100 

64H 

0110 0101 

65H 

0110 0110 

66H 

  —    —    —    —    —     

0111 1001 

79H 

0111 1010 

7AH 

0100 0001 

41H 

0100 0010 

42H 

0100 0011 

43H 

0100 0100 

44H 

0100 0101 

45H 

background image

43 

0100 0110 

46H 

—    —    —    —    —    — 

0101 1001 

59H 

0101 1010 

5AH 

 
 

0010 0001 

21H 

0010 0010 

22H 

0010 0011 

23H 

0010 0100 

24H 

    —    —    —    —    —     

0111 1011 

7BH 

0111 1100 

7CH 

0011 0000 

30H 

0011 0001 

31H 

0011 0010 

32H 

0011 0011 

33H 

—    —    —    —    —    — 

0011 1000 

38H 

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-

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.  
 

suma 

logiczna 

iloczyn 

logiczny 

suma 

modulo 

dwa 

 
 

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

d

e

s

k

ry

p

 

IR

Urządzenie 

N

d

e

s

k

ry

p

zegar systemowy, 
przerwanie wysyłane 
przez układ 8254 (w 
systemie DOS około 
18 razy/s) 

32 

 

zegar czasu 
rzeczywistego, 
przerwanie 
generowane 
ustalonym czasie 
(budzenie) 

40 

klawiatura, 
przerwanie wysyłane 
po naciśnięciu lub 
zwolnieniu klawisza 

33 

 

 

41 

połączone z drugim 
układem 8259 

 

 

10   

42 

łącze szeregowe 
COM2 

35 

 

11   

43 

łącze szeregowe 
COM1 

36 

 

12   

44 

łącze równoległe 
LPT2 

37 

 

13  koprocesor 

arytmetyczny 

45 

sterownik dyskietek 

38 

 

14  sterownik dysku 

twardego 

46 

łą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 

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, 

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 

Rozkaz 1 

 

 

 

 

 

 

 

 

Rozkaz 2 

Rozkaz 1 

 

 

 

 

 

 

 

Rozkaz 3 

Rozkaz 2 

Rozkaz 1 

 

 

 

 

 

 

Rozkaz 4 

Rozkaz 3 

Rozkaz 2 

Rozkaz 1 

 

 

 

 

 

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: loadstore

  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 

są 

na 

czteroelementowych 

wektorach 

liczb 

zmiennoprzecinkowych

. Wykonywane obliczenia są zgodne ze standardem IEEE 

754.  Dostępne  są  też  rozkazy  wykonujące  działania  na  liczbach 
stałoprzecinkowych (wprowadzone w wersji SSE2).  

Dla  SSE  w  trybie  32-bitowym  dostępnych  jest  8  rejestrów  oznaczonych 

symbolami XMM0 ÷ XMM7. Każdy rejestr ma 128 bitów i może zawierać: 

  4 liczby zmiennoprzecinkowe 32-bitowe (zob. rysunek), lub 

 

0

64

32

96

31

63

95

127

 

 

  2 liczby zmiennoprzecinkowe 64-bitowe, lub 
  16 liczb stałoprzecinkowych 8-bitowych, lub 
  8 liczb stałoprzecinkowych 16-bitowych, lub 

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 

tym 

samym 

wektorze. 

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