Katedra Architektury Systemów Komputerowych
Laboratorium
Architektury Komputerów
Materiały pomocnicze do ćwiczeń laboratoryjnych cz. II
S p i s t r e ś c i
Programowanie mieszane
Operacje na liczbach zmiennoprzecinkowych
Specyfika kodowania programów w systemie Windows i Linux
Obsługa przerwań sprzętowych
Opracował dr inż. Andrzej Jędruch
Gdańsk 2007
Laboratorium Architektury Komputerów
Ćwiczenie 4
Programowanie mieszane
Wprowadzenie
Współcześnie, bardziej złożone oprogramowanie tworzone jest przez zespoły kilku lub kilkunastu programistów. Zazwyczaj każdy z nich koduje (programuje) jeden lub więcej modułów funkcjonalnych, realizujących wyraźnie wyodrębnione operacje tworzonej aplikacji. W tego rodzaju pracach pożądane jest, by każdy z programistów miał szeroką swobodę działania, ograniczoną jedynie przez te elementy oprogramowania, które wiążą ze sobą poszczególne moduły funkcjonalne.
Powyższy postulat realizuje się poprzez kodowanie oprogramowania w postaci wielu oddzielnych plików, zawierających kod źródłowy programu. Zazwyczaj pojedynczy programista tworzy kilka takich plików. Są one następnie tłumaczone na kod zrozumiały przez procesor i scalane (konsolidowane), tworząc kompletny, gotowy program zapisany w pliku .EXE (system Windows) czy .out (system Linux).
W przypadku tworzenia oprogramowania współpracującego z urządzeniami niestandardowymi, zadaniem jednego z modułów funkcjonalnych jest organizowanie współpracy z tym urządzeniem. W wielu przypadkach, ze względu na specyficzne wymagania urządzenia, taki moduł musi być kodowany w asemblerze, niekiedy przez konstruktora urządzenia. W rozpatrywanej sytuacji kod asemblerowy musi być przystosowany do współdziałania z pozostałym oprogramowaniem, kodowanym zazwyczaj w języku wysokiego poziomu (np. C/C++).
Niniejsze opracowanie przybliża zagadnienia związane z współdziałaniem kodu napisanego w asemblerze z kodem w języku C (i po pewnych rozszerzeniach C++), w środowisku 32-bitowym systemu Windows. Bardzo podobne, lub identyczne mechanizmy stosowane są w innych systemach operacyjnych.
Kompilacja, linkowanie i ładowanie
W wielu środowiskach programowania wytworzenie programu wynikowego wykonywane jest w dwóch etapach. Najpierw kod źródłowy każdego modułu programu zostaje poddany kompilacji (jeśli moduł napisany jest w języku wysokiego poziomu) lub asemblacji (jeśli moduł napisany jest w asemblerze). W obu tych przypadkach uzyskuje się plik w języku pośrednim (rozszerzenie .OBJ). Następnie uzyskane pliki .OBJ poddaje się konsolidacji czyli linkowaniu. W trakcie linkowania dołączane są także wszystkie niezbędne programy biblioteczne. W rezultacie zostaje wygenerowany plik zawierający program wynikowy z rozszerzeniem .EXE. Plik ten zawiera kod programu w języku maszynowym (czyli zrozumiałym przez procesor), aczkolwiek niektóre jego elementy wymagają korekcji uzależnionej od środowiska, w którym program będzie wykonany. Korekcja ta następuje w trakcie ładowania programu.
Niektóre programy biblioteczne mają charakter uniwersalny i są wykorzystywane przez wiele programów użytkowych. Wygodniej byłoby więc dołączać te programy dopiero w trakcie wykonywania programu, co pozwoliłoby na zmniejszenie rozmiaru pliku .EXE. W takim przypadku mówimy, że program korzysta z biblioteki dynamicznej (zapisanej w pliku z rozszerzeniem DLL). Omawiane fazy translacji pokazane są na poniższym rysunku.
Pliki .OBJ generowane przez różne kompilatory (w danym środowisku) zawierają kod w tym samym języku, który możemy uważać za język pośredni, stanowiący jak gdyby "wspólny mianownik" dla różnych języków programowania.
Podprogramy w technice programowania mieszanego
Problem tworzenia programu, którego fragmenty napisane są w różnych językach programowania wymaga m.in. ustalenia sposobu komunikowania się poszczególnych fragmentów ze sobą. Komunikacja taka staje się stosunkowo łatwa do zrealizowania, jeśli poszczególne fragmenty programu mają postać podprogramów (procedur). Podprogramy stanowią, ze swej natury, w pewien sposób wyizolowaną część programu, a komunikacja z nimi odbywa się wg ściśle ustalonego protokołu, określającego formaty danych i wzajemne obowiązki programu wywołującego i wywoływanego podprogramu. Protokół ten nazywany jest także opisem interfejsu podprogramu (procedury). W ten sposób, w trakcie wykonywania programu, wywoływanie fragmentów napisanych w różnych językach programowania, sprowadza się do wywoływania odpowiednich podprogramów. W przypadku języka C wywołanie podprogramu oznacza po prostu wywołanie funkcji języka C, której kod został zdefiniowany w innym pliku, niekoniecznie napisanym w języku C.
Powyższe rozważania wskazują, że interfejs do podprogramów musi być jasno i przejrzyście zdefiniowany, a zarazem musi być na tyle uniwersalny, by mógł być implementowany przez kompilatory różnych języków programowania. Z tego powodu producenci oprogramowania (m.in. firma Microsoft) ustalają pewne niskopoziomowe protokoły wywoływania podprogramów, przeznaczone dla wytwarzanych przez nich kompilatorów języków programowania.
Obecnie, w oprogramowaniu komputerów osobistych rodziny PC, wyłoniły się trzy typy interfejsu procedur. Jeden z nich, używany jest przez translator języka C, drugi przez translator Pascala, a trzeci StdCall stanowi połączenie dwóch poprzednich.
Konwencje wywoływania procedur stosowane przez kompilatory języka C
1. Parametry podprogramu przekazywane są przez stos. Parametry ładowane są na stos w kolejności odwrotnej w stosunku do tej w jakiej podane są w kodzie źródłowym, np. wywołanie funkcji calc (a,b) powoduje załadowanie na stos wartości b a następnie a.
2. Jeśli parametr ma postać pojedynczego bajtu, to na stos ładowane jest podwójne słowo (32 bity), którego młodszą część stanowi podany bajt.
3. Jeśli parametrem jest liczba składająca się z kilku bajtów, to najpierw na stos ładowana jest najstarsza część liczby i kolejno coraz młodsze. Taki schemat ładowania stosowany jest w komputerach, w których liczby przechowywane są w standardzie mniejsze niżej, i wynika z faktu, że stos rośnie w kierunku malejących adresów.
4. Obowiązek zdjęcia parametrów ze stosu po wykonaniu podprogramu należy do programu wywołującego.
5. Kompilatory języka C stosują dwa typowe sposoby przekazywania parametrów: przez wartość i przez adres. Jeśli parametrem funkcji jest nazwa tablicy, to na stos ładowany jest adres tej tablicy; wszystkie inne obiekty, które nie zostały jawnie zadeklarowane jako tablice, przekazywane są "przez wartość".
6. Wyniki podprogramu przekazywane są przez rejestry:
wynik 1-bajtowy przez AL,
wynik 2-bajtowy przez AX,
wynik 4-bajtowy przez EAX.
Jeśli wynikiem podprogramu jest adres (wskaźnik), to przekazywany jest także przez rejestry.
7. Jeśli podprogram zmienia zawartość rejestrów EBX, EBP, ESI, EDI, SS, DS, to powinien w początkowej części zapamiętać je na stosie i odtworzyć bezpośrednio przed zakończeniem. Pozostałe rejestry robocze mogą być używane bez konieczności zapamiętywania i odtwarzania ich zawartości.
Elementy procedur asemblerowych wywoływanych z poziomu języka C
Podprogram w asemblerze przystosowany do wywoływania z poziomu języka C musi być skonstruowany dokładnie wg tych samych zasad co funkcje w języku C. Wynika to z faktu, że program w języku C będzie wywoływał podprogram w taki sam sposób w jaki wywołuje inne funkcje w języku C.
Wszystkie nazwy globalne zdefiniowane w treści podprogramu muszą być wymienione na liście dyrektywy PUBLIC. Jednocześnie nazwy innych używanych zmiennych globalnych i funkcji muszą być zadeklarowane na liście dyrektywy EXTRN.
Ze względu na konwencję nazw stosowaną przez kompilatory języka C, każdą nazwę o zasięgu globalnym wewnątrz podprogramu asemblerowego należy poprzedzić znakiem podkreślenia _ (nie dotyczy to konwencji StdCall). Niektóre kompilatory C rozróżniają tylko 8 pierwszych znaków nazwy, co należy brać pod uwagę przy tworzeniu nazw globalnych.
Należy też pamiętać, że w języku C małe i wielkie litery nie są utożsamiane. Asembler odróżnia małe i wielkie litery tylko wówczas, jeśli w linii wywołania asemblera podano odpowiednią opcję:
-Cp dla asemblera MASM (ml.exe),
-ml dla asemblera TASM.
Program przykładowy: szukanie największej liczby w tablicy
Część programu w języku C (plik oblicz.c)
/* Poszukiwanie największego elementu w tablicy liczb
całkowitych za pomoca funkcji (podprogramu)
szukaj_max, ktora zostala zakodowana w asemblerze.*/#include <stdio.h>extern int szukaj_max (int * tablica, int n);
int main(){ int wyniki [12] = {456, -15, 4000000, -345678, 88046592, 2297645,
-1, 444, 7867023, -19000444, 31, -12};
int wartosc_max; wartosc_max = szukaj_max(wyniki, 12); printf("\nNajwiekszy element tablicy jest = %d", wartosc_max); return 0;
}
Część programu w asemblerze (plik podaj_m.asm)
; Podprogram _szukaj_max poszukuje największej liczby w tablicy.
; Podprogram jest przystosowany do wywoływania z poziomu języka C.
.386
PUBLIC _szukaj_max_TEXT SEGMENT dword public 'CODE' use32
ASSUME cs:_TEXT
_szukaj_max PROC near
push ebp mov ebp, esp
push ebx
push esi
mov ecx, [ebp+12] ; liczba elementow tablicy
mov ebx, [ebp+8] ; adres tablicy
mov esi, 0
mov eax, [ebx+esi*4] ; pierwszy element tablicy
dec ecx ; ilość porównań jest mniejsza o 1
; od ilości elementów tablicy
ptl: inc esi
cmp eax, [ebx+esi*4] ; porównanie z kolejnym
; elementem tablicy
jge dalej
mov eax, [ebx+esi*4]
dalej: loop ptl
; obliczona wartość maksymalna pozostaje w rejestrze EAX i będzie
; wykorzystana przez kod programu napisany w języku C
pop esi
pop ebx pop ebp ret
_szukaj_max ENDP_TEXT ENDS
END
Tworzenie programu wynikowego w formacie EXE
W celu uzyskania pliku w formacie .EXE, który zawiera kod zrozumiały dla procesora, należy najpierw poddać kompilacji (lub asemblacji) pliki z kodem źródłowym, a następnie przeprowadzić scalanie uzyskanych plików .OBJ poprzez konsolidację (linkowanie). Działania te można wykonać w okienku konsoli poprzez kolejne uruchamianie potrzebnych kompilatorów, a w końcu konsolidatora (linkera). Istnieje też możliwość wykonania powyższych zadań w środowisku zintegrowanym, zawierającym połączony edytor, kompilator, linker, debugger i inne narzędzia. Do najbardziej znanych środowisk zintegrowanych należy system Borland C/C++ czy Microsoft Visual Studio.
Realizacja zadań w środowisku zintegrowanym ma szereg zalet, jak m.in. bardzo przejrzysta sygnalizacja błędów w programie, łatwe dostępne opisy funkcji systemowych, możliwość bezpośredniego użycia debuggera i wiele innych. Są to jednak systemy bardzo złożone, zawierające wiele opcji, często nieistotnych z punktu widzenia budowy mniejszych systemów. Złożoność ta jest szczególnie widoczna w przypadku środowiska Microsoft Visual Studio. Jednym z warunków efektywnego użycia takich środowisk jest posiadanie nowoczesnego komputera, z szybkim procesorem i dużą pamięcią operacyjną.
Z powyższych względów, w niniejszym opracowaniu skupimy uwagę przede wszystkim na tworzeniu programów wykonywalnych za pomocą kompilatorów zewnętrznych, nie używając środowiska zintegrowanego. Co więcej, wydaje się że użycie kompilatorów zewnętrznych stanowi metodę bardziej uniwersalną, która może być względnie łatwo przeniesiona do tworzenia oprogramowania na inne typy procesorów.
Istnieje wiele kompilatorów języka C/C++, a wśród nich najbardziej znane są kompilatory CL.EXE firmy Microsoft i BCC32.EXE firmy Borland. Pierwszy z nich dostępny jest dla studentów Wydziału ETI PG (jako element Microsoft Visual Studio) w ramach programu Academic Alliance, a drugi stanowi element systemu C++ Builder, a ponadto można go uzyskać na stronie WWW firmy Borland po uprzednim wypełnieniu krótkiej ankiety. Prócz tego istnieje wiele innych kompilatorów języka C/C++, które można bez ograniczeń kopiować ze stron WWW.
Poniżej opisano najważniejsze parametry precyzujące działanie kompilatorów firmy Microsoft, a także parametry typowych programów konsolidujących (linkerów). Wszystkie te programy wywołujemy w okienku konsoli, podając nazwę pliku EXE zawierającego kompilator (lub asembler lub linker), potrzebne opcje dodatkowe i nazwę kompilowanego pliku, np.
ml -c -Cp -omf podaj_m.asm
Każda opcja musi być poprzedzona znakiem lub /, przy czym opcji nie można łączyć, np. opcje "H u" nie są równoważne opcji "Hu". Opcje winny być rozdzielone co najmniej jedną spacją. Wyłączenie opcji następuje (zazwyczaj) poprzez umieszczenie dodatkowego znaku, np. u. Małe i wielkie litery opcji są rozróżniane. Omyłkowe zastąpienie, np. opcji -c przez -C powoduje często wystąpienie trudnych do wykrycia błędów.
Poniżej przedstawiono najważniejsze opcje dla kompilatorów, asemblerów i linkerów. Listę dostępnych opcji można zazwyczaj wyświetlić poprzez napisanie nazwy pliku i „-h” lub „/?”.
1. Asembler ML.EXE firmy Microsoft (wersja 8.0)
-omf tworzenie pliku OBJ w formacie OMF
-coff tworzenie pliku OBJ w formacie COFF
-c asemblacja programu bez automatycznego uruchamiania linkera
-Cp rozróżnianie małych i wielkich liter
-Fl tworzenie sprawozdania (listingu) z przebiegu asemblacji
-Zi dodanie do pliku informacji potrzebnych do debuggowania programu
2. Kompilator CL.EXE firmy Microsoft
-Zi dodanie do pliku informacji potrzebnych do debuggowania programu
-c kompilacja programu bez automatycznego uruchamiania linkera
3. Konsolidator (32-bitowy) LINK.EXE firmy Microsoft (wersja 7.10)
-debug dodanie do pliku informacji potrzebnych do debuggowania programu
-defaultlib: ścieżka dostępu do standardowych plików bibliotecznych (lib)
-libpth: ścieżka dostępu do innych plików bibliotecznych (lib)
-entry: punkt wejścia do programu
-out: nazwa pliku wynikowego
-subsystem: przeznaczenie programu (console, Windows, ...)
Kompilacja i konsolidacja (linkowanie) programu przykładowego
Przypomnijmy, że omawiany wcześniej program został umieszczony w plikach: oblicz.c, (kod w języku C) oraz podaj_m.asm (kod w asemblerze).Rozpatrzymy tworzenie programu w formacie EXE za pomocą oprogramowania dostępnego w laboratoriach MKZL na Wydziale ETI PG.
Potrzebne oprogramowanie firmy Microsoft zainstalowane jest laboratoriach MKZL, natomiast studenci mogą je uzyskać w ramach programu Microsoft Academic Alliance (bliższe informacje podane są na stronie internetowej Wydziału ETI PG).
Operacje asemblacji, kompilacji i linkowania należy poprzedzić wywołaniem pliku wsadowego VCVARS32.BAT. Plik ten powinien być wywołany z poziomu katalogu bieżącego, w którym znajdują się pliki źródłowe programu. Po uruchomieniu tego pliku, kompilatory (asemblery) i linkery można wywoływać bez podawania ścieżek dostępu. W laboratoriach MKZL ścieżka dostępu do pliku VCVARS32.BAT zależy od wersji MS Visual Studio i tak dla:
MS Visual Studio 2005
″c:\Program Files\Microsoft Visual Studio (x86)\VC\bin\VCVARS32.BAT″
MS Visual Studio .Net (wpisać w jednym wierszu)
″c:\Program Files\Microsoft Visual Studio .Net 2003\VC7\
bin\VCVARS32.BAT″
Kompilację i asemblację przeprowadza się następująco:
cl -c oblicz.c
ml -c -coff -Cp -Fl podaj_m.asm
zaś do linkowania użyjemy polecenia
link -subsystem:console -out:oblicz.exe podaj_m.obj oblicz.obj
Jeśli program był poprawny, uzyskamy plik oblicz.exe.
Omawiane tu polecenia wygodnie jest umieścić razem w pliku wsadowym z rozszerzeniem .BAT. Wówczas, poprzez podanie nazwy pliku wsadowego automatycznie zostanie przeprowadzona kompilacja, asemblacja i linkowanie.
M.BAT
@rem Programowanie mieszane
@echo Wcześniej trzeba uruchomić plik wsadowy VCVARS32.BAT
cl -c oblicz.c
ml -c -coff -Cp -Fl podaj_m.asm
link -subsystem:console -out:oblicz.exe oblicz.obj podaj_m.obj
Znacznie większe możliwości automatyzacji procesu przetwarzania ma program narzędziowy make (Microsoft: nmake), który dostępny jest na wielu platformach. Poniżej podano dwie wersje pliku makefile, odpowiednio dla kompilatorów Microsoft i Borland. Przeprowadzenie kompilacji, asemblacji i linkowania wymaga tylko uruchomienia programu make (Borland) lub nmake (Microsoft) — program make (nmake) odszukuje plik makefile i wykonuje wszystkie zawarte w nim polecenia. Wykonywane są jednak tylko te operacje, które są niezbędne dla wytworzenia nowego kodu wynikowego. W szczególności oznacza to, że kompilacja wykonywana jest tylko wówczas, jeśli nastąpiła zmiana pliku źródłowego.
# Programowanie mieszane (wersja dla Microsoft)
oblicz.exe : oblicz.obj podaj_m.obj
link -subsystem:console -out:oblicz.exe oblicz.obj podaj_m.obj
oblicz.obj : oblicz.c
cl -c oblicz.c
podaj_m.obj : podaj_m.asm
ml -c -coff -Cp -Fl podaj_m.asm
Laboratorium Architektury Komputerów
Ćwiczenie 5
Operacje na liczbach zmiennoprzecinkowych
Wprowadzenie
Liczby zmiennoprzecinkowe (zmiennopozycyjne) zostały wprowadzone do techniki komputerowej w celu usunięcia wad zapisu stałoprzecinkowego. Wady te są wyraźnie widoczne w przypadku, gdy w trakcie obliczeń wykonywane są działania na liczbach bardzo dużych i bardzo małych. Warto dodać, że format zmiennoprzecinkowy dziesiętny stosowany jest od dawna w praktyce obliczeń (nie tylko komputerowych) i polega na przedstawieniu liczby w postaci iloczynu pewnej wartości (zwykle normalizowanej do przedziału <1, 10) i potęgi o podstawie 10, np.
. Dane w tym formacie wprowadzane do komputera zapisuje się zazwyczaj za pomocą litery e, np. 3.37e6.
W komputerach używane są binarne formaty liczb zmiennoprzecinkowych, które od około dwudziestu lat są znormalizowane i opisane w amerykańskim standardzie IEEE 754. Wszystkie współczesne procesory, w tym koprocesor arytmetyczny w architekturze IA-32, spełniają wymagania tego standardu.
Ponieważ działania na liczbach zmiennoprzecinkowych są dość złożone, zwykle realizowane są przez odrębny procesor zwany koprocesorem arytmetycznym. Koprocesor arytmetyczny jest umieszczony w jednej obudowie z głównym procesorem, chociaż funkcjonalnie stanowi on oddzielną jednostkę, która może wykonywać obliczenia niezależnie od głównego procesora. Koprocesor arytmetyczny oferuje bogatą listę rozkazów wykonujących działania na liczbach zmiennoprzecinkowych, w tym działania arytmetyczne, obliczanie wartości funkcji (trygonometrycznych, logarytmicznych, itp.) i wiele innych.
Architektura koprocesora arytmetycznego
Koprocesor arytmetyczny stanowi odrębny procesor, współdziałający z procesorem głównym, i znajdujący się w tej samej obudowie. Liczby, na których wykonywane są obliczenia, składowane są w 8 rejestrach 80-bitowych tworzących stos. 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).
Z każdym rejestrem stosu koprocesora związany jest 2-bitowy rejestr pomocniczy (nazywany czasami polem stanu rejestru), w którym podane są informacje o zawartości odpowiedniego rejestru stosu. Ponadto aktualny stan koprocesora jest reprezentowany przez bity tworzące 16-bitowy rejestr stanu koprocesora. W rejestrze tym m.in. zawarte są informacje o zdarzeniach w trakcie obliczeń (tzw. wyjątki), które mogą, opcjonalnie, powodować zakończenie wykonywania programu lub nie.
Z kolei również 16-bitowy rejestr sterujący pozwala wpływać na pracę koprocesora, m.in. możliwe jest wybranie jednego z czterech dostępnych sposobów zaokrąglania.
Koprocesor oferuje bogatą listę rozkazów. Na poziomie asemblera mnemoniki koprocesora zaczynają się od litery F. Stosowane są te same tryby adresowania co w procesorze, a w polu operandu mogą występować obiekty o długości 32, 64 lub 80 bitów. Przykładowo instrukcja
fadd ST(0), ST(3)
powoduje dodanie do zawartości rejestru ST(0) zawartości rejestru ST(3). Rejestr ST(0) jest wierzchołkiem stosu, natomiast rejestr ST(3) jest rejestrem oddalonym od wierzchołka o trzy pozycje. Warto dodać, że niektóre instrukcje nie mają jawnego operandu, np. "fabs" zastępuje liczbę na wierzchołku stosu przez jej wartość bezwzględną.
Do przesyłania danych używane są przede wszystkim instrukcje FLD i FST. Instrukcja FLD ładuje na wierzchołek stosu koprocesora liczbę zmiennoprzecinkową pobranej z lokacji pamięci lub ze stosu koprocesora. Instrukcja FST powoduje przesłanie zawartości wierzchołka stosu do lokacji pamięci lub do innego rejestru stosu koprocesora. Obie te instrukcje mają kilka odmian, co pozwala m.in. na odczytywanie z pamięci liczb całkowitych z jednoczesną konwersją na format zmiennoprzecinkowy. Dostępne są też instrukcje wpisujące na wierzchołek stosu niektóre stałe matematyczne, np. FLDPI.
Warto zwrócić uwagę, że załadowanie wartości na wierzchołek stosu powoduje, że wartości wcześniej zapisane dostępne są poprzez indeksy większe o 1, np. wartość ST(3) będzie dostępna jako ST(4); z tych powodów poniższa sekwencja instrukcji jest błędna:
FST ST(7)
FLD xvar ; błąd! — ST(7) staje się ST(8), a takiego rejestru nie ma
W obliczeniach zmiennoprzecinkowych porównania występuje znacznie rzadziej w zwykłym procesorze. Dostępnych jest kilka instrukcji porównujących wartości zmiennoprzecinkowe, przy czym wynik porównania wpisywany jest do ustalonych bitów rejestru stanu koprocesora. M.in, instrukcja FCOM x porównuje ST(0) z operandem x i ustawia bity C3 i C0 w rejestrze stanu koprocesora: C3=C0=0, gdy ST(0) > x albo C3=0, C0=1 w gdy ST(0) < x. Jeśli porównywane wartości są równe, to C3=1, C0=0. Stan C3=C0=1 oznacza, że porównanie nie mogło być przeprowadzone.
Bity w rejestrze stanu koprocesora określające wynik porównania zostały umieszczone na pozycjach odpowiadających znaczników w rejestrze procesora - pozwala to na wykorzystanie zwykłych instrukcji skoków warunkowych (dla liczb bez znaku). Przedtem trzeba jednak przepisać starszą część rejestru stanu koprocesora do młodszej części rejestru znaczników procesora. Ilustruje to podana niżej sekwencja rozkazów.
15 |
14 |
13 |
12 |
11 |
10 |
9 |
8 |
|
B |
C3 |
ST |
|
|
C2 |
C1 |
C0 |
starsze bity rejestru stanu koprocesora |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
|
SF |
ZF |
|
AF |
|
PF |
|
CF |
młodsze bity rejestru znaczników procesora |
FCOM ST(1) ; porównanie ST(0) i ST(1)
FSTSW AX ; zapamiętanie rejestru stanu koprocesora w AX
SAHF ; przepisanie AH do rejestru znaczników
JZ ROWNE
JA WIEKSZE
Począwszy od procesora Pentium Pro dostępny jest także rozkaz FCOMI, który wpisuje wynik porównania od razu do rejestru znaczników procesora. Stan znaczników procesora (ZF, PF, CF) po wykonaniu rozkazu FCOMI podano w poniższej tabeli.
|
ZF |
PF |
CF |
ST(0) > x |
0 |
0 |
0 |
ST(0) < x |
0 |
0 |
1 |
ST(0) = x |
1 |
0 |
0 |
niezdefiniowane |
1 |
1 |
1 |
Koprocesor arytmetyczny może wykonywać obliczenia jednocześnie z głównym procesorem — wyłania się więc problem synchronizacji. Stosowana zasada jest następująca: w chwili, gdy procesor napotka instrukcję koprocesora, to czeka na zakończenie wykonywania poprzedniej instrukcji koprocesora, po czym zezwala na rozpoczęcie wykonywania nowej i przechodzi do wykonywania kolejnej instrukcji (zwykłej). W sytuacji kiedy dalsze obliczenia procesora zależą od wyniku działania koprocesora należy zatrzymać pracę procesora za pomocą instrukcji FWAIT. Procesor wznowi pracę po zakończeniu wykonywania rozkazu przez koprocesor arytmetyczny.
Przykłady obliczeń
Obliczenia realizowane za pomocą koprocesora arytmetycznego wymagają dość często dostosowania formuł obliczeniowych do specyfiki koprocesora. Przykładowo, obliczenie wartości funkcji ex wymaga użycia rozkazów
F2XM1 obliczenie ST(0) ← (2ST(0) − 1), przy czym ST(0) ∈ <−1, +1>
FSCALE obliczenie ST(0) ← ST(0) ∗ 2ST(1) , przy czym ST(1) jest wartością całkowitą
FLDL2E wpisanie na wierzchołek stosu koprocesora wartości log2 e
FRNDINT zaokrąglenie zawartości wierzchołka stosu do liczby całkowitej
Podane dalej symbole [ ]c i [ ]u oznaczają, odpowiednio, część całkowitą i ułamkową wartości podanej w nawiasach.
W obliczeniach wykorzystuje się zależność ab = 2 b ∗ log2 a, skąd wynikają podane niżej przekształcenia
fldl2e ; log 2 e
fmulp st(1), st(0) ; obliczenie x * log 2 e
fst st(1) ; kopiowanie obliczonej wartości do ST(1)
frndint ; zaokrąglenie do wartości całkowitej
fsub st(1), st(0) ; obliczenie części ułamkowej
fxch
; po zamianie: ST(0) - część ułamkowa, ST(1) - część całkowita
f2xm1 ; obliczenie wartości funkcji wykładniczej
; dla części ułamkowej wykładnika
fld1
faddp st(1), st(0) ; dodanie 1 do wyniku
fscale ; mnożenie przez 2^(część całkowita)
fstp st(1)
; przesłanie wyniku do ST(1) i usunięcie wartości z wierzchołka
; w rezultacie wynik znajduje się w ST(0)
Fragment programu wyznaczający pierwiastki równania kwadratowego
Poniżej podano fragment programu, w którym rozwiązywane jest równanie kwadratowe
, przy czym wiadomo, że równanie ma dwa pierwiastki rzeczywiste różne. Współczynniki równania a = 2, b = -1, c = -15 podane są w segmencie danych w postaci 32-bitowych liczb zmiennoprzecinkowych (format float). Fragment programu nie zawiera rozkazów wyświetlających pierwiastki równania (x1 = -2.5, x2 = 3) na ekranie — działanie programu można sprawdzić posługując się debuggerem.
.686
dane SEGMENT use16
; 2x^2 - x - 15 = 0
wsp_a dd +2.0
wsp_b dd -1.0
wsp_c dd -15.0
dwa dd 2.0
cztery dd 4.0
x1 dd ?
x2 dd ?
dane ENDS
— — — — — — — — — —
— — — — — — — — — —
mov ax, SEG dane
mov ds, ax
finit
fld wsp_a ; załadowanie współczynnika a
fld wsp_b ; załadowanie współczynnika b
fst st(2) ; kopiowanie b
; sytuacja na stosie: ST(0) = b, ST(1) = a, ST(2) = b
fmul st(0),st(0) ; obliczenie b^2
fld cztery
; sytuacja na stosie: ST(0) = 4.0, ST(1) = b^2, ST(2) = a, ST(3) = b
fmul st(0), st(2) ; obliczenie 4 * a
fmul wsp_c ; obliczenie 4 * a * c
fsubp st(1), st(0) ; obliczenie b^2 - 4 * a * c
; sytuacja na stosie: ST(0) = b^2 - 4 * a * c, ST(1) = a, ST(2) = b
fldz ; zaladowanie 0
; sytuacja na stosie: ST(0) = 0, ST(1) = b^2 - 4 * a * c,
; ST(2) = a, ST(3) = b
; rozkaz FCOMI jest akceptowany przez asembler tylko przy dyrektywie .686
; oba porównywane operandy musza być podane na stosie koprocesora
fcomi st(0),st(1)
fstp st(0) ; usuniecie zera z wierzchołka stosu
ja delta_ujemna ; skok, gdy delta ujemna
; w przykładzie nie wyodrębnia się przypadku delta = 0
; sytuacja na stosie: ST(0) = b^2 - 4 * a * c, ST(1) = a, ST(2) = b
fxch st(1)
; sytuacja na stosie: ST(0) = a, ST(1) = b^2 - 4 * a * c, ST(2) = b
fadd st(0), st(0) ; ; obliczenie 2 * a
fstp st(3)
; sytuacja na stosie: ST(0) = b^2 - 4 * a * c, ST(1) = b, ST(2) = 2 * a
fsqrt ; pierwiastek z delty
fst st(3) ; przechowanie obliczonej wartości
; sytuacja na stosie: ST(0) = sqrt(b^2 - 4 * a * c), ST(1) = b,
; ST(2) = 2 * a, ST(3) = sqrt(b^2 - 4 * a * c)
fchs ; zmiana znaku
fsub st(0), st(1); obliczenie -b - sqrt(delta)
fdiv st(0), st(2); obliczenie x1
fstp x1 ; zapisanie x1 w pamięci
; sytuacja na stosie: ST(0) = b, ST(1) = 2 * a, ST(2) = sqrt(b^2 - 4 * a * c)
fchs
fadd st(0), st(2)
fdiv st(0), st(1)
fstp x2
fstp st(0) ; oczyszczenie stosu
fstp st(0)
Rozkazy dla zastosowań multimedialnych
Zauważono pewną specyfikę programów wykonujących operacje na obrazach i dźwiękach: występują tam fragmenty kodu, które wykonują wielokrotnie powtarzające się działania arytmetyczne na liczbach całkowitych, często 8- i 16-bitowych. W architekturze IA-32 wprowadzono specjalne grupy rozkazów MMX i SSE przeznaczone do wykonywania ww. operacji. Rozkazy te wykonują równoległe operacje na kilku danych. Rozkazy grupy MMX wykonują działania na liczbach stałoprzecinkowych, natomiast rozkazy grupy SSE (ang. Streaming SIMD Extensions) na liczbach zmiennoprzecinkowych.
Rozkazy grupy SSE wykonują równoległe operacje na czterech 32-bitowych liczbach zmiennoprzecinkowych. Wprowadzone rozkazy przeznaczone są głównie do zastosowań w zakresie grafiki komputerowej, gdzie występują operacje przetwarzania dużych zbiorów liczb zmiennoprzecinkowych (mnożenie macierzy, transpozycja macierzy, itd.).
Dla SSE zdefiniowano 8 nowych rejestrów: każdy rejestr ma 128 bitów i zawiera 4 liczby zmiennoprzecinkowe; rejestry oznaczone są symbolami xmm0 ÷ xmm7.
W praktyce poprzez zastosowanie SSE uzyskuje się podwojenie prędkości przetwarzania (chociaż działania wykonywane są równolegle na czterech liczbach). Zestaw rozkazów SSE jest ciągle rozszerzany (SSE 2, SSE 3), ale podstawowy zestaw obejmuje 70 rozkazów, w tym:
50 rozkazów operacji zmiennoprzecinkowych,
12 rozkazów operacji stałoprzecinkowych
8 rozkazów pomocniczych;
Rozkazy 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;
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;
; Program przykładowy ilustrujący operacje SSE procesora
; Poniższy podprogram jest przystosowany do wywoływania
; z poziomu języka C (program arytmc_SSE.c)
.686
.XMM
public _dodaj_SSE, _pierwiastek_SSE, _odwrotnosc_SSE
_TEXT SEGMENT dword public 'CODE' use32
ASSUME cs:_TEXT
_dodaj_SSE PROC
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov esi, [ebp+8] ; adres pierwszej tablicy
mov edi, [ebp+12] ; adres drugiej tablicy
mov ebx, [ebp+16] ; adres tablicy wynikowej
; ladowanie do rejestru xmm5 czterech liczb zmiennoprzecinkowych
; 32-bitowych - liczby zostaja pobrane z tablicy, ktorej adres
; poczatkowy podany jest w rejestrze ESI
; mnemonik "movups" : mov, u - unaligned (adres obszaru nie jest
; podzielny przez 16), p - packed (do rejestru ladowane sa od razu
; cztery liczby), s - short (inaczej float, liczby zmiennoprzecinkowe
; 32-bitowe)
movups xmm5, [esi]
movups xmm6, [edi]
; sumowanie czterech liczb zmiennoprzecinkowych zawartych
; w rejestrach xmm5 i xmm6
addps xmm5, xmm6
; zapisanie wyniku sumowania w tablicy w pamieci
movups [ebx], xmm5
pop edi
pop esi
pop ebx
pop ebp
ret
_dodaj_SSE ENDP
;=========================================================
_pierwiastek_SSE PROC
push ebp
mov ebp, esp
push ebx
push esi
mov esi, [ebp+8] ; adres pierwszej tablicy
mov ebx, [ebp+12] ; adres tablicy wynikowej
; ladowanie do rejestru xmm5 czterech liczb zmiennoprzecinkowych
; 32-bitowych - liczby zostaja pobrane z tablicy, ktorej adres
; poczatkowy podany jest w rejestrze ESI
; mnemonik "movups" : mov, u - unaligned (adres obszaru nie jest
; podzielny przez 16), p - packed (do rejestru ladowane sa od razu
; cztery liczby), s - short (inaczej float, liczby zmiennoprzecinkowe
; 32-bitowe)
movups xmm6, [esi]
; obliczanie pierwiastka z czterech liczb zmiennoprzecinkowych
; znajdujacych sie w rejestrze xmm6 - wynik wpisywany jest do xmm5
sqrtps xmm5, xmm6
; zapisanie wyniku sumowania w tablicy w pamieci
movups [ebx], xmm5
pop esi
pop ebx
pop ebp
ret
_pierwiastek_SSE ENDP
;=========================================================
; rozkaz RCPPS wykonuje obliczenia na 12-bitowej mantysie
; (a nie na typowej 24-bitowej) - obliczenia wykonywane sa
; szybciej, ale sa mniej dokladne
_odwrotnosc_SSE PROC
push ebp
mov ebp, esp
push ebx
push esi
mov esi, [ebp+8] ; adres pierwszej tablicy
mov ebx, [ebp+12] ; adres tablicy wynikowej
; ladowanie do rejestru xmm5 czterech liczb zmiennoprzecinkowych
; 32-bitowych - liczby zostaja pobrane z tablicy, ktorej adres
; poczatkowy podany jest w rejestrze ESI
; mnemonik "movups" : mov, u - unaligned (adres obszaru nie jest
; podzielny przez 16), p - packed (do rejestru ladowane sa od razu
; cztery liczby), s - short (inaczej float, liczby zmiennoprzecinkowe
; 32-bitowe)
movups xmm5, [esi]
; obliczanie odwrotnosci czterech liczb zmiennoprzecinkowych
; znajdujacych sie w rejestrze xmm6 - wynik wpisywany jest do xmm5
rcpps xmm5, xmm6
; zapisanie wyniku sumowania w tablicy w pamieci
movups [ebx], xmm5
pop esi
pop ebx
pop ebp
ret
_odwrotnosc_SSE ENDP
_TEXT ENDS
END
=====================
/* Program przykladowy ilustrujacy operacje SSE procesora
styczen 2007
Ponizszy podprogram jest przystosowany do wywolywania
do wspolpracy z podprogramem zakodowanym w asemblerze
(plik arytm_SSE.asm)
*/
#include <stdio.h>
void dodaj_SSE (float *, float *, float *);
void pierwiastek_SSE (float *, float *);
void odwrotnosc_SSE (float *, float *);
int main()
{
float p[4] = {1.0, 1.5, 2.0, 2.5};
float q[4] = {0.25, -0.5, 1.0, -1.75};
float r[4];
dodaj_SSE (p, q, r);
printf ("\n%f %f %f %f", p[0], p[1], p[2], p[3]);
printf ("\n%f %f %f %f", q[0], q[1], q[2], q[3]);
printf ("\n%f %f %f %f", r[0], r[1], r[2], r[3]);
printf("\n\nObliczanie pierwiastka");
pierwiastek_SSE (p, r);
printf ("\n%f %f %f %f", p[0], p[1], p[2], p[3]);
printf ("\n%f %f %f %f", r[0], r[1], r[2], r[3]);
printf("\n\nObliczanie odwrotnosci - ze wzgledu na stosowanie");
printf("\n12-bitowej mantysy obliczenia sa malo dokladne");
odwrotnosc_SSE (p, r);
printf ("\n%f %f %f %f", p[0], p[1], p[2], p[3]);
printf ("\n%f %f %f %f", r[0], r[1], r[2], r[3]);
return 0;
}
Laboratorium Architektury Komputerów
Ćwiczenie 6
Specyfika kodowania programów
w systemie Windows i Linux
Asembler w systemie Linux
Dla systemu Linux istnieje przynajmniej kilka asemblerów o różniącej się (istotnie) składni. Jednym z bardziej “przyjaznych”, przypominającym składnią asemblery stosowane w systemie Windows jest NASM.
Poniżej przedstawiony jest program wyświetlający napis powitalny. Program można przeredagować za pomocą dowolnego edytora dostępnego w Linuksie, np. w edytora programie Midnight Commander (mc). Program źródłowy można skompilować i uruchomić w systemie Linux za pomocą następujących poleceń:
- kompilacja programu do postaci półskompilowanej (.o)
nasm -f elf program.asm
- tworzenie wersji wykonywalnej programu o nazwie program.out
ld -s -o program.out program.o
Składnia asemblera NASM jest podobna składni asemblera MASM (ml.exe) lub TASM. Kilka charakterystycznych przykładów różnic w składni podano w poniższej tablicy.
MASM (ML), TASM |
NASM |
v1 db ? |
v1 resb 1 |
v2 db 12 dup (?) |
v2 resb 12 |
v3 dw ? |
v3 resw 1 |
v4 db 21 dup ('A') |
v4 TIMES 21 db 'A' |
extrn |
extern |
public |
global |
Odpowiednikiem funkcji usługowych wywoływanych za pomocą rozkazu INT 21H w systemie Windows/DOS są funkcje wywoływane za pomocą INT 80H w Linuksie. Funkcje te wykonują jednak inne operacje (które są zgodne z funkcjami oferowanymi w bibliotece języka C). Wybrane funkcje systemowe wywoływane za pomocą INT 80H podane są w poniższej tablicy. Numer funkcji należy podać w rejestrze EAX.
1 |
Zakończenie wykonywania procesu; kod powrotu należy wpisać do rejestru EBX (zwykle 0) |
3 |
Odczyt pliku; EBX - uchwyt pliku, ECX - adres bufora docelowego, EDX - liczba bajtów do przeczytania. W przypadku EBX = 0 dane odczytywane są z klawiatury. Po odczycie w EAX podana jest liczba odczytanych bajtów albo kod błędu. |
4 |
Zapis do pliku; EBX - uchwyt pliku, ECX - adres bufora źródłowego, EDX - liczba bajtów do zapisania. W przypadku EBX = 1 dane wysyłane są na ekran monitora. Po odczycie w EAX podana jest liczba zapisanych bajtów albo kod błędu. |
5 |
Otwarcie pliku; EBX - adres obszaru, w którym zapisana jest nazwa pliku zakończona bajtem zerowym; ECX - bity dostępu (często = 0); EDX - prawa dostępu (często = 0). Po wykonaniu funkcji EAX zawiera uchwyt pliku albo kod błędu. |
6 |
Zamknięcie pliku (nie używa się dla uchwytów 0, 1, 2); EBX - uchwyt zamykanego pliku. |
; program przykładowy przystosowany do asemblera NASM
; nasm -f elf pierwszy.asm (asemblacja)
; ld -o hello pierwszy.o (konsolidacja)
section .text ; początek sekcji rozkazów programu
global _start ; deklaracja symbolu globalnego
_start: ; etykieta wskazująca pierwszy rozkaz programu
mov eax, 4 ; numer funkcji
; systemowej 'sys_write' (zapis do pliku)
mov ebx, 1 ; identyfikator pliku lub urządzenia (1 oznacza
; ekran)
mov ecx, tekst ; adres obszaru pamięci, w którym znajduje się
; wyświetlany tekst
mov edx, dlugosc ; liczba znaków tekstu
int 80h ; wywołanie funkcji systemowej
; zakończenie programu - wywołanie funkcji systemowej sys_exit
mov eax, 1 ; numer funkcji sys_exit
int 80h ; wywołanie funkcji
section .data ; początek sekcji danych programu
tekst db 10, 'Witam!', 10 ; wyświetlany tekst
dlugosc EQU $ - tekst ; długość napisu
Kodowanie programów dla systemu Windows
Przygotowanie programów dostosowanych do pracy w środowisku Windows, prócz pewnego doświadczenia w programowaniu, wymaga też zaakceptowania odmiennego sposobu funkcjonowania programów. Zazwyczaj programy tego typu tworzy się w językach wysokiego poziomu jak C/C++ czy Pascal, często wykorzystując narzędzia szybkiego programowania jakimi są np. Borland Delphi czy MS Visual Studio. Kodowanie w asemblerze, choć bardziej złożone, pozwala jednak na dokładniejsze poznanie mechanizmów wywoływania funkcji i przesyłania komunikatów.
Spośród wielu elementów programowania właściwych dla systemu Windows, jednym z najważniejszych jest system komunikatów. Można powiedzieć, że działanie programu w środowisku Windows sprowadza się do obsługi komunikatów. Po uruchomieniu programu i wyświetleniu związanego z nim okna, program oczekuje na komunikaty (zdefiniowano około 140 komunikatów). Ich źródłem mogą być naciśnięcia klawiszy na klawiaturze, kliknięcie myszy, zmiana rozmiarów okna, i wiele innych zdarzeń. Program po otrzymaniu komunikatu interpretuje go i podejmuje odpowiednie działania.
W celu uproszczenia omawianych zagadnień, prezentowany dalej program przykładowy ma postać okna dialogowego, na którym rozmieszczone są przyciski sterujące i okienka do wprowadzania i wyświetlania tekstu.
Zasoby programu
Programy przygotowywane dla środowiska Windows posiadają możliwość odrębnego zapisania kodu zawierającego właściwy algorytm oraz kodu stanowiącego opis wyglądu zewnętrznego programu. W ten sposób pewne cechy zewnętrzne programu mogą być łatwo modyfikowane bez konieczności korygowania całego programu. Wygląd zewnętrzny programów zakodowany jest w tzw. zasobach aplikacji. W zasobach mogą być zawarte okienka, obrazki, ikony, kursory, opisy menu, itp. Najważniejszą cechą zasobów jest to, iż mogą zostać utworzone niezależnie od reszty programu, a później dołączone do niego poprzez specyficzne linkowanie.
Opis zasobów umieszcza się zazwyczaj w pliku z rozszerzeniem .RC. Plik ten zostaje następnie przekształcony za pomocą kompilatora zasobów (rc.exe) do postaci zakodowanej RES, która jest scalana (linkowana) z programem EXE.
Edycję pliku .RC można wykonywać za pomocą dowolnego edytora tekstowego, np. za pomocą Notatnika. Zazwyczaj wygodniej jest korzystać z edytorów zasobów, które stanowią integralną część wielu środowisk programistycznych (np. MS Visual Studio).
Treść pliku zasobów zapisywana jest w stylu zbliżonym do języka C. Każdy element zasobów identyfikowany jest przez numer lub nazwę, np. 123 ICON jakas_ikona.ico . Oznacza to, że pewien zasób ma numer 123, typ ICON, a dane pobierane są z pliku jakas_ikona.ico . Typy zasobów skojarzone z numerami 1 ÷ 255 są zarezerwowane.
Plik ZadWin.rc
#include <windows.h>
#define ID_OKNO_DIALOGOWE 23453
#define RAMKA_ARGUMENT 23454
#define RAMKA_WYNIK 23455
ID_OKNO_DIALOGOWE DIALOG DISCARDABLE 0, 0, 230, 146
STYLE DS_MODALFRAME | DS_SETFOREGROUND | DS_3DLOOK | DS_CENTER |
WS_POPUP|WS_VISIBLE|WS_CAPTION|WS_SYSMENU
CAPTION "Program Windows zakodowany w asemblerze"
FONT 18, "MS Sans Serif"
{
CONTROL "Argument:", -1, "STATIC", SS_LEFT | WS_CHILD |
WS_VISIBLE | WS_GROUP, 5, 12, 40, 8
CONTROL "", RAMKA_ARGUMENT, "EDIT", ES_LEFT |
ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_TABSTOP, 50, 10, 105, 12
CONTROL "Wynik:", -1, "STATIC", SS_LEFT | WS_CHILD |
WS_VISIBLE | WS_GROUP, 5, 32, 35, 8
// opis okienka dla tekstu wynikowego
// ES_CENTER - wyswietlany tekst bedzie centrowany
CONTROL "Tu będzie wpisany wynik programu",
RAMKA_WYNIK, "EDIT", ES_CENTER |
ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE |
WS_BORDER | WS_TABSTOP, 50, 30, 120, 12
/* =========================================================
Opisy przyciskow OK i Cancel
BS_DEFPUSHBUTTON - ten przycisk moze zostac uaktywniony takze
przez nacisniecie klawisza ENTER
WS_VISIBLE - okno jest inicjalnie widoczne
WS_TABSTOP - pozwala przechodzić przez elementy okna
za pomoca klawisza TAB
*/
CONTROL "OK", 1, "BUTTON", BS_DEFPUSHBUTTON | WS_CHILD |
WS_VISIBLE | WS_TABSTOP, 20, 50, 30, 14
CONTROL "Cancel", 2, "BUTTON", BS_PUSHBUTTON | WS_CHILD |
WS_VISIBLE | WS_TABSTOP, 105, 50, 30, 14
}
MENU MainMenu
BEGIN
END
Program główny
W zasobach omawianego programu przykładowego zdefiniowano obiekt typu DIALOG, który zawiera opisy okienek sterujących. W działaniach związanych z tymi zasobami używane są funkcje DialogBoxParam i EndDialog. Pierwsza z tych funkcji tworzy okno dialogowe wg opisu podanego w pliku zasobów — stała ID_OKNO_DIALOGOWE (drugi parametr) jest identyfikatorem wskazującym na opis okna dialogowego, który zawarty jest w pliku zasobów (.rc). Czwarty parametr Oblicz_DialogProc jest wskaźnikiem do funkcji obsługującej komunikaty kierowane do okna. Z kolei funkcja EndDialog powoduje zamknięcie okna dialogowego.
Procedura obsługi zdarzeń (funkcja Oblicz_DialogProc) powinna zwracać wartość FALSE, jeśli zdarzenie nie zostało obsłużone, albo wartość TRUE, jeśli zdarzenie zostało obsłużone (nie potrzeba wywoływać funkcji DefWindowProc). Jeśli potrzebny jest uchwyt okna (handle), to trzeba umieścić odpowiedni kod w części obsługującej komunikat WM_INITDIALOG.
Podany tu program przykładowy jest ograniczony do okna dialogowego. Typowe programy w systemie Windows mają jednak budowę nieco bardziej złożoną, a głównym elementem takiego programu jest pętla komunikatów - krótki (kilka wierszy) fragment programu, przez który przesyłane są komunikaty sterujące wykonywaniem programu. Właściwy kod, zawierający operacje obsługi komunikatów realizuje odrębna funkcja nazywana funkcją okienkową.
Plik ZadWin.asm
; Okno dialogowe w systemie Windows
; program przykładowy w asemblerze (styczeń 2007)
.686
extrn _ExitProcess@4 : near
extrn _GetModuleHandleA@4 : near
extrn _DialogBoxParamA@20 : near
extrn _MessageBoxA@16 : near
extrn _EndDialog@8 : near
extrn _GetWindowTextA@12 : near
extrn _SetWindowTextA@8 : near
extrn _GetDlgItem@8 : near
public _WinMain@16
IDOK EQU 1
IDCANCEL EQU 2
WM_INITDIALOG EQU 0110H
WM_COMMAND EQU 0111H
WM_CLOSE EQU 0010H
TRUE EQU 1
FALSE EQU 0
ID_OKNO_DIALOGOWE EQU 23453
RAMKA_ARGUMENT EQU 23454
RAMKA_WYNIK EQU 23455
_DATA SEGMENT dword public 'DATA' use32
hInstance dd ?
napis db 'Wyjście z programu',0
napis2 db 'Naciśnieto Sprawdź',0
argument db 32 dup (?)
wynik db 32 dup (?)
napis3 db 'Tu wpisz argument',0
_DATA ENDS
_TEXT SEGMENT dword public 'CODE' use32
ASSUME cs:_TEXT, ds:_DATA
;---------------------------------------------------------------
; Funkcja obslugujaca komunikaty kierowane do okna dialogowego
; BOOL CALLBACK Oblicz_DialogProc
; (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
$Oblicz_DialogProc STRUC
dd ?, ? ; EBP, slad
hWnd dd ?
uMsg dd ? ; kod komunikatu
wParam dd ?
lParam dd ?
$Oblicz_DialogProc ENDS
Oblicz_DialogProc PROC
push ebp
mov ebp, esp
cmp ($Oblicz_DialogProc PTR [ebp]).uMsg, WM_CLOSE
jnz nieWmClose ; skok gdy inny komunikat
; obsluga komunikatu WM_CLOSE
ZamknijDialog:
;EndDialog(hWnd, 0);
; return TRUE;
push 0
push ($Oblicz_DialogProc PTR [ebp]).hWnd
call _EndDialog@8 ; zamkniecie okna dialogowego
mov eax, TRUE
pop ebp
ret
;------------------------------------------------------------------
nieWmClose: cmp ($Oblicz_DialogProc PTR [ebp]).uMsg, WM_INITDIALOG
jne nieWmInitdialog ; skok, gdy inny komunikat
; komunikat WM_INITDIALOG generowany jest w chwili tworzenia okna
; SetWindowText(GetDlgItem(hWnd,RAMKA_ARGUMENT),(LPCSTR)"Tu wpisz argument");
; return TRUE;
push RAMKA_ARGUMENT ; wartosc stalej
push ($Oblicz_DialogProc PTR [ebp]).hWnd
call _GetDlgItem@8 ; funkcja zwraca wartosc przez EAX
; wpisanie tekstu poczatkowego do okna dialogowego
push OFFSET napis3
push eax
call _SetWindowTextA@8
mov eax,TRUE
pop ebp
ret
;------------------------------------------------------------------
nieWmInitdialog:
cmp ($Oblicz_DialogProc PTR [ebp]).uMsg, WM_COMMAND
jne nieWmCommand
cmp ($Oblicz_DialogProc PTR [ebp]).wParam, IDOK
jne nieIdOK
; pobieranie tekstu z okienka RAMKA_ARGUMENT
; GetWindowText(GetDlgItem(hWnd,RAMKA_ARGUMENT),(LPSTR)argument,30);
push RAMKA_ARGUMENT ; identyfikator okienka
push ($Oblicz_DialogProc PTR [ebp]).hWnd
call _GetDlgItem@8 ; funkcja zwraca wartosc przez EAX
push 30
push OFFSET argument
push eax
call _GetWindowTextA@12
; kopiowanie 7 znakow do drugiego okna
mov ecx, 7
push esi
push edi
mov esi, OFFSET argument
mov edi, OFFSET wynik
kopiowanie: mov al, [esi]
mov [edi], al
inc esi
inc edi
loop kopiowanie
pop edi
pop esi
;----------------------------------------------------------------
; wpisywanie tekstu do okienka RAMKA_WYNIK
; SetWindowText(GetDlgItem(hWnd,RAMKA_WYNIK), wynik);
push RAMKA_WYNIK ; wartosc stalej
push ($Oblicz_DialogProc PTR [ebp]).hWnd
call _GetDlgItem@8 ; funkcja zwraca wartosc przez EAX
push OFFSET wynik
push eax
call _SetWindowTextA@8
mov eax, FALSE
pop ebp
ret
nieIdOK: cmp ($Oblicz_DialogProc PTR [ebp]).wParam, IDCANCEL
jne nieIdCancel
push IDCANCEL
push ($Oblicz_DialogProc PTR [ebp]).hWnd
call _EndDialog@8 ; zamkniecie okna dialogowego
mov eax,TRUE
pop ebp
ret
nieIdCancel:
nieWmCommand:
mov eax, FALSE
pop ebp
ret
Oblicz_DialogProc ENDP
;====================================================================
_WinMain@16:
; hInstance = GetModuleHandle (NULL);
push 0
call _GetModuleHandleA@4
mov hInstance,eax
Comment |
Funkcja DialogBoxParam tworzy okno dialogowe
odp = DialogBoxParam(hInstance, (LPCSTR)ID_OKNO_DIALOGOWE, 0,
Oblicz_DialogProc, (LPARAM) NULL);
hInstance - uchwyt do instancji aplikacji
ID_OKNO_DIALOGOWE - identfikator do opisu okna w pliku zasobow .RC
Oblicz_DialogProc - wskaznik do funkcji obslugujacej
komunikaty kierowane do okna
|
push 0
push offset Oblicz_DialogProc
push 0
push ID_OKNO_DIALOGOWE
push hInstance
call _DialogBoxParamA@20
; zakonczenie programu
push 0
call _ExitProcess@4 ; zakonczenie programu
_TEXT ENDS
END
Kompilacja i konsolidacja programu
W rozpatrywanym przykładzie program główny, będący aplikacją okienkową systemu Windows, zakodowany jest w asemblerze (plik ZadWin.asm). Zasoby programu zawarte są w pliku ZadWin.rc . W celu utworzenia programu wynikowego w pliku z rozszerzeniem .exe należy wykonać niżej podane operacje (wcześniej należy uruchomić plik wsadowy VCVARS32.BAT - zob. opis ćw. 1):
ml -c -Cp -coff -Fl ZadWin.asm
rc ZadWin.rc
(zapisać w jednym wierszu) link -subsystem:windows -out:ZadWin.exe ZadWin.obj
libcmt.lib user32.lib kernel32.lib ZadWin.res
Laboratorium Architektury Komputerów
Ćwiczenie 7
Obsługa przerwań sprzętowych
Wprowadzenie
Współczesne, wielozadaniowe systemy operacyjne starają się izolować programy użytkowe od sprzętu komputerowego. Programy mogą się komunikować z urządzeniami wyłącznie za pośrednictwem usług oferowanych przez system operacyjny. W tej sytuacji przeprowadzenie jakichkolwiek eksperymentów ilustrujących zasady sterowania urządzeniami komputerowymi nie może być zrealizowane na poziomie zwykłej aplikacji. Powyższe uwagi odnoszą się także do mechanizmów obsługi przerwań sprzętowych, które są tematem niniejszego ćwiczenia.
W tej sytuacji jedynym rozwiązaniem jest przeprowadzanie eksperymentów w zakresie sprzętu na komputerze pozbawionym systemu operacyjnego, którego rolę (w szczątkowym zakresie) przejmuje uruchomiony program - takie podejście jest stosowane w projektach wykonywanych w sem. 4 w ramach przedmiotu „Oprogramowanie systemowe”. Jednak taki mini-system operacyjny, nawet w najprostszej postaci jest dość złożony i trudny do analizy.
W ramach niniejszego ćwiczenia spróbujemy zrealizować przedstawione zamierzenia wykorzystując środowisko systemu DOS. System ten już od wielu lat nie jest używany, ale jego funkcje zostały przejęte przez system Windows, który jeszcze do niedawna oferował możliwość wykonywania programów przeznaczonych do wykonywania w środowisku systemu DOS.
System operacyjny DOS powstał w początkowym okresie rozwoju komputerów PC i założenia był systemem jednozadaniowym, przystosowanym do pracy przy dość ubogich zasobach sprzętowych (np. pamięć operacyjna 640 KB, procesor z zegarem 8 MHz). System DOS nie posiada żadnych mechanizmów ochrony systemu operacyjnego i pozwala w szczególności na ingerencję (oczywiście w przemyślany sposób) w mechanizmy obsługi przerwań. Stanowi to podstawę do ilustracji mechanizmów obsługi przerwań w ramach niniejszego ćwiczenia.
Przypomnijmy, że procesory zgodne z architekturą IA-32 mogą pracować w dwóch trybach pracy:
rzeczywistym, który naśladuje i pewnym stopniu rozszerza funkcje procesora 8086;
chronionym, nazywanym też wirtualnym, w którym dostępne są mechanizmy wielozadaniowości i stosowane są odmienne niż w trybie rzeczywistym sposoby adresowania i ochrony.
W ramach trybu chronionego wprowadzono także (począwszy od procesora 386) specjalny podtryb, określany jako tryb V86 (tryb wirtualny 8086), w którym, z punktu widzenia wykonywanych programów, procesor działa prawie dokładnie tak samo jak w trybie rzeczywistym.
Programy napisane dla systemu DOS mogą być wykonywane w trybie rzeczywistym lub w trybie V86. W systemie Windows możliwość wykonywania programów w trybie rzeczywistym istniała tylko w starszych wersjach systemu. Z kolei w trybie V86, który jest odmianą trybu chronionego, ze względu na stabilność systemu, nie jest możliwy bezpośredni dostęp do sprzętu. W tej sytuacji, konstruktorzy systemu Windows stworzyli warunki do wykonywania programów napisanych dla systemu DOS poprzez symulowanie sprzętu. Tak więc aplikacja DOSowa działająca w trybie V86 może bez ograniczeń wykonywać rozkazy działające na sprzęcie, przy czym nie są one wykonywane na rzeczywistym sprzęcie, ale dokładnie symulowane.
Sytuacja komplikuje się jeszcze bardziej w odniesieniu do komputerów, w których zainstalowane są 64-bitowe systemy operacyjne, np. Windows XP 64. Systemy tego typu nie akceptują programów dla systemu DOS i można je uruchamiać jedynie za pomocą maszyny wirtualnej, np. DOSBox, która opisana jest poniżej.
Maszyna wirtualna DOSBox
W laboratoriach komputerowych MKZL, w celu uruchomienia maszyny wirtualnej DOSBox należy uruchomić program c:\programy\DOSBox—0.63\dosbox.exe. Ponadto, wersja instalacyjna tego programu w postaci pliku DOSBox0.63-win32-installer2.exe dostępna jest na wielu stronach internetowych.
Po uruchomieniu maszyny wirtualnej na ekranie pojawi się pokazane obok okno.
Do asemblacji programów będziemy używać asemblera MASM, a do konsolidacji programu LINK (wersja 16-bitowa) W laboratoriach komputerowych MKZL programy MASM i LINK umieszczone są w katalogu c:\programy\masm, natomiast wszystkie pliki źródłowe i pliki tworzone podczas asemblacji i linkowania należy przechowywać w katalogu d:\studenci (lub w jednym z jego podkatalogów).
Dla podanych lokalizacji wskazane jest utworzenie dwóch dysków wirtualnych skojarzonych z ww. katalogami. W tym celu w okienku konsoli maszyny wirtualnej trzeba wprowadzić polecenia:
mount t c:\programy\masm
mount d d:\studenci
Po ustawieniu bieżacego napędu dyskowego (wirtualnego) na d: można przeprowadzić asemblację i linkowanie programu przykładowego:
t:\masm .......asm,,,;
t:\link ......obj;
W rezultacie powstanie plik ...exe, który można uruchomić w okienku maszyny wirtualnej.
Obsługa przerwań sprzętowych
Przerwania sprzętowe są pewnymi zdarzeniami zachodzącymi w urządzeniach komputera, które wymagają podjęcia niezwłocznej obsługi. W takim urządzenie wysyła do procesora sygnał przerwania, który powoduje, że procesor przerywa wykonywanie bieżącego programu i rozpoczyna wykonywanie innego programu, zazwyczaj stanowiącego część systemu operacyjnego. Zadaniem tego programu zbadanie przyczyn nadejścia sygnału przerwania i podjęcie odpowiedniej akcji, np. poinformowanie użytkownika, że kopiowanie pliku z Internetu zostało zakończone, albo też że operacja drukowania została zatrzymana ze względu na brak papieru. Po wykonaniu wszystkich czynności związanych z obsługą przerwania system operacyjny wznawia wykonywanie przerwanego programu.
Tak więc procesor, oprócz wykonywania rozkazów 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) kolejnego rozkazu do wykonania, który został by wykonany, gdyby nie nadeszło przerwanie. Zazwyczaj położenie to zapisywane jest na stosie.
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 procesorach zgodnych z architekturą IA-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 stosowanych w komputerach PC z procesorem o architekturze IA-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 omawianych procesorach 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ą wektorów przerwań. Przypomnijmy, że w trybie rzeczywistym tablica wektorów przerwań zawiera 256 adresów, z których każdy zajmuje 4 bajty. Adresy kodowane są w postaci segment:offset. Tablica umieszczona jest w pamięci począwszy od adresu fizycznego 0 (aczkolwiek jej położenie może zostać zmienione).
Po zapisaniu śladu na stosie procesor odszukuje w tablicy wektorów przerwań adres procedury obsługi przerwania i rozpoczyna ją wykonywać. Numer wektora przerwania, w którym zawarty jest adres procedury obsługi zależy w ustalony sposób od numeru linii IRQ, poprzez którą nadszedł sygnał przerwania. W przypadku programów 16-bitowych wykonywanych w systemie Windows/DOS numer wektora stanowi powiększony o 8 numer linii IRQ (dla linii IRQ 8 ÷ IRQ 15 numer powiększany jest o 104).
Podprogram obsługi przerwania kończy rozkaz IRET, która powoduje wznowienie wykonywania przerwanego programu poprzez odtworzenie rejestrów (E)IP, CS i (E)FLAGS, 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.
Aktualnie w komputerach PC system przerwań obsługiwany jest przez układ APIC (dawniej używano dwóch układów typu 8259), który pełni rolę "sekretarki" procesora. W trybie rzeczywistym procesora układ APIC pracuje w trybie konwencjonalnym naśladując pracę swoich poprzedników. Możemy zatem odnieść nasze rozważania do układów 8259, co pozwoli na dokładniejsze wyjaśnienie techniki obsługi przerwań. Układy te, pracujące w konfiguracji kaskadowej, mogą obsługiwać do 15 źródeł przerwań. Sygnały przerwań z poszczególnych urządzenia kierowane są do układów 8259 poprzez linie oznaczone symbolami IRQ 0 - IRQ 15.
Z każdą linią IRQ (ang. interrupt request) skojarzony jest wektor przerwania w tablicy wektorów (deskryptorów) przerwań. Skojarzenie to wykonywane poprzez odpowiednie zaprogramowanie układu 8259 — wykonuje to system operacyjny podczas inicjalizacji. Typowe przyporządkowanie stosowane w systemie DOS podane jest poniższej tabeli. W systemie Windows używane są deskryptory (wektory) 50H - 5FH, zaś w systemie Linux 20H - 2FH.
IRQ |
Urządzenie |
Nr wek-tora |
|
IRQ |
Urządzenie |
Nr wek-tora |
0 |
zegar systemowy, przerwanie wysyłane przez układ 8254 (w systemie DOS około 18 razy/s) |
8 |
|
8 |
zegar czasu rzeczywistego, przerwanie generowane ustalonym czasie (budzenie) |
112 |
1 |
klawiatura, przerwanie wysyłane po naciśnięciu lub zwolnieniu klawisza |
9 |
|
9 |
|
113 |
2 |
połaczone z drugim układem 8259 |
|
|
10 |
|
114 |
3 |
łącze szeregowe COM2 |
11 |
|
11 |
|
115 |
4 |
łącze szeregowe COM1 |
12 |
|
12 |
|
116 |
5 |
łącze równoległe LPT2 |
13 |
|
13 |
koprocesor arytmetyczny |
117 |
6 |
sterownik dyskietek |
14 |
|
14 |
sterownik dysku twardego |
118 |
7 |
łącze równoległe LPT1 |
15 |
|
15 |
|
119 |
Zatem, nadejście sygnału IRQ, np. IRQ 1 powoduje przerwanie i uruchomienie podprogramu obsługi przerwania, którego adres znajduje się w wektorze: 9 (DOS), 51H (Windows), 21H (Linux).
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.
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.
Przykład obsługi przerwania zegarowego
W celu zilustrowania mechanizmu przerwań rozpatrzymy przykład obsługi przerwania zegarowego, które w systemi Windows/DOS generowane jest co ok. 55 ms. Podany niżej program wyświetla znaki * w takt przerwań zegarowych. Naciśnięcie dowolnego klawisza powoduje zakończenie wykonywania programu. W podanej wersji programu po wyświetleniu gwiazdki sterowanie przekazywane jest do oryginalnej procedury BIOSu (która obsługuje przerwanie zegarowe).
.386
rozkazy SEGMENT use16
ASSUME CS:rozkazy
obsluga_zegara PROC ; procedura obsługi przerwań zegarowych
push ax ; przechowanie używanych rejestrów
push es
push bx
mov ax,0b800h ;adres pamięci ekranu
mov es,ax
mov al,'*'
mov bx,cs:licznik ;załadowanie adresu kolejnego znaku
mov es:[bx],al ;wyświetlenie '*'
mov al,7 ;kolor biały na czarnym tle
mov es:[bx+1],al
add bx,2
cmp bx,4000 ;sprawdzenie czy cały ekran
jb wysw_dalej ;skok gdy koniec ekranu
mov bx,0 ;wyzerowanie gdy ekran zapisany
wysw_dalej:
mov cs:licznik,bx ;zapisanie rejestru bx w zmiennej licznik
pop bx
pop es
pop ax
jmp dword PTR cs:wektor8
; dane programu ze względu na specyfikę obsługi przerwań umieszczone
; są w segmencie kodu
licznik dw 320
wektor8 dd 0
obsluga_zegara ENDP
zacznij:
mov al,0 ; ustalenie strony graficznej nr 0
mov ah,5
int 10h
mov ax, 0
mov ds,ax
mov eax,ds:[32] ;odczytanie wektora 8 do eax
mov cs:wektor8,eax ;odesłanie oryginalnego adresu
; procedury obsługi przerwania
; wpisanie do wektora 8 adresu własnej procedury
mov ax,seg obsluga_zegara
mov bx, offset obsluga_zegara
cli ;zablokowanie przerwań
mov ds:[32],bx ;zapisanie adresu naszej procedury
mov ds:[34],ax
sti ;odblokowanie przerwań
petla_oczekiwania:
mov ah,1 ;oczekiwanie na naciśniecie klawisza
int 16h
jz petla_oczekiwania
; odtworzenie wektora 8
mov eax,CS:wektor8
cli
mov ds:[32],eax ;odesłanie wektora 8
sti
mov al,0
; zakończenie programu
mov ah,4ch
int 21h
rozkazy ENDS
nasz_stos SEGMENT stack
db 128 dup (?)
nasz_stos ENDS
end zacznij
32