Art of Assembly - nieoficjalne polskie wydanie [PDF]

  • Author / Uploaded
  • coll.
  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

WYLACZNOŚĆ DO PUBLIKOWANIA TEGO TEKSTU, POSIADA RAG WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK konsultacja naukowa NEKRO [email protected]

[email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAL PIERWSZY: REPREZENTACJA DANYCH Prawdopodobnie największą przeszkodą jaką większość początkujących napotyka kiedy próbuje nauczyć się asemblera jest powszechne używanie binarnego i heksadecymalnego systemu liczbowego. Wielu programistów sądzi że liczby heksadecymalne stanowią absolutny dowód na to, że Bóg nigdy nie planował aby ktokolwiek pracował w asemblerze. Chociaż to prawda, że liczby heksadecymalne trochę się różnią od tego czego używamy na co dzień, ich zalety znacznie przewyższają ich wady. Pomimo to, opanowanie tych systemów liczbowych jest ważne ponieważ ich zrozumienie upraszcza inne ważne tematy wliczając w to algebrę boolowską i projekt logiczny, reprezentację numeryczną znaków, kody znaków i dane upakowane. 1.0 WSTĘP Ten rozdział omawia kilka ważnych pojęć, w tym binarny i heksadecymalny system liczbowy, organizację danych binarnych (bity ,nibbles ,bajty ,słowa i podwójne słowa),system liczb ze znakiem i bez znaku, operacje arytmetyczne, logiczne, przesunięcia i obroty na wartościach binarnych, pola bitów i pakowanie danych ,zbiór znaków ASCII. Jest to podstawowy materiał a dalsze czytanie tego tekstu zależy od zrozumienia tych pojęć. Jeśli już jesteś zapoznany z tymi terminami z innych kursów lub studiów powinieneś przynajmniej przejrzeć ten materiał przed przystąpieniem do następnego rozdziału. Jeśli nie jesteś zapoznany lub tylko ogólnikowo ,powinieneś przestudiować go starannie. CAŁY MATERIAŁ W TYM ROZDZIALE JEST WAŻNY! Nie opuszczaj nadmiernie materiału. 1.1 SYSTEMY LICZBOWE Większość nowoczesnych systemów komputerowych nie przedstawia wartości liczbowych używając systemu dziesiętnego. Zamiast tego, używają systemu binarnego lub systemu "dopełnienia do dwóch" .Żeby zrozumieć ograniczenia arytmetyki komputerów musimy zrozumieć w jaki sposób komputery przedstawiają liczby. 1.1.1 PRZEGLĄD SYSTEMU DZIESIĘTNEGO Używasz systemu dziesiętnego (opartego o 10) od tak dawna, że uważasz go za pewnik. Kiedy widzisz liczbę taką jak *123*,nie myślisz o wartości 123;raczej stwarzasz umysłowy obraz tego jaką pozycję ta wartość przedstawia. W rzeczywistości, liczba 123 przedstawia 1*102+2*101+3*100 lub 100+20+3 Każda cyfra pojawiająca się po lewej stronie przecinka przyjmuje wartość między zero a dziewięć mnożone przez dodatnie potęgi liczby dziesięć. Cyfra pojawiająca się po prawej stronie przecinka przyjmuje wartości między zero a dziewięć mnożone przez rosnące, ujemne potęgi liczby dziesięć. Na przykład wartość 123,456 to: 1*1022+2*1011+3*1000+4*10-1-1+5*10-2-2+6*10-3 lub 100+20+3+0.4+0.05+0.006

1.1.2 BINARNY SYSTEM LICZBOWY Większość nowoczesnych systemów komputerowych (w tym IBM PC) działa używając logiki binarnej. Komputer przedstawia wartości używając dwóch poziomów napięcia (zwykle 0V i 5V).Poprzez dwa takie poziomy możemy przedstawić dokładnie dwie rożne wartości. To mogłyby być dowolne dwie rożne wartości ale konwencjonalnie używamy wartości zero i jeden. Te dwie wartości, zbiegiem okoliczności, odpowiadają dwom cyfrom używanym przez binarny system liczbowy. Ponieważ jest zgodność między logicznymi poziomami używanymi przez 80x86 i dwoma cyframi używanymi w binarnym systemie liczbowym, nie było niespodzianki, ze IBM PC zastosował binarny system liczbowy. Binarny system liczbowy pracuje tak jak system dziesiętny z dwoma wyjątkami: system binarny uznaje tylko cyfry 0 i 1 (zamiast 0-9) i system binarny używa potęgi dwa zamiast potęgi dziesięć. Dlatego tez jest bardzo łatwo przekształcić liczbę binarna na dziesiętną. Dla każdego "1" lub ”0” w łańcuchu binarnym dodajemy 2n gdzie n jest pozycja cyfry binarnej liczonej od prawej strony (zera)Na przykład binarna wartosc11001010(2) przedstawia się tak: 1*27+1*26 +0*25+0*24+1*23+0*22+1*21+0*20 lub 8+64+8+2 = 202(10) Przekształcenie liczby dziesiętnej na binarna jest odrobinę bardziej skomplikowane Musisz znaleźć te potęgi dwójki, które dodane razem stworzą rezultat dziesiętny. Najłatwiejsza metoda to zacząć od dużych potęg dwójki aż do 20.Rozważmy dziesiętną wartość 1359: * 210=1024. 211=2048. 1024 jest największą potęga dwójki mniejszą niż 1359. Odejmujemy 1024 od 1359 i zaczynamy wartość binarna od lewej cyfra "1".Binarnie = "1".Resultat dziesiętny to 1359-1024=335. * Kolejna ,niższa potęga dwójki (29=512) jest większa niż powyższy rezultat, więc dodajemy "0" na koniec binarnego łańcucha. Binarnie="10",dziesiętnie jest wciąż 335 * Kolejną, niższą potęgą dwójki jest 256 (28).Odejmujemy ją od 335 i dodajemy cyfrę "1" na koniec binarnej liczby.Binarnie="101",rezultat dziesiętny to 79. * 128 (27) jest większe niż 79 więc dołączamy "0" do końca łańcucha binarnego. Binarnie ="1010",rezultat dziesiętny pozostaje 79. * Następna niższa potęga dwójki (26=64) jest mniejsza niż 79,wiec odejmujemy 64 i dołączamy "1" do końca binarnego łańcucha. Binarnie ="10101".Rezultat dziesiętny to 15 * 15 jest mniejsze niż następna potęga dwójki (25=32) więc po prostu dodajemy "0" do końca łańcucha binarnego.Binarnie="101010".Dziesietny rezultat to wciąż 15. * 16 (24) jest większa niż dotychczasowa reszta więc dołączamy "0" do końca łańcucha binarnego.Binarnie="1010100",rezultat dziesiętny to 15. * 23 (osiem) jest mniejsze niż 15 więc dokładamy następna cyfrę "1" na koniec binarnego lancucha.Binarnie="10101001",dziesiętnie rezultat to 7. * 22 jest mniejsze niż 7 więc odejmujemy cztery od siedmiu i dołączamy następną jedynkę do binarnego lancucha.Binarnie="101010011",dziesietnie jest 3. * 21 jest mniejsze niż 3 więc dodajemy jedynkę do łańcucha binarnego i odejmujemy dwa od wartości dziesiętnej.Binarnie="1010100111",dziesiętny rezultat to teraz 1. * Ostatecznie rezultat dziesiętny wynosi jeden ( 20) wiec dodajemy końcową "1" na koniec łańcucha binarnego. Końcowy rezultat binarny to: 10101001111 Liczby binarne, mimo iż maja małe znaczenie w językach programowania wysokiego poziomu, pojawiają się wszędzie w programach pisanych w asemblerze. 1.1.3 FORMAT DWÓJKOWY Każda binarna liczba zawiera bardzo duża liczbę cyfr (lub bitów - skrót od BInary digiTs).Na przykład ,przedstawiamy liczbę pięć poprzez: 101 00000101 0000000000101 000000000000101 Dowolna liczba zera może poprzedzać liczę binarną bez zmiany jej wartości. Przyjmiemy konwencję ignorowania poprzedzających zer. Na przyklad,101(2) przedstawia liczbę pięć .Ponieważ 80x86 pracuje z grupami ośmiobitowymi, przyjdzie nam dużo łatwiej rozszerzyć o zera wszystkie liczby binarne jako wielokrotności czterech lub ośmiu bitów. Dlatego tez podążając za konwencja, przedstawimy liczbę pięć jako 0101(2) lub 00000101(2).

W USA większość ludzi oddziela każde trzy cyfry przecinkiem, co czyni duże liczby łatwiejszymi do odczytu, na przykład 1,023,435,208 jest dużo łatwiejsze do przeczytania i pojęcia niż 1023435208.Przyjmiemy podobną koncepcje w tym tekście dla liczb binarnych. Oddzielimy każdą grupę czterech bitów spacją. Na przykład binarną wartość 1010111110110010 zapiszemy jako 1010 1111 1011 0010. Często pakujemy kilka wartości razem do tej samej liczby binarnej. Jedna z form instrukcji MOV 80x86 używa binarnego kodowania 1011 0rrr dddd dddd dla spakowania trzech pozycji do 16 bitów; pięć bitów kodu operacji (10110),trzy bity pola rejestrów (rrr) i osiem bitów wartości bezpośredniej (dddd dddd).Dla wygody, przydzielimy wartości liczbowe każdej pozycji bitu .Ponumerujemy każdy bit jak następuje: 1) Bit najbardziej na prawo jest bitem z pozycji zero. 2) Każdy bit na lewo ma kolejny ,większy numer Ośmiobitowa wartość binarna używa bitów od zero do siedem: x7,x6,x5,x4,x3,x2,x1,x0 Szesnastobitowa wartość binarna używa bitów od zera do piętnastu: x15,x14,x13,x12,x11,x10,x9,x8,x7,x6,x5,x4,x3,x2,x1,x0 Do bitu zerowego zazwyczaj odnosimy się jako najmniej znaczącego bitu (L.O). Bit najbardziej z lewej strony jest zwykle nazywany bitem najbardziej znaczącym (H.O.). Będziemy się odnosili do bitów pośrednich poprzez ich numery. 1.2 ORGANIZACJA DANYCH W czystej matematyce wartości mogą zawierać przypadkowe liczby bitów. Komputery ,generalnie rzecz biorąc pracują z określoną liczbą bitów. Powszechnie przyjęte to pojedyncze bity, grupy czterech bitów (zwane nibbles),grupy ośmiu bitów (zwane bajtami),grupy szesnastu bitów (zwane słowem) i więcej. Ich rozmiary nie są przypadkowe. Ta sekcja opisze grupy bitów powszechnie używanych w chipach Intela 80x86 1.2.1 BITY Najmniejsza "jednostka” danych w komputerze jest pojedynczy bit. Ponieważ pojedynczy bit jest zdolny do przedstawiania tylko dwóch rożnych wartości (typowo zero i jeden),możemy odnieść wrażenie, ze jest bardzo mało wartości jakie może przedstawić pojedynczy bit. Nie prawda! Jest ogromna liczba pozycji jakie możemy przedstawić za pomocą pojedynczego bitu. Za pomocą pojedynczego bitu możemy przedstawić dwie odrębne pozycje. Przykładem mogą być: zero lub jeden, prawda lub fałsz, włączony lub wyłączony, mężczyzna lub kobieta, Jednakże nie jesteśmy ograniczeni do przedstawiania typów danych binarnych (to znaczy tych obiektów które maja dwie rożne wartości). Możesz używać pojedynczego bitu do przedstawiania liczb 723 i 1,245 lub 6.254 i 5. Możesz również użyć pojedynczego bitu do przedstawiania kolorów czerwonego i niebieskiego. Możesz nawet przedstawić dwa nie powiązane ze sobą obiekty za pomocą pojedynczego bitu. Na przykład, możesz przedstawić kolor czerwony i liczbę 3.256 za pomocą pojedynczego bitu. Możesz przedstawić jakieś dwie rożne wartości za pomocą pojedynczego bitu. Jednakże możesz przedstawić tylko dwie rożne wartości za pomocą pojedynczego bitu. To mylące rzeczy, nawet bardzo, rożne bity mogą przedstawiać rożne rzeczy. Na przykład jeden bit może być używany do przedstawiania wartości zero i jeden, podczas gdy sąsiedni bit może być używany do przedstawiania wartości prawda i fałsz .Jak można to odróżnić patrząc na te bity? Odpowiedź, nie można. Ale to ilustruje cała ideę komputerowej struktury danych: dana jest to to co ty zdefiniujesz. Jeśli użyjesz bitu do przedstawienia boolowskiej (prawda/ fałsz) wartości wówczas ten bit (zdefiniowany przez ciebie) reprezentuje prawdę lub fałsz. Dla bitów mających prawdziwe znaczenie musisz być konsekwentny. To znaczy ,jeśli używasz bitu do przedstawiania prawdy lub fałszu w jednym punkcie swojego programu nie powinieneś używać wartości prawda/fałsz przechowywanej w tym bicie do późniejszego przedstawiania koloru czerwonego lub niebieskiego. Ponieważ większość danych których będziesz używał, wymaga więcej niż dwóch rożnych wartości, pojedyncze wartości bitów nie są najbardziej powszechnymi typami danych, które będziesz stosował. Ponieważ wszystko inne składa się z grup bitów, bity odgrywają ważną role w twoich programach. Oczywiście, jest kilka typów danych które wymagają dwóch odrębnych wartości, wiec wydaje się, ze bity są ważne same w sobie. Jednak wkrótce zobaczysz, że pojedyncze bity są trudne do manipulowania, wiec często będziemy używać innych typów danych do przedstawiania wartości boolowskich. 1.2.2 NIBBLESY

Nibble jest zbiorem czterech bitów. To nie jest szczególnie interesująca struktura danych za wyjątkiem dwóch przypadków: liczb BCD (Binary Coded Decimal) i liczb heksadecymalnych. Cztery bity przedstawiają pojedynczą cyfrę BCD lub heksadecymalną. Przy pomocy nibble’a możemy przedstawić do 16 odrębnych wartości. W przypadku liczb heksadecymalnych, wartości 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E i F są przedstawiane przez cztery bity (zobacz "Heksadecymalny System Liczbowy” ).BCD używa 10 rożnych cyfr (0,1,2,3,4,5,6,7,8,9) i wymaga czterech bitów. Faktycznie, każda z szesnastu odrębnych wartości może być przedstawiana przez nibble’a, ale cyfry heksadecymalne i BCD są podstawowymi pozycjami jakie możemy przedstawić za pomocą pojedynczego nibble'a. 1.2.3 BAJTY Bez wątpliwości, najważniejszą strukturą danych używaną przez mikroprocesor 80x86 jest bajt .Bajt składa się z ośmiu bitów i jest najmniejszym adresowalnym elementem danych w mikroprocesorze 80x86.Adresy pamięci głównej jak i I/O w 80x86 wszystkie są adresami bajtowymi. To znaczy, ze najmniejsza pozycja która może być dostępna przez program w 80x86 jest wartością ośmiobitową. Dostęp do czegokolwiek mniejszego wymaga abyś odczytał bajt zawierający dane i zamaskował niechciane bity. Bity w bajcie są zazwyczaj numerowane od zera do siedem, przy użyciu konwencji przedstawionej na rysunku 1.1. Bit 0 jest najmniej znaczącym bitem, bit 7 jest najbardziej znaczącym bitem. Do pozostałych bitów będziemy się odwoływać poprzez ich numery.

Rysunek 1.1 Numerowanie bitów w bajcie Zauważ, że bajt także zawiera dwa nibble (zobacz rysunek 1.2)

Rysunek 1.2 Dwa nibble w bajcie Bity 0..3 stanowią mniej znaczącego nibble'a, bity 4..7 bardziej znaczącego nibble'a. Ponieważ bajt zawiera dokładnie dwa nibble, wartość bajtu wymaga dwóch cyfr heksadecymalnych. Ponieważ bajt zawiera osiem bitów, można przedstawić 28 ,lub 256, rożnych wartości. Generalnie będziemy używać bajtu do przedstawiania wartości liczbowych w zakresie 0...255,liczb ze znakiem w zakresie -128..+127 (zobacz "Liczby ze znakiem i bez znaku" ),kodów znaków ASCII/IBM, i innych specjalnych typów danych wymagających nie więcej niż 256 rożnych wartości. Wiele typów danych ma mniej niż 256 pozycji, więc osiem bitów jest zazwyczaj wystarczających. Ponieważ 80x86 jest maszyną adresowalna bajtem (zobacz "Układ Pamięci i Dostęp") okazuje się bardziej efektywne manipulowanie całym bajtem niż pojedynczym bitem czy nibble'm. Z tego powodu większość programistów używa całego bajtu do przedstawienia typu danych który wymaga nie więcej niż 256 pozycji, nawet jeśli mniej niż osiem bitów wystarczyłoby. Na przykład, często przedstawiamy wartości boolowskie prawdę i fałsz (odpowiednio) jako 00000001(2) i 00000000(2). Prawdopodobnie najważniejszym zastosowaniem bajtu jest udział w kodzie znaku. Znaki pisane z klawiatury, wyświetlane na ekranie, i drukowane na drukarce, wszystkie mają wartości liczbowe. Pozwala to porozumiewać się z resztą świata, IBM PC używa wariantu ze znakami ASCII (zobacz "Zbiór znaków ASCII”). Jest 128 zdefiniowanych kodów w zbiorze znaków ASCII.IBM używa pozostałych 128 możliwych wartości dla rozszerzonego zbioru kodów znaków wliczając w to znaki europejskie ,symbole graficzne, litery greckie i symbole matematyczne. 1.2.4 SŁOWA

Słowo jest grupą 16 bitów. Ponumerujemy te bity zaczynając od zera w gore aż do piętnastu. Numeracja bitów pokazana jest na rysunku 1.3

Rysunek 1.3 Numeracja bitów w słowie Tak jak przy bajcie, bit 0 jest najmłodszym bitem a bit 15 najstarszym bitem. Kiedy odnosimy się do innych bitów w słowie używamy ich numerów pozycji. Zauważ, ze słowo zawiera dokładnie dwa bajty. Bity 0 do 7 tworzą mniej znaczący bajt, bity od 8 do 15 tworzą bardziej znaczący bajt.(Zobacz rysunek 1.4)

Rysunek 1.4 Dwa bajty w słowie Naturalnie, słowo może być dalej rozbite na cztery nibble jak pokazuje rysunek 1.5

Rysunek 1.5 Nibble w słowie Nibble zero jest najmniej znaczącym nibblem w słowie a nibble trzy najbardziej znaczącym nibblem słowa. Pozostałe dwa nibble to "nibble jeden" i nibble dwa". Mając 16 bitów możemy przedstawić 216 (65,536) rożnych wartości. To mogą być wartości w zakresie od 0 do 65 536 (lub zazwyczaj -32,768..+32,767) lub każdy inny typ danych o wartościach nie większych niż 65,536.Trzy ważne zastosowania dla słowa to wartości liczb całkowitych, offsetów i wartości segmentów (zobacz "Układ Pamięci i Dostęp” przy opisie segmentów i offsetów). Słowa mogą reprezentować wartości całkowite w zakresie 0..65 536 lub -32,768..+32,767.Wartości liczb bez znaku są reprezentowane przez wartości binarne zgodne z bitami w słowie. Wartości liczb ze znakiem używają formy „dopełnienia do dwóch” dla wartości liczbowych (zobacz "Liczby ze znakiem i bez znaku").Wartości segmentu, które są zawsze długie na 16 bitów stanowią adres paragrafu kodu, danych, danych specjalnych lub segmentu stosu w pamięci. 1.2.5 PODWÓJNE SLOWO Podwójne słowo jest dokładnie tym, na co wskazuje jego nazwa, parą słów .Dlatego tez długość podwójnego słowa wynosi 32 bity ,jak pokazuje rysunek 1.6.

Rysunek 1.6 Liczba bitów w podwójnym słowie Naturalnie, to podwójne słowo może być dzielone na słowo wyższego rzędu i słowo niższego rzędu, lub cztery rożne bajty, lub osiem rożnych nibbli (zobacz rysunek 1.7).Podwójne słowa mogą reprezentować wiele rodzajów rożnych rzeczy .Przede wszystkim na liście jest adresowanie segmentu. Inna powszechną pozycją reprezentowaną przez podwójne słowo jest 32 bitowa

Rysunek 1.7 Nibble, bajty i słowa w podwójnym słowie wartość całkowita (która określa liczby bez znaku w zakresie 0..4,294,967,295 lub liczby ze znakiem w zakresie 2,147,483,648..2,147,483,647).32-bitowa wartość zmiennoprzecinkowa także mieści się w podwójnym słowie. Większość czasu będziemy używać podwójnych słów dla adresowania segmentów. 1.3 HEKSADECYMALNY SYSTEM LICZBOWY Sprawę z binarnym systemem już omówiliśmy. Przedstawienie wartości 202(10) wymaga ośmiu cyfr binarnych. Wersja dziesiętna wymaga tylko trzech cyfr dziesiętnych, tak wiec przedstawianie liczb jest dużo łatwiejsze niż w przypadku systemu binarnego. Ten fakt nie umknął uwadze inżynierów którzy projektowali komputerowy system binarny. Kiedy pracowali z dużymi wartościami, liczby binarne szybko stały się zbyt nieporęczne .Niestety, komputer myśli binarnie, większość czasu ,w dogodnym w użyciu binarnym systemie liczbowym. Mimo, że umiemy konwertować między systemem dziesiętnym a systemem dwójkowym, przeliczanie nie jest błahym zadaniem. Heksadecymalny system liczbowy (oparty o 16) rozwiązuje te problemy. .Liczby heksadecymalne oferują dwie cechy ,których szukamy: są niewielkich rozmiarów i prosto zamienia się je na liczby binarne i vice versa .Z powodu tego, większość binarnych systemów komputerowych dzisiaj używa heksadecymalnego systemu liczbowego. Ponieważ podstawą liczby heksadecymalnej jest 16,każda heksadecymalna cyfra przedstawia jakąś wartość którą mnożymy przez kolejną potęgę liczby 16.Na przykład liczba 1234(16) równa się: 1*163*1623*161*160 lub 4096+512+48+4 = 4660(10) Każda cyfra heksadecymalna może reprezentować jedną z szesnastu wartości między 0 a 15(10).Ponieważ jest tylko 10 cyfr dziesiętnych, musimy wymyślić sześć dodatkowych cyfr dla przedstawienia wartości w zakresie 10(10) do

15(10) ..Zamiast tworzyć nowe symbole dla tych cyfr, użyjemy liter od A do F. Wszystkie niżej przedstawione wyrażenia są przykładami prawidłowych heksadecymalnych liczb: 1234(16) DEAD(16) BEEF(16) 0AFB(16) FEED(16) DEAF(16) Ponieważ będziemy musieli często wprowadzać liczby heksadecymalne do systemu komputerowego, będziemy potrzebować rożnych mechanizmów dla przedstawiania liczb heksadecymalnych. W końcu, w większości systemów komputerowych nie można wprowadzać indeksów dolnych oznaczających rodzaj wprowadzanej liczby. Przyjmiemy następującą konwencję: * Wszystkie wartości liczbowe (bez względu na ich indeks) zaczynamy cyfra dziesiętną * Wszystkie wartości heksadecymalne kończymy literą "h" np. 123A4h * Liczby dziesiętne mogą mieć przyrostek "t" lub "d" Przykłady prawidłowych liczb heksadecymalnych: 1234h 0DEADh 0BEEFh 0AFBh 0FEEDh 0DEAFh Więc jak widzisz, liczby heksadecymalne są niewielkich rozmiarów i łatwe do odczytu. W dodatku możesz łatwo przekształcać między liczbami heksadecymalnymi a binarnymi. Rozważ następująca tablice: Binarnie 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

Heksadecymalnie 0 1 2 3 4 5 6 7 8 9 A B C D E F

Tablica 1 Liczby binarne i heksadecymalne Ta tablica dostarcza wszystkich informacji których będziesz potrzebował przy konwersji liczby heksadecymalnej na binarną i vice versa. Konwersja liczby heksadecymalnej na binarną polega na zamianie odpowiednich czterech bitów każdej heksadecymalnej cyfry na liczbę. Na przykład, konwersja 0ABCDh na wartość binarną, polega na konwersji każdej heksadecymalnej cyfry według powyższej tabeli: 0 A B C D heksadecymalnie 0000 1010 1011 1100 1101 binarnie Konwersja liczby binarnej na format heksadecymalny jest prawie tak samo łatwa. Pierwszy krok to dodanie zer do liczby binarnej aby upewnić się, ze występuje wielokrotność czterech bitów w liczbie. Na przykład, mamy daną liczbę binarna 1011001010,pierwszym krokiem będzie dodanie dwóch bitów z lewej strony liczby, tak aby zawierała 12 bitów. Konwertowana wartość binarna to 001011001010.Następny krok to rozdzielenie wartości binarnej w grupy czterobitowe np. 0010 1100 1010.Na koniec sprawdzamy te wartości binarne w powyższej tablicy i zastępujemy je właściwymi cyframi heksadecymalnymi np. 2CA.Porównaj to z trudnościami przy konwersji między liczbami dziesiętnymi a binarnymi, czy dziesiętnymi i heksadecymalnymi. Ponieważ konwertowanie między liczbami heksadecymalnymi a binarnymi jest operacją, którą będziesz wykonywał wielokrotnie, wiec poświęć kilka chwil i naucz się powyższej tabelki na pamięć. Nawet jeśli masz kalkulator, który zrobi te konwersje dla ciebie, odkryjesz, że konwersja ręczna jest dużo szybsza i bardziej dogodna kiedy konwertujesz pomiędzy liczbami binarnymi i heksadecymalnymi. 1.4 OPERACJE ARYTMETYCZNE NA LICZBACH BINARNYCH I HEKSADECYMALNYCH

Jest kilka operacji, które możemy wykonać na liczbach binarnych i heksadecymalnych. Na przykład, możemy dodawać, odejmować, mnożyć, dzielić i wykonać inne operacje arytmetyczne. Mimo, że nie musisz zostać ekspertem w tej dziedzinie, powinieneś, umieć wykonać te operacje ręcznie używając kawałka papieru i ołówka. Mając powiedziane, ze powinieneś wykonać te operacje ręcznie, właściwym sposobem wykonania takiej operacji jest posiadanie kalkulatora który wykona to za ciebie. Jest kilka takich kalkulatorów na rynku; lista poniżej przedstawia kilku producentów którzy produkują takie urządzenia: Producenci kalkulatorów heksadecymalnych: * Casio * Hewlett-Packard * Sharp * Texas Instruments Ta lista nie jest wyczerpująca. Kalkulatory innych producentów są prawdopodobnie równie dobre. Urządzenia Hewlett-Packarda są być może najlepsze w swojej grupie ,jednakże są one droższe niż inne .Sharp i Casio wytwarzają kalkulatory, które sprzedają poniżej 50$.Jeśli spodziewasz się napisać program w języku asemblera, posiadanie jednego z tych kalkulatorów jest niezbędne. Alternatywa dla zakupu kalkulatora heksadecymalnego jest posiadanie programu TSR firmy SideKick, który zawiera wbudowany kalkulator. Jednakże, o ile nie masz już jednego z tych programów, lub potrzebujesz kilku innych cech, które oferują ,takie programy nie mają szczególnej wartości, ponieważ kosztują więcej niż kalkulator i nie są tak dogodne w użyciu. Aby zrozumieć dlaczego wydasz pieniądze na kalkulator rozważ następujący problem arytmetyczny: 9h + 1h ---kusi cię, żeby napisać odpowiedź "10h" jako rozwiązanie tego problemu. To nie jest prawidłowo! Prawidłowa odpowiedź to dziesięć, ale zapisana jako "0Ah","10h" nie jest prawidłowym zapisem heksadecymalnym. Podobny problem występuje w tym arytmetycznym problemie: 10h - 1h ----prawdopodobnie kusi cię odpowiedz "9h" pomimo, że prawdziwa odpowiedz to "0Fh".Pamiętaj, ten problem to pytanie "jaka jest różnica między szesnaście a jeden?" Odpowiedź: oczywiście, to piętnaście zapisane "0Fh". Nawet jeśli te dwa przykłady nie zaniepokoiły cię, w sytuacji stresowej twój mózg wróci do trybu dziesiętnego podczas pracy nad czymś istotnym i twoja praca przyniesie błędne rezultaty. Morał z tej historii - jeśli musisz robić arytmetyczne wyliczenia używaj liczb heksadecymalnych ręcznie, poświęć swój czas i bądź przy tym ostrożny. Nigdy nie będziesz wykonywał obliczeń w arytmetyce binarnej. Ponieważ liczby binarne zwykle zwierają długie łańcuchy bitów, wiec jest duża możliwość, że popełnisz błąd. Zawsze przekształcaj liczby binarne na heksadecymalne, wykonaj operacje na heksadecymalnych (najlepiej kalkulatorem heksadecymalnym) i przekształć wynik z powrotem na liczbę binarna ,jeśli to konieczne. 1.5 OPERACJE LOGICZNE NA BITACH Są cztery główne operacje logiczne, które możemy wykonać na liczbach heksadecymalnych i binarnych: AND,OR,XOR (exlusive-or) i NOT.W odróżnieniu od operacji arytmetycznych kalkulator heksadecymalny nie jest konieczny do wykonania tych operacji. Jest to często łatwiejsze do zrobienia ręcznie niż przy użyciu do obliczeń urządzeń elektronicznych. Operacja logiczna AND jest operacją na liczbach binarnych (operuje na dwóch operandach) Są to operacje na pojedynczych bitach. Operacja AND: 0 AND 0=0 0 AND 1=0 1 AND 0=0 1 AND 1=1 Prostym sposobem dla przedstawienia logicznej operacji AND jest tabela prawdy. Tabela prawdy AND wygląda następująco : AND 0 1

0 0 0

1 0 1

Tablica 2: Tabela prawdy AND

Wygląda to jak tabliczka mnożenia z którą zetknęliście się w szkole podstawowej. Kolumna z lewej strony i wiersz na górze reprezentują dane wejściowe operacji AND. Wartości umieszczone na przecięciu się wiersza i kolumny (dla poszczególnych par wartości wejściowych) są wynikiem logicznego ANDowania tych dwóch wartości razem. Po angielsku operacja AND : "Jeśli pierwszy operand równa się jeden i drugi operand równa się jeden ,wtedy wynik równa się jeden; w przeciwnym wypadku wynik równa się zero" Jednym ważnym faktem godnym odnotowania przy logicznej operacji AND, jest to, ze możesz użyć jej do wymuszenia wyniku zero. Jeśli jeden z operandów równa się zero, wynik zawsze jest zero bez względu na drugi operand. W tablicy prawdy powyżej, np. wiersz z zerową wartością wejściową zawiera tylko zera, a kolumna zawierającą zero zawiera wynik zerowy. Odwrotnie, jeśli jeden operand zawiera jeden wynik jest dokładnie wartością drugiego operandu. Ta cecha operacji AND jest bardzo ważna, szczególnie kiedy pracujesz z łańcuchem bitów i chcesz ustawić pojedynczy bit w łańcuchu na zero. Zbadamy to zastosowanie logicznej operacji AND w następnej sekcji. Logiczna operacja OR jest także operacja na bitach. Jej definicja: 0 OR 0 = 0 0 OR 1 = 1 1 OR 0 = 1 1 OR 1 = 1 Tablica prawdy dla operacji OR przybiera następującą formę: OR 0 1 0 0 1 1 1 1 Tablica 3: Tablica prawdy OR Potocznie, operacja logiczna OR: "jeśli pierwszy operand lub drugi operand (lub oba) mają wartość jeden, wynik wynosi jeden, w przeciwnym razie wynik równa się zero." Jest to również znane jako operacja inclusive-OR. Jeśli jeden z operandów operacji logicznej OR równa się jeden, wynik zawsze wynosi jeden bez względu na wartość drugiego operandu. Tak jak w logicznej operacji AND, jest to ważna cecha logicznej operacji OR, która udowadnia całkowitą przydatność podczas pracy z łańcuchami bitów.(zobacz następną sekcję). Odnotujmy, że jest różnica pomiędzy tą forma operacji logicznej OR a standardowym znaczeniem angielskim. Rozważmy takie zdanie: "Idę do sklepu LUB Idę do parku". Taka wypowiedz wskazuje, że mówca idzie do sklepu lub do parku, ale nie do obu miejsc naraz. Zatem, angielska wersja logicznego OR jest odrobinę rożna niż operacja inclusive-OR, rzeczywiście bliżej jej do operacji exclusive-OR. Logiczna operacja XOR (exclusive-OR) jest również operacja na bitach. Jej definicja znajduje się poniżej: 0 XOR 0 = 0 0 XOR 1 = 1 1 XOR 0 = 1 1 XOR 1 = 0 Tablica prawdy dla operacji XOR przybiera następującą formę: XOR 0 1 0 0 1 1 1 0 Tablica 4: Tablica prawdy XOR Operacja logiczna XOR: "jeśli pierwszy operand lub drugi operand, ale nie obydwa równocześnie, równają się jeden wynik równa się jeden; w przeciwnym razie wynik równa się zero" Zauważ, ze operacja exclusive-OR jest bliższa angielskiemu znaczeniu słowa "or" niż operacji logicznej OR. Jeśli jeden operand operacji logicznej exclusive-OR równa się jeden, wynik jest zawsze odwrotnością drugiego operandu; to znaczy ,jeśli jeden operand równa się jeden, wynik równa się zero jeśli drugi operand równa się jeden, a wynik równa się jeden jeśli drugi operand równa się zero. Jeśli pierwszy operand zawiera zero, wtedy wynik jest dokładnie wartością drugiego operandu. Ta cecha pozwala na selektywne odwracanie bitów w łańcuchu bitów. Operacja logiczna NOT jest operacja jedno argumentową (w znaczeniu, że przyjmuje tylko jeden argument): NOT 0=1 NOT 1=0

Tablica prawdy operacji NOT przyjmuje następującą formę: NOT

0 1 1 0 Tablica 5: Tablica Prawdy NOT

1.6 OPERACJE LOGICZNE NA LICZBACH BINARNYCH I ŁAŃCUCHACH BITÓW Jak zostało opisane w poprzedniej sekcji ,funkcje logiczne pracują tylko z pojedynczymi bitami operandów. Ponieważ 80x86 używa grup 8,16 lub 32 bitów, musimy rozszerzyć definicje tych funkcji dotyczące więcej niż dwóch bitów. Funkcje logiczne w 80x86 operują na zasadzie "bit przez bit" .Jeśli podano dwie wartości, funkcje te operują na bicie zero ustalając w wyniku bit zero. Operując na bicie jeden wartości wejściowych ,ustawiają w wyniku bit jeden itd. itp. Na przykład, jeśli chcesz obliczyć logiczne AND z następujących dwóch ośmiobitowych liczb, musisz wykonać operację logicznego AND na każdej kolumnie niezależnie od pozostałych: 1011 0101 1110 1110 ------------1010 0100 Ta forma "bit przez bit" może być śmiało zastosowana również dla innych logicznych operacji. Ponieważ zdefiniowaliśmy operacje logiczne pod względem wartości binarnych, przyjdzie ci dużo łatwiej wykonać operacje logiczne na wartościach binarnych niż na wartościach opartych o inne systemy liczbowe. Zatem jeśli chcesz wykonywać operacje logiczne na dwóch liczbach heksadecymalnych, przekonwertuj je najpierw na liczby binarne .Ma to zastosowanie do większości podstawowych operacji logicznych na liczbach binarnych (np. AND, OR ,XOR, itd) Umiejętność ustawiania bitów na zero lub jeden, przy użyciu operacji logicznych AND/OR i umiejętność odwracania bitów przy użyciu operacji logicznej XOR jest bardzo ważna kiedy pracujemy z łańcuchami bitów (np. liczbami binarnymi) .Te operacje pozwalają selektywnie manipulować pewnymi bitami wewnątrz jakiejś wartości podczas gdy pozostawiamy inne bity w spokoju. Na przykład, jeśli masz ośmiobitowa wartość "X" i chcesz mieć pewność, że bity od czwartego do siódmego będą zawierać zera, możesz logicznie "dodać"(AND) wartość "X" z binarna wartością 00001111.Ta operacja logiczna AND, "bit przez bit" ustawi bardziej znaczące cztery bity na zero i pozostawi mniej znaczące cztery bity z "X" bez zmian. Podobnie możesz ustawić mniej znaczące bity z "X" na jeden i odwrócić bit numer dwa z "X" ,odpowiednio, przez logiczne ORowanie "X" przez 0000 0001 i logiczne exclusiveORowanie przez 0000 0100.Użycie operacji logicznych AND,OR i XOR do manipulowania łańcuchami bitów w ten sposób ,jest znany jako maskowanie łańcucha bitów. Używamy terminu maskowanie, ponieważ możemy używać pewnych wartości (jeden dla AND, zero dla OR/XOR) do "zamaskowania" pewnych bitów podczas operacji zmiany bitów zero/jeden lub ich odwracania. 1.7 LICZBY ZE ZNAKIEM I BEZ ZNAKU Jak dotąd traktowaliśmy liczby binarne jako wartości bez znaku. Liczba binarna ...0000 reprezentowała zero,...0001 przedstawiała jeden ,0010 przedstawiała dwa, tak aż do nieskończoności. A co z liczbami ujemnymi? Wartości ze znakiem zostały wspomniane w pierwszej sekcji, gdzie wspomnieliśmy system „uzupełniania do dwóch”, ale nie omówiliśmy jak przedstawić liczby ujemne używając binarnego systemu liczbowego. To jest właśnie to o czym będzie ta sekcja! Przy przedstawianiu liczb bez znaku przy użyciu binarnego systemu liczbowego mamy ograniczone miejsce na nasze liczby :musza mieć skończoną i niezmienną liczbę bitów. Tak długo jak 80x86 pracuje ,nie jest to zbyt duże ograniczenie, w końcu 80x86 może adresować skończoną liczbę bitów. Z naszego punktu widzenia, mamy poważne ograniczenie liczby bitów do ośmiu,16,32 lub jakiejś innej, malej liczby bitów. Z niezmienną liczbą bitów możemy przedstawić tylko pewna liczbę obiektów .Na przykład, przy pomocy ośmiu bitów możemy przedstawić tylko 256 rożnych obiektów. Wartości ujemne są pełnoprawnymi obiektami, tak jak liczby dodatnie. Dlatego tez użyjemy kilku, z 256 rożnych wartości do przedstawienia liczb ujemnych. Innymi słowy, musimy użyć kilku liczb dodatnich do przedstawienia liczb ujemnych. Aby zrobić to sprawiedliwie przydzielimy połowę z możliwych kombinacji dla wartości ujemnych i połowę dla wartości dodatnich. Możemy więc przedstawić wartości ujemne jako -128..-1 i wartości dodatnie jako 0..127 za pomocą pojedynczego bajtu.Za pomocą 16 bitowego słowa możemy przedstawiać liczby w zakresie -32,768..+32.767.Przy pomocy 32 bitowego

podwójnego słowa przedstawimy wartości z zakresu -2,147,483,648..+2,147,483,647.Generalnie,za pomocą n bitów możemy przedstawić wartość ze znakiem z zakresu -2(n-1)..+2(n-1)-1. Okay, więc możemy przedstawiać wartości ujemne. Ale ,jak to robimy? Cóż, jest wiele sposobów, ale mikroprocesor 80x86 używa notacji „uzupełnienie do dwóch” .W systemie „uzupełnienia do dwóch” najbardziej znaczący bit jest bitem znaku. Jeśli jego wartość wynosi zero, wówczas liczba jest dodatnia, jeśli jego wartość wynosi jeden, wówczas liczba jest ujemna. Przykłady: Dla liczby 16-bitowej: 8000h jest liczbą ujemną, ponieważ bit znaku równa się jeden 100h jest liczbą dodatnią ponieważ bit znaku równa się zero 7FFFh jest dodatnia 0FFFFh jest ujemna. 0FFFh jest dodatnia Jeśli najbardziej znaczący bit wynosi zero, wtedy liczba jest dodatnia i jest przechowywana jako standardowa wartość binarna. Jeśli najbardziej znaczący bit równa się jeden, wtedy liczba jest ujemna i jest przechowywana w formie „uzupełnienia do dwóch”. Przy konwertowaniu liczby dodatniej na ujemną, używasz następującego algorytmu: 1) odwracasz wszystkie bity w liczbie np. używając logicznej funkcji NOT 2) dodajesz jeden do odwróconego wyniku Na przykład, obliczymy ośmiobitowy odpowiednik liczby -5: 0000 0101 Pięć (binarnie) 1111 1010 Odwracamy wszystkie bity 1111 1011 Dodajemy jeden do otrzymanego wyniku Jeśli weźmiemy minus pięć i wykonamy na niej operacje „uzupełnienia do dwóch”, otrzymamy naszą oryginalną wartość 000101,tak jak się spodziewaliśmy: 1111 1011 -5 po „uzupełnieni do dwóch” 0000 0100 Odwracamy wszystkie bity 0000 0101 Dodajemy jeden do otrzymanego wyniku (+5) Następujący przykład pokazuje kilka dodatnich i ujemnych 16-bitowych wartości ze znakiem: 7FFFh +32767, największą 16-bitowa liczba dodatnia 8000h -32768, najmniejsza 16-bitowa liczba ujemna 4000h +16384 Konwersje powyższych liczb do ich ujemnych odpowiedników (tj. ich negacji) robimy następująco: 7FFFh: 0111 1111 1111 1111 +32,767t 1000 0000 0000 0000 Odwrócenie wszystkich bitów (8000h) 1000 0000 0000 0001 Dodanie jedynki (8001h lub -32767t) 8000h: 1000 0000 0000 0000 -32,768t 0111 1111 1111 1111 Odwracamy wszystkie bity (7FFFh) 1000 0000 0000 0000 Dodajemy jedynkę (8000h lub -32768t) 4000h: 0100 0000 0000 0000 16,384 1011 1111 1111 1111 Odwracamy wszystkie bity (BFFFh) 1100 0000 0000 0000 Dodajemy jedynkę (0C000h lub -16,384t) Odwrócone 8000h zmieniło się w 7FFFh.Po dodaniu jedynki uzyskujemy 8000h!Czekaj!Co się tutaj dzieje? -(32,768) równa się -32,768? Oczywiście nie! .Ale wartość + 32,768 nie może być przedstawiana jako 16 bitowa liczba ze znakiem, wiec nie możemy zanegować najmniejszej ujemnej wartości. Jeśli spróbujesz takiej operacji, mikroprocesor 80x86 zgłosi błąd. Dlaczego mamy kłopotać się takim nieprzyjemnym systemem liczbowym? Dlaczego nie używać najbardziej znaczącego bitu jako znaku flagi, przechowując dodatni odpowiednik liczby w pozostałych bitach? Odpowiedź leży w hardware. Jak się okazuje negowanie ujemnych wartości jest nużącą pracą. Z systemem „uzupełnienia do dwóch” ,większość innych operacji jest tak łatwa jak system dwójkowy. Na przykład, przypuśćmy, że chcielibyśmy wykonać dodawanie 5+(-5).Wynik równa się zero. Rozważmy co się wydarzy, kiedy dodamy te dwie wartości w systemie uzupełnienia do dwóch: 000000101 111111011 ------------1 000000000

Skończyliśmy z przeniesieniem do 9 bitu i wyzerowanymi pozostałymi bitami .Okazuje się ,że jeśli zignorujemy przeniesienie najbardziej znaczącego bitu, dodanie dwóch wartości ze znakiem zawsze przyniesie prawidłowe rozwiązanie kiedy użyjemy systemu liczbowego uzupełnienia do dwóch. To oznacza ,że możemy używać tego samego hardware dla dodawania i odejmowania liczb ze znakiem i bez znaku. To nie mogłoby wystąpić w przypadku innych systemów liczbowych. Oprócz odpowiedzi na pytania na końcu tego rozdziału, nie musisz wykonywać „uzupełnienia do dwóch” ręcznie. Mikroprocesor 80x86 dostarcza instrukcje NEG (neguj),która wykona te operacje za ciebie. Co więcej ,wszystkie heksadecymalne kalkulatory wykonają ta operacje poprzez naciśniecie klawisza (+/- lub CHS).Niemniej jednak wykonanie „uzupełnienia do dwóch” jest łatwe a ty już wiesz jak to zrobić. Zapamiętaj raz jeszcze, że interpretacja danych reprezentowanych przez zbiór bitów binarnych zależy wyłącznie od kontekstu. Ośmiobitowa wartość binarna 11000000b może przedstawiać znak IBM/ASCII, może przedstawiać dziesiętną wartość bez znaku 192,lub dziesiętną wartość ze znakiem -64,itd.Jako programista, jesteś odpowiedzialny za właściwe używanie tych danych. 1.8 „ROZSZERZANIE ZNAKIEM” I „ROZSZERZENIE ZERAMI” Ponieważ format „uzupełnienia do dwóch” liczb całkowitych ma stała formę ,pojawia się mały problem. Co się stanie jeśli musisz skonwertować ośmiobitową wartość „uzupełnieniem do dwóch” do 16 bitów? Ten problem i jego odwrotność (konwertowanie 16 bitowej wartości do ośmiu bitów) może być realizowane przez „rozszerzenie znakiem” i operacje „ściągania”.80x86 pracuje ze stała długością wartości ,nawet kiedy pracuje na binarnej liczbie bez znaku. "Rozszerzenie o zero" pozwala ci skonwertować mała wartość bez znaku o dużej wartości bez znaku. Rozważmy wartość "-64".Osiem bitów, po „uzupełnieniu do dwóch „ dla tej wartości to "0C0h".16 bitowy odpowiednik tej liczby to 0FFC0h.Teraz,rozwazmy wartość "+64". Ośmio- i szesnastobitowa wersja tej wartości to odpowiednio 40h i 0040h. Różnica między ośmio i szesnastobitową liczbą może być opisana przez zasadę:" Jeśli liczba jest ujemna, najbardziej znaczący bajt z liczby 16 bitowej zawiera 0FFh;jeśli liczba jest dodatnia, najbardziej znaczący bajt z 16 bitowej liczby wynosi zero". „Rozszerzenie znakiem” wartości jakiejś liczby bitów do większej liczby bitów jest łatwe, skopiuj znak bitu do wszystkich dodatkowych bitów w nowym formacie. Na przykład, rozszerzając znak liczby ośmiobitowej do 16 bitowej, po prostu skopiuj siódmy bit z ośmiobitowej liczby do bitów 8..15 z liczby 16 bitowej. „Rozszerzając znak” 16 bitowej liczby do podwójnego słowa, po porostu skopiuj bit 15 do bitów 16..31 z podwójnego słowa. „Rozszerzenie znakiem” jest wymagane kiedy manipulujemy wartościami ze znakiem o rożnych długościach. Często musisz powiększyć bajt do wielkości słowa. Musisz „rozszerzyć znakiem” bajt do wielkości słowa nim ta operacja będzie miała miejsce. Inne operacje (w szczególności ,mnożenie i dzielenie) mogą wymagać poszerzenia do 32 bitów. Nie wolno ci rozszerzać wartości bez znaku. Przykłady „rozszerzenia znakiem”: Osiem bitów Szesnaście bitów Trzydzieści dwa bity 80h FF80h FFFFFF80h 28h 0028h 00000028h 9Ah FF9Ah FFFFFF9Ah 7Fh 007Fh 0000007Fh ---1020h 00001020h ---8088h FFFF8088h Do rozszerzenia bajtu bez znaku musisz zastosować „rozszerzenia zerem”. Rozszerzanie zerem” jest bardzo łatwe – postaw zero przed najbardziej znaczący bajt(y) . Na przykład, „rozszerzanie zerem” wartości 82h do 16-bitowej,po prostu dodajesz zera do bardziej znaczącego bajtu co daje 0082h. Osiem bitów Szesnaście bitów Trzydzieści dwa bity 80h 0080h 00000080h 28h 0028h 00000028h 9Ah 009Ah 0000009Ah 7Fh 007Fh 0000007Fh ---1020h 00001020h ---8088h 00008088h „Ściąganie znaku”, przekształcające wartość jakiejś liczby bitów do identycznej wartości z mniejszą liczbą bitów, jest troszkę bardziej dokuczliwe. „Rozszerzone znaki” nigdy nie zawodzą. Mając m-bitową wartość ze znakiem możesz zawsze przekształcić ją do n-bitowej liczby (gdzie n >m) używając „rozszerzenia znakiem” .Niestety, mając n-bitową liczbę nie zawsze możesz przekształcić ją do m-bitowej liczby jeśli m< n. Na przykład, rozważmy wartość -448. Jako 16-bitowa heksadecymalna liczba która ją reprezentuje mamy 0FE40h. Niestety, rozmiar tej liczby jest zbyt duży aby dopasować ja do wartości ośmiobitowej, wiec nie możesz ściągnąć znaku do ośmiu bitów. To jest przykład przepełnienia stanu ,który zdarza się przy konwersji. Przy stosowaniu „ściągania znaku” jednej wartości do

innej musisz przypatrzyć się najbardziej znaczącym bajt(om),które chcesz „wyrzucić”. Najbardziej znaczące bajty które pragniesz usunąć ,muszą zawierać albo zero albo 0FFh.Jesli napotkasz jakąś inna wartość, nie możesz jej skrócić bez przepełnienia. Ostatecznie, najbardziej znaczący bit z twojej wartości końcowej musi odpowiadać każdemu bitowi który usunąłeś z liczby. Przykłady ( 16 bitów do 8 bitów): FF80h znak nie może być skrócony do 80h 0040h znak nie może być skrócony do 40h FE40h znak nie może być skrócony do 8 bitów 0100h znak nie może być skrócony do 8 bitów

1.9 PRZESUNIĘCIA I OBROTY Innym zbiorem logicznych operacji które mają zastosowanie do łańcucha bitów są operacje przesunięcia i obrotów. Te dwie kategorie mogą być dalej rozbite na "przesunięcie w lewo", "obrót w lewo", "przesunięcie w prawo" i "obrót w prawo". Te operacje okazują się być niezwykle użyteczne dla programisty asemblerowego. Operacja przesunięcia w lewo, przesuwa każdy bit w łańcuchu bitów jedna pozycje w lewo (zobacz rysunek 1.8)

Rysunek 1.8: Operacja przesunięcia w lewo Bit zero przesuwa się na pozycje bitu jeden, poprzednia wartość bitu jeden przesuwa na pozycje bitu dwa itd. Są oczywiście dwa pytania, które pojawiają się w sposób naturalny: "Gdzie zmierza bit zero?" i „Gdzie znika bit siódmy?". Do najmniej znaczącego bitu wpisywana jest wartość zero, a wartość siódmego bitu jest przenoszona podczas tej operacji do znacznika flagi. Zapamiętaj, ze przesunięcie wartości w lewo jest tym samym co pomnożenie jej przez jej podstawę potęgi. na przykład, przesunięcie wartości dziesiętnej jedną pozycję w lewo (dodanie zera z prawej strony) faktycznie mnoży ją przez dziesięć (podstawa potęgi): 1234 SHL 1 = 12340 (SHL 1 = przesunięcie w lewo o jedna pozycje) Ponieważ podstawą potęgi liczby binarnej jest dwa, przesunięcie w lewo mnoży ją przez dwa. Jeśli przesuniemy wartość binarną w lewo dwa razy, mnożymy ją przez dwa razy (tj. mnożymy ją przez cztery).Jeśli przesuwamy wartość binarną w lewo trzy razy, mnożymy ją przez osiem (2*2*2).Generalnie, jeśli przesuwamy wartość w lewo n razy, mnożymy tą wartość przez 2n.Operacja przesunięcia w prawo pracuje na tej samej zasadzie, z wyjątkiem tego, że przesuwamy dane w przeciwnym kierunku. Bit siedem przesuwamy do bitu sześć, bit sześć przesuwamy do bitu piątego itd. Podczas przesunięcia w prawo, do bitu siódmego wpisujemy 0,a bit zero zostaje przeniesiony .(zobacz rysunek 1.9)

Rysunek 1.9: Operacja przesunięcia w prawo Jest jeden problem z przesunięciem w prawo w związku z dzieleniem: jak opisano powyżej, przesuniecie w prawo jest tylko odpowiednikiem bezznakowego dzielenia przez dwa. Na przykład, jeśli chcesz przesunąć wartość bez znaku 254 (0EFh) jedno miejsce w prawo, otrzymasz 127 (07Fh),dokładnie to co chciałeś osiągnąć. Jednak, jeśli przesuniesz binarny odpowiednik -2 (0FEh) w prawo o jedną pozycję, otrzymasz 127 (07Fh) która nie jest prawidłowa. Problem występuje ponieważ wstawiamy zero do bitu siódmego. Jeśli bit siódmy poprzednio zawierał jeden, zmienimy ja z liczby ujemnej na dodatnią. Aby stosować przesunięcie w prawo jako operator dzielenia, musimy zdefiniować trzecią operację przesunięcia: arytmetyczne przesunięcie w prawo. Pracuje ona dokładnie tak jak zwykła operacja przesunięcia w prawo, z jednym wyjątkiem: zamiast zmieniać bit siódmy na zero, pozostawia bit siódmy w spokoju, tzn. podczas operacji przesunięcia nie modyfikuje wartości siódmego bitu, jak pokazuje rysunek 1.10

Rysunek 1.10 Operacja arytmetycznego przesunięcia w prawo Generalnie przynosi to oczekiwane wyniki. Na przykład, jeśli wykonujesz arytmetyczne przesunięcie w prawo na -2 (0FEh) dostajesz -1(0FFh).Zapamiętaj jednak jedną rzecz .Ta operacja zawsze zaokrągla liczby do najbliższej wartości całkowitej która jest mniejsza lub równa rzeczywistemu wynikowi. Opierając się na doświadczeniach z programowania w językach wysokiego poziomu, i standardowych zasadach zaokrąglania wartości całkowitych, większość ludzi przyjmuje założenie, że dzieląc zawsze zaokrągla w kierunku zera. Ale nie jest tak prosto. Na przykład, jeśli zastosujesz operacje arytmetycznego przesunięcia w prawo dla –1 (0FFh),wynik jest -1 ,nie zero.-1 jest mniejsze niż zero wiec operacja arytmetycznego przesunięcia w prawo zaokrągli do -1.To nie jest błąd tej operacji. Jest to sposób dzielenia wartości całkowitych, typowo zdefiniowanych. Instrukcje dzielenia całkowitego 80x86 również dadzą taki wynik. Inną parą użytecznych operacji jest obrót w lewo i obrót w prawo. Te operacje zachowują się tak jak operacje przesunięcia w lewo i w prawo ze znaczącą różnicą: bit który jest przesuwany z jednego końca, zostaje "wsuwany" z końca drugiego .

Rysunek 1.11:Operacja obrotu w lewo

Rysunek 1.12:Operacja obrotu w prawo 1.10 POLA BITÓW I DANE UPAKOWANE Chociaż 80x86 operuje bardziej wydajnie na bajtach, słowach i podwójnych słowach, czasami musimy pracować na typach danych które używają innej liczby bitów niż 8,16 czy 32.Na przykład, mamy daną w postaci "4/2/88".Te trzy wartości liczbowe przedstawiają taką daną: miesiąc, dzień i rok. Miesiąc, oczywiście, przyjmuje wartości od 1 do 12.To wymagałoby co najmniej czterech bitów (maksimum szesnaście rożnych wartości) dla przedstawienia miesiąca. Zakres dni to 1..31.Wymaga to pięciu bitów (maksimum 32 rożne wartości) dla przedstawienia zapisu dnia. Wartość roku, biorąc pod uwagę, że pracujemy z wartościami z zakresu 0..99,wymaga siedmiu bitów (które mogą być użyte do przedstawienia do 128 rożnych wartości).Cztery plus pięć plus siedem to szesnaście bitów lub dwa bajty. Innymi słowy, możemy spakować nasza datę do dwóch bajtów zamiast do trzech, które byłyby wymagane gdybyśmy używali oddzielnych bajtów dla każdej wartości miesiąca, dnia i roku. Ta oszczędność jednego bajtu w pamięci dla każdej przechowywanej danej, może być pokaźną oszczędnością jeśli musimy przechowywać dużo danych. Te bity mogą być ułożone tak jak pokazano poniżej:

Rysunek 1.13 Format upakowania danych MMMM przedstawia cztery bity stanowiące wartość miesiąca, DDDDD przedstawia pięć bitów stanowiących wartość dnia a YYYYYYY to siedem bitów składających się na rok. Każdy zbiór bitów przedstawiający daną wartość nazywany jest „polem bitow”.2 kwietnia 1988 będzie przedstawiony jako 4158h:

0100 00010 1011000 = 0100 0001 0101 1000b lub 4158h 4 2 88 Chociaż wartości spakowane są wydajne (to znaczy, bardzo wydajne pod względem zużycia pamięci),są niewydajne przy obliczeniach (powolne!) Powód? Mamy dodatkowa instrukcje do wypakowania spakowanych danych ,do kilku pól bitów. Ta dodatkowa instrukcja wymaga dodatkowego czasu na wykonanie (i dodatkowych bajtów dla wykonywanej instrukcji);w związku z tym musisz ostrożnie rozważać czy spakowane pola danych zaoszczędza ci cokolwiek. Przykłady możliwych do zastosowania typów danych spakowanych można długo wyliczać .Możesz spakować osiem wartości boolowskich do pojedynczego bajtu, możesz spakować dwie cyfry BCD do bajtu itp. 1.11 ZBIÓR ZNAKÓW ASCII Zbiór znaków ASCI I(wyłączając znaki rozszerzone, zdefiniowane przez IBM) są podzielone na cztery grupy po 32 znaki. Pierwsze 32 znaki o kodach ASCII od 0 do 1Fh (31) są specjalnym zbiorem znaków niedrukowalnych zwanych znakami sterującymi Nazywamy je znakami sterującymi ponieważ wykonują one operacje sterujące na urządzeniach typu drukarka/ekran zamiast wyświetlać symbole. Przykładem może być „powrót karetki” ,który ustawia kursor po lewej stronie bieżącej linii znaków, „przesunięcie o jedną linię” (który przesuwa kursor w dół o jedną linię czy „cofnięcie” (który przesuwa kursor jedna pozycje w lewo). Niestety, rożne znaki sterujące wykonują rożne operacje na rożnych urządzeniach wyjściowych. Jest niewielka standaryzacja między urządzeniami wyjściowymi. Chcąc dowiedzieć się jak znaki sterujące wpływają na poszczególne urządzenia musisz zapoznać się z instrukcją. Druga grupa 32 kodów znaków ASCII stanowi kilka znaków interpunkcyjnych, znaki specjalne i cyfry Najbardziej godne uwagi w tej grupie to znak spacji (Kod ASCII 20h) i cyfry (kody ASCII od 30h do 39h).Zauważ, że cyfry różnią się od swoich wartości liczbowych tylko w najbardziej znaczącym nibble'u. Przez odjęcie 30h z kodu ASCII, od dowolnej cyfry otrzymasz liczbowy odpowiednik tej cyfry. Trzecia grupa 32 znaków ASCII jest zarezerwowana dla dużych znaków alfabetu. Kody ASCII dla znaków "A".."Z" leżą w zakresie 41h..5Ah( 65..90).Ponieważ jest tylko 26 rożnych znaków alfabetu, pozostałe sześć kodów otrzymało kilka specjalnych symboli. Czwarta, i końcowa, grupa 32 kodów znaków ASCII jest zarezerwowane dla małych liter alfabetu, pięć dodatkowych symboli specjalnych, i inne znaki sterujące (np. delete). Zauważ, ze znaki małych liter używają kodów ASCII 61h..7Ah.Jeśli przetworzysz kody dużych i małych liter na liczby binarne, zauważysz, że symbole dużych liter różnią się od swoich małych odpowiedników dokładnie w jednym bitem Na przykład, rozważmy kody znaków "E" i "e" (rysunek 1.14)

Rysunek 1.14:Kody ASCII dla "E' i "e" Te dwa kody różnią się między sobą tylko w jednym miejscu, w bicie piątym. Znak dużej litery zawsze zawiera zero w bicie piątym; znak malej litery zawsze zawiera jeden w tym bicie. Możesz wykorzystać ten fakt do szybkiego przekształcania między duża a małą literą. Jeśli masz dużą literę, możesz ustawić małą literę przez ustawienie bitu piątego na jeden. Jeśli masz małą literę i życzysz sobie zamienić na dużą, możesz zrobić to przez ustawienie bitu piątego na zero .Możesz przełączać znaki alfabetu pomiędzy dużymi i małymi literami poprzez prostą inwersję bitu piątego. Istotnie, bity piąty i szósty określają do której z czterech grup się odnosimy: Bit 5 O 0 1

Bit 6 0 1 0

Grupa Znaki sterujące Cyfry i znaki interpunkcyjne Duże litery i znaki specjalne

1

1

Małe litery i znaki specjalne

Więc możemy, na przykład, skonwertować każda dużą lub małą literę (lub odpowiednie znaki specjalne) do ich odpowiedników- znaków sterujących przez ustawienie bitów piątego i szóstego na zero. Rozważmy, na chwile, kody ASCI kilku cyfr: Znak „0” „1” „2” „3” „4” „5” „6” „7” „8” „9”

Dziesiętnie 48 40 50 51 52 53 54 55 56 57

Heksadecymalnie 30h 31h 32h 33h 34h 35h 36h 37h 38h 39h

Dziesiętne przedstawienie tych kodów ASCII nie jest zbyt pouczające. Jednakże ,przedstawienie heksadecymalne, tych kodów ASCII odsłania coś bardzo ważnego - najmniej znaczący nibble z kodu ASCII jest binarnym odpowiednikiem przedstawianej liczby. Przez pozbawienie ( tj. ustawienie na zero) najbardziej znaczącego nibble'a z numeru znaku, możesz przekształcić ten kod znaku na odpowiednią binarną wartość. Odwrotnie, możesz skonwertować wartość binarną w zakresie od 0 do 9 do ich znaków ASCII przez ustawienie bardziej znaczącego nibble'a na trzy. Zauważ, że możesz użyć tylko logicznej operacji AND do ustawienia najbardziej znaczących bitów na zero; podobnie możesz użyć logicznej operacji OR do ustawienia najbardziej znaczących bitów na 0011 (trzy).Nie możesz jednak skonwertować łańcucha znaków liczbowych do ich odpowiedników binarnych poprzez proste pozbawienie najbardziej znaczącego nibble'a z każdej cyfry w łańcuchu. Konwertowanie 123 (31h 32h 33h) w ten sposób przyniesie trzy bajty 010203h,a nie jest to prawidłowa wartość, którą jest 7Bh.Konwertowanie łańcucha cyfr całkowitych wymaga większej złożoności ; powyższa konwersja działa tylko dla cyfr pojedynczych. Siódmy bit w standardowym ASCII wynosi zawsze zero. To znaczy, ze zbiór znaków ASCII zużywa tylko połowę możliwych kodów znaków w ośmiobitowym bajcie. IBM używa pozostałych 128 kodów znaków dla rożnych znaków specjalnych zawierających znaki międzynarodowe, symbole matematyczne i inne .Zauważ, że te znaki specjalne są niestandardowym rozszerzeniem zbioru znaków ASCII. Oczywiście, nazwa IBM ma dużą siłę przebicia, więc prawie wszystkie nowoczesne komputery osobiste oparte o 80x86 opierają się o rozszerzony zbiór znaków IBM/ASCII. Większość drukarek opiera się również o IBMowski zbiór znaków. Jeśli będziesz musiał wymieniać dane z innymi komputerami, które nie są kompatybilne z PC, masz tylko dwie alternatywy: pozostać przy standardowym ASCII lub upewnić się, że komputer docelowy opiera się o rozszerzony zbiór znaków IBM-PC. Niektóre maszyny, takie jak Apple Macintosh nie dostarczają gotowego wsparcia dla rozszerzonego zbioru znaków, jednakże możesz uzyskać fonty PC, które pozwalają na wyświetlenie rozszerzonego zbioru znaków. Inne komputer(np. Amiga i Atari ST :-) ) maja podobne możliwości. .Jednak,128 znaków standardowym zbiorze znaków ASCII są jedynymi ,które mogą liczyć na przeniesienie z systemu do systemu. Pomimo faktu, ze jest to "standard", proste kodowanie twoich danych nie gwarantuje kompatybilności z innymi systemami. To prawda, ze "A" na jednej maszynie jest podobne do "A" na innej, jest niewielkie ujednolicenie w maszynach pod względem używania znaków sterujących. Istotnie, z 32 kodów sterujących plus delete, tylko cztery kody sterujące są powszechnie stosowane – backspace (BS), tab, carriage return (CF) i line feed (LF).Rożne maszyny często używają tych kodów sterujących na rożny sposób. End of line (koniec linii) jest szczególnie pouczającym przykładem. MS-DOS,CP/M i inne systemy oznaczają koniec linii dwuznakowa sekwencja CR/LF. Apple Macintosh, Apple II i wiele innych systemów oznacza koniec linii pojedynczym znakiem CR. System UNIX oznacza koniec linii pojedynczym znakiem LF. Rzecz jasna, próby wymiany prostego pliku tekstowego między takimi systemami mogą być przeżyciem frustrującym Nawet jeśli są używane standardowe znaki ASCII we wszystkich twoich plikach na tych systemach, będziesz musiał jeszcze skonwertować te dane, kiedy wymienisz pliki między nimi. Na szczęście, takie konwersje są dosyć proste. Pomimo kilku ważnych niedostatków, dane ASCII są standardem dla wymiany danych pomiędzy systemami komputerowymi i programami. Większość programów akceptuje dane ASCII; podobnie większość programów może tworzyć dane

ASCII. Ponieważ, będziesz miał do czynienia ze znakami ASCII w języku asemblera, będziesz musiał dokładnie przestudiować rozkład zbioru znaków i nauczyć na pamięć kilku kodów klawiszy ASCII(np. "0","A","a",itp). 1.12 PODSUMOWANIE Większość nowoczesnych systemów komputerowych używa binarnego systemu liczbowego do przedstawiania wartości. Ponieważ wartości binarne są w pewnym stopniu nieporęczne, często będziemy używać heksadecymalnej reprezentacji dla tych wartości .Jest tak, ponieważ jest bardzo łatwo konwertować pomiędzy heksadecymalną a binarną liczbą, w odróżnieniu od konwersji pomiędzy bardzo dobrze znanym systemem dziesiętnym a binarnym. Pojedyncza cyfra heksadecymalna używa czterech cyfr binarnych (bitów) a grupę czterech bitów nazywamy nibble. Zobacz: "Binarny System Liczbowy" "Format Binarny" "Heksadecymalny System Liczbowy" 80x86 pracuje najlepiej z grupą bitów o długości 8,16 lub 32 bitów. Obiekty o tych rozmiarach nazywamy, odpowiednio, bajtem, słowem i podwójnym slowem.Za pomocą bajtu, możemy przedstawić jedną z 256 unikalnych wartości. Za pomocą słowa możemy przedstawić jedna z 65,536 rożnych wartosci.Za pomocą podwójnego słowa możemy przedstawić ponad miliard rożnych wartości. Często przedstawiamy wartości całkowite (ze znakiem lub bez znaku) za pomocą bajtu, słowa lub podwójnego słowa.; jednakże często będziemy przedstawiać również inne wartości. Zobacz: "Organizacja Danych" "Bajty" "Słowa" "Podwójne Słowa" Żeby mówić o określonych bitach wewnątrz nibble, bajtu, słowa , podwójnego słowa lub innej struktury ,numerujemy bity zaczynając od zera (od najmniej znaczącego bitu) w gorę do n-1 (gdzie n to numer bitu w obiekcie. Również numerujemy nibble ,bajty i słowa w dużych strukturach w podobny sposób. Zobacz: "Format Binarny" Jest dużo operacji które możemy wykonywać na wartościach binarnych wliczając w to normalna arytmetykę (+,-,*, i /) i operacje logiczne (AND, OR, XOR, NOT, Przesuniecie w Lewo, Przesuniecie w Prawo, Obrót w Lewo, Obrót w Prawo).Logiczne AND,OR,XOR i NOT są zwykle określone dla operacji na pojedynczych bitach. .Przesunięcia i obroty są zawsze zdefiniowane dla stałych długości łańcuchów bitów. Zobacz: "Operacje Arytmetyczne Na Liczbach Binarnych I Heksadecymalnych" "Operacje Logiczne Na Bitach" "Operacje Logiczne NA Liczbach Binarnych I Łańcuchach Bitów" "Przesunięcia I Obroty" Są dwa typy wartości całkowitych, które możemy przedstawić za pomocą łańcuchów binarnych w 80x86: wartości całkowite bez znaku i wartości całkowite ze znakiem.80x86 przedstawia wartości całkowite bez znaku za pomocą standardowego formatu binarnego. Przedstawia wartości całkowite ze znakiem używając formatu „uzupełnienia do dwóch.” Ponieważ wartości całkowite bez znaku mogą mieć dowolną długość, można mówić o stałej długości binarnych wartości ze znakiem .Zobacz: "Liczby Ze Znakiem I Bez Znaku" "Rozszerzenie Znakiem i Rozszerzenie Zerem" Często nie można w sposób możliwy do zastosowania w praktyce przechowywać danych w grupach ośmio,szesnasto- i trzydziestodwubitowych. Dla zaoszczędzenia miejsca, możemy chcieć upakować kilka rożnych kawałków danych do tego samego bajtu, słowa lub podwójnego słowa. To zmniejszy pamięć potrzebną do wykonywania kosztownych operacji specjalnych do pakowania i rozpakowywania danych. Zobacz: "Pola Bitów I Dane Spakowane" Znak jest prawdopodobnie najpopularniejszym typem danych z którym się spotykamy, poza wartościami całkowitymi. IBM PC i kompatybilne używają wariantu ze zbiorem znaków ASCII – rozszerzonym zbiorem znaków

IBM/ASCII. Pierwsze ze 128 znaków jest standardowymi znakami ASCII, dalsze128 to znaki specjalne stworzone przez IBM dla języków międzynarodowych, matematyki i innych .Ponieważ użycie zbioru znaków ASCII jest bardzo popularne w nowoczesnych programach, zaznajomienie z tym zbiorem znaków jest niezbędne. Zobacz: "Zbiór Znaków ASCII" 1.14 PYTANIA 01) Przekształć następujące liczby dziesiętne na binarne: a)128 b)4096 c)256 d)65536 e)254 f)9 g)1042 h)15 i)334 j)998 k)255 l)512 m)1023 n)2048 o)4095 p)8192 q)16,384 r)32,768 s)6,334 t)12,334 u)23,465 v)5,643 w)463 x)67 y)888 02) Przekształć następujące wartości binarne na dziesiętne: a)1001 1001 b)1001 1101 c)1100 0011 d)0000 1001 e)1111 1111 f)0000 1111 g)0111 1111 h)1010 0101 i)0100 0101 j)0101 1010 k)1111 0000 l)1011 1101 m)1100 0010 n)0111 1110 o)1110 1111 p)0001 1000 q)1001 1111 r)0100 0010 s)1101 1100 t)1111 0001 u)0110 1001 v)0101 1011 w)1011 1001 x)1110 0110 y)1001 0111 03) Przekształć liczby binarne z punktu (2) na liczby heksadecymalne 04) Przekształć poniższe liczby heksadecymalne na binarne: a)0ABCD b)1024 c)0DEAD d)0ADD e)0BEEF f)8 g)05AAF h)0FFFF i)0ACDB j)0CDBA k)0FEBA l)35 m)0BA n)0ABA o)0CDBA p)0DAB q)4321 r)334 s)45 t)0E65 u)0BEAD v)0ABE w)0DEAF x)0DAD y)9876 Wykonaj następujące obliczenia heksadecymalne (wyniki podaj w liczbach heksadecymalnych): 05) 1234 + 9876 06) 0FFF + 0F34 07) 100 - 1 08) 0FFE - 1 09) Jakie znaczenie ma nibble? 10) Ile cyfr heksadecymalnych jest w: a)bajcie b)słowie c) podwójnym słowie 11) Ile bitów jest w: a) nibble'u b) bajcie c) słowie d) podwójnym słowie 12) Który bit (numer bitu) jest najbardziej znaczącym bitem w: a) nibble'u b)bajcie c) słowie d) podwójnym słowie 13) Jakiego znaku używamy jako przyrostka dla oznaczenia liczby heksadecymalnej, binarnej i dziesiętnej? 14) Zakładając 16 bitowy format "dopełnienia do dwóch", ustal która z wartości w pytaniu czwartym jest dodatnia a która ujemna. 15) „Rozszerz znak” wszystkich wartości w pytaniu drugim do szesnastu bitów. Podaj swoją odpowiedź w liczbach heksadecymalnych. 16) Dokonaj "bitowania" operacją AND na następujących parach wartości heksadecymalnych .Przedstaw swoją odpowiedź w liczbach heksadecymalnych. (Podpowiedź: skonwertuj wartości heksadecymalne na binarne, wykonaj operacje, potem przekonwertuj z powrotem na heksadecymalne): a)0FF00,0FF0 b)0F00F,1234 c)4321,1234 d)2341,3214 e)0FFF,0EDBC f)1111,5789 g)0FABA,4322 h)5523,0F572 i)2355,7466 j)4765,6543 k)0ABCD,0EEFDC l)0DDDD,1234 m)0CCCC,0ABCD n)0BBBB,1234 o)0AAAA,1234 p)0EEEE,1248 q)8888,1248 r)8086,124F s)8086,0CFA7 t)8765,3456 u)7089,0FEDC v)2435,0BCDE w)6355,0EFDC x)0CBA,6884 y)0AC7,365 17) Wykonaj operacje logiczną OR na powyższych parach liczb 18) Wykonaj logiczną operacje XOR na powyższych parach liczb 19) Wykonaj operacje logiczną NOT na wszystkich wartościach w pytaniu czwartym. Załóż, ze wszystkie wartości są 16 bitowe 20) Wykonaj operacje "dopełnienia do dwóch" na wszystkich wartościach w pytaniu czwartym. Załóż 16 bitowe wartości. 21) Rozszerz znak następujących heksadecymalnych wartości z ośmiu do szesnastu bitów. Odpowiedź przedstaw w liczbach heksadecymalnych.: a)FF b)82 c)12 d)56 e)98 f)BF g)0F h)78 i)7F j)F7 k)0E l)AE m)45 n)93 o)C0 p)8F q)DA r)1D s)0D t)DE u)54 v)45 w)F0 x)AD y)DD 22) Skróć znak następujących wartości z szesnastu do ośmiu bitów. Jeśli nie możesz wykonać tej operacji, wyjaśnij dlaczego:

a)FF00 b)FF12 c)FFF0 d)12 e)80 f)FFFF g)FF88 h)FF7F i)7F j)2 k)8080 l)80FF m)FF80 n)FF o)8 p)F q)1 r)834 s)34 t)23 u)67 v)89 w)98 x)FF98 y)F98 23) Rozszerz znak 16 bitowych wartości z pytania 22 do 32 bitów 24) Zakładając, że wartości w pytaniu 22 są 16 bitowe, wykonaj na nich operacje przesunięcia w lewo. 25) Zakładając, że wartości w pytaniu 22 są 16 bitowe, wykonaj na nich operacje przesunięcia w prawo. 26) Zakładając, że wartości w pytaniu 22 są 16 bitowe, wykonaj na nich operacje obrotu w lewo. 27) Zakładając, że wartości w pytaniu 22 są 16 bitowe, wykonaj na nich operacje obrotu w lewo. 28) Skonwertuj następujące daty na spakowany format opisany w tym rozdziale (zobacz: "Pola Bitów i Spakowane Dane" )Przedstaw swoje wyniki jako 16 bitowe liczby heksadecymalne: a)1/192 b)2/4/56 c)6/19/60 d)6/16/86 e)1/1/99 29) Opisz jak za pomocą przesunięć i operacji logicznych wydobyć pole dnia ze spakowanej danej z rekordu w pytaniu 28. 30) Przypuśćmy, ze masz wartość z zakresu 0..9.Wyjasnij jak mógłbyś zamienić na znak ASCI używając podstawowych operacji logicznych.

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK konsultacje naukowe NEKRO [email protected] [email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ DRUGI: ALGEBRA BOOLE’A Obwody logiczne są podstawą nowoczesnych systemów komputerowych. Aby zrozumieć, jak system komputerowy posługuje się nimi, musisz zrozumieć logikę cyfrową i algebrę Boole’a. Ten rozdział wprowadza tylko podstawowe informacje na temat algebry Boole’a. Temat ten jest często tematem całych podręczników. W rozdziale tym, skupimy się na tych aspektach, które będą wsparciem przy czytaniu następnych rozdziałów. 2.0 WSTĘP Logika boolowska stwarza podstawy dla wykonywania obliczeń w nowoczesnych, binarnych systemach komputerowych. Możemy przedstawiać różne algorytmy, lub różne elektroniczne obwody komputerowe używając systemu równań boolowskich. Ten rozdział dostarczy krótkiego wprowadzenia do algebry Boole’a, tablic prawdy, postaci kanonicznej, funkcji boolowskich, upraszczania boolowskich funkcji, projektów logicznych, kombinatoryki i obwodów sekwencyjnych, i równoważników hardware/software. Ten materiał jest szczególnie ważny dla tych ,którzy chcą projektować obwody elektroniczne lub pisać programy sterujące obwodami elektronicznymi. Nawet, jeśli nigdy nie planowałeś projektować hardware’u lub pisać programów nim sterujących, wprowadzenie do algebry boolowskiej, przedstawione w tym rozdziale jest jeszcze ważniejsze, ponieważ możesz używać takiej wiedzy do optymalizowania pewnych złożonych wyrażeń warunkowych, jak IF,WHILE i wielu innych wyrażeń. Sekcja o minimalizowaniu (optymalizowaniu) funkcji logicznych używa Diagramów Veitch’a lub Map Karnaugh’a. Technika optymalizacja to redukcja wielu warunków w funkcjach boolowskich. Musisz zdawać sobie sprawę, że wielu ludzi uważa tą technikę optymalizacji za przestarzałą ,ponieważ redukcja wielu warunków w równaniach nie jest już tak ważna jak była kiedyś. W tym rozdziale używamy metody mapowania jako przykład optymalizowania funkcji boolowskich, ale nie jako technikę stosowaną regularnie. Jeśli jesteś zainteresowany w projektowaniu obwodów i optymalizacji, musisz sięgnąć po teksty bardziej zaawansowane. Chociaż ten rozdział jest głównie zorientowany sprzętowo, przyjmij do wiadomości, że wiele pojęć w tym tekście będzie używało boolowskich równań (funkcji logicznych). Dlatego też powinieneś umieć sobie radzić z funkcjami boolowskimi przed kontynuowaniem dalszego czytania tej książki. 2.1 ALGEBRA BOOLE’A Algebra Boole’a jest systemem matematycznym zamkniętym w granicach wartości zero i jeden (prawda lub fałsz). Operator binarny „° ” określony przez ten zbiór wartości, przyjmuje parę wartości boolowskich jako dane wejściowe i zwraca pojedynczą wartość boolowską. Na przykład, boolowski operator AND, przyjmuje dwie wartości boolowskie na wejściu i zwraca na wyjściu pojedynczą wartość boolowską . Z danego systemu algebraicznego wynika kilka początkowych założeń ,lub aksjomatów, w jakim kierunku ten system pójdzie. Możesz wywnioskować dodatkowe zasady, twierdzenia, i inne właściwości systemu z tego zbioru podstawowych aksjomatów. System algebry boolowskiej często stosuje następujące aksjomaty: ∗ Zamknięcia. System boolowski jest zamknięty jeśli dla każdej pary wartości boolowskich, daje boolowski wynik. Na przykład, logiczne AND jest zamknięte w systemie boolowskim, ponieważ przyjmuje boolowskie operandy i daje tylko boolowskie wyniki. ∗ Przemienności. Mówimy, że operator binarny „° ” jest przemienny jeśli A°B =B°A dla wszystkich możliwych wartości boolowskich A i B ∗ Łączności. Mówimy, że operator binarny „° ” jest łączny jeśli (A°B) °C = A°(B°C) dla wszystkich wartości boolowskich A, B i C ∗ Rozdzielności. Dwa operatory binarne „°” i „%” są rozdzielne jeśli A°(B%C) = (A°B) % (A°C) dla wszystkich wartości boolowskich A, B i C

∗ Tożsamości. Mówimy, że wartość boolowska I jest elementem tożsamym w stosunku do operatora binarnego „°” jeśli A°I = A . ∗ Elementu odwrotnego. Mówimy, że boolowska wartość I jest elementem odwrotnym w stosunku do operatora binarnego „°´jeśli A°I=B a B jest wartością przeciwną do A w systemie boolowskim. Dla naszych celów oprzemy algebrę boolowską na następującym zbiorze operatorów i wartości: Dwie możliwe wartości w systemie boolowskim to zero i jeden. Często będziemy nazywać te wartości (odpowiednio) fałsz i prawda. Symbol „• ” przedstawia logiczną operację AND; np. A•B jest wynikiem logicznego ANDowania boolowskich wartości A i B. Kiedy używamy pojedynczych liter w nazwach zmiennych, wyrzucamy symbol „•” Dlatego też, AB również przedstawia logiczny AND zmiennych A i B (będziemy to również nazywać iloczynem A i B). Symbol „+” przedstawia logiczną operację OR; np. A+B jest wynikiem logicznego ORowania wartości boolowskich A i B (będziemy to również nazywać sumą A i B). Logiczne dopełnienie, negacja lub nie, jest operatorem bez znakowym. W tym tekście będziemy używać symbolu (‘) dla oznaczenia logicznej negacji . Jeśli kilka różnych operatorów pojawia się w pojedynczym wyrażeniu boolowskim, wynik wyrażenia zależy od „pierwszeństwa” operatorów. Będziemy stosować następujące zasady pierwszeństwa (od najwyższej do najniższej) dla operatorów boolowskich: nawiasy, logiczne NOT, logiczne AND potem logiczne OR. .Jeśli dwa operatory o tym samym pierwszeństwie są sąsiadujące, musisz oceniać je od lewej do prawej strony Możemy także użyć następujących zbiorów postulatów: P1 Algebra Boole’a jest zamknięta dla operacji AND, OR I NOT P2 Tożsamość elementów, ze względu, na to że „•” reprezentuje jeden a „+” zero. Brak tożsamości elementów dla operacji logicznej NOT . P3. Operatory „•” i „+” są zamienne. P4 • i + są rozdzielne względem siebie, tzn. A• (B+C) = (A•B)+(A•C) i A+(B•C)=(A+B) • (A+C). P5 Dla każdej wartości A istnieje wartość A’ taka, że A•A’= 0 i A+A’=1. Ta wartość jest logicznym uzupełnieniem (albo NOT) wartości A. P6 • i + oba są łączne. Tzn. (A•B) •C = A• (B•C) i (A+B)+C=A+(B+C). Możemy udowodnić wszystkie inne twierdzenia w algebrze boolowskiej używając tych postulatów. Ten tekst nie będzie się zagłębiał w formalne dowodzenie tych twierdzeń, jednakże to dobra myśl aby zaznajomić się z kilkoma ważnymi teoriami w algebrze boolowskiej. Oto próbka: Th1: Th2: Th3: Th4: Τh5: Th6: Th7: Th8: Th9: Τh10: Th11: Th12: Th13: Th14: Th15: Th16:

A+A=A A•A=A A+0=A A•1=Α A•0=0 A+1=1 (A+B)’ = A’ • Β’ (A•Β)' = Α’ + B’ A + A•Β = Α Α•(Α+Β) = Α A +A’B = A + B A’• (A+B’) =A’B’ AB + AB’ = A’ (A’+B’) • (A’+B) = A’ A + A’ = 1 A • A’ = 0

Twierdzenia siedem i osiem są nazywane Prawami DeMorgana, na cześć matematyka ,który je odkrył. Powyższe twierdzenia występują parami. Każda para (np. Th1 i Th2,Th3 i Th4) ma postać dualną. Najważniejszą zasadą w systemie algebry boolowskiej jest ta dualność. Każde ważne wyrażenie można stworzyć używając aksjomatów i twierdzeń algebry boolowskiej, korzystając z wymiany operatorów i stałych pojawiających się w wyrażeniu. Ściślej, jeśli wymieniamy operatory • i + i zamieniamy wartości 0 i 1 w wyrażeniu, otrzymujemy wyrażenie przestrzegające wszystkich zasad algebry boolowskiej. Nie znaczy to, że wyrażenia dualne obliczają takie same wartości., to tylko znaczy że oba wyrażenia są prawidłowe w systemie algebry boolowskiej. Mimo, że w tym tekście nie będziemy

udowadniać żadnych twierdzeń ze względu na algebrę boolowską, będziemy używać tych teorii dla pokazania ,że dwa boolowskie równania są identyczne. To jest ważna operacja, wtedy kiedy chcemy stworzyć postać kanoniczną wyrażenia boolowskiego lub kiedy upraszczamy wyrażenie boolowskie. 2.2 FUNKCJE BOOLOWSKIE I TABLICE PRAWDY Wyrażenie boolowskie jest sekwencją zer, jedynek i literałów oddzielonych operatorami boolowskim. Literał jest .nazwą zmiennej. Dla naszych celów wszystkie nazwy zmiennych będą pojedynczymi znakami alfabetu. Funkcja boolowska jest określonym boolowskim wyrażeniem; zazwyczaj nadajemy funkcji boolowskiej literę „F” czasami z indeksem dolnym. Na przykład, rozpatrzmy następującą funkcję: F0 =AB+C Ta funkcja oblicza logiczne AND z A i B a następnie logiczne OR z C. Jeśli A=1,B=0 a C=1 wtedy F0 zwraca wartość jeden (1•0+1). Innym sposobem przedstawienia funkcji boolowskiej jest tablica prawdy. W poprzedni rozdziale mieliśmy tablice prawdy przedstawiające funkcje AND i OR. Wyglądają następująco: AND 0 1

0 0 0

1 0 1

Tablica 6: Tablica prawdy AND OR 0 1

0 0 1

1 1 1

Tablica 7: Tablica prawdy OR Dla operatorów binarnych i dwóch zmiennych wejściowych, taka forma tablic prawdy jest bardzo naturalna i dogodna. Jednak, rozpatrzmy jeszcze raz powyższa funkcję F0 . Ta funkcja ma trzy zmienne wejściowe nie dwie ..Zatem nie możemy używać tablic prawdy w formie jaka jest przedstawiona powyżej. Na szczęście, jest bardzo łatwo zbudować tablice prawdy dla trzech lub więcej zmiennych. Poniższy przykład pokaże sposób zrobienia takiej tablicy dla funkcji dla trzech lub czterech zmiennych: BA F =AB +C

00

01

10

11

0

0

0

0

1

1

1

1

1

1

C Tablica 8: Tablica prawdy dla funkcji z trzema zmiennymi BA F =AB +CD

DC

00 01 10 11

00

01

10

11

0 0 0 1

0 0 0 1

0 0 0 1

1 1 1 1

Tablica 9: Tablica prawdy dla funkcji z czterema zmiennymi W powyższych tablicach prawdy ,cztery kolumny przedstawiają cztery możliwe kombinacje zer i jedynek dla zmiennych A i B (B jest bardziej znaczącym bitem ,A jest mniej znaczącym bitem).Podobnie cztery kolumny w drugiej tablicy prawdy przedstawiają cztery możliwe kombinacje zer i jedynek dla zmiennych C i D.D jest bardziej znaczącym bitem a C mniej znaczącym bitem. Tablica 10 pokazuje inny sposób przedstawiania tablic prawdy. Ta forma jest łatwiejsza do wypełniania .Zauważ, że powyższe tablice prawdy uwzględniają wartości dla trzech

oddzielnych funkcji z trzema zmiennymi. Chociaż można stworzyć ogromny zbiór funkcji boolowskich, nie wszystkie będą unikalne. Na przykład, F=A i F=AA są dwiema różnymi funkcjami. Jednak według twierdzenia 2,łatwo pokazać że te dwie funkcje są równoważne ,tzn. przyniosą dokładnie takie same dane wyjściowe dla wszystkich kombinacji danych wejściowych. Jeśli określisz liczbę zmiennych wejściowych, otrzymasz skończoną liczbę unikalnych, możliwych funkcji boolowskich. Na przykład, jest tylko 16 unikalnych funkcji boolowskich przy dwóch danych wejściowych i tylko 256 możliwych funkcji boolowskich dla trzech danych wejściowych. Dla danych n zmiennych wejściowych, jest 2**(2n) (dwa do potęgi 2 ) unikalnych funkcji boolowskich z tych n-zmiennych wejściowych. Dla dwóch zmiennych wejściowych mamy 2^(22) =24 lub 16 różnych funkcji. Dla trzech wartości wejściowych mamy 2**(23=28lub 256 możliwych funkcji. Dla czterech wartości wejściowych tworzymy 2**(2)4lub 216 lub 65,536 możliwych unikalnych funkcji boolowskich. C

B

0 0 0 0 1 1 1 1

0 0 1 1 0 0 1 1

A

F= ABC

F= AB+C

F=A+BC

0 0 0 0 1 0 0 1 0 0 0 0 1 0 1 1 0 0 1 0 1 0 1 1 0 0 1 1 1 1 1 1 Tablica 10: Inny format tablicy prawdy Kiedy mamy do czynieni z 16 funkcjami boolowskimi dosyć łatwo jest nazwać każdą funkcję .Poniższa tablica zawiera 16 możliwych funkcji boolowskich dla dwóch zmiennych wejściowych wraz z ich popularnymi nazwami i: Funkcja # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Opis Zero lub Czyszczenie. Zawsze zwraca zero bez wzgledu na wartosci wejsciowe A i B Logiczne NOR (NOT (A OR B) ) = (A+B)’ Inhibicja = BA’ (B not A). Równiez równowazne B > A lub A < B. NOT A. Ignoruje B i zwraca A’ Inhibicja =AB’ (A not B). Równiez równowazne lub B=A Kopia A. Zwraca wartosc A i ignoruje wartosc B Implikacja , A implikuje B = B+A’ (jesli A wtedy B). Równiez równowaznik A >=B Logiczne OR = A + B. Zwraca A OR B Jeden lub ustawione. Zawsze zwraca jeden bez wzgledu na wartosci wejsciowe A i B Tablica 11: 16 możliwych funkcji boolowskich dla dwóch zmiennych

f0- funkcja stała, f1- funkcja NOR, f2- funkcja implikacji (zakazu), f3- negacja A, f4- funkcja implikacji (zakazu), f5- negacja B,

f6- funkcja sumy wyłączającej, sumy modulo 2 lub funkcja EXOR, f7- funkcja NAND, f8- funkcja iloczynu, f9- funkcja równoważności, f10- funkcja tożsama ze zmienną, f11- funkcja implikacji, f12- funkcja tożsama ze zmienną, f13- funkcja implikacji, f14- funkcja sumy, f15- funkcja stała. Odwołujemy się do numeru funkcji raczej niż do jej nazwy .Na przykład F8 oznacza logiczne AND zmiennych A i B dla dwuwejściowej funkcji ,a F14 jest logiczną operacją OR. Tylko problemem jest ustalenie numeru funkcji. Na przykład dana jest funkcja z trzema zmiennymi F=AB+C, jaki jest jej odpowiedni numer? Ten numer jest łatwy do wyliczenia patrząc na tablicę prawdy dla funkcji (zobacz Tabela 14). Jeśli potraktujemy zmienne A,B i C jako bity w liczbie binarnej, z C jako najbardziej znaczącym bitem a A jako najmniej znaczącym bitem, stworzą one liczby binarne w zakresie od zera do siedmiu. Skojarzone z każdym z tych binarnych łańcuchów jest zero lub jeden jako wynik funkcji. Jeśli zbudujemy wartość binarną przez umieszczenie wyniku funkcji w miejscu określonym przez A,B i C to wartość końcowa liczby binarnej jest numerem funkcji. Rozpatrzmy tablicę prawdy dla F=AB+C: CBA F=AB+C

7 1

6 1

5 1

4 1

3 1

2 0

1 0

0 0

Jeśli potraktujemy wartość funkcji jako liczbę binarną ,otrzymamy wartość F816lub 24810.Zwykle będziemy oznaczać numery funkcji w systemie dziesiętnym. To również pozwala zrozumieć dlaczego jest 2**2n różnych funkcji z n zmiennych: Jeśli mamy n zmiennych wejściowych, jest 2n bitów w numerze funkcji. Jeśli mamy m bitów, jest 2m różnych wartości .Dlatego też dla n wartości wejściowych mamy m.=2n możliwych bitów i 2n lub 2**2n możliwych funkcji. 2.3 ALGEBRAICZBNE DZIAŁANIA NA WYRAŻENIACH BOOLOWSKICH Możemy przetworzyć jedno wyrażenie boolowskie na odpowiadające mu wyrażenie przez zastosowanie aksjomatów i twierdzeń algebry boolowskiej. Jest to ważne jeśli chcesz przekształcić dane wyrażenie do postaci kanonicznej (formy ujednoliconej) lub jeśli chcesz zminimalizować liczbę literałów lub warunki w wyrażeniu. Minimalizowanie warunków i wyrażeń może być ważne ponieważ obwody elektryczne często składają się z pojedynczych komponentów które implementują każdy warunek lub literał dla danego wyrażenia. Minimalizowanie wyrażenia pozwala projektantowi zużyć mniej elektrycznych komponentów i dlatego też może zredukować koszt systemu. Niestety, nie ma stałych zasad które pozwalają optymalizować dane wyrażenie. Podobnie jak budowa matematycznych dowodów ,indywidualne zdolności ułatwiają zrobienie tej transformacji. Niemniej jednak, można pokazać kilka przykładów ich możliwości: ab + ab’ + a’b

(a’b +a’b’ + b’)’

b(a+c) + ab’ +bc’ + c

= = = = = = = = = = = = = = = =

a(b+b’) + a’b a•1 + a’b a + a’b a + a’b + 0 a + a’b +aa’ a + b(a + a’) a + b•1 a+b (a’(b+b’) +b’)’ (a’ + b’)’ ((ab)’)’ ab ba + bc +ab’ +bc’ + c a(b+b’)+b(c+c’) + c a•1 + b•1 + c a+b+c

przez P4 przez P5 przez Th4 przez Th3 przez P5 przez P4 przez P5 przez Th4 przez P4 przez P5 przez Th8 brak definicji przez P4 przez P4 przez P5 przez Th4

Chociaż wszystkie te przykłady używają transformacji algebraicznych do upraszczania wyrażeń boolowskich, możemy również użyć operacji algebraicznych dla innych celów. Następna sekcja opisuje postacie kanoniczne wyrażeń boolowskich. Postać kanoniczna rzadko jest optymalna. 2.4 POSTAĆ KANONICZNA Ponieważ jest skończona liczba funkcji boolowskich z n zmiennymi wejściowymi, mimo to jest skończona liczba możliwych wyrażeń logicznych dzięki którym możemy budować z tych n zmiennych wejściowych, nie mniej jest ogromna liczba wyrażeń logicznych które są odpowiednikami (tj. dają te same wyniki przy danych tych samych zmiennych wejściowych) To pozwala eliminować możliwe pomyłki, projektanci logiczni generalnie wyszczególniają funkcje boolowskie używane w formie kanonicznej lub ujednoliconej. Dla każdej danej funkcji boolowskiej istnieje unikalna postać kanoniczna. To eliminuje pomyłki kiedy pracujemy z funkcjami boolowskimi. W rzeczywistości jest kilka różnych postaci kanonicznych. Będziemy tu omawiać tylko dwie i stosować tylko pierwszą z nich. Pierwsza jest nazywana sumą pełnych iloczynów a druga iloczynem pełnych sum. Używając zasady dualności ,jest bardzo łatwa konwersja między nimi. Warunek jest zmienną lub iloczynem (logiczne AND) kilku różnych .literałów. Na przykład, jeśli masz dwie zmienne A i B, istnieje osiem możliwych warunków: A,B,C,A’,B’,C’,A’B’,A’B,AB’ i AB. Dla trzech zmiennych mamy 26 różnych wartości: A,B,C,A’,B’,C’,A’B’,A’B,AB’,AB,A’C’,A’C,B’C’,B’C,BC’,BC,A’B’C’,AB’C;,A’BC’,ABC’,A’B’C,AB’ C,A’BC i ABC. Jak widzimy, jeśli liczba zmiennych wzrasta, liczba warunków wzrasta drastycznie. Implikant jest iloczynem zawierającym dokładnie n literałów. Na przykład, implikant dla dwóch zmiennych to A’B’,AB’,A’B i AB. Podobnie implikant dla trzech zmiennych A,B i C to: A’B’C’,AB’C’,A’BC’,ABC’,A’B’C,AB’C,A’BC i ABC. Generalnie mamy 2m implikantów dla m zmiennych. Zbiór możliwych implikantów jest bardzo prosty do stworzenia ponieważ one korespondują z porządkiem liczb binarnych Odpowiednik binarny Implikant (CBA) 000 A’B’C’ 001 AB’C’ 010 A’BC’ 011 ABC’ 100 A’B’C 101 AB’C 110 A’BC 111 ABC Tablica 12: Implikanty dla trzech zmiennych wejściowych Możemy wyspecyfikować każda funkcję boolowska używając sumy (logiczne OR) pełnych iloczynów. Danej funkcji F248 = AB+C odpowiada postać kanoniczna ABC+A’BC+AB’C+ABC’. Algebraiczne możemy pokazać te dwa odpowiedniki jako ABC + A’BC+AB’C+A’B’C+ABC’ = BC(A+ A’) + B’C(A+A’) + ABC’ = BC•1 +B’C•1 +ABC’ = C(B+B’) + ABC’ = C+ ABC’ = C+ ab Oczywiście postać kanoniczna nie jest forma optymalną, Z drugiej strony jest duża korzyść z sumy pełnych iloczynów postaci kanonicznej: jest bardzo łatwo stworzyć tablice prawdy dla funkcji w postaci kanonicznej. Co więcej jest również bardzo łatwo stworzyć logiczne równanie dla tabeli prawdy. Budowa tablicy prawdy z formy kanonicznej to prosta konwersja każdego implikantu wewnątrz wartości binarnej poprzez zastąpienie „1” dla. zmiennej „pozytywnej” j i „0” dla zmiennej „zanegowanej”. Potem umieścić „1” na odpowiedniej pozycji (wyspecyfikowanej przez binarną wartość implikantu) w tabeli prawdy: 1) Konwersja implikantu do binarnego odpowiednika: F248=CBA+CBA’+CB’A+CB’A+C’BA = 111+ 110+ 101+ 100+ 011 2) Zastępujemy jedynkę w tabeli prawdy dla każdej powyższej pozycji C

B

A

F =AB+C

0 0 0 0 1 1 1 1

0 0 1 1 0 0 1 1

0 1 0 1 0 1 0 1

1 1 1 1 1

Tabela 13:Tworzenie tabeli prawdy dla implikantów ,Krok Pierwszy Na końcu dodajemy zera do tych pozycji ,które nie są wypełnione jedynkami w kroku pierwszym C B A F =AB+C 0 0 0 0 0 0 1 0 0 1 0 0 0 1 1 1 1 0 0 1 1 0 1 1 1 1 0 1 1 1 1 1 Tabela 14: Tworzenie tabeli prawdy dla implikantów ,Krok Drugi Generowanie funkcji logicznej z tabeli prawdy jest prawie równie łatwe .Po pierwsze, lokalizujemy wszystkie punkty zawierające jeden. W tabeli powyżej, jest to ostatnie pięć punktów. Liczby punktów w tabeli zawierają jedynki wskazujące liczbę implikantów w równaniu kanonicznym. Tworząc pojedynczego implikanta, zastępujemy A,B i C przez jedynki a A’,B’ i C’ zerami w tabeli prawdy .Potem obliczamy sumę tych punktów. W powyższym przykładzie,F248 zawierał jeden dla CBA=111,110,101,100,011.Zatem F248=CBA+CBA’+CB’A+CB’A’+C’BA. Pierwszy warunek ,CBA pochodzi z ostatniego punktu w powyższej tabeli. C,B i A wszystkie zawierają jedynki więc tworzą implikant CBA (lub ABC jeśli wolisz)Drugi punkt zawiera 110 dla CBA więc tworzymy implikant CBA’. Podobnie 101 tworzy CB’A;100 tworzy CB’A i 011 tworzy C’BA. Oczywiście operacje logiczne OR i logiczne AND mogą przestawiać warunki wewnątrz implikantów jak sobie życzymy, i możemy przestawiać implikanty wewnątrz sumy jak zakładamy. Ten proces pracuje równie dobrze z każda liczbą zmiennych. Rozważmy funkcję F53504 = ABCD+A’BCD+A’B’CD+A’B’C’D. Umiejscawiając jedynki na odpowiednich miejscach w tabeli prawdy otrzymujemy coś takiego

D 0 0 0 0 0 0 0 0 1 1 1 1 1

C 0 0 0 0 1 1 1 1 0 0 0 0 1

B 0 0 1 1 0 0 1 1 0 0 1 1 0

A 0 1 0 1 0 1 0 1 0 1 0 1 0

F=ABCD+A’BCD+A’B’CD+A’B’C’D’

1

1

1 1 1

1 0 1 1 1 0 1 1 1 1 Tabela 15: Tworzenie tabeli prawdy dla czterech zmiennych z implikanta Pozostałe elementy w tabeli prawdy zawierają zera. Być może najłatwiejszym sposobem stworzenia postaci kanonicznej funkcji boolowskiej jest po pierwsze wygenerowanie tabeli prawdy dla tej funkcji a potem budowanie postaci kanonicznej z tabeli prawdy .Będziemy używać tej techniki ,na przykład, będziemy konwertować między dwoma postaciami kanonicznymi przedstawionymi w tym rozdziale Jest również prostą sprawą generowanie sumy pełnych iloczynów .Algebraicznie, poprzez użycie przedstawionych praw i twierdzenia 15 (A+A’=1) zrobimy to zadanie łatwo. Rozważmy F248=AB+C.Ta funkcja zawiera dwa warunki AB i C. Ale one nie są implikantami. Implikanty zawierają każdą z możliwych zmiennych w „pozytywnej” lub „zanegowanej” formie. Możemy skonwertować najpierw warunek do sumy pełnych iloczynów jak następuje: = AB • 1 Th4 = AB • (C + C’) Th15 = ABC + ABC’ prawo rozdzielności = CBA + C’BA prawo łączności Podobnie możemy skonwertować drugi warunek w F 248 do sumy pełnych iloczynów. Jak następuje: AB

C

= = = = = = =

C•1 C • (A + A’) CA + CA’ CA•1 +CA’•1 CA•(B +B’) +CA’ • (B+ B’) CAB+ CAB’ =CA;B + CA’B’ CBA + CBA’+CB’A +CB’A’

Th4 Th15 prawo rozdzielności Th4 Th4 prawo rozdzielności prawo łączności

Ostatni krok (przestawiania warunków) w tych dwóch konwersjach jest opcjonalny. Dostaliśmy finalną postać kanoniczną dla funkcji: F248

= =

(CBA + C’BA) + (CBA + CBA’ + CB’A + CB’A’) CBA + CBA’ + CB’A+ CB’A’+C’BA

Innym sposobem wygenerowania postaci kanonicznej jest użycie iloczynu pełnych sum. Implicent jest sumą (logiczne OR) wszystkich danych wejściowych, ”pozytywnych” lub „zanegowanych” Na przykład, rozpatrzmy następującą funkcje logiczną G z trzema zmiennymi: G=(A+B+C)*(A’+B+C)*(A+B’+C). Jako postać sumy pełnych iloczynów jest dokładnie jednak iloczynu pełnych sum dla każdej możliwej funkcji logicznej. Oczywiście, dla każdego iloczynu pełnych sum jest odpowiednia suma pełnych iloczynów. Faktycznie, funkcja G, powyżej, jest odpowiednikiem: F248=CBA+CBA’+CB’A=CB’A’+C’BA-AB+C Generowanie tablicy prawdy dla iloczynu pełnych sum nie jest dużo trudniejsze niż budowanie jej z sumy pełnych iloczynów. Używamy zasady dualności dla osiągnięcia tego. Pamiętaj, że zasada dualności mówi o wymianie AND na OR i zer na jedynki (i vice versa). Dlatego też dla zbudowania tabeli prawdy, musisz wymienić literały „pozytywne” i „negatywne” na przeciwne w G: G=(A’+B’+C’)*(A+B’+C’)*(A’+B+C’) Następnym krokiem jest zamiana logicznego OR i AND. To da: G=A’B’C’+AB’C’+A’BC’ Na koniec musisz wymienić wszystkie zera na jedynki. To znaczy, że musisz przechować zera w tablicy prawdy dla każdej z powyższej pozycji, a potem wypełnić resztę tablicy prawdy jedynkami To umiejscowi zera w punktach zerowych, jeden i dwa w tablicy prawdy. Wypełnienie pozostałych pozycji jedynkami da F248. Możesz łatwo konwertować między tymi dwoma postaciami kanonicznymi przez generowanie tablic prawdy dla jednej postaci i cofając się z tablic prawdy stworzyć drugą postać. Na przykład, rozpatrzmy funkcję z dwoma zmiennymi F7=A+B. Suma pełnych iloczynów to F7 =A’B+AB’+AB Tablica prawdy przyjmuje formę:

F7. 0 0 1 1

A 0 1 0 1

B 0 0 1 1

Tablica 16 F7 (OR) tablica prawdy dla dwóch zmiennych Cofając się, otrzymamy iloczyn pełnych sum, który ma zlokalizowane wszystkie pozycje zawierające zero. To jest punkt gdzie A i B równają się zero. To daje nam pierwszy krok G=A’B’. Jednak jeszcze musimy odwrócić wszystkie zmienne G=AB. Poprzez zasadę dualności musimy zamienić logiczne OR i logiczny AND zawierające G=A+B. To jest postać kanoniczna iloczynu pełnych sum. Ponieważ praca z iloczynem pełnych sum wygląda trochę inaczej niż z sumą pełnych iloczynów, w tym tekście generalnie używać będziemy sumy pełnych iloczynów. Ponadto suma pełnych iloczynów jest bardziej popularna w pracy z logika boolowska. Jednak spotkasz się z obiema postaciami kiedy będziesz studiował projektowanie logiczne. 2.5 UPROSZCZENIA FUNKCJI BOOLOWSKICH Ponieważ jest nieskończenie wiele wariantów funkcji boolowskich dla n zmiennych, ale tylko skończona liczba unikalnych funkcji boolowskich z tych n zmiennych, możesz się zastanawiać, czy jest jakaś metoda która pozwoli uprościć daną boolowska funkcję do stworzenia formy optymalnej Oczywiście możesz zawsze użyć algebraicznej transformacji do stworzenia optymalnej postaci, ale użycie heurystyki nie zagwarantuje optymalnej transformacji. Są ,a jakże, metody które pozwolą zredukować daną boolowską funkcje do jej optymalnej postaci .W tym tekście będziemy stosować metodę map, zobacz inne teksty o projektowaniu logicznym jeśli chcesz poznać inne metody. Od kiedy dla każdej funkcji logicznej musi istnieć forma optymalna, możesz dziwić się dlaczego nie użyto postaci optymalnej dla postaci kanonicznej. Są dwa powody: po pierwsze, może być kilka optymalnych form. Nie gwarantują jednak, że będą unikalne. Po drugie łatwo jest konwertować pomiędzy postacią kanoniczną a tablicą prawdy. Używanie metody map do optymalizacji funkcji boolowskich jest praktyczne tylko dla funkcji z dwoma, trzema i czterema zmiennymi. Ostrożnie, możesz używać jej dla funkcji z pięcioma i sześcioma zmiennymi ale metoda map jest nie efektywna do użycia w tym miejscu Dla więcej niż sześciu zmiennych zastosowanie metody map nie byłoby mądrym posunięciem. Pierwszym krokiem w użyciu metody map jest zbudowanie dwuwymiarowej tablicy prawdy dla funkcji, zobacz rysunek 2.1 A 0

0 B’A’

1 B’A

1

BA’

BA

B Tablica Prawdy dla dwóch zmiennych BA 0

00 CB’A’

01 C’B’A

11 C’AB

10 C’BA’

1

CB’A’

CB’A

CAB

CBA’

C Tablica Prawdy dla trzech zmiennych BA 00 01

00 D’C’B’A’ D’CB’A’

01 D’C’B’A D’CB’A

11 D’C’AB D’CAB

10 D’CBA’ D’CBA’

11

DCB’A’

DCB’A

DCAB

DCBA’

DC

10

DC’B’A’

DC’B’A

DC’AB

DC’BA’

Tablica prawdy dla czterech zmiennych Rysunek 2.1 Dwu, trzy i cztero dwuwymiarowe mapy prawdy Ostrzeżenie: bardzo uważnie patrz na te tablice prawdy. Nie używają takich samych form jakie pojawiały się wcześniej w tym rozdziale. W szczególności ciąg wartości wynosi 00,01,11,10 a nie 00,01,10,11.Jest to bardzo ważne! Jeśli organizujesz tablice prawdy według kolejności binarnej, optymalizacja metodą mapowania nie pracuje właściwie. Będziemy to nazywać mapą prawdy w odróżnieniu od standardowych tablic prawdy. Twoje funkcje boolowskie w postaci kanonicznej (suma pełnych iloczynów),wstawiają jedynki do każdego punktu mapy prawdy odpowiadającego implikantowi w funkcji. Miejsca zerowe gdziekolwiek indziej. Na przykład, rozważmy funkcje z trzema zmiennymi F=C’B’A+C’BA’+C’BA+CB’A’+CB’A+CBA’+CBA. Rysunek 2.2 pokazuje mapę prawdy dla tej funkcji BA 0

00 0

01 1

10 1

11 1

1

1

1

1

1

C F = C’B’A+C’BA’+C’BA+CB’A’+CB’A+CBA’+CBA Rysunek 2.2 : Próbka mapy prawdy Następnym krokiem jest narysowanie prostokątów wokół grup jedynek.. Prostokąty otaczające muszą mieć boki których długości są potęgami dwóch. Dla funkcji z trzema zmiennymi, prostokąty mogą mieć boki których długość wynosi jeden, dwa i cztery. Zbiór prostokątów narysowanych musi otaczać wszystkie komórki zawierające jedynki w mapie prawdy. Sztuką jest namalować wszystkie możliwe prostokąty chyba że prostokąt byłby zupełnie otoczony wewnątrz innego. Zauważ że prostokąty mogą zachodzić na siebie jeśli jeden nie otacza drugiego. W mapie prawdy na rysunku 2.2 są trzy takie prostokąty .Zobacz rysunek 2.3

BA 0

00 0

01 1

10 1

11 1

C 1

1 1 1 1 Rysunek 2.3: Otaczanie prostokątami grup jedynek w mapie prawdy Każdy prostokąt przedstawia warunek uproszczenia funkcji boolowskiej. Dlatego też uproszczenie funkcji boolowskiej będzie zawierało tylko trzy warunki. Zbudujemy każdy warunek używając procesu eliminacji. Wyeliminujemy każdą zmienną której „pozytywne” lub „negatywne” formy zawierają się wewnątrz prostokąta. Rozpatrzmy długi, chudy prostokąt powyżej który jest obecny w wierszu gdzie C=1.Ten prostokąt zawiera oba ,A i B, w „pozytywnej” lub „negatywnej” postaci. Dlatego też możemy wyeliminować A i B z warunku. Ponieważ prostokąt jest obecny w regionie C=1,prostokat przedstawia pojedynczy liberał C. Teraz rozpatrzmy kwadrat Ten kwadrat zawiera C,C’,B , B i A .Dlatego przedstawia pojedynczy warunek A Podobnie kwadrat z wykropkowaną linią zawiera C,C’,A,A’ i B Przedstawia on pojedynczy warunek B. Na koniec, funkcja jest przedstawiana przez trzy kwadraty .Zatem F=A+B+C. Nie musimy rozpatrywać kwadratów zawierających zera. Prawy brzeg mapy prawdy „owija się” wokół lewego brzegu (i vice-versa).Podobnie górny brzeg „owija się” wokół dolnego. To przedstawia dodatkowe możliwości kiedy otaczamy grupy jedynek w mapie. Rozpatrzmy funkcje boolowska F=C’B’A’+C’BA’+CB’A+CBA’. Rysunek 2,4 pokazuje mapę prawdy dla tej funkcji BA 00

01

10

11

0

1

0

0

1

1

1

0

0

1

C F=C’B’A’+C’BA’+CB’A+CBA’ Rysunek 2.4: Tablica prawdy dla funkcji F=C’B’A’+C’BA’+CB’A+CBA’ Przy pierwszym z nią zetknięciu ,można pomyśleć, że są tu dwa możliwe prostokąty ,jak pokazuje rysunek 2.5.Ponieważ mapa prawdy jest stałym obiektem połączonym z prawej i lewej strony, możemy stworzyć, jeden prostokąt jak to pokazuje rysunek 2.6 Wiec jak? Dlaczego mamy martwić się jeśli mamy jeden prostokąt lub dwa w mapie prawdy? Odpowiedź: są większe prostokąty i możemy wyeliminować więcej warunków. Mniejsze prostokąty, mniej warunków pojawi się w końcowej funkcji boolowskiej. Na przykład, był przykład z generowaniem dwóch prostokątów funkcji z dwoma warunkami. Pierwszy prostokąt (na lewo) eliminuje zmienną C, pozostawiając A’B’ jako jej warunki. Drugi prostokąt, na prawo, również eliminuje zmienną C pozostawiając warunek BA’. Dlatego ta mapa prawdy stworzy równanie F=A’B’+AB’. Wiemy, że nie jest to optymalne, zobacz twierdzenie 13.Teraz rozważmy drugą mapę prawdy. Mamy tu pojedynczy prostokąt, więc nasza funkcja boolowska będzie miała jeden warunek. Wyraźnie jest to bardziej optymalne niż równanie z dwoma warunkami. Ponieważ ten prostokąt zawiera i C i C’ jak również B i B’, tylko warunek z lewej to A’. Dlatego też, ta funkcja boolowska daje w wyniku F=A’. Są dwa przypadki kiedy mapa prawdy nie może być zastosowana właściwie: mapa prawdy zawiera same zera lub mapa prawdy zawiera same jedynki. Te dwa przypadki odpowiadają (odpowiednio) funkcji boolowskiej F=0 i F=1.Te funkcje są łatwe do stworzenia poprzez zbadanie mapy prawdy. Ważna rzecz jaka musisz zapamiętać kiedy optymalizujesz funkcję boolowska używając metody mapowania jest to, żebyś zawsze chciał wybierać największy prostokąt którego BA 0

00 1

01 0

10 0

11 1

1

1

0

0

1

C Rysunek 2.5: Pierwsza próba otoczenia prostokątem jedynek BA 0

00 1

01 0

10 0

11 1

1

1

0

0

1

C Rysunek 2.6: Prawidłowy prostokąt dla tej funkcji długość jest potęgą dwójki. Musisz to zrobić nawet dla zachodzących na siebie prostokątów (chyba że jeden prostokąt zawiera inny). Rozważmy funkcje boolowska :F=CB’A’+C’BA’+CB’A’+C’AB+CBA’+CBA. Daje ona mapę prawdy pokazaną na rysunku 2.7 Pierwszą pokusa jest stworzenie jednego zbioru prostokątów założonych na rysunku 2.8 Jednakże prawidłowe mapowanie jest pokazane na rysunku 2.9.Wszystkie trzy mapingi tworzą funkcje boolowska z dwóch warunków. Najpierw są stworzone wyrażenia F=B+A’B’ i F=AB+A’ Trzecia forma F=B+A’ Oczywiście ta ostatnia forma jest najbardziej optymalna niż dwie pozostałe (zobacz twierdzenia 11 i12)Dla funkcji z trzema zmiennymi, rozmiar prostokąta jest określony liczbą warunków ją reprezentujących: ∗ Prostokąt zawierający kwadrat przedstawiający implikant .Skojarzony implikant będzie miał trzy literały. ∗ Prostokąt otaczający dwa kwadraty zawierające jedynki przedstawia warunek zawierający dwa literały. ∗ Prostokąt otaczający cztery kwadraty zawierające jedynki przedstawia warunki zawierające pojedynczy literał. ∗ Prostokąt otaczający osiem kwadratów przedstawia funkcję F=1. Mapy prawdy tworzone dla funkcji z czterema zmiennymi są nawet podstępniejsze Jest tak ponieważ jest dużo miejsc prostokątnych mogących ukrywać się wzdłuż brzegów. Rysunek 2.10 pokazuje kilka możliwych prostokątnych miejsc ukrycia. BA

0

00 1

01 0

10 1

11 1

1

1

0

1

1

C Rysunek 2.7: Mapa prawdy dla F=CB’A’+C’BA’+CB’A’+C’AB+CBA’+CBA BA 0

00 1

01 0

10 1

11 1

1

1

0

1

1

0

00 1

01 0

10 1

11 1

1

1

0

00 1

C BA

C 0 1 Rysunek 2.8: Oczywisty wybór prostokątów

1

BA 01 0

10 1

11 1

C 1 1 0 1 1 Rysunek 2.9: Prawidłowy zbiór prostokątów dla F=CB’A’+C’BA’+CB’A’+C’AB+CBA’+CBA

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00 01 11 10

00 01 11 10 00 01 11 10

00 01 11

10 00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00 01 11 10 00 01 11 10 00 01 11 10 00 01 11 10 00 01 11 10

00 01 11 10 00 01 11 10 00 01 11 10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00

01

11

10

00 01 11 10 00 01 11 10 00 01 11 10 00 01 11 10 00 01 11 10

00 01 11 10 00 01 11 10 00 01 11 10

00 01 11 10 00

01

11

10

00

01

11

10

00 01 11 10 00 01 11 10 Rysunek 2.10: Wzory dla mapy prawdy 4x4 Ta lista wzorów nie obejmuje wszystkich z nich! Na przykład, te diagramy nie pokazują żadnego z prostokątów 1x2.Musisz ćwiczyć ostrożnie kiedy pracujesz z mapą dla czterech zmiennych ,zapewnić sobie wybór największej możliwej liczby prostokątów, zwłaszcza kiedy zachodzą na siebie. jest to szczególnie ważne kiedy następny prostokąt masz z brzegu mapy prawdy. Podobnie jak funkcje z trzema zmiennymi, rozmiar prostokątów w czterozmiennej mapie prawdy dyktuje liczba warunków ją reprezentująca. ∗ Prostokąt zawierający pojedynczy kwadrat przedstawia implikant.. Powiązany warunek ma cztery literały. ∗ Prostokąt otaczający dwa kwadraty zawierające jedynki przedstawia warunek zawierający trzy literały. ∗ Prostokąt otaczający cztery kwadraty zawierające jedynki przedstawia warunek zawierający dwa literały. ∗ Prostokąt otaczający osiem kwadratów zawierających jedynki przedstawia warunek z dwoma literałami. ∗ Prostokąt otaczający szesnaście kwadratów przedstawia funkcje F=1. Ten poniższy przykład demonstruje optymalizację funkcji zawierającej cztery zmienne. Mamy funkcję: F=D’C’B’A’+D’C’B’A+D’C’BA+D’C’BA’+D’CB’A+D’CBA+DCB’A+DCBA+DC’B’A+DC’BA’.Mapa prawdy jest na rysunku 2.11. Mamy tutaj dwa możliwe maksymalne zbiory prostokątów dla tej funkcji, każdy stwarzający trzy warunki (zobacz rysunek 2.12) Obie funkcje są sobie równoważne; obie są tak zoptymalizowane jak można było. Obie będą wystarczające dla naszych celów. Najpierw rozważmy warunki przedstawiane przez prostokąt tworzony w czterech rogach. Te prostokąty zawierają B,B’,D i D’: więc możemy wyeliminować te warunki. Pozostałe warunki zawarte wewnątrz tego prostokąta to C’ i A’, wiec ten prostokąt przedstawia warunek C’A’. Drugi prostokąt, wspólny dla obu map prawdy z rysunku 2.12 jest prostokątem sformowanym przez cztery środkowe kwadraty .Ten prostokąt zawiera warunki A,B,B’,C,D i D’. Eliminujemy B,B’,D i D’ (ponieważ oba „pozytywne” i „negatywne” warunki istnieją) dostajemy CA jako warunek dla tego prostokąta. Mapa z lewej strony na rysunku 2.12, ma trzy warunki przedstawiane przez górny wiersz, Ten warunek zawiera zmienne A,A’,B,B’C’ i D’. Ponieważ zawiera A,A’,B i B’ możemy 00

BA 01 11

10 00 DC 01 11 10 Rysunek 2.11:Mapa prawdy dla: F=D’C’B’A’+D’C’B’A+D’C’BA+D’C’BA’+D’CB’A+D’CBA+DCB’A+DCBA+DC’B’A+DC’BA’

Rysunek 2.12: Dwie kombinacje otaczania zmiennych zawierających trzy warunki. wyeliminować te warunki. Pozostaje nam C’D’. Dlatego też, funkcja przedstawiana przez mapę z lewej strony to F=C’A’+CA+C’D’. Mapa z prawej strony na rysunku 2.12 ma trzy warunki przedstawiane przez górne/środkowe kwadraty Ten prostokąt podsumowuje zmienne A,B,B’,C,C’ i D’. Możemy wyeliminować B,B’ ,C i C’ ponieważ oba „pozytywne’ i „negatywne” wersje się pojawiają, pozostaje warunek AD. Dlatego tez funkcja przedstawiana przez funkcje z prawej strony to F=C’A’+CA+AD’. Ponieważ oba wyrażenia są sobie równoważne, zawierają ta samą liczbę warunków i tą samą liczbę operatorów, obie formy są równoważne. Chyba, że jest inny powód wybrania jednej z nich, można używać obu form. 2.6 JAKI TO MA ZWIĄZEK Z KOMPUTERAMI? Chociaż istnieje słaby związek między funkcjami boolowskimi i wyrażeniami boolowskimi w językach programowania takich jak C lub Pascal, można się zastanawiać dlaczego spędziliśmy tak dużo czasu nad tym materiałem. Jednakże ,związki między logika boolowską i systemem komputerowym są bardzo silne. Jest to indywidualny związek między funkcjami boolowskimi a układami elektronicznymi. Inżynierowie którzy projektują CPU i inne pokrewne układy komputerowe muszą być gruntownie zaznajomieni z tymi rzeczami Nawet jeśli nie zamierzasz projektować swoich własnych obwodów elektronicznych, zrozumienie tych związków jest ważne jeśli chcesz pracować na systemach komputerowych. 2.6.1 RÓWNOWAŻNOŚĆ MIĘDZY UKŁADAMI ELEKTRONICZNYMI A FUNKCJAMI BOOLOWSKIMI Jest to indywidualna równoważność między układami elektronicznymi a boolowskimi funkcjami. Dla każdej funkcji boolowskiej możesz zaprojektować układ elektroniczny i vice versa. Ponieważ funkcje boolowskie zawierają tylko boolowskie operatory AND,OR i NOT, możemy skonstruować każdy układ elektroniczny używając wyłącznie tych operacji. Boolowskie funkcje AND,OR i NOT odpowiadają następującym układom elektronicznym bramkom AND,OR i inwertorowi (NOT) (zobacz rysunek 2.13) Jednym interesującym faktem jest to ,że potrzebujesz tylko typu pojedynczej bramki do wprowadzenia w życie każdego układu elektronicznego. Tą bramka jest bramka NAND, pokazana na rysunku 2.14 Dowiedziemy, że można zbudować każdą funkcje boolowska używając tylko bramki NAND, musimy tylko pokazać jak zbudować inwerter (NOT),bramkę AND i bramkę OR z NAND (ponieważ tworzymy każdą boolowska funkcję używając tylko AND,NOT i OR).Budowanie inwertera jest łatwe, należy jedynie połączyć dwa wejścia razem (zobacz rysunek 2.15). Zaraz po tym jak zbudowaliśmy inwerter, budowa bramki AND jest łatwa – jedynie trzeba odwrócić wartości wyjściowe z bramki NAND. Przecież NOT (NOT(A AND B)) jest odpowiednikiem A AND B .Oczywiście, bierzemy dwie bramki NAND, do stworzenia pojedynczej bramki AND, ale nie można powiedzieć

Rysunek 2.13: Bramki AND,OR i inwerter (NOT)

Rysunek 2.14: Bramka NAND

Rysunek 2.15: Inwerter zbudowany w oparciu o bramkę NAND

Rysunek 2.16: Konstruowanie bramki AND z dwóch bramek NAND

Rysunek 2.17:Konstruowanie bramki OR z bramek NAND że obwody budowane tylko z bramek NAND będą optymalne, tylko to jest to możliwe do zrobienia. Możemy łatwo skonstruować bramki OR z bramek NAND poprzez zastosowanie twierdzeń DeMorgana (A or B)’ = A’ and B’ Teoria DeMorgana A or B = (A’ and B’) Odwracanie obu stron równania A or B = A’ nand B’ Definicja operacji NAND Poprzez zastosowanie tych transformacji, otrzymamy układ z rysunku 2.17. Teraz możemy być zdziwieni dlaczego zawracaliśmy sobie tym głowę. W końcu ,dlaczego nie używać logicznego AND,OR i inwersji bezpośrednio? Są dwa powody. Po pierwsze bramki NAND generalnie są mniej kosztowne do zbudowania niż bramki pozostałe. Po drugie, jest również dużo łatwiej rozwijać złożone układy scalone z tych samych podstawowych bloków niż konstruować układy scalone używając różnych bramek podstawowych. Zauważ, nawiasem mówiąc ,że jest możliwe zbudowanie każdego logicznego układu używając tylko bramek NOR Równoważność między logicznym NAND i NOR jest ortogonalna do równoważności między dwoma postaciami kanonicznymi zawartymi w tym rozdziale (suma pełnych iloczynów i iloczyn pełnych sum).Podczas gdy logiczne NOR jest przydatne dla wielu układów, większość projektów elektronicznych używa logicznego NAND 2.6.2 UKŁADY KOMBINACYJNE Układ kombinacyjny jest układem zawierającym podstawowe operacje boolowskie (AND,OR,NOT),kilka danych wejściowych i zbiór danych wyjściowych. Ponieważ każda dana wyjściowa odpowiada pojedynczej funkcji logicznej ,układ kombinacyjny często oferuje kilka różnych funkcji boolowskich. Ważnym jest abyś zapamiętał ten fakt - każda dana wyjściowa reprezentuje różne funkcje boolowskie. CPU komputera zbudowane jest z różnych układów kombinacyjnych. Na przykład możesz wprowadzić dodatkowy układ używający funkcji boolowskich. Przypuśćmy, że masz dwie jednobitowe liczby A i B .Możesz stworzyć jednobitową sumę i jednobitowe przeniesienie z tego dodawania używając dwóch funkcji boolowskich: S=AB’+A’B Suma A i B C=AB Przeniesie z dodawania A i B Te dwie funkcje boolowskie określają „pół sumatorem”. Inżynierowie nazywają to „pół sumatorem” ponieważ dodajemy dwa bity razem ale nie można dodać przeniesienia z poprzedniej operacji. Sumator pełny dodaje trzy jedno bitowe wartości wejściowe (dwa bity plus przeniesienie z poprzedniego dodawania) i stwarza dwie wartości wyjściowe, sumę i przeniesienie. Dwa logiczne równania dla „pełnego sumatora” to

S = A’B’Cin+A’BC’in+AB’Cin’+ABCin Cout = AB+ACin+BCin Chociaż te logiczne równania tworzą tylko pojedyncze bity wyniku (ignorujemy przeniesienie),łatwo jest zbudować n-bitową sumę poprzez kombinacyjne dodawanie układów (zobacz rysunek 2.18).Ten przykład wyraźnie ilustruje ,że możemy użyć funkcji logicznych do wprowadzania operacji arytmetycznych i boolowskich.. Innym popularnym układem kombinacyjnym jest dekoder siedmiosegmentowy Jest to układ kombinacyjny który przyjmuje cztery dane wejściowe i ustala który z siedmiu segmentów na siedmiosegmentowym wyświetlaczu będzie załączony (logiczne jeden) lub wyłączony (logiczne zero).Ponieważ siedmiosegmentowy wyświetlacz zawiera siedem wartości wyjściowych (jedna dla każdego segmentu) będzie siedem logicznych funkcji stowarzyszonych z wyświetlaczem (od segmentu zero do sześć). Zobacz rysunek 2.19 Rysunek 2.20 pokazuje segmenty przydzielone dla każdej z dziesięciu wartości. Cztery wartości wejściowe dla każdej z siedmiu funkcji boolowskich są czterema bitami z liczby binarnej z zakresu 0 do 9. D jest najbardziej znaczącym bitem tej liczby a A najmniej znaczącym .Każda funkcja logiczna tworzy jeden (segment załączony) dla danego wejścia jeśli określony segment będzie wyświetlony. Na przykład S4 (segment cztery) będzie załączony (on) A0 B0

PÓŁ SUMATOR

A1

PEŁNY SUMATOR

S0. Przeniesienie S1.

B1 Przeniesienie A2

PEŁNY SUMATOR

B2

S2. Przeniesienie

• • • Rysunek 2.18: Budowanie n-bitowych sumatorów przy użyciu „Pół i Pełnego sumatora” S0. S1

S2

S3.

S4

S5

S6.

Rysunek 2.19: Siedmiosegmentowy wyświetlacz

Rysunek 2.20 : Siedmiosegmentowe wartości od „0” do „9” dla wartości binarnej 0000,0010,0110 i 1000.Dla każdej wartości wyświetla się segment będziemy mieć jeden implikant w logicznym równaniu: S4= D’C’B’A+D’C’BA’+D’CBA’+DC’B’A’ S0 jako drugi przykład, jest włączony dla wartości zero, dwa, trzy pięć, sześć, siedem ,osiem i dziewięć. Dlatego też, logiczna funkcja dla S0 jest :

S0 = D’C’B’A’+D’C’BA’+D’C’BA+D’CBA’+D’CBA+DC’B’A’+DC’B’A Możemy stworzyć inne pięć funkcji logicznych w podobny sposób. Układy kombinacyjne są podstawą dla wielu składników podstawowego systemu komputerowego. Możesz skonstruować układy dla dodawania, odejmowania, porównania, mnożenia, dzielenia i wielu innych operacji używających logiki kombinacyjnej. 2.6.3 UKŁADY SEKWENCYJNE I LICZNIKI W teorii, wszystkie logiczne funkcje wyjściowe zależą tylko od bieżących danych wejściowych. Każda zmiana wartości wejściowych natychmiast odbija się na danych wyjściowych. Niestety, komputery potrzebują zdolności zapamiętywania rezultatów poprzednich obliczeń. Jest to domena logiki sekwencyjnej i liczników. Komórka pamięci jest to układ elektroniczny który zapamiętuje wartości wejściowe po usunięciu tychże wartości. Najbardziej podstawową jednostką pamięci jest „przerzutnik SR” .Możesz zbudować przerzutnik SR używając dwóch bramek NAND jak pokazano na rysunku 2.21 Wartości wejściowe S i R są normalnie w stanie wysokim. Jeśli chwilowo ustawisz S na zero i zmienisz ją ponownie na jeden to wartość wyjściowa Q ustawi się na jeden. Podobnie jeśli przełączysz R z jeden na zero i ponownie na jeden to ustawisz Q na zero Q’ jest ogólnie rzecz biorąc odwrotnością wyjściowego Q. Zauważ że jeśli oba ,S i R ,mają wartość jeden, wtedy Q jest zależne od Q’. To znaczy, cokolwiek zdarzyłoby się z Q, górny NAND kontynuować będzie przetwarzanie tej wartości. jeśli Q miało początkowo jeden ,tak więc są dwie jedynki jako dane wejściowe dla dolnego przerzutnika(Q nand R) dające na wyjściu zero (Q’).

Rysunek 2.21:Przerzutnik SR zbudowany z bramek NAND Dlatego też dwie wartości wejściowe na górnym NAND to zero i jeden. Podaje to wartość jeden na wyjście ( odpowiadający oryginalnej wartości Q). Jeśli pierwotnie wartość Q była zero wtedy dane wejściowe dolnej bramki NAND to Q=0 i R=1.Dlatego też, dane wyjściowe tej bramki to jeden. Wartości wejściowe górnej bramki NAND to S=1 i Q’=1.To daje zero na wyjściu, pierwotna wartość Q. Przypuśćmy że Q=0,S=0 i R=0.Te ustawienia dwóch wartości wejściowych do przerzutnika to jeden i zero, ustawiając wyjście (Q) na jeden .Przywrócenie S do stanu wysokiego nie zmienia wcale wartości wyjściowej. Możemy uzyskać ten sam rezultat jeśli Q jest jeden, S zero a R jest jeden. Ponownie uzyskamy na wyjściu jeden. Ta wartość pozostanie jedynką nawet kiedy S przełączy się z zera na jeden .Dlatego też, przełączanie wejściowego S z jeden na zero i ponownie na jeden ,daje jeden na wyjściu (tj. ustawia przerzutnik) Ta sama zasada ma zastosowanie do wejścia R poza ustawieniem Q wyjściowego na zero zamiast jeden. Jest to jedno zastosowanie dla tego układu. Nie działa właściwie jeśli są ustawione oba wejścia S i R równocześnie. To ustawia oba wyjścia Q i Q’ na jeden (co jest logiczną sprzecznością).którekolwiek wyjście pozostanie na dłużej zerem, ustali końcowy stan przerzutnika. Jeśli przerzutnik pracuje w tym trybie ,mówimy że jest niestabilny. Problemem z przerzutnikiem RS jest to ,iż musimy używać oddzielnie danych wejściowych do pamiętania zero lub jeden Komórka pamięci będzie bardziej wartościowa dla nas jeśli wyspecyfikujemy wartość danej do zapamiętania na jednym wejściu i dostarczymy sygnał zegarowy do zatrzasku wartości wejściowej. Ten typ przerzutnika (przerzutnik D) pokazano na rysunku 2.22.Zakladając,że ustaliłeś Q i Q’ wyjściowe na wartości 0/1 lub 1/0,wysyłając impuls zegarowy ,z zera na jeden i zero, skopiujemy wejście D na wyjście Q. To również skopiuje D’ na Q’. Chociaż zapamiętywanie pojedynczych bitów często jest ważne, w większości systemów komputerowych chcielibyśmy zapamiętywać grupy bitów. Możemy zapamiętywać sekwencje bitów poprzez połączenie kilku przerzutników D równolegle Połączenie przerzutników przechowujących n-bitową wartość tworzy rejestr. Elektroniczny schemat z rysunku 2.23 pokazuje jak zbudować ośmiobitowy rejestr ze zbioru przerzutników D.

Rysunek 2.22:Implementacja przerzutnika D z bramek NAND

Rysunek 2.23: Ośmiobitowy rejestr stworzony z ośmiu przerzutników D Zauważ że osiem przerzutników używa wspólnej linii zegarowej. Ten diagram nie pokazuje wyjścia Q’ przerzutników ponieważ są one rzadko wymagane w rejestrze. Przerzutniki D są używane do budowania wielu układów sekwencyjnych nie tylko rejestrów Na przykład, możemy zbudować rejestr przesuwny który przesuwa bity z jednej pozycji na lewo przy każdym takcie zegarowym. czterobitowy rejestr przesuwny pokazano na rysunku 2.24 Można nawet zbudować licznik, który liczy wiele razy przełączanie zegara z zero do jeden i ponownie na zero używając przerzutników. Układ na rysunku 2.25 implementuje czterobitowy licznik używający przerzutnika. Możemy zbudować cały CPU z układów kombinacyjnych i tylko kilku dodatkowych układów sekwencyjnych poza tymi. 2.7 OKAY,ALE CO NAM TO DAJE PRZY PROGRAMOWANIU? Gdy tylko mamy rejestry liczniki i rejestry przesuwne, możemy zbudować ‘maszynę stanów” .Implementacja algorytmów w sprzętowym używaniu „maszyny stanu’ wybiega poza zakres tego tekstu .Jednakże, jeden ważny punkt musi być powiedziany: algorytm można implementować w software, można również implementować bezpośrednio w sprzęcie Wskazuje to, że logika boolowska jest podstawą obliczeń na nowoczesnych systemach komputerowych. Każdy program który napiszesz, można wyszczególnić jako równania boolowskie. Oczywiście, jest dużo łatwiej wyszczególnić rozwiązanie problemu programistycznego używając takich języków jak Pascal, C lub nawet asembler, niż używając równań boolowskich,. Zatem niepodobnym jest abyśmy zawsze wyszczególniali cały program jako zbiór układów logicznych. Niemniej jednak jest czas kiedy implementacja sprzętowa jest lepsza. Rozwiązanie sprzętowe może być jedno ,dwa, trzy lub więcej razy szybsze niż analogiczne rozwiązanie programowe. Dlatego też czasami krytyczne operacje mogą wymagać rozwiązań sprzętowych. Bardziej interesującym faktem jest to, że odwrotność tych powyższych spraw jest także prawdą. Nie tylko można implementować wszystkie funkcje programowe w hardware ale jest również możliwa implementacja wszystkich funkcji sprzętowych w programie. Jest to ważne ponieważ wiele operacji ,które zazwyczaj są implementowane w sprzęcie są dużo tańsze do implementacji używając programów w mikroprocesorze. Istotnie, jest to podstawowe zastosowanie języka asemblera w nowoczesnym systemie jako

Rysunek 2.24 Czterobitowy: rejestr przesuwny zbudowany z przerzutników D

Rysunek 2.25 : Czterobitowy licznik zbudowany z przerzutników D niedrogi zamiennik złożonych układów elektronicznych. Często jest możliwa zamiana dziesięcio – lub studolarowych elektronicznych komponentów na pojedynczy 25 dolarowy chip mikrokomputerowy. Cała grupa „systemów wbudowanych” uporała się z tym problemem. ”Systemy wbudowane” są systemami komputerowymi osadzonymi w innych produktach. Na przykład większość kuchenek mikrofalowych, odbiorników TV, gier wideo, odtwarzaczy CD i innych urządzeń konsumenckich zawiera jeden lub więcej kompletnych systemów komputerowych których jedynym celem jest zastępowanie złożonych projektów sprzętowych. Inżynierowie używają komputerów do tego celu ponieważ są mniej kosztowne i łatwiejsze do zaprojektowania niż tradycyjne układy elektroniczne. Łatwo możemy stworzyć program do odczytania stanu przełączników (zmienne wejściowe) i przełączać silnik, LED’y lub światło zamykać lub otwierać drzwi itp. (funkcje wyjściowe).Do napisania takiego programu będziemy potrzebować zrozumienia funkcji boolowskich i tego jak zaimplementować taką funkcję w programie. Oczywiście jest drugi powód do studiowania funkcji boolowskich nawet jeśli nigdy nie zamierzasz pisać programów przeznaczonych dla „systemów wbudowanych” lub pisać programy dla urządzeń rzeczywistych. Wiele języków wysokiego poziomu przetwarza wyrażenia boolowskie (np. te wyrażenia które sterują wyrażeniem. if lub pętlą while).Przez zastosowanie transformacji takich jak twierdzenia DeMorgana lub optymalizacji mapowej jest często możliwe poprawienie wyników kodu języków wysokiego poziomu. Dlatego, studiowanie funkcji boolowskich jest ważne nawet jeśli nigdy nie zamierzasz projektować układów elektronicznych. Może ci to pomóc napisać lepszy kod w tradycyjnym języku programowania. Na przykład, przypuśćmy, że masz następujące wyrażenie w Pascalu: If ((x=y) and (ab)) or ((x=y) and (c (a + b*2 + c* 4 + d*8)) & 1; } /* Program główny do kierowania ogólną funkcją logiczną napisaną w C */ main{} { int func , a ,b ,c ,d; /*Powtarzanie dopóki użytkownik nie wprowadzi zera*/ do { /* Pobranie numery funkcji (tablica prawdy) */ printf(”Wprowadź wartość funkcji (hex): „); scanf („%x, &func); /* Jeśli użytkownik określił zero jako numer funkcji nastąpi zatrzymanie programu */ if (func != 0) { printf („Wprowadź wartości dla d, c, b i a: „); scanf („%d%d%d%d”, &d, &c, &b, &a); printf (“Wynik to %d \n”, generic (func, a, b, c, d)); printf (“Func = %x, A=%d, B=%d, C=%d, D = %d \n”, func, a, b, c, d); }

*/ */ */ */

) while (func != 0); } Następujący program w Pascalu jest napisany w Pascalu standardowym. Standardowy Pascal nie zawiera żadnych operacji do manipulowania bitami, więc ten program jest przydługi, ponieważ musi symulować używanie bitów w tablicy liczb całkowitych. Większość nowoczesnych Pascali (w szczególności Turbo Pascal )zawiera wbudowane operacje na bitach lub biblioteki które operują na bitach. Ten program byłby łatwiejszy do napisania przy użyciu takich nie standardowych cech. Program GenericFunc (input , output); {*Ponieważ standardowy Pascal nie dostarcza łatwego sposobu bezpośredniej manipulacji bitami w liczbie {*, zasymulujemy numer funkcji używając tablicy 16 liczb całkowitych . „GFTYPE” jest typem tej tablicy

*} *}

type gftype = array [0..15] of integr; var a, b, c, d : integer; fresult; integer; func: gftype; (* Standardowy Pascal nie dostarcza możliwości przesuwania danej całkowitej z lewa w prawo. Dlatego też *) (* zasymulujemy 16 bitową wartość używając tablicy 16 liczb całkowitych,. Możemy zasymulować *) (* poprzez przenoszenie danych wokół tablicy. Zauważ ,że Turbo Pascal dostarcza operatorów shl i shr *) (* Jednak, kod ten jest napisany do działania ze standardowym Pascalem , a nie Turbo Pascalem tylko. *) (* ShiftLeft przesuwa wartości w func na pozycję w lewo i wprowadza przesuniętą wartość na „pozycję bitu”*) (* zero *) procedure ShiftLeft(shiftin: integr); var i : integer; begin for 1 := 1 5 downto 1 do func[i] := func[i-1]; func[0] := shiftin; end; (* ShiftNibble przesuwa daną w func w lewo o cztery pozycje I wprowadza cztery bity a (L.O.), b, c i d (H.O) *) (* na wakujące pozycje *) procedure ShiftNibble (d,c,b,a: integer); begin ShiftLeft (d); ShiftLeft ( c ); ShiftLeft (b); ShiftLeft(a); end; (* ShiftRight przesuwa dane w func jedną pozycję w prawo. Przesuwa zero do bitu H.O. tablicy

*)

procedure ShiftRight; var i : integer; begin for i := 0 to 14 do func[i] := func[i+1]; func[15] := 0; end; (* ToUpper konwertuje małe znaki na duże znaki.

*)

procedure toupper (var ch:char); begin if (ch in[‘a’ .. ‘z’]) then ch :=(ord(ch)- 32_;

end; (* ReadFunc odczytuje numer funkcji heksadecymalnej od użytkownika i odkłada t a wartość do tablicy func (* (bit po bicie) funkcja ReadFunc: integer; var ch:char; i, val : integer; begin write (‘Wprowadź numer funkcji (heksadecymalnie): ‘); for i := 0 to 15 do func[i] := 0; repeat read (ch); if not eoln then begin

*) *)

toupper (ch); case ch of ‘0’: ShiftNibble(0,0,0,0); ‘1’: ShiftNibble(0,0,0,1); ‘2’: ShiftNibble(0,0,1,0); ‘3’: ShiftNibble(0,0,1,1); ‘4’: ShiftNibble(0,1,0,0); ‘5’: ShiftNibble(01,0,1); ‘6’: ShiftNibble(0,1,1,0); ‘7’: ShiftNibble(0,1,1,1); ‘8’: ShiftNibble(1,0,0,0); ‘9’: ShiftNibble(1,0,0,1); ‘A’: ShiftNibble(1,0,1,0); ‘B’: ShiftNibble(1,0,1,1); ‘C’: ShiftNibble(1,1,0,0); ‘D’: ShiftNibble(1,1,0,1); ‘E’: ShiftNibble(1,1,1,0); ‘F’: ShiftNibble(1,1,1,1); end; end; until eoln; val := 0; for i := 0 t o15 do val := val + func[i]; ReadFunc := val end; (* Generic – oblicza ogólną funkcję logiczną określoną przez numer funkcji “func” na czterech danych (* zmiennych a ,b, c i d. Robi to przez zwracane bity d*8 + c*4 +b*2 + a z funkcji function Generic (var func: gftype; a,b,c,d: integer): integer; begin Generic := func [a+b*2 + c*4 + d*8]; end; begin (* main *) repeat fresult := ReadFunc; if (fresult 0) then begin write (Wprowadź wartości dla D , C., B i A (0/1): ‘); readln (d,c,b,a); writeln(“Wynik to ‘, Generic(func, a,b,c,d); end; until fresult = 0; end. Następujący kod pokazuje potęgę operacji manipulowania .Ta wersja kodu powyżej używa specjalnych cech

*) *)

przedstawionych w Turbo Pascalu, które pozwalają programistom na przesuwanie w lewo i w prawo i robienia bitowania logicznego AND na zmiennych całkowitych: program GenericFunc (input, output) ; const hex = [‘a’..’f’, ‘A’…’F’]; dziesiętnie = [‘0’…’9’]; var a,b,c,d :integer; fresult: integer; func: integer; (* Tu mamy drugą wersję ogólnej funkcji pascalowskiej , która używa cech Turbo Pascala do uproszczenia (* programu

*) *)

function ReadFunc: integer; var ch: char; i, val : integer; begin write (‘Wprowadź numer funkcji (heksadecymalnie): ‘; repeat read (ch); func := 0; if not eoln then begin if (ch in Hex) then func := (func shl 4) + (ord(ch) and 15) + 9 else if (ch in Decimal)) then func := (func shl 4) + (ord(ch) and 15) else write(chr(7)); end; until eoln; ReadFunc := func; end; (* Generic – oblicza ogólną funkcję logiczną określoną przez numer funkcji “func” dla czterech zmiennych *) (* a,b,c i d. Robi to przez zwracany bit d*8 + c*4 + b*2 + a z func. Wersja ta polega na operatorze przesunięcia*) (* w prawo Turbo Pascala i jego zdolności do operacji na poziomie bitowym na liczbach całkowitych *) function Generic (func, a, b, c, d: integer): integer; begin Generic := (func shr (a+ b*2+ c*4 + d*8)) and 1; end; begin

(*main *) repeat fresult := ReadFunc; if (fresult 0) then begin write (‘Wprowadź wartości dla D, C, B I A (0/1): ‘); readln(d,c,b,a); writeln (‘Wynik to ‘, Generic(func,a,b,c,d)); end; until fresult = 0;

end. 2.11 PODSUMOWANIE Algebra Boole’a dostarcza podstaw dla sprzętu i programów komputera. Pobieżne zrozumienie tego systemu matematycznego może pomóc ci lepiej rozumieć połączenia między programem a sprzętem. Algebra boolowska jest

systemem matematycznym ze swoim własnym zbiorem zasad (aksjomaty),twierdzeniami i wartościami. Pod wieloma względami, algebra boolowska jest podobna do prawdziwej algebry arytmetycznej, którą zajmowałeś się w szkole. Jednak pod wieloma względami algebra boolowska jest właściwie łatwiejsza do nauczenia się niż prawdziwa algebra. Ten rozdział zaczął się od omówienia cech tego systemu algebraicznego, zawierającego :operatory ,zamknięcie, przemienność, rozdzielność, łączność ,tożsamość i element odwrotny. Potem przedstawionych jest kilka ważnych aksjomatów i twierdzeń z algebry boolowskiej i omówiona zasada dualności która pozwala łatwo dowieść dodatkowych twierdzeń w algebrze boolowskiej po szczegóły zajrzyj: ∗ „Algebra Boole’a” Tablice prawdy są dogodnym sposobem wizualnej reprezentacji funkcji boolowskich lub wyrażeń Każda boolowska funkcja (lub wyrażenie) ma odpowiadającą mu tabelę prawdy, która dostarcza wszystkich możliwych wyników dla każdej kombinacji danych wejściowych. Ten rozdział przedstawia kilka różnych sposobów do budowania boolowskich tablic prawdy. Chociaż jest nieskończona liczba funkcji boolowskich można stworzyć danych n wartości wejściowych okazuje się że jest skończona liczba unikalnych funkcji możliwa dla danej liczby danych wejściowych. W szczególności jest 2^22 unikalnych funkcji boolowskich z n danych wejściowych. Na przykład. jest 16 funkcji dla dwóch zmiennych (2^22 = 16). Ponieważ jest mało funkcji boolowskich tylko z dwoma danymi wejściowymi, łatwo jest przydzielić różne nazwy dla każdej z tych funkcji (np. .AND,OR,NAND itp. Dla funkcji z trzema lub więcej zmiennymi, liczba funkcji jest zbyt duża aby dawać każdej funkcji jej własną nazwę Dlatego też. ,przydzielamy liczbę do tych funkcji opartą na bitach pojawiających się w tabeli prawdy funkcji. Po szczegóły zajrzyj: ∗ „Funkcje Boolowskie I Tablice Prawdy” Możemy manipulować funkcjami boolowskimi i wyrażeniami algebraicznymi. Pozwala to nam dowodzić nowych teorii w algebrze boolowskiej, upraszczać wyrażenia ,konwertować wyrażenia do postaci kanonicznych lub pokazywać że dwa wyrażenia są sobie równoważne. Zobacz kilka przykładów z algebraicznej manipulacji wyrażeniami algebraicznymi, sprawdź. ∗ „Manipulacja Algebraiczna Wyrażeniami Boolowskimi” Ponieważ jest nieskończony wybór możliwych funkcji boolowskich ,mimo to skończona liczba unikalnych funkcji boolowskich (dla stałej liczby danych wejściowych), jest nieskończona liczba różnych funkcji boolowskich które obliczają takie same wyniki. Aby uniknąć zamieszania, projektanci logiczni zwykle wyszczególniają funkcje boolowskie używając postaci kanonicznych. Jeśli dwa kanoniczne równania są różne. wtedy przedstawiają różne funkcje boolowskie. Ta książka opisuje dwie różne postacie kanoniczne: sumę pełnych iloczynów i iloczyn pełnych sum. Naucz się o tych postaciach kanonicznych ,jak konwertować przypadkowe boolowskie równania do formy kanonicznej i jak konwertować między dwoma postaciami kanonicznymi zobacz ∗ „Postacie Kanoniczne” Chociaż postacie kanoniczne dostarczają unikalnych przedstawień dla danej funkcji boolowskiej, wyrażenia pojawiające się w postaci kanonicznej, rzadko są optymalne. To znaczy ,wyrażenie kanoniczne często używa literałów i operatorów ,równoważników, wyrażeń. Znajomość jak stworzyć formę zoptymalizowaną boolowskiego wyrażenia jest bardzo ważna. Ten tekst omawia ten temat w ∗ „Upraszczanie Funkcji Boolowskich” Algebra boolowska nie jest systemem zaprojektowanym przez jakiegoś szalonego matematyka o małym znaczeniu w świecie. .Algebra Boole’a jest podstawą logiki cyfrowej, podstawą dla projektantów komputerowych. Co więcej jest indywidualna równoważność między cyfrowym hardware a komputerowym software Cokolwiek zbudujesz w hardware możesz zbudować z software i vice versa Ten tekst opisuje jak zaimplementować dodatkowo, dekodery, pamięć, rejestry przesuwne i liczniki używając tych funkcji boolowskich. Podobnie ten tekst opisuje jak poprawić wydajność software (np. Programy Pascala) przez zastosowanie zasad i teorii algebry boolowskiej. Wszystkie te szczegóły zobacz: ∗ „Jaki to ma związek z komputerami” ∗ „Równoważność między układami elektronicznymi a funkcjami boolowskimi” ∗ „Układy kombinacyjne” ∗ „Okay, co nam to da przy programowaniu?” 2.12 PYTANIA: 1. Jaki jest tożsamy element pod względem : a) AND b) OR c)XOR d)NOT e)NAND f)NOR 2. Stwórz tabele prawdy dla następujących funkcji z dwoma zmiennymi: a) And b)OR c)XOR d)NAND e) NOR f)Równoważnej g)AB i) A 3. Stwórz tablice prawdy dla następujących funkcji z trzema zmiennymi wejściowymi:

a) ABC(AND) b) A+B+C (OR) c) (ABC)’ (NAND) d) (A+B+C)’ (NOR) e) równoważnik (ABC)+(A’B’C’) f) XOR (ABC+A’B’C’)’ 4. Pokaż schematycznie (diagram układu elektrycznego) jak zaimplementować każdą z funkcji w pytaniu trzecim używając tylko dwóch bramek wejściowych i inwertora. Np. a) ABC =

5.Pokaż implementację bramek AND,OR i inwertera przy użyciu jednej lub więcej bramek NOR. 6. Co to jest zasada dualności? 7. Zbuduj pojedynczą tabelę prawdy która uwzględnia dane wyjściowe dla następujących funkcji boolowskich z trzema zmiennymi: Fx=A+BC Fy=AB+C’B Fz=A’B’C+ABC+C’B’A 8. Uzyskaj numer funkcji dla trzech funkcji z pytania siedem. 9.Ile możliwych (unikalnych) funkcji boolowskich mamy jeśli funkcja ma: a) jedno wejście b) dwa wejścia c) trzy wejścia d)cztery wejścia e) pięć wejść 10. Uprość następujące funkcje boolowskie używając transformacji algebraicznych. a) F=AB+AB’ b) F=ABC+BC’+AC+ABC’ c) F=A’B’C’D+A’B’C’D+A’B’CD+A’B’CD’ d) F=A’BC+ABC’+A’BC’+AB’C’+ABC+AB’C 11. Uprościj funkcje boolowskie z pytania 10 używając metody map. 12. Ułóż równania logiczne w postaci kanonicznej dla funkcji boolowskich S0...S6 dla siedmiu segmentów wyświetlacza (zobacz „Układy kombinacyjne”) 13. Stwórz tablice prawdy dla każdej funkcji z pytania 12 14. Zminimalizuj każdą funkcję z pytania 12 używając metody map 15. Równanie logiczne dla „pół sumatora” (w postaci kanonicznej) to: Sum=AB’+A’B Carry=AB a)stwórz diagram układu elektronicznego dla „pół sumatora” używając bramek AND,OR i inwertera. b)stwórz układ używając tylko bramki NAND 16.Równania kanoniczne dla „pełnego dodawania” przyjmują formę: Sum=A’B’C+A’BC’+AB’C’+ABC Carry=ABC+ABC’+AB’C+A’BC a)stwórz schemat dla tych układów używając bramek AND,OR i inwertera. b)optymalizuj te równania używając metody map c)stwórz układ elektroniczny dla wersji zoptymalizowanej (używając bramek AND,OR i inwertera) 17.Załóżmy,że masz przerzutnik D (użyj definicji x tego tekstu) którego dane wyjściowe obecnie to Q=1 a Q’=0. Opisz w najdrobniejszych szczegółach dokładnie co zdarzy się kiedy na linię zegara dojdzie: a) zmiana stanu z niskiego na wysoki przy D=0 b) zmiana stanu z wysokiego na niski przy D=0 18.Przepisz następujące wyrażenia Pascala tak ,aby uczynić je bardziej wydajnymi: a) if (x or (not x and y)) then write(‘1’); b) while(not x and not y) do somefunc(x,y); c) if not ((xy) and (a=b) then something; 19. Sprowadź do postaci kanonicznej (suma pełnych iloczynów) : a)F(A,B,C)=A’BC+AB+BC b)F(A,B,C,D)=A+B+CD’+D c) F(A,B,C)=A’B+B’A d) F(A,B,C,D)=A+BD’ e)F(A,B,C,D)=A’B’C’D+AB’C’D’+CD+A’BCD’ 20. Przekształć sumę pełnych iloczynów z pytania 19 do iloczynu pełnych sum

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by: KREMIK konsultacje naukowe: NEKRO [email protected]

[email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

ROZDZIAŁ TRZECI: ORGANIZACJA SYSTEMU Napisanie nawet skromnego programu w języku asemblera dla 80x86 wymaga znajomości rodziny 80x86. Napisanie dobrego programu w języku asemblera wymaga sporej wiedzy o wykorzystywanym sprzęcie. Niestety wykorzystywany sprzęt nie jest spójny. Techniki które są kluczowe dla 8088 mogą nie być przydatne dla systemów 80486. Podobnie, techniki programistyczne które pozwalają uzyskiwać większą wydajność na chipach 80486 mogą nie być pomocne we wszystkim na 80286.Szczęśliwie,niektóre techniki programistyczne pracują dobrze niezależnie jakiego mikroprocesora używamy. Ten rozdział omawia wpływ jaki sprzęt ma na wydajność programów komputerowych 3.0 WSTĘP Ten rozdział opisuje podstawowe komponenty które stanowią system komputerowy:CPU,pamięć, I/O ( wejście/wyjście ) i magistrale które je łączą. Chociaż możemy napisać program który nie zna tych pojęć jednak program na wysokim poziomie wymaga kompletnego zrozumienia tego materiału. Ten rozdział zaczyna się od omówienia organizacji magistral i pamięci. Te dwa komponenty sprzętowe,będą prawdopodobnie miały większy wpływ na wydajność twojego programu niż szybkość CPU. Zrozumienie organizacji systemu magistral pozwoli ci zaprojektować struktury danych działające z maksymalną szybkością. Podobnie wiedza o charakterystycznych właściwościach pamięci, rejonach danych i działaniu na pamięci podręcznej cache pomoże stworzyć ci program działający tak szybko jak to możliwe. Oczywiście, jeśli nie interesuje cię pisanie kodu szybko wykonywalnego możesz opuścić to omówienie jednakże większość ludzi troszczy się o szybkość lub inaczej ta wiedza jest dla nich użyteczna. Niestety, rodzina mikroprocesorów 80x86 jest złożoną grupą i często przytłacza początkujących. Dlatego też ten rozdział będzie używał czterech hipotetycznych członków z rodziny 80x86: mikroprocesory 886,8286,8486 i 8686.Przedstawiają one uproszczone wersje chipów 80x86 i pozwalają omówić różne cechy architektury bez grzęźnięcia poprzez ogromy zbiór instrukcji CISC. Ten tekst używa hipotetycznych procesorów x86 do opisu pojęć kodowania instrukcji, trybów adresowania ,wykonywania sekwencyjnego, kolejki rozkazów, potokowania i operacji superskalarnych. Rzecz jasna, tych pojęć nie musisz się uczyć jeśli chcesz pisać tylko poprawne programy. Jednakże ,jeśli chcesz pisać dobrze ,szybkie programy zwłaszcza na zawansowanych procesorach takich jak 80486,Pentium i innych, musisz nauczyć się tych pojęć. Niektórzy mogą argumentować że ten rozdział stanie się zbyt skomplikowany w związku z architekturą komputera. Będą uważać, że taki materiał powinien ukazać się w książce o architekturze, nie zaś w książce o programowaniu w języku asemblera. Nie jest to dalekie od prawdy! Napisanie dobrego programu asemblerowego wymaga solidnej wiedzy o architekturze. W związku z tym taki nacisk na architekturę komputera w tym rozdziale.

3.1 PODSTAWOWYY SYSTEM KOMPONENTÓW Podstawowy, gotów do działania, projekt systemu komputerowego nazywany jest jego architekturą. John von Neumann,pionier w projektowaniu komputerów, dał podstawy architektury większości komputerów dzisiaj używanych.Na przykład rodzina 80x86 używa Architektury Von Neumanna (VNA). Typowy system Von Neumanna zawiera trzy ważne komponenty: CPU (jednostka centralna),pamięć i wejścia/wyjścia (I/O).Sposób w jaki projektant systemu łączy te trzy komponenty wpływa na wydajność systemu. (zobacz rysunek 3.1). W maszynach VNA,takich jak rodzina 80x86, CPU bierze udział we wszystkich zachodzących zdarzeniach Wszystkie obliczenia zachodzą wewnątrz CPU. Dane i instrukcje CPU tkwią w pamięci dopóki nie zażyczy ich sobie CPU. Dla CPU ,większość urządzeń I/O, wygląda jak pamięć ponieważ może przechowywać dane

Rysunek 3.1: Typowa maszyna Von Neumanna z urządzenia wyjściowego i czytać dane z urządzenia wejściowego. Ważną różnicą pomiędzy położeniem w pamięci a położeniem I/O jest fakt że położenie I/O jest generalnie powiązane z zewnętrznymi urządzeniami. 3.1.1 SYSTEM MAGISTRAL System magistral łączy różne komponenty maszyny VNA.Rodzina 80x86 ma trzy ważne magistrale: magistralę adresową, magistralę danych i magistralę sterującą. Magistrala jest zbiorem przewodów,którymi sygnały elektryczne są przesyłane między komponentami w systemie. Te magistrale różnią się między procesorami. Jednak,każda magistrala przenosi porównywalne informacje we wszystkich procesorach, np. magistrala danych może być inaczej zaimplementowana na 80386 niż 8088,ale obie przenoszą dane pomiędzy procesorem, pamięcią i I/O. Typowy system komponentów 80x86 używa standardowych poziomów logicznych TTL To znaczy, każdy przewód na magistrali używa standardowego poziomu napięcia dla przedstawiania zera lub jedynki. Zawsze możemy wyszczególnić zero i jeden zamiast poziomów elektrycznych ponieważ te poziomy różnią się dla różnych procesorów (zwłaszcza laptopów). 3.1.1.1 MAGISTRALA DANYCH

Procesory 80x86 używają magistrali danych do przenoszenia danych pomiędzy różnymi komponentami w systemie komputerowym. Rozmiar tej magistrali różni się znacznie w rodzinie 80x86.W rzeczywistości, ta magistrala określa „rozmiar” procesora. W typowym systemie 80x86,magistrala danych zawiera osiem,16,32 lub 64 linie .Procesory 8088 i 80188 mają ośmiobitową magistralę danych (osiem linii danych). Procesory 8086,80186,80286 i 80386SX mają szesnastobitową magistralę danych. Procesory 80386DX,80486 i Pentium Overdrive mają 32-bitową magistralę danych. Procesory Pentium i Pentium Pro mają 64-bitową magistralę danych Przyszłe wersje chipów (80686/80786?) mogą mieć większe magistrale. Mając ośmiobitową magistralę danych procesor nie jest ograniczony do ośmiobitowych typów danych. Po prostu, procesor może uzyskać dostęp do jednego bajtu danych na cykl pamięci. (zobacz „Podsystem Pamięci” z opisem cyklu pamięci)

“Rozmiar” procesora Była spora różnica zdań pomiędzy inżynierami od sprzętu i oprogramowania dotycząca rozmiaru procesorów takich jak 8088. Z perspektywy projektantów sprzętu,8088 jest całkowicie ośmiobitowym procesorem – ma tylko osiem lini danych a jedna magistrala danych jest kompatybilna z pamięcią i urządzeniami I/O zaprojektowanymi pod kątem ośmiobitowych procesorów.Z drugiej strony,inżynierowie oprogramowania sprzeczają się,że 8088 jest 16 bitowym procesorem.Z ich perspektywy nie można rozpoznać między 8088 (z ośmiobitową magistralą danych), a 8086 (który ma 16 bitową magistralę danych).Istotnie,jedyna różnica jest w szybkości przy której te procesory działają;8086 z 16 bitową magisttralą jest szybszy.Ostaczne projektanci sprzętu wygrali.Pomimo faktu,że inżynierowie oprgramowania nie mogą rozróżnić 8088 i 8086 w swoich programach,nazywamy 8088 ośmiobitowym procesorem a 8086 16 bitowym procesorem.Podobnie, 80386SX (który ma 16 bitową magistralę danych) jest procesorem 16 bitowym,podczas gdy 80386DX (który ma pełną 32 bitową magistralę danych) jest 32 bitowym procesorem. Dlatego też ośmiobitowa magistrala w 8088 może przesyłać tylko połowę informacji na jednostkę czasu (cykl pamięci) podobnie jak 16 bitowa magistrala danych w 8086.Zatem procesory z magistralą 16 bitową są naturalnie szybsze niż procesory ośmiobitowe .Podobnie procesory 32 bitowe są szybsze niż te z 16- lub ośmiobitową magistralą danych.Rozmiar magistrali danych wpływa na wydajność systemu bardziej niż jakakolwiek inna magistrala. Często słyszymy o procesorach nazywanych procesorami ośmio- ,16 ,32 lub 64 bitowymi.Podczas gdy są umiarkowane kontrowersje dotyczące rozmiaru procesora, większość ludzi zgadza się że liczba linii danych w procesorze określa jego rozmiar.Ponieważ w rodzinie 80x86 magistrale danych są szerokie na oiem,16,32 lub 64 bity większość danych również jest ośmio,16,32 lub 64 bitowych.Chociaż jest możliwe przetwarzanie danych 12 bitowych w 8088,wielu programistów przetwarza 16 bitów ponieważ procesor i tak i tak pobiera i manipuluje 16 bitami.Jest tak ponieważ procesor zawsze pobiera osiem bitów.Pobranie12 bitów wymaga dwóch ośmiobitowych operacji na pamięci.Ponieważ procesor pobiera 16 bitów zamiast 12,większość programistów używa wszystkich 16 bitów.Ogólnie rzecz biorąc,manipulowanie danymi o długości 8,16,32 lub 64 bitów jest najbardziej wydajne. Chociaż 16,32 lub 64 bitowe członkowie rodziny 80x86 mogą przetwarzać dane do szerokości magistrali mogą jednak uzyskać dostęp do jednostek pamięci mniejszych niż osiem,16 lub 32 bity.Dlatego też, cokolwiek zrobisz na małej magistrali danych,będzie zrobione równie dobrze na większej magistrali danych.;jednak można uzyskać dostęp do pamięci szybciej i można uzyskać dostęp do większych kawałków danych w jednej operacji na pamięci.Możesz przeczytać dokładnie o tym dostępie do pamięci trochę później (zobacz „Podsystem Pamięci”).

Tablica 17: Rozmiar magistrali danych procesorów 80x86 3.1.1.2 MAGISTRALA ADRESOWA Magistrala danych w rodzinie procesorów 80x86 przesyła informacje pomiędzy kolejnymi komórkami pamięci lub urządzeniami I/O a CPU.Tylko pozostaje pytanie :”Które komórki pamięci lub urządzenia I/O?” Magistrala adresowa odpowiada na to pytanie.W celu rozróżnienia komórki pamięci i urządzeń I/O projektant systemu przydziela unikalne adresy pamięci dla każdego elementu pamięci i urządzenia I/O.Kiedy program chce uzyskać dostęp do jakiejś szczególnej komórki pamięci lub urządzenia I/O umieszcza odpowiedni adres na magistrali adresowej.Zespół układów skojarzonych z pamięcią lub urządzeniem I/O rozpoznaje ten adres i wydaje polecenie pamięci lub urządzeniu I/O odczytania danych lub umieszczenie danych na magistrali danych.W obu przypadkach,wszystkie inne komórki pamięci ignorują to wywołanie.Tylko urządzenie,którego adres pasuje do wartości na magistrali adresowej,odpowiada. Z pojedynczą linią adresową,procesor mógł stworzyć dokładnie dwa unikalne adresy:zero i jeden.Z n liniami adresowymi,procesor może stworzyć 2n unikalnych adresów (ponieważ jest 2n unikalnych wartości w nbitowej liczbie binarnej).Dlatego też liczba bitów na magistrali adresowej ustala maksymalną liczbę adresowalnej pamięci i położenia I/O. 8088 i 8086,na przykład, mają 20 bitowe magistrale adresowe.Zatem,mogą one uzyskać dostęp góra do 1,048,576 (lub 220) komórek pamięci.Większa magistrala adresowa może uzyskać dostęp do większej liczby komórek pamięci.8088 i 8086,na przykład,cierpią z powodu małej przestrzeni adresowej - ich magistrala adresowa jest zbyt mała.Późniejsze procesory mają większe magistrale adresowe:

Tablica 18: Rozmiary magistrali adresowych rodziny 80x86 Przyszłe procesory 80x86 prawdopodobnie,będą wsparte 48 bitową magistralą adresową.Nadchodzi czas kiedy większość programistów będzie uważać cztery gigabajty pamięci za zbyt małą.,chociaż dzisiaj uważają jeden megabajt za niewystarczający. Na szczęście architektura 80386,80486 i późniejsze chipy uwzględniają łatwość rozszerzenia do 48 bitowej magistrali adresowej przez segmentację. 3.1.1.3 MAGISTRALA STERUJĄCA Magistrala sterująca jest zbiorem sygnałów które sprawdzają jak procesor komunikuje się z resztą systemu.Rozważmy na chwilę magistralę danych. CPU wysyła dane do pamięci i przyjmuje dane z pamięci na magistralę danych.Tu rodzi się pytanie,”Czy on wysyła czy przyjmuje?” Są dwie linie na magistrali sterującej,odczyt i zapis,które wyszczególniają kierunek przepływu danych.inne sygnały zwierają system taktowania,linie przerwań,linie stanu itd.Magistrale sterujące zmieniają się wraz z procesorami rodziny 80x86.Jednak niektóre linie sterujące są powszechne we wszystkich procesorach i są warte krótkiej wzmianki. Linie sterujące „odczyt’ i „zapis” sterują kierunkiem danych na magistrali danych.Kiedy oba mają logiczną jedynkę,CPU i pamięć -I/O nie mogą się skomunikować między sobą. Jeśli linia odczytu jest w stanie niskim (logiczne zero) CPU jest w stanie odczytu danych z pamięci (to znaczy,system przesyła dane z pamięci do CPU) Jeśli linia zapisu jest w stanie niskim,system przenosi dane z CPU do pamięci. „Linie aktywujące bajt” są innym zbiorem ważnych linii sterujących.Te linie sterujące pozwalają 16,32 i 64 bitowym procesorom radzić sobie z mniejszymi kawałkami danych.Dodatkowe szczegóły pojawią się w następnej sekcji. Rodzina 80x86,w odróżnieniu od innych procesorów,dostarcza dwóch odrębnych przestrzeni adresowych: jedną dla pamięci i jedną dla I/O.Podczas gdy magistrale adresowe pamięci na różnych procesorach 80x86 różnią się rozmiarami,magistrala adresowa I/O na wszystkich CPU 80x86 ma szerokość 16 bitów.To pozwala procesorowi adresować do 65,536 różnych lokacji I/O.Większość urządzeń(takich jak klawiatura,drukarka,dyski,itp.) wymagają więcej niż jedną lokację I/O. Pomimo to, 65,536 lokacji I/O jest wystarczające dla większości aplikacji. Oryginalna konstrukcja IBM PC pozwala tylko na używanie 1,024 z nich. Chociaż rodzina 80x86 utrzymuje dwie przestrzenie adresowe,nie ma dwóch magistral adresowych (dla I/O i pamięci). Zamiast tego,system dzieli magistralę adresową na I/O i pamięć.Dodatkowe linie sterujące decydują czy adres jest przeznaczony dla pamięci czy I/O..Kiedy takie sygnały są aktywne, urządzenia I/O używają adresów z najmniej znaczących 16 bitów magistrali adresowej.Kiedy są nieaktywne,urządzenia I/O ignorują sygnały na magistrali adresowej (przejmuje je podsystem pamięci). 3.1.2 PODSYSTEM PAMIĘCI

Typowy procesor 80x86 adresuje maksymalnie 2n różnych komórek pamięci, gdzie n jest liczbą bitów na magistrali adresowej już widzieliśmy procesory 80x86 mają 20,24 i 32 bitową magistralę adresową (z 48 bitową „w drodze”). Oczywiście pierwszym pytaniem jakie możemy zadać jest, „Czym dokładnie jest (lokacja) komórka pamięci?”.80x86 wspiera „pamięć adresowalną bajtem”.Zatem,podstawową jednostką pamięci jest bajt.Więc z 20,24 i 32 liniami adresowymi procesor 80x86 może zaadresować ,odpowiednio,jeden megabajt,16 megabajtów i cztery gigabajty pamięci. Myślimy o pamięci jako o liniowej tablicy bajtów.Adres pierwszego bajtu to zero a adres ostatniego bajtu to 2n-1.Dla 8088 z 20 bitową magistralą adresową, następująca pseudo-Pascalowa deklaracja tablicy jest dobrym przybliżeniem pamięci: Memory:array[0..1048575] of byte; Wykonując odpowiednik Pascalowego wyrażenia”Memory[125]:=0” CPU umiejscawia wartość zero na magistrali danych, adres 125 na magistrali adresowej,i inicjuje linię zapisu (ponieważ CPU zapisuje daną do pamięci,zobacz rysunek 3.2) Wykonując odpowiednik wyrażenia „CPU:=Memory[125};” CPU umiejscawia adres 125 na magistrali adresowej,inicjuje linię odczytu (ponieważ CPU odczytuje dane z pamięci) a potem odczytuje dane wynikowe z magistrali danych (zobacz rysunek 3.3) Powyższe rozważania mają zastosowanie tylko kiedy uzyskujemy dostęp do pojedynczego bajtu w pamięci.Więc co się wydarzy kiedy procesor uzyska dostęp do słowa lub podwójnego słowa? Ponieważ pamięć składa się tablicy bajtów,jak możemy sobie poradzić z wartościami dłuższymi niż osiem bitów? Różne systemy komputerowe mają różne rozwiązania tego problemu.Rodzina 80x86 radzi sobie z tym problemem przez przechowanie mniej znaczącego bajtu słowa pod wyspecyfikowanym adresem a najbardziej znaczący bajt w następnej komórce.Dlatego też,słowo pochłania dwa kolejne adresy pamięci

Rysunek 3.2: Operacja zapisu do pamięci

Rysunek 3.3 : Operacja odczytu z pamięci (jak można się było spodziewać,ponieważ słowo składa się z dwóch bajtów). Podobnie podwójne słowo zużywa cztery kolejne komórki pamięci.Adres podwójnego słowa jest adresem jego najmniej znaczącego bajtu.Pozostałe trzy bajty następujące po najmniej znaczącym bajcie aż do najbardziej znaczącego, pojawiają się przy adresie podwójnego słowa „ plus trzy” (zobacz rysunek 3.4).Bajty,słowa i podwójne słowa mogą zaczynać się od każdego poprawnego adresu w pamięci.Wkrótce zobaczymy,jednak,że rozpoczynanie dużych obiektów od dowolnych adresów nie jest dobrym pomysłem.

Zauważ,że jest całkiem możłiwe nakładanie się na siebie wartości bajtu,słowa czy podwójnego słowa.Na przykład,na rysunku 3.4 możemy mieć słowo zaczynające się przy adresie 193,bajt przy adresie 194 i podwójne słowo zaczynające się przy adresie 192.Te wartości wszystkie nakładają się na siebie. Mikroprocesor 8088 i 80188 mają ośmiobitową magistralę danych.To znaczy,że CPU może przesyłać osiem bitów na raz.Ponieważ każdy adres pamięci odpowiada ośmiobitowemu bajtowi,oznacza to wiele dogodnych ustawień (z perspektywy sprzętowej).,zobacz rysunek 3.6. Termin „tablica pamięci adresowana bajtem” ,oznacza ,że CPU może adresować pamięć w kawałkach tak małych jak pojedynczy bajt.Znaczy to również,że jest to najmniejsza jednostka pamięci,do której możemy uzyskać dostęp od razu przez procesor.To znaczy,jeśli procesor chce uzyskać dostęp do wartości czterech bitów ,musi odczytać osiem bitów i potem zignorować pozostałe cztery bity.Również uświadomić sobie trzeba,że bajt adresujący nie sugeruje ,że CPU może uzyskać dostęp do ośmiu bitów dla każdego przypadkowego bitu granicznego.Kiedy wyspecyfikujemy adres 125 w pamięci, otrzymamy całe osiem bitów z tego adresu,ni mniej ni więcej.Adresy są całkowite ;nie możemy, na przykład, wyspecyfikować adresu 125,5 dla przeniesienia mniej niż ośmiu bitów. 8088 i 80188 mogą manipulować wartościami słowa i podwójnego słowa,nawet z ich ośmiobitową magistralą danych.Jednak,to wymaga wielokrotnych operacji na pamięci,ponieważ te procesory mogą tylko przenosić osiem bitów danych jednorazowo.Ładowanie słowa wymaga dwóch działań na pamięci;ładowanie podwójnego słowa wymaga czterech działań na pamięci.

Rysunek 3.4: Bajt,słowo i podwójne słowo przechowywane w pamięci

Rysunek 3.5: Wzajemne oddziaływanie ośmiobitowy CPU - Pamięć

Procesory 8086,80186,80286 i 80386SX mają szesnastobitowe magistrale danych.Pozwala to tym procesorom uzyskiwać dostęp do dwa razy takiej ilości pamięci w takiej samej ilości czasu niż ich ośmiobitowi bracia. Te procesory organizują pamięć w dwa banki:”parzysty” bank i „nieparzysty” bank (zobacz rysunek 3.6).Rysunek 3.7 ilustruje połączenie do CPU (D0-D7 oznacza mniej znaczący bajt z magistrali danych,D8-D15 oznacza bardziej znaczący bajt magistrali danych): 16 bitowi członkowie rodziny 80x86 mogą ładować słowo z każdego dowolnego adresu.Jak wspominaliśmy wcześniej,procesor pobiera mniej znaczący bajt wartości spod wyspecyfikowanego adresu a bardziej znaczący bajt z następnego, kolejnego adresu. To stwarza subtelny problem,jeśli spojrzysz dokładnie na diagram powyżej.Co się stanie,kiedy uzyskujesz dostęp do słowa spod nieparzystego adresu?Przypuśćmy,że chcesz odczytać słowo z komórki 125.Okay,najmniej znaczący bajt słowa przychodzi z komórki 125 a bardziej znaczący bajt pochodzi z komórki 126..W czym rzecz?Okazuje się,że są dwa problemy z tym związane.

Rysunek 3.6:Adresy bajtów w pamięci słowa

Rysunek 3.7: Organizacja pamięci 16 bitowego procesora (8086,80186,80286,80386SX) Po pierwsze, spójrz na rysunek 3.7. Linie 8-15 (bardziej znaczący bajt) magistrali danych są połączone z bankiem nieparzystym, a linie 0 -7 magistrali danych (mniej znaczący bajt) połączone są z bankiem „parzystym”.Uzyskujemy dostęp do komórki pamięci 125 przesyłając dane do CPU na bardziej znaczący bajt magistrali danych.; ale my chcemy te dane na mniej znaczącym bajcie! Na szczęście , CPU 80x86 rozpoznają tą sytuację i automatycznie przesyłają dane z D8-D15 do mniej znaczącego bajta.

Drugi problem jest nawet bardziej niejasny .Kiedy uzyskujemy dostęp do słów,w rzeczywistości uzyskujemy dostęp do dwóch oddzielnych bajtów,z których każdy ma swój własny adres bajtowy.Więc powstaje pytanie „Jaki adres pojawia się na magistrali danych?” 16 bitowe CPU 80x86 zawsze umiejscawiają parzyste adresy na magistrali.Parzyste bajty zawsze pojawiają się na liniach danych D0-D7 a bajty nieparzyste zawsze pojawiają się na liniach danych D8-D15. Jeśli chcemy uzyskać dostęp do słowa przy adresie parzystym,CPU możemy pobrać całkowicie 16 bitowy kawałek ,w jednym działaniu na pamięci.Podobnie jeśli chcemy uzyskać dostęp do pojedynczego bajtu,CPU uruchamia odpowiedni bank (używając „aktywującego bajtu” linii sterującej) Jeśli bajt pojawia się pod nieparzystym adresem,CPU automatycznie przeniesie go z bardziej znaczącego bajtu na magistrali do mniej znaczącego bajtu. Więc co się zdarzy kiedy CPU uzyska dostęp do słowa przy nieparzystym adresie,jak w przykładzie podanym wcześniej?Cóż, CPU nie może umieścić adresu 125 na magistrali adresowej i odczytać 16 bitów z pamięci.Nie ma nieparzystych adresów wychodzących z 16 bitowego CPU 80x86.Adresy są zawsze parzyste.Więc jeśli próbujesz położyć 125 na magistralę adresową,wtedy położysz 124 na magistralę adresową.Przy odczytywaniu 16 bitów z tego adresu,otrzymasz słowo od adresu 124 (mniej znaczący bajt) i 125 (bardziej znaczący bajt) - nie to czego oczekiwałeś.Uzyskanie dostępu do słowa przy adresie nieparzystym wymaga dwóch działań na pamięci. Najpierw,CPU musi odczytać bajt z pod adresu 125,potem musi odczytać bajt spod adresu 126.Ostatecznie trzeba pozamieniać pozycjami te bajty,ponieważ oba są wprowadzone na złych połówkach magistrali danych

Rysunek 3.8:Organizacja pamięci 32 bitowego procesora (80386,80486,Pentium Overdrive)

Rysunek 3.9: Uzyskanie dostępu do słowa przy (adres mod 4) = 3

Na szczęście, 16 bitowy CPU 80x86 ukrywa takie szczegóły przed nami.Nasze programy mogą uzyskiwać dostęp do słowa przy każdym adresie a CPU stosownie uzyska dostęp i wymieni (jeśli będzie konieczne) dane w pamięci.Jednakże,uzyskanie dostępu do słowa przy adresie nieparzystym wymaga dwóch operacji na pamięci (podobnie jak 8088/80188).Dlatego,uzyskanie dostępu do słów przy adresach nieparzystych na 16 bitowych procesorach jest wolniejsze niż przy adresach parzystych.Bądź ostrożny ustalając jak używasz pamięci możesz poprawić szybkość swojego programu. Uzyskanie dostępu do 32 bitowej wartości zawsze zabiera,co najmniej,dwie operacje na pamięci na 16 bitowym procesorze..Jeśli chcesz uzyskać dostęp do wielkości 32 bitowej od adresu niepatrzystego,procesor będzie potrzebował trzech operacji na pamięci dla dostępu do danych. 32 bitowe procesory (80386,80486 i Pentium Overdrive) używają czterech banków pamięci połączonych do 32 bitowej magistrali danych (zobacz rysunek 3.8).Adres umiejscowiony na magistrali adresowej jest zawsze mnożony przez cztery.Używając kilku linii „bajtu aktywującego” CPU może wybierać,do którego z czterech bajtów tego adresu,program chce uzyskać dostęp.Podobnie jak przy procesorze 16 bitowym,CPU automatycznie przestawia bajty jeśli jest to konieczne. Przy 32 bitowej pamięci,CPU może uzyskać dostęp do każdego bajtu przy jednej operacji na pamięci.Jeśli (Adres MOD 4) nie uzyskamy trzy,wtedy 32 bitowy CPU może uzyskać dostęp do słowa przy tym adresie używając pojedynczej operacji na pamięci.Jednak, jeśli reszta wynosi trzy, wtedy zabierze to dwie operacje na pamięci w uzyskaniu dostępu do słowa (zobacz rysunek 3.9) .Jest to ten sam problem z jakim zetknęliśmy się przy procesorach 16 bitowych, z tym,że zdarza się on o połowę częściej. 32 bitowy CPU może uzyskać dostęp do podwójnego słowa w pojedynczej operacji na pamięci jeśli adres tej wartości jest równo dzielony przez cztery.Jeśli nie,CPU wymaga dwóch operacji na pamięci. I znowu,CPU radzi sobie ze wszystkim automatycznie.Przy ładowaniu prawidłowych danych CPU radzi sobie ze wszystkim za ciebie.Jako generalna zasadę zawsze umiejscawiaj wartości słowa pod parzystymi adresami a wartości podwójnego słowa pod adresami które są zawsze podzielne przez cztery.To przyspieszy działanie twoich programów. 3.1.3 PODSYSTEM I/O Poza 20,24 i 32 liniami adresowymi,dzięki którym uzyskujemy dostęp do pamięci,rodzina 80x86 posiada 16 bitową magistralę adresową I/O.Daje to CPU 80x86 dwie oddzielne przestrzenie adresowe,jedną dla pamięci i jedną dla operacji I/O.Linie na magistrali sterującej rozróżniają pomiędzy adresami pamięci a I/O.Za wyjątkiem oddzielnych linii sterujących i mniejszej magistrali,adresowanie I/O następuje dokładnie tak jak adresowanie pamięci.Pamięć i urządzenia I/O obie dzielą tą samą magistralę danych i mniej znaczące 16 linii na magistrali adresowej Są trzy ograniczenia dotyczące podsystemu I/O na IBM PC: po pierwsze, CPU 80x86 wymagają specjalnych instrukcji dla uzyskania dostępu do urządzeń I/O; po drugie, projektanci z IBM PC użyli „najlepszych” lokacji I/O dla swoich własnych cełów,zmuszając innych do używania mniej osiągalnych lokacji;po trzecie, system 80x86 może adresować nie więcej niż 65,536 (216) adresów I/O. Kiedy popatrzymy, że typowa karta graficzna VGA wymaga ponad 128 000 różnych loakacji,widać,że będziemy mieli problem z rozmiarem magistrali I/O. Na szczęście,projektanci sprzętu mogą odwzorowywać swoje urządzenia I/O wewnątrz przestrzeni adresowej pamięci,tak łatwo jak w przestrzeni adresowej I/O.Tak więc przez użycie odpowiednich zespołów układów,mogą uczynić urządzenia I/O wyglądające tak jak pamięć. Dostęp do urządzeń I/O jest tematem,który powróci w późniejszych rozdziałach.Na razie założymy,że dostęp do I/O i pamięci następuje w ten sam sposób. 3.2 SYSTEM SYNCHRONIZACJI Chociaż nowoczesne komputery są całkiem szybkie i stają się szybsze, cały czas,wymagają jeszcze skończonej ilości czasu do osiągnięcia nawet najmniejszego zadania.Na maszynach Von Neumanna,takich jak 80x86,większość operacji jest szeregowanych.To znaczy,że komputer wykonuje polecenia w określonym porządku.. Nie wykona ,na przykład,wyrażenia I:=I*5+2 przed I:=J; w następującej sekwencji: I:=J; I:=I*5+2; Najwyraźniej potrzebujemy sposobu do sterowania które wyrażenie wykonać pierwsze a które drugie . Oczywiście,w rzeczywistym systemie komputerowym, operacje nie występują natychmiastowo. Przesunięcie kopii J do I zabiera określoną ilość czasu. Podobnie mnożenie I przez 5 a potem dodanie dwa i

zachowanie wyniku w I zabiera czas.Jak można się było spodziewać,drugie wyrażenie Pascalowskie zabiera wykonuje się troszkę dłużej niż pierwsze.Dla zainteresowanych pisaniem szybkich programów,naturalnym pytanie jest „Jak procesor wykonuje wyrażenia, i jak mierzymy,jak długo się one wykonują?” CPU jest bardzo złożonym elementem zespołu układów.Bez zagłębienia się w zbyt wiele szczegółów,możemy powiedzieć,że operacje wewnątrz CPU muszą być bardzo ostrożnie koordynowane ,lub CPU będzie tworzył błędne rezultaty.W celu zapewnienia,że wszystkie operacje odbędą się we właściwym momencie,CPU 80x86 używa alternatywnych sygnałów nazywanych systemem zegarowym. 3.2.1 ZEGAR SYSTEMOWY Przy większości podstawowych poziomów zegar systemowy obsługuje całą synchronizację wewnątrz systemu komputerowego.Zegar systemowy ,jest to sygnał elektryczny na magistrali sterującej,który naprzemiennie przechodzi od zero do jeden,w okresowym tempie (zobacz rysunek 3.10). CPU jest dobrym przykładem złożonego synchronicznego systemu logicznego (zobacz poprzedni rozdział).Zegar systemowy zawiera dużo logicznych bramek które przygotowują CPU pozwalając mu działać w sposób zsynchronizowany.

Rysunek 3.10: Zegar systemowy Częstotliwość z jaką zegar systemowy przechodzi od zera do jeden,jest to „częstotliwość zegara systemowego”. Czas jaki jest potrzebny zegarowi systemowemu do przełączenia między zero i jeden i ponownie do zera nazywa się „taktem zegarowym”.Jeden pełny okres nazywany jest również „cyklem zegarowym”. W większości nowoczesnych systemów komputerowych,zegar systemowy przełącza między zero i jeden w tempie przekraczającym kilka milionów razy na sekundę.Częstotliwość zegara jest po prostu liczbą cykli zegarowych które wykonują się w ciągu sekundy.Typowy chip 80486 pracuje z szybkością 66 milionów cykli na sekundę.”Herc” (Hz) jest technicznym terminem oznaczającym jeden cykl na sekundę.Dlatego też,wyżej wymieniony chip 80486 pracuje przy 66 milionach herców,lub inaczej 66 Megahercach (MHz).Typowa częstotliwość dla części 80x86 to zakres od 5 MHz do 200 MHz i wyższa.Zauważ,że jeden takt zegarowy (ilość czasu dla jednego kompletnego cyklu zegarowego) jest wartością odwrotną do częstotliwości zegara.Na przykład, zegar 1MHz będzie miał takt zegarowy jedną mikrosekundę (1/1,000,000 sekundy).Podobnie zegar 10 MHz,ma takt zegarowy 100 nanosekund (100 miliardów na sekundę).CPU pracujący przy 50 MHZ ma takt zegarowy 20 nanosekund.Zauważ,że zazwyczaj wyrażamy takt zegarowy w milionach lub miliardach na sekundę. Dla zapewnienia synchronizacji większość CPU zaczyna operacje albo przy zboczu opadającym (kiedy zegar opada z jeden na zero) albo przy zboczu wznoszącym (kiedy zegar rośnie od zera do jeden) Zegar systemowy poświęca większość swojego czasu albo zeru albo jedynce a bardzo mało czasu przełączając między nimi dwoma.Dlatego też zbocze zegarowe jest doskonałym punktem synchronizującym. Ponieważ wszystkie operacje CPU są synchronizowane poprzez zegar,CPU nie może wykonywać zadań szybciej niż zegar.Jednak,ponieważ CPU pracuje przy jakiejś częstotliwości zegara,nie znaczy to,że jest wykonywane tak dużo operacji w każdej sekundzie.Wiele operacji używa wielokrotności taktu zegarowego dla zakończenia więc CPU często wykonuje operacje przy znacznie niższym tempie. 3.2.2 DOSTĘP DO PAMIĘCI A ZEGAR SYSTEMOWY Dostęp do pamięci jest prawdopodobnie najpowszechniejszym zajęciem CPU.Dostęp do pamięci jest zdecydowanie operacją synchronizowaną przez zegar systemowy.To znaczy,odczytywanie wartości z pamięci lub zapisywanie wartości zdarza się nie częściej niż raz na każdy cykl zegarowy.Istotnie,na wielu procesorach 80x86,jest potrzebnych kilka cykli zegarowych przy dostępie do komórki pamięci.”Czas dostępu do pamięci” jest liczbą cykli zegarowych,których system wymaga przy dostępie do komórki pamięci;jest to ważna wartość ponieważ dłuższy czas dostępu do pamięci prowadzi do niższej wydajności.

Różne procesory 80x86 mają różne czasy dostępu do pamięci,w zakresie od jednego do czterech cykli zegarowych.Na przykład,8088 i 8086 wymagają czterech cykli zegarowych przy dostępie do pamięci;80486 wymaga tylko jednego.Zatem,80486 będzie wykonywał programy z dostępem do pamięci szybciej niż 8086,nawet kiedy pracują przy tej samej częstotliwości zegara.

Rysunek 3.11: Cykl odczytu z pamięci dla 80486

Rysunek 3.12: Cykl zapisu do pamięci dla 80486 Czas dostępu do pamięci jest to różnica czasu między operacją na pamięci (odczyt lub zapis) a czasem zakończenia tej operacji.Na 5 MHz CPU 8088/8086 czas dostępu do pamięci wynosi mniej więcej 800 nanosekund.Na 50 MHz 80486,ten czas jest mniejszy niż 20 ns.Zauważ,że czas dostępu do pamięci dla 80486 jest 40 razy szybszy niż dla 8088/8086.Jest tak ponieważ częstotliwość zegara 80486 jest 10 krotnie większa i wykorzystuje on jedną czwartą cyklu zegarowego przy dostępie do pamięci. Kiedy odczytujemy z pamięci,czas dostępu do pamięci jest to ilość czasu od punktu w którym CPU umieszcza adres na magistrali adresowej a CPU zbiera dane z magistrali danych.Na CPU 80486 z jednym cyklem czasu dostępu do pamięci,odczyt wygląda podobnie jak przedstawiony na rysunku 3.11.Zapisywanie danych do pamięci jest podobne (zobacz rysunek 3.11). Zauważ,że CPU nie czeka na pamięć.Czas dostępu jest wyspecyfikowany przez częstotliwość zegara.Jeśli podsystem pamięci nie pracuje dostatecznie szybko,CPU będzie odczytywał dane dziwne w czasie operacji odczytu z pamięci i nie będzie odpowiednio przechowywał danych dla operacji zapisu do pamięci.Będzie to z pewnością przyczyna nieprawidłowej pracy systemu. Pamięć ma różne parametry ale dwoma najważniejszymi są pojemność i szybkość (czas dostępu). Typowy dynamiczny RAM (pamięć o dostępie swobodnym) ma pojemność od czterech (lub więcej) megabajtów i szybkość 50-100 ns.Można kupić większe i szybsze urządzenia,ale są zbyt drogie.Typowy system 33 MHz 80486 używa pamięci 70 ns . Poczekaj chwilę!Przy 33 MHz takt zegarowy wynosi mniej więcej 33 ns. Jak twórca komputera może wykorzystać 70ns pamieci?Odpowiedzią jest stan oczekiwania (WAIT)

Rysunek 3.13:Dekodowanie i buforowanie opóźnień 3.2.3 STAN OCZEKIWANIA Stan oczekiwania jest niczym innym jak dodatkowym cyklem zegarowym dający jakiemuś urządzeniu czas na zakończenie operacji. Na przykład, 50 Megahercowy 80486 ma takt zegarowy 20 ns.To sugeruje,że potrzebujemy pamięci 20 ns .W rzeczywistości sytuacja jest gorsza.W większości systemów komputerowych jest dodatkowy zespół układów między CPU a pamięcią: dekodowania i buforowania logicznego. Te dodatkowe układy wprowadzają dodatkowe opóźnienie do systemu. (zobacz rysunek 3.13).Na tym diagramie system traci 10 ns na dekodowanie i buforowanie.Więc jeśli CPU potrzebuje danych w 20 ns,pamięć musi odpowiedzieć w mniej niż 10 ns. Możemy właściwie kupić pamięć 10 ns.Jednak jest to bardzo kosztowne,nieporęczne,pochłaniające dużo mocy i generujące dużo ciepła. To są złe cechy.Superkomputery używają pamięci tego typu.Jednak superkomputery kosztują miliony doalrów,zajmują całe pokoje,wymagają specjalnego chłodzenia i mają gigantyczne zasilanie.Żadna z tych rzeczy raczej nie zmieści się na Twoim biurku. Jeśli kosztowna pamięć nie chce pracować z szybkim procesorem,jak poradzić sobie bez kupowania szybszego PC? Jedną z odpowiedzi jest stan oczekiwania.Na przykład,jeśli mamy procesor 20 MHz z czasem cyklu pamięci 50 ns i tracimy 10 ns na buforowanie i dekodowanie,będziemy potrzebować pamięci 40 ns.A co jeśli możemy pozwolić sobie na pamięć 80ns w 20 MHZ systemie? Dodatkowy stan oczekiwania rozszerza cykl pamięci do 100 ns (dwa cykle zegarowe) co rozwiąże ten problem.Odjęcie 10 ns na dekodowanie i buforowanie pozostawi 90 ns.Zatem,pamięć 80 ns odpowie zanim CPU zażyczy sobie danych. Prawie każdy istniejący CPU dostarcza sygnał na magistralę sterującą pozwalając na wprowadzenie stanu oczekwiania.Generalnie,zestaw układów dekodujących zapewnia opóźnienie tej linii o jeden dodatkowy takt zegarowy,jeśli to konieczne.Daje to pamięci wystarczający czas dostępu a system pracuje właściwie.(zobacz rysunek 3.14). Czasami pojedynczy stan oczekiwania jest niewystarczający.Rozpatrzmy 80486 pracujący przy 50 MHz.Normalnie czas cyklu pamięci jest mniejszy niż 20 ns.Dlatego też,mniej niż 10 ns jest dostępnych po odjęciu dekodowania i buforowania.Jeśli używamy pamięci 60 ns w systemie,dodanie pojedynczego stanu oczekiwania nie zrobi tej sztuczki. Każdy stan oczekiwania daje nam 20 ns,więc pojedynczy stan oczekiwania potrzebowałby pamięci 30 ns.Pracując z pamięcią 60 ns będziemy musieli dodać trzy stany oczekiwania (zerowy stan oczekiwania = 10 ns, jeden stan oczekiwania = 30 ns,dwa stany oczekiwania =50 ns,trzy stany oczekwiania=70 ns). Rzecz jasna ,z punktu widzenia wydajności systemu,stany oczekiwania nie są dobrą rzeczą.Podczas gdy CPU czeka na dane z pamięci,nie może operować na danych.

Rysunek 3.14 : Wprowadzanie stanu oczekiwania do operacji odczytu z pamięci Dodanie pojedynczego stanu oczekiwania do cyklu pamięci w CPU 80486 podwaja ilość czasu wymaganą dla uzyskania dostępu do danych.To z kolei, zmniejsza szybkość dostępu do pamięci.Wykonywanie ze stanem oczekiwania przy każdym dostępie do pamięci jest prawie jak przecięcie częstotliwości zegara procesora na połowę.Możemy wtedy wykonać dużo mniej pracy w tej samej ilości czasu. Prawdopodobnie widziałeś coś takiego :80386DX,33MHz,8megabajtów 0 stanów oczekiwania RAM ...... tylko $1000!”Jeśli przyjrzałeś się dokładnie tej specyfikacji,zauważyłeś,że producent używa pamięci 80 ns..Jak można zbudować system który pracuje przy 33 MHz i ma zero stanów oczekiwania? Prosto.Kłamiąc. Nie ma mowy,żeby 80386 mógł pracować przy 33MHz,wykonując dowolny program,bez wprowadzania stanu oczekiwania.To jest niemożliwe.Jednak możliwe jest zaprojektowanie podsystemu pamięci który pod pewnymi,specjalnymi warunkami dałby sobie radę przy działaniu bez stanu oczekiwania jakiś czas. Jednak,nie jesteśmy skazani na wolne wykonywanie przez dodanie stanu oczekiwania.Jest kilka sztuczek projektantów sprzętu dzięki którym możemy osiągać zerowy stan oczekiwania przez większość czasu.Najbardziej powszechną z nich jest użycie pamięci podręcznej cache. 3.2.4 PAMIĘĆ PODRĘCZNA CACHE Jeśli patrzymy na typowy program odkryjemy,że ma zwyczaj uzyskiwać dostęp do tej samej komórki pamięci wielokrotnie.Co więcej odkryjemy,że program często uzyskuje dostęp do sąsiednich komórek pamięci.Techniczne nazwy tegoż zjawiska to czasowa lokalność odniesienia i przestrzenna lokalność odniesienia.Jeśli wskazujemy lokalność przestrzenną,program uzyskuje dostęp do sąsiednich komórek pamięci.jeśli wskazujemy czasową lokalność odniesienia program wielokrotnie uzyskuje dostęp do tej samej komórki pamięci podczas krótkiego odcinku czasu.Obie formy lokalności występują w następującym fragmencie kodu pascalowskiego: For i:= 0 to 10 do A[i] := 0; Występują wewnątrz tej pętli zarówno przestrzenna i czasowa lokalność odniesienia W powyższym pascalowym kodzie,program odnosi się do zmiennej i kilka razy.Pętla for, przypisuje zmiennej i wartości od 0 do 10,do czasu aż pętla się skończy.Zwiększa również i o jeden po wykonaniu każdego przypisania.To wyrażenie używa również i jako indeksu tablicy.To pokazuje czasową lokalność odniesienia w akcji, ponieważ CPU uzyskuje dostęp do i w trzech punktach,w krótkim okresie czasu.Ten program pokazuje również przestrzenną lokalność odniesienia.Pętla wypełnia zerami elementy tablicy A poprzez zapis zera do pierwszego

elementu w tablicy A,potem do drugiego elementu i tak dalej .Zakładając, że Pascal przechowuje elementy z A w kolejnych komórkach pamięci,każda iteracja pętli uzyskuje dostęp do sąsiedniej komórki pamięci. Jest to dodatkowy przykład czasowej i przestrzennej lokalności odniesienia w powyższym przykładzie w Pascalu,chociaż nie jest on tak oczywisty.Instrukcje komputerowe które mówią systemowi co robić z wybranym zadaniem,również występują w pamięci.Te instrukcje pojawiają się sekwencyjnie w pamięci - część lokalności przestrzennej.Komputer również wykonuje te instrukcje wielokrotnie, raz dla każdej iteracji pętli - część lokalności czasowej. Jeśli popatrzymy na charakterystykę wykonania typowego programu,odkryjemy,że typowy program wykonuje mniej niż połowę wyrażeń.Generalnie,typowy program może używać tylko 10 - 20% przydzielonej mu pamięci.W danym czasie, program jednomegabajtowy może uzyskać dostęp od czterech do ośmiu kilobajtów danych i kodu.Więc jeśli płacisz oburzające sumy pieniędzy za drogie RAMy z zerowym stanem oczekiwania,nie będziesz mógł używać większości z nich.Czyż nie byłoby milej,jeśli mógłbyś kupić mniejszą ilość szybszego RAMu z dynamiczniejszym przydzielaniem adresów przy wykonywaniu programu? To jest właśnie to co pamięć podręczna cache robi dla nas.Pamięć cache „siedzi” między CPU i pamięcią główną.Jest to mała ilość bardzo szybkiej (zero stanu oczekiwania) pamięci.W odróżnieniu od normalnej pamięci,bajty pojawiające się w cache’u nie mają stałych adresów.Zamiast tego,pamięć cache może zmienić przydział adresom danych.Pozwala to systemowi zachować ostatnio udostępnioną wartość danej w cache’u.Adresy,do których CPU nie uzyskał dostępu w pozostają w głównej (wolnej) pamięci.Ponieważ większość dostępów do pamięci to ostatnie uzyskane dostępy do zmiennych (lub blisko położonej komórki ostatnio uzyskanego dostępu do innej komórki),dane generalnie pojawiają się w pamięci cache. Pamięć cache,nie jest doskonała.Chociaż program może spędzić znaczną ilość czasu na wykonywaniu kodu w jednym miejscu,ostatecznie może wezwać procedurę lub przejść do jakiejś sekcji kodu na zewnątrz pamięci cache.W takim przypadku CPU musi przejść do pamięci głównej aby pobrać dane.Ponieważ pamięć główna jest wolna będzie to wymagało wprowadzenia stanu oczekiwania. Trafienie z pamięci podręcznej występuje ,gdy CPU uzyskuje dostęp do pamięci i znajduje dane w cache’u.W takim przypadku CPU może zazwyczaj uzyskać dane z zerowym stanem oczekiwania.Brak trafienia z pamięci podręcznej,występuje jeśli CPU uzyskuje dostęp do pamięci a dane nie są przechowywane w pamięci podręcznej.Wtedy CPU może odczytać dane z pamięci głównej, ponosząc utratę wydajności.Wykorzystując korzyści z lokalności odniesienia,CPU kopiuje dane do pamięci podręcznej zawsze kiedy przystępuje do adresu nie zawartego w pamięci podręcznej cache.Ponieważ jest prawdopodobne,że system będzie chciał wkrótce uzyskać dostęp to tej samej lokacji,system zaoszczędzi na stanie oczekiwania poprzez posiadanie danych w pamięci podręcznej Jak opisano powyżej,pamięć podręczna posługuje się czasowymi aspektami dostępu do pamięci,ale nie aspektami przestrzennych.Buforowanie podręczne komórek pamięci kiedy uzyskujemy dostęp do nich nie przyspiesza programu jeśli mamy stały dostęp do kolejnych komórek.(przestrzenna lokalność odniesienia)Rozwiązanie tego problemu jest większe buforowanie systemu przy odczytywaniu kilku kolejnych bajtów z pamięci kiedy występuje brak trafienia z pamięci podręcznej.Na przykład 80486 odczytuje 16 bajtów przy strzale do spudłowanego ( nie trafionego ) cache’a..Jeśli odczytujemy 16 bajtów,dlaczego odczytujemy je w blokach zamiast tak jak potrzebujemy?Jak się okazuje,większość chipów pamięci, dostępnych dzisiaj,ma specjalny tryb który pozwala szybko uzyskać dostęp do kilku kolejnych komórek pamięci .Pamięć podręczna cache wykorzystuje tą zdolność do redukowania liczby stanów oczekiwania potrzebnych przy dostępie do pamięci. Jeśli piszemy program który losowo uzyskuje dostęp do pamięci,używajac pamięci podręcznej cache można w rzeczywistości go spowolnić.Odczytywanie 16 bajtów przy każdym braku trafienia w pamięci podręcznej cache jest kosztowne jeśli uzyskujemy dostęp tylko do kilku bajtów w odpowiedniej linii cache.

Rysunek 3.15: Dwupoziomowy system cache Nie powinno to być niespodzianką że stosunek trafień w pamięci podręcznej do braku trafień w pamięci podręcznej wzrasta wraz z rozmiarem (w bajtach) podsystemu pamięci cache.Chip 80486,na przykład, ma 8,192 zintegrowanej z układem pamięci podręcznej cache.Intel twierdzi,że uzyskuje współczynnik trafień 80-95% trafień na taką pamięcią podręczną (w znaczeniu 80-95% czasu w jakim CPU znajduje dane w pamięci podręcznej). Wydaje się to bardzo imponujące.Jednakże,jeśli pobawimy się troszkę liczbami odkryjemy,że nie jest to wszystko imponujące.Przypuśćmy,że wybraliśmy 80% cyfr.Wtedy, średnio ,jeden z każdych pięciu dostępów do pamięci,nie będzie w pamięci podręcznej.Jeśli mamy procesor 50 MHz i pamięć o czasie dostępu 90 ns cztery z pięciu dostępów do pamięci potrzebują tylko jednego cyklu zegarowego (ponieważ są w pamięci podręcznej cache) a piąty będzie potrzebował około 10 stanów oczekiwania.Generalnie system wymaga 15 cykli zegarowych przy dostępie do pięciu komórek pamięci,lub trzech cyklów zegarowych na dostęp.Jest to odpowiednik dwóch stanów oczekiwania dodanych do każdego dostępu do pamięci.Teraz już wierzysz,że Twoja maszyna, pracuje przy zerowym stanie oczekiwania? Jest parę sposobów na poprawienie tej sytuacji.Po pierwsze,możemy dodać więcej pamięci podręcznej.To poprawi współczynnik trafień w pamięci podręcznej,zredukuje liczbę stanów oczekiwania.Na przykład, zwiększenie współczynnika trafień z 80% do 90% pozwoli nam uzyskać dostęp do 10 komórek pamięci w ciągu 20 cykli.To zredukuje średnią liczbę stanów oczekiwania na dostęp do pamięci do jednego stanu oczekiwania - solidna poprawa. Niestety nie można wyciągnąć chipu 80486,rozebrać na części i przylutować więcej pamięci podręcznej na chipe.Jednak,CPU 80586/Pentium ma znacznie większą pamięć podręczną niż 80486 i operuje zmniejszą ilością stanów oczekiwania. Innym sposobem poprawienia wydajności jest zbudowanie dwupoziomowego systemu pamięci podręcznej. Wiele systemów 80486 pracuje w ten sposób.Pierwszym poziomem jest zintegrowana z układem 8,192 bajtowa pamięć podręczna.Następnym poziomem ,pomiędzy zintegrowaną pamięcią podręczną a pamięcią główną jest pomocnicza pamięć podręczna wbudowany na płycie głównej komputera.(zobacz rysunek 3.15). Typowa pomocnicza pamięć podręczna zawiera gdzieś od 32,786 do jednego megabajta pamięci.Powszechnymi rozmiarami na PC są 65,536 i 262,114 bajty pamięci podręcznej. Możemy zapytać ”Dlaczego zawracać sobie głowę dwupoziomowym cache’m.?Dlaczego nie użyć 262,144 bajtów pamięci podręcznej cache od razu” Cóż,pomocnicza pamięć podrzędna generalnie nie operuje przy zerowym stanie oczekiwania.Układ wspierający 262,144 bajty z 10 ns pamięcią (20 ns całkowitego czasu dostępu) byłby bardzo kosztowny.Większość projektantów systemu używa wolniejszych pamięci,które wymagają jednego lub dwóch stanów ozekiwania.To jest i tak dużo szybsze niż pamięć główna.W połączeniu ze zintegrowaną z układem pamięcią podręczną ,możemy uzyskać lepszą wydajność systemu. Rozważmy poprzedzi przykład z 80% wskaźnikiem trafień.Jeśli pomocnicza pamięć podręczna wymaga dwóch cykli dla każdego dostępu do pamięci i trzech cykli dla pierwszego dostępu ,wtedy brak trafień na zintegrowanej z układem pamięci podręcznej będzie wymagał całkowicie sześciu cykli zegarowych.Średnią wydajnością systemu będą dwa cykle na dostęp do pamięci.Trochę szybciej niż trzy wymagane przez system bez pomocniczej pamięci podręcznej.Co więcej,pomocnicza pamięć podręczna może uaktualniać swoje wartości równolegle z CPU.Więc liczba braku trafień w pamięci podręcznej (które wpływają na osiągi CPU) idzie w dół. Prawdopodobnie myślisz ”Dotychczas,to wszystko wydaje się interesujące,ale co to ma wspólnego programowania?”Całkiem sporo,w rzeczywistości.Pisząc swoje programy starannie,wykorzystując sposób z systemem pamięci podręcznej,możemy poprawić wydajność swojego programu.Przez alokację zmiennych których powszechnie używamy razem w tej samej linii cache,możemy wymusić na pamięci podręcznej załadowanie tych zmiennych jako grupy,oszczędzając extra stany oczekiwania na każdy dostęp. Jeśli organizujemy, nasz program tak,że będzie wykonywał tą samą sekwencję instrukcji wielokrotnie będzie miał wysoki stopień czasowej lokalności odniesienia i zarazem,szybsze wykonywanie. 3.3 „HIPOTETYCZNE” PROCESORY 886,8286,8486 I 8686 Po zrozumienie jak można poprawić wydajność systemu,czas zgłębić wewnętrzne operacje CPU.Niestety,procesory z rodziny 80x86 są złożonymi bestiami. Omówienie ich wewnętrznych operacji mogłoby być przyczyną większego zamieszania niż rozjaśnienia sprawy.Więc użyjemy procesorów 886,8286,8486 i 8686 (procesorów „x86”) Te „papierowe procesory” są ekstremalnym uproszczeniem różnych członków rodziny 80x86.Określają one ważne cechy architektury 80x86. Procesory 886,8286,8486 i 8686 są prawie identyczne z wyjątkiem sposobu wykonywania instrukcji.One mają ten sam zbiór rejestrów, i „wykonują” ten sam zbiór instrukcji.Zdanie to zawiera kilka nowych pomysłów;zaatakujmy je od razu.

3.3.1 REJESTRY CPU Rejestry CPU są to bardzo specjalne komórki pamięci zbudowane z przerzutników.Nie są one częścią pamięci głównej,CPU implementuje je jako zintegrowane z układem. Różni członkowie rodziny 80x86 mają różne rozmiary rejestrów,CPU 886,8286,8486 i 8686 maja dokładnie cztery rejestry,wszystkie o szerokości 16 bitów.Wszystkie operacje arytmetyczne i na komórkach zdarzają się w rejestrach. Ponieważ procesor x86 ma tylko kilka rejestrów,nadamy każdemu rejestrowi jego własną nazwę i będziemy się odnosić do nich poprzez nazwę zamiast przez adres.Nazwy dla rejestrów x86 to: AX - akumulator BX - rejestr bazowy CX - licznik DX - rejestr danych Poza tymi rejestrami wymienionymi u góry,które są widoczne dla programisty ,procesor x86 ma również rejestr wskaźnika rozkazów (IP),który zawiera adres następnej instrukcji do wykonania.Jest również rejestr znaczników (flag),który przechowuje wyniki porównań.Restr flag zapamiętuje czy jedna wartość była mniejsza niż,równa czy tez większa niż inna wartość. Ponieważ rejestry są zintegrowane z układem i obsługiwane specjalnie przez CPU,muszą być dużo szybsze niż pamięć.Dostęp do komórki pamięci wymaga jednego lub więcej cyklu zegarowego.Dostęp do danych w rejestrach zabiera zero cykli zegarowych.Dlatego możemy spróbować trzymać zmienne w rejestrach. Zbiór rejestrów jest bardzo mały a większość rejestrów ma specjalne przeznaczenie które ogranicza ich używanie jako zmiennych,ale są one doskonałym miejscem na przechowywanie danych tymczasowych.

Rysunek 3.16 Tablica Połączeń programowych 3.3.2 JENDOSTKA ARYTMETYCZNO -LOGICZNA Jednostka arytmetyczno-logiczna (ALU) występuje tam gdzie ma miejsce większość zdarzeń wewnątrz CPU.Na przykład,jeśli chcemy dodać wartość pięć do rejestru AX,CPU: Kopiuje wartość z AX do ALU Wysyła wartość pięć do ALU Informuje ALU by dodało razem te wartości Przenosi z powrotem wynik do rejestru AX 3.3.3 JEDNOSTKA SPRZĘGAJĄCA Z MAGISTRALĄ Jednostka sprzęgająca z magistralą (BIU) jest odpowiedzialna za sterowanie magistral adresowej i danych kiedy uzyskujemy dostęp do pamięci głównej.Jeśli jest obecna pamięć podręczna,wtedy BIU jest również odpowiedzialna za uzyskiwanie dostępu do danych w tej pamięci. 3.3.4 JEDNOSTKA STERUJĄCA I ZBIÓR INSTRUKCJI

W tym punkcie rodzi się pytanie:”Jak dokładnie CPU wykonuje przydzielone zadania?”Jest to znakomite pytanie.zważywszy,że CPU pracuje na stałym zbiorze poleceń lub instrukcji.Zapamiętajmy,że projektanci CPU,skonstruowali te procesory używając bramek logicznych do wykonywania tych instrukcji..Utrzymują liczbę bramek logicznych w rozsądnym małym zbiorze (dziesięć lub sto tysięcy).Projektanci CPU muszą z konieczności ograniczać liczbę i złożoność poleceń rozpoznawanych przez CPU.Ten mały zbiór poleceń to zbiór instrukcji CPU. Wczesne programy (przed Von Neumannem) były często „zaszyte” wewnątrz układów połączeń.To znaczy,komputerowe połączenia decydowały jaki problem komputer mógłby rozwiązać.Musiano wymieniać układ połączeń żeby zmienić program.Bardzo trudne zadanie.Następnym posunięciem w projektowaniu komputerów były programowalne systemy komputerowe które pozwalały programiście łatwo „wymieniać” kolejność gniazdek i podłączać przewody.Program komputerowy składał się ze zbioru rzędu dziur(gniazdek),gdzie każdy rząd przedstawiał jedną operację w czasie wykonywania programu.Programista mógł wybrać jedną z kilku instrukcji,poprzez wetknięcie przewodu do odpowiedniego gniazdka dla żądanej instrukcji. (zobacz rysunek 3.16).Oczywiście,główną trudnością na tym schemacie jest to,że liczba możliwych instrukcji jest poważnie ograniczona przez liczbę gniazdek ,jaką fizycznie możemy umieścić w jednym rzędzie.Jednak projektanci CPU szybko odkryli,że mała ilość dodatkowych układów logicznych,mogłaby zredukować liczbę gniazdek wymaganych z n-dziur dla n-instrukcji do log2(n) dziur dla n-instrukcji.Zrobili to poprzez przydzielenie kodu liczbowego do każdej instrukcji a potem kodowaniu tych instrukcji jako liczby

Rysunek 3.17: Kodowanie instrukcji

Rysunek 3.18:Kodowanie instrukcji polami źródła i przeznaczenia

binarnej używając log2(n) dziur (zobacz rysunek 3.17),Wymagało to dodatkowych ośmiu funkcji logicznych do dekodowania bitów A,BiC z tablicy połączeń, ale ten extra układ jest wart tego kosztu ponieważ redukuje liczbę gniazdek które muszą być powtarzane dla każdej instrukcji. Oczywiście wiele instrukcji CPU nie jest autonomicznych.Na przykład, instrukcja przesyłania jest poleceniem które przesyła dane z jednej lokacji w komputerze do innej (np. z jednego rejestru do innego).Dlatego instrukcja ta wymaga dwóch argumentów(operandów):argumentu źródłowego i argumentu przeznaczenia.Projektanci CPU zazwyczaj kodują te operandy źródłowy i przeznaczenia jako część instrukcji maszynowych,pewnych gniazdek odpowiadających operandowi źródłowemu i pewnych gniazdek odpowiadających operandowi przeznaczenia..Rysunek 3.17 pokazuje jedną z możliwych kombinacji gniazdek wyjaśniających to.Instrukcja move przesunęłaby dane z rejestru źródłowego do rejestru przeznaczenia,instrukacja add dodałaby wartość z rejestru źródłowego do rejestru przeznaczenia,itd. Jednym z podstawowych postępów w projektowaniu komputerów jakie wprowadziła VNA jest koncepcja programu w pamięci .Jeden wielki problem z metodą programowania tablicą połączeń jest to,że liczba kroków programu (instrukcji maszynowych) jest ograniczona przez liczbę rzędów gniazdek dostępnych w maszynie.John Von Neumann i inni rozpoznali związki między gniazdkami na tablicy połączeń a bitami w pamięci:doszli do wniosku,że można przechowywać binarne odpowiedniki programu maszynowego w pamięci głównej i przekazywać każdy program z pamięci,ładować go do specjalnego rejestru dekodującego który byłby podłączony bezpośrednio do układu dekodującego instrukcje w CPU.Nie było sztuką dodanie jeszcze jednego układu do CPU.Ten układ,jednostka sterująca (CU),przekazywał kody instrukcji(znane również jako kody operacji lub opcody) z pamięci i przesuwał je do rejestru dekodującego instrukcje. Jednostka sterująca zawiera specjalny rejestr,wskaźnik rozkazów(IP),który zawiera adres wykonywanej instrukcji.CU pobiera kod instrukcji z pamięci i umieszcza ją w rejestrze dekodującym do wykonania.Po wykonaniu instrukcji ,CU zwiększa wskaźnik rozkazów i pobiera następna instrukcję z pamięci do wykonania i tak dalej. Kiedy projektowano zbiór instrukcji,projektanci CPU,generalnie wybrali opcody,które są wielokrotnością długości ośmiu bitów, więc CPU może łatwo pobierać komplet instrukcji z pamięci. Zadaniem projektanta CPU jest przydzielić odpowiednią liczbę bitów do pola klasy instrukcji (move,add,subtract,itd.) i pola operandów. Wybranie większej ilości bitów dla pola instrukcji pozwala mieć więcej instrukcji,wybranie dodatkowych bitów dla pola operandów pozwala wybrać większą liczbę operandów(np. komórki pamięci lub rejestry) Jest to dodatkowa komplikacja. Niektóre instrukcje mają tylko jeden operand lub, nie mają żadnego operandu wcale. Zamiast marnować bity skojarzone z tymi polami,projektanci CPU często ponownie używają tych pól do kodowania dodatkowych opcodów. Rodzina CPU Intela 80x86 używa maksymalnie instrukcji z zakresu od jednego do dziesięciu bajtów długości.Ponieważ jest to trochę zbyt trudne abyśmy poradzili sobie na tak wczesnym stadium,CPU x86 będą używały różnych, prostszych schematów kodowania. 3.3.5 ZBIÓR INSTRUKCJI x86 CPU x86 dostarcza 20 podstawowych klas instrukcji.Siedem z tych instrukcji ma dwa operandy,osiem z tych instrukcji ma pojedynczy operand,a pięć instrukcji nie ma wcale operandów.Te instrukcje to mov (dwie formy),add,sub,cmp,and,or,not,je,jne,jb,jbe,ja,jae,jmp,brk,iret,halt,get i put.Poniższy paragraf opisuje jak każda znich pracuje. Instrukcja mov jest właściwie dwoma klasami instrukcjami połączonymi wewnątrz tej samej instrukcji.Dwie formy instrukcji mov mają następujący kształt: mov reg, reg/pamięć/stała mov pamięć/reg gdzie reg jest jednym z rejestrów ax,bx,cx lub dx:;stała jest stałą liczbową (używając notacji heksadecymalnej),a pamięć wyszczególnioną komórką pamięci.Następna sekcja opisuje możliwe formy operandu pamięci jakich można używać. Operand ”reg/pamięć/stała” mówi nam,że tym szczególnym operandem może być rejestr,komórka pamięci lub stała. Arytmetyczne i logiczne instrukcje przyjmują następujące formy: add sub cmp and

reg, reg, reg, reg,

reg/pamięć/stała reg/pamięć/stała reg/pamięć/stała reg/pamięć/stała

or not

reg, reg/pamięć/stała reg/pamięć

Instrukcja add dodaje wartość drugiego operandu do pierwszego (rejestr) operandu,wynik umieszczając w pierwszym operandzie.Instrukcja sub odejmuje wartość drugiego operandu od pierwszego,różnicę umieszczając wynik w pierwszym operandzie.Instrukcja cmp porównuje pierwszy operand z drugim a wynik zachowuje do użycia w jednej z warunkowych instrukcji skoku (omówionych za chwilę) Instrukcje and i or obliczają odpowiadające sobie na poziomie bitowym,operacje logiczne na dwóch operandach i przechowują wynik w pierwszym operandzie.Instrukcja not odwraca bity w pojedynczym operandzie pamięci lub rejestru. Instrukcje skoków przerywają sekwencyjne wykonywanie instrukcji w pamięci i przenoszą sterowanie do innego punktu w pamięci albo bezwarunkowo, albo po sprawdzeniu wyniku poprzedzającej instrukcji cmp instrukcje są następujące: ja jae jb jbe je jne jmp iret

dest dest dest dest dest dest dest

-- skok,jeśli powyżej -- skok, jeśli powyżej lub równe -- skok, jeśli poniżej --skok,jeśli poniżej lub równe -- skok,jeśli równe --skok,jeśli nie równe --skok bezwarunkowy --powrót z przerwania

Pierwsze sześć instrukcji pozwala nam sprawdzić czy wynik poprzedzającej instrukcji cmp jest większy niż większy lub równy mniejszy niż mniejszy lub równy,równy lub nierówny.Na przykład,jeśli porównujemy rejestry ax i bx instrukcją cmp i wykonujemy instrukcję ja,CPU x86 skoczy do wyszczególnionego miejsca przeznaczenia jeśli ax będzie większe niż bx.Jeśli ax nie będzie większe niż bx,sterowanie przejdzie do następnej instrukcji w programie.Instrukcja skoku bezwarunkowego jmp przenosi sterowanie do instrukcji o adresie przeznaczenia.Instrukcja iret zwraca sterowanie z podprogramu obsługi przerwań który omówimy później. Instrukcje get i put pozwalają odczytać i zapisać wartości całkowite.Get zatrzyma i zachęci użytkownika do podania wartości heksadecymalnej a potem przechowa tą wartość w rejestrze ax.Instrukcja put,wyświetla (heksadecymalnie) wartość z rejestru ax. Pozostałe instrukcje nie wymagają żadnych operandów,są to instrukcje halt i brk.Halt przerywa wykonywanie programu a brk zatrzymuje program, w stanie w którym może być zrestartowany. Procesor x86 wymaga unikalnych opcodów dla każdej instrukcji,nie tak jak klasa instrukcji.Chociaż „mov ax,bx” i „mov ax,cx” ,obie są tej samej klasy muszą mieć inne opcody,żeby CPU mógł je odróżnić.Jednakże,zanim przejrzymy wszystkie możliwe opcody,być może będzie dobrym pomysłem nauczyć się o wszystkich możliwych operandach dla tych instrukcji. 3.3.6 TRYBY ADRESOWANIA W x86 Instrukcje x86 używają pięciu różnych typów operandów: rejestry,stałe i trzy schematy adresowania pamięci.Każda z tych form nazywana jest trybem adresowania.Procesory x86 obsługują tryb adresowania rejestrowy,tryb adresowania natychmiasowego,tryb adresowania pośredniego,tryb adresowania z indeksowaniem i bezpośredni tryb adresowania.Ten paragraf wyjaśni każdy z tych trybów. Operandy rejestrów jest najłatwiejszy do zrozumienia Rozpatrzmy następujące formy instrukcji mov: mov ax, ax mov ax, bx mov ax, cx mov ax , dx Pierwsza instrukcja nie realizuje absolutnie niczego.Kopiuje wartość z rejestru ax z powrotem do rejestru ax.Pozostałe trzy instrukcje kopiują wartości z bx,cx i dx do ax. Zauważ,że oryginalne wartości z bx,cx i dx pozostają bez zmian.Pierwszy operand (przeznaczenia) nie jest ograniczony tylko do ax;możemy przenosić wartości do każdego z tych rejestrów. Stałe są równie łatwe do opanowania Rozpatrzmy następujące instrukcje: mov ax, 25

mov bx, 195 mov cx, 2056 mov dx, 1000 Te wszystkie instrukcje są równie proste;ładują do rejestrów stałe heksadecymalne. Są trzy tryby adresowania które radzą sobie z uzyskaniem dostępu do danych w pamięci.Te tryby adresownia mają następujące formy: mov ax, [1000] mov ax, [bx] mov ax, [1000+bx] Pierwsza instrukcja powyżej,używa bezpośredniego trybu adresowania do ładowania ax, szesnastobitową wartością przechowywaną w pamięci zaczynającą się od komórki 1000h Instrukcja mov ax, [bx] ładuje ax z komórki pamięci wyszczególnionej przez zawartość rejestru bx.Jest to pośredni tryb adresowania.Zamiast używać wartości z bx,ta instrukcja uzyskuje dostęp do komórki pamięci której adres znajduje się w bx.Zauważ,że dwie następujące instrukcje: mov bx, 1000 mov ax, [bx] są równoważne jednej instrukcji : mov ax, [1000] Oczywiście,druga sekwencja jest bardziej pożądana.Jednak,jest wiele przypadków gdzie użycie trybu pośredniego jest szybsze,krótsze i lepsze. Zobaczymy tego kilka przykładów kiedy będziemy przyglądać się pojedynczym procesorom z rodziny x86 trochę później. Ostatnim trybem adresowania jest adresowanie z indeksowaniem.Przykładem takiego adresowania pamięci jest: mov ax, {1000+bx] Ta instrukcja dodaje zawartość rejestru bx i 1000,tworząc wartość adresu pamięci do przekazania.Ta instrukcja jest przydatna przy dostępie do elementów tablic,rekordów i innych struktur danych. 3.3.7 KODOWANIE INSTRUKCJI x86 Chociaż możemy przypadkowo przydzielać opcody do każdej instrukcji x86,zapamiętaj,że w rzeczywistości CPU używa układów logicznych do dekodowania opcodów i właściwego na nich działania.Typowo opcody CPU używają pewnej liczby bitów w opcodzie do oznaczania klasy instrukcji (np. mov,add,sub ) i pewnej liczby bitów do kodowania każdego z operandów.Niektóre systemy(np. CISC lub Komputer z pełną listą rozkazów) koduje te pola w bardzo złożony sposób,tworząc niewielkich rozmiarów instrukcje.Inne systemy (np. RISC lub Komputer o uproszczonej liście rozkazów) koduje opcody w bardzo prosty sposób nawet jeśli oznacza to marnowanie niektórych bitów w opcodzie lub ograniczenie liczby instrukcji.Rodzina Intela 80c86 jest zdecydowanie CISCowska i ma jeden z najbardziej złożonych schematów dekodowania jaki wymyślono.Celem hipotetycznych procesorów x86 jest przedstawienie koncepcji kodowania instrukcji bez całej złożoności rodziny 80x86, przez zademonstrowanie kodowania CISC. Typowa instrukcja x86 przyjmuje postać taką jak pokazana na rysunku 3.19.Podstawowa instrukcja ma długość albo jednego albo trzech bajtów. Opcod instrukcji składa się z pojedynczego bajtu który zawiera trzy pola.Pierwsze pole,najbardziej znaczące,trzy bitowe,definiuje klasę instrukcji. Daje to osiem kombinacji.Jeśli sobie przypominasz,mamy 20 klas instrukcji;nie możemy zakodować 20 klas instrukcji w trzech bitach, więc będziemy musieli zastosować sztuczkę aby uzyskać inne klasy.. Jak widać na rysunku 3.19,podstawowy opcod koduje instrukcję mov (dwie klasy,jedna gdzie pole rr określa miejsce przeznaczenia,jedna gdzie pole mmm określa miejsce źródłowe),instrukcje add,sub,cmp,and i or.Jest jedna dodatkowa klasa:specjalna.

Rysunek 3.19: Kodowanie podstawowych instrukcji x86

Rysunek 3.20:Kodowanie pojedynczego operandu instrukcji Ta specjalna klasa instrukcji dostarcza mechanizmów,które pozwalają nam powiększać liczbę dostępnych klas instrukcji,wrócimy do tych klas wkrótce.. W celu ustalenia poszczególnych opcodów instrukcji,musimy tylko wybrać właściwe bity dla pól iii,rr i mmm.Na przykład, dla kodowania instrukcji mov ax,bx wybierzemy iii=100 (mov reg,reg),rr=00(ax) i mmm= 001 (bx).Da to nam instrukcję jednobajtową 11000001 lub 0C0h. Niektóre instrukcje x86 wymagają więcej niż jednego bajtu.Na przykład, instrukcja mov ax,[1000] ładuje do rejestru ax zawartość spod adresu komórki 1000.Kodowanie tego opcodu to 11000110 lub 0C6h.jednakże,kodowanie dla opcodu mov ax,[2000] to również 0C6h.Wyraźnie jednak widać,że te dwie instrukcje robią różne rzeczy.,jedna ładuje do rejestru ax zawartość z komórki pamięci o adresie 1000h podczas gdy druga ładuje do rejestru ax wartość z komórki pamięci o adresie 2000h.Kodując adres w trybie adresowania [xxxx] lub [xxxx+bx],lub kodowania stałych w trybie natychmiastowym,musisz zastosować opcod adresowany 16 bitowo lub stałą,z najmniej znaczącym bajtem bezpośrednio następującym po opcodzie w pamięci i bardziej znaczącym bajtem po nim.Tak więc trzy bajty kodowane dla instrukcji mov ax,[2000] będą wynosić 0C6h,00h,20h. Specjalny opcod pozwala CPU x86 do rozszerzenia zbioru dostępnych instrukcji.Ten opcod obsługuje instrukcjami z jednym lub zerowym operandem jak pokazano na rysunku 3.20 i 3.21

Rysunek 3.21:Kodowanie instrukcji bez operandu

Rysunek 3.22: Kodowanie instrukcji skoku Mamy cztery klasy instrukcji jednooperandowych.Po pierwsze, odkodowanie (00) bardzo rozszerza ilość instrukcji za pomocą zero operandowych instrukcji(zobacz rysunek 3.21).Po drugie opcod jest również opcodem rozszerzonym który dostarcza wszystkich instrukcji skoku x86 (zobacz rysunek 3.22) Po trzeci opcod jest instrukcją not.Jest to logiczna operacja not na poziomie bitowym która odwraca wszystkie bity w rejestrze przeznaczenia lub operandzie pamięci.Po czwarte, opcod pojedynczego operandu nie jest obecnie używany.Każda próba wykonania tego opcodu zatrzyma procesor z instrukcją błędu. Projektanci CPU często rezerwują nieużywane opcody ,jako te do rozszerzenia zbioru instrukcji na przyszłe dane (jak zrobił Intel przechodząc z procesorów 80286 na procesory 80386). Jest siedem instrukcji skoku w zbiorze instrukcji x86.Wszysytkie mają następującą postać: jxx adres Instrukcja jmp kopiuje 16 bitową wartość bezpośrednią (adres) następnego opcodu do rejestru IP.Dlatego też,CPU będzie pobierał następna instrukcję z tego adresu docelowego;faktycznie program „skacze” od punktu instrukcji skoku do instrukcji pod adresem docelowym. Instrukcja jmp jest przykładem instrukcji skoku bezwarunkowego.Zawsze przekazuje sterowanie pod adres docelowy.Pozostałe sześć insrtukcji,jest instrukcjami skoków warunkowych.Sprawdzają one pewne warunki, i skaczą jeśli warunek jest spełniony,przechodzą do następnej instrukcji jeśli warunek nie jest spełniony

Te sześć instrukcji to: ja,jae,jb.jbe,je i jne pozwala sprawdzić czy większe niż,większe niż lub równe,mniejsze niż,mniejsze niż lub równe,równe nie równe.Normalnie będziemy wykonywać te instrukcje natychmiast po instrukcji cmp ponieważ ustawia ona flagi,które są sprawdzane przez instrukcje skoków..Zauważ,że jest osiem możliwych opcodów skoku,ale x86 używa tylko siedmiu z nich. Ósmy opcod jest innym niedopuszczalnym opcodem. Ostatnią grupą instrukcji,są instrukcje bezoperandowe, pokazane na rysunku 3.21.Trzy z tych instrukcji są niedopuszczalnymi opcodami instrukcji .Instrukcja brk (break) pauzuje CPU do momentu,aż użytkownik nie zrestartuje go ręcznie. Pauzowaniu programu jest użyteczne podczas prowadzenia obserwacji wyników działania. Instrukcja iret (interrupt return) zwraca sterowanie z podprogramu obsługi przerwań.Omówimy to później.Halt przerywa wykonywanie programu.Instrukcja get odczytuje wartość heeksadecymalną od użytkownika i zwraca tą wartość do rejestru ax; instrukcja put odczytuje tą wartość z rejestru ax. 3.3.8 WYKONYWANIE INSTRUKCJI KROK PO KROKU CPU x86 nie kończy wykonywania instrukcji w jednym cyklu zegarowym. CPU wykonuje kilka kroków dla każdej instrukcji.Na przykład, CU wydaje następujące polecenia przy wykonywaniu instrukcji mov reg,reg/pamieć/stała: • Pobiera rozkaz bajtowy z pamięci • Uaktualnia rejestr IP wskazujący następny bajt • Dekoduje instrukcję aby zobaczyć co ona robi • W razie potrzeby pobiera 16 bitowy operand instrukcji z pamięci • W razie potrzeby,uaktualnia IP do punktu za operandem • Oblicza adres operandu,w razie potrzeby (n.p,bx+xxxx) • Pobiera operand • Przechowuje pobraną wartość w rejestrze przeznaczenia Opis krok po kroku,może pomóc w wyjaśnieniu co CPU robi.W pierwszym kroku,CPU pobiera rozkaz bajtowy z pamięci.Robi to kopiując wartość z rejestru IP na magistralę adresową i odczytuje bajt spod tego adresu.To zajmuje jeden cykl zegarowy. Po pobraniu rozkazu bajtowego,CPU uaktualnia IP żeby wskazywał następny bajt w ciągu instrukcji.Jeśli bieżąca instrukcja jest instrukcją wielobajtową,IP wskazuje operand dla tej instrukcji.Jeśli instrukcja jest jednobajtowa,IP będzie wskazywał następną instrukcję.To zajmuje jeden cykl zegarowy. Następnym krokiem jest dekodowanie instrukcji aby zobaczyć co ona robi.Powie ona CPU,między innymi,czy musi pobrać dodatkowy bajt operandu z pamięci.To zabiera jeden cykl zegarowy. Podczas dekodowania, CPU określa wymagane typy operandów instrukcji .Jeśli instrukcja wymaga 16 bitowego stałego operandu (np. jeśli pole mmm wynosi 101,110 lub 111) wtedy CPU pobiera tą stałą z pamięci. Ten krok może wymagać zero, jednego lub dwóch cykli zegarowych. Nie wymaga żadnego cyklu jeśli nie jest 16 bitowym operandem; potrzebuje jednego cyklu jeśli 16 bitowy operand jest word-aligned (słowem wyrównanym) (to znaczy, zaczyna się przy parzystym adresie);potrzebuje dwóch cykli zegarowych jeśli operand nie jest słowem wyrównanym (to znaczy, zaczyna się przy nieparzystym adresie). Jeśli CPU pobiera 16 bitowy operand pamięci musi zwiększyć IP o dwa, żeby wskazać następny bajt następujący po operandzie. Ta operacja zajmuje zero lub jeden cykl zegarowy. Zero cykli zegarowych jeśli nie ma operandu; jeden cykl zegarowy jeśli operand jest obecny. Następnie CPU oblicza adres operandu pamięci. Ten krok jest wymagany tylko wtedy kiedy pole mmm rozkazu bajtowego wynosi 101 lub 100.jeśli pole mmm zawiera 101 wtedy CPU oblicza sumę rejestru bx i stałej szesnastobitowej; to wymaga dwóch cykli, jednego dla pobrania wartości bx, drugiej do obliczenia sumy z bx i xxxx. Jeśli pole mmm zawiera 100,wtedy CPU pobiera wartość w bx dla adresu pamięci ,wymaga to jednego cyklu. Jeśli pole mmm nie zawiera 100 lub 101,ten krok zajmuje zero cykli. Pobieranie operandu zabiera zero, jeden, dwa lub trzy cykle w zależności od rodzaju operandu.Jeśli operand jest stałą (mmm=111) wtedy ten krok wymaga zero cykli ponieważ.mamy już pobraną tą stałą z pamięci w poprzednim kroku.Jeśli operand jest rejestrem (mmm=000,001,010 lub 011) wtedy ten krok wymaga jednego cyklu.Jeśli operandem pamięci jest wyrównane słowo (mmm=100,101 lub 110) wtedy ten krok wymaga dwóch cykli zegarowych.Jeśli jest to nie ustawiony operand pamięci, zajmuje to trzy cykle zegarowe do pobrania jego wartości.

Ostatnim krokiem instrukcji mov jest przechowanie wartości wewnątrz miejsca przeznaczenia.Ponieważ miejscem przeznaczenia ładowania instrukcji jest zawsze rejestr ta operacja zawiera pojedynczy cykl. Generalnie instrukcja mov zabiera od pięciu do jedenastu cykli, w zależności od jej operandów i ich ustawienia (adresów startowych) w pamięci.. CPU robi następujące rzeczy dla instrukcji mov pamięć,reg: • Pobiera rozkaz bajtowy z pamięci (jeden cykl zegarowy) • Uaktualnia rejestr IP aby wskazywał następny bajt (jeden cykl zegarowy) • Dekoduje instrukcję,aby wiedzieć co robi (jeden cykl zegarowy) • W razie potrzeby,pobiera operand z pamięci (zero cykli jeśli w trybie adresowania [bx],jeden cykl jeśli w trybie adresowania [xxxx],[xxxx+bx] lub xxxx a opcod wartości natychmiastowej xxxx zaczyna się od parzystego adresu.,lub dwa cykle zegarowe jeśli wartość xxxx zaczyna się od adresu nieparzystego). •

W razie potrzeby uaktualnia IP aby wskazywał poza operand (zero cykli jeśli brak operandu jeden cykl jeśli operand jest) • Oblicza adres operandu (zero cykli jeśli tryb adresowania to nie [bx] lub [xxxx+bx],jeden cykl jeśli tryb adresowania to [bx] lub dwa cykle jeśli tryb adresowania to [xxxx+bx]. • Dostarcza wartość z rejestru do przechowania (jeden cykl zegarowy) • Przechowuje pobrana wartość w miejscu przeznaczenia (jeden cykl jeśli w rejestrze dwa cykle jeśli operand pamięci jest wyrównanym słowem lub trzy cykle jeśli operand pamięci o nieparzystym adresie). Synchronizacja dla tych dwóch ostatnich pozycji jest różne dla różnych mov,ponieważ ta instrukcja może czytać dane z pamięci ta „wersja” instrukcji mov „ładuje” jej dane z rejestru.Instrukcji tej, wykonanie, zajmuje pięć do jedenastu cykli. Instrukcje add,sub,cmp,and i or robią co następuje: • Pobierają rozkaz bajtowy z pamięci (jeden cykl) • Uaktualniają IP aby wskazywał następny bajt (jeden cykl) • Dekodują instrukcję (jeden cykl) • W razie potrzeby pobierają stały operand z pamięci (zero cykli jeśli tryb adresowania [bx],jeden cykl jeśli tryb adresowania [xxxx],[xxxx+bx] lub xxxx a opcod wartości natychmiastowej zaczyna się od parzystego adresu lub dwa cykle zegarowe jeśli wartość xxxx zaczyna się od adresu nieparzystego). • W razie potrzeby uaktualniają IP aby wskazywał stały operand (zero lub jeden cykli zegarowych) • Obliczają adres operandu(zero cykli jeśli trybem adresowania nie jest [bx] lib [xxxx+bx],jeden cykl jeśli tryb adresowania to [bx],lub dwa cykle jeśli trybem adresowania jest [xxxx+bx] • Pobierają wartość operandu i wysyłają go do ALU (zero cykli jeśli stała jeden cykl jeśli rejestr dwa cykle jeśli operand jest słowem wyrównanym lub trzy cykle jeśli nieparzysto ustawiony operand pamięci. • Pobierają wartość z pierwszego operandu (rejestr) i wysyłają go do ALU (jeden cykl zegarowy) • Instruują ALU o wartościach dodawania,odejmowania,porównania,logicznego AND lub logicznego OR (jeden cykl) •

Przechowują wynik w pierwszym operandzie rejestru (jeden cykl)

Te instrukcje wymagają od ośmiu do siedemnastu cykli zegarowych do wykonania. Instrukcja not jest podobna do powyższych,ale może być trochę szybsza ponieważ ma tylko pojedynczy operand: • Pobiera rozkaz bajtowy z pamięci (jeden cykl zegarowy) • Uaktualnia IP aby wskazywał następny bajt (jeden cykl zegarowy)

• •

Dekoduje instrukcję (jeden cykl) W razie potrzeby pobiera stały operand z pamięci (zero cykli zegarowych jeśli tryb adresowania to ]bx],jeden cykl jeśli tryb adresowania [xxxx]lub [xxxx+bx],a opcod wartości natychmiastowej xxxx zaczyna się od parzystego adresu lub dwa cykle zegarowe jeśli wartość xxxx zaczyna się od adresu nieparzystego) • W razie potrzeby uaktualnia IP aby wskazywał poza stały operand (zero lub jeden cykl zegarowy) • Oblicza adres operandu (zero cykli zegarowych jeśli tryb adresowania nie jest [bx] lub[xxxx+bx],jeden cykl jeśli tryb adresowania to [bx] lub dwa cykle jeśli tryb adresowania to [xxxx+bx] • Pobiera wartość operandu i wysyła ją do ALU (jeden cykl jeśli rejestr,dwa cykle jeśli operator pamięci jest słowem wyrównanym lub trzy cykle zegarowe jeśli operand pamięci o nieparzystym adresie) • Instruuje ALU o przeprowadzeniu logicznego not (jeden cykl zegarowy) • Przechowuje wynik w operandzie (jeden cykl jeśli to rejestr,dwa cykle jeśli komórka pamięci o parzystym adresie,trzy cykle jeśli komórka pamięci o nieparzystym adresie. Wykonanie instrukcji not zajmuje od sześciu do piętnastu cykli Instrukcja skoku warunkowego pracuje jak następuje: • Pobiera rozkaz bajtowy z pamięci (jeden cykl zegarowy) • Uaktualnia IP aby wskazywał następny bajt (jeden cykl) • Dekoduje instrukcję (jeden cykl) • Pobiera adres docelowy operandu z pamięci (jeden cykl jeśli xxxx jest parzystym adresem dwa cykle jeśli jest nieparzystym adresem) • Uaktualnia IP aby wskazywał poza ten adres (jeden cykl) • Testuje „mniejsze niż” lub „równe” flagi CPU (jeden cykl) • Jeśli wartości flag są właściwe dla poszczególnych warunków skoku,CPU kopiuje 16 bitową stałą do rejestru IP (zero cykli jeśli się nie rozgałęzia,jeden cykl jeśli występuje rozgałęzienie) Instrukcja skoku bezwarunkowego jest identyczna jak w instrukcją mov reg,xxxx z wyjątkiem rejestru przeznaczenia którym jest rejestr x86 IP zamiast ax,bx,cx lub dx. Instrukcje brk,iret,halt,put i get nie są tu dla nas interesujące.. Pojawiły się w zbiorze instrukcji głównie dla programów i eksperymentów.Nie możemy policzyć im „cykli” ponieważ mogą zabierać nieograniczoną ilość czasu dla wykonania tego zadania. 3.3.9 RÓŻNICE MIĘDZY PROCESORAMI x86 Wszystkie procesory x86 dzielą ten sam zbiór istrukcji,te same tryby adresowania i wykonują te instrukcje używając tej samej sekwencji kroków.Więc jakie są różnice?Dlaczego nie wymyślono jednego procesora zamiast czterech? Głównym powodem dla wykonania tego ćwiczenia jest wyjaśnienie przedstawionych różnic ,powiązanych czterech cech hardware: kolejka rozkazów,pamięć podręczna cache,przetwarzanie potokowe i projektowanie superskalarne.Procesor 886 jest niedrogim „urządzeniem” ,które nie implementuje każdej z tych wymyślnych cech. Procesor 8286 implementuje kolejkę rozkazów.8486 ma kolejkę rozkazów,pamięć podręczną cache i przetwarzanie potokowe.8686 ma wszystkie z powyższych cech z operacjami superskalarnymi.Poprzez studiowanie tych procesorów możemy zobaczyć korzyści tych cech. 3.3.10 PROCESOR 886 Procesor 886 jest najwolniejszym członkiem rodziny x86.Czasy wykonania dla każdej instrukcji były omawiane w poprzedniej sekcji.Instrukcja mov,na przykład, zabiera od pięciu do dwunastu cykli zegarowych zależnie od operandów.Następująca tablica pokazuje czasy wykonania dla różnych form instrukcji na procesorach 886.

Tablica 19: Czasy wykonania dla instrukcji 886 Są trzy ważne rzeczy do zapamiętania z tego.Po pierwsze dłuższa instrukcja zabiera więcej czasu na wykonanie.Po drugie,instrukcje nie mające odniesienia do pamięci generalnie wykonują się szybciej,jest to prawda zwłaszcza gdy są stany oczekiwania powiązane z dostępem do pamięci (powyższa tabela zakłada zero stanów oczekiwania)W końcu,instrukcje używające złożonych trybów adresowania wykonują się wolniej.Instrukcje,używające rejestru jako operandu są krótsze,nie mają dostępu do pamięci i nie używają złożonych trybów adresowania.Jest tak,dlatego,że trzymamy swoje zmienne w rejestrach. 3.3.11 PROCESOR 8286 Kluczem do poprawienia szybkości procesora jest równoległe przeprowadzanie operacji.Jeśli w czasach wykonania danych dla 886,moglibyśmy robić dwie operacje na każdy cykl zegarowy,CPU wykonywałby instrukcje dwa razy szybciej pracując z tą samą szybkością zegara.Jednakże, proste decydowanie o wykonaniu dwóch operacji na cykl zegarowy nie jest tak łatwe.Wiele kroków w wykonywaniu instrukcji dzieli jednostki funkcjonalne w CPU (jednostki funkcjonalne są logicznymi grupami które wykonują wspólne operacje np. ALU i CU).Jednostka funkcjonalna jest zdolna wykonać tylko jedna operację w czasie.Dlatego też,nie możemy zrobić dwóch operacji które używają tych samych jednostek funkcjonalnych jednocześnie.(np. zwiększanie rejestru IP i dodawanie dwóch wartości razem).Inną trudnością z robieniem pewnych operacji jednocześnie jest to,że jedna operacja może zależeć od wyniku innej.Na przykład,ostatnie dwa kroki instrukcji add wymagają dodania wartości i potem przechowania ich sumy.Nie możemy przechować sumy w rejestrze zanim wyliczymy sumę.Również kilka innych zasobów CPU nie może dzielić kroków w instrukcji.Na przykład jest tylko jedna magistrala danych;CPU nie może pobierać opcodów instrukcji w tym samym czasie kiedy próbuje przechować jakieś dane w pamięci.Sztuczka w projektowaniu CPU jest taka,że wykonywanie kilku kroków równolegle polega na ułożeniu tych kroków,tak aby zredukować konflikty lub dodać dodatkową logikę aby dwie (lub więcej) operacji mogło wystąpić równolegle,poprzez wykonanie w różnych jednostkach funkcjonalnych. Rozważmy znów kroki instrukcji mov reg , pamięć/reg/stała: • Pobranie rozkazu bajtowego z pamięci • Uaktualnienie rejestru IP wskazując następny bajt

• • • • • •

Dekodowanie instrukcji żeby wiedzieć co robi W razie potrzeby pobranie 16 bitowego operandu instrukcji z pamięci W razie potrzeby,uaktualnienie IP do punktu za operandem Obliczanie adresu operandu,w razie potrzeby (n.p,bx+xxxx) Pobranie operandu Przechowanie pobranej wartości w rejestrze przeznaczenia

Pierwsza operacja używa wartości z rejestru IP (więc nie możemy powiększać IP ) a używa magistrali do pobrania opcodu instrukcji z pamięci.Każdy krok ,który następuje po tym zależy od opcodu pobranego z pamięci,więc jest to niepodobne abyśmy mogli założyć wykonanie tego kroku z innymi. Druga i trzecia operacja nie dzielą żadnej jednostki funkcjonalnej, nie dekodują opcodu w zależności od wartości rejestru IP.Dlatego też,możemy łatwo modyfikować jednostkę sterującą żeby zwiększyć rejestr IP w tym samym czasie dekodując instrukcje.To ujmie jeden cykl z wykonywania instrukcji mov. Trzecia i czwarta operacja powyżej (dekodowanie i opcjonalne pobieranie 16 bitowego operandu) nie wyglądają aby mogły być zrobione równolegle ponieważ musimy dekodować instrukcje aby zdecydować czy CPU musi pobrać 16 bitowy operand z pamięci.Jednak moglibyśmy zaprojektować CPU tak aby i pobrał operand tak czy owak,aby był dostępny jeśli go będziemy potrzebować.Jest jeden problem z tym pomysłem ,musimy mieć adres operandu do pobrania (wartość w rejestrze IP) i musimy czekać dopóki nie zwiększymy rejestru IP przed pobraniem tego operandu.Jeśli zwiększamy IP w tym samym czasie kiedy dekodujemy instrukcję,musimy czekać do następnego cyklu który pobierze ten operand. Ponieważ następne trzy kroki są opcjonalne jest kila możliwych sekwencji instrukcji w tym punkcie: #1 (krok4, krok5, krok6 i krok7)- np. mov ax,[1000+bx] #2 (krok4, krok5 i krok7) - np. mov ax,[1000] #3 (krok6 i krok7) - np. mov ax,[bx] #4 (krok7) - np. mov ax,bx W tej sekwencji krok siedem zawsze jest zależny od poprzedniej instrukcji w sekwencji. Dlatego też krok siedem nie może wykonywać się równolegle z innym z kroków,Krok sześć również zależy od kroku czwartego.Krok pięć nie może wykonywać się równolegle z krokiem czwartym ponieważ krok czwarty używa wartości z rejestru IP,jednak,krok pięć może wykonywać się równolegle z każdym innym krokiem. Dlatego też,możemy ująć 2 cykle z pierwszych dwóch sekwencji jak następuje: #1 #2 #3 #4

(krok4, krok5/6 i krok7) (krok4, krok5/7) (krok6 i krok7) (krok7)

Oczywiście,nie ma mowy o wykonaniu kroku siedem i osiem w instrukcji mov,ponieważ ona musi na pewno pobrać wartość przed zachowaniem jej.Przez kombinację tych kroków, uzyskamy następujące kroki dla instrukcji mov: • Pobranie rozkazu bajtowego z pamięci • Dekodowanie instrukcji i uaktualnienie IP • W razie potrzeby,pobranie 16 bitowego operandu instrukcji z pamięci • Obliczenie adresu operandu,w razie potrzeby (np bx+xxxx) • Pobranie operandu,w razie potrzeby uaktualnienie IP aby wskazywał poza xxxx • Przechowanie przyniesionej wartości do rejestru przeznaczenia Poprzez dodanie małej ilości logiki do CPU,możemy odjąć jeden lub dwa cykle wykonania instrukcji mov.Ta prosta optymalizacja pracy z większością instrukcji jest dobra. Inny problem z wykonywaniem instrukcji mov dotyczy ustawienia opcodu.Rozważmy instrukcję mov ax,[1000] która pojawia się w komórce 100 pamięci. CPU daje jeden cykl pobierając opcod i ,po zdekodowaniu instrukcji i określeniu ,że ma 16 bitowy operand, bierze dwa dodatkowe cykle dla pobrania tego operandu z pamięci (ponieważ ten operand pojawia się pod nieparzystym

adresem -101).Prawdziwą parodią tu jest to,że ten dodatkowy cykl zegarowy pobierający te dwa bajty jest zbyteczny,mimo wszystko,CPU pobiera najmniej znaczący bajt operandu kiedy przejmuje opcod (pamiętaj,że CPU x86 są procesorami 16 bitowymi i zawsze pobierają 16 bitów z pamięci),dlaczego nie zachowa tego bajtu i nie użyje tylko dodatkowego cyklu zegarowego dla pobrania bardziej znaczącego bajtu.? To ujmie czas wykonywania kiedy instrukcja zaczyna się od parzystego adresu (więc operand wpada pod adres nieparzysty)Wymagałoby to tylko jednobajtowego rejestru i małej ilości dodatkowej logiki do osiągnięcia tego. Podczas gdy dodamy bajt operandu rejestru do bufora,ropatrzymy kilka dodatkowych otymalizacji,które mogą używać tej samej logiki.Na przykład rozpatrzmy co się stanie z instrukcją mov podczas wykonywania.Jeśli pobierzemy opcod i najmniej znaczący bajt operandu w pierwszym cyklu i bardziej znaczący bajt operandu w drugim cyklu,w rzeczywistości odczytamy cztery bajty ,nie trzy.Tym czwartym bajtem jest opcod następnej instrukcji.Gdybyśmy mogli zachować ten opcod aż do przeprowadzenia następnej instukcji,moglibyśmy odjąć cykl czasu wykonania ponieważ nie musi pobierać bajtu opcodu .Co więcej ponieważ dekoder instrukcji jest nieczynny podczas gdy CPU wykonuje instrukcję mov,możemy w rzeczywistości dekodować następną instrukcję podczas wykonywania bieżącej instrukcji,tym samym ujmowaniem następnego cyklu z wykonywania w następnej instrukcji Średnio możemy pobrać ten extra bajt na każdą inną instrukcję.Dlatego też implementacja tego prostego schematu pozwoli nam odjąć dwa cykle z około 50% instrukcji które wykonujemy.. Czy możemy zrobić cokolwiek z drugim 50% instrukcji? Odpowiedź brzmi tak. Zauważ że wykonanie instrukcji mov nie uzyskuje dostępu do pamięci w każdym cyklu zegarowym.Na przykład gdy przechowujemy dane w rejestrze przeznaczenia magistrala jest nieczynna.Podczas okresu czasu kiedy magistrala jest nieczynna możemy pobrać wstępnie opcody instrukcji i operandów i zachować te wartości dla wykonywania następnej instrukcji. Ważnym ulepszeniem procesora 8286 w stosunku do procesora 886 jest kolejka rozkazów.Zawsze kiedy CPU nie używa jednostki sprzęgającej z magistralą (BIU),BIU może pobrać dodatkowe bajty ze strumienia instrukcji Zawsze kiedy CPU potrzebuje bajtu instrukcji lub operandu,korzysta z następnego dostępnego bajtu z kolejki rozkazów. Jednak nie gwarantuje to,że wszystkie instrukcje i operandy będą osadzone w kolejce rozkazów kiedy ich potrzebujemy.Na przykład instrukcja jmp 1000 unieważnia zawartość kolejki rozkazów.Jeśli ta instrukcja występuje w komórkach 400,401 i 402 w pamięci, kolejka rozkazów będzie zwierała bajty spod adresów 403,404,405,406,407 itd.Po załadowaniu do IP 1000 spod adresu 403 itp.,niemożemy zrobić dużo.System więc musi spauzować na chwilę aby pobrać podwójne słowo spod adresu 1000 zanim może iść dalej. Inną poprawą jaką możemy zrobić jest zachodzenie na siebie dekodowania instrukcji w ostatnim kroku poprzedniej instrukcji.Po przetworzeniu przez CPU tego operandu następny dostępny bajt w kolejce rozkazów jest opcodem,a CPU może zdekodować go przewidując jego wykonanie..Oczywiście,jeśli bieżąca instrukcja modyfikuje rejestr IP, czas zużyty na dekodowanie następnej instrukcji marnuje się ale ponieważ zdarza się to równolegle z innymi operacjami,nie spowalnia to systemu.

Rysunek 3.23: CPU z Kolejką Rozkazów Ta sekwencja optymalizacji systemu wymaga sporo zmian w sprzęcie.Diagram systemu pokazano na rysunku 3.23.Sekwencja wykonywania instrukcji teraz przybiera następujące wydarzenia występujące w tle:

CPU Zdarzenie Pobierania Wstępnego: • Jeśli kolejka rozkazów nie jest pełna (ogólnie rzecz biorąc można trzymać miedzy ośmioma a trzydziestoma dwoma bajtami,w zależności od procesora) a BIU jest nieczynne w bieżącym cyklu zegarowym pobiera następne słowo z pamięci spod adresu w IP przy rozpoczynaniu cyklu zegarowego • Jeśli dekoder instrukcji jest nieczynny,a bieżąca instrukcja nie wymaga operandu instrukcji zaczyna dekodowanie opcodu przed kolejką rozkazów (jeśli obecny), w przeciwnym razie zaczyna dekodowanie trzeciego bajtu w kolejce rozkazów (jeśli obecny) Jeśli nie ma pożądanego bajtu w kolejce rozkazów to zdarzenie nie wykonuje się. Czasy wykonywania instrukcji spełniają kilka optymistycznych zalożeń,mianowicie każdy niezbędny opcod i operand instrukcji są zawsze obecne w kolejce rozkazów i,że już mają zdekodowane opcody bieżących instrukcji.Jeśli obojętnie który przypadek nie jest prawdą wykonywanie instrukcji 80286 będzie opóźniało chwilowo system przy pobieraniu danych z pamięci lub dekodowaniu instrukcji. Dla każdej instrukcji 8286 są następujące kroki: mov reg, pamięć/reg/stała • W razie potrzeby oblicza sumę z [xxxx+bx] (1 cykl) • Pobiera operand źródłowy.Zero cykli jeśli stała (znajdująca się już w kolejce rozkazów),jeden cykl jeśli rejestr,dwa cykle jeśli wyrównywana parzyście wartość pamięci,trrzy cykle jeśli wyrównywana nieparzyście wartość pamięci • Przechowuje wynik w rejestrze przeznaczenia jeden cykl mov pamięć, reg • • •

Jeśli jest to wymagane oblicza sumę [xxxx+bx](jeden cykl) Pobiera operand źródłowy (rejestr),jeden cykl Przechowuje wewnątrz operandu przeznaczenia.Dwa cykle jeśli wartość pamięci wyrównywana parzyście i trzy cykle jeśli wyrównywana nieparzyście wartość pamięci

Instr reg, pamięć/reg/stała

(instr = add,sub,cmp,and,or)

• W razie potrzeby oblicza sumę z [xxxx+bx] (jeden cykl) • Pobiera operand źródłowy.Zero cykli jeśli stałą( znajdujący się już w kolejce rozkazów) ,jeden cykl jeśli rejestr,dwa cykle jeśli wyrównywana parzyście wartość pamięci,trzy cykle jeśli wyrównywana nieparzyście wartość pamięci • Pobiera zawartość z pierwszego operandu (rejestr),jeden cykl • Oblicza sumę,różnicę itp.,odpowiednio,jeden cykl • Przechowuje wynik w rejestrze przeznaczenia,jeden cykl Not pamięć/reg • • • • •

jcc

xxxx

W razie potrzeby oblicza sumę z [xxxx+bx] (jeden cykl) Pobiera operand źródłowy.Jeden cykl jeśli rejestr,dwa cykle jeśli wyrównywana parzyście wartość pamięci,trzy cykle jeśli wyrównywana nieparzyście wartość pamięci Wykonuje logiczne NOT wartości jeden cykl Oblicza sumę,różnicę itp.,odpowiednio,jeden cykl Przechowuje wynik jeden cykl jeśli rejestr,dwa cykle jeśli wyrównywana parzyście wartość pamięci,trzy cykle jeśli wyrównywana nieparzyście wartość pamięci

(skok warunkowy cc=a,ae,b,be,e,ne) •

Testuje bieżący warunek wskaźnika (mniejszy niż lub równy) flag,jeden cykl



Jeśli wartość flag jest właściwa dla poszczególnego odgałęzienia warunku,CPU kopiuje 16 bitowy operand instrukcji do rejestru IP,jeden cykl



CPU kopiuje 16 bitowy operand instrukcji do rejestru IP,jeden cykl

jmp xxxx

Jeśli chodzi o 886 nie możemy rozpatrywać czasów wykonania innych instrukcji x86 ponieważ większość z nich jest nieokreślona. Wygląda na to,że instrukcje skoku wykonują się bardzo szybko na 8286.Faktycznie,mogą wykonywać się bardzo wolno.Nie zapomnijmy że skakanie z jednej lokacji do innej zmienia kolejność w kolejce rozkazów.Więc mimo że instrukcja skoku wygląda na możliwą do wykonania w jednym cyklu zmusza CPU do opróżnienia kolejki rozkazów,a zatem poświęca kilka cykli pobierając następną instrukcję dodatkowy operand i dekodując tą instrukcję.Istotnie, wykonują się dwie lub trzy instrukcje po instrukcji skoku zanim CPU wróci do punktu gdzie kolejka rozkazów operuje płynnie i CPU dekoduje opcody równolegle z wykonywaniem poprzedniej instrukcji.Ma to jedną,ważną implikację dla naszych programów: jeśli chcemy pisać szybkie kody upewnijmy się,że uniknęliśmy skakania w kółko w naszych programach tak bardzo jak to możliwe. Zauważ że instrukcje skoków warunkowych tylko unieważniają kolejkę rozkazów ,jeśli w rzeczywistości wykonują skok.Jeśli warunek jest fałszywy, przechodzą do następnej instrukcji i kontynuują używanie wartości w kolejce rozkazów jak i predekodują opcody instrukcji.Zatem,możesz określić,podczas pisania programu, który warunek programu jest bardziej prawdopodobny (np. mniejszy niż czy nie mniejszy niż) powinieneś zorganizować swoje programy tak,że najbardziej powszechne nie dochodzą do skutku więc raczej stawiaj na skoki zależne od warunków każdej instrukcji pokolei.Rozmiar instrukcji (w bajtach) może również wpływać na wydajność kolejki rozkazów.Nigdy nie wymaga to więcej niż jednego cyklu zegarowego pobranie pojedynczego bajtu instrukcji ale zawsze wymaga dwóch cykli do pobrania trzech bajtów instrukcji.Zatem,jeśli celem instrukcji skoku są dwie jednobajtowe instrukcje,BIU może pobrać obie instrukcje w jednym cyklu zegarowym i zacząć dekodowanie drugiej podczas wykonywania pierwszej.Jeśli te instrukcje są instrukcjami trzy bajtowymi,CPU może nie mieć dosyć czasu do pobrania i zdekodowania drugiej lub trzeciej zanim skończy pierwszą.Dlatego też powinniśmy próbować używać krótszych instrukcji kiedy to możliwe ponieważ one poprawiają wydajność kolejki rozkazów. Poniższa tabela uwzględnia (optymistyczne) czasowe wykonanie instrukcji 8286:

Tabela 20: Czasy wykonania instrukcji dla 8286

Zauważ jak dużo szybciej instrukcja mov chodzi na 8286 w porównaniu z 886.Jest tak dlatego,że kolejka rozkazów pozwala procesorowi nakładać na siebie wykonywanie sąsiadujących instrukcji.Jednakże,ta tablica rysuje bardzo różowy obraz.Zauważ to zaprzeczenie „zakładając że opcod jest obecny w kolejce rozkazów zostanie zdekodowany”.Rozpatrzmy następującą sekwencje trzech instrukcji: ????: jmp 1000 1000: jmp 2000 2000: mov cx, 3000[bx] Druga i trzecia instrukcja nie wykonają się tak szybko jak sugerują czasy wykonań w powyższej tabeli. Kiedy tylko zmodyfikujemy wartość rejestru IP,CPU opróżni kolejkę rozkazów.Więc CPU nie może pobrać i zdekodować następnej instrukcji Zamiast tego musi pobrać opcod, zdekodować go itp, zwiększając czasy wykonania tych instrukcji .W tym momencie tylko co możemy zrobić to wykonać operację „uaktualnienia IP” równolegle z innym krokiem. Zazwyczaj, zastosowanie kolejki rozkazów poprawia wydajność. Dlatego Intel wprowadził kolejkę rozkazów w każdym modelu 80x86 ,z 8088 włącznie. Na tych procesorach ,BIU stale pobiera dane dla kolejki rozkazów, gdy tylko program nie jest aktywny czytaniem lub zapisaniem danych Kolejka rozkazów pracuje najlepiej kiedy mamy szeroką magistralę danych. Procesor 8286 pracuje dużo szybciej niż 886 ponieważ może trzymać pełną kolejkę rozkazów. Zatem rozpatrzmy następujące instrukcje: 100: 105: 10A:

mov ax,[1000] mov bx,[2000] mov cx,[3000]

Ponieważ rejestry ax,bx i cx są szesnastobitowe, oto co się wydarzy (zakładając ,że pierwsza instrukcja jest w kolejce rozkazów i zdekodowana): • Pobranie bajtu opcodu z kolejki rozkazów (zero cykli) • Dekodowanie instrukcji (zero cykli) • Jest operand do tej instrukcji, więc otrzymujemy go z kolejki rozkazów (zero cykli) • Dostajemy wartość z drugiego operandu (jeden cykl)Uaktualnienie IP • Przechowanie przyniesionej wartości w rejestrze przeznaczenia (jeden cykl) • Pobranie dwóch bajtów ze strumienia kodu .Dekodowanie następnej instrukcji. Koniec pierwszej instrukcji. Dwa bajty obecnie w kolejce rozkazów. • Pobranie bajtu opcodu z kolejki rozkazów (zero cykli) • Dekodowanie instrukcji aby zobaczyć co robi (zero cykli) • Jeśli jest operand do tej instrukcji ,dostajemy ten operand z kolejki rozkazów (jeden cykl zegarowy ponieważ mamy brakujący jeden bajt) • Dostajemy wartość z drugiego operandu (jeden cykl).Uaktualnienie IP. • Przechowanie przyniesionej wartości w rejestrze przeznaczenia (jeden cykl)Pobranie dwóch bajtów z strumienia kodu. Dekodowanie następnej instrukcji. Koniec drugiej instrukcji. Trzy bajty obecnie w kolejce rozkazów. • Pobranie bajtu opcodu z kolejki rozkazów (zero cykli) • Dekodowanie instrukcji aby zobaczyć co robi (zero cykli) • Jeśli jest operand do tej instrukcji dostajemy ten operand z kolejki rozkazów (zero cykli) • Dostajemy wartość z drugiego operandu(jeden cykl).Uaktualnienie IP. • Przechowanie przyniesionej wartości w rejestrze przeznaczenia(jeden cykl)Pobranie dwóch bajtów ze strumienia kodu.Dekodowanie następnej instrukcji. Jak możemy zobaczyć ,druga instrukcja wymaga jeden więcej cykl niż pozostałe dwie instrukcje .Jest tak ponieważ BIU nie może wypełnić kolejki rozkazów tak szybko jak CPU wykonuje instrukcje. Ten problem doprowadza do rozpaczy, kiedy ograniczymy rozmiar kolejkę rozkazów do kilku bajtów. Ten problem nie istnieje na procesorach 8286,ale z dużą pewnością istnieje w procesorach 8086. Wkrótce zobaczymy, że procesory 80x86 mają w zwyczaju wyczerpywać kolejkę rozkazów bardzo łatwo .Oczywiście, jeśli kolejka rozkazów jest pusta, CPU musi czekać na BIU .który pobierze nowe opcody z pamięci, spowalniając program. Wykonywanie krótszych instrukcji pomoże utrzymywać pełną kolejkę rozkazów. Na

przykład,8286 może ładować dwie jednobajtowe instrukcje w pojedynczym cyklu pamięci, ale zabiera 1,5 cyklu zegarowego pobranie pojedynczej trzybajtowej instrukcji. Zazwyczaj, dłużej zabiera wykonanie tych czterech jednobajtowych instrukcji niż robi to wykonanie pojedynczej trzy bajtowej instrukcji. Daje to kolejce rozkazów czas do wypełnienia i zdekodowania nowych instrukcji które operują szybciej niż odpowiedni zbiór czterech czterobajtowych instrukcji. Powodem jest to,że kolejka rozkazów ma czas do powtórnego wypełnienia się krótszymi instrukcjami. Morał z tej historii: kiedy programujesz procesor z kolejką rozkazów, zawsze używaj możliwie jak najkrótszych instrukcji realizujących dane zadanie. 3.3.12 PROCESOR 8486 Wykonywanie instrukcji równolegle używając jednostki sprzęgającej z magistralą i jednostki wykonawczej jest specjalnym przypadkiem przetwarzania potokowego.8486 zawiera w sobie przetwarzanie potokowe do poprawy wydajności. Z kilkoma wyjątkami ,zobaczymy ,że przetwarzanie potokowe pozwala nam wykonywać jedną instrukcję na cykl zegarowy. Przewagą kolejki rozkazów było to,że pozwalała CPU nakładać na siebie pobieranie instrukcji i dekodowanie z wykonaniem instrukcji. To znaczy, podczas gdy jedna instrukcja jest wykonywana, BIU pobiera i dekoduje następna instrukcję .Zakładając ,że chętnie dodamy sprzętu

Rysunek 3.24 Implementacja potoku wykonywania instrukcji

Rysunek 3.25:Wykonywanie instrukcji w potoku możemy wykonywać prawie wszystkie operacje równolegle. To jest cała idea przetwarzania potokowego 3.3.12.1 PRZETWARZANIE POTOKOWE 8486 Rozważmy kroki do zrobienia ogólnej operacji: • Pobranie opcodu • Dekodowanie opcodu i (równolegle) wstępne pobranie możliwego 16 bitowego operandu • Obliczanie złożonego trybu adresowania (np [xxxx+bx], w stosownych przypadkach • Pobranie wartości źródłowej z pamięci (jeśli operand pamięci) i wartość rejestru przeznaczenia, w stosownych przypadkach • Obliczanie wyniku • Przechowanie wyniku w rejestrze przeznaczenia Zakładając, że chętnie zapłacimy za jakiś extra „krzem”, możemy zbudować mały „mini-procesor” obsługujący każdy z powyższych kroków. Organizacja mogłaby wyglądać tak jak na rysunku 3.24

Jeśli zaprojektujemy oddzielną część sprzętową dla każdego etapu w przetwarzaniu potokowym ,prawie wszystkie te kroki mogą mieć miejsce równolegle. Oczywiście,nie można pobrać i zdekodować opcodu dla każdej z instrukcji w tym samym czasie, ale możemy pobrać jeden opcod podczas dekodowania poprzedniej instrukcji.Jeśli mamy n-etapowe przetwarzanie potokowe, zazwyczaj będziemy mieli n wykonywanych instrukcji jednocześnie. Procesor 8486 ma sześć etapów przetwarzania potokowego ,więc nakłada się wykonanie sześciu oddzielnych instrukcji. Rysunek 3.25 Wykonywanie Instrukcji w przetwarzaniu potokowym ,pokazuje przetwarzanie potokowe.T1,T2,T3 itd. przedstawiają kolejne „odliczania” zegara systemowego. Przy T=T1 CPU pobiera bajt opcodu dla pierwszej instrukcji. Przy T=T2,CPU zaczyna dekodować opcod dla pierwszej instrukcji. Równocześnie, pobiera16 bitów z kolejki rozkazów w przypadku gdy instrukcja ma operand. Ponieważ pierwsza instrukcja nie potrzebuje dłużej układu pobierającego opcod, CPU instruuje go do pobrania opcodu drugiej instrukcji równolegle z dekodowaniem pierwszej instrukcji .Zauważ ,że jest tu mały konflikt .CPU próbuje pobrać następny bajt z kolejki rozkazów do użycia jako operandu,w tym samym czasie jest pobierane 16 bitów z kolejki rozkazów do użycia jako opcod. Jak możemy zrobić obie od razu? Zobaczymy to rozwiązanie za kilka chwil.

Rysunek 3.26:Kolejka przetwarzania potokowego Przy T=T3 CPU oblicza adres operandu dla pierwszej instrukcji. CPU nie robi nic na pierwszej instrukcji jeśli nie używa ona trybu adresowania [xxxx+bx].Podczas T3,CPU również dekoduje opcod drugiej instrukcji i pobiera potrzebny operand. Koniec końców ,CPU również pobiera opcod dla trzeciej instrukcji .Z każdym taktem zegara, inny krok w wykonywaniu każdej instrukcji w przetwarzaniu jest zakończony a CPU pobiera inną instrukcję z pamięci. Przy T=T6 CPU kończy wykonywanie pierwszej instrukcji, oblicza rezultat dla drugiej, itd. w końcu pobiera opcod dla szóstej instrukcji w potoku. Ważną rzeczą jaką widzimy jest to,że po T=T5 CPU kończy instrukcje na każdym cyklu zegarowym. Zaraz po tym jak CPU wypełni potok ,kończy jedną instrukcję w jednym cyklu .Zauważ, że jest to prawda nawet jeśli jest złożony tryb adresowania obliczający ,operandy pamięci do pobrania lub inne operacje które używają cykli na procesorach niepotokowych. Wszystko co musisz zrobić to dodać więcej etapów do potoku ,i możesz jeszcze bardziej skutecznie przetwarzać instrukcje w jednym cyklu. 3.3.12.2 KOLEJKA W PRZETWARZANIU POTOKOWYM Niestety ,przedstawiony scenariusz w poprzedniej sekcji jest trochę zbyt uproszczony. Są dwie wady tego uproszczonego przetwarzania potokowego :spór magistral między instrukcjami i niesekwencyjne wykonywanie programu. Oba problemy mogą zwiększyć średni czas wykonania instrukcji w potoku. Spór magistral występuje gdy tylko instrukcja musi uzyskać dostęp do jakiegoś punktu w pamięci. Na przykład ,jeśli instrukcja mov pamięć , reg musi przechować dane w pamięci a instrukcja mov pamieć, reg czyta dane z pamięci, spór magistrali adresowej i danych musi nastąpić ponieważ CPU będzie próbował równocześnie pobierać dane i wpisać dane w pamięci Jedyny sposób obejścia tego sporu magistral jest przez kolejka potoku. CPU, kiedy zaistnieje spór magistral, daje priorytet dalszym instrukcjom w potoku .CPU zawiesza pobieranie opcodów aż do pobrania opcodu

bieżącej instrukcji (lub przechowania) .To powoduje, że nowa instrukcja w potoku zabiera dwa cykle wykonania zamiast jednej (zobacz rysunek 3.26). Ten przykład jest tylko jednym przykładem sporu magistral .Jest ich dużo więcej. Na przykład ,jak zauważyliśmy wcześniej, pobieranie operandów instrukcji wymaga dostępu do kolejki rozkazów w tym samym czasie kiedy CPU musi pobrać opcod. Co więcej ,na procesorach bardziej zaawansowanych niż 8486 (np. 80486) są inne źródła sporu magistral .Podany powyżej prosty schemat ,jest mało prawdopodobny, dlatego,że większość instrukcji wykonywałoby w jednym cyklu jedną instrukcję (CPI) Na szczęście, inteligentne użycie systemu pamięci podręcznej może wyeliminować wiele kolejek potoku jak omówione powyżej. Następna sekcja o buforowaniu podręcznym ,omówi jak jest to robione. Jednakże, nie jest to zawsze możliwe ,nawet z pamięcią podręczną, unikania kolejek w potoku. Czego nie możemy naprawić sprzętowo ,możemy dopilnować programowo .Jeśli unikamy używania pamięci ,możemy zredukować spory magistral i nasz program będzie wykonywał się szybciej .Podobnie, używając krótszych instrukcji również zredukujemy spory magistral i możliwość kolejki potoku Co się stanie jeśli instrukcja zmodyfikuje rejestr IP? Zanim instrukcja jmp

1000

zakończy wykonywanie, mamy już rozpoczętych pięć innych instrukcji i jest tylko jeden cykl zegarowy po zakończeniu pierwszej instrukcji. Oczywiście, CPU nie może wykonać tych instrukcji lub obliczyć niewłaściwych wyników. Prawdopodobnym rozwiązaniem jest opróżnienie całego potoku i rozpoczęcie pobierania na nowo. Jednakże ,takie postępowanie jest ukarane dłuższym czasem wykonania. Zabiera to sześć cykli zegarowych (długość potoku 8486) zanim następna instrukcja zakończy wykonywania. Wyraźnie możemy uniknąć używania instrukcji ,które przerywają sekwencję wykonywania programu. Pokazuje to również, inny problem - długość potoku. Dłuższy potok oznacza ,że możemy więcej osiągnąć na cykl w systemie. Jednak, wydłużanie potoku może spowolnić program jeśli będzie skakał w kółko. Niestety, nie możemy sterować liczbą etapów w potoku. Możemy ,jednak, sterować liczbą przenoszonych instrukcji które pojawiają się w naszym programie. Oczywiście powinieneś trzymać ich minimalna w potoku. 3.3.12.3 PAMIĘĆ PODRĘCZNA,KOLEJKA ROZKAZÓW I 8486 Projektanci systemu mogą rozwiązać wiele problemów ze sporem magistral poprzez inteligentne używanie kolejki rozkazów i podsystemu pamięci podręcznej. Mogą zaprojektować kolejkę rozkazów do buforowania danych ze strumienia instrukcji i mogą zaprojektować pamięć podręczną z oddzielnymi polami danych i kodu. Obie techniki mogą poprawić wydajność systemu przez wyeliminowanie kilku konfliktów magistral. Kolejka rozkazów po prostu działa jako bufor między strumieniem instrukcji w pamięci a układem pobierającym opcody .Niestety, kolejka rozkazów w 8486 nie czerpie korzyści jakie ma 8286.Kolejka rozkazów pracuje dobrze dla 8286 ponieważ CPU nie ma stałego dostępu do pamięci. Kiedy CPU nie ma stałego dostępu do pamięci, BIU może pobrać dodatkowe opcody instrukcji do kolejki rozkazów. Niestety CPU 8486 ma stały dostęp do pamięci ponieważ pobiera bajt opcodu na każdy cykl zegarowy. Dlatego kolejka rozkazów nie może wykorzystywać każdego „martwego” cyklu magistrali do pobierania dodatkowych bajtów opcodów - nie ma żadnego „martwego” cyklu magistrali. Jednakże, kolejka rozkazów jest wartościowa dla 8486 z bardzo prostej przyczyny: BIU pobiera dwa bajty na każdy dostęp do pamięci ,ale mimo to instrukcje są tylko jednobajtowe. Bez kolejki rozkazów, system musiałby wyraźnie pobrać każdy opcod ,nawet jeśli BIU ma już „przypadkowo” pobrany opcod wraz z poprzednią instrukcją. Z kolejką rozkazów, jednak ,system nie może pobierać żadnych opcodów .Pobiera je raz i zachowuje do użycia przez jednostkę pobierania opcodów. Na przykład ,jeśli wykonujemy dwie jednobajtowe instrukcje z rzędu, BIU może pobrać oba opcody w jednym cyklu pamięci, zwalniając magistrale dla innych operacji. CPU może użyć tych wolnych cykli magistral do pobrania dodatkowych opcodów lub poradzenia sobie z innym dostępem do pamięci. Oczywiście nie wszystkie instrukcje są długie na jeden bajt.8486 ma dwa rozmiary instrukcji: jeden bajt i trzy bajty .Jeśli wykonujemy ładowanie kilku trzybajtowych instrukcji z rzędu, ruszymy wolniej, np. mov ax,1000 mov bx,2000 mov cx,3000 add ax,5000

Rysunek 3.27 Typowa maszyna Harwardzka Każda z tych instrukcji odczytuje bajt opcodu i 16 bitowy operand (stała). Dlatego też ,zabiera to średnio 1,5 cyklu zegarowego do odczytu każdej instrukcji powyższej. W wyniku ,instrukcje wymagają sześciu cykli zegarowych to wykonania zamiast czterech. Raz jeszcze wrócimy do tej samej zasady: najszybsze programy to programy, które używają najkrótszych instrukcji.Jeśli możemy używać krótszych instrukcji do osiągnięcia zadania, zróbmy to. Następująca sekwencja instrukcji dostarcza dobrego przykładu: mov ax,1000 mov bx,1000 mov cx,1000 mov dx,1000 Możemy zredukować rozmiar tego programu i zwiększyć szybkość wykonania przez zmianę: mov ax,1000 mov bx ,ax mov cx, ax mov ax ,ax Ten kod ma tylko pięć bajtów długości w porównaniu do 12 bajtów z poprzedniego przykładu. Poprzedni kod zabierałby minimum pięć cykli zegarowych do wykonania, więcej jeśli są inne problemy ze sporami magistral. Ostatni przykład zabiera tylko cztery .Co więcej, drugi przykład zostawia wolne magistrale dla trzech z tych czterech okresów zegarowych, więc BIU może załadować dodatkowe opcod. Pamiętaj, krótsze często znaczy szybsze. Podczas gdy kolejka rozkazów może uwolnić cykle magistral i wyeliminować spory magistral, niektóre problemy jeszcze istnieją .Przypuśćmy, że średnia długość instrukcji dla sekwencji instrukcji jest 2.5 bajtowa (osiąga się przez posiadanie trzech trzy bajtowych instrukcji i jednej jednobajtowej razem).W takim przypadku magistrala będzie zajęta pobieraniem opcodów i operandów instrukcji. Nie będzie żadnego wolnego czasu na odniesienie się do pamięci. Zakładając że kilka z tych instrukcji odnosi się do pamięci , potok wchodząc w kolejkę ,spowalnia wykonanie. Przypuśćmy ,na chwilę ,że CPU ma dwie oddzielne przestrzenie pamięci, jedną dla instrukcji i jedną dla danych, każda ze swoją własną magistralą .Jest to nazywane Architekturą Harwardzką ponieważ pierwsza taka maszyna została zbudowana na Harvardzie maszynie harwardzkiej nie byłoby sporów na magistralach. BUI może

kontynuować pobieranie opcodów na magistrali instrukcji podczas uzyskiwania dostępu do pamięci na magistrali dane/pamięć (zobacz rysunek 3.27)

Rysunek 3.28: Wewnętrzna struktura CPU 8486 W prawdziwym świecie, jest bardzo mało prawdziwych maszyn harvardzkich .Ekstra wyprowadzenia potrzebne procesorowi dla wsparcia dwóch fizycznie oddzielnych magistral powiększa koszt procesora i przedstawia wiele innych problemów inżynierskich .Jednakże, projektanci mikroprocesorów odkryli ,że mogą uzyskać większe profity z architektury harvardzkiej z mniejszymi wadami przez użycie oddzielnych wbudowanych pamięci podręcznych dla danych i instrukcji. Zaawansowane CPU używają wewnętrzną architekturę harvardzką i zewnętrzną architekturę Von Neumanna. Rysunek 3.28 pokazuje strukturę 8486 z oddzielnymi pamięciami podręcznymi danych i instrukcji. Każda ścieżka wewnątrz CPU przedstawia niezależne magistrale. Dane mogą przepływać na wszystkich ścieżkach jednocześnie. To znaczy ,że kolejka rozkazów może ściągać opcody instrukcji z pamięci podręcznej instrukcji podczas gdy jednostka wykonawcza zapisuje dane do pamięci podręcznej danych. Teraz BIU tylko pobiera opcody z pamięci zawsze kiedy nie może zlokalizować ich w pamięci podręcznej instrukcji .Podobnie, pamięć podręczna danych buforuje pamięć. CPU używa magistral danych/adresowej tylko kiedy odczytuje wartość która nie jest w pamięci podręcznej lub kiedy opróżnia bufor danych do pamięci głównej. W ten sposób,8486 radzi sobie z pobieraniem operandów/opcodów instrukcji załatwiając problem w ten podstępny sposób .Poprzez dodanie ekstra układu dekodera, dekoduje instrukcje zaczynając z kolejki rozkazów i trzy bajty do kolejki rozkazów równolegle. Wtedy, jeśli poprzednia instrukcja nie miała 16 bitowego operandu, CPU użyje wyniku z pierwszego dekodera, jeśli poprzednia instrukcja używa operandu ,CPU używa wyniku z drugiego dekodera. Chociaż nie można sterować obecnością, rozmiarem lub typem pamięci podręcznej w CPU ,jako programiści asemblerowi musimy być świadomi jak działa pamięć podręczna, przy pisaniu najlepszych programów. Instrukcje wbudowanej pamięci podręcznej są generalnie całkiem małe (8,192 bajty dla 80486,na przykład).Dlatego też ,skracajmy nasze instrukcje, większość z nich wypełni pamięć podręczną. Przy większości instrukcji które mamy w pamięci podręcznej mniej będzie występowało sporów magistral .Podobnie używając rejestrów do przechowania wyników tymczasowych ,umieszczamy mniejsze obciążenie w pamięci podręcznej danych, więc nie musimy często opróżniać danych do pamięci lub odzyskiwać danych z pamięci Używajmy rejestrów gdzie tylko możliwe!

Rysunek 3.29: Przypadek na 8486

Rysunek 3.30: Przypadek na 8486 3.3.12.4 PRZYPADKI NA 8486 Jest inny problem z używaniem przetwarzania potokowego : przypadkowe dane. Spójrzmy na profil wykonania następującej sekwencji instrukcji: mov bx,[1000] mov ax,[bx] Kiedy te dwie instrukcje są wykonywane, potok szuka wygląda jak na rysunku 3.29.Zauważ główny problem. Te dwie instrukcje pobierają 16 bitowe wartości których adres pojawia się w lokacji 1000 w pamięci. Ale ta sekwencja instrukcji nie pracuje poprawnie! Niestety ,druga instrukcja już użyła wartości w bx zanim pierwsza instrukcja załaduje zawartość lokacji pamięci 1000 (T4 i T6 w powyższym diagramie). Procesory CISC, podobnie jak 80x86, radzą sobie z przypadkiem automatycznie. Jednak, zakolejkują ( zatrzymają) potok aż do synchronizacji dwóch instrukcji. Wykonanie w rzeczywistości na 8486 będzie wyglądało jak na pokazano rysunku 3.30 Poprzez opóźnienie drugiej instrukcji o dwa cykle zegarowe,8486 gwarantuje ,że ładowana instrukcja załaduje ax z właściwego adresu. Niestety, druga ładowana instrukcja wykonuje się w trzech cyklach zamiast w jednym. Jednak ,zastosowanie dwóch extra cykli zegarowych jest lepsze niż tworzenie niewłaściwych wyników. Na szczęście, możemy zredukować wpływ przypadku na szybkość wykonywania naszego programu. Zauważ ,że dane przypadkowe występują wtedy kiedy operand źródłowy jednej instrukcji był operandem przeznaczenia poprzedniej instrukcji .Nie ma nic złego w ładowaniu bx z [1000] a potem ładowania ax z [bx],chyba, że występują one jedna po drugiej .Przypuśćmy ,że mamy taką sekwencję kodu: mov cx,2000 mov bx,[1000] mov ax,[bx]

Rysunek 3.31: Wewnętrzna struktura CPU 8686 Możemy zredukować efekt przypadku które istnieje w tej sekwencji kodu poprzez proste przestawienie instrukcji. Zróbmy to i uzyskamy co następuje: mov bx,[1000] mov cx,2000 mov ax,[bx] Teraz instrukcja mov ax wymaga tylko jednego dodatkowego cyklu zamiast dwóch .Przez wprowadzenie dodatkowej instrukcji między instrukcje mov bx a mov ax możemy wyeliminować dane przypadkowe całkowicie. Na procesorze potokowym, porządek instrukcji w programie może dramatycznie wpłynąć na wydajność tego programu. Zawsze szukajmy możliwego przypadku w naszych sekwencjach instrukcji .Eliminujmy je gdziekolwiek to możliwe przez przestawianie instrukcji. 3.3.13 PROCESOR 8686 Z architekturą potokową z 8486 możemy osiągnąć w najlepszym razie, czas wykonania instrukcji jeden CPI (clock per instruction).Czy jest możliwe wykonywać instrukcje szybciej? Na pierwszy rzut oka możesz pomyśleć ”Oczywiście, że nie ,możemy zrobić najwyżej jedną operację na cykl .Więc nie ma sposobu abyśmy wykonali więcej niż jedną instrukcję na cykl ”Zapamiętaj jednak ,że pojedyncza instrukcja nie jest pojedynczą operacją. W przykładach wcześniejszych każda instrukcja zabierała między sześć a osiem zakończonych operacji .Przez dodanie siódmej lub ósmej oddzielnej jednostki do CPU,możemy skutecznie wykonać te osiem operacji w jednym cyklu dając jeden CPI. Jeśli dodamy więcej sprzętu i wykonamy, powiedzmy 16 operacji od razu, czy możemy osiągnąć 0,5 CPI? Połowiczna odpowiedź brzmi „tak” CPU zawierający ten dodatkowy sprzęt jest to CPU superskalarny i może wykonać więcej niż jedną instrukcję podczas pojedynczego cyklu. Taką możliwość dodaną ma procesor 8686. CPU superskalarny ma ,zasadniczo ,kilka jednostek wykonawczych (zobacz rysunek 3.31).Jeśli spotka dwie lub więcej instrukcji w strumieniu instrukcji (np. kolejka rozkazów) ,które mogą wykonywać się niezależnie ,robi to. Są dwie zalety stosowania superskalarów. Przypuśćmy ,że mamy następujące instrukcje w strumieniu instrukcji: mov ax,1000 mov bx,2000

Jeśli nie ma problemów lub przypadku w kodzie otaczającym, i wszystkie sześć bajtów dla tych dwóch instrukcji jest obecnych w kolejce rozkazów ,nie ma powodów dlaczego CPU nie może pobrać i wykonać obu instrukcji równolegle. Wszystko to zależy od ekstra „krzemu” w chipie CPU implementującego dwie jednostki wykonawcze. Poza tym przyspieszając niezależne instrukcje, CPU superskalarny może również przyspieszyć sekwencje programu zawierające przypadek. Jedynym ograniczeniem 8486 jest to,że przypadek się zdarza, naruszając całkowicie instrukcje w kolejce potoku .Każda instrukcja która następuje również będzie musiała czekać aż CPU zsynchronizuje wykonywanie instrukcji .Z superskalarnym CPU ,jednak, instrukcja następująca po przypadku może kontynuować wykonywanie poprzez potok tak długo dopóki nie mają swoich własnych przypadków. Programista asemblerowy, chcący pisać programy dla CPU superskalarnego, może radykalnie wpłynąć na jego wydajność. Pierwszą i główną zasadą która prawdopodobnie już znasz jest :używaj krótkich instrukcji .Skracaj swoje instrukcje, większość instrukcji CPU może pobrać w pojedynczej operacji ,dlatego też, jest bardziej prawdopodobne, że CPU będzie wykonywał szybciej niż jeden CPI .Większość superskalarnych CPU nie całkiem powiela jednostki wykonawcze. Mogą to być wielokrotne ALU, jednostki zmiennoprzecinkowe, itp. To znaczy, że pewna sekwencja instrukcji może wykonywać się bardzo szybko, podczas gdy inne nie. Musisz przestudiować dokładnie budowę swojego CPU, aby zadecydować która sekwencja instrukcji stworzy najlepsze wydajność 3.4 I/O (WEJŚCIE/WYJŚCIE) Są trzy podstawowe postacie wejścia i wyjścia ,których używa typowy system komputerowy :I/O-mapped I/O(odwzorowywanie I/O),memory mapped input/outpu (odwzorowywanie w pamięci I/O) i direct memory access (DMA) (bezpośredni dostęp do pamięci). I/O mapped I/O używa specjalnych instrukcji do przenoszenia danych między systemem komputerowym a światem zewnętrznym; memory mapped I/O używa specjalnej lokacji w pamięci w normalnej przestrzeni adresowej CPU do porozumiewania się z urządzeniami świata realnego; DMA jest specjalna postacią memory-mapped I/O gdzie urządzenia peryferyjne odczytują i zapisują pamięć bez przechodzenia przez CPU. Każdy mechanizm I/O ma swój własny zbiór zalet i wad, które omówimy w tej sekcji. Pierwszą rzeczą do nauczenia się o subsystemie I/O jest to,że I/O w typowym komputerze jest radykalnie różny niż I/O w typowym języku programowania wysokiego poziomu. W prawdziwym systemie komputerowym rzadko znajdziemy instrukcje maszynowe które zachowują się jak writeln, printf lub nawet instrukcje x86 get i put. Faktycznie, większość instrukcji I/O zachowuje się dokładnie jak instrukcja mov x86.Wysyłając dane do urządzenia wyjściowego, CPU po prostu przesuwa te dane do specjalnej lokacji pamięci ( w przestrzeni adresowej I/O, jeśli I/O-mapped I/O [zobacz Podsystem I/O] lub pod adres w przestrzeni adresowej pamięci jeśli używamy memorymapped I/O).Odczytując dane z urządzenia wejściowego, CPU po prostu przesuwa dane spod adresu (I/O lub pamieć) tego urządzenia wewnątrz CPU. Zazwyczaj jest więcej stanów oczekiwania powiązanych z typowym urządzeniem peryferyjnym niż rzeczywista pamięć ,operacje wejścia lub wyjścia wyglądają bardzo podobnie jak operacje odczytu lub zapisu pamięci. (zobacz Dostęp do Pamięci a System zegarowy) Port I/O jest urządzeniem które wygląda jak komórka pamięci komputera ale zawiera połączenia do świata zewnętrznego. Port I/O typowo używa zatrzasków zamiast przerzutników do implementacji komórki pamięci. Kiedy CPU zapisuje pod adres powiązany z zatrzaskiem, urządzenie zatrzasku przechwytuje dane umożliwiając podstawienie przewodów zewnętrznych do CPU (Zobacz rysunek 3.32) Zauważ ,że port I/O może być tylko do odczytu, tylko do zapisu lub odczytu/zapisu. Port na rysunku 3.32, na przykład, jest portem tylko do zapisu. Ponieważ

Rysunek 3.32:Port wyjściowy stworzony z pojedynczego zatrzasku

Rysunek 3.33Port I/O wymagający dwóch zatrzasków wyjścia na zatrzasku nie zwracają na magistralę danych CPU,CPU nie może odczytać danych zawartych na zatrzasku .Oba ,dekodowanie adresu i zapis linii sterujących muszą być aktywne przy operacji na zatrzasku.; kiedy odczytuje z adresu zatrzasku linia dekodująca jest aktywna, ale linia sterująca zapisem nie. Rysunek 3.33 pokazuje jak stworzyć port odczyt/zapis I/O. Dane zapisane na wyjście portu zwracają przeźroczysty zatrzask .Obojętnie kiedy CPU odczytuje adres dekodujący linie odczytu i dekodujące są wzbudzone a to wzbudzenie obniża zatrzask. Miejsca gdzie poprzednio zapisano dane do portu wyjściowego na magistrali danych CPU, pozwalają CPU odczytać te dane. Port tylko do odczytu (wejściowy) jest po prostu obniżony do połowy z rysunku 3.33;system ignoruje dane zapisane do portu wejściowego. Doskonałym przykładem portu wyjściowego jest równoległy port drukarki. CPU typowo zapisuje znaki ASCII szerokości bajtu do portu wyjściowego który łączy łączem DB-25F z tyłu komputera. Kabel transmituje tą daną do drukarki gdzie port wejściowy (drukarki) odbiera daną. Procesor wewnątrz drukarki konwertuje te znaki ASCII na sekwencję punktów drukowanych na papierze. Generalnie dane urządzenie peryferyjne używa więcej niż pojedynczego portu I/O.W PC typowe równoległe łącze drukarki, na przykład, używa trzech portów :port odczyt/zapis ,port wejściowy, port wyjściowy .Port odczyt/zapis jest portem danych (pozwala CPU odczytać ostatni znak ASCII zapisany do portu drukarki).Port wejściowy zwraca sygnały sterujące z drukarki ,sygnały te wskazują czy drukarka jest gotowa do zaakceptowania kolejnego znaku ,czy jest wyłączona, czy jest papier itp. Port wyjściowy przekazuje informacje sterujące do drukarki takie jak czy dane są dostępne do druku. Dla programisty, różnicami między operacjami I/O mapped i memory-mapped I/O są używane instrukcje .Dla memory-mapped I/O każda instrukcja która uzyskuje dostęp do pamięci, może uzyskać dostęp do portu memory-mapped I/O W x86 instrukcje mov, add,sub,cmp,and ,or i not mogą czytać pamięć ;instrukcje mov i not mogą zapisać dane do pamięci. I/O mapped I/O używa specjalnych instrukcji przy dostępie do portów. Na przykład, CPU x86 używa instrukcji get i put .Rodzina Intela 80x86 używa instrukcji in i out. Instrukcje in i out 80x86 pracują tak jak instrukcja mov z wyjątkiem umiejscowienia swoich adresów na magistrali adresowej I/O zamiast magistrali adresowej pamięci (zobacz Podsystem I/O). Podsystem memory-mapped I/O i podsystem I/O mapped obie wymagają CPU do przesunięcia danych między urządzeniem peryferyjnym a pamięcią główna .Na przykład, dane wejściowe 10 bajtowe są podawane z portu wejściowego i przechowywane w pamięci, CPU musi odczytać każdą wartość i przechować ją w pamięci. Dla bardzo szybkich urządzeń I/O CPU może być zbyt wolny kiedy przetwarza te dane po bajcie. Takie urządzenia generalnie zawierają interfejs do magistrali CPU więc odczytuje i zapisuje bezpośrednio pamięć. Jest to znane jako bezpośredni dostęp do pamięci ponieważ urządzenia peryferyjne uzyskują dostęp do pamięci bezpośrednio ,bez użycia CPU jako pośrednika. To często pozwala kontynuować operacje I/O równolegle z innymi operacjami CPU

,tym samym rośnie całkowita szybkość systemu .Zauważ ,jednak ,że CPU i urządzenia DMA nie mogą oba używać magistral adresowych i danych w tym samym czasie. Zatem ,bezpośrednia obróbka zdarza się jeśli CPU ma pamięć podręczną a wykonywany kod i dostępne dane znajdują się w pamięci podręcznej (więc magistrala jest wolna).Niemniej jednak, nawet jeśli CPU musi się zatrzymać i czekać na zakończenie operacji DMA ,I/O jest dużo szybsze ponieważ wiele operacji na magistralach I/O lub memory mapped I/O składa się z instrukcji pobrania lub dostępu do portu I/O które są nieobecne podczas operacji DMA. 3.5 PRZERWANIA I ZAPYTANIE I/O Wiele urządzeń I/O nie może przyjmować danych w przypadkowy sposób .Na przykład, Pentium jest zdolny wysyłać kilka milionów znaków na sekundę do drukarki, ale ta drukarka (prawdopodobnie) nie jest w stanie wydrukować tak dużo znaków na sekundę. Podobnie ,urządzenia wejściowe takie jak klawiatura jest niezdolna dostarczyć kilka milionów uderzeń w klawisze na sekundę. (ponieważ to zależy od szybkości człowieka nie komputera) CPU potrzebuje jakiegoś mechanizmu do koordynowania przekazywania danych między komputerem a jego urządzeniami peryferyjnymi. Jednym z powszechnych sposobów koordynowania transferu danych jest dostarczenie kilku bitów stanu w pomocniczym porcie wejściowym. Na przykład, jeden w pojedynczym bicie w porcie I/O może powiedzieć CPU, że drukarka jest gotowa do zaakceptowania więcej danych, zero wskazywałoby, że drukarka jest zajęta i CPU nie powinno wysyłać nowych danych do drukarki. Podobnie bit jeden w różnych portach może powiedzieć CPU, że uderzenie w klawisz z klawiatury jest dostępne na porcie danych klawiatury ,zero na tym samym bicie może wskazywać, że uderzenie w klawisze jest niedostępne. CPU może testować te bity przed odczytaniem klawisza z klawiatury lub zapisu znaku do drukarki. Załóżmy, że port danych drukarki jest odwzorowany w pamięci pod adresem 0FE0h a stan portu drukarki jest bitem zero z portu odwzorowanego w pamięci pod 0FFE2h.Następujący kod czeka aż do chwili kiedy drukarka jest gotowa do zaakceptowania bajtu danych potem zapisania bajtu w najmniej znaczącym bajcie ax do portu drukarki: 0000: 0003: 0006: 0009: 000C:

mov bx,[FFE2] and bx,1 cmp bx,0 je 0000 mov [FFE0],ax • • • • Pierwsza instrukcja pobiera dane do portu wejścia .Druga instrukcja logicznie dodaje tą wartość z jedynką zerując bity od jeden przez piętnaście i ustawia bit zero na bieżący stan portu drukarki. Zauważ ,że to tworzy wartość zero w bx, jeśli drukarka jest zajęta. tworzy wartość jeden w bx jeśli drukarka jest gotowa do akceptacji dodatkowej danej .Trzecia instrukcja sprawdza bx aby zobaczyć czy zawiera zero (np. drukarka jest zajęta)Jeśli drukarka jest zajęta ,ten program skacze do lokacji zero i powtarza ten proces tak długo jak długo bit stanu drukarki wynosi jeden. Następujący kod dostarcza przykładu czytania z klawiatury. Przyjmiemy, że bit stanu klawiatury jest zero spod adresu 0FFE6h (zero znaczy, że nie wciśnięto klawisza) a kod ASCII klawisza pojawia się pod adresem 0FFE4h kiedy bit zero z lokacji 0FFE6h zawiera jeden: 0000: mov bx,[FFE6] 0003: and bx,1 0006: cmp bx,0 0009: je 0000 000C: mov ax,[FFE4] Ten typ operacji I/O, gdzie CPU stale testuje port aby zobaczyć ,czy dana jest dostępna, to - technika odpytywania ,to znaczy ,CPU pyta port czy ma dostępną daną lub czy jest zdolny do przyjęcia danej. Zapytanie I/O jest z natury systemem niewydolnym. Rozważmy co zdarzy się w poprzednim segmencie kodu jeśli użytkownikowi zajmie 10 sekund naciśnięcie klawisza na klawiaturze - CPU wykona nic nie robiącą pętle dla tych dziesięciu sekund.

W pierwszych komputerach osobistych (np. .Apple II),jest dokładnie opisane jak program mógł odczytać dane z klawiatury :Kiedy musiał odczytać klawisz z klawiatury, musiał odpytywać stan portu klawiatury aż do momentu kiedy klawisz był dostępny. Takie komputery nie mogły robić innych operacji podczas oczekiwania na wciśnięcie klawisza .Co ważniejsze, jeśli zbyt dużo czasu mija między sprawdzeniem stanu portu klawiatury, użytkownik mógł nacisnąć drugi klawisz a pierwsze naciśnięcie było gubione. Rozwiązaniem tego problemu jest wprowadzenie mechanizmu przerwań .Przerwanie jest to zewnętrzne zdarzenie sprzętowe (jak naciśnięcie klawisza),które powoduje ,że CPU przerywa bieżącą sekwencję instrukcji i wywołuje specjalny podprogram obsługi przerwań (ISR).ISR zachowuje wartość wszystkich rejestrów i flag (żeby nie przeszkadzały w obliczaniu przerwań), robi jakieś operacje konieczne do poradzenia sobie z przerwaniem, przywraca rejestry i flagi, a potem rozpoczyna się na nowo wykonywanie kodu sprzed przerwania .W wielu systemach komputerowych ( np. PC) wiele urządzeń I/O generuje przerwania, kiedy tylko mają dostępne dane lub mogą zaakceptować dane z CPU.ISR szybko przetwarza prośbę w tle ,pozwalając kilku innym obliczeniom kontynuowanie pierwszoplanowe. CPU ,które wspierają przerwania muszą dostarczyć jakiś mechanizm, który pozwoli programiście wyspecyfikować adres ISRa do wykonania kiedy wystąpi przerwanie. Typowym wektorem przerwania jest specjalna komórka pamięci która zawiera adres ISRa do wykonania kiedy wywołane jest przerwanie. CPU x86,na przykład zawierają dwa wektory przerwań: jeden dla przerwań ogólnego zastosowania i jeden dla przerwania reset (przerwanie reset odpowiada naciśnięciu przycisku reset na większości PC).Rodzina Intela 80x86 wspiera do 256 różnych wektorów przerwań. Po zakończeniu operacji ISR, ogólnie rzecz biorąc, zwraca sterowanie do zadania pierwszoplanowego specjalną instrukcją „powrót z przerwania” W x86,to zadanie spełnia instrukcja iret (interrupt returns).ISR zawsze powinno kończyć się tą instrukcją ponieważ ISR może zwrócić sterowanie do programu który przerwał. Typowy wejściowy system sterowany przerwaniami używa ISR do odczytu danych z portu wejściowego i buforowania go gdy tylko dane staną się dostępne. Program pierwszoplanowy może czytać te dane z bufora bez pośpiechu bez zagubienia żadnej danej z portu. Podobnie, wyjściowy system sterowany przerwaniami (przerwanie występuje gdy tylko urządzenie wyjściowe jest gotowe zaakceptować więcej danych ) może usunąć dane z bufora gdy tylko urządzenie peryferyjne jest gotowe zaakceptować nowe dane. 3.8 PODSUMOWANIE Napisanie dobrego programu w asemblerze wymaga sporej wiedzy o wykorzystywanym sprzęcie. Prosta znajomość zbioru instrukcji jest nie wystarczająca .Chcąc tworzyć najlepsze programy musimy zrozumieć jak sprzęt wykonuje nasz program i uzyskuje dostęp do danych. Większość nowoczesnych systemów komputerowych przechowuje programy i dane w tej samej przestrzeni pamięci (architektura Von Neumanna).Jak większość maszyn vonNeumana, system 80x86 ma trzy główne komponenty: CPU ,I/O i pamięć .Zobacz: • „Podstawowy System Komponentów” Dane podróżują między CPU, urządzeniami I/O i pamięcią w systemie magistral. Są trzy główne magistrale zastosowane w rodzinie 80x86,magistrala adresowa, magistrala danych i magistrala sterująca. Magistrala adresowa przenosi liczby binarne które są wyspecyfikowane w lokacji pamięci lub porcie I/O do którego CPU życzy sobie uzyskać dostęp; magistrala danych przenosi dane między CPU a pamięcią lub I/O, magistrala sterująca przenosi ważne sygnały które określają czy CPU odczytuje czy zapisuje dane z pamięci lub uzyskuje dostęp do portu I/O. Zobacz: • ’System Magistral” • ’Magistrala Danych” • „Magistrala Adresowa” • „Magistrala Sterująca” Liczba linii danych na magistrali danych określa rozmiar procesora. kiedy mówimy, że procesor jest ośmiobitowy, mamy na myśli osiem linii danych na jego magistrali danych. Rozmiar danych z którymi procesor może sobie radzić CPU nie wpływa na rozmiar CPU. Zobacz: • „Magistrala Danych” • „Rozmiar Procesora”

Magistrala adresowa przenosi liczbę binarną z CPU do pamięci i I/O wybierając poszczególne elementy pamięci lub portu I/O. Liczba linii na magistrali adresowej ustawia maksymalną liczbę lokacji do których CPU może mieć dostęp. Typowy rozmiar magistrali adresowej w 80x86 to 20.24 lub 32 bity. Zobacz: • „Magistrala Adresowa” CPU 80x86 również mają magistralę sterującą która zawiera kilka sygnałów koniecznych dla właściwych operacji systemu Zegar systemowy, odczyt/zapis sygnałów sterujących, sygnały sterujące I/O i pamięci są kilkoma próbkami wielu linii które pojawiają się na magistrali sterującej .Zobacz: • :Magistrala sterująca” Podsystem pamięci to miejsce gdzie CPU przechowuje instrukcje programu i danych. W systemach opartych na 80x86, pamięć jawi się jako tablica bajtów ,każdy z własnym unikalnym adresem. Adres pierwszego bajtu w pamięci to zero, a adres ostatniego wolnego bajtu w pamięci to 2n-1,gdzie n to liczba linii na magistrali adresowej.80x86 przechowuje słowa w dwóch kolejnych lokacjach w pamięci. najmniej znaczący bajt słowa jest pod niższym adresem z tych dwóch bajtów; bardziej znaczący bajt bezpośrednio następuje w następnym, wyższym adresem. Chociaż słowo zużywa dwa adresy pamięci, kiedy pracujemy ze słowem ,po prostu używamy adresu jego najmniej znaczącego bajtu jako adresu słowa. Podwójne słowo zużywa cztery kolejne bajty w pamięci. Najmniej znaczący bajt pojawia się pod najniższym adresem z tych czterech, najbardziej znaczący pod najwyższym. ”Adresem” podwójnego słowa jest adres bajtu najmniej znaczącego .Zobacz: • „Podsystem Pamięci” CPU z 16,32 lub 64 bitowymi magistralami danych generalnie organizują pamięć w banki.16 bitowy podsystem pamięci używa dwóch banków po osiem bitów każdy,32 bitowy podsystem pamięci używa czterech banków po osiem bitów każdy a 64 bitowy podsystem pamięci używa ośmiu banków po osiem bitów każdy. Uzyskanie dostępu do słowa lub podwójnego słowa pod tym samym adresem wewnątrz wszystkich banków jest szybsze niż uzyskanie dostępu do obiektu, który jest podzielony między dwa adresy w różnych bankach Dlatego, powinniśmy próbować ustawiać dane słowa tak,żeby zaczynały się pod parzystym adresem a podwójne słowa danych tak,żeby zaczynały się pod adresem który równo dzieli się przez cztery .Możemy umiejscowić bajt danych pod każdym adresem .Zobacz • „Podsystem Pamięci” CPU 80x86 dostarcza oddzielnej 16 bitowej przestrzeni adresowej I/O która pozwala CPU uzyskać dostęp do każdego z 65,536 różnych portów I/O Typowe urządzenie I/O połączone z IBM PC używa tylko 10 z tych linii adresowych, ograniczając system do 1,024 różnych portów .Głównym zyskiem z używania przestrzeni adresowej I/O zamiast odwzorowywania wszystkich urządzeń I/O w przestrzeni pamięci jest to,że urządzenia I/O nie muszą naruszać przestrzeni adresowej pamięci. I/O i dostęp do pamięci, rozróżniają specjalne linie sterujące w systemie magistral. Zobacz: • „Magistrala Sterująca • „Podsystem I/O” Zegar systemowy steruje szybkością przy której procesor wykonuje podstawowe operacje. Większość CPU działa opierając się na rosnących lub opadających zboczach zegarowych. Przykłady obejmują wykonywanie instrukcji, dostęp do pamięci i sprawdzanie stanu oczekiwania. Im szybciej chodzi zegar tym szybciej wykonuje się program; jednakże pamięć musi być tak szybka jak zegar systemowy lub musimy wprowadzić stany oczekiwania ,które spowalniają system. Zobacz: • „System Synchronizacji” • „Zegar Systemowy” • „Dostęp Do Pamięci i Zegar systemowy” • „Stan Oczekiwania” Większość programów wykazuje lokalność odniesienia. Uzyskują dostęp do tej samej lokalizacji pamięci wielokrotnie na przestrzeni małego okresu czasu (czasowa lokalność) lub uzyskują dostęp do sąsiadujących lokacji pamięci podczas krótkiego okresu czasu ( przestrzenna lokalność) Podsystem pamięci podręcznej wykorzystuje ten fenomen do zredukowania stanów oczekiwania w systemie. Mała pamięć podręczna może osiągnąć poziom 80-95% trafień. Dwupoziomowa pamięć podręczna używa dwóch różnych pamięci podręcznych (jedna zintegrowana z CPU, jedna poza CPU) dla osiągnięcia lepszej wydajności systemu.. Zobacz:



„Pamięć Podręczna Cache”

CPU ,takie jak z rodziny 80x86, przerywają wykonywanie instrukcji maszynowych wewnątrz kilku odrębnych kroków ,każdy wymagający jednego cyklu zegarowego. Te kroki zawierają pobieranie opcodów instrukcji ,dekodowania tych opcodów, pobierania operandów dla instrukcji, obliczania adresów pamięci ,uzyskiwania dostępu do pamięci, wykonywania podstawowych operacji i przechowywania wyników dalej .Na bardzo uproszczonym CPU, proste instrukcje mogą zabierać kilka cykli zegarowych. Najlepszym sposobem poprawy wydajności CPU jest wykonanie kilku wewnętrznych operacji równolegle z innymi. Prosty schemat to umieszczenie instrukcji w kolejce rozkazów w CPU. Pozwala to zachodzić na siebie opcodom pobieranym i dekodowanie wykonywanych instrukcji ,często obcinając czas wykonania o połowę .Inną alternatywą jest użycie instrukcji potokowych ,gdzie możemy wykonać kilka instrukcji równolegle .W końcu, możemy zaprojektować superskalarny CPU który wykonuje dwie lub więcej instrukcji w tym samym czasie. Te techniki pozwalają przyspieszyć działanie programów. Zobacz: • „Procesor 886” • „Procesor 8286” • :Procesor 8486” • „Procesor 8686” Chociaż CPU potokowy i superskalarny poprawiają całkowicie wydajność systemu, uzyskanie najlepszej wydajności z tak złożonego CPU wymaga ostrożnego planowania przez programistę. Kolejki potokowe i przypadki mogą spowodować poważną stratę wydajności w kiepsko zorganizowanym programie. Poprzez ostrożne organizowanie sekwencji instrukcji w programie możemy uczynić program dwu lub trzy razy szybszym. Zobacz: • „Przetwarzanie Potokowe w 8486” • „Kolejka potoku” • „Pamieć Podręczna, Kolejka Rozkazów i 8486” • „Przypadek w 8486” • „Procesor 8686” Podsystem I/O jest trzecim głównym komponentem z maszyny VonNeumanna. Są trzy podstawowe sposoby przemieszczania danych między systemem komputerowym a światem zewnętrznym: I/O mapped I/O, memory mapped I/O i bezpośredni dostęp do pamięci (DMA).Po więcej informacji zajrzyj: • „I/O (Wejście/Wyjście)” Dla poprawienia wydajności systemu, większość nowoczesnych komputerów używa przerwań dla zawiadomienia CPU kiedy operacja I/O jest zakończona. Pozwala to CPU kontynuować inne przetwarzanie zamiast czekać na zakończenie operacji I/O (odpytywanie portów I/O)Po więcej informacji na ten temat zajrzyj • „Przerwania i Zapytanie I/O”

3.9 PYTANIA 1. 2.

3 4

Jakie są trzy komponenty stanowiące maszynę Von Neumanna Jaki jest cel: a) magistrali systemowej b) magistrali adresowej c) magistrali danych d) magistrali sterującej

3. Jak magistrala definiuje „rozmiar” procesora? 4. Z której magistrali sterującej możemy mieć dużo pamięci? 5. Czy rozmiar magistrali danych steruje maksymalną wartością jaką CPU może przetworzyć? Wyjaśnij 6. jakie są rozmiary magistrali danych: a) 8088 b)8086 c)80286 d)80386sx e)80386 f)80486 g)80586?Petium 7. jaki jest rozmiar magistral adresowych powyższych procesorów? 8. Jak dużo „banków” pamięci posiada każdy z powyższych procesorów?

9. Wyjaśnij jak przechowuje się słowo w pamięci adresowanej bajtem (to znaczy pod jakim adresem).Wyjaśnij jak przechowuje się podwójne słowo 10. Jak dużo operacji na pamięci zabiera odczyt słowa z następujących adresów na tych procesorach? 11.Powtórz powyższe dla podwójnego słowa 12. Wyjaśnij który adres jest najlepszy dla zmiennych bajtowych, słowa i podwójnego słowa w procesorach 8088,80286 i 80386. 13. Jak dużo różnych lokacji I/O można zaadresować w chipie 80x86?Jak dużo jest dostępnych na PC? 14. Jaki jest cel zegara systemowego? 15. Co to jest cykl zegarowy? 16. .Jakie są związki między częstotliwością zegara a okresem zegarowym? 17. Jak dużo cykli zegarowych jest wymaganych dla każdego następnego odczytu z pamięci? a) 8088 b)8086 c)080486 18. Co oznacza termin „ czas dostępu do pamięci”” 19. Co to jest stan oczekiwania 20. Jeśli jest uruchomiony 80486 przy następujących szybkościach zegara, jak dużo stanów oczekiwania jest wymaganych jeśli używamy 80 ns RAM (nie zakładając innych opóźnień) a) 20 MHz b)25 MHz c) 33 MHz d)50 MHz e) 100 MHz 21. Jeśli CPU pracuje przy 50 MHz,20 ns RAM prawdopodobnie nie będzie dość22. szybki aby operować23. przy zerowym stanie oczekiwania Wyjaśnij dlaczego 24. Ponieważ pod-10ns RAM jest dostępny, dlaczego niema wszystkich zerowych stanów oczekiwania w systemie? 25. Wyjaśnij jak pamięć podręczna obsługuje zachowanie stanów oczekiwania? 26. Jaka jest różnica między czasową lokalnością odniesienia przestrzenną lokalnością odniesienia? 27. Wyjaśnij gdzie czasowa i przestrzenna lokalność odniesienia wydarzy się w następującym kodzie pascalowskim: while 111F00=>11F0:0 Zauważ ,że to omówienie dotyczy tylko procesorów 80x86 operujących w trybie rzeczywistym. W trybie chronionym nie ma bezpośredniej odpowiedniości między adresami segmentowymi a adresami fizycznymi więc ta technika nie działa .Jednakże, ten tekst zajmuje się głównie programami, które pracują w trybie rzeczywistym, więc znormalizowane wskaźniki pojawiają się w całym tekście. 4.5 REJESTRY SEGMENTOWE W 80x86 Kiedy Intel projektował 8086 w 1976 roku ,pamięć była cenną rzeczą .Zaprojektowali swój zbiór instrukcji tak,żeby każda instrukcja używała tka mało bajtów jak to możliwe. To uczyniło programy mniejszymi więc systemy komputerowe stosowały procesory Intelowskie używające mniej pamięci. Jako takie ,te systemy komputerowe były tańsze do wytworzenia. Oczywiście, koszt pamięci spadał do punktu gdzie nie ma powodu do martwienia się o nią jak było kiedyś. Jednej rzeczy chciał uniknąć Intel dołączając 32 bitowy adres (segment:offset) do końca instrukcji które odnoszą się do pamięci .Chcieli zredukować to do 16 bitów (tylko offset) przez dokonanie pewnych założeń o tym do których segmentów w pamięci mogą uzyskać dostęp instrukcje. Procesory 8086 do 80286 mają cztery rejestry segmentowe ;cs,ds.,ss i es.80386 i późniejsze procesory mają te rejestry segmentowe plus fs i gs. Rejestr cs (code segment) wskazuje na segment zwierający bieżący ,wykonywany kod .CPU zawsze pobiera instrukcje z adresu podanego przez cs:ip. Domyślnie CPU oczekuje dostępu do większości zmiennych w segmencie danych .pewne zmienne i inne operacje występują w segmencie stosu .Kiedy uzyskujemy dostęp do danych w tych wyspecyfikowanych obszarach, żadna wartość segmentu nie jest konieczna. Przy dostępie do danych w jednym z tych ekstra segmentów (es ,fs lub gs),tylko jeden bajt jest potrzebny do wybrania właściwego rejestru segmentowego. Tylko kilka instrukcji sterujących przepływem pozwala nam wyspecyfikować pełny 32 bitowy adres segmentowy. Teraz może się to wydawać ograniczeniem .W końcu tylko z czterema rejestrami segmentowymi w 8086 możemy zaadresować maksimum 256 kilobajtów (64K na segment),a nie cały obiecany megabajt. Jednakże możemy zmieniać rejestry segmentowe pod kontrolą programu, więc jest możliwe adresować każdy bajt poprzez zmianę wartości w rejestrze segmentowym. Oczywiście, zabierze parę instrukcji zamiana wartości jednego z rejestrów segmentowych 80x86.Te instrukcje zużywają pamięć i zabierają czas na wykonanie .Tak więc zachowanie dwóch bajtów na dostęp do pamięci nie opłaca się, jeśli uzyskujemy dostęp do danych w różnych segmentach cały czas. Na szczęście, większość kolejnych dostępów do pamięci występuje w tym samym segmencie. W związku z tym, ładowanie rejestrów segmentowych nie jest czymś co robimy bardzo często. 4.6 TYBY ADRESOWANIA 80x86 Tak jak opisane procesory x86 w poprzednim rozdziale, procesory 80x86 pozwalają nam uzyskać dostęp do pamięci na wiele różnych sposobów. Tryby adresowania pamięci 80x86 dostarczają elastycznego dostępu do pamięci ,pozwalając nam na łatwy dostęp do zmiennych, tablic ,rekordów, wskaźników i innych złożonych typów danych. Biegłe opanowanie trybów adresowania 80x86 jest pierwszym krokiem do biegłego opanowania języka asemblera 80x86. Kiedy Intel projektował oryginalny procesor 8086,wyposażył go w elastyczny, chociaż ograniczony ,zbiór trybów adresowania pamięci .Intel dodał kilka nowych trybów adresowania, kiedy wprowadzono mikroprocesor 80386.Zauważ,że 80386 zachował wszystkie tryby z poprzednich procesorów; nowe tryby zostały dodane jako dodatki. Kiedy musimy napisać kod który pracuje na procesorze 80286 lub wcześniejszych, nie będziemy mogli wykorzystać tych nowych trybów .Jednakże jeśli zamierzamy uruchomić nasz kod na procesorze 80386sx lub wyższych możemy użyć tych nowych trybów .Ponieważ wielu programistów pisze jeszcze programy które pracują na 80286 lub wcześniejszych maszynach ,jest ważne oddzielenie omawiania tych dwóch zbiorów trybów adresowania, aby uniknąć pomylenia ich. 4.6.1 TRYB ADRESOWANIA REJESTRÓW

Większość instrukcji 8086 może działać na zbiorze rejestrów ogólnego przeznaczenia 8086.Poprzez wyspecyfikowanie nazwy rejestru jako operandu instrukcji, możemy uzyskać dostęp do zawartości tego rejestru .Rozważmy instrukcję mov (move) 8086: mov

przeznaczenie, źródło

Instrukcja ta kopiuje dane z operandu źródłowego do operandu przeznaczenia. Ośmio i szesnasto bitowe rejestry są z pewnością ważnymi operandami dla tej instrukcji .Ograniczeniem jest tylko to,że oba operandy muszą być tego samego rozmiaru .popatrzmy na kilka rzeczywistych instrukcji mov 8086: mov mov mov mov mov mov

ax, bx dl, al. si, dx sp, bp dh,cl ax,ax

;kopiuje wartość z bx do ax ;kopiuje wartość z al. do dl ;kopiuje wartość z dx do si ;kopiuje wartość z bp do sp ;kopiuje wartość z cl do dh ;tak, to jest dozwolone!

Pamiętajmy ,że rejestry są najlepszym miejscem do trzymania często używanych zmiennych .Jak zobaczymy trochę później ,instrukcje używające rejestrów są krótsze i szybsze niż te które uzyskują dostęp do pamięci.. W całym tym rozdziale będziemy widzieć skrócone nazwy operandów reg i r/p. (rejestr/pamięć) używane wszędzie gdzie będziemy używali rejestrów ogólnego przeznaczenia. Oprócz rejestrów ogólnego przeznaczenia, wiele instrukcji 8086 (wliczając w to instrukcję mov) pozwala nam wyspecyfikować jeden z rejestrów segmentowych jako operand. Są dwa ograniczenia przy używaniu rejestrów segmentowych z instrukcja mov. Po pierwsze ,nie możemy wyspecyfikować cs jako operandu przeznaczenia, po drugie tylko jeden z operandów może być rejestrem segmentowym. Nie możemy przenosić danych z jednego rejestru segmentowego do innego w pojedynczej instrukcji mov .Kopiowanie wartości z cs do ds. musi używać sekwencji instrukcji podobnej do tej: mov ax, cs mov ds., ax Nie powinniśmy nigdy używać rejestrów segmentowych jako rejestru danych do przetrzymywania przypadkowych wartości. powinny one zwierać tylko adresy segmentów. Ale więcej o tym później .W całym tekście będziemy używać skróconych nazw operandów sreg ,wtedy kiedy rejestr segmentowy będzie przeznaczony (wymagany) jako operand. 4.6.2 TRYBY ADRESOWANIA PAMIĘCI 8086 8086 dostarcza 17 różnych sposobów dostępu do pamięci .Może to wydawać się to wydawać sporo z początku ale na szczęście większość trybów adresowania jest prostymi wariantami tak, że są one łatwe do nauczenia. A powinniśmy się ich nauczyć! Kluczem do dobrego programowania w asemblerze jest właściwe użycie trybów adresowania pamięci Tryby adresowania dostarczone przez rodzinę 8086 obejmują „tylko –przemieszczenie”, bazowy, bazowy z przemieszczeniem, bazowy indeksowany i bazowy indeksowany z przemieszczeniem. Odmiany tych pięciu form dostarczają 17 różnych trybów adresowania w 8086.Zobaczmy ,z 17 do 5.Nie jest wcale tak źle! 4.6.2.1 TRYB ADRESOWANIA ‘TYLKO PRZEMIESZCZENIE ” Najbardziej powszechnym trybem adresowania i jednym z łatwiejszych do zrozumienia, jest tryb adresowania „tylko przesunięcie” lub „bezpośredni”. Tryb adresowania „tylko przemieszczenie” składa się z 16 bitowej stałej która wyszczególnia adres lokacji docelowej .Instrukcja mov al .,ds:[8088h] ładuje do rejestru al. kopię bajtu spod komórki pamięci 8088h

Składnia MASM dla Trybów Adresowania Pamięci Assembler z Microsoftu używa kilku różnych odmian na oznaczenie indeksowego, bazowo indeksowego i bazowo indeksowanego z przemieszczeniem trybów adresowania. Zobaczymy, że wszystkich tych form będziemy używać zamiennie w tym tekście. Następująca lista kilku możliwych kombinacji które są poprawne dla różnych trybów adresowania 80x86: disp[bx],[bx] [disp],[bx+disp],[disp] [bx], i [disp+bx] [bx] [si], [bx+si],[si] [bx], i [si+bx] disp [bx] [si],disp[bx+si],[disp+bx+si],[disp+bx] [si],disp [si] [bx], [disp+si] [bx],[disp+si+bx],[si+disp+bx],[bx+disp+si], itp. MASM traktuje symbol „[ ]” jako operator „+”.Ten operator jest zamienny podobnie jak operator „+”.Oczywiście, to omówienie stosuje się we wszystkich trybach adresowania 8086,nie dotyczy to BX i SI .Możemy zastąpić każdy poprawny rejestr w powyższych trybach adresowania

Rysunek 4.8: Tryb adresowania „Tylko przemieszczenie”(Bezpośredni) Podobnie,instrukcja mov ds.:[1234h],dl zapamiętuje wartość rejestru dl w komórce pamięci 1234h (zobacz rysunek 4.8) Tryb adresowania „tylko przemieszczenie” jest doskonały dla uzyskania dostępu do prostych zmiennych .Oczywiście, będziemy woleli używać nazwy takiej jak „I” lub „J” zamiast „DS.:[1234h]” lub „DS.:[8088h]”Cóż ,wkrótce zobaczymy ,że jest możliwe zrobić dokładnie tak. Intel nazwał ten tryb trybem adresowania „tylko przemieszczenie” ponieważ 16 bitowa stała (przemieszczenie) występuje jako opcod instrukcji mov w pamięci .W tym względzie jest to całkiem podobne do bez[pośredniego trybu adresowania w procesorach x86 (zobacz poprzedni rozdział).Jest jednak kilka drobnych różnic .Przede wszystkim, przemieszczenie jest dokładnie tym – odległością od innego punktu .W x86,adres bezpośredni może być potraktowany jako przemieszczenie od adresu zero .W procesorach 80x86.to przesunięcie jest offsetem od początku segmentu (w tym przypadku segmentu danych)Nie martwmy się jeśli nie ma to dużego sensu w tej chwili. Dostaniemy możliwość do studiowania segmentów, trochę później w tym segmencie .Na razie możemy myśleć o trybie adresowania „tylko przemieszczeniu” jako bezpośrednim trybie adresowania. Przykłady w tym rozdziale będą uzyskiwać dostęp do bajtu pamięci .Nie zapomnij jednak ,że możemy również uzyskać dostęp do słowa w procesorach 8086 (zobacz rysunek 4.9) Domyślnie ,wszystkie wartości „tylko przemieszczenia” dostarczają offsety do segmentu danych .Jeśli chcemy dostarczyć offset do innego segmentu, musimy użyć przedrostka przesłonięcia segmentu, przed naszym adresem .Na przykład ,aby uzyskać dostęp do komórki 1234h w ekstra segmencie(es) użyjemy instrukcji mov w postaci mov ax,es:[1234h].Podobnie ,aby uzyskać dostęp do komórki w segmencie kodu użyjemy instrukcji mov ax,cs:[1234h].Przedrostek ds. nie jest przesłonięciem segmentu. CPU używa rejestru segmentu danych domyślnie. Te określone przykłady wymagają ds: ponieważ jest to składniowe ograniczenie MASMa

Rysunek 4.9 Dostęp do słowa

Rysunek 4.10 Tryb adresowania [BX] 4.6.2.2 TRYB ADRESOWANIA POŚREDNIEGO PRZEZ REJESTR CPU 80x86 pozwala uzyskać dostęp do pamięci pośrednio przez rejestr używając trybu adresowania pośredniego przez rejestr .Są cztery formy tego trybu adresowania w 8086,najlepiej dowieść na następujących instrukcjach: mov al.,[bx] mov al.,[bp] mov al.,[si] mov al.,[di] Możemy użyć przedrostka przesłonięcia segmentu, jeśli mamy życzenie uzyskać dostęp do danych w różnych segmentach .Następujące instrukcje demonstrują użycie przesłonięcia: mov al.,cs:[bx] mov al.,ds:[bp] mov al.,ss:[si] mov al.,es:[di] Intel odnosi się do [bx] i [bp] jako trybu adresowania bazowego a bx i bp jako rejestrów bazowych(faktycznie bp oznacza wskaźnik bazowy).Intel odnosi się do trybu adresowania [si] i [di] jako trybu adresowania indeksowego (si oznacza indeks źródłowy ,di oznacza indeks przeznaczenia).jednak te tryby adresowania są funkcjonalnie równoważne.] Zauważ: tryb adresowania [si] i [di] pracują dokładnie w ten sam sposób ,jak si i di dla bx powyżej.

Rysunek 4.11 Tryb adresowania [BP] 4.6.2.3 TRYB ADRESOWANIA INDEKSOWY Tryb adresowania indeksowy używa następującej składni: mov al., disp[bx] mov al.,disp[bp] mov al.,disp[si] mov al.,disp[di]

Jeśli bx zawiera 1000h,wtedy instrukcja mov cl,20h[bx] załaduje cl z komórki pamięci ds.:1020h.Podobnie,jeśli bp zawiera 2020h, mov dh,1000h[bp] załaduje dh z komórki pamięci ss:3020. Offset wytworzony w tym trybie adresowym to suma stałej i wyspecyfikowanego rejestru. Tryby adresowania wymagające bx,si i di wszystkie używają segmentu danych, tryb adresowania disp[bp] używa domyślnie segmentu stosu. Przy korzystaniu z trybu adresowania pośredniego przez rejestr, możemy użyć przedrostków przesłonięcia segmentu dla wyspecyfikowania innego segmentu: mov al.,ss:disp[bx] mov al.,es:disp[bp] mov al.,cs:disp[si] mov al.,ss:disp[di] Możemy zastąpić si lub di z rysunku 4.12 trybem adresowania [si+disp] i [di+disp].Zauważ, że Intel odnosi się do tych trybów adresowania jako adresowania bazowego i adresowania indeksowego. Literatura Intela nie rozróżnia pomiędzy tymi trybami z lub bez stałej .Jeśli popatrzymy na to jako pracę sprzętową, jest to sensowna definicja. Z programistycznego punktu widzenia te tryby adresowania są całkowicie użyteczne ---------------------------------------------------------------------------------------------------------------------------------------ADRESOWANIE BAZOWE I INDEKSOWE W rzeczywistości jest subtelna różnica między bazowym a indeksowym adresowaniem. Oba tryby adresowania składają się z przesunięcia dodanego razem z rejestrem. Główną różnica między nimi jest względny rozmiar wartości przesunięcia i rejestru .W trybie adresowania indeksowego stała dostarcza adresu wyspecyfikowanej struktury danych a rejestr dostarcza offsetu względem tego adresu. W trybie adresowania bazowego, rejestr zawiera adres struktury danych a stała dostarcza przemieszczenia liczonego od tego punktu. ponieważ dodawanie jest przemienne, te dwie prezentacje są zasadniczo równoważne. ponieważ Intel obsługuje jeden i dwa bajty przemieszczenia (zobacz ”Instrukcja MOV 80x86”) większy sens będzie miało nazywanie ich trybem adresowania bazowego.

Rysunek 4.12 Tryb Adresowania [BX+disp]

Rysunek 4.13 Tryb Adresowania dla innych rzeczy .Dlatego ten tekst używa różnych terminów do ich opisu. Niestety jest bardzo mała jednomyślność w używaniu tych terminów w świecie 80x86. 4.6.2.4 TRYB ADRESOWANIA BAZOWY INDEKSOWANY Tryb adresowania bazowy indeksowany jest po prostu kombinacją trybu adresowania pośredniego przez rejestr. Te tryby adresowania tworzy się poprzez dodanie offsetu razem z rejestrem bazowym (bx lub bp) i rejestrem indeksowym (si lub di).Dopuszczalne formy tego trybu adresowania to: mov al., [bx] [si] mov al., [bx] [di] mov al., [bp] [si] mov al., [bp] [di] Przypuśćmy ,że bx zawiera 1000h a si 880h.Wtedy instrukcja mov al.,[bx] [si] będzie ładowała al. z komórki DS:1880h.Podobnie jeśli bp zawiera 1598h a di 1004, mov ax,[bp+di] załaduje 16 bitów w ax z komórki SS:259C i SS:259D. Tryb adresowania który nie wymaga bp używa domyślnie segmentu danych .Jeśli używamy bp jako operandu, domyślnie używamy segmentu stosu. Podstawimy di w rysunku 4.12 uzyskamy tryb adresowania [bx+di].Podstawiamy di w rysunku 4.13 dla trybu adresowania [bp+di]. 4.6.2.5 TRYB ADRESOWANIA BAZOWY INDEKSOWANY Z PRZEMIESZCZENIEM Ten tryb adresowania jest drobną modyfikacją trybu adresowania bazowego /indeksowego z dodaniem ośmio- lub szesnastobitowej stałej. Poniżej są przykłady tego trybu adresowania (zobacz rysunek 4.14 i rysunek 4.15)

Rysunek 4.14 Tryb adresowania [BX+SI]

Rysunek 4.15 Tryb adresowania [BP+SI]

Rysunek 4.16 Tryb adresowania [BX+SI+disp]

Rysunek 4.17 Tryb adresowania [BP+SI+disp] mov mov mov mov

al.,disp[bx] [si] al.,disp[bx+di] al.,[bp+si+disp] al.,[bp] [di] [disp]

Możemy zamienić di w rysunku 4.16 i stworzymy tryb adresowania [bx+di+disp].Możemy zamienić di w rysunku 4.17 i stworzymy tryb adresowania [bp=di+disp].

Rysunek 4.18 Tablica generująca poprawne tryby adresowania 8086. Przypuśćmy, że bp zawiera 1000h,bx 2000h,si 120h a di 5.Wtedy mov al.,10h[bx+si] ładuje al spod adresu DS.:2130; mov ch,125h[bp+di] ładuje ch z komórki SS:112A; a mov bx,cs:2[bx][di] ładuje bx z komórki CS: 2007. 4.6.2.6 ŁATWY SPOSÓB NA ZAPAMIĘTANIA TRYBÓW ADRESOWANIA PAMIĘCI

Ogólna liczba poprawnych trybów adresowania w 8086 wynosi 17: disp,[bx],[bp],[si],[di],disp[bx],disp[bp],disp[si],disp[di],[bx][si],[bx][di],[bp][si],[bp][di],disp[bx][si],disp[bx][d i],disp[bp][di] i disp[bp][si].Możemy wprowadzić do pamięci wszystkie te formy żeby poznać ,który jest poprawny (i przez pominięcie, które są nieprawidłowe).Jednakże jest łatwiejszy sposób poza wprowadzaniem do pamięci tych 17 form. Rozpatrzmy tablicę z rysunku 4.18. Jeśli wybierzemy pozycję zero lub jeden z każdej kolumny i zakończymy przynajmniej jedną pozycją, otrzymamy prawidłowy tryb adresowania pamięci 8086.Kilka przykładów: • Wybierzmy disp z kolumny jeden, nic z kolumny drugiej,[di] z kolumny 3,otrzymamy disp [di] • Wybierzmy disp,[bx] i [di].Mamy disp[bx][di] • Pomińmy kolumny jeden i dwa, wybieramy [si].Mamy [si] • Pomińmy kolumnę jeden, wybieramy [bx],potem wybieramy [di].mamy [bx][di] Podobnie, jeśli mamy tryb adresowania, którego nie możemy zbudować z tej tablicy, wtedy nie jest to poprawne. Na przykład ,disp[dx][si] jest nielegalne, ponieważ nie możemy otrzymać [dx] z żadnej z kolumny powyżej. 4.6.2.7 KILKA KOŃCOWYCH UWAG O TRYBACH ADRESOWANIA 8086 Adres efektywny jest końcowym offsetem stworzonym przez obliczenia trybu adresowego. Na przykład ,jeśli bx zawiera 10h,adres efektywny dla 10h[bx] to 20h.Zauważmy,że termin adres efektywny występuje w prawie każdym omówieniu trybów adresowania 8086.Jest nawet specjalna instrukcja ładuj adres efektywny (lea) który oblicza adres efektywny. Nie wszystkie tryby adresowania są tworzone równo! Różne tryby adresowania mogą zabierać różną ilość czasu do obliczenia adresu efektywnego. Dokładne różnice zmieniają się z procesora na procesor. Generalnie, im bardziej złożony tryb adresowania tym dłużej trwa obliczanie adresu efektywnego. Złożoność trybu adresowania jest bezpośrednio powiązana z liczbą elementów w trybie adresowania .Na przykład, disp[bx][si] jest bardziej złożona niż [bx].Zobacz zbiór instrukcji w dodatkach do informacji odnośnie czasu cyklu różnych trybów adresowania na różnych procesorach 80x86. Pole przemieszczenia we wszystkich trybach adresowania z wyjątkiem „tylko przemieszczenie” może być ośmiobitową stałą ze znakiem lub 16 bitową stałą ze znakiem .Jeśli offset jest z zakresu -128...+127 instrukcje będą krótsze (a zatem szybsze) niż instrukcje z przemieszczeniem poza tym zakresem. Rozmiar wartości w rejestrze nie wpływa na czas wykonania lub rozmiar .Więc jeśli możemy wstawić dużą liczbę do rejestru(ów) i użyć małego przemieszczenia ,jest bardziej pożądane duża stała i mała wartość w rejestrze. Jeśli adres efektywny po obliczeniach tworzy wartość większą niż 0FFFFh,CPU ignoruje przepełnienie a wynik przechodzi cyklicznie z powrotem do zera. Na przykład, jeśli bx zawiera 10h,wtedy instrukcja mov al.,0FFFFh[bx] załaduje rejestr al. z komórki ds.:0Fh nie z komórki 1000Fh. W tym omówieniu widzieliśmy jak działają te tryby adresowe. Poprzednie omówienie nie wyjaśniło dla czego ich używamy. Przyjdzie to trochę później. tak długo jak wiemy jak każdy tryb adresowania wykonuje obliczanie swojego adresu efektywnego, będzie dobrze. 4.6.3 TRYB ADRESOWANIA REJESTRÓW 80386 Procesory 80386 (i późniejsze) posiadają 32 bitowe rejestry .Wszystkie osiem rejestrów ogólnego przeznaczenia ma swoje 32 bitowe odpowiedniki .Są to eax,ebx,ecx,edx,esi,edi,ebp i esp. Jeśli używamy procesora 80386 lub późniejszego, możemy używać tych rejestrów jako operandów dla kilku instrukcji 80386. 4.6.4 TRYBY ADRESOWANIA PAMIĘCI Procesor 80386 uogólnia tryby adresowania pamięci .Podczas gdy 8086 pozwalał nam tylko na użycie bx lub bp jako rejestrów bazowych a si lub di jako rejestrów indeksowych,80386 pozwala nam używać prawie każdego 32 bitowego rejestru ogólnego przeznaczenia jako rejestru bazowego lub indeksowego. Co więcej,80386 wprowadza nowy tryb adresowania indeksowego ze skalowaniem, który upraszcza uzyskanie dostępu do elementów tablic .Poza zwiększeniem do 32 bitów, nowy tryb adresowania w 80386 jest prawdopodobnie największym ulepszeniem chipu w stosunku do wcześniejszych procesorów. 4.6.4.1 TRYB ADRESOWANIA POŚREDNIEGO PRZEZ REJESTR W 80386 możemy wyspecyfikować każdy 32 bitowy rejestr ogólnego przeznaczenia kiedy używamy trybu adresowania pośredniego przez rejestr. [eax],[ebx],[ecx],[edx],[esi] i [edi] zapewniają domyślnie offset w segmencie danych. tryby adresowania [ebp] i [esp] używają domyślnie segmentu stosu. Zauważ ,że podczas pracy w 16 bitowym trybie rzeczywistym w 80386,offset w tych 32 bitowych rejestrach musi być z zakresu 0 ....0FFFFh.Nie możemy użyć wartości większych niż ta do której uzyskujemy dostęp więcej niż 64K w segmencie. Zauważ również musimy używać 32 bitowych nazw rejestrów .Nie możemy użyć nazw 16 bitowych .Następujące instrukcje demonstrują wszystkie poprawne formy:

mov al.,[eax] mov al.,[ebx] mov al.,[ecx] mov al.,[edx] mov al.,[esi] mov al.,[edi] mov al.,[ebp] ; używa SS domyślnie ---------------------------------------------------------------------------------------------------------------------------------------4.6.4.2 TRYBY ADRESOWANIA INDEKSOWY,BAZOWY INDEKSOWANY,BAZOWY INDEKSWANY Z PRZEMIESZCZENIEM 80386 Tryb adresowania indeksowego (pośrednio przez rejestr plus przemieszczenie) pozwala nam połączyć 32 bitowy rejestr ze stałą. Tryb adresowania bazowego/ indeksowego pozwala nam łączyć dwa 32 bitowe rejestry .W końcu tryb adresowania bazowy indeksowany z przemieszczeniem pozwala nam łączyć stałą i dwa rejestry do sformowania adresu efektywnego .Zapamiętajmy, że offset stworzony przez obliczenie adresu efektywnego musi być długi na szesnaście bitów jeśli działa w trybie rzeczywistym. W 80386 termin rejestr bazowy i rejestr indeksowy właściwie mają takie samo znaczenie .Kiedy łączymy dwa 32 bitowe rejestry w trybie adresowania ,pierwszy rejestr jest rejestrem bazowym a drugi rejestrem indeksowym .Jest to prawda bez względu na nazwy rejestrów. Zauważ ,że 80386 pozwala nam użyć tego samego rejestru jako obu, rejestru bazowego indeksowego, które są właściwie użyteczne czasami .Następujące instrukcje dostarczają reprezentatywnych przykładów trybów adresowania bazowego i indeksowego w różnymi wariantami składniowymi: mov al.,disp[eax] mov al.,[ebx+disp] mov al.,[ecx] [disp] mov al.,disp[edx] mov al.,disp[esi] mov al.,disp[edi] mov al.,disp[ebp] mov al.,disp[esp] Następujące instrukcje wszystkie używają trybu adresowania bazowego +indeksowego .Pierwszy rejestr w drugim operandzie jest rejestrem bazowym, drugim jest rejestr indeksowy.. Jeśli rejestr bazowy to esp lub ebp adres efektywny używa segmentu stosu. W przeciwnym razie ,adres efektywny używa segmentu danych. Zauważ, że wybór rejestru indeksowego nie wpływa na wybór segmentu domyślnego. mov mov mov mov mov mov mov mov

al.,[eax][ebx] al.,[ebx+edx] al.,[ecx][edx] al.,[edx][ebp] al.,[esi][edi] al.,[edi][esi] al.,[ebp+ebx] al.,[esp][ecx]

Naturalnie, możemy dodać przemieszczenie do powyższych trybów adresowania, stworzymy tryb adresowania bazowy indeksowany z przemieszczeniem .Następujące instrukcje pokazują reprezentatywne przykłady możliwych trybów adresowania: mov al., disp[eax][ebx] mov al., disp[ebx+ebx] mov al.,[ecx+edx+disp] mov al., disp[edx+ebp] mov al.,[esi][edi] [disp] mov al.,[edi][disp][esi] mov al., disp[ebp+ebx] mov al.,[esp+ecx][disp] Jest jedno ograniczenie w 80386 przy ustalaniu rejestru indeksowego. Nie możemy użyć rejestru esp jako rejestru indeksowego. Jest OK., jeśli użyjemy esp jako rejestru bazowego, ale nie jako rejestru indeksowego.

4.6.4.3TRYB ADRESOWANIA INDEKSWOEGO ZE SKALOWANIEM Tryby adresowania indeksowy, bazowy /indeksowy i bazowo indeksowy z przemieszczeniem są specjalnymi przypadkami trybu adresowania indeksowego ze skalowaniem. Te tryby adresowania są szczególnie użyteczne przy dostępie do elementów tablicy, chociaż nie są one ograniczone tylko do tego celu. Te tryby pozwolą nam pomnożyć rejestr indeksowy w trybie adresowania przez jeden, dwa, cztery lub osiem. Ogólna składnia tego trybu adresowania disp[index*n] [base][index*n] lub disp[base][index*n] gdzie „baza” i „indeks” reprezentują każdy z rejestrów ogólnego przeznaczenia 80386 a n jest to wartość jeden ,dwa, cztery lub osiem. 80386 oblicza adres efektywny przez dodanie disp ,bazy i indeks*n razem. Na przykład ,jeśli ebx zawiera 1000h a esi 4 wtedy mov al.,8[ebx][esi*4] ;ładuje AL. z komórki 1018h mov al.,1000h[ebx][ebx*2] ;ładuje AL. z komórki 4000h mov al.,1000h[esi*8] ;ładuje AL. z komórki 1020h Zauważ ,że 80386 rozszerza tryby adresowania indeksowy, bazowy indeksowany, bazowo indeksowany z przemieszczeniem naprawdę jako specjalne przypadki trybu adresowania indeksowego ze skalowaniem z n równym jeden. To znaczy, następujące pary instrukcji są absolutnie identyczne dla 80386: mov al.,2[ebx][esi*1] mov al.,2[ebx][esi] mov al.,[ebx][esi*1] mov al.,[ebx][esi] mov al.,2[esi*1] mov al.,2[esi] Oczywiście .MASM pozwala na mnóstwo różnych wariantów tych trybów adresowania .Poniżej mamy kilka możliwych przykładów: Disp [bx][si*2],[bx+disp][si*2],[bx+si*2+disp],[si*2+bx][disp], disp[si*2][bx],[si*2+disp][bx],[disp+bx][si*2] 4.6.4.4 KILKA KOŃCOWYCH UWAG O TRYBACH ADRESOWANIA W 80386 Ponieważ tryby adresowania 80386 są bardziej ortogonalne, mogą one dużo łatwiej wprowadzać do pamięci niż tryby adresowania 8086.Dla programistów pracujących na procesorach 80386 istnieje zawsze pokusa do pomijania trybów adresowania 8086 i używania wyłącznie zbiór 80386.Jednakże,jak zobaczymy w następnej sekcji ,tryby adresowania 8086 naprawdę są bardziej wydajne niż porównywalne tryby adresowania 80386.Zatem ważne jest aby poznać wszystkie tryby adresowania i wybrać tryb odpowiedni dla danego problemu. Kiedy używamy trybu adresowania bazowego/indeksowego i bazowego indeksowanego z przemieszczeniem w 80386 bez operacji skalowania, pierwszy rejestr staje się w trybie adresowania rejestrem bazowym a rejestr drugi rejestrem indeksowym. Jest to ważny punkt ponieważ wybór domyślnego segmentu dokonuje się przez wybór rejestru bazowego. Jeśli rejestr bazowy to ebp lub esp,80386 domyślnie używa segmentu stosu. We wszystkich innych przypadkach 80386 uzyskuje dostęp domyślnie do segmentu danych, nawet jeśli rejestr indeksowy to ebp. Jeśli używamy operatora indeksu skalowania(„*n”) w rejestrze ;ten rejestr jest zawsze rejestrem indeksowym bez względu na to gdzie pojawia się on w trybie adresowym:

Rysunek 4.19 Ogólna instrukcja MOV [ebx][ebp] [ebp][ebx] [ebp*1][ebx]

;używa domyślnie DS. ;używa domyślnie SS ;używa domyślnie DS.

[ebx][ebp*1] [ebp][ebx*1] [ebx*1][ebp] es:[ebx][ebp*1]

;używa domyślnie DS. ;używa domyślnie SS ; używa domyślnie SS ;używa ES

4.7 INSTRUKCJA MOV 80x86 Przykłady w tym rozdziale dość obszernie używają instrukcję (move) mov 80x86.Co więcej, instrukcja mov jest najpowszechniejszą instrukcją maszynową 80x86.Zatem,jest warte zachodu spędzić kilka chwil na omówieniu działania tej instrukcji. Jako odpowiednik x86,instrukcja jest bardzo prosta. Przybiera formę: mov przez, źródło Mov robi kopię Źródła i przechowuje tą wartość w Przez. instrukcja nie wpływa na oryginalną zawartość Źródła. Nadpisuje poprzednią wartość w Przez. Przeważnie ,operacje tej instrukcji można opisać wyrażeniem pascalowskim: Przez := Źródło; Ta instrukcja ma wiele ograniczeń .Dostaniemy pokaźną możliwość zajmowania się nimi przez cały czas studiowania języka asemblera 80x86.Zrozumienie dlaczego te ograniczenia istnieją, przypatrzymy się kodom maszynowym dla kilku różnych postaci tej instrukcji .Kodowanie dla instrukcji mov jest prawdopodobnie najbardziej złożoną w zbiorze instrukcji. Pomimo to ,bez studiowania kodu maszynowego tej instrukcji nie będziemy zdolni do jej docenienia. ani nie będziemy mieli pełnego zrozumienia jak pisać optymalne kody używając tej instrukcji. Zobaczymy dlaczego pracowaliśmy procesorami x86 w poprzednim rozdziale zamiast używać rzeczywistych instrukcji 80x86. Jest kilka wersji instrukcji mov. Mnemonik mov opisuje dwanaście różnych instrukcji w 80386.Najbardziej powszechne użycie instrukcji mov ma następujące binarne kodowanie pokazane na rysunku 4.19. Opcodem jest pierwsze osiem bitów instrukcji. Bity zero i jeden definiują szerokość instrukcji (8,16 lub 32 bity) i kierunek przeniesienia. Kiedy omawiamy specyficzne instrukcje w tym tekście zawsze wypełniamy wartości d i w dla siebie. Pojawiają się tu tylko dlatego,że prawie w każdym innym tekście na ten temat wymagane jest aby wypełnić te wartości. Następujące opcody są adresowane bajtem czule nazywanym bajtem „mod-reg-r/m.” przez większość programistów. Ten bajt wybiera z 256 różnych możliwych kombinacji operandów pozwalających generować instrukcje mov. Ogólna instrukcja mov przybiera trzy różne formy w języku asemblera: mov reg, pamięć mov pamięć, reg mov reg, reg Zauważ, że co najmniej jeden z tych operandów jest rejestrem ogólnego przeznaczenia. Pole reg w bajcie mod/reg/rm specyfikuje ten rejestr.(lub jeden z rejestrów jeśli używamy trzech powyższych form).Bit d (direction –kierunku) w opcodzie decyduje czy przechowywanie danych będzie w rejestrze (d=1) lub pamięci (d=0). Bity w polu reg pozwalają wyselekcjonować jeden z ośmiu różnych rejestrów.8086 wspiera 8 ośmiobitowych rejestrów i 8 szesnastobitowych rejestrów ogólnego przeznaczenia.80386 również wspiera osiem 32 bitowych rejestrów ogólnego przeznaczenia. CPU dekoduje znaczenie pole reg jak następuje:

Tablica 23:kodowanie bitu REG

Rozróżniając 16 i 32 bitowe rejestry,80386 i późniejsze procesory używając specjalnych przedrostków bajtów opcodów przed instrukcjami używającymi 32 bitowych rejestrów. W przeciwnym razie, kodowanie instrukcji następuje w ten sam sposób dla obu typów instrukcji. Pole r/m. ,wraz z polem mod, wybierają tryb adresowania. Pole mod jest kodowane jak następuje:

Tabela 24 Kodowanie MOD Pole mod wybiera między przesunięciem rejestr-do-rejestru i przesunięcia rejestr-do/z-pamięci. Wybiera również rozmiar przemieszczenia(zero, jeden, dwa lub cztery bajty) który występuje przy trybie adresowania pamięci. Jeśli MOD=00,wtedy mamy jeden z trybów adresowania bez przemieszczenia (pośrednie przez rejestr lub bazowe / indeksowe).Zauważmy szczególny przypadek gdzie MOD=00 a r/m.=110.Będzie to odpowiadało trybowi adresowaniu [bp].8086 używa tego kodowania dla trybu adresowania „tylko-przemieszczenie”. To oznacza, że nie jest prawdziwy tryb adresowania [bp] w 8086. Aby zrozumieć dlaczego możemy używać trybu adresowania [bp] w naszych programach, popatrzmy na MOD=01 i MOD=10 w powyższej tabeli Te bity próbują uruchomić tryby adresowania disp[reg] i disp [reg][reg].”Więc co? Nie jest to samo co tryb adresowania [bp]”racja. Jednak rozważmy następujące instrukcje: mov al.,0[bx] mov ah,0[bp] mov 0[si],al. mov 0[di],ah Te wyrażenia używając trybu adresowania indeksowego, wykonują takie same operacje jak ich odpowiedniki w trybie adresowania pośredniego przez rejestr (uzyskując to poprzez usunięcie przemieszczenia z powyższych instrukcji).Prawdziwa różnica między tymi dwoma formami jest to ,że tryb adresowania indeksowego jest długi na jeden bajt(jeśli MOD=01,długi na dwa bajty jeśli MOD=10) do utrzymania zerowego przemieszczenia. Ponieważ są one długie, te instrukcje mogą również pracować trochę wolniej. Ta cech 8086 – dostarcza dwa lub więcej sposobów osiągnięcia tej samej rzeczy-pojawia się w całym zbiorze instrukcji. MASM generalnie wybiera najlepsze formy instrukcji automatycznie .Jeśli zapiszemy powyższy kod i zasemblujemy go używając MASM, wygeneruje tryb adresowania pośredniego przez rejestr dla wszystkich instrukcji za wyjątkiem mov ah,0[bp].Wyemituje tylko jednobajtowe przemieszczenie które jest krótsze i szybsze niż ta sam instrukcja z zerowym przemieszczeniem dwubajtowym .zauważmy ,że MASM nie wymaga żeby zapisać 0[bp],możemy zapisać [bp] a MASM automatycznie wstawi bajt zero przed [bp]. Jeśli MOD nie równa się 11b,pole r/m. koduje tryb adresowania pamięci jak następuje:

Tablica 25 Kodowanie Pola R/M. Nie zapomnijmy ,że tryby adresowania wymagają aby z bp używał segmentu stosu (ss) domyślnie. Wszystkie inne używają domyślnie segmentu danych (DS.). Jeśli to omówienie namieszało nam w głowach ,nie widzieliśmy jeszcze gorszych rzeczy. Zapamiętajmy ,jest kilka trybów adresowania 8086.Przyjrzeliśmy się wszystkim trybom adresowania 80386.Prawdopodobnie zaczęliśmy rozumieć co znaczy, kiedy mówimy o złożonym zbiorze instrukcji .Jednak, ważnym pojęciem jest to,że możemy zbudować instrukcje 80x86 w ten sam sposób jak zbudowano instrukcje x86 w Rozdziale Trzecim – przez zbudowanie instrukcji bit po bicie .Po pełne szczegóły jak 80x86 koduje instrukcje zajrzymy do dodatków. 4.8 KILKA KOŃCOWYCH UWAG O INSTRUKCJI MOV Jest kilka ważnych faktów o których powinniśmy pamiętać ,przy instrukcji mov. Przede wszystkim, nie ma przesunięcia z pamięci do pamięci .Z tego samego powodu, nowi użytkownicy języka asemblera mają ciężko przyswoić ten punkt. Podczas gdy jest para instrukcji które wykonują przesunięcie z pamięci do pamięci ,ładowanie rejestru a potem przechowywania tego rejestru prawie zawsze wydajnie. Innym ważnym faktem do zapamiętania o instrukcji mov, jest to,że jest wiele różnych instrukcji mov które osiągają te same rzeczy. Podobnie, jest kilka różnych trybów adresowania których możemy używać przy dostępie do tej samej komórki pamięci. Jeśli jesteśmy zainteresowani pisaniem możliwie najkrótszych i najszybszych programów w asemblerze ,musimy stale być świadomi różnic między odpowiednim instrukcjami. W tym rozdziale zajmowaliśmy się głównie omawianiem ogólnej instrukcji mov, więc mogliśmy zobaczyć jak procesory 80x86 kodują tryby adresowania pamięci i rejestrów w instrukcji mov .Inne formy instrukcji mov pozwalają nam przenosić dane między 16 bitowymi rejestrami ogólnego przeznaczenia i rejestrami segmentowymi 80x86.Inne pozwalają nam załadować rejestry lub komórki pamięci stałymi. Te warianty instrukcji mov używają różnych opcodów .Po więcej szczegółów zajrzyjmy do kodowania instrukcji w Dodatku D Jest kilka dodatkowych instrukcji mov w 80386 które pozwalają nam załadować rejestry specjalnego przeznaczenia 80386.W tym tekście ich nie rozpatrywaliśmy .Są również instrukcje łańcuchowe w 80x86 które wykonują operacje pamieć do pamięci. Takie instrukcje pojawią się w następnym rozdziale. To nie są dobrym substytutem dla instrukcji mov. 4.11 PODSUMOWANIE Ten rozdział przedstawił organizację pamięci i strukturę danych 80x86.Nie jest to oczywiście kompletny kurs o strukturze danych, faktycznie ten temat pojawi się znowu później w Tomie Drugim. Ten rozdział omawia prymitywne i proste połączenie typów i danych i w jaki sposób deklarować i używać ich w naszych programach. Mnóstwo dodatkowych informacji na temat deklarowania i używania prostych typów danych pojawi się w „MASM :Dyrektywy i Pseudo-Opcody”. 8088,8086,80188,80186 i 80286 wszystkie dzielą powszechny zbiór rejestrów których używają typowe programy. Ten zbiór rejestrów zawiera rejestry ogólnego przeznaczenia :ax,bx,cx,dx,si,di,bp i sp; rejestry segmentowe: cs, ds .,es i ss; i rejestry specjalnego przeznaczenia ip i flagi. Te rejestry są szerokie na szesnaście bitów ,Te procesory maja również 8 ośmiobitowych rejestrów: al .ah .bl .bh .cl .ch .dl i dh które nakładają się na rejestry ax,bx,cx i dx .Zobacz: • 8086 Rejestry Ogólnego Przeznaczenia • 8086 Rejestry Segmentowe • 8086 Rejestry Specjalnego Przeznaczenia W dodatku,80286 wspiera kilka rejestrów specjalnego przeznaczenia do zarządzania pamięcią, które są użyteczne w systemie operacyjnym i innych programów z poziomu systemu. Zobacz: • Rejestry 80286 80386 i późniejsze procesory rozszerzają zbiór rejestrów ogólnego i specjalnego przeznaczenia do 32 bitów. Te procesory również dodają dwa dodatkowe rejestry segmentowe które możemy używać w programach użytkowych. Oprócz tej poprawy ,którą każdy program może wykorzystać, procesory 80386/486 mają również kilka dodatkowych rejestrów z poziomu systemu dla zarządzania pamięcią, uruchomieniowych i testujących procesor .Zobacz • Rejestry 80386/486 Rodzina 80x86 Intela używa rozbudowanych schematów adresowania pamięci, znanych jako adresowanie segmentowe które dostarcza symulowanych dwuwymiarowych adresów .Pozwala to nam zgrupować logicznie pokrewne bloki danych wewnątrz segmentu. Dokładny format tych segmentów zależy od tego czy CPU działa w trybie rzeczywistym czy chronionym. Większość programów DOSowskich działa w trybie rzeczywistym. Kiedy pracujemy w trybie rzeczywistym, bardzo łatwo jest konwertować logiczny(segmentowy) adres do

liniowego fizycznego adresu. Jednak, w trybie chronionym taka konwersja jest znacznie bardziej utrudniona. Zobacz: • Segmenty w 80x86 Z powodu sposobu odwzorowania adresu segmentowego do adresu fizycznego w trybie rzeczywistym ,jest całkiem możliwe mieć dwa różne adresy segmentowe które odnoszą się do tej samej komórki pamięci .Jednym rozwiązaniem tego problemu jest użycie adresu znormalizowanego .Jeśli dwa adresy znormalizowane ie mają tego samego wzoru bitów, wskazują różne adresy. Znormalizowane wskaźniki są używane kiedy porównujemy wskaźniki w trybie rzeczywistym .Zobacz: • Znormalizowane adresy w 80x86 Z wyjątkiem dwóch instrukcji,80x86 nie pracuje właściwie z pełnym 32 bitowym dresem segmentowym. Zamiast tego, używa rejestrów segmentowych do przechowania domyślnej wartości segmentu. Pozwoliło to projektantom z Intela zbudować dużo mniejszy zbiór instrukcji ponieważ adresy są tylko 16 bitowe(tylko część offsetowa) zamiast 32 bitowej długości.80286 i wcześniejsze procesory zawierają cztery rejestry segmentowe: cs,ds.,es iss;80386 i późniejsze procesory dostarczają sześć rejestrów segmentowych: cs,ds.,es,fs,gs i ss .Zobacz: • Rejestry segmentowe w 80x86 Rodzina 80x86 dostarcza wiele różnych sposobów dostępu do zmiennych, stałych i innych danych. Nazwa dla mechanizmu przez który uzyskujemy dostęp do komórki pamięci to tryb adresowania. Procesory 8088,8086 i 80286 dostarczają dużego zbioru trybów adresowania pamięci. Zobacz: • Tryby adresowania 80x86 • Tryb adresowania rejestrów 8086 • Tryb adresowania pamięci 8086 Procesor 80386 i późniejsze dostarczają rozszerzony zbiór trybów adresowania rejestrów i pamięci. Zobacz: • Tryby adresowania rejestrów 80386 • Tryby adresowania pamięci 80386 Większość powszechnych instrukcji 80x86 to instrukcje mov.Ta instrukcja wspiera większość trybów adresowania dostępnych w rodzinie procesorów 80x86.Dlatego też, instrukcja mov jest dobrą instrukcją kiedy studiujemy kodowanie i działanie instrukcji 80x86.Zobacz: • Instrukcja MOV 80x86 Instrukcja mov przybiera kilka ogólnych form, pozwalających nam przenosić dane między rejestrem a inną lokacją. Możliwe lokacje źródło / przeznaczenie zawiera: (1) inne rejestry,(2) komórki pamięci (używając generalnego trybu adresowania pamięci),(3) stałe (używając trybu adresowania natychmiastowego) i (4) rejestry segmentowe. Instrukcja mov pozwala przenosić dane między dwoma komórkami (chociaż nie możemy przenosić danych między dwoma komórkami pamięci) 4.12 PYTANIA 1) Chociaż procesory zawsze używają adresowania segmentowego, kodowanie instrukcji dla instrukcji ,takiej jak „mov AX,I” ma tylko 16 bitowy offset kodowany w opcodzie .Wyjaśnij 2) Adresowanie segmentowe jest najlepiej opisane jako schemat dwuwymiarowego adresowania. Wyjaśnij. 3) Skonwertuj następujące adresy logiczne do adresów fizycznych Załóż wszystkie wartości jako heksadecymalne i działanie w trybie rzeczywistym w 80x86: a) 1000:1000 b) 1234:5678 c) 0:1000 d) 100:9000 e) FF00:1000 f) 800:8000 g) 8000:800 h) 234:9843 i) 1111:FFFF j) FFFF:10 4) Doprowadź powyższe adresy do postaci znormalizowanej 5) Wymień wszystkie tryby adresowania pamięci w 80x86 6) Wymień wszystkie tryby adresowania w 80386 (i późniejszych) które nie są dostępne w 8086 (uzyj ogólnej formy jak disp[reg]),nie wyliczaj wszystkich możliwych kombinacji. 7) Oprócz trybu adresowania pamięci jakie inne dwa główne tryby adresowania są w 8086 8) Opisz powszechne użycie dla każdego z następujących trybów adresowania: a) rejestrów b) Tylko przemieszczenie c) Natychmiastowy d) Bezpośredni przez rejestr e) Indeksowany f) Bazowy indeksowany g) Bazowy indeksowany z przemieszczeniem h) Indeksowany ze skalowaniem

9) Podaj wzorzec bitów dla generowania instrukcji MOV (zobacz „Instrukcja MOV 80x86”),wyjaśnij dlaczego 80x86 nie wspiera operacji przesunięcia z pamięci do pamięci 10) Która z następujących instrukcji MOV nie jest obsługiwana przez opcod ogólnej instrukcję MOV Wyjaśnij. a) mov ax,bx b)mov ax,1234 c)mov ax,1 d) mov ax,[bx] e) mov ax, ds. f) mov [bx],2 11) Załóżmy ,że zmienna „I” jest offsetem 20h w segmencie danych .Dostarcz kodowania binarnego dla powyższych instrukcji 12) Co określa ,że pole R/M. specyfikuje rejestr albo pamięć jako operand? 13) Jakie pole w bicie REG-MOD-R/M. określa rozmiar przemieszczenia następujących instrukcji? Jaki rozmiar przemieszczenia wspiera 8086? 14) Dlaczego tryb adresowania „tylko z przemieszczeniem” nie wspiera wielokrotnego przemieszczenia rozmiarów? 15) Dlaczego nie chcielibyśmy zamieniać dwóch instrukcji „mov ax,[bx]” i „mov ax,[ebx}”? 16) Pewne instrukcje 80x86 przybierają kilka postaci .Na przykład, są dwie różne wersje instrukcji MOV, która ładuje rejestr wartością natychmiastową. Wyjaśnij dlaczego projektanci włączyli tą nadmiarowość do zbioru instrukcji? 17) Dlaczego nie jest to prawdziwy tryb adresowania [bp]? 18) Wymień wszystkie ośmiobitowe rejestry 80x86. 19) Wymień wszystkie 16 bitowe rejestry ogólnego przeznaczenia. 20) Wymień wszystkie rejestry segmentowe (te dostępne na wszystkich procesorach) 21) Opisz „specjalne przeznaczenie” każdego z rejestrów ogólnego przeznaczenia. 22) Wymień wszystkie 32 bitowe rejestry ogólnego przeznaczenia 80386/486/586 23) Jakie są związki pomiędzy 8,16 i 32 bitowymi rejestrami ogólnego przeznaczenia w 80386? 24) Jakie wartości pojawiają się w rejestrze flag 8086?Rejestrze flag 80286? 25) Które flagi są kodami stanu? 26) Który rejestr ekstra segmentu pojawia się w 80386 ale nie we wcześniejszych procesorach?

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK Konsultacje naukowe: NEKRO [email protected] [email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ PIĄTY: ZMIENNE I STRUKTURY DANYCH Rozdział Pierwszy omawiał podstawowe formaty danych w pamięci. Rozdział Trzeci omawiał jak system komputerowy fizycznie organizuje te dane. Ten rozdział kończy to omawianie poprzez połączenie koncepcji reprezentacji danych z ich rzeczywistą fizyczną reprezentacją. Jak sugeruje tytuł, ten rozdział, zajmie się dwoma głównymi tematami: zmiennymi i strukturami danych. Ten rozdział nie zakłada, że mamy jakąś znajomość struktur danych ,chociaż taka umiejętność byłaby użyteczna. 5.0 WSTĘP Ten rozdział omawia jak deklarować i uzyskać dostęp do zmiennych skalarnych, całkowitych, rzeczywistych, typów danych, wskaźników, tablic i struktur. Musimy opanować te tematy przed przejściem do następnego rozdziału. Deklarowanie i dostęp do tablic, wydaje się być problematyczne dla początkującego programisty asemblerowego. Jednakże, reszta tego tekstu zależy od zrozumienia tych struktur danych i ich reprezentacji w pamięci .Nie próbuj się prześlizgiwać przez ten materiał oczekując ,że nauczysz się go jeśli będziesz go potrzebował później .Potrzebujesz go teraz a próbowanie nauczyć się tego materiału wraz z późniejszymi materiałami, tylko pogmatwa ci w głowie. 5.1 KILKA DODATKOWYCH INSTRUKCJI:LEA,LES,ADD i MUL Celem tego rozdziału nie jest przedstawienie zbioru instrukcji 80x86.Jednak,są cztery instrukcje dodatkowe które okażą swoją przydatność przy omawianiu reszty tego rozdziału. Są to instrukcje ładuj adres efektywny (lea), ładuj adres dalekiego wskaźnika używając ES (LES), dodawanie całkowite (ADD) i mnożenie bez znaku (MUL).Te instrukcje razem z instrukcją mov dostarczają wszystkich niezbędnych możliwości przy dostępie do różnych typów danych omawianych w tym rozdziale. Instrukcja lea przyjmuje formę: lea reg16, pamięć reg16 jest 16 bitowym rejestrem ogólnego przeznaczenia. Pamięć, jest to komórka pamięci reprezentowana przez bajt mod/reg/rm (poza tym musi być komórką pamięci ,nie może być rejestrem). Ta instrukcja ładuje do 16 bitowego rejestru offset z komórki wyspecyfikowanej przez operand pamięci. lea ax,1000h[bx][si],na przykład, załaduje ax adresem z komórki pamięci wskazywanej przez 1000h[bx][si].Jest to oczywiście wartość 1000h+bx+si.Lea jest również całkiem użyteczną przy uzyskiwaniu adresu zmiennej. Jeśli mamy gdzieś w pamięci zmienną I, lea bx, I załaduje do rejestru bx adres (offset) z I. Instrukcja les przyjmuje formę: les reg16, pamięć32 Ta instrukcja ładuje rejestr es i jeden z 16 bitowych rejestrów ogólnego przeznaczenia z wyspecyfikowanego adresu pamięci. Zauważmy, że każdy adres pamięci który możemy wyszczególnić bajtem mod/reg/rm jest prawidłowy, ale tak jak dla instrukcji lea musi to być komórka pamięci nie rejestr. Instrukcja les ładuje wyszczególniony rejestr ogólnego przeznaczenia słowem spod danego adresu ,ładuje rejestr es z następnego słowa w pamięci. Ta instrukcja, i jej towarzysz lds (który ładuje ds.) są tylko instrukcjami dla maszyn 80386,które manipulują 32 bitami na raz.

Instrukcja add, podobnie jak jej odpowiednik w x86,dodaje dwie wartości w 80x86.Ta instrukcja przyjmuje kilka form. Jest pięć form na których się tu skoncentrujemy. Są to: add reg, reg add reg, pamięć add pamięć, reg add reg, stała add pamięć ,stała Wszystkie te instrukcje dodają drugi operand do pierwszego, sumę zachowując w pierwszym operandzie. Na przykład, add bx,5,oblicza bx:=bx+5. Ostatnią instrukcją jaką się zajmiemy jest instrukcja mul (mnożenie).Ta instrukcja ma tylko jeden operand i przybiera formę: mul reg / pamięć. Jest wiele ważnych szczegółów dotyczących mul, które w tym rozdziale pominiemy. Ze względu na omówienie, które nastąpi, założymy, że rejestr lub komórka pamięci jest 16 bitowym rejestrem lub komórką pamięci. W takim przypadku ta instrukcja oblicza dx:ax;=ax*reg/mem. Zauważmy, że nie ma bezpośredniego trybu dla tej instrukcji. 5.2 DEKLAROWANIE ZMIENNYCH W PROGRAMIE JĘZYKA ASSEMBLERA Chociaż prawdopodobnie się już domyślamy, że komórki pamięci i zmienne są w pewnym stopniu powiązane, ten rozdział nie będzie wychodził poza wyciąganie silnych podobieństw między nimi dwoma. Cóż, czas naprawić tą sytuację. Rozważmy następujący krótki (i bezużyteczny) program pascalowski: program useless (input, output) var i,j:integer; begin i:=10; write (‘Podaj wartość dla j:’); readln (j); i:=i*j+j*j; wrtieln(‘Wynik to’,j); end. Kiedy komputer wykona wyrażenie i:=10,zrobi kopię wartości 10 i jakoś zapamiętuję tą wartość dla późniejszego użycia. Osiągamy to tak, że kompilator rezerwuje miejsce w pamięci, specjalnie dla wyłącznego użytkowania zmiennej i .Zakładając, że kompilator określił arbitralnie lokację DS:10h dla naszego celu, możemy użyć instrukcji mov ds:[10h],10 aby to osiągnąć. Jeśli i jest szesnastobitowym słowem, kompilator prawdopodobnie przydzieli zmiennej j słowo startowe od lokacji 12h lub 0Eh.Zakładając,że jest to lokacja 12h,drugie wyznaczone wyrażenie w tym programie mogłoby wyglądać jak następuje: mov ax, ds:[10h] ;pobiera wartość z I mul ds:[12h] ;mnoży przez j mov ds:[10h],ax ;przechowuje w I (pomija przepełnienie) mov ax, ds:[12h] ;pobiera J mul ds:[12h] ;Oblicza J*J add ds:[10h],ax ;Dodaje I*J+J*J, przechowuje w I Chociaż jest kilka brakujących szczegółów w tym kodzie, jest dosyć prosty i możemy łatwo zobaczyć co będzie robił ten program. Teraz wyobraźmy sobie 5000 linijek programu takich jak ten, używający zmiennych takich jak: ds:[10h],ds:[12h],ds:[14h] itd. Czy chcielibyśmy umiejscowić wyrażenie, tam gdzie przypadkowo przechowujemy wynik obliczenia w j zamiast i? Dlaczego powinniśmy się martwić nawet ,że zmienna i jest pod lokacją 10h a j pod 12h?Dlaczego nie powinniśmy używać nazw takich jak i i j zamiast niepokoić się o te adresy numeryczne? Wydaje się sensowne przepisanie tego powyższego kodu tak: mov ax, i mul j mov i, ax mov ax, j mul j add i, ax Oczywiście możemy tak zrobić w języku asemblera! Istotnie, jedną z podstawowych zalet asemblera

takiego jak MASM, jest to, że pozwala nam na użycie nazw symbolicznych dla komórek pamięci. Ponadto asembler będzie nawet przydzielał lokacje do nazw automatycznie. Nie potrzebujemy się martwić faktem, że zmienna i jest rzeczywiście słowem z komórki pamięci DS:10h chyba ,że jesteśmy ciekawscy. Nie będzie to dla nas żadna niespodzianka, że ds będzie wskazywał na dseg segment w pliku SHELL.ASM. Istotnie, skonfigurujemy tak ds żeby wskazywał dseg jako jedną z pierwszych rzeczy kiedy wykona się główny program SHELL.ASM. Dlatego też, wszystko co musimy zrobić to powiedzieć asemblerowi żeby zarezerwował jakieś komórki dal naszych zmiennych w dseg i połączył offset wymienionych zmiennych z nazwami tych zmiennych. Jest to bardzo prosty proces i jest tematem kilku następnych sekcji. 5.3 DEKLAROWANIE I DOSTĘP DO ZMIENNYCH SKALARNYCH Skalarne zmienne przechowują proste wartości. Zmienne i i j z poprzedniej sekcji są przykładem zmiennych skalarnych. Przykładami struktur danych ,które nie są skalarne są tablice ,rekordy ,zbiory i listy .Te ostatnie typy danych są tworzone z wartości skalarnych. Są one typami zbiorowymi .Zobaczymy, typy zbiorowe trochę później ;najpierw musimy nauczyć się czegoś o typach skalarnych. Aby zadeklarować zmienną w dseg, musimy użyć wyrażenia takiego jak następujące: ByteVar byte ? ByteVar jest etykietą. Powinna się zaczynać w pierwszej kolumnie w segmencie dseg (to jest ,między wyrażeniami segmentem dseg i dseg ends).Dowiemy się wszystkiego o etykietach w kilku rozdziałach ,teraz możemy założyć, że większość poprawnych identyfikatorów w Pascalu/C/Adzie jest również ważnymi etykietami języka asemblera. Jeśli potrzebujemy więcej niż jedną zmienną w naszym programie, wprowadzamy dodatkową linię w segmencie dseg deklarującą te zmienne. MASM automatycznie przydzieli unikalne lokacji dla zmiennych (czyż nie byłoby zbyt dobrze mieć i i j umiejscowione teraz pod tym samym adresem?).Po deklaracji wymienionych zmiennych, MASM pozwala nam odnosić się do tych zmiennych przez nazwy zamiast przez lokacje w programie .Na przykład, po wprowadzeniu powyższych wyrażeń do segmentu danych (dseg),możemy używać instrukcji takich jak mov ByteVar, al. w programie. Pierwszej zmiennej, którą umieścimy w segmencie danych zostaje przydzielona komórka pamięci DS:0.Następnej zmiennej w pamięci zostaje przydzielona komórka za poprzednią zmienną. Na przykład, jeśli zmienna spod lokacji zero była zmienną bajtową ,następnej zmiennej zostaje przydzielona komórka pamięci spod DS:1.Jednak,jeśli pierwsza zmienna była słowem ,drugiej zmiennej zostaje przydzielona komórka pamięci DS:2 .MASM zawsze uważnie przydziela zmienne ,w taki sposób aby one na siebie wzajemnie nie zachodziły .Rozważmy następującą definicję dseg: dseg segment para public ‘data’ bytevar byte ? ;bajt rezerwuje bajty worvar word ? ;word rezerwuje słowa dwordvar dword ? ;dword rezerwuje podwójne słowo byte2 byte ? word2 word ? dseg ends MASM przydziela pamięć dla bytevar dla lokacji DS:0.Ponieważ bytevar jest długości jednego bajta ,następną dostępną komórką pamięci będzie DS:1.MASM,zatem,przydzieli pamięć dla wordvar od lokacji DS:1.Ponieważ słowo wymaga dwóch bajtów ,następna dostępna komórka pamięci po wordvar to DS:3,dla której MASM przydziela dwordvar. Dwordvar jest długości czterech bajtów, więc MASM przydziela pamięć dla byte 2 zaczynając od DS:7.Podobnie MASM przydziela pamięć dla word2 od lokacji DS:8.gdybyśmy wpisali inną zmienną po word2,MASM przydzieliłby dla niej lokację DS:0A. Kiedy będziemy się odnosić do jednej z powyższych nazw, MASM automatycznie zastąpi stosowny offset. Na przykład, MASM przetłumaczy instrukcję mov ax, wordvar jako mov ax ,ds:[1].Więc teraz możemy używać nazw symbolicznych i kompletnie pominąć fakt, że te zmienne są w rzeczywistości komórkami pamięci z odpowiednimi offsetami w segmencie danych. 5.3.1 DEKLAROWANIE I UŻYWANIE ZMIENNYCH BYTE Więc po co są właściwie zmienne? Cóż ,możemy oczywiście przedstawiać różne typy danych, które mają mniej niż 256 różnych wartości w pojedynczym bajcie. Obejmuje to kilka ważnych i często używanych typów danych wliczając w to typ danych znakowych, typ danych boolowskich, większość typów danych wyliczeniowych i mały typ danych całkowitych (ze znakiem i bez znaku), wystarczy tylko wymienić. Znaki w typowym kompatybilnym z IBM systemie używają ośmiobitowego zestawu znaków ASCII/IBM (zobacz „A Zestaw znaków ASCII/IBM „) 80x86 dostarcza bogatego zbioru instrukcji do manipulowania danymi

znakowymi .Nie jest to żadna niespodzianka ,że większość zmiennych bajtowych przechowuje dane znakowe. Typ danych boolowskich przedstawia tylko dwie wartości :prawda lub fałsz. Zatem, do przedstawienia wartości boolowskich wykorzystujemy pojedynczy bit.Jednakże,80x86 w rzeczywistości chce pracować z danymi przynajmniej o szerokości ośmiu bitów. W rzeczywistości używa ekstra kod do manipulowania pojedynczym bitem zamiast całym bajtem Dlatego ,powinniśmy używać całego bajtu dla przedstawiania wartości boolowskiej. Większość programistów używa wartości zero dla przedstawienia fałszu i jeden dla przedstawiania prawdy. Znacznik zera 80x86 wykonuje testy zero / nie zero bardzo łatwo .Zauważmy ,że ten wybór zera lub nie-zera jest głównie dla wygody. Możemy używać każdej z dwóch wartości (lub dwóch różnych zbiorów wartości) dla przedstawienia prawdy lub fałszu. Większość języków wysokiego poziomu, które wspierają typ danych wyliczeniowych przekształca je (dla użytku wewnętrznego) na liczby całkowite bez znaku .Pierwsza pozycja na liście to w zasadzie pozycja zero ,druga pozycja na liście to pozycja jeden, trzecia pozycja do dwa itd.) Na przykład, rozważmy następujący pascalowski typ wyliczeniowy: Kolory = (czerwony,niebieski,zielony,purpurowy,pomarańczowy,żółt,biały,czarny); Większość kompilatorów Pascala przydzieli wartość zero do czerwonego, jeden do niebieskiego itd. Później zobaczymy jak w rzeczywistości tworzy się własne dane wyliczeniowe w asemblerze. Wszystko czego potrzebujemy teraz, to jak przydzielić pamięć dla zmiennych które przechowują wartości wyliczeniowe .Ponieważ jest niemożliwe ,aby było więcej niż 256 pozycji danych wyliczeniowych ,możemy użyć pojedynczej zmiennej bajtowej dla przechowywania wartości. Jeśli ,powiedzmy ,mamy zmienną kolor typu kolory użyjemy instrukcji mov color,2,co oznacza to samo co kolor :=zielony w Pascalu.(Później nauczymy się jak używać bardziej sensownych wyrażeń, takich jak mov kolor, zielony dla przydzielenia koloru zielonego do zmiennej kolor). Oczywiście, jeśli mamy małą wartość całkowitą bez znaku (0..255) lub małą wartość całkowitą ze znakiem (-128..127) pojedyncza zmienna bajtowa jest najlepszym sposobem w większości przypadków .Zauważmy ,że większość programistów traktuje wszystkie typy danych z wyjątkiem liczb całkowitych ze znakiem jako wartości nieoznaczone. To znaczy, znaki ,wartości boolowskie, typy wyliczeniowe i liczby całkowite bez znaku są zawsze wartościami bez znakowymi. W bardzo specjalnym przypadku możemy potraktować znak jako wartość że znakiem, ale większość czasu znaki są wartościami bez znakowymi. Są trzy główne wyrażenia dla deklaracji zmiennej bajtowej w programie. Oto one: identyfikator db ? identyfikator byte ? identyfikator sbyte ? Identyfikator przedstawia nazwę naszej zmiennej bajtowej. ”db” jest starszym terminem, z przed pojawienia się MASM 6.x.Zobaczymy,że tej dyrektywy używano w innych programach (zwłaszcza tych, które nie używają MASM 6.x lub późniejszych) ale Microsoft uznał, że będzie to termin przestarzały; powinniśmy w zamian używać deklaracji byte lub sbyte. Deklaracja byte deklaruje zmienną bajtową bez znaku. Powinniśmy używać tej deklaracji dla wszystkich zmiennych bajtowych z wyjątkiem małych liczb całkowitych ze znakiem. Dla liczb całkowitych ze znakiem używamy dyrektywy sbyte (bajt ze znakiem). Kiedy zadeklarujemy jakieś zmienne bajtowe w tych wyrażeniach, możemy odnosić się do tych zmiennych wewnątrz naszego programu poprzez ich nazwy: i db ? j byte ? k sbyte ? mov i,0 mov j,245 mov k,-5 mov al. ,i mov j, al. itd. Chociaż MASM 6,x wykonuje małą ilość sprawdzeń zgodności typów, nie powinniśmy ulec wrażeniu, że język asemblera jest językiem z silną kontrola typów. Faktycznie MASM 6.x sprawdza tylko wartości które przemieszczasz by sprawdzić ,czy będą się mieścić w lokacji docelowej. Wszystkie następujące instrukcje są poprawne w MASM 6.x:

mov k,255 mov j,-5 mov i,-127 Ponieważ wszystkie z tych zmiennych są zmiennymi wielkości bajtu i wszystkie stale skojarzone i dopasowane do ośmiu bitów, MASM na szczęście zezwala na każde z tych wyrażeń. Jeszcze jeśli patrzymy na nie, są one logicznie niepoprawne. Co to znaczy przesunięcie -5 do zmiennej bajtowej bez znakowej? Ponieważ wartość bajtu ze znakiem musi być z zakresu od -128..127,co się zdarzy, kiedy przechowamy wartość 255 wewnątrz zmiennej bajtowej ze znakiem? Cóż ,MASM, po prostu skonwertuje te wartości do ich ośmiobitowych odpowiedników (-5 staje się 0FBh,255 staje się ) FFh [-1],itd.). Być może późniejsze wersje MASM wprowadzą silniejsze badanie zgodności typów wartości, które wkładamy do tych zmiennych albo nie. Jednakże ,powinniśmy zawsze pamiętać ,że zawsze będzie możliwe pominięcie tego sprawdzenia. Pozwoli nam to na pisanie poprawnych programów. Asembler nie pomaga nam tak jak Pascal czy Ada. Oczywiście, nawet jeśli asembler odrzuci takie wyrażenie, będzie łatwo obejść zgodność typów. Rozpatrzmy następującą sekwencję: mov al.,-5 ;jakaś liczba wyrażeń które nie wpływają na AL. mov j, al. Niestety nie ma sposobu, żeby asembler mógł nas poinformować, że przechowujemy nieprawidłową wartość w j. Rejestry ,z natury rzeczy, nie są znakowe ani bez znakowe. Dlatego też. asembler pozwala przechować rejestr wewnątrz zmiennej bez względu na wartość jaka może być w rejestrze. Chociaż asembler nie sprawdza czy oba operandy instrukcji są ze znakiem czy bez znaku, z dużą pewnością sprawdza ich rozmiar. Jeśli rozmiar nie zgadza się asembler zgłosi stosowny komunikat błędu. Następujące przykłady są niepoprawne: mov i, ax ;nie można przenieść 16 bitów do ośmiu mov i,300 ;300 przekracza 8 bitów mov k,-130 ;-130 przekracza osiem bitów Możemy zapytać „jeśli asembler rzeczywiście nie rozróżnia wartości ze znakiem i bez znaku, dlaczego zawracamy sobie nimi głowę? Dlaczego nie używać po prostu db cały czas? ”Cóż, są dwie przyczyny. Po pierwsze uczyni to nasze programy łatwiejsze do odczytania i zrozumienia jeśli jasno określimy (poprzez użycie byte i sbyte) które zmienne są ze znakiem a które bez znaku. Po drugie, kto mówił coś ,że asembler ignoruje czy zmienne są ze znakiem czy bez znaku? Instrukcja mov ignoruje ale są inne instrukcje ,które nie ignorują. W punkcie końcowym warto wspomnieć o sprawach dotyczących deklarowania zmiennych bajtowych .We wszystkich deklaracjach widzimy, że pole operandu instrukcji zawsze zawiera pytajnik. Pytajnik mówi asemblerowi, że zmienna powinna być pozostawiona niezainicjowaną kiedy DOS ładuje program do pamięci. Możemy wyspecyfikować wartość początkową dla zmiennej, która może być ładowana do pamięci przed rozpoczęciem wykonywania programu., poprzez zastąpienie znaku zapytania naszą wartością początkową. Rozważmy następującą deklarację zmiennej bajtowej: i db 0 j byte 255 k sbyte -1 W tym przykładzie, asembler inicjuje odpowiednio i, j i k zerem,255 i -1,kiedy program ładuje się do pamięci. Ten fakt okaże całą swoją użyteczność nieco później, zwłaszcza kiedy będziemy omawiali tablice .Asembler tylko sprawdzi rozmiar operandu Nie sprawdza ,aby upewnić się, że operand dla dyrektywy byte jest pozytywny lub, że wartość pola operandu sbyte jest z zakresu -128..127.MASM pozwala na wartość z zakresu -128..255 w polu operandu każdego z tych wyrażeń. W przypadku, gdy odniesiemy wrażenie, że nie istnieje rzeczywisty powód używania byte i sbyte w programie, powinniśmy zauważyć, że MASM czasami ignoruje różnice w tych definicjach .Debugger Microsoft CodeView nie. Jeśli zadeklarujemy zmienną jako wartość ze znakiem, CodeView wyświetli go jako taki (wliczając w to znak minus jeśli to konieczne).Z drugiej strony CodeView zawsze wyświetla zmienne db i byte jako wartości dodatnie. 5.3.2 DEKLAROWANIE I UŻYWANIE ZMIENNEJ WORD Większość programów 80x86 używa wartości słowa dla trzech rzeczy: 16 bitowych wartości całkowitych ze znakiem,16 bitowych wartości całkowitych bez znaku i offsetów (wskaźników).Z pewnością możemy używać słowa

dla mnóstwa innych rzeczy równie dobrze, ale te trzy przedstawiają typ danych słowa w większości programów .Ponieważ słowo jest największym typem danych jakim mogą się posługiwać procesory 8086,8088,80186,80188 i 80286,odkryjemy,że dla większości programów, słowo stanowi podstawę obliczeń. Oczywiście 80386 i późniejsze CPU pozwalają na 32 bitowe obliczenia ,ale wiele programów nie używa tych 32 bitowych instrukcji ponieważ są one ograniczone do uruchamiania na 80386 lub późniejszych CPU. Używamy wyrażenia dw, word lub sword do deklaracji zmiennej słowa. Następujący przykład zademonstruje ich użycie: NoSignedWord dw ? Unsignedord word ? SignedWord sword ? Initialized0 word 0 InitializedM1 sword -1 InitializedBig word 65535 InitializedOfs dw NoSignedWord Większość z tych deklaracji jest drobną modyfikacją deklaracji byte ,które widzieliśmy w ostatniej sekcji. Oczywiście ,możemy zainicjować każdą zmienną słowa wartością z zakresu -32768..65535 (związek zakresu dla stałych 16 bitowych ze znakiem i bez znaku).Ostatnia z powyższych deklaracji ,jest nowa .W tym przypadku, etykieta pojawia się w polu operandu (nazwa zmiennej NoSignedWord).Kiedy pojawia się etykieta w polu operandu, asembler zastąpi offset tej etykiety (wewnątrz segmentu zmiennych).Jeśli były one tylko deklaracjami w dseg i pojawiają się w tym porządku ,ostatnia z powyższych deklaracji zainicjuje InitializedOfs wartością zero ponieważ offset NoSignedWord to zero wewnątrz segmentu danych .Ta forma inicjacji jest całkiem użyteczna dla inicjacji wskaźników. Ale więcej o tym temacie później. Debugger CodeView rozróżnia zmienne dw / word i zmienne sword .Zawsze wyświetla wartość bez znaku jako dodatnią wartość całkowitą. Z drugiej strony ,będzie wyświetlał zmienne sword jako wartości ze znakiem (ze znakiem minus jeśli wartość będzie ujemna).Debuggowanie wspiera jeden z głównych powodów dla jakiego chcesz używać word lub sword 5.3.3 DEKLAROWNIE I UŻYWANIE ZMIENNYCH DWORD Możemy użyć instrukcji dd ,dword i sdword dla deklaracji czterobajtowych wartości całkowitych ,wskaźników i innych typów zmiennych. Takie zmienne używają wartości z zakresu -2,147,483,648..4,294,967,295 (związek z zakresu czterobajtowych zmiennych ze znakiem lub bez znaku).Użyjemy tych deklaracji podobnie jak deklaracji word: NoSignedDWord dd ? UnsignedDWord dword ? SignedDWord sword ? InitBig dword 4000000000 InitNegative sdword -1 InitPtr dd InitBig Ostatni przykład inicjuje podwójne słowo wskaźnikiem spod adresu segment :offset zmiennej InitBig. Jeszcze raz, warte jest podkreślenia, że asembler nie sprawdza typu tych zmiennych kiedy inicjuje je wartościami.. Jeśli wartość mieści się w 32 bitach, asembler ją zaakceptuje. Jednak sprawdzanie rozmiaru jest ściśle egzekwowane. Ponieważ tylko 32 bitowe instrukcje mov na procesorach wcześniejszych niż 80386 mają les i lds, otrzymamy błąd jeśli spróbujemy uzyskać dostęp do zmiennej dword na wcześniejszych procesorach używając instrukcji mov. Oczywiście ,nawet na 80386 nie możemy przenieść 32 bitowej zmiennej do 16 bitowego rejestru, musimy użyć 32 bitowego rejestru. Później ,nauczymy się manipulować 32 bitowymi zmiennymi ,nawet na 16 bitowych procesorach. Do tego czasu, będziemy udawać, że nie możemy. Zapamiętajmy, że CodeView rozróżnia pomiędzy dd /dword a sdword .Pozwoli nam to zobaczyć rzeczywistą wartość naszych zmiennych jaką mamy kiedy debuggujemy nasz program .CodeView tylko robi to ,jeśli użyjemy właściwej deklaracji dla naszych zmiennych .Zawsze używamy dword dla wartości bez znaku i dd lub dword (dword jest lepsze) dla wartości bez znaku. 5.3.4 DEKLAROWANIE I UŻYWANIE ZMIENNYCH FWORD,QWORD I TBYTE MASM 6.x również pozwala nam zadeklarować sześciobajtowe ,ośmiobajtowe i dziesięciobajtowe zmienne używające wyrażeń df / fword, dq / qword i dt. tbyte .Deklaracje używające tych wyrażeń były początkowo planowane dla wartości zmiennoprzecinkowych i BCD. Są lepsze dyrektywy dla zmiennych zmiennoprzecinkowych i nie musimy się martwić innymi typami danych które używają tych dyrektyw. To omówienie występuje tylko dla

zasady. Wyrażenia df /fword są głównie przydatne przy deklarowaniu 48 bitowych wskaźników w 32 bitowym trybie chronionym w 80386 i późniejszych CPU .Chociaż możemy używać tej dyrektywy do stworzenia przypadkowej sześciobajtowej zmiennej, są lepsze dyrektywy do tego celu .Powinniśmy używać tylko dyrektyw dla 48 bitowych dalekich wskaźników 80386. Dq /qword pozwala nam zadeklarować quadword (ośmio bajtową) wartość .Pierwotnym celem tej dyrektywy było tworzenie 64 bitowych zmiennych zmiennoprzecinkowych o podwójnej precyzji i 64 bitowej zmiennej całkowitej. Są lepsze dyrektywy dla tworzenia zmiennych zmiennoprzecinkowych. Ponieważ 64 bitowa zmienna całkowita, nie jest zbyt często wykorzystywana w CPU 80x86 (przynajmniej nie dotąd, dopóki Intel nie udostępni członków z rodziny 80x86 z 64 bitowymi rejestrami ogólnego przeznaczenia) Dyrektywa dt /tbyte alokuje 10 bajtową pamięć ,Są dwa rdzenne typy danych w rodzinie 80z87 (koprocesor matematyczny) które używają dziesięciobajtowych typów danych: wartość 10 bajtowa BCD i wartości zmiennoprzecinkowej o rozszerzonej precyzji (80 bitów).Ten tekst całkowicie pomija typ danych BCD .Jeśli chodzi o typ zmiennoprzecinkowy, są lepsze sposoby do ich tworzenia. 5.3.5 DEKLAROWANIE ZMIENNYCH ZMIENNOPRZECINKOWYCH REAL4,REAL8 I REAL10 Są dyrektywy, które powinniśmy używać, kiedy deklarujemy zmienne zmiennoprzecinkowe. Podobnie jak dd. dq i dt te wyrażenia rezerwują cztery, osiem lub dziesięć bajtów. Pole operandu dla tych wyrażeń może zawierać znak zapytania (jeśli nie chcemy inicjować zmiennej) lub może zwierać wartość inicjującą w postaci zmiennoprzecinkowej. Następujące przykłady demonstrują ich używanie: x real4 1.5 y real8 1.0e-25 z real10 -1.2594e+10 Zauważ ,że pole operandu musi zawierać ważną stałą zmiennoprzecinkową używając albo dziesiętnej albo heksadecymalnej notacji .W szczególności nie jest dozwolona stała całkowita .Asembler będzie protestował jeśli użyjemy operandu takiego jak: x real4 1 Prawidłowo będzie zmienić pole operandu na”1.0” Proszę zauważyć ,że potrzeba specjalnego sprzętu dla wykonania operacji zmiennoprzecinkowych (np. chip 80x87 lub 80x86z wbudowanym koprocesorem matematycznym).Jeśli taki sprzęt jest niedostępny ,musimy pisać oprogramowanie dla wykonywania operacji jak zmiennoprzecinkowe dodawanie, odejmowanie, mnożenie itp. .W szczególności nie możemy używać instrukcji add 80x86 dla dodawania dwóch wartości zmienno przecinkowych .W tym tekście będziemy omawiać arytmetykę zmiennoprzecinkową w późniejszych rozdziałach(zobacz „Arytmetyka Zmiennoprzecinkowa”).Pomimo to ,jest właściwe omówić jak zadeklarować zmienne zmiennoprzecinkowe w rozdziale o strukturze danych. MASM również pozwala nam użyć dd, dq i dt dla deklaracji zmiennych zmiennoprzecinkowych (ponieważ te dyrektywy rezerwują konieczną cztero, ośmio lub dziesięcio bajtową przestrzeń).Możemy nawet zainicjować takie zmienne zmiennoprzecinkowymi stałymi w polu operandu. Ale są dwie główne wady deklarowania zmiennych w ten sposób. Po pierwsze, jako bajty, słowa i podwójne słowa, debugger CodeView wyświetli tylko nasze zmienne zmiennoprzecinkowe właściwie jeśli użyjemy dyrektyw real4,real8 i real10.Jeśli użyjemy dd, dq lub dt, CodeView wyświetli nasze wartości jako cztero-, ośmio- lub dziesięcio bajtowe liczby całkowite bez znaku. innym, potencjalnym dużym problemem z używaniem dd, dq i dt jest to ,że pozwalają nam inicjować i stałymi całkowitymi i zmiennoprzecinkowymi (pamiętamy, że real4,real8 i real10 nie).Teraz widzimy jaka to dobra cecha ,na pierwszy rzut oka. Jednak całkowita reprezentacja dla wartości jeden nie jest tym samym co reprezentacja zmiennoprzecinkowa dla wartości 1.0.Więc jeśli przypadkiem wprowadzimy wartość „1” w pole operandu ,kiedy rzeczywiście miało być „1.0” asembler na szczęście to strawi i da nam nieprawidłowy wynik. W związku z tym powinniśmy zawsze używać wyrażeń real4,real8 i real10 dla deklaracji zmiennych zmiennoprzecinkowych. 5.4 TWORZENIE WŁASNYCH NAZW TYPÓW Z TYPEDEF Powiedzmy, że po prostu jesteśmy niezadowoleni z nazw, które Microsoft postanowił używać dla deklaracji bajtu, słowa ,podwójnego słowa, real i innych zmiennych. Powiedzmy ,że lubimy nazewnictwo Pascalowe lub nazewnictwo C. Chcemy używać terminów takich jak integer ,float, double, char ,boolean lub jakiekolwiek inne. Gdyby to był Pascal ,moglibyśmy przedefiniować nazwy w sekcji type programu .W C moglibyśmy użyć wyrażenia „#define” lub typedef do wykonania tego zadania. Cóż, MASM 6.x ma swoje własne wyrażenie typedef które również pozwala nam stworzyć aliasy tych nazw. Następujący przykład demonstruje jak wprowadzić jakieś zgodne pascalowskie nazwy do naszego programu w języku asemblera:

integer char boolean float colors

typedef sword typedef byte typedef byte typedef real4 typedef byte Teraz możemy zadeklarować nasze zmienne bardziej sensownymi wyrażeniami jak: i integer ? ch char ? FoundIt boolean ? X float ? HouseColor colors ? Jeśli jesteśmy programistami ADY,C lub FORTRANa (lub innych języków)możemy wybrać nazwę typu bardziej wygodną. Oczywiście, nie zmienia to ani na jotę sposobu w jaki 80x86 lub MASM reagują na te zmienne, ale pozwala to nam tworzyć programy które są łatwiejsze do odczytu i zrozumienia ponieważ nazwy typów są bardziej komunikatywne niż faktyczny, odpowiedni typ. Zauważmy, że CodeView szanuje odpowiednie typy danych. Jeśli zdefiniujemy wartość całkowitą jako typ sword, CodeView wyświetli zmienne typu całkowitego jako wartość z znakiem .Podobnie, jeśli zdefiniujemy float w znaczeniu real4,CodeView wyświetli jeszcze poprawnie zmienną float jako czterobajtową wartość zmienno przecinkową. 5.5 TYP DANYCH WSKAŹNIKOWYCH Niektórzy ludzie odnoszą się do wskaźników jako typu danych skalarnych, inni odnoszą się jako do zbiorowego typu danych. Ten tekst traktuje je jako typ danych skalarnych, pomimo, że wykazują właściwości obu ,skalarnego i zbiorowego typu danych. (po kompletny opis zbiorowych typów danych, zajrzyj do „Zbiorowe Typy Danych”). Oczywiście, zaczniemy od pytania: „Co to jest wskaźnik?” Prawdopodobnie mieliśmy do czynienia ze wskaźnikami po raz pierwszy w Pascalu,. C lub Adzie i prawdopodobnie doszliśmy do wniosku, że są straszne .Prawie każdy ma złe doświadczenia kiedy pierwszy raz zetknął się ze wskaźnikami w językach wysokiego poziomu .Spoko ,bez strachu! Wskaźniki są w rzeczywistości łatwe do opanowania w asemblerze. Poza tym, większość problemów ze wskaźnikami które mieliśmy, nie leżała po stronie samych wskaźników, ale raczej w listach powiązanych i strukturze drzewa danych, które próbowaliśmy z nimi implementować. Z drugiej strony wskaźniki ,mają mnóstwo zastosowań w języku asemblera ,nie mających nic wspólnego z listami powiązanymi, drzewami i innymi strasznymi strukturami danych .Istotnie proste struktury danych, takie jak tablice i rekordy, często wymagają użycia wskaźników. Więc jeśli mamy jakiś głęboko zakorzeniony strach przed wskaźnikami ,zapomnijmy o wszystkim co o nich wiemy. Nauczymy się, jak wspaniałe mogą być rzeczywiście wskaźniki. Prawdopodobnie najlepszym punktem startu jest zdefiniowanie wskaźnika. Więc dokładnie czym jest ten wskaźnik? Niestety języki wysokiego poziomu jak Pascal mają tendencję do ukrywania prostoty wskaźników za murem abstrakcji. To dodaje złożoności przestraszonym programistom ,ponieważ oni nie rozumieją o co chodzi. Teraz jeśli boimy się wskaźników ,cóż, zignorujmy je do czasu, kiedy zaczniemy pracować z tablicami. Rozważmy następującą deklarację tablicy w Pascalu: M:array [0..1023] of integer; Nawet jeśli nie znamy Pascala, koncepcja tu przedstawiona jest bardzo łatwa do zrozumienia. M jest tablicą 1024 liczb całkowitych w niej zawartych, indeksowanych od M[0] do M[1023].Każdy z elementów tablicy może przechowywać wartość całkowitą która jest niezależna od wszystkich innych .Innymi słowy, ta tablica daje nam 1024 różnych zmiennych całkowitych, do których odnosimy się poprzez jej numer (indeks tablicy) zamiast przez nazwę. Jeśli spotkamy program ,który ma wyrażenie M[0]:=100,prawdopodobnie nie musielibyśmy myśleć co się z tym dzieje .Wartość 100 jest przechowywana w pierwszym elemencie tablicy M. Teraz rozważmy następujące dwa wyrażenia: i:=0; (*zakładamy, że i to zmienna całkowita*) M[i]:=100; Powinniśmy się zgodzić bez większego wahania ,że te dwa wyrażenia wykonują dokładnie tą samą operację M[0]:=100;.Istotnie,prawdopodobnie chętnie zgodzimy się, że możemy używać każdego wyrażenia całkowitego z zakresu 0..1023 jako indeksów wewnątrz tej tablicy. Następujące wyrażenie wykonuje to samo zadanie jak nasze pojedyncze zadanie dla indeksu zero: i:=5; (*zakładamy, że wszystkie zmienne są całkowite*)

j:=10; k:=50; m[i*j-k]:=100; „Okay, więc co to jest wskaźnik?” myślimy prawdopodobnie .”Wszystkie te wyniki z zakresu wartości całkowitych 0..1023 są poprawne. Więc co? ”.Okay, a co myślisz o tym? M[1]:=0; M[M[1]}:=100; Łoł! Teraz kilka chwil na przetrawienie .Jednak ,gdy weźmiemy to sobie po woli, nabierze to sensu, i odkryjemy ,że te dwie instrukcje wykonują tą samą operację jaką wykonywaliśmy wcześniej .Pierwsze wyrażenie przechowuje zero w elemencie tablicy M[1].Drugie wyrażenie pobiera wartość z M[1] ,które jest całkowite, więc możemy go użyć jako indeks wewnątrz M.,i użyć tej wartości (zero) do kontroli gdzie jest przechowana wartość 100. Jeśli zaakceptujemy powyższe jako sensowne, być może dziwaczne, ale użyteczne pomimo to, wtedy nie będziemy mieli problemów ze wskaźnikami. Ponieważ M[1] jest wskaźnikiem! Cóż ,nie całkiem ,ale jeśli zmienimy M na pamięć i potraktujemy tą tablicę jako całą pamięć, to jest dokładna definicja wskaźnika. Wskaźnik jest po prostu komórką pamięci której wartość jest adresem (lub indeksem ,jeśli wolimy) jakiejś innej komórki pamięci. Wskaźniki są bardzo łatwe do deklarowania i używania w programach asemblerowych. Nie musimy nawet martwić się o indeksy tablicy lub o coś w tym rodzaju. Faktycznie ,jedyną komplikacją jaką będziemy napotykali jest to, że 80x86 wspiera dwa rodzaje wskaźników: bliskie wskaźniki i dalekie wskaźniki. Bliski wskaźnik jest to 16 bitowa wartość która dostarcza offset do segmentu. Może to być każdy segment ale generalnie używamy segmentu danych (dseg w SHELL.ASM).Jeśli mamy zmienną słowo p ,która zawiera 1000h,wtedy p „wskazuje” komórkę pamięci 1000h w dseg. Uzyskując dostęp do słowa na które wskazuje p ,możemy użyć następującego kodu: mov bx ,p ;ładuje BX wskaźnikiem mov ax,[bx] ;pobiera dane na które wskazuje p Przez załadowanie wartości z p do bx, kod ten ładuje wartość 1000h do bx (zakładając, że p zawiera 1000h,a a zatem wskazuje komórkę pamięci 1000h w dseg)Druga z powyższych instrukcji ładuje do rejestru ax słowo zaczynające się w komórce której offset pojawia się w bx. Ponieważ bx zawiera 1000h,więc ax będzie ładowany z komórek DS:1000 i DS:1001. Dlaczego więc nie ładujemy ax bezpośrednio z komórki 1000h używając instrukcji takiej jak mov ax ,ds:[1000h]? No cóż ,jest mnóstwo powodów. Ale podstawowym powodem jest to, że pojedyncza instrukcja zawsze ładuje ax z lokacji 1000h.O ile nie chcemy się bawić z samomodyfikującym się kodem ,,nie możemy zmienić komórki z której jest ładowany ax. Poprzednie dwie instrukcje ,jednak, zawsze ładują ax z komórki na którą wskazuje p. Jest bardzo łatwo zmienić to pod kontrolą programu., bez używania kodu samomodyfikującego . Faktycznie, prosta instrukcja mov p,2000h sprawi, że te dwie powyższe instrukcje ładują ax z komórki pamięci DS:2000 w następnym czasie w którym się wykonają .Rozważmy następujące instrukcje: lea bx,i mov p,bx

lea mov mov mov

bx,j p,bx bx,p ax,[bx]

Ten krótki przykład demonstruje dwie ścieżki wykonania tego programu. Pierwsza ścieżka ładuje zmienną p spod adresu zmiennej i (pamiętajmy, lea ładuje bx offsetem drugiego operandu)Druga ścieżka kodu ładuje p adresem zmiennej j. Obie ścieżki wykonania zbiegają się w ostatnich dwóch instrukcjach mov, które ładują ax i lub j w zależności od tego która ścieżka wykonania była zastosowana. Pod wieloma względami jest to jak parametr procedury w językach wysokiego poziomu np. Pascalu. Wykonanie tej samej instrukcji odwołuje się do różnych zmiennych w zależności od tego czyj adres (i lub j) pojawi się w p. Szesnastobitowe bliskie wskaźniki są małe, szybkie a 80x86 dostarcza wydajnych odwołań do ich

używania. Niestety, mają one jedną poważną wadę - możemy uzyskać dostęp tylko do 64K danych (jeden segment) kiedy używamy bliskich wskaźników .Dalekie wskaźniki przezwyciężają to ograniczenie kosztem stworzenia 32 bitowej długości. Jednakże, dalekie wskaźniki pozwalają nam na uzyskanie dostępu do każdej części danych gdziekolwiek w przestrzeni pamięci. Z tego powodu i z faktu, że Standardowa Biblioteka UCR używa wyłącznie dalekich wskaźników ten tekst będzie używał dalekich wskaźników większość czasu .Ale zapamiętajmy, że jest to decyzja oparta na próbie utrzymania rzeczy prostszymi. Kod, który używa bliskich wskaźników zamiast dalekich będzie krótszy i szybszy. Dostęp do danych ,do których odnosimy się przez 32 bitowy wskaźnik ,będzie musiał załadować część offsetową (mniej znaczące słowo) wskaźnika do bx, bp, si lub di a część segmentową do rejestru segmentowego (typowo es).Wtedy możemy uzyskać dostęp do obiektu używając trybu adresowania bezpośredniego. Ponieważ instrukcja les jest dogodna do tej operacji, jest to doskonały wybór dla ładowania es i jednego z powyższych czterech rejestrów wartością wskaźnika. Następujący przykładowy kod przechowuje wartość w al w bajcie wskazywanym przez daleki wskaźnik p: les bx,p ;ładuje p do ES:BX mov es:[bx],al ;przechowuje dalej al. Ponieważ bliskie wskaźniki są długości 16 bitów a dalekie wskaźniki są długości 32 bitów ,możemy po prostu użyć dyrektyw dw /word i dd /dword do alokowania pamięci dla naszych wskaźników (wskaźniki są z natury bez znakowe, więc nie możemy używać normalnie sword lub sdword dla deklaracji wskaźników). Jednakże, jest dużo lepszy sposób dla tego celu poprzez użycie wyrażenia typedef. Rozważmy następujące formy: typename typedef near ptr basetype typename typedef far ptr basetype W tych dwóch przykładach typename reprezentuje nazwy nowych typów ,które tworzymy ,podczas gdy basetype jest nazwą tego typu, który chcemy stworzyć dla wskaźnika. Spójrzmy na określone przykłady: nbytptr fbytptr colorsptr wptr intptr intHandle

typedef typedef typedef typedef typedef typedef

near ptr byte far ptr byte far ptr colors near ptr word near ptr integer near ptr intptr

(te deklaracje zakładają, że zostały zdefiniowane typy colors i integer, wyrażeniem typedef).Wyrażenie typedef z operandem near ptr tworzy 16 bitowy bliski wskaźnik. Z operandem far ptr tworzy 32 bitowy daleki wskaźnik. MASM 6.x ignoruje typy bazowe dostarczone po near ptr lub far ptr .Jednak ,CodeView używa typów bazowych by wyświetlić obiekt wskaźnika w jego poprawnej formie. Zauważmy, że możemy używać każdego typu jako bazowego dla wskaźników. Jak zademonstrował ostatni przykład, możemy nawet definiować wskaźnik do innego wskaźnika (uchwyt).CodeView wyświetlał by poprawnie obiekt zmiennej typu intHandle wskazujący na adres. Z powyższymi typami ,możemy teraz wygenerować zmienną wskaźnikową jak następuje: bytestr nbytptr ? bytesttr2 fbytptr ? CurrentCollor colorsptr ? CurrentItem wptr ? Last Int intptr ? Oczywiście możemy zainicjować te wskaźniki w czasie asemblowania ,jeśli wiemy gdzie będą wskazywały kiedy program rozpocznie się po raz pierwszy. Na przykład, możemy zainicjować zmienną bytestr offsetem MyString używającym następującej deklaracji: Bytestr

nbytptr

MyString

5.6 ZBIOROWE TYPY DANYCH Zbiorowe typy danych są to te zbudowane z innych (głównie skalarnych) typów danych. Tablica jest dobrym przykładem zbiorowego typu danych - jest zbiorem elementów, wszystkich tego samego typu. Zauważmy ,że zbiorowe typy danych nie muszą być złożone ze skalarnych typów danych, są tablice tablic, na przykład, ale ostatecznie możemy rozłożyć zbiorowych typ danych do podstawowego, skalarnego typu.

Ta sekcja omawia dwa z wielu powszechnych zbiorowych typów danych; tablice i rekordy. Jest trochę za wcześnie na omawianie innych bardziej zaawansowanych, złożonych typów danych. 5.6.1 TABLICE Tablice są prawdopodobnie najbardziej powszechnie używanymi zbiorowymi typami danych. Mimo to większość początkujących programistów ma bardzo słabe pojęcie jak tablice działają i związanymi z nimi sprawami. Zaskakujące jest jak wielu nowicjuszy (a nawet zawodowych) programistów postrzega tablice z kompletnie różnych perspektyw ,kiedy uczą się jak zastosować tablice na poziomie maszynowym. Abstrakcyjnie ,tablica jest sumą typów danych których członkowie (elementy) są tego samego typu. Wybór elementu z tablicy następuje poprzez indeks całkowity .Różne indeksy wybierają unikalne elementy z tablicy. Ten tekst zakłada, że indeksy całkowite są sąsiadujące

Rysunek 5.1 Implementacja jednowymiarowej tablicy (chociaż nie jest to wymagane).To znaczy, jeśli numer x jest poprawnym indeksem w tablicy i y jest również poprawnym indeksem, to jeśli x bswap eax < Obliczenia wymagające drugiej kopii AX > Możemy użyć tej techniki w 80486 dla uzyskania dwóch kopii ax, bx, cx ,dx ,si, di i bp. Musimy być bardzo ostrożni jeśli używamy tej techniki z rejestrem sp. Notka :przy konwertowaniu 16 bitowej wartości big endian do 16 bitowej wartości little endian używamy instrukcji 80x86 xchg. Na przykład, jeśli ax zawiera 16 bitową wartość big endian możemy zamienić ją na 16 bitową wartość little endian (lub vice versa) używając: xchg al., ah Instrukcja bswap nie wpływa na żadną z flag w rejestrze flag 80x86 6.4.3 INSTRUKCJA XLAT Instrukcja xlat przenosi wartość do rejestru al. w oparciu o tablicę połączeń w pamięci. Robi to następująco: temp := al+bx al=ds.:[temp] to znaczy bx wskazuje tablicę w bieżącym segmencie danych. Xlat zastępuje wartość w al bajtem spod offsetu oryginalnego w al. Jeśli al zawiera cztery, xlat zastępuje wartość w al piątą pozycją (offset cztery) w środku tablicy wskazywanej przez ds:bx. Instrukcja xlat przybiera formę: xlat Zazwyczaj nie ma operandu. możemy wyszczególnić jeden ale asembler praktycznie go zignoruje. Jedynym celem wyszczególnienia operandu jest to ,że możemy dostarczyć nadpisania przedrostka segmentu xlat esL Table Powie to asemblerowi aby wyemitował bajt es:przedrostek segmentu przed instrukcją. musimy jeszcze załadować bx adresem Table; powyższa forma nie dostarcza adresu Table do instrukcji. Tylko przedrostek przesłonięcia segmentu w operandzie jest znaczący. Instrukcja xlat nie wpływa na rejestr flag 80x86. 6.5 INSTRUKCJE ARYTMETYCZNE 80x86 dostarcza wielu operacji arytmetycznych: dodawanie ,odejmowanie ,negacja, mnożenie ,dzielenie / modulo (reszta) i porównywanie dwóch wartości. Instrukcje wykonujące te operacje to add, adc, sub

,sbb, mul ,imul ,div, idiv, cmp, neg, inc, dec, xadd, cmpxchg i kilka różnych instrukcji konwersji: aaa, aad, aam, aas i das. Następne sekcje opisują te instrukcje szczegółowo. Ogólne formy dla tych instrukcji to:

6.5.1 INSTRUKCJE DODAWANIA: ADD,ADC,INC.XADD,AAA I DAA Instrukcje te przybierają formy: add reg, reg add reg, mem add mem, reg add reg, dana natychmiastowa add mem, dana natychmiastowa add eax/ax/al., dana natychmiastowa formy adc są identyczne jak ADD inc reg inc mem inc reg16 xadd mem, reg xadd reg, reg aaa daa Zauważmy ,że instrukcje aaa i daa używają niejawnego trybu adresowania i nie zawierają operandów. 6.5.1.1 INSTRUKCJE ADD I ADC Składnia add i adc (dodawanie z przeniesieniem) jest podobna do mov. Podobnie jak mov, są specjalne formy dla rejestrów ax /eax które są bardziej wydajne. W odróżnieniu od mov ,nie możemy dodać wartości do rejestru segmentowego tymi instrukcjami. Instrukcja add dodaje zawartość operandu źródłowego do operandu przeznaczenia. Na przykład, add ax, bx, dodaje bx do ax zostawiając sumę w ax. Add oblicza dest := dest +source podczas gdy Adc oblicza dest := dest +source +C gdzie C przedstawia wartość flagi przeniesienia (carry).Dlatego też jeśli flaga przeniesienia jest wyczyszczona przed wykonaniem, adc zachowuje się dokładnie jak instrukcja add. Obie instrukcje wpływają na flagi identycznie. Ustawiają flagi jak następuje: * Flaga przepełnienia oznacza przepełnienie liczby ze znakiem * Flaga przeniesienia oznacza przepełnienie liczby bez znaku

* Flaga znaku oznacza wynik ujemny (tj. najbardziej znaczący bit wyniku wynosi jeden) * Flaga przeniesienia połówkowego zawiera jeden jeśli przepełnienie BCD występuje w mnij znaczącym nibblu * Flaga parzystości jest ustawiona lub wyczyszczona w zależności od parzystości najmniej znaczących ośmiu bitów wyniku. Jeśli jest parzysta liczba bitów jeden w wyniku, instrukcja ADD ustawi flagę parzystości na jeden Jeśli jest nieparzysta liczba bitów w wyniku, instrukcja ADD wyzeruje flagę Instrukcje add i adc nie wpływają na żadne inne flagi. Instrukcje add i adc uznają ośmio- szesnasto i (w 80386 i późniejszych CPU) trzydziesto dwu bitowe operandy. Obydwa operandy źródłowy i przeznaczenia muszą być tego samego rozmiaru .Zobacz Rozdział Dziewiąty jeśli chcesz dodawać operandy których rozmiar jest różny. Ponieważ nie ma dodawania pamięci do pamięci, musimy załadować operand pamięci do rejestru jeśli chcemy dodać dwie zmienne razem. Następujący przykładowy kod demonstruje możliwe formy dla instrukcji add: ; J:=K+M mov ax, K add ax, M. mov J, ax Jeśli chcemy dodać kilka wartości razem ,możemy łatwo obliczyć sumę w pojedynczym rejestrze: : J:=K+M+N+P mov ax, K add ax, M add ax, N add ax, P mov J, ax Jeśli chcemy zredukować liczbę przypadków na procesorze 80486 lub Pentium możemy użyć kodu takiego jak ten: mov bx, K mov ax, M add bx, N add ax, P add ax, bx mov J, ax Jedną rzeczą o której często zapominają początkujący programiści asemblerowi jest to, że możemy dodać rejestr do komórki pamięci. Czasami początkujący programiści nawet wierzą, że oba operandy muszą być w rejestrach ,kompletnie zapominając lekcje z Rozdziału Czwartego. 80x86 jest procesorem CISC, który pozwala nam używać trybów adresowania pamięci z różnymi instrukcjami jak add. Często jest bardziej wydajnie wykorzystać potencjał adresowania pamięci ; J:=K+J mov ax, K add J, ax ;Początkujący często kodują powyższe jako jedną z dwóch powyższych sekwencji ; To jest zbyteczne! mov ax, J ;rzeczywiście zły sposób obliczania mov bx, K ;J:=J+K add ax, bx mov J, ax mov ax, J ;lepiej, ale jeszcze nie dobry sposób add ax, K ;obliczania J :=J+K mov J, ax Oczywiście jeśli chcemy dodać stałą do komórki pamięci, potrzebujemy pojedynczej instrukcji.80x86 pozwala nam bezpośrednio dodać stałą do pamięci: ; J := J+2 add J, 2 Są specjalne formy instrukcji add i adc ,które dodają bezpośrednio stałe do rejestru al., ax lub eax. Formy te są krótsze niż standardowa instrukcja add reg, dana bezpośrednia. Inne instrukcje również dostarczają

krótszych form, kiedy używają tych rejestrów; dlatego też, powinniśmy utrzymywać obliczenia w rejestrze akumulatora (al., ax i eax) tak długo jak możliwe. add bl, 2 ;długa na trzy bajty add al., 2 ;długa na dwa bajty add bx, 2 ;długa na cztery bajty add ax, 2 ;długa na trzy bajty itd. Inne sprawy związane z używaniem małych znakowych stałych z instrukcjami add i adc. Jeśli wartość jest z zakresu –128..127, instrukcje add i adc powielają znak ośmiobitowej stałej natychmiastowej do koniecznego rozmiaru operandu przeznaczenia (osiem, szesnaście lub trzydzieści dwa bity)Dlatego powinniśmy próbować używać małych stałych, jeśli to możliwe, z instrukcjami add i adc 6.5.1.2 INSTRUKCJA INC Instrukcja INC (increment – zwiększenie) dodaje jeden do własnego operandu.Za wyjątkiem flagi przeniesienia, inc ustawia flagi w ten sam sposób jak add operand, 1. Zauważmy, że są dwie formy inc dla 16 i 32 bitowego rejestru. Są to instrukcje inc reg i inc reg 16.Instrukcje inc reg i inc mem są takie same. Ta instrukcja składa się z opcodu bajtu określonego przez bajt mod-reg-r/m. (zobacz Appendix D po szczegóły).instrukcja inc reg16 ma pojedynczy opcod bajtu. Dlatego jest krótsza i zazwyczaj szybsza. Operand inc może być ośmio- szesnasto- lub trzydziesto dwu bitowym rejestrem lub komórką pamięci.. Instrukcja inc jest często szybsza niż odpowiadająca jej instrukcja add reg, 1 lub add mem, 1.Faktycznie,instrukcja inc reg16 jest długa na jeden bajt, więc okazuje się, że dwie takie instrukcje są krótsze niż porównywalne instrukcje add reg, 1;jednak dwie instrukcje zwiększania będą pracowały wolniej na bardziej nowoczesnych członkach rodziny 80x86. Instrukcja inc jest bardzo ważna ponieważ dodawanie jeden do rejestru jest bardzo powszechną operacją. Zwiększanie zmiennych sterowania pętlą lub indeksowanie wewnątrz tablicy jest bardzo popularną operacją, doskonałą dla instrukcji inc .Fakt, że inc nie wpływa na flagę przeniesienia jest bardzo ważny. Pozwala to nam zwiększać indeksy tablicy bez wpływania na wynik operacji arytmetycznych o zwielokrotnionej precyzji (zobacz „Arytmetyczne i Logiczne Operacje” po więcej szczegółów o arytmetyce o zwielokrotnionej precyzji) 6.5.1.3 INSTRUKCJA XADD Xadd (wymian i dodawanie) jest inną instrukcją 80486 (i późniejszych) procesorów. Nie pojawiła się w 80386 i wcześniejszych procesorach, instrukcja ta dodaje operand źródłowy do operandu przeznaczenia a sumę przechowuje w operandzie przeznaczenia. Jednak przed przechowaniem sumy kopiuje oryginalną wartość operandu przeznaczenia w operandzie źródłowym. następujący algorytm opisuję tę operację; xadd przez , źródło temp :=przez przez:= przez+ źródło źródło:= temp Xadd ustawia flagi tak jak instrukcja add. Instrukcja xadd pozwala na ośmio- szesnasto- i trzydziesto dwu bitowe operandy. Oba operandy, źródłowy i przeznaczenia muszą być tego samego rozmiaru. 6.5.1.4 INSTRUKCJE AAA I DAA Instrukcje AAA (Modyfikowanie ASCII po dodawaniu) i DAA (Modyfikowanie dziesiętne dla dodawania) wspierają arytmetykę BCD. Poza tym rozdziałem ten tekst nie stosuje arytmetyki BCD lub ASCII ponieważ jest stosowana głównie dla sterowników aplikacji a nie ogólnego zastosowania programowania aplikacji. Wartości BCD są dziesiętnymi wartościami całkowitymi kodowanymi binarnie z jedną cyfrą dziesiętną na nibble’a. Wartość ASCII (numeryczna) zawiera pojedynczą cyfrę dziesiętną na bajt, bardziej znaczący nibble bajtu powinien zawierać zero. Instrukcje aaa i daa modyfikują wynik binarnego dodawania do właściwego dla arytmetyki ASCII lub dziesiętnej. Na przykład, dodanie dwóch wartości BCD, dodamy je jak gdyby były wartościami binarnymi a potem wykonamy instrukcję daa w celu korekcji otrzymanego wyniku. Podobnie, możemy użyć instrukcji aaa dla modyfikacji wyniku dodawania ASCII wykonaniu instrukcji add .Proszę zapamiętać, że te dwie instrukcje zakładają, że operandy dodawania były właściwymi wartościami dziesiętnymi lub ASCII. Jeśli dodamy binarnie (nie- dziesiętnie lub nie- ASCII) wartości razem i spróbujemy zmodyfikować je tymi instrukcjami, nie otrzymamy poprawnego wyniku. Wybór nazwy „arytmetyka ASCII” jest niefortunny, ponieważ te wartości nie są prawdziwymi znakami ASCII. Nazwa taka jak „nie upakowane BCD” byłaby bardziej odpowiednia. Jednak Intel używa nazwy ASCII,

więc ten tekst też będzie to robił aby uniknąć nieporozumień. Jeśli będziemy słyszeli termin „nie upakowane BCD” będzie chodziło o ten typ danych. Aaa (która zazwyczaj wykonuje się po instrukcjach add, adc lub xadd) sprawdza wartość w al. dla przepełnienia BCD. Wykonuje to według takiego algorytmu:

Instrukcja aaa jest użyteczna głównie dla dodawania łańcuchów cyfr gdzie jest dokładnie jedna cyfra dziesiętna na bajt w łańcuchu liczbowym. Ten tekst nie będzie się zajmował łańcuchami liczbowymi BCD i ASCII, więc możemy spokojnie zignorować te instrukcje teraz. Oczywiście, możemy użyć instrukcji aaa w każdej chwili jeśli musimy zastosować powyższy algorytm, ale będzie to raczej wyjątkowa sytuacja. Instrukcja daa funkcjonuje podobnie jak aaa z wyjątkiem tego, że operuje wartościami upakowanego BCD zamiast jedną cyfrą na bajt nie upakowanych wartości stosowanych przez aaa. Podobnie jak aaa, głównym celem daa jest dodawania łańcuchów cyfr BCD (z dwoma cyframi na bajt) Algorytm dla daa:

6.5.2 INSTRUKCJE ODEJMOWANIA: SUB,SBB,DEC,AAS I DAS Instrukcje sub (odjąć),sbb (odjąć z pożyczką),dec (zmniejszanie),aas (modyfikowanie ASCII dla odejmowania) i das (modyfikowanie dziesiętne dla odejmowania) działają tak jak tego oczekujemy. Ich składnia jest bardzo podobna do tej z instrukcji add: sub reg, reg sub reg, mem sub mem, reg sub reg, dana bezpośrednia sub mem, dana bezpośrednia sub eax, ax, al., dana bezpośrednia forma sbb jest identyczna jak sub dec reg dec mem dec reg16 aas das

Instrukcja sub oblicza wartość przez := przez – źródło. Instrukcja sbb oblicza przez := przez – źródło – C. Zauważmy, że odejmowanie nie jest przemienne. Jeśli chcemy obliczyć wynik dla przez := źródło – przez musimy użyć kilku instrukcji ,zakładając, że musimy zachować operand źródłowy). Jednym z tematów wartym omówienia jest to jak instrukcja sub wpływa na rejestr flag 80x86.Instrukcje sub, sbb i dec wpływają na flagi jak następuje: • Ustawiają flagę zera jeśli wynik jest zero. Występuje to jeśli operandy są różne dla sub i sbb. Instrukcja dec ustawia flagę zera tylko kiedy zmniejsza wartość jeden • Instrukcje te ustawiają flagę znaku jeśli wynik jest ujemny • Ustawiają flagę przepełnienia jeśli wystąpi przepełnienie /niedomiar ze znakiem • Ustawiają flagę przeniesienia połówkowego jeśli to konieczne dla arytmetyki BCD/ASCII • Ustawiają flagę parzystości według liczby bitów jeden występujących w wartości wyniku • Instrukcje sub i sbb ustawiają flagę przeniesienia jeśli wystąpi przepełnienie bez znaku. Zauważmy ,że instrukcja dec nie wpływa na flagę przeniesienia Instrukcja aas, podobnie jak jej odpowiednik aaa, pozwala nam działać na łańcuchach liczb ASCII z jedną cyfrą dziesiętną ( z zakresu 0..9) na bajt. Będziemy używali tej instrukcji po instrukcji sub lub sbb na wartości ASCII. Instrukcja ta używa następującego algorytmu:

Instrukcja das wykonuje te same operacje dla wartości BCD gdy używa algorytmu:

Ponieważ odejmowanie nie jest przemienne nie możemy używać instrukcji sub tak swobodnie jak instrukcji add. Poniższy przykład demonstruje na jakie problemy możemy się natknąć:

Zauważmy, że instrukcje sub i sbb podobnie jak add i adc dostarczają krótkich form do odejmowania stałych z rejestru akumulatora (al., ax lub eax).Z tej przyczyny powinniśmy próbować trzymać operacje arytmetyczne w rejestrze akumulatora tak długo jak to możliwe .Instrukcje sub i sbb dostarczają również krótszych form kiedy odejmujemy stałe z zakresu –128..+127 z komórki pamięci lub rejestru. Instrukcje te automatycznie powielają znak ośmiobitowej wartości ze znakiem do koniecznego rozmiaru przed wykonaniem odejmowania. Zajrzyj do Appendix D po szczegóły. W praktyce ,nie potrzeba instrukcji które odejmują stałe z rejestru lub komórki pamięci – dodanie wartości ujemnej da ten sam wynik. Niemniej jednak Intel dostarczył instrukcji bezpośrednich. Po wykonaniu instrukcji sub, bity kodów błędu (przeniesienia, znaku, przepełnienia i zera) w rejestrze flag zawierają wartości które możemy przetestować aby zobaczyć czy jeden z operandów sub jest równy, nie równy, mniejszy niż, mniejszy niż lub równy, większy niż lub większy niż lub równy dla inne operacji. Zobacz instrukcję cmp po więcej szczegółów. 6.5.3 INSTRUKCJA CMP Instrukcja cmp (porównanie) jest podobna do instrukcji sub z jednym zasadniczym wyjątkiem – nie przechowuje różnicy w operandzie przeznaczenia. Składnia dla instrukcji cmp jest bardzo podobna do sub, ogólna forma cmp przez, źródło Określone formy: cmp reg, reg cmp reg, mem cmp mem, reg cmp reg, dana bezpośrednia cmp mem, dana bezpośrednia cmp eax/ax/al., dana bezpośrednia Instrukcja cmp uaktualnia flagi 80x86 według wyników operacji odejmowania (przez – źródło).Możemy przetestować wynik porównania poprzez sprawdzenie właściwych flag w rejestrze flag. Po szczegóły na temat jak to się robi, zajrzyj do „Ustawienia Instrukcji Warunkowych” i „Instrukcje skoków warunkowych”. Zazwyczaj chcielibyśmy wykonać instrukcję skoku warunkowego po instrukcji cmp. Te dwa kroki procesu, porównanie dwóch wartości i ustawienie bitów flag, potem testowanie bitów flag przez instrukcje skoków warunkowych jest bardzo wydajnym mechanizmem dla podejmowani decyzji przez program. Prawdopodobnie pierwszą rzeczą od jakiej zaczniemy badanie instrukcji cmp jest przyjrzenie się jak instrukcja cmp wpływa na flagi. Rozpatrzmy następującą instrukcje cmp: cmp ax, bx Instrukcja ta dokonuje obliczenia ax – bx i ustawia flagi w zależności od wyniku obliczenia. Flagi są ustawione jak następuje: Z:

flaga zera jest ustawiona jeśli i tylko jeśli ax = bx. jest to jedyny mement kiedy ax – bx tworzy w wyniku zero. W związku z tym, możemy użyć flagi zero to sprawdzenia równości bądź nierówności. S: flaga znaku jest ustawiona na jeden jeśli wynik jest ujemny. Na pierwszy rzut oka, możemy pomyśleć, że flaga będzie ustawiona jeśli ax jest mniejsze niż bx, ale nie jest tak zwykle. Jeśli ax = 7FFFh a bx =-1 (0FFFFh) odjęcie ax od bx da nam 8000h,które jest ujemne (więc flaga znaku będzie ustawiona)Więc, dla porównania liczb całkowitych ze znakiem flaga znaku nie zawiera właściwego stanu. Dla operandu bez znakowego, rozważmy ax=0FFFFh i bx =1.Ax jest większe niż bx ale ich różnica wynosi 0FFFEh czyli jest jeszcze negatywna. Okazuje się, że flaga znaku i flaga przepełnienia, wzięte razem mogą być używane dla porównania dwóch wartości ze znakiem. O: flaga przepełnienia jest ustawiana po operacji cmp jeśli różnica między ax i bx tworzy przepełnienie lub niedomiar. Jak wspomniano powyżej, flaga znaku i flaga przepełnienia są używane kiedy wykonujemy porównanie ze znakiem. C: flaga przeniesienia jest ustawiana po operacji cmp jeśli odejmowanie bx od ax wymaga pożyczki. Zdarza się to tylko wtedy kiedy ax jest mniejsze niż bx gdzie ax i bx są wartościami bez znaku. Instrukcja cmp również wpływa na flagi parzystości i przeniesienia połówkowego, ale rzadko będziemy testować te dwie flagi po operacji porównania. Dajmy na to, że instrukcja cmp ustawi flagi w ten sposób, możemy spróbować porównać dwa operandy z następującymi flagami: cmp Operand1, Operand2

Tablica 27: Ustawienia wskaźników po CMP Aby zrozumieć dlaczego te flagi są ustawione w ten sposób, rozważmy następujący przykład:

Pamiętajmy, że operacja cmp jest w rzeczywistości odejmowaniem, dlatego też, pierwszy przykład powyżej oblicza (-1)-(-2) co daje (+1).wynik jest dodatni a przepełnienie nie występuje więc flagi S i O mają zero. Ponieważ (S xor O) wynosi zero,Operand1 jest większy niż lub równy Operandowi2. W drugim przykładzie, instrukcja cmp będzie obliczała (-32768) – (+1) co daje (-32769).Ponieważ 16 bitowa wartość całkowita ze znakiem nie może przedstawić tej wartości, wartość zawija się do 7FFFh (+32767) i ustawia flagę przepełnienia. Ponieważ wynik jest dodatni (przynajmniej zawartości 16 bitów) flaga znaku jest wyzerowana. Ponieważ (S xor O) wynosi jeden,Operand1 jest mniejszy niż Operand2. W trzecim przykładzie, cmp oblicza (-2)-(-1) czyli mamy (-1).Nie występuje żadne przepełnienie więc flaga O wynosi zero, wynik jest ujemny więc flaga znaku ma wartość jeden. Ponieważ (S xor O) to jeden,Operand1 jest mniejszy niż Operand2. W czwartym ( i końcowym) przykładzie, cmp oblicza (+32767) –(-1).Tworzy to (+32768),ustawia flagę przepełnienia. Co więcej wartość zawija się do 8000h (-32768) więc flaga znaku jest również ustawiona. Ponieważ (xor O) wynosi zero,Operand1 jest większy niż lub równy Operandowi2. 6.5.4 INSTRUKCJE CMPXCHG I CMPXCHG8B Instrukcja cmpxchg (porównanie i wymiana) jest dostępna tylko na 80486 i późniejszych procesorach. Ich składnia: cmpxchg reg, reg cmpxchg mem, reg Operandy muszą być tego samego rozmiaru (osiem, szesnaście lub trzydzieści dwa bity).Ta instrukcja również używa rejestru akumulatora: automatycznie wybiera al ,ax lub eax dopasowując do rozmiaru operandów. Ta instrukcja porównuje al, ax lub eax z pierwszym operandem i ustawia flagę zera jeśli są równe. Jeśli tak, wtedy cmpxchg kopiuje drugi operand do pierwszego. Jeśli nie są równe, cmpxchg kopiuje pierwszy operand do akumulatora. Następujący algorytm opisuje tą operację:

Cmpxchg wspiera pewne struktury danych systemu operacyjnego wymagające operacji atomowych (są to operacje, których system nie może przerwać) i semafory .Oczywiście, jeśli możemy wstawić powyższy algorytm do naszego kodu, możemy użyć instrukcji cmpxchg jako właściwą. Notka: w odróżnieniu od instrukcji cmp, instrukcja cmpxchg wpływa tylko na flagę zera 80x86.Nie możemy testować flag po cmpxchg podobnie jak po instrukcji cmp. Procesor Pentium wspiera 64 bitową instrukcję porównania i wymiany – cmpxchg8b.Jej składnia: cmpxchg8b ax, mem64 Instrukcja ta porównuje 64 bitową wartość w edx:eax z wartością pamięci. Jeśli są równe, Pentium przenosi ecx:ebx do komórki pamięci, w przeciwnym razie ładuje edx:eax z komórki pamięci .Instrukcja ta ustawia flagę zera według wyniku. nie wpływa na żadne inne flagi. 6.5.5 INSTRUKCJA NEG Instrukcja NEG (negacja) stosuje uzupełnia do dwóch bajtu lub słowa. bierze pojedynczą (przeznaczenie) operację i neguje ją .Składnia dla tej instrukcji: neg przeznaczenie Wykonuje następujące obliczenie: dest := 0 –dest To skutecznie odwraca znak operandu przeznaczenia. Jeśli operand to zero, jego znak nie zmienia się ,chociaż czyści flagę przeniesienia. Negowanie każdej innej wartości ustawia flagę przeniesienia. Negowanie bajtu zawierającego –128,słowa zawierającego –32768 lub podwójnego słowa zawierającego –2,147,483,648 nie zmienia operandu, ale ustawi flagę przepełnienia. NEG zawsze uaktualnia flagi A,S,P i Z podobnie kiedy używaliśmy instrukcji sub. Dostępne postacie to; neg reg neg mem Operandy mogą być ośmio ,szesnasto lub (na 80386 i późniejszych) trzydziesto dwu bitowe. Kilka przykładów: ; J := -J neg J ;J := -K mov ax, K neg ax mov J, ax 6.5.6 INSTRUKCJE MNOŻENIA: MUL,IMUL I AAM Instrukcje mnożenie dostarczają nam możliwość poczucia nieregularności w zbiorze instrukcji 80x86.instrukcje takie jak add, adc, sub i wiele innych w zbiorze instrukcji 80x86 używa bajtu mod-reg-r/m. dla wsparcia dwóch operandów. Niestety, nie ma dość bitów w bajtach opcodach 80x86 aby wesprzeć wszystkie instrukcje, więc 80x86 używa bitów reg w bajcie mod-reg-r/m. jako rozszerzenia opcodu. Na przykład inc ,dec i neg nie wymagają dwóch operandów, więc CPU 80x86 używają bitów reg jako rozszerzenia do ośmiu bitów opcodu.. Pracuje to świetnie dla pojedynczych operandów instrukcji, pozwalając projektantom Intela kodować kilka instrukcji (w rzeczywistości, osiem) z pojedynczym opcodem. Niestety instrukcje mnożenia wymagają specjalnego traktowania a projektanci Intela nadal pragnęli skracać opcody więc zaprojektowali instrukcje mnożenia do używania z pojedynczym operandem. Pole reg zawiera rozszerzony opcod zamiast wartości rejestru .Oczywiście mnożenie jest funkcją dwóch operandów. Ta nieregularność czyni stosowanie mnożenia w 80x86 trochę bardziej trudniejszym niż innych instrukcji ponieważ jeden operand musi być w rejestrze akumulatora .Intel zaadoptował to nie ortogonalne podejście ponieważ uznali ,że programiści będą używali mnożenia w dużo mniejszym stopniu niż instrukcji takich jak add czy sub. Jedynym problemem z dostarczeniem tylko formy mod-reg-r/m. instrukcji jest to, że nie możemy pomnożyć rejestru akumulatora przez stałą; bajt mod-reg-r/m. nie wspiera bezpośredniego trybu adresowania .Intel szybko odkrył, że musi wesprzeć mnożeni przez stałą i zmienił to w procesorze 80286.Było to szczególnie ważne przy dostępie do wielowymiarowych tablic. Zanim pojawił się 80386,Intel uogólnił jedną postać operacji mnożenia przeznaczoną dla standardowego operandu mod-reg-r/m. Są dwie formy instrukcji mnożenia: mnożenie bez znaku (mul) i mnożenie ze znakiem (imul).W odróżnieniu od dodawania i odejmowania, musimy oddzielić instrukcje dla tych dwóch operacji.. Instrukcje mnożenia przyjmują następujące formy: Mnożenie Bez Znaku; mul reg mul mem

Mnożenie Ze Znakiem (Całkowite): imul reg imul mem imul reg, reg, dana bezpośrednia imul reg, mem, dana bezpośrednia imul reg, dana bezpośrednia imul reg, reg imul reg, mem Operacja Mnożenia BCD: aam Jak możemy zobaczyć, instrukcje mnożenia są prawdziwie nieuporządkowane. Gorzej jeszcze ,musimy używać procesorów 80386 lub późniejszych aby otrzymać prawie pełną funkcjonalność. W końcu, jest kilka ograniczeń w tych instrukcjach, nie tak oczywistych powyżej. Niestety, jedyny sposób wykorzystania tych instrukcji to wprowadzenie tych operacji do pamięci. Mul dostępna na wszystkich procesorach, mnoży bez znakowe 8- 16 lub 32 bitowe operandy .Zauważmy, że kiedy mnożymy dwie n-bitowe wartość ,wynik może wymagać 2*n bitów. Dlatego też, jeśli operand jest ośmio bitową wielkością ,wynik będzie wymagał szesnaście bitów. Podobnie, operand 16 bitowy tworzy 32 bitowy rezultat a 32 bitowy operand wymaga 64 bitów jako wyniku. Instrukcja mul ,z ośmio bitowym operandem, mnoży rejestr al., przez operand i przechowuje 16 bitowy wynik w ax. Więc mul operand8 lub imul operand8 oblicza: ax := al.* operand8 „*” przedstawia mnożenie bez znaku dla mul i mnożenie ze znakiem dla imul. Jeśli wyszczególnimy 16 bitowy operand, wtedy mul i imul obliczają: dx:ax := ax* operand16 „*” ma takie samo znaczenie jak powyżej a dx:ax oznacza, że dx zawiera bardziej znaczące słowo 32 bitowego wyniku a ax zawiera mniej znaczące słowo 32 bitowego wyniku. Jeśli wyszczególnimy 32 bitowy operand, wtedy mul i imul obliczają co następuje: Edx:eax := eax * operand32 „*” ma takie samo znaczenie jak powyżej a edx:eax oznacza, że edx zawiera bardziej znaczące podwójne słowo z 64 bitowego wyniku a eax zawiera mniej znaczące podwójne słowo z 64 bitowego wyniku Jeśli iloczyn 8x8,16x16 lub 32x32 bitów wymaga więcej niż, (odpowiednio) osiem, szesnaście lub trzydzieści dwa bity, instrukcje mul i imul ustawiają flagi przeniesienia i przepełnienia. Mul i imul pozmieniają flagi A,P,S i Z. Zwłaszcza zauważmy, że flagi znaku i zera nie zawierają znaczących wartości po wykonaniu tych dwóch instrukcji. Imul (mnożenie całkowite) działa na operandach ze znakiem. Jest wiele różnych form tej instrukcji ponieważ Intel próbował uogólnić tą instrukcję w kolejnych procesorach. Poprzedni paragraf omawiał pierwszą formę instrukcji imul, z pojedynczym operandem. następne trzy formy instrukcji imul są dostępne tylko na procesorach 80286 i późniejszych. dostarczają one zdolności do mnożenie rejestry przez wartość bezpośrednią. Ostatnie dwie formy, dostępne tylko na 80386 i późniejszych procesorach, dostarczają zdolności mnożenia przypadkowego rejestru przez inny rejestr lub komórkę pamięci.

Instrukcje imul reg, dana bezpośrednia jest specjalną składnią dostarczoną przez asembler. Kodowanie dla tych instrukcji jest takie same jak imul reg, reg, dana bezpośrednio. Asembler po prostu dostarcza taką samą wartość rejestru dla obu operandów.

Instrukcje te obliczają: operand1 := operand2 * dana bezpośrednia operand1 := operand1 * dana bezpośrednia Poza liczbą operandów, jest kilka różnic miedzy tymi formami a pojedynczym operandem instrukcji mul/ imul: • Nie ma dostępnego mnożenia bitów 8x8 (operand8 bezpośredni po prostu daje prostszą formę tej instrukcji. Wewnętrznie, CPU powiela znak operandu do 16 lub 32 bitów jeśli to konieczne). • instrukcje te nie tworzą wyniku 2*n bitów. To znaczy, mnożenie 16x16 daje wynik 16 bitowy. podobnie mnożenie 32x32 daje 32 bitowy wynik. Instrukcje te ustawiają flagi przeniesienia przepełnienia jeśli wynik nie mieści się w rejestrze przeznaczenia. • Wersja 80286 instrukcji imul pozwala na operand bezpośredni, standardowe instrukcje mul /imul nie. Ostatnie dwie formy instrukcji imul są dostępne tylko na procesorach 80386 i późniejszych. Z tymi dodatkowymi formami, instrukcja imul jest prawie tak ogólna jak instrukcja add: imul reg, reg imul reg, mem Instrukcje te obliczają reg := reg * reg i reg := reg * mem Oba operandy muszą być tego samego rozmiaru. Dlatego też, podobnie jak forma dla 80286 instrukcji imul, musimy sprawdzić flagi przeniesienia i przepełnienia do wykrycia przepełnienia. Jeśli wystąpiło przepełnienie, CPU gubi bardziej znaczące bity wyniku. Ważna Uwaga: Zapamiętajmy, że flaga zera zawiera nieokreślony wynik po wykonaniu instrukcji mnożenia. Nie możemy przetestować flagi zera aby zobaczyć czy wynik to zero po mnożeniu. Podobnie instrukcje te zmieniają flagę znaku. jeśli musimy sprawdzić te flagi ,porównamy wynik do zera po przetestowaniu flag przeniesienia i przepełnienia. Instrukcja aam (Modyfikacja ASCII Po Mnożeniu),podobnie jak aaa i aas, pozwala nam modyfikować nie upakowane dziesiętnie wartości po mnożeniu. Instrukcja ta operuje bezpośrednio na rejestrze ax. Zakładając, że pomnożymy razem dwie wartości ośmiobitowe z zakresu 0..9 a wynik jest usytuowany w ax (w rzeczywistości wynik jest usytuowany w al, ponieważ 9*9 daje nam 81,największą możliwą wartość; ah musi zawierać zero).Instrukcja ta dzieli ax przez 10 i zostawia iloraz w ah a resztę w al: ah := ax div 10 al. := ax mod 10 W odróżnieniu do innych instrukcji modyfikacji dziesiętnej/ ASCII, program asemblerowy regularnie używa aam ponieważ konwersja pomiędzy podstawami liczb używa tego algorytmu. Notka: instrukcja aam składa się z dwóch bajtów opcodu, drugi z nich jest stałą bezpośrednią 10.Programiści asemblerowi odkryli, że jeśli zastąpi tą stałą inną wartością bezpośrednią, możemy zmienić dzielnik w powyższym algorytmie. Jest to jednak cecha nie udokumentowana. Działa ona na wszystkich odmianach procesorów Intela, tworząc dane ,ale nie ma gwarancji, że Intel będzie wspierał ją w następnych procesorach.Ocywiście80286 i późniejsze procesory pozwalają nam mnożyć przez stała, więc ta sztuczka jest prawie nie potrzebna w nowoczesnych systemach. Nie ma instrukcji dam (modyfikowanie dziesiętne dla mnożenia) w procesorach 80x86 Być może najbardziej użytecznym zastosowaniem instrukcji imul jest obliczanie offsetów w tablicach wielowymiarowych. Rzeczywiście, jest to prawdopodobnie główny powód dla którego Intel dodał zdolność mnożenia rejestru przez stałą w procesorze 80286.W Rozdziale Czwartym, ten tekst używa standardowej instrukcji mul dla obliczania indeksów tablic. Jednakże, rozszerzona składnia instrukcji imul daje nam lepszy wybór jak pokazuje następujący przykład:

Nie zapomnijmy, że instrukcje mnożenia są bardzo wolne; często bywają wolniejsze niż instrukcje dodawania. Są szybsze sposoby mnożenia wartości przez stałą. Zobacz :”Mnożenie bez MUL i IMUL” po więcej szczegółów. 6.5.7 INSTRUKCJE DZIELENIA: DIV,IDIV I ADD Instrukcje dzielenia 80x86 wykonują dzielenie 64/32 (tylko 80386 i późniejsze),32/16 lub 16/8.Instrukcej te przyjmują formę: div reg dla dzielenia bez znakowego div mem idiv reg dla dzielenia ze znakiem idiv mem aad modyfikowanie ASCII dla dzielenia Instrukcja div wykonuje dzielenie bez znakowe. Jeśli operand jest ośmiobitowym operandem, div dzieli rejestr ax przez operand zostawiając iloraz w al. a reszta (modulo) w ah. Jeśli operand jest 16 bitową wielkością, wtedy instrukcja div dzieli 32 bitowy wielkość w dx:ax przez operand pozostawiając iloraz w ax a resztę w dx. Z 32 bitowym operandem (tylko 80386 lub późniejsze) div dzieli wartość 64 bitową w edx:eax przez operand pozostawiając iloraz w eax a resztę w edx. W 80x86 nie możemy po prostu podzielić jednej ośmio bitowej wartości przez inną. Jeśli mianownik jest ośmio bitową wartością ,liczebnik musi być wartością szesnasto bitową. Jeśli musimy podzielić jedna ośmio bitową wartość bez znaku przez inną, musimy powielić zero liczebnika do szesnastu bitów. Możemy to osiągnąć poprzez załadowanie liczebnika do rejestru al a potem przesunąć zero do rejestru ah. Wtedy możemy podzielić ax przez operator mianownika uzyskując właściwy wynik. Opuszczenie powielenia zera dla al. przed wykonaniem div może spowodować w 80x86 uzyskanie niewłaściwego wyniku! Kiedy musimy podzielić dwie szesnastobitowe wartości bez znaku, musimy powielić zero rejestru ax (który zawiera liczebnik) do rejestru dx. Właściwie ładujemy bezpośrednią wartość zero do rejestru dx .Jeśli musimy podzielić jedną 32 bitową wartość przez inną, musimy powielić zero rejestru eax do edx (poprzez załadowanie zer do edx) przed dzieleniem. Jest jeszcze inna zasadzka instrukcji dzielenia 80x86:możemy popełnić fatalny błąd kiedy użyjemy tej instrukcji. Po pierwsze, możemy próbować dzielić wartość przez zero .Ponadto, iloraz może być zbyt długi do przechowania w rejestrze eax, ax lub al. Na przykład dzielenie 16/8 „8000h/2” tworzy iloraz 4000h z resztą zero. 4000h nie mieści się w ośmiu bitach. Jeśli zdarzy się coś takiego lub spróbujemy podzielić przez zero,80x86 wygeneruje przerwanie int 0.To zazwyczaj znaczy ,że BIOS wydrukuje „dzielenie przez zero: lub „błąd dzielenia” i przerwie wykonywanie programu. Jeśli zdarzy się to nam, prawdopodobnie nie powieliliśmy zera lub znaku naszego liczebnika przed wykonaniem operacji dzielenia. Ponieważ ten błąd przyczynia się do rozłożenia naszego programu, powinniśmy być bardzo ostrożni co do wartości które wybieramy kiedy używamy dzielenia. Flagi przeniesienia połówkowego, przeniesienia, przepełnienia ,parzystości, znaku i zera są niezdefiniowane po operacji dzielenia. Jeśli wystąpi przepełnienie (lub spróbujemy dzielić przez zero) wtedy 80x86 wykona INT 0 (przerwanie 0). Zauważmy, że 80286 i późniejsze procesory nie dostarczają specjalnych form dla idiv tak jak dla imul. Większość programów używa dzielenia mniej częściej niż używają mnożenia, więc projektanci Intela nie zawracali sobie głowy tworzeniem specjalnych instrukcji dla operacji dzielenia. Nie ma sposobu dzielenia przez wartość bezpośrednią. Musimy załadować wartość bezpośrednią do rejestru lub komórki pamięci i wykonać dzielenie przez ten rejestr lub komórkę pamięci. Instrukcja aad (modyfikowanie ASCII przed dzieleniem) jest inną nie upakowaną operacją dziesiętną. Dzieli ona wartość BCD przed operacją dzielenia ASCII. Chociaż ten tekst nie stosuje arytmetyki BCD, instrukcja aad jest użyteczna dla innych operacji. Algorytm który opisuje tą instrukcję: al := ah*10 + al ah :=0 Instrukcja ta jest całkiem użyteczna dla konwertowania łańcuchów cyfr do wartości całkowitych (zobacz pytania na końcu tego rozdziału). Następujący przykład pokazuje jak podzielić jedną 16 bitową wartość przez inną. ; J := K / M. (bez znaku) mov mov div mov ; J := K/M. (ze znakiem)

ax, K dx, o M J, ax

;ustawienie dzielnej ; powielenie zera wartości bez znaku w ax do dx

mov cwd idiv mov

ax, K

mov imul idiv mov

ax, K M P J, ax

;ustawienie dzielnej ;powielenie znaku wartości ze znakiem w ax do dx

M J, ax

; J := (K*M.)/P ;Zauważmy, że instrukcja imul tworzy ;32 bitowy wynik w DX:AX, więc nie ;musimy powielać znaku ax tutaj ; miejmy nadzieję, że wynik mieści się w 16 bitach

6.6 INSTRUKCJE LOGICZNE, OBROTU I BITOWE Rodzina 80x86 dostarcza pięć logicznych instrukcji, cztery instrukcje obrotów i trzy instrukcje przesunięcia. Instrukcje logiczne to and, or, xor, test i not; obrotu to ror, rol, rcr i rcl; instrukcje przesunięcia to shl /sal ,shr i sar. Procesory 80386 i późniejsze dostarczają nawet bogatszy zbiór operacji. Są to bt, bts, btr, btc, bsf, bsr, shld, shrd i zbiór instrukcji warunkowych (setcc). Instrukcje te mogą manipulować bitami, konwertują wartości, robią logiczne operacje, pakują i rozpakowują dane i robią operacje arytmetyczne. Ta sekcja omawia każdą z tych instrukcji szczegółowo. 6.6.1 INSTRUKCJE LOGICZNE: AND,OR,XOR I NOT Logiczne instrukcje 80x86 działają na podstawie bit przez bit. Istnieją dwie ośmio, szesnasto i trzydziesto dwu bitowe wersje każdej instrukcji. Instrukcje and, not, or i xor robią co następuje: and przez, źródło ;przez := przez and źródło or przez, źródło ;przez := przez or źródło xor przez, źródło ;przez := przez xor źródło not przez ‘przez := not przez Określone warianty to and reg, reg and mem, reg and reg, mem and reg, dana bezpośrednia and mem, dana bezpośrednia and eax/ax/al., dana bezpośrednia or używa tych samych form jak AND xor używa tych samych form co AND not rejestr not pamięć Za wyjątkiem, instrukcje not wpływają na flagi jak następuje: • Czyszczą flagę przeniesienia • Czyszczą flagę przepełnienia • Ustawiają flagę zera jeśli wynik to zero, w przeciwnym razie czyszczą ją. • Kopiują bardziej znaczący bit wyniku do flagi znaki. • Ustawiają flagę parzystości według parzystości (liczby bitów jeden) w wyniku • Zmieniają flagę przeniesienia połówkowego Instrukcja not nie wpływa na żadną z flag. Testowanie flagi zera jest szczególnie użyteczne. Instrukcja and ustawia flagę zera jeśli dwa operandy nie mają żadnej jedynki na odpowiadających sobie pozycjach bitów (ponieważ uzyskujemy wynik zero);na przykład, jeśli operand źródłowy zawierał pojedynczy jeden bit, wtedy flaga zera będzie ustawiona jeśli odpowiadające bit przeznaczenia wynosi zero, w innym razie będzie jedynką. Instrukcja or ustawia tylko flagę zera jeśli oba operandy zawierają zero. instrukcja xor ustawi flagę zera tylko jeśli oba operandy są różne. Zauważmy, że operacja xor stworzy wynik zero jeśli i tylko jeśli dwa operandy są równe. Wielu programistów powszechnie używa tego faktu do czyszczenia rejestru szesnasto bitowego do zera ponieważ instrukcja w postaci xor reg16, reg16 jest krótsza niż porównywalna instrukcja mov reg, 0.

Podobnie jak instrukcje dodawania i odejmowania, instrukcje and, or i xor dostarczają specjalnych form wymagających rejestru akumulatora i danej bezpośredniej. Te formy są krótsze i czasami szybsze niż ogólne formy „rejestr, dana bezpośrednia”. Chociaż nikt normalnie nie myśli o działaniu na danych znakowych z tymi instrukcjami,80x86 dostarcza specjalnej formy instrukcji „reg /mem, dana bezpośrednia” która powiela znak w zakresie –128..127 do szesnastu lub 32 bitów, jeśli to konieczne. Wszystkie operandy tych instrukcji muszą być tego samego rozmiaru. Na pre-80386 procesorach mogły być ośmio lub szesnasto bitowe. Na 80386 i późniejszych procesorach mogą być długości 32 bitów. Instrukcje te obliczają oczywiste operacje logiczne na poziomie bitowym na swoich operandach, zobacz Rozdział Jeden po więcej szczegółów o tych operacjach. Możemy użyć instrukcji and do ustawienia wyselekcjonowanych bitów na zero w operandzie przeznaczenia. Jest to znane jako maskowanie danych. Podobnie ,możemy użyć instrukcji or do wymuszenia pewnych bitów na jeden w operandzie przeznaczenia; zobacz „Operacje maskowania operacją OR”. Możemy użyć tych instrukcji razem z instrukcjami przesunięcia i obrotu opisanymi dalej ,do pakowania i rozpakowywania danych. Zobacz „Pakowanie i Rozpakowywanie typów danych” po więcej szczegółów. 6.6.2 INSTRUKCJE PRZESUNIĘCIA: SHL/SAL,SHR,SAR,SHLD I SHRD 80x86 wspiera trzy różne instrukcje przesunięcia (shl i sal są tymi samymi instrukcjami): shl (przesunięcie w lewo),sal (arytmetyczne przesunięcie w lewo), shr (przesunięcie w prawo) i sar (przesunięcie arytmetyczne w prawo).Procesory 80386 i późniejsze dostarczają dwie dodatkowe przesunięcia: shld i shrd. Instrukcje przesunięcia przenoszą bity w rejestrze lub komórce pamięci. Ogólny format dla instrukcji przesunięcia to shl przez, liczba sal przez, liczba shr przez, liczba sar przez, liczba Przez jest wartością do przesunięcia a liczba wyszczególnia liczbę o ile bitów chcemy przesunąć. Na przykład, instrukcja shl przesuwa bity w operandzie przeznaczenia w lewo o liczbę bitów wyszczególnioną w operandzie liczba. Instrukcje shld i shrd używają formatu; shld przez ,źródło, liczba shrd przez, źródło, liczba Specyficzne formy dla tych instrukcji to: shl reg, 1 shl mem, 1 shl reg, imm shl mem, imm shl reg, cl shl mem, cl sal jest synonimem dla shl i używa tych samych form. shr używa tych samych form jak shl sar używa tych samych form jak shl.

Rysunek 6.2: Operacja przesunięcia w lewo shld reg, reg, imm shld mem, reg, imm shld reg, reg, cl shld mem, reg, cl Dla CPU 8088 i 8086 liczba bitów do przesunięcia to albo „1” albo wartość w cl. W 80286 i późniejszych procesorach możemy używać ośmio bitowej stałej bezpośredniej. Oczywiście wartość w cl lub stałą bezpośrednia powinny być mniejsze lub równe liczbie bitów w operandzie przeznaczenia. Byłoby marnotrawieniem czasu przesuwać w lewo dziewięć bitów(osiem stworzy ten sam wynik jak wkrótce zobaczymy. Algorytmicznie możemy myśleć o operacji przesunięcia z liczbą inną niż jeden jak następuje:

for temp := 1 do liczba do shift dest, 1 Jest drobna różnica w sposobie traktowania przez instrukcje przesunięcia flagi przepełnienia kiedy liczba nie jest jedynką, ale możemy to ignorować większość czasu. Instrukcje shl ,sal ,shr i sar na ośmio- szesnasto- i trzydziesto dwu bitowych operandach. Instrukcje shld i shrd działają na 16 i 32 bitowych operandach. 6.6.2.1 SHL/SAL Mnemoniki shl i sal są synonimami. Przedstawiają one te same instrukcje i używają identycznego binarnego kodowania. instrukcje te przenoszą każdy bit w operandzie przeznaczenia o jedną pozycję w lewo ilość razy wyszczególnioną w operandzie. Zera wypełniają opuszczone pozycje w najmniej znaczącym bicie; bardziej znaczący bit przesuwany jest do flagi przeniesienia (zobacz Rysunek 6.2) Instrukcja shl /sal ustawia bity kodu stanu jak następuje: • Jeśli liczba przesunięcia wynosi zero, instrukcja shl nie wpływa na żadne flagi. • Flaga przeniesienia zawiera ostatni bit przesunięty z bardziej znaczącego bitu operandu • Flaga przepełnienia będzie zawierała jeden jeśli dwa bardziej znaczące bity były różne przed przesunięciem pojedynczego bitu. Flaga przepełnienia jest niezdefiniowana jeśli przesunięcie nie wynosi jeden. • Flaga zera będzie wynosić jeden jeśli przesunięcie stworzy zero jako wynik. • Flaga znaku będzie zawierała bardziej znaczący bit wyniku • Flaga parzystości będzie zwierała jeden jeśli są parzyste liczby jedynek w najmniej znaczącym bajcie wyniku. • Flaga A jest zawsze niezdefiniowana po instrukcji shl /sal. Instrukcja przesunięcia w lewo jest zwłaszcza użyteczna dla danych upakowanych. Na przykład przypuśćmy, że mamy dwa nibble w al. i ah które chcemy połączyć. Możemy użyć następującego kodu do wykonaniu tego shl ah, 4 ;ta forma wymaga 80286 i późniejszego or al., ah ; łączenie czterech bitów Oczywiście al. musi zawierać wartość z zakresu 0..F dla tego kodu dla właściwej pracy (operacja przesunięcia w lewo automatycznie czyści mniej znaczące cztery bity ah przed instrukcją or).Jeśli bardziej znaczące cztery bity

Rysunek 6.3: Operacja Arytmetycznego Przesunięcia w Prawo al nie są zerami ,przed tą operacją, możemy łatwo wyczyścić je instrukcją add: shl ah, 4 ;przenosi mniej znaczące bity na bardziej znaczące pozycje and al., 0Fh ;czyści cztery bardziej znaczące bity or al., ah ;łączy bity Ponieważ przesuwanie wartości całkowitych w lewą stronę o jedną pozycję jest równoważne tej wartości przez dwa, możemy również użyć instrukcji przesuwania w lewo dla mnożenia przez potęgę dwóch: shl ax, 1 ;odpowiednik AX*2 shl ax, 2 ;odpowiednik AX*4 shl ax, 3 ;odpowiednik AX*8 shl ax,4 ;odpowiednik AX*16 shl ax, 5 ;odpowiednik AX*32 shl ax,6 ;odpowiednik AX*64 shl ax,7 ;odpowiednik AX*128 shl ax, 8 ;odpowiednik AX*256 Zauważmy, że shl ax,8 jest odpowiednikiem następujących instrukcji: mov ah, al. mov al., 0

Instrukcja shl /sal mnoży obie wartości ze znakiem i bez znaku przez dwa dla każdego przesunięcia. Ta instrukcja ustawia flagę przeniesienia jeśli wynik nie mieści się w operandzie przeznaczenia (tj. wystąpi bez znakowe przepełnienie).Podobnie, ta instrukcja ustawia flagę przepełnienia jeśli wynik ze znakiem nie mieści się w operandzie przeznaczenia .Wystąpi to kiedy przesuniemy zero do bardziej znaczącego bitu liczby ujemnej lub przesuniemy jeden do bardziej znaczącego bitu nie ujemnej liczby. 6.6.2.2 SAR Instrukcja sar przesuwa wszystkie bity w operandzie przeznaczenia w prawo o jeden bit kopiując bardziej znaczący bit (zobacz Rysunek 6.3). Instrukcja sar ustawia bity flag jak następuje: • Jeśli liczba przesunięcia to zero, instrukcja sar nie wpływa na żadną flagę. • Flaga przeniesienia zawiera ostatni bit przesunięty z mniej znaczącego bitu operandu. • Flaga przepełnienia będzie zawierała zero jeśli przesunięcie to jeden. Przepełnienie może nigdy nie wystąpić z tą instrukcją. Jednakże, jeśli liczba ta to nie jeden, wartość flagi przepełnienia jest niezdefiniowana. • Flaga zera będzie zawierała jeden jeśli przesunięcie tworzy wynik zero. • Flaga znaku będzie zawierała najbardziej znaczący wyniku • Flaga parzystości będzie zawierała jeden jeśli jest parzysta liczba jedynek w najmniej znaczącym bajcie wyniku. • Flaga przepełnienia połówkowego jest zawsze niezdefiniowana po instrukcji sar. Głównym celem wykonania instrukcja sar jest dzielenie ze znakiem przez potęgi dwójki. Każde przesunięcie w prawo dzieli wartość przez dwa. Wielokrotne przesunięcie w prawo dzieli poprzednią przesuniętą wartość przez dwa ,więc wielokrotne przesunięcie tworzy następujące rezultaty:

Jest bardzo ważna różnica pomiędzy instrukcjami sar i idiv. Instrukcja idiv zawsze zaokrągla do zera podczas gdy sar zaokrągla wynik do mniejszego wyniku. Dla wyników dodatnich, arytmetyczne przesunięcie w prawo o jedną pozycję tworzy taki sam wynik jak całkowite dzielenie przez dwa. Jednak, jeśli iloraz jest ujemny, instrukcja idiv zaokrągla do zera podczas gdy sar zaokrągla do ujemnej nieskończoności. Następujące przykłady demonstrują te różnice: mov ax, -15 cwd mov bx, 2 idiv ;daje –7 mov ax, -15 sar ax, 1 ;daje –8 Zapamiętajmy to jeśli używamy sar dla operacji dzielenia całkowitego. Instrukcja sar ax, 8 faktycznie kopiuje ah do al. a potem powiela znak al. do ah. Jest tak ponieważ sar ax, 8 przesunie ah do al. ale pozostawi kopię najstarszego bitu ah na wszystkich pozycjach bitów ah .Istotnie, możemy użyć instrukcji sar w 80286 i późniejszych procesorach do powielenia znaku jednego rejestru do innego .Następująca sekwencja daje nam przykład takiego zastosowania: ;Odpowiednik CBW: mov ah, al. sar ah, 7 ;Odpowiednik CWD: mov dx, ax sar dx, 15 ;Odpowiednik CDQ: mov edx, eax

sar edx, 31 Oczywiście ,może wydawać się głupie użycie dwóch instrukcji tam gdzie może wystarczyć pojedyncza instrukcja; jednakże instrukcje cbw, cwd i cdq tylko powielają znak al. do ax, ax do dx:ax i eax do edx:eax. Instrukcja sar pozwala nam powielić znak jednego rejestru do innego rejestru o tym samym rozmiarze, z drugiego rejestru zawierającego powielony znak bitów: ; Powielenie znaku bx do cx:bx mov cx, bx sar cx, 15 6.6.2.3 SHR Instrukcja shr przesuwa wszystkie bity w operandzie przeznaczenia w prawo o jeden bit przesuwając zero do najbardziej znaczącego bitu (zobacz Rysunek 6.4) Instrukcja shr ustawia bity flag jak następuje: • Jeśli liczba do przesunięcia to zero, instrukcja shr nie wpływa na żadną flagę. • Flaga przeniesienia zawiera ostatni bit przesunięty z najmniej znaczącego bitu operandu • Jeśli liczba do przesunięcia to jeden, flaga przepełnienia będzie zawierała wartość najbardziej znaczącego bitu operandu przed przesunięciem. (tj. instrukcja ta ustawia flagę przepełnienia

Rysunek 6.4:Operacja przesunięcia w prawo • • • • •

Jeśli zmienia się znak).Jednakże ,jeśli liczba nie jest jedynką wartość flagi przepełnienia jest Niezdefiniowana Flaga zera będzie jedynką jeśli przesunięcie stworzy wynik zero Flaga znaku będzie zwierała bardziej znaczący bit wyniku, który jest zawsze zero Flaga parzystości będzie zawierała jedynkę jeśli jest parzysta liczba bitów jeden w najmniej znaczącym bajcie wyniku Flaga przeniesienia połówkowego jest zawsze niezdefiniowana po instrukcji shr

Instrukcja przesunięcia w prawo jest użyteczna zwłaszcza dla danych nie upakowanych. Na przykład przypuśćmy że chcemy uzyskać dwa nibble w rejestrze al., pozostawiając bardziej znaczący nibble w ah i najmniej znaczący nibble w al. Moglibyśmy używać następującego kodu do zrobienia tego: mov ah, al. shr ah, 4 and al., 0Fh Ponieważ przesunięcie wartości całkowitej bez znaku w prawo o jedną pozycję jest odpowiednikiem dzielenia tej wartości przez dwa ,możemy również używać instrukcji przesunięcia w prawo dla dzielenia przez potęgę dwójki:

Zauważmy, że shr ax, 8 jest odpowiednikiem następujących dwóch instrukcji; mov al., ah mov ah, 0

Pamiętajmy, że dzielenie przez dwa używa shr tylko działając dla operandów bez znakowych. jeśli ax zawiera –1 a mamy wykonać shr ax,1 wynik w ax będzie 32767 (7FFFh), nie –1 lub zero jak można by się spodziewać. Używamy instrukcji sar jeśli musimy podzielić wartość całkowitą ze znakiem przez potęgę dwójki. 6.6.2.4 INSTRUKCJE SHLD I SHRD Instrukcje shld i shrd dostarczają podwójnej precyzji operacji przesunięcia w lewo i prawo, odpowiednio. Te instrukcje są dostępne tylko na procesorach 80386 o późniejszych. Ich ogólna forma shld operand1, operand2, stała bezpośrednia shld operand1, operand2, cl shrd operand1, operand2, stała bezpośrednia shrd operand1,operand2, cl Operand2 musi być szesnasto lub trzydziesto dwu bitowym rejestrem .Operand 1 może być rejestrem lub komórką pamięci. Oba operandy muszą być tego samego rozmiaru. Operand bezpośredni może być wartością z zakresu od zera do n-1,gdzie n jest liczbą bitów w dwóch operandach; wyszczególnia liczbę bitów do przesunięcia. Instrukcja shld przesuwa bity w operandzie1 w lewo. Najbardziej znaczący bit przesuwany jest do flagi przeniesienia, a najbardziej znaczący bit operandu2 przesuwa się do najmniej znaczącego bitu operandu1.Zauważmy,że ta instrukcja

Rysunek 6.5: Operacja przesunięcia w lewo z podwójną precyzją

Rysunek 6.6: Operacja przesunięcia w prawo z podwójną precyzją nie modyfikuje wartości operandu2,używa czasowej kopii operandu2 podczas przesunięcia. Operand bezpośredni wyszczególnia liczbę bitów do przesunięcia. jeśli liczba to n, wtedy shld przesuwa bit n-1 do flagi przeniesienia. Przesuwa również n najbardziej znaczących bitów operandu2 do n najmniej znaczących bitów operandu1.Obrazowo,instrukcja shld wygląda tak jak na rysunku 6.5 Instrukcja shld ustawia bity flag jak następuje: • Jeśli liczba przesunięcia wynosi zero, instrukcja shld nie wpływa na żadną flagę • Flaga przeniesienia zawiera ostatni bit przesunięty najbardziej znaczącego bitu operandu1. • Jeśli liczba przeniesienia to jeden ,flaga przepełnienia będzie zawierała jeden jeśli bit znaku operandu1 zmieni się podczas przesunięcia. jeśli liczba nie jest jedynką, flaga przepełnienia jest niezdefiniowana. • Flaga zera będzie miał jeden jeśli przesunięcie stworzy wynik zero. • Flaga znaku będzie zawierała najbardziej znaczący bit wyniku

Instrukcja shld jest użyteczna dla danych upakowanych z wielu różnych źródeł. Na przykład przypuśćmy, że chcemy stworzyć słowo poprzez połączenie bardziej znaczących nibbli z czterech innych słów. Możemy to zrobić przy pomocy następującego kodu: mov ax, wartość4 ;pobieranie najbardziej znaczącego nibbla shld bx, ax, 4 ;kopiowanie bardziej znaczących bitów z AX do BX mov ax, wartość3 ;pobieranie nibbla #2 shld bx, ax, 4 ;łączenie w bx mov ax, wartość2 ;pobieranie nibbla #1 shld bx, ax, 4 ;łączenie w bx mov ax, wartość ;pobieranie najmniej znaczącego nibbla shld bx,ax, 4 ;BX zawiera teraz wszystkie cztery nibble Instrukcja shrd jest podobna do shld z wyjątkiem tego ,że przesuwa bity w prawo zamiast w lewo. Pokazuje to rysunek 6.6

Rysunek 6.7: Pakowanie danych instrukcją shrd Instrukcja shrd ustawia bity flag jak następuje: • Jeśli liczba przesunięcia wynosi zero, nie wpływa na żadną flagę. • Flaga przeniesienia zawiera ostatni bit przesunięty z najmniej znaczącego bitu operandu1. • Jeśli liczba przesunięcia to jeden, flaga przepełnienia będzie zawierała jeden jeśli najbardziej znaczący bit operandu1 się zmieni. Jeśli liczba nie jest jedynką, flaga przepełnienia jest niezdefiniowana. • Flaga zera będzie jedynką jeśli przesunięcie stworzy wynik zero • Flaga znaku będzie zawierała najbardziej znaczący bit wyniku. Szczerze mówiąc, te dwie instrukcje byłyby prawdopodobnie odrobinę bardziej użyteczne gdyby Operand2 mógł być komórką pamięci. Intel stworzył te instrukcje pozwalające na szybkie przesunięcia (64 bity lub więcej) o zwiększonej dokładności. Po więcej informacji zajrzyj do „Operacje przesunięcia o rozszerzonej precyzji”. Instrukcja shrd jest tylko nieznacznie bardziej użyteczna niż shld dla pakowania danych. Na przykład przypuśćmy, że ax zawiera wartość z zakresu 0..99 przedstawiającą rok (1900..1999),bx zawiera wartość z zakresu 1..31 przedstawiającą dzień i cx zawierającą wartość z zakresu 1..12 przedstawiającą miesiąc (zobacz „Pola bitów i dane upakowane”).Możemy łatwo użyć instrukcji shrd do pakowania tej danej do dx jak następuje: shrd dx,ax,7 shrd dx,bx,5 shrd dx,cx,4

6..6.3INSTRUKCJE OBROTU: RCL,RCR,ROL I ROR Instrukcje obrotu przesuwają bity w koło, podobnie jak instrukcje przesunięcia, z wyjątkiem tego ,że przesunięte bity operandu przez instrukcje obrotu krążą po operandzie. Zawierają one rcl (obrót w lewo z uwzględnieniem flagi przeniesienia),rcr (obrót w prawo z uwzględnieniem flagi przeniesienia),rol (obrót w lewo) i ror (obrót w prawo).Wszystkie te instrukcje przyjmują następujące formy:

Rysunek 6.8: Operacja obrotu w lewo z uwzględnieniem flagi przeniesienia rcl przez, liczba rol przez, liczba rcr przez, liczba ror przez, liczba Specjalne formy to: rcl reg, 1 rcl mem, 1 rcl reg, bezp rcl mem, bezp rcl reg, cl rcl mem, cl rol ,rcr, ror używa tego samego formatu co rcl. 6.6.3.1 RCL Rcl (obrót w lewo z uwzględnieniem flagi przeniesienia) ,jak sama nazwa wskazuje ,obraca bity w lewo z uwzględnieniem flagi przeniesienia i wraca do bitu zero po prawej stronie (zobacz Rysunek 6.8) Zauważ, że jeśli obracamy z uwzględnieniem przeniesienia object n+1 razy, gdzie n jest liczbą bitów w obiekcie, zakończymy z oryginalną wartością .Zapamiętajmy jednak, że kilka flag może zawierać różne wartości po n+1 operacji rcl. Instrukcja rcl ustawia bity flag jak następuje: • Flaga przeniesienia zawiera ostatni bit przesunięty z najbardziej znaczącego bitu operandu • Jeśli liczba przesunięcia to jeden, rcl ustawia flagę przepełnienia jeśli znak zmieni się jako wynik obrotu. Jeśli liczba nie jest jedynką, flaga przepełnienia jest niezdefiniowana. • Instrukcja rcl nie modyfikuje flag zera, znaku, parzystości i przeniesienia połówkowego. Ważna uwaga: W odróżnieniu od instrukcji przesunięcia, instrukcje obrotu nie wpływają na flagi znaku, zera ,parzystości lub przeniesienia połówkowego. Ten brak ortogonalności może sprawić nam dużo kłopotów jeśli zapomnimy o nim i spróbujemy przetestować te flagi po operacji rcl. Jeśli musimy przetestować jedną z tych flag po operacji rcl ,sprawdzimy najpierw flagi przeniesienia i przepełnienia (jeśli to konieczne) potem porównujemy wynik z zerem ustawiając inne flagi. 6.6.3.2 RCR Instrukcja rcr (obrót w prawo z uwzględnieniem flagi przeniesienia) jest uzupełnieniem operacji instrukcji rcl Przesuwa bity w prawo z uwzględnieniem flagi przeniesienia i wraca z powrotem do najbardziej znaczącego bitu (zobacz rysunek 6.9) Instrukcja ta ustawia flagi w porządku analogicznym do rcl: • Flaga przeniesienia zawiera ostatni bit przesunięty z najmniej znaczącego bitu operandu

• •

Jeśli liczba przesunięcia to jeden, rcr ustawia flagę przepełnienia jeśli znak zmieni się (w znaczeniu najbardziej znaczącego bitu a flaga przeniesienia nie była taka sama przed wykonaniem tej operacji)Jednak jeśli liczba nie jest jedynką, wartość flagi przepełnienia jest niezdefiniowana. Instrukcja rcr nie wpływa na flagi zera ,znaku ,parzystości lub przepełnienia połówkowego.

Instrukcji tej dotyczą te same uwagi jak powyższej instrukcji rcl 6.6.3.3 ROL Instrukcja rol jest podobna do instrukcji rcl w tym, że obraca swój operand w lewo o określoną liczbę bitów. Główna różnica jest taka, że rol przesuwa najbardziej znaczący bit operandu zamiast przeniesienia do bitu zero.

Rysunek 6.9:Operacja obrotu w prawo z uwzględnieniem przeniesienia

Rysunek 6.10: Operacja obrotu w lewo Rol również kopiuje wartość najbardziej znaczącego bitu do flagi przeniesienia (zobacz rysunek 6.10) Instrukcja rol ustawia flagi identycznie do rcl.Za wyjątkiem wartości źródła przesuwanego do bitu zero, instrukcja ta zachowuje się jak instrukcja rcl. Nie zapomnij ostrzeżenia o flagach! Podobnie jak shl, instrukcja rol jest często użyteczna dla pakowania i rozpakowania danych. Na przykład przypuśćmy ,że chcemy usunąć bity 10..14 w ax i pozostawić w bitach 0..4. Następująca sekwencja kodu osiągnie oba cele tak: shr ax, 10 and ax, 1Fh rol ax, 6 and ax, 1Fh 6.6.3.4 ROR Instrukcja ror nawiązuje do instrukcji rcr w taki sam sposób jak instrukcja rol do instrukcji rcl. To znaczy, jest to prawie ta sama operacja z wyjątkiem bitu wejściowego źródła operandu. Zamiast przesunięcia poprzedniej flagi przeniesienia do bardziej znaczącego bitu operacji przeznaczenia, ror przesuwa bit zero do najbardziej znaczącego bitu (zobacz rysunek 6.11)

Rysunek 6.11:Operacja obrotu w prawo Instrukcja ror ustawia flagi identycznie jak rcr. Z wyjątkiem bitu źródłowego przesuwanego do bardziej znaczącego bitu, instrukcja ta zachowuje się dokładnie jak instrukcja rcr. Nie zapomnij uwagi o flagach! 6.6.3 OPERACJE BITOWE Zabawa z bitami jest jedną z tych łatwiejszych operacji do wykonania w języku asemblera niż w innych językach. I nic dziwnego .Większość języków wysokiego poziomu chroni nas przed przedstawianiem maszynowym odpowiednich typów danych. Instrukcje takie jak and ,or, xor ,not i obrotu i przesunięcia wykonują o ile to możliwe testowanie, ustawianie, zerowanie, odwracanie pól bitów wewnątrz łańcuchów bitów. Nawet C++ słynący ze swoich działań manipulowania bitami, nie dostarcza takich zdolności do manipulowania bitami jak asembler. Procesory rodziny 80x86,zwłaszcza 80386 i późniejsze, idą dużo dalej. Poza standardowymi instrukcjami logicznymi, przesunięcia i obrotu, są instrukcje do testowania bitów wewnątrz operandu, do testowania i ustawiania, zerowania lub odwracania wyszczególnionych bitów w operandzie, i wyszukiwania dla zbioru bitów. Operacje te to: test przez, źródło bt źródło, indeks btc źródło, indeks btr źródło, indeks bts źródło, indeks bsf przez, źródło bsr przez, źródło Formy określone: test reg, reg test reg, mem test mem, reg test reg, bezp test mem, bezp test eax,ax,al., bezp bt bt bt bt

reg ,reg mem, reg reg, bezp mem, bezp

btc, btr i bts używają tego samego formatu co bt bsf reg, reg bsf reg, mem Zauważmy, że bt, btc, btr, bts, bsf i bsr wymagają operandu 16- lub 32 bitowego. Operacje bitowe są użyteczne kiedy implementujemy zbiór typów danych używających mapy bitów. 6.6.4.1 TEST Instrukcja test logicznie dodaje swoje dwa operandy i ustawia flagi, ale nie zapisuje rezultatu. Test i and dzieli ten sam związek co cmp i sub. Chcielibyśmy użyć tej instrukcji aby zobaczyć czy bit zawiera jeden. Rozważmy następująca instrukcję:

test al., 1 Instrukcja ta doda logicznie al. z wartością jeden. Jeśli bit zero al zawiera jeden, wynik nie jest zerowy a 80x86 czyści flagę zera. Jeśli bit zero al zawiera zero wtedy wynik jest zerem a operacja test ustawia flagę zera. Możemy testować flagę zera po tej instrukcji aby zdecydować czy al zawierał zero lub jeden w bicie zero. Instrukcja test może również sprawdzić czy jeden lub więcej bitów w rejestrze lub komórce pamięci nie jest zerem. Rozważmy następującą instrukcję: test dx, 105h Instrukcja ta logicznie dodaje dx z wartością 105h.tworzy to nie zerowy wynik (i dlatego też czyści flagę zera) jeśli przynajmniej jeden z bitów zero, dwa lub osiem zawiera jeden. Wszystkie muszą mieć wartość zero do ustawiania flagi zera. Instrukcja test ustawia flagi identycznie jak instrukcja and: • Zeruje flagę przeniesienia • Czyści flagę przepełnienia • Ustawia flagę zera jeśli wynikiem jest zero, inaczej ją zeruje • Kopiuje bardziej znaczący bit wyniku do flagi znaku • Ustawia flagę parzystości wedle parzystości (liczby bitów jeden) w mniej znaczącym bajcie wyniku • Zmienia flagę przeniesienia połówkowego.

6.6.4.2 INSTRUKCJE TESTOWANIA BITÓW: BT,BTS,BTR I BTC W 80386 i późniejszych procesorach, możemy użyć instrukcji bt (bit test) do testowania pojedynczego bitu. Jej drugi operand wyszczególnia indeks bitu w pierwszym operandzie. Bt kopiuje adresowany bit do flagi przeniesienia. Na przykład, instrukcja bt ax, 12 kopiuje bit dwunasty z ax do flagi przeniesienia. Instrukcje bt/bts/btr/btc wykorzystują operandy 16 lub 32 bitowe. To nie jest ograniczenie tej instrukcji. W końcu jeśli chcemy przetestować trzeci bit rejestru al., możemy łatwo przetestować trzeci bit rejestru ax. Z drugiej strony ,jeśli indeks jest większy niż rozmiar operand rejestru, wynik jest niezdefiniowany. Jeśli pierwszy operand jest komórką pamięci, instrukcja bitowa testuje bit w danym offsecie pamięci, bez względu na wartość indeksu. Na przykład, jeśli bx zawiera 65 wtedy bt TestMe, bx skopiuje bit z komórki TestMe+8 do flagi przeniesienia .Jeszcze raz, rozmiar operandu nie ma znaczenia. Praktycznie rzecz biorąc, operand pamięci jest bajtem i możemy przetestować każdy bit po bajcie z właściwym indeksem. Faktyczny bit testowany przez bt to pozycja bitu indeks mod 8 a offset pamięci adres efektywny + indeks/8. Instrukcje bts, btr i btc również kopiują adresowany bit do flagi przepełnienia. Jednakże instrukcje te również ustawiają, resetują (zerują) lub dopełniają (odwracają) bit w pierwszym operandzie po skopiowaniu go do flagi przeniesienia. dostarczają operacji testuj i ustaw, testuj i zeruj, testuj i odwróć koniecznych dla kilku równoległych algorytmów. Instrukcje bt, bts, btr i btc nie wpływają na żadną inną flagę niż flaga przeniesienia. 6.6.4.3 WYSZUKIWANIE BITÓW: BSF I BSR Instrukcje bsf i bsr szukają pierwszego lub ostatniego ustawionego bitu w 16 lub 32 bitowej wielkości. Ogólna forma tych instrukcji to bsf przez, źródło bsr przez, źródło Bsf umiejscawia pierwszy ustawiony bit w operandzie źródłowym ,szukając od bitu zero do najbardziej znaczącego bitu. Bsr umiejscawia pierwszy ustawiony bit szukając od bardziej znaczącego bitu w dół do najmniej znaczącego bitu. Jeśli te instrukcje umiejscawiają jedynkę, zerują flagę zero i przechowują indeks bitu (0..31) w operandzie przeznaczenia. Jeśli operand źródłowy to zero, instrukcje te ustawiają flagę zera i przechowują nieokreśloną wartość w operandzie przeznaczenia. Szukając dla pierwszego bitu zawierającego zero (zamiast jeden),robimy kopię operandu źródłowego i odwracamy go (używając not),potem wykonujemy bsf i bsr na tej odwróconej wartości. Flaga zera byłaby ustawiona po tej operacji jeśli nie byłoby bitów zero w oryginalnej wartości źródłowej, w przeciwnym razie operand przeznaczenia zawierałby pozycję pierwszego bitu zawierającego zero.

6.6.5 INSTRUKCJE WARUNKOWE Instrukcje setcc ustawiają pojedynczy bajt operandu (rejestr lub komórka pamięci) na zero lub jeden w zależności od wartości w rejestrze flag Ogólny format dla instrukcji setcc to

setcc reg8 setcc mem8 Setcc przedstawia mnemonik pojawiający się w następującej tabeli. Instrukcje przechowują zero w odpowiednim operandzie jeśli warunek jest fałszywy, przechowują jeden w ośmio bitowym operandzie jeśli warunek jest prawdziwy.

Tabela 28: Instrukcje Setcc, które testują flagi Powyższe instrukcje setcc po prostu testują flagi bez żadnych innych powiązań do tej operacji. Możemy, na przykład, użyć setcc do sprawdzenia flagi przeniesienia po przesunięciu ,obrocie, testowaniu bitów lub operacji arytmetycznych. .Podobnie, możemy użyć instrukcji senz po instrukcji test do sprawdzenia wyniku. Instrukcja cmp działa w synergii z instrukcją setcc. Bezpośrednio po operacji cmp flagi dostarczają informacji dotyczących względnych wartości tych operandów .Pozwalają nam zobaczyć czy jeden operand jet mniejszy niż, równy, większy niż lub ich kombinację. Są dwie grupy instrukcji setcc które są bardzo użyteczne po operacji cmp. Pierwsza grupa zajmuje się wynikiem bez znakowego porównania, druga grupa zajmuje się wynikami porównania znakowego.

Tabela 29: Instrukcje Setcc dla porównania bez znakowego Odpowiadająca tabela dla porównania znakowego to

Tabela 30: Instrukcje Setcc dla porównania znakowego Instrukcje setcc są szczególnie wartościowe ponieważ mogą konwertować wynik porównania do wartości boolowskiej (prawda/fałsz lub 0/1).jest to szczególnie ważne kiedy tłumaczymy instrukcje z języków wysokiego poziomu jak Pascal lub C++ na asembler. Następujący przykład pokazuje jak użyć tych instrukcji w tym celu: ; Bool := A < Here”, 0 16 dup (0) „insert this’, 0 0 0

lesi mov strinsml byte mov mov

InsertInMe cx, 8

lesi mov strinsl byte

InsertInMe cx, 8

InsertInMe InsertStr cx, 8

;wstaw przed „insert this< here Drugi ciąg: Insert> insert that < here Trzeci ciąg: Insert > insert this < here 15.4.8 STRLEN Strlen oblicza długość ciągu wskazywanego przez es:di. Zwraca liczbę znaków aż do, ale nie wliczając, bajtu zakończonego zerem. Zwraca tą długość w rejestrze cx. Przykład: GetLen

byte lesi strlen print byte mov puti print byte

„this string is 33 characters long”, 0

GetLen „this string is „, 0 ax, cx

;puti potrzebuje długości w AX!

„characters long”, cr, lf, 0

15.4.9 STRLWR, STRLWRM,STRUPR,STRUPRM Strlwr i Strlwrm konwertują dużej litery w ciągu na litery małe. Strupr i Struprm konwertują małe litery w ciągu na litery duże. Podprogramy te nie wpływają na żadne inne znaki obecne w ciągu. Dla wszystkich czterech podprogramów, es:di wskazują ciąg źródłowy do konwersji. Strlwr i strupr modyfikują znaki bezpośrednio w ciągu. Strlwrm i struprm robią kopię ciągu na stercie a potem konwertują znaki w nowym ciągu. Zwracają one również wskaźnik do tego nowego ciągu w es:di. Jak zwykle przy podprogramach StdLib UCR, strlwrm i struprm zwracają ustawioną flagę przeniesienia jeśli wystąpi błąd alokacji pamięci. Przykład: String1 String2 StrPtr1 StrPtr2

byte byte dword dword lesi struprm jc mov mov

„This string has lower case.” , 0 „THIS STRING has Upper Case.” , 0 0 0

String1 ;konwersja małych liter na duże error word ptr StrPtr1, di word ptr StrPtr1+2, es

lesi strlwrm jc mov mov

String2 konwersja dużych liter na małe litery error word ptr StrPtr2, di word ptr StrPtr2+2, es

lesi strlwr

String1

lesi strupr

String2

printf byte byte byte byte dword

;konwersja na małe litery, na miejscu ;konwersja na duże litery, w miejscu „struprm: %^s\n” „strlwrm: %^s\n” „strlwr: %s\n” „strupr: %s\n”, 0 StrPtr1, StrPtr2, String1, String2

Powyższy fragment drukuje co następuje: struprm: THIS STRING HAS LOWER CASE strlwrm: this string has upper case strlwr: this string has lower case strupr: THIS STRING HAS UPPER CASE 15.4.10 STRREV, STRREVM Te dwa podprogramy odwracają znaki w ciągu. Na przykład jeśli przekażemy do strrev ciąg „ABCDEF” skonwertuje go do ciągu „FEDCBA” .Jak możemy oczekiwać, podprogram strrev odwraca ciąg którego adres przekazujemy w es:di; strrevm najpierw robi kopię ciągu na stercie i odwraca znaki pozostawiając niezmieniony ciąg oryginalny. Oczywiście strrevm zwróci ustawioną flagę przeniesienia jeśli wystąpi błąd alokacji pamięci. Przykład: Palindrome NotPaldrm StrPtr1

byte byte dword lesi strrevm jc mov mov

„radar”, 0 „x+y – z”, 0 0

lesi strrev

NotPaldrm

Palindrome error word ptr StrPtr1, di word ptr StrPtr1+2, es

printf byte „First string: %^s\n” byte „Second string: %s\n”, 0 dword StrPtr1, NotPaldrm Powyższy kod da nam takie dane wyjściowe: First string: radar Second string: z – y+x 15.4.11 STRSET, STRSETM

Strset i strsetm replikują pojedynczy znak w całym ciągu . ich zachowanie nie jest jednak całkiem podobne. W szczególności podczas gdy strsetm jest trochę podobna do funkcji repeat, strset nie. Oba podprogramy oczekują wartości pojedynczego znaku w rejestrze al. Replikują ten znak w całym ciągu .Strsetm wymaga również licznika w rejestrze cx. Tworzy na stercie ciąg składający się z cx znaków i zwraca wskaźnik do tego ciągu w es:di (zakładając, ze nie wystąpi żaden błąd). Strset, z drugiej strony, oczekuje przekazania mu adresu istniejącego ciągu w es:di. Zamienia on każdy znak w tym ciągu ze znakiem w al. Zauważmy, że nie określamy długości kiedy używamy funkcji strset, strset używa długości istniejącego ciągu. Przykład: String1

byte lesi mov strset

„Hello there”, 0

String1 al., ‘*’

mov cx, 8 mov al., ‘#’ strsetm print byte „String2: „,0 puts printf byte „\nString1: %s\n”, 0 dword String1 Powyższy kod da nam dane wyjściowe takie: String2: ######## String1: *********** 15.4.12 STRSPAN. STRSPANL, STRCSPAN, STRCSPANL Te cztery podprogramy szukają w całym ciągu znaku który jest albo w jakimś określonym zbiorze znaków (strspan, strspanl) lub nie jest członkiem jakiegoś zbioru znaków (strcspan, strcspanl) . Te podprogramy pojawiły się w Bibliotece Standardowej UCR tylko dlatego, że pojawiają się w bibliotece standardowej C. Rzadko powinniśmy używać tych podprogramów. Biblioteka Standardowa UCR zawiera inne podprogramy do manipulowania zbiorami znaków i wykonywania operacji dopasowywania znaków. Pomimo to te podprogramy są czasami użyteczne i warte zajęcia się nimi tutaj. Te podprogramy oczekują przekazania im adresów dwóch ciągów: ciągu źródłowego i ciągu zbioru znaków. Oczekują adresu ciągu źródłowego w es:di. Strspan i strcspan chcą adresu ciągu zbioru znaków w dx:si; ciąg zbioru znaków następuje po wywołaniu strspanl i strcspanl. Przy zwracaniu , cx zawiera indeks do ciągu, zdefiniowanego jak następuje: strspan, strspanl: strcspan, strcspanl:

indeks pierwszego znaku w źródle znalezionego z zbiorze znaków indeks pierwszego znaku w źródle nie znalezionego w zbiorze znaków

Jeśli wszystkie znaki są w zbiorze (lub nie są w zbiorze) wtedy cx zawiera indeks do ciągu zakończonego zerem. Przykład: Source byte „ABCDEFG 0123456”, 0 Set1 byte „ABCDEFGHIJKLMNOPQRSTUVWXYZ”, 0 Set2 byte „0123456789”, 0 Index1 word ? Index2 word ? Index3 word ? Index4 word ? -

lesi Source ldxi Set1 strspan mov Index1, cx lesi Source lesi Set2 strspan mov Index2, cx

;szukanie pierwszego znaku alfabetu ;indeks pierwszego znaku alfabetu

;szukanie pierwszego znaku liczbowego

lesi Source strcspanl byte „ABCDEFGHIJKLMNOPQRSTUVWXYZ”, 0 mov Index3, cx lesi Set2 strcspanl byte „0123456789”, 0 mov Index4, cx printf byte byte byte byte dword Kod ten da dane wyjściowe takie:

„First alpha char in Source is at offset %d\n” „First numeric char is at offset %d\n” „First non-alpha in Source is at offset %d\n” „First non-numeric in Set2 is at offset %d\n” Index1, Index2, Index3, Index4

First alpha char in Source is at offset 0” First numeric char is at offset 8 First non-alpha in Source is at offset 7 First non-numeric in Set2 is at offset 10 15.4.13 STRSTR, STRSTRL Strstr poszukuje pierwszego wystąpienia jednego ciągu wewnątrz innego. Es:di zawiera adres ciągu w którym chcemy poszukać drugiego ciągu. Dx:si zawiera adres drugiego ciągu dla podprogramu strstr, strstrl przeszukuje drugi ciąg bezpośrednio występujący po wywołaniu w strumieniu kodu. Przy powrocie z strstr lub strstrl, flaga przeniesienia będzie ustawiona jeśli drugi ciąg nie jest obecny w ciągu źródłowym. Jeśli flaga ta jest wyzerowana, wtedy drugi ciąg jest obecny w ciągu źródłowym a cx bezie zawierał (oparty o zero) indeks gdzie drugi ciąg został znaleziony. Przykład: SourceStr SearchStr

byte byte lesi ldxi strstr jc print byte mov puti putcr lesi strstrl

„Search for ‘this’ in this string”, 0 „this”, 0

SourceStr SearchStr NotPresent „Found string at offset „, 0 ax, cx ;potrzebny offset in AX dla puti

SourceStr

byte jc print byte mov puti putcr

„for”, 0 NotPresent „Found ‘for’ at offset „, 0 ax, cx

NotPresent: Powyższy kod wydrukuje co następuje: Found string at offset 12 Found ‘for’ at offset 7

15.4.14 STRTRIM, STRTRIMM Te dwa podprogramy są trochę podobne do strbdel i strbdelm. Zamiast usuwania czołowych spacji, obcinają końcowe spacje z ciągu. Strtrim obcina każdą końcową spację bezpośrednio w określonym ciągu w pamięci. Strtimm najpierw kopiuje ciąg źródłowy a potem obcina spacje w pamięci. Oba podprogramy oczekują przekazania adresu ciągu źródłowego w es:di. Strtrimm zwraca wskaźnik do nowego ciągu (jeśli może go zaalokować) w es:di. Zwraca również ustawione przeniesienie lub wyzerowanie do oznaczenia błąd/ brak błędu. Przykład: String1 String2 StrPtr1 StrPtr2

byte „Space at the end „,0 byte „ Space on both sides „ , 0 dword 0 dword 0 ;TrimSpcs obcina spacje z obu końców ciągu . Zauważmy ,że jest to trochę bardziej wydajne wykonanie ;najpierw strbdel a potem strtrim. Ten podprogram tworzy nowy ciąg na stercie i zwraca wskaźnik do tego ;ciągu w ES:DI TrimSpcs

BadAlloc: TrimSpcs

proc strbdelm jc BadAlloc strtrim clc ret endp lesi String1 strtrimm jc error mov word ptr StrPtr1, di mov word ptr StrPtr1+2, es lesi call jc mov mov

String2 TrimSpcs error word ptr StrPtr2, di word ptr StrPtr2+2, es

printf byte byte

„First string: ‘%s’\n” „Second string: ‘%s’\n”, 0

;zwracany jeśli błąd

dword StrPtr1, StrPtr2 Kod daje wynik następujący: First string: ‘Spaces at the end” Second string: „Spaces on both sides” 15.4.15 INNE PODPROGRAMY CIĄGÓW W BIBLIOTECE STANDARDOWEJ UCR Oprócz podprogramów strxxx wypisanych w tej sekcji, jest wiele dodatkowych podprogramów ciągów dostępnych w Bibliotece Standardowej UCR. Podprogramy do konwersji typów numerycznych (całkowite, hex, rzeczywiste) do ciągów lub vice versa, podprogramy dopasowania do wzorca i zbiór znaków i wiele innych konwersji. Podprogramy opisane w tym rozdziale są to te, których definicje pojawiają się w pliku nagłówkowym „strings.a” . Po więcej szczegółów o innych podprogramach ciągów zajrzyj do odnośnych części Biblioteki Standardowej UCR. 15.5 PODPROGRAMY ZBIORU ZNAKÓW W BIBLIOTECE STANDARDOWEJ UCR Standardowa Biblioteka UCR dostarcza szerokiej kolekcji podprogramów zbioru znaków. Podprogramy te pozwalają nam stworzyć zbiór, wyzerować zbiór (ustawić je na pusty zbiór), dodawać i usuwać jedną lub więcej pozycji , przetestować członków zbioru , skopiować zbiór, obliczać sumę, iloczyn i różnice i wyciąganie pozycji ze zbioru, Chociaż przeznaczone go manipulowania zbiorami znaków, możemy użyć podprogramów zbioru znaków StdLib do manipulowania każdym zbiorem z 256 lub mniejszą ilością pozycji. Pierwszą rzeczą do odnotowania o zbiorach StdLib jest ich format pamięciowy. 256 bitowa tablica zazwyczaj używa 32 kolejnych bajtów. Z powodu wydajności, zbiory formatów Standardowej biblioteki upakowywują osiem oddzielnych zbiorów do 272 bajtów (256 bajtów dla ośmiu zbiorów plus 16 bajtów górnych). Dla deklaracji zmiennych zbioru w segmencie danych powinniśmy użyć makra set. Makro to przybiera postać: set SetName1, SetName2...., SetName8 Setname1...SetName8 przedstawia nazwy do ośmiu zmiennych zbioru. Możemy mieć mniej osiem nazw w polu . operandu, ale robiąc tak zmarnujemy kilka bitów w ustawieniu tablicy Podprogram CreateSets dostarcza innego mechanizmu dla tworzenia zmiennych zbioru. W odróżnieniu od makra set, którego używaliśmy do tworzenia zmiennej zbioru w segmencie danych, podprogram CreateSets alokuje pamięć dla ośmiu dynamicznych zbiorów w czasie wykonania. Zwraca wskaźnik do pierwszej zmiennej zbioru w es:di. Pozostałe siedem zbiorów następuje w lokacjach es:di+1, es:di+2......, es:di+7. Typowy program, który alokuje zmienne zbioru dynamicznie może mieć taki kod: Set0 Set1 Set2 Set3 Set4 Set5 Set6 Set7

dword ? dword ? dword ? dword ? dword ? dword ? dword ? dword ? CreateSets mov word mov word mov word mov word mov word mov word mov word mov word mov inc

ptr ptr ptr ptr ptr ptr ptr ptr

Set0+2, es Set1+2, es Set2+2, es Set3+2, es Set4+2, es Set5+2, es Set6+2, es Set7+2, es

word ptr Set0, di di

mov inc mov inc mov inc mov inc mov inc mov inc mov inc

word di word di word di word di word di word di word di

ptr Set1,di ptr Set2, di ptr Set3, di ptr Set4, di ptr Set5, di ptr Set6, di ptr Set7, di

Ten fragment kodu tworzy osiem różnych zbiorów na stercie, wszystkie puste, i przechowuje wskaźnik do nich w stosownej zmiennej wskaźnikowej. Plik SHELL.ASM dostarcza skomentowanej lini kodu w segmencie danych, która zawiera plik STDSETS.A. Ten plik dostarcza definicji dla ośmiu powszechnie stosowanych zbirów znaków. Są to alpha (duże i małe litery alfabetu), lower (małe litery alfabetu), upper (duże litery alfabetu), digits ((„0”...”9”), xdigits („0”...”9”, „A”...”F”, i „a”...”f”), alphanum (duże i małe litery plus cyfry), whitespace (spacja, tabulator, powrót karetki, przesuniecie o jedną linię) i delimiters (białe znaki plus przecinki, średniki, mniejsze niż ,większy niż i pasek poziomy). Jeśli chcielibyśmy użyć tych standardowych zbiorów znaków w naszym programie, musimy usunąć średniki z początku instrukcji include w pliku SHELL.ASM. Standardowa Biblioteka UCR dostarcza 16 podprogramów zbioru znaków: CreateSets, EmptySet, RangeSet, AddStr, AddStrl , RmvStr, RmvStrl, AddChar, RmvChar,Member,CopySet, SetUnion, SetIntersect, SetDifference, NextItem i RmvItem. Wszystkie te podprogramy z wyjątkiem CreateSets wymagają wskaźnika do zmiennej zbioru znaków w rejestrach es:di. Określone podprogramy mogą wymagać również innych parametrów. Podprogram EmptySet zeruje wszystkie bity w zbiorze tworząc zbiór pusty. Podprogram ten wymaga adresu zmiennej zbioru w es:di. Poniższy przykład zeruje zbiór wskazywany przez Set1: les si, Set1 EmptySet RangeSet łączy ,w pewnym zakresie, wartości w zmiennej zbioru wskazywanej przez es:di. Rejestr al. zawiera dolną granicę zakresu pozycji, ah zawiera górną granicę. Zauważmy, że al. musi być mniejszy niż lub równy ah. Poniższy przykład konstruuje zbiór wszystkich znaków sterujących (kody ASCII od jeden do 31, znak null [kod ASCII zero] nie jest ujęty w tym zbiorze): les di, CtrlCharSet ;wskaźnik do ctrl char set mov al., 1 mov ah, 31 RangeSet AddStr i AddStrl dodają wszystkie znaki w ciągu zakończonym zerem zbiór znaków. Dla AddStr para rejestrów dx:si wskazuje ciąg zakończony zerem. Dla AddStrl ciąg zakończony występuje po wywołaniu AddStrl w strumieniu kodu. Podprogramy te łączą każdy znak określonego ciągu w zbiór. Poniższy przykład dodaje cyfry i znaki specjalne w zbiór FPDigits: Digits FPDigits

byte set dword ldxi les AddStr -

„0123456789”, 0 FPDigitsSet FPDigitsSet

Digits di, FPDigits

;ładuje DX:SI adresem Digits

les di, FPDigits AddStrl byte „Ee.+-„, 0 RmvStr i RmvStrl usuwają znaki ze zbioru. Znaki dostarczamy w ciągu zakończonym zerem. Dla RmvStr, dx:si wskazuje ciąg znaków do usunięcia z ciągu. Dla RmvStrl, ciąg zakończony znakiem występuje po wywołaniu. Poniższy przykład używa RmvStrl usuwa specjalne symbole z FPDigits: les di, FPDigits RmvStrl byte „Ee.+-„, 0 Podprogramy AddChar i RmvChar pozwalają nam dodawać lub usuwać pojedyncze znaki. Jak zwykle, es:di wskazuje zbiór; rejestr al zawiera znak jaki życzymy sobie dodać lub usunąć ze zbioru. Poniższy przykład dodaje spację do zbioru FPDigits i usuwa znak „,” (jeśli jest): les di, FPDigits mov al., ‘ ‘ AddChar les di, FPDigits mov al., ‘,’ RmvChar Funkcja Member sprawdza czy znak jest obecny w zbiorze. Na wejściu es:di musi wskazywać zbiór a al musi zawierać znak do sprawdzenia. Na wyjściu, flaga zera jest ustawiana jeśli znak jest zawarty w zbiorze, flaga ta będzie wyzerowana jeśli znak nie należy do tego zbioru. Poniższy przykład odczytuje znaki z klawiatury dopóki użytkownik nie naciśnie klawisza, który jest białym znakiem SkipWS:

get lesi WhiteSpace member je SkipWS

;odczyt znaku od użytkownika do AL. ;adres zbioru WS w es:di

Podprogramy CopySet, SetUnion, SetIntersect i SetDifference działają na dwóch zbiorach znaków. Rejestry es:di wskazują zbiór znaków przeznaczenia, rejestry dx:si wskazują źródłowy zbiór znaków. CopySet kopiuje bity ze zbioru źródłowego do zbioru przeznaczenia, zamieniając oryginalne bity w zbiorze przeznaczenia. SetUnion oblicza sumę dwóch zbiorów i przechowuje wynik w zbiorze przeznaczania. SetIntersect oblicza iloczyn logiczny zbiorów i przechowuje wynik w zbiorze przeznaczenia. W końcu podprogram SetDifference oblicza DestSet : = DestSet – SrcSet. Podprogramy NextItem i RmvItem pozwalają nam wyodrębnić elementy ze zbioru. NextItem zwraca w al. kod ASCII pierwszego znaku znalezionego w zbiorze. RmvItem robi to samo z tym, że usuwa ten znak ze zbioru. Podprogramy te zwracają zero w al. jeśli zbiór jest pusty (zbiory StdLib nie mogą zawierać znaku NULL) Możemy użyć podprogramu RmvItem do zbudowania podstawowego iteratora dla zbioru znaków. Podprogramy zbiorów znaków Standardowej Biblioteki UCR są bardzo mocne. Z nimi możemy łatwo manipulować danymi ciągów znaków, zwłaszcza kiedy poszukujemy różnych wzorców wewnątrz ciągów. Będziemy rozpatrywać te podprogramy później kiedy będziemy się zajmować później w tym tekście dopasowywaniem do wzorca. 15.6 UZYWANIE INSTRUKCJI CIAGÓW Z INNYMI TYPAMI DANYCH Instrukcje ciągów działają z innymi typami danych niż tylko ciągi znaków. Możemy użyć instrukcji ciągów do kopiowania całych tablic z jednej zmiennej do innej, inicjalizowania dużych struktur danych pojedynczą wartością lub do porównywania całych struktur danych dal równości lub nierówności. Jeśli kiedyś będziemy działać na strukturach danych zawierających kilka bajtów ,możemy zastosować instrukcje ciągów. 15.6.1 CIAGI CAŁKOWITE O WIELOKTOTNEJ PRECYZJI Instrukcja cmps jest użyteczna przy porównaniu (bardzo) dużych wartości całkowitych. W odróżnieniu od ciągów znaków nie możemy porównywać liczb całkowitych cmps od najmniej znaczącego bajtu do bajtu

najbardziej znaczącego. Zamiast tego musimy porównywać je od bardziej znaczącego bajtu w dół do bajtu najmniej znaczącego. Poniższy kod porównuje dwie 12 bajtowe wartości całkowite: lea di, integer1+10 lea si, integer2+10 mov cx, 6 std repe cmpsw Po wykonaniu instrukcji cmpsw, flagi będą zawierały wynik porównania. Możemy łatwo przypisać jeden długi ciąg całkowity do innego przy zastosowaniu instrukcji movs. Nic skomplikowanego, po prostu ładujemy rejestry si, di i cx i mamy to. Możemy wykonać inne operacje, wliczając w to operacje arytmetyczne i logiczne używając metod poszerzonej precyzji opisanej w rozdziale o operacjach arytmetycznych.

15.6.2 DZIAŁANIE Z CAŁYMI TABLICAMI I REKORDAMI Jedynymi operacjami które stosujemy, generalnie , do wszystkich struktur tablicowych i rekordowych są przydzielanie i porównywanie (tylko dla równość / nierówność). Możemy zastosować instrukcji movs i cmps dla tych działań. Działania takie jak dodawanie skalarne, transpozycje itp. mogą być łatwo zsyntetyzowane przy zastosowaniu instrukcji lods i stos. Poniższy kod pokazuje jak łatwo można dodać wartość 20 do każdego elementu tablicy całkowitej A: lea si, A mov di, si mov cx, SizeOfA cld AddLoop: lodsw add ax, 20 stosw loop AddLoop Możemy zaimplementować inne operacje w podobny sposób 15.10 PODSUMOWANIE 80x86 dostarcza potężnego zbioru instrukcji ciągów. Jednak instrukcje te są bardzo prymitywne, użyteczne głównie do manipulowania blokami bajtów. Nie odpowiadają one instrukcjom ciągów jakie można znaleźć w językach wysoko poziomowych, Możemy jednak użyć instrukcji ciągów 80x86 do zsyntetyzowania tych funkcji normalnie powiązanych z HLL’ami. Rozdział ten wyjaśnia jak zbudować wiele z bardzo popularnych funkcji ciągów. Oczywiście głupotą jest stale odkrywanie koła, więc rozdział ten również opisuje wiele funkcji ciągów dostępnych w Bibliotece Standardowej UCR. Instrukcje ciągów 80x86 dostarczają podstaw dla wielu operacji na ciągach pojawiających się w tym rozdziale. Dlatego też rozdział ten zaczyna się od przeglądu i szczegółowego omówienia instrukcji ciągów 80x86: przedrostków powtórzenia i flagi kierunku. Rozdział ten omawia działanie każdej instrukcji ciągu i opisuje jak możemy jej użyć do wykonania odnośnych zadań. Aby zobaczyć jak działają instrukcje ciągów zobacz: • „Instrukcje ciągów 80x86” • „Jak działają instrukcje ciągów” • „Przedrostki REP/REPE/REPZ i REPNZ/REPNE” • „Flaga kierunku” • „Instrukcja MOVS’ • „Instrukcja CMPS • „Instrukcja SCAS” • „Instrukcja STOS” • Instrukcja LODS” • „Budowanie złożonych funkcji ciągów z LODS i STOS” • „Przedrostki i instrukcje ciągów”

Chociaż Intel nazwał je „instrukcjami ciągów” w rzeczywistości nie działają one na abstrakcyjnych typach danych, jak zwykle myślimy o ciągach znaków. Instrukcje ciągów po prostu manipulują tablicami bajtów, słów lub podwójnych słów. Niestety nie ma pojedynczej definicji ciągu znaków co, bez wątpienia, jest powodem, ze nie ma specjalnych instrukcji w zbiorze instrukcji 80x86. Dwa z najbardziej popularnych typów ciągów znaków to ciągi z przedrostkiem długości i ciągi zakończone zerem, których używają, odpowiednio, Pascal i C • •

„Ciągi znaków” „Typy ciągów”

Ponieważ zdecydowaliśmy się określić typ danych dla naszych ciągów znaków, następnym krokiem jest implementacja różnych funkcji dla przetwarzania tych ciągów. Rozdział ten dostarcza przykładów kilu różnych funkcji ciągów stworzonych specjalnie dla ciągów z przedrostkiem długości. Nauczmy się o tych funkcjach i zobaczmy kod ,który je implementuje przeglądając następujące sekcje: • „Przypisywanie ciągów” • „Porównywanie ciągów” • „Funkcje ciągów znaków” • „Substr” • „Index” • „Repeat” • „Insert” • „Delete” • „Konkatenacja” Biblioteka Standardowa UCR dostarcza bardzo bogatego zbioru funkcji ciągów specjalnie stworzonych dla ciągów zakończonych zerem. Po opis wielu z tych podprogramów sięgnij do poniższych sekcji: • „Funkcje ciągów w Standardowej Bibliotece UCR” • „StrBDel, StrBDelm” • „Strcat, Strcatl, Strcatm, Strcatml” • „Strchr” • „Strcmp, Strcmpl, Stricmp, Stricmpl” • „Strcpy, Strcpyl, Strdup, Strdupl” • „Strdel ,Strdelm” • „Strins, Strinsl, Strinsm, Strinsml” • „Strlen” • „Strlwr, Strlwrm, Strupr, Struprm” • „Strrev, Strrevm” • „Strset, Strsetm” • „Strspan, Strspanl. Strcspan, Strcspanl” • „Strstr, Strstrl” • „Strtrim, Strtrimm” • „Inne podprogramy ciągów w Bibliotece Standardowej UCR” Jak wspomniano wcześniej, instrukcje ciągów są całkiem użyteczne dla wielu operacji poza manipulowaniem ciągiem znaków. Rozdział ten zamyka sekcje opisujące inne zastosowania dla instrukcji ciągów. • „Zastosowanie instrukcji ciągów z innymi typami danych” • „Ciągi całkowite o wielokrotnej precyzji” • „Działanie z całymi tablicami i rekordami” Zbiór jest innym powszechnym abstrakcyjnym typem danych znajdowanym w dzisiejszych programach. Zbiór jest strukturą danych, która przedstawia członków (lub brak takowych) jakiejś grupy obiektów. Jeśli wszystkie obiekty są tego samego podstawowego typu i jest ograniczona liczba możliwych obiektów w tym zbiorze, wtedy możemy użyć wektora bitów (tablicy wartości boolowskich) dla przedstawienia zbioru. Implementacja wektora bitów jest bardzo wydajny dla małych zbiorów. Biblioteka Standardowa UCR

dostarcza kilku podprogramów do manipulacji zbiorami znaków i innymi zbiorami z maksimum 256 składowymi. • „Podprogramy zbioru znaków w Bibliotece Standardowej UCR”

15.11 PYTANIA 1) Do czego są używane przedrostki powtórzenia? 2) Jakie przedrostki ciągów są używane z następującymi instrukcjami? a) MOVS b) CMPS c) STOS d) SCAS 3) Dlaczego nie ma, zwykle używanych, przedrostków powtórzeń z instrukcją LODS/ 4) Co się stanie z rejestrami SI,DI i CX kiedy jest wykonywana instrukcja MOVSB (bez przedrostka powtórzenia) i : a) jest ustawiona flaga kierunku b) flaga kierunku jest wyzerowana 5) Wyjaśnij jak działają instrukcje MOVSB i MOVSW. Opisz jak wpływają one na pamięć i rejestry z i bez przedrostka powtórzenia. Opisz co się stanie kiedy flaga kierunku jest ustawiona i wyzerowana. 6) Jak zachowamy wartość flagi kierunku przy wywołaniu procedury? 7) Jak możemy zapewnić, że flaga kierunku zawsze zawierać będzie właściwą wartość przed instrukcją ciągu bez zachowania jej wewnątrz procedury? 8) Jak jest różnica pomiędzy instrukcjami „MOVSB”, „MOVSW” i „MOVS oprnd1, oprnd2”? 9) Rozważ definicję tablicy Pascalowskiej: a: array [0..31] of record a,b,c :char i,j,k: integer; end; Zakładając, że A[0] zostało zainicjalizowane jakąś wartością, wyjaśnij jak można użyć instrukcji MOVS do inicjalizacji pozostałych elementów A taką samą wartością jak w A[0} 10) Podaj przykład działania MOVS kiedy wymagane jest aby flaga kierunku była: a) wyzerowana b) ustawiona 11) 12) 13) 14) 15) 16) 17) 18) 19) 20) 21)

22)

Jak działa instrukcja CMPS? (Co robi? Jak wpływa na rejestry i flagi itp.) Który segment zawiera ciąg źródłowy” Ciąg przeznaczenia? Do czego używamy instrukcji SCAS? Jak szybko można zainicjalizować całą tablicę zerami? Jak są używane instrukcje LODS i STOS do zbudowania złożonych operacji ciągów Jak można zastosować funkcję SUBSTR do wydobycia podciągu o długości 6 poczynając od offsetu 3 w zmiennej StrVar przechowując podciąg w zmiennej NewStr? Jaki rodzaj błędu może wystąpić kiedy jest wykonywana funkcja SUBSTR? Podaj przykład demonstrujący użycie każdej z poniższych funkcji ciągu: a) INDEX b) REPEAT c) INSERT d) DELETE e) CONCAT Napisz krótką pętlę, która mnoży każdy element jednowymiarowej tablicy przez 10. Użyj instrukcji ciągów do pobrania i przechowania każdego elementu tablicy. Biblioteka Standardowa UCR nie dostarcza podprogramu STRCPYM. Jaki jest podprogram, który wykonuje to zadanie? Przypuśćmy, że napisałeś „grę przygodową” w której gracz wypisuje zdania a ty chcesz wybrać dwa słowa „GO” i „NORTH”, jeśli są obecne, w lini wejściowej. Jakiej (nie StdLib UCR) funkcja pojawiająca się w tym rozdziale użyłbyś do wyszukania tych słów? Jaki podprogram Biblioteki Standardowej UCR wykonałby to? Wyjaśnij jak wykonać porównanie całkowite o podwyższonej precyzji używając CMPS.

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK Konsultacje naukowe: NEKRO [email protected] [email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ SZESNASTY: DOAPSOWANIE DO WZORCA Ostatni rozdział omawiał ciągi znaków i różne działania na tych ciągach. Typowy program odczytuje sekwencję ciągu od użytkownika i porównuje ciągi czy są dopasowane. Na przykład DOS’owski program COMMAND.COM odczytuje linię poleceń od użytkownika i porównuje ciąg użytkownika z wpisanym stałym ciągiem takim jak „COPY”, „DEL”, „RENAME” i tak dalej. Takie polecenia są łatwe do analizowania ponieważ zbiór dostępnych poleceń jest skończony i stały. Czasami jednak ciągi jakie chcemy przetestować nie są stałe; zamiast tego należą do (możliwie nieskończonego) zbioru różnych ciągów. Na przykład jeśli wykonujemy DOS’owe polecenie „DEL .BAK”, MS-DOS nie próbuje usunąć pliku nazwanego „.BAK”. Zamiast tego usuwa wszystkie pliki, do których pasuje ogólny wzorzec „.BAK”. To oczywiście jest każdy plik, który zawiera cztery lub więcej znaków i kończą się „.BAK”. W świecie MS-DOS, ciąg zawierający znaki takie jak „*” i „?” są nazywane symbolami wieloznacznymi; znaki symboli wieloznacznych po prostu dostarczają sposobu do określenia różnych poprzez wzorce. DOS’owe znaki symboli wieloznacznych maja bardzo ograniczoną postać, co jest znane jako wyrażenia regularne; wyrażenia regularne mają , generalnie, ograniczoną postać wzorca. Rozdział ten opisuje jak stworzyć wzorzec, który dopasowuje różne ciągi znaków i pisać podprogramy dopasowania do wzorca, aby zobaczyć czy szczególny ciąg dopasowany jest do danego wzorca. 16.1 WPROWADZENIE DO TEORII JĘZYKA FORMALNEGO (AUTOMATÓW) Dopasowanie do wzorca jest ważnym tematem w informatyce. Istotnie, dopasowanie do wzorca jest głównym paradygmatem programistycznym w kilku językach programowania takich jak Prolog, SNOBOL i Icon Kilka programów używanych cały czas stosuje dopasowanie do wzorca jako ważną część ich pracy. MASM na przykład używa dopasowania do wzorca do określenia czy symbole są poprawnie sformułowane, wyrażenia są właściwe itd. Kompilatory języków wysokiego poziomu jak Pascal i C również używają dopasowania do wzorca dla analizy pliku źródłowego określając czy jest on syntaktycznie poprawny. Niespodziewanie dość, ważne wyrażenie znane jako Hipoteza Church’a sugeruje, że każda obliczalna funkcja może być zaprogramowana jako problem dopasowania do wzorca. Oczywiście, nie ma żadnej gwarancji, że to rozwiązanie będzie wydajne (zazwyczaj nie jest) ale możemy dojść do poprawnego rozwiązania. Prawdopodobnie nie będziemy musieli nic wiedzieć o maszynie Turinga (temat hipotezy Church’a) jeśli interesuje nas pisanie, powiedzmy obliczanie otrzymanych pakietów. Jednakże, jest wiele sytuacji gdzie możemy chcieć wprowadzić umiejętność dopasowania jakiegoś ogólnego wzorca; więc zrozumienie teorii dopasowania do wzorca jest ważna. Te obszar informatyki nazywa się teorią języka formalnego lub teorią automatów. Kursy z tego tematu są często mniej niż popularne ponieważ wprowadzają one dużo dowodów, matematyki i , cóż, teorii. Jednakże, pojęcia poza dowodami są całkiem proste i bardzo użyteczne. W rozdziale tym nie będziemy się zajmowali próbować dowodzić wszystkiego o dopasowaniu do wzorca. Zamiast tego będziemy akceptować fakt, że działa to w rzeczywistości i stosujemy to. Pomimo to musimy omówić pewne tematy z teorii automatów, więc bez dalszych wstępów. 16.1.1 MASZYNY KONTRA JĘZYKI Znajdziemy odniesienie do terminu „maszyna” w całej literaturze teorii automatów. Termin ten nie odnosi się do jakichś określonych komputerów , na których wykonuje się program. Zamiast tego, jest to zazwyczaj jakaś funkcja, która odczytuje ciąg symboli na wejściu i tworzy jeden lub dwa wyjścia :dopasowanie lub niepowodzenie. Typowa maszyna (lub automat) dzieli wszystkie możliwe ciągi na dwa zbiory – te ciągi, która akceptuje (lub dopasowuje) i te ciągi które odrzuca. Język akceptowany przez tą maszynę jest zbiór wszystkich ciągów, które maszyna akceptuje. Zauważ, że ten język może być nieskończony, skończony lub zbiorem pustym (tj. maszyna odrzuca wszystkie ciągi wejściowe). Zauważ też, że język nieskończony nie wskazuje, ze maszyna akceptuje wszystkie ciągi. Jest całkiem możliwe, że maszyna akceptuje nieskończony liczbę ciągów a odrzuca

większą liczbę ciągów. Na przykład, bardzo łatwo jest zaprojektować funkcję, która akceptuje wszystkie ciągi których długość jest wielokrotnością trzech. Funkcja ta akceptuje nieskończoną liczbę ciągów (ponieważ jest nieskończona liczba ciągów , których długość jest wielokrotnością trzech) mimo to odrzuca dwa razy więcej ciągów niż akceptuje. Jest to bardzo łatwa funkcja do napisania. Rozważmy poniższy program 80x86, który akceptuje wszystkie ciągi o długości trzy (zakładając, że znak powrotu karetki kończy ciąg): MatchLen3

Failure: Accept: MatchLen3

proc near getc cmp al., cr je Accept getc cmp al., cr je Failure getc cmp al., cr jne Match Len3 mov ax, 0 ret mov ax, 1 ret endp

;pobiera znak #1 ;znak zera jeśli EOLN ;pobranie znaku #2 ;pobranie znaku #3 ;zwraca zero oznaczające niepowodzenie ;zwraca jeden oznaczające powodzenie

Przez śledzenie całego kodu powinniśmy łatwo przekonać się sami, że zwraca jeden w ax jeśli powiodło się (odczytano ciąg, którego długość jest wielokrotnością trzech) i zero w przeciwnym razie. Maszyny są z natury rozpoznawaczami. Sama maszyna jest ucieleśnieniem wzorca. Rozpoznaje każdy ciąg wejściowy, który dopasowuje do wbudowanego wzorca. Dlatego też, kodyfikacja tych automatów jest podstawową pracą programisty, który chce dopasować jakieś wzorce. Jest wiele różnych maszyn i języków, które one rozpoznają. Od prostych do złożonych, ważna klasyfikacją jest deterministyczny skończony stan automatów (który jest ekwiwalentem niedeterministycznego skończonego stanu automatów), deterministyczny automat stosowy, niedeterministyczny automat stosowy i maszyna Turinga. Każda kolejna maszyna na tej liście dostarcza nadzbioru zdolności maszyn pojawiających się przed nią. Jedynym powodem dla którego nie używamy maszyny Turinga dla wszystkiego, jest to, że jest dużo bardziej złożone zaprogramowanie niż, powiedzmy, deterministycznego skończonego stanu automatu. Jeśli możemy dopasować wzór używając deterministycznego skończonego stanu automatu, prawdopodobnie będziemy chcieli zakodować go w ten sposób niż jako maszynę Turinga. Każda klasa maszyny ma klasę języka z nią powiązaną. Deterministyczne i niedeterministyczne skończone stany automatów rozpoznają język regularny. Niedeterministyczny automat stosowy rozpoznaje język bez kontekstowy. Maszyna Turinga może rozpoznać wszystkie rozpoznawalne języki. Będziemy omawiali każdy z tych zbiorów języków i ich właściwości po kolei. 16.1.2 JĘZYKI SKOŃCZONE Języki skończone są najmniej złożonymi językami opisanymi w poprzedniej sekcji. Nie znaczy to, że są mniej użyteczne; faktycznie, wzorce oparte o wyrażenia skończone są prawdopodobnie bardziej popularne niż inne 16.1.2.1 WYRAŻENIA SKOŃCZONE Najbardziej zwartym sposobem określenia ciągów, które należą do języka skończonego jest wyrażenie skończone. Zdefiniujemy wyrażenia skończone według następujących zasad : • ∅ (zbiór pusty) jest jeżykiem skończonym i oznacza zbiór pusty • ε jest wyrażeniem skończonym. Oznacza zbiór języków zawierających tylko pusty ciąg: {ε}. • Każdy pojedynczy symbol ,a ,jest wyrażeniem skończonym (będziemy używali małych liter do oznaczania przypadkowych symboli). Ten pojedynczy symbol dopasowuje dokładnie jeden znak w ciągu wejściowym, który to znak musi być równy pojedynczemu symbolowi w wyrażeniu skończonym. Na przykład, wzorzec „m” dopasowuje pojedynczy znak „m” w ciągu wejściowym.

Zauważmy, że ∅ i ε nie są tym samym. Zbiór pusty jest skończonym językiem, który nie akceptuje żadnego ciągu, wliczając ciągi o długości zero. Jeśli język skończony jest oznaczony przez {ε} wtedy akceptuje dokładnie jeden ciąg, ciąg długości zero. Ten ostatni język skończony akceptuje coś, pierwszy nie. Trzecia z powyższych zasad dostarcza nam podstaw dla definicji rekurencji. Teraz będziemy definiować wyrażenie skończone rekurencyjnie. W następujących definicjach, zakładamy, że r , s i t są poprawnymi wyrażeniami skończonymi. • Konkatenacja. Jeśli r i s są wyrażeniami skończonymi, więc to rs. Wyrażenie skończone rs dopasowuje każdy ciąg, który zaczyna się ciągiem dopasowanym przez r i kończy ciągiem dopasowanym przez s • Suma logiczna / Unia. Jeśli r i s są wyrażeniami skończonymi, więc r | s (czytamy to jako r lub s) Jest to odpowiednik dla r ∪ s (czytamy jako r unia s). To wyrażenie skończone dopasowuje każdy ciąg , który dopasowuje r lub s • Iloczyn logiczny. Jeśli r i s są wyrażeniami skończonymi, więc r ∩ s. Jest to zbiór wszystkich ciągów, które dopasowują oba r i s. • Kleene Star. Jeśli r jest wyrażeniem skończonym, więc r*. To wyrażenie skończone dopasowuje zero lub więcej wystąpień r. To znaczy, dopasowuje ε, r, rr, rrr, rrrr,....... • Różnica. Jeśli r i s są wyrażeniami skończonymi, więc r – s. To oznacza zbiór ciągów dopasowanych przez r, które nie są dopasowane również przez s. • Pierwszeństwo. Jeśli r jest wyrażeniem skończonym, więc (r ). To dopasowuje każdy ciąg dopasowany przez samo r. Normalne algebraiczne prawa łączności i rozdzielności mają tu zastosowanie, więc (r | s) t jest odpowiednikiem rt | st. Operatory te wykorzystują zwykłe prawa łączności i rozdzielności mają następujące pierwszeństwo; Najwyższe:

Najniższe:

(r ) Kleene Star Konkatenacja Iloczyn logiczny Różnica Suma logiczna

Przykłady: (r | s) t = rt | st rs* = r(s*) r ∪ t – s = r ∪ (t – s) r ∩ t – s = (r ∩ t) – s Generalnie, będziemy używali nawiasów okrągłych aby uniknąć niejasności. Chociaż ta definicja jest wystarczająca dla klasy teorii automatów, są praktyczne aspekty tej definicji, która pozostawia trochę do życzenia. Na przykład, dla zdefiniowania wyrażenia skończonego, które dopasowuje pojedynczy znak alfabetu, będziemy musieli stworzyć coś takiego jak (a | b | c |.... | y | z). Trochę pisania jak na tak trywialny zbiór znaków. Dlatego też powinniśmy dodać jakąś notację aby uczynić łatwiejszym określanie wyrażeń skończonych. • Zbiór Znaków. Każdy zbiór znaków otoczonych przez nawiasy kwadratowe np. [abcdefg] jest wyrażeniem skończonym i dopasowuje pojedynczy znak ze zbioru. Możemy określić zakres znaków używając myślnika tj. „[a – z]” oznaczający zbiór małych liter a to wyrażenie skończone dopasowuje pojedynczy znak małej litery • Kleene Plus. Jeśli r jest wyrażeniem skończonym, więc r+. To wyrażenie skończone dopasowuje jedno lub więcej wystąpień r. To znaczy, dopasowuje r, rr, rrr, rrrr,.. Pierwszeństwo Kleene Plus jest takie samo jak dla Kleene Star. Zauważmy, że r+ = rr* . • Σ przedstawia dowolny pojedynczy znak z dostępnego zbioru znaków. Σ* przedstawia zbiór wszystkich możliwych ciągów. Wyrażenie skończone Σ* - r jest uzupełnieniem r – to znaczy, zbiór wszystkich ciągów, których r nie dopasowało Nadszedł czas aby omówić jak w rzeczywistości używamy wyrażeń skończonych przy specyfikacji dopasowania do wzorca. Następujące przykłady powinny nam dać odpowiednie wprowadzenie

Identyfikatory:

Większość języków programowania, takich jak Pascal lub C/C++ określa poprawne formy dla identyfikatorów używając wyrażeń skończonych. Używając angielskiej terminologii określamy je: „Identyfikator musi zaczynać się znakiem alfabetu i następuje po nim zero lub więcej znaków alfanumerycznych lub znaku podkreślenia.” Używając składni wyrażenia skończonego (WS) opisanej w tej sekcji, identyfikator to [a-zA-Z][a-zA-Z0-9_]*

Stałe Całkowite: Wyrażenie skończone dla stałych całkowitych jest relatywnie łatwe do zaprojektowania .Stałe całkowite składają się opcjonalnie z plus lub minusa i następujących po nich jednej lub więcej cyfr. WS to (+ | - | ε | ) [0-9]+. Zauważ, że użycie pustego ciągu ( ε ) czyni plus lub minus opcjonalnym Stałe rzeczywiste: stałe rzeczywiste są trochę bardziej złożone, ale łatwe do określenia przy użyciu WS. Nasz definicja wzorca, która dla stałych rzeczywistych pojawia się w programie pascalowskim – opcjonalnie plus lub minus, po którym następuje jedna lub więcej cyfr; opcjonalnie następuje punkt dziesiętny i zero lub więcej cyfr; opcjonalnie następuje po „e” lub „E” z opcjonalnym znakiem i jedną lub więcej cyframi: ( + | - | ε ) [0-9]+ ( „ .”[0-9]* | ε ) (((e | E) (+ | - | ε )[0-9]+ ) | ε) Ponieważ WS jest relatywnie złożone, powinniśmy je rozłożyć kawałek po kawałku. Pierwszy człon w nawiasach daje nam opcjonalny znak. Jedna lub więcej cyfr są obowiązkowe przed punktem dziesiętnym, drugim dostarczonym członem. Trzeci człon pozwala ma punkt dziesiętny po którym następuje zero lub więcej cyfr. Ostatni człon dostarcza opcjonalnego wykładnika składającego się z „e” lub „E”, następujący po opcjonalnym znaku lub jednej lub więcej cyfrach. Słowa zarezerwowane: Jest bardzo łatwo dostarczyć wyrażenie skończone, które dopasowuje zbiór zarezerwowanych słów. Na przykład, jeśli chcemy stworzyć wyrażenie skończone, które dopasowuje słowa zarezerwowane MASM’a , możemy użyć WS podobnego do tego Parzystość: Zdania:

(mov | add | and |... | mul) Wyrażenie skończone (ΣΣ)* dopasowuje wszystkie ciągi, których długość jest wielokrotnością dwóch Wyrażenie skończone: (Σ* „ „ *)* run („ „ +(Σ* „ „+ | ε )) fast („ „ Σ*)*

Rysunek 16.1 NFA dla Wyrażenia Skończonego (+|-|e)[0-9]+(„.”[0-9]|e)(((e|E)(+|-e)[0-9]+)|e dopasowuje wszystkie ciągi, które zawierają oddzielne słowa „run” następujące po nim „fast” gdzieś w linii. To dopasowuje ciągi jakie „I want to run very fast” i „run as fast as you can” tak jak i „run fast”

Podczas gdy WS są dogodne do określania wzorca jaki chcemy rozpoznać, nie są one szczególnie użyteczne tworzenia programów (tj. „maszyn”), które w rzeczywistości rozpoznają takie wzorce. Zamiast tego, powinniśmy najpierw skonwertować WS do niedeterministycznego skończonego stanu automatu, lub NFA. Jest bardzo łatwo skonwertować NFA do programu asemblerowego 80x86; jednakże takie programy rzadko są wydajne tak jak mogłyby być. Jeśli wydajność jest dużym zmartwieniem, możemy skonwertować NFA do deterministycznego skończonego stanu automatu (DFA) , który również jest łatwy do skonwertowania do kodu asemblowanego 80x86, ale konwersja jest dużo bardziej sprawna 16.1.2.2 NIEDERTMINISTYCZNE SKOŃCZONE STANY AUTOMATÓW (NFA) NFA jest bezpośrednim wykresem z liczbą stanów powiązanych z każdym węzłem i znakiem lub ciągiem znaków powiązanych z każdym brzegiem wykresu. Stan wyróżniający się, stan startowy określa gdzie maszyna zaczyna próbę dopasowania ciągu wejściowego. Maszyna w stanie startowym porównuje znaki wprowadzone ze znakami lub ciągami na każdym brzegu wykresu. Jeśli zbiór znaków wejściowych jest dopasowany do jednego z brzegów, maszyna może zmienić stan z węzła na początku brzegu (ogon) do stanu na końcu brzegu (głowa) Pewne inne stany, znane jako końcowy lub akceptowalny, są zazwyczaj również obecne Jeśli maszyna przeszła do stanu końcowego po wyczerpaniu wszystkich znaków wejściowych, wtedy maszyna ta akceptuje lub dopasowuje ten ciąg. Jeśli maszyna wyczerpała już wprowadzane znaki i przeszła do stanu, który nie jest stanem końcowym, wtedy maszyna ta odrzuca ten ciąg. Rysunek 16.1 pokazuje przykład NFA dla zmienno przecinkowego WS przedstawianego wcześniej. Przez konwencję, zawsze będziemy zakładać, ze stan startowy to stan zero. Oznaczymy stany końcowe (których może być więcej niż jeden) przez użycie podwójnego okręgu dla tego stanu ( powyżej stan osiem jest stanem końcowym). NFA zawsze zaczyna się ciągiem wejściowym w stanie startowym (stan zero). Na każdym brzegu wychodzącym ze stanu jest albo ε, pojedynczy znak lub ciąg znaków. Pomoc przy nie zasłoniętym diagramie NFA, pozwoli na wyrażenia w postaci „xxx | yyy | zzz |...” gdzie xxx, yyy, zzz są to ε , pojedynczym znakiem lub ciągiem znaków. Odpowiada to wielokrotnym brzegom z jednego stanu do innego z pojedynczą pozycją na każdym brzegu. W powyższym przykładzie :

0

+|-|ε

1

jest odpowiednikiem +

0

-

1

ε

Podobnie jak pozwolimy zbiorowi znaków, określonemu przez ciąg w postaci x – y, oznaczyć wyrażeniem x | x+1 | x+2 | ... | y. Zauważ, że NFA akceptuje ciąg jeśli jest jakaś ścieżka ze stanu startowego do stanu akceptowalnego, która wyczerpuje ciąg wejściowy. To mogą być wielokrotne ścieżki od stanu startowego do różnych stanów końcowych. Co więcej, to może być jakaś określona ścieżka ze stanu startowego do stanu nie akceptowalnego, która wyczerpała ciąg wejściowy. Niekoniecznie to znaczy, że NFA odrzuca ten ciąg; jeśli jest jakaś inna

ścieżka ze stanu startowego do stanu akceptowalnego, wtedy NFA akceptuje ten ciąg. NFA odrzuca ciąg tylko jeśli nie ma ścieżek ze stanu startowego do stanu akceptowalnego, które wyczerpują ten ciąg. Przejście przez stan akceptowalny nie powoduje, że NFA akceptuje ciąg. Musimy dotrzeć do stanu końcowego i wyczerpać ciąg wejściowy. Przetwarzanie ciągu wejściowego z NFA zaczyna się w stanie startowym. Brzeg wychodzący ze stanu startowego zawiera znak, ciąg lub ε, z nim powiązane. Jeśli wybierzemy przesunięcie z jednego stanu do innego wzdłuż brzegu z pojedynczym znakiem, wtedy usuwamy ten znak z ciągu wejściowego i przesuwamy do nowego stanu wzdłuż brzegu przemierzonego przez ten znak. Podobnie , jeśli wybieramy przesuniecie wzdłuż brzegu z ciągiem znaków, usuwamy ten ciąg znaków ciągu wejściowego i przełączamy do nowego stanu. Jeśli jest brzeg z pustym ciągiem ε, wtedy możemy wybrać przesunięcie do nowego stanu danego przez ten brzeg bez usuwania jakiegoś znaku z ciągu wejściowego. Rozważmy ciąg „1.25e2” i NFA z rysunku 16.1. Z punktu startowego możemy przesunąć do stanu jeden używając ciągu ε (nie ma początkowego plus lub minus, więc tylko ε jest naszą opcją). Ze stanu jeden możemy przesunąć do stanu dwa przez dopasowanie „1” w naszym ciągu wejściowym ze zbiorem 0-9, to zjada „1” z naszego ciągu wejściowego pozostawiając”.25e2”. Ze stanu dwa przesuwamy do stanu trzy i zjada kropkę z ciągu wejściowego pozostawiając „25e2”. Stan trzy jest zapętlony, wiec zjada znaki „2” i „5” z początku naszego ciągu wejściowego i wraca do stanu trzy z nowym ciągiem wejściowym „e2”. Następnym znakiem wejściowym jest „e”, ale nie ma wychodzącego brzegu ze stanu trzy z „e” na nim; jednakże mamy brzeg - ε, wiec możemy go użyć do przesunięcia do stanu cztery. To przesunięcie nie zmienia ciągu wejściowego. Ze stanu cztery możemy przesunąć do stanu pięć po znaku „e”. Zjada to „e” i pozostawia nas z ciągiem „2”. Ponieważ nie ma znaku plus lub minus ,musimy przesunąć ze stanu pięć do stanu sześć po brzegu ε. Przesuniecie ze stanu sześć do siedem zjada ostatni znak w naszym ciągu. Ponieważ ciąg jest pusty (i, w szczególności, nie zawiera żadnej cyfry), stan siedem nie może się zapętlić. Jesteśmy obecnie w stanie siedem (który nie jest stanem końcowym) a nasz ciąg wejściowy jest wyczerpany. Jednakże, możemy przesunąć do stanu ósmego 9stan akceptowalny) ponieważ przejściem pomiędzy stanem siedem a osiem jest brzeg ε.Ponieważ jesteśmy w stanie końcowym i wyczerpaliśmy ciąg wejściowy, NFA akceptuje ten ciąg wejściowy. 16.1.2.3 KONWERTOWANIE WYRAŻEŃ SKOŃCZONYCH DO NFA Jeśli mamy wyrażenie skończone i chcemy zbudować maszynę, która rozpoznaje ciągi w języku skończonym określonym przez to wyrażenie, musimy skonwertować WS do NFA. Okazuje się łatwym do skonwertowania wyrażenia skończonego do NFA. Zrobimy to posługując się następującymi zasadami: • •

NFA przedstawiające język skończony oznaczony przez wyrażenie skończone ∅ (zbiór pusty) jest pojedynczym, nie akceptowalnym stanem. Jeśli wyrażenie skończone zawiera ε, pojedynczy znak lub ciąg, tworzy dwa stany i rysuje łuk pomiędzy nimi z ε, pojedynczym znakiem lub ciągiem jako etykietą. Na przykład, WS „a” jest konwertowane do NFA jako a

* Symbol

oznacza NFA , który rozpoznaje jakiś język

skończony określony przez jakieś skończone wyrażenie r, s lub t. Jeśli wyrażenie skończone przybiera formę rs wtedy odpowiednie NFA to ε r •

s

Jeśli wyrażenie skończone przybiera postać r | s wtedy odpowiednie NFA to

ε

r

ε

s

ε

ε

* Jeśli wyrażenie skończone przybiera postać r* wtedy odpowiednie NFA to

r

ε

ε

Wszystkie inne formy wyrażeń skończonych są łatwo syntetyzowane z tych, dlatego też konwertowanie tych innych postaci wyrażeń skończonych do NFA jest po prostu dwu krokowym procesem, konwertuje Ws do jednej z tych form, a potem konwertuje tą postać do NFA. Na przykład konwertując r+ do NFA, najpierw skonwertujemy r+ do rr*. To tworzy NFA: r

ε r

ε

ε

Następny przykład konwertuje wyrażenie skończone dla stałej całkowitej do NFA. Pierwszym krokiem jest stworzenie NFA dla wyrażenia skończonego (+ | - | ε ). Kompletna konstrukcja + ε

ε ε

ε

ε

-

ε

ε

Chociaż możemy oczywiście zoptymalizować to tak

+|-ε

Następnym krokiem jest działanie na wyrażeniu skończonym [0-9]*; po optymalizacji daje to NFA

0-9

0-9 Teraz po prostu łączymy wszystko tworząc 0-9 +|-|ε

ε

0-9

Wszystko co teraz trzeba do znaleźć stan startowy i stan końcowy. Stan startowy jest zawsze pierwszym stanem NFA tworzonym przez konwersję pozycji najbardziej na lewo w wyrażeniu skończonym. Stan końcowy jest zawsze ostatnim stanem NFA tworzonym przez konwersję pozycji najbardziej na prawo w wyrażeniu skończonym. Dlatego też, kompletne wyrażenie skończone dla stałych całkowitych (po optymalizacji powyższego środkowego brzegu, który nie służy żadnemu celowi) to 0-9 0-9

0

+|-|ε

1

2 0-9

16.1.2.4 KONWERSJA NFA DO JĘZYKA ASEMBLERA Jest tylko jeden ważny problem z konwersją NFA do właściwej funkcji dopasowującej – NFA jest nie deterministyczne. Jeśli jesteśmy w jakimś stanie i mamy jakiś znak wejściowy, powiedzmy „a”, nie ma gwarancji, że NFA powie nam co robić dalej. Na przykład, nie jest wymagane aby brzegi wychodzące ze stanu maiły unikalną etykietę. Możemy mieć dwa lub więcej brzegów wychodzących ze stanu, wszystkie prowadzące do różnych stanów pojedynczego znaku „a”. Jeśli NFA akceptuje ciąg, tylko gwarantuje, że jest jakaś ścieżka, która prowadzi do stanu akceptowalnego, nie gwarantuje, że ta ścieżka będzie łatwa do odnalezienia. Podstawową techniką jaką będziemy stosować do rozwiązania nie deterministycznych zachowań NFA jest backtracing – sprawdzanie wsteczne. Funkcja , która próbuje dopasować wzorzec używając NFA zaczyna w stanie startowym i próbuje dopasować pierwszy znak(i) ciągu wejściowego do brzegu opuszczającego stan

startowy. Jeśli jest tylko jedno dopasowanie, kod musi następować po tym brzegu. Jednakże, jeśli są dwa możliwe brzegi , wtedy kod musi arbitralnie wybrać jeden z nich i również zapamiętać drugi, jako bieżący punkt w ciągu wejściowym. Później, jeśli okaże się , że algorytm wybrał niewłaściwy brzeg, może wrócić i próbować inną z alternatyw( tj. wraca i próbuje innej ścieżki). Jeśli algorytm wyczerpie wszystkie alternatywy bez przechodzenia do stanu końcowego ( z pustym ciągiem wejściowym), wtedy NFA nie akceptuje ciągu. Prawdopodobnie najłatwiejszy sposób implementacji backtracingu jest poprzez procedurę wywołującą. Zakładamy, że procedura dopasowująca zwraca ustawioną flagę przeniesienia jeśli powodzenie (tj. akceptuje ciąg) i zwraca wyzerowaną flagę przeniesienia jeśli niepowodzenie (tj. odrzucony ciąg) Jeśli NFA oferuje wielokrotny wybór, możemy zaimplementować tą część NFA jak następuje:

r ε

AltRST

Success: AltRST:

proc push mov call jc mov call jc mov call pop ret endp

ε

ε

s

ε

t

near ax ax, di r Success di, ax s Success di, ax t ax

ε

ε

;celem tych dwóch instrukcji jest zachowanie di w ;przypadku niepowodzenia ;Przywrócenie di (może być modyfikowane przez r) ;Przywrócenie di (może być modyfikowany przez s) ;przywrócenie ax

Jeśli procedura dopasowująca r zakończy się powodzeniem, nie ma potrzeby próbować s I t. Z drugiej strony jeśli r zakończyła się niepowodzeniem, wtedy musimy próbować s. Podobnie, jeśli r i s, oba są błędne, próbujemy t. AltRST zakończy się niepowodzeniem, tylko jeśli r,s i t wszystkie są błędne. Kod ten zakłada, ze es:di wskazuje ciąg wejściowy do dopasowania. Przy zwrocie, es:di wskazuje następny dostępny znak w ciągu po dopasowaniu lub wskazuje jakiś przypadkowy punkt jeśli dopasowanie skończyło się niepowodzeniem. Kod ten zakłada ,że r,s i t wszystkie zachowują rejestr ax, więc zachowują wskaźnik do bieżącego punktu w ciągu wejściowym w ax jeśli r lub s są błędne. Działanie na pojedynczym NFA powiązanym z prostym wyrażeniem skończonym (tj. dopasowanie ε lub pojedynczego znaku) nie jest wcale trudne. Przypuśćmy, że funkcja dopasowująca r dopasowuje wyrażenie skończone (+ | - | ε). Kompletna procedura dla r to r

r_matched: r_nomatch: r

proc cmp je cmp jne inc stc ret endp

near byte ptr es:[di] , ‚+’ r_matched byte ptr es:[di], ‚-‚ r_nomatch di

Zauważ, że nie ma wyraźnego testu dla ε. Jeśli ε jest jedną z alternatyw, funkcja próbuje dopasować najpierw jedną z alternatyw. Jeśli żadna z alternatyw nie zakończyła się powodzeniem, wtedy funkcja dopasowująca będzie poprawna zawsze, chociaż nie konsumuje żadnych znaków wejściowych (dlatego powyższy kod przeskakuje instrukcję inc di, jeśli nie dopasowuje „+” lub „-„). Dlatego też, każda funkcja dopasowująca , która ma ε jako alternatywę zawsze będzie kończyć się powodzeniem. Oczywiście, nie wszystkie funkcje dopasowujące kończą się powodzeniem w każdym przypadku. Przypuśćmy, że funkcja dopasowująca s akceptuje pojedynczą dziesiętną cyfrę, kod dla s może wyglądać jak następuje: s

s_fails: s

proc cmp jb cmp ja inc stc ret clc ret endp

near byte ptr es:[di], ‘0’ s_fails byte ptr es:[di], ‚9’ s_fails di

Jeśli NFA przybiera postać x r

s

gdzie x jest przypadkowym znakiem lub ciągiem lub ε, odpowiedni kod asemblerowy dla tej procedury będzie ConcatRxS proc near call r jnc CRxS_Fail ;jeśli żadnego r, niepowodzenie ;Notka, jeśli x = ε wtedy po prostu usuwamy następujące trzy instrukcje. Jeśli x jest ciągiem zamiast ;pojedynczym znakiem, wkładamy dodatkowy kod dopasowujący wszystkie znaki w ciągu. cmp byte ptr es:[di], ‘x’ jne CRxS_Fail inc di call jnc stc ret CRxS_Fail: ConcatRxS

s CRxS_Fail ;Powodzenie!

clc ret endp

Jeśli wyrażenie skończone jest w postaci r* a odpowiedni NFA ma postać

r

ε

ε

wtedy odpowiedni kod asemblerowy 80x86 może wyglądać jak coś takiego: RStar

RStar

proc call jc stc ret endp

near r RStar

Wyrażenie skończone oparte na Kleene Star zawsze kończy się powodzeniem ponieważ pozwala na zero lub więcej wystąpień. Jest tak dlatego, ze kod ten zawsze zwraca ustawioną flagę przeniesienia. Operacja Kleene Plus jest tylko odrobinę bardziej złożone, odpowiedni (odrobinę zoptymalizowany) kod asemblerowy Rplus RplusLp:

Rplus_Fail: Rplus

proc call jnc call jc stc ret

near r Rplus_Fail r RPlusLP

clc ret endp

Odnotuj jak podprogram ten kończy się niepowodzeniem jeśli nie ma przynajmniej jednego wystąpienia r. Ważnym problemem z backtracingiem jest to, że jest potencjalna niewydajność. Jest to bardzo łatwo stworzyć wyrażenie skończone, które, kiedy konwertujemy do NFA i kodu asemblerowego, generuje znaczne sprawdzenie wsteczne w pewnym ciąg wejściowym. Jest to później zaostrzone przez fakt, że podprogramy dopasowujące, jeśli są napisane jak opisano powyżej, są generalnie bardzo krótkie; tak krótkie, faktycznie, że procedura wywołująca i powrotna zajmują znaczną część czasu wykonania. Dlatego też, dopasowanie do wzorca w ten sposób, chociaż łatwe, może być wolniejsze niż może być. To jest właśnie próba jak skonwertować WS do NFA do języka asemblera. Nie pójdziemy dalej po więcej szczegółów w tym rozdziale; nie dlatego że nie jest to interesujące, ale dlatego, że rzadko będziemy używali tej techniki w rzeczywistych programach. Jeśli potrzebujemy wysoko wydajnego dopasowania do wzorca, nie możemy używać technik niedeterministycznych , takich jak te. Jeśli chcemy łatwości programowania oferowanej przez konwersję NFA do asemblera nie używajmy tej techniki. Zamiast twego Biblioteka Standardowa UCR dostarcza bardzo silnych udogodnień dopasowania do wzorca (które przekraczają zdolności NFA),więc powinniśmy używać ich w zamian; ale więcej o tym później. 16.1.2.5 DETERMINISTYCZNE SKOŃCZONE STANY AUTOMATU (DFA) Nie deterministyczny skończony stan automatu, kiedy konwertuje rzeczywisty kod programu, może cierpieć na problemy z wydajnością z powodu backtracingu, który wystąpi kiedy dopasujemy ciąg. Deterministyczny skończony stan automatu rozwiązuje ten problem przez porównanie różnych ciągów równolegle. Podczas gdy, w najgorszym przypadku, NFA może wymagać n porównań, gdzie n jest sumą długości wszystkich ciągów rozpoznawanych przez NFA, DFA wymaga tylko m porównań (najgorszy przypadek), gdzie m jest długością najdłuższego ciągu rozpoznawanego przez DFA. Na przykład przypuśćmy, że mamy NFA, który dopasowuje następujące wyrażenia skończone (zbiór mnemoników trybu rzeczywistego 80x86, które zaczynają się na „A”): (AAA | AAD | AAM | AAS | ADC | ADD | AND ) Typowa implementacja jako NFA może wyglądać następująco: MatchAMnem: proc

near

strcmpl byte je strcmpl byte je strcmpl byte je strcmpl byte je strcmpl byte je strcmpl byte je strcmpl byte je clc ret matched: MatchAMnem

add stc ret endp

„AAA”, 0 matched „AAD”, 0 matched „AAM”, 0 matched „AAS”, 0 matched „ADC”, 0 matched „ADD”, 0 matched „AND”, 0 matched

di, 3

Jeśli przekazujemy do NFA ciąg, który nie jest dopasowany np. „AAND” , musi wykonać siedem porównań ciągów, które +wykonuje około 18 porównań znaków (plus wszystkie koszty wywołania strcmpl). Faktycznie, DFA może określić, że nie dopasuje tego ciągu znaków przez porównywanie tylko trzech znaków

Rysunek 16.2 DFA dla Wyrażenia Skończonego (+ | - ε )[0-9]+

Rysunek 16.3 Uproszczone DFA dla Wyrażenia Skończonego (+ | - ε) [0-9]+ DFA jest specjalną formą NFA z dwoma ograniczeniami. Po pierwsze, musi być dokładnie jeden brzeg wychodzący z każdego węzła dla każdego z możliwych znaków wejściowych; implikuje to , że musi jeden brzeg dla każdego symbolu wejściowego i nie może mieć dwóch brzegów z takimi samymi symbolami wejściowymi. Po drugie, nie możemy przesuwać z jednego stanu do innego w pustym ciągu ε. DFA jest deterministyczne ponieważ przy każdym stanie, następny wprowadzany symbol określa następny stan do jakiego będziemy wchodzili. Ponieważ każdy symbol wejściowy ma z nim powiązany brzeg, nigdy nie m przypadku, że DFA się ‘zablokuje” ponieważ nie możemy opuścić stanu tego symbolu wejściowego. Podobnie, każdy nowy stan jaki wprowadzamy nie jest nigdy niejasny, ponieważ jest tylko jeden brzeg danego szczególnego stanu z bieżącym symbolem wejściowym na nim. Rysunek 16.2 pokazuje DFA, które działa na stałych całkowitych opisanych przez wyrażenie skończone (+ | - | ε ) [0-9]+ . Zauważ, że wyrażenie w postaci „Σ - [0-9]” oznacza każdy znak z wyjątkiem cyfr, to znaczy kompletnego zbioru [0-9]. Stan trzy jest stanem niepowodzenia. To nie jest stan akceptowalny a DFA raz wchodzi w stan niepowodzenia, jest tam zablokowany (tj. konsumuje wszystkie dodatkowe znaki w ciągu wejściowym bez opuszczenia stanu niepowodzenia). Po wejściu do stanu niepowodzenia, DFA już odrzuciła ciąg wejściowy. Oczywiście, nie jest to jedyne sposób odrzucenia ciągu; powyższe DFA, na przykład, odrzuca pusty ciąg (ponieważ opuściliśmy stan zero) i odrzuca ciąg zawierający tylko znaki „+” lub „-„. DFA generalnie zawiera więcej stanów niż porównywalne NFA. Pomocą w utrzymaniu rozmiaru DFA pod kontrolą, pozwolimy sobie na kilka skrótów, które, w żaden sposób, nie wpływają na działania DFA. Po pierwsze usuwamy ograniczenia, które były przy brzegach powiązanych z każdym możliwym symbolem wejściowym opuszczającym każdy stan. Większość brzegów opuszczających określony stan prowadzi do stanu niepowodzenia. Dlatego też naszym pierwszym uproszczeniem będzie zezwolenie DFA opuścić brzegi , które prowadza do stanu niepowodzenia. Jeśli symbol wejściowy nie jest reprezentowany na brzegu wychodzącym z jakiegoś stanu, założymy, że prowadzi on do stanu niepowodzenia. Powyższe DFA z tym uproszczeniem pojawia się na Rysunku 16.2

Rysunek 16.4 DFA, które rozpoznaje AND, AAA, AAD,AAM. AAS, AAD i ADC Drugim skrótem, który jest obecny rzeczywiście w dwóch powyższych przykładach, jest zezwolenie zbiorowi znaków ( lub symbolowi alternatywy) powiązać kilka znaków z pojedynczym brzegiem. W końcu, również pozwolimy ciągom przyłączyć się do brzegu. Jest to notacja skrótowa dla listy stanów, które rozpoznają pomyślnie każdy znak tj. dwa następujące DFA, są ekwiwalentami:

abc

a

b

c

Wracając do wyrażenia skończonego, które rozpoznaje mnemoniki trybu rzeczywistego 80x86 zaczynające się na „A”, możemy skonstruować DFA, które rozpoznaje takie ciągi jak pokazano na rysunku 16,4. Jeśli przejdziemy przez to DFA z kilkom ciągami zaakceptowanymi i odrzuconymi , odkryjemy, ze wymaga on nie więcej niż sześć znaków porównania do określenia czy DFA powinna zaakceptować lub odrzucić ciąg wejściowy. Chociaż nie będziemy omawiać tu specyfiki, okazuje się, że wyrażenia skończone. NFA i DFA są ekwiwalentami. To znaczy, możemy skonwertować coś z jednego do drugiego. W szczególności możemy zawsze skonwertować NFA do DFA. Chociaż konwersja nie jest całkowicie trywialna, zwłaszcza jeśli chcemy zoptymalizować DFA, zawsze jest to możliwe do zrobienia. Konwertowanie pomiędzy wszystkimi tymi formami oznaczałoby przekroczenie zakresu tego tekstu. Jeśli interesują cię szczegóły , każdy tekst o języku formalnym lub teorii automatów Ci ich dostarczy. 16.1.2.6 KONWERSJA DFA DO JĘZYKA ASSEMBLERA Jest stosunkowo prosto skonwertować DFA do sekwencji instrukcji asemblerowych. Na przykład kod asemblerowy dla DFA, który akceptuje mnemoniki –A w poprzedniej sekcji DFA_A_Mnem

Fail: DoAN: Succeed: DoAD:

DoAA:

proc cmp jne cmp je cmp je cmp je clc ret

near byte ptr es:[di], ‘A’ Fail byte ptr es:[di + 1], A’ DoAA byte ptr es:[di+1], ‚D’ DoAD byte ptr es:[di], ‚N’ DoAN

cmp jne add stc ret cmp je cmp je clc ret cmp je cmp je cmp je cmp je clc ret

byte ptr es:[di+2], ‘D’ Fail di, 3 byte ptr es:[di+2], ‚D’ Succeed byte ptr es:d[di+2], ‚C’ Succeed ;Zwracane niepowodzenie byte ptr Succeed byte ptr Succeed byte ptr Succeed byte ptr Succeed

es:[di+2], ‘A’ es:[di+2], ‚D’ es:[di+2], ‚M’ es:[di+2], ‚S’

DFA_A_Mnem

endp

Chociaż ten schemat działa i jest znacznie bardziej wydajny niż kodowanie w schemacie NFA, napisanie tego kodu może być nużące, zwłaszcza kiedy konwertujemy duże DFA do kodu asemblerowego. Jest to technika, która czyni konwertowanie DFA do języka asemblera prawie trywialnym, chociaż może pochłonąć całkiem dużo miejsca – użyć stanów maszynowych. Prosty stan maszynowy jest dwu wymiarowa tablicą. Kolumny są indeksami możliwych znaków w ciągu wejściowym a wiersze są indeksami liczby stanów (tj. stanów w DFA).Każdy element tablicy jest nowa liczbą stanu. Algorytm dopasowujący dany ciąg używający stanu maszynowego jest trywialny: state := 0; while (inny znak wejściowy) do begin ch := następny znak wejściowy; state := StateTable [state] [ch]; end; if (state in FinalStates) then accept else reject; FinalStates jest zbiorem stanów akceptowalnych. Jeśli bieżąca liczba stanów jest w tym zbiorze po wyczerpaniu przez algorytm znaków w ciągu, wtedy stan maszynowy akceptuje ciąg, w przeciwnym razie odrzuca go. Poniższa tablica stanów odpowiada DFA dla mnemoników „A” pojawiających się w poprzedniej sekcji: Stan 0 1 2 3 4 5 F

A 1 3 F 5 F F F

C F F F F 5 F F

D F 4 5 5 5 F F

M F F F 5 F F F

N F 2 F F F F F

S F F F 5 F F F

Else F F F F F F F

Tablica 62: Stany maszynowe dla Instrukcji “A” DFA 80x86 Stan pięć jedynie jest stanem akceptowalnym. Jest tylko jeden ważny minus zastosowania tej tablicy schematów – tablica będzie całkiem duża. Nie jest to widoczne w powyższej tablicy ponieważ kolumna „Else;” ukrywa wiele szczegółów. W prawdziwej tablicy stanu będziemy potrzebowali jednej kolumny dla każdego możliwego znaku wejściowego, ponieważ jest 256 możliwych znaków wejściowych (lub przynajmniej 128 jeśli ograniczymy się do siedmiu bitów ASCII), powyższa tablica będzie miała 256 kolumn. Tylko jeden bajt na element, to daje około 2K dla tego małego stanu maszynowego. Duże stany maszynowe mogą generować bardzo duże tablice. Jedyny sposób redukcji rozmiaru tablicy przy (bardzo) małej utracie szybkości wykonania jest klasyfikacja znaków przed użyciem ich jako indeksu w tablicy stanu. Przez użycie pojedynczej 256 bajtowej tablicy połączeń, łatwo jest zredukować stan maszynowy do powyższej tablicy. Rozważmy 256 bajtową tablicę połączeń, która zawiera: • • • • • • •

Jeden na pozycji Base +”a” i Base + „A” Dwa przy lokacji Base + ”c” i Base + „C” Trzy przy lokacji Base + „d” i Base + „D” Cztery przy lokacji Base + „m” i Base + „M” Pięć przy lokacji Base + „n” i Base + „N” Sześć przy lokacji Base + „s” i Base + „S”, i Zero wszędzie gdzie można

Teraz możemy zmodyfikować powyższą tabelę tworząc Stan 0 1 2

0 6 6 6

1 1 3 6

2 6 6 6

3 6 4 5

4 6 6 6

5 6 2 6

6 6 6 6

7 6 6 6

3 4 5 6

6 6 6 6

5 6 6 6

6 5 6 6

5 5 6 6

5 6 6 6

6 6 6 6

5 6 6 6

6 6 6 6

Tabela 63 Tabela sklasyfikowanych stanów maszynowych dla instrukcji „A” DFA 80x86 Powyższa tabela zawiera dodatkową kolumnę „7”, której nie będziemy używać. Powodem dodania dodatkowej kolumny jest uczynienie łatwiejszym indeksowanie w tej dwu wymiarowej tablicy (ponieważ ta dodatkowa kolumna pozwala nam mnożyć numer stanu przez osiem zamiast siedem). Zakładając, że Classify jest nazwą tablicy połączeń, poniższy kod 80386 rozpoznaje ciągi określone przez to DFA: DFA2_A_Mnem

WhileNotEOS:

AtEOS:

Accept:

DFA2_A_Mnem

proc push push push xor mov mov lea mov cmp je xlat mov inc jmp cmp stc je clc pop pop pop ret endp

near ebx eax ecx eax, eax ebx, eax ecx, eax bx, Classify al, es:[di] al., 0 AtEOS

;wskaźnik do Classify ;bieżący znak ;bieżący stan ;EAX := 0 ;EBX := 0 ;ECX (stan) := 0 ;pobranie następnego znaku wejściowego ;koniec ciągu?

;znak sklasyfikowany cl, State_Tbl [eax+ecx*8] ;pobranie nowego stanu # di ;przesuniecie na następny znak WhileNotEOS cl, 5 ;czy w stanie akceptowalnym? ;zakładamy akceptację Accept ecx eax ebx

Chociaż używamy tabeli stanu w ten sposób upraszczając kodowanie asemblerowe, cierpimy z powodu dwóch wad. Po pierwsze, jak wspomniano wcześniej, jest to wolne. Technika ta musi wykonać wszystkie te instrukcje w całej pętli dla każdego znaku w dopasowaniu; a instrukcje te nie są szczególnie szybkie. Drugą wadą jest to ,że musimy stworzyć tablicę stanu dla stanu maszynowego; to przetwarzanie jest nużące i skłonne do błędów. Jeśli potrzebujemy absolutnie wysokiej wydajności, możemy użyć technik stanu maszynowego opisanych w „Stany maszynowe i skoki pośrednie”. Tu sztuczką jest przedstawianie każdego stanu jako krótkiego segmentu kodu i jego własną jedno wymiarową tablicą stanu. Każde wejście w tablicy jest adresem docelowym segmentu kodu przedstawiającego następny stan. Poniżej mamy przykład naszego stanu maszynowego „A Mnemonic” napisanego w ten sposób. Jedyną różnicą jest to, że bajt zero jest zaklasyfikowany jako wartość siódma (zero oznacza koniec ciągu, więc będziemy tego używać do określenia kiedy napotykamy koniec ciągu) Odpowiednia tablica stanu może być: Stan 0 1 2 3 4 5 6

0 6 6 6 6 6 6 6

1 1 3 6 5 6 6 6

2 6 6 6 6 5 6 6

3 6 4 5 5 5 6 6

4 6 6 6 5 6 6 6

5 6 2 6 6 6 6 6

6 6 6 6 5 6 6 6

7 6 6 6 6 6 5 6

Tabela 64 Inna tablica stanu maszynowego dla instrukcji „A” DFA 80x86 Kod 80x86 to DFA3_A_Mnem

State0:

State0Tbl State1:

State1Tbl Statet2:

State2Tbl State3:

State3Tbl Staet4:

State4Tbl State5:

State6:

proc push push push xor lea mov xlat inc jmp word word mov xlat inc jmp word word mov xlat inc jmp word word mov xlat inc jmp word word mov xlat inc jmp word word

ebx eax ecx eax, eax ebx, Classify al., es:[di] di cseg: state0Tbl [eax*2] State6, State1, State6, State6 State6, State6, State6, State6 al, es:[di] di cseg: State1Tbl [eax*2] State6, State3, State6, State4 State6, State2, State6, State6 al, es:[di] di cseg: State2Tbl [eax*2] State6, State6.State6, State5 State6, State6,State6, State6 al, es:[di] di cseg;State3Tbl [eax*2] Staet6,State5, State6, State5 State5, State6, State5,State6 al, es:[di] di cseg: State4Tbl [eax*2] State6, State6, State5, State5 State6, State6,State6,State6

mov al, es:[di] cmp al, 0 jne State6 stc pop ecx pop eax pop ebx ret clc pop ecx pop eax pop ebx

Są dwie ważne cechy które powinniśmy odnotować o tym kodzie. Po pierwsze wykonuje tylko cztery instrukcje na porównanie znaku (średnio mniej niż inne techniki). Po drugie, instancja DFA wykrywając niepowodzenia

zatrzymuje przetwarzanie znaków wejściowych. Inna tablica ukierunkowana przez technikę DFA na oślep przetwarza cały ciąg, nawet po tym jak jest oczywiste, że maszyna utknęła na stanie niepowodzenia. Odnotujmy również, ze kod ten traktuje stany akceptowalne i niepowodzenia trochę inaczej niż ogólny kod tabeli stanu. Kod ten rozpoznaje fakt, że już jesteśmy w stanie pięć i albo zakończymy z powodzeniem (jeśli EOS jest następnym znakiem) lub niepowodzeniem. Podobnie w stanie sześć kod ten zna i nie próbuje szukać dalej. Oczywiście ta technika nie jest łatwa do zmodyfikowania dla różnych DFA’ów jako prosta wersja tablicy stanów, ale jest trochę szybsza. Jeśli szukamy szybkości, jest to dobry powód do kodowania w DFA. 16.1.3 JĘZYK BEZKONTEKSTOWY Języki bezkontekstowe dostarczają nadzbioru języków skończonych – jeśli możemy określić klasę wzorców z wyrażeniami skończonymi, możemy wyrazić taki sam język używając gramatyki bezkontekstowej. Dodatkowo możemy określić wiele języków, które nie są skończone używając gramatyki bezkontekstowej (CFG). Przykłady języków, które są bezkontekstowe, ale nie skończone, zawierają zbiór wszystkich ciągów reprezentujących powszechne wyrażenia arytmetyczne, poprawne Pascalowe lub C pliki źródłowe i makra MASM. Języki bezkontekstowe są charakteryzowane przez zrównoważenie i zagnieżdżenie. Na przykład, wyrażenia arytmetyczne równoważy zbiór nawiasów okrągłych. Instrukcje języka wysokiego poziomu takie jak repeat ... until pozwalają na zagnieżdżanie i zawsze są zrównoważone (np. dla każdego repeat jest odpowiednia instrukcja until dalej w pliku źródłowym) Jest tylko drobne rozszerzenie języka skończonego do działania na języku bezkontekstowym – wywołanie funkcji. W wyrażeniach skończonych, uznajemy tylko obiekty, które chcemy dopasować i określamy operatory WS takie jak „ | „, „*”, konkatenacje i tak dalej. Rozszerzając język skończony do języka bezkontekstowego potrzebujemy tylko dodać rekursywne funkcje wywołujące dla wyrażeń skończonych. Chociaż byłoby łatwe stworzenie składni pozwalającej na wywoływanie funkcji wewnątrz wyrażeń skończonych, informatyka używa zupełnie innej notacji dla języka bezkontekstowego – gramatykę bezkontekstową. Gramatyka bezkontekstowa składa się z dwóch typów symboli: symboli terminalnych (kończących) i symboli nieterminalnych (pomocniczych). Symbole terminalne są pojedynczymi znakami i ciągami , które gramatyka bezkontekstowa dopasowuje plus ciąg pusty ε. Gramatyka bezkontekstowa używa symboli nieterminalnych dla wywołania funkcji i definicji. W naszej gramatyce bezkontekstowej używać będziemy kursywy do oznaczania symboli nieterminalnych i zwykłej czcionki do oznaczania symboli terminalnych. Gramatyka bezkontekstowa składa się ze zbioru definicji funkcji znanych jako wyroby Wyrób przybiera formę: Nazwa_ funkcji → Nazwa funkcji z lewej strony strzałki jest nazywana lewostronnym wyrobem. Treść funkcji, która jest listą symboli terminalnych i nieterminalnych, jest nazywana prawostronnym wyrobem. Poniżej mamy gramatykę dla prostych arytmetycznych wyrażeń: expression → expression + factor expression → expression - factor expression → factor factor → factor * term factor → factor / term factor → term term → IntegerConstant term → ( expression) IntegerConstant → digit IntegerConstant → digit IntegerConstant digit → 0 digit → 1 digit → 2 digit → 3 digit → 4 digit → 5 digit → 6 digit → 7 digit → 8 digit → 9

Zauważmy, że możemy mieć wielokrotne definicje dla tej samej funkcji. Gramatyka bezkontekstowa zachowuje się w trybie niedeterministycznym, tak jak NFA. Kiedy próbujemy dopasować ciąg używając gramatyki bezkontekstowej , ciąg jest dopasowany jeśli istnieje jakaś funkcja dopasowująca, która dopasowuje bieżący ciąg wejściowy. Ponieważ jest powszechne posiadanie wielu identycznych lewostronnych wyrobów, będziemy używali alternatywnych symboli z wyrażeniami skończonymi do redukcji liczby linii w gramatyce. Następujące dwie podgramatyki są identyczne: expression → expression + factor expression → expression - factor expression → factor Powyższe jest odpowiednikiem : expression → expression + factor | expression → expression – factor | factor Pełna gramatyka arytmetyczna, używająca tej notacji skrótowej to expression → expression + factor | expression → expression - factor | factor factor → factor * term | factor / term | term term → IntegerConstant | (expression) IntegerConstant → digit | digit IntegerConstant digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 Jeden z nieterminalnych symboli, zazwyczaj pierwszy wyrób, jest symbolem startowym. Jest to mniej więcej odpowiednik stanu startowego w skończonym stanie automatu. Symbol startowy jest pierwszą funkcją dopasowującą jaką wywołujemy kiedy chcemy przetestować jakiś ciąg wejściowy aby zobaczyć czy jest składnikiem języka bezkontekstowego. W powyższym przykładzie expression jest symbolem startowym. Podobnie jak NFA i DFA rozpoznaje ciągi w języku skończonym określonym przez wyrażenia skończone, niedeterministyczny automat stosowy i deterministyczny automat stosowy rozpoznają ciągi należące do języka bezkontekstowego określonego przez gramatykę bezkontekstową. Nie będziemy jednak wnikać w szczegóły automatów stosowych (lub PDA), ale musimy być świadomi ich obecności. Możemy dopasować ciągi bezpośrednio przez gramatykę. Na przykład rozważmy ciąg 7+5*(2+1) Dopasowując ten ciąg, zaczniemy przez wywołanie funkcji symbolu startowego, expression, używając funkcji expression → expression + factor. Pierwszy znak plus sugeruje, że termin expression musi dopasować „7” a factor musi dopasować „5*|(2+1)”. Teraz musimy dopasować nasz ciąg wejściowy ze wzorcem expression + factor . Robimy to wywołując ponownie funkcję expression, tym razem używając wyrobu expression → factor. Daje to nam redukcję: expression ⇒ expression + factor ⇒ factor + factor Symbol ⇒ oznacza zastosowanie wywołania nieterminalnej funkcji (redukcji). Następnie, wywołujemy funkcję factor używając wyrobu factor → term zwracającej redukcję: expression ⇒ expression + factor ⇒ factor + factor ⇒ term + factor Kontynuując , wywołujemy funkcję term tworząc redukcję: expression ⇒ expression + factor ⇒ factor + factor ⇒ term + factor⇒ IntegerConstant + factor Następnie wywołujemy funkcję IntegerConstant zwracając: expression ⇒ expression + factor ⇒ factor + factor⇒ term+ factor ⇒ IntegerConstant + factor⇒ 7 + factor W tym punkcie, pierwsze dwa symbole naszego generowanego ciągu dopasowują pierwsze dwa znaki ciągu wejściowego, więc możemy usunąć je z ciągu i skoncentrować na następnych pozycjach. Sukcesywnie, wywołujemy funkcję factor tworząc redukcję 7 + factor* term a potem wywołujemy factor, term i IntegerConstant aby uzyskać 7+5 * term. W podobny sposób możemy zredukować termin „(expression)” i redukujemy wyrażenie „2+1”. Kompletne wyprowadzenie dla tego ciągu to Expression

⇒ expression + factor

⇒ factor + factor ⇒ term + factor ⇒ IntegerConstant + factor ⇒ 7 + factor ⇒ 7+factor * term ⇒ 7 + term * term ⇒ 7 + IntegerConstant 8 term ⇒ 7 + 5 * term ⇒ 7+5* (expression) ⇒ 7+5* (expression + factor) ⇒ 7+5*(factor + factor) ⇒ 7 + 5* (IntegerConstant + factor) ⇒ 7+5*(2 + factor) ⇒ 7 + 5 *(2+ term) ⇒ 7+5*(2 + IntegerConstant) ⇒ 7+5*(2 +1) Kompletną końcową redukcję wyprowadzamy z naszego ciągu wejściowego , więc ciąg 7+5*(2+1) jest w języku określonym przez gramatykę bezkontekstową 16.1.4 ELIMINACJE LEWOSTRONNIE REKURENCYJNE I OPUSZCZANIE WSPÓŁCZYNNIKA CFG W następnej sekcji będziemy omawiali jak skonwertować CFG do programu w języku asemblera. Jednakże, technika jakiej będziemy używali dla tej konwersji będzie wymagała zmodyfikowania pewnych gramatyk przed jej skonwertowaniem. Gramatyczne wyrażenia arytmetyczne w poprzedniej sekcji są dobrym przykładem takiej gramatyki – która jest lewostronnie rekurencyjna. Gramatyka lewostronnie rekurencyjna stanowi dla nas problem ponieważ sposób w jaki będziemy zazwyczaj konwertować wyrób do kodu asemblowego, jest wywołanie funkcji zgodnej z nieterminalną i porównanie z symbolami terminalnymi. Jednakże przeciwdziałamy temu jeśli spróbujemy skonwertować wyrób używając tej techniki: expression → expression = factor Taka konwersja do kodu asemblerowego wyglądałaby podobnie do poniższego: expression

Fail: expression

proc call jnc cmp jne inc call jnc stc ret clc Ret endp

near expression fail byte ptr es:[di], ‘+’ fail di factor fail

Oczywisty problem z tym kodem jest taki, ze generuje pętlę nieskończoną. Na wejściu do funkcji expression kod ten bezpośrednio wywołuje expression rekurencyjnie, który bezpośrednio wywołuje expression rekurencyjnie, który bezpośrednio wywołuje expression rekurencyjnie, najwyraźniej musimy rozwiązać ten problem jeśli napiszemy rzeczywisty kod dopasowujący ten wyrób. Sztuczka rozwiązująca rekurencję lewostronną jest tak, że jeśli jest wyrób , które cierpi z powodu rekurencji lewostronnej, musi być jakiś, taki sam lewostronny wyrób , który nie jest lewostronnie rekurencyjny. Wszystko co musimy zrobić to przepisać wywołanie lewostronnie rekurencyjne pod względem wyrobu, który nie ma żadnej rekurencji lewostronnej. To brzmi jak trudne zadanie, ale w rzeczywistości jest całkiem łatwe.

Zobaczmy jak wyeliminować rekurencję lewostronną, Xi i Yi reprezentują jakiś zbiór symboli terminalnych lub nieterminalnych, które nie mają prawej strony zaczynającej się nieterminalnym A. Jeśli mamy jakieś wyroby w postaci: A → AX1 | AX2 | … | AXn | Y1 | Y2 | … | Ym . Możemy przetłumaczyć to na odpowiednią gramatykę bez rekurencji lewostronnej przez zastąpienie każdego elementu w postaci A → Yi przez A → YiA i każdy element w postaci A → AXi przez A’ →XiA’ | ε. Na przykład, rozważmy trzy wyroby z gramatyki arytmetycznej: expression → expression + factor expression → expression - factor expression → factor W tym przykładzie A odpowiada expression, X1 odpowiada „+ factor”, X2 odpowiada „- factor” a Y1 odpowiada „factor” .Odpowiednia gramatyka bez rekurencji lewostronnej expression → factor E’ E’ → - factor E’ E’ → + factor E’ E’ → ε Kompletna gramatyka arytmetyczna z usuniętą rekurencją lewostronną to: expression ’ → factor E’ E’ → + factor E’ | - factor E’ | ε factor → term E’ F’ → * term F’ | / term F’ | ε term →IntegerConstant | (expression) IntegerConstant → digit | digit IntegerConstant digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 Inną użyteczną transformacją gramatyczną jest gramatyką opuszczania współczynnika. Może ona zredukować potrzebę backtracingu, poprawiając wydajność naszego kodu dopasowującego do wzorca. Rozważmy fragment następującego CFG: stmt → if expression then stmt endif stmt → if expression then stmt else stmt endif Te dwa wyroby zaczynają się takim samym zbiorem symboli. I jeden i drugi wyrób będzie dopasowywał wszystkie znaki w instrukcji if do punktu dopasowującego napotkany algorytm else lub endif. Jeśli algorytm dopasowujący przetwarza pierwszą instrukcję do punktu z symbolem terminalnym endif i napotyka zamiast tego symbol terminalny else, musi wrócić do stanu początkowego całej drogi do symbolu if i zaczynać ponownie. Może to być strasznie nieefektywne z powodu rekurencyjnego wywołania stmt (wyobraź sobie 10000 lini programu, które mają pojedynczą instrukcję if we wszystkich 10000 linii, kompilator używając tej techniki dopasowania do wzorca będzie musiała zrekompilować cały program ze skreśleniem, jeśli używamy backtracingu w ten sposób). Jednakże poprzez gramatykę lewego współczynnika przed konwersją jej do kodu programu możemy wyeliminować potrzebę backtracingu. W gramatyce lewego współczynnika zbieramy wszystkie wyroby, które maja taką samą lewą stronę i zaczynają z tymi samymi symbolami po prawej stronie. W tych dwóch powyższych wyrobach tymi symbolami są „if expression then stmt” . Łączymy ciągi w pojedynczy wyrób a potem dołączamy nowy symbol nieterminalny na koniec tego nowego wyrobu np. stmt → if expression then stmtNewNonTerm W końcu, tworzymy nowy zbiór wyrobów używając tego nowego nieterminala dla każdego z przyrostków wspólnego wyrobu: NewNonTerm → endif | else stmt endif Wyeliminuje to backtracing ponieważ algorytm dopasowania może przetwarzać if, expression, then I stmt przed tym nim wybierze endif I else.

16.1.5 KONWERSJA WS DO CFG Ponieważ języki bezkonekstowe są nadzbiorem języków skończonych, nie powinno być niespodzianką ,ze jest możliwe konwertowanie wyrażeń skończonych do gramatyki bezkontekstowej. Jest to bardzo łatwy proces wymagający tylko kilku intuicyjnych zasad. 1) Jeśli wyrażenie skończone składa się z sekwencji znaków xyz, możemy łatwo stworzyć wyrób dla tego wyrażenia skończonego w postaci P → xyz. Odnosi się to równie do ciągu pustego ε. 2) Jeśli r i s są dwoma wyrażeniami skończonymi które konwertujemy do CFG tworząc wyroby R i S i mamy wyrażenie skończone rs które chcemy skonwertować do wyrobu, po prostu tworzymy nowy wyrób w postaci T → R S 3) Jeśli r i s są dwoma skończonymi wyrażeniami, które skonwertowaliśmy do CFG tworząc wyroby R i S, i mamy wyrażenie skończone r | s, które chcemy skonwertować do wyrobu, po prostu tworzymy nowy wyrób w postaci T → R | S 4) Jeśli r jest wyrażeniem skończonym , które skonwertowaliśmy tworząc wyrób R i chcemy stworzyć wyrób dla r* po prostu używamy wyrobu Rstar → R Rstar | ε 5) Jeśli r jest wyrażeniem skończonym, które skonwertowaliśmy tworząc wyrób R i chcemy stworzyć wyrób dla r+, po używamy wyrób Rplus → R Rplus | R 6) Dla wyrażeń skończonych mamy operacje o różnych pierwszeństwach. Wyrażenia skończone również pozwalają na nawiasy okrągłe do przesłonięcia domyślnego pierwszeństwa. Ta notacja pierwszeństwa nie przenosi się na CFG. Zamiast tego musimy zakodować pierwszeństwo bezpośrednio w gramatyce. Na przykład kodując RS* prawdopodobnie użyjemy wyrobów w postaci: T → R SStar SStar → S SStar | ε Podobnie, działając na gramatyce w postaci (RS)* możemy użyć wyrobów w postaci: T→R S T|ε RS → R S 16.1.6 KONWERSJA CFG DO JĘZYKA ASEMBLERA Jeśli mamy usuniętą lewostronną rekurencję i mamy gramatykę lewego współczynnika, łatwo jest skonwertować taką gramatykę do programu w języku asemblera, który rozpoznaje ciągi w języku bezkontekstowym. Pierwszą konwencją jaką przyjmiemy jest to , że es:di zawsze wskazują początek ciągu jaki chcemy dopasować. Drugą konwencją jaką przyjmiemy jest stworzenie funkcji dla każdego nieterminala. Funkcja ta zwraca sukces (przeniesienie ustawione) jeśli dopasowała powiązany podwzorzec, zwraca niepowodzenie (przeniesienie wyzerowane) w przeciwnym razie. Jeśli wystąpiło powodzenie, pozostawia di wskazujące na następny znak, który jest startowy po dopasowaniu wzorca; jeśli mamy niepowodzenie, zachowuje wartość w di po wywołaniu funkcji. Konwertując zbiór wyroby do odpowiedniego kodu asemblerowego, musimy zadziałać z czterema rzeczami ; symbolami terminalnymi, nieterminalnymi, sumą logiczną i pustym ciągiem. Najpierw rozpatrzymy proste funkcje (nieterminalne), które nie mają wielokrotnych wyrobów (tj. suma logiczna) Jeśli wyrób przybiera postać T → ε i nie ma innych wyrobach powiązanych z T, wtedy ten wyrób zawsze kończy się powodzeniem. Odpowiedni kod asemblerowy: T T

proc stc ret endp

near

Oczywiście nie ma rzeczywistej potrzeby zawsze wywoływać t i testować zwracanej wartości ponieważ wiemy,, że zawsze kończy się powodzeniem. Z drugiej strony, jeśli T jest stub, wtedy zamierzamy wprowadzić później, powinniśmy wywołać T Jeśli wyrób przybiera postać T → xyz, gdzie xyz jest ciągiem z jednym lub więcej symboli terminalnych, wtedy funkcja zwraca powodzenie jeśli kilka następnych wprowadzanych znaków dopasowuje się do xyz, zwraca niepowodzenie w przeciwnym razie. Pamiętajmy, że jeśli przedrostek ciągu wejściowego dopasuje się do xyz, wtedy funkcja dopasowująca musi przesunąć di poza te znaki. Jeśli pierwsze znaki ciągu wejściowego nie są dopasowane do xyz, musi zachować di. Poniższy podprogram demonstruje dwa przypadki gdzie xyz jest pojedynczym znakiem i gdzie xyz jest ciągiem znaków:

T1

Success: T1 T2

T2

proc cmp je clc ret inc stc ret endp

near byte ptr es:[di], ‘x’ Success

proc call byte ret endp

near MatchPrefix ‘xyz’ , 0

;pojedynczy znak ;zwraca niepowodzenie

di

;przeskok dopasowanego znaku ;zwraca powodzenie

MatchPrefix jest podprogramem, który dopasowuje przedrostek ciągu wskazywanego przez es:di do ciągu następującego po wywołaniu w strumieniu kodu. Zwraca ustawione przeniesienie i modyfikuje di jeśli ciąg w strumieniu kodu jest przedrostkiem ciągu wejściowego, zwraca wyzerowaną flagę przeniesienia i zachowuje di jeśli ciąg literalny nie jest przedrostkiem wprowadzanym. Kod MatchPrefix jest następujący: MatchPrefix

CmpLoop:

Success:

Failure:

proc push mov push push push push

far bp bp, sp ax ds si di

;musi być far!

lds mov cmp je cmp jne inc inc jmp add inc mov pop pop pop pop stc ret

si, 2[bp] al., ds:[si] al., 0 Success al., es;[di] Failure si di CmpLoop sp, 2 si 2[bp], si si ds. ax bp

;pobranie adresu zwrotnego ;pobranie ciągu do dopasowania ;jeśli koniec przedrostka ;mamy powodzenie ;zobacz czy dopasowany przedrostek ;jeśli nie, bezpośrednio niepowodzenie

inc cmp jne inc mov

si byte ptr ds.:[si], 0 Failure si 2[bp], si

pop pop pop pop pop clc

di si ds. ax bp

;nie przywracaj di ;przeskakujemy bajt zakończony zerem ;zachowanie jako adresu zwrotnego

;zwraca powodzenie ;potrzeba skoku do bajtu zerowego

;zachowanie jako adres powrotu

;zwraca niepowodzenie

ret endp

MatchPrefix

Jeśli wyrób przybiera postać T → R gdzie R jest nieterminalne, wtedy funkcja T wywołuje R i zwraca jakikolwiek stan R zwraca np. T

proc near call R ret T endp Jeśli prawa strona wyrobu zawiera ciąg z symbolami terminalnymi i nieterminalnymi, odpowiedni kod asemblerowy sprawdza każdą pozycję po kolei . Jeśli każde sprawdzenie jest błędne, wtedy funkcja zwraca niepowodzenie. Jeśli wszystkie pozycje zakończyły się powodzeniem, wtedy funkcja zwraca powodzenie. Na przykład jeśli mamy wyrób w postaci T → R abc S możemy zaimplementować w języku asemblera T

proc push

call jnc call byte jnc call jnc add stc ret Failure: pop clc ret T endp

near di

;jeśli błąd, musimy zachować di

R Failure MatchPrefix „abc”, 0 Failure S Failure sp, 2

;nie zachowujemy di jeśli mamy powodzenie

di

Zobacz jak ten kod zachowuje di jeśli niepowodzenie, ale nie zachowuje jeśli powodzenie Jeśli mamy wielokrotne wyroby takie same lewostronne (tj. suma logiczna), wtedy napisana właściwa funkcja dopasowującą dla wyrobów jest odrobinę bardziej złożona niż w przypadku pojedynczego wyrobu. Jeśli mamy wielokrotne wyroby powiązane z pojedynczym nieterminalem lewostronnym, wtedy tworzymy sekwencję kodu dopasowującą każdy pojedynczy wyrób. Połączymy je razem w pojedynczą funkcję dopasowującą, po prostu piszemy funkcję tak aby uzyskała powodzenie jeśli jedna z tych sekwencji kodu kończy się powodzeniem. Jeśli jeden z wyrobów jest w postaci T → e, wtedy testujemy drugi z warunków. Jeśli żaden z nich nie może być wybrany, funkcja kończy się powodzeniem. Na przykład rozważmy wyroby: E’ → + factor E’ | - factor E’ | ε Tłumaczymy go na następujący na kod asemblerowy Eprime

Success: TryMinus:

proc push cmp jne inc call jnc call jnc add stc ret cmp

near di byte ptr es:[di] TryMinus di factor EP_Failed Eprime EP_Failed sp, 2 byte ptr es:[di], ‘-‘

EP_Failed: Eprime

jne inc call jnc call jnc add stc ret pop stc ret endp

EP_Failed di factor EP_Failed Eprime EP_Failed sp, 2 di ;powodzenie ponieważ E’ → ε

Ten podprogram zawsze kończy się powodzeniem ponieważ ma wyrób E’ → ε. Jest tak dlatego, że instrukcja stc pojawia się po etykiecie EP_Failed Wywołując funkcję dopasowania do wzorca, po prostu ładujemy es:di z adresem ciągu jaki chcemy przetestować i wywołujemy funkcję dopasowania do wzorca. Przy zwrocie, flaga przeniesienia będzie zawierała jeden jeśli dopasowano do wzorca ciąg do punktu zwracanego w di. Jeśli chcemy zobaczyć czy dopasowano cały ciąg do wzorca, po prostu sprawdzamy czy es:di wskazuje na bajt zero kiedy wracamy z funkcji wywołującej Jeśli chcemy zobaczyć czy ciąg należy do języka bezkontekstowego powinniśmy wywołać funkcję powiązaną z symbolem startowym dla danej gramatyki bezkontekstowej. Poniższy podprogram implementuje gramatykę arytmetyczną jakiej używaliśmy jako przykładów w kilku poprzednich sekcjach. Kompletna implementacja: ; ARTH.ASM ; ; Prosty rekurencyjny analizator dla gramatyki arytmetycznej .xlist include stdlib.a include stdlib.lib .list dseg

segment

para public ‘data’

; Gramatyka dla prostej gramatyki arytmetycznej ( wspiera +, - , * , /): ; ; E → FE’ ; E’ → + F E’ | - F E’ | ; F → TF’ ; F’ → * T F’ | / T F’ | ; T → G | (E) ;G→H|HG ;H→0|1|2|3|4|5|6|7|8|9 ; InputLine

byte

128 dup (0)

dseg

ends

cseg

segment para public ‚code’ assume cs: cseg, ds:dseg

; Funkcje dopasowujące dla gramatyki ; Funkcje te zwracają ustawioną flagę przeniesienia jeśli dopasowują swoje pozycje odpowiednio. Zwracają ; wyzerowaną flagę przeniesienia jeśli kończą się niepowodzeniem. Jeśli kończą się niepowodzeniem, ; zachowują di. Jeśli kończą się niepowodzeniem di wskazuje pierwszy znak po dopasowaniu.

; E → FE’ E

E_Failed: E

proc push call jnc call jnc add stc ret

near di F E_Failed EPrime E_Failed sp, 2

pop clc ret endp

di

;zobacz czy F, wtedy E’ powodzenie

,Powodzenie nie odtwarzamy di

;Niepowodzenie, musimy odtworzyć di

; E’ → F E’ | - F E’ | ε EPrime

proc push

near di

; Próbujemy tu + F E’ cmp jne inc call jnc call jnc add stc ret

Success:

byte ptr es;[di], ‚+’ TryMinus di F EP_Failed EPrime EP_Failed sp, 2

; Próbujemy tu - F E’ TryMinus:

cmp jne inc call jnc call jnc add stc ret

byte ptr es:[di], ‘-‘ Success di F EP_Failed EPrime EP_Failed sp, 2

; Jeśli żaden z powyższych nie zakończył się powodzeniem, zwraca sukces tak czy owak ponieważ mamy ; wyrób w postaci E’ → ε EP_Failed: EPrime

pop stc ret endp

di

proc push

near di

; F → TF’ F

F_Failed: F

call jnc call jnc add stc ret

T F_Failed FPrime F_Failed sp, 2

pop clc ret endp

di

;powodzenie, nie przywracamy di

; F → *T F’ | / T F’ | ε Fprime

Success:

proc push cmp jne inc call jnc call jnc add stc ret

near di byte ptr es:[di], ‚*’ TryDiv di T FP_Failed Fprime FP_Failed sp, 2

;zaczynamy z „*“? ;przeskakujemy „*”

;Próbujemy tu F → /T F’ TryDiv:

cmp jne inc call jnc call jnc add stc ret

byte ptr es:[di], ‘/’ Success di T FP_Failed FPrime FP_Failed sp, 2

;zaczynamy z „/“ ? ;powodzenie ;przeskakujemy „/”

; Jeśli powyższe oba są błędne, zwraca sukces ponieważ mamy wyrób w postaci F → ε FP_Failed: Fprime

pop stc ret endp

di

proc

near

; T → G | (E) T

;Próbujemy ty T → G call jnc ret

G TryParens

; Próbujemy tu T → (E) Tryparens:

push

di

;zachowujemy jeśli błąd

T_Failed: T

cmp jne inc call jnc cmp jne inc add stc ret

byte ptr es:[di], ‘(‘ T_Failed di E T_Failed byte ptr es:[di], ‘)’ T_Failed di sp, 2

pop clc ret endp

di

;zaczynamy z „(„? ;błąd jeśli nie ;przeskakujemy znak „(„ ;Koniec z „)“? ; błąd jeśli nie ;przeskakujemy „)” ; nie przywracamy di jeśli powodzenie

; Poniżej jest swobodna translacja ; ;G→H|HG ; H→ 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ; ; Podprogram ten sprawdza czy jest przynajmniej jedna cyfra. Jest błąd jeśli nie ma przynajmniej jeden cyfry; ; powodzenie i przeskok wszystkich cyfr jeśli jest jedna lub więcej cyfr G

DifitLoop:

G_Succeeds: G_Failed; G

proc cmp jb cmp ja

near byte ptr es:[di], ‘0’ G_Failed byte ptr es:[di], ‘9’ G_Failed

inc cmp jb cmp jbe stc ret clc ret endp

di byte ptr es:[di], ‘0’ G_Succeeds byte ptr es:[di], ‘9’ DigitLoop

;sprawdzamy obecność przynajmniej jednej cyfry

;przeskakujemy pozostałe znalezione cyfry

;błąd jeśli żadnych cyfr

; Program główny testuje powyższe funkcje dopasowujące i demonstruje jak wywołać funkcje dopasowujące Main

proc mov mov mov printf byte lesi gets call jnc

ax, seg dseg ds., ax es, ax

;ustawiany rejestr segmentowy

„Wprowadź wyrażenie arytmetyczne: „, 0 InputLine E BadExp

; Dobrze, ale czy jesteśmy na końcu ciągu? cmp byte ptr es:[di], ‘0 jne BadExp ;Okay, to naprawdę dobre wyrażenie w tym miejscu

printf byte „’%s’ jest poprawnym wyrażeniem”, cr, lf , 0 dword InputLine jmp Quit BadExp: Quit: Main

printf byte „ ‘%s’ jest niepoprawnym wyrażeniem arytmetycznym”, cr, lf, 0 dword InputLine ExitPgm endp

cseg

ends

sseg stk sseg

segment para stack ‘stack’ byte 1024 dup (“stack”) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ byte 16 dup (?) ends end main

16.1.7 KILKA KOŃCOWYCH UWAG NA TEMAT CFG Techniki przedstawiane w tym rozdziale do konwersji CFG do kodu asemblerowego nie działa dla wszystkich CFG. Działają tylko na (dużych) nadzbiorach CFG znanych jako gramatyki LL(1). Kod który te techniki tworzy jest to rekurencyjne zmniejszanie składni predykcyjnej. Chociaż zbiór języków bezkontekstowych rozpoznawalnych przez gramatykę LL(1) jest nadzbiorem języków bezkontekstowych, jest bardzo duży nadzbiór i nie powinniśmy napotykać zbyt wiele różnic używając tej techniki. Jedną ważna cechą analiz prognostycznych jest to ,że nie wymagają żadnego backtracingu. Jeśli jesteśmy zżyci z nieefektywnością związaną z backtracingu, łatwo można rozszerzyć rekurencyjne zmniejszanie składni do działania z każdym CFG. Zauważmy ,że kiedy używamy backtracingu , odchodzi przymiotnik predykcyjny, pozostajemy z systemem niedeterministycznym zamiast systemem deterministycznym (predykcyjny i deterministyczny są bardzo blisko znaczeniowo w tym przypadku) Jest inny system CFG również LL(1). Tak zwany operator pierwszeństwa i LR(k) CFG ma dwa przykłady. Po więcej informacji o składni i gramatyce, skonsultuj z dobrym tekstem o teorii języka skończonego lub konstrukcji kompilatora. 16.18 POZA JĘZYKAMI BEZKONTEKSTOWYMI Chociaż większość wzorców jakie będziemy chcieli prawdopodobnie przetwarzać będzie skończonych lub bezkontekstowych, może będzie czas kiedy musimy rozpoznać pewne typ wzorców, które są poza tymi dwoma (np. języki kontekstowe). Jak się okazuje, skończony stan automatu jest najprostszą maszyna; automat stosowy (który rozpoznaje języki bezkontekstowe) jest następnym krokiem. Po automacie stosowym, następnym krokiem jest maszyna Turinga. Jednakże maszyny Turinga mają odpowiedniki w silnym 80x86, więc dopasowanie do wzorca rozpoznawane przez maszyny Turinga nie różnią się od napisania zwykłego programu. Kluczem do napisania funkcji, która rozpoznaje wzorce , które nie są bezkontekstowe jest zachowanie informacji w zmiennych i użycia tych zmiennych do decydowania który z kilku wyrobów chcemy użyć w danym czasie. Technika ta wprowadza kontekstowość. Takie techniki są bardzo użyteczne w programach sztucznej inteligencji (takie jak przetwarzanie języka naturalnego) gdzie niejasne rozwiązania zależą od przeszłej wiedzy lub bieżącego kontekstu operacji dopasowania do wzorca. Jednak stosowanie takich typów dopasowania do wzorca szybko wykroczy poza zakres tego tekstu o programowaniu w języku asemblera. 16.2 PODPROGRAMY DOPASOWANIA DO WZORCA STANDARDOWEJ BIBLIOTEKI UCR Standardowa Biblioteka UCR dostarcza bardzo wyszukanego zbioru podprogramów dopasowania do wzorca Są one wzorowane na dopasowaniu do wzorca według SNOBOLA $, wspierającego CFG i dostarczającego w pełni automatycznego backtracingu , jeśli to konieczne. Co więcej , przez napisanie tylko pięciu instrukcji języka asemblera, możemy dopasować proste lub złożone wzorce.

Jest niewiele kodu asemblerowego kiedy używamy podprogramów dopasowania do wzorca Biblioteki Standardowej, ponieważ większość działań występuje w segmencie danych. Używając podprogramów dopasowania do wzorca, najpierw konstruujemy wzorcową strukturę danych w segmencie danych. Potem przekazujemy adres tego wzorca i ciągu jaki życzymy sobie przetestować do podprogramu Biblioteki Standardowej match. Podprogram match zwraca niepowodzenie lub powodzenie w zależności od stanu porównania. Nie jest to takie całkiem łatwe jak brzmi; nauczenie się jak konstruować wzorcowe struktury danych to prawie tak jak nauczyć się programowania w nowym języku .Na szczęście, jeśli przebrnęliśmy przez omówienie języków bezkontekstowych, nauczenie się tego nowego „języka” jest lekkie. Wzorcowa struktura danych Biblioteki Standardowej przybiera następującą postać: Pattern MatchFunction MatchParm MatchAlt NextPattern EndPattern StartPattern StrSeg Pattern

struct dword dword dword dword dword word word ends

? ? ? ? ? ? ?

Pole MatchFunction zawiera adres podprogramu do wywołania, wykonującego jakąś część porównania. Powodzenie lub porażka tej funkcji określa czy dopasowano ciąg wejściowy. Na przykład Standardowa Biblioteka UCR dostarcza funkcji MatchStr, która porównuje jakiś n znakowy ciąg wejściowy z innym ciągiem znaków. Pole MatchParm zawiera adres lub wartość parametru (jeśli odpowiedni) dla podprogramu MatchFunction. Na przykład, jeśli podprogramem MatchFunction jest MatchStr, wtedy pole MatchParm zawiera adres ciągu do porównania z wprowadzanymi znakami. Podobnie podprogram MatchChar porównuje kolejne znaki w ciągu z najmniej znaczącym bajtem pola MatchParm. Niektóre funkcje dopasowujące nie wymagają żadnych parametrów, będą ignorowały każdą wartość przypisaną do pola MatchParm. Przez konwencję większość programistów przechowuje zero w nieużywanych polach struktury Pattern. Pole MatchAlt zawiera albo zero (NULL) albo adres jakiejś innej wzorcowej struktury danych. Jeśli aktualnie dopasowujemy znaki wejściowe, podprogramy dopasowania do wzorca ignorują to pole. Jednakże jeśli wzorzec jest błędnie dopasowany do ciągu wejściowego, wtedy podprogramy dopasowania do wzorca próbują dopasować wzorzec którego adres pojawia się w tym polu. Jeśli powiedzie się to alternatywne dopasowanie do wzorca, wtedy podprogram dopasowania do wzorca zwraca powodzenie do funkcji wywołującej, w przeciwnym razie zwraca niepowodzenie. Jeśli pole MatchAlt zawiera NULL, wtedy podprogram dopasowania do wzorca bezpośrednio zawodzi jeśli główny wzorzec jest nie dopasowany. Struktura danych Pattern dopasowuje tylko jedną pozycję na przykład, może dopasować pojedynczy znak, pojedynczy ciąg lub znak ze zbioru znaków. Rzeczywisty wzorzec słowa będzie zawierał prawdopodobnie kilka mniejszych wzorców połączonych razem np. wzorzec dla identyfikatora Pascal składa się z pojedynczych znaków ze zbioru znaków alfabetycznych następującym po jednym lub więcej znaku ze zbioru [a-zA-Z0-9]. Pole NextPattern pozwala nam tworzyć łączny wzorzec jako połączenie dwóch pojedynczych wzorców. Dal takiego połączonego wzorca zwracającego powodzenie, bieżący wzorzec musi być dopasowany a potem wzorzec określony przez pole NextPattern również musi być dopasowany. Zauważmy, że możemy łączyć wiele wzorców razem jeśli używamy tego pola. Ostatnie trzy pola EndPattern, StartPattern i StrSeg są do uzytku wewnętrznego podprogramu dopasowania do wzorca. Nie powinniśmy modyfikować ani analizować tych pól. Jeśli już stworzyliśmy wzorzec, bardzo łatwo jest przetestować ciąg aby zobaczyć czy jest dopasowany do tego wzorca. Sekwencja wywołująca dla podprogramu Biblioteki Standardowej UCR match to lesi ldxi mov match jc

cx, 0 Success

Podprogram match Biblioteki Standardowej oczekuje wskaźnika do ciągu wejściowego w rejestrach es:di; oczekuje wskaźnika do wzorca jaki chcemy dopasować w parze rejestrów es:di. Rejestr cx powinien zawierać długość ciągu jaki chcemy przetestować. Jeśli cx zawiera, podprogram match będzie testował cały ciąg wejściowy. Jeśli cx zawiera wartość niezerową, podprogram match będzie tylko testował pierwsze znaki cx w

ciągu . Zauważmy ,że koniec ciągu (bajt zakończony zerem) nie może pojawić się w ciągu przed pozycją określoną w cx. Dla większości aplikacji, ładujemy cx zerem przed wywołaniem match jest najbardziej stosowną operacją. Przy powrocie z podprogramu match, flaga przeniesienia oznacza powodzenie lub niepowodzenie. Jeśli flaga przeniesienia jest ustawiona, dopasowujemy ciąg do wzorca; jeśli flaga przeniesienia jest wyzerowana, wzorzec nie jest dopasowany do ciągu. W przeciwieństwie do przykładów podanych we wcześniejszych sekcjach, podprogram match nie modyfikuje rejestru di, nawet jeśli dopasowano pozytywnie. Zamiast tego zwraca pozycję niepowodzenie / powodzenie w rejestrze ax. Jest to pozycja pierwszego znaku po dopasowaniu jeśli match zwraca powodzenie, jest to pozycja pierwszego niedopasowanego znaku jeśli match zakończyło się niepowodzeniem.

16.3 FUNKCJE DOPASOWANIA DO WZORCA BIBLIOTEKI STANDARDOWEJ Standardowa Biblioteka UCR dostarcza około 20 wbudowanych funkcji dopasowania do wzorca. Funkcje te są oparte na umiejętnościach dopasowania do wzorca dostarczanych przez język programowania SNOBOLA4, więc w rzeczywistości są bardzo silne! Prawdopodobnie odkryjemy , ze te podprogramy rozwiązują wszystkie nasze potrzeby dopasowania do wzorca, chociaż łatwo jest napisać własny podprogram dopasowania do wzorca (zobacz „Projektowanie Własnych Podprogramów Dopasowania Do Wzorca”) jeśli żaden z nich nie jest odpowiedni. Poniższe subsekcje opisują każdy z tych podprogramów dopasowania do wzorca szczegółowo. Są dwie rzeczy jakie powinniśmy odnotować jeśli używamy pliku SHELL.ASM Biblioteki Standardowej kiedy tworzymy programy, które używają dopasowania do wzorca i zbiorów znaków. Po pierwsze jest linia na samym początku pliku SHELL.ASM, która zawiera instrukcję „matchfuncs”. Linia ta jest w rzeczywistości komentarzem ponieważ zawiera średnik w kolumnie jeden. Jeśli mamy zamiar używać zdolności dopasowania do wzorca Biblioteki Standardowej, musimy odkomentować tą linię przez usunięcie średnika z kolumny jeden. Jeśli będziemy chcieli skorzystać z zdolności zbioru znaków Biblioteki Standardowej UCR (bardzo popularne jeśli używamy udogodnień dopasowania do wzorca) możemy chcieć odkomentować linię zawierającą „include stdsets.a” w segmencie danych. Plik „stdsets.a” zawiera kilka popularnych zbiorów znaków, wliczając w to alfabetyczny, cyfrowy, alfanumeryczny, białych znaków i tak dalej. 16.3.1 SPANCSET Podprogram spancset przeskakuje wszystkie znaki należące do zbioru znaków. Ten podprogram bezie dopasowywał zero lub więcej znaków w określonym zbiorze i ,dlatego też, zawsze kończy się powodzeniem. Pole MatchParm we wzorcowej strukturze danych musi wskazywać zmienną zbioru znaku Biblioteki Standardowej UCR. Przykład: SkipAlphas pattern {spanceset , alpha} lesi StringWAlphas ldxi SkipAlphas xor cx, cx match 16.3.2 BRKCSET Brkcset jest przeciwne do spancset – dopasowuje zero lub więcej znaków w ciągu wejściowym, które nie są składnikami określonego zbioru znaków. Innymi słowy, brkcset będzie dopasowywał wszystkie znaki w ciągu wejściowym do znaku w określonym zbiorze znaków (lub końca ciągu). Pole matchparm zawiera adres zbioru znaków do dopasowania. Przykład: DoDigits pattern {brkcset, digits, 0 , DoDigits2} DoDigits2 pattern {spancset, digits} lesi StringWDigits

ldxi DoDigits xor cx, cx match jnc NoDigits Powyższy kod dopasowuje jakiś ciąg, który zawiera ciąg z jedną lub więcej cyfr gdzieś w ciągu. 16.3.3 ANYCSET Anycset dopasowuje pojedynczy znak w ciągu wejściowym ze zbioru znaków. Pole matchparm zawiera adres zmiennej zbioru znaków. Jeśli kolejny znak w ciągu wejściowym jest składnikiem tego ciągu, anycset ustawia akceptację ciągu i przeskakuje ten znak. Jeśli kolejny wprowadzany znak nie jest składnikiem tego zbioru, anycset zwraca niepowodzenie. Przykład: DoID DoID

pattern pattern lesi ldxi xor match jnc

{anycset, alpha, 0, DOID2} {spancset, alphanum}

StringWID DoID cx, cx NoID

Ten fragment kodu sprawdza ciąg StirngWID aby zobaczyć czy zaczyna się identyfikatorem określonym przez wyrażenie skończone[a-zA-Z][a-zA-Z0-9]*. Pierwszy pod wzorzec z anycset upewnia się ,że jest znak alfabetyczny na początku ciągu (alpha jest ustawianą zmienną stdsets.a, która ma jako składniki wszystkie znaki alfabetu) Jeśli ciąg nie zaczyna się znakiem alfabetu, wzorzec DoID to niepowodzenie. Drugi podwzorzec DoID2 przeskakuje każdy kolejny znak alfanumeryczny używający funkcji dopasowującej spancset. Zauważmy, że spancset zawsze kończy się powodzeniem. Powyższy kod po prostu nie dopasowuje ciągu, który jest identyfikatorem; dopasowuje ciąg, który zaczyna się poprawnym identyfikatorem. Na przykład, dopasowując „hisIsAnID” z „thisISAnID+SolThis-5”. Jeśli chcemy tylko dopasować pojedynczy identyfikator i nic więcej, musimy wyraźnie sprawdzić koniec ciągu w naszym wzorcu. 16.3.4 NOTANYCSET Notanycset dostarcza uzupełnienia do anycset - dopasowuje pojedynczy znak w ciągu wejściowym , który nie jest składnikiem zbioru znaków. Pole matchparm, jak zwykle, zawiera adres zbioru znaków, którego składniki, nie muszą pojawiać się jako kolejne znaki w ciągu wejściowym. Jeśli notanycset pomyślnie dopasuje znak( to znaczy kolejny znak wprowadzony nie jest w wyznaczonym zbiorze znaków), funkcja przeskakuje znak i zwraca powodzenie; w przeciwnym razie zwraca niepowodzenie. Przykład: DoSpecial pattern {notanycset, digits, 0, DoSpecial2} DoSpecial2 pattern {spancset, alphanum} lesi StringWSpecial ldxi DOSpecial xor cx, cx match jnc NoSpecial Kod ten jest podobny do wzorca DoID w poprzednim przykładzie. Dopasowuje ciąg zawierający jakiś znak z wyjątkiem cyfr a potem dopasowuje ciąg znaków alfanumerycznych 16.3.5 MATCHSTR

Matchstr porównuje kolejne zbiór znaków wejściowych z ciągiem znaków. Pole matchparm zawiera adres ciągu zakończonego zerem do porównania. Jeśli matchparm kończy się powodzeniem, zwraca ustawioną flagę przeniesienia i przeskakuje znaki, które dopasowano; jeśli kończy niepowodzeniem, próbuje alternatywnej funkcji dopasowującej lub zwraca niepowodzenie jeśli nie ma alternatywy. Przykład: DoSting pattern {matchstr, MyStr} MyStr byte “Match this!”, 0 lesi String ldxi DoString xor cx, cx match jnc NotMatchThis Ten przykładowy kod dopasowuje ciąg, który zaczyna się znakami „Match This!” 16.3.6 MATCHISTR Matchistr jest podobny do matchstr na tyle , że porównuje kolejnych kilka znaków z wartością ciągu zakończonego zerem. Jednakże, matchistr robi porównanie bez rozróżniania małych i dużych liter, Podczas porównania konwertuje znaki w ciągu wejściowym do dużych liter przed ich porównaniem za znakami, na które wskazuje pole matchparm. Dlatego też, ciąg wskazywany przez pole matchparm musi zawierać duże litery gdziekolwiek pojawiają się znaki alfabetu. Jeśli ciąg matchparm zawiera jakieś małe znaki, funkcja matchistr będzie zawsze błędne. Przykłady: DoString pattern {matchistr, Mystr} MyStr byte “Match THIS!”, 0 lesi String ldxi DoString xor cx,cx match jnc NotMatchThis Ten przykład jest identyczny do jednego z poprzednich sekcji z wyjątkiem tego, że bezie dopasowywał znaki “match this!” używając kombinacji dużych i małych liter. 16.3.7 MATCHTOSTR Matchtostr dopasowuje wszystkie znaki w ciągu wejściowym w tym znaki określone przez parametr matchparm. Ten podprogram kończy się powodzeniem jeśli określony ciąg pojawia się gdzieś w ciągu wejściowym , kończy niepowodzeniem jeśli ciąg nie pojawia się w ciągu wejściowym. Ta funkcja wzorcowa jest całkiem użyteczna dla lokacji podciągu i ignorowania wszystkiego co przyszło przed podciągiem. Przykład: DoString pattern {matchtostr, MyStr} MyStr byte :match this!”, 0 lesi String ldxi DoString xor cx, cx match jnc NotMatchThis

Podobnie jak poprzednie dwa przykłady, ten fragment kodu dopasowuje ciąg „Match This!”. Jednakże nie jest wymagane aby ciąg wejściowy (String) zaczynał się „Match this!”. Zamiast tego wymagane jest tylko, aby „Match this!” pojawiło się gdzieś w ciągu. 16.3.8 MATCHCHAR Funkcja matchchar dopasowuje pojedynczy znak. Najmniej znaczący bajt pola matchchar zawiera znak jaki chcemy dopasować . Jeśli kolejny znak w ciągu wejściowym jest tym znakiem, wtedy ta funkcja kończy się powodzeniem, w przeciwnym razie kończy się niepowodzeniem. Przykład: DoSpace pattern {matchchar, ‘ ‘} lesi String ldxi DoSpace xor cx, cx match jnc NoSpace Ten fragment kodu dopasowuje każdy ciąg, który zaczyna się spacją. Zapamiętajmy, że podprogram match sprawdza tylko przedrostek ciągu. Jeśli chcielibyśmy zobaczyć czy ciąg zawierał tylko spacje (zamiast ciągu który zaczyna się spacją) będziemy musieli wyraźnie sprawdzić koniec ciągu po spacji. Oczywiście, byłoby dużo bardziej wydajne zastosowanie strcmp zamiast match do tego celu! Zauważ, że w odróżnieniu od matchstr, kodujemy znak jaki chcemy dopasować bezpośrednio w polu matchparm. To pozwala nam określić znak jaki chcemy przetestować bezpośrednio w definicji wzorca. 16.3.9 MATCHTOCHAR Podobnie jak matchtostr, matchtochar dopasowuje wszystkie znaki wliczając w to znak jaki określiliśmy. Jest podobna do brkcset z wyjątkiem tego, że musimy tworzyć zbioru znaków zawierającego pojedynczego składnika i brkcset skacze ale nie do wliczonego, określonego znaku(ów). Matchtochar kończy się niepowodzeniem jeśli nie można znaleźć określonego znaku w ciągu wejściowego Przykład: DoToSpace pattern {matchtochar, ‘ ‘) lesi String ldxi DoSpace xor cx, cx match jnc NoSpace To wywołanie match skończy się niepowodzeniem jeśli nie ma spacji w ciągu wejściowym. Jeśli są wywołanie matchtochar przeskoczy wszystkie znaki do pierwszej spacji. Jest to użyteczny wzorzec dla przeskakiwania nad słowami w ciągu. 16.3.10 MATCHCHARS Matchchars pomija zero lub więcej wystąpień pojedynczego znaku w ciągu wejściowym. Jest to podobne do spancset z wyjątkiem tego, że możemy określić pojedynczy znak zamiast całego zbioru znaków z pojedynczym składnikiem. Podobnie jak matchchar, matchchars oczekuje pojedynczego znaku w najmniej znaczącym bajcie pola matchparm. Ponieważ ten podprogram dopasowuje zero lub więcej wystąpień tego znaku, zawsze kończy się powodzeniem. Przykład: Skip2NextWord pattern {matchchars, ‘ ‘ , 0 , SkipSpcs} SkipSpcs pattern {matchchars, ‘ ‘ } -

lesi ldxi xor match jnc

String Skip2NextWord cx, cx NoWord

Ten fragment kodu skacze do początku następnego słowa w ciągu. Kończy się niepowodzeniem jeśli nie ma dodatkowego słowa w ciągu (tj. ciąg nie zawiera spacji) 16.3.11 MATCHTOPAT Matchtopat dopasowuje wszystkie znaki w ciągu w tym podciągi dopasowywane przez jakieś inne wzorce. Jest to jeden z dwóch podprogramów dopasowania do wzorca Biblioteki Standardowej UCR dostarczonej aby pozwolić na implementację wywołania funkcji nieterminalnej. Ta funkcja dopasowująca kończy się pozwodzeniem jeśli znajduje dopasowywany ciąg określony wzorcem gdzieś w linii. Jeśli kończy się powodzeniem pomija znaki po ostatnim znaku dopasowanym przez parametr pattern. Jak można oczekiwać, pole matchparm zawiera adres wzorca do dopasowania Przykład: ; Zakładamy, że jest wzorzec „expression”, który dopasowuje wyrażenia arytmetyczne. Poniższy wzorzec ;określa czy jest takie wyrażenie po którym następuje średnik FindExp MatchSemi

pattern pattern lesi ldxi xor match jnc

{matchtopat, expression, 0 , matchSemi} {matchchar, ‘;’}

String FindExp cx, cx NoExp

13.3.12 EOS EOS dopasowuje wzorzec końca ciągu. Ten wzorzec , który musi oczywiście pojawić się na końcu listy wzorca, jeśli pojawia się w ogóle., sprawdza bajt zakończony zerem. Ponieważ podprogramy Biblioteki Standardowej dopasowują tylko przedrostki, powinniśmy wstawić ten wzorzec na koniec listy jeśli chcemy zapewnić, że wzorzec dokładnie dopasowuje ciąg bez żadnych resztek znaków na końcu. EOS kończy się powodzeniem jeśli dopasowuje bajt zakończony zerem, niepowodzeniem w przeciwnym razie. Przykład: SkipNumber SkipDigits EOSPat

pattern pattern pattern lesi ldxi xor match jnc

{anycset, digits, 0, SkipDigits} {spancset, digits, 0 , EOSPat} {EOS}

String SkipNumber cx, cx NoNumber

SkipNumber dopasowuje ciąg wzorcowy, który zawiera tylko cyfry dziesiętne (od początku dopasowania do końca ciągu) Zauważ, że EOS nie wymaga parametrów, nawet parametru matchparm. 16.3.13 ARB

ARB dopasowuje liczbę dowolnych znaków. Ta funkcja dopasowania do wzorca jest odpowiednikiem Σ*. Zauważmy, że ARB jest bardzo niewydajnym podprogramem w użyciu. Działa przy założeniu, że można dopasować wszystkie pozostałe znaki w ciągu a potem próbować dopasować wzorzec określony przez pole nextpattern. Jeśli pozycja nextpattern kończy się niepowodzeniem , ARB wraca jeden znak i próbuje dopasować nextpattern ponownie. Jest to kontynuowane dopóki wzorzec określony przez nextpattern nie zakończy się sukcesem lub ARB wróci do swojej początkowej pozycji startowej. ABC kończy się powodzeniem, jeśli wzorzec określony przez nextpattern kończy się powodzeniem, niepowodzeniem, jeśli wraca do swojego punktu startowego. Daje to ogromna ilość backtracingu, który może wystąpić z ARB (zwłaszcza przy długich ciągach), więc powinniśmy próbować unikać takich wzorców jeśli to możliwe. Funkcje matchtostr, matchtochar i matchtopat realizują więcej niż może zrealizować ARB, działają one w przód zamiast w tył w ciągu źródłowym i mogą być bardziej wydajne. ARB jest użyteczna głównie jeśli jesteśmy pewni, że kolejny wzorzec pojawi się później w ciągu jaki dopasowujemy lub jeśli ciąg jaki chcemy dopasować wystąpi kilka razy i chcemy dopasować ostatnie wystąpienie (matchtostr, matchtochar i matchtopat zawsze dopasowują pierwszy wystąpienie jakie znajdą). Przykład: SkipNumber SkipDigit SkipDigits

pattern pattern pattern lesi ldxi xor match jnc

{ARB,0, 0, SkipDigit} {anycset, digits, 0 , SkipDigits} {spancset, digits}

String SkipNumber cx, cx NoNumber

Ten przykładowy kod dopasowuje ostatnią liczbę, która pojawia się w linii wejściowej. Zauważmy, że ARB nie używa pola matchparm, więc powinniśmy go ustawić domyślnie na zero. 16.3.14 ARBNUM ARBNUM dopasowuje dowolną liczbę (zero lub więcej) wzorców, które występują w ciągu wejściowym. Jeśli R przedstawia jakąś nieterminalną liczbę (funkcja dopasowania do wzorca) wtedy ARBNUMR jest odpowiednikiem wyrobu ARBNUM → R ARBNUM | ε. Pole matchparm zawiera adres wzorca, który ARBNUM próbuje dopasować. Przykład: SkipNumbers SkipNumber SkipDigits EndDigits EndString

pattern pattern pattern pattern pattern lesi ldxi xor match jnc

{ARBNUM, SkipNumber} {anycset, digits, 0 ,SkipDigits} {spancset, digits, 0 , EndDigits} {matchchars, ‚ , , EndString} {EOS}

String SkipNumbers cx, cx IllegalNumbers

Kod ten akceptuje ciąg wejściowy jeśli składa się z sekwencji zera lub więcej liczb oddzielonych spacjami i zakończonych wzorcem EOS. Odnotujmy użycie pola matchalt we wzorcu EndDigits do wyboru EOS zamiast spacji dla ostatniej liczby w ciągu. 16.3.15 SKIP

Skip dopasowuje n dowolnych znaków w ciągu wejściowym .Pole matchparm jest wartością całkowitą zawierającą liczbę znaków do przeskoczenia. Chociaż pole matchparm jest podwójnym słowem , podprogram ten ogranicza liczbę znaków do przeskoczenia do 16 bitów (65,535 znaków); to znaczy, n jest najmniej znaczącym słowem w polu matchparm. Powinno to udowodnić swoją przydatność w wielu potrzebach. Skip kończy się powodzeniem, jeśli jest przynajmniej n znaków pominiętych w ciągu wejściowym; niepowodzeniem jeśli jest mniej niż n znaków pominiętych w ciągu wejściowym. Przykład: Skiplst16 SkipNumber SkipDIgits EndDigits

pattern pattern pattern pattern lesi ldxi xor match jnc

{skip, 6, 0, SkipNumber} {anycset, digits, 0 , SkipDigits} {spancset, digits, 0, EndDigits} {EOS}

String Skiplst6 cx, cx IllegalItem

To przykład dopasowania ciągu zawierającego sześć dowolnych znaków po których następuje jedna lub więcej cyfr i bajt zakończony zerem. 16.3.16 POS Pos kończy się powodzeniem jeśli funkcje dopasowujące są obecnie przy n-tym znaku w ciągu, gdzie n jest wartością w najmniej znaczącym słowie pola matchparm. Pos kończy się niepowodzeniem jeśli funkcja dopasowująca nie jest obecnie na pozycji n w ciągu. W odróżnieniu od innych funkcji dopasowujących, pos nie pochłania znaków wejściowych. Zauważmy, że ciąg zaczyna się od pozycji zero. Więc kiedy używamy funkcji pos , kończy się powodzeniem jeśli dopasowaliśmy n znaków w tym punkcie. Przykład: SkipNumber SkipDigits EndDigits

pattern pattern pattern lesi ldxi xor match jnc

{anycset, digits, 0, SkipDigits} {spancset, digits, 0 , EndDigits} {pos, 4}

String SkipNumber cx, cx IllegalItem

Kod ten dopasowuje ciąg, który zaczyna się dokładnie 4 cyframi dziesiętnymi . 16.3.7 RPOS Rpos działa podobnie jak funkcja pos z wyjątkiem tego, że kończy się powodzeniem jeśli bieżąca pozycja jest pozycją n znaku z końca ciągu. Podobnie jak w pos, n jest 16, najmniej znaczącymi bitami pola matchparm. Również jak w pos, rpos nie pochłania znaków wejściowych Przykład: SkipNumber SkipDigits EndDigits

pattern {anycset, digits, 0 , SkipDigits} pattern {spancset, digits, 0 , EndDigits} pattern {rpos, 4} -

lesi ldxi xor match jnc

String SkipNumber cx, cx IllegalItem

Kod ten dopasowuje jakiś ciąg, który jest cały z cyfr dziesiętnych, z wyjątkiem ostatnich czterech znaków ciągu. Ciąg musi być długi przynajmniej na pięć znaków , aby powyższe dopasowanie do wzorca zakończyło się powodzeniem. 16.3.18 GOTOPOS Gotopos skacze ponad znakami w ciągu dopóki nie osiągnie pozycji znaku n w ciągu. Funkcja ta zawodzi jeśli wzorzec jest już poza pozycją n w ciągu. Najmniej znaczące słowo pola matchparm zawiera wartość dla n. Przykład: SkipNumber MatchNmbr SkipDigits EndDigits

pattern pattern pattern pattern lesi ldxi xor match jnc

{gotopos, 10, 0, MatchNmbr} {anycset, digits, 0, SkipDigits} {spancset, digits,0 , EndDigits} {rpos, 4}

String SkipNumber cx, cx IllegalItem

Ten przykładowy kod skacze do pozycji 10 w ciągu i próbuje dopasować ciąg cyfr zaczynając od znaku jedenastego. Ten wzorzec kończy się powodzeniem jeśli pozostały cztery znaki po przetworzeniu wszystkich cyfr. 16.3.19 RGOTOPOS Rgotopos działa podobnie jak gotopos z wyjątkiem tego, że idzie do pozycji określonej na końcu ciągu. Rgotopos kończy się niepowodzeniem jeśli podprogram dopasowujący jest już poza pozycją n z końca ciągu. Podobnie jak przy gotopos, najmniej znaczące słowo pola matchparm zawiera wartość dla n Przykład: SkipNumber MatchNmbr SkipDigits

pattern pattern pattern lesi ldxi xor match jnc

{rgotopos, 10, 0, MatchNmbr} {anycset, digits, 0 , SkipDigits} {spancset, digits}

String SkipNumber cx, cx IllegalItem

Ten przykład skacze do dziesiątego znaku z końca ciągu a potem próbuje dopasować jedną lub więcej cyfr startując z tego punktu. Kończy się niepowodzeniem jeśli nie ma przynajmniej 11 znaków w ciągu lub ostatnie 10 znaków nie zaczyna się ciągiem z jedną lub więcej cyframi 16.3.20 SL_MATCH2

Podprogram sl_match2 jest niczym więcej niż rekurencyjnym wywołaniem dopasowania. Pole matchparm zawiera adres wzorca do dopasowania. Jest to całkiem użyteczne dla udawania nawiasów okrągłych wokół wzorca w wyrażeniu wzorcowym. Jeśli chodzi o poniższe ciągi dopasowywane pattern1 i pattern2, są one odpowiednikami: Pattern1 pattern {sl_match2, Pattern1} Pattern2 pattern {matchchar, ‚a’} Jedyna różnica między wywołaniem wzorca bezpośrednio i wywołaniem go z sl_match2 jest taka, że sl_match2 pociąga kilka wewnętrznych zmiennych śledząc pozycję dopasowania wewnątrz ciągu wejściowego. Później możemy wyciągnąć ciąg znaków dopasowanych przez sl_match2 używając podprogramu patgrab. 16.4 PROJEKTOWANIE WŁASNEGO PRDROGRAMU DOPASOWANIA DO W ZORCA Chociaż Biblioteka Standardowa UCR dostarcza szerokiej gamy funkcji dopasowujących, nie ma sposobu aby przewidzieć potrzeby dal wszystkich aplikacji. Dlatego też, prawdopodobnie odkryjemy, że biblioteka nie wspiera pewnych funkcji drapowania do wzorca jakich potrzebujemy . Na szczęście, bardzo łatwo stworzymy swoje własne funkcje dopasowujące zwiększając ich dostępność w Bibliotece Standardowej UCR. Kiedy określimy nazwę funkcji dopasowującej we wzorcowej strukturze danych, podprogram dopasowujący wywoła określony adres używając dalekiego wywołania i przekaże następujące parametry: es:di ds:sicx -

Wskazuje kolejny znak w ciągu wejściowym. Nie powinniśmy patrzeć na znaki przed tym adresem. Co więcej, nigdy nie powinniśmy zaglądać poza koniec ciągu (zobacz poniżej cx) Zawiera cztero bajtowy parametr z pola matchparm Zawiera ostatnią pozycję, plus jeden, w ciągu wejściowym, pozwalając się nam przypatrzeć. Zauważmy, że nasz podprogram nie powinien wychodzić poza lokację es:cx lub bajt zakończony zerem; którykolwiek nadejdzie jako pierwszy.

Przy powrocie z funkcji , ax musi zawierać offset do ciągu (wartość di) ostatniego znaku dopasowanego plus jeden, jeśli nasza funkcja dopasowująca zakończyła się powodzeniem. Musi również ustawić flagę przeniesienia oznaczającą sukces. Po naszym dopasowaniu do wzorca, podprogram dopasowujący może wywołać inną funkcję dopasowującą (jedynie określoną przez kolejne pole pattern) a ta funkcja zaczyna dopasowanie spod lokacji es:ax. Jeśli dopasowanie wypadło niepomyślnie, wtedy musimy zwrócić oryginalna wartość di w rejestrze ax i zwrócić wyzerowaną flagę przeniesienia. Zauważmy, że nasza funkcja dopasowująca musi zachować wszystkie inne rejestry. Jest jeden ważny szczegół, o którym nigdy nie możemy zapomnieć pisząc własne podprogramy dopasowania do wzorca – ds nie wskazuje naszego segmentu danych, zawiera najbardziej znaczące słowo parametru matchparm. Dlatego też, jeśli mamy zamiar uzyskać dostęp do zmiennych globalnych w naszym segmencie danych będziemy musieli odłożyć ds., załadować go adresem dseg i zdjąć ds. przed opuszczeniem. Kilka przykładów w tym rozdziale demonstruje jak to zrobić. Jest kilka oczywistych przeoczeń w (bieżącej wersji) zakresie Biblioteki Standardowej UCR. Na przykład powinny być prawdopodobnie funkcje matchtostr, matchichar i matchtoichar Poniższy przykładowy kod demonstruje jak dodać podprogram matchtoistr 9doapsowanie do ciągu, wykonuje porównanie bez rozróżniania małych i dużych liter) .xlist include includelib matchfuncs .list

stdlib.a stdlib.lib

dseg

segment para public ‘data’

TestString

byte

TestPat xyz

pattern {matchtoistr, xyz} byte “XYZ”, 0

“This is the string ‘xyz’ in it”, cr, lf, 0

dseg

ends

cseg

segment para public ‘code’ assume cs:cseg. Ds:dseg

;MatchToiStr; ; ; ;inputs: ; ; ; ;outputs: ; ; ; ; MatchToiStr

Dopasowuje wszystkie znaki w ciągu w górę, i wliczając, określone parametry ciągu. Parametr ciągu musi składać się z dużych znaków. Dopasowuje ciąg używając porównania bez rozróżniania małych i dużych liter. es:ds.- ciąg źródłowy ds.:si – Ciąg do dopasowania cx – maksymalna pozycja dopasowania ax – wskazuje pierwszy znak poza końcem dopasowywanego ciągu jeśli sukces, zawiera początkową wartość DI jeśli wystąpi niepowodzenie carry – 0 jeśli niepowodzenie, 1 jeśli sukces proc pushf push push cld

far di si

;Sprawdzamy aby zobaczyć czy już jesteśmy poza punktem , które pozwoli nam przeszukać w ciąg ;wejściowy cmp di, cx jae MtiSFailure ;Jeśli ciąg wzorcowy jest ciągiem pustym, zawsze dopasowanym cmp je

byte ptr ds.:[si’, 0 MTSuccess

;Następująca pętla przeszukuje cały ciąg wejściowy szukając pierwszego znaku w ciągu ;dopasowywanym ScanLoop:

FinfdFirst:

DoCmp:

push lodsb

si

dec inc cmp jae

di di di, cx CantFindlst

mov cmp jb cmp ja and cmp jne

ah, es:[di] ah, ‘a’ DoCmp ah, ‘z’ DoCmp ah, 5fh al, ah FindFirst

;Pobranie pierwszy znak ciągu ;przesuwamy na następny (lub ostatni) znak ;jeśli przy cx wtedy musimy mieć niepowodzenie ;pobiera wprowadzany znak ;konwertuje wprowadzany znak do ;dużego znaku jeśli jest to mały znak

;Porównanie znaku wprowadzonego ;ciągu wzorcowego

;W tym punkcie, umiejscawiamy pierwszy znak w ciągu wejściowym, który dopasowuje pierwszy znak ciągu ;wzorcowego Zobacz czy ciągi są równe push

di

;zachowanie punktu restartu

CmpLoop:

DoCmp2:

StrnotThere: CanFindlst: MtiSFailure:

cmp jae lodsb cmp je

di, cx StrNotThere

inc mov cmp jb cmp ja and cmp je pop pop jmp

di ah, es:[di] ah, ‘a’ DoCmp2 ah, ‘z’ DoCmp2 ah, 5fh al, ah CmpLoop di si Scanloop

add add pop pop mov popf

sp, 2 sp, 2 si di ax, di

al., 0 MTSsuccess2

clc ret MTSSuccess2: MTSSuccess:

MatchToiStr Main

add add mov pop pop popf stc ret endp

Quit: Main

;pobranie kolejnego wprowadzanego znaku ;konwertuje znak wprowadzany do dużego znaku jeśli ;jest to mały znak

;porównuje znak wejściowy

;usuwa di ze stosu ;usuwa si ze stosu ;zwraca błędną pozycję w AX ;zwraca niepowodzenie

sp, 2 sp, 2 ax, di si di

;usuwa wartość DI ze stosu ;usuwa wartość SI ze stosu ;zwraca kolejną pozycję w AX

;zwraca powodzenie

proc mov ax, dseg mov ds, ax mov es, ax meminit lesi ldxi xor match jnc print byte jmp

NoMatch:

;zobacz czy idziemy poza ostatnią ;dostępną pozycję ;pobranie kolejnego wprowadzanego znaku ;Czy koniec parametru ciągu? Jeśli tak, powodzenie

print byte ExitPgm endp

TestString TestPat cx, cx NoMatch “Matched”, cr, lf, 0 Quit “Did not match”, cr,lf, 0

cseg sseg stk sseg

ends segment para stack ‘ stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup(?) ends end Main

16.5 WYCIAGANIE PODCIĄGÓW Z DOPASOWYWANEGO WZORCA Często, po prostu określamy, że dopasowywanie ciągu do danego wzorca jest niewystarczające Możemy chcieć wykonać różne operacje, które zależą do aktualnej informacji w ciągu. Jednakże, udogodnienia dopasowania do wzorca opisane do tej pory nie dostarczały mechanizmu dla testowania pojedynczych składników ciągu wejściowego. W tej sekcji zobaczymy jak wyciągnąć część wzorca dla dalszego przetwarzania. Być może przykład może pomóc wyjaśnić potrzebę wyekstrahowania części ciągu. Przypuśćmy, że piszemy program kupna / sprzedaży giełdowej i chcemy go przetworzyć poleceniami opisanymi przez następujące wyrażenie skończone: (buy | sell) [0-9]* shares of (ibm | apple | hp | dec) Podczas gdy łatwo jest wynaleźć wzór Biblioteki Standardowej, który rozpozna ciągi w tej postaci wywołując podprogram match, powie on nam tylko, że mamy poprawne polecenie kupna sprzedaży. Nie powie nam czy kupujemy czy sprzedajemy, kto kupuje lub sprzedaje lub jak dużo akcji kupujemy lub sprzedajemy. Oczywiście możemy wziąć różne produkty z (buy | sell |) z (ibm | apple | hp | dec) i wygenerować osiem różnych wyrażeń skończonych, które w unikalny sposób określą czy kupujemy czy sprzedajemy i czyimi akcjami handlujemy, ale nie możemy przetworzyć wartości całkowitych w ten sposób (chyba że mamy miliony wyrażeń skończonych). Lepszym rozwiązaniem byłoby wyodrębnienie podciągu ze wzorca i przetwarzać te podciągi po tym jak zweryfikujemy, że mamy poprawne polecenie kupna lub sprzedaży. Na przykład, możemy wyodrębnić kupno lub sprzedaż z jednego ciągu , cyfry z drugiego i nazwę firmy z trzeciego. Po weryfikacji składni polecenia, możemy przetwarzać pojedyncze wyekstrahowane ciągi. Podprogram Biblioteki Standardowej UCR patgrab dostarcza takiej właściwości. Zwykle wywołujemy patgrab po wywołaniu match i weryfikacji, że dopasowujemy ciąg wejściowy. Patgrab oczekuje pojedynczego parametru – wskaźnika do wzorca ostatnio przetwarzanego przez match. Patgrab tworzy ciąg na stercie składający się ze znaków dopasowanych przez dany wzorzec i zwraca wskaźnik do tego ciągu w es:di. Zauważmy, że patgrab zwraca tylko ciąg powiązany z pojedynczym wzorcową strukturą danych, nie łańcuchem wzorcowych struktur danych. Rozważmy następujący wzorzec: PatToGrab pattern {matchstr, str1, 0, Pat2} Pat2 pattern {matchstr, str2} str1 byte „Hello”, 0 str2 byte “ there”, 0 Wywołując match dla PatToGrab będziemy dopasowywać ciąg „Hello there”. Jednak, jeśli po wywołaniu match wywołamy patgrab i przekażemy mu adres PatToGrab, patgrab zwróci wskaźnik do ciągu „Hello” Oczywiście, możemy chcieć odebrać ciąg, który jest połączeniem kilku ciągów dopasowywanych wewnątrz naszego wzorca (tj. części listy wzorców). Rozważmy poniższy wzorzec: Numbers FirstNumber OtherDigs

pattern {sl_match2, FirstNumber} pattern {anycset, digits,0, OtherDigs} pattern {spancset, digits}

To dopaoswuje do wzorca ciagi takie same jak Numbers OtherDigs

pattern {anycset, digits, 0 ,OtherDigs} pattern {spancset, digits}

Więc dlaczego zawracamy sobie głowę dodatkowym wzorcem, który wywołuje sl_match2? Cóż, jak się okazuje funkcja dopasowująca sl_match2 pozwala nam tworzyć wzorce nawiasowe. Wzorzec nawiasowy jest listą wzorców, które podprogramy dopasowania do wzorca (zwłaszcza patgrab) traktują jako pojedynczy wzorzec.

Chociaż podprogram match będzie dopasowywał takie same ciągi bez względu na to jaką wersję Numbers użyjemy, patgrab stworzy dwa całkowicie różne ciągi w zależności od wybrania jednego z powyższych wzorców. Jeśli użyjemy drugiej wersji patgrab zwróci tylko pierwszą cyfrę liczby. Jeśli użyjemy pierwszej wersji ( z wywołaniem sl_match2), wtedy patgrab zwróci cały ciąg dopasowany przez sl_match2 a to wyłącza cały ciąg cyfr. Następujący program przykładowy demonstruje jak używać wzorców nawiasowych do wyodrębniania adekwatnych informacji z poleceń giełdowych przedstawionych wcześniej. Używa wzorców nawiasowych dla poleceń kupno / sprzedaż, liczby akcji i nazwy firmy .xlist include stdlib.a includelib stdlib.lib matchfuncs .list dseg segment para public ‘data’ ;Zmienne używane do przechowania liczby akcji sprzedanych / kupionych , wskaźnik do ;ciągu zawierającego polecenie kup / sprzedaj i wskaźnik do ciągu zawierającego nazwę ;firmy Count CmdPtr CompPtr

word 0 dword ? dword ?

;Jakieś ciągi testowy do wypróbowania: Cmd1 Cmd2 Cmd3 Cmd4 BadCmd0

byte byte byte byte byte

„Buy 25 shares of apple stock”, 0 “Sell 50 shares of hp stock”, 0 “Buy 123 shares of dec stock”, 0 “Sell 15 shares of ibm stock”, 0 “This is not buy/sell command”, 0

;Wzorce dla polecenia kupno / sprzedaż: ; ;StkCmd dopasowuje kupno lub sprzedaż i tworzy wzorzec nawiasowy, który zawiera ;ciąg „buy’ lub „sell” StkCmd

pattern {sl_match2, buyPat, 0 , skipspcs1}

buyPat buystr

pattern {matchistr, buystr, sellpat} byte “BUY”, 0

sellpat sellstr

pattern {matchistr, sellstr} byte „SELL“, 0

;Przeskakujemy zero lub więcej białych znaków po poleceniu kupuj skipspcs1

pattern {spancset, whitespace, 0, CountPat}

;CountPat jest wzorcem nawiasowym, który dopasowuje jeden lub więcej znaków CountPat Numbers RestOfNum

pattern {sl_match2, Numbers, 0, skipspcs2} pattern {anycset, digits,0, RestOfNum} pattern {spancset, digits}

;następujące wzorce dopasowują „ shares of „ pozwalając na białe znaki pomiędzy słowami skipspcs2 sharesPat

pattern {spancset, whitespace, 0, sharesPat} byte “SHARES”, 0

skipspcs3

pattern {spancset, whitesopace, 0, ofPat}

ofPat ofStr

pattern {matchistr, ofStr, 0, skipspcs4} byte “OF”, 0

skipspcs4

pattern {spancset, whitespace, 0, CompanyPat}

;Poniżysz wzorzec nawiasowy dopasowuje nazwę firmy. Dostępny ciąg patgrab będzie ;zawierał nazwę firmy CompanyPat

pattern {sl_match, ibmpat}

ibmpat ibm

pattern {matchistr, ibm, applePat} byte “IBM”, 0

applePat apple

pattern {matchistr, apple, hpPat} byte “APPLE”, 0

hpPat hp

pattern {matchistr, hp, decPat} byte “HP”, 0

decPat decstr

pattern {matchistr, decstr} byte “DEC”, 0 include stdsets.a

dseg

ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

;DoBuySell ; ; ; ; ; ;

Podprogram ten przetwarza polecenia giełdy kup / sprzedaj Po dopasowaniu polecenia, przechwytuje składowe polecenia i wyprowadza je jako właściwe. Podprogram demonstruje jak używać patgrab do wyodrębniania podciągów z ciągu wzorcowego

DoBuySell

proc ldxi xor match jnc

near StkCmd cx, cx

lesi patgrab mov mov lesi patgrab atoi mov free

StkCmd

Na wejściu, es:di musi wskazywać polecenia kup / sprzedaj jakie chcemy przetworzyć.

NoMatch

word ptr CmdPtr, di word ptr CmdPtr+2, es CountPat ;konwertuje cyfry na liczby całkowite Count, ax

lesi ComapnyPat patgrab mov word ptr CompPtr, di mov word ptr CompPtr+2, es

; zwraca pamięć ze sterty

printf byte byte byte dword les free les free ret NoMatch: DoBuySell Main

print byte ret endp proc mov mov mov

“Stock command: %^s\n” “Numbers of shares: %d\n” “Company to trade: %^s\n\n”, 0 CmdPtr, Cout, CompPtr di, CmdPttr di, CompPtr

“Illegal buy/sell command”, cr, lf, 0

ax, dseg ds, ax es, ax

meminit lesi call lesi call lesi call lesi call lesi call

Cmd1 DoBuzSell Cmd2 DoBuzSell Cmd3 DoButSell Cmd4 DoBuzSell BadCmd10 DoBuzSell

Quit: Main

ExitPgm endp

cseg

ends

sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segemnt para public ‘zzzzzz’ db 16 dup (?) ends end Main

Przykład danych wyjściowych: Stock command: Buy Number of shares: 25 Company to trade: apple Stock command: Sell Number of shares: 50 Company to trade: hp Stock command: Buy

Number of shares: 123 Company to trade: dec Stock command: Sell Number of shares: 15 Company to trade: ibm Illegal buy/sell command

16.6 ZASADY SEMATYCZNE I AKCJE Teoria automatów jest głównie interesuje się czy lub nie dopasowano ciąg danym wzorcem. Podobnie jak wiele nauk teoretycznych , praktyka teorii automatów jest tylko skoncentrowana na tym czy coś jest możliwe, praktyczne aplikacje nie są ważne. Przy rzeczywistych programach jednakże chcemy wykonać pewne działania jeśli dopasowujemy ciąg lub wykonujemy jeden ze zbiorów operacji w zależności od tego jak dopasowujemy ciąg. Zasada semantyczna lub akcja semantyczna jest działaniem jakie wykonujemy w oparciu o typ wzorca jaki dopasowujemy. To znaczy, jest to kawałek kodu wykonywany kiedy jesteśmy zadowoleni z zachowania dopasowania do wzorca. Na przykład, wywołanie patgrab w poprzedniej sekcji jest przykładem akcji semantycznej. Normalnie wykonujemy kod powiązany z zasadą semantyczną po powrocie z wywołania match. Z pewnością kiedy przetwarzamy wyrażenie skończone, nie ma potrzeby przetwarzania akcji semantycznej w środku operacji dopasowania do wzorca. Jednakże nie jest to przypadek dla gramatyki bezkontekstowej. Gramatyki bezkontekstowe często wymagają rekurencji lub możemy użyć takiego samego wzorca kilka razy kiedy dopasowujemy pojedynczy ciąg (to znaczy, możemy się odnosić do takiego samego nieterminala kilka razy podczas dopasowywania wzorca).Struktura danych dopasowania do wzorca tylko utrzymuje wskaźniki (EndPattern, StartPattern i StrSeg) do ostatniego podciągu dopasowywanego przez dany wzorzec. Dlatego też jeśli używamy ponownie podwzorca przy dopasowywaniu ciągu i musimy wykonać zasadę semantyczną powiązaną z tym podwzorcem, będziemy musieli wykonać zasadę semantyczną w środku operacji dopasowywania do wzorca, zanim odniesiemy się to tego podciągu ponownie. Okazuje się bardzo łatwe wprowadzanie zasad semantycznych w środku operacji dopasowania do wzorca. Wszystko co musimy zrobić to napisanie funkcji dopasowania do wzorca, która zawsze kończy się powodzeniem (tj. zwraca wyzerowaną flagę przeniesienia) . Wewnątrz ciała naszego podprogramu dopasowania do wzorca możemy wybrać zignorowanie ciągu dopasowywanego kodu, i przeprowadzenia testowania i wykonania innych akcji jakie sobie życzymy. Nasz podprogram akcji semantycznej, przy zwrocie, musi ustawić flagę przeniesienia i musi skopiować oryginalną zawartość di do ax. Musi zachować wszystkie inne rejestry. Nasza akcja semantyczna nie może wywołać podprogramu match (zamiast tego wywołujemy sl_match). Match nie pozwala na rekurencję (nie jest współbieżny) i wywołując match wewnątrz podprogramu akcji semantycznej spaskudzimy dopasowanie do wzorca w toku. Następujący przykład dostarcza kilka przykładów podprogramów akcji semantycznej wewnątrz programu. Program ten konwertuje wyrażenia arytmetyczne w postaci (algebraicznej) wzrostkowej do postaci odwróconej notacji polskiej ; INFIX.ASM ; ;Prosty program który demonstruje podprogram dopasowania do wzorca w bibliotece UCR. Program akceptuje ; wyrażenia arytmetyczne w linii poleceń ( nie jest dozwolone żadne przeplatanie miejsca, to znaczy, musi być ;tylko jeden parametr w linii poleceń0 i konwertuje go z notacji wzrostkowej do notacji odwrotnej (rpn) .xlist include stdlib.a includelib stdlib.lib .list dseg

segment para public ‘data’

;Gramatyka dla prostej operacji translacji infix -> postfix (akcje semantyczne ;klamrowymi): ; ; E → FE’ ; E → +F {output ‘+’} E’ | -F {output ‘ – ‘} E’ | ; F → TF’ ; F → *T {output ‘*’} F’ | /T {output ’/’} F’ | ;T → -T {output ‘neg’} | S ; S → {output stała} | (E) ; ;Wzorzec Biblioteki Standardowej UCR , który działa na powyższej gramatyce: ; Wyrażenie składa się z pozycji „E” po której następuje koniec ciągu: infix2rpn EndOfString

są otoczone nawiasami

pattern {sl_Match2, E, EndOfString} pattern {EOS}

; pozycja “E” składa się z pozycji “F” opcjonalnie, po której następuje “+” lub “-“ i inna ;pozycja „E”: E Eprime epf epPlus

pattern pattern pattern pattern

{sl+maych2, F, , Eprime} {MatchChar, ‘+’, Eprome2, epf} {sl_match2, F,,epPlus} {OutputPlus,,,Eprime}

Eprome2 emf epMinus

pattern {MatchChar, ‘-‘, Succeed, emf} pattern {sl_match2, F,,epMinus} pattern {OutputMinus,,,Eprime} ;zasada semantyczna

;zasada semantyczna

;Pozycja “F” składa się z pozycji “T” opcjonalnie po której następuje „*” lub „/”, po którym ;następuje inna pozycja „T”: F Fprime fmf pMul

pattern pattern pattern pattern

{sl_match2, T, Fprime} {MatchChar, ‘*’ , Fprime2, fmf} {sl_match2, T, 0, pMul} {OutputMul,,,Fprime} ;zasada semantyczna

Fprime2 fdf pDiv

pattern {MatchChar, ‘/’, Succeed, fdf} pattern {sl_match2, T, 0, pDiv} pattern {OutputDiv, 0,0, Fprime}

;zasada semantyczna

;Pozycja „T“ składa się z pozycji „S“ lub „-„ po których następuje inna pozycja „T“: T TT tpn

pattern {MatchChar, ‘-‘, S, TT} pattern {sl_match2, T, 0, tpn} pattern {OutputNeg} ;zasada semantyczna

;Pozycja „S” jest albo ciągiem z jedną lub więcej cyfr albo „(„ po którym następuje i pozycja „E” ; po której następuje „)”: Const spd

pattern {sl_Match2, DoDigits, 0, spd} pattern {OutputDigits}

DoDigits SpanDigits

pattern {Anycset, Digits, 0, SpanDigits} pattern {Spancset, Digits}

S IntE CloseParen

pattern {MatchChar, ‘(‘, Const, IntE} pattern {sl_Match2, E,0, CloseParen} pattern {MatchChar, ‘)’}

Succeed

pattern {DoSucceed}

Include stdsets.a dseg

ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

;DoSucceed dopasowuje pusty ciąg. Innymi słowy, dopasowuje cokolwiek i zawsze zwraca powodzenie ;bez zjadania jakiegoś znaku z ciągu wejściowego DoSucceed

DoSucceed

proc mov stc ret endp

near ax, di

;OutputPlus jest zasadą semantyczną , która wyprowadza operator „+” po analizie poprawności operatora ;dodawania w ciągu wzrostkowym OutputPlus

OutputPlus

proc print byte mov stc ret endp

far „ +”, 0 ax, di

;wymagane przez sl_Match

;OutputMinus jest zasadą semantyczną , która wyprowadza operator „-„ po analizie poprawności operatora ;odejmowania w ciągu wzrostkowym OutputMinus

OutputMinus

proc print byte mov stc ret endp

far „ –„ , 0 ax, di

;wymagane przez sl_match

;OutputMul jest zasadą semantyczną , która wyprowadza operator „*” po analizie poprawności operatora ;mnożenia w ciągu wzrostkowym OutputMul

OutputMul

proc print byte mov stc ret endp

far „ *”, 0 ax, di

;wymagane przez sl_match

;OutputDiv jest zasadą semantyczna która wyprowadza operator „/” po analizie poprawności operatora dzielenia ; w ciągu wzrostkowym OutputDiv

OuyputDiv

proc print byte mov stc ret endp

far „ /”, 0 ax, di

;wymagane przez sl_Match

;OutputNeg jest zasadą semantyczną która wyprowadza jednoargumentowy operator „-„ po analizie poprawności operatora negacji w ciągu wzrostkowym OutputNeg

OutputNeg

proc print byte mov stc ret endp

far „ neg”, 0 ax, di

;wymagane przez sl_match

;OutputDigits wyprowadza wartość numeryczną kiedy napotyka poprawną wartość całkowitą w ciągu ;wejściowym OutputDigits

OutputDigits

proc push push mov putc lesi patgrab puts free stc pop mov pop ret endp

far es di al., ‘ ‘ stała

di ax, di es

;Okay, tu mamy program główny, który pobiera parametr z linii poleceń i analizuje go Main

proc mov mov mov

ax, dseg ds, ax es, ax

meminit print byte getsm print byte ldxi xor match jc

Succeeded:

print byte putcr

Quit: Main

ExitPgm endp

cseg

ends

;pamięć na stercie „Enter an arithemtic expression: „, 0 “Expression in postfix form: “,0 infix2rpn cx, cx Succeeded “Syntax error”, 0

;Alokacja rozsądnej ilości miejsca na stosie (8k)

sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

;zzzzzzseg musi być ostatnim segmentem ładowanym do pamięci! Zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

16.7 KONSTRUOWANIE WZORCÓW DLA PODPROGRAMU MATCH Głównym tematem jaki omówimy jest to ,jak konwertować wyrażenia skończone i gramatyki bezkontekstowe do wzorców odpowiednich dla podprogramów dopasowania do wzorca Biblioteki Standardowej UCR. Większość przykładów pojawiających się do tego punktu używało doraźnych schematów translacji; teraz jest najwyższy czas dostarczyć algorytmu do wykonania tego zadania. Poniższy algorytm konwertuje gramatykę bezkontekstową do wzorcowej struktury danych Biblioteki Standardowej UCR. Jeśli chcemy skonwertować wyrażenie skończone do wzorca, najpierw konwertujemy wyrażenie skończone do gramatyki bezkontekstowej (zobacz „Konwertowanie WS do CFG”). Oczywiście, łatwo jest skonwertować wiele postaci wyrażeń skończonych bezpośrednio do wzorca, kiedy takie konwersje są oczywiste możemy ominąć następujący algorytm; na przykład powinno być oczywiste, że możemy użyć spancset do dopasowania wyrażenia skończonego takiego jak [0-9]*. W pierwszym kroku musimy zawsze wyeliminować lewostronną rekurencję z gramatyki. Wygenerujemy pętlę nieskończoną (i krach maszyny) jeśli próbujemy kodować gramatykę zawierającą lewostronną rekurencję we wzorcowej strukturze danych. Po informacji o eliminowaniu rekurencji lewostronnej, zobacz „Eliminowanie Rekurencji Lewostronnej I opuszczanie współczynnika CFG” Możemy również chcieć opuszczenia współczynnika gramatyki podczas eliminacji rekurencji lewostronnej Podprogramy Biblioteki Standardowej w pełni wspierają backtracing, więc opuszczanie współczynnika nie jest wyłącznie konieczne, jednak podprogramy dopasowujące będą wykonywały się szybciej jeśli nie będzie backtracku. Jeśli gramatyka wyrobu przybiera formę A → B C gdzie A, B i C są nieterminalnymi symbolami, stworzymy poniższy wzór: A pattern {sl_match2, B, 0, C} Ten wzorzec opisany dla A sprawdza wystąpienia wzorca B po którym następuje wzorzec C. Jeśli B jest relatywnie prostym wyrobem (to znaczy możemy skonwertować go do pojedynczej wzorcowej struktury danych),możemy zoptymalizować to do: A

pattern {B’s Matching Function, B’s parametr, 0, C}

Pozostałe przykłady zawsze będą wywoływały sl_match2. Jednakże tak długo jak te nieterminalne są po prostu wywoływane, możemy je zagiąć do wzorca A’’ Jeśli gramatyka wyrobu przybiera postać A → B | C gdzie A,B i C są nieterminalnymi symbolami możemy stworzyć następujący wzór: A

pattern {sl_match2, B, C}

Ten wzór próbuje dopasować B. Jeśli kończy się powodzeniem, kończy się powodzeniem A; jeśli kończy się niepowodzeniem, próbuje dopasować C. W tym punkcie A’’ kończy się sukcesem lub niepowodzeniem , sukcesem lub niepowodzeniem kończy się C. Działanie z symbolami terminalnymi jest kolejną rzeczą do rozważenia. To jest całkiem łatwe – wszystko co musimy zrobić to użycie właściwej funkcji dopasowującej dostarczanej przez Bibliotekę Standardową np. matchstr lub matchchar. Na przykład jeśli mamy wyrób w postaci A → abc | y skonwertujemy do następującego wzorca: A ac ypat

pattern {matchstr, abc, ypat} byte “abc”, 0 pattern {matchstr, ‘y’}

Jedynym pozostałym szczegółem do rozpatrzenia jest ciąg pusty. Jeśli mamy wyrób w postaci A → ε wtedy musimy napisać funkcję dopasowania do wzorca która zawsze kończy się powodzeniem. Eleganckim sposobem zrobienia tego jest napisanie zwykłej funkcji dopasowania do wzorca. Ta funkcja to succeed

succeed

proc mov stc ret endp

far ax, di

;wymagane przez sl_match ;zawsze powodzenie

Innym , podstępnym, sposobem do osiągnięcia sukcesu jest użycie matchstr i przekazanie pustego ciągu do dopasowania np. succees emptystr byte

pattern {matchstr, emptustr} 0

Pusty ciąg zawsze dopasowuje ciąg wejściowy, bez względu co zawiera ciąg wejściowy. Jeśli mamy wyrób z kilkoma alternatywami a ε jest jedna z nich, musimy przetworzyć ostatnie ε. Na przykład, jeśli mamy wyrób A → abc | y | BC | ε użyjemy poniższego wzorca: A abc tryY tryBC DoSucceess

pattern byte pattern pattern pattern

{matchstr, abc, tryY} “abc”, 0 {matchchar, ‘y’, tryBC} {sl_match2, B, DoSuccess, c} {succeed}

Technika opisana powyżej pozwala nam skonwertować każdą CFG do wzorca, który może przetworzyć Biblioteka Standardowa, co z pewnością nie wykorzystuje udogodnień Biblioteki Standardowej, nie tworząc szczególnie wydajnego wzorca. Na przykład rozważmy wyrób: Digits → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 Konwertują to do wzorca używając tej techniki opisanej powyżej dostajemy wzorzec: Digits try1 try2 try3 try4 try5 try6 try7 try8 try9

pattern pattern attern pattern pattern pattern pattern pattern pattern pattern

{matchchar, ‘0’, try1} {matchchar, ‘1’,try2} {matchchar, ‘2,try3} {matchchar, ‘3’,try4} {matchchar, ‘4’,try5} {matchchar, ‘5’,try6} {matchchar, ‘6’,try7] {matchchar, ‘7’,try8} {matchchar, ‘8’,try9] {matchchar, ‘9’}

Oczywiście nie jest to bardzo dobre rozwiązanie ponieważ możemy dopasować ten sam wzór w pojedynczej instrukcji: Digits

pattern {anycset, digits}

Jeśli nasz wzorzec jest łatwy do określenia przy użyciu wyrażeń skończonych, powinniśmy spróbować zakodować go przy użyciu wbudowanych funkcji dopasowania do wzorca i powrócić do powyższego algorytmu ponieważ działamy na wzorcach niskiego poziomu jak najlepiej można . Z doświadczenia możemy wybrać właściwą równowagę pomiędzy algorytmem w tej sekcji a doraźnymi metodami odkrytymi przez nas. 16.11 PODSUMOWANIE Z pewnością był to długi rozdział. Generalnie tematowi dopasowania do wzorca jest poświęcono niewystarczająca ilość uwagi w różnych tekstach. Faktycznie, rzadko widać więcej niż dwanaście stron

dedykowanych teorii automatów, kompilatorów lub językom dopasowania do wzorca takich jak Icon lub SNOBOL4. Jest to jeden z głównych powodów dla którego ten rozdział jest rozległy, pomagając pokryć niedostatki dostępnej gdzie indziej. Jednakże, jest inny powód dla długości tego rozdziału a zwłaszcza liczby linii kodu pojawiającego się w tym rozdziale – demonstruje jak łatwo jest odkryć pewną klasę programów używających technik dopasowania do wzorca. Czy możesz sobie wyobrazić napisanie programu takiego jak Madventure używając standardowego C lub technik programistycznych Pascala? Program wynikowy byłby prawdopodobnie dłuższy niż wersja asemblerowa pojawiająca się w tym rozdziale! Jeśli nie jesteś pod wrażeniem siły dopasowania do wzorca, być może powinieneś jeszcze raz przeczytać ten rozdział. Jest bardzo zaskakujące jak mało programistów naprawdę rozumie teorię dopasowania do wzorca , zwłaszcza rozważając jak wiele programów używa , lub może korzystać z technik dopasowania do wzorca. Rozdział zaczyna się omówieniem teorii poza dopasowaniem do wzorca. Omawia proste wzorce, znane jako języki skończone i opisuje jak zaprojektować niedeterministyczne i deterministyczne skończone stany automatów – funkcje, które dopasowują wzorce opisane przez wyrażenia skończone. Rozdział ten opisuje również jak skonwertować NFA i DFA do programów asemblerowych. * ”Wprowadzenie do teorii języka formalnego (automatów) * ”Maszyny kontra Języki’ * „Języki skończone” * „Wyrażenia skończone” * „Niedeterministyczne Skończone stany automatów (NFA) * „Konwertowanie Wyrażeń skończonych do NFA” * „Konwertowanie NFA do języka asemblera” * „Deterministyczne skończone stany automatów (DFA) * „Konwertowanie DFA do języka asemblera” Chociaż języki skończone są prawdopodobnie najpowszechniej przetwarzanymi wzorcami w nowoczesnych programach dopasowania do wzorca, są one również tylko małym podzbiorem możliwych typów wzorców jakie możemy przetwarzać w programie. Języki bezkontekstowe ,wliczając wszystkie języki skończone jako podzbiór, wprowadzają wiele typów wzorców które nie są skończone. Do przedstawienia języka bezkontekstowego często używamy gramatyki bezkontekstowej. CFG zawiera zbiór wyrażeń znanych jako wyroby. Ten zbiór wyrobów, zbiór nieterminalnych symboli, zbiór symboli terminalnych i specjalnego nieterminala, symbolu startowego, dostarcza podstaw do konwersji wzorców do języka programowania. W tym rozdziale posługujemy się specjalnym zbiorem gramatyk bezkontekstowych znanych jako gramatyka LL(1). Aby właściwie zakodować CFG do asemblera musimy najpierw skonwertować gramatykę do gramatyki LL(1). Kodowanie to daje nam rekurencyjne zmniejszenie analizy predykcyjnej. Dwoma pierwszymi krokami wymaganymi przed konwersją gramatyki do programu który rozpoznaje ciągi w języku bezkontekstowym jest eliminacja lewostronnej rekurencji z gramatyki i opuszczanie współczynnika gramatyki. Po tych dwóch krokach jest relatywnie łatwo skonwertować CFG do asemblera *”Języki bezkontekstowe” *”Eliminacja rekurencji lewostronnej i opuszczanie współćzynnika CFG’ *”Konwersja CFG do języka asemblera” *”Końcowe uwagi na temat CFG” Czasami łatwiej jest działać z wyrażeniami skończonymi zamiast gramatykach bezkontekstowych. Ponieważ CFG są bardziej mocniejsze niż wyrażenia skończone, ten tekst generalnie adoptuje gramatyki gdzie tylko to możliwe. Jednakże wyrażenia skończone są generalnie łatwiejsze do pracy (dla prostych wzorców) , zwłaszcza we wczesnych etapach projektowania Wcześniej czy później możemy potrzebować skonwertować wyrażenie skończone do CFG, wiec połączymy go z innym składnikiem gramatyki. Jest to bardzo łatwe do zrobienia i mamy prosty algorytm do konwersji WS do CFG. *”Konwersja WS do CFG” Chociaż konwersja CFG do asemblera jest prostym procesem, jest bardzo nużące. Biblioteka Standardowa UCR wprowadza zbiór podprogramów dopasowania do wzorca, które w zupełności eliminują to znużenie i dostarczają dodatkowych udogodnień (takich jak automatyczny backtracing, pozwalający kodować gramatyki, które nie są LL(1)) Pakiet dopasowania do wzorca w Bibliotece Standardowej jest prawdopodobnie najnowocześniejszym i silnym zbiorem dostępnych podprogramów. Powinniśmy zdecydowanie zbadać zastosowanie tych podprogramów, co może zabrać sporo czasu. *”Podprogramy dopasowania do wzorca Biblioteki Standardowej UCR” *”Funkcje dopasowania do wzorca Biblioteki Standardowej”

Jedna z cech dostarczaną przez Bibliotekę Standardową jest nasza zdolność do pisania przerobionych funkcji dopasowani do wzorca. Dodatkowo te funkcje dopasowania do wzorca pozwalają nam dodać zasady semantyczne do naszej gramatyki. *”Projektowanie własnych podprogramów dopasowania do wzorca” *”Wyodrębnianie podciągów z dopasowywanego wzorca” *”zasady semantyczne i akcje” Chociaż Biblioteka Standardowa UCR dostarcza silnego zbioru podprogramów dopasowania do wzorca, ich bogactwo może być ich podstawową wadą. Ci, którzy napotkali podprogramy dopasowania do wzorca Biblioteki Standardowej po raz pierwszy mogą czuć się przytłoczeni, zwłaszcza kiedy próbują pogodzić materiał z sekcji o gramatyce bezkontekstowej z wzorcami Biblioteki Standardowej. Na szczęście jest prosty, choć niewydajny, sposób przetłumaczenia CFG na wzorzec Biblioteki Standardowej. *”Konstruowanie wzorców dla podprogramu MATCH” Chociaż dopasowanie do wzorca jest silnym paradygmatem, z którym większość programistów powinna się zapoznać, większość ludzi ma kłopoty z aplikacjami, kiedy pierwszy raz napotykają dopasowanie do wzorca. 16.12 PYTANIA 1) Załóżmy, że mamy dwa wejścia, które są albo zerem albo jedynką. Stwórz DFA implementujące poniższe funkcje logiczne (zakładamy, że przejście do stanu końcowego jest odpowiednikiem prawdy, jeśli działamy w nieakceptowanym stanie, zwracamy fałsz) a) OR b) XOR c) NAND d) NOR e) Equals (XNOR) f) AND

2) Jeśli r, s I t są wyrażeniami skończonymi, jaki ciąg dopasujemy dla następujących wyrażeń skończonych? d) r | s a) r* b) r s c) r+ 3) Dostarcz wyrażenia skończonego dla liczb całkowitych, które pozwala na przecinki co trzy cyfry, jak w składani US (np. dla każdych trzech cyfr od prawej strony musi być dokładnie jeden przecinek). Nie pozwolono na złe umieszczenie przecinków 4) Pascalowska stała rzeczywista ma przynajmniej jedna cyfrę przed punktem dziesiętnym. Dostarcz wyrażenia skończonego dla stałej rzeczywistej FORTRAN’a, która nie ma takiego ograniczenia. 5) W wielu systemach języków(np. FORTRAN lub C) są dwa typy liczb zmienno przecinkowych o pojedynczej i podwójnej precyzji. Dostarcz wyrażenia skończonego dla liczb rzeczywistych, które pozwala na wprowadzenie liczb zmienno przecinkowych przy użyciu znaków [dDeE] jako symbol wykładnika (d/D) oznaczający podwójną precyzję. 6) Dostarcz NFA, które rozpoznaje mnemoniki dla zbioru instrukcji 886 7) Skonwertuj powyższe NFA do języka asemblera. Nie używaj podprogramów dopasowania do wzorca Biblioteki Standardowej. 8) Powtórz pytanie (7) przy użyciu podprogramów dopasowania do wzorca Biblioteki Standardowej 9) Stwórz DFA dla identyfikatorów Pascala 10) Skonwertuj powyższe DFA do kodu asemblerowego używając prostych instrukcji asemblera 11) Skonwertuj powyższe DFA do kodu asemblera używając tablicy stanu z sklasyfikowanymi wejściami. Opisz dane w swojej sklasyfikowanej tablicy. 12) Wyeliminuj lewostronną rekurencję w poniższej gramatyce

13) Opuść współczynnik gramatyki stworzony w problemie 12 14) Skonwertuj wynik z pytania (13) do języka asemblera bez używania podprogramów dopasowania do wzorca Biblioteki Standardowej 15) Skonwertuj wynik z pytania (13) do języka asemblera używając podprogramów dopasowania do wzorca Biblioteki Standardowej 16) Skonwertuj wyrażenie skończone uzyskane w pytaniu (3) do zbioru wyrobów dla gramatyki bezkontekstowej 17) Dlaczego funkcja dopasowująca ARB jest niewydajna? Opisz jak wzorzec (ARB „hello” ARB) można dopasować do ciągu „hello there” 18) Spancset dopasowuje zero lub więcej wystąpień jakichś znaków w zbiorze znaków. Napisz funkcję dopasowania do wzorca , wywoływanej jako pierwsze pole wzorcowego typu danych, która dopasowuje jedno lub więcej wystąpień jakiegoś znaku (zerknij do źródeł spancset) 19) Napisz funkcję dopasowania do wzorca matchichar, która dopasowuje pojedynczy znak bez względu na wielkość (zerknij do źródeł matchchar) 20) Wyjaśnij jak użyć funkcji dopasowania do wzorca do implementacji zasady semantycznej 21) Jak wyodrębnić podciąg z dopasowywanego wzorca? 22) Co to są wzorce nawiasowe? Jak je tworzymy?

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK Konsultacje naukowe: NEKRO [email protected]

[email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAL SIEDEMNASTY: PRZERWANIA, PRZERWANIA KONTROLOWANE I WYJATKI Koncepcja przerwan jest czyms, co rozwijalo sie w ciagu lat. Rodzina 80x86 powiekszyla tylko zamieszanie wokól przerwan poprzez wprowadzenie instrukcji int ( przerwania programowe). Istotnie, rózni producenci uzywaja terminów takich jak wyjatki, bledy, zawieszenia, pulapki i przerwania do opisania zjawiska, który omawia ten rozdzial. Niestety nie ma wyraznej jednomyslnosci, co do dokladnego znaczenia tych terminów. Rózni autorzy róznie adoptuja te terminy na swój wlasny uzytek. Chociaz jest kuszace unikanie uzywania takich naduzywanych generalnie terminów, dla celów tego omówienia byloby milo miec zbiór dobrze zdefiniowanych terminów, jakie mozemy uzyc w tym rozdziale. Dlatego tez, wybierzemy trzy z powyzszych terminów, przerwania, pulapki i wyjatki i je zdefiniujemy. Rozdzial ten spróbuje uzyc najpowszechniejszego znaczenia dla tych terminów, ale nie bedzie niespodzianka, jesli znajdziemy inne teksty uzywajace ich w innych kontekstach. W 80x86 sa trzy typy popularnie znane przerwania: pulapki, wyjatki i przerwania (przerwania sprzetowe). Rozdziale ten bedzie opisywal kazda z tych form i opisze ich wsparcie dla CPU 80x86 i kompatybilne maszyny PC. Chociaz terminy przerwanie kontrolowane i wyjatki sa czesto uzywane zamiennie, bedziemy uzywali terminu przerwanie kontrolowane do oznaczania oczekiwania przekazania sterowania do specjalnego podprogramu obslugi. Pod wieloma wzgledami przerwanie kontrolowane jest niczym wiecej niz. wywolanym wyspecjalizowanym podprogramem. Wiele tekstów odnosi sie do przerwan kontrolowanych jako przerwan programowych. Instrukcja int 80x86 jest glównym narzedziem dla wykonania przerwania kontrolowanego. Zauwazmy, ze przerwania kontrolowane sa zazwyczaj bezwarunkowe; to znaczy, kiedy wykonujemy instrukcje int, sterowanie zawsze jest przekazywane do procedury powiazanej z przerwaniem kontrolowanym. Poniewaz przerwania kontrolowane wykonuja sie przez wyrazne instrukcje, latwo jest okreslic dokladnie, które instrukcje w programie beda wywolywaly podprogram obslugi przerwan kontrolowanych. Wyjatek jest automatycznie generowana pulapka (wymuszenie zamiast prosby), która wystepuje w odpowiedzi na warunek wyjatku. Generalnie, nie ma okreslonej powiazanej z wyjatkiem, zamiast tego wyjatek wystepuje w odpowiedzi na niewlasciwe zachowanie wykonania zwyklego programu 80x86. Przyklady warunków, które moga zglaszac (powodowac) wyjatki obejmuja wykonywanie instrukcji dzielenia przez zero, wykonywanie niedozwolonych opcodów i bledy ochrony pamieci. Obojetnie, kiedy taki warunek wystapi, CPU natychmiast zawiesza wykonywanie biezacej instrukcji i przekazuje sterowanie do podprogramu obslugi wyjatków. Ten podprogram moze zdecydowac jak obsluzyc warunek wyjatku; moze próbowac naprawiac problem lub przerwac program i wydrukowac wlasciwy komunikat bledu. Chociaz generalnie nie wykonujemy okreslonych instrukcji powodujacych wyjatek, podobnie przerwan sprzetowych (pulapek), wykonywanie instrukcji jest czyms, co powoduje wyjatki. Na przyklad, dostaniemy blad dzielenia, kiedy wykonujemy instrukcje dzielenia gdzies w programie. Przerwania sprzetowe, trzecia kategoria, do której bedziemy sie odnosic po prostu jako do przerwan, sa przerwaniami sterowanymi opartymi na zewnetrznych zdarzeniach sprzetowych (zewnetrzne dla CPU) Generalnie te przerwania nie maja nic do roboty z biezaco wykonywanymi instrukcjami; zamiast tego, niektóre zdarzenia, takie jak nacisniecie klawisza na klawiaturze lub limit czasu na tajmerze chipa, informuja CPU, ze urzadzenie potrzebuje jego uwagi, a potem zwraca sterowanie ponownie do programu. Podprogram obslugi przerwan jest procedura napisana specjalnie do dzialania z pulapkami, wyjatkami i przerwaniami. Chociaz rózne zjawiska powoduja pulapki, wyjatki i przerwania, struktura podprogramu obslugi przerwan lub ISR jest w przyblizeniu taka sama dla kazdego z nich.

17.1 STRUKTURA PRZERWAN 80X86 I PODPROGRAM OBSLUGI PRZERWAN (ISR) Pomimo róznych powodów wystapienia przerwan kontrolowanych, wyjatków i przerwan, dziela one wspólny format dla swoich podprogramów obslugi. Oczywiscie, te podprogramy obslugi przerwan beda wykonywaly rózne dzialania w zaleznosci od zródla wywolania., Ale jest calkiem mozliwe napisanie pojedynczego podprogramu obslugi przerwan, który przetwarza przerwania kontrolowane, wyjatki i przerwania sprzetowe. Jest to rzadko robione, ale struktura systemu przerwan 80x86 pozwala na to. Sekcja ta bedzie opisywala strukture przerwan 80x86 i to jak napisac podstawowy podprogram obslugi przerwan dla przerwan trybu rzeczywistego 80x86. Chipy 80x86 pozwalaja na 256 wektorów przerwan. To znaczy, ze mamy do 256 róznych zródel przerwan, a 80x86 bezposrednio wywola podprogram obslugi dla tego przerwania bez przetwarzania programowego. Kontrastuje to z niewektorowymi przerwaniami, które przekazuja sterowanie bezposrednio do pojedynczego podprogramu obslugi przerwan, bez wzgledu na zródlo przerwania. 80x86 dostarcza 256 wejsc do tablicy wektorów przerwan poczynajac od adresu 0:0 w pamieci. Jest to 1KB tablica zawierajaca 256 4 bajtowych wejsc. Kazde wejscie w tej tablicy zawiera adres segmentowany, który wskazuje podprogram obslugi przerwan w pamieci. Generalnie, bedziemy sie odnosili do przerwan poprzez ich indeks w tej tablicy, tak wiec zerowy adres przerwania (wektor) jest w komórce pamieci 0: 0, wektorze przerwania jeden jest pod adresem 0: 4, wektor przerwania dwa jest pod adresem 0: 8 itd. Kiedy wystapi przerwanie, bez wzgledu na zródlo, 80x86 robi, co nastepuje? 1) CPU odklada rejestr flag na stos 2) CPU odklada daleki adres powrotny (segment: offset) na stos, najpierw wartosc segmentu 3) CPU okresla powód przerwania (tj. numer przerwania) i pobiera cztero bajtowy wektor przerwania spod adresu 0: wektor*4 4) CPU przekazuje sterowanie do podprogramu okreslonego przez wejscie tablicy wektorów przerwan Po ukończeniu tych kroków, sterownie ma podprogram obsługi przerwań. Kiedy podprogram obsługi przerwań chce zwrócić sterowanie musi wykonać instrukcję iret (interrupt return – powrót z przerwania). Zdejmuje ona daleki adres powrotny i flagi ze stosu. Zauważmy, że wykonanie dalekiego powrotu jest niewystarczające, ponieważ na stosie pozostaną flagi. Jest jedna ważna różnica pomiędzy tym jak 80x86 przetwarza przerwania sprzętowe a innymi typami przerwań – na wejściu do podprogramu obsługi przerwań sprzętowych, 80x86 blokuje dalsze przerwania sprzętowe przez wyzerowanie flagi przerwania. Przerwania kontrolowane i wyjątki nie robią tego. Jeśli chcemy odrzucić dalsze przerwania sprzętowe wewnątrz procedury przerwania kontrolowanego lub wyjątku, musimy wyraźnie wyzerować flagę przerwania instrukcją cli. Odwrotnie, jeśli chcemy zezwolić na przerwania wewnątrz podprogramu obsługi przerwań sprzętowych, musimy wyraźnie włączyć ją ponownie instrukcją sti. Zauważmy, że na blokowanie flagi przerwania 80x86 wpływa tylko przerwanie sprzętowe.. Wyzerowanie flagi przerwania nie będzie zapobiegało wykonaniu przerwania kontrolowanego lub wyjątku. ISR’y są napisane podobnie jak prawie każda inna procedura w języku asemblera z wyjątkiem tego, że wracają one instrukcją iret a nie ret. Chociaż odległość procedury ISR (near kontra far) nie ma zazwyczaj znaczenia, powinniśmy uczynić wszystkie procedury ISR far. Uczyni programowanie łatwiejszym, jeśli zdecydujemy się wywołać ISR bezpośrednio zamiast używać normalnych mechanizmów procedur przerwania. ISR’y wyjątków i przerwań sprzętowych mają bardzo specjalne ograniczenia: muszą zachować stan CPU. W szczególności, te ISR’y muszą zachować wszystkie rejestry, które modyfikują. Rozważmy następujący ekstremalnie prosty ISR: SimpleISR SimpleISR

proc mov iret endp

far ax, 0

Ten ISR oczywiście nie zachowuje stanu maszynowego; wyraźnie narusza wartość w ax a potem zwraca z przerwania. Przypuśćmy, że wykonaliśmy poniższy fragment kodu, kiedy przerwanie sprzętowe przekazało sterowanie do powyższego ISR’a:

mov ax, 5 add ax, 2 ;Przypuśćmy, że tu wystąpiło przerwanie puti Podprogram obsługi przerwań, ustawi rejestr ax na zero a nasz program wydrukuje zero zamiast wartości pięć. Gorzej, przerwania sprzętowe są generalnie asynchroniczne, w znaczeniu, że mogą wystąpić w każdym czasie i rzadko występują w tym samym miejscu programu. Dlatego też, powyższa sekwencja kodu drukowałaby siedem większość czasu; inaczej będzie drukował zero lub dwa (będzie drukował dwa, jeśli przerwanie wystąpi pomiędzy instrukcjami, mov ax, 5 a add ax, 2) Błędy w podprogramach obsługi przerwań sprzętowych są bardzo trudne do odnalezienia, ponieważ takie błędy często wpływają na wykonywanie nie powiązanego kodu. Rozwiązaniem tego problemu oczywiście jest upewnienie się, że zachowaliśmy wszystkie rejestry, jakich używamy w podprogramie obsługi przerwań dla przerwań sprzętowych i wyjątków. Ponieważ pułapki są wywoływane jasno, zasady zachowywania stanów maszynowych w takich programach są identyczne jak dla procedur. Napisanie ISR’a jest tylko pierwszym krokiem do implementacji programu obsługi przerwania. Musimy również zainicjalizować wejście tablicy wektorów przerwań adresem naszego ISR’a. Są dwa popularne sposoby wykonania tego – przechowanie adresu bezpośrednio w tablicy wektorów przerwań lub wywołanie DOS’a i pozwolenie, aby DOS wykonał to za nas. Przechowanie samego adresu jest łatwym zadaniem, Wszystko, co musimy zrobić to załadować rejestr segmentowy zerem, (ponieważ tablica wektorów przerwań jest w segmencie zero) i przechować cztery bajty adresu pod właściwym offsetem wewnątrz segmentu. Następująca sekwencja kodu inicjalizuje wejście do przerwania 255 adresem podprogramu SimpleISR przedstawionego wcześniej: mov mov pushf cli mov mov popf

ax, 0 es, ax word ptr es:[0ffh*4], offset SimpleISR word ptr es:[0ffh*4 +2], seg SimpleISR

Odnotuj jak ten kod wyłącza przerwania podczas zmiany tablicy wektorów przerwań. Jest to ważne, jeśli poprawiamy wektor przerwań sprzętowych, ponieważ nie robi tego dla przerwań występujących pomiędzy ostatnimi dwoma powyższymi instrukcjami mov; w tym punkcie, wektor przerwań jest w wewnętrznie sprzecznym stanie i wywołując przerwanie w tym punkcie przekażemy sterowanie do offsetu SimpleISR i segmentu poprzedniego programu obsługi przerwania 0FFh. To oczywiście będzie katastrofa Instrukcje, które wyłączają przerwania podczas poprawiania wektora są zbyteczne, jeśli poprawiamy adres programu obsługi pułapki lub wyjątku. Być może lepszym sposobem inicjalizacji wektora przerwań jest użycie wywołania DOS’owskiego Zbioru Wektorów Przerwań. Wywołanie DOS (zobacz „MS-DOS, PC-BIOS i I/O Plików) z ah równym 25h dostarcza tej funkcji. To wywołanie oczekuje numeru przerwania w rejestrze al. I adresu podprogramu obsługi przerwań w ds:dx. Wywołanie MS-DOS, które wykonuje tą samą rzecz jak powyższa to mov mov mov lea int mov mov

ax, 25ffh dx, seg SimpleISR ds., dx dx, SimpleISR 21h ax, dseg ds., ax

;AH=25h, AL = 0FFh ;£aduje DS:DX adresem ISR ;Wywołanie DOS ;Przywrócenie DS, więc ponownie wskazuje DSEG

Chociaż ta sekwencja kodu jest trochę bardziej złożona niż włożenie danych bezpośrednio do tablicy wektora przerwań, jest bezpieczniejsza. Wiele programów monitoruje zmiany robione na tablicy wektorów przerwań przez DOS., Jeśli wywołujemy DOS, który zmienia wejście tablicy wektora przerwań, programy te uświadomią sobie swoje zmiany. Jeśli pominiemy DOS, programy te mogą nie odkryć, że poprawiono ich własne przerwania i mogą nie działać. Ogólnie, jest to bardzo zły pomysł poprawianie tablicy wektora przerwań i nie przywracanie oryginalnego wejścia po zakończeniu naszego programu. Cóż programy zawsze zachowują poprzednią wartość wejścia tablicy wektora przerwań i przywracają tą wartość przed zakończeniem. Poniższa sekwencja kodu demonstruje jak to zrobić. Po pierwsze przez poprawianie tablicy bezpośrednio: mov ax, 0 mov es, ax ;Zachowanie bieżącego wejścia w zmiennej dword IntVectSave: mov mov mov mov

ax, es:[IntNumber*4] word ptr IntVectSave, ax ax, es:[IntVect*4 +2] word ptr IntVectSave+2, ax

;Poprawienie tablicy wektora przerwań adresem naszego ISR’a pushf ;wymagane, jeśli jest to przerwanie hw cli ;„ „ „ „ „ „„ „ mov mov

word ptr es:[IntNumber*4], offset OurISR word ptr es:[IntNumber*4+2], seg OurISR

popf

;wymagane jeśli jest to przerwania hw

;Przywrócenie wejścia wektora przerwań przed opuszczeniem: mov mov

ax, 0 es, ax

pushf cli mov mov mov mov

;wymagane, jeśli jest to przerwanie hw ;” „ „ „ „ „ ax, word ptr IntVectSave es:[IntNumber*4[, ax ax, word ptr IntVectSave+2 es:[IntNumber*4+2], ax

popf -

;wymagane, jeśli jest to przerwania hw

Jeśli wolelibyśmy wywołanie DOS do zachowania i przywrócenia wejścia tablicy wektora przerwań, możemy uzyskać adres istniejącego wejścia tablicy przerwań używając wywołania DOS Pobranie Wektora Przerwań. Wywołanie to z ah = 35h, oczekuje numeru przerwania w al.; zwraca istniejący wektor dla tego przerwania w rejestrach es:bx. Próbka kodu, który zachowuje wektor przerwań używając DOS to ;Zachowanie bieżącego wejścia w zmiennej dword IntVectSave:

mov int mov mov

ax, 3500h + IntNumber 21h word ptr IntVectSave, bx word ptr IntVectSave+2, es

;AH=35h, AL = Int #

;Poprawa tablicy wektora przerwań adresem naszego ISR’a mov mov lea mov int

dx, seg OurISR ds, dx dx, OurISR ax, 2500h + IntNumber 21h

;AH=25, AL=Int #

;Przywrócenie wejścia wektora przerwań przed opuszczeniem: lds mov int -

bx, IntVectSave ax, 2500h+IntNumber 21h

;AH=25, AL=Int #

17.2 PRZERWANIA KONTROLOWANE Przerwanie kontrolowane jest przerwaniem wywoływanym programowo. Wykonując przerwanie kontrolowane używamy instrukcji int 80x86 (przerwanie programowe). Są tylko dwie podstawowe różnice pomiędzy przerwaniem kontrolowanym a dowolnym wywołaniem procedury far: instrukcja, jakiej używamy do wywołania podprogramu (int kontra call) i fakt, że przerwanie kontrolowane odkłada flagi na stos, więc musimy użyć instrukcji iret do powrotu z niej. W przeciwnym razie, rzeczywiście nie ma różnicy pomiędzy kodem programu obsługi przerwania kontrolowanego a ciałem typowej procedury far. Głównym celem przerwania kontrolowanego jest dostarczenie stałego podprogramu, który różne programy mogą wywoływać bez znajomości aktualnego adresu i czasu wykonania. MS-DOS jest doskonałym przykładem. Instrukcja int 21h jest przykładem wywołania przerwania kontrolowanego Nasze programy nie muszą znać aktualnego adresu pamięci punktu wejścia DOS’a do wywołania DOS. Zamiast tego, DOS poprawia wektor przerwanie 21h kiedy ładuje go do pamięci.Kiedy wykonujemy int 21h, 80x86 automatycznie przekazuje sterowanie do punktu wejścia DOS gdziekolwiek w pamięci się to wydarzy. Jest duża lista podprogramów wspierających, które używają mechanizmu przerwań kontrolowanych do połączenia aplikacji z nią samą. DOS, BIOS, sterownik myszy i Netware oto kilka przykładów. Ogólnie, używamy przerwań kontrolowanych do wywołania funkcji rezydentnych. Programy rezydentne ładują się same do pamięci i pozostają w pamięci dopóki się nie zakończą Poprze poprawę wektora przerwań wskazujemy podprogram wewnątrz kodu rezydentnego, inne programy, które działają po zakończeniu programu rezydentnego mogą wywołać rezydentne podprogramy poprzez wykonanie właściwej instrukcji int. Większość programów rezydentnych nie używa oddzielnych wejść do wektorów przerwań dla każdej funkcji jaką dostarczają. Zamiast tego, zazwyczaj poprawiają pojedynczy wektor przerwań i przekazują sterowanie do właściwego podprogramu używając numeru funkcji, który kod wywołujący przekazuje w rejestrze Poprzez konwencję większość programów rezydentnych oczekuje numeru funkcji w rejestrze ah typowy podprogram obsługi przerwań kontrolowanych. Typowy podprogram obsługi przerwań kontrolowanych będzie wykonywał instrukcję wyboru na wartości z rejestru ah i przekaże sterowanie do właściwego podprogramu obsługi funkcji. Ponieważ program obsługi przerwań kontrolowanych są praktycznie identyczne z procedurami far pod względem zastosowania, nie będziemy tu omawiać przerwań kontrolowanych bardziej szczegółowo. Jednakże, tekst tego rozdziału będzie zgłębiał ten temat bardziej, kiedy omawiać będzie programy rezydentne.

17.3 WYJĄTKI Wyjątki występują (lub są wywoływane0 kiedy wystąpi anormalny warunek podczas wykonywania. Jest mniej niż osiem możliwych wyjątków na maszynie pracującej w trybie rzeczywistym. Wykonywanie w trybie chronionym dostarcza wielu innych, ale nie będziemy ich rozpatrywać tutaj, będziemy tylko rozważać te wyjątki, które działają w trybie rzeczywistym. Chociaż procedury obsługi wyjątków są zdefiniowane dla użytkownika, sprzęt 80x86 definiuje wyjątki, które mogą wystąpić 80x86 również przypisuje stałą liczbę przerwań do każdego wyjątku. Poniższe sekcje opisuje każdy z tych wyjątków szczegółowo. Generalnie podprogram obsługi wyjątków powinien zachować wszystkie rejestry. Jednakże, jest kilka specjalnych przypadków, gdzie możemy chcieć wyciągnąć wartość rejestru przed zwróceniem. Na przykład, jeśli wychodzimy poza granice zakresu, możemy chcieć zmodyfikować wartość w rejestrze określonym przez instrukcję bound przed zwróceniem. Niemniej jednak, nie powinniśmy dowolnie modyfikować rejestrów w podprogramie obsługi wyjątków chyba ,że zamierzamy natychmiast przerwać wykonywanie naszego programu. 17.3.1 WYJĄTEK BŁĘDU DZIELENIA (INT 0) Wyjątek ten występuje wtedy kiedy próbujemy dzielić wartość przez zero lub iloraz nie mieści się w rejestrze przeznaczenia kiedy używamy instrukcji div lub idiv. Zauważmy, że instrukcje FPU fdiv i fdivr , nie wywołują tego wyjątku. MS-DOS dostarcza ogólnego programu obsługi wyjątku dzielenia, który drukuje informację taką jak „divide error” i zwraca sterowanie do MS-DOS. Jeśli chcemy obsłużyć błąd dzielenia sami, musimy napisać swój własny program obsługi wyjątku i poprawić adres tego podprogramu pod lokacją 0:0. W procesorach 8086, 8088, 80186 i 80188 adres powrotny na stosie wskazywał następną instrukcję po instrukcji dzielenia. Na 80286 9 późniejszych procesorach, adres powrotny wskazuje początek instrukcji dzielenia (wliczając w to bajt przedrostka, który się pojawia) . Kiedy wystąpi wyjątek dzielenia, rejestry 80x86 nie są modyfikowane; to znaczy zawierają wartości jakie przechowywały, kiedy 80x86 pierwszy raz wykonywał instrukcje div lub idiv. Kiedy wystąpi wyjątek dzielenia, są trzy sensowne rzeczy jakich możemy próbować: przerwać program (najłatwiejsze wyjście) , skok do sekcji, kodu , który próbuje kontynuować wykonywanie programu zważywszy na błąd , lub próbujemy dojść dlaczego wystąpił błąd, poprawić go i ponownie wykonać instrukcję dzielenia. Kilu ludzi wybierze tą ostatnią alternatywę ponieważ jest taka trudna. 17.3.2 WYJĄTEK POJEDYNCZEGO KROKU (ŚLEDZENIA) (INT1) Wyjątek pojedynczego kroku wystąpi po każdej instrukcji jeśli bit trace w rejestrze flag jest równy jeden. Debbugery i inne programy często będą ustawiały tą flagę ponieważ mogą one śledzić wykonywanie się programu. Kiedy wystąpi ten wyjątek, adres powrotny na stosie jest adresem następnej instrukcji do wykonania. Program obsługi wyjątku śledzenia może zdekodować ten opcod i zdecydować jak postąpić dalej. Większość debbugerów używa wyjątku śledzenia do sprawdzania punktów kontrolnych i innych zdarzeń, które zmieniają się dynamicznie podczas wykonywania programu. Debuggery, które używają wyjątku śledzenia dla pojedynczych kroków często disasemblują kolejną instrukcję używając adresu powrotnego na stosie jako wskaźnika do tego bajtu opcodu instrukcji. Generalnie, program obsługi wyjątku pojedynczego kroku powinien zachować wszystkie rejestry 80x86 i inne informacje o stanie. Jednak, jak zobaczymy interesujące zastosowanie wyjątku śledzenia później w tym tekście, gdzie będziemy celowo modyfikować wartości rejestrów czyniąc zachowanie jednej instrukcji zachowaniem innej (zobacz „Klawiatura PC”) Przerwanie jeden jest również dzielone przez możliwości uruchomienia wyjątków na 80386 i późniejszych procesorów. Procesory te dostarczają wsparcia zintegrowanego z układem poprzez rejestry uruchomieniowe. Jeśli wystąpi jakiś warunek, który dopasuje wartość w jednym z rejestrów uruchomieniowych, 80386 i późniejsze procesory wygenerują wyjątek uruchomieniowy, który używa wektora przerwania jeden. 17.3.3 WYJĄTEK PUNKTU ZATRZYMANIA (INT 3)

Wyjątek punktu zatrzymania jest w rzeczywistości przerwaniem kontrolowanym, nie wyjątkiem. Występuje kiedy CPU wykonuje instrukcję int 3. Jednakże, będziemy rozpatrywać to jako wyjątek ponieważ programiści rzadko wkładają instrukcje int 3 bezpośrednio do swoich programów. Zamiast tego debugger taki jak CodeView często dają sobie radę z rozmieszczeniem i usunięciem instrukcji int 3. Kiedy 80x86 wywołuje podprogram obsługi wyjątku punktu zatrzymania, adres powrotny na stosie jest adresem następnej instrukcji po opcodzie punktu zatrzymania. Odnotujmy jednak, że są dwie instrukcje int, które przekazują sterowanie do tego wektora. Ogólnie, jest jednobajtowa instrukcja int 3, której opcod to 0cch; w przeciwnym razie jest dwubajtowy odpowiednik; 0cdh, 03h. 17.3.4 WYJĄTEK PRZEPEŁNIENIA (INT 4 / INTO) Wyjątek przepełnienia, podobnie jak int 3, jest technicznie przerwaniem kontrolowanym. CPU wywołuje ten wyjątek tylko, kiedy wykonuje instrukcję into a flaga przepełnienia jest ustawiona. Jeśli flaga ta jest wyzerowana, instrukcja into jest faktycznie nop, jeśli flaga przepełnienia jest ustawiona, into zachowuje się jak instrukcja int 4, Programista może wprowadzić instrukcję into po obliczeniu całkowitym dla sprawdzenia przepełnienia arytmetycznego. Użycie into jest odpowiednikiem następującej sekwencji kodu:

jno GoodCode int 4 GoodCode: Jedna dużą zaletą instrukcji into jest to ,że nie opróżnia potoku lub kolejki wstępnego pobrania jeśli flaga przeniesienia nie jest ustawiona. Dlatego też użycie instrukcji into jest dobrą techniką jeśli dostarczamy podprogramu obsługi pojedynczego przepełnienia (to znaczy nie mamy jakiegoś określonego kodu dla każdej sekwencji gdzie może wystąpić przepełnienie) Adres powrotny na stosie jest adresem kolejnej instrukcji po into. Generalnie, program obsługi przepełnienia nie zwraca tego adresu. Zamiast tego zazwyczaj przerywa program lub zdejmuje adres i flagi ze stosu i próbuje obliczeń w inny sposób. 17.3.5 WYJĄTEK GRANICZNY (INT 5 / BOUND) Podobnie jak into, instrukcja bound (zobacz „Instrukcje INT, INTO, BOUND i IRET”) powoduje wyjątek warunkowy. Jeśli określony rejestr jest poza określoną granicą, instrukcja bound jest odpowiednikiem instrukcji int 5; jeśli rejestr jest wewnątrz określonej granicy , instrukcja bound jest faktycznie nop. Adres powrotny , który odkłada bound jest adresem samej instrukcji bound, a nie instrukcji następującej po bound. Jeśli wracamy z wyjątku bez modyfikacji wartości w rejestrze (lub modyfikacji granic) wygenerujemy pętlę nieskończoną ponieważ kod ponownie będzie wykonywał instrukcję bound i powtarzał ten proces ciągle i ciągle. Jedną sprytną sztuczką z instrukcją bound jest generowanie globalnego maksimum i minimum dla tablicy liczb całkowitych ze znakiem. Poniższy kod demonstruje jak możemy to zrobić: ;Ten program demonstruje jak obliczyć minimalną i maksymalną wartość dla tablicy liczb całkowitych ze znakiem ; używając instrukcji bound. .xlist .286 include includelib .list dseg

stdlib.a stdlib.lib

segment para public ‘data’

;Poniższe dwie wartości zawierają granice dla instrukcji BOUND LowerBound

word

?

UpperBound

word

?

; Tu zachowamy adres INT 5 OldInt5

dword ?

;Tu mamy tablicę dla której chcemy obliczyć minimum i maksimum: Array

word word word

1, 2, -5, 345, -26, 23 ,200 ,35, -100 ,20, 45 62, -30, -1, 21, 85, 400, -265, 3, 74, 24, -2 1024, -7, 1000, 100, -1000, 29, 78, -87, 60

ArraySize

=

($ - Array) / 2

dseg

ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

; Nasz ISR przerwania 5. Oblicza wartość w AX największej i najmniejszej granicy i przechowuje AX w jednej z ; nich (wiemy że AX jest poza zakresem z racji tego faktu ,że jesteśmy w tym ISR). ; ; Odnotujmy: w tym szczególnym przypadku wiemy ,że DS. wskazuje dseg, więc nie będziemy się martwić ;przeładowaniem tego ISR’a. ; ;Uwaga: kod ten nie obsługuje konfliktów pomiędzy bound / int 5 a klawiszem print screen. Wciskając prtsc podczas ; wykonywania tego kodu możemy wytworzyć niepoprawny wynik (zobacz tekst) BoundISR

proc cmp jl

near ax, LowerBound NewLower

; Musi być naruszona górna granica mov UpperBound, ax iret NewLower:

mov Iret

LowerBound, ax

BoundISR

endp

Main

proc mov ax, dseg mov ds, ax meminit

; Zaczynamy od poprawienia adresu naszego ISR’a w wektorze 5 mov mov mov mov mov mov

ax, 0 es, ax ax, es:[5*4] word ptr OldInt5, ax ax, es:[5*4+2] word ptr OldInt5 + 2, ax

mov mov

word ptr es:[5*4], offset BoundISR es:[5*4 +2], cs

;Okay, przetwarzamy elementy tablicy. Zaczynamy inicjalizacją górnej i dolnej wartości granicy pierwszym ; elementem tablicy mov mov mov

ax, Array LowerBound, ax UpperBound, ax

;Teraz przetwarzamy każdy element tablicy

GetMinMax:

mov mov mov bound add loop

bx, 2 cx, ArraySize ax, Array[bx] ax, LowerBound bx, 2 GetMinMax

.;zaczynamy od drugiego elementu

;przejście do kolejnego elementu

printf byte „Minimalna wartość to %d\n” byte „Maksymalna wartość to %d\n”, 0 dword LowerBound, Upper Bound ;Okay, przywracamy wektor przerwań: mov mov mov mov mov mov

ax, 0 es, ax ax, word ptr OldInt5 es:[5*4], ax ax, word ptr OldInt+2 es:[5*4+2], ax

Quit: Main

ExitPgm endp

cseg

ends

sseg stk sseg

segment para stack ‘stack’ db 1024 dup {“stack”) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

;makro DOS do wyjścia z programu

Jeśli tablica jest duża a wartości pojawiającej się w niej są względnie losowe, kod ten demonstruje szybki sposób określania wartości minimalnej i maksymalnej w tablicy. Alternatywne, porównanie każdego elementu z górną i dolną granicą i przechowanie wartości jeśli jest poza zakresem , jest generalnie wolniejszym podejściem. Prawda, jeśli instrukcja bound powoduje przerwanie kontrolowane, jest to dużo wolniejsze niż metoda porównania i przechowania. Jednakże przy dużej tablicy z wartościami losowymi naruszenie granicy będzie występowało rzadko.

Większość czasu instrukcja bound będzie wykonywała się w 7 – 13 cykli i nie będzie opróżniała potoku i kolejki wstępnego pobrania. Uwaga: IBM , w swojej nieskończonej mądrości, zdecydował użyć int 5 do operacji print screen. Domyślnie obsługując int 5 będziemy zrzucać zawartość ekranu do drukarki. Z tego wynikają dwie implikacje dla tego kto będzie stosował instrukcję bound w swoich programach. Po pierwsze, nie zainstalujesz sobie swojego własnego programu obsługi int 5 i wykonasz instrukcję bound, która wygeneruje wyjątek graniczny, powodując wydruk zawartości ekranu. Po drugie, jeśli naciśniesz klawisz PrtSc z zainstalowanym programem obsługi int 5, BIOS wywoła twój program. Ten pierwszy przypadek jest błędem programistycznym, ale ten drugi przypadek znaczy, że musisz uczynić program obsługi wyjątku granicznego trochę sprytniejszym. Powinien poszukać bajtu wskazującego na adres powrotny. Jeśli jest w opcodzie instrukcji int 5 (0cdh), wtedy musi wywołać oryginalny program obsługi int 5, lub po prostu wrócić z przerwania. Jeśli nie ma opcodu int 5, wtedy ten wyjątek został wywołany prawdopodobnie przez instrukcję bound. Zauważmy ,że kiedy wykonujemy instrukcje bound adres powrotny może nie być wskazywany bezpośrednio w opcodzie bound (0c2h). Może wskazywać bajt przedrostka instrukcji bound (np. segment, tryb adresowania lub rozmiar przesłonięcia). Dlatego też lepiej jest sprawdzić opcod int 5. 17.3.6 WYJĄTEK NIEPRAWIDŁOWEGO OPCODU (INT 6) 80286 i późniejsze procesory wywołują ten wyjątek jeśli próbujemy wykonać opcod, który nie odpowiada poprawnej instrukcji 80x86. Procesory te również wywołują ten wyjątek jeśli próbujemy wykonać bound, lds, les, lidt lub inne instrukcje . które wymagają operandu pamięci ale wyszczególniamy operand rejestru w polu mod/rm bajtu mod/reg/rm. Adres powrotny na stosie wskazuje niepoprawny opcod. Przez analizę tego opcodu możemy rozszerzyć zbiór instrukcji 80x86. na przykład możemy uruchomić kod 80486 na procesorze 80386 przez dostarczenie podprogramu, który imituje dodatkowe instrukcje 80486 (takie jak bswap, cmpxchg, itp.). 17.3.7 NIEDOSTĘPNY KOPROCESOR (INT 7) 80286 i późniejsze procesory wywołują ten wyjątek jeśli próbujemy wykonać instrukcje FPU (lub innego koprocesora) bez zainstalowanego koprocesora. Możemy użyć tego wyjątku do symulowania koprocesora w oprogramowaniu. Na wejściu do programu obsługi wyjątku, adres powrotny wskazuje opcod koprocesora który generuje wyjątek. 17.4 PRZERWANIA SPRZĘTOWE Przerwania sprzętowe są formą bardziej inżynierską (jako przeciwieństwo programistów PC) powiązaną z terminem przerwanie. Zaadoptujemy taką samą strategię i odtąd będziemy używali niezmodyfikowanego terminu „przerwanie” w znaczeniu przerwania sprzętowego. Na PC, przerwania pochodzą z wielu różnych źródeł. Podstawowymi źródłami przerwań jednakże są chip zegarowy PC, klawiatura , port szeregowy, port równoległy, stacje dyskowe, zegar czasu rzeczywistego CMOS, mysz, karta dźwiękowa i inne urządzenia peryferyjne. Urządzenia te są podłączone do programowalnego sterownika przerwań (PIC) Intel 8259A, który ustawia priorytet przerwań i łączy z CPU 80x86. Chip 8259A dodaje znaczną złożoność do programu , który przetwarza przerwania, więc ma sporo sensu omówienie najpierw PIC, przed próbą opisania jak podprogramy obsługi przerwań działa z nim. Później w tej sekcji opiszemy krótko każde urządzenie i warunki pod jakimi przerywają pracę CPU. Tekst ten w pełni opisze wiele z tych urządzeń w późniejszych rozdziałach, wiec ten rozdział nie będzie wnikał w szczegóły, z wyjątkiem tego kiedy omówimy przerwania zegarowego. 17.4.1 PROGRAMOWALNY STEROWNIK PRZERWAŃ (PIC) 8259A Chip programowalnego sterownika przerwań 8259A (odtąd 8259 lub PIC) akceptuje przerwania z ośmiu różnych urządzeń. Jeśli jedno z tych urządzeń żąda obsługi, 8259 przełączy linię wyjściowa przerwania (przyłączoną do CPU) i przekaże programowalny wektor przerwań do CPU. Możemy kaskadować urządzenia wspierając do 64 urządzeń przez połączenie dziewięciu 8259 razem: osiem urządzeń z ośmioma wejściami każde,

których wyjścia stają się ośmioma wejściami dziewiątego urządzenia. Typowy PC używa dwóch z tych urządzeń dostarczając 15 wejść przerwań (siedem na PIC master z ośmioma wejściami pochodzącymi z PIC slave przetwarzającymi osiem wejść) Następująca sekcja po tej opisze urządzenia połączone do każdego z tych wejść, a teraz skoncentrujemy się na tym co 8259 robi z tymi wejściami. Pomimo to ze względu na to omówienie, poniższa tablica listuje źródła przerwań na PC: Wejście 8259 IRQ 0 IRQ 1 IRQ 2

INT 80x86 8 9 0Ah

IRQ 3 IRQ 4 IRQ 5

0Bh 0Ch 0DH

IRQ 6 IRQ 7 IRQ 8 / 0 IRQ 9 / 1

0Eh 0Fh 70h 71h

IRQ 10 / 2 IRQ 11 / 3 IRQ 12 / 4

72h 73h 74h

IRQ 13 / 5 IRQ 14 / 6 IRQ 15 / 7

75h 76h 77h

Urządzenie Chip zegara Klawiatura Kaskada dla sterownika 2 (IRQ 8 – 15) Port szeregowy 2 Port szeregowy 1 Port równoległy 2 w AT, zarezerwowany w systemie PS/2 Stacja dyskietek Port równoległy 1 Zegar czasu rzeczywistego Powrót pionowy CGA (i inne urządzenia IRQ 2) Zarezerwowane Zarezerwowane Zarezerwowane w AT, urządzenie pomocnicze w systemie PS/2 Przerwanie FPU Sterownik dysku twardego Zarezerwowane

Tablica 66: Wejścia programowalnego sterownika przerwań PIC 8259 jest bardzo złożonym chipem do oprogramowania. Na szczęście, całe to trudne zadanie zostaje zrobione przez BIOS kiedy jest ładowany system. Nie będziemy omawiali tu jak zainicjalizować 8259, w tym tekście, ponieważ taka informacja jest użyteczna tylko dla piszących systemy operacyjne takie jak Linux, Windows lub OS/2, Jeśli chcesz uruchomić swój podprogram obsługi przerwań poprawnie pod DOS’em lub innym OS’em, nie musisz reinicjalizować PIC’a. Sprzęgamy PIC z systemem poprzez cztery lokacje I/O; port 20h / 0A0h i 21h / 0A1h. Pierwszy adres w każdej parze jest adresem master’a PIC’a (IRQ 0-7)

Rysunek 17.1 Rejestr maskowania przerwań 8259 drugi adres w każdej parze odpowiada slave’owi PIC (IRQ 8-15). Port 20h / 0A0h jest lokacją odczyt / zapis, z której zapisujemy polecenia PIC i odczytujemy status PIC, będziemy się do tego odnosili jako rejestrów poleceń lub rejestrów stanu. Rejestr poleceń jest tylko do zapisu, rejestr statusu jest tylko do odczytu. W zasadzie dzielą one tą samą lokację I/O linia odczyt/zapis w PIC określa który rejestr CPU jest dostępny. Port 21h / 0A1h jest lokacją odczyt / zapis, która zawiera rejestr maskowania przerwań, do którego będziemy się odnosili jako rejestru maski. Wybieramy właściwy adres w zależności od tego, jakiego sterownika przerwań chcemy użyć. Rejestr maskowania przerwań jest ośmiobitowym rejestrem, który pozwala nam pojedynczo blokować i odblokowywać przerwania z urządzenia do systemu. Jest to podobne do działań instrukcji cli i sti .Zapisanie zera do odpowiedniego bitu aktywuje dane przerwanie urządzenia. Zapis jedynki blokuje przerwanie z urządzenia. Zauważmy, że nie jest to intuicyjne. Rysunek 17.1 pokazuje rejestr maskowania przerwań. Kiedy zmieniamy bity w rejestrze maski, ważne jest aby po prostu nie załadować al wartością i wyprowadzić jej bezpośrednio do portu rejestru maski.. Zamiast tego powinniśmy odczytać rejestr maski a potem logicznym or wprowadzić lub and wyprowadzić bity jakie chcemy zmienić. Następująca sekwencja kodu aktywuje COM1: przerwanie bez wpływania na inne: in al., 21h ;odczyt istniejących bitów and al., 0efh ;włączenie IRQ4 (COM1) Rejestr poleceń dostarcza więcej opcji, ale są tylko trzy polecenia, jakie chcielibyśmy wykonać na tym chipie, jakie są kompatybilne z inicjalizacja BIOS’a 8259; wysłanie polecenia końca przerwania i wysłanie jednego z dwóch poleceń rejestru statusu. Przy wystąpieniu określonego przerwania, 8259 maskuje dalsze przerwania z tego urządzenia dopóki nie otrzyma sygnału końca przerwania z podprogramu obsługi przerwań. W DOS’ie wykonujemy to poprzez wpisanie wartości 20h do rejestru poleceń.. Robi to poniższy kod: mov al., 20h out 20h, al. ;Port 0A0h jeœli IRQ 8-15 Musimy wysłać dokładnie jedno polecenie końca przerwania do PIC dla każdego przerwania jakie obsługujemy. Jeśli nie wyślemy polecenia końca przerwania, PIC nie będzie honorował żadnego innego przerwania z tego urządzenia; jeśli wyślemy dwa lub więcej poleceń końca przerwania, możliwe jest , że przypadkowo potwierdzimy nowe przerwanie , które może być w toku i zgubimy to przerwanie. Dla pewnych programów obsługi przerwań nasz ISR nie będzie jedynie ISR’em, który wywołuje przerwania. Na przykład, BIOS PC dostarcza ISR dla przerwania zegarowego , który zajmuje się czasem. Jeśli

pogrzebiemy w tym przerwaniu, będziemy musieli wywołać ISR BIOS’a PC aby system działał poprawnie z czasem i obsłużyć pokrewne czasowo zadania (zobacz „Podprogramu obsługi przerwań wiązań łańcuchowych”) . Jednakże ISR zegara BIOS’a wyprowadza polecenie końca przerwania. Dlatego też nie powinniśmy wyprowadzać polecenia końca przerwania sami, w przeciwnym razie BIOS wyprowadzi drugie polecenie końca przerwania i możemy zgubić przerwanie w procesie. Inne dwa polecenie jakie możemy wysłać do 8259 pozwalają nam wybrać odczyt z rejestru obsługiwanego przerwania (ISR) lub rejestru zgłaszającego przerwanie (IRR). Rejestr obsługiwanego przerwania zawiera zbiór bitów dla każdego aktywnego ISR’a (ponieważ 8259 zezwala na priorytetowość ISR, jest całkiem możliwe ,że jeden ISR będzie przerwany przez ISR o wyższym priorytecie). Rejestr zgłaszający przerwanie zawiera zbiór bitów na odpowiednich pozycjach dla przerwania, które nie było jeszcze obsłużone (prawdopodobnie przerwanie ma mniejszy priorytet niż przerwanie aktualnie obsługiwane przez system). Odczytując rejestr obsługiwanego przerwania wykonamy następujące instrukcje: ;Odczyt rejestru obsługiwanego przerwania w PIC #1 (pod adresem I/O 20h) mov al., 0bh out 20h, al. in al, 20h Odczytując rejestr zgłaszający przerwania użyjemy poniższego kodu: ;Odczyt rejestru zgłaszającego przerwanie w PIC #1 (pod adresem I/O 20h) mov al., 0ah out 20h, al in al, 29h Zapisanie innych wartości poleceń portów może spowodować niepoprawne działanie systemu. 17.4.2 PRZERWANIE ZEGAROWE (INT 8) Płyta główna PC zawiera kompatybilny chip zegarowy 8254. Chip ten zawiera trzy kanały zegarowe, każdy generujący przerwania (w przybliżeniu) co 55 ms. Jest to około 1/18.2 sekundy. Często słyszymy, że to przerwanie odnosi się do „zegara osiemnastosekundowego”. Po prostu będziemy wywoływać to przerwanie zegarowe. Wektor przerwania zegarowego jest prawdopodobnie jest najpowszechniej poprawianym przerwaniem w systemie. Okazuje się, że są dwa wektory przerwań zegarowych w systemie. Int 8 jest wektorem sprzętowym powiązanym z przerwaniem zegarowym (ponieważ przychodzi od IRQ 0 w PIC). Generalnie nie powinniśmy łatać tego przerwania jeśli chcemy napisać zegarowy ISR. Zamiast tego powinniśmy poprawić drugie przerwanie zegarowe, przerwanie 1ch. Podprogram obsługi przerwania zegarowego BIOS (int 8) wykonuje instrukcję 1ch zanim wraca. Daje to użytkownikowi poprawionego podprogramu dostęp do przerwania zegarowego. Chyba ,że chętnie zduplikujemy kod zegarowy D|BIOS i DOS, chociaż nigdy nie powinniśmy całkowicie zamieniać istniejących zegarowych ISR’ów na jeden ze swoich własnych, powinniśmy zawsze zakładać, że ISR’y BIOS lub DOS wykonają dodatkowo nasz ISR. Poprawka w wektorze 1ch jest najłatwiejszym sposobem zrobienia tego. Ale nawet zamiana wektora 1ch na wskaźnik do naszego ISR’a jest bardzo niebezpieczna. Podprogram obsługi przerwań zegarowych jest jednym z najczęściej poprawianych przez różne programy rezydentne (zobacz „Programy rezydentne”) Prze proste zapisanie adresu naszego ISR’a w wektorze przerwania zegarowego możemy zablokować taki program rezydentny i spowodować, że nasz system będzie źle funkcjonował. Do rozwiązania tego problemu musimy stworzyć łańcuch przerwań. Po więcej szczegółów zajrzyjmy do :Podprogramy obsługi przerwań wiązań łańcuchowych”. Domyślnie przerwanie zegarowe jest zawsze włączone w chipie sterownika przerwań. Faktycznie, zablokowanie tego przerwania może spowodować krach naszego systemu lub co najmniej złe funkcjonowanie. Albo co najmniej system nie będzie wskazywał poprawnie czasu jeśli zablokujemy przerwanie zegarowe. 17.4.3 PRZERWANIE KLAWIATURY (INT 9) Mikrokontroler klawiatury na płycie głównej PC generuje dwa przerwania przy każdym naciśnięciu klawiszy – jeden kiedy naciskamy klawisz i jeden kiedy go zwalniamy. Jest to IRQ 1 na master PIC’u. BIOS

odpowiada na to przerwanie poprzez odczyt kodu klawisza klawiatury, konwertując go do znaku ASCII i przechowuje kod i kod ASCII w systemowym buforze klawiatury. Domyślnie to przerwanie jest zawsze włączone. Jeśli zablokujemy to przerwanie, system nie będzie mógł odpowiedzieć na wciskanie klawiszy, wliczając w to ctrl-alt-del. Dlatego też nasze programy powinny zawsze włączać to przerwanie, jeśli zostało ono zablokowane. Po więcej informacji o przerwaniu klawiatury zajrzymy do „Klawiatura PC”. 17.4.4 PRZERWANIA PORTU SZEREGOWEGO (INT 0Bh i INT 0Ch) PC używa dwóch przerwań IRQ 3 i IRQ 4 do wsparcia przerwania komunikacji szeregowej. Chip sterownika komunikacji szeregowej (SCC) 8250 (lub kompatybilny) generuje przerwanie w jednej z czterech sytuacji: pojawia się znak na lini szeregowej, SCC kończy transmisję znaku i mamy żądanie innego, pojawił się błąd lub wystąpiła zmiana statusu. SCC aktywuje taka samą linię przerwania (IRQ 3 lub 4 ) dla wszystkich czterech źródeł przerwań. Podprogram obsługi przerwań jest odpowiedzialny za dokładne określenie natury przerwania poprzez zapytanie SCC. Domyślnie, system blokuje IRQ 3 i IRQ 4. Jeśli instalujemy szeregowy ISR, będziemy musieli wyzerować bit maski przerwania w 8259 PIC przed tym nim będziemy odpowiadać na przerwania z SCC. Co więcej, projekt SCC zawiera własną maskę przerwania. Będziemy musieli również odblokować maskę przerwania na chipie SCC. 17.4.5 PRZERWANIA PORTU RÓWNOLEGŁEGO (INT 0Dh I 0Fh) Przerwania portu równoległego są zagadką IBM zaprojektował oryginalny system pozwalający na dwa przerwania portu równoległego a potem natychmiast zaprojektował kartę interfejsu drukarki, która nie wspierała zastosowania tych przerwań. W wyniku, jedynie oprogramowanie oparte nie o DOS używa przerwań portu równoległego (IRQ 5 i IRQ 7). Istotnie w systemie PS/2 IBM zarezerwował IRQ 5 , które uprzednio używane było dla LPT2. Jednakże, przerwania te nie marnują się. Wiele urządzeń, których inżynierowie IBM nie mogli przewidzieć, kiedy projektowali pierwsze PC, mogą znaleźć dobre zastosowanie dla tych przerwań. Przykładami są karty SCSI i karty dźwiękowe.. Wiele dzisiejszych urządzeń zawiera „zworki przerwań”, które pozwalają nam wybierać IRQ 5 lub IRQ 7 kiedy instalujemy urządzenie. Ponieważ IRQ 5 i IRQ 7 mają takie małe zastosowanie dla przerwań portu równoległego, zignorujemy „przerwania portu równoległego” w tym tekście. 17.4.6 PRZERWANIA DYSKIETKI I DYSKU TWARDEGO (INT 0Eh I INT 76H) Dyskietka i dysk twardy generują przerwania przy finalizowaniu operacji dyskowych. Jest to bardzo użyteczna cecha dla systemów wielozadaniowych, takich jak OS/2, Linux czy Windows. Podczas gdy dysk odczytuje lub zapisuje dane, CPU może wykonywać instrukcje dla innego procesu. Kiedy dysk kończy operację odczytu lub zapisu, przerywa CPU więc może on wznowić oryginalne zadanie. Gdybyśmy zajęli się urządzeniami dyskowymi jako interesującym tematem w tym tekście, książka ta musiałaby by ć znacznie dłuższa. Dlatego też, tekst ten unika omawiania przerwań urządzeń dyskowych (IRQ 6 i IRQ 14). Jest wiele tekstów, które omawiają nisko poziomowe I/O w asemblerze. Domyślnie przerwania dyskietki i twardego dysku są zawsze włączone. Nie powinniśmy zmieniać tego stanu jeśli zamierzamy używać urządzeń dyskowych w systemie. 17.4.7 PRZERWANIE ZEGARA CZASU RZECZYWISTEGO (INT 70h) PC/AT i późniejsze maszyny zawierają zegar czasu rzeczywistego CMOS. Urządzenie to jest zdolne do generowania przerwania zegarowego w wielokrotności 976 mikrosekund (wywołanie 1ms). Domyślnie przerwanie zegara czasu rzeczywistego jest zablokowane. Powinniśmy włączać tylko to przerwanie jeśli mamy zainstalowany ISR int 70h. 17.4.8 PRZERWANIE FPU (INT 75h)

FPU 80x87 generuje przerwanie jeśli kiedykolwiek wystąpi wyjątek zmienno przecinkowy. W CPU z wbudowanym FPU (80486DX i lepszych) jest bit w jednym z rejestrów sterujących, jaki możemy ustawić do symulowania przerwania wektorowego BIOS generalnie inicjalizuje takie bity dla zgodności z istniejącym systemem. Domyślnie BIOS blokuje przerwanie FPU. Większość programów, które używają FPU wyraźnie testuje rejestr stanu FPU do określenia czy wystąpił błąd. Jeśli chcemy pozwolić na przerwanie FPU, musimy włączyć przerwania w 8259 i FPU 80x87. 17.4.9 PRZERWANIA NIEMASKOWALNE (INT 2) Chipy 80x86 w rzeczywistości dostarczają dwóch rodzajów końcówek przerwań. Pierwsze to przerwania maskowalne. Jest to końcówka do której jest dołączony PIC 8259. Przerwanie to jest maskowalne ponieważ możemy włączyć lub wyłączyć go instrukcjami cli i sti. Przerwanie niemaskowalne, jak wskazuje nazwa, nie może być zablokowane programowo. Generalnie, PC używa tego przerwania do zasygnalizowania błędu parzystości pamięci, chociaż pewne systemy używają tego przerwania również do innych celów Wiele starczych systemów PC przyłącza FPU do tego przerwania. To przerwanie nie może być zamaskowane, więc domyślnie zawsze jest włączone. 17.4.10 INNE PRZERWANIA Jak wspomniano w sekcji o PIC 8259, jest kilka przerwań zarezerwowanych przez IBM. Wiele systemów używa tych zarezerwowanych przerwań dla myszki i innych celów. Ponieważ takie przerwania są z natury zależne od systemu, nie będziemy ich tu omawiać. 17.5 PODPROGRMAY OBSŁUGI PRZERWAŃ WIĄZAŃ ŁAŃCUCHOWYCH Podprogramy obsługi przerwań dzielą się na dwie podstawowe grupy – te , które potrzebują zastrzeżonego dostępu do wektora przerwań i te, które musza dzielić wektor przerwań z kilkoma innymi ISR’ami. Te z pierwszej kategorii wliczają w to obsługę błędów ISR (np. błąd dzielenia lub przepełnienia) i pewne sterowniki urządzeń. Port szeregowy jest dobrym przykładem urządzenia, które rzadko ma więcej niż jeden ISR powiązany ze sobą w danym czasie. ISR’y zegara, zegara czasu rzeczywistego i klawiatury generalnie podpadają pod drugą kategorię. Nie jest wcale niezwykłe znaleźć kilka ISR’ów w pamięci, dzielących każde z tych przerwań. Dzielenie wektora przerwań jest raczej łatwe. Wszystko co ISR musi zrobić przy dzieleniu wektora przerwań to zachować stary wektor przerwań kiedy instalowany jest ISR (czasami musimy zrobić to tak i tak, więc możemy przywrócić wektor przerwań kiedy nasz kod się skończy) a potem wywołać oryginalny ISR przed lub po tym jak przetworzymy nasz własny ISR. Jeśli zachowamy adres oryginalnego ISR’a w dseg, w zmiennej podwójnego słowa OldIntVect, możemy wywołać oryginalny ISR kodem takim jak ten: ; Przypuszczalnie DS. wskazuje DSEG w tym punkcie pushf ;symulowanie instrukcji INT przez odłożenie flag i wykonanie call OldIntVect ;dalekiego wywołania Ponieważ OldIntVect jest zmienną dword, kod ten generuje dalekie wywołanie do podprogramu, którego adres segmentowy pojawia się w zmiennej OldIntVect. Kod ten nie skacze do lokacji zmiennej OldIntVect. Wiele programów obsługi przerwań nie modyfikuje rejestru ds. wskazującego lokalny segment danych. Faktycznie, niektóre proste ISR’y nie zmieniają żadnego rejestru segmentowego. W takich przypadkach jest popularne wstawienie koniecznej zmiennej (zwłaszcza wartości starego segmentu) bezpośrednio w segmencie kodu. Jeśli to zrobimy nasz kod może skoczyć bezpośrednio do oryginalnego ISR’a zamiast go wywoływać. Możemy użyć takiego kodu: MyISR

proc jmp

near

cs: OldIntVect

MyISR OldIntVect

endp dword ?

Ta sekwencja kodu przekazuje flagi naszego ISR’a, adres powrotny flag i wartość adresu powrotnego do oryginalnego ISR’a. Świetnie, kiedy oryginalny ISR wykonuje instrukcję iret, będzie wracał bezpośrednio do kodu przerywającego (zakładając, że nie przekazał sterowania do jakiegoś innego ISR w łańcuchu). Zmienna OldIntVect musi być w segmencie kodu jeśli używamy tej techniki do przekazania sterowania do oryginalnego ISR’a. W końcu kiedy wykonujemy powyższą instrukcję jmp, musimy mieć już przywrócony stan CPU, wliczając w to rejestr ds. Dlatego też, nie wiemy jaki segment wskazuje ds. a jest prawdopodobne, że nie wskazuje naszego segmentu lokalnego. Istotnie, jedyny rejestr segmentowy jakiego wartość jest znana do cs, więc musimy przechować adres wektora w segmencie kodu. Poniższy prosty program demonstruje przerwania łańcuchowe. Ten krótki program poprawia wektor 1ch. ISR zlicza sekundy i powiadamia program główny o każdej mijającej sekundzie . Program główny drukuje krótką wiadomość co sekundę. Kiedy minie 10 sekund, program usuwa ISR z łańcucha przerwań i kończy się ;TIMER.ASM ;Program ten demonstruje jak poprawić wektor przerwania zegarowego 1Ch i stworzyć łańcuch przerwań .xlist .286 include includelib .list dseg

stdlib.a stdlib.lib

segment para public ‘data’

;TIMERISR będzie uaktualniał poniższe dwie zmienne ;Uaktualni zmienną MSEC co 55 ms ;Uaktualni zmienną TIMER co sekundę MSEC TIMER

word word

0 0

dseg

ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

; Zmienna OldIntVect musi być w segmencie kodu z powodu sposobu w jaki TimerISR przekazuje sterownie do ; następnego ISR’a w łańcuchu int 1Ch OldIntVect

dword ?

; Podprogram obsługi przerwania zegarowego ; Zwiększa zmienną MSEC o 55 przy każdym przerwaniu. ; Ponieważ to przerwanie wywoływane jest co 55 ms (w przybliżeniu) ; zmienna MSEC zawiera bieżąca liczbę milisekund. ; Kiedy wartość ta przekroczy 1000 (jedna sekunda), ISR odejmie ; 1000 od zmiennej MSEC i zwiększy TIMER o jeden TimerISR

proc push push mov mov

near ds. ax ax, dseg ds., ax

SetMSEC:

TimerISR: Main

mov add cmp jb inc sub mov pop pop jmp endp

ax, MSEC ax, 55 ax, 1000 SetMSEC Timer ax, 1000 MSEC, ax ax ds cseg: OldIntVect

;przerwania co 55 ms ;właśnie przekazano sekundę ;modyfikacja wartości MSEC

;przekazanie do oryginalnego ISR’a

proc mov ax, dseg mov ds, ax meminit

;Zaczynamy od podstawienie adresu naszego ISR’a do wektora 1Ch. Zauważmy, że musimy wyłączyć ; przerwania podczas poprawiania wektora przerwań i musimy założyć, że przerwania są później przywrócone; ; instrukcje cli i sti. Jest to wymagane ponieważ przerwanie zegarowe może nadejść pomiędzy tymi dwoma ; instrukcjami, które zapisują do wektora przerwania 1Ch. Może to nieźle namieszać mov mov mov mov mov mov

ax, 0 es, ax ax, es:[1ch*4] word ptr OldInt1C, ax ax, es:[1Ch*4+2] word ptr OldInt1C+2, ax

cli mov mov sti

word ptr es:[1Ch*4], offset TimerISR es:[1Ch*4+2],cs

;Okay, ISR uaktualni³ zmienn¹ TIMER co sekundê ; stale drukując tą wartość dopóki nie minie 10 sekund. Potem kończy TimerLoop:

mov printf byte dword cmp jbe

Timer, 0 “Timer = %d\n”, 0 Timer Timer, 10 TimerLoop

;Okay, przywracamy wektor przerwań. Musimy wyłączyć przerwania z powodów jak powyżej mov mov cli mov mov mov mov sti

ax, 0 es, ax ax, word ptr OldInt1C es:[1Ch*4], ax ax, word ptr OldInt1C+2 es:[1Ch*4+2], ax

Quit: Main cseg

ExitPgm endp ends

;makro DOS do wyjścia z programu

sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

17.6 PROBLEMY WSPÓŁBIEŻNOŚCI Drobnym problemem konstrukcyjnym przy tworzeniu ISR’a jest to co zdarzy się jeśli włączymy przerwanie w ISR a nadejdzie drugie przerwanie z tego samego urządzenia? To przerwie ISR i potem wejdzie ponownie od samego początku ISR’a. Wiele aplikacji nie zajmuje się właściwie tymi warunkami. Aplikacja, która może właściwie obsłużyć taką sytuację jest nazywana współbieżną . Segment kodu, który nie działa poprawnie przy współbieżności jest nazywany niewspółbieżnym. Rozpatrzmy program TIMER.ASM z poprzedniej sekcji. Jest to przykład programu nie współbieżnego. Przypuśćmy, że podczas wykonywania ISR’a, mamy przerwanie w następującym punkcie: TimerISR

proc push push mov mov

near ds. ax ax, dseg ds., ax

mov add cmp jb

ax, MSEC ax, 55 ax, 1000 SetMSEC

;przerwanie co 55 ms

;>

SetMSEC:

TimerISR

inc sub mov pop pop jmp endp

Timer ax, 1000 MSEC, ax ax ds cseg:OldInt1C

;sekunda przekazana ;modyfikacja wartości MSEC

;przekazanie do oryginalnego ISR’a

Przypuśćmy, że przy pierwszym wywołaniu przerwania, MSEC zawiera 950 a Timer zawiera trzy. Jeśli wystąpi drugie przerwanie w powyższym określonym miejscu, ax będzie zawierało 1005. Więc przerwanie zawiesi ISR i powróci do jego początku. Zauważmy, że TimerISR jest wystarczający dość dla zachowania rejestru ax zawierającego wartość 1005. Kiedy wykonuje się drugie wywołanie przerwania TimerISR, znajduje w MSEC jeszcze 950 ponieważ pierwsze wywołanie nie uaktualniło jeszcze MSEC. Dlatego też, dodaje 55 do tej wartości, określając, że przekroczono 1000, zwiększa Timer (ma teraz cztery) a potem przechowuje pięć w MSEC. Potem wraca (przez skok do następnego ISR’a w łańcuchu int 1Ch). Ewentualnie przekaże sterowanie do pierwszego wywołania podprogramu TimerISR. W tym czasie (mniejszym niż 55 ms po uaktualnieniu Timer przez drugie wywołanie) kod TimerISR zwiększa ponownie zmienną Timer i uaktualnia MSEC do pięć. Problem z tą sekwencją jest taki, że zwiększa zmienną Timer dwukrotnie w czasie mniejszym niż 55 ms.

Teraz możemy roztrząsać, że przerwania sprzętowe zawsze zerują flagę blokowania przerwania, więc nie byłoby możliwe dla tego przerwania aby było współbieżne. Co więcej, możemy roztrząsać, że ten podprogram jest zbyt krótki, więc nigdy nie osiagnie55ms do znanego punktu w powyższym kodzie. Jednakże zapominamy o czymś; w systemie mogą być jakieś inne zegarowe ISR’y, które wywołują nasz kod po tym jak się wykona. Taki kod osiąg a 55 ms i zdarzy się włączenie przerwania, czyniąc doskonałą możliwość aby nasz kod mógł stać się współbieżnym. Kod pomiędzy instrukcjami mov ax, MSEC a mov MSEC, ax jest nazywany obszarem krytycznym lub krytyczną sekcją. Program nie może być współbieżny podczas wykonywania w rejonie krytycznym. Posiadanie rejonu krytycznego nie oznacza, ze program nie jest współbieżny. Większość programów, nawet te, które są współbieżne mają różne rejony krytyczne. Kluczem jest zapobieżenie przerwaniom, które powodują rejony krytyczne, będące współbieżnymi, w tych rejonach krytycznych . Najłatwiejszym sposobem zapobieżenia takiemu wystąpieniu jest wyłączenie przerwań podczas wykonywania kodu w krytycznej sekcji. Możemy łatwo zmodyfikować TimerISR do tego celu: TimerISR

proc near push ds. push ax mov ax, dseg mov ds., ax ;Zaczynamy sekcję krytyczna, wyłączamy przerwania pushf cli

SetMSEC:

; zachowanie bieżącego stanu flagi I ;upewnienie czy przerwania wyłączone

mov add cmp jb

ax, MSEC ax, 55 ax, 1000 SetMSEC

inc sub mov

Timer ax, 1000 MSEC, ax

;przerwanie co 55 ms

;przekazana sekunda ;modyfikacja wartości MSEC

;Koniec rejonu krytycznego, przywrócenie flagi I do jej poprzedniego stanu popf pop ax pop ds. jmp cseg:OldInt1C ;przekazanie do oryginalnego ISR’a TimerISR endp Powrócimy do problemu współbieżności i rejonów krytycznych w następnych dwóch rozdziałach tego tekstu. 17.7 SPRAWNOSĆ SYSTEMU STEROWANEGO PRZERWANIAMI. Przerwania wprowadzają znaczną ilość złożoności do systemu oprogramowania (zobacz „Debuggowanie ISR’a”). Można spytać czy używanie przerwań jest rzeczywiście warte tych problemów. Odpowiedź to oczywiście tak. Czy ludzie używaliby przerwań gdyby udowodniono, że nie są warte zachodu? Jednakże, przerwania są jak wiele innych świetnych rzeczy w informatyce - mają swoje miejsce; jeśli spróbujemy użyć przerwań w niewłaściwy sposób sprawy mogą zrobić się gorsze. Następująca sekcja zgłębia aspekty wydajności zastosowania przerwań. Jak wkrótce odkryjemy, system sterownia przerwaniami jest zazwyczaj lepszy pomimo złożoności. Jednakże, nie zawsze. Dla wielu systemów metody alternatywne dostarczają lepszej wydajności. 17.7.1 UKŁAD WE/WY STEROWANY PRZERWANIAMI KONTRA ODPYTYWANIE

Celem systemu sterowania przerwaniami jest zezwolenie CPU na kontynuowanie przetwarzania instrukcji podczas gdy wystąpi aktywność I/O. Jest to bezpośredni kontrast z systemem odpytywania gdzie CPU nieustannie testuje urządzenia I/O aby zobaczyć czy operacje I/O są zakończone. W systemie sterowania przerwaniami, CPU zajmuje się swoimi sprawami, a przerwaniami urządzeń I/O zajmuje się kiedy wymagają obsługi. Generalnie jest to bardziej wydajne niż marnowanie cykli CPU na odpytywanie urządzeń kiedy nie są gotowe. Port szeregowy jest doskonałym przykładem urządzenia , które działa zupełnie dobrze ze sterowanymi przerwaniami I/O. Możemy uruchomić program komunikacyjny, który zaczyna ściąganie pliku przez modem. Przy każdym nadejściu znaku generuje przerwanie a program komunikacyjny uruchamia się, buforuje znaki a potem wracamy z przerwania. Tymczasem inny program (jak word procesor) może być uruchomiony z prawie znikomym pogorszeniem wydajności ponieważ zabiera tak mało czasu przy przetwarzaniu przerwań portu szeregowego. Scenariusz ten kontrastuje z tym gdzie program do komunikacji szeregowej ciągle odpytuje chip komunikacji szeregowej aby zobaczyć czy przyszedł jakiś znak. W tym przypadku CPU cały czas zajmuje się szukaniem znaku wejściowego pomimo, że rzadko (w terminologii CPU) nadchodzi. Dlatego też żadne cykle CPU nie mogą być użyte do przetwarzania takiego jak uruchomienie word procesora. Przypuśćmy, że przerwania nie są dostępne a chcemy pozwolić na ściąganie w tle podczas używania programu word procesora. Program word procesora będzie musiał testować dane wejściowe portu szeregowego co kilka milisekund zapobiegając utracie jakiejś danej. Czy możemy sobie wyobrazić jak trudno byłoby napisać taki word procesor? System przerwań jest w tym przypadku wyborem oczywistym. Jeśli pobieranie danych podczas przetwarzania tekstu wydaje się zbyt skomplikowane, rozważmy prostszy przypadek – klawiaturę PC. Jeśli wystąpi przerwanie naciśnięcia klawiatury, ISR klawiatury odczyta naciśnięty klawisz i zachowa go w systemowym buforze klawiatury do chwili kiedy aplikacja będzie chciała odczytać daną klawiatury. Czy możemy sobie wyobrazić jak trudno będzie napisać taką aplikację jeśli musimy stale odpytywać port klawiatury zachowując zagubione znaki? Nawet w środku długiego obliczenia? Ponownie przerwania dostarczają łatwego rozwiązania. 17.7.2 CZAS OBSŁUGI PRZERWANIA Oczywiście, właśnie omówiony system komunikacji szeregowej jest przykładem najlepszego scenariusza. Program komunikacyjny pobiera tak mało czasu na wykonanie swojej pracy, że większość czasu pozostaje poza programem przetwarzania tekstu. Jednakże uruchamiając różne systemy I/O sterowane przerwaniami, na przykład kopiowanie jednego dysku do innego, podprogram obsługi przerwań będzie miał zauważalny wpływ na wydajność systemu przetwarzania tekstu. Dwa czynniki sterują wpływem ISR’a na system komputerowy: częstotliwość przerwań i czas obsługi przerwania. Częstotliwość to, jak wiele razy na sekundę ( lub inny pomiar czasu) poszczególnego wystąpienia przerwania. Czas obsługi przerwań to, ile czasu potrzeba ISR’owi na obsługę przerwania. Właściwość częstotliwości różni się w zależności od źródła przerwania. Na przykład, chip zegarowy generuje równomierne przerwania około 18 razy na sekundę, podobnie, port szeregowy odbierający 9600 bps generuje więcej niż 100 przerwań na sekundę. Z drugiej strony, klawiatura rzadko generuje więcej niż około 20 przerwań na sekundę i nie są one bardzo regularne. Czas obsługi przerwania jest oczywiście uzależniony od liczby instrukcji, które musi wykonać ISR. Czas obsługi przerwania jest również zależny od określonego CPU i częstotliwości zegara. Taki sam ISR wykonujący identyczne instrukcje na dwóch CPU, będzie wykonywał się mniej razy na szybszej maszynie. Ilość czasu potrzebna programowi obsługi przerwania do obsłużenia przerwania pomnożona przez częstotliwość przerwania określa wpływ jaki będzie miało przerwanie na wydajność systemu. Pamiętajmy, każdy cykl CPU zużyty przez ISR jest jednym cyklem mniej dostępnym programom użytkowym. Rozważmy przerwanie zegarowe. Przypuśćmy ,że ISR zegarowy potrzebuje 100µs do zakończenia swojego zadania. To znaczy, że przerwanie zegarowe zużywa 1,8 ms co każdą sekundę lub około 0,18% całkowitego czasu komputera. Używając szybszego CPU zredukujemy te procenta (poprzez redukcję czasu zużywanego przez ISR); używając wolniejszego CPU zwiększamy te procenta. Niemniej jednak zauważmy, że taki krótki ISR jak ten nie będzie miał znaczącego wpływu na całkowitą wydajność systemu. Sto mikrosekund to szybko dla typowego ISR’a, zwłaszcza kiedy nasz system ma kilka zegarowych ISR’ów połączonych razem. Jednakże, nawet jeśli ISR zegarowy pobrał 10 razy tyle przy wykonaniu, pozbawiłby tylko system mniej niż 2% dostępnych cykli CPU. Nawet jeśli pobrałby 100 razy więcej (10ms), wystąpiłoby pogorszenie wydajności tylko o 18%; większość ludzi ledwie zauważyłoby takie pogorszenie .

Oczywiście nie można pozwolić aby ISR pobierał tyle czasu ile chce. Ponieważ przerwanie zegarowe występuje co 55 ms, maksymalny jaki może użyć ISR jest poniżej 55 ms. Jeśli ISR wymaga więcej czasu niż jest pomiędzy przerwaniami, system może ostatecznie zgubić przerwanie. Co więcej, system zużyje cały swój czas na obsługę przerwań zamiast zajmować się czymś innym. Wiele systemów mających ISR’y zużywające 10% całkowitych cykli CPU nie dostarcza problemu. Jednakże, zanim przestaniemy go lubić i zaczniemy projektować wolniejszy podprogram obsługi przerwań, powinniśmy zapamiętać, że nasz ISR nie jest prawdopodobnie jedynym ISR’em w systemie. Jeśli nasz ISR zajmuje 25% cykli CPU, może być inny ISR, który robi to samo; i inny, i inny i... Co więcej mogą być ISR’y, które wymagają szybszej obsługi. Na przykład ISR portu szeregowego może musieć odczytać znak z chipu komunikacji szeregowej co każdą milisekundę. Jeśli nasz ISR zegarowy wymaga 4 ms dla wykonania i robi to z wyłączonym przerwaniem, ISR portu szeregowego starci pewne znaki. Ostatecznie oczywiście chcemy napisać ISR’y, które byłyby tak szybkie jak to tylko możliwe, więc muszą mieć mniejszy wpływ na wydajność systemu. Jest to jeden z głównych powodów dla których większość ISR’ów pod DOS jest pisana w języku asemblera. Chyba ,że projektujemy system wbudowany, na którym działa tylko Twoja aplikacja, wtedy musimy zdać sobie sprawę, ż nasze ISR’y muszą koegzystować z innymi ISR’ami i aplikacjami; nie chcemy aby wydajność naszego ISR’a niekorzystnie wpływał na wydajność innego kodu w systemie. 17.7.3 WSTRZYMANIE OBSŁUGI PRZERWANIA Wstrzymanie obsługi przerwania jest to czas pomiędzy punktem w którym urządzenie sygnalizuje ,że potrzebuje obsługi a punktem w którym ISR dostarcza potrzebnej obsługi. Nie jest to natychmiastowe! PIC 8259 musi zasygnalizować CPU, CPU musi przerwać bieżący program, odłożyć flagi i adres powrotu, uzyskać adres ISR i przekazać sterowanie do ISR’a. ISR może wymagać odłożenia różnych rejestrów, ustawienia pewnych zmiennych, sprawdzenia stanu urządzenia dla określenia źródła przerwania i tak dalej. Ponadto mogą być inne ISR’y połączone w wektor przerwań przed naszym i wykonują się one całkowicie przed przekazaniem sterowania do naszego ISR’a, który w rzeczywistości obsługuje to urządzenie. Ostatecznie ISR w rzeczywistości robi to co urządzenie uważa ,ze powinno być zrobione. W najlepszym przypadku na najszybszym mikroprocesorze z prostym ISR’em, opóźnienie może być rzędu mikrosekund. W wolniejszych systemach z kilkoma ISR’ami w łańcuchu, opóźnienie może sięgać kilku milisekund. Dla pewnych urządzeń, wstrzymanie obsługi przerwania jest ważniejsze niż faktyczny czas obsługi. Na przykład, urządzenie wejściowe może przerwać CPU co 10 sekund. Jednakże, to urządzenie może nie potrafić przechować dane na swoim porcie wejściowym przez więcej niż milisekundę. Teoretycznie czas obsługi przerwania mniejszy niż 10 sekund jest dobry; ale CPU musi odczytać dane w przeciągu jednej milisekundy swojego wejścia lub system zgubi dane. Niskie wstrzymanie obsługi przerwania (to znaczy, odpowiadające szybko) jest bardzo ważne w wielu aplikacjach. Istotnie, w pewnych aplikacjach wymagania co do opóźnienia są ścisłe jeśli musimy użyć bardzo szybkiego CPU lub musimy porzucić całkowicie przerwania i powrócić do odpytywania. Momencik! Czy odpytywanie nie jest mniej wydajne niż system sterowany przerwaniami? Jak odpytywanie może coś poprawić? System sterowany przerwaniami I/O poprawia wydajność systemu poprzez zezwolenie CPU na działanie z innymi zadaniami pomiędzy operacjami I/O. W zasadzie obsługiwanie przerwań zabiera bardzo mało czasu CPU w porównaniu do nadchodzących przerwań w systemie. Poprzez zastosowanie I/O sterowanych przerwaniami możemy użyć wszystkich tych innych cykli CPU dla jakichś innych celów. Jednak przypuśćmy, że urządzenie I/O tworzy żądanie obsługi z taką szybkością, że nie ma wolnych cykli CPU. I/O sterowane przerwaniami dostarczy kilku korzyści w takim przypadku. Na przykład przypuśćmy, że mamy ośmiobitowe urządzenie I/O połączone z dwoma portami I/O. Przypuśćmy, że bit zero portu 310h zawiera jeden jeśli dana jest dostępna i zero w przeciwnym wypadku. Jeśli dana jest dostępna, CPU musi odczytać osiem bitów z portu 311h. Odczytanie portu 311h zeruje bit zero portu 310h dopóki nie nadejdzie kolejny bajt. Jeśli chcemy odczytać 8192 bajty z tego portu możemy zrobić to z następującym fragmentem kodu: mov cx, 8192 mov dx, 310h lea bx, Array ;wskazuje bx jako bufor pamięci DataAvailLP: in al., dx ;odczyt statusu portu shr al, 1 ;test bitu zero jnc DataAvailLp ;czekaj dopóki jest dana dostępna inc dx ;wskazuje port danych

in mov inc dec loop -

al., dx [bx], al. bx dx DataAvailLp

;odczyt danych ;przechowanie danych w buforze ;przesunięcie na następny element tablicy ;pokazuje ponownie status portu ;powtarzane 8192 razy

Kod ten używa klasycznej pętli odpytywania (DataAvailLp) do oczekiwania na każdy dostępny znak. Ponieważ są tylko trzy instrukcje w pętli odpytującej, pętla ta może prawdopodobnie wykonywać się mniej niż w mikrosekundę. Więc może on zając jedną mikrosekundę do określenia czy dana jest dostępna, w którym przypadku kod nie dochodzi do skutku i przez sekundę instrukcje w tej sekwencji odczytamy dane z urządzenia. Bądźmy szczerzy i powiedzmy, że pobiera następną mikrosekundę. Przypuśćmy, zamiast tego, że używamy podprogramu obsługi przerwań. Dobrze napisany ISR połączony z dobrze zaprojektowanym systemem sprzętowym będzie prawdopodobnie miał opóźnienia mierzone w mikrosekundach. Mierząc najlepsze opóźnienia możemy mieć nadzieję, że osiągniemy wymaganą część zegara sprzętowego, który zaczyna odliczać, kiedy wystąpi przerwanie. Na wejściu do naszego programu obsługi przerwania możemy odczytać ten licznik do określenia ile czasu minęło pomiędzy przerwaniem a jego obsługą. Na szczęście, takie urządzenie istnieje na PC – chip zegarowy 8254, który dostarcza źródła przerwania 55 ms. Chip zegarowy 8254 w rzeczywistości zawiera trzy oddzielne zegary: zegar #0, zegar #1 i zegar #2. Pierwszy zegar (zegar #0) dostarcza przerwania zegarowego, więc skupimy się na nim w naszym omówieniu.. Zegar zawiera 16 bitowy rejestr, który 8254 zmniejsza w regularnych odstępach (1,193,180 razy na sekundę). Kiedy zegar osiąga zero, generuje przerwanie na lini IRQ 0 8259 a potem przechodzi cyklicznie do 0FFFFh i kontynuuje odliczanie w dół od tego punktu. Ponieważ licznik automatycznie resetuje do 0FFFFh po wygenerowaniu każdego przerwania, to znaczy, że zegar 8254 generuje przerwania co 65,536 / 1,193,180 sekund lub co każde 54,9254932198 ms, czyli 18.2064819336 razy na sekundę. Będziemy wywoływać to co 55 ms lub 18 (lub 18,2) razy na sekundę odpowiednio. Innym sposobem przedstawienia tego jest to ,że 8254 zmniejsza licznik co 838 nanosekund (lub 0,838 µs). Poniższy krótki program asemblerowy odmierza opóźnienie przerwania poprzez poprawianie wektora 8. Kiedykolwiek chip zegarowy odlicza w dół do zera, generuje przerwanie, które bezpośrednio wywołuje ten ISR programu. ISR szybko odczytuje rejestr licznika chipu zegarowego, negując wartość (więc 0FFFFh staje się jeden, 0FFFEh staje się dwa itd.) a potem dodaje do całości. ISR również zwiększa licznik aby można było prześledzić liczbę razy jaką trzeba dodać wartość licznika do wartości całkowitej. Potem ISR skacze do oryginalnego programu obsługi int 8. Program główny, w między czasie, po prostu oblicza i wyświetla bieżący średni odczyt z licznika. Kiedy użytkownik naciska jakiś klawisz, program się kończy. ;Program ten mierzy opóźnienie ISR Int 08. ;Działa przez odczyt chipu zegarowego bezpośrednio na wejściu do ISR INT 08. Przez uśrednienie tej wartości ; dla jakiejś liczby wykonań, możemy określić średnie opóźnienie dla tego kodu. .xlist .386 option include includelib .list cseg

segment:use16 stdlib.a stdlib.lib

segment para public ‘code’ assume cs:cseg, ds: nothing

;Wszystkie zmienne są w segmencie kodu żeby zredukować opóźnienie ISR’a (nie musimy odkładać i ustawiać ; DS., zachowując kilka instrukcji na początku ISR’a) OldInt8

dword ?

SumLatency Executions Average

dword 0 dword 0 dword 0

; Program ten odczytuje chip zegarowy 8254. Ten chip zlicza od 0FFFFh w dół do zera a potem generuje przerwanie. Zawija od 0 do 0FFFFh i kontynuuje odliczanie w dół generując przerwanie ; ; Adres portu chipu zegarowego 8254 : Timer0_8254 Cntrl_8254

equ equ

40h 43h

;Następujący ISR odczytuje chip zegarowy 8254 , neguje wynik (ponieważ zegar odlicza w tył), dodaje wynik ; do zmiennej SumLatency, a potem zwiększa zmienną Executions, która liczy liczbę wykonań tego kodu. ;Tymczasem program główny jest zajęty obliczaniem i wyświetlaniem średniego opóźnienia dla tego ISR’a ; ;Odczytując 16 bitową wartość licznika 8254, kod ten musi zapisać zero do portu sterującego 8254 a potem odczytać ;dwukrotnie port zegarowy (odczytuje najmniej i najbardziej znaczący bajt). Musi być krótkie opóźnienie pomiędzy ; odczytem dwóch bajtów z tego samego adresu portu TimerISR

SettleDelay:

TimerISR Main

proc push mov out in mov jmp in xchg neg add inc pop jmp endp

near ax eax, 0 Cntrl_8254, al. al., Timer0_8254 ah, al. SettleDelay al., Timer0_8254 ah, al ax cseg: SumLatency, eax cseg:Executions ax cseg: oldInt8

;Ch 0, zatrzask i odczyt danych ;Wyprowadzenie do lini poleceń rejestru 8253 ;Odczyt zatrzasku #0 (LSB) & ignoruje ;osadzenie opóźnienia dla chipu 8254 ;Odczytuje zatrzask #0 (MSB)

proc Meminit

; Zaczynamy od poprawienia adresu naszego ISR’a w wektorze int 8. Zauważmy ,że musimy wyłączyć przerwania ;podczas rzeczywistego poprawiania wektora przerwań i musimy założyć, że przerwania są ponownie włączane; ; stąd instrukcje cli i sti. Są wymagane ponieważ przerwania zegarowe mogą nadejść między dwoma instrukcjami ;które zapisują do wektora int 8. Ponieważ wektor przerwań jest w tym punkcie w niespójnym stanie, o może ;powodować krach systemu mov mov mov mov mov mov

ax, 0 es, ax ax, es:[8*4] word ptr OldInt8, ax ax, es:[8*4+2] word ptr OldInt8+2, ax

cli mov mov

word ptr es:[8*4], offset TimerISR es:[8*4+2], cs

sti ;Najpierw czekamy na pierwsze wywołanie powyższego ISR’a. Ponieważ będziemy dzielili przez wartość w ; zmiennej Executions, musimy upewnić się, że jest większa niż zero przed wykonaniem Wait4Non0:

cmp je

cseg: Executions, 0 Wait4Non0

; Okay, zaczynamy wyświetlanie dobrej wartości dopóki użytkownik naciśnie klawisz na klawiaturze do ;zatrzymania wszystkiego: DisplayLp:

mov cdq div mov printf byte dword

eax, SumLAtency

„Count: %ld, average: %ld\n”, 0 Executions, Average

mov int je mov int

ah, 1 16h DisplayLp ah, 0 16h

;rozszerzamy eax -> edx Executions Average, eax

;Test dla naciœniêcia klawisza ;Odczyt tego naciśnięcia klawisza

;Okay, przywracamy wektor przerwań. Musimy tu wyłączyć przerwania z tego samego powodu co powyżej mov mov cli mov mov mov mov sti

ax, 0 es, ax ax, word ptr OldInt8 es:[8*4], ax ax, word ptr OldInt8+2 es:[8*4+2], ax

Quit: Main

ExitPgm endp

cseg

ends

sseg stk sseg

segment para stack ‘ stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

;Makro DOS dla wyjścia z programu

Na procesorze 66 MHz 80486DX/2 powyższy kod raportuje średnią wartość 44 po jego uruchomieniu około 10000 iteracji. Wyniesie to około 37 µs pomiędzy urządzeniem sygnalizującym przerwanie a ISR’em, który może go przetworzyć. Opóźnienie odpytywania I/O byłoby prawdopodobnie mniejszego rozmiaru niż ten!

Generalnie, jeśli mamy jakąś aplikację o dużej szybkości, taką jak nagrywanie audio lub video lub odgrywanie, prawdopodobnie nie możemy pozwolić na opóźnienia związane z przerwaniami I/O. Z drugiej strony taka aplikacja żąda takiej wysokiej wydajności z systemu, której prawdopodobnie nie będziemy mieli cykli CPU potrzebnych do innych procesów podczas oczekiwania na I/O. Inną sprawą w związku z opóźnieniem ISR jest zgodność opóźnienia. To znaczy, czy jest taka sama ilość opóźnienia od przerwania do przerwania? Pewne ISR’y mogą tolerować znaczne opóźnienia tak długo jak jest ono spójne (to znaczy, opóźnienie jest mniej więcej takie samo od przerwania do przerwania). Na przykład przypuśćmy, że chcemy poprawić przerwanie zegarowe aby można było odczytać port wejściowy co 55 ms i przechować dane. Później, kiedy przetwarzamy dane, nasz kod może działać przy założeniu, że dane są odczytywane co 55 ms (lub 54.9). Może to nie być prawdą jeśli są inne ISR’y w łańcuchu przerwania zegarowego przed naszym ISR’em/ Na przykład, może być ISR, który odlicza 18 przerwań a potem wykonuje jakąś sekwencję kodu, która wymaga 10 ms.. To znaczy, że 16 z każdych 18 przerwań naszego programu gromadzenia danych będzie gromadził dane z 55 ms przerwami . Ale kiedy wystąpi osiemnaste przerwanie, inny ISR zegarowy będzie opóźniony o 10 ms przed przekazaniem sterowania do naszego podprogramu. To znaczy, że nasz siedemnasty odczyt to będzie 65 ms od ostatniego odczytu. Nie zapomnijmy, że chip zegara jeszcze odlicza w dół podczas wszystkiego tego, co oznacza, że teraz są tylko 45 ms do następnego przerwania. Dlatego też nasz osiemnasty odczyt wystąpiłby 45 ms po siedemnastym. Ledwie zgodnie ze wzorcem. Jeśli nasz ISR potrzebuje zgodnego opóźnienia, powinniśmy spróbować zainstalować nasz ISR najwcześniej jak to możliwe w łańcuchu przerwań. 17.7.4 PRZERWANIA PRIORYTETOWE Przypuśćmy ,że mamy wyłączone przerwania (być może przetwarzamy jakieś przerwanie) i dwa żądania przerwań przyszłe podczas gdy przerwania są wyłączone. Co się zdarzy kiedy ponownie włączymy przerwania? Które przerwanie najpierw obsłuży CPU? Oczywistą odpowiedzią byłoby „przerwanie które wystąpiło jako pierwsze”. Jednakże, przypuśćmy, że oba wystąpiły w dokładnie tym samym czasie (lub przynajmniej wewnątrz dość krótkiego odcinka czasu, który nie pozwala nam określić, które wystąpiło jako pierwsze), lub być może, jeśli rzeczywiście wystąpi taki przypadek, PIC 8259 nie może wyśledzić , które przerwanie wystąpiło pierwsze? Co więcej, co jeśli jedno z przerwań jest ważniejsze niż drugie? Na przykład przypuśćmy, że jedno przerwanie mówi, że użytkownik właśnie nacisnął klawisz na klawiaturze a drugie przerwanie mówi, że reaktor jądrowy stopi się, jeśli nie zrobimy czegoś w ciągu następnych 100 µs. Czy chcielibyśmy najpierw przetwarzać naciśnięcie klawisza, nawet jeśli nadeszło pierwsze? Prawdopodobnie nie. Zamiast tego chcielibyśmy ustalić priorytety przerwań na podstawie ich ważności; przerwanie z reaktora nuklearnego jest prawdopodobnie trochę ważniejsze niż przerwanie naciśnięcia klawisza i obsłużylibyśmy go jako pierwsze. PIC 8259 dostarcza kilka schematów priorytetów, ale BIOS PC inicjalizuje 8259 do używania stałych priorytetów. Kiedy używamy stałych priorytetów, urządzenie na IRQ 0 (zegar) ma najwyższy priorytet a urządzenie na IRQ 7 ma najniższy priorytet. Dlatego też, 8259 w PC (działający DOS) zawsze rozwiązuje konflikty w ten sposób. Jeśli mamy zamiar podłączyć reaktor nuklearny do PC, prawdopodobnie użyjemy przerwania niemaskowalnego ponieważ ma wyższy priorytet niż inne dostarczone przez 8259 (a nie możemy zamaskować go instrukcją CLI) 17.8 DEBUGGOWANIE ISR’ÓW Chociaż napisanie ISR’ów może uprościć zaprojektowanie wielu typów programów, ISR’y są prawie zawsze bardzo trudne do zdebuggowania. Są dwa główne powody dla których ISR’y są trudniejsze niż zwykłe aplikacje do zdebuggowania. Po pierwsze, jak wspomniano wcześniej, niesforny ISR może modyfikować wartości używane przez program główny (lub jeszcze gorzej, przez jakiś inny program w pamięci) i jest trudno sprecyzować źródło błędu. Po drugie, większość debuggerów protestuje kiedy próbujemy ustawić punkt przerwania wewnątrz ISR. Jeśli nasz kod zawiera jakieś ISR’y a program wydaje się źle zachowywać i nie możemy bezpośrednio zobaczyć powodu, powinniśmy bezpośrednio podejrzewać ingerencję ISR’a. Wielu programistów zapomina o ISR’ach pojawiających się w ich kodzie i spędzają tygodnie próbując zlokalizować błąd w swoich nie – ISR’owych kodach, tylko odkrywają problem jaki był z ISR’em. Zawsze najpierw podejrzewaj ISR. Generalnie, ISR’y są krótkie i możemy szybko wyeliminować ISR jako przyczynę problemu zanim spróbujemy prześledzić błąd. Debuggery często mają problemy ponieważ nie są one współbieżne lub wywołują BIOS lub DOS (które nie są współbieżne) więc jeśli ustawimy punkt przerwania w ISR’ze , który przerywa BIOS lub DOS a debugger

wywołuje BIOS lub DOS, system może nie funkcjonować z powodu problemów współbieżności. Na szczęście większość nowoczesnych debuggerów ma tryb zdalnej korekty, który pozwala nam połączyć terminal lub inny PC do portu szeregowego i wykonać polecenia debuggujące na drugim monitorze i klawiaturze. Ponieważ debugger porozumiewa się bezpośrednio z chipem szeregowym, unikajmy wywoływania BIOS lub DOS i unikajmy problemu współbieżności. Oczywiście, nie pomoże wiele jeśli napiszemy ISR szeregowy, ale działa dobrze z większością innych programów. Dużym problemem kiedy debuggujemy podprogramy obsługi przerwań jest to ,że nastąpi awaria systemu bezpośrednio po tym jak poprawimy wektor przerwań. Jeśli nie mamy udogodnienia zdalnej kontroli najlepszym podejściem do debuggowania tego kodu jest rozłożenie ISR na jego podstawowe elementy. Może to być kod, który po prostu przekazuje sterowanie do kolejnego ISR’a w łańcuchu przerwań (w stosownych przypadkach) Potem dodaje jedną sekcję kodu po kolei do naszego ISR’a dopóki ISR nie zawiedzie. Oczywiście, najlepszą strategią debuggowania podprogramów obsługi przerwań jest napisanie kodu, który nie ma błędów. Jednak nie jest to praktyczne rozwiązanie, jedyną rzeczą jaką możemy zrobić to próbować zrobić to jak najbardziej jest to możliwe w ISR’ze. Im mniejszy ISR tym mniej złożony i wyższe prawdopodobieństwo, że nie będzie zawierał żadnego błędu. Debuggowanie ISR’ów, niestety, nie jest łatwe a nie jest to coś czego nie możemy nauczyć się z książki. Potrzeba wiele doświadczeń i popełnienia wielu błędów. Niestety tak jest , ale nie ma niczego zastępczego dla doświadczenia kiedy debuggujemy ISR’y. 17.9 PODSUMOWANIE Rozdział ten omawia trzy zjawiska występujące w systemie PC: przerwania (sprzęt), przerwania kontrolowane i wyjątki. Przerwanie jest to procedura asynchroniczna wywołująca generowanie CPU w odpowiedzi na zewnętrzne sygnał sprzętowy. Przerwanie kontrolowane jest wywołaniem programowym i jest specjalną formą procedury wywołania. Wyjątki występują kiedy program wykonuje się a instrukcja generuje jakiś rodzaj błędu. Po dodatkowe szczegóły zajrzyj *”Przerwania, Przerwania kontrolowane i wyjątki” Kiedy wystąpi przerwanie, przerwanie kontrolowane lub wyjątek, CPU 80x86 odkłada flagi i przekazuje sterowanie do podprogramu obsługi przerwań (ISR). 80x86 wspiera tablicę wektorów przerwań, która dostarcza adresów segmentowych dla 256 różnych przerwań. Kiedy piszemy swój własny ISR, musimy przechować adres naszego ISR’a we właściwej lokacji w tablicy wektorów przerwań do uruchomienia tego ISR’a. Dobrze wychowany program również zachowuje wartość oryginalnego wektora przerwań aby można było przywrócić ją kiedy będzie koniec. Po szczegóły zajrzyj: *”Struktura przerwań 80x86 i podprogramy obsługi przerwań (ISR)” Przerwanie kontrolowane lub przerwanie programowe jest niczym więcej jak wykonywanie instrukcji 80x86 „int n”. Takie instrukcje przekazują sterowanie do ISR’a, którego wektor pojawia się w n-tym wejściu w tablicy wektorów przerwań. Generalnie będziemy używali przerwań kontrolowanych do wywoływania podprogramów w programach rezydentnych pojawiających się gdzieś w pamięci (podobnie jak DOS lub BIOS) *”Przerwania kontrolowane” Wyjątek wystąpi jeśli tylko CPU wykonuje instrukcje a instrukcja ta jest niedozwolona lub wykonanie tej instrukcji generuje jakiś rodzaj błędu (jak dzielenie przez zero). 80x86 dostarcza kilku wbudowanych wyjątków, chociaż ten tekst tylko operuje wyjątkami dostępnymi w trybie rzeczywistym. *”Wyjątki” *”Wyjątek błędu dzielenia (INT 0)” *”Wyjątek pojedynczego kroku (śledzenia) (INT 1) *”Wyjątek punktu zatrzymania (INT 3)” *”Wyjątek przepełnienia (INT 4/INTO)” *”Wyjątek graniczny (INT 5/BOUND”)

*”Wyjątek niepoprawnego opcodu (INT 6)” *”Koprocesor nie dostępny (INT 7)” PC dostarcza sprzętowego wsparcia dla 15 przerwań wektorowych używając pary chipów programowalnych sterowników przerwań (PIC) 8259A. Urządzenia, które zwykle generują przerwania sprzętowe wliczając w to zegar, klawiaturę, porty szeregowe, porty równoległe, urządzenia dyskowe, karty dźwiękowe, zegar czasu rzeczywistego i FPU. 80x86 pozwala nam włączyć lub zablokować wszystkie przerwania maskowalne instrukcjami cli i sti . PIC pozwala również maskować indywidualnie urządzenia, które mogą przerywać system. Jednak, 80x86 dostarcza specjalnych przerwań niemaskowalnych, które mają wyższy priorytet niż pozostałe przerwania sprzętowe i nie może być zablokowane przez program. *”Przerwania sprzętowe” *”Programowalny sterownik przerwań (PIC) 8259A” *”Przerwanie zegarowe (INT 8)” *”Przerwanie klawiatury (INT 9)” *”Przerwanie portu szeregowego (INT 0Bh i INT 0Ch)” *”Przerwanie portu równoległego (INT 0Dh i INT 0Fh”) *”Przerwanie dyskietki i dysku twardego (INT 0Eh i INT 76h)” *”Przerwanie zegara czasu rzeczywistego (INT 70h)” *”Przerwanie FPU (INT 70h)” *”Przerwania niemaskowalne (INT 2)” *”Inne przerwania” Podprogramy obsługi przerwań które napiszemy mogą koegzystować z innymi ISR’ami w pamięci. W szczególności nie będziemy mogli po prostu zamienić wektora przerwań adresem naszego ISR’a i pozwolić działać ISR’owi stamtąd. Często, będziemy musieli stworzyć łańcuch przerwania i wywołać poprzedni ISR w łańcuchu przerwania, kiedy wykonujemy przetwarzanie przerwania. Aby zobaczyć dlaczego tworzymy łańcuch przerwania i nauczyć się jak je tworzyć zobacz *”Podprogramy obsługi przerwań wiązań łańcuchowych” Wraz z przerwaniami nadchodzi możliwość współbieżności, to znaczy taka możliwość, że podprogram może być przerwany i wywołany ponownie przed pierwszym końcowym wykonaniem . Rozdział ten wprowadził koncepcję współbieżności i daje kilka przykładów, które demonstrują problemy z kodem niewspółbieżnym *”Problemy współbieżności” Głównym celem systemu sterowanego przerwaniami jest poprawienie wydajności tego systemu. Dlatego też, nie powinno nas zaskoczyć, że ISR’y powinny być tak wydajne jak tylko to możliwe. Rozdział ten omawia dlaczego system I/O sterowany przerwaniami może być bardziej wydajny i porównuje I/O sterowane przerwaniami z odpytywaniem I/O. Jednakże przerwania mogą powodować problemy jeśli odpowiedni ISR jest zbyt wolny. Dlatego programiści którzy piszą ISR’y muszą być świadomi takich parametrów jak czas obsługi przerwania, częstotliwość przerwania i opóźnienie przerwania. *”Sprawność systemu sterowania przerwaniami” *”Układ We/Wy sterowany przerwaniami kontra odpytywanie” *”Czas obsługi przerwania” *”Opóźnienie przerwania” Jeśli wielokrotne przerwania występują równocześnie, CPU musi zdecydować które przerwanie obsłużyć najpierw. PIC 8259 i PC używają schematu przerwań priorytetowych , który przydziela najwyższy priorytet dla zegara.80x86 zawsze przetwarza przerwanie , najpierw z najwyższym priorytetem. *”Przerwania priorytetowe”

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK Konsultacje naukowe: NEKRO [email protected]

[email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ OSIEMNASTY: PROGRAMY REZYDENTNE Większość aplikacji MS-DOS jest nierezydentnych. Ładują się one do pamięci, wykonują, kończą a DOS używa zaalokowanej pamięci aplikacji dla kolejnego programu wykonywanego przez użytkownika. Programy rezydentne stosują te same zasady , z wyjątkiem tej ostatniej. Program rezydentny, po zakończeniu, nie zwraca całej pamięci doz powrotem do DOS’a. Zamiast tego część programu pozostaje rezydentna, gotów do reaktywacji przez jakiś inny program w przyszłym czasie. Programy rezydentne ,również znane jako programy „zakończ i zostań” lub TSR, malutkiej ilości wielozadaniowości w jedno zadaniowym systemie operacyjnym. Dopóki Microsoft Windows będzie popularny, programy rezydentne będą najpopularniejszym sposobem zezwalania pozostawania aplikacjom wielodostępnym na koegzystencję w pamięci w jednym czasie. Chociaż Windows zmniejszył zapotrzebowanie na TSR’y do przetwarzania w tle, TSR’y są jeszcze doceniane przy pisaniu sterowników, narzędzi antywirusowych i łat programów. Rozdział ten omawia kwestie jakie wypada znać kiedy piszemy programy rezydentne 18.1 UŻYWANIE PAMIĘCI PRZEZ DOS I TSR’Y Kiedy po raz pierwszy inicjujemy DOS’a, rozmieszczenia komórek pamięci wygląda następująco: Obszar Wysokiej Pamięci(HMA) i Blok Pamięci Wyższej (UMB)

0FFFFFh

Przestrzeń pamięci Video, ROM i Kontrolera

0BFFFFh (640K) Pamięć dostępna dla aplikacji użytkowych

Wskaźnik Wolnej Pamięci

00000h

Wektory przerwań, zmienne BIOS, zmienne DOS o najniższa część pamięci DOS

Mapa Pamięci DOS (brak aktywnej aplikacji) DOS utrzymuje wskaźnik wolnej pamięci, który wskazuje początek bloku wolnej pamięci. Kiedy użytkownik uruchomi jakiś program użytkowy, DOS ładuje tą aplikację poczynając od adresu, który zawiera wskaźnik wolnej pamięci. Ponieważ DOS generalnie uruchamia tylko jedną aplikację w czasie, cała pamięć ze wskaźnika wolnej pamięci , do końca RAM (0BFFFFh) jest dostępna dla aplikacji:

0FFFFFh 0BFFFFh (640K)

Wskaźnik Wolnej Pamięci Pamięć używana przez aplikację

00000h Mapa Pamięci DOS (uruchomiona aplikacja) Kiedy program kończy się normalnie poprzez funkcję DOS’a 4Ch (makro Biblioteki Standardowej exitpgm), MS-DOS żąda zwrotu pamięci używanej przez aplikację i resetuje wskaźnik wolnej pamięci do poziomu niskiej pamięci DOS. MS-DOS dostarcza drugiego wywołania zakończenia, który jest identyczny z wywołaniem zakończenia z jednym wyjątkiem, nie resetuje wskaźnika wolnej pamięci odbierając całą pamięć używaną przez aplikację. Zamiast tego ten TSR wywołuje zwolnienie określonego bloku pamięci. Wywołanie TSR’a (ah=31h) wymaga dwóch parametrów, kod procesu zakończenia w rejestrze al. (zazwyczaj zero) a dx musi zawierać rozmiar bloku pamięci do ochrony, w paragrafach. Kiedy DOS wykonuje ten kod, modyfikuje wskaźnik wolnej pamięci żeby wskazywał na lokację dx*16 bajtów powyżej PSP programu. To pozostawi pamięć jak pokazano: 0FFFFFh 0BFFFFh(640K)

Wskaźnik wolnej pamięci Pamięć używana przez aplikację rezydentną

00000h

Mapa Pamięci DOS (z aplikacją rezydentną) Kiedy użytkownik wykonuje nową aplikację, DOS ładuje ją do pamięci pod nowy adres wskaźnika wolnej pamięci 0FFFFFh Wskaźnik wolnej pamięci

0BFFFFh(640K) Pamięć używany przez normalną aplikację

Pamięć używana przez aplikację rezydentną

00000h

Mapa Pamięci DOS ( z aplikacją rezydentną i normalną)

Kiedy ta zwykłą aplikacja się zakończy, DOS odbierze jej pamięć i ustawia wskaźnik wolnej pamięci na jej lokacji przed uruchomieniem aplikacji – powyżej programu rezydentnego. Poprzez zastosowanie schematu wskaźnika wolnej pamięci, DOS może chronić pamięć używaną przez program rezydentny. Sztuczką z użyciem wywołani TSR’a jest obliczanie jak wiele paragrafów powinno pozostać rezydentnymi. Większość TSR’ów zawiera dwie sekcje kodu: część rezydentną i część nie rezydentną. Część rezydentna jest danym, głównym programem i wspiera podprogramy które wykonują się kiedy uruchamiamy program z linii poleceń. Kod ten prawdopodobnie nigdy nie wykona się ponownie. Dlatego też, nie powinniśmy zostawiać go w pamięci kiedy kończy się nasz program. W końcu każdy bajt zużyty przez program TSR jest jednym bajtem mniej dostępnym dla innych aplikacji. Część rezydentna programu jest kodem, który pozostaje w pamięci i dostarcza jakichś funkcji koniecznych TSR’owi. Ponieważ PSP jest zazwyczaj przed pierwszym bajtem kodu programu, skuteczne użycie wywołania przez DOS TSR’a, nasz program musi być zorganizowane jak następuje: Adres górny

SSEG,ZZZZZZSEG, itd. Kod nierezydentny

Kod rezydentny i dane Adres dolny

PSP

Organizacja pamięci dla programu rezydentnego Aby skutecznie używać TSR’a musimy zorganizować nasz kod i dane tak aby część kodu rezydentnego załadować pod dolny adres pamięci a część nie rezydentną załadować pod górny adres pamięci. MASM i Microsoft Linker dostarczają udogodnień, które pozwalają nam sterować porządkiem ładowania segmentów wewnątrz naszego kodu. Prostym rozwiązaniem, jednak, jest włożenie wszystkich naszych kodów rezydentnych i danych do pojedynczego segmentu i upewnić się, że ten segment pojawi się najpierw w każdym module źródłowym programu. W szczególności, jeśli używamy pliku SHELL.ASM z Biblioteki Standardowej UCR, musimy upewnić się, że zdefiniowaliśmy nasz rezydentny segment przed zawarciem dyrektyw dla plików biblioteki standardowej. W przeciwnym razie MS-DOS załaduje wszystkie podprogramy biblioteki standardowej przed segmentem rezydentnym co spowoduje zmarnowanie znacznej ilości pamięci. Zauważmy, że musimy tylko zdefiniować najpierw nasz segment rezydentny, nie musimy umieszczać wszystkich kodów rezydentnych i danych przed dołączeniami. Poniższe pracuje dobrze: ResidentSeg ResidentSeg

segment para public ‘rsiedent’ ends

EndResident EndResident

segment para public ‘EndRes’ ends .xlist include inlucdelib .list

stdlib.a stdlib.lib

ResidentSeg

segment para public ‘resident’ assume cs: ResidentSeg, ds.:ResidentSeg

PSP

word

?

; Wkładamy kod rezydentny i dane tu ResidentSeg

ends

dseg

segment para public ‘data’

,ta zmienna musi być tutaj

cseg

segment para public ‘code’ asume cs:cseg, ds:dseg

; Wkładamy tu kod nierezydentny cseg

ends itd.

Cel segmentu EndResident stanie się jasny za chwilę. Po więcej informacji na temat porządku pamięci DOS zajrzyj do Rozdziału Szóstego. Teraz jedynym problemem jest wyliczenie rozmiaru kodu rezydentnego, w paragrafach. Z konstrukcji naszego kodu w sposób pokazany powyżej, określenie rozmiar programu rezydentnego jest całkiem łatwe, używając następujących instrukcji kończących część nierezydentny naszego kodu (w cseg): mov mov mov int mov

ax, ResidentSeg es, ax ah, 62h 21h es:PSP, bx

;potrzebny dostęp do ResidentSeg ;wywołanie przez DOS PSP ;zachowuje wartosć PSP w zmiennej PSP

; Następujący kod oblicza rozmiar części rezydentnej kodu. Segment EndResident jest pierwszym segmentem ; w pamięci po kodzie rezydentnym. Wartość PSP programu jest adresem segmentu na początku bloku ;rezydentnego, Przez obliczenie EndResident – PSP obliczymy rozmiar części rezydentnej w paragrafach mov sub

dx, Endresident dx, bx

;pobranie adresu segmentu EndResident ;odjęcie PSP

;Okay, wykonujemy wywołanie TSR, zabezpieczamy tylko kod rezydentny mov int

ax, 3100h 21h

;AH=31h (TSR), AL=0 (kod powrotu)

Wykonywanie powyższego kodu zwraca sterowanie do MS-DOS, zachowując nasz kod rezydentny w pamięci. Jest jeden szczegół końcowego zarządzania pamięcią do rozważenia przed przejściem do innych tematów związanych z programami rezydentnymi – dostęp do danych wewnątrz programu rezydentnego. Procedury wewnątrz programu rezydentnego stają się aktywne w odpowiedzi na bezpośrednie wywołanie z jakiegoś innego programu lub przerwania sprzętowego (zobacz następną sekcję) Na wejściu, podprogram rezydentny może wyszczególnić, że pewne rejestry zawierają różne parametry, ale jednej rzeczy jakiej nie możemy oczekiwać od kodu wywołującego to właściwego ustawienia rejestrów segmentowych dla nas. Faktycznie, jedyny rejestr segmentowy jaki będzie zawierał znaczącą wartość (dla kodu rezydentnego) jest kod rejestru segmentowego. Ponieważ wiele funkcji rezydentnych będzie chciało uzyskać dostęp do danych lokalnych, to znaczy, że te funkcje mogą musieć ustawić ds. lub jakiś inny rejestr(y) na wejściu początkowym. Na przykład przypuśćmy, że mamy funkcję, licznik, który po prostu zlicza liczbę razy jaką jakiś inny kod wywołał ją ponieważ jest rezydentny. Jedyną rzeczą jaką zawierałoby ciało tej funkcji to pojedyncza instrukcja inc counter. Niestety taka instrukcja zwiększałaby zmienną przy offsecie counter w aktualnym segmencie danych( to jest segmencie wskazywanym przez rejestr ds.). Jest mało prawdopodobne, że ds. wskazywałby segment danych związany z procedurą zliczania. Dlatego też będziemy zwiększać jakieś słowo w innym segmencie (prawdopodobnie w segmencie danych kodu wywołującego). Może to dać katastrofalny wynik. Są dwa rozwiązania tego problemu. Pierwszym jest włożenie wszystkich zmiennych w segmencie kodu (bardzo popularna praktyka w sekcji kodu rezydentnego) i użycie przedrostka przesłonięcia segmentu cs: we wszystkich zmiennych. Na przykład, do zwiększenia zmiennej counter możemy użyć instrukcji inc cs:counter. Ta technika działa dobrze jeśli jest tylko kilka odniesień do zmiennych w naszej procedurze. Jednak cierpi ona na kilka poważnych wad. Po pierwsze przedrostek przesłonięcia segmentu czyni nasze instrukcje większymi i wolniejszymi; jest to poważny problem jeśli uzyskujemy dostęp do wielu różnych zmiennych w całym kodzie rezydentnym. Po drugie, łatwo jest zapomnieć umieścić przedrostek przesłonięcia segmentu przed zmienną, w skutek czego powodujemy, że funkcja TSR zniszczy pamięć w segmencie danych kodu wywołującego. Innym rozwiązaniem problemu segmentu jest zmiana wartości w rejestrze ds. na wejściu do procedury rezydentnej i przywrócić ją na wyjściu. Następujący kod demonstruje jak to zrobić:

push push pop inc pop

ds. cs ds. counter ds.

;zachowanie oryginalnej wartości DS. ;skopiowanie wartości CS do DS. ;zwiększenie wartości zmiennej ;przywrócenie oryginalnej wartości DS.

Oczywiście, używając przedrostka przesłonięcia segmentu cs: jest tu dużo bardziej sensownym rozwiązaniem . Jednakże kod będzie rozleglejszy a dostęp do wielu zmiennych lokalnych, ładowanie ds. z cs (zakładając, że wkładamy nasze zmienne do segmentu rezydentnego) byłby bardziej wydajne. 18.2 TSR’Y AKTYWNE KONTRA BIERNE Microsoft identyfikuje dwa typy podprogramów TSR: aktywne i bierne. Bierny TSR jest TSR’em, który aktywuje się w odpowiedzi na wyraźne wywołanie z wykonywanego programu. Aktywny TSR jest TSR’em który odpowiada na przerwanie sprzętowe lub wywołanie przerwania sprzętowego. TSR’y są prawie zawsze podprogramami obsługi przerwań (zobacz „Struktura przerwań 80x86 i Podprogramy obsługi przerwań (ISR)”). Aktywne TSR’y są typowymi podprogramami obsługi przerwań sprzętowych a TSR’y bierne są generalnie programami obsługującymi przerwania kontrolowane (zobacz „Przerwania kontrolowane”). Chociaż, w teorii, jest możliwe dla TSR, określenie adresu podprogramu w biernym TSR’ze i bezpośrednie wywołanie tego podprogramu, mechanizm przerwań kontrolowanych 80x86 jest doskonałym urządzeniem dla wywoływania takich podprogramów, więc wiele TSR’ów używa go. TSR’y bierne generalnie dostarczają wywoływalnej biblioteki podprogramu lub rozszerzenia jakiegoś wywołania DOS lub BIOS. Na przykład, możemy chcieć ponownie wyznaczyć trasę wszystkich znaków wysyłanych do drukarki z pliku. Przez aktualizację wektora 17h (zobacz „Porty równoległe PC”) możemy przechwycić wszystkie znaki przeznaczone dla drukarki. Lub możemy dodać dodatkowy sposób działania do podprogramu BIOS przez zmianę w jego wektorze przerwań. Na przykład możemy dodać nową funkcję wywołującą do podprogramu obsługi video BIOS 10h (zobacz „MS-DOS, PC_BIOS i I/O plików”) poprzez wyszukanie specjalnych wartości w ah i przekazanie wszystkich innych wywołań int 10h do oryginalnego programu obsługi. Innym zastosowaniem biernego TSR’a jest dostarczenie nowego rodzaju zbioru usług w całym nowym wektorze przerwań, których nie może już dostarczyć BIOS. Obsługa myszki dostarczana przez sterownik mouse.com jest dobrym przykładem takiego TSR’a. Aktywne TSR’y generalnie służą jednej z dwóch funkcji. Albo obsługują bezpośrednio przerwania sprzętowe albo nakładają się na przerwania sprzętowe żeby można było aktywować je w podstawowym okresie bez jawnego wywołania z aplikacji. Programy pop-up są dobrym przykładem aktywnego TSR’a. Program popup sam podłącza się pod przerwanie klawiatury PC (int 9). Naciskając klawisz aktywujemy taki program. Pogram może czytać z portu klawiatury PC (zobacz „Klawiatura PC”) aby zobaczyć czy użytkownik wcisnął specjalną sekwencję klawiszy. Jeśli pojawiłaby się taka sekwencja klawiszy, aplikacja może zachować część pamięci ekranu i „pop-up” na ekran, wykonując jakieś funkcje żądane przez użytkownika a potem przywrócić ekran kiedy zostanie to zrobione. Program Sidekick™ firmy Borland jest przykładem niezmiernie popularnego programu TSR , chociaż istnieje wiele innych. Nie wszystkie aktywne TSR’y są jednak pop-up. Dobrym przykładem aktywnego TSR’a są pewne wirusy. Aktualizują one różne wektory przerwań, które aktywują je automatycznie aby mogły wykonać swoje nikczemne dzieło. Na szczęście, niektóre programy anty wirusowe są również dobrymi przykładami aktywnego TSR’a, aktualizują te same wektory przerwań i wykrywają aktywne wirusy próbując ograniczyć szkody jakie może wyrządzić wirus Zauważmy, że TSR może zawierać oba składniki, aktywny i bierny,. To znaczy, mogą być podprogramy, które wywołują przerwania sprzętowe i inne, które aplikacja wywołuje jawnie. Jednakże jeśli podprogram jest aktywny w programie rezydentnym, będziemy twierdzić , że cały TSR jest aktywny. Poniższy program jest krótkim przykładem TSR’a, który dostarcza podprogramów aktywnych i biernych. Program ten aktualizuje wektory przerwań int 9 (przerwanie klawiatury) i int 16 (przerwania kontrolowane klawiatury) . Co jakiś czas system generuje przerwanie klawiaturowe , aktywuje podprogram (int 9) zwiększając licznik. Ponieważ klawiatura zazwyczaj generuje dwa przerwania klawiaturowe przy naciśnięciu klawisza, dzieląc tą wartość przez dwa tworzymy przybliżoną liczbę klawiszy wciśniętych podczas startu TSR’a. Podprogram bierny, związany z wektorem int 16h ,zwraca liczbę naciśnięć klawisza do programu wywołującego. Poniższy kod dostarcza dwóch programów, TSR’a i krótkiej aplikacji wyświetlającej liczbę naciśnięć klawisza od startu TSR’a. ; To jest przykład aktywnego TSR’a, który zlicza aktywowane przerwania klawiaturowe ;Zdefiniowany segment rezydentny musi przyjść przed wszystkim innym

ResidentSeg ResidentSeg

segment ends

para public ‘Resident’

EndResidentSeg segment EndResidentSeg ends

para public ‘EndRes’

.xlist include includelib .list

stdlib.a stdlib.lib

; Segment rezydentny, który przechowuje kod TSR’a: ResidentSeg

segment assume

para public ‘Resident’ cs:ResidentSeg, ds:nothing

; następująca zmienna zlicza liczbę przerwań klawiaturowych KeyIntCnt

word

0

;Te dwie zmienne zawierają oryginalne wartości wektora przerwań INT 9 i INT 16 OldInt9 OldInt16

dword ? dword ?

;MyInt9 ; ;

System wywołuje ten podprogram zawsze kiedy wystąpi przerwanie klawiaturowe. Podprogram ten zwiększa zmienną KeyIntCnt a potem przekazuje sterowanie do oryginalnego programu obsługi Int 9

MyInt9 MyInt9

proc inc jmp endp

;MzInt16 + ; ; ; ;

Jest to bierny składnik tego TSR’a. Aplikacja jawnie wywołuje ten podprogram instrukcją INT 16h. Jeśli AH zawiera 0FFh, podprogram ten zwraca liczbę przerwań klawiaturowych w rejestrze AX. Jeśli AH zawiera jakąś inną wartość podprogram ten przekazuje sterownie do oryginalnego podprogramu obsługi INT 16 (przerwania kontrolowane klawiatury)

MyInt16

proc cmp je jmp

far ResidentSeg:KeyIntCnt ResidentSeg:OldInt9

far ah, 0FFh ReturnCnt ResidentSeg : OldInt16

; Jeśli AH = 0FFh, zwraca liczbę przerwań klawiaturowych ReturnCnt: MyInt16

mov iret endp

ResidentSeg

ends

cseg

segment assume

Main

proc Meminit Mov

ax, ResidentSeg : KeyIntCnt

para public ‘code’ cs:cseg, ds:ResidentSeg

ax, ResidentSeg

;wywołanie oryginalnego podprogramu

mov mov mov

ds, ax ax, 0 es, ax

print byte byte

“Keybord inetrrupt counter TSR program”, cr, lf “Installing.....”, cr, lf,0

;Aktualizacja wektorów przerwań INT 9 i INT 16. Zauważmy ,że powyższe instrukcje uczynią ResidentSeg ; aktualnym segmentem danych, więc możemy przechować stare wartości INT 9 i INT 16 bezpośrednio w ; zmiennych OldInt9 i OldInt16 cli mov mov mov mov mov mov

;wyłączenie przerwań! ax, es:[9*4] word ptr OldInt9, ax ax, es:[9*4+2] word ptr OldInt9+2, ax es:[9*4], offset MyInt9 es:[9*4+2], seg ResidentSeg

mov mov mov mov mov mov sti

ax, es:[16h*4] word ptr OldInt16, ax ax, es:[16h*4+2] word ptr OldInt16+2, ax es:[16h*4], offset MyInt16 es:[16h*4+2], seg ResidentSeg ;OK, włączamy ponownie przerwania

;Połączyliśmy, teraz jedyną rzeczą do zrobienia jest zakończenie i pozostanie rezydentnym print byte

„Installed.”, cr, lf,0

mov int

ah, 62h 21h

;pobranie wartości PSP tego programu

Main cseg

mov dx, EndResident ;obliczenie rozmiaru programu sub dx, bx mov ax, 3100h int 21h endp ends

sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

;polecenie TSR DOS’a

Tu jest aplikacja, która wywołuje MyInt16 drukującą liczbę naciśnięć klawisza: ;jest to program towarzyszący TSR’owi keycnt. Program wywołuje podprogram „MyInt16” w TSR’ze ; określając liczbę przerwań klawiaturowych. Wyświetla przybliżoną liczbę uderzeń w klawisze ; (przerwania klawiaturowe / 2) i wychodzi) .xlist

include includelib .list

stdlib.a stdlib.lib

cseg

segemnt assume

para public ‘code’ cs:cseg, ds:nothing

Main

Main cseg

proc meminit print byte mov int 16h shr ax, 1 putu putcr ExitPgm endp ends

sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBaytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

“Approximate number of keys pressed: “,0 ah, 0FFh ;musi być dzielone przez dwa

18.3 WSPÓŁBIEŻNOŚĆ Jednym dużym problemem z aktywnymi TSR’ami jest to, że ich wywołanie jest asynchroniczne. Mogą się aktywować przy dotknięciu klawiatury, przerwaniu zegarowym lub przez przybyły znak do portu szeregowego. Ponieważ aktywują się one przy przerwaniach sprzętowych, PC może akurat wykonywać jakiś kod kiedy nadciągnie przerwanie. O nie jest problem, chyba ,że TSR sam zdecyduje wywołać jakiś obcy kod, na przykład podprogram DOS’a lub BIOS’a lub jakiś inny TSR. Na przykład głównym programem może być wywołanie DOS’a kiedy przerwanie zegarowe Aktywuje TSR, przerwanie wywołujące DOS , podczas gdy CPU jeszcze wykonuje kod wewnątrz DOS’a. Jeśli TSR próbuje wywołać DOS w tym miejscu, nastąpi powrót do DOS’a. Oczywiście, DOS nie jest współbieżny, wić stworzy to wiele różnych problemów (zazwyczaj zawieszenie systemu). Kiedy piszemy aktywnego TSR’a , który wywołuje inne podprogramy poza tymi dostarczonymi bezpośrednio w TSR’ze, musimy zwrócić uwagę na możliwy problem współbieżności . Zauważmy, że bierne TSR’y nigdy nie cierpiały z tego powodu. Faktycznie, każdy podprogram TSR wywoływany biernie, będzie wykonywane w środowisku kodu wywołującego chyba, ze jakiś inny sprzętowy ISR lub aktywny TSR wykona wywołanie naszego podprogramu, wtedy nie musimy martwić się o współbieżność biernego podprogramu. Jednakże współbieżność jest kwestią aktywnego TSR’a i podprogramu biernego, który wywołuje aktywny TSR. 18.3.1 PROBLM WSPÓŁBIEŻNOŚCI Z DOS DOS jest prawdopodobnie najdotkliwszym punktem projektantów TSR.DOS nie jest współbieżny, zawiera wiele usług jakich może użyć TSR. Uświadomiwszy to sobie , Microsoft dodał pewne wsparcie dla DOS pozwalające TSR’om sprawdzać czy DOS jest aktualnie aktywny. W końcu współbieżność jest problemem tylko jeśli wywołujemy DOS podczas gdy jest już aktywny. Jeśli nie jest aktywny, możemy wywołać z TSR’a bez żadnych ubocznych skutków. MS-DOS dostarcza specjalnej jednobajtowej flagi (InDOS), która zawiera zero jeśli DOS jest aktualnie aktywny i wartość niezerową jeśli DOS przetwarza żądanie aplikacji. Poprzez testowanie flagi InDOS nasz TSR może określić czy może bezpiecznie dokonać wywołania DOS’a. Jeśli ta flaga to zero, możemy zawsze wywołać DOS. Jeśli flaga ta zawiera jeden nie będziemy zdolni do wywołania DOS’a .MS-DOS dostarcza funkcji wywołującej Get InDOS Flag Addess, która zwraca adres flagi InDOS. Używając tej funkcji ładujemy ah

wartością 34h i wywołujemy DOS. DOS zwróci adres flagi InDOS w es:bx. Jeśli zachowamy ten adres, nasz program rezydentny będzie mógł przetestować lagę InDOS aby sprawdzić czy DOS jest aktywny. W rzeczywistości są dwie flagi, które powinniśmy przetestować, flaga InDOS i flaga błędu krytycznego (critter). Obie te flagi powinny zawierać zero przed wywołaniem DOS’a z TSR’a. W DOS’ie w wersji 3.1 i późniejszych, flaga błędu krytycznego pojawia się w bajcie tuż przed flagą InDOS. Więc co powinniśmy zrobić jeśli te dwie flagi nie są zerami? Dosyć łatwo jest powiedzieć „hej, wrócimy do tego później , kiedy MS-DOS wróci do programu użytkownika” Ale jak to zrobić? Na przykład ,jeśli przerwanie klawiatury aktywuje nasz TSR i przekazujemy sterowanie do rzeczywistego podprogramu obsługi klawiatury ponieważ DOS jest zajęty, nie możemy oczekiwać, że nasz TSR magicznie się zrestartuje później, kiedy DOS nie będzie dłużej aktywny. Sztuczką jest aktualizacja naszego TSR w przerwaniu zegarowym tak jak i przerwaniu klawiaturowym. Kiedy przerwanie naciśnięcia klawisza wzbudzi nasz TSR i odkryjemy ,że DOS jest zajęty, ISR klawiatury może po prostu ustawić flagę, która powie mu ,żeby spróbował później; wtedy przekazuje sterowanie do oryginalnego programu obsługi klawiatury. Tymczasem ISR zegara, jaki napisaliśmy ,stale sprawdza tą flagę jaką stworzyliśmy. Jeśli flaga jest wyzerowana, po prostu przekazujemy sterowanie do oryginalnego podprogramu obsługi przerwania zegarowego, jeśli flaga jest ustawiona, wtedy kod sprawdza flagi InDOS i CritErr. Jeśli mówią, że DOS jest zajęty, ISR zegarowy przekazuje sterowanie do oryginalnego programu obsługi . Wkrótce po zakończeniu DOS’a kiedykolwiek się to zdarzy, przerwanie zegarowe nadejdzie i wykryje, że DOS nie jest dłużej aktywny. Teraz nasz ISR może przejąć i zrobić konieczne wywołania DOS’a. Oczywiście, ponieważ nasz kod zegarowy określa, że DOS nie jest zajęty, powinien wyzerować flagę „ Ja chcę obsłużyć” aby przyszłe przerwanie zegarowe nie nieumyślnie zrestartowały TSR . Jest tylko jeden problem z tym podejściem. Są pewne wywołania DOS , które mogą pobierać nieskończoną ilość czasu do wykonania. Na przykład, jeśli wywołujemy DOS do odczytu klawisza z klawiatury (lub wywołujemy podprogram getc z Biblioteki Standardowej, który wywołuje DOS do odczytu klawisza), mogą być godziny, dni lub nawet dłużej zanim ktoś naciśnie klawisz. Wewnątrz DOS’a jest pętla, która czeka dopóki użytkownik rzeczywiście ni naciśnie klawisza. I dopóki użytkownik naciska jakiś klawisz, flaga InDOS będzie pozostawała nie zerowa. Jeśli napisaliśmy TSR oparty o zegar, buforujący dane co kilka sekund i potrzebujemy zapisać wynik na dysku, przepełnimy nasz bufor nowymi danymi jeśli czekamy na użytkownika, który właśnie poszedł na obiad, naciskając klawisz w DOS’owym programie command.com. Szczęśliwie, MS-DOS dostarcza rozwiązanie tego problemu – przerwania jałowe. Kiedy MS-DOS jest w nieskończonej pętli oczekującej na urządzenie I/O, nieustannie wykonuje instrukcję int 28h. Przez aktualizację wektora int 28h, nasz TSR może określić kiedy DOS znajduje się w takiej pętli. Kiedy DOS wykonuje instrukcję int 28h, bezpiecznie jest wywołać DOS, którego numer funkcji (wartość w ah) jest większa niż 0Ch. Więc jeśli DOS jest zajęty kiedy nasz TSR chce wykonać wywołanie DOS’a, musi użyć albo przerwania zegarowego albo przerwania jałowego (int 28h) do aktywacji części naszego TSR’a, który musi wykonać wywołanie DOS. Jedna rzecz jaką musimy zapamiętać na końcu to to, że kiedykolwiek testujemy lub modyfikujemy każdą z powyższych flag, jesteśmy w sekcji krytycznej. Upewnijmy się, że przerwania są wyłączone. Jeśli nie , nasz TSR wykona aktywację dwóch kopii samego siebie lub może skończyć wprowadzania DOS’a w tym samym czasie kiedy inny TSR wprowadza DOS. Przykład TSR’a używającego tej techniki pojawi się trochę później, ale jest kilka dodatkowych problemów współbieżności jakie musimy omówić najpierw. 18.3.2 PROBLEM WSPÓŁBIEŻNOŚCI Z BIOS DOS nie jest jedynym niewspółbieżnym kodem jaki może chcieć wywołać TSR. Podprogramy BIOS PC również podlegają pod tą kategorię. Niestety , BIOS nie dostarcza flagi „InBIOS” lub zwielokrotnionego przerwania. Sami musimy dostarczyć takiej funkcjonalności. Kluczem do zapobieżenia współbieżności podprogramu BIOS’a jaki chcemy wywołać jest zastosowanie „otoczki”. Otoczka jest to krótkim ISR który aktualizuje istniejące przerwanie BIOS specjalnie do manipulowania flagą InUSe. Na przykład przypuśćmy, że musimy wywołać int 10h (usługa video) z wewnątrz naszego TSR. Możemy użyć następującego kodu do dostarczenia flagi „Int10InUse” którą nasz TSR może przetestować: MyInt10

MyInt10

proc inc pushf call dec iret endp

far cs:Int10InUse cs: OldInt10 cs: Int10InUse

Zakładając, że zainicjalizowaliśmy zmienną Int10InUse zerem, flaga w użyciu będzie zawierał zero, kiedy bezpiecznie jest wykonana instrukcja int 10h w naszym TSR’ze, będzie zawierała wartość niezerową kiedy program obsługi przerwania int 10h jest zajęty. Możemy użyć tej flagi jak flagi InDOS do wstrzymania wykonywania naszego kodu TSR. Podobnie Jak w DOS’ie jest kilka podprogramów BIOS, które mogą pobierać nieokreśloną ilość czasu do zakończenia. Odczytując klawisz z bufora klawiatury, odczytujemy lub zapisujemy znaki do portu szeregowego, lub drukujemy znaki na drukarce. W pewnych przypadkach możliwe jest stworzenie otoczki, która pozwoli naszemu TSR’owi aktywować się, podczas gdy podprogram BIOS wykonuje jedną z tych pętli odpytujących, co nie jest prawdopodobnie żadną korzyścią. Na przykład, jeśli program czeka aż drukarka odbierze znak zanim wyśle inny do drukowania, nasz TSR musi zapobiec temu i próbować wysłać znak do drukarki, która tego nie osiąga (innymi słowy, składa dane wysyłane do drukarki). Dlatego też otoczki BIOS generalnie nie martwią się o nieokreślone odroczenie w podprogramie BIOS 5,8,9,D,E,10, 13, 16, 17,21,28 Jeśli ten problem wpadnie naszemu TSR’owi i pewnej aplikacji ,możemy chcieć umieścić otoczki wokół następujących przerwań aby zobaczyć czy to rozwiąże nasz problem: int 5, int 8, int 9, int B, int C, int D, int E, int 10, int 13, int 14, int 16 lub int 17. To są popularni sprawcy problemów, kiedy konstruujemy TSR. 18.3.3 PROBLEM WSPÓŁBIEŻNOŚCI Z INNYM KODEM Problem współbieżności w innym kodzie może być również wywołany. Na przykład rozważmy Bibliotekę Standardową UCR. Biblioteka Standardowa UCR nie jest współbieżna. Nie jest to zazwyczaj duży problem z dwóch powodów. Po pierwsze TSR’y nie wywołują podprogramów Biblioteki Standardowej. Zamiast tego dostarczają wyników, których może użyć normalna aplikacja; te aplikacje używające Biblioteki Standardowej manipulują takimi wynikami. Drugim powodem jest to, że kiedy zawrzemy jakiś podprogram Biblioteki Standardowej w TSR’ze, aplikacja miałaby oddzielną kopie tego podprogramu bibliotecznego. TSR może wykonać instrukcję strcmp kiedy aplikacja jest w środku podprogramu strcmp, ale to nie jest ten sam podprogram! TSR nie jest współbieżny z kodem aplikacji, wykonuje oddzielny podprogram. Jednakże wiele z funkcji Biblioteki Standardowej wywołują DOS lub BIOS. Wywołania takie nie sprawdzają czy DOS lub BIOS są już aktywne. Dlatego też, wywołanie wielu podprogramów Biblioteki Standardowej z wnętrza TSR może spowodować współbieżność DOS’a lub BIOS’a . Istnieje jedna sytuacja, kiedy TSR może powrócić do programu Biblioteki Standardowej. Przypuśćmy, że nasz TSR ma oba składniki, bierny i aktywny. Jeśli aplikacja główna dokonuje wywołania podprogramu pasywnego w TSR’ze a ten program wywołuje program Biblioteki Standardowej, istnieje możliwość, że system przerwań może przerwać podprogram Biblioteki Standardowej a aktywna część TSR’a może ponownie wrócić do tego samego kodu. Chociaż taka sytuacja są raczej rzadkie, powinniśmy mieć na uwadze taką możliwość. Oczywiście najlepszym rozwiązaniem jest unikanie Biblioteki Standardowej wewnątrz TSR’ów . Z innych powodów, podprogramy Biblioteki Standardowej są trochę duże, a TSR’y powinny być tak małe jak to możliwe. 18.4 PRZWRANIE RÓNOCZESNYCH PROCESÓW (INT 2FH) Kiedy instalujemy bierny TSR lub aktywny TSR z biernym składnikiem, będziemy musieli wybrać jakiś wektor przerwań do aktualizacji, aby inne programy mogły komunikować się z naszym biernym podprogramem. Możemy wybrać wektor przerwań prawie losowo, powiedzmy int 84, ale spowoduje to problem kompatybilności. Co się zdarzy jeśli ktoś inny używa tego wektora przerwań? Czasami wybór wektora przerwań jest jasny. Na przykład, jeśli nasz bierny TSR rozszerza usługę klawiaturową int 16h, ma sens aktualizacja wektora int 16h i dodanie dodatkowo funkcji przed i po tych dostarczonych już przez BIOS. Z drugiej strony, jeśli tworzymy sterownik dla nowego urządzenia dla PC, prawdopodobnie nie będziemy chcieli nakładać funkcji wspierającej dla tego urządzenia na jakieś inne przerwania. Mimo to przypadkowe wykorzystanie nieużywanego wektora przerwań jest ryzykowne; jak wiele innych programów zdecydowało się na zrobienie tego samego? NA szczęście MS-DOS dostarcza rozwiązania: przerwanie równoczesnych procesów. Int 2Fh dostarcza ogólnego mechanizmu dla instalacji, testowania obecności i komunikowania z TSR’em. Używając przerwania równoczesnych procesów, aplikacja umieszcza wartość identyfikacyjną w ah i numer funkcji w al. a potem wykonuje instrukcję int 2Fh. Każdy TSR w łańcuchu int 2Fh porównuje wartość w ah ze swoją własną unikalną wartością identyfikatora. Jeśli wartości pasują, TSR przetwarza polecenia określone przez wartość w rejestrze al. Jeśli identyfikowane wartości nie pasują, TSR przekazuje sterowanie do następnego w łańcuchu programu obsługi int 2Fh.

Oczywiście, to zredukuje problem tylko w pewnym stopniu, ale nie wyeliminuje go. Pewnie, nie musimy odgadywać losowego numeru wektora przerwań, ale musimy jeszcze wybrać losowy numer identyfikacyjny. Przecież wydaje się rozsądne, że musimy wybrać ten numer przed zaprojektowaniem TSR’a i jakieś aplikacji, która go wywołuje, w końcu jak aplikacja wiedziałaby jaką wartość załadować do ah jeśli dynamicznie przydzielalibyśmy tą wartość kiedy TSR byłby rezydentny? Cóż, jest parę sztuczek jakie możemy wykorzystać do dynamicznego przydzielania identyfikatora TSR i pozwalają zainteresowanej aplikacji określić ID TSR’a. Poprzez konwencję, funkcja zero jest wywołaniem „Czy tu jesteś?” Aplikacja powinna zawsze wykonywać tą funkcję do określenia czy TSR jest obecny w pamięci przed wykonaniem jakiejś żądanej usługi. Zazwyczaj , funkcja zero zwraca zero w al. jeśli TSR nie jest obecny, zwraca 0FFh jeśli jest obecny. Jednakże , kiedy ta funkcja zwraca 0FFh, jedynie mówi nam, że jakiś TSR odpowiedział na zapytanie; nie gwarantuje, że to TSR, który nas interesuje jest obecny w pamięci. Jednak przez rozszerzenie nieco konwencji, jest bardzo łatwo zweryfikować obecność żądanego TSR. Przypuśćmy, że funkcja zero wywołuje również zwrot wskaźnika do unikalnej identyfikacji ciągu w rejestrze es:di. Wtedy kod testujący obecność określonego TSR’a, może testować ten ciąg kiedy int 2Fh wykryje obecność TSR’a. Poniższy fragment kodu demonstruje jak TSR może określić czy TSR identyfikowany jako „Randy’s INT 10h Extension” jest obecny w pamięci; kod ten może również określać unikalną identyfikację kodu dal tego TSR’a dla późniejszych odniesień: ; Skanowanie wszystkich możliwych TSR’ów. Jeśli jest zainstalowany jeden, zobaczmy czy jest to ten ; którym jesteśmy zainteresowani IDLoop:

TryNext: Success:

mov mov push mov int pop cmp je strcmpl byte byte je loop jmp

cx, 0FFh ah, cl cx al., 0 2Fh cx al., 0 TryNext

;to będzie numer ID ; ID -> AH ;zachowanie cx przed wywołaniem ;test obecności kodu funkcji ;wywołanie przerwania równoczesnych procesów ;przywrócenie cx ;zainstalowano TSR? ;zwraca zero jeśli nie ma żadnego ;zobacz czy to ten jaki chcemy

„Randy’s INT „ „10h Extension”, 0 Success IDLoop NotInstalled

;skocz jeśli to nasz ;w przeciwnym razie spróbuj ponownie ;niepowodzenie jeśli doszliśmy do tego miejsca

mov -

FuncID, cl

;zachowanie wyniku funkcji

Jeśli ten kod zakończy się powodzeniem, zmienna FuncID zawiera wartość identyfikującą dal rezydentnego TSR’a. Jeśli niepowodzeniem, program prawdopodobnie będzie przerwany lub w przeciwnym razie zapewnić, że nigdy nie wywoła zaginionego TSR’a. Powyższy kod pozwala aplikacji łatwe wykrycie obecności i określenia numeru ID dal określonego TSR’a. Kolejne pytanie: „Jak zdobędziemy numer ID dal TSR po raz pierwszy?” . Następna sekcja zajmie się tą kwestią, również tym jak TSR musi odpowiedzieć na przerwanie równoczesnych procesów. 18.5 INSTALOWANIE TSR’A Chociaż omawialiśmy już jak uczynić program rezydentnym, jest kilka aspektów instalacji TSR, które musimy poznać. Po pierwsze co się stanie jeśli użytkownik zainstaluje TSR a potem spróbuje zainstalować go po raz drugi bez usunięcia tego , który już jest rezydentny? Po drugie jak możemy przydzielić numer identyfikacyjny TSR’owi, który nie wchodzi w konflikt z TSR’em który już jest zainstalowany? Ta sekcja zwróci się ku tym kwestiom. Pierwszym problemem jest próba reinstalacji TSR’a. Chociaż można wyobrazić sobie typ TSR’a, który pozwala na wiele kopii samego siebie w pamięci w tym samym czasie, takich TSR’ów jest kilka i przejściowych. W większości przypadków , mając wiele kopii TSR’a w pamięci, w najlepszym razie marnujemy pamięć, a w najgorszym razie mamy krach systemu. Dlatego też, chyba, że napiszemy specyficzny TSR, który pozwala na wiele kopii samego siebie w pamięci w tym samym czasie, powinniśmy sprawdzać czy TSR jest już

zainstalowany przed ponowną jego instalacją. Kod ten jest identyczny z kodem aplikacji używany aby zobaczyć czy TSR jest zainstalowany, jedyną różnicą jest to, że TSR powinien wydrukować nieprzyjemną wiadomość i odmówić przejścia do TSR’a jeśli znajdzie jego kopię już zainstalowaną w pamięci. Robi to poniższy kod: SearchLoop:

TryNext: AlreadyThere:

mov mov push mov int pop cmp je strcmpl byte byte je loop jmp

cx, 0FFh ah, cl cx al, 0 2Fh cx al, 0 TryNext “Randy’s INT “ “10h Extension”, 0 AlreadyThere SearchLoop NotInstaled

print byte “A copy of this TSR already exist i n memory”, cr,lf byte “Aburting installation process.”, cr, lf, 0 ExitPgm -

W poprzedniej sekcji, pokazaliśmy jak napisać kod, który pozwoliłby aplikacji określić ID TSR’a określonego programu rezydentnego. Teraz musimy popatrzeć jak dynamicznie wybrać numer identyfikacyjny TSR’a, który nie wchodzi w konflikt z innym TSR’em. Jest to jeszcze inna modyfikacja pętli przeszukującej. Faktycznie, możemy zmodyfikować powyższy kod, tak aby wykonywał to dla nas. Wszystko co musimy zrobić to zachować jakąś wartość ID którą ma zainstalowany TSR. Musimy tylko dodać kilka linijek do powyższego kodu dla wykonania tego:

SearchLoop:

mov mov mov push mov int pop cmp je strcmpl byte byte je

FuncID, 0 cx, 0FFh ah, cl cx al, 0 2Fh cx al, 0 TryNext

;inicjalizacja FuncID zerem

“Randy’s INT “ “10h Extension”, 0 AlreadyThere

; Notka: przypuszczalnie DS wskazuje na rezydentny segment danych, który zawiera zmienną FuncID. W przeciwnym razie musimy zmodyfikować następujący punkt jakiegoś rejestru segmentowego W segmencie zawierającym FuncID i użyć właściwego segmentu przesłaniającego FuncID TryNext:

mov loop jmp

FuncID, cl SearchLoop NotInsatlled

;zachowanie możliwego ID funkcji jeśli ten ; identyfikator nie jest używany

AlreadyThere:

print byte “A copy of this TSR already exist i n memory”, cr,lf byte “Aburting installation process.”, cr, lf, 0 ExitPgm

NotInstalled:

cmp FuncID, 0 ;jeśli ID ne są dostępne będzie zawierała zero jne GoddID print byte „There are too many TSRs alraedy installed.”, cr, lf byte “Sorry, aborting instalation process.”, cr. Lf, 0 ExitPgm

GoodID: Jeśli ten kod dotrze do etykiety “GoodID” wtedy poprzednia kopia TSR nie jest obecna w pamięci a zmienna FuncID zawiera niewykorzystywany identyfikator funkcji. Oczywiście kiedy instalujemy nasz TSR w ten sposób, musimy nie zapomnieć zaktualizować przerwanie 2Fh w łańcuchu int 2Fh. Również, musimy napisać program obsługi przerwania 2Fh przetwarzający wywołani int 2Fh. Poniżej mamy bardzo prosty programik obsługi przerwania równoczesnych procesów dla kodu jaki skonstruowaliśmy: FuncID OldInt2F

byte 0 dword ?

MyInt2F

proc cmp je jmp

;powinien być w segmencie rezydentnym

far ah, cs: FuncID ItsUs cs: OldInt2F

;wywołanie dla nas?

;Teraz dekodujemy wartość funkcji w AL: ItsUs:

IDString

cmp jne mov lesi iret byte byte

al., 0 TryOtherFunc al., 0FFh IDString

;weryfikujemy obecność wywołania? ;zwracamy wartość “obecności” w AL ;zwracamy wskaźnik do ciągu w es:di ;powrót do kodu wywołującego

„”Randy’s INT „ “10h Extension”, 0

; w dole, obsługa innych żądań zwielokrotnionych. Ten kod nie oferuje niczego, ale jest gdzie mam być ;testuje wartość w AL. określając która funkcja będzie się wykonywać TryOtherFunc:

MyInt2F

iret endp

18.6 USUWANIE TSR’A Usunięcia TSR jest trochę trudniejsze niż jego instalacja. Są trzy rzeczy, które musi zrobić kod usuwający aby właściwie usunąć TSR’a z pamięci: po pierwsze musi zatrzymać jakieś oczekujące działanie (np. TSR może mieć ustawione jakieś flagi do rozpoczęcia działania w przyszłości) ; po drugie musi odtworzyć wszystkie wektory przerwań do ich poprzedniej postaci; po trzecie musi zwrócić całą zarezerwowaną pamięć z powrotem do DOS’a aby inne aplikacje mogły z niej korzystać. Podstawową trudnością z tymi trzema działaniami jest to, że nie zawsze jest możliwe poprawne zwrócenie wektorów przerwań. Jeśli usuwany kod naszego TSR po prostu przywraca starą wartość wektora przerwań, możemy stworzyć rzeczywiście duży problem. Co się stanie jeśli użytkownik uruchomi jakieś inne TSR’y po uruchomieniu naszego i aktualizują one ten sam wektor przerwań jak nasz? To stworzyłoby łańcuch przerwań, który może wyglądać jak poniższy: Wektor przerwań

TSR # 1

TSR # 1

Nasz TSR

Oryginalny TSR

Jeśli odtwarzamy wektor przerwań oryginalną wartością, stworzymy co następuje: Wektor przerwań

TSR # 1

TSR # 1

?

Oryginalny TSR

To skutecznie blokuje TSR’y w łańcuchu w naszym kodzie. Gorzej jeszcze, to blokuje tylko te przerwania, które te TSR’y mają w użyciu wraz z naszym TSR’em, inne przerwania które te TSR’y aktualizują są jeszcze aktywne. Kto wie jak te przerwania będą się zachowywały w takich okolicznościach? Jednym rozwiązaniem jest wydrukowanie komunikatu o błędzie informujący użytkownika, że nie mogą usunąć tego TSR’a dopóki nie usuną TSR’ów zainstalowanych uprzednio. Jest to powszechny problem z TSR’ami i większości użytkowników DOS’a, którzy instalują i usuwają TSR’y powinno być wygodniej ze świadomością ,że muszą usuwać TSR’y w odwrotnym porządku niż je instalowali. Byłoby kuszącą sugestią nowa konwencja, że TSR’y powinny być posłuszne; być może jeśli numer funkcji to 0FFh, TSR powinien przechować wartość w es:bx w wektorze przerwań określonym w cl. To pozwoliłoby TSR’owi, który chciałby usunąć się przekazać adres swojego oryginalnego programu obsługi do poprzedniego TSR’a w łańcuchu. Są w związku z tym podejściem trzy problemy: po pierwsze prawie żaden TSR istniejący obecnie nie wspiera tej cechy; po drugie jakieś TSR’y mogą używać funkcji 0FFh do czegoś jeszcze, wywołując je z tą wartością, nawet jeśli znamy ich numer ID, możemy stworzyć problem; w końcu, ponieważ usunęliśmy TSR z łańcucha przerwań nie znaczy to ,że możemy (naprawdę) zwolnić pamięć używaną przez TSR. Schemat zarządzania pamięcią DOS (sprawa wolnego wskaźnika) działa podobnie jak stos. Jeśli są inne TSR’y zainstalowane powyżej naszego w pamięci, większość aplikacji nie będzie zdolna do użycia zwolnionej pamięci przez usunięcie naszego TSR’a. Dlatego też, również zaadoptujemy strategię prostego informowania użytkownika, że nie może usunąć TSR jeśli są zainstalowane inne w dzielonym łańcuchu przerwań. Oczywiście to przywołuje dobre pytanie, jak możemy określić czy są inne TSR’y włączone w nasze przerwania? Cóż, nie jest to tak trudne. Wiemy, że wektory przerwań 80x86 powinny wskazywać jeszcze na nasze podprogramy, jeśli uruchomiliśmy ostatni TSR. Więc wszystko co musimy zrobić to porównać zaktualizowane wektory przerwań z adresami naszych podprogramów obsługi. Jeśli WSZYSTKIE pasują, wtedy możemy bezpiecznie usunąć TSR z pamięci. .tylko jeden z nich nie pasuje, wtedy nie możemy usunąć TSR’a z pamięci . Poniższa sekwencja kodu testuje aby zobaczyć czy jest OK. usunięcie TSR’a zawierającego ISR’y dla int 2Fh i int 9: ;OkayToRmv ; ; ;

Podprogram ten zwraca ustawioną flagę przeniesienia jeśli usunięcie bieżącego TSR’a z pamięci jest OK. Sprawdza wektory przerwań dla int 2fh i int 9 upewniając się ,że wskazują one jeszcze nasze lokalne podprogramy. Kod ten zakłada, że DS. wskazuje segment danych kodu rezydentnego

OkayToRmv

proc push mov mov mov cmp jne mov cmp jne

near es ax, 0 ;ES wskazuje tablice wektora przerwań es, ax ax, word ptr OldInt2F ax, es: [2fh*4] CantRemove ax, word ptr oldInt2F+2 ax, es; [2Fh*4+2] CantRemove

mov cmp jne mov cmp jne

ax, word ptr Oldint9 ax, es:[9*4] CantRemove ax, word ptr OldInt+2 ax, es: [9*4+2] CantRemove

;Możemy bezpiecznie usuwać ten TSR z pamięci stc pop

es

ret ; jeśli coś jest nie tak, nie możemy usunąć tego TSR’a CantRemove:

clc pop ret

es

OkayToRmv Zanim TSR spróbuje usunąć sam siebie, powinien wywołać podprogram taki jak ten aby zobaczyć czy usuwanie jest możliwe. Oczywiście, fakt ,że żaden inny TSR nie jest połączony do tych samych przerwań, nie gwarantuje ,że nie ma TSR’ów powyżej naszego w pamięci. Jednakże , usuwając TSR w tym przypadku nie spowoduje krachu systemu. Prawda, możemy nie być zdolni do odebrania pamięci jeśli TSR jest używany (przynajmniej dopóki suwamy inne TSR’y), ale przynamniej usuwanie nie tworzy komplikacji. Usuwanie TSR’a z pamięci wymaga dwóch wywołań DOS, jednego dla zwolnienia pamięci używanej przez TSR i jednego do zwolnienia pamięci używanej przez obszar środowiska powiązanego z TSR’em. Robiąc to musimy zrobić dealokację wywołania DOS’a. To wywołanie wymaga przekazania adresu segmentu bloku udostępnionego w rejestrze es. Dla samego programu TSR , musimy przekazać adres PSP TSR’a . Jest to jeden z powodów dla którego musimy zachować jego PSP kiedy instalujemy go po raz pierwszy. Innym wywołaniem zwalniającym jaki musimy zrobić jest zwolnienie przestrzeni związanej z blokiem środowiska. Adres tego bloku jest pod offsetem 2Ch w PSP. Więc prawdopodobnie powinniśmy zwolnić go najpierw. Poniższy kod wykonuje zwalnianie pamięci powiązanej z TSR’em: ;Prawdopodobnie, zmienna PSP została zainicjalizowana adresem PSP tego programu przed wywołaniem ; TSR’a mov mov mov int

es, PSP es, es:[2Ch] ah, 49h 21h

mov es, PSP mov ah, 49h int 21h

;pobranie adresu bloku środowiska ;wywołanie dealokacji bloku DOS ;teraz zwalniamy pamięć przestrzeni programu

Niektóre słabo napisane TSR’y nie dostarczają żadnych udogodnień pozwalających nam usuwać je z pamięci. Jeśli ktoś chce usunąć taki TSR będzie musiał ponownie uruchomić PC. Oczywiście, jest to kiepskie projektowanie. Każdy TSR jaki zaprojektujemy do czegoś innego niż szybki test, powinien posiadać zdolność do usunięcia się z pamięci. Przerwanie równoczesnych procesów z numerem funkcji jeden jest często używane do tego celu. Usuwając TSR z pamięci, jakiś program przekazuje ID TSR’a i numer funkcji jeden do TSR’a. Jeśli TSR może usunąć się z pamięci, robi to i zwraca wartość oznaczającą powodzenie. Jeśli TSR nie może usunąć się z pamięci, zwraca jakąś część warunku błędu. Generalnie, program usuwający jest samym TSR’em ze specjalnym parametrem, który mówi mu o usunięciu TSR’a bieżąco załadowanego do pamięci. Trochę później w tym rozdziale przedstawimy przykład TSR’a, który działa dokładnie w ten sposób . 18.7 INNE ZAGADNIENIA ZWIĄZANE Z DOS Oprócz problemów współbieżności z DOS, jest kilka innych kwestii z jakim nasz TSR musi działać jeśli zamierza czynić wywołania DOS. Chociaż nasze wywołania nie muszą powodować współbieżności DOS’a, jest całkiem możliwe ,że wywołania DOS’a przez nasze TSR’y przeszkadzają strukturom danych używanym przez wykonywane aplikacje. Te struktury danych zawierają stos aplikacji, PSP, DTA i rozszerzony rekord informacji o błędzie DOS. Kiedy aktywny lub bierny TSR przejmuje sterowanie z CPU, działa w środowisku głównej (pierwszoplanowej) aplikacji. Na przykład, TSR’y zwracają adres i jakąś wartość zachowaną na stosie, odłożoną na stos aplikacji. Jeśli TSR nie używa dużo przestrzeni stosu, jest świetnie , nie musimy przełączać stosów. Jednakże, jeśli TSR zżera znaczne ilości przestrzeni stosu z powodu wywołania rekurencyjnego lub alokacji

zmiennych lokalnych, TSR powinien zachować wartość ss i sp aplikacji i przełączyć na stos lokalny. Przed powrotem TSR powinien przełączyć się z powrotem na stos aplikacji pierwszoplanowej. Podobnie, jeśli TSR wykonuje DOS’owe pobranie adresu psp, DOS zwraca adres PSP pierwszoplanowej aplikacji, a nie PSP TSR’a. PSP zawiera kilka ważnych adresów, których używa DOS przy zajściu błędu. Na przykład PSP zawiera adres programu zakończenia, program obsługi ctrl-braek, i obsługi błędu krytycznego. Jeśli nie przełączymy PSP z aplikacji pierwszoplanowej na TSR’a i wystąpi jeden z wyjątków (np. wystąpi ctrk-braek, lub błąd dyskowy) program obsługi związany z tą aplikacją może go przejąć. Dlatego też, kiedy robimy wywołania DOS, które w wyniku mogą dać jeden z tych warunków, musimy przełączać PSP. Podobnie, kiedy nasz TSR zwraca sterowanie do aplikacji pierwszoplanowej , musi przywrócić wartość PSP. MS-DOS dostarcza dwóch funkcji, które pobierają i ustawiają bieżący adres PSP. Funkcja DOS GetPSP (ah=51h) ustawia aktualny adres PSP programu do wartości z rejestru bx. Funkcja DOS GetPSP (ah = 50h) zwraca adres bieżącego PSP programu w rejestrze bx. Zakładając, że nierezydentna część naszego TSR’a zapisała jego PSP w zmiennej PSP, przełączamy pomiędzy PSP TSR’a a PSP aplikacji pierwszoplanowej jak pokazano: ;Zakładamy, że wprowadziliśmy kod TSR’a, określając, że jest OK. wywołując DOS, i przełączamy DS. ; żeby wskazywał nasze lokalne zmienne mov int mov mov mov int mov mov int

ah, 51h 21h AppPSP, bx bx, PSP ah, 50h 21h

;pobranie adresu PSP aplikacji ;zachowanie lokalnie PSP aplikacji ;zmiana systemowego PSP na PSP TSR’a ;ustawienie funkcji Set PSP ;kod TSR’a

bx, AppPSP ah, 50h 21h

;przywrócenie adresu systemowego PSP aby ;wskazywał PSP aplikacji

< porządkujemy i wracamy z TSR’a> Inną globalną strukturą, której używa DOS jest adres transferu dysku .Ten bufor adresowy był używany dla I/O dysków w DOS wersji 1.0. Od tego czasu głównym zastosowaniem dla DTA były funkcje znajdowania pierwszego pliku i znajdowanie pliku kolejnego (zobacz „MS-DOS, PC-BIOS i I/O plików”). Oczywiście, jeśli aplikacja jest w środku stosowania danych w DTA a nasz TSR robi wywołanie DOS’a, które zmienia dane w DTA, będziemy wpływać na proces pierwszoplanowy. MS-DOS dostarcza dwóch funkcji, które pozwalają nam pobrać i ustawić adres DTA. Funkcja Get DTA Address, z ah -= 2Fh, zwraca adres DTA w rejestrach es:bx. Funkcja Set DTA Address (ah = 1Ah) ustawia DTA na wartość z pary rejestrów ds.:dx. Tymi dwoma funkcjami możemy zachowywać i przywracać DTA, co robiliśmy dla powyższego adresu PSP. DTA jest zazwyczaj pod offsetem 80h w PSP, następujący kod zachowuje DTA aplikacji pierwszoplanowej i ustawia aktualny DTA TSR’ów pod offsetem PSP:80 ;Kod ten czyni takie same założenia jak przykład poprzedni mov int mov mov

ah, 2Fh ;ustawienie DTA aplikacji 21h word ptr AppDTA, bx word ptr AppDTA+2, es

push mov mov mov int pop -

ds ds, PSP dx, 80h ah, 1ah 21h ds.

;DTA jest w PSP ;pod offsetem 80h ;ustawienie funkcji Set DTA

;kod TSR’a

push mov mov mov int

ds. dx, word ptr AppDTA ds, word ptr AppDTA+2 ax, 1ah 21h

;ustawienie funkcji Set DTA

Ostatnią kwestią związaną z TSR’em to to jak współpracuje z informacją o rozszerzonym błędzie w DOS. Jeśli TSR przerywa program bezpośrednio po tym jak DOS wraca do tego programu, może być jakaś informacja o błędzie, którą aplikacja pierwszoplanowa musi sprawdzić w rozszerzonej informacji o błędzie DOS’a. Jeśli TSR robi wywołanie DOS’a DOS może umieścić tą informację w statusie wywołania DOS’a przez TSR. Kiedy sterowanie jest zwracane do aplikacji pierwszoplanowej, może ona odczytać rozszerzony status błędu i pobrać tą informację generowaną przez wywołanie DOS’a przez TSR, nie wywołującą aplikację DOS. DOS dostarcza dwóch funkcji asymetrycznych, Get Exteneded Error i Set Extended Error, które ,odpowiednio, odczytują i zapisują te wartości. Funkcja Get Extended Error zwraca status błędu w rejestrach ax, bx, cx, dx, si ,di i ds. Musimy zachować rejestry w strukturze danych, która przybiera następującą postać: ExtError eeAX eeBX eeCX eeDX eeSI eeDI eeDS eeES ExtError

struct word word word word word word word word word ends

? ? ? ? ? ? ? ? 3 dup (0)

;zarezerwowane

Funkcja Set Extended Error wymaga przekazania adresu do tej struktury w rejestrach ds:si (to dlatego te dwie funkcje są asymetryczne). Dla zachowania rozszerzonej informacji o błędzie możemy użyć kodu podobnego do tego: ; zachowujemy założenia takie jak dla powyższych podprogramów. Również zakładamy, że struktura ;danych błędu nazywa się ERR i jest w tym samym segmencie co kod push mov mov int

ds. ah, 59h bx, 0 21h

mov cs: ERR.eeDS, ds. pop ds mov ERR.eeAX, ax mov ERR.eeBX, bx mov ERR.eeCX, cx mov ERR.eeDX, dx mov ERR.eeSI, si mov ERR.eeDI, di mov ERR.eeES, es mov si, offset ERR mov ax, 5D0Ah int 21h

18.8 TSR MONITORA KLAWIATURY

;zachowujemy wskaźnik do naszego DS. ;ustawimy funkcję Get extended error ;wymagane przez tą funkcję

;odzyskanie wskaźnika do naszego DS

;tu przychodzi kod TSR’a ;ds już wskazuje poprawny segment ;5D0Ah jest kodem Set Extended Error

Poniższy program rozszerza program licznika naciśnięć klawiszy przedstawiany trochę wcześniej w tym rozdziale. Ten program monitoruje naciśnięcia klawiszy i co minutę zapisuje dane do pliku listującego dane, czas i przybliżoną liczbę naciśnięć w ostatniej minucie. Ten pogram może nam pomóc odkryć ile czasu spędzamy pisząc w przeciwieństwie do rozmyślań przy monitorze ;Jest to przykład aktywnego TSR’a który zlicza przerwania klawiaturowe podczas aktywacji. Co minutę ; zapisuje liczbę przerwań klawiaturowych, które wystąpiły w poprzedniej minucie do pliku wyjściowego. ; Kontynuuje dopóki użytkownik nie usunie programu z pamięci. ; ; Użycie: ; nazwa pliku KEYEVAL zaczyna zapisywać dane z naciśnięć klawiszy do tego pliku ; KEYEVAL REMOVE usuwa program rezydentny z pamięci ; ; Ten TSR sprawdza aby upewnić się ,że nie ma już aktywnej kopii w pamięci. Kiedy mamy przerwanie ; z dysku I/O, sprawdza aby upewnić się ,że DOS nie jest zajęty i zachowuje aplikacje globalne (PSP, DTA ; i rozszerzone info o błędzie). Kiedy usuwa się z pamięci , upewnia się, że nie ma innych łańcuchów ; przerwań w jakimś z jego przerwań zanim zacznie się usuwać. ; ; Definicja segmentu rezydentnego musi nadejść przed wszystkim innym ResidentSeg ResidentSeg

segment ends

para public ‘Resident’

EndResident EndResident

segment ends

para public ‘EndRes’

.xlist .286 .include includelib .list

stdlib.a stdlib.lib

; Segment rezydentny przechowujący kod TSR’a: ResidentSeg

segment assume

para public ‘Resident’ cs:ResidentSeg, ds:nothing

;Int 2Fh numer ID dla tego TSR’a: MyTSRID

byte

0

;Następująca zmienna zlicza liczbę przerwań klawiaturowych KeyIntCnt

word

0

; Counter licznik zliczający liczbę milisekund jakie minęły, SecCounter zlicza liczbę sekund (do 60) Counter SecCounter

word word

0 0

;FileHandle jest uchwytem dla pliku logującego: FileHandle

word

0

;NeedIO określa czy mamy w toku operacje I/O NeedIO

word

0

;PSP jest adresem psp dla tego programu

PSP

word

0

;Zmienne, które mówią nam czy DOS, INT 13h lub INT 16h są zajęte: InInt13 InInt16 InDOSFlag

byte 0 byte 0 dword ?

;Zmienne te zawierają oryginalne wartości wektorów przerwań jakie zaktualizowaliśmy OldInt9 OldInt13 OldInt16 OldInt1C OldInt28 OldInt2F

dword dword dword dword dword dword

? ? ? ? ? ?

;struktura danych DOS: ExtErr eeAX eeBX eeCX eeDX eeSI eeDI eeDS eeES ExtErr

struct word word word word word word word word word ends

XErr AppPSP AppDTA

ExtErr {} word ? dword ?

? ? ? ? ? ? ? ? 3 dup (0) ;status rozszerzonego błędu ;wartość PSP aplikacji ;adres DTA aplikacji

;Następujacec dane są w rekordzie wyjściowym. Po posortowaniu tych danych do tych zmiennych ; TSR zapisuje te dane na dysk month day year hour minute second Keystrokes RecSize

byte byte word byte byte byte word =

0 0 0 0 0 0 0 $ - month

;MyInt9 ; ; ; MyInt9

System wywołuje ten podprogram za każdym razem kiedy wystąpi przerwanie klawiaturowe. Zwiększa zmienną KeyIntCnt a potem przekazuje sterowanie do oryginalnego programu obsługi przerwania Int 9

MyInt9

proc inc jmp endp

far ResidentSeg : KeyIntCnt ResidentSeg : OldInt9

;Myint1C;

Przerwanie zegarowe. Zlicza 60 sekund a potem próbuje zapisać rekord pliku wyjściowego. Oczywiście ta funkcja musi skakać po różnych problematycznych częściach kodu.

MyInt1C

proc far assume ds.ResidentSeg push push pusha mov mov pushf call

ds es ;zachowujemy wszystkie rejestry ax, ResidentSeg ds., ax OldInt1C

; Najważniejsze najpierw to ustawić nasz licznik przerwań żeby mógł liczyć co minutę. Ponieważ, mamy ; przerwania co 54.92549 milisekundy, musimy wykazać się większą precyzją niż 18 razy na sekundę, ; żeby synchronizacja nie odbiegła zbyt daleko add cmp jb sub inc

Counter , 549 Counter, 10000 NotSecYet Counter, 10000 SecCounter

;54,9 ms na int 1C ;1 sekunda

NotSecYet: ;If NeedIO nie jest zerem, wtedy w toku jest operacja I/O. Nie narusza to wartości wyjściowej jeśli jest ; taki przypadek cli cmp jne

;to jest rejon krytyczny NeedIO, 0 SkipSetNIO

;Okay, żadnego I/O w toku, zobaczmy czy minuta minęła od ostatniego razu kiedy zapisywaliśmy naciśnięcie ; klawisza do pliku. Jeśli tak, czas zacząć inną operację I/O

SkipSetNIO:

Int1CDone:

MyInt1C ;MyInt28 ; ;

cmp jb mov mov shr mov mov mov

SecCounter, 60 Int1Cdone NeedIO, 1 ax, KeyIntCnt ax, 1 KeyStrokes, ax KeyIntcnt,0 SecCounter, 0

cmp jne

NeedIO, 1 Int1CDone

;czy I/O jest już w toku? Lub zrobiony?

call jnc

ChkDOSStatus Int1CDone

;zobacz czy DOS / BIOS są wolne ;skocz jeśli zajęte

call

DoIO

;zrób I/O jeśli DOS jest wolny

popa pop es po ds. iret endp assume ds.:nothing

;przeszła już minuta? ;flaga potrzebna dla I/O ;kopiuje to do bufora wyjściowego ;po obliczeniu # naciśnięcia klawisza ;reset do kolejnej minuty

;przywrócenie rejestrów i wyjście

przerwanie jałowe. Jeśli DOS jest w pętli oczekiwania na zakończenie I/O, wykonuje instrukcję int 28h za każdym razem, kiedy przechodzi przez pętlę Możemy zignorować flagi InDOS i CritErr w tym czasie i zrobić I/O jeśli inne przerwania są wolne

MyInt28

Int28Done:

MyInt28

proc far assume ds.:ResidentSeg push push pusha

ds es

mov mov

ax, ResidentSeg ds., ax

pushf call

OldInt28

cmp jne

NeedIO, 1 Int28Done

;czy mamy tymczasem I/O?

mov or jne

al, InInt13 al.,InInt16 Int28Done

;zobacz czy BIOS jest zajęty

call

DoIO

;zróbmy I/O jeśli BIOS jest wolny

;zachowanie wszystkich rejestrów

;wywołanie kolejnego INT 28h ; ISR w łańcuchu

popa pop es po ds. iret endp assume ds.:nothing

;MyInt16-

jest to otoczka dla programu obsługi INT 16h (przerwane kontrolowane klawiatury)

MyInt16

proc Inc

far ResidentSeg : InInt16

; Wywołanie oryginalnego programu obsługi : pushf call

ResidentSeg: OldInt16

;Dla INT 16h musimy zwrócić flagi, które pochodzą z poprzedniego wywołania

MyInt16

pushf dec popf retf endp

;MyInt13-

To jest tylko otoczka dla programu obsługi INT 13h (przerwanie kontrolowane I/O dysku)

MyInt13

proc inc pushf call pushf dec popf retf endp

MyInt13 ; ChkDOSStatus-

ResidentSeg :InInt16 2

;lewy IRET do zachowania flag

far ResidentSeg: InInt13 ResidentSeg: OldInt13 ResidentSeg: InInt13 2 Zwraca wyzerowaną flagę przeniesienia jeśli podprogramy DOS lub BIOS są zajęte

;

i nie możemy ich przerwać

ChkDOSStatus proc assume les mov or or or je clc ret

near ds.: ResidentSeg bx, InDOSFlag al, es:[bx] al, es:[bx-1] al, InInt16 al., InInt13 Okay2Call

;pobranie flagi InDOS ;OR z flagą CritErr ;OR z naszą wartością otoczki

Okay2Call:

clc Ret ChkDOSStatus endp assume ds:nothing ; PreserveDOS- pobranie kopii bieżącego PSP, DTA i rozszerzonej informacji DOS’a i zachowanie tego ; stanu rzeczy. Potem ustawia PSP do naszego lokalnego PSP i DTA do PS;80h PreserveDOS

proc near assume ds :ResidentSeg mov int mov

ah, 51h 21h AppPSP, bx

;pobranie PSP aplikacji

mov int mov mov

ah, 2Fh 21h word ptr AppDTA, bx word ptr AppDTA+2, es

;pobranie DTA aplikacji

push mov xor int

ds ah, 59h bx, bx 21h

mov pop mov mov mov mov mov mov mov

cs: Xerr.eeDS, ds. ds XErr.eeAX, ax XErr.eeBX, bx XErr.eeCX, cx XErr.eeDX, dx XErr.eeSI, si XErr.eeDI, di XErr.eeES, es

;zachowanie na później

;pobranie rozszerzonej informacji o błędzie

; Okay, wskazują wskaźniki DOS’a na nas: mov mov int

bx, PSP ah, 50h 21h

push mov mov mov int pop

ds. ds., PSP dx, 80h ah, 1Ah 21h ds.

;ustawienie PSP ;ustawienie DTA pod adresem PSP:80h ;ustawienie funkcji DTA

PreserveDOS

ret endp assume ds.:nothing

;RestoreDOS-

Przywraca ważne globalne dane DOS’a z powrotem do wartości aplikacji

RestoreDOS

proc near assume ds.: ResidentSeg

RestoreDOS

mov mov int

bx, AppPSP ah, 50h 21h

push lds mov int pop push

ds. dx, AppDTA ah, 1Ah 21h ds. ds.

mov mov int pop ret endp assume

si, offset XErr ax, 5D0Ah 21h ds.

;ustawienie PSP

;ustawienie DTA

; zachowanie rozszerzonego błędu ;przywrócenie funkcji XErr

ds.:nothing

;DoIO-

Ten podprogram przetwarza każdą z tych operacji I/O wymagane do zapisania danych do pliku

DoIO

proc near assume ds.:ResidentSeg mov

NeedIO, 0FFh

;dla nas flaga zajęta

; włączamy z powrotem przerwania (wyzerowaliśmy sekcję krytyczną ponieważ zapisaliśmy 0FFh do NeedIO) sti call

PreserveDOS

;zachowujemy dane DOS

mov int mov mov mov

ah, 2Ah 21h month, dh day, dl year, cx

;funkcja Get Date DOS

mov int mov mov mov

ah, 2Ch 21h hour, ch minute, cl second, dh

;pobranie funkcji Get Time DOS

mov mov mov mov int mov mov int

ah, 40h bx, FileHandle cx, RecSize dx, offset month 21h ah, 68h bx, FileHandle 21h

;funkcja zapisu DOS ;zapis danych do tego pliku ;tyle bajtów ;zaczynamy od tego adresu ;ignorujemy zwracane błędy (!) ;funkcja potwierdzająca DOS ;zapis danych do tego pliku ;ignorujemy zwracany błąd (!)

mov call

NeedIO, 0 RestoreDOS

;gotowy do ponownego startu

PhasesDone: DoIO

ret endp assume ds: nothing

;MyInt2F; ; ; ; ; ; ;

Dostarcza wsparcia int 2Fh (przerwanie równoczesnych procesów) dla tego TSR’a. Przerwanie równoczesnych procesów rozpoznaje następujące pod funkcje (przekazane w AL.): 00 – Weryfikacja obecności. Zwraca 0FFh w AL. i wskaźnik do ID ciągu w es:di jeśli ID TSR’a (w AH) pasuje do tego szczególnego TSR’a

MyInt2F

01 – Usunięcie

Usuwa TSR z pamięci. Zwraca 0 w AL. jeśli pomyślnie 1 w AL. jeśli niepowodzenie

proc far assume ds:nothing cmp je jmp

ah, MyTSRID YepItsOurs OldInt2F

;Pasuje do naszego identyfikatora TSR’a?

;Okay, wiemy ,że to jest nasze ID, teraz sprawdzamy na możliwość funkcji usuwającej YepItsOurs:

IDString TryRmv:

cmp jne mov lesi iret

al., 0 TryRmv al., 0ffh IDString

;weryfikacja funkcji

byte cmp jne

:Keypress Logger TSR”, 0 al, 1 ;wywołanie usuwania IllegalOp

call je mov iret

TstRmvable CanRemove ax, 1

;zwraca z powodzeniem ;powrót do kodu wywołującego

;zobacz czy można usunąć ;skocz jeśli nie można ;teraz zwraca niepowodzenie

;Okay, chcemy usunąć go i możemy usunąć go z pamięci. Zajmiemy się wszystkim tutaj assume ds:nothing CanRemove

push push pusha cli mov mov mov mov

ds. es

mov mov mov mov

ax, word ptr OldInt9 es:[9*4], ax ax, word ptr OldInt9+2 es:[9*4+2], ax

mov mov

ax, word ptr OldInt13 es:[13h*4], ax

;wyłączając przerwania nabroimy w wektorach przerwań ax, 0 es, ax ax, cs ds., ax

mov mov

ax, word ptr OldInt13+2 es:[13h*4+2], ax

mov mov mov mov

ax, word ptr OldInt16 es:[16h*4], ax ax, word ptr OldInt16+2 es:[16h*4+2], ax

mov mov mov mov

ax, word ptr OldInt1C es:[1Ch*4], ax ax, word ptr OldInt1C+2 es:[1Ch*4+2], ax

mov mov mov mov

ax, word ptr OldInt28 es:[28*4], ax ax, word ptr OldInt28+2 es:[28*4+2], ax

mov mov mov mov

ax, word ptr OldInt2F es:[2Fh*4], ax ax, word ptr OldInt2F+2 es:[2Fh*4+2], ax

;Okay, w ten sposób zamknęliśmy plik. Notka: INT 2F nie powinno musieć działać z DOS ponieważ jest to ; funkcja biernego TSR’a mov mov int

ah, 3Eh bx, FileHandle 21h

;polecenia zamknięcia pliku

;Okay, ostatnia rzecz przed wyjściem – Oddajmy zaalokowaną pamięć dla tego TSR’a z powrotem do DOS mov mov mov int mov mov mov int

ds., PSP es, ds.:[2Ch] ah, 49h 21h ax, ds es, ax ah, 49h 21h

popa pop pop mov

es ds. ax, 0

;wskaźnik do bloku środowiska ;DOS zwalnia funkcję pamięci ;przestrzeń zwalnianego kodu programu

;zwrócone z powodzeniem

,Wywołanie z niepoprawnymi wartościami funkcji. Próbujemy zrobić to z jak najmniejszą szkodą IllegalOp: MyInt2F

mov ax, 0 iIret endp assume ds. :nothing

;Kto wie co myślano?

; TstRmvable ;

Sprawdzamy aby zobaczyć czy możemy usunąć ten TSR z pamięci. Zwraca ustawioną flagę jeśli możemy usunąć go, wyzerowaną w przeciwnym razie

TstRmvable

proc cli push

near ds

TRDone: TstRmvable ResidentSeg cseg

mov mov

ax, 0 ds, ax

cmp jne cmp jne

word ptr ds:[9*4], offset MyInt9 TRDone word ptr ds:[9*4 +2], seg MyInt9 TRDone

cmp jne cmp jne

word ptr ds:[13h*4], offset MyInt13 TRDone word ptr ds:[13h*4 +2], seg MyInt13 TRDone

cmp jne cmp jne

word ptr ds:[16h*4], offset MyInt16 TRDone word ptr ds:[16h*4 +2], seg MyInt16 TRDone

cmp jne cmp jne

word ptr ds:[1Ch*4], offset MyInt1Ch TRDone word ptr ds:[1Ch*4 +2], seg MyInt1Ch TRDone

cmp jne cmp jne

word ptr ds:[28h*4], offset MyInt28 TRDone word ptr ds:[28h*4 +2], seg MyInt28 TRDone

cmp jne cmp pop sti ret endp ends

word ptr ds:[2Fh*4], offset MyInt2F TRDone word ptr ds:[2Fh*4 +2], seg MyInt2F ds

segment assume

;SeeIfPresent; SeeIfPresent

IDLoop:

TryNext:

para public ‘code’ cs:cseg, ds: ResidentSeg Sprawdzamy aby zobaczyć czy nas TSR jest już obecny w pamięci. Ustawia flagę zera jeśli jest, wyzerowaną jeśli nie jest

proc push push push mov mov push mov int pop cmp je strcmpl byte je

near es ds. di cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 TryNext

dec

cl

;zaczynamy z ID 0FFh ;funkcja weryfikacji obecności ;obecny w pamięci?

„Keypress Logger TSR”, 0 Success ;testuje ID użytkownika od 80h..FFh

Success:

SeeIfPresent ;FindID; ; ; ; FindID

IDLoop:

Success:

FindID Main

js cmp pop pop pop ret endp

IDLoop cx, 0 di ds es

;zeruje flagę zera

Określamy pierwszy (cóż, ostatni właściwie) ID TSR’a dostępnego w łańcuchu przerwań równoczesnych procesów. Zawraca tą wartość w rejestrze CL Zwraca ustawioną flagę zera jeśli zlokalizuje puste gniazdo Zwraca wyzerowaną flagę zera jeśli niepowodzenie proc push push push

near es ds di

mov mov push mov int pop cmp je dec js xor cmp pop pop pop ret endp

cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 Success cl IDLoop cx, cx cx, 1 di ds. es

;zaczynamy z ID 0FFh ;funkcja weryfikacji obecności ;obecny w pamięci? ;testowanie ID użytkownika od 80h..FFh ;zerowanie flagi zera

proc meminit mov mov

ax, residentSeg ds, ax

mov int mov

ah, 62h 21h PSP, bx

;pobranie wartości PSP tego programu

;Zanim zrobimy cokolwiek. Musimy sprawdzić parametry linii poleceń. Musimy mieć albo poprawną ; nazwę pliku albo polecenie „usuń”. Jeśli usunięcie pojawi się w linii poleceń, wtedy usuwając rezydenta ; kopiujemy z pamięci używając przerwania równoczesnych procesów (2Fh) . Jeśli usunięcia nie ma w linii ;poleceń, będziemy mieli nazwę pliku i lepiej będzie nie kopiować już załadowanego do pamięci argc cmp cx,1 ;musi mieć dokładnie jeden parametr je GoodParamCnt print byte „Usage: „ , cr, lf byte “ KeyEval filename”, cr. Lf byte “ or KeyEval REMOVE”, cr, lf, 0 ExitPgm

; Sprowadzenie dla polecenia REMOVE GoodParamCnt: mov ax,1 argv atricmpl byte “REMOVE”. 0 jne TstPresent call SeeIfPresent je RemoveIt print byte “TSR is not present in memory, cannot rmove” byte cr, lf, 0 ExitPgm RemoveIt:

mov MyTSRID, cl printf byte “Removing TSR (ID #%d) from memory...”,0 dword MyTSRID mov ah, cl mov al, 1 int 2Fh cmp al., 1 je RmvFailure print byte „removed>”, cr, lf, 0 ExitPgm

RmvFailure:

;usunięcie cmd, ah zawiera ID ;powodzenie?

print byte cr, lf byte “Could not remove TSR from memory.”, cr, lf byte “Try removing other TSRs in the reverse order “ byte “you instaled them.”, cr, lf, 0 ExitPgm

;Okay, zobaczmy czy TSR jest już w pamięci. Jeśli tak przerywamy proces instalacji TstPresent:

call jne print byte byte ExtPgm

SeeIfPresent GetTSRID “TSR is already present in memory”, cr, lf “Aborting instalation process”, cr, lf, 0

; Pobieramy ID dla naszego TSR’a i zapisujemy go GetTSRID:

call FindID je GetFileName print byte „Too many resident TSRs, cannot instal”, cr, lf,0 ExitPgm

;sprawdzamy nazwę pliku i otwieramy ten plik GetFileName:

mov printf byte byte

MyTSRID, cl “Keypress logger TSR program”,cr, lf “TSR ID = %d”,cr, lf

byte “Processing file: “, 0 dword MyTSRID puts putcr mov ah, 3Ch mov cx, 0 push ds. push es pop ds. mov dx, di int 21h jnc GoodOpen print byte „DOS error #”, 0 puti print byte „opening file.“, cr,lf,0 ExitPgm GoodOpen: InstallInts:

pop mov

ds FileHandle, ax

print Byte

„Installing interrrupts...”, 0

;tworzymy plik ;normalny plik ;nazwę wskazuje ds.:dx ;otwarcie pliku

;zachowujemy uchwyt pliku

;Aktualizujemy wektory przerwań int 9, 13h, 16h, 1Ch, 28h i 2Fh. Zauważmy ,ze powyższe instrukcje czynią ; ResidentSeg bieżącym segmentem danych, więc możemy przechować starą wartość bezpośrednio w zmiennej ; OldIntxx cli mov mov mov mov mov mov mov mov

;wyłączamy przerwania! ax, 0 es, ax ax, es:[9*4] word ptr OldInt9, ax ax, es:[9*4+2] word ptr OldInt9+2, ax es:[9*4], offset MyInt9 es:[9*4+2], seg ResidentSeg

mov mov mov mov mov mov

ax, es:[13h*4] word ptr OldInt13, ax ax, es:[13h*4+2] word ptr OldInt13+2, ax es:[13h*4], offset MyInt13 es:[13h*4+2], seg ResidentSeg

mov mov mov mov mov mov

ax, es:[16h*4] word ptr OldInt16, ax ax, es:[16h*4+2] word ptr OldInt16+2, ax es:[16h*4], offset MyInt16 es:[16h*4+2], seg ResidentSeg

mov mov mov mov

ax, es:[1Ch*4] word ptr OldInt1C, ax ax, es:[1Ch*4+2] word ptr OldInt1C+2, ax

mov mov

es:[1Ch*4], offset MyInt1C es:[1Ch*4+2], seg ResidentSeg

mov mov mov mov mov mov

ax, es:[28h*4] word ptr OldInt28, ax ax, es:[28h*4+2] word ptr OldInt28+2, ax es:[28h*4], offset MyInt28 es:[28h*4+2], seg ResidentSeg

mov mov mov mov mov mov sti

ax, es:[2Fh*4] word ptr OldInt2F, ax ax, es:[2Fh*4+2] word ptr OldInt2F+2, ax es:[2Fh*4], offset MyInt2F es:[2Fh*4+2], seg ResidentSeg ;włączamy ponownie przerwania

; Jedyna rzecz jaka pozostaje to TSR print byte

„Installed.”, cr, lf, 0

Main cseg

mov sub mov int endp ends

dx, EndResident dx, PSP ax, 3100h 21h

sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

;Obliczamy rozmiar programu :polecenie TSR DOS

Poniżej mamy krótki przykład programu, który czyta dane z pliku tworzony z powyższego programu i tworzy prosty raport o dacie, czasie i naciskaniu klawiszy ;Program ten odczytuje plik stworzony przez program TSR KEYEVAL.EXE . Wyświetla log zawierający dane, ; czas i liczbę naciśnięć klawiszy .xlist .286 include includelib .list dseg

segment

FileHandle

word

?

month day year

byte byte byte

0 0 0

stdlib.a stdlib.lib para public ‘data’

hour minute second KeyStrokes RecSize

byte byte byte word =

dseg

ends

cseg

segemnt para public ‘code’ assume cs: cseg, ds: dseg

,SeeIfPresent ; ; SeeIfPresent

IDLoop:

TryNext: Success:

SeeIfPresent Main

0 0 0 0 $ - month

Sprawdzamy czy nasz TSR jest obecny w pamięci. Ustawiamy flagę zera jeśli jest zeruje jeśli nie jest proc push push pusha mov mov push mov int pop cmp je strcmpl byte je dec js cmp popa pop pop ret endp

near es ds. cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 TryNext

;weryfikujemy funkcję obecności ;Obecny w pamięci?

„Keypress Logger TSR”, 0 Success cl IDLoop cx, 0

;Testujemy ID użytkownika od 80h..FFh ;Zerowanie flagi zera

ds. es

proc meminit mov mov

ax, dseg ds., ax

argc cmp cx, 1 je GoodParmCnt print byte „Usage:”,cr,lf byte “ KEYRPT filename”, cr, lf,0 ExitPgm GoodParmCnt

;Zaczynamy z ID 0FFh

mov argv print byte byte puts

;musi mieć przynajmniej jeden parametr

ax, 1

“Keypress logger report program”, cr, lf “Porcessing file:”,0

putcr mov ah, 3Dh mov al., 0 push ds. push es pop ds. mov dx, di int 21h jnc GoodOpen print byte „DOS error #”, 0 puti print byte ‚ opening file.“, cr, lf,0 ExitPgm GoodOpen:

pop mov

ds FileHandle, ax

;polecenie otwarcia pliku ;otwarcie do odczytu ;ds.:dx wskazuje nazwę ;otwarcie pliku

;zachowanie uchwytu pliku

;Okay, czytamy daną i wyświetlamy ją: ReadLoop:

ReadError: Quit:

Main

mov mov mov mov int jc test je

ah, 3Fh bx, FileHandle cx, RecSize dx, offset month 21h ReadError ax, ax Quit

mov mov mov dtoam puts free print byte

cx, year dl, day dh, month

mov mov mov mov ttoam puts free printf byte dword jmp print Byte

ch, hour cl, minute dh, second dl, 0

;liczba bajtów ;miejsce na umieszczenie danych ;EOF?

“, “, 0

“, keystroke = %d\n”, 0 KeyStorkes ReadLoop “Eror reading file”, cr, lf, 0

mov bx, FileHandle mov ah, 3Eh int 21h ExitPgm endp

;polecenie odczytu pliku

;zamknięcie pliku

cseg

ends

sseg stk sseg

segemnt para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

18. 9 PROGRAMY PÓŁREZYDENTNE Program półrezydentny, jest to program, który czasowo ładuje się do pamięci, wykonuje inny program (proces potomny) a potem usuwa się z pamięci po zakończeniu procesu potomnego. Programy półrezydentne zachowują się jak programy rezydentne podczas wykonywania potomka, ale nie pozostają w pamięci kiedy potomek się kończy. Głównym zastosowaniem programów półrezydentnych jest rozszerzenie istniejących aplikacji lub aktualizację aplikacji (proces potomny). Fajną rzeczą programu półrezydentnego jest to, ze nie musimy modyfikować pliku „.EXE” aplikacji bezpośrednio na dysku. Jeśli z jakiegoś powodu aktualizacja się nie powiodła, nie musimy niszczyć pliku „.EXE”, musimy tylko zlikwidować kod obiektu w pamięci Aplikacja półrezydentna, podobnie jak TSR ma część nierezydentną i rezydentną . Część rezydentna pozostaje w pamięci dopóki wykonuje się proces potomny. Część nierezydentna inicjalizuje program a potem przekazuje sterowanie do części rezydentnej , która ładuje aplikację potomną nad częścią rezydentną. Kod nierezydentny aktualizuje wektory przerwań i robi wszystkie rzeczy jakie robi TSR z wyjątkiem, że nie wykonuje poleceń TSR’a. Zamiast tego, program rezydentny ładuje aplikację do pamięci i przekazuje sterowanie do tego programu. Kiedy aplikacja zwraca sterowanie do programu rezydentnego, wychodzi do DOS używając standardowej funkcji ExitPgm (ah = 4Ch) Kiedy aplikacja jest uruchomiona, kod rezydentny zachowuje się jak inny TSR. Chyba ,że proces potomny jest świadomy programu półrezydentnego, lub program półrezydentny aktualizuje wektor przerwań normalnie używanej aplikacji, program półrezydentny prawdopodobnie będzie aktywnym programem rezydentnym aktualizującym jedno lub więcej przerwań sprzętowych. Oczywiście, wszystkie zasady które obowiązywały przy aktywnych TSR’ach również obowiązują przy aktywnych programach półrezydentnych Poniżej jest bardzo ogólny przykład programu półrezydentnego. Program ten „RUN.ASM”, uruchamia aplikację, której nazwa i parametry linii poleceń pojawiają się jako parametry lini poleceń do uruchomienia. Innym słowy: c> run pgm. Exe parm1 parm2 itd. jest odpowiednikiem pgm parm1 parm1 itd Zauważmy, że musimy dostarczyć rozszerzenie „.EXE” lub „.COM” do nazwy programu. Kod ten zaczyn się od wyodrębnienia nazwy programu i parametrów lini poleceń z uruchomionej lini poleceń. Uruchamia wbudowaną strukturę exec a potem wywołuje DOS do wykonania programu. Przy zwrocie, uruchamia stały stos i wraca do DOS ;RUN.ASM szkielet programu półrezydentnego ; ; Usage: ; RUN ; lub RUN ; ; RUN wykonuje określony program z dostarczonymi parametrami linii poleceń. Początkowo może to wyglądać ; na głupi program. W końcu dlaczego nie uruchomić programu bezpośrednio z DOS i zupełnie przeskoczyć ; RUN? W rzeczywistości jest dobry powód dla RUN – Pozwala nam (przez zmodyfikowanie pliku źródłowego ; RUN) ustawić środowisko wcześniej uruchomionego programu i zeruje to środowisko po zakończeniu ; programu („środowisko” w tym sensie nie koniecznie odnosi się do obszaru środowiska MS-DOS).

; ; Na przykład, mamy użyć tego programu do przełączenia do trybu TSR wcześniej wykonywanego pliku EXE ; a potem przywrócić tryb działania tego TSR’a po zakończeniu programu. ; ; Ogólnie, możemy stworzyć nową wersję RUN.EXE (i, prawdopodobnie, daje unikalny numer) dla każdej aplikacji dla jakiej chcemy użyć tego programu ; ; ;-----------------------------------------------------------------------------------------------------------------------------; ; ; wkładamy ten segment jako pierwszy ponieważ chcemy załadować podprogramy Biblioteki Standardowej jako ; ostatnie w pamięci, więc podciągają pod część nierezydentną CSEG CSEG SSEG SSEG ZZZZZZZSEG ZZZZZZZSEG

segment para public ‘CODE’ ends segemnt para stack ‘stack’ ends segment para public ‚zzzzzzseg’ ends

; zawieramy makra Biblioteki Standardowej UCR include include include include include include

consts.a stdin.a stdout.a misc.a memory.a strings.a

includelib CSEG

stdlib.lib

segment para public ‘CODE’ assume cs:cseg, ds:cseg

; Zmienne używane przez ten program ; struktura EXEC MS-DOS ExecStruct

dw dd dd dd

0 CmdLine DfltFCB DfltFCB

DfltFCB CmdLine PgmName

db db dd

3, „ „, 0, 0 ,0 ,0 ,0 0, 0dh, 126 dup („ „); ?

Main

proc mov mov

ax, cseg ds., ax

Meminit

;linia poleceń dla programu ;wskazuje nazwę programu ;pobranie wskaźnika do segmentu zmiennych ;start menadżera pamięci

;jeśli chcemy zrobić coś zanim wykonamy linię poleceń określonego programu , tu jest dobre miejsce na to

; -----------------------------------------------------------------------------------------------------------------

; Teraz pobieramy nazwę programu, itp., z linii poleceń i wykonujemy go argc or jz mov argv mov mov

;zobaczmy ile parametrów lini poleceń mamy cx, cx Quit

;wychodzimy kiedy brak

ax, 1

;obranie pierwszego parametru (nazwa programu)

word ptr PgmName, di ;zachowujemy wskaźnik do nazwy word ptr PgmNanme+2, es

;Okay, dla każdego słowa w lini poleceń po nazwie pliku, kopiujemy to słowo do bufora CmdLine i ; oddzielamy każde słowo spacją, podobnie jak COMMAND.COM robi a parametrami lini poleceń w ; procesie ParmLoop:

Cpylp:

StrDone:

lea dec jz

si, CmdLine+1 cx ExecutePgm

;indeks do cmdline

inc argv

ax

;wskazuje następny parametr

push mov inc inc mov cmp je inc mov inc inc jmp

ax byte ptr [si], ‘ ‘ CmdLine si al., es:[di] al, 0 StrDone CmdLine ds.:[si], al. si di CpyLp

mov pop jmp

byte ptr ds:[si], cr ax ParmLoop

; pierwsza pozycja i separator w linii

;zwiększenie bajtu

;w tym przypadku jest koniec ;pobranie parametru #

;Okay, zbudujemy strukturę wykonującą MS-DOS i konieczną linię poleceń, teraz zobaczymy uruchamianie ; programu. Pierwszy krok to zwolnienie całej pamięci, której ten program nie uzywa. ExecutePgm:

mov int mov mov sub mov mov int

ah, 62h 21h es, bx ax, zzzzzzseg ax, bx bx, ax ah, 4ah 21h

;pobranie wartości naszego PSP ;obliczamy rozmiar kodu rezydentnego ;udostępnienie nieużywanej pamięci

;Ostrzeżenie! Żadnej funkcja Biblioteki Standardowej po tym punkcie. Udostępniamy pamięć, którą ;one tu sytuują. mov mov mov lds mov int

bx, seg Execstruct es, bx bx, offset ExecStruct dx, PgmName ax, 4b00h 21h

;wskaźnik do rekordu programu ;exec pgm

; Kiedy wrócimy, nie możemy liczyć ,ż będzie poprawnie. Najpierw, stały wskaźnik stosu a potem możemy ; zakończyć wszystko co mus być zrobione mov mov mov mov mov

ax, sseg ss, ax sp, offset EndStk ax, seg cseg ds, ax

;Okay, jeśli mamy wykonać jakąś wielką rzecz po programie, jest to dobre miejsce do wstawienia takiego czegoś ; ; -------------------------------------------------------------------------------------------------; ; Zwrócenie sterowania do MS-DOS Quit: Main cseg

ExitPgm endp ends

sseg

segment para stack ‘stack’ dw 128 dup (0) dw ? ends

endstk sseg

; zarezerwowanie jakieś miejsca dla sterty zzzzzzseg Heap zzzzzseg

segment para public ‘zzzzzzseg’ db 200h dup ( ?) ends end Main

Ponieważ RUN.ASM jest raczej prosty, być może przyda się przykład bardziej złożony. Poniżej znajduje się w pełni funkcjonalna aktualizacja dla gry XWING™ firmy Lucasart. Motywacją tej aktualizacji była nieustanna irytacja koniecznością podawania hasła za każdym razem kiedy chce się zagrać w grę. Ta małą aktualizacja przeszukuje kod, który wywołuje podprogram hasła i przechowuje NOP’y ponad kodem w pamięci. Działanie tego kodu jest trochę trudniejsze niż RUN.ASM. Program RUN wysyłał polecenie wykonania do DOS, który uruchamiał żądany program Wszystkie zmiany systemowe jakich RUN potrzebuje wykonać musi być zrobione przed lub po wykonaniu aplikacji. XWPATCH działa trochę inaczej. Ładuje program XWING.EXE do pamięci i przeszukuje jakiś określony kod (wywołujący podprogram hasła). Ponieważ znajduje ten kod, przechowuje instrukcje NOP na szczycie tego kodu. Niestety, życie nie jest tak całkiem proste. Kiedy XWING.EXE się ładuje, kod hasła nie jest jeszcze obecny w pamięci. XWING ładuje ten kod później jako nakładkę. Więc program XWPATCH znajduje coś co XWING.EXE ładuje natychmiast do pamięci – kod joysticka. Z tego punktu widzenia, XWPATCH jest po prostu wchłania przestrzeń pamięci ; XWING nigdy nie wywoła go ponownie dopóki XWING się nie zakończy ;XWPATCH.ASM ; ; Użycie: ; XWPATCH musi być w tym samym katalogu co XWING.EXE ; ; Program ten wykonuje program XWING.EXE i aktualizuje go tak, aby unikać wprowadzania ; hasła za każdym razem gdy go uruchamiamy. ; ; Program ten jest przedstawiony tylko do celów edukacyjnych. Jest on demonstracją tego jak napisać ; półrezydentny program. Nie jest intencją zachęcenie do piracenia programów komercyjnych ; takie zastosowani jest nielegalne i ścigane przez prawo. ; Program jest oferowany bez gwarancji na poprawne działanie. W związku z dynamiczną naturą ; projektowania, program który aktualizuje inny program może nie pracować z drobną zmianą w ; aktualizowanym programie (XWING.EXE). UŻYWASZ TEGO KODU NA WŁASNE RYZYKO. ;

;-----------------------------------------------------------------------------------------------------------------------------byp wp

textequ texteequ

; Wkładamy tu definicję segmentu ponieważ Biblioteka Standardowa UCR będzie ładowana po zzzzzzseg ; (w sekcji rezydentnej). cseg cseg

segment para public ‘CODE’ ends

sseg sseg

segment para stack ‘STACK’ ends

zzzzzzseg zzzzzzseg

segment para public ‘zzzzzzseg’ ends .286 include includelib

CSEG

stdlib.a stdlib.lib

segment para public ‘CODE’ assume cs:cseg, ds :nothing

;CountJSCalls – Liczba razy wywołań xwing przez kod Joystick zanim zaktualizujemy wywołanie hasła. CountJSCalls

dw

250

;PSP - Przedrostek Segmentu Programu . Musimy zwolnić pamięć zanim uruchomimy program ; rzeczywisty PSP

dw

0

;Program ładuje struktury danych (dla DOS) ExecStruct

LoadSSSP LoadCSIP PgmName DfltFCB CmdLine Pgm

dw dd dd dd dd dd dd db db db

0 CmdLine DfltFCB DfltFCB ? ? Pgm 3, “ ,” , 0 ,0 ,0 ,0 ,0 2, “ “, 0dh, 16 dup (“ “) ‘XWING.EXE”, 0

;Linia poleceń dla programu

;***************************************************************************************** ; XWPATCH zaczyn się tutaj. Jest to część rezydentna pamięci. Wkładamy tu kod, który musi być obecny w ; czasie wykonania lub musi być rezydentny po zwolnieniu pamięci. ;***************************************************************************************** Main

proc mov mov mov

cs:PSP, ds. ax, cseg ds, ax

mov ax, zzzzzzseg mov es, ax mov cx, 1024/16 meminit2

;pobranie wskaźnika do segmentu zmiennych

; Teraz, zwalniamy pamięć ZZZZZZSEG czyniąc miejsce dla XWING ;Notka: Absolutnie nie wywołujemy podprogramów Biblioteki Standardowej z tego punktu! (ExitPgm jest ; OK., jest to makro które wywołuje DOS) Zauważmy, że po wykonaniu tego kodu, żaden kod ani dane z ;zzzzzzseg nie jest poprawny mov sub inc mov mov int jnc

bx, zzzzzzseg bx, PSP bx es, PSP ah, 4ah 21h GoodRealloc

;Okay, skłamałem. Tu jest wywołanie StdLib, ale jest OK. ponieważ nie zdołamy załadować aplikacji na szczyt ;kodu biblioteki standardowej. Ale od tego punktu absolutni żadnych więcej wywołań! print byte byte jmp

„Memory allocation error” cr, lf, 0 Quit

GoodRealloc: ;Teraz ładujemy program XWING do pamięci: mov mov mov lds mov inc jc

bx, seg ExecStruct es, bx bx, offset ExecStruct dx, PgmName ax, 4b01h 21h Quit

;wskaźnik do rekordu programu ;ładownie , nie exec, pgm jeśli błąd ładujemy plik

;Niestety, kod hasła jest ładowny dynamicznie później. Więc nie ma go nigdzie w pamięci, gdzie moglibyśmy ; go przeszukać. Ale wiemy, że kod joysticka jest w pamięci, więc przeszukujemy ten kod. Kiedy go znajdziemy ; zaktualizujemy go tak, że wywoła nasz program SearchPW Zauważmy, że musimy użyć joysticka (i mieć ;zainstalowany) aby ta łatka działała poprawnie mov mov xor

si, zzzzzzseg ds., si si, si

mov mov mov mov call jc

di, cs es, di di, offset JoystickCode cx, JoyLength FindCode Quit

;jeśli nie znaleziono kodu joysticka

;Aktualizujemy tu kod joysticka XWING mov mov mov

byp ds.:[si], 09ah wp ds.:[si+1] wp ds:[si+3], cs

;dalekie wywołani ;offset SearchPW

;Okay ,zaczynamy uruchamianie programu XWING.EXE mov int mov

ah, 62 21h ds., bx

;pobranie PSP

mov mov mov mov mov jmp Quit: Main

es, bx wp ds:[10] , offset Quit wp ds:[12], cs ss wp cseg:LoadSSSP+2 sp wp cseg:LoadSSSP dword ptr cseg:LoadCSIP

ExitPgm endp

; SearchPW pobieranie wywołanie z XWING, kiedy próbuje skalibrować joystick. Wywołujemy z XWING ; joystick kilkaset razy zanim w rzeczywistości wyszukamy kod hasła. Powód zrobienia jest taki, że XWING ; wywołuje kod joysticka wcześniej przy teście na obecność joysticka. Kiedy wchodzimy do kodu kalibracji ; wywołujemy odpowiednio kod joysticka, więc kilkaset wywołań nie będzie bardzo długo tracić ważności ; Kiedy jesteśmy w kodzie kalibracji, kod hasła będzie załadowany do pamięci, więc możemy go tam ; przeszukać SearchPW

proc cmp je dec sti neg neg ret

far cs: CountJSCalla, 0 DoSearch cs: CountJSCalls ; kod jaki wykradliśmy z xwing do aktualizacji bx di

;Okay, przeszukujemy kod hasła DoSearch:

push mov push push pusha

bp bp, sp ds es

;Przeszukujemy kod hasła w pamięci: mov mov xor

si, zzzzzzseg ds., si si, si

mov mov mov mov call jc

di, cs es, di di, offset PasswordCode cx, PWLength FindCode NotThere

;nieśli nie znaleziono kodu hasła

; Aktualizujemy kod hasła XWING tutaj. Najpierw przechowujemy NOP’y ponad pięcioma bajtami ; dalekiego wywołania podprogramu hasła mov mov mov mov mov

byp byp byp byp byp

ds:[si+11], 090h ds:[si+12], 090h ds:[si+13], 090h ds:[si+14], 090h ds:[si+15], 090h

;wyNOPowianie dalekiego wywołania

;Modyfikujemy adres powrotny i przywracamy zaktualizowany kod joysticka aby nie przeszkadzał skokom NotThere:

sub les

word ptr [bp+2], 5 bx, [bp+2]

;zrobienie kopi adresu powrotnego ;pobranie adresu powrotnego

;Zachowanie oryginalnego kodu joysticka po wywołaniu aktualizacji tego podprogramu mov mov mov mov mov mov

SearchPW

popa pop pop pop ret endp

ax, word ptr JoyStickCode es:[bx], ax ax, word ptr JoyStickCode+2 es:[bx+2], ax al, byte ptr JoyStickCode +4 es:[bx+4], al es ds bp

;***************************************************************************************** ; ; FindCode: na wejściu, ES:DI wskazują na jakiś kod w *tym* programie, który pojawia się w grze ; XWING. DS.:SI wskazują n blok pamięci w grze XWING. FindCode przeszukuje całą ; pamięć aby znaleźć podejrzany kawałek kodu i zwraca w DS.:SI wskazują początek tego ; kodu. Ten kod zakłada, że znajdzie kod! ; Zwraca wyzerowaną flagę przeniesienia jeśli go znajdzie, ustawioną jeśli nie ; FindCode

proc push push push

near ax bx dx

DoCmp: CmpLoop:

mov push push push cmpsb pop pop pop je inc dec jne sub mov inc mov cmp jb

dx, 1000h di si cx

;Szukanie w 4kb blokach ;zachowanie wskaźnika do kodu porównującego ;zachowanie wskaźnika do początku ciągu ;zachowanie licznika

cx si di FoundCode si dx CmpLoop si, 1000h ax, ds ah ds, ax ax, 9000h DoCmp

;zatrzymanie pod adresem 9000:0 ;i niepowodzenie jeśli nie znaleziono

pop pop pop stc ret

dx bx ax

pop pop pop clc ret

dx bx ax

repe

FoundCode:

FindCode

endp

;***************************************************************************************** ; ; Wywołanie kodu hasła, który pojawia się w grze XWING. Jest to zazwyczaj dana, której mamy zamiar ; poszukać w kodzie obiektu XWING PasswordCode

PasswordCode EndPW: PWLength

proc call mov mov push push byte endp

near $-47h [bp-4], ax bp-2], dx dx ax 9ah, 04h, 00

=

EndPW – PasswordCode

; Poniżej mamy kod joysticka, który mamy zamiar przeszukać JoyStickCode

proc sti neg neg pop pop pop ret mov in mov not and jnz in

near bx di bp dx cx bp, bx al., dx bl, al. al al, ah $+11h al, dx

JoystickCode EndJSC:

endp

JoyLength cseg

= ends

sseg

segment para stack ‘STACK’ dw 256 dup (0) dw ? ends

endstk sseg zzzzzzseg Heap Zzzzzzseg

EndJSC – JoystickCode

segment para public ‘zzzzzzseg’ db 1024 dup (0) ends end Main

18.10 PODSUMOWANIE Programy rezydentne dostarczają małej ilości wielozadaniowości pojedynczych zadań w świecie DOS’a. DOS dostarcza wsparcia dla programów rezydentnych w całym elementarnym systemie zarządzania pamięcią. Kiedy aplikacja korzysta z funkcji TSR, DOS modyfikuje wskaźnik pamięci aby zarezerwowana przestrzeń pamięci przez kod TSR’a był chroniony przed działaniami załadowanych w przyszłości programów. Po więcej szczegółów dotyczących tego procesu zobacz:

*”Używanie pamięci przez DOS i TSR’y” TSR posiada dwie podstawowe formy: aktywną i bierną. Bierne TSR’y nie są samo aktywujące. Pierwszoplanowa aplikacja musi wywołać podprogram pasywnego TSR’a a by go aktywować. Generalnie, interfejs aplikacji pasywnego TSR’a używa mechanizmu przerwań kontrolowanych 80x86 (przerwania programowe). Uruchomienie aktywnego TSR, z drugiej strony, nie zależy od pierwszoplanowej aplikacji. Zamiast tego, łączą się one z przerwaniami sprzętowymi, które aktywują je niezależnie od procesu pierwszoplanowego. *”Aktywne i pasywne TSR’y” Natura aktywnych TSR’ów wprowadza wiele problemów zgodności. Podstawowym problemem jest to, że aktywny TSR może chcieć wywołać podprogram DOS’a lub BIOS’a, mając właśnie przerwany jeden z tych systemów. Stwarza to problem ponieważ DOS i BIOS nie są współbieżne. Na szczęście, MS-DOS dostarcza kilka punktów zaczepienia, które dają aktywnym TSR’om zdolność do planowania wywołań DOS kiedy DOS jest nieaktywny. Chociaż podprogramy BIOS nie dostarczają takiej samej zdolności, łatwo jest dodać otoczkę wokół wywołania BIOS pozwalająca nam planować poprawnie wywołania. Dodatkowym problemem z DOS jest to, że aktywny TSR może przeszkadzać jakimś globalnym zmiennym używanym przez proces pierwszoplanowy. Na szczęście DOS pozwala TSR’om zachować i przywracać te wartości, zapobiegając złośliwym problemom zgodności. *„Współbieżność” *”Problemy współbieżności z DOS” *”Problemy współbieżności z BIOS” *„Problem współbieżności z innym kodem” *”Inne zagadnienia związane z DOS” MS-DOS dostarcza specjalnego przerwania do koordynowania komunikacji pomiędzy TSR’ami a innymi aplikacjami. Przerwanie równoczesnych procesów pozwala nam łatwo sprawdzić obecność TSR’a w pamięci, usuwać TSR z pamięci , lub przekazywać różne informacje pomiędzy TSR’em a aktywną aplikacją. *”Przerwanie równoczesnych procesów (INT 2Fh) Cóż, pisanie TSR’ów trzyma się surowych zasad. W szczególności, dobry TSR zachowuje pewne konwencje podczas instalacji i zawsze dostarcza użytkownikowi bezpiecznego mechanizmu usunięcia całej wolnej pamięci będącej w użyciu przez TSR. W tych rzadkich przypadkach gdzie TSR nie może się sam usunąć, zawsze pokazuje właściwy błąd i instrukcję jak rozwiązać problem. Po więcej informacji o ładowaniu i usuwaniu TSR’ów zajrzyj *”Instalowanie TSR’ów” *”Usuwanie TSR’ów” *”TSR monitora klawiatury” Program półrezydentny jest programem, który jest rezydentny podczas wykonywania jakiegoś określonego programu. Sam automatycznie wyładowuje się kiedy kończy się aplikacja. Aplikacja półrezydentna znajduje aplikację jako program zaktualizowany i „TSR’em czasowo udostępnionym” *”Programy półrezydentne”

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG

HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK Konsultacje naukowe: NEKRO [email protected] [email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ DZIEWIĘTNASTY: PROCESY, WSPÓŁPROGRAMY I WSPÓŁBIEŻNOŚĆ Kiedy większość ludzi mówi wielozadaniowość , zazwyczaj w znaczeniu zdolności do uruchamiania kilku różnych aplikacji w tym samym czasie na jednej maszynie. Dana struktura oryginalnych chipów 80x86 i projektowania oprogramowania MS-DOS jest trudna do osiągnięcia, kiedy jest uruchomiony DOS. Przypatrzymy się jak Microsoft używa Windows do wielozadaniowości. Po problemach dużych firm takich jak Microsoft z działaniem wielozadaniowości, możemy sądzić,że jest to bardzo trudna rzecz do zarządzania. Jednak nie jest to prawda, Microsoft ma problemy próbując uczynić różne aplikacje, które są nieświadome innych harmonijnych prac. Szczerze mówiąc nie mają one istniejących aplikacji DOS działających dobrze w ramach wielozadaniowości. Zamiast tego, działają na rzecz rozwijania nowych programów, które działają dobrze pod Windowsem. Wielozadaniowość nie jest sprawą błahą, ale nie jest tak trudna, kiedy piszemy aplikację z wielozadaniowością. Możemy nawet pisać programy, które są wielozadaniowe pod DOS’em, jeśli tylko zabezpieczymy się kilkoma środkami ostrożności. W tym rozdziale będziemy omawiali koncepcję procesów DOS, współprogramy i ogólnie procesy. 19.1 PROCESY DOS Chociaż MS-DOS jest jednozadaniowym systemem operacyjnym, nie znaczy to, że może być tylko jeden program w tym czasie w pamięci. Faktycznie głównym celem poprzedniego rozdziału było opisanie jak ulokować dwa lub więcej działających w pamięci w tum samym czasie. Jednakże, nawet, jeśli zignorujemy początkowo TSR’y, możemy jeszcze załadować kilka programów do pamięci w jednym czasie pod DOS’em. Jedyną pułapką jest to, że DOS dostarcza zdolności im do działania tylko jednego w czasie w bardzo określony sposób. Chyba, że procesy współpracują, ich profil wykonania podąża za bardzo ścisłym wzorcem. 19.1.1 PROCESY POTOMNE W DOS Kiedy aplikacja DOS jest uruchomiona, można załadować i wykonywać jakiś inny program używając funkcji EXEC DOS (zobacz „MS-DOS, PC-BIOS i I/O Plików”). W normalnych warunkach, kiedy aplikacja (macierzysta) uruchamia drugi program (potomny), proces potomny wykonuje się do zakończenia i potem wraca do macierzystej. Jest to bardzo podobne do wywołania procedury, z wyjątkiem tego, że jest trochę trudniejsze przekazanie parametrów między nimi. MS-DOS dostarcza kilka funkcji, jakie możemy zastosować do ładowania i wykonania kod programu, kończymy proces i uzyskujemy stan wyjścia dla procesu. Poniżej mamy tablicę wielu z tych operacji. Funkcja (AH) 4Bh

Parametry wejściowe

Parametry wyjściowe

Opis

al – 0 ds:dx – wskaźnik do nazwy programu es:bx – wskaźnik do struktury LOADEXEC

Ax – kod błędu, jeśli ustawione przeniesienie

Ładuje i wykonuje program

al. –1 ds:dx – wskaźnik do nazwy programu es:bx – wskaźnik do struktury LOAD al.- 3 ds:dx – wskaźnik do nazwy programu es:bx – wskaźnik do struktury OVERLAY al. – proces zwracania kodu

4Bh

4Bh

4Ch 4Dh

ax – kod błędu jeśli ustawione

Ładuje program

ax – kod błędu jeśli ustawione przeniesienie

Ładownie nakładki

Wykonanie zakończenia al – zwracana wartość ah – metoda zakończenia

Tabela 67: Funkcje DOS zorientowane znakowo

19.1.1.1 ZAŁADUJ I WYKONAJ Funkcja „załaduj i wykonaj” wymaga dwóch parametrów. Pierwszy w ds.:dx, jest wskaźnikiem do ciągu zakończonego zerem zawierającego ścieżkę dostępu programu do wykonania. To musi być plik „.COM” lub „.EXE” a ciag musi zawierać rozszerzenie nazwy programu. Drugi parametr, w es:bx, jest wskaźnikiem do struktury danych LOADEXEC. Ta struktura danych przybiera postać: LOADEXEC EnvPtr CmdLinePtr FCB1 FCB2 LOADEXEC

struct word dword dword dword ends

? ? ? ? ?

;wskaźnik do obszaru środowiska ;wskaźnik do lini poleceń ;wskaźnik do domyślnego FCB1 ; wskaźnik do domyślnego FCB2

EnvPtr jest adresem segmentu bloku środowiska DOS stworzonego dla nowej aplikacji. Jeśli to pole zawiera zero, DOS tworzy kopię aktualnego bloku środowiska procesu dla procesu potomnego. Jeśli program, jaki jest uruchomiony nie uzyskuje dostępu do bloku środowiska, możemy zachować kilkaset bajtów do kilku kilobajtów przez wskazanie wskaźnika pola środowiska dla ciągu czterech zer. Pole CmdLinePtr zawiera adres lini poleceń dostarczonych do programu. DOS będzie kopiował linię poleceń do offsetu 80h w nowym PSP stworzonym dla procesu potomnego. Poprawna linia poleceń składa się z bajtu zawierającego licznik znaku, mniej ważną przestrzeń, znak należący do linii poleceń i kończący znak powrotu karetki (0Dh). Pierwszy bajt powinien zawierać długość znaków ASCII w linii poleceń, nie wliczając powrotu karetki. Jeśli ten bajt zawiera zero, wtedy drugi bajt linii poleceń powinien być powrotem karetki, nie przestrzeni. Przykład: MyCmdLine

byte

12, „file1, file2”, cr

Pola FCB1 I FCB2 muszą wskazywać dwa domyślne bloki kontrolne pliku dla tego programu. FCB’y stały się przestarzałe wraz z DOS’em 2.0, ale Microsoft zachowała FCB’y dla kompatybilności. Dla większości programów możemy wskazać oba te pole w następującym ciągu bajtów: DfltFCB

byte·3, ‘ „, 0, 0 ,0 ,0

Funkcja załaduj i wykonaj będzie zakończona niepowodzeniem, jeśli jest niewystarczająca ilość pamięci dla załadowania procesu potomnego. Kiedy tworzymy plik”..EXE’ używając MASM’a, tworzy on plik wykonywalny, który przechwytuje całą dostępną pamięć, domyślnie. Dlatego też, jeśli nie będzie dostępnej pamięci dla procesu potomnego DOS zawsze zwróci błąd. Dlatego też musimy ustawić alokację pamięci dla procesu macierzystego zanim spróbujemy uruchomić proces potomny. Jak to zrobić opisuje sekcja „Programu półrezydentne” Są inne możliwe błędy. Na przykład, DOS może nie być zdolny zlokalizować nazwy programu, jaka określiliśmy ciągiem zakończonym zerem. Lub być może, jest zbyt dużo otwartych plików i DOS nie ma wolnego

dostępnego bufora dla I/O pliku. Jeśli wystąpi błąd, DOS zwróci ustawioną flagę przeniesienia i właściwy kod błędu w rejestrze ax. Następujący przykład wykonuje program „COMMAND.COM”, pozwalając użytkownikowi wykonywać polecenia DOS z wnętrza naszej aplikacji. Kiedy użytkownik wpisuje, „exit” w lini poleceń DOS, DOS zwróci sterownie do naszego programu. ; RUNDOS.ASM- Demonstruje jak wywołać kopię COMMAND.COM, interpretera lini poleceń DOS z naszego programu

dseg

include includelib

stdlib.a stdlib.lib

segment

para public ‘data’

; Struktura EXEC MS-DOS ExecStruct

word dword dword dword

0 CmdLine DfltFCB DfltFCB

DfltFCB CmdLine PgmName

byte

3, „ „ , 0, 0 ,0 ,0 byte 0, 0dh dword filename

filename

byte

dseg

ends

cseg

segemnt para public ‘code’ assume cs:cseg, ds:dseg

Main

proc mov mov

;używamy bloku środowiska macierzystego ; dla parametrów lini poleceń

;linia poleceń dla programu ;wskazuje nazwę programu

„C:\command.com”, 0

ax, dseg ds., ax

meminit

;pobranie wskaźnika do segmentu zmiennych ;start menadżera pamięci

; Okay, zbudowaliśmy strukturę wykonawczą MS-DOS i potrzebną linię poleceń, teraz zobaczmy uruchamianie programu ; Pierwszym krokiem jest zwolnienie całej pamięci, której ten program nie używa. To będzie wszystko od zzzzzzseg. ; ; Notka: podobnie jak w poprzednich przykładach w innych rozdziałach, jest okay wywoływać podprogramy Biblioteki ; Standardowej w tym programie po zwolnieniu pamięci. Różnica tu jest to, że podprogramy Biblioteki Standardowej są ; ładowane wcześniej w pamięci i nie możemy zwolnić pamięci , która jest tam usytuowana. mov int mov mov uruchomieniowego sub mov mov int

ah, 62h 21h es, bx ax, zzzzzzseg ax, bx bx, ax ah, 4ah 21h

;pobranie wartości naszego PSP ;obliczenie

rozmiaru

rezydentnego

; zwolnienie nie używanej pamięci

kodu

; powiedzenie użytkownikowi co się dzieje: print byte byte byte byte

cr, lf „RUNDOS – Wykonanie kopii command.com”, cr, lf „Wpisanie ‘EXIT’ zwraca sterowanie do RUN.ASM”, cr, lf 0

; Ostrzeżenie! Żadnej funkcji Biblioteki Standardowej po tym punkcie. Zwolniliśmy pamięć, którą one zajmowały. Więc ; ładując program zlikwidujemy kod Biblioteki Standardowej mov bx, seg ExecStruct mov es, bx mov bx, offset ExecStruct ;wskaźnik do rekordu programu lds dx, PgmName mov ax, 4b00h ;exec pgm int 21h ; w MS-DOS 6.0 poniższy kod nie jest wymagany. Ale w starszych wersjach MS-DOS, stos jest rujnowany od tego punktu. ; będzie bezpieczniej, jeśli zresetujemy wskaźnik stosu do przyzwoitego miejsca w pamięci. ; ;Zauważmy, że kod ten zachowuje flagę przeniesienia a wartość w rejestrze AX, więc możemy przetestować warunki błędu dla ; DOS kiedy wykonaliśmy poprawiani stosu mov mov mov mov mov ;Test dla błędu DOS: jnc print byte puti print byte byte jmp

bx, sseg ss, ax sp, offset EndStk bx, seg dseg ds, bx GoodCommand „DOS error #”, 0 „ kiedy próbujesz uruchomić COMMAND.COM”, cr, lf 0 Quit

;Wydruk wiadomości końcowej GoodCommand: print byte byte byte

„Witamy ponownie w RUNDOS. Mam nadzieję, że bawiłeś się dobrze”, cr, lf „Teraz wracamy do COMMAND.COM w wersji MS-DOS” cr, lf, lf,0

; Zwrócenie sterowania do MS-DOS Quit: Main cseg

ExitPgm endp ends

sseg

segment para stack ‘stack’ dw 128 dup (0) ends

zzzzzzseg Heap Zzzzzzseg

segment para public ‘zzzzzzseg’ db 200h dup (?) ends end Main

19.1.1.2 ŁADOWANIE PROGRAMU Funkcja ładowania i wykonywania daje procesowi macierzystemu bardzo małą kontrolę nad procesem potomnym. Chyba, że potomek komunikuje się z procesem macierzystym poprzez przerwania kontrolowane lub przerwania, DOS zawiesza proces macierzysty dopóki nie zakończy się potomek. W wielu przypadkach pogram macierzysty może chcieć załadować kod aplikacji a potem wykonać jakąś dodatkowe działanie zanim przejmie to proces potomny. Programy półrezydentne, pojawiające się w poprzedni rozdziale, dostarczają dobrych przykładów. Funkcja DOS’a „ładowania programu” dostarcza tej zdolności; będzie ładować program z dysku i zwraca sterowanie z powrotem do procesu macierzystego. Proces macierzysty może robić co konieczne jeśli jest to właściwe przed przekazaniem sterowania do procesu potomnego. Funkcja ładowania programu wymaga parametrów, które są bardzo podobne do funkcji ładowania i wykonania. Faktycznie , jedyną różnica jest użycie struktury LOAD zamiast LOADEXEC, a nawet te struktury są bardzo podobne do siebie. Struktura danych LOAD dołącza dwa nowe pola nie obecne w strukturze LAODEXEC: LOAD EnvPtr CmdLinePtr FCB1 FCB2 SSSP CSIP LOAD

struct word dword dword dword dword dword ends

? ? ? ? ? ?

;wskaźnik do obszaru środowiska ;wskaźnik do lini poleceń ;wskaźnik do domyślnego FCB1 ;wskaźnik do domyślnego FCB2 ; wartość SS:SP dla procesu potomnego ;Inicjalizacja programu z punktu startowego

Polecenie LOAD jest użyteczna dla wielu celów. Oczywiście, funkcja ta dostarcza podstawowego narzędzia dla tworzenia programów półrezydentnych’; jednakże, jest również całkiem użyteczne przy odzyskiwaniu dodatkowego błędu,przeadresowywania aplikacji I/O i ładowanie kilku wykonywalnych procesów do pamięci dla współbieżnego wykonania. Po załadowaniu programu poleceniem load DOS’a, możemy uzyskać adres PSP dla tego programu przez wydanie przez DOS funkcji pobrania adresu PSP (zobacz „MS-DOS, PC-BIOS i pliki I/O”). Pozwoliłoby to procesowi macierzystemu na zmodyfikowanie jakiejś wartości pojawiającej się w PSP procesu potomnego przed jego wykonaniem. DOS przechowuje adres zakończenia dla procedury w PSP. Jeśli nie zmieniasz tej lokacji, program będzie wracał do pierwszej instrukcji poza instrukcja int 21h dla załadowanej funkcji. Dlatego też przed rzeczywistym przekazaniem sterowania do aplikacji użytkownika, powinniśmy zmienić ten adres zakończenia. 19.1.1.3 ŁADOWANIE NAKŁADEK Wiele programów zawiera bloki kodu, które są niezależne jeden od drugiego, to znaczy, jeśli podprogram w jednym bloku kodu się wykonuje, program ni będzie wywoływał podprogramów w innym bloku kodu. Na przykład, nowoczesne gry mogą zawierać jakiś kod inicjalizujący obszar „publicznego udostępnienia” gdzie użytkownik wybiera pewne opcje, „obszar działania” gdzie użytkownik gra w grę i „obszar wypytywania”, który sprawdza działania gracza. Kiedy uruchomimy maszynę w 640 k MS-DOS ,cały ten kod może nie zmieścić się w dostępnej pamięci w tym samym czasie. Dla pokonania tego ograniczenia pamięci, wiele dużych programów używa nakładek. Nakładka jest części kodu programu, która dzieli pamięć dla jego kodu z innymi modułami kodu. Funkcja DOS’a ładowania nakładek dostarcza wsparcia dla dużych programów, które muszą używać nakładek.

Podobnie jak funkcje ładowania i ładowania / wykonania, ładowanie nakładek oczekuje wskaźnika do kodu ścieżki dostępu pliku w parze rejestrów ds:dx i adresu struktury danych w parze rejestrów es:bx. Struktura danych nakładki ma następujący format overlay struct StartSeg word ? Relocfactor word 0 Overlay ends Pole StartSeg zawiera adres segmentu gdzie chcemy, aby DOS załadował program. Pole RelocFactor zawiera stałą przemieszczenia. Wartość ta powinna być zerem., chyba , że chcemy aby offset startowy segmentu był inny niż zero. 19.1.1.4 ZAKOŃCZENIE PROCESU Funkcja zakończenia procesu jest niczym nowym dla nas teraz, używamy tej funkcji ciągle i ciągle, jeśli napisaliśmy jakiś program asemblerowy i uruchamiamy go pod DOS’em (makro Biblioteki Standardowej ExitPgm wykonuje to polecenie) W sekcji tej zobaczymy dokładnie jak pracuje funkcja kończenia procesu. przede wszystkim, funkcja zakończenia procesu daje nam umiejętność przekazywania pojedynczego bajtu kodu zakończenia z powrotem do procesu macierzystego. Jakąkolwiek wartość przekazujemy w al do zakończenia staje się kodem zwrotnym lub zakończenia. Proces macierzysty może testować wartość używając funkcji Get Child Process Return Value (zobacz następną sekcję). Możemy również przetestować wartość zwracaną w pliku wsadowym DOS’a używając instrukcji „if errorlevel” Polecenie zakończenia procesu robi co następuje: • • • • •

Opróżnia bufory plików i zamyka pliki Przywrócenie adresu zakończenia (int 22h) z offsetu 0Ah w PSP (jest to adres powrotu procesu) Przywraca adres programu obsługi Break (int 23h) z offsetu 0Eh w PSP Przywraca adres programu obsługi błędu krytycznego (int 24h) z offsetu 12h w PSP Dealokuje pamięć przechowywaną przez proces

Chyba ,że rzeczywiście wiemy co robimy, nie powinniśmy zmieniać wartości pod offsetami 0Ah, 0Eh lub 12h w PSP. Przez zrobienie tego możemy stworzyć wewnętrznie sprzeczny system kiedy nasz program się kończy. 19.1.1.5 UZYSKANIE KODU POWOROTU PROCESU POTOMNEGO Proces macierzysty może uzyskać kod powrotny z procesu potomnego poprzez wywołanie funkcji Get Child Process Return Code. Ta funkcja zwraca wartość w rejestrze al w punkcie zakończenia plus informację, która mówi nam jak zakończył się proces potomny. Ta funkcja (ah =4Dh) zwraca kod zakończenia w rejestrze al. Również zwraca powód zakończenia w rejestrze ah Rejestr ah będzie zawierał jedną z następujących wartości: Wartość w AH 0 1 2 3

Powód zakończenia Zakończenie normalne (int 21h, ah = 4Ch) Zakończenie przez ctrl+C Zakończenie przez błąd krytyczny Zakończenie TSR (int 21h, ah= 31h)

Tablice 68: Powody zakończenia Kod zakończenia pojawiający się w al. jest poprawny tylko dla zakończeń normalnego i TSR Zauważmy, że możemy tylko raz wywołać ten podprogram po zakończeniu procesu potomnego. MS-DOS zwraca wartość bez znaczenia w AX po pierwszym takim wywołaniu. Podobnie , jeśli użyjemy tej funkcji bez uruchamiania procesu potomnego, wyniki jakie uzyskamy będą bez sensu .DOS nie wraca jeśli to zrobimy.

19.1.2 OBSŁUGA WYJĄTKÓW W DOS: OBSŁUGA BREAK Jeśli kiedykolwiek użytkownik naciśnie klawisze ctrl + C lub ctrl+ Braek MS-DOS może przerwać taką sekwencję klawiszy i wykonać instrukcję int 23h. MS-DOS dostarcza domyślnego podprogramu obsługi break, który kończy program. Jednakże, dobrze napisany program generalnie zamienia domyślny podprogram obsługi break z jednym ze swoich, więc może przechwycić sekwencję klawiszy ctrl + C lub ctrl + Break i wyłączyć program w uporządkowany sposób. Kiedy DOS kończy program z powodu przerwania break, opróżnia bufor plików, zamyka wszystkie otwarte pliki, zwalnia pamięć należącą do aplikacji i wszystkie normalne rzeczy przy zamykaniu programu. Jednakże, nie przywraca żadnego wektora przerwań (inaczej niż przerwania 23h i 24h) . Jeśli nasz kod zamieniał jakieś wektory przerwań, zwłaszcza wektory przerwań sprzętowych, wtedy wektory te będą jeszcze wskazywały na programy obsługi przerwań naszego programu po zakończeniu przez DOS naszego programu. Wtedy prawdopodobnie wystąpi krach systemu, kiedy DOS załaduje nowy program na szczycie naszego kodu. Dlatego też, powinniśmy napisać program obsługi break, aby nasza aplikacja mogła zamknąć się sama w uporządkowany sposób jeśli użytkownik naciśnie ctrl + C lub ctrl + break. Najłatwiejszy, i być może najbardziej uniwersalny program obsługi break składa się z pojedynczej instrukcji iret .Jeśli wskażemy wektor przerwania int 23h przy instrukcji iret. MS-DOS po prostu zignoruje każde naciśnięcie klawiszy ctrl+C lub ctrl + Break. Jest to bardzo użyteczne dla wyłączania programu obsługi break podczas sekcji krytycznych kodu, których nie chcemy by użytkownik przerywał. Z drugiej strony, po prostu wyłączamy program obsługi ctrl + C lub ctrl + break w całym programie jeśli nie jest satysfakcjonujący. Jeśli z tego samego powodu użytkownik chce przerwać program, naciśnięcie ctrl + C lub ctrl + break jest prawdopodobnie tym co spróbuje zrobić. Jeśli nasz program na to nie zezwala, użytkownik może posunąć się do czegoś bardziej drastycznego, jak ctrl + alt + delete, dla zresteowania maszyny. To z pewnością zrujnuje jakiś otwarte pliki i może spowodować inne problemy (oczywiście, oczywiście nie musimy martwić się o przywracanie wektorów przerwań!) Aktualizowanie naszego własnego programu obsługi break jest łatwe – przechowujemy adres naszego podprogramu obsługi break w wektorze przerwań 23h. Nie musimy nawet zapisywać starej wartości. DOS robi to automatycznie (przechowa wartość wektora pod offsetem 0Eh w PSP). Potem, kiedy użytkownik naciśnie ctrl+ C lub ctrl + break, MS-DOS przekaże sterownie do naszego programu obsługi break. Być może najlepszą reakcją na przerwanie break jest ustawienie jakiejś flagi mówiącej aplikacji o wystąpieniu break a potem wyjście do aplikacji testującej tą flagę sensownie wskazując czy powinna się zamknąć. Oczywiście wymaga to żebyśmy testowali tą flagę w różnych miejscach naszego programu, zwiększając złożoność naszego kodu. Inną alternatywa jest zachowanie oryginalnego wektora int 23h i przekazanie sterowania do DOS’owego programu obsługi break, po tym jak sami obsłużymy jakąś inna ważną operację .Możemy również napisać wyspecjalizowany program obsługi break zwracający do DOS kod zakończenia, który może odczytać proces macierzysty. Oczywiście, nie ma powodów abyśmy nie mogli zmienić wektora przerwań 23h w różnych punktach całego naszego programu obsługując wymagane zmiany. W różnych punktach możemy zablokować przerwanie break całkowicie., przywracając wektory przerwań, albo zachęcić użytkownika w innym punkcie. 19.1.3 OBSŁUGA WYJĄTKÓW W DOS: OBSŁUGA BŁĘDU KRYTYCZNEGO DOS wywołuje podprogram obsługi błędu krytycznego przez wykonanie instrukcji int 24h gdy tylko wystąpi jakiś rodzaj błędu I/O. Domyślnie program obsługi drukuje dobrze znaną wiadomość: I/O Devixe Specific Error Message Abort, Retry , Ignore, Fail? Jeśli użytkownik naciśnie “A”, kod bezpośrednio wróci do programu DOS’a COMMAND.DOM; nie zamknie nawet żadnego otwartego pliku. Jeśli użytkownik naciśnie „R” , dla ponów, MS-DOS będzie ponawiał operację I/O, mimo, że zazwyczaj wynikiem jest wywołanie innego programu obsługi błędu krytycznego. Opcja „I” mówi DOS’owi, żeby zignorował błąd i wrócił do wywołującego programu jak gdyby nic się nie stało. „F” instruuje DOS, aby zwrócił kod błędu do wywołującego programu i pozwolił obsłużyć ten problem. Z powyższych opcji, naciśnięcie przez użytkownika „A” jest najbardziej niebezpieczne. Powoduje natychmiastowy powrót do DOS, a nasz kod nie dostaje szansy na poprawienie niczego. Na przykład, jeśli zaktualizujemy jakieś wektory przerwań, program nie będzie miał możliwości przywrócenia ich jeśli użytkownik

wybierze opcję przerwij. Może to spowodować krach systemu, kiedy MS-DOS załaduje następny program na szczycie naszych podprogramów obsługi przerwań w pamięci. Dla przechwycenia krytycznych błędów DOS, będziemy musieli zaktualizować wektor przerwań 24h aby wskazywał nasz podprogram obsługi przerwań. Na wejściu do naszego programu obsługi przerwania 24h stos będzie zawierał następujące dane: FLAGI CS IP ES DS. BP DI SI DX CX BX AX FLAGI CS IP

Oryginalny adres powrotny INT 24h

Rejestry DOS odłożone dla naszego programu obsługi INT 24h

Adres powrotny (do DOS) dla naszego programu obsługi

Zawartość Stosu Na Wejściu Do Programu Obsługi Błędu Krytycznego MS-DOS przekazuje ważne informacje w kilku tych rejestrach do naszego programu obsługi błędu krytycznego. Poprzez sprawdzenie tych wartości możemy określić powód błędu krytycznego i urządzenie na którym on wystąpił. Najbardziej znaczący bit w rejestrze ah określa czy wystąpił błąd w strukturze bloku urządzenia (zazwyczaj dysk lub taśma) lub znaku urządzenia. Pozostałe bity w ah mają następujące znaczenie: Bit(y) 0 1-2

3 4 5 6 7

Opis 0 = Operacja odczytu 1 = Operacja zapisu Wskazuje sztuczne obszary dysku 00 – obszar MS-DOS 01 – tablica alokacji plików (FAT) 10 – Katalog główny 11 – Obszar pliku 0 – Nie uznana błędna odpowiedź 1- Odpowiedź błędna jest OK 0 – Odpowiedź ponowienia nie uznana 1- Odpowiedź ponowienia jest OK. 0 – Odpowiedź zignorowania nie uznana 1- Odpowiedź zignorowania jest OK. Niezdefiniowane 0 – Błąd urządzenia znakowego 1 – Błąd struktury blokowej urządzenia

Tablica 69: Bity błędów urządzenia w AH Dodatkowe bity w ah, dla struktury blokowej urządzeń w rejestrze al. zawierają numer urządzenia gdzie wystąpił błąd (0=A, 1=B,2=C, itd.). Wartość w rejestrze al. jest niezdefiniowana dla urządzenia znakowego. Niższa połówka rejestru di zawiera dodatkowe informacje o błędzie urządzenia blokowego (najwyższy bajt di jest niezdefiniowany, musimy zamaskować te bity zanim spróbujemy przetestować tą daną)

Kod błędu 0 1 2 3 4 5 6 7 8 9 0Ah 0Bh 0Ch 0Fh

Opis Zapis błędu ochrony Nieznane urządzenie Urządzenie nie gotowe Niewłaściwe polecenie Błąd danych (błąd CRC) Długość żądanej struktury jest niewłaściwa Błąd przeszukiwania na urządzeniu Dysk niesformatowany dla MS-DOS Nie znaleziono sektora Brak papieru w drukarce Błąd zapisu Błąd odczytu Niepowodzenie ogólne Dysk zmieniony w nieodpowiednim czasie

Tablica 70: Kody błędów struktury blokowej urządzenia (w najmniej znaczącym bajcie DI) Na wejściu do naszego programu obsługi błędu krytycznego, przerwania są wyłączane. Ponieważ ten błąd wystąpi jako wynik jakieś funkcji MS-DOS, MS-DOS jest już wprowadzony i nie będziemy mogli zrobić żadnego wywołania innych funkcji niż 1-0Ch i 59h (pobranie informacji rozszerzonego błędu) Nasz program obsługi błędu krytycznego musi zachować wszystkie rejestry z wyjątkiem al. Program musi wrócić do DOS instrukcją iret a al. musi zawierać jeden z poniższych kodów: Kod 0 1 2 3

Znaczenie Zignoruj błąd urządzenia Ponowne ponowienie operacji I/O Zakończenie procesu (przerwanie) Wywołanie błędu systemu bieżącego

Poniższy kod dostarcza trywialnego przykładu obsługi błędu krytycznego. Program główny próbuje wysłać znak do drukarki. Jeśli nie ma połączonej drukarki lub wyłączyliśmy drukarkę przed uruchomieniem programu, bezie wygenerowany błąd krytyczny. ; Próbka programu obsługi błędu krytycznego IT 24h ; ; Kod ten demonstruje próbkę programu obsługi błędu krytycznego. Aktualizuje INT 24h i wyświetla właściwą ; wiadomość o błędzie i pyta użytkownika czy chce ponowić, przerwać , zignorować lub zaniedbać (podobnie jak ; DOS) .xlist include includelib .list

stdlib.a stdlib.lib

dseg

segment para public ‘data’

Value ErrCode

word word

dseg

ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

0 0

; Zastąpienie programu obsługi błędu krytycznego. Zauważmy, że ten podprogram jest nawet gorszy niż DOS’a, ale ; demonstruje jak napisać taki podprogram. Zauważmy, ze nie możemy wywołać żadnego programu I/O Biblioteki ; Standardowej w programie obsługi błędu krytycznego ponieważ nie używają one funkcji DOS 1-0Ch, które ; są jedynie dostępne w DOS CritErrMsg

byte byte byte

cr, lf “DOS Crirtical Error!”, cr, lf “A)bort R)etry, I)gnore, F)ail? $”

MyInt24

proc push push push

far dx ds ax

push pop

cs ds

lea mov int

dx, CritErrMsg ah, 9 21h

mov int and

ah, 1 21h al., 5Fh

cmp jne pop mov jmp

al, ‘I’ NotIgnore ax al, 0 Quit

;ignorujemy?

NotIgnore:

cmp jne pop mov jmp

al, ‘r’ NotRetry ax al, 1 Quit24

;ponawiamy?

NotRetry:

cmp jne pop mov jmp

al, ‘A’ NotAbort ax al., 2 Quit24

NotAbort:

cmp jne pop mov

al., ‘F’ BadChar ax al, 3

Quit24:

pop pop iret

ds dx

BadChar:

mov mov jmp endp

ah, 2 dl, 7 Int24Lp

Int24Lp:

MyInt24

;wydruk ciągu DOS ;funkcja odczytu DOS ;Konersja l.c →u .c

;przerywamy?

;znak dzwonka

Main

proc mov ax, seg mov ds, ax mov es, ax meminit mov mov mov mov

ax, 0 es, ax word ptr es:[24h*4], offset MyInt24 es:[24h*4+2], cs

mov mov int rcl and mov printf byte byte byet dword

ah, 5 al, ‚a’ 21h Value, 1 Value, 1 ErrCode, ax

Quit: Main

ExitPgm endp

cseg

ends

cr, lf, lf “Print char returned with error status %d and “ “error code %d\n”, 0 Value, ErCode ;makro DOS wyjścia z programu

; Alokacja stosownej ilości pamięci na stosie (8k). Notka: jeśli użyjemy pakietu dopasowani do wzorca powinniśmy ; ustawić jakiś duży stos sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

;zzzzzzseg LastBytes Zzzzzzseg

segemnt para public ‘zzzzzz’ db 16 dup (?) ends end Main

19.1.4 OSBSŁUGA WYJĄTKÓW W DOS: PRZERWANIA KONTROLOWANE W dodatku do wyjątków break i błędów krytycznych, 80x86 ma wyjątki, które mogą zdarzyć się podczas wykonywania naszego programu. Przykłady to wyjątek błędu dzielenia, wyjątki graniczne i wyjątki nielegalnych opcodów. Dobrze napisana aplikacja powinna zawsze obsługiwać wszystkie możliwe wyjątki. DOS nie dostarcza bezpośredniego wsparcia dla tych wyjątków, inaczej niż możliwe domyślne pogramy obsługi. W szczególności, DOS nie przywraca takich wektorów kiedy program się kończy; jest jakaś aplikacja, program obsługi break i obsługi błędu krytycznego, które muszą się tym zająć. 19.1.5 PRZEKIEROWANIE I/O DLA PROCESU POTOMNEGO Kiedy proces potomny zaczyna się wykonywać, dziedziczy wszystkie otwarte pliki z procesu macierzystego ( z wyjątkiem pewnych plików otwieranych funkcjami plików sieciowych). W szczególności, wliczamy w to domyślne pliki otwierane dla DOS urządzeń standardowego wejścia, standardowego wyjścia, standardowego błędu, pomocniczych i drukarki. DOS przypisuje uchwytom plików wartość od zera do cztery, odpowiednio, dla tych

urządzeń. Jeśli proces macierzysty zamyka jeden z tych uchwytów plików a potem zmieniamy uchwyt funkcją Force Duplicat File Handle . Zauważmy, że funkcja DOSEXEC nie przetwarza operatorów przekierowania I/O („” i „|”). Jeśli chcemy przekierować standardowe I/O procesu potomnego, musimy zrobić to przed załadowaniem i wykonaniem tego procesu potomnego. Przekierowując jeden z pięciu standardowych urządzeń I/O, powinniśmy wykonać następujące kroki: 1) 2) 3) 4) 5) 6) 7)

Duplikujemy uchwyt pliku jaki chcemy przekierować (np. przrekierowujemy standardowe wyjście, duplikujemy uchwyt pliku jeden) Zamykamy plik (np. uchwyt pliku jeden dla standardowego wyjścia) Otwieramy plik używając standardowej funkcji DOS’a Craete lub Create New Używamy funkcji Force Duplicate File Handle do skopiowania nowego uchwytu pliku na uchwyt pliku jeden Uruchamiamy proces potomny Przy powrocie z potomka, zamykamy plik Kopiujemy uchwyt pliku zduplikowany w kroku jeden z powrotem do standardowego wyjścia uchwytu pliku używając funkcji Force Duplicate Handle

Ta technika wygląda jak gdyby była doskonała dla przekierowania drukarki lub portu szeregowego I/O. Niestety wiele programów omija DOS kiedy wysyła dane do drukarki i używa funkcji BIOS lub, co gorsze, wysyła bezpośrednio do sprzętu. Prawie żadne oprogramowanie nie zawraca sobie głowy wsparciem portu szeregowego DOS – to naprawdę jest złe. Jednakże, większość programów robi wywołanie DOS’a dla znaków wejściowych i wyjściowych w standardowych urządzeniach wejścia, wyjścia i błędu. Poniższy kod demonstruje jak przekierować wyjście procesu potomnego do pliku. ; REDIRECT.ASM – Demonstruje jak przekierować I/O dla procesu potomnego. Ten szczególny program wywołuje ; COMMAND.COM do wykonania polecenia DIR, kiedy wysyłamy do określonego pliku wyjściowego include includelib dseg

stdlib.a stdlib.lib

segment para public ‘data’

OrigOutHandle word FileHandle word FileName byte

? ? „dirctry.txt”, 0

;przechowanie kopii uchwytu STDOUT ;uchwyt I/O ;nazwa pliku dla danych wyjściowych

; struktura EXEC MS-DOS ExecStruct

word dword dword dword

0 CmdLine DfltFCB DfltFCB

DfltFCB CmdLine PgmName PgmNameStr dseg

byte byte dword byte ends

3, „,”, 0, 0, 0, 0,0 7, “/c DIR”, 0dh PgmNameStr „c:\command.com”, 0

cseg

segemnt para public ‘ code’ assume cs:cseg, ds;dseg

Main

proc mov

ax, dseg

;używamy bloku środowiska macierzystego ;dla parametrów linii poleceń

;plecenie katalogu ;wskaźnik do nazwy pgm

;pobranie wskaźnika do segmentu zmiennych

mov ds., ax Meminit ;start menadżera pamięci ; Zwolnienie jakiejś pamięci dla COMMAND.COM: mov int mov mov sub mov mov int

ah, 62h 21h es, bx ax, zzzzzzseg ax, bx bx, ax ah, 4ah 21h

;pobranie wartości naszego PSP ;obliczanie rozmiaru uruchomionego kodu rezydentnego ;zwolnienie nie używanej pamięci

; zachowanie oryginalnej uchwytu pliku wyjściowego mov bx, 1 ;std out jest uchwytem pliku 1 mov ah, 45h ;duplikujemy uchwyt pliku int 21h mov OrigOutHandle, ax ;zachowanie zduplikowanego uchwytu ;Otwieramy plik wyjściowy: mov mov lea int mov

ah, 3ch cx, 0 dx, FileName 21h FileHandle, ax

;tworzymy plik ;normalny atrybut ;zachowanie otwieranego uchwytu pliku

; Wymuszamy standardowe wyjście do wysłania danych wyjściowych do tego pliku ; Robimy to przez wymuszenie uchwytu pliku do uchwytu pliku #1 (stdout) mov ah, 46h ;wymuszenie uchwytu pliku mov cx, 1 ;istniejący uchwyt do zmiany mov bx, FileName ;nowy uchwyt pliku do użycia int 21h ; Wydruk pierwszej linii do pliku: print byte „Redirected directory listing:”, cr,lf,0 ;Okay, wykonujemy polecenie DIR DOS’a (to znaczy, wykonuje COMMAND.COM parametrem ; lini poleceń „/c DIR”) mov bx, seg ExecStruct mov es, bx mov bx, offset ExecStruct ;wskaźnik do rekordu programu lds dx, PgmName mov ax, 4b00h ;exec pgm int 21h mov mov mov mov mov

bx, sseg ss, ax sp, offset EndStk bx, seg dseg ds, bx

;resetujemy stos przy zwrocie

;Okay, zamykamy plik wyjściowy I przekształcamy standardowe wyjście z powrotem do konsoli mov

ah,3eh

;zamykamy plik wyjściowy

mov int

bx, FileHandle 21h

mov mov mov int

ah, 46h cx, 1 bx, OrigOutHandle 21h

;wymuszenie duplikacji uchwytu ;StdOut ;Przywrócenie poprzedniego uchwytu

;Zwrot sterowania do MS-DOS Quit: Main cseg

ExitPgm endp ends

sseg

segment para stack ‘stack’ dw 128 dup (0) dw ? ends

endstk sseg zzzzzzseg Heap Zzzzzzseg

segment para public ‘zzzzzzseg’ db 200 dup (?) ends end Main

19.2 PAMIĘĆ DZIELONA Jedynym problem z uruchamianiem różnych programów DOS jako część pojedynczej aplikacji jest komunikacja międzyprocesowa. To znaczy, jak wszystkie te programy przemawiają jeden do drugiego? Kiedy typowa aplikacja DOS działa, DOS ładuje cały kod i segmenty danych; nie ma żadnego zabezpieczenia, inaczej niż odczytywanie danych z pliku lub kod zakończenia procesu, gdzie jeden proces przekazuje informacje do drugiego. Chociaż I/O plików będzie działało, jest to nieporęczne i wolne. Idealnym rozwiązaniem byłoby aby jeden proces zostawił kopie różnych zmiennych, które mogą dzielić inne procesy. Nasze programy mogą łatwo to zrobić przy użyciu pamięci dzielonej. Większość nowoczesnych wielozadaniowych systemów operacyjnych dostarcza pamięci dzielonej – pamięci, która pojawia się w przestrzeni adresowej dwóch lub więcej procesów. Co więcej, taka pamięć dzielona jest często trwała, w znaczeniu , że trwa przechowywanie wartości po tym jak proces tworzenia się kończy. Pozwala to innym procesom zaczynać się później i używać wartości pozostawionych przez twórcę zmiennych. Niestety, MS-DOS nie jest nowoczesnym wielozadaniowym systemem operacyjnym i nie wspiera pamięci dzielonej. Jednakże, możemy łatwo napisać program rezydentny, który dostarcza tej zagubionej przez DOS zdolności. Poniższa sekcja opisuje jak stworzyć dwa typowe regiony pamięci dzielonej – statyczny i dynamiczny. 19.2.1 STATYCZNA DZIELONA PAMIĘĆ TSR implementujący statycznie dzieloną pamięć jest trywialny. Jest to bierny TSR, który dostarcza trzech funkcji – sprawdzania obecności, usuwania i wskaźnika segmentu powrotu. Nierezydentna część po prostu alokuje 64Kb segment danych a potem się kończy. Inne procesy mogą uzyskać adres 64K bloku pamięci dzielonej poprzez wywołanie „wskaźnika segmentu powrotu”. Procesy te mogą umieszczać wszystkie swoje dzielone dane w segmencie należącym do TSR’a. Kiedy jeden proces kończy się , dzielony segment pozostaje w pamięci jako część TSR’a. Kiedy drugi proces się uruchamia i łączy z dzielonym segmentem , zmienne z segmentu dzielonego są jeszcze nienaruszone, wiec nowy proces może uzyskać dostęp do tych zmiennych. Kiedy wszystkie procesy przeszły dane dzielone, użytkownik może usunąć dzieloną pamięć TSR’a funkcją usuwania. Jak przedstawiono powyżej, nie jest prawie niczym zrobienie pamięci dzielonej TSR. Implementuje to następujący kod: ; SHARDMEM.ASM ;

; Ten TSR odkłada 64k region pamięci dzielonej dla innych procesów ; ; Użycie: ; SHARDMEM Ładowanie rezydentnej części i aktywowowanie zdolności pamięci ; dzielonej ; SHARDMEM REMOVE Usuwanie pamięci dzielonej TSR’a z pamięci ; Ten TSR sprawdza, aby się upewnić, że nie ma już aktywnej kopii w pamięci. Kiedy usuwamy go z pamięci ; upewniamy się, że nie ma innych łańcuchów przerwań w INT 2Fh przed dokonaniem usuwania. ; ; Następujący segment musi pojawić się w tym porządku i przed włączeniem Biblioteki Standardowej ResidentSeg ResidentSeg

segment para public „Resident’ ends

SharedMemory SharedMemory

segemnt para public ‘Shared’ ends

EndResident EndResident

segment para public ‘EndRes’ ends .xlist .286 .include .includelib .list

stdlib.a stdlib.lib

;Segment rezydentny, który przechowuje kod TSR’a: ResidentSeg

segment para public ‘Resident’ assume cs:ResidentSeg, ds:nothing

; numer ID Int 2Fh dla tego TSR’a: MyTSRID

byte byte

0 0

; PSP jest adresem psp tego programu PSP OldInt2F

word 0 dword ?

; MyInt2F ; ; ; ; ; ; ; ; ; ;

Dostarcza wsparcia int 2Fh (przerwanie różnych procesów) dla tego TSR’a. Przerwanie różnych procesów rozpoznaje poniższe podfunkcje (przekazane w AL.):

MyInt2Fh

00h – sprawdzanie obecności:

Zwraca 0FFh w AL i wskaźnik do ciągu ID w es:di jeśli ID TSR’a (w AH) jest dopasowany do tego szczególnego ciągu.

01h- usuwanie:

Usuwa TSR z pamięci. Zwraca 0 w AL jeśli powodzenie, 1 w AL jeśli niepowodzenie

10h- wskaz. Adr.seg. -

Zwraca adres segmentu dzielonego w ES

proc far assume ds.:nothing

cmp je jmp

ah, MyTSRID YepItsOurs OldInt2F

;dopasowano identyfikator naszego TSR’a

;Okay, wiemy, że to jest nasze ID, teraz sprawdzamy obecność, usuwanie lub wywołanie zwracanego ; segmentu YepItsOurs:

cmp jne mov lesi iret

al., 0 TryRmv al., 0ffh IDString

;funkcja weryfikacji

IDString

byte

„Static Shared Memory TSR”, 0

TryRmv:

cmp jne

al, 1 TryRetSeg

;zwracane powodzenie ;wracamy do kodu wywołującego

;funkcja usuwania

;Zobaczmy czy możemy usunąć ten TSR:

TRDone:

push mov mov cmp jne cmp je mov pop iret

es ax, 0 es, ax word ptr es:[2Fh*4], offset MyInt2F TRDone word ptr es:[2Fh*4 +2], seg MyInt2Fh CanRemove ;skok jeśli można ax,1 ;zwraca teraz niepowodzenie es

;Okay, chcemy usunąć to *i* możemy usuwać go z pamięci ; dopilnujmy wszystkiego tu assume ds: ResidentSeg CanRemove:

push Pusha cli mov mov mov mov

ds. ax, 0 es, ax ax, cs ds., ax

mov mov mov mov

ax, word ptr OldInt2F es:[2Fh*4], ax ax, word ptr OldInt2F+2 es:[2Fh*4+2], ax

;wyłączamy przerwania kiedy pracujemy z ; z wektorami przerwań

;Okay, jedna ostatnia rzecz przed wyjściem – oddajemy zaalokowaną pamięć dla tego TSR z powrotem ; do DOS’a mov ds., PSP mov es ds:[2Ch] ;wskaźnik do bloku środowiska mov ah, 49h ;funkcja zwalniania pamięci DOS int 21h mov

ax, ds.

;zwolnienie przestrzeni kodu programu

mov mov int

es, ax ah, 49h 21h

popa pop po mov

ds. es ax, 0

;zwraca powodzenie

;Zobaczmy czy zwracano adres segmentowy naszego dzielonego segmentu tutaj TryRetSeg:

cmp al., 10h ;opcod segmentu powrotu jne IllegalOp mov ax, SharedMemory mov es, ax mov ax, 0 ,zwrot z powodzeniem clc iret ; wywołanie z nielegalną wartością podfunkcji. Próbujemy zrobić jak najmniej szkody jeśli to możliwe IllegalOp: MyInt2F ResidentSeg

mov ax, 0 iret endp assume ds:nothing ends

;kto wie co o tym myśleć?

;Tu , segment, będzie aktualnie przechowywał dzielone dane SharedMemory segment para public ‘Shared’ db 0FFFFh dup (?) SharedMemory ends Cseg

segment para public ‚code’ assume cs:cseg, ds:ResidentSeg

;SeeIfPresent;

Sprawdzamy aby zobaczyć, czy nasz TSR jest już obecny w pamięci. Ustawiamy flagę zera jeśli jest, zeruje ta flagę jeśli nie jest

SeeIfPresent

proc push push push mov mov push mov int pop cmp je strcmpl byte je

near es ds. di cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 TryNext

dec js

cl IDLoop

IDLoop:

TryNext:

;start z ID 0FFh ; funkcja weryfikacji obecności ;Obecny w pamięci

„Static Shared Memory TSR” , 0 Success ;test ID użytkownika 80f..FFh

Success:

SeeIfPresent ;FindID ; ; ; ; FindID

IDLoop:

Success:

FindID Main

cmp

cx, 0

pop pop pop ret endp

di ds. es

; zerowanie flagi zera

Określa pierwszy (cóż w rzeczywistości ostatni) ID TSR’a dostępnego w łańcuchu przerwań równoczesnych procesów. Zwraca tą wartość w rejestrze CL Zwraca ustawioną flagę zera jeśli lokuje pusty slot. Zwraca wyzerowaną flagę zera jeśli niepowodzenie proc push push push

near es ds di

mov mov push mov int pop cmp je dec js xor cmp pop pop pop ret endp

cx, offh ah, cl cx al, 0 2Fh cx al., 0 Success cl IDLoop cx, cx cx, 1 di ds. es

;start z ID 0FFh ;funkcja weryfikacji obecności ;obecny w pamięci? ;test ID użytkownika 80h..FFh ;zerowanie flagi zera

proc meminit mov mov

ax, ResidentSeg ds, ax

mov int mov

ah, 62h 21h PSP, bx

;pobranie wartości PSP tego programu

; zanim cokolwiek zrobimy musimy sprawdzić parametry linii poleceń. Jeśli jest to jeden i jest to słowo ; „REMOVE”, wtedy usuwamy kopię rezydentną z pamięci używając przerwania równoczesnych procesów ; (2Fh)

Usage:

argc cmp jb je print

cx,1 TstPresent DoRemove

;musi mieć zero lub jeden parametr

byte „Usage:”, cr,lf byte “ shardmem”, cr, lf byte “or shardmem REMOVE”, cr, lf,0 ExitPgm ; sprawdzenie polecenia REMOVE DoRemove:

mov ax, 1 argv stricmpl byte “REMOVE”,0 jne Usage call SeeIfPresent je RemoveIt print byte “TSR nie jest obecny w pamięci, nie można usunąć” byte cr, lf,0 ExitPgm

RemoveIt:

mov MyTSRID, cl printf byte “Usuwanie TSR’a (ID #%d) z pamięci…”, 0 dword MyTSRID

mov ah, cl mov al., 1 ;usuwanie cmd, ah zawiera ID int 2Fh cmp al., 1 ;Powodzenie? je RmvFailure print byte „removed”, cr,lf,0 ExitPgm RmvFailure: print byte cr, lf byte “Nie można usunąć TSR’a z pamięci”, cr, lf byte „Spróbuj usunąć inne TSR’y w odwrotnej kolejności” byte „zainstalowaliśmy je”, cr, lf,0 ExitPgm ;Okay, zobaczmy czy TSR jest już w pamięci. Jeśli tak, przerywamy proces instalacji TstPresent:

call SeeIfPresent jne GetTSRID print byte „TSR jest już obecny w pamięci” ,cr, lf byte „przerwanie procesu instalacji”, cr, lf,0 ExitPgm ;Pobranie ID naszego TSR’a i zachowanie go GetTSRID:

GetFileName:

call FindID je GetFileName print byte „Zbyt wiele rezydentnych TSR’ów, nie można instalować”,cr,lf,0 ExitPgm mov MyTSRID, cl print

byte

“Instalowanie przerwań….”, 0

;Aktualizacja łańcucha przerwań INT 2Fh cli mov mov mov mov mov mov mov mov sti

;wyłączenie przerwań ax,0 es, ax ax, es:[2Fh*4] word ptr OldInt2F, ax ax, es;[2Fh*4+2] word ptr OldInt2F+2, ax es:[2Fh*4], offset MyInt2F es:[2Fh*4+2], seg ResidentSeg ;włączamy ponownie przerwania

; mamy podłączone, jedyna rzecz jaka pozostała to wyzerowanie segmentu pamięci dzielonej a potem TSR’a printf byte „Instalowanie , TSR ID #%d.”, cr,lf,0 dword MyTSRID mov mov mov xor mov stosw

ax, SharedMemory es, ax cx, 32768 ax, ax di, ax dx, EndResident dx, PSP ax, 3100h 21h

Main cseg

mov mov mov int endp ends

sseg stk sseg

segemt para stack ‘stack’ db 256 dup (?) ends

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

rep

;zerowanie segmentu pamięci dzielonej ; 32K słów = 64K bajtów ;zachowanie wszystkich zer ;zaczynamy spod offsetu zero ;obliczamy rozmiar programu ;polecenie TSR’a DOS

Pogram ten po prostu wykrawa kawałek pamięci (64K w segmencie SharedMemory) i zwraca wskaźnik do niej w es, jeśli jakiś program wykonuje właściwe wywołanie int 2Fh (ah = TSR ID a al= 10h) Jedyny problem to jak Zadeklarować zmienne dzielone w aplikacji, która używa pamięci dzielonej? Cóż , jest to dosyć łatwe jeśli zagramy podstępną sztuczkę z MASM’em , LINK’erem , DOS’em i 80x86. Kiedy DOS ładuje nasz program do pamięci, generalnie ładuje segmenty w takiej kolejności w jakiej pojawiają się w naszym pliku źródłowym. Biblioteka Standardowa UCR, na przykład, wykorzystuje to poprzez naleganie na włączenie segmentu nazywanego zzzzzzseg na końcu wszystkich naszych asemblerowych plików źródłowych. Podprogramy zarządzania pamięcią Biblioteki Standardowej UCR budują stertę zaczynającą się przy zzzzzzseg, która musi być ostatnim segmentem (zawierającym poprawne dane) ponieważ podprogramy zarządzania pamięcią mogą nadpisywać jakikolwiek zzzzzzseg. Dla naszego segmentu pamięci dzielonej, chcielibyśmy stworzyć segment taki jak poniższy:

SharedMemory segment para public ‘Shared’ < definiujemy tu wszystkie zmienne dzielone > SharedMemory ends Aplikacje, które dzielą dane zdefiniują wszystkie dzielone zmienne w tym dzielonym segmencie. Jest jednakże pięć problemów. Pierwszy , to jak powiadomimy asembler / linker/ DOS/ 80x86, że jest to segment dzielony, zamiast mieć oddzielny segment dla każdego programu? Cóż, ten problem jest łatwy do rozwiązania; nie musimy się martwić powiadamianiem MASM’a , linkera lub DOS o czymkolwiek. Sposobem wykonania tego by różne aplikacje, wszystkie , dzieliły ten sam segment w pamięci, jest wywołanie pamięci dzielonej TSR w powyższym kodzie z kodem funkcji 10h. Zwraca ona adres segmentu SharedMemory TSR’a w rejestrze es. W naszych programach asemblerowych oszukamy MASM, który sądzi, że es wskazuje lokalny segment pamięci dzielonej, kiedy faktycznie es wskazuje segment globalny. Drugi problem jest drobny ale mimo to irytujący. Kiedy tworzymy segment MASM, linker i DOS rezerwują miejsce w pamięci na segment. Jeśli zadeklarujemy dużą liczbę zmiennych w segmencie dzielonym, może to zmarnować pamięć ponieważ program w rzeczywistości będzie używał przestrzeni pamięci w globalnym dzielonym segmencie. Łatwym sposobem żądania zwrotu pamięci, którą MASM zarezerwował dla tego segmentu jest zdefiniowanie segmentu dzielonego po zzzzzzseg w naszej aplikacji z dzieloną pamięcią. Poprzez zrobienie tego, Biblioteka Standardowa wchłonie zarezerwowaną pamięć dla (fikcyjnego) segmentu dzielonej pamięci na stercie, ponieważ cała pamięć po zzzzzzseg należy do sterty (kiedy używamy standardowej funkcji meminit) Trzeci problem jest trochę trudniejszy do zajęcia się nim. Ponieważ nie będziemy używali segmentu lokalnego, nie możemy zainicjalizować żadnej zmiennej w segmencie pamięci dzielonej przez umieszczenie wartości w polu operandu dyrektyw bajtu, słowa, podwójnego słowa itd. Robiąc to inicjalizujemy tylko pamięć lokalną na stercie, system nie skopiuje tej danej do segmentu dzielonego globalnie. Generalnie, nie jest to problem ponieważ procesy normalnie nie inicjalizują pamięci dzielonej jeśli są ładowane. Zamiast tego, będą prawdopodobnie pojedyncze aplikacje, najpierw uruchomione, które zainicjalizują obszar pamięci dzielonej dla reszty procesów, które używają globalnego segmentu dzielonego. Czwartym problemem jest to, że nie możemy zainicjalizować żadnej zmiennej adresem obiektu w pamięci dzielonej. Na przykład, jeśli zmienna shared_K jest w segmencie pamięci dzielonej, nie możemy użyć instrukcji takich jak te: printf byte „Wartością shared_K jest %d\n”, 0 dword shared_K Problem z tym kodem jest taki, że MAMS inicjalizuje podwójne słowo po powyższym ciągu adresem zmiennej shared_K w lokalnej kopii dzielonego segmentu danych. Nie drukuje kopii w globalnie dzielonym segmencie danych. Ostatni problem jest drobny. Wszystkie programy , które używają globalnie dzielonego segmentu pamięci muszą zdefiniować swoje zmienne pod identycznym offsetem wewnątrz dzielonego segmentu MASM przydziela offsety do zmiennych wewnątrz segmentu, jeśli jest jeden bajt w deklaracji jakiejś zmiennej, nasz program będą przydzielone jego zmienne pod różnymi adresami, które inne procesy współużytkują w globalnym dzielonym segmencie. To będzie zaciemniało pamięć i stworzy katastrofę Jedynym sensownym sposobem deklaracji zmiennych dla programów z dzieloną pamięcią jest stworzenie pliku zawierającego deklaracje wszystkich dzielonych zmiennych dla wszystkich odnośnych programów. Potem zawieramy ten pojedynczy plik we wszystkich programach , które współużytkują te zmienne. Teraz możemy dodać, usuwać lub modyfikować zmienne bez martwienia się o deklaracje zmiennych dzielonych w innych plikach. Następujące dwie próbki programów demonstrują użycie pamięci dzielonej. Pierwsza aplikacja odczytuje ciąg od użytkownika i upycha go w pamięci dzielonej. Druga aplikacja odczytuje ciąg z pamięci dzielonej i wyświetla go na monitorze. Najpierw, mamy tu plik zawierający deklaracje zmiennej dzielonej używanej przez obie aplikacje: ;shmvars.asm ; ; Plik tez zawiera deklarację zmiennej pamięci dzielonej używanej przez wszystkie aplikacje, które odnoszą się do ; pamięci dzielonej InputLine

byte

128 dup (?)

Tu mamy pierwszą aplikację, która odczytuje ciąg wejściowy od użytkownika i popycha do pamięci dzielonej: ; :SHMAPP1.ASM ; ;To jest aplikacja o dzielonej pamięci, która używa statycznie dzielonej pamięci TSR (SHARDMEM.ASM). ; Program ten wprowadza ciąg od użytkownika i przekazuje ten ciąg do SHMAPP2.ASM w całym obszarze ; pamięci dzielonej ; .xlist include stdlib.a includelib stdlib.lib .list dseg ShmID dseg

segment para public ‘data’ byte 0 ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg, es:SharedMemory

;SeeIfPresent;

Sprawdzamy czy pamięć dzielona TSR jest już obecna w pamięci. Ustawiamy flagę zera jeśli jest zerujemy flagę zera jeśli nie jest. Podprogram ten zawraca również ID TSR’a w CL

SeeIfPresent

proc push push push mov mov push mov int pop cmp je strcmpl byte je dec js cmp pop pop pop ret endp

IDLoop:

TryNext: Success:

SeeIfPresent

near es ds di cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 TryNext „Static Shared Memory TSR”, 0 Success cl IDLoop cx, 0 di ds. es

;start z ID 0FFh ;funkcja weryfikacji obecności ;obecny w pamięci

;testujemy ID użytkownika 80h..FFh ; zerowanie flagi zera

; Program główny dla aplikacji #1 włącza pamięć dzieloną TSR a potem odczytuje ciąg od użytkownika ; (przechowując ciąg w pamięci dzielonej) a potem kończy Main

proc assume cs:cseg, ds.:dseg, es:Sharedmemory mov ax, dseg mov ds, ax meminit

print byte

“Shared memory aplication #1”, cr, lf,0

;zobaczmy czy pamięć dzielona TSR jest w okolicy: call SeeIfPresent je ItsThere print byte „Shared Memory TSR (SHARDMEM) is not loaded”,cr, lf byte “This program cannot continue execution”,cr,lf,0 ExitPgm ;Jeśli pamięć dzielona TSR jest obecna, pobieramy adres dzielonego segmentu do rejestru ES: ItsThere:

mov mov int

ah, cl al, 10h 21h

;ID naszego TSR’a ;pobieramy adres dzielonego segmentu

;Pobieramy wejściową linie od użytkownika: print byte „Wprowadź ciąg: „ ,0 lea gets print byte puts print byte

di, InputLine

;ES już wskazuje właściwy segment

„Wprowadzono ‘:, 0 „’do pamięci dzielonej.”, cr,lf,0

Quit: Main

ExitPgm endp

cseg

ends

sseg stk sseg

segemnt para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segemnt para public ‘zzzzzz’ db 16 dup (?) ends

; Segment pamięci dzielonej musi pojawić się po „zzzzzzseg”. Zauważmy ,że nie jest to fizyczna ; pamięć dla danych w dzielonym segmencie. Jest to w rzeczywistości miejsce składowania więc możemy ; zadeklarować zmienne i generować ich właściwe offsety. Biblioteka Standardowa UCR będzie używać ; ponownie pamięci powiązanej z tym segmentem dla sterty. Dla uzyskania dostępu do danych w segmencie ; dzielonym, aplikacja ta wywołuje pamięć dzieloną TSR dla uzyskania prawdziwego adresu segmentu ; pamięci dzielonej . Może potem uzyskać dostęp do zmiennych w segmencie pamięci dzielonej ; ; Zauważmy, ze wszystkie zmienne zadeklarowane wchodzą do pliku wejściowego. Wszystkie aplikacje , ; odnoszą się do segmentu pamięci dzielonej wliczając w to ten plik w segmencie SharedMemory. Zakładamy, że ; wszystkie dzielone segmenty mają dokładnie takie samo rozmieszczenie SharedMemory

segment para public ‘Shared’

SharedMemory

Include shmvars.asm ends end Main

Druga aplikacja jest bardzo podobna, oto ona: ; SHMAPP2.ASM ; ; Jest to aplikacja z dzieloną pamięcią, która używa statycznie dzielonej pamięci TSR (SHARDMEM.ASM). ; Program ten zakłada, że użytkownik ma już uruchomiony program SHMAPP1 wprowadzający ciąg do ; pamięci dzielonej. Program ten po prostu drukuje ten ciąg z pamięci dzielonej .xlist include includelib .list

stdlib.a stdlib.lib

dseg ShmID dseg

segemnt para public ‘data’ byte 0 ends

cseg

segemnt para public ‘code’ assume cs:cseg, ds:dseg, es:SharedMemory

; SeeIfPresent ; SeeIfPresent

IDLoop:

TryNext: Success:

SeeIfPresent

Sprawdzamy żeby zobaczyć czy pamięć dzielona TSR jest obecna w pamięci. Ustawia flagę zera jeśli jest, zeruje flagę zera jeśli nie ma. Podprogram ten również zwraca ID TSR’a w CL proc push push push mov mov push mov int pop cmp je strcmpl byte je

near es ds. di cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 TryNext

dec js cmp pop pop pop ret endp

cl IDLoop cx, 0 di ds. es

;zaczynamy z ID 0ffh ;funkcja weryfikacji obecności ;obecny w pamięci?

„Static Shared Memory TSR”, 0 Success ;test ID użytkownika 80h..FFh ;zerowanie flagi zera

; Program główny dla aplikacji #1 łączy się z pamięcią dzieloną TSR a potem czyta ciąg od użytkownika ; (przechowywany w pamięci dzielonej) a potem kończy

Main

proc assume cs:cseg, ds.:dseg, es:SharedMemory mov ax, seg mov ds, ax meminit print byte

“Shared memory application #2”, cr, lf, 0

;Zobaczmy czy jest pamięć dzielona TSR call SeeIfPresent je ItsThere print byte „Shared Memoery TSR (SHARDMEM) is not loaded.”,cr, lf byte “This program cannot continue execution.”,cr, lf,0 ExitPgm ; Jeśli pamięć dzielona TSR jest obecna, pobieramy adres dzielonego segmentu do rejestru ES: ItsThere:

mov ah, cl mov al, 10h int 2Fh ;Wydruk ciągu wejściowego w SHMAPP1: print byte lea puts print byte

;ID naszego TSR’a ;pobieranie adresu dzielonego segmentu

„String from SMAPP1 id ‘”,0 di, InputLine

;ES juz wskazuje właściwy segment

„’ from shared memory.”, cr, lf,0

Quit: Main

ExitPgm endp

cseg

ends

sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends

; Segment dzielonej pamięci musi pojawić się po “zzzzzzseg”. Zauważmy, że nie jest to fizyczna pamięć dla danych ; w segmencie dzielonym. Jest to tylko miejsce przechowywania więc możemy zadeklarować zmienne i generować ; ich właściwe offsety. Biblioteka standardowa UCR użyje ponownie pamięci powiązanej z tym segmentem dla ; sterty. Aby uzyskać dostęp do danych aplikacja ta wywołuje pamięć dzieloną TSR aby uzyskać prawdziwy adres ; segmentowy segmentu dzielonej pamięci. Może potem uzyskać dostęp do zmiennych w segmencie pamięci ; dzielonej ; ;Zauważmy, że wszystkie deklaracje zmiennych pochodziły z pliku wejściowego. Wszystkie aplikacje, które ; odnoszą się do segmentu pamięci dzielonej zawierają ten plik w segmencie SharedMemory. To zakłada, że

; wszystkie dzielone segmenty mają takie samo rozmieszczenia SharedMemory segment para public ‘Shared’ include SharedMemory ends end

shmvars.asm Main

19.2.2 DYNAMICZNA PAMIĘĆ DZIELONA Chociaż statycznie dzielona pamięć opisana w poprzedniej sekcji jest bardzo użyteczna, cierpi na kilka ograniczeń. Przede wszystkim, program, który używa globalnie dzielonego segmentu musi być świadomy lokacji każdego innego programu , który używa segmentu dzielonego. To świadczy, że używanie dzielonego segmentu jest ograniczone do pojedynczego zbioru współpracujących procesów danych w jednym czasie, Nie możemy mieć dwóch niezależnych zbiorów programów używających pamięci w tym samym czasie. Innym ograniczeniem systemu statycznego jest to, że musimy znać rozmiar wszystkich zmiennych, kiedy piszemy nasz program, nie możemy tworzyć dynamicznych struktur danych , których rozmiar różni się w czasie wykonania. Byłoby miłe, na przykład, mieć funkcje jak shmalloc i shmfree, które pozwoliły by nam dynamicznie alokować i zwalniać pamięć w dzielonym regionie. Na szczęście, jest bardzo łatwo pokonać te ograniczenia poprzez stworzenie dynamicznie dzielonego menadżera pamięci. Sensowny dzielony menadżer pamięci będzie miał cztery funkcje: inicjalizacja, shmalloc, shmattach i shmfree. Funkcja inicjalizacyjna odzyskuje całą używaną pamięć dzieloną. Funkcja shmalloc pozwala procesowi zaalokować nowy blok pamięci dzielonej. Tylko jeden proces w grupie współpracujących procesów robi to wywołanie. Skoro shmalloc alokuje blok pamięci, inne procesy używają funkcji shmattach dla uzyskania adresu bloku pamięci dzielonej. Poniższy kod implementuje dynamicznego menadżera pamięci dzielonej. Kod jest podobny do tego z Biblioteki Standardowej, z wyjątkiem kodu zezwalającego na maksimum 64K pamięci na stercie. ; SHMALLOC.ASM ; ; Ten TSR ustawia system dynamicznej pamięci dzielonej ; ; TSR ten sprawdza aby upewnić czy nie ma już aktywnej kopii w pamięci. Kiedy usuwa się z pamięci, ; upewnia się, że nie ma innych łańcuchów przerwań w INT 2Fh zanim dokona usunięcia. ; ; Poniższe segmenty muszą pojawić się w takiej kolejności i przed zawarciem Biblioteki Standardowej ResidentSeg ResidentSeg

segment para public ‘Resident’ ends

SharedMemory segment para public ‘Shared’ SharedMemory ends EndResident EndResident

segment para public ‘EndRes’ ends .xlist .286 include includelib .list

stdlib.a stdlib.lib

; Segment rezydentny ,który przechowuje kod TSR: ResidentSeg

segment para public ‘Resident’ assume cs: ResidentSeg, ds: nothing

NULL

equ

0

;Struktura danych dla alokowanego regionu danych ; ; Key- użytkownik dostarcza ID do powiązanego regionu z określonym zbiorem procesów ; ; Next- wskazuje następny alokowany blok ; Prev- Wskazuje poprzedni alokowany blok ; Size- Rozmiar (w bajtach) alokowanego bloku, nie zwiera struktury nagłówka Region key next prev blksize Region

struct word word word word ends

? ? ? ?

Stratmem

equ

Region ptr [0]

AllocatedList FreeList

word word

0 0

;Wskazuje łańcuch alokowanego bloku ; Wskazuje łańcuch wolnych bloków

;Numer ID Int 2Fh dla tego TSR’a MyTSRID

byte byte

0 0

;możemy go wydrukować

;PSP jest adresem psp dla tego programu PSP

word

0

OldInt2F

dword ?

; MyInt2F; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;

Dostarczamy int 2Fh (przerwanie równoczesnych procesów) dla tego TSR’a. Przerwanie równoczesnych procesów rozpoznaje następujące podfunkcje (przekazane w AL.): 00h- weryfikacja obecności: 01h- usuwanie 11h- shmalloc

12h-shmfree 13h-shminit 14h- shmattach

Zwraca 0FFh w rejestrze AL i wskaźnik do ID ciągu w es:di jeśli ID TSR’a (w AH) jest dopasowane do tego szczególnego TSR’a Usuwa TSR z pamięci. Zwraca 0 w AL jeśli powodzenie, 1 w AL jeśli niepowodzenie CX zawiera rozmiar bloku do alokacji. DX zawiera key do tego bloku. Zwraca wskaźnik do bloku w ES:DI i rozmiar alokowanego bloku w CX. Zwraca kod błędu w AX. Zero nie jest błędem, jeden jest „już istniejącym key”, dwa jest „niewystarczającym zapotrzebowaniem na pamięć” DX zawiera key dla bloku. Funkcja ta zwraca określony blok z pamięci. Inicjalizuje system pamięci dzielonej zwalniając wszystkie bloki aktualnie w użyciu DX zawiera key dla bloku. Przeszukuje ten blok i zwraca jego adres w ES:DI. AX zawiera zero jeśli powodzenie, trzy jeśli nie można ulokować bloku określonym key.

MyInt2F

proc far assume ds.:nothing cmp je jmp

ah, MyTSRID YepItsOurs OldInt2F

;identyfikator naszego TSR’a dopasowany?

;Okay, znamy ten nasz ID, teraz sprawdzamy funkcje weryfikacji, usuwania lub zwracanego segmentu YepItsOurs

cmp jne mov lesi iret

al., 0 TryRmv al., 0ffh IDString

IDString

byte

„Dynamic Shared Mmeory TSR”,0

TryRmv

cmp jne

al, 1 Tryshmalloc

;funkcja weryfikacji ;zwraca powodzenie ;wraca do kodu wywołującego

; funkcja usuwania

;zobaczmy czy możemy usunąć ten TSR:

TRDone:

push mov mov cmp jne cmp je mov pop iret

es ax, 0 es, ax word ptr es:[2Fh*4] , offset MyInt2F TRDone word ptr es:[2Fh*4+2], seg MyInt2F CanRemove ; skok jeśli możemy ax, 1 zwraca niepowodzenie es

; Okay chcemy to usunąć i możemy to usunąć z pamięci . Dopilnujemy tego wszystkiego tutaj CanRemove:

assume push pusha cli mov mov mov mov

ds: ResidentSeg ds ax, 0 es, ax ax, cs ds., ax

mov mov mov mov

ax, word ptr OldInt2F es:[2Fh*4], ax ax, word ptr OldInt2F+2 es:[2Fh*4+2], ax

;wyłączamy przerwania kiedy mieszamy w ; wektorach przerwań

;Okay , ostatnia rzecz przed wyjściem – Podajemy zaalokowaną pamięć dla tego TSR’a z powrotem do DOS mov mov mov int

ds., PSP es, ds.:[2Ch] ah, 49h 21h

;Wskaźnik do bloku środowiska

mov mov mov int

ax, ds. es, ax ah, 49h 21h

popa pop pop mov

ds. es ax, 0

;zwolnienie przestrzeni kodu programu

;zwraca powodzenie

; Wkładamy BadKey tutaj, aby zamknąć jego powiązany skok (poniżej) ; ; Jeśli przychodzi tu, odkrywamy zaalokowany blok z określonym key. Zwraca kod błędu (AX =1) ; i rozmiar tego zaalokowanego bloku (w CX) BadKey:

mov cx, [bx].Region.BlkSize mov ax, 1 ;już zaalokowany błąd pop bx pop ds. iret ;zobaczmy czy jest to funkcja shmalloc ; jeśli tak, na wejściu – ; DX zawiera key ; CX zawiera liczbę bajtów do zaalokowania ; ; na wyjściu : ; ; ES:DI wskazują na alokowany blok (jeśli pomyślnie) ; CX zawiera aktualny rozmiar alokowanego bloku )>=CX na wyjściu) ; AX zawiera kod błędu, 0 jeśli nie ma błędu Tryshmalloc:

cmp jne

al., 11h Tryshmfree

;kod funkcji shmalloc

;najpierw, przeszukujemy całą alokowaną listę aby zobaczyć czy blok z aktualnym numerem key’a ; już istnieje. DX zawiera żądany klucz . assume ds.: SharedMemory assume bx: ptr Region assume di: ptr Region

SearchLoop:

push push mov mov mov test je

ds bx bx, SharedMemory ds, bx bx, ResidentSeg: AllocatedList bx, bx SrchFreeList

cmp je mov test jne

dx, [bx]. Key BadKey bx, [bx].Next bx, bx SearchLoop

;coś na tej liście? ;czy klucz już istnieje? ;pobranie kolejnego regionu ;NULL? Jeśli nie spróbuj inne ;wejście na liście

;Jeśli alokowany blok z określonym key’em nie istnieje, wtedy próbujemy alokować jeden z listy wolnej pamięci

SrchFreeList:

FirstFitLp:

mov test Je

bx, ResidentSeg: FreeList bx, bx OutaMemory

cmp jbe mov test jne

cx, [bx].BlkSize GotBlock bx, [bx].Next bx, bx FirstFitLp

; Lista pusta? ;czy ten blok jest wystarczająco duży? ;jeśli nie, następny ;czy cos jeszcze na liście?

;Jeśli znaleźliśmy się tutaj ,nie byliśmy w stanie znaleźć bloku, który był wystarczająco duży aby spełnić żądanie. ; Zwraca właściwy błąd OutaMemory:

mov mov pop pop iret

cx, 0 ax, 2 bx ds.

, nic nie dostępne ; błąd niewystarczającej pamięci

;Jeśli znajdziemy dość duży blok, możemy wyciąć z niego nowy blok i zwrócić resztę pamięci do listy wolnej ; pamięci. Jeśli wolny blok jest przynajmniej 32 bajty większy niż żądany rozmiar, zrobimy to . Jeśli ; wolny blok jest mniejszy niż 32 bajty, po prostu dajemy ten wolny blok do żądanego procesu. Powód 32 bajtowości ; jest prosty: Potrzebujemy ośmiu bajtów dla nowego nagłówka bloku (wolny blok ma już jeden) i nie ma sensu ; rozkładanie bloków na rozmiar poniżej 24 bajtów. To zwiększałoby czas przetwarzania, kiedy procesy zwalniają ; bloki przez wymaganie większej pracy przy łączenie bloków. GotBlock:

mov sub cmp jbe

ax, [bx].BlkSize ax, cx ax, 32 GrabWholeBlk

;obliczenie różnicy w rozmiarze ;przynamniej 32 bajty? ;jeśli nie bierzemy ten blok

; Okay, wolny blok jest większy niż wymagany rozmiar 32 bajtów. Wycinamy nowy blok z końca wolnego bloku ; (w ten sposób nie musimy zmieniać wskaźników wolnego bloku, tylko rozmiar) mov add sub

di, bx di, [bx]. BlkSize di, cx

;skok na koniec, minus 8 ; wskazuje nowy blok

sub sub

[bx].BlkSize, cx [bx].BlkSize, 8

;usuwamy zaalokowany blok I ;miejsce na nagłówek

mov mov

[di].BlkSize, cx [di].Key, dx

;zachowanie rozmiaru bloku ;zachowanie key’a

;Przyłączamy nowy blok do listy zaalokowanych bloków

NoPrev: RmvDone;

mov mov mov test je mov mov add mov mov

bx, ResidentSeg:AllocatedList [di].Next, bx [di].Prev, NULL bx, bx NoPrev [bx].Prev, di residentSeg:AllocatedList, di di, 8 ax, ds es, ax

;NULL poprzedni wskaźnik ;zobaczmy czy była pusta lista ;ustawimy poprzedni wskaźnik dla starego ;wskazuje aktualny obszar danych ; zwraca wskaxnik w es:di

mov ax, 0 ;zwraca powodzenie pop bx pop ds. iret ; Jeśli bieżący wolny blok jest większy niż żądany, ale nie większy niż 32 bajty, dajemy użytkownikowi cały blok GrabWholeBlk:

mov mov cmp je cmp je

di, bx cx, [bx].BlkSize [bx].Prev, NULL Rmvlst [bx].Next, NULL RmvLast

;zwraca aktualny rozmiar ;pierwszy człon na liście? ;Ostatni człon na liście?

;Okaz, rekord ten jest wciśnięty między dwa inne w liście. Wycinamy go z pomiędzy nich mov mov mov

ax, [bx].Next bx, [bx].Prev [bx].Next, ax

;zachowujemy wskaźnik do kolejnej ; pozycji poprzedniej pozycji w kolejnym ; polu

mov mov mov jmp

ax, bx bx, [di].Next [bx].Prev, bx RmvDone

;zachowujemy wskaźnik do poprzedniej ; pozycji w następnej pozycji poprzedniego ;pola

;Blok jaki chcemy usunąć jest na początku listy wolnych bloków. Może być również jedynie pozycją na liście! RmvLast:

mov mov jmp

ax, [bx].Next FreeList, ax RmvDone

;usuwanie z listy wolnych bloków

;Jeśli blok jaki chcemy usunąć jest na końcu listy obsługujemy ten tu RmvLast:

mov mov jmp

bx, [bx].Prev [bx].Next, NULL RmvDone

assume ds: nothing, bx:nothing, di:nothing ; ten kod obsługuje funkcję SHMFREE. Na wejściu DX zawiera key dla zwalnianego bloku , musimy przeszukać ; całą listę zaalokowanych bloków i znaleźć blok z tym key’em. Jeśli nie znajdziemy takiego bloku, kod ten wróci ; bez wykonania czegokolwiek. Jeśli znajdziemy blok, musimy dodać jego pamięć do wspólnego wolnego obszaru ; Jednakże, nie możemy po prostu wstawić tego bloku na początku listy wolnych bloków (jaki robiliśmy dla bloków ; alokowanych). Możemy założyć, że ten blok zwolniony jest przyległy do jednego lub dwóch innych wolnych ; bloków. Kod ten musi połączyć takie bloki w pojedynczy wolny blok. Tryshmfree:

cmp jne

al., 12h Tryshminit

;najpierw, przeszukamy listę zaalokowanych bloków aby sprawdzić czy możemy znaleźć blok do usunięcia. Jeśli ; nie znajdziemy go na tej liście nigdzie, powrót assume ds: SharedMemory assume bx: ptr Region assume di: ptr egion push

ds

push push

di bx

mov mov mov

bx, SharedMemory ds, bx bx, ResidentSeg: AllocatedList

test bx, bx ;czy pusta lista alokacji? je FreeDone SrchList: cmp dx, [bx].Key ;przeszukanie dla key’a w DX je FoundIt mov bx, [bx].Next test bx, bx ;czy koniec listy? jne SrchList FreeDone: pop bx pop di ; nic zaalokowanego, więc powrót do pop ds. ; kodu wywołującego iret ;Okay znaleźliśmy blok jaki użytkownik chce usunąć. Usuwamy go z listy alokacji. Są trzy przypadki do ; rozpatrzenia: (1) jest na początku listy alokacji, (2) jest na końcu listy alokacji i (3) jest w środku listy ; alokacji FoundIt:

cmp je cmp je

[bx].Prev, NULL Freelst [bx].Next, NULL FreeLast

;pierwsza pozycja na liście? ;ostatnia pozycja na liście?

;Okay, usuwamy zaalokowaną pozycję ze środka listy alokacji mov mov mov xchg mov jmp

di, [bx].Next ax, [bx].Prev [di].Prev, ax ax, di [di].Next, ax AddFree

;[next].prev := [cur].prev

;[prev].next := [cur].next

;Obsłużymy przypadek gdzie usuwamy pierwszą pozycję z listy alokacji. Jest to możliwe, że jest to jedyna pozycja ; na liście (tj. jest to pierwsza i ostatnia pozycja na liście), ale ten kod obsługuje przypadek bez takich problemów Freelst:

mov mov jmp

ax, [bx].Next ResidentSeg:AllocatedList, ax AddFree

;Jeśli usuwamy ostatni człon w łańcuchu, po prostu ustawiamy następne pole poprzedniego węzła w liście na NULL FreeLast:

mov Mov

di, [bx].Prev [di].next, NULL

;Okay, teraz możemy włożyć zwolniony blok do listy wolnych bloków. Lista wolnych bloków jest posortowana ; według adresów. Musimy wyszukać pierwszy wolny blok, którego adres jest większy niż blok, który właśnie ; zwolniliśmy i wprowadzić nowy wolny blok przed nim. Jeśli dwa bloki są przyległe, wtedy musimy je podzielić ; na pojedyncze wolne bloki. Również, jeśli blok przed jest przyległy, musimy podzielić go. To połączy wszystkie ; wolne bloki w liście wolnych bloków więc jest kilka wolnych bloków możliwych, a bloki te są tak duże jak to ; możliwe AddFree:

mov

ax, ResidentSeg :FreeeList

test jne

ax,ax SrchPosn

,Pusta lista?

;Jeśli lista jest pusta, zlepimy te człony jedynie na wejściu mov ResidentSeg: FreeList, bx mov [bx].Next, NULL mov [bx].Prev, NULL jmp FreeDone ; Jeśli lista wolnych bloków nie jest pusta, wyszukujemy pozycję tego bloku na liście: SrchPosn:

mov cmp jb mov test jne

di, ax bx, di FoundPosn ax, [di].Next ax, ax SrchPosn

;Koniec listy?

;Jeśli jesteśmy tu, znaczy ,że wolny blok należy do końca listy. Zobaczymy czy musimy podzielić ; nowy blok ze starym mov add add cmp je

ax, di ax, [di].BlkSize ax, 8 ax, bx MergeLast

;obliczamy adres pierwszego ; bajtu po tym bloku

;Okay, właśnie dodajemy wolny blok do końca listy mov mov mov jmp

[di].Next, bx [bx].Prev, di [bx].Next, NULL FreeDone

;Dzielimy zwolniony blok z blokiem wskazywanym przez DI MergeLast:

mov ax, [di].Blksize add ax, [bx].BlkSize add ax, 8 mov [di].BlkSize, ax jmp FreeDone ; Jeśli znaleźliśmy wolny blok zanim przypuszczalnie wprowadziliśmy aktualny wolny blok, wrzucamy go tu i ; obsługujemy FoundPos:

mov add add cmp jne

ax, bx ax, [bx].BlkSize ax,8 ax, di DontMerge

;obliczamy adres kolejnego bloku w pamięci ; równe temu blokowi?

; Jeśli kolejny wolny blok jest przyległy do jednego ze zwolnionych, wiec dzielimy dwa mov add add mov

ax, [di].BlkSize ax, 8 [bx].BlkSize, ax ax, [di].Next

;dzielimy rozmiary razem

mov mov mov jmp

[bx].Next, ax ax, [di].Prev [bx].Prev, ax tryMergeB4

;Jeśli nie są przyległe, łączymy je tutaj razem DontMerge:

mov mov mov mov

ax, [di].Prev [di].Prev,bx [bx].Prev, ax [bx].Next, di

;Teraz zobaczymy czy możemy podzielić aktualny wolny blok z poprzednim wolnym blokiem TryMergeB4:

mov mov add add cmp je pop pop pop iret

di, [bx].Prev ax, di ax, [di].BlkSize ax, 8 ax, bx CanMerge bx di ds.

;Nic zaalokowanego, wracamy ;do kodu wywołującego

; Jeśli możemy podzielić poprzedni i aktualny wolny blok, robimy to tutaj: CanMerge:

mov mov mov add add pop pop pop iret

ax, [bx].Next [di.Next, ax ax, [bx].BlkSize ax, 8 [di].BlkSize, ax bx di ds

assume ds:nothing assume bx:nothing assume di:nothing ; Tutaj obsługujemy funkcję inicjalizacyjną (SHMINIT) dzielonej pamięci. Wszystko co musimy zrobić to stworzyć ; pojedynczy blok w liście wolnych bloków (cała dostępna pamięć), opróżnić listę alokacji i wyzerować całą ; dzieloną pamięć Tryshminit:

cmp jne

al., 13h TryShmAttach

; Resetujemy obszar alokacji pamięci zawierający pojedynczy, wolny blok pamięci, którego rozmiar to 0FFF8h ; (musimy zarezerwować osiem bajtów dla struktury danych bloku) push push push

es di cx

mov

ax, SharedMemory

;zerujemy segment pamięci dzielonej

rep

mov mov xor mov stosw

es, ax cx, 32768 ax, ax di, ax

;Notka: zakomentowane poniższe linie nie są konieczne ponieważ powyższy kod wyzerował już ; cały segment pamięci dzielonej. Notka: nie możemy odłożyć pierwszego rekordu pod offsetem zero ponieważ ; zero jest to specjalna wartość dla wskaźnika NULL. Zamiast tego użyjemy 4 mov di, 4 ; mov es:[di].Region.Key, 0 ;Key jest arbitralny ; mov es:[di].Region.Next ,0 ; Żadnych innych wejść ; mov es:[di}.Region.Prev, 0 ; jak wyżej ; mov es:[di].Region.BlkSize, 0FFF8h ;Reszta segmentu mov ResidentSeg:FreeList, di pop cx pop di pop es mov ax, 0 ; nie zwrócono błędu iret ;Funkcję SHMATTACH obsługujemy tutaj. Na wejściu, DX zawiera numer key’a. Przeszukujemy zaalokowany ; blok z tym numerem key’a i zwracamy wskaźnik do tego bloku (jeśli znaleziono) w ES:DI. Zwracamy kod błędu ; jeśli nie można znaleźć bloku TryShmAttach:

FindOurs:

cmp jne mov mov

al., 14h IllegalOp ax, SharedMemory es, ax

;opcod przyłączenia

mov cmp je mov test jne mov iret

di, ResidentSeg:AllocatedList dx, es:[di].Region.Key FoundOurs di, es:[di].Region.Next di, di FoundOurs ax, 3

;nie można znaleźć key’a

;wywoływanie z niepoprawną wartością funkcji. Spróbujemy zrobić jak najmniej szkód jak to możliwe IllegalOp: MyInt2F ResidentSeg

mov ax, 0 iret endp assume ds:nothing ends

;Kto wie co to ma być?

;tutaj jest segment w którym będziemy przechowywać dzielone dane SharedMemory segment para public ‘Shared’ db 0FFFFh dup (?) SharedMemory ends cseg

segment para public ‚code’ assume cs:cseg, ds:ResidentSeg

; SeeIfPresent; ;

Sprawdza aby zobaczyć czy nasz TSR jest już obecny w pamięci. Ustawia flagę zera jeśli jest, zeruje flagę zera jeśli nie ma

SeeIfPresent

proc push push push mov mov push mov int pop cmp je strcmpl byte je

near es ds. di cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 TryNext

dec js cmp pop pop pop ret endp

cl IDLoop cx, 0 di ds. es

IDLoop:

TryNext: Success:

SeeIfPresent

;start z ID 0FFh ;weryfikacja obecności ; obecny w pamięci?

„Dynamic Shared Memory TSR”, 0 Success ;testuje ID użytkownika 80h..FFh ;zerowanie flagi zera

;FindID; ; ; ;

Określamy pierwszy (cóż w rzeczywistości ostatni) ID TSR’a dostępny w łańcuchu równoczesnych procesów. Zwraca tą wartość w rejestrze CL.

FindID

proc push push push

near es ds. di

mov mov push mov int pop cmp je dec js xor cmp pop pop pop ret

cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 Success cl IDLoop cx, cx cx, 1 di ds. es

IDLoop:

Success:

Zwraca ustawioną flagę zera jeśli lokuje pusty slot Zwraca wyzerowaną flagę zera jeśli niepowodzenie

;start z ID 0FFh ; weryfikacja obecności ;obecny w pamięci? ; test ID użytkownika 80h..FFh ; zerowanie flagi zera

FindID

endp

Main

proc meminit mov mov

ax, ResidentSeg ds., ax

mov int mov

ah, 62h 21h PSP, bx

;pobranie wartości PSP programu

; Zanim zrobimy cokolwiek, musimy sprawdzić parametry linii poleceń. Jeśli jest jeden, i jest to ; słowo „REMOVE”, wtedy usuwamy rezydentną kopię z pamięci używając przerwania równoczesnych ; procesów argc cmp jb je Usage:

cx, 1 TstPresent DoRemove

;musi mieć 0 lub 1 parametr

print byte „Usage:”, cr, lf byte “shmalloc”, cr, lf byte “ or shmalloc REMOVE”,cer,lf,0 ExitPgm

;Sprawdzenie na polecenie REMOVE DoRemove

mov ax, 1 argv strimcpl byte “Remove”, 0 jne Usage call SeeIfPresent je RemoveIt print byte “TSR is not present in memory, cannot remove” byte cr, lf,0 ExitPgm

RemoveIt:

mov MyTSRID, cl printf byte “rremoving TSR (ID #%d) from memory…..”, 0 dword MyTSRID mov ah, cl mov al, 1 ;usuwanie cmd, ah zawiera ID int 2Fh cmp al., 1 ;Powodzenie? je RmvFailure print byte „removed”,cr,lf,0 ExitPgm

RmvFailure:

print byte cr, lf byte “Could not remove TSR from memmory”,cr,lf byte “Try removing inne TSR’y in reverse order” byte „you installed them”,cr,lf,0 ExitPgm

;Okay, zobaczmy czy nasz TS jest już w pamięci. Jeśli tak, przerywamy proces instalacji TstPresent:

call SeeIfPresent jne GetTSRID print byte „TSR jest już obecny w pamięci ”,cr,lf byte „Przerwanie procesu instalacji”,cr,lf,0 ExitPgm

; Pobranie ID dla naszego TSR’a i zachowanie go GetTSRID:

call FindID je GetFileName print byte „Zbyt wiele rezydentnych TSR’ów, nie można instalować”,cr,lf,0 ExitPgm

; Instalujemy przerwania GetFileName:

mov print byte

MyTSRID, cl “Instalowanie przerwań…”, 0

;Aktualizacja łańcucha przerwań INT 2Fh cli mov mov mov mov mov mov mov mov sti

;wyłączamy przerwania ax, 0 es, ax ax, es:[2Fh*4] word ptr OldInt2F, ax ax, es:[2Fh*4+2] word ptr OldInt2F+2, ax es:[2Fh*4], offset MyInt2F es:[2Fh*4+2], seg ResidentSeg ;Ok , włączamy przerwania

; Jedyna rzecz jak nam pozostała to inicjalizacja segmentu pamięci dzielonej a potem TSR printf byte „Instalowanie , TSR ID #%d.”,cr,lf,0 dword MyTSRID mov mov int

ah, MyTSRID al, 13h 2Fh

;funkcja inicjalizująca

mov sub mov int

dx, EndResident dx, PSP ax, 3100h 21h

;oblicza rozmiar programu ;polecenie TSR DOS’a

Main cseg

endp ends

sseg stk sseg

segment para stack ‘stack’ db 256 dup (?) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

Możemy zmodyfikować dwoi aplikacje z poprzedniej sekcji próbując takiego kodu: ;SHMAPP3.ASM ; ; Jest to aplikacja z dzieloną pamięcią, która używa dynamicznie dzielonej pamięci TSR (SHMALLOC.ASM) ; Program ten wprowadza ciąg od użytkownika i przekazuje ten ciąg do SHMAPP4.ASM w całym obszarze ; pamięci dzielonej. .xlist include includelib ;list

stdlib.a stdlib.lib

dseg ShmID dseg

segment para public ‘data’ byte 0 ends

cseg

segemnt para public ‘code’ assume cs:cseg, ds: dseg, es:SharedMemory

; SeeIfPresent; ; SeeIfPresent

IDLoop:

TryNext: Success:

Sprawdza aby zobaczyć czy TSR pamięci dzielonej jest obecny w pamięci Ustawia flagę zera jeśli jest, zeruje flagę zera jeśli nie. Ten podprogram również zwraca ID TSR’a w CL proc push push push mov mov push mov int pop cmp je strcmpl byte je dec js cmp pop pop

near es ds di cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 TryNext

;start z ID 0FFH ;weryfikacja obecności ;obecny w pamięci?

„Dynamic Shared Memory TSR”, 0 Success cl ;test ID użytkownika 80f..FFh IDLoop cx, 0 ;zerowanie flagi zera di ds.

SeeIfPresent

pop ret endp

es

; Program główny dla aplikacji #1 łączy TSR pamięci dzielonej a potem odczytuje ciąg od użytkownika. ; (przechowując ciąg w pamięci dzielonej) a potem kończy Main

proc assume cs:cseg, ds.:dseg, es:SharedMemory mov ax, dseg mov ds, ax meminit print byte “Shared memory application #3”,cr,lf,0

;zobaczmy czy jest TSR pamięci dzielonej: call SeeIfPresent je ItsThere print byte „Shared Memory TSR (SHMALLOC) nie jest załadowany ”,cr,lf byte „Ten program nie może kontynuować wykonywania”,cr,lf,0 ExitPgm ; pobranie lini wejściowej od użytkownika ItsThere:

mov print byte lea getsm

ShmID, cl “Wprowadź ciąg: “,0 di, InputLine

;ES już wskazuje właściwy segment

; Ciąg jest w naszej przestrzeni sterty. Przesuniemy go ponad segment pamięci dzielonej strlen inc push push

cx es di

;dodajemy jeden do zera bajtów

mov mov mov int

dx,1234h ah, ShmID al., 11h 2Fh

; wartość “naszego” key’a

mov mov

si, di dx, es

; zachowujemy jako wskaźnik przeznaczenia

pop pop strcpy

di es

; odzyskanie adresu źródłowego

print byte puts print

;funkcja shmalloc

;kopiujemy z lokalnego do dzielonego „Wprowadzono ‘”, 0

byte

„’ do pamięci dzielonej”, cr,lf,0

Quit: Main

ExitPgm endp

;makro DOS’a do wyjścia z programu

cseg

ends

sseg stk sseg

segemnt para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

; SHMAPP4.ASM ; ; Jest to aplikacja z dzieloną pamięcią, która używa dynamicznie dzielonej pamięci TSR (SHMALLOC.ASM) ; Program ten zakłada, że użytkownik ma już uruchomiony program SHMAPP3 wprowadzający ciąg do ; pamięci dzielonej. Program ten po prostu drukuje ten ciąg z pamięci dzielonej .xlist include includelib .list

stdlib.a stdlib.lib

dseg ShmID dseg

segment para public ‘data’ byte 0 ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg, es:SharedMemory

; SeeIfPresent; ; SeeIfPresent

Sprawdzamy czy pamięć dzielona TSR jest obecna w pamięci. Ustawiamy flagę zera jeśli jest, zeruje flagę zera jeśli nie. Ten podprogram również zwraca ID TSR’a w CL

IDLoop:

TryNext:

proc push push push mov mov push mov int pop cmp je strcmpl byte je

near es ds. di cx, 0ffh ah, cl cx al, 0 2Fh cx al., 0 TryNext

dec

cl

;start z ID 0FFH ;funkcja weryfikacji obecności ;obecny w pamięci?

„Dynamic Shared Memory TSR”,0 Success ;Test ID użytkownika 80h..FFh

Success:

SeeIfPresent

js cmp pop pop pop ret endp

IDLoop cx, 0 di ds. es

;zerujemy flagę zera

;Program główny dla aplikacji #1 łączy pamięć dzieloną TSR a potem odczytuje ciąg od użytkownika ; (przechowywany w pamięci dzielonej) a potem kończy Main

proc assume cs:cseg, ds.:dseg, es: SharedMemory mov ax, dseg mov ds,ax meminit print byte

“shared mempory application #4”, cr,lf,0

;zobaczmy czy jest pamięć dzielona TSR call SeeIfPresent je ItsThere print byte „Pamięć dzielona TSR (SHMALLOC) nie jest załadowana” ,cr, lf byte „Program ten nie może kontynuować wykonywania”, cr,lf,0 ExitPgm ;Jeśli pamięć dzielona TSR jest obecna, pobieramy adres dzielonego segmentu do rejestru ES: ItsThere:

mov mov mov int

ah, cl al, 14h dx, 1234h 2Fh

;ID naszego TSR’a ;funkcja łączenia ;wartość naszego key’a

; Drukujemy ciąg print byte puts print byte

„Ciąg z SHMAPP3 to ‘ „, 0 „ ‘ z pamięci dzielonej ”,cr, lf,0

Quit Main

ExitPgm endp

cseg

ends

sseg stk sseg

segemnt para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segemnt para public ‘zzzzzz’ db 16 dup (?) ends

end

Main

19.3 WSPÓŁPROGRAMY Procesy DOS, nawet kiedy używają pamięci dzielonej, cierpią z powodu jednej poważnej wady – każdy program wykonuje się do końca zanim zwróci sterowanie do procesu macierzystego. Chociaż taki paradygmat jest odpowiedni dla wielu aplikacji, z pewnością nie jest wystarczający dla wszystkich. Popularnym paradygmatem dla dwóch programów jest wymiana sterowania z CPU tam i z powrotem podczas wykonywania. Mechanizm ten, nieznacznie różni się od wywołania podprogramów i mechanizmu powrotu, to współprogram. Przed omówieniem współprogramów, dobrym pomysłem jest dostarczenie solidnej definicji dla terminu proces. W dużym skrócie, proces jest to program, który jest wykonywany. Program może istnieć na dysku; procesy istnieją w pamięci i mają stos programu (z adresem powrotnym itd.) powiązany z nimi. Jeśli jest wiele procesów w pamięci w tym samym czasie, każdy program musi mieć swój własny stos programu. Operacja współwywołania przekazuje sterowanie pomiędzy dwoma procesami. Współwywołanie jest skutecznym wywołaniem i zwraca instrukcje wszystkie skierowane na jedna operację. Z punktu widzenia procesu wykonującego współwywołanie, operacja współwywołania jest odpowiednikiem procedury call; z punktu widzenia procesu będącego wywoływanym, operacja współwywołania jest odpowiednikiem operacji powrotu . Kiedy drugi proces współwywołuje pierwszy, sterowanie nie rozpoczyna się od początku pierwszego procesu, ale bezpośrednio po operacji współwywołania . Jeśli dwa procesy wykonują sekwencję wzajemnych współwywołań, sterowanie będzie przekazywane między dwoma procesami w następujący sposób: Proces # 1

Proces #2

cocall prcs2 cocall prcs1

cocall prcs2

cocall prcs2 cocall prcs1

cocall prcs1 Sekwencja współwywołania pomiędzy dwoma procesami Współwywołania są całkiem użyteczne przy grach, gdzie „gracze” jeden po drugim, wywołuje różne strategie. Pierwszy gracz wykonuje jakiś kod robiąc pierwszy ruch, potem współwywołuje drugiego gracza i zezwala na wykonanie ruchu. Po drugim graczu, który wykonał swój ruch, współwywołuje pierwszy proces i daje pierwszemu graczowi drugi ruch, bezpośrednio po jego współwywołaniu. Takie przekazywanie sterowania występuje dopóki jeden gracz nie wygra. CPU 80x86 nie dostarczają instrukcji współwywołania. Jednakże, łatwo jest zaimplementować współwywołania z istniejących instrukcji. Mimo to, istnieje potrzeba dostarczenia własnego mechanizmu współwywołania, Biblioteka Standardowa UCR dostarcza pakietu współwywołania dla procesorów 8086 80186 i

80286. Do tego pakietu zaliczają się struktura danych pcb (blok sterowani procesem) i trzy funkcje jakie możemy wywołać: coinit, cocall i cocall1. Struktura pcb utrzymuje bieżący stan procesu. Utrzymuje wszystkie wartości rejestrów i inne liczące się informacje dla procesu. Kiedy proces dokonuje współwywołania, przechowuje adres powrotu dla współwywołania w pcb. Później, kiedy jakiś inny proces współwywoła ten proces, operacja współwywołania po prostu przeładuje rejestry, wliczając w to cs:ip, z pcb, i zwróci sterowanie do następnej instrukcji po współwywołaniu pierwszego procesu .Struktura pcb przybiera następującą postać: pcb

struct

NextProc regsp regss regip regcs regax regbx regcx regdx regsi regdi regbp regds reges regflags PrcsID StartingTime StartingDate CPUTime

dword word word word word word word word word word word word word word word word dword dword dword

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

;łącze do następnego PCB (przy wielozadaniowości)

Cztery z tych pół istnieje dla wielozadaniowości z wywłaszczeniem i nie ma znaczenia przy współprogramach. Będziemy omawiali wielozadaniowość z wywłaszczeniem w następnej sekcji. Są dwie ważne rzeczy, które powinny być widoczne z tej struktury. Po pierwsze, głównym powodem istnienia wsparcia przez Bibliotekę Standardową współprogramów jest ograniczenie do 16 bitowych rejestrów ponieważ jest tylko miejsce dla 16 bitowych wersji dla każdego rejestru w pcb. Jeśli chcemy wesprzeć 80386 i późniejsze 32 bitowe zbiory rejestrów, będziemy musieli zmodyfikować strukturę pcb i kod, który zachowuje i przywraca rejestry w pcb. Druga rzecz jaka powinna być widoczna, jest to ,że kod wpółpółgoramu zachowuje wszystkie rejestry w poprzek współwywołania. To znaczy ,że nie możemy przekazać informacji z jednego procesu do drugiego w rejestrach kiedy używamy współwywołania. Będziemy musieli przekazać dane pomiędzy procesami w lokacji globalnej pamięci. Ponieważ współprogram generalnie istnieje w tym samym programie, nie będziemy musieli uciekać się do technik pamięci dzielonej. Zmienne jakie zadeklarujemy w segmencie danych będą widoczne dla wszystkich współprogramów. Odnotujmy, że program może zawierać więcej niż dwa współprogramy. Jeśli współprogram jeden współwywołuje współprogram dwa, a współprogram dwa współwywołuje współprogram trzy, a potem współprogram trzy współwywołuje współprogram jeden, współprogram jeden wystąpi bezpośrednio po współwywołaniu go uczynionym przez współprogram trzy.

Proces #1

Proces #2

cocall prcs2

cocall prcs3

Proces #3

cocall 1

Współwywołanie pomiędzy trzema procesami Ponieważ współwywołanie faktycznie wraca do współprogramu docelowego, możemy zastanowić się co się zdarzy przy pierwszym współwywołaniu jakiegoś procesu. W końcu, jeśli ten proces nie wykonywał żadnego kodu, nie ma „adresu powrotnego” gdzie rozpoczyna wykonanie. Jest to prosty problem do rozwiązania, musimy tylko zainicjalizować adres powrotny takiego procesu, adresując pierwszą instrukcję do wykonania w tym procesie. Podobny problem istnieje dla stosu. Kiedy program zaczyna wykonywania, program główny (współprogram jeden) pobiera sterowanie i używa stosu związanego z całym programem. Ponieważ każdy proces musi mieć swój własny stos, gdzie inne współprogramy mają swoje stosy? Najłatwiejszym sposobem zainicjalizowania stosu i początkowego adresu dla współprogramu, jest zrobienie tego kiedy deklarujemy pcb dla procesu. Rozważmy następującą deklarację zmiennej pcb: ProcessTwo

pcb

{0,

offset offset

EndStack2, seg EndStack2, StartLoc2, seg StarLoc2}

Definicja ta inicjalizuje pole NextProc NULL’em ( funkcja współprogramu Biblioteki Standardowej nie używa tego pola) i inicjalizuje pola ss:sp i cs:ip ostatnim adresem obszaru stosu (EndStack2) i pierwszą instrukcją procesu (StartLoc2 ). Teraz co musimy zrobić to zarezerwować rozsądną ilość pamięci stosu dla procesu. Możemy stworzyć wiele stosów w sseg SHELL.ASM jak poniżej: sseg

segment para stack ‘stack’

;Stos dla procesu #2: stk2 EndStack2

byte word

1024 dup (?) ?

:Stos dla procesu #3: stk3 EndStack3

byte word

1024 d up(?) ?

;Pierwszy stos dla programu głównego (proces #1) musi pojawić się na końcu sseg stk sseg

byte ends

1024 dup (?)

Teraz jest pytanie „jak dużo miejsca powinniśmy zarezerwować dla każdego stosu?”. To jest różnie z aplikacjami. Jeśli mamy prostą aplikację, która nie używa rekurencji lub alokuje zmienne lokalne na stosie można założyć najmniej 256 bajtów stosu dla procesu.. Z drugiej strony, jeśli mamy podprogramy rekurencyjne lub alokujemy pamięć na stosie, będziemy potrzebowali znacznie więcej miejsca. Dla prostego programu 1 –8 K pamięci stosu powinno być wystarczające. Zapamiętajmy, ze możemy zaalokować maksimum 64K w sseg SHELL.ASM. jeśli potrzebujemy dodatkowej przestrzeni stosu, będziemy musieli pożyczyć z innych stosów w różnych segmentach (nie muszą być w sseg, jest to konwencjonalne miejsce dla nich) lub będziemy musieli zaalokować inaczej przestrzeń stosu.

Zauważmy, że nie musimy alokować przestrzeni stosu jako tablicy wewnątrz naszego programu. Możemy również zaalokować przestrzeń stosu dynamicznie używając funkcji malloc Biblioteki Standardowej. Poniższy kod demonstruje jak ustawić 8K dynamicznie alokowanego stosu dla pcb zmiennej Proces2: mov malloc jc mov mov

cx. 8192 InsufficientRoom Process2.ss, es Process2.ss, di

Konfigurowanie wywoływanych współprogramów programu głównego jest całkiem łatwe. Jednakże, istnieje kwestia skonfigurowania pcb dla programu głównego. Nie możemy zainicjalizować pcb dla programu głównego w ten sam sposób jak inicjalizowaliśmy pcb dla innych procesów; jest już uruchomiony i ma poprawne wartości cs:ip i ss:sp. Gdybyśmy zainicjalizowali pcb głównego programu w ten sam sposób jak zrobiliśmy to dla innych procesów, wtedy system zrestartowałby po prostu program główny kiedy wykonalibyśmy współwywołanie z powrotem do niego. Przy inicjalizacji pcb dla programu głównego musimy użyć funkcji coinit Funkcja coinit oczekuje, że przekażemy jej adres pcb programu głównego w parze rejestrów es:di. Zainicjalizuje jakieś zmienne wewnątrz Biblioteki Standardowej aby pierwsza operacja współwywołania zachowała stan maszynowy 80x86 w pcb jaki określiliśmy w es:di. Po wywołaniu coinit, możemy zacząć wykonywać współwywołania do innych procesów w naszym programie. Do współwywołania współprogramów używamy funkcji cocall z Biblioteki Standardowej. Wywołanie funkcji cocall przybiera dwie formy. Bez żadnych parametrów funkcja ta przekazuje sterowanie do współprogramu , którego adres pcb pojawia się w parze rejestrów es:di. Jeśli adres pcb pojawia się polu operandu tej instrukcji, cocall przekazuje sterowanie do określonego współprogramu (nie zapomnij , nazwa pcb, nie procesu, musi pojawić się w polu operandu) Najlepszy sposób nauczenia się jak stosować współprogramy jest poprzez przykład. Następujący program jest interesującym kawałkiem kodu, który generuje labirynt na ekranie PC .Algorytm generowania labiryntu ma jedno ważne ograniczenie – musi być nie więcej niż jedno poprawne rozwiązanie. Program główny tworzy zbiór działających w tle procesów zwanych „demonami”. Każdy demon wycina część tematu labiryntu głównego ograniczenia. Każdy demon wykopuje jedną komórkę z labiryntu a potem przekazuje sterowanie do innego demona. Okazuje się , że demony „same siebie mogą zagnać do kąta” i umrzeć (demony żyją tylko dla kopania). Kiedy to się zdarzy, demon usuwa się z listy aktywnych demonów. Kiedy wszystkie demony zginą, labirynt (teoretycznie) jest kompletny. Ponieważ demony giną dość regularnie, musi być jakiś mechanizm tworzenia nowych demonów. Dlatego też ten program losowo daje początek nowym demonom, które zaczynają kopanie swoich własnych tuneli pionowych do ich macierzystych. To pozwala założyć , że jest wystarczający zapas demonów do wykopania całego labiryntu; wszystkie demony zginą tylko wtedy kiedy niema, lub kilka, komórek pozostało do wykopania w labiryncie. ;AMAZE.ASM ; ; Program do wytworzenia / rozwiązania labiryntu ; ; Pogram generuje labirynt 80x25 i bezpośrednio rysuje labirynt na monitorze. Demonstruje zastosowanie ; współprogramów wewnątrz programu .xlist include includelib .list byp dseg

stdlib.a stdlib.lib

textequ segment para public ‘data’

; Stałe: ; ; Definiujemy symbol „ToScreen” dla jakieś wartości) jeśli labirynt ma 80x25 i chcemy wyświetlić go na monitorze

ToScreen

equ

0

; Maksymalne współrzędne X i Y dla labiryntu (dopasowanie do wyświetlacza) MaxXCoord MaxYCoord

equ equ

80 25

; Użyteczne stałe X,Y: WordPerRow BytePerRow

= =

MaxXCoord+2 WordPerRow*2

StartX StartY EndX EndY

equ equ equ equ

1 3 MaxXCoord maxYCoord-1

EndLoc StartLoc

= =

((EndY-1)*MaxXCoord+End-1)*2 ((StartY-1)*MaxXCoord+StartX-1)*2

;początkowa współrzędna X dla labiryntu ;początkowa współrzędna Y dla labiryntu ;końcowa współrzędna X dla labiryntu ;końcowa współrzędna Y dla labiryntu

; Specjalne 16 bitowe kody znaków PC dla ekranu dla symboli malowanych podczas generowania labiryntu. ; Zobacz rozdział o monitorze komputera po szczegóły WallChar NoWallChar VisitChar PathChar

ifdef equ equ equ equ

mono 7dbh 720h 72eh 72ah

else WallChar NoWallChar VisitChar PathChar

equ equ equ equ

; monitor monochromatyczny ; stały blok znaków ; spacja ; kropka ; gwiazdka ;ekran kolorowy

1dbh 0edbh 0bdbh 4e2ah

;stały blok znaków ;spacja ;kropka ;gwiazdka

endif ; poniżej są stałe, które mogą pojawić się w tablicy Maze: Wall NoWall Visited

= = =

0 1 2

;poniżej są kierunki w jakich mogą iść demony w labiryncie North South East West

= = = =

0 1 2 3

;jakieś ważne zmienne ; Tablica Maze musi zawierać dodatkowe wiersze i kolumny wokół zewnętrznych brzegów aby ; nasz algorytm działał poprawnie

Maze

word

(MaxYCOord+2) dup ((MaxXCoord+2) dup (Wall))

;Poniższe makro oblicza indeks do powyższej tablicy zakładając, że współrzędne X I Y demona ; są, odpowiednio, w rejestrach dl i dh. Zwraca indeks w rejestrze AX. MazeAdrs

macro mov mov mul add adc shl endm

al., dh ah, WordPerRow ah al, dl ah, 0 ax, 1

;indeks do tablicy jest obliczony ; (Y*words / row+X)*2 ;konwersja do indeksu bajtowego

;Poniższe makro oblicza indeks do tablicy ekranu, używając takich samych założeń jak powyżej. ; Zauważmy, że macierz ekranu to 80x25 podczas gdy macierz labiryntu to 82x27; Współrzędne X/Y w DL/DH ; to 1 .. 80 i 1..25 zamiast 0..79 i 0..24 (jak potrzebujemy). To makro poprawia to SctrnAdrs

macro mov al., dh dec al mov ah, MaxXCoord mul ah add al, dl adc ah, 0 dec ax shl ax, 1 endm ; PCB dla programu głównego. Będzie to wywoływał ostatni żywy demon , kiedy zemrze MainPCB

pcb

{}

;Lista 32 demonów MaxDemons ModDemons

= =

32 MaxDemons –1

DemonList

pcb

MaxDemons dup {( )}

DemonIndex DemonCnt

byte byte

0 0

;musi być potęga dwójki ;maska dla obleczenia MOD

;indeks do listy demonów ;Liczba demonów na liście

;Generator liczb losowych (będziemy używali naszego generatora liczb losowych zamiast z biblioteki ; standardowej ponieważ chcemy móc określać wartość początkowa Seed

word

dseg

ends

0

;Poniżej mamy adres segmentowy monitora, zmieniamy do od 0B800h do 0B000h jeśli mamy monitor ; monochromatyczny zamiast kolorowego ScreenSeg Screen ScreenSeg

segment at 0b800h equ this word ends

;nie generuj tu danej

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

; całkowicie fałszywy generator liczb losowych, ale nie potrzebujemy więcej jak jednego dla tego programu ; Kod ten używa swojego własnego generatora liczb losowych zamiast tego z Biblioteki Standardowej, więc ; możemy pozwolić użytkownikowi stosować ustalone zakresy dla tworzenia tego samego labiryntu ( w tym samym ; zakresie) lub różnych labiryntów (przez wybranie różnych zakresów) RandNum

RandNum ;Init;

proc push mov and add mov xor rol xor inc mov pop ret endp

near cx cl, byte ptr Seed cl, 7 cl, 4 ax, Seed ax, 55aah ax, cl ax, Seed ax Seed, ax cx

Obsługuje wszystkie prace inicjalizacyjne dla programu głównego. W szczególności , inicjalizuje pakiet współprogramu, pobiera zakres liczb losowych od użytkownika i inicjalizuje monitor

Init

proc print byte getsm atoi free mov

near „Wprowadź małą liczbę całkowitą dla zakresu liczby losowej:”, 0

Seed, ax

; Wypełniamy wnętrze labiryntu znakami ściany, wypełniamy zewnętrzne dwa wiersze i kolumny wartościami. ; Będą to zapobiegało przed wędrowaniem demonów na zewnątrz labiryntu ;Wypełniamy pierwszy wiersz wartościami Visited

rep

cld mov lesi mov stosw

cx, WordsPerRow Maze ax, Visited

; Wypełniamy ostatni wiersz wartościami NoWall mov cx, WordsPerRow lea di, Maze+(MaxYCoord+1)*BytesPerRow rep stosw ; Zapisujemy wartość NoWall na pozycji startowej mov

Maze+(StartY*WordsPerRow+StartX)*2, NoWall

; Zapisujemy wartości NoWall wzdłuż dwóch pionowych brzegów labiryntu lesi

Maze

EdgesLoop:

mov mov mov add loop

cx, MaxYCoord+1 es:[di],ax es:[di+BytesPerRow-2], ax di, BytesPerRow EdgesLoop

ifdef

ToScreen

;zatykamy lewy brzeg ;zatykamy prawy brzeg

;Okay, wypełnimy ekran wartościami WallChar:

rep

lesi mov mov stosw

Screen ax, WallChar cx, 2000

; Zapiszemy właściwe znaki do lokacji początkowej i końcowej: mov mov

word ptr es:Screen+EndLoc, pathChar word ptr es:Screen+StartLoc, NoWallChar

endif

;ToScreen

; Wyzerowanie DemonList:

rep Init

,mov lea mov mov xor stosb

cx, (size pch)*MaxDemons di, DemonList ax, dseg es, ax ax, ax

ret endp

; CanStartFunkcja ta sprawdza aktualną pozycję aby zobaczyć czy generator może kopać ; nowy tunel w kierunku pionowym do aktualnego tunelu,. Możemy tylko zacząć nowy tunel jeśli ; są znaki ściany na przynajmniej dwóch pozycjach w żądanym kierunku: ; ## ; *## ; ## ; ; Jeśli „*” jest aktualną pozycją a „#” przedstawia znaki ściany, a bieżącym kierunkiem jest północ ; lub południe, wtedy generator labiryntu zaczyna nową ścieżkę w kierunku wschodnim. Zakładając ; że „ . „ przedstawia tunel, nie możemy zacząć nowego tunelu w kierunku wschodnim jeśli ; wystąpi jakiś z tych wzorów: ; ; .# #. ## ## ## ## ; *## *## *. # *#. *## *## ; ## ## ## ## .# #. ; ; CanStart zwraca prawdę (ustawiona flaga przeniesienia) jeśli możemy zacząć nowy tunel od ścieżki ; wykopanej przez aktualnego demona. ; ; Na wejściu, dl jest współrzędną X demona ; dh jest współrzędną Y demona ; cl jest kierunkiem demona

CanStart

proc push push

near ax bx

MazeAdrs mov bx, ax

;oblicza indeks do demon(x,y) w labiryncie

; CL zawiera aktualny kierunek, 0= północ, 1=południe, 2= wschód, 3=zachód. Zauważmy, że ; możemy przetestować bit #1 dla północ / południe (0) lub wschód / zachód (1) test jz

cl, 10b NorthSouth

;zobacz czy północ/południe czy wschód/zachód

; Jeśli demon idzie w kierunku wschodnim lub zachodnim, możemy zacząć nowy tunel jeśli jest ; sześć bloków ściany powyżej lub poniżej aktualnego demona Notka: sprawdzamy czy wszystkie ; wartości w tych sześciu blokach są wartościami Wall. Ten kod zależy od faktu czy znaki Wall są ; zerem a suma tych sześciu bloków będzie zerem jeśli ruch jest możliwy.

ReturnFalse

mov add add je

al., byp Maze[bx+BytesPerRow*2] al, byp Maze[bx+BytesPerRow*2+2] al, byp Maze [bx+BytesPerRow*2-2] ReturnTrue

;Mze[x,y+2] ;Maze[x+1, y+2] ;Maze[x-1, y+2]

mov add add je clc pop pop ret

al, byp Maze[bx-BytesPerRow*2] ;Maze[x, y-2] al, byp Maze[bx-BytesPerRow*2+2] ;Maze[x+1, y-2] al, byp Maze[bx-BytesPerRows*2-2] ;Maze[x-1, y-2] returnTrue ;wyzerowana flaga przeniesienia = fałsz bx ax

; Jeśli demon idzie w kierunku północnym lub południowym, możemy zacząć nowy tunel jeśli jest sześć ; bloków ścian na lewo lub prawo bieżącego demona NorthSouth:

ReturnTrue:

mov add add je

al., byp Maze[bx+4] al, byp Maze[bx+BytesPerRow+4] al, byp Maze[bx-BytesPerRow+4] returnTrue

;Maze[x+2, y] ;Maze[x+2, y+1] ;Maze[x+2, y-1]

mov add add jne

al, byp Maze[bx-4] al, byp Maze[bx+BytesPerRow-4] al, byp Maze[bx-BytesPerRow-4] ReturnFalse

;Maze[x-2,y] ;Maze[x-2, y+1] ;maze[x-2, y-1]

stc pop pop ret

bx ax

;ustawiona flaga przeniesienia = prawda

CanStart endp ;CanMove; ; ; ;

testuje aby zobaczyć czy aktualny demon (kierunek = cl, x=dl, y=dh) może ruszyć się określonym kierunku. Przesunięcie jest możliwe jeśli demon nie będzie pochodził z wewnątrz jednego kwadratu innego tunelu. Funkcja ta zwraca prawdę (flaga przeniesienia ustawiona) jeśli ruch jest możliwy. Na wejściu, CH zawiera kierunek tego kodu, który powinniśmy przetestować.

CanMove

proc push push

ax bx

MazeAdrs mov bx, ax cmp jb je cmp je

;wkładamy Maze[x,y] do ax

ch, South IsNorth IsSouth ch ,East IsEast

; Jeśli demon porusza się na zachód, sprawdza bloki w prostokącie sformowanym przez Maze ; [x-2, y-1] do Maze[x-1,y+2] aby upewnić się ,że są wszystkie wartości ściany.

ReturnFalse:

mov add add add add add je clc pop pop ret

al.,byp Maze[bx-BytesPerRow-4] al, byp Maze[bx-BytesPerRow-2] al, byp Maze[bx-4] al, byp Maze[bx-2] al, byp Maze[bx+BytesPerRow-4] al, byp Maze[bx+BytesPerRow-2] ReturnTrue

;Maze[x-2, y-1] ;Maze[x-1, y-1] ;Maze[x-2,y] ;Maze[x-1, y] ;Maze[x-2, y+1] ;Maze [x-1, y+1]

bx ax

; Jeśli demon idzie na wschód sprawdza bloki w prostokącie sformowanym przez Maze[x+1, y-1] ; do Maze[x+2, y+1] aby upewnić się , że wszystkie to wartości ściany. IsEast:

mov add add add add add jne

al., byp Maze[bx-BytesPerRow+4] al, byp Maze[bx-BytesPerRow+2] al, byp Maze[bx+4] al, byp Maze[bx+2] al, byp Maze[bx+BytesPerRow+4] al, byp Maze[bx+BytesPerRow+2] ReturnFalse

ReturnTrue:

stc pop pop ret

bx ax

;Maze[x+2,y-1] ;Maze[x+1, y-1] ;Maze[x+2,y] ;Maze[x+1,y] ;Maze[x+2, y=1] ;Maze[x+1,y+1]

; Jeśli demon idzie na północ, sprawdza bloki w prostokącie sformowanym przez Maze[x-1,y-2] do Maze[x+1,y-1] ; aby upewnić się, że wszystkie są wartościami ściany IsNorth:

mov add add add add add jne stc pop

al., byp Maze[bx-bytesPerRow-2] al, byp Maze[bx-BytesPerRow*2-2] al, byp Maze[bx-BytesPerRow] al, byp Maze[bx-BytesPerRow*2] al, byp Maze[bx-BytesPerRow+2] al, byp Maze[bx-BytesPerRow*2+2] ReturnFalse bx

;Maze[x-1, y-1] ;Maze[x-1, y-2] ;Maze[x, y-1] ;Maze[x+1, y-1] ;Maze[x+1, y-1] ;Maze[x+1, y-2]

pop ret

ax

; Jeśli demon idzie na południe, sprawdza bloki w prostokącie sformowanym przez Maze[x-1, y+2] do ; Maze[x+1, y+1] aby upewnić się, że wszystkie są wartościami ściany IsSouth:

CanMove

mov add add add add add jne stc pop pop ret

al., byp Maze[bx+BytesPerRow-2] al, byp Maze[bx+BytesPerRow*2-2] al, byp Maze[bx+BytesPerRow] al, byp Maze[bx+BytesPerRow*2] al, byp maze[bx+BytesPerRow+2] al, byp Maze[bx+BytesPerRow*2+2] ReturnFalse

;Maze[x-1, y-1] ;Maze[x-1, y+2] ;Maze[x, y-1] ;Maze[x+1, y+1] ;Maze[x+1, y+1] ;Maze[x+1, y+2]

bx ax

endp

;SetDir- zmienia bieżący kierunek. Algorytm kopania labiryntu decyduje o zmianie kierunku tunelu poczynając ; kopanie od jednego z demonów. Kod ten sprawdza czy możemy zmienić kierunek i wybrać nowy jeśli to możliwe ; ; Jeśli demon idzie na północ lub południe, zmiana kierunku powoduje, że demon idzie na wschód lub zachód. ; Podobnie jeśli demon idzie na wschód lub zachód, zmiana kierunku wymusza kierunek na północ lub południe. ; Jeśli demon nie może zmienić kierunków (ponieważ nie może pójść w nowym kierunku z powodu tego lub innego ; powodu.. SetDir wraca bez robienia czegokolwiek. Jeśli zmiana kierunku jest możliwa , wtedy SetDir wybiera ; nowy kierunek. Jeśli jest możliwy tylko jeden nowy kierunek, demon wysyłany jest w tym kierunku. Jeśli demon ; może wyruszyć w jednym z dwóch różnych kierunków, SetDir wybiera jeden z tych dwóch nowych kierunków ; ; Funkcja ta zwraca nowy kierunek w al. SetDir

proc

near

test je

cl, 10b IsNS

;zobacz czy północ / południe lub ;wschód / zachód

; idziemy na wschód lub zachód. Jeśli możemy ruszyć albo na północ albo południe z tego punktu, losowo ; wybieramy jeden z tych kierunków. Jeśli możemy ruszyć tylko jednokierunkowo, wybieramy ten kierunek. Jeśli ; nie możemy iść żadną drogą, wraca bez zmiany kierunku.

DoNorth: NotNorth: DoSouth:

mov call jnc mov call jnc call and

ch, North CanMove NotNorth ch, South CanMove DoNorth RandNum ax, 1

mov ret mov call jnc mov

ax, North ch, South CanMove TryReverse ax, South

;Zobaczmy czy możemy ruszyć na północ ;Zobaczmy czy możemy ruszyć na południe ;Pobranie losowego kierunku ;północ lub południe

ret ;Jeśli demon przesuwa się na północ lub południe, wybieramy nowy kierunek wschód lub zachód, jeśli możliwe IsNS:

mov call jnc mov call jnc call and or ret

ch, East CanMove NotEast ch, West CanMove DoEast RandNum ax, 1b al., 10b

DoEast:

mov ret

ax, East

DoWest:

mov ret

ax, West

NotEast:

mov call jc

ch, West CanMove DoWest

;zobaczmy czy możemy iść na Wschód ;zobaczmy czy możemy na Zachód ;pobranie losowego kierunku ;Wschód lub Zachód

;Jeśli nie możemy przełączyć na kierunek pionowy, zobaczymy czy można się odwrócić TryReverse:

mov xor call jc

ch, cl ch, 1 CanMove ReverseDir

; Jeśli nie możemy się odwrócić , wtedy musimy iść w tym samym kierunku mov mov ret

ah, 0 al., cl

;zostajemy przy tym samym kierunku

; w przeciwnym razie odwracamy kierunek w dół ReverseDir:

SetDir

mov mov xor ret endp

ah, 0 al, cl al, 1

; Stuck- Funkcja ta sprawdza aby zobaczyć , czy demon jest zablokowany i nie może ruszyć się w żadnym kierunku. ; Zwraca prawdę, jeśli demon jest zablokowany i musi być zabity Stuck

proc mov call jc mov call jc

near ch, North CanMove NotStuck ch, South CanMove NotStuck

NotStuck: Stuck

mov call jc mov call ret endp

;NextDemon;

przeszukuje całą listę demonów aby znaleźć następny dostępny demon. Zwraca wskaźnik do niego w es:di.

NextDemon

proc push

near ax

NDLoop:

inc and mov mul mov add cmp je

DemonIndex DemonIndex, ModDemons al, size pcb DemonIndex di ,ax di, offset DemonList byp [di].pcb.NextProc, 0 NDLoop

mov mov pop ret endp

ax, ds es, ax ax

NextDemon ; Dig; ;

ch, East CanMove NotStuck ch, West CanMove

;przejście do następnego demona ; MOD MaxDemons ;Obliczenie indeksu do DemonList ;zobacz czy demon pod tym offsetem ;jest aktywny

To jest proces demona. Przesuwa demona jedną pozycję (jeśli możliwe) w jego aktualnym kierunku. Po przesunięciu o jedną pozycję w przód, jest 25% szansy, że zmieni swój kierunek. jest 25% szans, że ten demon będzie uruchamiał proces potomny dla wykopania w kierunku pionowym

Dig

proc

near

; Zobacz czy bieżący demon jest zablokowany. Jeśli demon jest zablokowany, wtedy musimy usunąć go z listy ; demonów. Jeśli nie jest zablokowany, wtedy musi kontynuować kopanie. Jeśli jest zablokowany i jest to ostatni ; aktywny wtedy zwraca sterowanie do programu głównego call jc

Stuck NotStuck

; Okay, zabijamy aktywny demon. ; Notka: nie zabijemy nigdy ostatniego demona ponieważ mamy uruchomiony proces zegarowy .Proces zegarowy ; jest tym , który zawsze zatrzymuje program dec

DemonCnt

; Ponieważ licznik nie jest zerem, musi być więcej demonów na liście demonów. Zwalniamy przestrzeń stosu ; powiązaną z aktualnym demonem, potem wyszukujemy następny aktywny demon . MoreDemons:

mov mul mov

al., size pcb DemonIndex bx, ax

; Zwalniamy przestrzeń stosu powiązanego z procesem. Zauważmy, że ten kod jest krnąbrny. Zakłada, że stos jest

; zaalokowany podprogramem malloc Biblioteki Standardowej, który zawsze tworzy adres bazowy 8 mov mov free

es, DemonList[bx].regss di, 8

;Oznaczamy wejście demona do tego jako nieużywane mov

byp DemonList[bx]. NextProc, 0

;oznaczone jako nieużywane

;Okay, lokujemy następny aktywny demon na liście FndNxtDmn;

call NextDemon Cocall

;nigdy nie wraca

; Jeśli demon nie jest zablokowany, wtedy kontynuujemy kopanie NotStuck:

mov call jnc

ch, cl CanMove DontMove

;Jeśli możemy ruszyć, wtedy modyfikujemy stosowne współrzędne demona: cmp jb je cmp jne

cl, South MoveNorth MoveSouth cl, East MoveWest

; Przesuwanie na Wschód: inc jmp

di MoveDone

MoveWest:

dec jmp

dl MoveDone

MoveNorth:

dec jmp

dh MoveDone

MoveSouth:

inc

dh

;Okay, przechowujemy wartość NoWall przy tym wejściu w labiryncie i wyprowadzamy znak NoWall na ekran ; jeśli piszemy dane na monitorze) MoveDone:

MazeAdrs mov bx, ax mov Maze[bx], NoWall ifdef ToScreen ScrnAdrs mv bx, ax push es mov ax, ScreenSeg mov es, ax mov word ptr es:[bx], NoWallChar

pop endif

es

; Przed opuszczeniem zobaczmy, czy demon nie powinien zmienić kierunku DontMove:

call and jne call mov

RandNum al., 11b NoChangeDir SetDir cl, al.

;25% szansy, że wynik to zero

NoChangeDir: ; Zobaczmy również, czy demon powinien dać początek procesowi potomnemu call and jne

RandNum al., 11b NoSpawn

;Daje to nam 25% szans

;Okay, zobaczmy, czy jest możliwe uruchomienie nowego procesu w tym punkcie: call jnc

CanStart NoSpawn

;Zobaczmy, czy mamy już aktywny MaxDemons cmp jae inc

DemonCnt, MaxDemons NoSpawn DemonCnt

;dodanie innego demona

;Okay, tworzymy nowego demona i dodajemy go do listy push push

dx cx

;zachowujemy info naszego demona

:Lokujemy wolny slot dla tego demona FindSlot:

lea add cmp jne

si, DemonList – size pcb si, size pcb byp [si].pcb.NextProc, 0 FindSlot

;Alokujemy jakąś przestrzeń stosu dla nowego demona mov cx, 256 malloc

;256 bajtów stosu

;Ustawiamy wskaźnik stosu dla niego: add mov mov

di, 248 [si].pcb.regss, es [si].pcb.regsp, di

;Ustawiamy adres wykonywalny dla niego: mov

[si].pcb regcs, cs

;wskazuje koniec stosu

mov

[si]. Pcb.regip, offset Dig

; Inicjalizujemy współrzędne I kierunek dla niego: mov

[si].pcb.regdx, ds.

:Wybieramy kierunek dla niego pop push

cx cx

call mov mov

SetDir ah, 0 [si].pcb. regcx, ax

mov sti pushf pop mov

[si].pcb regds, seg dseg [si].pcb.regflags byp [si].pcb.NextProc, 1

; wyszukanie kierunku

;oznaczono aktywację

; Przywracamy parametry aktualnego procesu pop pop

cx dx

;przywrócenie aktualnego demona

NoSpawn: ;Okay ,po zrobieniu wszystkiego powyższego, czas przekazać sterowanie do nowego kopania. Poniższe ; współwywołanie przekazuje sterowanie do następnego kopacza w DemonList GetNextDmn:

call

NextDemon

;Okay, mamy wskaźnik do następnego demona na liście (może to być ten sam demon jeśli jest tylko jeden), ; przekazujemy sterowanie do tego demona

Dig

cocall jmp endp

Dig

; TimerDemon- Ten demon wprowadza opóźnienie między każdym cyklem na liście demonów. Zwalnia to ; generowanie labiryntu więc możemy zobaczyć budowanie labiryntu (co czyni program ; bardziej interesującym do oglądania) TimerDemon

Wait4Change

proc push push

near es ax

mov mov mov cmp je

ax, 40h es, ax ax, es:[6Ch] ax, es:[6Ch] Wait4Change

cmp

DemonCnt, 1

;obszar zmiennej BIOS ;lokacja timera BIOS ;zmiana BIOS co każde 1/18 sekundy

QuitProgram: TimerDemon

je pop pop call cocall jmp cocall endp

QuitProgram es ax NextDemon TimerDemon MainPCB

;wyjście z programu

; funkcja solvemaze(x,y:integer): boolean sm_X sm_Y

textequ textequ

SolveMaze

proc push mov

near bp bp, sp

;zobaczmy czy rozwiążemy labirynt: cmp jne cmp jne mov pop ret

byte ptr sm_X, EndX NotSolved byte ptr sm_Y, EndY NotSolved ax, 1 bp 4

;zwraca prawdę

;zobaczmy czy przesunięcie do tego miejsca było poprawnym ruchem. To byłaby wartość NoWall ; w tej komórce w labiryncie jeśli ruch jest właściwy NotSolved:

mov dl, sm_X mov dh, sm_Y MazeAdrs mov bx, ax cmp Maze[bx], NoWall je MoveOK mov ax, 0 pop bp ret 4

;zwraca niepowodzenie

; Cóż jest możliwe przesuniecie do tego punktu, więc umieszczamy właściwą wartość na ekranie ; i poszukujemy rozwiązania MoveOK:

mov

Maze[bx], Visited

ifdef ToScreen push es ScrnAdrs mov bx, ax mov ax, ScreenSeg mov es, ax mov word ptr es:[bx], VisitChar pop es endif

;zapisuje znak “VisitChar’ na ekranie na ; pozycji X, Y

; Wywołujemy rekurencyjnie SolveMaze dopóki pobieramy rozwiązanie. Ponieważ wywołujemy SolveMaze dla ; czterech możliwych kierunków (góra, dół, lewa prawa) w jakie idziemy. Ponieważ opuszczamy wartość „Visited” ; w Maze, nie będziemy przypadkowo przeszukiwać ścieżki jaką już przeszliśmy. Co więcej, jeśli nie możemy iść ; w jednym z czterech kierunków, SolveMaze będzie przechwytywał to bezpośrednio na wejściu (zobaczmy kod na ; początku tego podprogramu. mov dec push push call test jne

ax, sm_X ax ax sm_Y SolveMaze ax ,ax Sovled

push mov dec push call test jne

sm_X ax, sm_Y ax ax SolveMaze ax, ax Solved

mov inc push push call test jne

ax, sm_X ax ax sm_Y SolveMaze ax, ax Solved

push mov inc push call test jne pop ret

sm_X ax, sm_Y ax ax SolveMaze ax, ax Solved bp 4

;próbujemy ścieżki spod lokacji (X-1,Y)

;Rozwiązanie? ;spróbuj ścieżki spod lokacji (X,Y-1)

;Rozwiązanie? ;spróbuj ścieżki spod lokacji (X+1, Y)

;Rozwiązanie? ;Spróbuj ścieżki spod lokacji (X, Y+1)

;Rozwiązanie?

Solved: ifdef ToScreen push es mov dl, sm_X mov dh, sm_Y ScrnAdrs mov bx, ax mov ax, ScreenSeg mov es, ax mov word ptr es:[bx], PathChar pop es mov ax, 1 endif pop ret

bp 4

;rysuje ścieżkę powrotną

;zwraca prawdę

SolveMaze

endp

;Tu jest program główny, który kieruje całą rzeczą: Main

proc mov ax, dseg mov ds, ax mov es, ax meminit call lesi coinit

Init MainPCB

;Inicjalizuje labirynt ;inicjalizuje pakiet współprogramów

;Tworzenie pierwszego demona. Ustawiamy wskaźnik stosu dla niego: mov malloc add mov mov

cx, 256 di, 248 DemonList.regsp, di DemonList.regss, es

; Ustawiamy adres wykonania dla niego: mov mov

DemonList.regcs, cs DemonList.regip, offset Dig

; Początkowe współrzędne I kierunek dla niego: mov mov mov mov mov

cx, East dh, StartY dl, StartX DemonList.regcx, cx DemonList.regdx, dx

;zaczynamy od wschodu

; Ustawiamy inne różności: mov sti pushf pop mov inc mov

DemonList.regds, seg dseg DemonList.regflags byp DemonList. NextProc, 1 DemonCnt DemonIndex, 0

;Demon jest “aktywny”

; Ustawiamy demona Timer: mov mov

DemonList.regsp+(size pcb), offset EndTimerStk demonList.regss+(size pcb), ss

; Ustawiamy adres wykonania dla niego: mov mov

DemonList.regcs +(size pcb), cs DemonList.regip+(size pcb), offset TimerDemon

; Ustawiamy inne różności:

mov DemonList.regds+(size+ pcb), seg dseg sti pushf pop DemonList. Regflags+(size pcb), seg d seg mov byp DemonList.NextProc+(size pcb), 1 int DemonCnt ; Puszczenie mechanizmu w ruch mov mv lea cocall

ax, ds. es, ax di, DemonList

;poczekajmy na naciśnięcie klawisza przez użytkownika: getc mov push mov push call

ax, StartX ax ax, StartY ax SolveMaze

; Czekamy na inne naciśnięcie przed opuszczeniem: getc mov int Quit: Main ceg sseg

ax, 3 10h

ExitPgm endp ends segment para stack ‘stack’

;czyścimy ekran i resetujemy tryb video ;makro DOS do wyjścia z programu

; tworzymy stos dla demona timer (inne stosy alokujemy dynamicznie) TimerStk EndTimerStk

byte word

256 dup (?) ?

; Stos programu głównego stk sseg

byte ends

512 dup (?)

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

Istniejący pakiet współprogramów Biblioteki Standardowej nie jest odpowiedni dla programów, które używają 80386 i zbioru rejestrów 32 bitowych. Jak wspomniano wcześniej, problem leży w fakcie, że Biblioteka Standardowa zachowuje tylko rejestry 16 bitowe , kiedy przełącza pomiędzy procesami. Jednakże, jest stosunkowo trywialnie rozszerzyć modyfikację Biblioteki Standardowej, żeby zachowywała 32 bitowe rejestry. Zrobimy to zmieniając definicję pcb 9z\robimy miejsce na 32 bitowe rejestry) i podprogram sl_cocall: .386 option segemnt:use16

dseg

segemnt para public ‘data’

wp

equ

; PCB 32 bitowe. Odnotujmy, że możemy przetrzymać tylko najmniej znaczące 16 bitów SP ponieważ ; działamy w trybie rzeczywistym. pcb32 regsp regss regip regcs

struct word word word word

? ? ? ?

regeax regebx regecx regedx regesi regedi regebp

dword dword dword dword dword dword dword

? ? ? ? ? ? ?

regds reges regflags pcb32

word ? word ? dword ? ends

DefaultPCB DefaultCortn

pcb32 pcb32

CurCoroutine

dword DefaultCortn

;wskazuje bieżąco wykonywany współprogram

dseg ends cseg segment para public ‘slcode’ ;================================================================================== ; ; Wsparcie dla 32-bitowych współprogramów ; ; COINIT32- ES:DI zawiera adres bieżącego (domyślnego) PCB procesu Coinit32

Coinit32

proc assume push push mov mov mov mov pop pop ret endp

far ds:dseg ax ds ax, dseg ds, ax wp dseg:CurCoroutine, di wp dseg:CurCoroutine+2, es ds ax

; COCALL32 – przekazuje sterowanie do współprogramu. ES:DI zawiera adres PCB. Podprogram przekazuje ; sterowanie to tego współprogramu a potem zwraca wskaźnik do kodu wywołującego PCB w ES:DI cocall32

proc

far

assume pushfd push push push push mov mov cli

ds:dseg ds. es edi eax ax, dseg ds., ax

;zachowanie tego na później

;region krytyczny

; zachowanie stanu bieżącego procesu: les pop mov mov mov mov pop mov

di, dseg:CurCoroutine es:[di].pcb32.regeax es:[di].pcb32.regebx, ebx es:[di].pcb32.regecx, ecx es:[di].pcb32.regedx, edx es:[di].pcb32.regesi, esi es:[di].pcb32.regedi es:[di].pcb32.regebp, ebp

pop pop pop pop pop mov mov

es:[di].pcb32.reges es:[di].pcb32.regds es:[di].pcb32.regflags es:[di].pcb32.regip es:[di].pcb32.regcs es:[di].pcb32.regsp, sp es:[di].pcb32.regss, ss

mov mov mov mov mov mov mov mov mov

bx, es ecx, edi edx, es:[di].pcb32.regedi es, es:[di].pcb32.reges di, dx wp dseg:CurCoroutine, di wp dseg:CurCoroutine+2, es es:[di].pcb32.regedi, ecx es:[di].pcb32.reges, bx

;Okay przełączamy na nowy proces: mov mov mov mov mov mov mov mov mov

ss, es:[di].pcb32.regss sp, es:[di].pcb32.regsp eax, es:[di].pcb32.regeax ebx, es:[di].pcb32.regebx ecx, es:[di].pcb32.regecx edx, es:[di].pcb32.regedx esi, es:[di].pcb32.regesi ebp, es:[di].pcb32.regebp ds, es:[di].pcb32.regds

push push push push mov

es:[di].pcb32.regflags es:[di].pcb32.regcs es:[di],pcb32regip es:[di].pcb32.regedi es, es:[di].pcb32.reges

;zachowamy, więc możemy później zwrócić w ES:DI

;es:di wskazuje nowe PCB ;ES:DI zwraca wartości

cocall32

pop iret endp

; Cocall321działa jak powyższa cocall, z wyjątkiem adresu pcb następującego po wywołaniu w ; strumieniu kodu zamiast być przekazywanym w ES:DI. Notka: kod ten nie zwraca adresu PCB ; kodu wywołującego w ES:DI cocall321

proc assume push mov pushfd push push push push mov mov cli

far ds:dseg ebp bp, sp ds es edi eax ax, dseg ds, ax ;region krytyczny

; Zachowanie stanu bieżącego procesu: les pop mov mov mov mov pop pop pop pop pop pop pop mov mov

di, dseg:CurCorputine es:[di].pcb32.regeax es:[di].pcb32.regebx, ebx es:[di].pcb32. regecx, ecx es:[di].pcb32.regedx. edx es:[di].pcb32.regesi, esi es:[di].pcb32.regedi es:[di].pcb32.reges es:[di].pcb32.regds es:[di].pcb32.regflags es:[di].pcb32.regebp es:[di].pcb32.regip es:[di].pcb32.regcs es:[di].pcb32.regsp, sp es:[di].pcb32.regss, ss

mov mov add mov mov les mov mov

dx, es:[di].pcb32.regip cx, es:[di].pcb32.regcs es:[di].pcb32.regip, 4 es, cx di,dx di, es:[di] wp dseg:CurCoroutine, di wp dseg:CurCoroutine+2, es

;Okay, przełączamy do nowego procesu: mov mov mov mov mov mov

ss, es:[di].pcb32.regss sp, es:[di].pcb32.regsp eax, es:[di].pcb32.regeax ebx, es:[di].pcb32.regebx ecx, es:[di].pcb32.regecx edx, es:[di].pcb32.regedx

;pobranie adresu zwrotnego (wskaźnik do adresu PCB ;pobranie wskaźnika do nowego adresu pcb, potem ; pobieramy wartość pcb

cocall321 cseg

mov mov mov

esi, es:[di].pcb32.regesi ebp, es:[di].pcb32.regebp ds, es:[di].pcb32.regds

push push push push mov pop iret

es:[di].pcb32.regflags es:[di].pcb32.regcs es:[di].pcb32.regip es:[di].pcb32.regedi es, es:[di].pcb32.reges edi

endp ends

19.4 WIELOZADANIOWŚĆ Współprogramy dostarczają sensownego mechanizmu do przełączania pomiędzy procesami, które muszą się zmieniać. Na przykład, program do generowania labiryntu z poprzedniej sekcji generowałby marny labirynt gdyby procesy demonów nie zmieniały się usuwając jedną komórkę z labiryntu. Jednakże, paradygmat współprogramów nie zawsze jest odpowiedni; nie wszystkie procesy muszą się zmienić. Na przykład przypuśćmy, ze piszemy grę akcji, gdzie użytkownik gra przeciwko komputerowi. Dodatkowo, komputer działa niezależnie od użytkownika w czasie rzeczywistym. To może być, na przykład, kosmiczna gra wojenna lub symulator lotu (gdzie prowadzisz walkę z innymi pilotami). Idealnie, byłoby mieć dwa komputery. Jeden dla interakcji użytkownika i drugi dla komputera. Oba systemy przekazywałyby swoje ruchy jeden drugiemu podczas gry. Jeśli gracz-człowiek siedziałby i obserwował ekran, gracz-komputer wygrałby ponieważ jest aktywny a człowiek nie. Oczywiście, byłoby znacznym ograniczeniem w sprzedaży gry gdyby były wymagane dwa komputery do gry. Jednakże, możemy użyć wielozadaniowości do symulowania dwóch oddzielnych systemów na pojedynczym CPU. Podstawową ideą wielozadaniowości jest to, że jeden proces działa w okresie czasu (kwantowanie czasu lub odcinek czasu) a potem występuje proces przerwania zegarowego. Czasowy ISR zachowuje stan procesu a potem przełącza sterowanie do innego procesu. Proces ten działa w swoim odcinku czasu a potem przerwanie zegarowe przełącza na inny proces. W ten sposób, każdy proces zabiera jakąś ilość czasu komputera. Zauważmy, że wielozadaniowość jest bardzo łatwa do implementacji jeśli mamy pakiet współprogramów. Wszystko co musimy zrobić to napisać czasowy ISR, który współwywoła różne procesy, jeden na przerwanie zegarowe. Przerwanie zegarowe, które przełącza pomiędzy procesami to dyspozytor. Decyzję jaką musimy podjąć, kiedy projektujmy dyspozytora jest założenie co do wybrania algorytmu dla procesu. Prostym założeniem jest umieszczenie wszystkich procesów w kolejce a potem rotacja między nimi. Jest to znane jako założenie cykliczne . Ponieważ jest to założenie jakiego używa pakiet procesów Biblioteki Standardowej , zaadoptujemy go również. Jednak, są inne kryteria wyboru dla procesu, generalnie wymagające nadrzędności procesu ,które są również dostępne. Wybór kwantowania czasu może mieć duży wpływ na wydajność. Ogólnie, będziemy chcieli aby kwantowanie czasu było małe. Podział czasu (przełączanie pomiędzy procesami oparte o zegar) będzie dużo płynniejszy jeśli użyjemy małego kwantu czasu. Na przykład przypuśćmy, że wybraliśmy 5 sekundowy kwant czasu i uruchomiliśmy jednocześnie cztery procesy . Każdy proces będzie miał pięć sekund; będzie uruchamiany bardzo szybko podczas tych pięciu sekund. Jednak na koniec tego kawałka czasu będzie czekała na zmianę trzech innych procesów, 15 sekund, nim ponownie się uruchomi. Użytkownicy takich programów byliby bardzo sfrustrowani tym, użytkownicy chcieliby programów których wydajność jest stosunkowo spójna od jednego momentu do drugiego. Jeśli zrobimy jedno milisekundowy odcinek czasu, zamiast pięciu sekund, każdy proces działałby przez jedną milisekundę, a potem przełączał do kolejnego procesu. To znaczy, każdy proces korzystałby z jednej milisekundy. Jest to zbyt mały kwant czasu dla użytkownika aby odczuł przerwy między procesami. Ponieważ mniejsze kwanty czasu są lepsze, możemy zastanawiać się „dlaczego nie uczynić ich tak małymi jak to tylko możliwe?”. Na przykład PC wspiera jedno milisekundowe przerwanie zegarowe. Dlaczego nie użyć go do przełączania pomiędzy procesami? Problem jest tego typu, że jest wymagana równomierna ilość kosztów do przełączenia od jednego do drugiego procesu. Mniejsze uczynią kwantowanie czasu, większe będą kosztowały odcinek czasu. Dlatego też, chcemy wybrać kwant czasu, który balansuje pomiędzy procesem łagodnego

przełączenia a zbyt dużymi kosztami. Okazuje się, że 1/18 sekundowy zegar jest prawdopodobnie najlepszy dla większości wymagań wielozadaniowości 19.4.1 PROCESY NIESKOMPLIKWOANE I SKOMPLIKWOANE Są dwa główne typy procesów w świecie wielozadaniowości: procesy nieskomplikowane , znane również jako wątki i procesy skomplikowane. Te dwa typy procesów różnią się głównie w szczegółach zarządzania pamięcią. Proces skomplikowany zmienia tablice zarządzania pamięcią i przesuwa dużo danych . Wątki zmieniają tylko stos i rejestry CPU. Wątki mają dużo mniejsze koszta niż procesy skomplikowane Nie będziemy w tym tekście rozpatrywać procesów skomplikowanych. Pojawiają się one w trybie chronionym systemów operacyjnych takich jak UNIX, Linux, OS/2 lub Windows NT. Ponieważ rzadko zarządzanie pamięcią (na poziomie sprzętu) wychodzi dalej niż do DOS, temat zmiany tablic zarządzania pamięcią będą poddane pod dyskusję. Przełączanie z jednej aplikacji skomplikowanej do innej generalnie odpowiada przełączeniu z jednej aplikacji do drugiej. Używanie procesów nieskomplikowanych (wątków) jest doskonałym zastosowaniem pod DOS. Wątki (skrót od „ wątek wykonania” lub „wątek wykonawczy”) odpowiadają dwóm lub więcej jednoczesnym wykonaniom ścieżki wewnątrz tego samego programu. Na przykład, możemy myśleć o każdym demonie w programie generującym labirynt jako będącym oddzielnym wątkiem wykonawczym. Chociaż wątki mają różne stosy i stany maszynowe, dzielą one kod i dane pamięci. Nie ma potrzeby użycia „pamięci dzielonej TSR” dostarczającej globalnej pamięci dzielonej. Zamiast tego, wykorzystanie lokalnych zmiennych jest trudniejszym zadaniem.. Musimy albo zaalokować zmienne lokalne na stosie procesu (który jest oddzielny dla każdego procesu) albo musimy się upewnić, że nie ma innych procesów używających zmiennych jakie zadeklarowaliśmy w segmencie danych określonym dla jednego wątku. Możemy łatwo napisać własny pakiet wątków, ale nie musimy; Biblioteka Standardowa dostarcza tej możliwości w pakiecie processes. Zobaczmy jak włączyć wątki do naszych programów, czytając..... 19.4.2 PAKIET PROCESSES BIBLIOTEKI STANDARDWOEJ UCR Biblioteka Standardowa UCR dostarcza sześciu podprogramów pozwalających zarządzać wątkami. Podprogramy te to prcsinit, prcsquit, fork, die, kill i yield. Funkcje te pozwalają nam inicjalizować I zamykać wątki systemu, zaczynać nowy proces, kończyć procesy Ii dobrowolnie przekazać CPU do innego procesu. Funkcje Prcsinit i prcsquit pozwalają zainicjalizować i zamknąć system. Funkcja prcsinit przygotowuje wątki pakietu. Musimy wywołać ten podprogram przed wykonaniem jakiegoś innego z pięciu podprogramów procesu .Funkcja prcsquit zamyka wątki systemu przygotowując się na zamknięcie programu. Prcsinit aktualizuje przerwanie zegarowe (przerwanie 8). Prcsquit przywraca wektor przerwania 8. Jest to bardzo ważne, że wywołujemy prcsquit zanim program wróci do DOS’a Niepowodzenie w wykonaniu , opuszczenie wektora int 8 wskazującego na pamięć, może spowodować krach systemu, kiedy DOS załaduje kolejny program. Nasz program musi zaktualizować wektory wyjątków break i błędu krytycznego, zakładając, że wywołamy prcsquit w przypadku anormalnego zakończenia programu. Niepowodzenie w wykonaniu tego może spowodować krach systemu jeśli użytkownik zakończy program ctrl-break lub przerwie błąd I/O. Prcsinit i prcsquit nie wymagają żadnych parametrów , nie zwracają żadnych wartości. Funkcja fork daje początek nowemu procesowi. Na wejściu es:di muszą wskazywać pcb nowego procesu. Pola regss i regsp pcb muszą zawierać adres szczytu obszaru stosu dla tego nowego procesu. Funkcja fork wypełnia inne pola pcb (wliczając w to cs:ip) Dla każdego wywołania robimy fork., podprogram fork wraca dwukrotnie, raz dla każdego wątku wykonawczego. Proces macierzysty zazwyczaj wraca pierwszy, ale nie jest to takie pewne; proces potomny jest zazwyczaj zwracany jako drugi przez funkcję fork. Rozróżniając te dwa wywołania, fork zwraca dwa identyfikatory procesu (PID’y) w rejestrach ax i bx. Dla procesu macierzystego, fork zwraca ax zawierający zero a bx zawierający PID procesu potomnego. Dla procesu potomnego, fork zwraca ax zawierający PID potomka i bx zawierający zero. Zakuwamy, że oba wątki wracają i kontynuują wykonywanie tego samego kodu po wywołaniu fork. Jeśli chcemy aby potomny i macierzysty proces pobrały oddzielne ścieżki, wykonamy kod podobny do poniższego: lesi fork

NewPCB

;zakładamy, że regss / regsp są zainicjalizowane

test je

ax, ax ParentProcess

; Macierzysty PID to zero w tym miejscu ; idziemy gdzie indziej jeśli proces macierzysty

; Proces potomny kontynuuje wykonywanie tutaj Proces macierzysty powinien zachować PID potomka. Możemy użyć PID’a do zakończenia procesu w późniejszym czasie. Powtarzam, że ważne jest to, że musimy zainicjalizować pola regss i regsp w pcb przed wywołaniem fork. Musimy zaalokować pamięć dla stosu (dynamicznie lub statycznie) a ss:sp wskazują ostatnie słowo tego obszaru stosu kiedy wywołamy fork, pakiet procesu użyje jakiejś wartości, która będzie w polach regss i regsp. Jeśli nie zainicjalizujemy tych wartości, będą one prawdopodobnie zawierały zera a kiedy zacznie się proces , zniszczy dane z pod adresu 0:FFFE. Może to spowodować krach systemu a tym lub innym punkcie. Funkcja die , zabija aktualny proces .Jeśli jest wiele uruchomionych procesów, funkcja ta przekazuje sterowanie do jakiegoś innego procesu czekającego na uruchomienie .Jeśli bieżący proces jest jedynym w systemowej kolejce uruchomień, wtedy funkcja ta spowoduje krach systemu. Funkcja kill pozwala jednemu procesowi zakończyć inny. Zazwyczaj, proces macierzysty będzie używał tej funkcji do kończenia procesu potomnego. Aby zabić proces, po prostu ładujemy rejestr ax PID’em procesu jaki chcemy zakończyć a potem wywołujemy kill. Jeśli proces dostarcza swojego własnego PID’a do funkcji kill, proces sam się kończy (to znaczy, jest to odpowiednik funkcji die). Jeśli jest tylko jeden proces w kolejce uruchomień a proces sam się zabija, wtedy nastąpi krach systemu.. Ostatnim podprogramem zarządzania wielozadaniowością w pakiecie process jest funkcja yield. Yield dobrowolnie poddaje się CPU. Jest to bezpośrednia funkcja dyspozytora, która będzie przełączać na inne zadanie w kolejce uruchomień. Sterowanie jest zwracane po funkcji yield, kiedy dany jest następny odcinek czasu do procesu. Jeśli aktualny proces jest jedynym w tej kolejce, yield wraca natychmiast. Zwykle używamy funkcji yield dla zwalniania CPU pomiędzy długimi operacjami I/O (jak oczekiwanie na naciśnięcie klawisza). Pozwala to innym zadaniom wykorzystać maksymalnie CPU podczas gdy nasz proces obraca się w pętli oczekując na zakończenie jakiejś operacji I/O . Podprogramy wielozadaniowe Biblioteki Standardowej pracują tylko ze zbiorem rejestrów 16 bitowych rodziny 80x86. Podobnie jak pakiet współprogramów, będziemy musieli zmodyfikować pcb i kod dyspozytora jeśli chcemy wesprzeć zbiór 32 bitowych rejestrów procesora 80386 i późniejszych. Zadanie to jest stosunkowo proste a kod jest trochę podobny do tego, który pojawił się w sekcji o współprogramach; więc nie trzeba przedstawiać tego rozwiązania tutaj . 19.4.3 PROBLEMY Z WIELOZADANIOWOŚCIĄ Kiedy wątki dzielą kod i dane, mogą pojawić się pewne problemy. Przede wszystkim, problem wielobieżności. Nie możemy wywołać nie wielobieżnego podprogramu (jak DOS) z dwóch oddzielnych wątków jeśli jest nawet taka możliwość, że kod nie współbieżny może być przerywany a sterowanie przekazane do drugiego wątku, który współużytkuje ten sam program. Jednak nie jest to jedyny problem. Jest całkiem możliwe zaprojektowanie dwóch podprogramów, które mają dostęp do zmiennych dzielonych a te podprogramy zachowują się źle w zależności od tego gdzie wystąpi przerwanie w sekwencji kodu. Będziemy drążyć ten temat w sekcji o synchronizacji., jednak teraz musimy być świadomi, że ten problem istnieje. Zauważmy, że proste wyłączenie przerwań (cli) może nie rozwiązać problemu współużytkowania. Rozważmy następujący kod: cli mov mov int sti

ah, 3Eh bx, Handle 21h

; zapobieżenie współużywalności ;funkcja zamykania DOS ;powrotne włączenie przerwań

Kod ten nie będzie zapobiegał przed współbieżnością, ponieważ DOS (i BIOS) włączają ponownie przerwania!. Jest rozwiązanie tego problemu ale nie przez użycie cli i sti. 19.4.4 PRZYKŁADOWY PROGRAM Z WĄTKAMI

Poniższy program dostarcza prostej demonstracji pakietu process. Ten krótki program tworzy dwa wątki – program główny i proces zegarowy. Przy każdym takcie zegarowym proces drugoplanowy (zegar)) jest wykopywany i zwiększa się zmienna pamięci. I wtedy zwraca CPU z powrotem do programu głównego następny takt zegarowy przekazuje sterowanie do procesu drugoplanowego i cały cykl się powtarza, Program główny czyta ciąg od użytkownika podczas gdy proces drugoplanowy zlicza takty zegarowe. Kiedy użytkownik kończy linię przez naciśnięcie klawisza Enter, pogram główny zabija proces drugoplanowy a potem drukuje ilość czasu konieczną do wprowadzenia linii tekstu. Oczywiście nie jest to najbardziej wydajny sposób obliczania jak długo ktoś wprowadza linię tekstu, ale dostarcza przykładu cech wielozadaniowości Biblioteki Standardowej. Ta krótka część programu demonstruje wszystkie podprogramy process z wyjątkiem die. Zauważmy, ze demonstruje również fakt, że musimy dostarczyć programy obsługi int 23h i int 24h, kiedy używamy pakietu process/ ; MULTI.ASM ; Prosty program demonstrujący zastosowanie multitaskingu ,xlist include includelib .list dseg ChildPID BackGndCnt

stdlib.a stdlib.lib

segment para public ‘data’ word 0 word 0

;PID potomka, więc możemy go zabić ;zliczanie taktów zegara w tle

; PCB dla naszego procesu. Tu inicjalizujemy ss:sp BkgndPCB

pcb

{0, offset EndStk2, seg EndStk2}

;Bufor danych przechowujący ciąg wprowadzany InputLine

byte

128 dup (0)

dseg

ends

cseg

segment para public ‚code’ assume cs:cseg, ds:dseg

; Zastąpienie programu obsługi błędu krytycznego. Podprogram wywołuje prcsquit jeśli użytkownik zdecydował się ; przerwać program CritErrMsg

byte byte byte

cr, lf “DOS Critical Error!”, cr, lf “A)bort, R)etry, I)gnore, F)ail? $”

MyInt24

proc push push push push pop lea mov int

far dx ds ax cs ds dx, CritErrMsg ah, 9 21h

mov int

ah, 1 21h

Int24Lp:

;funkcja DOS drukująca ciąg ;funkcja DOS odczytu znaku

and

al., 5Fh

;konwertujemy l.c → u.c

cmp jne pop mov jmp

al, ‚I’ NotIgnore ax al, 0 Quit24

;Ignorujemy?

NotIgnore:

cmp jne pop mov jmp

al, ‚r’ NotRetry ax al, 1 Quit24

;Ponawiamy?

NotRetry:

cmp jne prcsquit pop mov jmp

al, ‘A’ NotAbort

; Przerywamy

NotAbort:

Quit24:

BadChar: MyInt24

;jeśli wychodzimy , poprawiamy INT 8 ax al., 2 Quit24

cmp jne pop mov pop pop iret

al., ‘F’ BadChar ax al, 3 ds dx

mov mov jmp endp

ah, 2 dl, 7 Int24Lp

;znak dzwonka

; Będziemy blokowali INT 23h (wyjątek break) MyInt23 MyInt23

proc iret endp

far

; Okay, to jest słaby proces drugoplanowy, ale demonstruje jak używać funkcji Biblioteki Standardowej BackGround

BackGround Main

proc sti mov mov inc yield jmp endp

ax, dseg ds, ax BackGndCnt BackGround

proc mov ax, dseg mov ds, ax mov es, ax meminit

; Inicjalizujemy wektory programów obsługi wyjątków INT 23h I INT 24h mov mov mov mov mov mov

ax, 0 es, ax word ptr es:[24h*4], offset MyInt24 es:[24h*4+2], cs word ptr es:[23h*4], offset MyInt23 es:[23h*4+2], cs

prcsinit

ParentPrcs:

;start systemu wielozadaniowego

lesi fork test je jmp

BkgndPCB

; odpalamy nowy proces

ax, ax ParentPrcs BackGround

;powrót procesu macierzystego? ;idziemy do drugoplanowego

mov

ChildPID, bx

;zachowujemy ID procesu potomnego

print byte byte byte

„Podliczam Cię kiedy wpisujesz ciąg. Więc pisz” dr, lf „szybko: „ , 0

lesi gets mov kill printf byte byte dword

InputLine ax, ChildPID

„Wpisując ‘%s’ pobrałeś %d taktów zegara” cr, lf,0 InputLine, BackGndCnt

prcsquit Quit: Main

ExitPgm endp

cseg

ends

sseg

segment para stack ‘stack’

; Tu jest stos dla procesu drugorzędnego jaki zaczęliśmy stk2 Endstk2

byte word

;zatrzymanie potomka uruchomionego

256 dup (?) ?

;Tu jest stos dla programu głównego / procesu pierwszoplanowego stk sseg

byte ends

1024 dup (?)

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

19.5 SYNCHRONIZACJA PROCESÓW Wiele problemów występuje w jednoczesnej współpracy wykonywanych procesów ze względu na synchronizację procesów (lub brak tego). Na przykład jeden proces może tworzyć dane , które inne procesy zużywa. Jednakże, może to być dużo dłuższe dla producenta tworzącego tą dana niż dla konsumującego go. Pewne mechanizmy muszą w pewnym momencie założyć, że konsument nie próbuje używać danej zanim producent go stworzy. Podobnie , musimy założyć, że konsument używa danej tworzonej przez producenta zanim producent stworzy więcej danych Problem producent – konsument jest jednym z kilku bardzo znanych problemów synchronizacji procesów w teorii systemów operacyjnych. W problemie producent – konsument jest jeden lub więcej procesów, które tworzą dane i zapisują te dane do dzielonego bufora. Podobnie, jest jeden lub więcej konsumentów, którzy czytają dane z tego bufora. Są dwa zagadnienia synchronizacji procesów jakimi musimy się zająć – pierwszy to zapewnienie, że producenci nie tworzą więcej danych niż bufor może przechować ( odwrotnie, musimy zapobiec przed usuwaniem danych przez konsumentów z pustego bufora); po drugie zapewnić integralność struktury danych bufora poprzez zezwolenie na dostęp tylko jednego procesu w czasie. Rozważmy, co może się wydarzyć w prostym problemie producent – konsument. Przypuśćmy, że procesy producenta i konsumenta dzielą pojedynczą strukturę bufora danych zorganizowanego jak następuje: buffer Count InPtr OutPtr Data buffer

struct word word word byte ends

0 0 0 MaxBufSize dup (?)

Pole Count określa liczbę bajtów danych obecnych w buforze. InPtr wskazuje kolejną dostępną lokację mieszczącą dane w buforze. OutPtr jest adresem kolejnego bajtu do usunięcia z bufora. Data jest faktyczną tablicą bufora. Dodawanie i odejmowanie danych jest bardzo łatwe. Następujący segment kodu prawie wykonuje tą pracę: ; Producer;

Procedura ta dodaje wartość w al do bufora. Zakładamy, że zmienna buforowa MyBuffer jest w segmencie danych

Producer

proc pushf sti push

near ; musimy włączyć przerwania bx

; Poniższa pętla czeka dopóki jest miejsce w buforze dla wprowadzenia innego bajtu WaitForRoom:

cmp jae

MyBuffer.Count, MaxBufSize WaitForRoom

; Okay, wprowadzamy bajt do bufora mov mov inc inc

bx, MyBuffer.InPtr MyBuffer.Data[bx], al Mybuffer.Count MyBuffer.InPtr

; Jeśli jesteśmy na fizycznym końcu bufora, zawijamy do początku cmp jb

MyBuffer.InPtr, MaxBufSize NoWrap

; dodajemy bajt do bufora ; przesuwamy na następną pozycję w buforze

mov

MyBuffer.InPtr, 0 ax

Producer

pop popf ret endp

; Consumer-

Procedura ta czeka na dane (jeśli konieczne) i zwraca kolejny dostępny bajt z bufora

Consumer

proc pushf sti push cmp je

NoWrap:

WaitForData:

near ; musimy mieć włączone przerwania bx Count, 0 WaitForData

;Czy bufor jest pusty? ;Jeśli tak czekamy na przybycie danych

;Okay, pobranie znaku wejściowego mov mov dec inc cmp jb mov

bx, MyBuffer.OutPtr al, MyBuffer.Data[bx] MyBuffer.Count MyBuffer.OutPtr MyBuffer.OutPtr, MaxBufSize NoWrap MyBuffer.OutPtr, 0

pop popf ret endp

bx

NoWrap:

Consumer

Jedyny problem z tym kodem jest taki, że nie zawsze działa jeśli jest wiele procesów producenckich i konsumenckich. Faktycznie, łatwo jest zbliżyć się do wersji tego kodu, który nie działa dla pojedynczego zbioru procesów producenckiego i konsumenckiego (chociaż ten powyższy kod będzie działał dobrze, w specjalnych przypadkach) Problem jest taki, że te procedury uzyskują dostęp do zmiennych globalnych i dlatego nie są współbieżne. W szczególności problem leży w sposobie w jaki te dwie procedury manipulują buforem sterującym zmiennymi. Rozważmy, na chwilę, poniższe instrukcje z procedury Consumer: dec MyBuffer.Count < przypuśćmy, że tu wysterują przerwania > inc cmp jb mov

MyBuffer.OutPtr MyBuffer.OutPtr, MaxBufSize NoWrap MyBuffer.OutPtr, 0

NoWrap: Jeśli wystąpi przerwania w określonym punkcie powyższego kodu i przekaże sterowanie do innego procesu konsumenckiego, który współużywa ten kod, drugi konsument będzie działał źle. Problem w tym, że pierwszy konsument pobierał dane z bufora, ale ma jeszcze uaktualniony wskaźnik wyjściowy. Drugi konsument pojawia się i usuwa ten sam bajt co pierwszy konsument. Drugi konsument wtedy właściwie uaktualnia wskaźnik wyjściowy, wskazujący kolejną dostępną lokację w buforze cyklicznym. Kiedy sterowanie ostatecznie zwracane jest do procesu pierwszego konsumenta, kończy on działanie poprzez zwiększenie wskaźnika wyjściowego. Powoduje to ,że system

przeskakuje kolejny bajt, którego żaden proces nie czyta. Końcowym efektem jest to, że dwa procesy konsumenckie pobierają ten sam bajt a potem pomijają bajt w buforze. Problem ten jest łatwy do rozwiązania poprzez rozpoznanie faktu, ze kod, który manipuluje buforem danych jest regionem krytycznym. Poprzez ograniczenie wykonywania w tym krytycznym regionie do jednego procesu po kolei, możemy rozwiązać ten problem. W prostym powyższym przykładzie, możemy łatwo zapobiec współużytkowaniu poprzez wyłączenie przerwań w regionie krytycznym. Dla procedury konsumenckiej, kod może wyglądać jak ten: ; Consumer-

Procedura ta oczekuje na dane (jeśli konieczne) i zwraca kolejny dostępny bajt z bufora

Consumer

proc pushf sti push cmp je

WaitForData:

near ;musimy włączyć przerwania bx Count, 0 WaitForData

;czy bufor jest pusty? ;jeśli tak, czekamy na przybycie danych

; Poniżej mamy region krytyczny więc wyłączamy przerwania cli ;Okay pobieramy znak wejściowy mov mov dec inc cmp jb mov

bx, MyBuffer.OutPtr al, MyBuffer.Data[bx] MyBuffer.Count MyBuffer.OutPtr MyBuffer.OutPtr, MaxBufSize NoWrap MyBuffer.OutPtr, 0

pop popf ret endp

bx

NoWrap:

Consumer

;przywracamy flagę przerwania

Zauważmy, że nie możemy wyłączyć przerwań podczas wykonywania całej procedury. Przerwania muszą być dopóki ta procedura oczekuje na dane, w przeciwnym razie proces producencki, nigdy nie będzie mógł odłożyć danych do bufora dla konsumenta. Po prostu wyłączenie przerwań nie zawsze działa. Pewne regiony krytyczne mogą pobierać znaczne ilości czasu (sekundy, minuty lub nawet godziny) i nie możemy pozostawić wyłączonych przerwań dla tej ilości czasu. Innym problemem jest to ,że region krytyczny może wywołać procedurę, która włącza przerwania ponownie a nie mamy nad tym kontroli. Dobrym przykładem jest procedura, która wywołuje DOS. Ponieważ MS-DOS nie jest współużytkowy, MS-DOS , z definicji, jest sekcją krytyczną; możemy pozwolić tylko na jeden proces w danym czasie wewnątrz MS-DOS. Jednakże, MS-DOS ponownie aktywuje przerwania, więc nie możemy po prostu wyłączyć przerwań przed wywołaniem funkcji MS-DOS, z wyjątkiem tej, która zachowuje współbieżność. Wyłączenie przerwań nie działa dla konsumenta / producenta danych wcześniej. Zauważmy, że przerwania muszą kiedy konsument czeka na przybycie danych do bufora (odwrotnie, producenci muszą mieć przerwania kiedy czekają na miejsce w buforze). Jest całkiem możliwe dla tego kodu, że wykryje obecność danych przed wykonaniem instrukcji cli, przekazuje sterowanie do drugiego procesu konsumenckiego. Kiedy nie jest możliwe dla obu procesów zaktualizowanie jednocześnie zmiennych bufora, jest możliwe dla drugiego procesu konsumenckiego usunięcie tylko wartości danej z bufora wejściowego a potem przełączenie ponowne do pierwszego konsumenta, który usuwa wartość z bufora (i powoduje ,ze zmienna Count staje się ujemna). Jedną kiepską stroną tego rozwiązania jest zastosowanie flag dla uzyskania dostępu do regionu krytycznego . Proces, przed wejściem do regiony krytycznego, testuje flagi aby zobaczyć czy jakiś inny proces jest obecnie w regionie krytycznym; jeśli nie, proces ustawia flagi na „ w użyciu” a potem wprowadza region krytyczny. Przy

opuszczaniu regionu krytycznego, proces ustawia flagi na „nie używane”. Jeśli proces chce wprowadzić region krytyczny i wartość flag jest „ w użyciu”, proces musi czekać dopóki proces obecny w sekcji krytycznej zakończy się i zapisze wartość „nie używane” do flag. Jedyny problem z tym rozwiązaniem jest taki, że nie jest to nic więcej niż specjalny przypadek problemu producent / konsument. Instrukcje, te aktualizują postać flag w użyciu swoją własna sekcją krytyczną, którą musimy chronić. Generalnie, idea flag jako rozwiązanie nie jest najlepsza. 19.5.1 OPERACJE NIEPODZIELNE, TESTOWANIE I USTAWIANIE , I AKTYWNE CZEKANIE Problem z ideą flag w użyciu jest taki, że potrzebuje kilku instrukcji do testowania i ustawiania flagi. Przykładowy fragment kodu, który testuje taką flagę, odczytywałby jej wartość i określał czy sekcja krytyczna jest w użyciu. Jeśli nie , wtedy zapisywałby wartość „w użyciu” do flag , co pozwoliłoby innym procesom poznać, że to jest sekcja krytyczna. Problem jest tego typu, że przerwanie może wystąpić po kodzie testującym flagi, ale przed ustawieniem flag do „w użyciu”. Wtedy mogą się pojawić jakieś inne procesy, przetestować flagi i znaleźć tą, która nie jest w użyciu i wprowadzić region krytyczny. System może przerwać drugi proces kiedy jest jeszcze w regionie krytycznym i przekazać sterowanie Z powrotem do pierwszego. Ponieważ pierwszy proces już określił, że region krytyczny nie jest w użyciu, ustawia flagi na „w użyciu” i wprowadza region krytyczny. Teraz mamy dwa procesy w regionie krytycznym i system z naruszeniem wymaganego wzajemnego wykluczenia ( tylko jeden proces w regionie krytycznym w czasie) Problem ten pojawia się jeśli testowanie i ustawianie flag w użyciu nie jest operacją nieprzerwaną (niepodzielną). Jeśli była, wtedy nie będzie problemu. Oczywiście, łatwo jest wykonać sekwencję instrukcji nie przerywalnych przez wstawienie instrukcji cli między nie.. Dlatego też, możemy testować i ustawiać flagi w operacji niepodzielnej jak następuje (zakładamy w użyciu zero, nie w użyciu jeden): pushf TestLoop: cli ;wyłączamy przerwania podczas testowania i cmp Flag, 0 ; ustawiania flag je IsInUse ; czy już w użyciu? mov Flag, 0 , jeśli nie , zrób to więc IsInUse: sti ;zezwolenie na przerwania (jeśli już w użyciu) je TestLoop ; czekaj dopóki nie w użyciu popf : Kiedy dotrzemy tu, flagi były „nie w użyciu” i ustawiam w „w użyciu”. Teraz mamy zastrzeżony dostęp do ; regionu krytycznego Innym rozwiązaniem jest zastosowanie tak zwanych instrukcji „testowanie i ustawianie” – testują one określony warunek i ustawiają flagę na żądaną wartość. W naszym przypadku potrzebujemy instrukcji, które testują flagę aby zobaczyć czy nie jest w użyciu i ustawiają ją na w użyciu w tym samym czasie,. (jeśli flaga była już w użyciu, pozostanie w użyciu później). Chociaż 80x86 nie wspiera określonych instrukcji testowania i ustawiania, dostarcza kilku innych, które mogą osiągnąć taki sam efekt, Do instrukcji tych zalicza się xchg, shl, shr, sar, rcl, ror, rol, btc / btr /bts (dostępne tylko na 80386 i późniejszych procesorach ) i cmpxchg (dostępnej tylko na 80486 i późniejszych procesorach). W ograniczonym znaczeniu możemy również użyć instrukcji dodawania i odejmowania (add, sub, adc, sbb, inc i dec). Instrukcja wymiany dostarcza najogólniejszej formy operacji testowania i ustawiania. Jeśli mamy flagę (0= w użyciu, 1= nie w użyciu) możemy przetestować i ustawić tą flagę bez bałaganienia w przerwaniach używając takiego kodu: InUseLoop:

mov xchg cmp je

al., 0 al., Flag al, 0 InUseLoop

;0 = W użyciu

Instrukcja xchg niepodzielnie wymienia wartość w al z wartością w zmiennej flagi. Chociaż instrukcja xchg nie testuje w rzeczywistości wartości, robi miejsce dla oryginalnej wartości flagi w lokacji (al) zabezpieczając ją przed modyfikacją przez inny proces. Jeśli flaga pierwotnie zawierała zero (w użyciu), ta sekwencja wymiany zamieni zero

dla istniejącego zera a potem powtórzy pętle. Jeśli flaga pierwotnie zawierała jeden (nie w użyciu) wtedy ten kod wymienia zero (w użyciu) dla jeden i wypada z używania pętli. Instrukcje przesunięcia i obrotu również działają jako instrukcje testowania i ustawiania, zakładając, ze używamy właściwych wartości dla flagi w użyciu. Z w użyciu równym zero i nie użyciu równym jeden, poniższy kod demonstruje jak używać instrukcji shr dla operacji testowania i ustawiania: InUseLoop:

shr Jnc

Flag, 1 InUseLoop

;Bit w użyciu do flagi przeniesienia, 0 → Flaga ;Powtarzamy jeśli już w użyciu

Kod ten przesuwa bit w użyciu (bit numer zero) do flagi przeniesienia i czyści flagę w użyciu. W tym samym czasie zeruje zamienną Flag, zakładając, ze Flag zawsze zawiera zero lub jeden. Kod dla testu niepodzielnej sekwencji testowania i ustawiania używający innych przesunięć i obrotów jest bardzo podobny . Startując z 80386, Intel dostarcza zbioru instrukcji wyraźnie zaplanowanych do operacji testowania i ustawiania : btc (testuj bit i uzupełnij), bts (testuj bit i ustaw) i btr (testuj bit i resetuj). Instrukcje te kopiują określony bit z operandu docelowego do flagi przeniesienia a potem uzupełniają, ustawiają lub repetują (czyszczą) ten bit. Poniższy kod demonstruje jak używać instrukcji btr do manipulowania naszą flagą w użyciu: InUseLoop:

btr jnc

Flag, 0 InUseLoop

;flaga w użyciu jest w bicie zero

Instrukcja btr jest trochę bardziej elastyczna niż instrukcja shr ponieważ nie musimy gwarantować, że wszystkie pozostałe bity w zmiennej Flag są zerami; testuje i zeruje bit zero bez wpływania na inne bity w zmiennej Flag Instrukcja cmpxchg 80486 (i późniejszych) dostarcza bardzo ogólnego prymitywu synchronizacji. Instrukcja „porównaj i wymień” wydaje się być jedyną instrukcją niepodzielną jakiej potrzebujemy do implementacji prawie wszystkich prymitywów synchronizacji .Jednakże, jej ogólna struktura oznacza, że jest trochę zbyt złożona dla prostych operacji testowania i ustawiania. Więcej szczegółów o cmpxchg znajduje się w „Instrukcje CMPXCHG i CMPXCHG8B”. Wracając do problemu producent / konsument, możemy łatwo rozwiązać problem regionu krytycznego, który istnieje w tych podprogramach używając sekwencji instrukcji testowania i ustawiania przedstawionych powyżej. Poniższy kod robi to dla procedury Producer, będziemy mogli zmodyfikować procedurę Consumer w podobny sposób ;Producer;

Procedura ta dodaje wartość w al. do bufora. Zakładamy, że zmienna buforowa MyBuffer jest w segmencie danych

Producer

proc pushf sti

near ;musimy włączyć przerwania

;Okay, jesteśmy przy wprowadzeniu regionu krytycznego, więc testujemy flagę w użyciu aby zobaczyć czy ten ; region krytyczny jest już w użyciu InUseLoop:

shr jnc

Flag, 1 InUseLoop

push

bx

;Poniższa pętla oczekuje dopóki jest miejsce w buforze do wprowadzenia innego bajtu WaitForRoom:

cmp jae

MyBufer.Count, MaxBufSize WaitForRoom

; Okay, wprowadzamy bajt do bufora mov mov

bx, MyBuffer.InPtr MyBuffer.Data[bx], al

inc inc

Mybuffer.Count MyBuffer.InPtr

;dodaliśmy bajt do bufora ;przesuwamy na kolejną pozycję w buforze

;Jeśli jesteśmy na fizycznym końcu bufora, zawijamy do początku cmp jb mov

MyBuffer.InPtr, MaxBufSize NoWrap MyBuffer.InPtr, 0

mov pop popf ret endp

Flag, 1 bx

NoWrap:

Producer

;Ustawienie flagi nie do użycia

Jednym ważnym problem z podejściem do ochrony regionu krytycznego przy testowaniu i ustawianiu jest to ,że używa pętli aktywnego czekania. Kiedy region krytyczny nie jest dostępny, proces kręci się w pętli oczekując swojej kolei w regionie krytycznym. Jeśli proces, który jest obecnie w regionie krytycznym pozostaje tam znaczną ilość czasu (powiedzmy sekundy, minuty lub godziny), proces(y) czekające na wejście do regionu krytycznego kontynuują marnotrawienie czasu CPU oczekując na flagę. To , z kolei, marnuje czas CPU, który mógłby być wykorzystany lepiej, pobranie procesu w regionie krytycznym przez niego, aby mógł wejść inny proces. Inny problem, który może zaistnieć, to to, że istnieje możliwość dla jednego procesu wchodzącego do regionu krytycznego , zamknięcie innych procesów , opuszczenie krytycznego regionu, wykonanie jakiegoś przetwarzania, a potem współużywać wszystko w tym samym odcinku czasu .Jeśli okaże się, że proces jest zawsze w regionie krytycznym kiedy występuje przerwanie zegarowe, żaden z innych procesów oczekujących na wejście do regionu krytycznego nie będzie tego robił .jest to problem znany jako trwałe zablokowanie – procesy oczekujące na wejście do regionu krytycznego nigdy tego nie zrobią ponieważ jakiś proces zawsze im to wybije „z głowy” Jedynym rozwiązaniem tych dwóch problemów jest zastosowanie obiektu synchronizacji znanego jako semafor Semafory dostarczają wydajnych i ogólnego przeznaczenia mechanizmów dla ochrony regionów krytycznych. 19.5.2 SEMAFORY Semafor jest obiektem z dwoma podstawowymi metodami: oczekiwanie i sygnał (lub udostępnienie). Używając semafora tworzymy zmienną semaforową (instancję) dla poszczególnego regionu krytycznego lub innego zasobu jaki chcemy chronić. Kiedy proces chce używać danego zasobu, oczekuje na semafor. Jeśli żaden inny proces nie jest aktualnie używanym zasobem, wtedy funkcja oczekująca ustawia semafor w użycia, bezpośrednio wraca do procesu. W tym czasie proces ma wyłączny dostęp do zasobu. Jeśli jakiś inny proces używa już tego zasobu (np. jest w regionie krytycznym), wtedy semafor blokuje bieżący proces poprzez przesunięcie go z kolejki uruchomienia do kolejki semaforu. Kiedy proces, który aktualnie przechowuje udostępniony zasób, operacja udostępniania usuwa proces oczekujący z kolejki semafora i umieszcza go ponownie w kolejce uruchomienia. W kolejnym dostępnym odcinku czasu ten nowy proces wraca ze swojej funkcji oczekującej i może wejść do swojego regionu krytycznego. Semafory rozwiązują dwa ważne problemy z pętlą aktywnego czekania opisanej w poprzedniej sekcji. Po pierwsze, kiedy proces czeka a semafor blokuje proces, proces ten już nie jest w kolejce uruchomienia, więc nie konsumuje więcej czasu CPU, do chwili, kiedy operacja udostępniania umieści go z powrotem w kolejce uruchomienia. W odróżnieniu od aktywnego czekania, mechanizm semaforu nie marnuje (tak bardzo) czasu CPU w procesach, które czekają na jakiś zasób. Semafory mogą również rozwiązać problem trwałego zablokowania.. Operacja czekania, kiedy blokuje proces, może umieścić go na końcu kolejki semaforu FIFO. Operacja udostępniania może pobrać nowy proces z przodu kolejki FIFO umieszczając z powrotem w kolejce uruchomienia. Ta zasada zakłada, e każdy proces wprowadza kolejkę semaforu która staje się równa priorytetowemu dostępowi do zasobu. Implementacja semaforów jest łatwym zadaniem,. Semafor ogólnie składa się ze zmiennej całkowitej i kolejki. System inicjalizuje zmienną całkowitą liczbą procesów, które mogą dzielić zasób w jednym czasie (tą wartością jest zazwyczaj jeden dla regionów krytycznych i innych zasobów wymagających zastrzeżonego dostępu) Operacja oczekiwania zmniejsza trą zmienną. Jeśli wynik jest większy niż lub równy zero, funkcja oczekiwania po

prostu wraca do kodu wywołującego; jeśli wynik jest mniejszy niż zero, funkcja oczekiwania zachowuje stan maszynowy. Przesuwa pcb procesu z kolejki uruchomieniowej do kolejki semaforu a potem przełącza CPU na inne procesy (tj. funkcja yield) Funkcja udostępniania jest prawie odwrotnością. Zwiększa wartość całkowitą. Jeśli wynik nie jest jedynką, funkcja udostępniania przesuwa pcb z przodu kolejki semaforu do kolejki uruchomienia. Jeśli wartość całkowita staje się jedynką, nie ma więcej procesów w kolejce semaforu, więc funkcja udostępniania po prostu wraca do kodu wywołującego. Zauważmy ,że funkcja udostępniania nie aktywuje procesu usuwanego z kolejki procesów semafora. Po prostu umieszcza ten proces w kolejce uruchomienia .Sterowanie zawsze wraca do procesu, który wykonał wywołanie udostępnienia (chyba ,że oczywiście wystąpi przerwanie zegarowe podczas wykonywania funkcji udostępniania). Oczywiście, obojętnie kiedy manipulujemy systemową kolejką uruchomienia, jesteśmy w regionie krytycznym. Dlatego też, wydaje się nam, że główny problem mamy tu – celem semafora jest ochrona regionu krytycznego, mimo, że semafor sam ma region krytyczny, który musimy chronić. Wydaje się, ze wymaga to wnioskowania cyklicznego. Jednakże ten problem łatwo się rozwiązuje. Pamiętajmy, że głównym powodem dla którego nie wyłączamy przerwań chroniąc region krytyczny jest to, że region krytyczny może pobierać dużo czasu na wykonanie lub może wywołać inny podprogram, który włączy je z powrotem .Sekcja krytyczna w semaforze jest bardzo krótka i nie wywołuje innych podprogramów. Dlatego też, na krótko wyłączamy przerwania podczas gdy w regionie krytycznym semafora jest wszystko w porządku. Jeśli nie wolno nam wyłączyć przerwań, możemy zawsze użyć instrukcji testowania i ustawiania w pętli do ochrony regionu krytycznego. Chociaż wprowadza to pętlę aktywnego czekania, okazuje się, że nie będziemy nigdy czekali więcej niż dwa odcinki czasu zanim opuścimy pętlę aktywnego czekania, więc nie zmarnujemy dużo czasu CPU czekając na wejście do regionu krytycznego semafora. Chociaż semafory rozwiązują dwa ważne problemy z pętlą aktywnego czekania, łatwo jest popaść w kłopoty kiedy używamy semaforów. Na przykład, jeśli proces oczekuje na semafor a semafor przyznaje zastrzeżony dostęp do powiązanego zasobu, wtedy ten proces nigdy nie udostępni semafora, żaden proces oczekujący na semafor będą zawieszone w nieskończoność.. Podobnie, proces, który oczekuje na ten sam semafor dwukrotnie bez udostępnienia po środku będzie zawieszał się, a inne procesy, które czekają na semafor , w nieskończoność. Proces, który nie udostępnia zasobu już nie potrzebuje naruszać pojęcia semafor i jest błędem logicznym w programie .Są również inne problemy, które mogą się ujawnić, jeśli proces oczekuje na wiele semaforów przed udostępnieniem jakiegoś. Wrócimy do tego problemu w sekcji o blokadzie systemu (zobacz „Blokada systemu) Chociaż możemy napisać własny pakiet semafora, pakiet process Biblioteki Standardowej dostarcza własnych funkcji oczekiwania i udostępniania wraz z definicją zmiennej semaforowej. Opisuje to następna sekcja 19.5.3 WSPARCIE BIBLIOTEKI STANDARDOWEJ UCR DLA SEMAFORA Pakiet process Biblioteki Standardowej UCR dostarcza dwóch funkcji do manipulowania zmienną semaforową: WaitSemaph i RlsSemaph. Funkcje te , odpowiednio, oczekują i sygnalizują semafor. Podprogramy te zagłębiają się w udogodnienia zarządzania procesem, czyniąc go łatwiejszym do implementacji synchronizacji używając semaforów w naszych programach. Pakiet process dostarcza poniższych definicji dla typu danych semafora: semaphore SemaCnt smaphrLst endsmaphrLst semaphore

struct word ? dword ? dword ? ends

Pole SemaCnt określa jak wiele procesów może dzielić zasobów (jeśli dodatnie), lub jak wiele procesów aktualnie oczekuje na zasób (jeśli ujemne). Domyślnie pole to jest inicjalizowane wartością jeden .to pozwala jednemu procesowi w tym czasie używać zasobu chronionego przez semafor. Za każdym razem proces oczekując na semafor, zmniejsza to pole. Jeśli wynik zmniejszony jest dodatni lub zerowy, operacja oczekiwania natychmiast wraca. Jeśli zmniejszony wynik jest ujemny wtedy operacja czekania przesuwa pcb aktualnego procesu z kolejki uruchomienia do kolejki semaforu zdefiniowanej przez pola smaphrLst i endsmaphrLst z powyższej struktury. Większość czasu będziemy używali domyślnej wartości jeden dla pola SemaCnt. Jest kilka sytuacji, jednak kiedy możemy chcieć zezwolić aby więcej niż jeden proces uzyskał dostęp do zasobu. Na przykład przypuśćmy, że projektujemy grę dla wielu graczy, który komunikuje się pomiędzy różnymi maszynami używając szeregowego

portu komunikacyjnego lub karty sieciowej. Możemy mieć obszar w grze, który ma miejsce dla tylko dwóch graczy w tym samym czasie. Na przykład, gracze mogą ścigać się poszczególnymi „transporterami” w przestrzeni kosmicznej, ale jest miejsce tylko dla dwóch graczy w transporterze w tym samym czasie. Przez inicjalizowanie zmiennej semaforowej liczbą dwa, zamiast jeden, operacja oczekiwania pozwoli dwóm graczom kontynuować w tym samym czasie zamiast tylko jednemu. Kiedy trzeci gracz próbuje wprowadzić transporter, funkcja WaitSemaph będzie blokować gracza przed wprowadzeniem dopóki jedne z pozostałych graczy opuści grę. Zastosowanie funkcji WaitSemaph lub RlsSemaph jest bardzo łatwe; ładujemy do pary adresów żądaną zmienną semaforową i podajemy właściwe wywołanie funkcji. RlsSemaph zawsze wraca natychmiast (zakładając, że nie wystąpi przerwanie zegarowe podczas RlsSemaph , funkcja WaitSemaph wraca kiedy semafor zezwoli na dostęp do zasobu, który chroni. Przykłady tych dwóch funkcji pojawią się w następnej sekcji. Podobnie jak współprogramy i pakiet process Biblioteki Standardowej , tak pakiet semafora zachowuje tylko zbiór 16 bitowych rejestrów CPU 80x86. jeśli chcemy użyć zbioru 32 bitowych rejestrów 80386 i późniejszych procesorów będziemy musieli zmodyfikować kod źródłowy funkcji WaitSemaph i RlsSemaph . Kod jaki musimy zmienić jest prawie identyczny z kodem we współprogramach i pakiecie process, więc jest to trywialna zmiana. Zapamiętajmy jednak, ze będziemy musieli zmienić ten kod jeśli używamy udogodnień 32 bitów 80386 i późniejszych procesorów. 19.5.4 UZYWANIE SEMAFORÓW DO OCHRONY REGIONÓW KRYTYCZNYCH Możemy użyć semaforów dla uzyskania wspólnego zastrzeżonego dostępu do jakiegoś zasobu. Na przykład, jeśli kilka procesów chce użyć drukarki, możemy stworzyć semafor, który zezwoli na dostęp do drukarki przez tylko jeden proces w czasie (dobry przykład procesu, który będzie w „regionie krytycznym” przez kilka minut) Jednakże większość powszechnych zadań dla semafora to ochrona regionów krytycznych przed współużytkowaniem. Trzema przykładami kodu, które potrzebują ochrony przed współużytkowaniem to funkcje DOS, funkcje BIOS i różne funkcje Biblioteki Standardowej >Semafory są idealne dla sterowania dostępem do tych funkcji. Chroniąc DOS przed współużytkowaniem przez kilka różnych procesów musimy stworzyć zmienną DOSmsaph i wykonać wywołanie właściwej funkcji WaitSemaph i RlsSemaph przy wywołaniu DOS’a Poniższy kod przykładowy demonstruje jak to zrobić ; MULTIDOS.ASM ; ; Program ten demonstruje jak używać semaforów do ochrony funkcji DOS .xlist include includelib .list dseg DOSsmaph

stdlib.a stdlib.lib

segment para public ‘data’ semaphore {}

; Makra oczekiwania i udostępniania semafora DOS DOSWait

macro push es push di lesi DOSsmaph WaitSemaph pop di pop es endm

DOSRls

makro push es push di

lesi DOSsmaph RlsSemaph pop di pop es endm ; PCB dla naszego procesu drugoplanowego: BkgndPCB

pcb

{0, offset EndStk2, seg EndStk2}

; Wydruk danych dla procesów pierwszoplanowego i drugoplanowego: StrPtrs1

dword str1_a, str1_b, str1_c, str1_d, str1_e, str1_f dword str1_g, str1_h, str1_i, str1_j, str1_k, str1_l dword 0

str1_a str1_b str1_c str1_d str1_e str1_f str1_g str1_h str1_i str1_j str1_k str1_l

byte byte byte byte byte byte byte byte byte byte byte byte

StrPtrs2

dword str2_a, str2_b, str2_c, str2_d, str2_e, str2_f dord str2_g, str2_h, str2_i dword 0

str2_a str2_b str2_c str2_d str2_e str2_f str2_g str2_h str2_i

byte byte byte byte byte byte byte byte byte

dseg

ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

„Foreground: string ‘a’ „, cr, lf, 0 „Foreground: string ‘b’ „, cr, lf, 0 „Foreground: string ‘c’ „, cr, lf, 0 „Foreground: string ‘d’ „, cr, lf, 0 „Foreground: string ‘e’ „, cr, lf, 0 „Foreground: string ‘f’ „, cr, lf, 0 „Foreground: string ‘g’ „, cr, lf, 0 „Foreground: string ‘h’ „, cr, lf, 0 „Foreground: string ‘i’ „, cr, lf, 0 „Foreground: string ‘j’ „, cr, lf, 0 „Foreground: string ‘k’ „, cr, lf, 0 „Foreground: string ‘l’ „, cr, lf, 0

„Background: string ‘a’ „, cr, lf, 0 „Background: string ‘b’ „, cr, lf, 0 „Background: string ‘c’ „, cr, lf, 0 „Background: string ‘d’ „, cr, lf, 0 „Background: string ‘e’ „, cr, lf, 0 „Background: string ‘f’ „, cr, lf, 0 „Background: string ‘g’ „, cr, lf, 0 „Background: string ‘h’ „, cr, lf, 0 „Background: string ‘i’ „, cr, lf, 0

; Zastąpienie programu obsługi błędu krytycznego. Podprogram ten wywołuje prcsquit jeśli ; użytkownik zdecyduje przerwać program CritErrMsg

byte byte byte

cr, lf “DOS Critical Error!”, cr, lf “A)bort, R)etry, I)gnore, F)ail? $”

MyInt24

proc push push push

far dx ds ax

push pop lea mov int

cs ds dx, CritErrMsg ah, 9 21h

mov int and

ah, 1 21h al., 5Fh

;funkcja DOS’a odczytująca znak

cmp Jne pop mov jmp

al, ‘I’ NotIgnore ax al, 0 Quit

;ignorujemy?

NotIgnore:

cmp jne pop mov jmp

al, ‘r’ NotRetry ax al, 1 Quit24

;powtarzamy?

NotRetry:

cmp jne prcsquit pop mov jmp

al, ‘A’ NotAbort

Int24Lp:

NotAbort:

Quit24:

BadChar: MyInt24

;funkcja DOS’a drukująca ciąg

; konwersja l.c → u.c

;przerywamy ;jeśli wychodzimy, zmieniamy INT 8

ax al., 2 Quit24

cmp jne pop mov pop pop iret

al., ‘F’ BadChar ax al, 3 ds dx

mov mov jmp endp

ah, 2 dl, 7 Int24Lp

; znak dzwonka

; Będziemy blokować INT 23h (wyjątek break) MyInt23 MuInt23

proc Iret endp

far

; Ten proces drugoplanowy wywołuje DOS do wydrukowania kilku ciągów na ekranie. W między czasie proces ; pierwszoplanowy również drukuje ciągi na ekranie. Aby zapobiec współużywalności, lub przynajmniej plątaninie ; znaków na ekranie, kod ten używa semaforów do ochrony funkcji DOS. Dlatego też, każdy proces będzie drukował

; jedną kompletną linie, potem udostępni semafor. Jeśli inny proces oczekuje, będzie drukował swoją linię BackGround

PrintLoop:

proc mov ax, dseg mov ds, ax lea bx, strPtrs2 cmp word ptr [bx+2], 0 je BkGndDone les di, [bx] DOSWait puts DOSRls add bx, 4 jmp PrintLoop

BkGndDone: BackGround

die endp

Main

proc mov ax, dseg mov ds, ax mov es, ax meminit

;tablica wskaźników ciągów ;czy koniec wskaźników? ;pobranie ciągu do drukowania ;funkcja DOS dla drukowania ciągu ;wskazuje następny wskaźnik ciągu ;zakończenie tego procesu

;Inicjalizacja wektorów programów obsługi wyjątków INT23h i INT24h mov mov mov mov mov mov

ax, 0 es ,ax word ptr es:[24h*4], offset MyInt24 es:[24h*4+2],cs word ptr es:[23h*4], offset MyInt23 es:[23h*4+2], cs

prcsinit lesi fork test je jmp

;start systemu wielozadaniowego BkgndPCB

;odpalamy nowy proces

ax, ax ParentPrcs BackGround

;powrót macierzystego procesu? ; idziemy do drugiego planu

; Proces rodzicielski będzie drukował wiązkę ciągów w tym samym czasie co proces drugoplanowy. Użyjemy ; semafora DOS dla ochrony wywołania DOS’ które wykonuje PUTS ParentPrcs: DlyLp0: DlyLp1: DlyLp2:

PrintLoop:

DOSWait mov cx, 0 loop DlyLp0 loop DlyLp1 loop DlyLp2 DOSRls

;wymuszenie innych procesów kończących ; oczekiwanie w kolejce semafora przez ;opóźnienie przynajmniej dla jednego cyklu z ; zegarowego

lea bx, StrPtrs1 cmp word ptr [bx+2], 0 je ForeGndDone les di, [bx] DOSWait

;Tablica wskaźników do ciągów ;czy koniec wskaźników? ;pobranie ciągu do druku

puts DOSRls add bx, 4 jmp PrintLoop ForeGndDone:

prcsquit

Quit Main

ExitPgm endp

cseg

ends

sseg

segment para stack ‘stack’

;funkcja DOS dla wydruku ciągu ;wskazuje kolejny wskaźnik do ciągu

; Tu jest stos dla rozpoczętego procesu drugoplanowego stk2 EndStk2

byte word

1024 dup (?) ?

; Tu mamy sto dla programu głównego / procesu pierwszoplanowego stk sseg

byte ends

1024 dup (?)

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

Program ten bezpośrednio nie wywołuje DOS’a, ale wywołuje podprogram puts Biblioteki Standardowej . Ogólnie, możemy użyć pojedynczego semafora do ochrony wszystkich funkcji BIOS’a, DOS’a i Biblioteki Standardowej. Jednak nie jest to szczególnie efektywne. Na przykład podprogramy dopasowania do wzorca Biblioteki Standardowej nie potrzebują żadnych funkcji DOS; dlatego też czekając na semafor DOS’a robimy dopasowanie do wzorca podczas gdy inny proces wykorzystuje funkcje DOS niekoniecznie opóźniając to dopasowanie do wzorca. Nie jest niczym złym mieć jeden proces robiący dopasowanie do wzorca podczas gdy inny korzysta z funkcji DOS’a. Niestety, pewne podprogramy Biblioteki Standardowej robią wywołania DOS’a (puts jest dobrym przykładem), więc musimy użyć semafora DOS przy takich wywołaniach. Teoretycznie, możemy użyć oddzielnych semaforów do ochrony DOS, różnych funkcji BIOS’a i różnych funkcji Biblioteki Standardowej. Jednak śledząc wszystkie te semafory wewnątrz programu jest dużym zadaniem. Co więcej, zakładając, że funkcja DOS nie wywołuje również niechronionych podprogramów BIOS ,jest trudnym zadaniem. Więc większość programistów używa pojedynczego semafora do ochrony funkcji Biblioteki Standardowej, DOS’a i BIOS’a. 19.5.5 ZSTOSOWANIE SEMAFORÓW DO SYNCHRONIZACJI BARIERY Chociaż podstawowym zastosowaniem semaforów jest dostarczenie zastrzeżonego dostępu do jakiegoś zasobu, są inne zastosowania synchronizacji dla semaforów. W tej sekcji przypatrzymy się zastosowaniu obiektów semaforowych Biblioteki Standardowej do tworzenia barier. Bariera jest punktem w programie, gdzie proces się zatrzymuje i czeka na inne procesy do synchronizacji (docierając do ich właściwych barier) . W tym sensie bariera jest podwójnym semaforem. Semafor uniemożliwia więcej niż n procesom korzystanie z dostępu do jakiegoś zasobu. Bariera nie przyznaje dostępu dopóki przynajmniej n procesów nie zażąda dostępu. Mając dane różne właściwości tych dwóch metod synchronizacji, możemy pomyśleć., że będzie trudno użyć programów WaitSemaph i RlsSemaph do implementacji barier. Jednak, okazuje się to być całkiem proste . Powiedzmy, że pole semafora SemaCnt było zainicjalizowane zerem zamiast jedynką. Kiedy pierwszy proces oczekuje na ten semafor, system będzie natychmiast blokował ten proces. Podobnie, każdy dodatkowy proces, który

oczekuje na ten semafor będzie blokowany i oczekiwał w kolejce semafora. Normalnie byłaby to katastrofa ponieważ nie ma aktywnego procesu, który sygnalizowałby semafor, więc będą aktywowane procesy zablokowane. Jednak, jeśli zmodyfikujemy funkcję oczekiwania aby sprawdzała pole SemaCnt przed rzeczywistym oczekiwaniem, n-ty proces może przeskoczyć funkcje oczekiwania i reaktywować inne procesy. Rozważmy poniższe makro: barrier

AllHere: AllDone:

macro Wait4Cnt local AllHere, AllDone cmp es:[di].semaphore. SemaCnt, jle AllHere WaitSemaph cmp es:[di]. Semqphore.SemaCnt ,0 je AllDone RlsSemaph

-(Wait4Cnt-1)

endm Makro to oczekuje pojedynczego parametru, który powinien być liczbą procesów (wliczając w to proces bieżący), które muszą być obarierowane zanim jakiś proces może być kontynuowany. Pole SemaCnt jest wartością ujemną której wartość absolutna określa jak wiele procesów aktualnie oczekuje na semafor. Jeśli bariera wymaga czterech procesów, żaden proces nie może kontynuować dopóki czwarty proces uderza w barierę; w tym czasie pole SemaCnt będzie zawierało minus trzy. Powyższe makro oblicza jaka powinna być wartość pola SemaCnt jeśli wszystkie procesy są za barierą. Jeśli SemaCnt dopasowuje tą wartość, sygnalizuje semafor, który zaczyna łańcuch operacji z każdym zablokowanym procesem udostępniającym kolejny. Kiedy SemaCnt trafi zero, ostatni zablokowany proces nie udostępnia semafora ponieważ nie ma innych procesów oczekujących w kolejce. Ważne jest aby pamiętać o inicjalizacji pola SemaCnt zerem przed użyciem semaforów dla synchronizacji barier w ten sposób. Jeśli nie zainicjalizujemy SemaCnt zerem, funkcja WaitSemaph nie będzie prawdopodobnie blokowała żadnych procesów. Poniższy przykładowy program dostarcza prostego przykładu zastosowania synchronizacji barier przy użyciu pakietu semaforowego Biblioteki Standardowej: ;BARRIER.ASM ; ; Ten przykładowy program demonstruje jak używać obiektów semaforowych Biblioteki Standardowej do ; synchronizacji kilku procesów przy barierze. Program ten jest podobny do programu MULTIDOS.ASM ; na tyle na ile wszystkie procesy drugorzędne drukują zbiór ciągów. Jednakże zamiast używania pętli opóźnienia ; do synchronizacji procesów pierwszorzędnego i drugorzędnego, ten kod używa synchronizacji barier do ; osiągnięcia tego .xlist include includelib .list

stdlib.a stdlib.lib

dseg

segment para public ‘data’

BarrierSemaph DOSsmaph

semaphore semaphore

{0} {}

;Makra do oczekiwania i udostępniania semafora DOS: DOSWait

macro push es push di lesi DOSsmaph WaitSemaph pop di pop es

;musimy zainicjalizować SemaCnt zerem

DOSRls

endm macro push es push di lesi DOSsmaph RlsSemaph pop di pop es endm

;Makro do synchronizacji w barierze Barrier

AllHere: AllDone:

macro local AllHere, AllDone cmp es:[di]. Semaphore. SemaCnt, -(Wait4Cnt-1) jle AllHere WaitSemaph cmp es:[di]. Semaphore.SemaCnt, 0 jge AllDone RlsSemaph endm

; PCB’y dla naszych procesów drugorzędnych: BkgndPCB2 BkgndPCB2

pcb pcb

{0, offset EndStk2, seg EndStk2} {0, offset EndStk3, seg EndStk3}

;Dane do drukowania procesów pierwszoplanowych i drugoplanowych: StrPtrs1

dword str1_a, str1_b, str1_c, str1_d, str1_e, str1_f dword str1_g, str1_h, str1_i, str1_j, str1_k, str1_l dword 0

str1_a str1_b str1_c str1_d str1_e str1_f str1_g str1_h str1_i str1_j str1_k str1_l

byte byte byte byte byte byte byte byte byte byte byte byte

„Foreground: string ‘a’ “, cr, lf, 0 „Foreground: string ‘b’ “, cr, lf, 0 „Foreground: string ‘c’ “, cr, lf, 0 „Foreground: string ‘d’ “, cr, lf, 0 „Foreground: string ‘e’ “, cr, lf, 0 „Foreground: string ‘f’ “, cr, lf, 0 „Foreground: string ‘g’ “, cr, lf, 0 „Foreground: string ‘h’ “, cr, lf, 0 „Foreground: string ‘i’ “, cr, lf, 0 „Foreground: string ‘j’ “, cr, lf, 0 „Foreground: string ‘k’ “, cr, lf, 0 „Foreground: string ‘l’ “, cr, lf, 0

strPtrs2

dword dword dword byte byte byte byte byte byte

str2-a, str2_b, str2_c, str2_d, str2_e, str2_f str2_g, str2_h, str2_I 0 „Background: string ‘a’ “, cr, lf, 0 „Background: string ‘b’ “, cr, lf, 0 „Background: string ‘c’ “, cr, lf, 0 „Background: string ‘d’ “, cr, lf, 0 „Background: string ‘e’ “, cr, lf, 0 „Background: string ‘f’ “, cr, lf, 0

str1_a str1_b str1_c str1_d str1_e str1_f

str1_g str1_h str1_i

byte byte byte

„Foreground: string ‘g’ “, cr, lf, 0 „Foreground: string ‘h’ “, cr, lf, 0 „Foreground: string ‘i’ “, cr, lf, 0

StrPtrs3 str3_a str3_b str3_c str3_d str3_e str3_f str3_g str3_h str3_i

dword dword dword byte byte byte byte byte byte byte byte byte

str3_a, str3_b, str_c, str3_d, str3_e, str3_f str3_g, str3_h, str3_I 0 „Background 2: string ‘j’ “, cr, lf, 0 „Background 2: string ‘k’ “, cr, lf, 0 „Background 2: string ‘l’ “, cr, lf, 0 „Background 2: string ‘m’ “, cr, lf, 0 „Background 2: string ‘n’ “, cr, lf, 0 „Background 2: string ‘o’ “, cr, lf, 0 „Background 2: string ‘p’ “, cr, lf, 0 „Background 2: string ‘q’ “, cr, lf, 0 „Background 2: string ‘r’ “, cr, lf, 0

dseg

ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

; Zastąpienie programu obsługi błędu krytycznego. Ten podprogram wywołuje prcsquit jeśli ; użytkownik zadecyduje się przerwać program CritErrMsg

byte byte byte

cr, lf “DOS Critical Error!”, cr, lf “A)bort, R)etry, I)gnore, F)ail? $”

MyInt24

proc push push push

far dx ds ax

push pop lea mov int

cs ds dx, critErrMsg ah, 9 21h

mov int and

ah, 1 21h al., 5Fh

;funkcja DOS’a odczytu znaku

cmp jne pop mov jmp

al., ‘I’ NotIgnore ax al., 0 Quit24

;ignorujemy?

cmp jne pop mov jmp

al., ‘r’ NotRetry ax al, 1 Quit24

;ponowić?

Int24Lp:

NotIgnore:

; funkcja DOS’a drukowania ciągu

;konwertuje l.c → u.c

NotRetry:

NotAbort:

Quit24: BadChar: MyInt24

cmp jne prcsquit pop mov jmp cmp jne pop mov pop pop iret mov mov jmp endp

al, ‘A’ NotAbort

;przerywamy? ;jeśli wychodzimy, zmieniamy INT 8

ax al., 2 Quit24 al, ‘F’ BadChar ax al, 3 ds dx ah, 2 dl, 7 Int24Lp

;znak dzwonka

; Będziemy blokować INT 23 (wyjątek break) MyInt23 MyInt23

proc Iret endp

far

; Te procesy drugoplanowe wywołują DOS do drukowania kilku ciągów na ekranie. Tymczasem proces ; drugorzędny również drukuje ciągi na ekranie. Uniemożliwiając współużywalności , lub przynajmniej ; bałaganowi na ekranie, kod ten używa semaforów do ochrony funkcji DOS. Dlatego też, każdy proces ; będzie drukował jedną skończoną linię potem udostępnia semafor. Jeśli inny proces oczekuje, będzie drukował ; swoje linie. BackGround1

proc mov mov

ax, dseg ds, ax

; Czekanie czy wszyscy inni są gotowi: lesi BarrierSemaph barrier 3 ; Okay, zaczynamy drukować ciągi: PrintLoop:

lea bx, StrPtrs2 cmp word ptr [bx+2], 0 je BkGndDone les di, [bx] DOSWait puts DOSRls add bx, 4 jmp PrintLoop

BkGndDone: BackGround1

die endp

BackGround2

proc

;tablica wskaźników ciągów ;koniec wskaźników? ;pobranie ciągu do druku ;wywołanie DOS dla drukowania ciągu ;wskazuje kolejny wskaźnik ciągu

mov mov

ax, dseg ds,a x

lesi BatrrierSema barrier 3 Print Loop:

lea bx, StrPtrs3 cmp word ptr [bx+2], 0 je BkGndDone les di, [bx] DOSWait puts DOSRls add bx, 4 jmp PrintLoop

;tablica wskaźników do ciągów ;koniec wskaźników? ;pobranie ciągu do druku ;funkcja DOS drukowania ciągu ;wskazuje kolejny wskaźnik do ciągu

BkGndDone: die BackGFround2 endp Main

proc mov ax, dseg mov ds, ax mov es, ax meminit

; Inicjalizujemy wektory obsługi wyjątków INT 23h i INT 24h mov mov mov mov mov mov

ax, 0 es, ax word ptr es:[24h*4], offset MyInt24 es:[24h*4 + 2], cs word ptr es:[23h*4], offset MyInt23 es:[23h*4+2], cs

prcsinit

;zaczynamy system wielozadaniowy

; Zaczynamy pierwszy z procesów drugorzędnych: lesi fork test je jmp

BkgndPCB2

;odpalamy nowy proces

ax, ax StartBG2 BackGround1

; wracamy do procesu macierzystego? ;wracamy do podrzędnego

; Zaczynamy drugi proces podrzędny: StartBG2;

lesi fork test je jmp

BkgndPCB3

;odpalamy nowy proces

ax, ax ParentPrcs BackGround2

;powrót do procesu macierzystego ; wracamy do podrzędnego

; Proces macierzysty będzie drukował grupę ciągów w tym samym czasie co proces drugorzędny. ; Będziemy używali semafora DOS do ochrony funkcji DOS, które robi PUTS. ParentPrcs:

lesi

BarrierSemaph

barrier 3 PrintLoop;

lea bx, StrPtrs1 cmp word ptr [bx+2], 0 je ForeGndDone les di, [bx] DOSWait Puts DOSRls add bx, 4 jmp PrintLoop

ForeGndDone: Quit: Main

prcsquit ExitPgm endp

cseg sseg

ends aegment para stack ‘stack’

;Tablica wskaźników do ciągów ;koniec wskaźników ;pobranie ciągu do druku ;funkcja DOS do drukowania ciągu ;wskazuje następny ciąg do wskaźnika

; Tu są stosy dla procesów drugorzędnych stk2 EndStk2

byte word

1024 dup (?) ?

stk3 EndStk3

byte word

1024 (?) ?

; Tu jest stos dla programu głównego / procesu pierwszoplanowego stk sseg

byte ends

1024 dup (?)

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

Przykładowe dane wyjściowe: Background 1: string ‘a’ Background 1: string ‘b’ Background 1: string ‘c’ Background 1: string ‘d’ Background 1: string ‘e’ Background 1: string ‘f’ Background : string ‘a’ Background 1: string ‘g’ Background 2: string ‘j’ Background : string ‘b’ Background 1: string ‘h’ Background 2: string ‘k’ Background : string ‘c’ Background 1: string ‘i’ Background 2: string ‘l’ Background : string ‘d’ Background 2: string ‘m’

Background: string ‘e’ Background 2: string ‘n’ Background : string ‘f’ Background 2: string ‘o’ Background : string ‘g’ Background 2: string ‘p’ Background : string ‘h’ Background 2: string ‘q’ Background : string ‘i’ Background 2: string ‘r’ Background : string ‘j’ Background : string ‘k’ Background : string ‘l’ Zanotujmy jak proces drugorzędny numer jeden działa o jeden cykl zegarowy przed innymi procesami oczekującymi na semafor DOS. Po tej początkowej grupie, wszystkie procesy jeden po drugim wywołują DOS. 19.6 ZAKLESZCZENIE Chociaż semafory mogą rozwiązać każdy problem synchronizacji, nie odnieśmy wrażenia, że semafory nie wprowadzają swoich własnych problemów. Jak już widzieliśmy, niewłaściwe zastosowanie semaforów może wpłynąć na nieskończone zawieszenie się procesu oczekiwania w kolejce semaforu. Jednakże, nawet jeśli oczekujemy i sygnalizujemy pojedyncze semafory, jest całkiem możliwa poprawna operacja na kombinacjach semaforów dających taki sam efekt .Nieskończone zawieszanie procesu z powodu problemów z semaforami jest poważną kwestią. Ta denerwująca sytuacja jest znana zakleszczenie lub martwy punkt. Zakleszczenie wystąpi kiedy jeden proces przechowuje zasób i oczekuje na inny podczas gdy drugi proces przechowuje ten zasób i oczekuje na pierwszy. Aby zobaczyć jak może wystąpić zakleszczenie rozpatrzmy następujący kod: ; Process one: lesi Semaph1 WaitSemaph < Zakładamy wystąpienie przerwania tutaj > lesi Semaph2 WaitSemaph ; Process two: lesi Semaph2 WaitSemaph lesi Semaph1 WaitSemaph Proces one chwyta semafor powiązany z Semaph1. Potem pojawia się przerwanie zegarowe które powoduje przestawienie kontekstu do procesu two. Proces two chwyta semafor powiązany z Semaph2 i potem próbuje dostać Semaph1. jednakże, proces one już przechowuje Semaph1, więc proces two blokuje i czeka na proces one i udostępnienie tego semafora To powoduje (ostatecznie) przekazanie sterowania do procesu one. Proces one próbuje wtedy uchwycić Semaph2. Niestety, proces dwa już przechowuje Semaph2, więc proces one blokuje czekając na

Semaph2. teraz oba procesy są zablokowane czekając na inny. Od tego czasu żaden proces nie może się uruchomić, żaden proces nie może udostępnić semaforu dla innego potrzebującego. Oba procesy są zakleszczone. Jednym z łatwych sposobów uniknięcia zakleszczenia jest nigdy nie pozwolić aby proces przechowywała więcej niż jeden semafor w czasie. Niestety, nie jest to rozwiązanie praktyczne; wiele procesów może potrzebować zastrzeżonego dostępu do kilku zasobów w jednym czasie. Jednakże, możemy obmyślić inne rozwiązanie poprzez zaobserwowanie wzorca, który doprowadził do zakleszczenia w poprzednim przykładzie. Zakleszczenie następuje ponieważ dwa procesy przechwytują różne semafory a potem próbują przechwycić semafor, który przechowuje inny proces. Innymi słowy, przechwytują dwa semafory w różnym porządku ( proces one przechwytywał najpierw Semaph1 potem Semaph2, proces two najpierw Semaph2 a potem Semaph1). Okazuje się, ze dwa procesy nigdy się nie zakleszczą jeśli oczekują one na semafory w takim samym porządku. Możemy zmodyfikować poprzedni przykład eliminując możliwość zakleszczenia: ; Process one: lesi Semaph1 WaitSemaph lesi Semaph2 WaitSemaph Process two: lesi Semaph1 WaitSemaph lesi Semaph2 WaitSemaph Teraz, obojętnie gdzie powyżej wystąpi przerwanie, nie wystąpi zakleszczenie. Jeśli przerwanie wystąpi pomiędzy drugim WaitSemaph wywołanym w procesie one , kiedy proces dwa próbuje oczekiwać Semaph1, będzie zablokowane a proces one będzie kontynuował z dostępnym Semaph2. Łatwym sposobem na unikanie problemów z zakleszczeniem jest liczba wszystkich zmiennych semaforowych i upewnienie się, ze wszystkie procesy posiadają (obsługują) semafory od najmniejszej liczby semaforów do największej. Zakłada to ,że wszystkie procesy posiadają semafory w takim samym porządku, i potem zakłada ,że zakleszczenie nie wystąpi. Zauważmy, że to założenie posiadania semaforów stosuje tylko semafory, które proces przechowuje jednocześnie. Jeśli proces potrzebuje semafora sześć na chwilę, a potem potrzebuje semafora dwa po tym jak udostępnił semafor sześć, nie ma żadnego problemu posiadania semafora dwa po udostępnieniu semafora sześć. Jednakże, jeśli w jakimś punkcie proces musi przechować oba semafory, musi posiadać najpierw semafor dwa. Procesy mogą udostępniać semafory w dowolnym porządku. Porządek w jakim proces udostępnia semafory nie wpływa na to czy zakleszczenie może wystąpić. Oczywiście, procesy powinny udostępniać semafory jak tylko proces upora się z zasobem chronionym przez semafor; może to być inny proces obsługujący ten semafor. Powyższy schemat działa i jest łatwy do implementacji nie jest to absolutnie jedyny sposób obsługi zakleszczenia, i nie jest zawsze najbardziej wydajny. Jednakże jest prosty do implementacji i zawsze działa. 19.7 PODSUMOWANIE Pomimo faktu, że DOS nie jest współużytkowalny i nie wspiera bezpośrednio wielozadaniowości, co nie znaczy, że nasza aplikacja nie może być wielozadaniowa; jest trudno sprawić aby różne aplikacje działały niezależnie jedna od drugiej pod DOS’em. Chociaż DOS nie przełącza pomiędzy różnymi programami w pamięci, DOS z pewnością zezwala na załadowanie wielu programów do pamięci w jednym czasie. Jedynie jednak tylko jeden taki program jest wykonywany na bieżąco. DOS dostarcza kilku funkcji do załadowania i wykonania plików „.EXE” i „.COM” z

dysku. Procesy te zachowują się jak wywołania podprogramów, ze zwracaniem sterowania do programu wywołującego taki program tylko po zakończeniu programu „potomka” *”Procesy DOS” *”Procesy potomne w DOS” *”Ładowanie i wykonywanie” *”Ładowanie programu” *”Ładowanie nakładek” *”Zakończenie procesu” *” Uzyskanie kodu powrotu i procesu potomnego” Podczas wykonywania procesu DOS mogą wystąpić pewne błędy, które przekazują sterowanie do programu obsługi wyjątków. Poza wyjątkami 80x86, DOS’owe programy obsługi break i błędu krytycznego są podstawowymi przykładami. Każdy program który dopasowuje wektory przerwań powinien dostarczyć swojego własnego programu obsługi wyjątków dla tych warunków więc może przywrócić przerwania błędu wyjątku ctrl-C lub I/O. Co więcej dobrze napisany program zawsze dostarcza zastępczego programu obsługi dla tych dwóch warunków, które dostarczają lepszego wsparcia niż domyślne programy DOS’a. *”Obsługa wyjątków w DOS: Obsługa break” *”Obsługa wyjątków w DOS: Obsługa błędu krytycznego” *”Obsługa wyjątków w DOS: Przerwania kontrolowane” Kiedy proces macierzysty wywołuje proces potomny funkcjami LOAD lub LOADEXEC, proces potomny dziedziczy wszystkie otwarte pliki z procesu macierzystego. W szczególności, proces potomny dziedziczy urządzenia standardowego wejścia, standardowego wyjścia , standardowego błędu, pomocniczego I/O i drukarki. Proces macierzysty może łatwo przekierować I/O do/z tego urządzenia przed przekazaniem sterowania do procesu potomnego. To w praktyce przekierowuje I/O podczas wykonywania procesu potomnego *”Przekierowanie I/O dla procesu potomnego” Kiedy programy DOS’owe chcą skomunikować się jeden z drugim, zazwyczaj odczytują lub zapisują dane do pliku. Jednak tworzenie, otwieranie, odczytywanie i zapisywanie plików to dużo pracy, zwłaszcza przy dzieleniu kilku wartości zmiennych . Lepszą alternatywą jest użycie pamięci dzielonej. Niestety DOS nie dostarcza wsparcia dla dwóch programów, aby mogły dzielić wspólny blok pamięci. Jednakże bardzo łatwo jest napisać TSR, który zarządza pamięcią dzieloną dla różnych programów *”Pamięć dzielona” *”Statyczna pamięć dzielona” *”Dynamiczna pamięć dzielona” Funkcja współprogramu jest podstawowym mechanizmem dla przełączania sterowania między dwoma procesami. Operacja „współwywołania” jest odpowiednikiem podprogramu wywołania i zwraca wszystko zawinięte do jednej operacji. Współwywołanie przekazuje sterowanie do jakiegoś innego procesu. Kiedy jakiś inny proces zwraca sterowanie do współprogramu (poprzez współwywołanie) sterowanie zaczyna się od pierwszej instrukcji po współwywołującym kodzie. Biblioteka Standardowa UCR dostarcza całkowitego wsparcia dla współprogramów więc możemy łatwo wstawić współprogramy do naszego programu asemblerowego. *”Współprogramy” Chociaż możemy użyć współprogramów do symulowani wielozadaniowości (wielozadaniowość równoległa), głównym problemem ze współprogramami jest to, że każda aplikacja musi zadecydować kiedy przełączyć do innego procesu poprzez współwywołanie.. Chociaż eliminuje to pewne problemy współużytkowalności i synchronizacji, decydowanie kiedy i gdzie zrobić takie wywołanie zwiększa pracę konieczną do napisania aplikacji wielozadaniowej. Lepszym podejściem jest zastosowanie wielozadaniowości z wywłaszczeniem gdzie przerwanie zegarowe wykonuje przełączanie kontekstowe. Problemy współużytkowania i synchronizacji rozwijają się w takim systemie, ale przy ostrożności problemy te są do przezwyciężenia.

*”Wielozadaniowość” *”Procesy nieskomplikowane i skomplikowane” *”Pakiet process Biblioteki Standardowej UCR” *”Problemy z wielozadaniowością” *”Przykład programu z wątkami” Wielozadaniowość z wywłaszczeniem otwiera puszkę Pandory. Chociaż wielozadaniowość czyni pewne programy łatwiejszymi do implementacji, , problemy synchronizacji procesu i współużywalności są groźne w systemie wielozadaniowym .Wiele procesów wymaga jakiegoś rodzaju zsynchronizowanego dostępu do zmiennych globalnych. Dalej, większość procesów będzie musiało wywołać DOS, BIOS lub jakiś inny podprogram (np. Biblioteka Standardowa), która nie jest współużywalna. Jakoś musimy kontrolować dostęp do takiego kodu aby wielokrotne procesy nie wpływały niekorzystnie na inne. Synchronizacja jest osiągalna przez używanie kilku różnych technik. W kilu prostych przypadkach możemy po prostu wyłączyć przerwania, eliminując problem współużywalności. W innych przypadkach możemy użyć testuj i ustaw lub semaforów do ochrony regionów krytycznych. *”Synchronizacja” *”Operacje nierozdzielne, testuj i ustaw i aktywne oczekiwanie’ *”Semafory” *”Wsparcie semaforów Biblioteki Standardowej” *”Używanie semaforów do ochrony regionów krytycznych’ *”Używanie semaforów do synchronizacji bariery” Stosując obiektów synchronizacji, takich jak semafory, możemy wprowadzić nowy problem do systemu. Zakleszczenie jest doskonałym przykładem. Zakleszczenie wystąpi kiedy jeden proces przechowuje jakieś zasoby i chce innego a drugi proces przechowuje żądany zasób i chce zasobu przechowywanego przez pierwszy proces. Możemy łatwo uniknąć zakleszczenia poprzez kontrolowanie porządku w jakim różne procesy nabywają grupy semaforów. *”Zakleszczenie”

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG

HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK Konsultacje naukowe: NEKRO [email protected] [email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ DWUDZIESTY: KLAWIATURA PC Klawiatura PC jest podstawowym ludzkim urządzeniem wejściowym w systemie. Chociaż wydaje się raczej przyziemna, klawiatura jest podstawowym urządzeniem wejściowym dla większości programów, więc nauczenie się jak oprogramować właściwie klawiaturę jest bardzo ważne w rozwijaniu aplikacji. IBM i niezliczone wytwórnie klawiatur produkują liczne klawiatury dla PC i kompatybilnych. Większość nowoczesnych klawiatur dostarcza przynajmniej 101 różnych klawiszy i są umiarkowanie kompatybilne z 101 Klawiszową rozszerzoną Klawiaturą IBM PC / AT. Te które dostarczają dodatkowych klawiszy ogólnie programują te klawisze do emitowania sekwencji uderzeń klawiszy lub pozwalają użytkownikowi oprogramować sekwencję uderzeń w klawisze dodatkowych klawiszy. Ponieważ 101 klawiszowa klawiatura jest wszechobecna, zakładamy, że używamy jej w tym rozdziale. Kiedy IBM rozwijał pierwszy PC, używali bardzo prostego sprzęgu pomiędzy klawiaturą a komputerem. Kiedy IBM wprowadził PC/AT, zupełnie przeprojektowali interfejs klawiatury. Od czasu wprowadzenia PC/AT, prawie każda klawiatura jest dostosowana do standardu PC/AT. Nawet kiedy IBM wprowadził system PS/2, zmiany w interfejsie klawiatury i zgodna oddolnie z projektem PC/AT. Dlatego też, rozdział ten będzie również ograniczony do urządzeń kompatybilnych z PC/AT ponieważ tylko kilka systemów i klawiatur PC/XT jest jeszcze w użyciu. Jest pięć głównych elementów klawiatury jakie będziemy rozpatrywać w tym rozdziale – podstawowe informacje o klawiaturze, interfejs DOS, interfejs BIOS, podprogram obsługi przerwania klawiatury int 9 i sprzętowy interfejs do klawiatury. Ostatnia sekcja tego rozdziału bezie omawiała jak udawać wejście klawiatury w naszych aplikacjach. 20.1 PODSTAWY KLAWIATURY Klawiatura PC jest komputerem systemowym w swoim własnym rozumieniu. Ukryto wewnątrz klawiatury chip mikrokontrolera 8042, który stale skanuje przełączniki klawiatury aby sprawdzić czy naciśnięto jakiś klawisz. Ten proces pracuje równolegle z normalną działalnością PC, dlatego klawiatura nigdy nie gubi naciśniętego klawisza jeśli 80x86 w PC jest zajęty. Typowo uderzenie w klawiaturę zaczyna się kiedy użytkownik naciśnie klawisz na klawiaturę. To zamyka elektryczny kontakt w przełączniku mikrokontroler i wskazuje ,że naciśnięto przełącznik. Niestety , przełączniki (będące mechanicznymi rzeczami) nie zawsze zamykają (mają kontakt) tak gładko. Często, kontakty odbijają się kilka razy zanim przejdą do stałego kontaktu. Jeśli chip mikrokontrolera odczyta stały przełącznik, to odbijanie się kontaktów będzie wyglądało jak bardzo szybka seria naciśnięć klawisza i zwolnień. To może generować wielokrotne naciśnięcia klawiszy na głównym komputerze, zjawisko znane jako odbijanie klawisza, powszechne w wielu tanich i starych klawiaturach. Ale nawet na droższych i nowszych klawiaturach odbijanie klucza jest problemem jeśli popatrzymy na przełączniki milion razy na sekundę; mechaniczne przełączniki nie mogą po prostu uspokoić się tak szybko. Dlatego wiele algorytmów skanowania klawiatury kontroluje jak często skanowana jest klawiatura. Typowy niedrogi klawisz będzie uspokajał się w ciągu pięciu milisekund, więc jeśli oprogramowanie skanujące klawiaturę przygląda się temu klawiszowi co dziesięć milisekund, więc kontroler będzie faktycznie gubił odbijanie klawisza. Zanotujmy, ze naciśnięty klawisz nie jest wystarczającym powodem generowaniem kodu klawisza. Użytkownik może przetrzymać naciśnięty klawisz wiele dziesiątków milisekund nim go zwolni. Sterownik klawiatury nie musi generować owej sekwencji klawisza za każdym razem kiedy skanuje klawiaturę i znajduje wciśnięty klawisz. Zamiast tego, powinien wygenerować pojedynczą wartość kodu klawisza kiedy klawisz zmienia pozycję z górnej na dolną (operacja naciśnięcia) Po wykryciu, wciśniętego klawisza, mikrokontroler wysyła kod klawisza do PC. Kod klawisza nie odnosi się do kodu ASCII dla tego klawisza, jest to wartość jaką wybrał IBM kiedy po raz pierwszy zaprojektował klawiaturę dla PC.

Klawiatura PC w rzeczywistości generuje dwa kody klawisza dla każdego naciśniętego klawisza. Generuje kod dolny, kiedy klawisz jest naciśnięty i kod górny, kiedy zwalniamy klawisz. Chip mikrokontrolera 8042 przekazuje te kody klawisza do PC, gdzie są przetwarzane przez podprogram obsługi przerwań klawiaturowych. Posiadanie oddzielnego kodu dolnego i górnego jest ważne ponieważ pewne klawisze (takie jak shift, control i alt) mają znaczenie tylko jeśli są wciśnięte. Poprzez generowanie górnych kodów dla wszystkich klawiszy, klawiatura zapewnia ,że podprogram obsługi przerwań klawiaturowych wie jakie klawisze naciśnięto podczas gdy użytkownik trzyma wciśnięty jeden z tych klawiszy. Poniższa tablica pokazuje kody klawiszy, jakie mikrokontroler przekazuje do PC: Klawisz Esc 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) -_ =+ Bksp Tab Q W E R T Y U I O P

Dół 1 2 3 4 5 6 7 8 9 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19

Góra 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99

Klawisz Dół [{ 1A ]} 1B Enter 1C Ctrl 1D A 1E S 1F D 20 F 21 G 22 H 23 J 24 K 25 L 26 ;: 27 ‚„ 28 `~ 29 L shift 2A \| 2B Z 2C X 2D C 2E V 2F B 30 N 31 M 32

Góra 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2

Klawisz ,< .> /? R shift *PrtScr alt Space CAPS F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 NUM SCRL home up pgup Lefy

Dół 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B

Góra B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB

Klawisz center right + end down pgdn ins del / enter F11 F12 ins del home end pgup pgdn left right up down R alt R ctrl Pause

Dół 4C 4D 4E 4F 50 51 52 53 E0 35 E0 1C 57 58 E0 52 E0 53 E0 47 E0 4F E0 49 E0 51 E0 4B E0 4D E0 48 E0 50 E0 38 E0 1D E1 D1 45 E1 9D C5

Góra CC CD CE CF D0 D1 D2 D3 B5 9C D7 D8 D2 D3 C7 CF C9 D1 CB CD C8 D0 B8 9D -

Tablica 72: Kody klawiszy klawiatury PC (w hex) Klawisze pogrubioną czcionką są to klawisze z klawiatury numerycznej. Zauważmy ,że pewne klawisze przekazują dwa lub więcej kodów klawiszy do systemu. Klucze które przekazują więcej niż jeden kod klawisza to nowe klawisze, dodane kiedy IBM zaprojektował 101 klawiszową rozszerzoną klawiaturę. Kiedy kod klawisza przychodzi do PC, drugi mikrokontroler odbiera ten k0d, konwertuje na kod klawisza, robi go dostępnym na porcie I/O 60h a potem wysyła przerwanie do procesora a potem zostawia go w ISR’ze klawiatury pobierającym kod klawisza z portu I/O. Podprogram obsługi przerwań klawiaturowych (int 9) odczytuje kod klawisza z portu wejściowego klawiatury i odpowiednio przetwarza ten kod klawisza. Zauważmy, że kod klawisza system odbiera z mikrokontrolera klawiatury jako pojedynczą wartość, pomimo, że pewne klawisze reprezentują do czterech różnych wartości. Na przykład klawisz „A” na klawiaturze może stworzyć A, a, ctrl-A lub alt-A. Rzeczywisty kod w systemie zależy od bieżącego stanu klawiszy modyfikujących (shift, ctrl, alt, capslock i numlock). NA przykład, jeśli kod klawisza A nadchodzi jako (1Eh) i jest wciśnięty klawisz shift, system tworzy kod ASCII dla dużej litery. Jeśli użytkownik naciśnie wiele klawiszy modyfikujących , system zadziała według priorytetów od najniższego do najwyższego jak następuje: • • • •

Żaden klawisz modyfikujący nie wciśnięty Numlock / Capslock (takie samo pierwszeństwo, najniższy priorytet) Shift Ctrl



Alt (najwyższy priorytet)

Numlock i Capslock wpływają na różne zbiory klawiszy, więc nie ma żadnych niejasności wynikających z ich równego pierwszeństwa w powyższym zestawieniu. Jeśli użytkownik naciśnie dwa klawisze modyfikujące w tym samym czasie, system rozpozna tylko klawisz modyfikujący o najwyższym priorytecie. Na przykład, jeśli użytkownik naciśnie klawisze cytr i alt, system rozpozna tylko klawisz alt. Klawisze numlock, capslock i shift są specjalnymi przypadkami. Jeśli numlock lub capslock są aktywne, naciśnięcie shift uczyni je nieaktywnymi. Podobnie jeśli numlock lub capslock są nieaktywne, naciśnięcie klawisza shift skutecznie „aktywuje” te modyfikatory. Nie wszystkie modyfikatory są poprawne dla każdego klawisza. Na przykład ctrl – 8 jest niepoprawną kombinacją. Podprogram obsługi przerwania klawiaturowego zignoruje wszystkie kombinacje naciśnięć klawiszy z niepoprawnymi klawiszami modyfikującymi. Z nieznanych powodów IBM zadecydował o uczynieniu pewnych kombinacji poprawnymi a innych niepoprawnymi. Na przykład ctrl – left i ctrl - right są poprawne ale ctrl – up i ctrl –down już nie. Jak poprawić ten problem zobaczymy trochę później. Klawisze shift, alt i ctrl są aktywnymi modyfikatorami. To znaczy, modyfikacja naciśniętego klawisza wystąpi tylko, kiedy użytkownik trzyma wciśnięty jeden z tych klawiszy modyfikujących. ISR klawiatury śledzi czy klawisze te są wciśnięte czy nie. Przeciwnie, klawisze numlock, scroll lock i capslock są modyfikatorami przełączającymi. ISR klawiaturowy odwraca powiązane bity za każdym razem kiedy widzi dolny kod następujący po kodzie górnym, dla tych klawiszy. Większość klawiszy klawiatury PC odpowiada znakom ASCII. Kiedy ISR klawiaturowy napotka taki znak, tłumaczy go na 16 bitową wartość, której najmniej znaczący bajt jest kodem ASCII a bardziej znaczący bajt kodem klawiaturowym klawisza. Na przykład, naciskając „A”, bez modyfikatora, z shift i z control tworzymy odpowiednio 1E61h, 1E41h i 1E01h, („a”, „A” i ctrl-A) Wiele sekwencji klawiszy nie ma odpowiedników kodów ASCII. Na przykład klawisze funkcyjne, klawisz sterujący kursorem i sekwencja klawisza alt nie mają odpowiedników kodów ASCII. Dla tych specjalnych, rozszerzonych kodów, ISR klawiaturowy przechowuje zero w mnij znaczącym bajcie (gdzie zazwyczaj jest kod ASCII) a rozszerzony kod idzie do bardziej znaczącego bajtu. Kod rozszerzony jest zazwyczaj, chociaż z pewnością nie zawsze, kodem klawiaturowym dla tego klawisza. Jedyny problem z kodem rozszerzonym jest taki, że wartość zero jest poprawnym znakiem ASCII ( znak NUL). Dlatego też nie możemy bezpośrednio wprowadzić znaku NUL do aplikacji. Jeśli aplikacja musi wprowadzić znak NUL, IBM ma zarezerwowane miejsce w pamięci dla rozszerzonego kodu 0300h (ctrl-3) . Aplikacja wyraźnie musi skonwertować ten rozszerzony kod do znaku NUL (w rzeczywistości tylko rozpoznać bardziej znaczący bajt wartości 03 ponieważ najmniej znaczący bajt jest już znakiem NUL). Na szczęście, bardzo niewiele programów musi zezwalać na wprowadzanie znaku NUL z klawiatury, więc ten problem jest rzadkością. Następująca tablica pokazuje kod klawiaturowy i rozszerzony ISR klawiaturowego generowane dla aplikacji w odpowiedzi na naciśnięcie klawisza z różnymi modyfikatorami Kody rozszerzone są pogrubione. Wszystkie wartości ( z wyjątkiem kolumny kodów klawiaturowych) przedstawiają osiem najmniej znaczących bitów 16 bitowego kodu. Bardziej znaczący bajt pochodzi z kolumny kodów klawiaturowych. Klawisz Esc 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) -_ =+ Bksp Tab Q W

Kod klawiaturowy 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11

ASCII

Shift

Ctrl

1B 31 32 33 34 35 36 37 38 39 30 2D 3D 08 09 71 77

1B 21 40 23 24 25 5E 26 2A 28 29 5F 2B 08 0F00 51 57

1B 0300

1E

1F

Alt 7800 7900 7A00 7B00 7C00 7D00 7E00 7F00 8000 8100 8200 8300

7F 11 17

1000 1100

Num

Caps

1B 31 32 33 34 35 36 37 38 39 30 2D 3D 08 09 71 77

1B 31 32 33 34 35 36 37 38 39 30 2D 3D 08 09 51 57

Shift Caps 1B 31 32 33 34 35 36 37 38 39 30 5F 2B 08 0F00 71 77

Shift Num 1B 31 32 33 34 35 36 37 38 39 30 5F 2B 08 0F00 51 57

E R T Y U I O P [{ ]} enter ctrl A S D F G H J K L ;: ‘“ `~ Lshift \| Z X C V B N M ,< .> /? Rshift * PrtSc alt space caps F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 num scrl home up pgup left

12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3a 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B

65 72 74 79 75 69 6F 70 5B 5D 0D

45 52 54 59 55 49 4F 50 7B 7D 0D

05 12 14 19 15 09 0F 10 1B 1D 0A

1200 1300 1400 1500 1600 1700 1800 1900

65 72 74 79 75 69 6F 70 5B 5D 0D

45 52 54 59 55 49 4F 50 5B 5D 0D

65 72 74 79 75 69 6F 70 7B 7D 0A

45 52 54 59 55 49 4F 50 7B 7D 0A

61 73 64 66 67 68 6A 6B 6C 3B 27 60

41 53 44 46 47 48 4A 4B 4C 3A 22 7E

01 13 04 06 07 08 0A 0B 0C

1E00 1F00 2000 2100 2200 2300 2400 2500 2600

61 73 64 66 67 68 6A 6B 6C 3B 27 60

41 53 44 46 47 48 4A 4B 4C 3B 27 60

61 73 64 66 67 68 6A 6B 6C 3A 22 7E

41 52 44 46 47

5C 7A 78 63 76 62 6E 6D 2C 2E 2F

7C 5A 58 43 56 42 4E 4D 3C 3E 3F

1C 1A 18 03 16 02 0E 0D

2C00 2D00 2E00 2F00 3000 3100 3200

5C 7A 78 63 76 62 6E 6D 2C 2E 2F

5C 5A 58 43 56 42 4E 4D 2C 2E 2F

7C 7A 78 63 76 62 6E 6D 3C 3E 3F

7C 5A 58 43 56 42 4E 4D 3C 3E 3F

2A

INT 5

10

2A

2A

INT 5

INT 5

20

20

20

20

20

20

20

3B00 3C00 3D00 3E00 3F00 4000 4100 4200 4300 4400

5400 5500 5600 5700 5800 5900 5A00 5B00 5C00 5D00

5E00 5F00 6000 6100 6200 6300 6400 6500 6600 6700

3B00 3C00 3D00 3E00 3F00 4000 4100 4200 4300 4400

3B00 3C00 3D00 3E00 3F00 4000 4100 4200 4300 4400

5400 5500 5600 5700 5800 5900 5A00 5B00 5C00 5DD0

5400 5500 5600 5700 5800 5900 5A00 5B00 5C00 5D00

4700 4800 4900 2D 4B00

37 38 39 2D 34

7700

37 38 39 2D 34

4700 4800 4900 2D 4B00

37 38 39 2D 34

4700 4800 4900 2D 4B00

8400 7300

6800 6900 6A00 6B00 6C00 6D00 6E00 6F00 7000 7100

48 4A 4B 4C 3A 22 7E

center right + end down pgdn ins del

4C 4D 4E 4F 50 51 52 53

4C00 4D00 2B 4F00 5000 5100 5200 5300

35 36 2B 31 32 33 30 2E

35 36 2B 31 32 33 30 2E

7400 7500 7600

4C00 4D00 2B 4F00 5000 5100 5200 5300

35 36 2B 31 32 33 30 2E

4C00 4D00 2B 4F00 5000 5100 5200 5300

Tablica 73: Kody klawiatury (w hex) Klawiatura 101 klawiszowa ogólnie rzecz biorąc dostarcza klawisza enter i „/” w bloku klawiatury numerycznej. Chyba, że piszemy własny ISR klawiaturowy int 9, wtedy nie będziemy mogli rozróżniać tych klawiszy od klawiszy z klawiatury głównej. Oddzielny blok sterowania kursorem również generuje taki sam kod rozszerzony co blok numeryczny, z wyjątkiem tego ,że nigdy nie generuje numerycznego kodu ASCII. W przeciwnym razie, nie możemy rozróżnić klawiszy z odpowiadającymi klawiszami na klawiaturze numerycznej (zakładając oczywiście, że numlock jest wyłączony). ISR klawiaturowy dostarcza specjalnej umiejętności, która pozwala nam wprowadzać kod ASCII dla wciskanych klawiszy bezpośrednio z klawiatury. Robiąc to wciskamy klawisz alt i wpisujemy dziesiętny kod ASCII (0..255) dla znaku z klawiatury numerycznej. ISR klawiaturowy skonwertuje to naciśnięcie klawisza na ośmiobitową wartość, dokładając zero w bardziej znaczącym bajcie do znaku i używa tego jako kodu znaku. ISR klawiaturowy wprowadza 16 bitową wartość do bufora roboczego PC Systemowy bufor roboczy jest cykliczną kolejką która używa następujących zmiennych: 40:1A 40:1C 40:1E

- HeadPtr - TailPtr - Buffer

word word word

? ? 16 dup (?)

ISR klawiaturowy wprowadza dane pod lokację wskazywaną przez TailPtr. Funkcja klawiaturowa BIOS usuwa znaki z lokacji wskazywanej przez zmienną HeadPtr. Te dwa wskaźniki prawie zawsze zawierają offset tablicy Buffer. Jeśli te dwa wskaźniki są równe, bufor roboczy jest pusty. Jeśli wartość w HeadPtr jest dwa razy większa niż wartość w TailPtr (lub HeadPtr zawiera 1Eh a TailPtr 3Ch) wtedy bufor jest pełny a ISR klawiaturowy będzie odrzucał dodatkowe naciskania klawiszy. Zauważmy, że zmienna TailPtr zawsze wskazuje następną dostępną lokację w buforze roboczym. Ponieważ nie ma zmiennej„licznik” dostarczającej liczby wejść w buforze, musimy zawsze zostawić jedno wolne wejście w obszarze bufora; to oznacza, że bufor roboczy może przechowywać tylko 15 a nie 16 naciśnięć klawisza. Oprócz bufora roboczego, BIOS utrzymuje kilka innych zmiennych powiązanych z klawiaturą w segmencie 40h. Poniższa tablica pokazuje te zmienne i ich zawartość: Nazwa KbdFlags1 (flagi modyfikatorów)

Adres 40:17

Rozmiar Bajt

KbdFlags2

40:18

Bajt

Opis Bajt ten utrzymuje bieżący stan klawiszy modyfikujących na klawiaturze. Bity mają następujące znaczenie: bit 7: wprowadzono tryb przełączania bit 6: przełączony Capslock (1 = włączony) bit 5: przełączenie Numlock (1 = włączony) bit 4: przełączony Scroll lock (1 = włączony) bit 3: klawisz Alt (1 = wciśnięty) bit 2: klawisz Ctrl (1 = wciśnięty) bit 1: Lewy shift (1 = włączony) bit 0: prawy shift (1 = włączony) Określa czy przełączany klawisz jest obecnie w dole bit 7: Klawisz Insert (w dole jeśli 1 bit 6: Klawisz Capslock (jak wyżej) bit 5:Klawisz Numlock (jak wyżej) bit 4: Klawisz Scroll lock (jak wyżej) bit 3: Stan pauzy (ctrl+Numlock) jeśli 1 bit 2: Klawisz SysReq (jak wyżej) bit 1: Klawisz lewy alt (jak wyżej) bit 0: Klawisz lewy ctrl (jak wyżej)

AltKpd

40:19

Bajt

BufStart

40:80

Słowo

BufEnd KbdFlags3

40:82 40:96

Słowo Bajt

KbdFlags4

40:97

Bajt

BIOS używa go do obliczania kodu ASCII dla sekwencji alt – blok klawiszy Offset startowy bufora klawiatury (1Eh). Notka: zmienna ta nie jest wspierana przez wiele systemów, bądźmy ostrożni stosując ją Offset końcowy bufora klawiatury (3Eh). Różne flagi klawiatury bit 7: odczyt ID klawiatury w toku bit 6: ostatni znak jest pierwszym znakiem ID klawiatury bit 5: wymuszenie numlock przy resecie bit 4: 1 jeśli klawiatura 101 klawiszowa, 0 jeśli 83/84 bit 3: naciśnięty prawy klawisz alt jeśli 1 bit 2: naciśnięty prawy klawisz ctrl jeśli 1 bit 1: ostatnim kodem klawiaturowym był E0h bit 0: Ostatnim kodem klawiaturowym był E1h Więcej różnych flag klawiatury bit 7: błąd przesyłania klawiatury bit 6: aktualizacja wskaźnika trybu bit 5: flaga odbiorcza ponownego wysyłania bit 4: potwierdzenia odbioru bit 3: musi zawsze być zerem bit 2: LED Capslocka (1 = włączona) bit 1: LED Numlocka (1 = włączona) bit 0: LED Scroll lock (1 = włączona)

Tablica 74: Zmienne BIOS’a powiązane z klawiaturą Krótki komentarz do KbdFlags1 i KbdFlags3. Bity od zera do dwóch zmiennej KbdFlags3 są bieżącym ustawieniem BIOS’a dla diod LED na klawiaturze, okresowo BIOS porównuje te wartości dal capslocka, numlocka i scroll locka w KbdFlags1 z tymi trzema bitami w KbdFlags4. Jeśli nie są zgodne, BIOS będzie wysyłał właściwe polecenie do klawiatury aktualizując LED’y i zmieniając wartości w zmiennej KbdFlags4 aby system był spójny. Dlatego też maskując nowe wartości dla numlock, capslock lub scroll lock, BIOS będzie automatycznie modyfikował KbdFlags4 i ustawiał odpowiednio LED’y . 20.2 INTERFEJS SPRZĘTOWY KLAWIATURY IBM użył bardzo prostego sprzętowego projektu dla portu klawiatury w oryginalnych maszynach PC i PC/XT. Kiedy wprowadzili PC/AT, IBM kompletnie przeprojektował interfejs między PC a klawiaturą. Od tego czasu prawie każdy model PC i klony PC mają ten standardowy interfejs klawiatury. Chociaż IBM rozszerzył zdolności sterownika klawiatury, kiedy wprowadził swój system PS/2, modele PS/2 są jeszcze kompatybilne w górę z PC/AT. Ponieważ jest jeszcze kilka oryginalnych PC w użyciu dzisiaj ( a kilku ludzi pisze oryginalne programy dla nich), zignorujemy oryginalny interfejs klawiatury PC i skoncentrujemy się na AT i późniejszych projektach. Są dwa mikrokontrolery klawiatury, które komunikują się z systemem – jeden na płycie głównej PC (mikrokontroler zintegrowany) i jeden wewnątrz obudowy klawiatury (mikrokontroler klawiatury). Komunikacja z zintegrowanym mikrokontrolerem następuje przez port I/O 64h.Odczyt tego bajtu dostarczy stanu kontrolera klawiatury. Zapisując do tego bajtu wysyłamy polecenie do zintegrowanego mikrokontrolera. Organizacja bajtu stanu

Komunikacja z mikrokontrolerem w klawiaturze następuje przez adresy I/O 60h i 64h. Bity zero i jeden w bajcie stanu portu 64h dostarczają sterowania z potwierdzeniem dla tych portów. Przed zapisaniem danych do tych portów, bit zero portu 64h musi być wyzerowany; dana jest dostępna do odczytu z portu 60h, kiedy bit jeden portu 64 zawiera jeden. Klawiatura włącza i wyłącza bity bajtu poleceń (port 64h) określa czy klawiatura jest aktywna i czy klawiatura będzie przerywać system kiedy użytkownik naciska (lub zwalnia) klawisz, itd. Bajty zapisane do portu 60h są wysyłane do mikrokontrolera klawiatury a bajty zapisane do portu 64h są wysyłane do mikrokontrolera zintegrowanego. Bajty odczytane z portu 60h ogólnie rzecz biorąc pochodzą z klawiatury, chociaż możemy oprogramować zintegrowany mikrokontroler aby również zwracał pewne wartości spod tego portu. Poniższa tabele pokazują polecenia wysyłane do mikrokontrolera klawiatury i wartości jakich się możemy się spodziewać. Pokazują również dostępne polecenia jakie możemy zapisać do portu 64h: Wartość (w hex) 20 60 A4 A5

A6 A7 A8 A9 AA AB

AC AD AE

Opis Przekazuje bajt poleceń kontrolera klawiatury do systemu jako kod klawiaturowy do portu 60h Kolejny bajt zapisuje do portu 60h będzie przechowywany w bajcie poleceń kontrolera klawiatury Testuje czy jest zainstalowane sprawdzanie praw dostępu ( tylko PS/2) . Wynik ponownie wraca do portu 60h. 0FAh oznacza ,że jest zainstalowany, 0F1h ,że nie Przekazanie hasła (tylko PS/2). Start odbierania hasła. Kolejna sekwencja kodów klawiaturowych zapisana do portu 60h, zakończona bajtem zerowym, jest nowym hasłem. Dopasowanie hasła. .Znaki z klawiatury są porównywane z hasłem dopóki nie wystąpi dopasowanie. Blokuje myszkę (tylko PS/2). Ustawiamy piąty bit bajtu poleceń. Włącza myszkę (tylko PS/2) Zerujemy piąty bit bajtu poleceń. Testuje myszkę. Zwraca 0 jeśli jest OK, 1 lub 2 jeśli jest zablokowany zegar, 3 lub 4 jeśli zablokowana jest linia danych. Wynik zwracany w porcie 60h Inicjacja programu samostartującego . Zwraca 55h w porcie 60h jeśli powodzenie Test interfejsu klawiatury. Testuje interfejs klawaitury. Zwraca 0 jeśli OK, 1 lub 2 jeśli jest zablokowany zegar, 3 lub 4 jeśli zablokowana linia danych. Wynik wraca do portu 60h Diagnostyka. Zwraca 16 bajtów z chipu mikrokontrolera klawiatury. Nie dostępny w systemach PS/2. Zablokowanie klawiatury. Operacje ustawiają bit cztery rejestru poleceń Odblokowanie klawiatury. Operacje zerują bit cztery rejestru poleceń

C0

Odczyt wejściowego portu klawiatury z portu 60h. Ten port wejściowy zawiera poniższe wartości: bit 7: Hamowanie przełączania klawiszy (0= zahamowane, 1 = odblokowane) bit 6: wyświetlanie (0= kolor, 1= mono) bit 5: wytwarzanie zworki bit 4: płyta systemowa RAM (zawsze 10 bit 0-3; niezdefiniowane

C1

Kopiowanie bitów 0-3 portu wejściowego do bitów stanu 4-7 (tylko PS/2) Kopiowanie bitów 4-7 portu wejściowego do bitów stanu 4-7 portu (tylko PS/2) Kopiowanie wartości portu wyjściowego mikrokontrolera do portu 60h (zobacz poniższą definicję) Zapis następnego bajtu danych zapisanego do portu 60h portu wyjściowego mikrokontrolera: bit 7: Dana klawiatury bit 6: zegar klawiatury bit 5: flaga pustego bufora wejściowego bit 4: flaga pełnego bufora wyjściowego bit 3: niezdefiniowany bit 2: niezdefiniowany bit 1: linia Gate A20 bit 0: reset systemu (jeśli zer0) Notka: zapisanie zera do bity zero zresetuje maszynę Zapis jeden do bitu jeden połączy adres lini 19 i 20 na szynie adresowej PC Zapis bufora klawiatury. Kontroler klawiatury zwraca kolejną wartość wysłaną do portu 60h jak gdyby naciśnięcie klawisza tworzyło tą wartość (tylko PS/2) Zapis bufora myszy. Kontroler klawiatury zwraca kolejną wartość wysyłaną do portu 60h jak gdyby działanie myszy tworzyło tą wartość Zapis kolejnego bajtu danych (60h) do myszki (pomocniczo) (tylko PS/2) Odczyt testu wejściowego. Zwraca w porcie 60h stan lini szeregowej klawiatury. Bit zero zawiera zegar wejściowy klawiatury, bit jeden zawiera daną wejściową klawiatury Impuls portu wyjściowego (zobacz definicję D1). Bity 0-3 bajtu poleceń kontrolera klawiatury są impulsowane do portu wyjściowego. Reset systemu jeśli bit zero jest zerem.

C2 D0 D1

D2 D3 D4 E0

Fx

Tablica 75: Polecenia zintegrowanego kontrolera klawiatury (Port 64h) Polecenia 20h i 60h pozwalają odczytać i zapisać bajt poleceń kontrolera klawiatury. Bajt ten jest wewnątrz zintegrowanego mikrokontrolera i ma następujące rozmieszczenie:

System przekazuje bajty zapisane do portu I/O 60h bezpośrednio do mikrokontrolera klawiatury. Bit zero rejestru stanu musi zawierać zero przed zapisaniem danej do tego portu. Polecenia klawiatury są rozpoznawane: Wartość (w hex) ED

EE F0

F2 F3

F4 F5 F6 F7 F8 F9 FA FB FC FD FE

Opis Przesyłanie bitów LED. Kolejny bajt zapisany do portu 60h aktualizuje LED’y na klawiaturze. Bity zawierają: bity 3-7: Muszą być zerowe bit 2: LED Capslock (1 = włączona, 0 = wyłączona) bit 1: LED Numlock (1 = włączony, 0 = wyłączony) bit 0: LED Scroll lock (1= włączona, 0 = wyłączona) Polecenia echa. Zwraca 0Eeh w porcie 60h jako pomoc diagnostyczną Wybiera alternatywny zbiór kodów klawiaturowych (tylko PS/2). Kolejny bajt zapisany do portu 60h wybiera jedną z opcji: 00: Raportuje aktualny zbiór kodów klawiaturowych (kolejny bajt odczytany z portu 60h) 01: Wybór zbioru kodów klawiaturowych #1 (standardowy zbiór kodów PC/AT 02: Wybór zbioru kodów klawiaturowych # 2 03: Wybór zbioru kodów klawiaturowych # 3 Wysyłanie dwu bajtowego kodu ID klawiatury jako kolejnych dwóch bajtów odczytanych z portu 60h (tylko PS/2) Ustawienie opóźnienia autopowtarzania i częstotliwości powtarzania. Kolejny bajt zapisany do portu 60h określa częstotliwość: bit 7 : musi być zerem bity 5-6: Opóźnienie 00-1/ 4 sek, 01 –1/ 2 sek, 10 3 / 4 sek, 11 – 1 sek bity 0-4: Częstotliwość powtarzania 0 –ok. 30 znaków / sek d0 1Fh – ok. 2 znaki / sek Włąćzenie klawiatury Reset włączenia i oczekiwanie na polecenie włączenia Reset włączenia i początek skanowania klawiatury Uczynienie wszystkich klawiszy automatycznie powtarzanymi (tylko PS/2) Ustawienie wszystkich klawiszy do generowania kodu górnego i dolnego (tylko PS/2) Ustawienie wszystkich klawiszy do generowania tylko kodu górnego (tylko PS/2) Ustawienie wszystkich klawiszy na autopowtarzanie I generowanie tylko kodu górnego (tylko PS/2= Ustawienie pojedynczych klawiszy na autopowtarzanie. Kolejny bajt zawiera kod klawiaturowy żądanego klawisza (tylko PS/2) Ustawienie pojedynczego klawisza do generowanie kodów górnego i dolnego. Kolejny bajt zawiera kod klawiaturowy żądanego klawisza Ustawienie pojedynczego klawisza do generowania tylko kodów dolnych. Kolejny bajt zawiera kod klawiaturowy żądanego kodu Ponowne wysłanie ostatniego wyniku. Używamy tego polecenia jeśli ił bł d dbi d j

FF

wystąpił błąd odbioru danej Reset klawiatury w stanie włączenia i start programu samo startującego

Tablica 76: Polecenia mikrokontrolera klawiatury (port 60h) Poniższy, krótki program demonstruje jak wysłać polecenia do kontrolera klawiatury. Ten mały TSR pokazuje „pokaz świateł” LED na klawiaturze ; LEDSHOW.ASM ; ; Ten krótki TSR tworzy pokaz świateł z LED na klawiaturze. Ten kod nie implementuje złożonego programu ; jaki możemy usuwać ten TSR raz zainstalowany. Zobaczmy rozdział o programach rezydentnych jak ; można to zrobić. ; ; cseg i EndResident muszą wystąpić przed segmentem biblioteki standardowej! cseg cseg

segment para public ‘code’ ends

; Oznaczamy segment, znajdujemy koniec sekcji rezydentnej EndResident EndResident

segment para public ‘Resident’ ends .xlist include includelib .list

stdlib.a stdlib.lib

byp

equ

< byte ptr >

cseg

segment para public ‘code’ assume cs:cseg, ds:cseg

; SetCmd;

Wysyła bajt poleceń w rejestrze AL do chipu mikrokontrolera klawiatury 8042 (rejestr poleceń portu 64h)

SetCmd

proc push push cli

near cx ax

;zachowujemy wartość polecenia ; region krytyczny, żadnych przerwań

; Czekamy dopóki 8042 nie przetworzy bieżącego polecenia Wait4Empty:

xor in test loopnz

cx, cx al., 64 al., 10b Wait4Empty

; zezwolenie na 65,536 razy przejść pętle ;odczyt rejestru stanu klawiatury ; bufor wejściowy pełny? ; jeśli talk, czekamy na opróżnienie

; Okay, wysyłamy polecenie do 8042:

SetCmd

pop out sti pop ret endp

ax 64h, al.

; wyszukanie polecenia

; SendCmd-

Podprogram wysyła polecenie lub bajt danych do portu danych klawiatury (port 60h)

; okay, przerwania mogą być ponownie cx

SendCmd

proc push push push mov mov mov

near ds. bx cx cx, 40h ds., cx bx, ax

mov call cli

al., 0Adh SetCmd

;zachowanie bajtu danych ;zablokowanie klawiatury ;blokowanie przerwań

; Czekamy dopóki 8042 przetworzy bieżące polecenie Wait4Empty:

xor in test loopnz

cx, cx al., 64 al., 10b Wait4Empty

; zezwolenie na 65,536 razy przejść pętle ;odczyt rejestru stanu klawiatury ; bufor wejściowy pełny? ; jeśli talk, czekamy na opróżnienie

; Okay, wysyłamy daną do portu 60h mov out mov call sti

al., bl 60h, al. al, 0Aeh SetCmd cx bx ds.

SendCmd

pop pop pop ret endp

;SetLEDs;

Zapisuje wartość w AL. do LED’ów na klawiaturze. Bity 0..2 odpowiadają odpowiednio scroll, num i caps lock

SetLEDs

proc push push mov mov call mov call

near ax cx ah, al al., 0Edh SendCmd al., ah SendCmd cx ax

SetLEDs

pop pop ret endp

;MyInt1C;

Co 1/ 4 sekundy (co czwarte wywołanie) podprogram ten obraca LED’y tworząc interesujący pokaz świateł

CallsPerInt CallCnt LEDIndex LEDTable

equ byte word byte byte byte byte

;ponownie włączamy klawiaturę ;zezwolenie na przerwania

;zachowanie bitów LED ; 8042 ustawia polecenia LED ;wysłanie polecenia do 8042 ;pobranie bajtu parametru ;wysłanie parametru do 8042

4 CallsPerInt LEDTable 111b, 110b, 101b, 011b, 111b ,110b, 101b, 011b 111b, 110b, 101b, 011b, 111b, 110b, 101b, 011b 111b, 110b, 101b, 011b, 111b, 110b, 101b, 011b 111b, 110b, 101b, 011b, 111b, 110b, 101b, 011b

byte byte byte byte

000b, 100b, 010b, 001b, 000b, 100b, 010b, 001b 000b, 100b, 010b, 001b, 000b, 100b, 010b, 001b 000b, 100b, 010b, 001b, 000b, 100b, 010b, 001b 000b, 100b, 010b, 001b, 000b, 100b, 010b, 001b

byte byte byte byte

000b, 001b, 010b, 100b, 000b, 001b, 010b, 100b 000b, 001b, 010b, 100b, 000b, 001b, 010b, 100b 000b, 001b, 010b, 100b, 000b, 001b, 010b, 100b 000b, 001b, 010b, 100b, 000b, 001b, 010b, 100b

byte byte byte byte

010b, 001b, 010b, 100b, 010b, 001b, 010b, 100b 010b, 001b, 010b, 100b, 010b, 001b, 010b, 100b 010b, 001b, 010b, 100b, 010b, 001b, 010b, 100b 010b, 001b, 010b, 100b, 010b, 001b, 010b, 100b

byte byte byte byte

000b, 111b, 000b, 111b, 000b, 111b, 000b, 111b 000b, 111b, 000b, 111b, 000b, 111b, 000b, 111b 000b, 111b, 000b, 111b, 000b, 111b, 000b, 111b 000b, 111b, 000b, 111b, 000b, 111b, 000b, 111b

TableEnd

equ

this byte

OldInt1C

dword ?

MyInt1C

proc far assume ds:cseg push push push

ds ax bx

mov mov

ax, cs ds, ax CallCnt NotYet CallCnt, CallsPerInt bx, LEDIndex al, [bx] SetLEDs bx bx, offset TableEnd SetTbl bx, LEDTable LEDIndex, bx bx ax ds cs:OldInt1C

MyInt1C

dec jne mov mov mov call inc cmp jne lea mov pop pop pop jmp endp

Main

proc

SetTbl: NotYet:

mov mov

ax, cseg ds, ax

print byte byte

“LED Light Show”, cr, lf “Installing….”, cr, lf,0

; Aktualizujemy wektor przerwania 1C. Zauważmy, że powyższe instrukcje czynią cseg bieżącym ; segmentem danych, więc możemy przechować starą wartość INT 1Ch bezpośrednio w zmiennej ; OldInt1C cli mov mov mov mov mov mov mov mov sti

;wyłączamy przerwania ax, 0 es, ax ax, es:[1Ch*4] word ptr OldInt1C, ax ax, es:[1Ch*4+2] word ptr OldInt1C+2, ax es:[1Ch*4], offset MyInt1C es:[1Ch*4+2], cs ; włączamy przerwania

; jedyna rzecz jaka pozostała to zakończenie i pozostanie w pamięci print byte

„Installed”, cr, lf, 0

mov int

ah, 62h 21h

;pobranie wartości PSP programu

dx, EndResident dx, bx ax, 3100h 21h

;obliczenie rozmiaru programu

Main cseg

mov sub mov int endp ends

sseg stk sseg

segemnt para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

;polecenie TSR DOS’a

Mikrokontroler klawiatury również wysyła dane do zintegrowanego mikrokontrolera do przetworzenia i udostępnienie systemowi przez port 60h. Większość z tych wartości jest kodami klawiaturowymi naciskanych klawiszy (kody górny lub dolny), ale klawiatura przekazuje również kilka innych wartości . Dobrze zaprojektowany podprogram obsługi przerwań klawiaturowych powinien móc obsłużyć (lub przynajmniej zignorować) wartości kodów nie klawiaturowych. W szczególności , program który wysyła polecenia do klawiatury musi móc obsłużyć ponowne wysłanie i potwierdzenie poleceń, które mikrokontroler klawiatury zwraca w porcie 60h. Mikrokontroler klawiatury wysyła do systemu następujące wartości : Wartość (hex) 00 1..58 81..D8 83AB AA EE F0

Opis Przepełnienie danych. System wysyła bajt zero jako ostatnią wartość kiedy przepełni się wewnętrzny bufor kontrolera klawiatury. Kody klawiaturowe dla naciśniętych klawiszy. Wartości dodatnie są kodami dolnymi, wartości ujemne (ustawiony najbardziej znaczący bit) są kodami górnymi ID klawiatury zwracane w odpowiedzi na polecenie F2 (tylko PS/2) Zwracane podczas podstawowego testu bezpieczeństwa po resecie .Również górny kod dla lewego klawisza Shift Zwracane przez polecenie ECHO Przedrostek pewnych górnych kodów

Potwierdzenie klawiatury dla poleceń klawiaturowych innych niż ponowne wysłanie lub ECHO Niepowodzenie podstawowego testu bezpieczeństwa (tylko PS/2) Niepowodzenie diagnostyki (nie dostępne na PS/2)

FA FC FD FE

Ponowne wysłanie. Klawiatura wymaga od systemu ponownego wysłania ostatniego polecenia Błąd klawisza (tylko PS/2)

FF

Tablica 77: Transmisja z klawiatury do systemu Zakładając, że mamy nie zablokowane przerwania klawiaturowe (zobacz bajt poleceń kontrolera klawiatury), każda wartość mikrokontrolera klawiatury wysyłana do systemu przez port 60h będzie generowała przerwanie na lini jeden IRQ (int 9). Dlatego też podprogram obsługi przerwań klawiaturowych zazwyczaj obsługuje wszystkie powyższe kody. Jeśli aktualizujemy int 9, nie zapomnijmy wysłać sygnału zakończenia przerwania (EOI) do PIC 8259 na końcu naszego kodu ISR’a. Również nie zapomnijmy, że możemy odblokować lub zablokować przerwanie klawiatury z pod 8259A Ogólnie rzecz biorąc, nasze aplikacje nie powinny mieć bezpośredniego dostępu do sprzętu klawiatury. Robiąc tak, prawdopodobnie uczynimy nasz software niekompatybilnym z oprogramowaniem użytkowym takim jak rozszerzona klawiatura (makro pogramy klawiatury), oprogramowanie pop-up i inne rezydentne programy, które czytają z klawiatury lub wprowadzają dane do systemowego bufora roboczego. Na szczęście, DOS i BIOS dostarczają doskonałego zbioru funkcji do odczytu i zapisu danych klawiaturowych. Nasze programy będą dużo bardziej stabilne jeśli będziemy przestrzegali stosowania tych funkcji. Dostęp bezpośredni do sprzętu klawiatury powinien być pozostawiony ISR’owi klawiaturowemu i tych poprawek klawiatury i programów pop-up, które koniecznie muszą komunikować się bezpośrednio ze sprzętem. 20.3 INTERFEJS DOS KLAWIATURY MS-DOS dostarcza kilku funkcji do odczytu z T znaczy, że gubimy informacje kodów klawiaturowych podprogramu obsługi przerwań klawiaturowych zachowanych w buforze roboczym. Jeśli naciśniemy klawisz, który ma rozszerzony kod zamiast kodu ASCII, MS-DOS zwróci dwa kody klawiszy. Najpierw funkcja DOS zwróci wartość zero. To mówi nam, że musimy ponownie wywołać podprogram pobrania znaku. Kod MS-DOS zwracany w drugim wywołaniu jest rozszerzonym kodem klawisza. Zauważmy, że podprogramy Biblioteki Standardowej wywołują MS-DOS do odczytu znaków z klawiatury. Dlatego też, podprogram getc Biblioteki Standardowej również zwraca kod klawisza w ten sposób. Podprogramy gets i getsm odrzucają każde nie –ASCII uderzenie klawisza ponieważ nie może być dobrą rzeczą wprowadzenie bajtów zerowych w środek ciągu zakończonego zerem. 20.4 INTERFEJS BIOS KALWIATURY Chociaż MS-DOS dostarcza stosownego zbioru podprogramów do odczytu kodów ASCII i znaków rozszerzonych z klawiatury, BIOS PC dostarcza dużo lepszych udogodnień wejścia klawiatury Co więcej, jest dużo interesujących powiązanych zmiennych w obszarze BIOS, jakie możemy wyszperać. Generalnie, jeśli nie potrzebujemy zdolności przekierowania I/O dostarczanych przez MS-DOS, odczytujemy wejście klawiatury używając funkcji BIOS dostarczających dużo większej elastyczności. Wywołujemy usługi klawiaturowe MS-DOS używając instrukcji int 16. BIOS dostarcza następujących funkcji klawiaturowych: Funkcja # (AH)

Parametry wejściowe

Parametry wyjściowe

0

al. –znak ASCII ah – kod klawiaturowy

1

ZF – ustawiona jeśli brak klawisza ZF – wyzerowana jeśli klawisz jest dostępny al – kod ASCII ah – kod klawiaturowy

Opis Odczyt znaku. Odczytuje kolejne dostępne znaki z systemowego bufora roboczego. Czeka na naciśnięcie klawisza jeśli bufor jest pusty. Sprawdza czy znak jest dostępny w buforze roboczym. Ustawia flagę zera jeśli klawisz nie jest dostępny, czyści tą flagę jeśli jest dostępny. Jeśli klawisz jest dostępny, funkcja zwraca kod ASCII i klawiaturowy w ax.

al – flagi przesunięcia

2

3

5

al =5 bh = 0, 1,2, 3 dla opóźnienia 1/4 , ½, ¾ sekundy bl =0..1Fh dla 30 /sek do 2/sek ch = kod klawiaturowy cl = kod ASCII

10h

al – znak ASCII ah – kod klawiaturowy

11h

ZF –ustawiona jeśli brak klawisza ZF – wyczyszczona jeśli klawisz jest dostępny al – kod ASCII ah – kod klawiaturowy

12h

al – flagi przesunięcia ah – rozszerzone flagi przesunięcia

Wartość ax jest niezdefiniowana jeśli żaden klawisz nie jest dostępny Zwraca bieżący stan flagi przesunięcia w al. Flaga przesunięcia jest zdefiniowana jak następuje: bit 7: przełączony Insert bit 6: przełączony Capslock bit 5: przełączony Numlock bit 4: przełączony Scroll lock bit 3: klawisz Alt naciśnięty bit 2: klawisz Ctrl naciśnięty bit 1: nacisnięty lewy Shift bit 0: naciśnięty prawy Shift Ustawienia częstotliwości auto powtarzania. Rejestr bh zawiera ilość czasu oczekiwania przed startem operacji auto powtarzania, rejestr bl zawiera częstotliwość auto powtarzania Przechowuje kod klawisza w buforze. Funkcja ta przechowuje wartość w rejestrze cx na końcu bufora roboczego. Zauważmy, że kod klawiaturowy w ch nie musi odpowiadać kodowi ASCII pojawiającego się w cl/. Ten podprogram będzie po prostu wprowadzał dane jakie dostarczymy do systemowego bufora roboczego Odczytuje rozszerzony znak. Podobnie jak wywołanie ah =0,poza tym jednym przekazaniem wszystkich kodów klawiszy, ah =0 odrzuca kody, które nie są kompatybilne z PC/XT Jak funkcja ah =0 poza tym jednym nie wyrzuca kod ów klawiszy, które nie są kompatybilne z PC/XT (tzn. znalezione dodatkowe klawisze na klawiaturze 101 klawiszowej) Zwraca bieżący stan flag przesunięcia w ax. Flagi przesunięcia są zdefiniowane tak: bit 15: naciśnięty klawisz SysReq bit 14: aktualnie naciśnięty Capslock bit 13: aktualnie wciśnięty klawisz Numlock bit 12: aktualnie wciśnięty Scroll lock bit 11: wciśnięty prawy alt bit 10: wciśnięty prawy ctrl bit 9: wciśnięty lewy alt bit 8: wciśnięty lewy ctrl bit 7: przełączony Insert bit 6: przełączony Capslock bit 5; przełączony Numlock bit 4: przełączony Scroll lock bit 3: jakiś alt wciśnięty (pewne maszyny tylko lewy) bit 2: jakiś ctrl wciśnięty bit 1: lewy shift wciśnięty bit 0: prawy shift wciśnięty

Zauważmy, że wiele z tych funkcji nie jest wspartych w każdym BIOS’ie, jaki był napisany. Faktycznie, tylko pierwsze trzy funkcje były dostępne na oryginalnym PC. Jednakże, od kiedy nadszedł AT, większość BIOS’ów wsparło przynajmniej powyższe funkcje. Wiele BIOS’ów dostarcza dodatkowych funkcji, i

jest wiele aplikacji TSR, które mogą rozszerzyć tą listę w przyszłości ,Możemy t łatwo rozszerzyć jeśli mamy takie życzenie. ; INT16.ASM ; ; Krótki bierny TSR, który zamienia obsługę int 16h BIOS’a. Podprogram ten demonstruje funkcjonowanie ; każdej z funkcji int 16h, których standardowo dostarcza BIOS ; ; Zauważmy ,że kod ten nie aktualizuje int 2Fh (przerwanie równoczesnych procesów), ani też nie możemy ; usunąć tego kodu z pamięci za wyjątkiem przeładowania systemu. Jeśli chcemy móc zrobić te dwie rzeczy (jak ; również sprawdzić poprzednią instalację), spójrzmy do rozdziału o programach rezydentnych. Kod taki był ; pominięty dla tego programu z powodu ograniczenia długości. ; ; ; cseg i EndResident muszą wystąpić przed segmentem biblioteki standardowej! cseg cseg

segemnt para public ‘code’ ends

; Oznaczamy segment, znajdując koniec sekcji rezydentnej EndResident EndResident

segment para public ‘Resident’ ends .xlist .include .includelib .list

stdlib.a stdlib.lib

byp

equ

< byte ptr >

cseg

segemnt para public ‘code’ assume cs:cseg, ds:cseg

OldInt16

dword ?

; Zmienne BIOS: KbdFlags1 KbdFlags2 AltKpd HeadPtr TailPtr Buffer EndBuf

equ equ equ equ equ equ equ



1eh 3eh

KbdFlags3 KbdFlags4

equ equ



incptr

macro which local NoWrap add bx, 2 cmp bx, EndBuf jb NoWrap mov bx, Buffer mov which, bx endm podprogram ten przetwarza żądane funkcje 16h AH Opis ----------------------------------------------------------------------------------------------------------

NoWrap: ; MyInt16 ; ;

; ; ; ; ; ; ; ; ; ; ;

00h 01h

05h 10h 11h 12h

Pobiera klawisz z klawiatury, zwraca kod w AX Test dla dostępnego klawisza, ZF=1 jeśli brak, ZF=0 a AX zawiera kolejny kod klawisza jeśli klawisz jest dostępny pobiera stan przesunięcia. Zwraca stan klawisza Shift w AL. Ustawia częstotliwość auto powtarzania. BH=0,1,2,3 (czas opóźnienia w czwartej sekundzie), BL=0..1Fh dla 30 znaków/sek do 2 znaków / sek częstotliwości powtarzania. Przechowuje kod klawiaturowy (w CX) w buforze roboczym pobranie klawisza (to samo co 00h w tej implementacji) Test klawisza (to samo co 01h) Pobranie stanu klawisza rozszerzonego. Zwraca stan w AX

MyInt16

proc test je cmp jb je cmp je cmp je cmp je cmp je

far ah, 0EFh GetKey ah, 2 TestKey GetStatus ah, 3 SetAutoRpt ah, 5 StoreKey ah, 11h TestKey ah, 12h ExtStatus

02h 03h

;sprawdzenie od 0h do 10h ;sprawdzenie od 01h do 02h ;sprawdzenie funkcji Autopowtarzania ;sprawdzenie funkcji StoreKey ; test rozszerzonego opcodu klawisza ; funkcja rozszerzonego stanu

; Cóż, jeśli jest funkcja o której nie wiemy , wracamy do kodu wywołującego iret ; Jeśli użytkownik określił ah =0 lub ah = 10h, spadamy tutaj (nie będziemy rozróżniać między funkcją getc ; oryginalną a rozszerzoną GetKey:

mov int je

ah, 11h 16h GetKey

;zobaczmy czy klawisz jest dostępny ;czekamy na naciśnięcie

push ds. push bx mov ax, 40h mov ds., ax cli ;region krytyczny, wyłączamy przerwania mov bx, HeadPtr ;wskaźnik do kolejnego znaku mov ax, [bx] ; pobranie znaku incptr HeadPtr pop bx pop ds. iret ; przywracamy flag przerwania Sprawdza czy klawisz jest dostępny w buforze klawiatury. Tu musimy włączyć przerwania ( wiec ISR klawiaturowy może umieścić znak w buforze). Generalnie, będziemy chcieli zachować tu flagę przerwań . Ale BIOS zawsze wymusza włączenie przerwa, więc musi być jakieś programy zewnętrzne, które zależą od tego, więc nie „rozwiążemy” tego problemu

; TestKey; ; ; ; ; ; ; ;

Zwracamy status klawisza w ZF i AX. Jeśli ZF = 1 wtedy żaden klawisz nie jest dostępny a wartość w AX jest nieokreślona. Jeśli ZF=0 wtedy klawisz jest dostępny a AX zawiera kod klawiaturowy / ASCII kolejnego dostępnego klawisza. Ta funkcja nie usuwa kolejnego znaku z bufora wejściowego

TestKey:

sti

;włączamy przerwania

push push mov mov cli mov mov cmp pop pop sti retf

ds bx ax, 40h ds., ax ;region krytyczny, wyłączamy przerwania bx, HeadPtr ax, [bx] bx, TailPtr bx ds. 2

;BIOS zwraca dostępny kod klawisza ;ZF =1 jeśli pusty bufor ;ponownie włączamy przerwania ;zdejmujemy flagi, (ważne jest ZF )!

; Funkcja GetStatus zwraca zmienną KbdFlags1 w AL. GetStatus:

push mov mov mov pop iret

; StoreKey-

Wprowadza wartość w CX do bufora roboczego

StoreKey:

push push mov mov cli mov push mov incptr cmp jne pop sub add pop pop iret

StoreOkay:

ds. ax, 40h ds, ax al, KbdFlags1 ds

ds. bx ax, 40h ds, ax bx, TailPtr bx [bx], cx TailPtr bx, HeadPtr StoreOkay TailPtr sp, 2 sp, 2 bx ds.

;wyłączamy przerwania, region krytyczny ;adres gdzie możemy włożyć kolejny kod ;klawisza ;przechowanie kodu klucza ;przesuwamy na kolejne wejście w buforze ; przepełnienie danych/ ;jeśli nie, skok, jeśli tak ignorujemy wejście ;klawisza ;stos dopasowuje ścieżkę alt ;usuwamy śmiecie ze stosu ;przywracamy przerwania

; ExtStatus;

wyszukujemy rozszerzony status klawiatury i zwracamy go w AH, również zwracamy standardowy status klawiatury w AL.

ExtStatus:

push mov mov mov and test je or

ds. ax, 40h ds, ax ah, KbdFlags2 ah, 7Fh ah, 100b NoSysReq ah, 80h

and mov and or mov and

ah, 0F0h al., KbdFlags3 al., 1100b ah, al. al., KbdFlags2 al., 11b

;czyścimy końcowe pole sysreq ;test bieżącego bitu sysreq ;przeskok jeśli zero ;ustawienie końcowego bitu sysreq

NoSysReq: ;zerowanie bitów alt / ctrl ;przechwycenie bitów prawych alt / ctrl ; dzielimy w AH ;przechwycenie bitów lewego alt / ctrl

or

ah , al

;dzielimy w AH

mov pop iret

al., KbdFlags1 ds.

;AL zawiera zwykłe flagi

; SetAutoRpt; ;

Ustawia częstotliwość autopowtarzania. Na wejściu, bh =0 ,1,2 lub 3 (opóźnienie ¼ sek przed startem autopowtarzania) i bl = 0..1Fh ( częstotliwość powtarzania od 2: 1 do 30:1 (znak :sekunda).

SetAutoRpt:

push push

cx bx

mov call

al., 0Adh SetCmd

and mov shl and or mov call

bh, 11b cl, 5 bh, cl bl, 1Fh bh, bl al., 0F3h SendCmd

mov call mov call

al., 0AEh SetCmd al., 0F4h SendCmd

pop pop iret

bx cx

;blokujemy klawiaturę ;wymuszamy właściwą częstotliwość ;przesuwamy na końcową pozycję ;wymuszenie właściwej częstotliwości ; dane bajtu polecenia 8042 ;polecenie ustawienia częstotliwości powtarzania 8042 ;wysłanie parametrów do 8042 ; odblokowanie klawiatury ;restart skanowania klawiatury

MyInt16

endp

; SetCmd;

wysyła bajt poleceń w rejestrze AL. do chipu mikrokontrolera klawiatury 8042 (rejestr poleceń przy porcie 64h)

SetCmd

proc push push cli

near cx ax

;zachowanie wartości polecenia ;region krytyczny, przerwań brak

; Czekamy dopóki 8042 nie przetworzy bieżącego polecenia Wait4Empty:

xor in test loopnz

cx, cx al, 64h al, 10b Wait4Empty

;odczyt rejestru statusu klawiatury ;bufor wejściowy pełny? ;jeśli tak, czekamy dopóki nie będzie pusty

; Okay, wysyłamy polecenie do 8042:

SetCmd

pop out sti pop ret endp

ax 64h, AL.

;wyszukanie polecenia

; SendCmd-

Poniższy podprogram wysyła polecenie lub bajt danych do portu danych klawiatury

; przywracamy przerwania cx

;

(port 60h)

SendCmd

proc push push push mov mov mov

near ds. bx cx cx, 40h ds., cx bx, ax

mov cli

bh, 3

RetryLp:

;zachowanie bajtu danych ;powtarzamy polecenie ;blokujemy przerwania

; czyścimy flagi błędu, potwierdzenia odbioru i ponownego wysłania w KbdFlags4 and

byte ptr KbdFlags4, 4fh

;Czekamy dopóki 8042 nie przetworzy bieżącego polecenia Wait4Empty:

xor in test loopnz

cx, cx al, 64h al, 10b Wait4Empty

;odczyt rejestru statusu klawiatury ;bufor wejściowy pełny? ;jeśli tak, czekamy dopóki nie będzie pusty

;Okay, wysyłamy dane do portu 60h mov out sti

al., bl 60h, al. ;zezwalamy na przerwania

;Czekamy na nadejście potwierdzenia z ISR’a klawiaturowego: Wait4Ack:

xor test jnz loop dec jne

cx, cx byp KbdFlags4, 10 GotAck Wait4Ack bh RetryLp

;czekamy dłuższy czas jeśli trzeba ; bit potwierdzenia odbioru ; robimy powtórkę z nim

;Jeśli operacja zakończyła się niepowodzeniem po 3 wyszukiwaniach, ustawiamy bit błędu i ; wychodzimy or

byp KbdFlags4, 80h cx bx ds

SendCmp

pop pop pop ret endp

Main

proc

GotAck:

mov mov

ax, cseg ds, ax

print byte byte

“INT 16h Replecement”, cr, lf “Installing…”,cr,lf, 0

;ustawiamy bit błędu

;Aktualizujemy wektory przerwań INT 9 i INT 16. Zauważmy, że powyższe instrukcje czynią z

; cseg aktualny segment danych. Więc możemy tam przechować stare wartości INT 9 i INT 16 ; bezpośrednio w zmiennych OldInt9 i OldInt16. cli mov mov mov mov mov mov mov mov sti

;wyłączamy przerwania ax, 0 es, ax ax, es:[16h*4] word ptr OldInt16, ax ax, es:[16*4+2] word ptr OldInt16+2, ax es:[16h*4], offset MyInt16 es:[16h*4+2], cs ;OK włączamy przerwania

; Jedyna rzecz jaka nam pozostaje to zakończyć i pozostać w pamięci print byte

Main cseg sseg stk sseg zzzzzzseg LastBytes zzzzzzseg

mov int mov sub mov int endp ends

„Installed”,cr,lf,0 ah, 62h 21h dx, EndResident dx, bx ax, 3100h 21h

;pobieramy wartość PSP programu ;obliczamy rozmiar programu ;polecenie DOS’a TSR

segemnt para stack ‘stack’ db 1024 dup (“stack”) ends segemnt para public ‘zzzzzz’ db 16 dup (?) ends end Main

20.5 PODPROGRAM OBSŁUGI PRZERWAŃ KLAWIATUROWYCH ISR int 16h sprzęga aplikację z klawiaturą. W podobny tonie, ISR int 9 sprzęga między sprzętem klawiatury a ISR’em int 16h. Pracą ISR’a int 9 jest przetwarzanie przerwań sprzętu klawiaturowego, konwersji nadchodzących kodów klawiaturowych do kombinacji kodów klawiaturowego / ASCII i umieszcza je w buforze roboczym, i przetwarza inne wiadomości generowane przez klawiaturę. Konwertując kody klawiaturowe do kodów klawiaturowych / ASCII, ISR int 9 musi śledzić bieżący stan klawiszy modyfikujących. Kiedy nadchodzi kod klawiaturowy, ISR int 9 może użyć instrukcji xlat do translacji kodu klawiaturowego na kod ASCII używając tablicy int 9 wybranej na podstawie flag modyfikatorów. Inną ważną kwestią jest to ,że program obsługi int 9 musi obsłużyć specjalną sekwencję klawiszy taką jak ctrl-alt-del (reset) i PrtSc. Poniższy kod asemblerowy dostarcza prostego programu obsługi int 9 dla klawiatury. Nie wspiera alt-blok klawiszy kodu ASCII na wejściu lub kilka innych drobnych cech, ale wspiera prawie wszystko co potrzeba programowi obsługi przerwań klawiaturowych. Z pewnością demonstruje wszystkie te techniki jakie musimy znać oprogramowując klawiaturę ; INT9.ASM ; ; Krótki TSR dostarczający sterownika dla sprzętowego przerwania klawiatury ; ; Zauważmy, ze kod ten nie aktualizuje int 2Fh (przerwanie równoczesnych procesów), nie możemy usunąć tego ; kodu z pamięci z wyjątkiem ponownego startu. Jeśli chcemy móc zrobić te dwie rzeczy (jak również sprawdzić ; poprzednią instalację), zobaczmy rozdział o programach rezydentnych. Kod taki pominie my w tym programie ; z powodu ograniczenia długości. ;

; cseg i EndResident muszą wystąpić przed segmentem biblioteki standardowej! cseg OldInt9 cseg

segment para public ‘code’ dword ? ends

;Oznaczamy segment, znajdując koniec sekcji rezydentnej EndResident EndResident

segment para public ‘Resident’ ends .xlist include inlcudelib .list

NumLockScan ScrlLockScan CapsLockScan CtrlScan AltScan RShiftScan LShiftScan InsScanCode DelScanCode

equ equ equ equ equ equ equ equ equ

stdlib.a stdlib.lib

45h 46h 3ah 1dh 38h 36h 2ah 52h 53h

; Bity dla różnych klawiszy modyfikujących RshfBit LShfBit CtrlBit AltBit SLBit NLBit CLBit InsBit

equ equ equ equ equ equ equ equ

1 2 4 8 10h 20h 40h 80h

KbdFlags KbdFlags2 KbdFlags3 KbdFlags4

equ equ equ equ



byp

equ

< byte ptr>

cseg

segment para public ‘code’ assume ds: nothing

; Tablica translacji kodów klawiaturowych. Przychodzące z klawiatury kody klawiaturowe stanowią wiersz. ; Kolumny stanowią status modyfikatorów. Słowo na ich przecięciu to kod klawiaturowy / ASCII do włożenia ; do bufora roboczego PC. Jeśli wartość pobrana z tablicy to zero, wtedy nie kładziemy żadnego znaku do bufora ; ; norm shft ctrl alt num caps shcap shnum ScanXlat

word word word word word word word

0000h, 011bh, 0231h, 0332h, 0433h, 0534h, 0635h,

0000h, 011bh, 0231h, 0340h, 0423h, 0524h, 0625h,

0000h, 011bh, 0000h 0300h, 0000h, 0000h 0000h,

0000h, 011bh, 7800h, 7900h, 7a00h, 7b00h, 7c00h,

0000h, 011bh, 0231h, 0332h, 0433h, 0534h, 0635h,

0000h, 011bh, 0231h, 0332h, 0433h, 0534h, 0635h,

0000h, 011bh 0231h, 0332h, 0423h, 0524h, 0625h,

0000h 011bh 0321h 0332h 0423h 0524h 0625h

; ESC ;1 ! ;2 @ ;3 # ;4 $ ;5 %

word word word word word word word word word

0736h, 0837h, 0938h, 0a39h, 0b30h, 0c2dh, 0d3dh, 0e08h, 0f09h,

075eh, 0826h, 092ah, 0a28h, 0b29h, 0c5fh, 0d2bh, 0e08h, 0f00h,

071eh, 0000h, 0000h, 0000h, 0000h, 0000h, 0000h, 0e7fh, 0000h,

7d00h, 7e00h, 7f00h, 8000h, 8100h, 8200h, 8300h, 0000h, 0000h

0736h, 0873h, 0938h, 0a39h, 0b30h, 0c2dh, 0d3dh, 0e08h, 0f09h,

0736h, 0873h, 0938h, 0a39h, 0b30h, 0c2dh, 0d3dh, 0e08h, 0f09h,

075eh, 0826h, 092ah, 0a28h, 0n29h, 0c5fh, 0d2bh, 0e08h, 0f00h,

075eh 0826h 092ah 0a28h 0b29h 0c5fh 0d2bh 0e08h 0f00h

norm

shft

ctrl

alt

num

caps

shcap

shnum

word word word word word word word word

1071h, 1177h, 1265h, 1372h, 1474h, 1579h, 1675h, 1769h,

1051h, 1057h, 1245h, 1352h, 1454h, 1559h, 1655h, 1749h,

1011h, 1017h, 1205h, 1312h, 1414h, 1519h, 1615h, 1709h,

1000h, 1100h, 1200h, 1300h, 1400h, 1500h, 1600h, 1700h,

1071h, 1077h, 1265h, 1272h, 1474h, 1579h, 1675h, 1769h,

1051h, 1057h, 1245h, 1252h, 1454h, 1559h, 1655h, 1749h,

1051h, 1057h, 1245h, 1252h, 1454h, 1579h, 1675h, 1769h,

1071h 1077h 1265h 1272h 1474h 1559h 1655h 1749h

;Q ;W ;E ;R ;T ;Y ;U ;I

word word word word word word word word

186fh, 1970h, 1a5bh, 1b5dh, 1c0dh, 1d00h, 1e61h, 1f73h,

184fh, 1950h, 1a7bh, 1b7dh, 1c0dh, 1d00h, 1e41h, 1h53h,

180fh, 1910h, 1a1bh, 1b1dh, 1c0ah, 1d00h, 1e01h, 1f13h,

1800h 1900h, 0000h, 0000h, 0000h, 1d00h, 1e00h, 1f00h,

186fh, 1970h, 1a5bh, 1b5dh, 1c0dh, 1d00h, 1e61h, 1f73h,

184fh, 1950h, 1a5bh, 1b5dh, 1c0dh, 1d00h, 1e41h, 1f53h,

186fh, 1970h, 1a7bh, 1b7dh, 1coah, 1d00h, 1e61h, 1f73h,

184fh 1950h 1a7bh 1b7dh 1c0ah 1d00h 1e41h 1f53h

;O ;P ;[ { ;] } ; enter ; ctrl ;A ;S

word word word word word word word word word word word word word word word word

norm 2064h, 2166h, 2267h, 2368h, 246ah, 256bh, 266ch, 273bh, 2827h, 2960h, 2a00h, 2b5ch, 2c7ah, 2d78h, 2e63h, 2f76h,

shft 2044h, 2146h, 2247h, 2348h, 244ah, 254bh, 264ch, 273bh, 2822h, 297eh, 2a00h, 2b7ch, 2c5ah, 2d58h, 2e43h, 2f56h,

ctrl 2004h, 2106h, 2207h, 2308h, 240ah, 250bh, 260ch, 0000h, 0000h, 0000h, 2a00h, 2b1ch, 2c1ah, 2d18h, 2e03h, 2f16h,

alt 2000h, 2100h, 2200h, 2300h, 2400h, 2500h, 2600h, 0000h, 0000h, 0000h, 2a00h, 0000h, 2c00h, 2d00h, 2e00h, 2f00h,

num 2064h, 2166h, 2267h, 2368h, 246ah, 256bh, 266ch, 273bh, 2827h, 2960h, 2a00h, 2b5ch, 2c7ah, 2d78h, 2e63h, 2f76h,

caps 2044h, 2146h, 2247h, 2348h, 244ah, 254bh, 264ch, 273bh, 2827h, 2960h, 2a00h, 2b5ch, 2c5ah, 2d58h, 2e43h, 2f56h,

shcap 2064h, 2166h, 2267h 2368h, 246ah, 256bh, 266ch, 273ah 2822h, 297eh, 2a00h, 2b7ch, 2c7ah, 2d78h, 2e63h, 2f76h,

shnum 2044h 2146h 2247h 2348h 244ah 254bh 264ch 273ah 2822h 297eh 2a00h 2b7ch 2c5ah 2d58h 2e43h 2f56h

;D ;F ;G ;H ;J ;K ;L ;; : ;‘ “ ; ` ~ ; LShf ;\| ;Z ;X ;C ;V

word word word word word word word word word word word

norm 3062h, 316eh, 326dh, 332ch, 342eh, 352fh, 3600h, 372ah, 3800h, 3920h, 3a00h,

shft 3042h, 3143h, 324dh, 333ch, 343eh, 353fh, 3600h, 0000h, 3800h, 3920h, 3a00h,

ctrl 3002h, 310eh, 320dh, 0000h, 0000h, 0000h, 3600h, 3710h, 3800h, 3920h, 3a00h,

alt 3000h, 3100h, 3200h, 0000h, 0000h, 0000h, 3600h, 0000h, 3800h, 0000h, 3a00h,

num 3062h, 316eh, 326dh, 332ch, 342eh, 352fh, 3600h, 372ah, 3800h, 3920h, 3a00h,

caps 3042h, 314eh, 324dh, 332ch, 342eh, 352fh, 3600h, 372ah, 3800h, 3920h, 3a00h,

shcap 3062h, 316eh, 326dh, 333ch, 343eh, 353fh, 3600, 0000h, 3800h, 3920h, 3a00h,

shnum 3042h 314eh 324dh 333ch 343eh 353fh 3600 0000h 3800h 3920h 3a00

;B ;N ;M ; ,< ;.> ;/? ; rshf ;* PS ; alt ; spc ; caps

;

;

;

;6 ^ ;7 & ;8 * ;9 ( ;0 ) ;- _ ;= + ;bksp ; Tab

word word word word word

3b00h, 3c00h, 3d00h, 3e00h, 3f00h,

5400h, 5500h, 5600h, 5700h, 5800h,

5e00h, 5f00h, 6000h, 6100h, 6200h,

6800h, 6900h, 6a00h, 6b00h, 6c00h,

3b00h, 3c00h, 3d00h, 3e00h, 3f00h,

3b00h, 3c00h, 3d00h, 3e00h, 3f00h,

5400h, 5500h, 5600h, 5700h, 5800h,

5400h 5500h 5600h 5700h 5800h

; F1 ; F2 ; F3 ; F4 ; F5

word word word word word word word word word word word word word word word word

norm 4000h, 4100h, 4200h, 4300h, 4400h, 4500h, 4600h, 4700h, 4800h, 4900h, 4a2dh, 4b00h, 4c00h, 4d00h, 4e2bh, 4f00h,

shft 5900h, 5a00h, 5b00h, 5c00h, 5d00h, 4500h, 4600h, 4737h, 4838h, 4939h, 4a2dh, 4b34h, 4c35h, 4d36h, 4e2bh, 4f31h,

ctrl 6300h, 6400h, 6500h, 6600h, 6700h, 4500h, 4600h, 7700h, 0000h, 8400h, 0000h, 7300h, 0000h, 7400h, 0000h, 7500h,

alt 6d00h, 6e00h, 6f00h, 7000h, 7100h, 4500h, 4600h, 0000h, 0000h, 0000h, 0000h, 0000h, 0000h, 0000h, 0000h, 0000h,

num 4000h, 4100h, 4200h, 4300h, 4400h, 4500h, 4600h, 4737h, 4838h, 4939h, 4a2dh, 4b34h, 4c35h, 4d36h, 4e2bh, 4f31h,

caps 4000h, 4100h, 4200h, 4300h, 4400h, 4500h, 4600h, 4700h, 4800h, 4900h, 4a2dh, 4b00h, 4c00h, 4d00h, 4e2bh, 4f00h,

shcap 5900h, 5a00h, 5b00h, 500h, 5d00h, 4500h, 4600h, 4737h, 4838h, 4939h, 4a2dh, 4b34h, 4c35h, 4d36h, 4e2bh, 4f31h,

shnum 5900h 5a00h 5b00h 5c00h 5d00h 4500h 4600h 4700h 4800h 4900h 4a2dh 4b00h 4c00h 4d00h 4e2bh 4f00h

; F6 ; F7 ; F8 ; F9 ; F10 ; num ; scrl ; home ; up ; pgup ;; left ;center ; right ;+ ; end

word word word word word word word word word

norm shft ctrl 5000h, 5032h, 0000h, 5100h, 5133h, 7600h, 5200h, 5230h, 0000h, 5300h, 532eh, 0000h, 0, 0 ,0 ,0 ,0, 0, 0 ,0 0, 0 ,0 ,0 ,0, 0, 0 ,0 0, 0 ,0 ,0 ,0, 0, 0 ,0 5700h, 0000h, 0000h, 5800h, 0000h, 0000h,

alt 0000h, 0000h, 0000h, 0000h,

num 5032h, 5133h, 5230h, 532eh,

caps 5000h, 5100h, 5200h, 5300h,

shcap 5032h, 5133h, 5230h, 532eh,

shnum 5000h 5100h 5200h, 5300h

;

;

0000h, 5700h, 5700h, 0000h, 0000h 0000h, 5800h, 5800h, 0000h, 0000h

; down ; pgdn ; ins ; del ; -; -; -; F11 ; F12

;****************************************************************************************** ; ; AL zawiera kody klawiaturowe klawiatury PutInBuffer

proc push push

near ds. bx

mov mov

bx, 40h ds., bx

;ES wskazuje zmienne BIOS

; Jeśli aktualny kod klawiaturowy to E0 lub E1, musimy odnotować ten fakt, aby poprawnie przetworzyć klawisz ; kursora

TryE1:

cmp jne or and jmp

al., 0e1h TryE1 KbdFlags3, 10b KbdFlags3, 0Feh Done

cmp jne or and jmp

al., 0e1h DoScan KbdFlags3, 1 KbdFlags3, 0Feh Done

;ustawienie flagi E0 ; czyszczenie flagi E1

;ustawienie flagi E1 ; czyszczenie flagi E0

; Zanim zrobimy cokolwiek sprawdzamy czy jest Ctrl-Alt-Del: DoScan:

RebootAdrs

cmp jnz mov and cmp jne mov jmp

al., DelScanCode TryIns bl, KbdFlags bl, AltBit or CtrlBit bl, AltBit or CtrlBit DoPIB word ptr ds:[72h], 1234h dword ptr cs:RebootAdrs

dword 0ffff0000h

; Alt = bit 3, ctrl = bit 2 ; flaga gorącego restartu ; restart Komputera ; adres resetu

; Sprawdzamy klawisz INS. Jedynka musi przełączyć bit ins we flagach zmiennych klawiaturowych TryIns:

TryInsUp:

cmp jne or jmp

al., InsScanCode TryInsUp KbdFlags2, InsBit DoPIB

cmp jne and xor jmp

al., InsScanCode+80h TryShiftDn KbdFlags2, not InsBit KbdFlags, InsBit QuitPIB

;notujemy czy INS w dole ; wciśnięty klawisz INS ; górny kod klawiaturowy INS ; czy INS w górze ; przełączamy bit INS

; Obsługujemy tu klawisze lewego i prawego Shift w dole TryShiftDn:

TryLShiftUp:

TryRShiftDn:

cmp jne or jmp

al., LshiftScan TryLShiftUp KbdFlags, LshiftUp QuitPIB

cmp jne and jmp

al, LshiftScan+80h TryRShiftDn KbdFlags, not LShfBit QuitPIB

cmp jne or jmp

al, RshiftScan TryRShiftUp KbdFlags, RShfBit QuitPIB

TryRShiftUp:

cmp jne and jmp ; Obsługa klawisza ALT

al., RshiftScan+80h TryAltDn KbdFlags, not RshfBit QuitPIB

TryAltDn:

cmp jne or jmp

al., AltScan TryAltUp KbdFlags, AltBit QuitPIB

cmp jne and jmp

al, AltScan+80h TryCtrlDn KbdFlags, not AltBit DoPIB

GotoQPIB: TryAltUp:

;lewy shift w dole

; lewy shift w górze

; prawy shift w dole

; prawy shift w górze

; klawisz alt w dole

; klawisz alt w górze

; Tu działamy z klawiszem control w dole TryCtrlDn:

TryCtrlUp:

cmp jne or jmp

al., CtrlScan TryCtrlUp KbdFlags, CtrlBit QuitPIB

cmp jne and jmp

al, CtrlScan+80h TryCapsDn KbdFlags, not CyrlBit Qi\uitPIB

; klawisz ctrl w dole

; klawisz ctrl w górze

; tu działamy z klawiszem Capslock w dole TryCapsDn:

TryCapsUp:

cmp jne or xor jmp cmp jne and call jmp

al., CapsLockScan TryCapsUp KbdFlags2, CLBit KbdFlags, CLBit QuitPIB al, CapsLockScan+80h TrySLDn KbdFlags2, not CLBit SetLEDs QuitPIB

; Capslock w dole ; przełączenie capslock

; Capslock w górze

;działamy z klawiszem Scroll Lock TrySLDn:

TrySLUp:

cmp jne or xor jmp

al., ScrlLockScan TrySLUp KbdFlags2, SLBit KbdFlags, SLBit QuitPIB

cmp jne and call jmp

al, ScrlLockScan+80h TryNLDn KbdFlags2, not SLBit SetLEDs QuitPIB

; scroll lock w doel ; przełączenie scrl lock

; scrl lock w górze

; obsługa klawisza NumLock TryNLDn:

TryNLUp:

cmp jne or xor jmp

al., NumLockScn TryNLUp KbdFlags2, NLBit KbdFlags, NLBit QuitPIB

cmp jne and call jmp

al, NumLockScan+80h DoPIB KbdFlags2, not NLBit SetLEDs QuitPIB

;NumLock w dole ; przełączenie numlock

;numlock w górze

; Obsługujemy wszystkie inne klawisze: DoPIB:

test jnz

al., 80h QuitPIB

;ignorujemy inne klawisze w górze

; Jeśli najbardziej znaczący bit jest ustawiony w tym punkcie, lepiej będzie mieć zero w AL. W ; przeciwnym razie, jest to górny kod, który możemy bezpiecznie zignorować

call test je

Convert ax, ax QuitPIB

push mov mov int pop

cx cx, ax ah, 5 16h cx

QuitPIB:

and

KbdFlags3, 0FCh

Done:

pop pop ret endp

bx ds.

PutCharInBuf:

PutCharInBuf

;sprawdzenie na zły kod

;przechowanie kodu klawiaturowego w ; buforze roboczym ; E0, E1 nie są ostatnim kodem

;****************************************************************************************** ; ; ConvertAL zawiera kod klawiaturowy PC. Konwertuje do pary kod ASCII / kod klawiaturowy i ; zwraca wynik w AX. Kod ten zakłada, że DS. wskazuje przestrzeń zmiennych BIOS (40h) ; Convert

proc push

near bx

test jz mov mov jmp

al., 80h DownScanCode ah, al al., 0 CSDOne

;sprawdza czy górny kod

; Okay, mamy dolny klawisz. Ale przed pójściem dalej, zobaczymy czy nie ma sekwencji ALT- BlokKlawiatury DownScanCode: mov mov shl shl shl

bh, 0 bl, al bx,1 bx, 1 bx, 1

;mnożymy przez osiem aby obliczyć ; indeks wiersza tablicy xlat kodów ; klawiaturowych

; Obliczamy indeks modyfikatora: ; ; jeśli alt wtedy modyfikator = 3 test je add jmp

KbdFlags, AltBit NotAlt bl, 3 DoConvert

;

jeśli ctrl, wtedy modyfikator = 2

NotAlt:

test je add jmp

KbdFlags, CtrlBit NotCtrl bl, 2 DoConvert

; Bez względu na ustawienie shift, musimy działać z numlock I capslock. Numlock jest tylko problemem jeśli ; kod klawiaturowy jest większy lub równy 47h. Capslock, jeśli ten kod klawiaturowy jest mniejszy niż ten.

NotCtrl:

NumOnly:

cmp jb test je test je add jmp

al., 47h DoCapsLk KbdFlags, NLBit NoNumLck KbdFlags, LShfBit ot RshfBit NumOnly bl, 7 DoConvert

add Jmp

bl, 4 DoConvert

;testowanie bitu Numlock ;sprawdzenie l/p shift

;tylko numlock

; Jeśli numlock nie jest aktywny, zobaczymy czy jest klawisz shift NoNumLck:

test je add jmp

KbdFlags, LShfBit or RshfBit DoConvert bl, 1 DoConvert

;sprawdza l/p shift ;normalnie jeśli brak shift

; Jeśli wartość kodu klawiaturowego jest poniżej 47h, musimy sprawdzić capslock DoCapsLk:

CapsOnly:

test je test je add jmp

KbdFlags, CLBit DoShift KbdFlags, LShfBit or RshfBit CapsOnly bl, 6 DoConvert

add jmp

bl, 5 DoConvert

;sprawdzenie bitu capslock ; sprawdzenie l/p shift ;Shift i capslock ;CapsLock

;Cóż, nic więcej nie jest aktywne, sprawdzamy klawisz shift DoShift: DoConvert: CSDOne: Convert

test je add shl mov pop Ret endp

KbdFlags, LShfBIt ot RshfBit DoConvert bc, 1 bx, 1 ax, ScanXlat[bx] bx

; l/ p shift ;Shift ; tablica słó

; SetCmd; ;

Wysyła bajt poleceń w rejestrze AL do chipu mikrokontrolera klawiatury 8042 (rejestr poleceń przy porcie 64h)

SetCmd

proc push push cli

near cx ax

;zachowujemy wartość polecenia ;region krytyczny, żadnych przerwań

; Czekamy dopóki 8042 przetworzy bieżące polecenie Wait4Empty:

xor in test loopnz

cx, cx al, 64h al., 10b Wait4Empty

;Okay, wysyłamy polecenie do 8042:

;rejestr odczytu stanu klawiatury ;pełny bufor? ;jeśli tak czekamy aż będzie pusty

SetCmd

pop out sti pop ret endp

; SendCmd;

Poniższy podprogram wysyła polecenie lub bajt danych do portu danych klawiatury (port 60h)

SendCmd

proc push push push mov mov mov

near ds. bx cx cx, 40h ds., cx bx, ax

mov cli

bh, 3

RetryLp:

ax 64h, al

;wyszukujemy polecenie ;włączenie przerwań

cx

;zachowanie bajtu danych ;ponowienie polecenia ; blokujemy przerwania

; Flagi czyszczenia błędu, potwierdzenie odebrania i ponownego wysłani w KbdFlags4 and

byte ptr KbdFlags4, 4fh

; czekamy dopóki 8042 nie przetworzy bieżącego polecenia Wait4Empty:

xor in test loopnz

cx, cx al, 64h al., 10b Wait4Empty

;rejestr odczytu stanu klawiatury ;pełny bufor? ;jeśli tak czekamy aż będzie pusty

; Okay wysyłamy daną do portu 60h mov out sti

al., bl 60h, al. ;włączamy przerwania

; Czekamy na nadejście potwierdzenia z ISR’a klawiaturowego: Wait4Ack:

xor test jnz loop dec jne

cx, cx byp KbdFlags4, 10h GotAck Wait4Ack bh RetryLp

;czekamy dłuższy czas jeśli trzeba ;bit potwierdzenia odbioru ; ponowienie

; jeśli operacja się nie powiodła po 3 próbach, ustawiamy bit błędu i wychodzimy

GotAck:

SendCmd ;SetLEDs; ;

or

byp

pop pop pop ret endp

cx bx ds.

KbdFlags4, 80h ; ustawiony bit błędu

uaktualnia bity LED KbdFlags4 ze zmiennej KbdFlags a potem przekazuje nowe ustawienie flagi klawiatury

SetLEDs

proc push push mov mov shr and and or mov

near ax cx al., KbdFlags cl, 4 al., cl al., 111b KbdFlags4, 0F8h KbdFlags4, al. ah, al.

mov call

al., 0ADH SetCmd

;zablokowanie klawiatury

mov call mov call

al., 0Edh SendCmd al., ah SendCmd

;ustawienie poleceń LED 8042 ; wysłanie polecenia do 8042 ; pobranie parametrów bajtu ;wysłanie parametrów do 8042

al., 0AEh SetCmd al., 0F4h SendCmd cx ax

; odblokowanie klawiatury

SetLEDs

mov call mov call pop pop ret endp

; MyInt9-

Podprogram obsługi przerwania dla sprzętowego przerwania klawiatury

MyInt9

proc push push push

far ds ax cx

mov mov

ax, 40h ds., ax

mov call cli

al., 0ADh SetCmd

xor in test loopz in cmp je cmp jne or jmp

cx, cx al, 64h al., 10b Wait4Data al., 60h al., 0EEh QuitInt9 al., 0FAh NotAck KbdFlags4, 10h QuitInt9

cmp jne or jmp

al., 0Feh NotResend KbdFlags4, 20h QuitInt9

Wait4Data:

NotAck:

;czyszczenie bitów LED , maskowanie nowych bitów ;zachowanie bitów LED

;restart skanowania klawiatury

;blokada klawiatury ;blokada przerwań ;odczyt stanu portu klawiatury ;dana w buforze? ;czekaj póki dana jest dostępna ;pobrane danej klawiaturowej ; odpowiedź echa? ; potwierdzenie? ;ustawienie bitu potwierdzenia ;polecenie ponownego wysłania? ; ustawienie bitu ponownego wysłania

;Notka: inne polecenia sterownika klawiatury, wszystkie mają dwój najbardziej znaczący bit ustawiony

; a podprogram PutInBuffer będzie je ignorował NotResend: QuitInt9:

MyInt9 Main

call mov call

PutInBuffer al., 0AEh SetCmd

;włożenie do bufora roboczego ;ponowne odblokowanie klawiatury

mov out pop pop pop iret endp

al., 20h 20h, al. csx ax ds

; wysłanie EOI (koniec przerwania) ; do PIC 8259A

proc assume ds:cseg mov mov

ax, cseg ds, ax

print byte byte

“INT 9 Replacement” ,cr,lf “Installing…”,cr, lf, 0

; Aktualizujemy wektor przerwania INT 9. Zauważmy, że powyższe instrukcje zrobiły cseg ; bieżącym segmentem danych, wiec możemy przechować starą wartość INT 9bezpośrednio w ; zmiennej OldInt9 cli mov ax, 0 mov es, ax mov ax, es:[9*4] mov word ptr OldInt9, ax mov ax, es:[9*4+2] mov word ptr OldInt9+2, ax mov es:[9*4]. Offset MyInt9 mov es:[984+2], cs sti ; pozostało nam zakończyć i pozostawić w pamięci print byte

;przerwania wyłączone

; włączamy przerwania

„Installed”, cr, lf,0

Main cseg

mov int mov sub mov int endp ends

ah, 62h 21h dx, EndResident dx, bx ax, 3100h 21h

sseg stk sseg

segment para stack ‘stack’ byte 1024 dup (“stack”) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

; pobranie wartości PSP programu ;obliczamy rozmiar programu ;polecenie DOS TSR

20.6 AKTUALIZOWANIE PODPROGRAMU OBSŁUGI PRZERWANIA INT 9 Dla wielu programów, takich jak programy pop-up lub poprawionej klawiatury, możemy musieć zatrzymać pewne „gorące klawisze” i przekazać wszystkie pozostałe kody klawiaturowe do domyślnego podprogramu obsługi przerwania klawiaturowego. Możemy wprowadzić ISR int 9 do łańcucha przerwań dziewięć podobnie jak każde inne przerwanie. Kiedy klawiatura przerywa systemowi, wysyła kod klawiaturowy, program obsługi przerwania może odczytać ten kod z portu 60h i zadecydować czy przetwarzać sam kod klawiaturowy czy przekazać sterowanie do innego programu obsługi int 9. Poniższy program demonstruje tą zasadę; deaktywuje funkcję resetu ctrl-alt-del na klawiaturze poprzez przechwycenie i odrzucenie usuniętych kodów klawiaturowych kiedy bity ctrl i alt są ustawione w bajcie flagi klawiatury ; NORESET.ASM ; ; Krótki TSR, który aktualizuje przerwanie int 9 i przechwytuje sekwencje klawiszy ctrl-alt-del. ; ; Zauważmy, że kod ten nie aktualizuje przerwania 2Fh (przerwania równoczesnych procesów), nie można ; usunąć go z pamięci z wyjątkiem restartu. Jeśli chcemy móc zrobić te dwie rzeczy (jak również sprawdzenie ; poprzedniej instalacji) zajrzymy do rozdziału o programach rezydentnych. Kod taki został pominięty dla tego ; programu z powodu ograniczenia długości. ; ; cseg i EndResident muszą pojawić się przed segmentami biblioteki standardowej! ; cseg segment para public ‘code’ OldInt9 dword ? cseg ends ;Oznaczamy segment znajdując koniec sekcji rezydentnej EndResident Endresident

segment para public ‘Resident’ ends

DelScanCode

.xlist include includelib .list equ 53h

stdlib.a stdlib.lib

;Bity dla zmiennych klawiszy modyfikujących CtrlBit AltBit KbdFlags

equ equ equ

cseg

segment para public ‘code’ assume ds:nothing Wysyła bajt poleceń w rejestrze AL do chipa mikrokontrolera klawiatury 8042

;SetCmdSetCmd

proc push push cli

4 8

near cx ax

;zachowujemy wartość polecenia ;region krytyczny, żadnych przerwań

; Czekamy dopóki 8042 nie przetworzy bieżącego polecenia Wait4Empty

xor in test loopnz

cx, cx al, 64h al., 10b Wait4Empty

;odczyt rejestru stanu klawiatury ;czy bufor pełny? , jeśli tak, czekamy aż będzie pusty

;okay, wysyłamy polecenia do 8042:

SetCmd

pop out sti pop ret endp

; MyInt9; ; ; ;

Podprogram obsługi przerwania dla sprzętowego przerwania klawiatury. Testuje aby sprawdzić czy użytkownik nacisnął klawisz DEL. Jeśli nie, przekazuje sterowanie do oryginalnego programu obsługi int 9. Jeśli tak sprawdza czy są wciśnięte klawisze ctrl i alt; jeśli nie przekazuje sterowanie do oryginalnego programu W przeciwnym razie zjada kody klawiaturowe nie przekazując bezpośrednio DEL .

MyInt9

proc push push push

far ds. ax cx

mov mov

ax, 40h ds., ax

mov call cli xor in test loopz

al., 0ADh SetCmd cx, cx al, 64h al., 10b Wait4Data

in cmp jne mov and cmp jne

al., 60h al., DelScanCode OrigiInt9 al, KbdFlags al., AltBit or CtrlBit al, AltBit or CtrlBit OrigInt9

Wait4Data;

ax 64h, al.

;wyszukujemy polecenie ;włączamy przerwania

cx

;blokada klawiatury ;blokada przerwań ; odczyt stanu portu klawiatury ;dana w buforze? ;czekamy dopóki dana jest dostępna ;pobranie danej klawiaturowej ; czy to klawisz Delete? ;okay. Mamy Del, czy ctrl+alt wciśnięte?

; Jeśli wciśnięto ctrl+alt+del, zjadamy kod DEL I nie przekazujemy go bezpośrednio mov call

al., 0AEh SetCmd

;odblokowanie klawiatury

mov out pop pop pop iret

al., 20h 20h, al. cx ax ds

;wysłanie EOI (koniec przerwania) ;do PIC 8259A

; Jeśli ctrl i alt nie są oba wciśnięte, przekazujemy DEL do oryginalnego programu obsługi INT 9 OrigInt9:

mov call

al., 0Aeh SetCmd

pop pop pop jmp

cx ax ds. cs:OldInt9

;odblokowanie klawiatury

MyInt9

endp

Main

proc assume ds: cseg mov mov

ax, cseg ds, ax

print byte byte

“Ctrl-Alt-Del Filter”,cr, lf “Installing…”, cr, lf,0

;Aktualizujemy wektor przerwania INT 9. Zauważmy, że powyższe instrukcje uczyniły cseg aktualnym ; segmentem danych, więc możemy przechować starą wartość INT 9 bezpośrednio w zmiennej OldInt9 cli mov mov mov mov mov mov mov mov sti

;wyłączamy przerwania ax, 0 es, ax ax, es:[9*4] word ptr OldInt9, ax ax, es:[9*4+2], ax word ptr OldInt9+2, ax es:[9*4], offset MyInt9 es:[9*4+2], cs ;włączamy przerwania

; Pozostało zakończyć I pozostawić w pamięci print byte mov int

„Installed”, cr,lf,0 ah, 62h 21h

Main cseg

mov sub mov int endp ends

dx, EndResident dx, bx ax, 3100h 21h

sseg stk sseg

segment para stack ‘stack’ db 1024 dup (“stack”) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

;pobranie wartości PSP programu ;obliczanie rozmiaru programu ;polecenie DOS TSR

20.7 SYMULOWANIE UDERZEŃ W KLAWISZE Czasami możemy chcieć pisać pogramy, które przekazują naciśnięte klawisze do innej klawiatury. Na przykład możemy chcieć napisać makro klawiaturowe TSR, które pozwoli nam przechwycić pewne klawisze na klawiaturze i wysyłać sekwencję klawiszy bezpośrednio do odpowiedniej aplikacji. Być może będziemy chcieli oprogramować całe ciągi znaków normalnie nie używanych sekwencji klawiaturowej( np. ctrl-up lub ctrldown). W pewnych przypadkach nasz program będzie używał pewnych technik do przekazania znaków do aplikacji pierwszoplanowej . Są trzy dobrze znane techniki dla zrobienia tego: przechowanie kodu klawiaturowego/ ASCII bezpośrednio w buforze klawiatury, użyć flagi śledzenia dla symulacji instrukcji in al., 60h lub oprogramowanie mikrokontrolera zintegrowanego 8042 do przekazywania nam kodu klawiaturowego. Kolejne trzy sekcje opisują te techniki szczegółowiej.

20.7.1 WSTAWIANIE ZNAKÓW DO BUFORA ROBOCZEGO Być może najłatwiejszym sposobem wstawiania naciśniętych klawiszy do aplikacji jest wprowadzenie ich bezpośrednio do systemowego bufora roboczego. Większość nowoczesnych BIOS’ów dostarcza w tym celu funkcji int 16h. Nawet jeśli nasz system nie dostarcza tej funkcji łatwo jest napisać swój własny kod wprowadzający dane do systemowego bufora roboczego; lub można skopiować kod z programu int 16h pokazanego wcześniej w tym rozdziale. Fajne w tym podejściu jest to, ze możemy działać ze znakami ASCII (przynajmniej dla tych sekwencji klawiszy które są ASCII) Nie musimy martwić się o wysyłanie przesunięcia górnego i dolnego kodu dla kodu klawiaturowego „A”, aby uzyskać duża literę „A”, musimy tylko wprowadzić 1E41h do bufora. Faktycznie większość programów ignoruje kody klawiaturowe, więc możemy po prostu wprowadzić 0041h do bufora i prawie każda aplikacja będzie akceptowała kod klawiaturowy zero. Główna wadą techniki wkładania do bufora jest to ,że wiele (popularnych) aplikacji omija DOS i BIOS kiedy odczytuje klawiaturę. Program takie wchodzą bezpośrednio do portu klawiatury (60h) i odczytują dane. Jako takie pokazywanie kodów klawiaturowych / ASCII w buforze roboczym nie będzie odnosiło skutku. Idealnie byłoby gdybyśmy mogli wprowadzić kod klawiaturowy bezpośrednio do chipu mikrokontrolera klawiatury i zwracać ten kod klawiaturowy jak gdyby jakiś rzeczywiście naciśnięty klawisz. Niestety ,nie ma uniwersalnego sposobu na zrobienie tego. Jednakże są bliskie przybliżenia. 20.7.2 UŻYWANIE FLAGI ŚLEDZENIA 80X86 DO SYMULOWANIA INSTRUKCJI IN AL., 60H Jedyny sposób zajęcia się aplikacjami, które uzyskują bezpośrednio dostęp do sprzętu klawiatury to zasymulowanie zbioru instrukcji 80x86. Na przykład przypuśćmy, że przejmiemy sterowanie ISR int 9 i wykonamy każdą instrukcję pod naszą kontrolą. Możemy wybrać zezwolenie na wszystkie instrukcje z wyjątkiem instrukcji in wykonywanej zazwyczaj. Przy napotykaniu instrukcji in (której ISR klawiaturowy używa do odczytu ) sprawdzamy czy jest dostęp do portu 60h. Jeśli tak, po prostu ładujemy rejestr al. żądanym kodem klawiaturowym zamiast rzeczywiście wykonywaną instrukcją in. Ważne jest, aby sprawdzić instrukcję out, ponieważ ISR klawiaturowy będzie chciał wysłać sygnał EOI do PIC 8259A po odczytaniu danej klawiaturowej, możemy po prostu zignorować instrukcje out ,która zapisuje do portu 20h. Jedyną trudniejszą częścią jest powiedzenie 80x86 o przekazaniu sterowania do naszego podprogramu kiedy napotykamy pewne instrukcje (jak in i out) i wykonujemy normalnie inne instrukcje. Nie jest to bezpośrednio możliwe w trybie rzeczywistym, jest to bliskie przybliżenie jakie możemy uczynić. CPU 80x86 dostarczają flagę śledzenia, która generuje wyjątek po wykonaniu każdej instrukcji. Normalnie debuggery używają flagi śledzenia do przejścia przez program w pojedynczych krokach. Jednakże poprzez napisanie własnego programu obsługi wyjątku dla wyjątku śledzenia możemy wzmocnić sterowanie maszyną pomiędzy wykonaniem każdej instrukcji. Wtedy możemy przyjrzeć się opcodowi kolejnej instrukcji do wykonania. Jeśli nie jest to instrukcja in lub out, możemy określić adres I/O i zadecydować czy symulować czy wykonać instrukcję. Oprócz instrukcji in i out, będziemy musieli zasymulować instrukcję int. Powód jest taki, że instrukcja int odkłada flagi na stos a potem czyści bit śledzenia w rejestrze flag To oznacza, że podprogram obsługi przerwań powiązany z instrukcją int wykonuje się normalnie a my możemy zgubić pojawiające się w tym instrukcje in i out Jednakże, łatwo jest zasymulować instrukcję int pozostawiając włączoną flagę śledzenia, więc dodamy int do naszej listy instrukcji do przetłumaczenia. Jedyny problem x tym podejściem jest taki, że jest wolne. Chociaż podprogram pułapki śledzenia będzie wykonywał tylko kilka instrukcji na każde wywołanie, robi to dla każdej instrukcji ISR’a int 9. W wyniku, podczas symulacji, podprogram obsługi przerwania będzie działał 10 d0 20 razy wolniej niż kod rzeczywisty. Nie jest to generalnie problem ponieważ większość ISR’ ów klawiaturowych jest bardzo krótkich. Jednakże, możemy spotkać się z aplikacją, która ma duży wewnętrzny ISR int 9 a metoda ta zauważalnie zwolni program. Jednak dla większości aplikacji technika ta działa poprawnie i nie zauważymy żadnego spowolnienia wydajności podczas pisania na klawiaturze. Poniższy kod asemblerowy dostarcza krótkiego przykładu programu obsługi śledzenia, który symuluje naciskanie klawisz w ten sposób: .xlist include includelib .list cseg

stdlib.a stdlib.lib

segemnt para public ‘code’

assume ds:nothing ; ScanCode musi być w segmencie kodu ScanCode byte 0 ;****************************************************************************************** ; ; KbdSimPrzekazuje kod klawiaturowy w AL. bezpośrednio do kontrolera klawiatury używając flagi ; śledzenia. Sposób w jaki działa to włączenie bitu śledzenia w rejestrze flag. Każda instrukcja ; wtedy wywołuje pułapkę śledzenia (Zainstalowany0 program obsługi śledzenia patrzy na ; każdą instrukcję obsługującą IN, OUT, INT i inne specjalne instrukcje. Po napotkaniu IN AL., ; 60 (lub odpowiednika) kod ten symuluje tą instrukcję i zwraca określony kod klawiaturowy ; zamiast aktualnie wykonywanej instrukcji IN. Inne instrukcje również potrzebują specjalnego ; traktowania. Zobacz kod szczegółowo. Kod ten jest całkiem niezły przy symulowaniu sprzętu ; ale działa dość wolno i ma kilka problemów kompatybilności/ KbdSim

proc

near

pushf push push push

es ax bx

xor mov cli mov

bx, bx es, bx cs:ScanCode, al.

push push

es:[1*4] es:2[1*4]

;wskazuje tablicę wektorów przerwań ; (do symulowania INT 9) ; żadnych przerwań ; zachowanie wyjściowego kodu ; klawiaturowego ; zachowanie aktualnego wektora INT 1 ;aby odzyskać go później

;wektor INT 1 wskazuje na nasz program obsługi INT 1: mov mov

word ptr es:[1*4], offset MyInt1 word ptr es:[1*4+2], cs

; Włączamy pułapkę śledzenia (bit 8 rejestru flag) pushf pop or push popf

ax ah, 1 ax

;Symulujemy instrukcje INT 9. Notka: nie możemy tu w rzeczywistości wykonać INT 9 ponieważ instrukcje ; INT wyłączają operację śledzenia pushf call

dword ptr es:[9*4]

; Wyłączamy operację śledzenia pushf pop and push popf

ax ah, 0feh ax

; Blokujemy operację śledzenia

;czyścimy bit śledzenia

pop es:[1*4+2] pop es:[1*4] ; Okay, zrobione. Przywracamy rejestry i wracamy VMDone:

KbdSim

pop pop pop popf ret endp

;przywrócenie poprzedniego programu ;obsługi INT 1

bx ax es

;--------------------------------------------------------------------------------------------------------------------------------------; ; MyInt1- Obsługuje pułapkę śledzenia (INT1). Kod ten przygląda się kolejnemu opcodowi określając czy jest to ;jeden ze specjalnych opcodów, jaki musimy obsłużyć sami. MyInt1

proc push mov push push

far bp bp, sp bx ds.

;zyskujemy dostęp do adresu powrotnego poprzez ; BP

; Jeśli jesteśmy tu, to dlatego, że ta pułapka śledzenia jest bezpośrednio z powodu naszego dziurawego bitu ; śledzenia. Przetwarzamy pułapkę śledzenia dla symulowania zbioru instrukcji 80x86 ; ; Pobranie adresu powrotnego w DS.:BX NextInstr:

lds

bx, 2[bp]

;Poniżej jest specjalny przypadek do szybkiego eliminowania większości opcodów I przyspieszenia tego kodu ; poprzez ograniczenie ilości

NotSimple:

TryInOut0:

cmp jnb pop pop pop iret

byte ptr[bx], 0cdh NotSimple ds. bx bp

;większość opcodów jest mniejszych niż 0cdh, ; w związku z tym szybko wrócimy do ;;rzeczywistego programu

je

IsIntInstr

; Czy to jest instrukcja INT

mov cmp je jb

bx, [bx] bl, 0e8h ExecInstr TryInOut0

;pobranie opcodu bieżącej instrukcji ; opcod CALL

cmp je cmp je pop pop pop iret

bl, 0ech MayBeIn60 bl, 0eeh MayBeOut20 ds bx bp

;IN al dx instrukcja

cmp je cmp je

bx, 60e4h IsINAL60 bx, 20e6h IsOut20

;OUT dx, al instrukcja ; zwykła instrukcja

;IN al, instrukcja 60h ; out 20, al instrukcja

; Jeśli nie była to jedna z magicznych instrukcji , wykonujemy ja i kontynuujemy ExecInstr:

pop pop pop iret

ds. bx bp

; Jeśli ta instrukcja to IN AL, DX, musimy popatrzyć na wartość w DX odkreślając czy jest to rzeczywiście ; instrukcja IN Al., 60h MayBeIn60:

cmp jne inc mov jmp

dx, 60h ExecInstr word ptr 2[bp] al., cs:ScanCode NextInstr

;przeskakujemy 1 bajt tej instrukcji

;Jeśli jest to instrukcja IN AL, 60h, symulujemy ją poprzez załadowanie bieżącego kodu klawiaturowego do AL. IsInAL60:

mov add jmp

al., cs:ScanCode word ptr 2[bp], 2 NextInstr

Przeskakujemy ponad dwoma bajtami instrukcji

; Jeśli jest to instrukcja OUT DX, AL., musimy popatrzyć do DX aby zobaczyć czy wychodzimy do lokacji 20h ; (8259) MayBeOut20:

cmp jne inc jmp

dx, 20h ExecInstr word ptr 2[bp] NextInstr

;przeskakujemy ten 1 bajt instrukcji

; Jeśli jest to instrukcja OUT 20h, al., po prostu przeskakujemy to IsOut20:

add jmp

word ptr 2[bp], 2 NextInstr

;przeskakujemy instrukcję

; IsIntInstrWykonujemy ten kod jeśli jest to instrukcja INT ; ; Problem z instrukcjami INT jest taki, że resetują bit śledzenia przy wykonaniu. Dla pewnych z nich możemy t ; tego nie mieć ; ;Notka: w tym punkcie stos wygląda jak następuje: ; ; flags ; ; rtn cs -+ ; | ; rtn ip +-- Wskazuje kolejną instrukcję CPU do wykonania ; bp ; bx ; ds. ; ; Musimy zasymulować właściwą instrukcję INT poprzez: ; (1) dodanie dwa do adresu powrotnego na stosie (więc wracamy poza instrukcję INT) ; (2) odłożenie flag na stos ; (3) odłożenie fałszywego adresu powrotu na stos, który symuluje adres powrotu ; przerwania INT 1, ale który „zwraca” nam określony program obsługi przerwania ; ; Wszystkie te wynik na stosie wyglądają jak następuje: ;

; ; ; ; ; ; ; ; ; ; ; ; ; ; IsINTInstr:

MyInt1

flags rtn cs -+ | rtn +-- Wskazuje kolejną instrukcję poza instrukcję INT flags --- Fałszywe flagi dla symulowania tych odłożonych przez instrukcję INT rtn cs -+ | rtn ip +-- „Adres powrotny” który wskazuje na ISR dla tego INT bp bx ds. add mov mov shl shl

word ptr 2][bp], 2 bl, 1[bx] bh, 0 bx, 1 bx, 1

;wypadniecie adresu powrotnego poza instrukcję INT

push push push

[bp-0] [bp-2] [bp-4]

;pobranie i zachowanie BP ;pobranie i zachowanie BX ; pobranie i zachowanie DS.

push xor mov

cx cx, cx ds., cx

;wskazuje DS jako tablicę wektorów przerwań

mov mov

cx, [bp+6] [bp-0], cx

;pobranie oryginalnych flag ;zachowanie odłożonych flag

mov mov mov mov

cx, ds.:2[bx] [bp-2], cx cx, ds.:[bx] [bp-4] ,cx

;pobranie wektora I użycie go jako adresu powrotnego

pop pop pop pop iret

cx ds bx bp

;Mnożenie przez 4 do pobrania adresu wektora

endp

;Program główny – symuluje jakieś naciśnięcia klawiszy dal demonstracji powyższego kodu Main

proc mov mov

ax, cseg ds, ax

print byte byte byte

“Simulating keystrokes via Trace Flag”,cr,lf “This program places ‘DIR’ in the keybord buffer” cr, lf, 0

mov call mov

al, 20h KbdSim al., 0a0h

; dolny kod “D” ; górny kod “D”

call

KbdSim

mov call mov call

al., 17h KbdSim al., 97h KbdSim

; dolny kod “I”

mov call mov call

al. 13h KbdSim al., 93h KbdSim

; dolny kod “R”

mov call mov call

al., 1Ch KbdSim al., 9Ch KbdSim

; dolny kod klawisza Enter

Main

ExitPgm endp

cseg sseg stk sseg

ends segment para stack ‘stack’ byte 1024 dup (“stack”) ends

zzzzzzseg LastBytes Zzzzzzseg

segemnt para public ‘zzzzzz’ db 16 dup (?) ends end Main

;górny kod “I”

;górny kod “R”

;górny kod klawisza Enter

20.7.3 UŻYCIE MIKROKONTROLERA 8042 DO SYMULOWANIA UDERZEŃ W KLAWISZE Chociaż flaga śledzenia oparta na podprogramie „klawiaturowym” działa z większością oprogramowania, bezpośrednio komunikując się ze sprzętem, ma jeszcze kilka problemów. Ściśle, nie działają pod wszystkimi programami, które operują w trybie chronionym poprzez bibliotekę „DOS Extender” (biblioteka programistyczna, która pozwala programistom na uzyskani dostępu do więcej niż jednego megabajt pamięci podczas pracy pod DOS) Ta ostania technika jakiej się przyjrzymy jest to oprogramowanie zintegrowanego mikrokontrolera klawiatury 8042 dla przekazywani nam uderzeń w klawisze. Są dwa sposoby zrobienia tego: sposób PS/2 i sposób twardy. Mikrokontroler PS/2 zawiera ściśle zaprojektowane polecenia do zwracania użytkownikowi programowalnych kodów klawiaturowych systemu .Poprzez wpisanie bajtu 0D2h do portu poleceń kontrolera (64h) i bajt kodu klawiaturowego do portu 60h, możemy zmusić kontroler do zwrócenia kodu klawiaturowego jak gdyby użytkownik nacisnął klawisz na klawiaturze. Używając tej techniki dostarczamy największej kompatybilności ( z istniejącym oprogramowaniem) przy zwracaniu kodu klawiaturowego do aplikacji. Niestety, ta sztuczka działa na maszynach mających kontrolery , które są kompatybilne z PS/2; nie jest to większość maszyn. Jednakże jeśli napiszemy kod dla PS/2 lub kompatybilnych, jest to najlepszy sposób. Kontroler klawiatury na PC/AT i większości innych kompatybilnych maszynach PC nie wspiera polecenia 0D2h. Niemniej jednak jest to podstępny sposób wymuszenia na kontrolerze klawiatury przekazanie kodu klawiaturowego, jeśli złamiemy kilka zasad. Sztuczka ta może nie działać na wszystkich maszynach (jest wiele maszyn na których ta sztuczka jest znana jako błąd), ale jest dostępna na dużej liczbie kompatybilnych maszyn PC. Sztuczka jest prosta . Chociaż kontroler klawiatury nie ma polecenia do zwracania bajtu, wysyłamy go, dostarcza polecenia do zwrotu bajtu polecenia kontrolera klawiatury (KCCB). Dostarcza również innego polecenia do zapisu wartości do KCCB. Przez zapisanie wartości do KCCB a potem wydanie polecenia odczyty KCCB możemy oszukać system wprowadzając kod programowalny użytkownika. Niestety KCCB zawiera pewne zarezerwowane, niezdefiniowane bity, które mają różne znaczenie na różnego rodzaju chipach mikrokontrolera klawiatury. Jest to główny powód, że technika ta nie działa na wszystkich maszynach. Poniższy kod asemblerowy demonstruje jak używać tych metod dla PS/2 i kontrolera klawiatury PC:

.xlist include includelib .list cseg

stdlib.a stdlib.lib

segment para public ‘code’ assume ds:nothing

;****************************************************************************************** ; ; PutInATBuffer; ; Poniższy kod wkłada kod klawiaturowy do chipa mikrokontrolera klawiatury klasy AT i zapytuje go czy wyśle ; kod klawiaturowy z powrotem do nas (poprzez sprzętowy port) ; ; Kontroler klawiatury AT: ; ; Port danych jest pod adresem I/O 60h ; Port stanu jest pod adresem I/O 64h (tylko odczyt) ; Port polecenia jest pod adresem I/O 64h (tylko zapis) ; ; Kontroler odpowiada poniższymi wartościami wysyłanymi do portu polecenia: ; ; 20h – Odczyt bajtu polecenia kontrolera klawiatury (KCCB) i wysłanie danych do portu danych (adres I/O 64h) ; ; 60h – zapis do KCCB. Kolejny bajt zapisywany do adresu I/O 60h jest umieszczany w KCCB. Bity w KCCB ; są definiowane jak następuje : ; ; bit 7- Zarezerwowane, powinno być zero ; bit 6- Tryb komputera przemysłowego ; bit 5- Tryb komputera przemysłowego ; bit 4- Blokowanie klawiatury ; bit 3- Zakaz przykrycia ; bit 2- System flag ; bit 1- Zarezerwowane, powinno być zero ; bit 0- Odblokowanie bufora wyjściowego pełnego przerwań ; ; AAh - Autotest ; ABh - Test interfejsu ; ACh - Zrzut diagnostyczny ; ADh - Zablokowanie klawiatury ; AEh - Odblokowanie klawiatury ; C0h - Odczyt portu wejściowego Kontrolera Klawiatury ; D0h - Odczyt portu wyjściowego Kontrolera Klawiatury ; D1h - Zapis do portu wyjściowego Kontrolera klawiatury ; E0h - Odczyt testów wejściowych ; F0h – FFh - Impuls portu wyjściowego ; ; Port wyjściowy kontrolera klawiatury jest zdefiniowany jak następuje: ; ; bit 7- Dane klawiatury (wyjście) ; bit 6- Zegar klawiatury (wyjście) ; bit 5- Bufor wejściowy pusty ; bit 4- Bufor wyjściowy pełny ; bit 3- niezdefiniowany ; bit 2- niezdefiniowany ; bit 1- Gate A20 ; bit 0- Reset systemu (0 = reset) ;

; Port wejściowy kontrolera klawiatury jest zdefiniowany jak następuje: ; ; bit 7- Zakazane przełączanie klawiatury (0 = zabronione) ; bit 6- Przełączanie monitora ( 0 = kolor, 1 = mono) ; bit 5- Zworka ; bit 4- RAM płyty głównej (0= zablokowany 256k RAM na płycie głównej) ; bity 0-3 Niezdefiniowane ; ; Port stanu kontrolera klawiatury (64h) jest zdefiniowany jak następuje: ; ; bit 1 – Ustawiony jeśli dana wejściowa (60h) nie jest dostępna ; bit 2- Ustawiony jeśli port wyjściowy (60h) nie może uzyskać dostępu do danej PutInATBuffer proc assume pushf push push push push mov

near ds.:nothing ax bx cx dx dl, al

;zachowanie znaku na wyjście

;Czekamy dopóki kontroler klawiatury nie będzie zawierał danej przed kontynuowaniem WaitWhlFull:

xor in test loopnz

cx, cx al, 64h al, 1 WaitWhlFull

;Najpierw maskujemy chip kontrolera przerwań (8259) aby powiedzieć mu o ignorowaniu ; przerwań pochodzących z klawiatury. Jednakże włączając przerwania właściwie przetwarzamy l przerwania z innych źródeł (jest to ważne jeśli będziemy wysyłać fałszywe EOI do kontrolera przerwań ; wewnątrz podprogramu BIOS INT 9 cli in push or out

al., 21h ax al., 2 21h, al.

;pobranie bieżącej maski ; zachowanie maski przerwań ; maska przerwania klawiatury

; Przekazujemy żądany kod klawiaturowy do kontrolera klawiatury. Wywołujemy ten bajt nowym ; poleceniem kontrolera klawiatury (wyłączyliśmy klawiaturę, więc to nie wpływa na nic) ; ; Poniższy kod mówi kontrolerowi klawiatury aby pobrał kolejny bajt i wysłał do niego i użył tego ; bajtu jako KCCB: call mov out

WaitToXmit al., 60h 64h, al.

;zapis nowego polecenia KCCB

;Wysyłamy kod klawiaturowy jako nowy KCCB: call mov out

WaitToXmit al., dl 60h, al

; Poniższy kod instruuje system o przekazaniu KCCB (tj. kodu klawiaturowego) do systemu: call

WaitToXmit

Wait4OutFull:

mov out

al., 20h 64h, al.

xor in test loopz

cx, cx al, 64h al, 1 Wait4OutFull

;polecenie “Wysłania KCCB”

;Okay, wysyłamy 45h z powrotem jako nowy KCCB pozwalając normalnej klawiaturze pracować ; poprawnie call WaitToXmit mov al., 60h out 64h, al. call mov out

WaitToXmit al, 45h 60h, al

;Okay wykonujemy podprogram INT 9 więc BIOS (lub kto inny0 może odczytać klawisz jaki właśnie ; upchnęliśmy w kontrolerze klawiatury. Ponieważ zamaskowaliśmy INT 9 w kontrolerze przerwań, nie będzie ; żadnych przerwań pochodzących z klawisza upchniętych w buforze. DoInt9:

in int

al. 60h 9

;zachowanie przerwań w kodzie ;symulowanie sprzętowych przerwań klawiatury

; Odblokowujemy klawiaturę call mov out

WaitToXmit al., 0aeh 64h, al.

; Okay, przywracamy maskę przerwania dla klawiatury w 8259a pop out pop pop pop pop popf ret PutInATBuffer endp

ax 21h, al. dx cx bx ax

; WaitToXmit- czekamy dopóki jest OK, wysyłania bajtu polecenia do portu kontrolera klawiatury WaitToXmit

proc push push xor TstCmdPortLp: in test loopnz pop pop ret WaitToXmit endp

near cx ax cx, cx al, 64h al, 2 TstCmdPortLp ax cx

; sprawdzamy pełny bufor wejściowy flag

;******************************************************************************************

; ; PutInPS2Buffer – Podobnie jak PutInATBuffer, używa chipu mikrokontrolera klawiatury do zwracania kodu ; klawisza. Jednakże, kompatybilne kontrolery PS/2 mają aktualne polecenie zwrotu kodu klawisza PutInPS2Buffer proc near pushf push ax push bx push cx push dx mov

dl, al

;czekamy dopóki kontroler klawiatury nie będzie zawierał danej WaitWhlFull

xor in test loopnz

cx, cx al, 64h al, 1 WaitWhlFull

; Poniższy kod mówi kontrolerowi klawiatury aby pobrał kolejny bajt do wysłania i zwrócił go jako kod ; klawiaturowy call mov out

WaitToXmit al., 0d2h 64h, al.

; zwracanie polecenia kodu klawiaturowego

; Wysyłanie kodu klawiaturowego call mov out pop pop pop pop popf ret PutInPS2Buffer endp

WaitToXmit al., dl 60h, al. dx cx bx ax

;Program główny - Symuluje uderzenia klawisza demonstrujący powyższy kod Main

proc mov mov

ax, cseg ds, ax

print byte byte byte

“Simulating keystrokes via Trace Flag”, cr, lf “This program places ‘DIR’ in the keyboard buffer” cr, lf, 0

mov call mov call

al, 20h PutInATBuffer al., 0a0h PutInATBuffer

; dolny kod “D”

mov call mov

al., 17h PutInATBuffer al., 97h

;dolny kod “I”

;górny kod “D”

;górny kod “I”

call

PutInATBuffer

mov call mov call

al., 13h PutInATBuffer al., 93h PutInATBuffer

;dolny kod “R”

mov call mov call

al., 1Ch PutInATBuffer al., 9Ch PutInATBuffer

;dolny kod Enter

Main

ExitPgm endp

cseg

ends

sseg stk sseg

segment para stack ‘stack’ byte 1024 dup (“stack”) ends

zzzzzzseg LastBytes zzzzzzseg

segment para public ‘zzzzzz’ db 16 dup (?) ends end Main

;górny kod “R”

;górny kod Enter

20.8 PODSUMOWANIE Rozdział ten może wydawać się nadmiernie długi jak na tak przyziemny I/O klawiatury. W końcu Biblioteka Standardowa dostarcza tylko jeden, prosty podprogram dla klawiatury , getc. Jednakże klawiatura PC jest bestią złożoną, mającą nie mniej niż dwa wyspecjalizowane mikroprocesory nim sterujące. Mikroprocesory te akceptują polecenia z PC i wysyłają polecenia i dane do PC. Jeśli chcemy napisać jakiś skomplikowany kod obsługi klawiatury, musimy dobrze rozumieć klawiaturę sprzętu bazowego. Rozdział ten zaczyna się opisem działania systemu kiedy użytkownik naciśnie klawisz. Okazuje się , że system przekazuje dwa kody klawiaturowe za każdym razem kiedy naciskamy klawisz – jeden kod klawiaturowy kiedy naciskamy klawisz i jeden kiedy zwalniamy klawisz. Są one nazwane dolnym i górnym kodem, odpowiednio. Kody klawiaturowe przekazywane do systemu mają trochę powiązań ze standardowym zbiorem znaków ASCII. Zamiast tego, klawiatura używa swojego własnego zbioru a podprogram obsługi przerwania klawiaturowego tłumaczy te kody klawiaturowe na ich właściwe kody ASCII. Niektóre klawisze nie mają kodów ASCII, dla tych klawiszy system przekazuje rozszerzony kod klawisza do aplikacji żądającej wejścia z klawiatury. Podczas tłumaczenia kodu klawiaturowego na kod ASCII, ISR klawiaturowy stosuje pewne flagi BIOS’a , które śledzą pozycję klawiszy modyfikujących. Do klawiszy tych zalicza się klawisze shift, ctrl, alt, capslock i numlock. Klawisze te są znane jako modyfikujące ponieważ modyfikują normalny kod tworzony przez klawisze na klawiaturze . ISR klawiaturowy upycha nadchodzące znaki w systemowym buforze roboczym i aktualizuje inne zmienne BIOS w segmencie 40h. Aplikacja lub inny system usług może mieć dostęp do tej danej przygotowanej przez podprogram obsługi przerwań klawiaturowych. *”Podstawy klawiatury” Interfejs PC klawiatury używa dwóch oddzielnych chipów mikrokontrolerów. Chipy te dostarczają użytkownikowi rejestrów programowych i bardzo elastycznego zbioru poleceń. Jeśli chcemy oprogramować klawiaturę poza prostym odczytem naciśnięć klawiszy (np. manipulowanie LED’ami na klawiaturze), będziemy musieli bliżej się zapoznać z tymi rejestrami i zbiorem poleceń tych mikrokontrolerów *”Interfejs sprzętowy klawiatury” Oba, DOS i BIOS dostarczają umiejętności odczytu klawisza z systemowego bufora roboczego. Jaka zwykle funkcje BIOS’a dostarczają elastyczności pod względem osiągania sprzętu. Co więcej, podprogram BIOS int 16h pozwala nam sprawdzić stan klawisza shift, wkłada kody klawiaturowe/ ASCII do bufora roboczego, modyfikując częstotliwość autopowtarzania i więcej. Mając tą elastyczność, trudno jest zrozumieć

dlaczego ktoś chciałby komunikować się bezpośrednio ze sprzętem klawiaturowym, zwłaszcza zważywszy na problemy kompatybilności, które wydają się plagą takich projektów. Aby nauczyć się właściwego sposobu odczytu znaku z klawiatury zajrzyj: *”Interfejs DOS klawiatury” *”Interfejs BIOS klawiatury” Chociaż bezpośredni dostęp do sprzętu klawiatury jest złym pomysłem dla większości, jest mała klasa programów, jak poprawiona klawiatura i programy pop-up, które rzeczywiście muszą uzyskać bezpośredni dostęp do sprzętu klawiaturowego. Programy te muszą dostarczyć podprogramu obsługi przerwania dla przerwania (klawiatury) int 9 *”Podprogram obsługi przerwań klawiaturowych” *”Aktualizacja podprogramu obsługi przerwania INT 9” Program klawiatury makro (poprawiona klawiatura) jest doskonałym przykładem programu, który może musieć komunikować się bezpośrednio ze sprzętem klawiatury. Jeden problem z takimi programami polega na tym, że muszą przekazywać znaki do podstawowej aplikacji. Znając naturę aplikacji obecnych w świecie, może to być trudnym zadaniem, jeśli chcemy mieć kompatybilność z dużą liczbą aplikacji PC. Te problemy i pewne rozwiązania pojawią się w: *”Symulowanie uderzeń w klawisze” *”Wstawianie znaków do bufora roboczego” *”Używanie flagi śledzenia 80x86 do symulowania instrukcji IN AL., 60H” *”Używanie mikrokontrolera 8042 do symulowania uderzeń w klawisze”

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG

HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK Konsultacje naukowe: NEKRO [email protected] [email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ DWUDZIESTY PIERWSZY: PORTY RÓWNOLEGŁE Oryginalny projekt IBM’owski dostarczał wsparcia dla trzech portów równoległych drukarki, które IBM nazwał LPT1:,LPT2:, i LPT3:. IBM prawdopodobnie przewidział maszyny, które będą wspierały drukarki mozaikowe, drukarkę z głowicą wirującą i być może inne typy drukarek dla różnych celów, wszystkie na jednej maszynie (drukarki laserowe miały pojawić się dopiero kilka lat później) . Z pewnością IBM nie przewidywał ogólnego zastosowania tych portów równoległych, gdyż prawdopodobnie zaprojektował by je inaczej. Dzisiaj, porty równoległe PC sterują klawiaturą, dyskami, streamerami, kontrolerami SCSI, kontrolerami ethernet (lub innymi sieciowymi), kontrolerem joysticka, pomocniczymi blokami klawiszy i różnymi urządzeniami, no i oczywiście drukarkami. Rozdział ten nie będzie próbował opisywać jak stosować port równoległy dla tych wszystkich różnych celów – ta książka już jest dość duża. Jednak gruntowne omówienie jak interfejs równoległy steruje drukarką i aplikacją portu równoległego (komunikacja krzyżowa) powinno dostarczyć nam dosyć pomysłów na implementację kolejnego wielkiego urządzenia równoległego. 21.1 PODSTAWOWE INFORMACJE O PORCIE RÓWNOLEGŁYM Są dwa podstawowe metody transmisji danych nowoczesnych obliczeń: równoległa transmisja danych szeregowa transmisja danych. Przy szeregowej transmisji danych (zobacz „Port szeregowy PC”) jedno urządzenie wysyła dane do innego jako pojedynczy bit w czasie po jednej linii. W transmisji równoległej, jedno urządzenie wysyła dane do innego jako kilka bitów w czasie (równolegle) kilkoma różnymi liniami. Na przykład, port równoległy PC dostarcza ośmiu lini danych w porównaniu do jednej lini danych portu szeregowego. Dlatego też, wydawałoby się, że port równoległy mógłby transmitować dane osiem razy szybciej ponieważ jest osiem razy więcej linii w kablu. Podobnie wydawałoby się, że kabel szeregowy, w takiej samej cenie jak kabel równoległy, mógłby iść osiem razy wolniej ponieważ jest mnij lini w kablu. Mamy kolejny problem z metodami komunikacji równoległej kontra szeregowej: szybkość kontra koszt. W praktyce, komunikacja równoległa nie jest osiem razy szybsza niż komunikacja szeregowa, także kable równoległe nie kosztują osiem razy więcej. Generalnie, ci, którzy projektowali kable szeregowe (np. kable ethernetowe) użyli najlepszych materiałów i ekranowania. Podnosi to koszt kabli ale pozwala transmitować dane, bit w czasie, dużo szybciej. Co więcej, lepsze kable pozwalają na większy dystans pomiędzy urządzeniami. Kable równoległe, z drugiej strony, są generalnie tańsze i zaprojektowane dla bardzo krótkich połączeń (mniej więcej od sześciu do dziesięciu stóp). Problemy świata rzeczywistego z szumem elektrycznym i przesłuchem tworzy problemy kiedy używamy długich kabli równoległych i ogranicza szybkość systemu przy transmisji danych. Faktycznie, oryginalna specyfikacja portu drukarki Centronics wskazuje na nie więcej niż 1000 znaków/ sekundę częstotliwości transmisji danych, więc wiele drukarek zaprojektowano do obsługi danych dla tej częstotliwości transmisji. Większość portów równoległych może łatwo przewyższyć osiągami tą wartość; jednakże czynnikiem ograniczającym jest jeszcze kabel, żadne wrodzone ograniczenie w nowoczesnych komputerach. Chociaż system komunikacji równoległej może używać różnej liczby linii do transmisji danych, większość systemów równoległych używa ośmiu lini danych do transmisji bajtu w czasie. Jest kilka godnych uwagi wyjątków. Na przykład interfejs SCSI jest interfejsem równoległym, nowsze wersje standardu SCSI pozwalają na ośmio- szesnasto- a nawet trzydziesto dwu bitowy transfer danych. W rozdziale tym skoncentrujemy się na transferach o rozmiarze bajta ponieważ port równoległy PC dostarcza ośmiobitowej danej . Typowy system komunikacji równoległej może być jednokierunkowy (unidirectional) lub dwu kierunkowy (bidirectional). Port równoległy PC wspiera komunikację jednokierunkowa (z PC do drukarki), więc rozpatrzymy najpierw ten najprostszy przypadek.

W systemie jednokierunkowej komunikacji równoległej są dwie rozpoznawalne węzły: węzeł transmisji i węzeł odbioru. Węzeł transmisji umieszcza dane na lini danych i informuje węzeł odbioru , że dana jest dostępna; wtedy węzeł odbioru odczytuje linię danych i informuje węzeł transmisji ,że pobrał dane. Odnotujmy jak te dwa węzły synchronizują swój dostęp do lini danych – węzeł odbioru nie odczytuje lini danych dopóki węzeł transmisji nie przekaże mu tego, węzeł transmisji nie umieszcza nowej wartości na lini danych dopóki węzeł odbioru nie usunie danych i nie przekaże węzłowi transmisji, że ma dane. Uzgodnienie (handshaking) jest terminem, które opisuje jak te dwa węzły koosdynuja transfer danych. Właściwa implementacja uzgodnienia wymaga dwóch dodatkowych linii. Linia strobe (lub strobowanie danych) jest tym czego używa węzeł transmisji do przekazania węzłowi odbioru , że dana jest dostępna. Linia acknowledge (potwierdzenia) jest tym czego węzeł odbioru używa do przekazania węzłowi transmisji, że pobrał już dane i jest gotów na więcej. W rzeczywistości port równoległy dostarcza trzeciej lini uzgodnienia, busy, której węzeł odbiorczy może użyć do przekazania węzłowi transmisji, że jest zajęty i węzeł transmisji nie próbował wysyłać danych. Typowa sesja transmisji danych wygląda podobnie jak następuje: Węzeł transmisji: 1) Węzeł transmisji sprawdza linię busy aby sprawdzić czy odbiór jest zajęty. Jeśli linia busy jest aktywna, nadajnik czeka w pętli dopóki linia busy stanie się nieaktywna 2) Węzeł transmisji umieszcza dane na lini danych 3) Węzeł transmisji aktywuje linię strobe 4) Węzeł transmisji czeka w pętli aż linia potwierdzenia stanie się aktywna 5) Węzeł transmisji ustawia nieaktywna strobe 6) Węzeł transmisji czeka w pętli aż linia potwierdzenia stanie się nieaktywna 7) Tan transmisji powtarza kroki od jeden do sześć dla każdego bajtu jaki musi przesłać Węzeł odbiorczy: 1) 2) 3) 4) 5) 6) 7)

Węzeł odbiorczy ustawia linię busy nieaktywną (zakładając gotowość do akceptacji danej) Węzeł odbiorczy oczekuje w pętli dopóki linia strobe nie stanie się aktywna. Węzeł odbiorczy odczytuje daną z lini danych (i przetwarza tą daną, jeśli to konieczne) Węzeł odbiorczy aktywuje linię potwierdzenia Węzeł odbiorczy oczekuje w pętli dopóki linia strobe nie stanie się nieaktywna Węzeł odbiorczy ustawia nieaktywną linię potwierdzenia Węzeł odbiorczy powtarza kroki od jeden do sześć dla każdego dodatkowego bajtu jaką musi odebrać

Ostrożnie korzystajmy z tych kroków, węzły odbiorczy i transmisji starannie koordynują swoje działania więc węzeł transmisji nie próbuje odłożyć kilku bajtów na linie danych zanim węzeł odbiorczy nie skonsumuje ich a węzeł odbiorczy nie próbuje czytać danych, których nie wysłał węzeł transmisji. Dwukierunkowa transmisja danych jest często niczym więcej niż dwom jednokierunkowymi transmisjami danych z rolą węzła transmisji i odbiorczego odwróconą dla drugiego kanału komunikacji. Niektóre porty równoległe PC (szczególnie w systemach PS/2 i wielu notebookach) dostarczają dwukierunkowego portu równoległego. Dwukierunkowa transmisja danych na takim sprzęcie nieco bardziej złożona niż w systemach, które implementują komunikację dwukierunkową z dwóch portów jednokierunkowych. Komunikacja dwukierunkowa w dwukierunkowym porcie równoległym wymaga dodatkowego zbioru lini sterujących, więc te dwa węzły mogą określić kto zapisuje do wspólnej lini danych w czasie. 21.2 SPRZĘT PORTU RÓWNOLEGŁEGO Standardowy jednokierunkowy port równoległy w PC dostarcza więcej niż 11 lini opisanych w poprzedniej sekcji (osiem lini danych , trzy linie uzgodnienia). Port równoległy PC dostarcza następujących sygnałów: Numer końcówki złącza 1 2 –9

Kierunek I/O wyjście wyjście

Aktywność Biegunowość 0 -

10

wejście

0

11

wejście

0

Opis sygnałów Strobe (sygnał dostępnej danej Linia danych (bit 0 to pin 2, bit 7 to pin 9 Linia potwierdzenia (aktywna kiedy zdalny system pobrał daną) Linia busy (aktywna kiedy system

12

wejście

1

13

wejście

1

14

wyjście

0

15

wejście

0

16

wyjście

0

17

wyjście

0

18 - 25

-

-

zdalny jest zajęty i nie można zaakceptować danej Brak papieru (aktywna kiedy w drukarce brak papieru) Wybór. Aktywna kiedy jest wybrana drukarka. Autoprzesuw. Aktywna kiedy drukarka automatycznie przesuwa linię po każdym powrocie karetki Błąd. Aktywna kiedy mamy błąd drukarki Inicjalizacja. Sygnał ten powoduje, że drukarka sam się inicjalizuje. Wybór wejścia. Sygnał ten, kiedy jest nieaktywny, wymusza autonomiczną drukarkę Sygnał uziemienia

Tablica 79; Sygnały portu równoległego Zauważmy, że port równoległy dostarcza 12 lini wyjściowych (osiem lini danych, strobe, autoprzesuwania, inicjalizacji i wyboru wejścia0 i pięć lini wejściowych (potwierdzenia, busy, brak papieru, wyboru i błędu). Pomimo, że port jest jednokierunkowy, jest dobrą mieszanką dostępnych lini wejściowych i wyjściowych w porcie. Wiele urządzeń (jak dysk lub streamer), które wymagają dwukierunkowego transferu danych używają tych dodatkowych lini do wykonania dwukierunkowego transferu danych W dwukierunkowym porcie równoległym ( system PS/2 i laptopy), linia danych i strobe, oba , są liniami wejściowymi i wejściowymi. Jest bit w rejestrze sterującym powiązanym z portem równoległym, który jest wybrany, który steruje kierunkiem w danym momencie (nie możemy przekazać danych w obu kierunkach równocześnie). Są trzy adresy I/O powiązane z typowym PC porcie równoległym. Adres te należą do rejestru danych, rejestru statusu i rejestru sterującego. Rejestr danych jest ośmiobitowym portem odczyt / zapis. Odczytując rejestr danych ( w trybie jednokierunkowym) zwracamy wartość ostatnio zapisaną do rejestru danych. Rejestr sterujący i statusu dostarcza interfejsu do innych lini I/O. Organizacja tego portu jest następująca:

Bit dwa (potwierdzenie drukarki) jest dostępny tylko na PS/2 i innych systemach, które wspierają dwukierunkowy port drukarki. Inne systemy nie używają tego bitu

Rejestr sterujący portu równoległego jest rejestrem wyjściowym. Odczytując tą lokację zwracamy ostatnią wartość zapisaną do rejestru sterującego z wyjątkiem bitu pięć, który jest tylko do zapisu. Bit pięć, bit kierunku danej, jest dostępny tylko w PS/2 i innych systemach, które wspierają dwukierunkowy port równoległy. Jeśli zapiszemy zero do tego bitu, linia strobe i danej są bitami wyjściowymi, podobnie jak jednokierunkowy port równoległy. Jeśli zapisujemy jeden do tego bitu, wtedy linie strobe i danej są wejściowe. Odnotujmy, że w trybie wejściowym (bit 5 =1), bit zero rejestru sterującego jest w rzeczywistości wejściowym. Notka: zapiszmy jeden do bitu cztery rejestru sterującego odblokowującego IRQ drukarki (IRQ 7). Jednakże , cecha ta nie działa na wszystkich systemach , więc bardzo mało programów próbuje używać przerwań z portu równoległego. Kiedy jest aktywny, port równoległy będzie generował int 0Fh kiedy drukarka potwierdza transmisję danych. Ponieważ PC wspiera do trzech oddzielnych portów równoległych, może być nie mniej niż trzy zbiory tych rejestrów portów równoległych w systemie w tym samym czasie. Są trzy adresy bazowe portu równoległego powiązane z trzema możliwymi portami równoległymi :3BCh, 378h i 278h. Będziemy się odnosili do tego jako adresów bazowych dla LPT1: , LPT2:, i LPT3:, odpowiednio. Rejestr danych portu równoległego jest zawsze ulokowany pod adresem bazowym dla portu równoległego, rejestr stanu pojawia się pod adresem bazowym plus jeden a rejestr sterujący pojawia się pod adresem bazowym plus dwa. Na przykład , dla LPT1:, rejestr danych jest pod adresem I/O 3BCh, rejestr statusu pod adresem I/O 3BDh a rejestr sterujący pod adresem I/O 3BEh. Jest jedno ważne zakłócenie. Adresy I/O dla LPT1:, LPT2:, i LPT3: dane powyżej są adresami fizycznymi dla portu równoległego. BIOS dostarcza również adresów logicznych dla tych portów równoległych. Pozwala to użytkownikom ponownie odwzorować ich drukarki (ponieważ większość programów tylko zapisuje do LPT1. Wykonując to, BIOS rezerwuje osiem bajtów w przestrzeni zmiennej BIOS (40:8, 40:0A, 40:0C i 40:0E). Lokacja 40:8 zawiera adres bazowy dla logicznego LPT1: , lokacja 40:0A zawiera adres bazowy dla logicznego LPT2:, itd. Kiedy oprogramowanie uzyskuje dostęp do LPT1:, LPT2:, itd., generalnie uzyskuje dostęp do portu równoległego, którego adres bazowy pojawia się w jednej z tych lokacji. 21.3 STEROWANIE DRUKARKI POPRZEZ PORT RÓWNOLEGŁY Chociaż jest wiele urządzeń które przyłącza się do portu równoległego PC, drukarki wykonują największą ilość takich połączeń. Dlatego też, opisanie jak sterować drukarką z portu równoległego PC jest prawdopodobnie najlepszym pierwszym przykładem do przedstawienia. Jak przy klawiaturze, nasze oprogramowanie może działać na trzech różnych poziomach: może drukować dane stosując DOS, stosując BIOS lub przez zapisanie bezpośrednio do sprzętu portu równoległego. Podobnie jak przy interfejsie klawiatury , stosowanie DOS lub BIOS jest najlepszym podejściem jeśli chcemy utrzymać kompatybilność z innymi urządzeniami podłączonymi do portu równoległego. Oczywiście, jeśli sterujemy jakimś innym typem urządzenia, przejście bezpośrednio do sprzętu jest tylko naszym wyborem. Jednakże, BIOS dostarcza dobrego wsparcia dla drukarek, więc podejście bezpośrednio do sprzętu jest rzadko koniecznością jeśli po prostu chcemy wysłać dane do drukarki. 21.3.1 DRUKOWANIE POPRZEZ DOS MS-DOS dostarcza dwóch funkcji jakie możemy użyć wysyłając dane do drukarki. Funkcja DOS’a 05h zapisuje znak w rejestrze dl bezpośrednio do drukarki. Funkcja 40h, z logicznym numerem pliku 04h, również wysyła daną do drukarki. Ponieważ rozdział o DOS i BIOS dokładnie opisał te funkcje, nie będziemy ich omawiać dalej tutaj.

21.3.2 DRUKOWANIE POPRZEZ BIOS Chociaż DOS dostarcza stosownego zbioru funkcji dla wysyłania znaków do drukarki, nie dostarcza funkcji, która pozwoli nam zainicjalizować drukarki lub uzyskać bieżącego stanu drukarki. Dlatego też DOS tylko drukuje do LPT1:. Podprogram BIOS’a PC int 17h dostarcza trzech funkcji, drukuj, inicjalizuj i status. Możemy zastosować te funkcje do każdego portu równoległego w systemie. Funkcja drukuj jest przybliżonym odpowiednikiem funkcji DOS’a drukowani znaku. Funkcja inicjalizacji inicjalizuje drukarkę przy użyciu systemowej informacji zależnej czasowo. Status drukarki zwraca informację z portu stanu drukarki wraz z informacją limitu czasu. 21.3.3 PODPROGRAM OBSŁUGI PRZERWANIA INT 17H Być może najlepszym sposobem zobaczenia jak funkcje BIOS działają jest napisanie zastępczego ISR’a 17h dla drukarki. Sekcja ta wyjaśni protokół uzgodnienia i zmienne używane przez drukarkę. Opisuje również działanie i zwrot wyniku powiązanego z każdą maszyną. Jest osiem zmiennych w przestrzeni zmiennych BIOS (segment 40h) jakich używa drukarka. Poniższa tabela opisuje każdą z tych zmiennych. Adres 40:08 40:0A 40:0C 40:0E 40:78

40:79 40:7A 40:7B

Opis Adres bazowy urządzenia LPT1: Adres bazowy urządzenia LPT2: Adres bazowy urządzenia LPT3: Adres bazowy urządzenia LPT4: Wartość ograniczenia czasu LPT1:. Oprogramowanie portu drukarki powinno zwracać błąd jeśli drukarka nie odpowiada w stosownej ilości czasu. Zmienna ta (jeśli nie zero) określa jak wiele pętli z 65,536 iteracji urządzenie będzie oczekiwało na potwierdzenie z drukarki. Jeśli zero, urządzenie będzie czekało zawsze Wartość ograniczenia czasu LPT2: .Jak wyżej Wartość ograniczenia czasu LPT3: .Jak wyżej Wartość ograniczenia czasu LPT4: .Jak wyżej Tablica 80: Zmienne BIOS portu równoległego

Zwrócimy uwagę na drobne odchylenie w protokole uzgodnienia w powyższym kodzie. Sterownik drukarki nie oczekuje na potwierdzenie z drukarki po wysłaniu znaku. Zamiast tego, sprawdza aby zobaczyć czy drukarka wysłała potwierdzenie dla poprzedniego znaku zanim wyśle znak. Zajmuje to małą ilość czasu ponieważ program drukując znaki może kontynuować działanie równolegle z odebraniem potwierdzenia z drukarki. Odnotujmy również, że to szczególne urządzenie nie monitoruje lini busy. Prawie każda istniejąca drukarka pozostawia tą linie nieaktywną (not busy), więc nie musimy jej sprawdzać. Jeśli napotkamy drukarkę, która manipuluje linią busy, modyfikacja tego kodu jest banalna. Poniższy kod implementuje usługę int 17h ; INT17.ASM ; ; Krótki bierny TSR, który zamienia program obsługi BIOS’a int 17h. Ten podprogram demonstruje ; funkcjonowanie każdej z funkcji int 17h, której standardowo dostarcza BIOS. ; ; Zauważmy, że kod ten nie aktualizuje int 2Fh (przerwania równoczesnych procesów) ani też nie możemy ; usunąć tego kodu z pamięci z wyjątkiem przeładowania. Jeśli chcemy móc zrobić te dwie rzeczy (jak również ; sprawdzić poprzednią instalację), zobacz rozdział o programach rezydentnych. Kod taki zostanie pominięty ; w tym programie z powodu ograniczenia długości ; ; cseg i EndResident muszą pojawić się przed segmentem biblioteki standardowej! cseg cseg

segment para public ‘code’ ends

; Oznaczamy segment znajdując koniec sekcji rezydentnej EndResident EndResident

segment para public ‘Resident’ ends

byp

.xlist include stdlib.a includelib stdlib.lib .list equ < byte ptr >

cseg

segment para public ‘code’ assume cs:cseg, ds:cseg

OldInt17

dword ?

; Zmienne BIOS: PrtrBase PrtrTimeOut

equ equ

8 78h

; Kod ten obsługuje działanie INT 17H. INT 17h jest podprogramem BIOS wysyłającym dane do drukarki i ; i raportują status drukarki. Są trzy różne funkcje dla tego podprogramu , w zależności od zawartości rejestru ; AH. Rejestr DX zawiera numer portu drukarki ; ; DX=0 –Używamy LPT1: ; DX=1 – Używamy LPT2: ; DX=2 – Używamy LPT3: ; DX=3 – Używamy LPT4: ; ; AH=0 -Drukujemy znak w AL na drukarce. Status drukarki jest zwracany w AH. Jeśli bit #0 = 1 wtedy ; wystąpi błąd limitu czasu ; ; AH=1 -Inicjalizacja drukarki. Status zwracany w AH ; ; AH=2 -Zwrot statusu drukarki w AH ; ; Bity statusu zwracane w AH są takie jak następuje ; Bit Funkcja Wartość bez błędu ; ----------------------------------;0 1 = błąd limitu czasu 0 ;1 nie używane x ;2 nie używane x ;3 1 = błąd I/O 0 ;4 1 = wybrany, 0 = nie wybrany 1 ;5 1 = brak papieru 0 ;6 1 = potwierdzenie x ;7 1 = nie zajęta x ; ; Zauważmy, że sprzęt zwraca bit 3 z zerem jeśli wystąpił błąd, z jedynką jeśli nie ma błędu. Program zazwyczaj ; odwraca ten bit przed zwróceniem go do programu wywołującego ; ; Lokacje sprzętowego portu drukarki: ; ; PrtrPortAdrs -Port wyjściowy gdzie dana jest wysyłana do drukarki (8 bitów) ; PrtrPortAdrs+1 -Port wejściowy gdzie może być odczytany status drukarki (8 bitów) ; PrtrPortAdrs+2 -Port wyjściowy gdzie informacja sterująca jest wysyłana do drukarki

; ; Port wyjściowy danych – 8 – bitowa dana jest transmitowana do drukarki przez ten port ; ; Status portu wejściowego: ; bit 0: nie używany ; bit 1: nie używany ; bit 2: nie używany ; bit 3; - Błąd, zazwyczaj ten bit oznacza, że drukarka napotkała błąd. Jednakże z ; zainstalowanym P101 jest to dana lini sygnału zwrotnego dla skanowania ; klawiatury ; ; bit 4: +SLTC, zazwyczaj bit ten jest używany do określenia czy drukarka jest ; wybrana czy nie. Z zainstalowanym P101 jest to dana lini sygnału zwrotnego ; skanowanej klawiatury ; ; bit 5: +PE, 1 w tym bicie oznacza ,że drukarka wykryła koniec papieru. Na wielu ; portach drukarek , bit ten jest nieczynny ; ; bit 6: -ACK, zero w tym bicie oznacza, ze drukarka zaakceptowała ostatni znak i ; jest gotowa do przyjęcia kolejnego. Bit ten nie jest zazwyczaj używany przez ; BIOS ponieważ bit 7 również spełnia taką funkcję (lub więcej) ; ; bit 7: -Busy, kiedy ten sygnał jest aktywny (0) wtedy drukarka jest zajęta i nie ; może zaakceptować danej. Kiedy bit ten jest ustawiony na jeden, drukarka ; może zaakceptować kolejny znak ; ; Wyjściowy port sterujący: ; bit 0: +Strobe, 0,5 µs (minimum) aktywny impuls na tym bicie zegarowym ; zamyka dane portu wyjściowego danych drukarki przed drukarką ; bit 1: +Auto FD XT – 1 przechowywane pod tym bitem powoduje, że drukarka ; przesuwa linię po lini wydrukowanej. W pewnych interfejsach ; drukarek (np. karta graficzna Hercules) bit ten jest nieczynny ; ; bit 2: -INIT, zero w tym bicie (dla minimum 50 us) bezie powodował, że ; drukarka sama będzie się (re)inicjalizowała ; ; bit 3: +SLCT, jeden w tym bicie wybiera drukarkę. Zero spowoduje przejście ; drukarki do trybu off –line ; ; bit 4: +IRQ ENABLE, jeden w tym bicie pozwala na wystąpienie przerwań ; kiedy –ACK zmieni się z jeden na zero ; bit 5: Sterowanie kierunkiem w porcie dwukierunkowym. 0 = ; wyjście, 1= wejście ; ; bit 6: zarezerwowane, musi być zerem ; bit 7: zarezerwowane, musi być zerem ; MyInt17

proc far assume ds.:nothing push push push push

ds bx cx dx

mov mov

bx, 40h ds., bx

;DS wskazuje zmienne BIOS

cmp ja

dx, 3 InvalidPrtr

;musi być LPT1...LPT4

cmp jz cmp jb je

ah, 0 PrtChar ah, 2 PrtrInit PrtrStatus

;skocz do właściwego kodu dla funkcji drukowania

; Jeśli przekazano nam opcod jakiego nie znaliśmy, wracamy InvalidPrtr:

jmp

ISR17Done

;Inicjalizujemy drukarkę poprzez impulsowanie lini init dla przynajmniej 50 µs. Poniższa pętla ; opóźniająca będzie opóźniała dobrze ponad 50 µs nawet na szybszych maszynach PrtrInit:

PIDelay:

mov shl mov test je add in and out mov loop or out jmp

bx, dx bx, 1 dx, PrtrBase[dx] dx, dx InvalidPrtr dx, 2 al., dx al., 11011011b dx, al cx, 0 PIDelay al., 100b dx, al. ISR17Done

;pobranie wartości portu drukarki ;konwersja do bajtu indeksu ;pobranie adresu bazowego drukarki ;czy ta drukarka istnieje? ;wyjście jeśli nie ma takiej drukarki ;dx wskazuje na rejestr sterujący ;odczyt bieżącego statusu ; zerowanie bitów INIT/BIDIR ;reset drukarki ; to będzie tworzyło przynajmniej 50 µs opóźnienie ;zatrzymanie resetu drukarki

; Zwracamy bieżący status drukarki. Kod odczytuje status portu drukarki i formatuje bity do zwrotu do kodu ; wywołującego PrtrStatus:

mov shl mov mov test je inc in and jmp

bx, dx bx, 1 dx, PrtrBase[bx] al., 00101001b dx, dx InvalidPrtr dx al., dx al., 11110000b ISR17Done

;pobranie wartości portu drukarki ; konwersja do bajtu indeksu ;adres bazowy portu drukarki ;Domyślnie: każdy możliwy błąd ;czy ta drukarka istnieje? ;wychodzimy jeśli nie ;wskazuje status portu ;odczyt statusu portu ;zerowanie bitów nieużywanych / przekroczenia czasu

;Druk znaku w akumulatorze! PrtChar:

mov mov shl mov or jz

bx, dx cl, PrtrTimeOut[bx] bx, 1 dx, PrtrBase[bx] dx, dx NoPrtr2

;pobranie wartości przekroczenia czasu ;konwersja do bajtu indeksu ;pobranie adresu portu drukarki ;wskaźnik nie zerowy? ;skok jeśli zerowy

;Poniższy kod sprawdza aby zobaczyć czy z drukarki zostało odebrane potwierdzenie. Jeśli ten kod czeka zbyt ; długo, jest zwracany błąd przekroczenia czasu. Potwierdzenie jest dostarczane w bicie 7 portu statusu drukarki ; (który jest kolejnym adresem po porcie danych drukarki) push

ax

WaitLp1: WaitLp2:

inc mov mov xor in mov test jnz loop dec jnz

dx bl, cl bh, cl cx, cx al., dx ah, al al., 80h GotAck WaitLp2 bl WaitLp1

;wskazuje status portu ;włożenie wartości przekroczenia czasu do bl I bh ;inicjalizacja licznika 65536 ;odczyt statusu portu ;zachowanie statusu ;potwierdzenie drukarki? ;skok jeśli potwierdzenie ;powtarzanie 65536 razy ;zmniejszanie wartości przekroczonego czasu ;powtarzanie 65536*TimeOut razy

;Zobaczmy czy użytkownik wybrał czas przekroczenia: cmp bh, 0 je WaitLp1 ; WYSTĄPIŁ BŁĄD PRZEKROCZENIA CZASU! ; ; Przekroczenie czasu – błąd I/O jest zwracany do systemu przez ten port. Albo nie dochodzi do skutku ; ten punkt (błąd przekroczenia czasu) albo odnośny port drukarki nie istnieje. W innym przypadku zwraca ; błąd NoPrtr2:

or and xor

ah, 9 ah, 0F9h ah, 40h

;ustawienie flagi błędu I/O – przekroczenia czasu ;wyłączenie nie używanych flag ;

;Okay, przywracamy rejestry i wracamy do kodu wywołującego pop cx ;usunięcie starego ax mov al., cl ;przywrócenie starego al jmp ISR17Done ;Jeśli port drukarki istnieje i odebraliśmy potwierdzenie, wtedy jest możliwość przekazania danych do drukarki. GotAck:

mov loop pop push dec pushf cli out

cx, 16 GALp ax ax dx

;krótkie opóźnienie jeśli drukarka potrzebuje czasu ; po potwierdzeniu ;pobranie znaku na wyjściu i ponowne zachowanie

dx, al.

;dane wyjściowe do drukarki

;DX wskazuje port drukarki ;wyłączenie przerwań

; Poniższe krótkie opóźnienie daje danym czas na przeniesienie przez linie równoległe. To zapewnia, że dane ; przybywają do drukarki przed strobe (czas ten może zależeć od przepustowości kabli lini równoległej) DataSettleLp:

mov loop

cx, 16 DataSettleLp

; czas danej przed wysłaniem strobe

; Teraz dana została zamknięta w porcie wyjściowym drukarki, strobe musi zostać wysłany do drukarki. Linia ; strobe jest połączona do bitu zero portu sterującego. Odnotujmy również, że czyści to bit 5 portu sterującego. ; Zapewnia to, że port kontynuuje działania porcie wyjściowym jeśli jest to urządzenie dwukierunkowe. Kod ten ; również czyści bity sześć i siedem, które IBM zaleca ustawiać na zero inc

dx

;DX

wskazuje

na

wyjściowy

inc in and

dx al., dx al., 01eh

;pobranie bieżących bitów sterujących ;wymuszenie lini strobe na zero

drukarki

port

out

dx, al.

;i upewnienie się ,że to port wyjściowy

mov loop

cx, 16 Delay0

;krótkie opóźnienie pozwalające danym ;stać się dobrymi

or out

al., 1 dx, al

;wys³¹nie (+) strobe ; wyjściowy (+) strobe do bitu 0

mov loop

cx, 16 StrobeDelay

;krótkie opóźnienie wydłużające strobe

and out popf

al., 0Feh dx, al.

;zerowanie bitu strobe ; wyjście do portu sterującego ;przywrócenie przerwań

pop mov

dx al., dl

;pobranie starej wartości AX ‘przywrócenie starej wartości AL.

dx cx bx ds.

MyInt17

pop pop pop pop iret endp

Main

proc

Delay0:

StrobeDelay:

ISR17Done:

mov mov

ax, cseg ds, ax

print byte byet

“INT 17 Replacement”,cr, lf “Installing…”, cr,lf,0

; Aktualizujemy wektor przerwania INT 17. Zauważmy, że powyższe instrukcje uczyniły cseg bieżącym ;segmentem danych, więc możemy przechować starą wartość INT 17 bezpośrednio w zmiennej OldInt17 cli mov mov mov mov mov mov mov mov sti

;wyłączenie przerwań ax, 0 es, ax ax, es:[17h*4] word ptr OldInt17, ax ax, es:[17h*4+2] word ptr OldInt17+2, ax es:[17h*4], offset MyInt17 es:[17h*4+2], cs ;Ok, załączamy przerwania

; Jedyne co pozostało to zakończyć i pozostawić w pamięci print byte

„Installed.”,cr, lf,0

mov int

ah, 62h 21h

;pobranie wartości PSP programu

mov sub mov

dx, EndResident dx, bx ax, 3100h

;obliczamy rozmiar programu ;polecenie TSR DOS’a

Main cseg

int endp ends

21h

sseg stk sseg

segment para stack ‘stack’ byte 1024 dup (‘stack’) ends

zzzzzzseg LastBytes zzzzzzseg

segemnt para public ‘zzzzzz’ byte 16 dup (?) ends end Main

21.4 MIĘDZY KOMPUTEROWA KOMUNIKACJA PORTEM RÓWNOLEGŁYM Chociaż drukowanie jest najpopularniejszym zastosowaniem dla portu równoległego na PC, wiele urządzeń stosuje port równoległy dla innych celów, jak wspomniano wcześniej. Nie pasowałoby zamknąć tego rozdziału bez przynajmniej jednego przykładu aplikacji nie drukarkowej portu równoległego .Sekcja ta bezie opisywała jak ustawić dwa komputery do przekazywania plików z jednego do drugiego z wykorzystaniem portu równoległego. Program Laplink™ firmy Travelling Software jest dobrym przykładem produktu komercyjnego, który może przekazać dane przez port równoległy PC; chociaż poniższe oprogramowanie nie jest tak silne jak Laplink, demonstruje podstawowe zasady takiego oprogramowania. Zauważmy, że nie możemy połączyć dwóch portów równoległych komputerów prostym kablem, który ma łącza DB25 na każdym końcu. Faktycznie robiąc tak możemy uszkodzić porty równoległe komputerów ponieważ połączylibyśmy cyfrowe wyjście do cyfrowego wyjścia (w rzeczywistości nie –nie) . Jednakże, zakupując kable „kompatybilne z Laplinkiem” ( lub rzeczywisty kabel Laplink do tego celu) mamy poprawne połączenie pomiędzy portami równoległymi dwóch komputerów. Jak możemy sobie przypomnieć z sekcji o sprzęcie portu równoległego, port równoległy jednokierunkowy dostarcza pięć sygnałów wejściowych. Kabel Laplink wyznacza drogę czterech lini danych do czterech lini wejściowych w obu kierunkach. Połączenia w kompatybilnym z Laplink kablu pokazano jak następuje:

Dane zapisywane w bitach od zero do trzy rejestru danych węzła transmisji pojawiają się , nie zmienione, w bitach od trzy do sześć portu stanu węzła odbiorczego. Bit cztery węzła transmisji pojawia się, odwrócony, w bicie siedem węzła odbiorczego. Zauważmy ,że kable kompatybilne z Laplink są dwukierunkowe. To znaczy, możemy przekazywać dane z jednego węzła do innego używając powyższego połączenia. Jednakże, ponieważ jest tylko pięć bitów w porcie równoległym, musimy przekazać cztery bity danych w jednym czasie 9potrzebujemy jeden bit na daną strobującą). Ponieważ węzeł odbiorczy musi potwierdzić transmisję danych, nie możemy zasymulować transmisji danych w obu kierunkach. Musimy użyć jednej z lini wyjściowych węzła odbiorczego danych do potwierdzenia przychodzącej danej. Ponieważ dwa węzły współpracują w transferze danych przez kabel równoległy, muszą jedna po drugiej przesyłać i odbierać dane, muszą utworzyć protokół, aby każdy uczestnik wymiany danych wiedział

kiedy następuje przesył i odbiór danych. Nasz protokół będzie bardzo prosty – węzeł jest albo przekaźnikiem albo odbiorcą, ich role nie będą przełączane. Zaprojektowanie bardziej złożonego protokołu nie jest trudne, ale ten prosty protokół będzie wystarczający dla tego przykładu. Później w tym rozdziale omówimy sposób stworzenia protokołu, który pozwala na transmisję dwukierunkową. Poniższy przykład będzie przekazywał i odbierał pojedynczy plik przez port równoległy. Używając tego programu, uruchamiamy program przekaźnika w węźle transmisji i program odbiorcy w węźle odbiorczym. Program transmitera pobiera nazwę pliku z lini poleceń DOS i otwiera ten plik do odczytu (generując błąd i wychodząc, jeśli plik nie istnieje). Zakładając, że plik istnieje, program przekaźnika odpytuje węzeł odbiorczy aby zobaczyć czy jest dostępny. Przekaźnik sprawdza obecność węzła odbiorczego poprzez kolejne zapisywanie zer i jedynek do wszystkich bitów wyjściowych potem odczytując jego bity wejściowe. Węzeł odbiorczy będzie odwracał te wartości i zapisywał je z powrotem kiedy będzie on-line. Zauważmy, że porządek wykonania (najpierw przesłanie lub najpierw odbiór) nie ma znaczenia. Te dwa programy będą próbowały uzgadniać dopóki nie nadejdzie coś innego on-line. Kiedy oba węzły dokonają inwersji trzy razy, zapiszą wartość 05h do swoich portów wyjściowych, mówiąc innym węzłom ,że są gotowe do kontynuacji. Funkcja przekroczenia czasu przerywa program jeśli jakiś inny węzeł nie odpowiada prze stosowną ilość czasu. Ponieważ te dwa węzły są zsynchronizowane, węzeł transmisji określa rozmiar pliku a potem przekazuje nazwę pliku i rozmiar do węzła odbiorczego. Węzeł odbiorczy zaczyna oczekiwanie na odbiór danej. Węzeł transmisji wysyła 512 bajtów danych do węzła odbiorczego. Po przekazaniu 512 bajtów, węzeł odbiorczy opóźnia wysłanie potwierdzenia i zapisuje 512 bajtów danych na dysk. Potem węzeł odbiorczy wysyła potwierdzenie a węzeł transmisji zaczyna wysyłanie kolejnych 512 bajtów. Proces ten powtarza się , dopóki węzeł odbiorczy nie zaakceptuje wszystkich bajtów z pliku. Tu mamy kod przekaźnika: ; TRANSMIT.ASM ; ; Program ten jest częścią przekaźnikową programu, który przesyła pliki poprzez kompatybilny z Laplink ; kablem równoległym. ; ; Program ten zakłada, że użytkownik chce użyć LPT1: dla transmisji. Modyfikuje przyrównywanie lub czyta ; port z lini poleceń jeśli jest to niewłaściwe. .286 .xlist include includelib .list

stdlib.a stdlib.lib

dseg

segment para public ‘data’

TimeOutConst PrtrBase

equ equ

4000 10

;około 1 minuty na 66MHz 486 ; offset adresu LPT1:

MyPortAdrs FileHandle FileBuffer

word word byte

? ? 512 dup (?)

;przechowuje adres portu drukarki ;obsługa pliku wyjściowego ;bufor dla nadchodzących danych

FileSize FileNamePtr

dword ? dword ?

dseg

ends

cseg

segment para public ‘code’ assume cs:cseg, ds:dseg

; TestAbort; ;

sprawdza aby zobaczyć czy użytkownik wcisnął ctrl-C i chce przerwać program. Podprogram wywołuje BIOS dla sprawdzenia czy użytkownik nacisnął klawisz. Jeśli tak, wywołuje DOS do odczytu tego klucza (funkcja AH=8, odczyt klucza w/o echo i sprawdzeniem ctrl-C

;rozmiar nadchodzącego pliku ;przechowuje wskaźnik do pliku

TestAbort

TestAbort

proc push push push mov int je mov int pop pop pop ret endp

; SendByte-

Przekazanie bajtu w AL do czterech bitów węzła odbiorczego w jednym czasie

SendByte

proc push push mov

near cx dx ah, al.

;zachowanie bajtu do transmisji

mov

dx, MyPortAdrs

; adres bazowy portu LPT1:

NoKeyPress:

near ax cx dx ah, 1 16h NoKeyPress ah, 8 21h dx cx ax

;zobacz czy wciśnięto klawisz ;powrót jeśli nie ;odczyt znaku, sprawdzenie na ctrl-c ;przerwanie DOS jeśli ctrl-C

; Najpierw, aby być pewnym, zapisujemy zero do bitu #4. Zostanie odczytane jako jeden w bicie ; busy odbiorcy mov out

al., 0 dx, al.

;dana jeszcze nie gotowa

;Czekamy dopóki odbiornik jest zajęty. Odbiornik zapisze zero do bitu #4 do swojego rejestru danych ; kiedy jest zajęty. Wychodzi on jako jeden w naszym bicie busy (bit 7 rejestru stanu). Pętla oczekuje ; dopóki odbiornik nie powie nam, że jest gotowy odebrać daną poprzez zapisanie jeden do bitu #4 (który my ; odczytujemy jako zero). Zauważmy, że sprawdzamy ctrl-C często w przypadku kiedy użytkownik chce ; przerwać transmisję. inc W4NBLp: mov Wait4NotBusy: in test loopne je call jmp

dx cx, 10000 al., dx al., 80h Wait4NotBusy ItsNotBusy TestAbort W4NBLp

;wskazuje rejestr stanu ;odczyt wartości rejestru stanu ;Bit 7 =1 jeśli zajęty ;powtarzamy kiedy zajęty, 10000 razy ;opuszczamy pętlę jeśli nie zajęty ;sprawdzamy dla Ctrl-C

;Okay, wkładamy daną na linie danych: ItsNotBusy:

dec mov and out or out

dx al., ah al., 0Fh dx, al. al., 10h dx, al.

;wskazuje rejestr danych ;pobranie kopii danej ;usunięcie bardziej znaczącego nibble’a ‘”Pierwsza” linia danych, dana nie dostępna ;włączenie dostępnej danej ;wysłanie danej

;Czekamy na potwierdzenie z węzła odbiorczego. Co jakiś czas sprawdzamy ctrl-C, czy użytkownik ; może przerwała transmisję programu z wnętrza tej pętli. W4Alp:

inc mov

dx cx, 10000

;wskazuje rejestr stanu ;czas pętli pomiędzy sprawdzeniem Ctrl-C

Wait4Ack:

in test loope jne call jmp

al., dx al, 80h Wait4Ack GotAck TestAbort W4Alp

;odczyt stanu portu ;Ack = 1 kiedy odbiorca potwierdza ;powtarzanie 10000 razy lub jeśli potwierdzenia ;skok jeśli mamy potwierdzenie ;sprawdzenie Ctrl - C użytkownika

;Wysyłamy daną niedostępnego sygnału do odbiorcy: GotAck:

dec mov out

dx al., 0 dx, al.

;wskazuje rejestr danych ;zapis zera do bitu 4, pojawia się jako jeden ;w bicie busy odbiorcy

;okay ,w bardziej znaczącym nibble’u: inc W4NB2: mov Wait4NotBusy2: in test loopne call jmp

cx cx, 10000 al., dx al., 80h Wait4NotBusy TestAbort W4NB2

;wskazuje rejestr stanu ;10000 wywołań pomiędzy sprawdzaniem ctrl-C ;odczyt rejestru stanu ;;Bit 7 = 1 jeśli zajęty ;bardziej znaczący bit wyzerowany (nie zajęty)? ;sprawdzenie ctrl-C

;Okay, wkładamy daną na linie danych: NotBusy2:

dec mov shr

dx al., ah al., 4

;wskazuje rejestr danych ;odzyskujemy dane bardziej znaczącego nibble’a ;przesuwamy bardziej znaczący nibble do

out or out

dx, al. al., 10h dx, al.

;”pierwsza” linia danych ; dana + dana dostępny strobe ;wysłanie danej

mniej

znaczącego

;Czekamy na potwierdzenie z węzła odbiorczego: W4A2Lp: Wait4Ack2:

inc mov in test loope jne call jmp

dx cx, 10000 al., dx al, 80h Wait4Ack2 GotAck2 TestAbort W4A2Lp

; wskazuje rejestr stanu ;odczyt stanu portu ;Ack = 1 ;kiedy brak potwierdzenia ;bardziej znaczący bit = 1 (potwierdzenie)? ;sprawdzenie ctrl-C

;wysłanie danej niedostępnego sygnału do odbiorcy: GotAck2:

SendByte

dec mov out

dx al., 0 dx, al.

;wskazuje rejestr danych ;zero w bicie 4 (który staje się busy=1 u odbiorcy)

mov pop pop ret endp

al, ah dx cx

;przywrócenie oryginalnej danej w AL

; Podprogram synchronizacji: ; ; Send0sPrzesłanie zera do węzła odbiorczego a potem oczekiwanie aby zobaczyć czy została

; ;

jedynka. Zwraca ustawioną flagę przeniesienia jeśli to działa, wyczyszczoną jeśli nie ustawia jedynki w rozsądnej ilości czasu.

Send0s

proc push push

near cx dx

mov

dx, MyPortAdrs

mov out

al, 0 dx, al.

;zapis wartości początkowego zera do naszego ; portu wyjściowego

xor inc in dec and cmp loopne je clc pop pop ret

cx, cx dx al., dx dx al. 78h al., 78h Wait41s Got1s

;sprawdza jedynkę 10000 razy ;wskazuje stan portu ;odczyt stanu portu ;wskazuje ponownie port danych ;maskuje bity wejściowe ;wszystkie jedynki?

Wait41s:

Got1s:

;skok jeśli sukces ;zwraca niepowodzenie

dx cx

Send0s

stc pop pop ret endp

; Send1s; ;

Przesyła wszystkie jedynki do węzła odbiorczego a potem oczekuje aby zobaczyć czy są ustawione ponownie zera. Zwraca ustawioną flagę przeniesienia jeśli działa, wyczyszczoną jeśli nie ma ustawionych zer w rozsądnej ilości czasu

Send1s

proc push push

near