&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
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 (a<>b)) or ((x=y) and (c<=d)) then SomeStmt; Możemy użyć rozpowszechnionych praw do uproszczenia tego do: If ((x=y) and (a<>b) or (c<=d)) then SomeStmt; Podobnie możemy użyć twierdzeń DeMorgana do redukcji: While (not((a=b) and (c=d)) do Something; Do While (a<>b) or (c<>d) do Something; 2.8. OGÓLNE FUNKCJE BOOLOWSKIE
Dla określonej aplikacji, możemy stworzyć funkcję logiczną która osiąga określone wyniki. .Przypuśćmy, że potrzebujemy napisać program do symulowania wszystkich możliwych funkcji boolowskich. Na przykład, na dołączonej dyskietce ,jest program który pozwoli ci wejść do przypadkowej funkcji boolowskiej ,z jedną do czterech zmiennych. Ten program będzie odczytywał dane wejściowe i tworzył odpowiednie wyniki. Ponieważ liczba unikalnych czterech zmiennych funkcji jest duża (65,536,dokładnie) nie jest możliwe do zastosowania w praktyce zawrzeć określone rozwiązanie dla każdej jedynki w programie. Co jest konieczne dla ogólnej funkcji logicznej, po pierwsze, obliczanie wyników dla przypadkowej funkcji. Ta sekcja opisuje jak napisać taką funkcję. Ogólna funkcja boolowska dla czterech zmiennych potrzebuje pięciu parametrów. – cztery parametry wejściowe i piąty parametr który wyszczególnia funkcję do obliczenia. Podczas gdy jest wiele sposobów do wyszczególnienia funkcji do obliczeń. podamy numer funkcji boolowskiej jako piąty parametr. Na pierwszy rzut oka możemy być zdumieni ,jak można obliczyć funkcje używając numeru funkcji. Jednakże, pamiętajmy, że bity które stanowią numer funkcji pochodzą bezpośrednio z tablicy prawdy dla tej funkcji. Dlatego też ,jeśli wyciągniemy te bity z numeru funkcji, możemy zbudować tablicę prawdy dla tej funkcji .Istotnie, jeśli zaznaczymy i-ty bit z numeru funkcji, gdzie i=D*8+C*4+B*2+A,otrzymamy wynik funkcji dla poszczególnych wartości A,B,C i D. Następujące przykłady w C i Pascalu, pokazują jak napisać taką funkcję. /*******************************************************************************************/ /* */ /* Ten program w C demonstruje jak napisać ogólną funkcję logiczną, która może obliczyć dowolną funkcję */ /* czterech zmiennych. Podane operatory manipulowania bitami C wraz z heksadecymalnym I/O czynią to */ /* zadanie łatwym do wykonania w języku C */ /* */ /*******************************************************************************************/ #include #include /* Ogólna funkcja logiczna. Parametr „Func” zawiera 16 bitowy numer funkcji logicznej. Jest to w /* rzeczywistości zaszyfrowana tablica prawdy dla funkcji. Parametry a, b, c, d są danymi wejściowymi do /* funkcji logicznej . Jeśli potraktujemy ‘func’ jako tablicę bitów 2 x 2 x 2 x 2. /* ta określona funkcja wybierze bit „func [d,c,b,a] z func. int generic (int func, int a, int b, int c, int d) { /* Zwraca bit określony przez a, b, c i d */ return (func >> (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 ((x<>y) 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 1<10 do begin x:=x*1; i:=1+l; end; 28. Jak pamięć29. podręczna poprawia wydajność sekcji kodu wystawionej w przestrzennej lokalności odniesienia 30. W jakich okolicznościach pamięć podręczna nie zachowa żadnego stanu oczekiwania 31. Jaka jest skuteczna (średnia) liczba stanów oczekiwania w następujących systemach posługujących się pod: a) 80% trafień,10 stanów oczekiwania(WS) dla pamięci,0 WS dla pamięci podręcznej b) 90% trafień,7 WS dla pamięci,0 WS dla pamięci podręcznej c) 95 trafień ,10 WS pamięć,1 WS pamięć podręczna d) 50 % trafień,2 WS pamięć,0 WS pamięć podręczna 32. Jaki jest cel dwupoziomowego systemu pamięci podręcznej? Co on przechowuje? 33. Jaka jest skuteczna liczba stanów oczekiwania dla następujących systemów: a) 80 % trafień głównej pamięci podręcznej (HR) zero WS;95% pomocnicza pamięć podręczna HR z 2 WS;10 WS dla dostępu do głównej pamięci b) 50 % główna pamięć podręczna HR, zero WS,98% pomocnicza pamięć podręczna HR, jeden WS; pięć WS dla dostępu do pamięci głównej c) 95% główna pamięć podręczna HR, jeden WS;98% pomocnicza pamięć podręczna HR,4 WS;10 WS dla pamięci głównej 34. Wyjaśnij cel jednostki sprzęgającej z magistralą (BIU),jednostki wykonawczej i jednostki sterującej 35. Dlaczego zabiera więcej niż jeden cykl zegarowy wykonanie instrukcji. Podaj kilka przykładów x86 36. Jak kolejka rozkazów przechowuje czas? Podaj kilka przykładów 37. Jak przetwarzanie potokowe pozwala nam (pozornie) wykonywać jedną instrukcję na cykl zegarowy? Podaj przykład 38. Co to jest przypadek? 39. Co zdarzy się w 8486 kiedy pojawi się przypadek? 40. Jak można wyeliminować efekt przypadku?
41. Jak skok (Jmp/jcc) wpływa na: a) kolejkę rozkazów b) przetwarzanie potokowe 42. Co to jest kolejka potokowa? 43. Poza tym oczywistym pożytkiem redukowania stanów oczekiwania ,jak pamięć podręczna może poprawić wydajność systemu potokowego? 44. Co to jest Harvardzka Architektura Maszyny 45. Jak superskalarny CPU przyspiesza wykonanie? 46. Jakie są dwie główne techniki które powinniśmy użyć47. w superskalarnym CPU aby zapewnić48. wykonywanie kodu tak szybko jak to możliwe?(zauważ, są to szczegóły mechaniczne, ”lepsze algorytmy” nie liczą się tutaj) 49. Co to jest przerwanie? Jak poprawia osiągi systemu 50. Co to jest odpytywanie I/O? 51. Jaka jest różnica między memory mapped i mapped I/O 52. DMA jest specjalnym przypadkiem memory-mapped I/O. Wyjaśnij.
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
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Ł CZWARTY:ROZMIESZCZENIE W PAMIĘCI I DOSTĘP Rozdział Pierwszy omawia podstawowe formaty danych w pamięci. Rozdział Trzeci pokazuje jak system komputerowy fizycznie organizuje te dane. Ten rozdział omawia jak CPU 80x86 uzyskują dostęp do danych w pamięci. 4.0 WSTĘP Ten rozdział tworzy ważny pomost pomiędzy sekcją jeden i dwa (odpowiednio Organizacja Maszynowa i Podstawy Języka Asemblera).Z punktu widzenia organizacji maszynowej, ten rozdział omawia adresowanie pamięci, organizację pamięci ,tryby adresowania CPU i przedstawianie danych w pamięci .Z punktu widzenia programowania w jeżyku asemblera, rozdział ten omawia zbiór rejestrów 80x86,tryby adresowania pamięci 80x86 i złożone typy danych. Jest to rozdział kluczowy. Jeśli nie zrozumiemy materiału w tym rozdziale, będziemy mieli kłopoty ze zrozumieniem następnych rozdziałów. Dlatego też, przestudiujmy ten rozdział starannie zanim będziemy kontynuować. Rozdział zaczyna się od omówienia rejestrów procesorów 80x86.Te procesory są wyposażone w zbiór rejestrów ogólnego przeznaczenia, rejestry segmentowe i kilku rejestrów specjalnego przeznaczenia .Pewni członkowie rodziny ,są wyposażeni w dodatkowe rejestry, chociaż typowe aplikacje ich nie używają. Po przedstawieniu rejestrów ,rozdział opisuje organizację pamięci i segmentację w 80x86.Segmentacja jest trudną koncepcją dla wielu początkujących programistów asemblerowych. Istotnie, tekst ten stara się unikać adresowania segmentowego przez cały wstępny rozdział. Niemniej jednak. segmentacja jest potężną koncepcją która musi stać się dobrze znana komuś kto chce pisać nie trywialne programy pod 80x86. Tryby adresowania pamięci 80x86 są, być może ,najważniejszym tematem w tym rozdziale .Jeżeli nie opanujesz całkowicie używania tych trybów adresowania ,nie będziesz mógł pisać rozsądnych programów asemblerowych .Nie przechodź dalej nim zadowalająco nie zapoznasz się z trybami adresowania. Ten rozdział omawia również rozszerzone tryby adresowania z procesora 80386 (i późniejszych). Wiedza o tych trybach adresowania nie jest tak ważna ,ale jeśli się ich nauczymy, możemy używać ich kiedy piszemy kod dla procesorów 80386 i późniejszych. Rozdział ten również wprowadza garść instrukcji 80x86.Chociaż pięć lub więcej instrukcji tego rozdziału jest niewystarczających dla napisania prawdziwego programu asemblerowego ,dostarczają one wystarczającego zbioru instrukcji, który pozwala manipulować strukturą zmiennych i danych – temat następnego rozdziału. 4.1 CPU 80x86: SPOJRZENIE PROGRAMISTY Teraz ,jest czas aby omówić kilka prawdziwych procesorów:8088/8086/80188,80286 i 80386/80486/80586/Pentium.Rozdział Trzeci zajmował się dużo sprzętowymi aspektami systemu komputerowego. Te komponenty sprzętowe wpływają na sposób w jaki piszemy programy ,lecz dla CPU jest dużo więcej rzeczy niż tylko cykle magistrali i potoki. Jest czas spojrzeć na te komponenty CPU które są bardziej widoczne dla nas, programistów asemblerowych. Najbardziej widocznymi komponentami CPU jest zbiór rejestrów .Podobnie jak nasze hipotetyczne procesory ,chipy 80x86 mają zbiór rejestrów zintegrowanych na płycie .Zbiór rejestrów dla każdego procesora w rodzinie 80x86 jest nadzbiorem tamtych poprzednich CPU. Najlepszym miejsce do rozpoczęcia jest zbiór rejestrów procesorów 8088,8086,80188 i 80186 ponieważ te cztery procesory mają takie same rejestry. Przy ich omawianiu ,termin „8086” będzie sugerował każdy z tych czterech CPU.
Projektanci Intela zaklasyfikowali rejestry 8086 do trzech kategorii: rejestry ogólnego przeznaczenia, rejestry segmentowe i różnorodne rejestry .Rejestry ogólnego przeznaczenia są tymi które mogą stać się operandami arytmetycznych ,logicznych i pokrewnych instrukcji. Chociaż te rejestry są „ogólnego przeznaczenia”, każdy z nich ma swojej własne przeznaczenie. Intel używa terminu „ogólnego przeznaczenia” luźno.8086 używa rejestrów segmentowych przy dostępie do bloków pamięci nazywanych, dość niespodziewanie, segmentami. Zobacz „Segmenty w 80x86” po większą ilość szczegółów dotyczącą natury rejestrów segmentowych. Ostatnią klasą rejestrów 8086 są rejestry. Są dwa specjalne rejestry w tej grupie które omówimy wkrótce. 4.1.1 REJESTRY OGÓLNEGO PRZEZNACZENIA 8086 Jest osiem 16 bitowych rejestrów ogólnego przeznaczenia w 8086: ax,bx,cx,dx,si,di,bp i sp .Możemy używać wielu z tych rejestrów zamiennie w obliczeniach, wiele instrukcji pracuje bardziej wydajnie lub całkowicie wymaga specyficznego rejestru z tej grupy. Tyle o ogólnym przeznaczeniu. Rejestr ax (akumulator) występuje wszędzie tam gdzie mają miejsce arytmetyczne lub logiczne obliczenia. Chociaż możemy wykonywać operacje arytmetyczne i logiczne na innych rejestrach ,często bardziej wydajne jest używanie rejestru ax do takich obliczeń. Rejestr bx (bazowy) ma również kilka specjalnych przeznaczeń .Jest on powszechnie używany do przechowywani adresu pośredniego, podobnie jak rejestr bx z procesorów x86.Rejestr cx (licznik),jak wskazuje jego nazwa wskazuje służy do obliczeń .Często będziemy go używać do zliczania iteracji w pętli lub specyfikowania liczby znaków w łańcuchu. Rejestr dx (danych) ma dwa specjalne przeznaczenia: przechowuje przepełnienie z pewnych arytmetycznych operacji i przechowuje adresy I/O kiedy uzyskujemy dostęp do danych na szynie I/O w procesorze 80x86. Rejestry si i di (indeks źródłowy i indeks przeznaczenia) również mają kilka specjalnych przeznaczeń. Możemy używać tych rejestrów jako wskaźników (podobnie jak rejestr bx) do pośredniego dostępu do pamięci .Będziemy również używać tych rejestrów z instrukcjami łańcuchowymi 8086 kiedy przetwarzamy łańcuchy znaków. Rejestr bp (wskaźnik bazowy) jest podobny do rejestru bx. Ogólnie rzecz biorąc, będziemy używać tego rejestru przy uzyskaniu dostępu do parametrów i zmiennych lokalnych w procedurze. Rejestr sp (wskaźnik stosu) ma bardzo specjalne znaczenie – utrzymuje stos programu. Normalnie, nie będziemy używać tego rejestru dla obliczeń arytmetycznych. Właściwe operacje większości programów zależą od ostrożnego używania tego rejestru. Poza tymi ośmioma 16 bitowymi rejestrami ,CPU 8086 ma również 8 ośmiobitowych rejestrów .Intel nazywa te rejestry al.,ah,bl,bh,cl,ch,dl i dh. Prawdopodobnie zauważyłeś podobieństwa miedzy tymi nazwami a nazwami kilku rejestrów 16 bitowych (ax,bx,cx i dx)Te ośmiobitowe rejestry nie są niezależnymi rejestrami al reprezentuje „mniej znaczący bajt ax”,ah reprezentuje „bardziej znaczący bajt ax”. Nazwy innych ośmiobitowych rejestrów znaczą to samo dla rejestrów bx,cx i dx. Rysunek 4.1 pokazuje zbiór rejestrów ogólnego przeznaczenia. Zauważ, że ośmiobitowe rejestry nie tworzą niezależnego zbioru rejestrów. modyfikowanie al. zmieni wartość ax; więc zmodyfikuje ah .Wartość al. dokładnie odpowiada bitom od zera do siedem ax .Wartość ah odpowiada bitom od osiem do piętnaście ax .Dlatego też modyfikacja al lub ah zmodyfikuje wartość ax .Podobnie, zmodyfikowanie ax zmieni oba al. i ah .Zauważ, jednak ,że zmieniający się al. nie wpłynie na wartość ah i vice versa .Odnosi się to również do bx/bl/bh,cx.cl.ch i dx,dl,dh. Rejestry si,di,bp i sp są tylko rejestrami 16 bitowymi .Nie ma sposobu na bezpośredni dostęp do pojedynczych bajtów w tych rejestrach tak jak możemy uzyskać dostęp do młodszych i starszych bajtów rejestrów ax,bx,cx i dx.
Rysunek 4.1 Zbiór rejestrów 8086 4.1.2 REJESTRY SEGMENTOWE 8086 8086 ma cztery specjalne rejestry segmentowe: cs,ds,es i ss .Oznaczają one odpowiednio Segment Kodu, Segment Danych, Segment Extra i Segment Stosu. Te rejestry wszystkie są szerokie na 16 bitów .Zajmują się one wyselekcjonowywaniem bloków (segmentów) pamięci głównej. Rejestr segmentowy (np. cs) wskazuje początek segmentu w pamięci. Segmenty pamięci w 8086 nie mogą być dłuższe niż 65,536 bajtów .Jest to niesławne „ograniczenie segmentu do 64K” przeszkadzające wielu programistom. Zobaczymy później kilka problemów z ograniczeniem 64K,i kilka rozwiązań tego problemu. Rejestr cs wskazuje segment zawierający obecnie wykonywane instrukcje maszynowe. Zauważmy ,że pomimo ograniczenia segmentu do 64K,programy 8086 mogą być dłuższe niż 64K.Po prostu potrzebujemy większą ilość segmentów kodu w pamięci. Ponieważ możemy zmieniać wartość rejestru cs ,możemy przejść do nowego segmentu kodu kiedy chcemy wykonać kod tam umieszczony. Rejestr segmentu danych, ds., ogólnie rzecz biorąc, wskazuje na globalne zmienne programu .Znowu jesteśmy ograniczeni 65,536 bajtami danych w segmencie danych; ale zawsze możemy zmienić wartość rejestru ds. aby uzyskać dostęp do dodatkowych danych w innym segmencie. Rejestr ekstra segmentu, es ,jest dokładnie tym - rejestrem ekstra segmentu. Programy 8086 często używają tego rejestru segmentowego aby uzyskać dostęp do segmentów kiedy trudno jest lub jest niemożliwe modyfikowanie innych rejestrów segmentowych. Rejestr ss wskazuje segment zawierający stos 8086.Stos występuje wtedy kiedy przechowujemy ważne informacje stanu maszyny, powrót z podprogramu adresowania, parametry procedury i lokalne zmienne. Ogólnie rzecz biorąc, nie modyfikujemy rejestru segmentu stosu ponieważ zbyt wiele rzeczy w systemie zależy od tego. Chociaż jest teoretycznie możliwe przechowywać dane w rejestrach segmentowych, nie jest to dobry pomysł. Rejestry segmentowe mają bardzo specjalne przeznaczenie – wskazywanie dostępnych bloków pamięci. Próby używania tych rejestrów do innych celów mogą w wyniku dać wiele zmartwienia ,zwłaszcza jeśli zamierzamy przejść na lepszy CPU np. 80386
Rysunek 4.2 : Rejestr Flag 4.1.3 REJESTRY SPECJALNEGO PRZEZNACZENIA 8086 Są dwa rejestry specjalnego przeznaczenia w CPU 8086: wskaźnik instrukcji (IP) i rejestr flag .Nie uzyskamy dostępu do tych rejestrów w ten sam sposób jak do innych rejestrów 8086.Zamiast tego ,CPU manipuluje tymi rejestrami bezpośrednio. Rejestr IP jest odpowiednikiem rejestru IP procesorów x86 – zawiera adres obecnie wykonywanej instrukcji .Jest to 16 bitowy rejestr ,który zapewnia wskaźnik do bieżącego segmentu kodu (16 bitów pozwala wybrać jeden z 65,536 różnych komórek pamięci).Wrócimy do tego rejestru kiedy będziemy omawiać później transmisję sterowania instrukcjami. Rejestr flag nie jest podobny do innych rejestrów na 8086.Inne rejestry przechowują ośmio- lub 16 bitowe wartości. Rejestr flag jest po prostu zbiorem kompilacyjnych jednobitowych wartości które pomagają określić bieżący stan procesora .Chociaż rejestr flag jest szeroki na 16 bitów,8086 używa tylko dziewięciu z tych
bitów. Z tych flag ,czterech flag używamy cały czas: znacznik zera, znacznik przeniesienia, znacznik znaku i znacznik nadmiaru. Te flagi są kodami stanu 8086.Rejestr flag jest pokazany na rysunku 4.2 4.1.4 REJESTRY 80286 Mikroprocesor 80286 dodaje jedną ważną z programistycznego punktu widzenia cechę do 80286operacje trybu chronionego. Ten tekst nie obejmuje operacji trybu chronionego dla 80286 z różnych przyczyn. Po pierwsze ,tryb chroniony 80286 był kiepsko zaprojektowany. Po drugie jest on interesujący tylko dla programistów piszących swoje własne systemy operacyjne lub nisko poziomowe programy systemowe dla takiego systemu operacyjnego .Nawet jeśli piszemy oprogramowanie dla trybu chronionego systemu operacyjnego takiego jak UNIX czy OS/2 nie będziemy używać cech trybu chronionego 80286.Pomimo to jest wart zachodu do wskazywania ekstra rejestrów i stanu flag obecnych w 80286 właśnie w przypadku natknięcia się na nie. Są trzy dodatkowe bity obecne w rejestrze flag 80286.Poziom Uprzywilejowania I/O to wartość dwóch bitów (bity 12 i 13).Specyfikują jeden z czterech uprzywilejowanych poziomów potrzebnych do wykonania operacji I/O. Te dwa bity generalnie zawierają 00b kiedy użytkujemy tryb rzeczywisty na 80286 (tryb emulowany na 8086).Flaga NT (nested task – znacznik zagnieżdżonego zadania) steruje operacją instrukcji powrotu z przerwania (IRET).Normalnie NT ma zero dla programu w trybie rzeczywistym. Poza tymi ekstra bitami w rejestrze flag,80286 ma również pięć dodatkowych rejestrów używanych przez system operacyjny do wspomagania zarządzania pamięcią i przetwarzanie wieloprogramowe :rejestr stanu (msw),rejestr globalnej tablicy deskryptorów (gdtr)rejestr lokalnej tablicy deskryptorów(ldtr),rejestr tablicy deskryptorów przerwań(idtr) i rejestr stanu zadania (tr) Przy używaniu typowej aplikacji dla trybu chronionego na 80286,mamy dostęp do więcej niż jednego megabajtu RAMu .jednakże 80286 jest praktycznie przestarzały a jest lepszy sposób dostępu do większej ilości pamięci na późniejszych procesorach, programiści rzadko używają tej formy trybu chronionego. 4.1.5 REJESTRY 80386/80486 Procesor 80386 radykalnie rozszerzył zbiór instrukcji 8086.Dodatkowo wszystkie rejestry w 80286 (zatem i 8086),80386 dodał kilka nowych rejestrów i rozszerzył definicję istniejących rejestrów.80486 nie dodał żadnych nowych rejestrów do podstawowego zbioru rejestrów 80386,ale zdefiniował kilka bitów w kilku rejestrach niezdefiniowanych przez 80386. Bardzo ważną zmiana, z punktu widzenia programisty, było wprowadzenie w 80386 zbioru 32 bitowych rejestrów .Ax,bx,cx,dx,si,di,bp,sp, flagi i rejestr ip ,zostały wszystkie rozszerzone do 32 bitów.80386 nazwał te 32 bitowe wersje eax,ebx,ecx,ex,esi,edi,ebp,esp, eflagi i eip w odróżnieniu ich od ich 16 bitowych wersji (które są jeszcze dostępne w 80386) .Poza 32 bitowymi rejestrami,80386 również wprowadził dwa nowe 16 bitowe rejestry segmentowe fs i gs, które pozwalają programiście jednocześnie uzyskać dostęp do sześciu różnych segmentów w pamięci bez ładowania rejestru segmentu. Zauważ ,że wszystkie rejestry segmentowe w 80386 są 16 bitowe.80386 nie rozszerzyły rejestru segmentów do 32 bitów ,jak zrobiły to z innymi rejestrami. 80386 nie zrobił żadnych zmian w bitach w rejestrze flag. Zamiast tego rozszerzył rejestr flag do 32 bitów (rejestr eflag) i zdefiniował bity 16 i 17.Bit 16 jest znacznikiem wznowienia(RF) i używanym w zbiorze rejestrów uruchomieniowych .Bit 17 jest znacznikiem trybu V86(VM).który określa czy procesor pracuje w trybie wirtualnym V86 (symulowanym w 8086) lub standardowym trybie chronionym.80486 dodaje trzeci bit do rejestru eflag na pozycji 18 - flaga stanu wyrównania .Razem z rejestrem sterującym zero (CR0) w 80486,flaga ta wymusza pułapkę (przerwanie programu) kiedy procesor uzyskuje dostęp do nie wyrównanych danych.(np. słowo spod nieparzystego adresu lub podwójne słowo spod adresu który nie dzieli się przez cztery) 80386 dodał cztery rejestry sterujące:CR0-CR3.rejestry te rozszerzają rejestr msw 80286 (80386 emuluje rejestr msw z 80286 dla kompatybilności ,ale informacja w rzeczywistości pojawia się w rejestrach CRx)W 80386 i 80486 te rejestry sterują funkcjami takimi jak zarządzanie pamięcią stronicowaną ,włączanie/wyłączanie operacji pamięci podręcznej (tylko 80486),operacji trybu chronionego i innych. 80386/486 dodają również osiem rejestrów uruchomieniowych .Program uruchomieniowy taki jak Microsoft Codeview lub Turbo Debugger mogą używać tych rejestrów do ustawiania punktów kontrolnych ,kiedy próbujemy zlokalizować błąd wewnątrz programu. Kiedy nie będziemy używać tych rejestrów w programach użytkowych ,często odkryjemy ,że używanie takiego debuggera zredukuje czas potrzebny do wyeliminowania błędów z naszych programów. Oczywiście, debugger który uzyska dostęp do tych rejestrów będzie funkcjonował właściwie na procesorach 80386 lub późniejszych. Ostatecznie, procesory 80386/486 dodają zbiór rejestrów testujących system ,które testują stosowne operacje procesora kiedy system jest włączany. Jest prawdopodobne, że Intel dołoży te rejestry do chipu co pozwoli testować bezpośrednio po produkcji ,ale projektanci systemu mogą wykorzystać te rejestry do robienia testów po włączeniu zasilania. Przeważnie programiści asemblerowi nie muszą martwić się ekstra rejestrami dodanymi do procesorów 80386/80486/Pentium.Jednakże 32 bitowe rozszerzenie i ekstra rejestry segmentowe są całkiem
użyteczne. Dla programów użytkowych ,model programowania dla 80386/80486/Pentium wygląda jak ten pokazany na rysunku 4.3
Rysunek 4.3 Rejestry 80386 (Dostępne programiście asemblerowemu) 4.2 FIZYCZNA ORGANIZACJA PAMIĘCI 80x86 Rozdział Trzeci omawiał podstawową organizację Architektury Von Neumanna (VNA) systemów komputerowych. W typowej maszynie VNA,CPU łączy pamięć przez magistrale.80x86 wybiera kilka szczególnych elementów pamięci używając liczb binarnych na magistrali adresowej. Innym sposobem obejrzenia pamięci jest tablica bajtów .Pascalowska struktura danych, która z grubsza pokrywa się z pamięcią będzie wyglądać tak: Memory : array [0..MaxRAM] of byte; Wartość na magistrali adresowej odpowiada indeksowi dostarczonemu do tablicy. Np. zapisaniu danych do pamięci odpowiada : Memory [adres] :=Wartość_Do_Zapisania; Odczytaniu danych z pamięci odpowiada : Wartość_Odczytana := Memory [adres]; Różne CPU mają różne magistrale adresowe które sterują maksymalną liczbą elementów w tablicy pamięci (zobacz „Magistrala Adresowa”).Jednakże, bez względu na liczbę linii adresowych na magistrali ,większość komputerów nie ma jednego bajtu pamięci dla każdej adresowalnej lokacji .Na przykład, procesor 80386ma 32 linie adresowe pozwalające zwiększyć pamięć do czterech gigabajtów. Bardzo mało systemów 80386 w rzeczywistości ma cztery gigabajty .Zazwyczaj, mamy od jednego do 256 megabajtów w systemach opartych 0 80x86. Pierwszy megabajt pamięci ,od adresu zero do 0FFFFFh jest specjalny w 80x86.Odpowiada to całej przestrzeni adresowej mikroprocesorów 8088,8086,80186 i 80188.Większość programów DOS ogranicza swoje programy i adresy danych do lokacji w tym zakresie. Adresy ograniczone do tego zakresu są nazywane adresami rzeczywistymi po trybie rzeczywistym 80x86. 4.3 SEGMENTY W 80x86 Nie możemy omawiać adresowania pamięci w rodzinie procesorów 80x86 bez omówienia najpierw segmentacji. Pomiędzy innymi rzeczami, segmentacja dostarcza silnych mechanizmów zarządzania pamięcią. Pozwala to programistom dzielić swoje programy na moduły które działają niezależnie jeden od drugiego. Segmenty dostarczają sposobu łatwej implementacji programów zorientowanych obiektowo. Segmenty pozwalają dwóm procesom łatwo dzielić dane. W sumie, segmentacja jest rzeczywiście zgrabną cechą. Z drugiej strony, jeśli spytamy programistów co myślą o segmentacji, co najmniej dziewięciu na dziesięciu stwierdzi ,że to straszne. Dlaczego taka odpowiedź? Cóż, okazuje się,że segmentacja dostarcza drugiej zmyślnej cechy: pozwala nam rozszerzyć adresowalność procesora. W przypadku 80x86,segmentacja pozwoliła projektantom Intela rozszerzyć
pamięć adresowalną z 64K do jednego megabajta. Eee ..nieźle brzmi. Dlaczego wszyscy się skarżą? Cóż, krótka lekcja historii pozwoli zrozumieć co stało się złego. W 1976 roku ,kiedy Intel zaczął projektować procesor 8086,pamięć była bardzo droga .Komputery osobiste ,takie które były w tym czasie ,typowo miały cztery tysiące bajtów pamięci. Nawet kiedy IBM wprowadził PC pięć lat później,64K było jeszcze odpowiednią pamięcią. Jeden megabajt był olbrzymią ilością .Projektanci Intela uważali, że 64K pamięci pozostanie dużą wartością przez cały czas życia procesora 8086.Pomyłką jaką zrobili ,było kompletne przecenienie czasu życia procesora 8086.Doszli do wniosku, że będzie to około pięciu lat ,podobnie jak wcześniej procesora 8080.Zaplanowali mnóstwo innych procesorów przez ten czas, a „86” nie był przyrostkiem w nazwie każdego z nich .Intel doszedł do wniosku ,że będą one grupą .Z pewnością jeden megabajt byłby więcej niż wystarczający do momentu aż nie wyszliby z czymś lepszym. Niestety ,Intel nie liczył na IPM PC i ogromną ilość oprogramowania pojawiającego się dla niego. W 1983 roku, było jasne, że Intel nie może porzucić architektury 80x86.Utknęli na niej ,ale do tego czasu ludzie zaczęli protestować przeciwko jedno megabajtowemu ograniczeniu 8086.Więc Intel dał nam 80286.Ten procesor mógł adresować do 16 megabajtów pamięci. Z pewnością więcej niż dość. Problemem było tylko to,że całe cudowne oprogramowanie napisane dla IBM PC było napisane w taki sposób ,że nie mogło wykorzystywać żadnej pamięci poza jeden megabajt. Okazało się,że maksymalna ilość adresowalnej pamięci nie jest dla wszystkich główną dolegliwością. Prawdziwym problemem było to,że 8086 był procesorem 16 bitowym, z 16 bitowymi rejestrami i 16 bitowymi adresami. To ograniczało procesor do adresowania 64K kawałków pamięci. Bystrzachy z Intela użyli rozszerzonych segmentów do jednego megabajta, ale adresowanie więcej niż 64K w jednym czasie stanowiło wysiłek. Adresowanie więcej niż 256K w jednym czasie stanowiło duży wysiłek. Pomimo tego co usłyszeliśmy ,segmentacja nie jest zła. Faktycznie ,jest to naprawdę wielki program zarządzania pamięcią. Co jest złego w tym ,że Intel w 1976 zaimplementował segmentację ciągle używaną dzisiaj .Nie możemy winić Intela za to,że – przenieśli problem z 80 na 80386.Prawdziwym sprawcą jest MS-DOS, który wymusił na programistach kontynuowania używanego od 17976 roku stylu segmentacji. Na szczęście nowsze systemy operacyjne takie jak Linux, UNIX ,Windows 9x,Windows NT i OS/2 nie cierpią z tego powodu tak jak MS-DOS. Ponadto użytkownicy chętnie przechodzą na nowsze systemy operacyjne ,więc programiści mogą wykorzystywać nowe cechy rodziny 80x86. Odłóżmy lekcję historii na bok ,jest to prawdopodobnie dobra myśl rozpracować czym jest segmentacja. Rozważmy bieżący widok pamięci :wygląda ona jak liniowa tablica bajtów .Pojedynczy indeks (adres) wybiera jakiś szczególny bajt z tej tablicy .Nazwijmy ten typ adresowania liniowym lub płaskim .Adresowanie segmentowe używa dwóch komponentów do wyspecyfikowania komórki pamięci: wartość segmentu i offset (przesuniecie) wewnątrz tego segmentu. Idealnie ,wartości segmentu i offsetu są od siebie niezależne .Najlepszym sposobem opisu
Rysunek 4.4 Adresowanie segmentowe jako dwu wymiarowy proces adresowania segmentowego jest dwu wymiarowa tablica .Segment dostarcza jednego indeksu do tablicy ,offset dostarcza drugiego (zobacz rysunek 4.4) Teraz możesz się zastanawiać „Dlaczego robimy ten proces bardziej złożonym?” Adresowanie liniowe wydaje się w działaniu świetny, dlaczego zawracać sobie głowę tym dwuwymiarowym schematem adresowania? Cóż ,rozważymy sposób napisania typowego programu .Gdy napiszemy ,powiedzmy, procedurę SIN(X) i potrzebujemy jakichś zmiennych czasowych, prawdopodobnie nie użyjemy zmiennych globalnych .Zamiast tego użyjemy zmiennych lokalnych wewnątrz funkcji SIN(X).W sensie ogólnym, jest
to jedna z cech którą oferuje segmentacja – umiejętność przyłączenia bloku zmiennych (segmentu) do konkretnej części kodu. Możemy, na przykład, mieć segment zawierający lokalne zmienne dla SIN, segment dla SQRT, segment dla DRAW-Window itp. .Ponieważ zmienne dla SIN pojawiają się w segmencie dla SIN, jest mniej prawdopodobne, że nasza procedura SIN wpłynie na zmienne należące do procedury SQRT. Istotnie, w 80286 i późniejszych operujących w trybie chronionym ,CPU może uniemożliwić jednemu programowi przypadkowo zmodyfikować zmienne w różnych segmentach. Pełen adres segmentowany zawiera część segmentową i część offsetową .W tym tekście będziemy pisać adres segmentowy jako segment:offset. W 8086 przez 80286 te dwie wartości są 16 bitowymi stałymi. W 80386 i późniejszych offset może być 16 lub 32 bitową stałą. Rozmiar offsetu ograniczony jest do maksymalnego rozmiaru segmentu. W 8086 z 16 bitowym offsetem ,segment może być nie dłuższy niż 64K;może być mniejszy ale nigdy dłuższy.80386 i późniejsze procesory pozwalają 32 bitowym offsetom pracować z segmentami tak długimi jak cztery gigabajty. Części segmentu są 16 bitowe na wszystkich procesorach 80x86.Pozwala to pojedynczym programom mieć 65,536 różnych segmentów w programie. Większość programów ma mniej niż 16 segmentów (lub coś koło tego) więc nie jest to praktyczne ograniczenie. Oczywiście, pomimo faktu ,że rodzina 80x86 używa adresowania segmentowego, rzeczywista (fizyczna) pamięć podłączona do CPU jest liniową tablicą bajtów. Jest funkcja która konwertuje wartość segmentu do adresu pamięci fizycznej. Procesor wtedy dodaje offset do tego fizycznego adresu uzyskując rzeczywisty adres danych w pamięci. W tym tekście będziemy się odnosić do adresów w naszych programach jako adresów segmentowych lub adresów logicznych. Rzeczywisty adres liniowy który pojawia się na magistrali adresowej jest adresem fizycznym (zobacz rysunek 4.4). W 8086,8088,80186 i 80188 (i inne procesory operujące w trybie rzeczywistym),funkcja która odwzorowuje segment do adresu fizycznego jest bardzo prosta. CPU mnoży wartość segmentu przez szesnaście (10h) i dodaje część offsetową. Na przykład, rozważmy adres segmentowy: 1000:1F00..Konwertując go na adres fizyczny ,mnożymy wartość segmentu (1000h) przez
Rysunek 4.5 Adresowanie segmentowe w pamięci fizycznej
Rysunek 4.6 Konwertowanie adresu logicznego na adres fizyczny
szesnaście.. Mnożenie przez podstawę systemu jest bardzo łatwe .Najpierw dołączamy zero na końcu liczby. Po dodaniu zera do 1000h otrzymujemy 10000h.Dodajemy 1F00h do tego i otrzymujemy 11F00h.Więc 11F00h jest fizycznym adresem który odpowiada adresowi segmentowemu 1000:1F00h (zobacz rysunek 4.4) Ostrzeżenie: Bardzo popularnym błędem ludzi robionym, kiedy wykonują te obliczenia jest zapominanie ,że pracują w systemie heksadecymalnym nie dziesiętnym. To zaskakujące widzieć jak dużo ludzi dodaje 9+1 i otrzymuje 10h zamiast prawidłowej odpowiedzi 0Ah. Intel, kiedy zaprojektował 80286 i późniejsze procesory, nie rozszerzył adresowania przez dodanie więcej bitów do rejestrów segmentowych. Zamiast tego, zmienili funkcję CPU używaną do konwersji adresu logicznego na adres fizyczny. Jeśli piszemy kod który zależy od funkcji „mnożenia przez 16 i dodanie offsetu” nasz program będzie mógł pracować tylko na procesorach 80x86 operujących w trybie rzeczywistym i będziemy ograniczeni do pamięci jednomegabajtowej. W procesorach 80286 i późniejszych ,Intel wprowadził segment trybu chronionego .Wśród innych zmian, Intel zupełnie zreformował algorytm dla odwzorowywania segmentu w przestrzeni adresów liniowych .Zamiast używania funkcji (takiej jak mnożenie wartości segmentu przez 10h),tryb chroniony procesorów używa tablicy odwzorowań do obliczenia adresu fizycznego. W trybie chronionym,80286 i późniejsze procesory używają wartości segmentu jako indeksu wewnątrz tablicy .Zawartość wyselekcjonowanego elementu tablicy dostarcza (między innymi) adres startowy dla segmentu. CPU dodaje tą wartość do offsetu uzyskując adres fizyczny.(zobacz rysunek 4.4). Zauważ ,że nasze aplikacje nie mogą bezpośrednio modyfikować tablicy deskryptora segmentów .Tryb chroniony systemów operacyjnych (UNIX,Linux,Windows,OS/2,itp.) posługują się tą operacją.
Rysunek 4.7 Konwertowanie adresu logicznego na fizyczny w trybie chronionym Najlepsze programy nigdy nie zakładają ,że segment jest ulokowany w szczególnym miejscu pamięci. Powinniśmy zostawić to systemowi operacyjnemu, który umieści nasz program w pamięci i nie generować żadnego własnego adresu segmentowego. 4.4 ZNORMALIZOWANE ADRESY W 80x86 Kiedy pracujemy w trybie rzeczywistym, występuje interesujący problem. Możemy odnosić się do pojedynczego obiektu w pamięci używając kilku różnych adresów. Rozważmy adres z poprzedniego przykładu,1000:1F00h.Jest kilka różnych adresów pamięci, które odnoszą się do tego samego adresu fizycznego. Na przykład,11F0:0,1100:F00 a nawet 1080:1700 wszystkie odpowiadają adresowi fizycznemu 11F00h.Kiedy pracujemy z pewnymi typami danych a zwłaszcza kiedy porównujemy wskaźniki, jest dogodnie jeśli adresy segmentowe wskazują różne obiekty w pamięci kiedy ich reprezentacje bitowe są różne. Wyraźnie nie jest to przypadek w trybie rzeczywistym procesorów 80x86. Na szczęście jest łatwy sposób uniknięcia tego problemu .Jeśli musimy porównać dwa adresy dla (nie) równości ,możemy użyć adresów znormalizowanych. Adresy znormalizowane przyjmują specjalną postać ,przez co wszystkie są unikalne .To znaczy ,o ile dwie znormalizowane wartości segmentów nie są dokładnie takie same, nie wskazują na ten sam obiekt w pamięci. Jest wiele różnych sposobów (16 faktycznie) do stworzenia adresów znormalizowanych. Przez konwencję ,większość programistów (i języków wysokiego poziomu) definiuje adres znormalizowany jak następuje:
• •
Część segmentowa adresu może być każą 16 bitową wartością Część offsetowa musi być wartością z zakresu 0..0Fh
Wskaźniki znormalizowane ,które przyjmują taką formę są bardzo łatwo konwertowane na adres fizyczny.. Wszystko co musimy zrobić to dołączyć pojedynczą cyfrę heksadecymalną z offsetu do wartości segmentu. Postać znormalizowana z 1000:1F00 to 11F0:0.Możemy otrzymać adres fizyczny poprzez dodanie offsetu (zero) na końcu 11F0 otrzymując 11F00. Jest to bardzo łatwy sposób konwersji dowolnej wartości segmentowej do adresu znormalizowanego. Po pierwsze konwertujemy nasz adres segmentowy do adresu fizycznego używając funkcji „mnożenie przez 16 i dodania w offsecie” Potem dajemy dwukropek między ostatnie dwie cyfry pięciocyfrowego wyniku: 1000:1F00 =>11F00=>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
teraz na kilka określonych przykładów: CharArray char 128 dup (?) IntArray integer 8 dup (?) BytArray byte 10 dup (?) PtrArray dword 4 dup (?)
;array[0..127] of char ,array[0..7] of integer ;array[0..9] of byte ;array[0..3] of dword
Pierwsze dwa przykłady zakładają, że użyliśmy wyrażenia typedef do zdefiniowania typu danych char i integer. Te wszystkie definicje alokują pamięć dla nie zainicjowanych zmiennych. Możemy również wyspecyfikować które elementy tablicy będą zainicjowane pojedynczą wartością używając deklaracji podobnej do następującej: RealArray real4 8 dup (1.0) IntegerArray integer 8 dup (1) Obie te definicje tworzą tablice z ośmioma elementami. Pierwsza definicja inicjuje każdą czterobajtową wartość rzeczywistą 1.0,druga deklaracja inicjuje każdy element całkowity jedynką. Ten mechanizm inicjacji jest spoko jeśli chcemy mieć każdy element tablicy o tej samej wartości. A co jeśli chcemy zainicjować każdy element tablicy różnymi wartościami? Cóż ,jest to również łatwe do wykonania. Deklaracja wyrażeń zmiennych jest taka sama jaką kiedyś widzieliśmy lecz z innym typem formy inicjacji nazwa type wartość1,wartość2,wartość3, -,wartośćn Ta forma alokuje n zmiennych typu type .Inicjuje pierwszą pozycję wartością1,drugą pozycję wartością2,itd.Wiec po przez proste wypisanie każdej wartości w polu operandu, możemy stworzyć tablicę z pożądanymi wartościami inicjującymi. W następującej tablicy całkowitej, na przykład, każdy element zawiera kwadrat z jego indeksu: Kwadraty integer 0,1,4,9,16,25,36,49,64,81,100 Jeśli nasza tablica ma więcej elementów niż mieści się w jednej linii, jest kilka sposobów na kontynuowanie tablicy w następnej linii .Najprostszą metodą jest użycie innej instrukcji całkowitej ale bez etykiety: Kwadraty integer 0,1,4,9,16,25,36,49,64,81,100 integer 121,144,169,196,225,256,289,324 integer 361,400 Inna opcją, która jest lepsza w danej sytuacji jest użycie lewego ukośnika (backslash) na końcu każdej linii, co powie MASMowi 6.x,żeby kontynuował czytanie od następnej linii.: Kwadraty integer 0,1,4,9,16,25,36,49,64,81,100, \ 121,144,169,196,225,256,289,324, \ 361,400 Oczywiście, jeśli nasza tablica zawiera kilka tysięcy elementów ,wypisywanie ich wszystkich nie byłoby raczej zabawne. Większość tablic inicjuje w ten sposób nie więcej niż parę setek danych, ale generalnie, dużo mniej niż 100. Musimy nauczyć się jednej głównej techniki inicjacji jednowymiarowej tablicy przed pójściem dalej. Rozważmy następującą deklarację: BigArray word 256 dup (0,1,2,3) Ta tablica ma 1024 elementy ,nie 256.Operand n dup (xxxx) mówi MASMowi aby powielił xxxx n razy ,nie tworzył tablicy z n elementami. Jeśli xxxx składa się z pojedynczej pozycji, wtedy operator dup stworzy tablice n-elementową. Jednak, jeśli xxxx składa się z dwóch pozycji oddzielonych przecinkiem, operator dup stworzy tablicę z 2*n elementami. Jeśli xxxx składa się z trzech pozycji oddzielonych przecinkami, operator dup tworzy tablice z 3*n pozycjami i tak dalej .Ponieważ mamy cztery pozycje w nawiasach powyżej ,operator dup tworzy 256*4 ,lub 1024 pozycji w tablicy. Wartości w tablicy będą inicjowane przez 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3. Zobaczymy ,wiele możliwości operatora dup kiedy przyjrzymy się wielowymiarowym tablicom trochę później. 5.6.1.2 UZYSKIWANIE DOSTĘPU DO ELEMENTÓW TABLICY JEDNOWYMIAROWEJ Przy uzyskiwaniu dostępu tablicy zero-based ( opartej o zero ) możemy użyć uproszczonej formuły: Adres_Elementu=Adres_Bazowy+Indeks*Rozmiar_Elementu Dla pola Adres_Bazowy możemy użyć nazwy tablicy (ponieważ MASM kojarzy adres pierwszego operandu z etykietą). Pole Rozmiar_Elementu jest liczbą bajtów każdego elementu tablicy. Jeśli obiektem jest tablica bajtów, pole Rozmiar_Elementu wynosi jeden (prowadzi do tego bardzo proste obliczenie).Jeśli każdy element tablicy jest słowem (lub wartością całkowitą ,lub innym dwu bajtowym typem) wtedy Rozmiar_Elemetu wynosi dwa. I tak dalej. Dla uzyskania dostępu do elementów tablicy Kwadraty z poprzedniej sekcji, użyjemy następującej formuły: Adres_Elementu+Kwadraty+indeks*2
80x86 koduje równoważnik instrukcji AX:=Kwadraty[indeks] jako mov bx ,indeks add bx, bx ;sprytny sposób obliczenia 2*bx mov ax, Kwadraty[bx] Są dwie ważne rzeczy do zapamiętania. Przede wszystkim, ten kod używa instrukcji add zamiast instrukcji mul do obliczenia 2*indeks.Głównym powodem dla wybrania add jest to, że jest bardziej dogodna (pamiętamy, że mul nie pracuje ze stałymi i działa tylko na rejestrze ax)Zresztą add jest dużo szybsza niż mul na wielu procesorach, ale ponieważ prawdopodobnie nie wiemy tego, czy był to wybór właściwy tej instrukcji Drugą rzeczą do zapamiętania o tej sekwencji instrukcji jest to, że niezbyt wyraźnie oblicza sumę adresu bazowego i indeksu razy dwa. Faktycznie stosuje tryb adresowania indeksowego do pośredniego obliczania tej sumy. Instrukcja mov ax, Kwadraty[bx] ładuje ax z lokacji Kwadraty+bx, która jest adresem bazowym plus indeks * 2 (ponieważ bx zawiera indeks*2).Moglibyśmy użyć lea ax, Kwadraty add bx, ax mov ax,[bx] w miejsce ostatniej instrukcji, ale dlaczego używamy trzech instrukcji gdzie jedna zrobi tą samą pracę? Jest to dobry przykład dlaczego powinniśmy znać tryby adresowania „od podszewki” .Wybierając właściwy tryb adresowania możemy zredukować rozmiar naszego programu, tym samym go przyspieszając. Tryb adresowania indeksowego w 80x86 jest naturalnym dla uzyskiwaniu dostępu do elementów tablicy jednowymiarowej. Istotnie, jego składnia nawet sugeruje dostęp do tablicy .Jedną rzeczą do zapamiętania jest to ,że musimy pamiętać o pomnożeniu indeksu przez rozmiar elementu .Nieprawidłowe wykonanie da w efekcie niepoprawny wynik. Jeśli używamy 80386 lub późniejszych CPU, możemy wykorzystać tryb adresowania indeksowego ze skalowaniem do przyspieszenia uzyskania dostępu do elementów tablicy. Rozważmy następujące instrukcje: mov ebx, indeks ;zakładamy 32 bitową wartość mov ax, Kwadraty [ebx*2] To zmniejsza wykonywanie programu o 2 instrukcje .Zobaczymy wkrótce ,że dwie instrukcje są niekoniecznie szybsze niż trzy instrukcje ,ale mam nadzieję że rozumiesz o co chodzi .Znajomość trybów adresowania może nam z pewnością pomóc. Przed przejściem do tablic wielowymiarowych, parę dodatkowych punktów o trybach adresowania i tablicach. Powyższa sekwencja pracuje dobrze jeśli chcemy uzyskać dostęp do pojedynczego elementu z tablicy Kwadraty. Jednak, jeśli chcemy uzyskać dostęp do kilku różnych elementów z tablicy w środku krótkiej sekcji kodu i możemy sobie pozwolić „na utratę” innego rejestru dla tej operacji, możemy skrócić nasz kod i, być może, przyspieszyć go. Instrukcja mov ax, Kwadraty[bx] jest długa na cztery bajty (zakładając ,że potrzebujemy dwu bajtowego przemieszczenia do przechowania offsetu Kwadratów w segmencie danych)Możemy zredukować to do instrukcji dwu bajtowej poprzez użycie trybu adresowania bazowego indeksowanego jak następuje: lea bx, Kwadraty mov si, indeks add si, si mov ax, [bx][si] Teraz bx zawiera adres bazowy a si zawiera wartość indeks*2.Oczywiście,zastąpiło to pojedynczą cztero bajtową instrukcję trzy bajtową i dwu bajtową instrukcją, bardzo dobra zamiana. Jednakże ,nie musimy załadowywać bx adresem bazowym Kwadratów dla następnego dostępu .Następująca sekwencja jest o jeden bajt krótsza niż porównywalna sekwencja która nie ładuje adresu bazowego do bx: lea bx ,Kwadraty mov si indeks add si ,si mov ax,[bx] [si] ;Założenie :Bx jest zostawione w spokoju ;bezpośrednio w tym kodzie mov si,indeks2 add si, si mov cx, [bx][si] Oczywiście lepszy dostęp do Kwadratów uzyskamy bez załadowywania do bx ,będziemy mieli większe oszczędności .Lekko skomplikowana sekwencja kodu takiego jak ten czasami opłaci się pokaźnie .Jednak
oszczędności zależą wyłącznie od tego jakiego procesora używamy. Sekwencja kodu, która działa szybko na 8086 może rzeczywiście pracować wolniej na 80486 (i vice versa).Niestety, jeśli chodzi nam o szybkość to nie ma żadnych twardych, szybkich reguł .Faktycznie jest bardzo trudno przewidzieć szybkość większości instrukcji na prostym 8086,a nawet na procesorach takich jak 80486 i Pentium/80586 które oferują przetwarzanie potokowe, zintegrowaną pamięć podręczną a nawet operacje superskalarne. 5.6.2 TABLICE WIELOWYMIAROWE Sprzęt 80x86 może łatwo obsługiwać jednowymiarowe tablice. Niestety, nie ma magicznego trybu adresowania pozwalającego nam łatwo uzyskiwać dostęp do elementów tablic wielowymiarowych. To wymaga sporej pracy i mnóstwa instrukcji. Przed omówieniem jak zadeklarować lub uzyskać dostęp do tablic wielowymiarowych, będzie dobrym pomysłem wykombinować jak zaimplementować je w pamięci. Pierwszy problem to wymyślić jak przechować wielowymiarowy obiekt w jednowymiarowej przestrzeni pamięci. Rozważmy przez chwile tablicę Pascalowską w postaci A:array[0..3,0..3] of char. Ta tablica zawiera 16 bajtów zorganizowanych w 4 wiersze po cztery znaki. Jakoś musimy znaleźć związek z każdym z 16 bajtów w tej tablicy i 16 sąsiadującymi bajtami w pamięci głównej. Rysunek 5.2 pokazuje jeden sposób zrobienia tego. Rzeczywiste mapowanie nie jest ważne tak długo aż nie wydarzą się dwie rzeczy: (1) każdy element odwzorowuje unikalną komórkę pamięci (to znaczy, żadne dwa wejścia w tablicy nie uzyskują dostępu do tej samej komórki pamięci) i (2) odwzorowywanie jest spójne. To znaczy, dany element w tablicy zawsze odwzorowywuje tą samą komórkę pamięci. Więc to czego potrzebujemy to funkcji z dwoma parametrami wejściowymi (wiersze i kolumny) która wstawi offset do szesnastobitowej liniowej tablicy. Teraz każda funkcja która wypełni powyższe ograniczenia będzie pracowała dobrze. Istotnie moglibyśmy losowo wybierać odwzorowywanie tak długo jak byłoby unikalne .Jednakże, to co rzeczywiście chcemy odwzorować to wydajne obliczanie czasu wykonania i praca dla każdego rozmiaru tablicy (nie tylko 4x4 lub nawet ograniczenie do dwóch wymiarów).Podczas gdy jest duża liczba możliwych funkcji
Rysunek 5.2: Odwzorowanie tablicy 4x4 w pamięci
Rysunek 5.3: Rzędowe pozycjonowanie elementów które odpowiednio to wyliczają, są dwie funkcje, których używa większość programistów i języków wysokiego poziomu: Rzędowe pozycjonowanie elementów i kolumnowe pozycjonowanie elementów 5.6.2.1Rzędowe pozycjonowanie elementów Rzędowe pozycjonowanie elementów przydziela następujące po sobie elementy, przesuwając wzdłuż wierszy a potem w dół kolumn, do następujących po sobie komórek pamięci. Odwzorowywanie jest najlepiej pokazane na rysunku 5.3. Rzędowe pozycjonowanie elementów jest metodą stosowaną przez większość języków wysokiego poziomu, takich jak Pascal,C,Ada,Modula-2 itp. .Łatwo jest zaimplementować i łatwo użyć języka maszynowego (zwłaszcza w debuggerze takim jak CodeView).Konwersja ze struktury dwuwymiarowej do tablicy liniowej jest bardzo intuicyjne.
Rysunek 5.4:Inny widok rzędowego pozycjonowania elementów dla tablicy 4x4 Zaczynamy od pierwszego wiersza (wiersz numer zero) a potem łączymy drugi wiersz do jego końca. Potem łączymy trzeci wiersz do końca listy, potem czwarty wiersz itd. (zobacz rysunek 5.4) Dla tych którzy lubią myśleć pod kątem kodu programu, następująca zagnieżdżona pętla Pascalowska również demonstruje jak pracuje Rzędowe pozycjonowanie elementów :
index:=0; for colindex := 0 to 3 do for rowindex:= 0 to 3 do begin memory[index]:=rowmajor[colindex][rowindex]; index:=index+1; end; Ważną rzeczą do zapamiętania z tego kodu jest to, ,że indeks najbardziej na prawo wzrasta najszybciej .Jest tak ,ponieważ alokujemy kolejne komórki pamięci, następuje przyrost indeksu najbardziej na prawo aż do momentu kiedy dotrzemy do końca bieżącego wiersza. Po dotarciu do końca, przestawiamy indeks z powrotem na początek wiersza i zwiększamy następny sąsiadujący indeks o jeden (to znaczy przechodzimy w dół do następnego wiersza).Działa to równie dobrze dla każdej liczby wymiarów .Następujący Pascalowski segment demonstruje rzędowe pozycjonowanie elementów dla tablicy 4x4x4: Index:=0; for depthindex ;=0 to 3 do for colindex:=0 to 3 do for rowindex:=0 to 3 do begin memory[index]:=rowmajor [depthindex][colindex][rowindex]; index:=index+1; end; Rzeczywista funkcja która konwertuje listę wartości indeksów do offsetu nie wymaga pętli lub dużych wyliczeń .Istotnie, jest drobna modyfikacja formuły dla obliczenia adresu elementu jednoelementowej tablicy. Formuła oblicza offset dla dwu wymiarowej tablicy rzędowego pozycjonowanie elementów zadeklarowanej jako A:array[0..3,0..3] of integer. Adres_Elementu=Adres_Bazowy+(colindex*row_size+rowindex)*Rozmiar_Elementu Jak zwykle ,Adres_Bazowy jest to adres pierwszego elementu tablicy (A[0][0] w tym przypadku) a Rozmiar_Elementu jest rozmiarem pojedynczego elementu tablicy, w bajtach. Colindex jest indeksem lewym ,rowindex jest indeksem prawym w tablicy .Row_size jest liczbą elementów w jednym wierszu (cztery w tym przypadku, ponieważ każdy wiersz ma cztery elementy).Zakładając ,Rozmiar_Elementu jeden, ta formuła oblicza następujący offset z adresu bazowego:
Dla trzy wymiarowej tablicy, formuła obliczania offsetu w pamięci jest następująca: Adres=Baza+((depthindex*col_size+colindex)*row_size+row_index)*Rozmiar_Elementu
Col_size jest to liczba pozycji w kolumnie, row_size jest liczbą pozycji w wierszu. W Pascalu ,jeśli zadeklarujemy tablicę jako „A:array[i..j][k..l[[m..n] of type, formuła obliczania adresów elementów tablicy to: Adres=Baza+((LeftIndex*depth_size+depthindex)*col_size+colindex)*row_size+rowindex)*Rozmiar_Elementu. Depth_size równa się i-j+1,col_size i row_size są takie same jak wcześniej. Lewy indeks przedstawia wartość indeksu lewego. Teraz zobaczymy wzór. Jest to ogólna formuła, która obliczy offset w pamięci dla tablicy o różnych wymiarach,jednak rzadko będziemy używać więcej niż cztery wymiary. Innym dogodnym sposobem myślenia o tablicach rzędowych jest tablica tablic. Rozważmy następującą definicję jedno wymiarową tablicy: A:array [0..3] of sometype; Zakładamy, że sometype jest typem „sometype=array[0..3] of char”. A jest jednowymiarową tablicą.Jej pojedyncze elementy to tablice, ale możemy śmiało zignorować je na razie. Formuła do obliczania adresu elementów jednowymiarowej tablicy to: Adres_Elementu=Baza+Index*Rozmiar_Elelmentu W tym przypadku Rozmiar_Elementu wydaje się być cztery ponieważ każdy element z A jest tablicą czterech znaków .Więc co ta formuła oblicza? Oblicza adres bazowy każdego wiersza w tej tablicy znaków 4x4.(zobacz rysunek 5.5). Oczywiście, raz obliczywszy adres bazowy wiersza, możemy ponownie obliczyć jednowymiarową formułę aby otrzymać adres poszczególnego elementu. Podczas gdy nie wpływa to na obliczenia znacznie, jest prawdopodobnie trochę łatwiej zająć się kilkoma jednowymiarowymi obliczeniami zamiast obliczeniami adresów elementów złożonej wielowymiarowej tablicy.
Rysunek 5.5: Obraz tablicy 4x4 jako Tablica tablic Rozważmy pascalowską tablicę zdefiniowana jako: type OneD = array [0..3] of char; TwoD = arra [0..3] of OneD; ThreeD = array [0..3] of TwoD; FourD - array [0..3] of ThreeD; var A:array [0..3] of FourD; OneD jest rozmiaru czterech bajtów. Ponieważ TwoD zawiera cztery tablice OneD, jej rozmiar to 16 bajtów. Podobnie, ThreeD to cztery TwoD, więc jest długa na 64 bajty. W końcu, FourD to cztery ThreeD, więc jest długa na 256 bajtów. Do obliczania adresu „A[b][c][d][e][f]” możemy użyć sekwencji następujących kroków: *Obliczamy adres A[b] jako „Baza+b*rozmiar”.Gdzie rozmiar to 256 bajtów. Użyjemy tego wyniku jako nowego adresu bazowego w następnym obliczeniu. *Obliczamy adres A[b][c] z formuły „Baza+c*rozmiar”, gdzie Baza jest wartością uzyskaną
bezpośrednio powyżej a rozmiar to 64.Użyjemy tego wyniku jako nowej bazy w następnym obliczeniu. *Obliczamy adres A[b][c][d] przez „Baza+d*rozmiar” z Bazą pochodzącą z obliczenia powyżej a rozmiar wynosi 16. *Obliczamy adres A[b][c][d][e] z formuły „Baza+e*rozmiar” z Bazą z powyższego obliczenia i rozmiarem cztery. Używamy tej wartości jako bazy dla następnego obliczenia. *W końcu obliczamy adres A[b][c][d][e][f] używając formuły „Baza+f*rozmiar” gdzie baza pochodzi z powyższego obliczenia a rozmiar to jeden (oczywiście możemy po prostu zignorować końcowe mnożenie)Wynik jaki otrzymaliśmy w tym miejscu jest adresem pożądanego elementu Nie tylko ten schemat jest łatwiejszy do wykonania niż powyższe formuły, lecz także jest łatwiej obliczyć (używając pojedynczej pętli) równie dobrze. Przypuścmy, że mamy dwie tablice zainicjowane jak następuje A1={256,64,16,4,1} i A2={b,c,d,e,f} Wtedy kod pascalowski wykona obliczenia adresów elementów : for i:=0 to 4 do base:=base+A1[i]*A2[i]; Przypuszczalnie baza zawiera adres bazowy tablicy przed wykonaniem tej pętli. Zauważmy, że możemy łatwo rozszerzyć ten kod do każdej liczby wymiarów poprzez proste ,właściwe, inicjowanie A1 i A2 i zmieniającą się wartość końcową pętli.
Rysunek 5.6 Kolumnowa organizacja elementów Okazuje się ,że kosztowne obliczenia dla pętli takiej jak ta jest zbyt wspaniałe do rozwiązania w praktyce. Moglibyśmy użyć tylko algorytmu takiego jak ten, jeśli potrzebowalibyśmy wyspecyfikować liczbę wymiarów podczas wykonywania programu.. Istotnie, jednym z głównych problemów, których nie znajdziemy w wysoko wymiarowych tablicach w asemblerze jest to, że asembler wyświetla nie prawidłowości związane z tym adresem .Łatwo jest wprowadzić coś takiego jak „A [b,c,d,e,f] do programu pascalowskiego, nie zdając sobie sprawy co kompilator robi z kodem. Programiści asemblerowi nie są tak nonszalanccy - widzą bałagan który się wprowadza kiedy używają wysoko wymiarowych tablic. Dobry asemblerowy programista próbuje unikać dwu wymiarowych tablic i często ucieka się do sztuczek żeby uzyskać dostęp do danych w takiej tablicy kiedy jej użycie staje się absolutną koniecznością. Ale więcej o tym trochę później. 5.6.2.2 Kolumnowa organizacja elementów Kolumnowa organizacja elementów jest inną funkcją często używaną do obliczania adresu elementu tablicy.
FORTRAN i różne dialekty BASICa (np. Microsoft) używają tej metody do indeksowania tablic. W rzędowej organizacji elementów indeks najbardziej na prawo wzrasta szybciej ponieważ przenosimy bezpośrednio kolejne komórki pamięci. W kolumnowej organizacji elementów indeks najbardziej na lewo wzrasta szybciej. Obrazowo, kolumnowa organizacja elementów tablicy jest zorganizowana tak jak pokazano na rysunku 5.6 Formuła do obliczania adresu elementu tablicy kiedy używamy kolumnowej organizacji elementów jest bardzo podobna do tej dla rzędowej organizacji elementów. Po prostu odwracamy indeksy i rozmiary w obliczeniach: Dla dwu wymiarowej kolumnowej tablicy: Adres_Elemnetu=Adres_Bazowy+(rowindex*col_size+colindex)*Rozmiar_Elementu Dla trzy wymiarowej kolumnowej tablicy: Adres=Baza+((rowindex*col_size+colindex)*depth_size_depthindex)*Rozmiar_Elementu Dla cztero wymiarowej kolumnowej tablicy: Adres= Baza+(((rowindex*col_size+colindex)*deoth_size+depthindex)*Left_size+Leftindex)*Rozmiar_Elementu Pojedyncza pętla pascalowska utrzymuje dostęp do rzędowej tablicy pozostający niezmienny (dostęp do A [b][c][d][e][f]): for i := 0 to 4 do base:=base+A1[i]*A2[i]; Podobnie, wartość inicjująca tablicy A1 pozostaje niezmienna: A1={256,64,16,4,1} Jedynie rzeczą którą musimy zmienić jest wartość inicjująca tablicy A2, a wszystko co musimy tu zrobić to odwrócić porządek indeksów: A2={f,e,d,c,b} 5.6.2.3 PRZYDZIELANIE PAMIĘCI DLA TABLIC WIELOWYMIAROWYCH Jeśli mamy tablicę m. x n, będziemy mieli m.*n elementów i wymagane m.*n*Romiar_Elemntu bajtów pamięci. Przydzielając pamięć dla tablicy musimy zarezerwować taką ilość pamięci .Jak zwykle jest kilka różnych sposobów zrealizowania tego zadania. Ten tekst spróbuje wykonać to tak aby było łatwo odczytać i zrozumieć nasze programy. Zastanówmy się ponownie nad operatorem dup dla rezerwowania pamięci. n dup (xxxx) powtarza xxxx n razy .Jak widzieliśmy wcześniej, ten operator dup pozwala nie tylko jedną, ale kilka pozycji w nawiasach powielić określoną ilość razy. Faktycznie, operator dup pozwala na pracę z tym co możemy spodziewać się znaleźć w polu operandu wyrażenia bajtowego wliczając w to dodatkowe wystąpienia operatora DUP. Rozważmy następującą instrukcję: A byte 4 dup (4 dup(?)) Pierwszy operator dup powtarza wszystko w środku nawiasów cztery razy. Wewnątrz nawiasów operacja 4 DUP (?) mówi MASMowi aby zarezerwował miejsce w pamięci dla czterech bajtów. Cztery kopie czterech bajtów dają 16 bajtów, liczbę konieczną dla tablicy 4x4.Oczywiście,rezerwowanie pamięci dla tej tablicy może być łatwiejsze przez użycie instrukcji A byte 16 dup (?) Inny sposobem w asemblerze jest zarezerwowanie 16 sąsiednich bajtów w pamięci. Jeśli chodzi o 80x86,nie ma różnic między tymi dwoma formami. Z drugiej strony, dawne wersje dostarczały lepszego znaku, że A to tablica 4x4 niż późniejsze wersje. Późniejsze wersje wyglądają jak jednowymiarowa tablica z szesnastoma elementami. Możemy bardzo łatwo rozszerzyć to pojęcie tablic z większą liczbę argumentów operatora. Deklaracja dla trójwymiarowej tablicy, A;array[0..2,0..3,0..4] of integer mogłaby być A integer 3 dup (4 dup (5 dup (?))) (oczywiście, potrzebujemy instrukcji integer typedef word w naszym programie dla tego wykonania) Ponieważ był już przypadek jednowymiarowych tablic, możemy zainicjować każdy element tablicy specyficzną wartością poprzez zastąpienie znaku zapytania jakąś konkretną wartością. Na przykład, inicjacja powyższej tablicy, żeby każdy element zawierał jedynkę, użyjemy kodu A integer 3 dup (4 dup (5 dup (1)))) Jeśli chcemy zainicjować każdy element tablicy różnymi wartościami ,musimy wprowadzić każdą wartość indywidualnie. Jeśli rozmiar wiersza jest dosyć mały ,najlepszym sposobem wykonania tego zadania jest umieszczenie danych dla każdego wiersza tablicy w osobnej linii. Rozważmy następującą deklarację tablicy 4x4: A integer 0,1,2,3 integer 1,0,1,1 integer 5,7,2,2
integer 0,0,7,6 Znowu asembler nie troszczy się jak podzieliliśmy linie, ale powyższe opis jest dużo łatwiejszy do identyfikacji jako tablica 4x4 jeśli zapiszemy to jako: A integer 0,1,2,3,1,0,1,1,5,7,2,2,0,0,7,6 Oczywiście, jeśli mamy dużą tablicę, tablicę z prawdziwie długimi wierszami lub tablicę wielowymiarową, jest mała nadzieja że wsadzisz tam coś mądrego. 5.6.2.4 DOSTĘP DO ELEMENTÓW WIELOWYMIAROWEJ TABLICY W JĘZYKU ASSEMBLERA Cóż, widzieliśmy już formuły dla obliczania adresów elementów tablicy. Patrzyliśmy nawet na kod pascalowski, jaki możemy użyć przy dostępie do elementów wielowymiarowej tablicy. Teraz nadszedł czas zobaczyć jak uzyskujemy dostęp do elementów tych tablic przy użyciu języka asemblera. Instrukcje mov, add i mul mało pracują z różnymi równaniami, które obliczają offset tablicy wielowymiarowej .Rozpatrzmy najpierw tablicę dwu wymiarową : ;Uwaga: rozmiar wiersza TwoD to 16 bajtów. TwoD i j
integer 4 dup (8 dup (?)) integer ? integer ? ;wykonujemy operację TwoD[i,j]:=5 używając kodu: mov ax ,8 ;8 elementów w wierszu mul i add ax,j add ax, ax ;mnożenie przez rozmiar elementu (2) mov bx, ax ;włożenie do rejestru który używamy mov TwoD [bx],5 Oczywiście, jeśli mamy chip 80386 (lub lepszy) możemy użyć następującego kodu: mov eax,8 mul i add ax, j mov TwoD[eax*2],5 Zauważmy, że ten kod nie wymaga użycia dwóch rejestrów w trybie adresowania na 80x86.Chociaż tryb adresowania taki jak TwoD[bx][si] wygląda tak jak powinien być naturalny dostęp do dwuwymiarowych tablic, choć nie jest to cel tego trybu adresowania. Teraz rozpatrzmy drugi przykład, który używa trójwymiarowej tablicy: ThreeD integer 4 dup (4 dup (4 dup (?))) i integer ? j integer ? k integer ? ;wykonamy operację ThreeD[i,j,k]:=1,używamy kodu: mov bx, 4 ;4 elementy w kolumnie mov ax,1 mul bx add ax, j mul bx ;4 elementy w wierszu add ax, k add ax,ax ;mnożenie przez rozmiar elementu (2) mov bx,ax ;włożenie do rejestru którego używamy mov ThreeD [bx],1 Oczywiście, jeśli mamy procesor 80386 lub lepszy, możemy to wykonać poprzez użycie następującego kodu:
mov mov mul add mul add mov
ebx ,4 eax, ebx i ax, j bx k ThreeD[eax*2],1
5.6.3 STRUKTURY Druga główną zbiorową strukturą danych jest pascalowski rekord lub struktura z C. Terminologia Pascalowska jest prawdopodobnie lepsza ,ponieważ jest tendencja do unikania zamieszania z ogólną terminologią struktur danych. Jednak, MASM używa nazwy „struktury” więc nie ma sensu odstępować od tego .Ponadto MASM używa terminu rekord do określenia jakichś drobnych różnic, kolejny powód do trzymania się jednej terminologii . Podczas gdy tablice są homogeniczne, czyli elementy są tego samego typu, elementy w strukturze mogą być różnych typów. Tablice pozwalają nam wyselekcjonować poszczególne elementy poprzez indeks całkowity. W strukturach, musimy wybrać element (znany jako pole) poprzez nazwę. Celem struktury jest pozwolić nam na hermetyzowanie różnych, ale logicznie pokrewnych, danych wewnątrz jednego pakietu. Deklaracja pascalowskiego rekordu dla studenta jest prawdopodobnie najlepszym przykładem: student = rekord Name:string[64]; Major: integer; SSN: string[11]; Midterm1: integer; Midterm2: integer; Final: integer; Homework: integer; Projects: integer; end; Większość pascalowskich kompilatorów alokuje każde pole rekordu w sąsiadujących komórkach pamięci. To znaczy, że Pascal zarezerwuje pierwsze 65 bajtów dla Name, następne dwa bajty zatrzyma dla Major, następne 12 bajtów dla SSN itp. W asemblerze, możemy również stworzyć typ strukturalny używając instrukcji struct MASMa .Możemy zakodować powyższy rekord w asemblerze jak następuje: student struct Name char 65 dup (?) Major integer ? SSN char 12 dup (?) Midterm1 integer ? Midterm2 integer ? Final integer ? Homework integer ? Projects integer ? student ends
Rysunek 5.7: Przechowywanie w pamięci struktury danych Student
Zauważmy, że strukturę kończymy instrukcją ends (od end structure).Etykieta w instrukcji ends musi być taka sama jak w instrukcji struct. Nazwy pól wewnątrz struktury muszą być unikalne. To znaczy, ta sam nazwa nie może występować dwa lub więcej razy w tej samej strukturze .jednak, wszystkie nazwy pól są lokalne w danej strukturze. Dlatego też, możemy użyć ponownie te nazwy pól gdzie indziej w programie. Dyrektywa struct tylko definiuje typ strukturalny .Nie rezerwuje pamięci dla zmiennej strukturalnej. W rzeczywistości dla zarezerwowania pamięci musimy zadeklarować zmienną używającą nazwy struktury jako instrukcji MASMa ,np.: John student {} Nawiasy klamrowe muszą pojawić się w polu operandu. Każda wartość inicjująca musi pojawić się między nawiasami. Powyższa deklaracja alokuje pamięć jak pokazano na rysunku 5.7 Jeśli etykieta John zgadza się z adresem bazowym tej struktury, wtedy pole Name jest offsetem John+0,pole Major jest offsetem John+65,pole SSN jest offsetem John+67 itd. Przy dostępie do elementów struktury musimy znać offset od początku struktury żądanego pola .Na przykład, pole Major zmiennej John jest offsetem 65 od adresu bazowego John .Dlatego też, możemy przechować wartość w ax w tym polu używając instrukcji mov John[65],ax. Niestety, wprowadzanie do pamięci wszystkich offsetów pól w strukturze obala potrzebę użycia tablic. W końcu jeśli mamy zająć się tymi offsetami numerycznymi dlaczego nie użyć tablicy bajtów zamiast struktury? Cóż, jak się okazuje, MASM pozwala nam odnieść się do nazwy pola w strukturze używając tego samego mechanizmu. C i Pascal używają operatora dot (kropki).Przechowując ax w polu Major, możemy użyć mov John. Major, ax, zamiast poprzedniej instrukcji. Jest to dużo bardziej czytelne i łatwiejsze w użyciu. Zauważmy ,że użycie operatora dot nie wprowadza nowego trybu adresowania. Instrukcja mov John. Major, ax używa trybu adresowania „tylko przemieszczenie”. MASM po prostu dodaje adres bazowy John do offsetu pola Major (65) uzyskując rzeczywiste przemieszczenie dla kodowania instrukcji. Możemy również wyspecyfikować domyślną wartość inicjującą kiedy tworzymy strukturę. W poprzednim przykładzie, pola struktury student zawierały wartości nieokreślone, wyspecyfikowane przez „?” w polu operandu każdego zadeklarowanego pola. Okazuje się, że są dwa różne sposoby dla specyfikacji wartości inicjującej dla pól struktury. Rozważmy następującą definicję „punktu” struktury danych: Punkt struct x word 0 y word 0 z word 0 Punkt ends Zawsze gdy zadeklarujemy zmienną typu punkt używając instrukcji podobnej do CurPoint Punkt {} MASM automatycznie zainicjuje zmienne CurPoint.x,CurPoint.y i CurPoint. z zerami. Układa się to świetnie w tych przypadkach gdzie nasze obiekty rozpoczynają się od tych samych wartości inicjujących. Oczywiście, możemy założyć ,że chcielibyśmy zainicjować pola X,Y i Z punktu ,ale chcemy nadać każdemu punktowi inną wartość. Jest to łatwe do osiągnięcia poprzez wyspecyfikowanie wartości inicjujących wewnątrz nawiasów: Punkt1 point {0,1,2} Punkt2 point {1,1,1} Punkt3 point {0,1,1} MASM wypełni wartościami te pola w takim porządku w jakim pojawiają się one w polu operandu. Dla Punktu1 MASM zainicjuje pole X zerem, pole Y jedynką a pole Z dwójką. Typ wartości inicjującej w polu operandu musi pasować do typu odpowiadającego mu pola w definicji struktury. Nie możemy, na przykład, wyspecyfikować stałej całkowitej dla pola real4,lub wartości większej niż 256 dla pola bajt. MASM nie wymaga, żebyśmy inicjowali wszystkie pola w strukturze. Jeśli opuścimy jakieś pole, MASM użyje wartości domyślnej (nieokreślonej jeśli specyfikujemy „?” zamiast wartości domyślnej). 5.6.4 STRUKTURY TABLIC I TABLICE/STRUKTURY JAKO POLA STRUKTURY Struktury mogą zawierać inne struktury lub tablice jako pola. Rozważmy następującą definicję: Pixel struct Pt point {} Color dword ?
Pixel ends Powyższa definicja definiuje pojedynczy punkt z 32 bitowym komponentem color. Kiedy inicjujemy obiekt typu Pixel, pierwsza inicjacja odpowiada polu Pt, nie polu współrzędnej x .Następująca definicja jest nieprawidłowa: ThisPt Pixel {5,10} Wartość pierwszego pola („5”) nie jest obiektem typu punkt. Dlatego, asembler wygeneruje błąd kiedy napotka taką instrukcję. MASM pozwala nam zainicjować pole ThisPt używając deklaracji takiej jak następująca: ThisPt Pixel {,10} ThisPt Pixel {{},10} ThisPt Pixel {{1,2,3},10} ThisPt Pixel {{1,,1};,10} Pierwszy i drugi z powyższych przykładów używają wartości domyślnej dla pola Pt (x=0,y=0,z=0) i ustawiają pole Color na 10.Zauważmy,że używamy nawiasów otaczających wartości inicjujące dla typu punkt w drugim, trzecim i czwartym przykładzie. Trzeci przykład inicjuje odpowiednio pola x,y i z pola Pt wartościami jeden, dwa i trzy .Ostatni przykład inicjuje pola x i z jedynkami i pozwala polu y pobrać wartość inicjującą wyspecyfikowaną przez strukturę Punkt (zero). Dostęp do pól Pixel jest bardzo łatwy. Podobnie jak w językach wysokiego poziomu używamy jednej kropki odnosząc się do pola Pt i drugiej kropki przy dostępie do pól x,y i z punktu: mov ax, ThisPt.Pt.X mov ThisPt.Pt.Y,0 mov ThisPt.Pt.Z,d1 mov ThisPt.Color, EAX Możemy również zadeklarować tablice jako pola struktury. Następująca struktura tworzy typ danych zdolnych do przedstawiania obiektu z ośmioma punktami (np. sześcian): Object8 struct Pts punkt 8 dup (?) Color dword 0 Object8 ends Ta struktura alokuje pamięć dla ośmiu różnych punktów. Dostęp do elementów tablicy Pts wymaga znajomości rozmiaru obiektu typu punkt (pamiętamy, że musimy pomnożyć indeks tablicy przez rozmiar jednego elementu, sześć w tym szczególnym przypadku).Przypuśćmy ,na przykład, że mamy zmienną CUBE typu Object8.Możemy uzyskać dostęp do elementów Pts jak następuje: ; CUBE.Pts[i].X:=0; mov ax ,6 mul i ;sześć bajtów na element. mov si, ax mov CUBAEA.Pts[si].X,0 Jednym nieszczęśliwym aspektem tego wszystkiego jest to ,że musimy znać rozmiar każdego elementu tablicy Pts .Na szczęście MASM dostarcza operatora który oblicza rozmiar elementu tablicy (w bajtach) dla nas. 5.6.5 WSKAŹNIKI DO STRUKTUR Podczas wykonywania naszych programów możemy odnieść się do obiektów struktury bezpośrednio lub pośrednio używając wskaźników. Kiedy używamy wskaźnika przy dostępie do pola struktury, musimy załadować jeden z rejestrów wskaźnikowych 80x86 (si, di, bx lub bp na procesorach wcześniejszych niż 80386) offsetem a es, ds., ss lub cs segmentem żądanej struktury. Przypuśćmy, że mamy następujące zmienne zadeklarowane (zakładając strukturę Object8 z poprzedniej sekcji): Cube Obkject8 {} CubePtr dword Cube
CubePtr zawiera adres (jest to wskaźnik do) obiektu Cube .Dostęp do pola Color obiektu Cube możemy uzyskać instrukcją taką jak mov eax, Cube. Color. Kiedy uzyskujemy dostęp do pola poprzez wskaźnik musimy załadować adres obiektu do pary segemnt:rejestr wskaźnikowy ,tak jak es:bx. Instrukcja les bx,CubePtr zrobi tą sztuczkę. Po zrobieniu tego możemy uzyskać dostęp do pola obiektu Cube używając trybu adresowania disp+bx. Powstaje problem „Jak wyspecyfikować do którego pola chcemy uzyskać dostęp?” Rozważmy na krótko następujący nieprawidłowy kod: les bx,CubePtr mov eax, es:[bx].Color Jest jeden główny problem z powyższym kodem .Ponieważ nazwy pól są lokalne w strukturze i jest możliwe użycie ponowne nazwy pola w dwóch lub więcej strukturach, jak MASM ustali który offset reprezentuje Color? Kiedy uzyskujemy dostęp do członków struktury bezpośrednio (np. mov eaz, Cube.Color) nie ma niejasności ponieważ Cube ma specyficzny typ który asembler może sprawdzić. Es:bx ,z drugiej strony, może wskazywać cokolwiek. W szczególności, może wskazywać każdą strukturę która zawiera pole Color. Więc asembler nie może, zdecydować który offset jest używany dla symbolu Color. MASM rozwiązuje tą niejasność poprzez wymagane wyraźne określenie typu w tym przypadku. Prawdopodobnie najłatwiejszym sposobem zrobienia tego jest wyspecyfikowanie nazwy struktury jako pseudo-pola: les bx,CubePtr mov eax, es:[bx].Object8Color Poprzez wyspecyfikowanie nazwy struktury, MASM wie która wartość offsetu jest używana dla symbolu Color. 5.10 PODSUMOWANIE Ten rozdział przedstawia widok centralny organizacji pamięci i struktur danych 80x86.To oczywiście nie jest kompletnym kursem struktur danych. Ten rozdział omawia podstawowe i proste komponenty typów danych i jak deklarować i używać ich w programach. Mnóstwo dodatkowych informacji na temat deklarowania i używania prostych typów danych pojawi się w późniejszych rozdziałach. Jednym z głównych problemów tego rozdziału jest omówienie jak deklarować i używać zmiennych w programach asemblerowych. W programie asemblerowym możemy łatwo tworzyć bajt, słowo, podwójne słowo i inne typy zmiennych. Takie jak skalarne typy danych wspierające boolowskie, znaki, liczby całkowite, real i inne pojedyncze typy danych znane z języków wysokiego poziomu. Zobacz: • Deklarowanie zmiennych w programie asemblerowym • Deklarowanie i używanie zmiennych Byte • Deklarowanie i używanie zmiennych Word • Deklarowanie i używanie zmiennych Dword • Deklarowanie i używanie zmiennych Fword, Qword i Tbyte • Deklarowanie Zmiennych zmienno przecinkowych real4,real8 i real10 Dla tego kto nie lubi używać nazw zmiennych takich jak byte, word itp. MASM pozwala tworzyć swoje własne typy nazw. Możemy je nazwać Integers zamiast Words? Żaden problem, możemy zdefiniować własne typy nazw używając instrukcji typedef .Zobacz: • Tworzenie własnych typów nazw z TYPEDEF Innym ważnym typem danych jest wskaźnik. Wskaźniki są niczym więcej niż adresami pamięci przechowywanymi w zmiennych (zmienna słowo lub podwójne słowo).CPU 80x86 wspiera dwa typy wskaźników bliskie i dalekie wskaźniki. W trybie rzeczywistym, bliskie wskaźniki są długie na szesnaście bitów i zawierają offset wewnątrz znanego segmentu (zwykle w segmencie danych).Dalekie wskaźniki są długie na 32 bity i zawierają pełny adres logiczny segment:offset. Pamiętajmy, że musimy użyć jednego z rejestrów pośrednich lub trybu adresowania indeksowanego przy dostępie do danych wskazywanych przez wskaźniki. Dla tych którzy chcą tworzyć swoje własne typy wskaźnikowe (zamiast po prostu używać word lub dword dla deklaracji bliskich i dalekich wskaźników) instrukcja typedef pozwala tworzyć nazwy typów wskaźników. Zobacz: • Typ danych wskaźnikowych Zbiorowy typ danych jest tworzony z innego typu danych. Przykłady zbiorowych typów danych można mnożyć ,ale dwa z najbardziej popularnych zbiorowych typów danych to tablice i struktury (rekordy).Tablica jest grupą zmiennych, wszystkich tego samego typu. Program wybiera element z tablicy używając indeksu całkowitego w tej tablicy. Struktury, z drugiej strony, mogą zawierać pola których typy są różne. W programie, wybieramy żądane pole poprzez dostarczenie pola nazwy z operatorem dot. Zobacz: • Tablice
• • • •
Tablice wielowymiarowe Struktury Tablice i Struktury i Tablice/Struktury jako Pola Struktury Wskaźniki na Struktury
5.11 PYTANIA 1) W jakim segmencie (8086) normalnie umiejscawiamy nasze zmienne? 2) Który segment w pliku SHELL.ASM odpowiada segmentowi zawierającemu nasze zmienne? 3) Opisz jak zadeklarować zmienne bajtowe. Daj kilka przykładów. Po co używamy zmiennych bajtowych w programie? 4) Opisz jak zadeklarować zmienne word. Daj kilka przykładów. Opisz po co używamy ich w programach 5) Powtórz pytanie 2 dla zmiennych podwójne słowo 6) Wyjaśnij cele instrukcji typedef. Daj kilka przykładów jej użycia 7) Co to jest zmienna wskaźnikowa? 8) Jak uzyskać dostęp do obiektu wskazywanego przez daleki wskaźnik. Daj przykład używając instrukcji 8086 9) Jak jest różnica między bliskim a dalekim wskaźnikiem? 10) Co to jest zbiorowy typ danych? 11) Jak deklarujemy tablice w asemblerze? Podaj kod dla następujących tablic: a) dwuwymiarowa tablica bajtów 4x4 b) tablica zawierająca 128 podwójnych słów c) tablica zawierająca 16 słów d) trójwymiarowa tablica słów 4x5x5 12) Omów jak możemy uzyskać dostęp do pojedynczego elementu każdej z powyższych tablic. 13) Dostarcz kodu 80386,używając trybu adresowania indeksowego ze skalowaniem, przy uzyskaniu dostępu do elementów powyższych tablic 14) Wyjaśnij różnice pomiędzy rzędową a kolumnową organizacją elementów tablic 15) Przypuśćmy, że mamy dwu wymiarową tablicę której wartości chcemy zainicjalizować jak następuje:
Dostarcz deklaracje zmiennych do wykonania tego .Notka: Nie używaj instrukcji maszynowych 8086 dla inicjacji tablicy. Zainicjalizuj tablicę w swoim segmencie danych
16) Przypuśćmy, że ES:BX wskazuje na obiekt typu VideoTape. Jaka jest instrukcja która poprawnie załaduje pole Rating do AL.
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
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Ł SZÓSTY:ZBIÓR INSTRUKCJI PROCESORA 80x86 Dotychczas ,tylko trochę omówiliśmy dostępne instrukcje w mikroprocesorze 80x86.Ten rozdział naprawi tą sytuację. Zauważmy, że ten rozdział jest głównie odniesieniem .Wyjaśnia co każda instrukcja robi, nie wyjaśnia jak łączą się te instrukcje w programie napisanym w języku asemblera. Reszta tej książki wyjaśnia jak się to robi. 6.0 WSTĘP Ten rozdział omawia zbiór instrukcji trybu rzeczywistego 80x86.Podobnie jak w innych językach programowania, będzie kilka instrukcji, których będziemy używać cały czas ,kilka będziemy używać okazjonalnie ,a kilku będziemy używać rzadko, jeśli w ogóle. Rozdział ten będzie przedstawiał instrukcje według klas zamiast według znaczenia. Ponieważ początkujący programiści asemblerowi nie muszą uczyć się całego zbioru instrukcji żeby pisać sensowne programy asemblerowe ,nie będziemy się musieli uczyć jak każda instrukcja działa. Następująca lista opisuje instrukcje omawiane w tym rozdziale. Symbol „•” oznacza ważne instrukcje w każdej grupie. Jeśli nauczymy się tylko tych instrukcji, będziemy mogli napisać każdy program asemblerowy jaki zechcemy .Jest wiele dodatkowych instrukcji, zwłaszcza w procesorze 80386 i późniejszych .Te dodatkowe instrukcje czynią programowanie w asemblerze łatwiejszym, ale nie musimy ich znać aby zacząć pisać programy. Instrukcje 80x86 mogą być ( z grubsza) podzielone na osiem różnych klas: 1) Instrukcje przenoszenia danych • mov, lea, les, push, pop ,pushf, popf 2) Konwersja • cbw, cwd, xlat 3) Instrukcje arytmetyczne • Add ,inc ,sub ,dec ,cmp ,neg ,mul, imul, div ,idiv 4) Instrukcje logiczne ,przesunięcia, obrotu i bitowe • and, or, xor, not, shl, shr ,rcl ,rcr 5) Instrukcje I/O • in, out 6) Instrukcje łańcuchowe • movs, stos ,lods 7) Instrukcje sterowania strumieniem danych w programie • jmp, call, ret, skoki warunkowe 8) Instrukcje różne • clc, stc, cmc Następna sekcja omówi wszystkie te instrukcje w tych grupach i jak one działają. Raz jeden jeszcze w tym tekście nie rekomenduje się używania rozszerzonego zbioru instrukcji 80386.Program,który używa takich instrukcji może nie pracować właściwie na procesorze 80286 lub wcześniejszych. Używanie tych dodatkowych instrukcji może ograniczyć liczbę maszyn na których nasz kod będzie działał. Jednak, procesor 80386 zostanie w tym tekście opisany. Możemy bezpiecznie założyć, że większość systemów będzie zawierać procesor 80386sx lub późniejsze. Ten tekst często używa zbioru instrukcji 80386 w różnych przykładowych programach. Zapamiętajmy, jest to zrobione tylko dla wygody .Nie ma
programu który pojawia się w tym tekście, który nie mógłby być przekodowany przy użyciu tylko instrukcji asemblerowych CPU 8088. Słowo porady, szczególnie dla tych, którzy uczą się tylko powyższych instrukcji: jeśli czytasz o zbiorze instrukcji 80x86,odkryjesz,że pojedyncze instrukcje 80x86 nie są bardzo złożone i mają prostą semantykę. Jednak jeśli nadejdzie
Rysunek 6.1 :Rejestr flag 80x86 koniec tego rozdziału, możesz odkryć ,że nie masz pojęcia jak ułożyć te proste instrukcje razem w formie złożonego programu. Nie bój nic, jest to powszechny problem. Późniejsze rozdziały omówią jak sformować złożony program z tych prostych instrukcji. Jedna szybka uwaga: ten rozdział wyliczył dużo instrukcji jakie „dostępne są na procesorze 80286 i późniejszych procesorach”. Faktycznie wiele z tych instrukcji było również dostępnych w mikroprocesorze 80186.Ponieważ tylko kilka systemów PC stosuje mikroprocesor 80186,ten tekst ignoruje ten CPU 6.1 REJESTRY STANU PROCESORA (FLAGI) Rejestr flag utrzymuje bieżący tryb działania CPU i informuje o jego stanie. Rysunek 6.1 pokazuje układ rejestru flag. Znaczniki przeniesienia, parzystości, zera, znaku i przepełnienia są specjalne ponieważ możemy testować ich stan (zero lub jeden) setcc i instrukcjami skoków warunkowych (zobacz „Zbiór instrukcji warunkowych” i „Instrukcje skoków warunkowych”) 80x86 używa tych bitów ,kodów błędu, do podjęcia decyzji podczas wykonywania programu. Różne arytmetyczne ,logiczne i inne różne instrukcje wpływają na znacznik przepełnienia (overflow).Po operacjach arytmetycznych, flaga ta zawiera jeden jeśli wynik nie mieści się w operandzie przeznaczenia ze znakiem. Na przykład, jeśli próbujemy dodawać 16 bitową liczbę ze znakiem 7FFFh i 0001h,wynik jest zbyt duży więc CPU ustawia flagę przepełnienia. Jeśli wynik operacji arytmetycznej nie tworzy przepełnienia, wtedy CPU czyści tą flagę. Ponieważ generalnie operacje logiczne stosują wartości bez znaku, instrukcje logiczne 80x86 po prostu czyszczą flagę przepełnienia. Inne instrukcje 80x86 opuszczają flagę przepełnienia zawierającą dowolna wartość. Instrukcje łańcuchowe 80x86 używają znacznika kierunku (direction flag).Kiedy flaga kierunku jest wyczyszczona, 80x86 przetwarza elementy łańcucha od adresu niższego do wyższego; kiedy ustawiona CPU przetwarza łańcuch w kierunku przeciwnym. Zobacz ”Instrukcje Łańcuchowe: po dodatkowe szczegóły. Znacznik zezwolenia na przerwanie (interrupt enable/disable flag) steruje zdolnością 80x86 do odpowiedzi na zewnętrzne zdarzenie znane jako prośba o przerwanie. Niektóre programy zawierają pewną sekwencję instrukcji, których nie wolno przerwać CPU. Flaga zezwolenia na przerwanie włącza lub wyłącza przerwania gwarantując, że CPU nie przerwie tej krytycznej sekcji kodu. Znacznik stanu śledzenie (trace flag) włącza lub wyłącza tryb śledzenia. Debuggery (takie jak CodeView) używają tego bitu do włączania lub wyłączania operacji pojedynczego kroku śledzenia. Kiedy jest ustawiony, CPU przerywa każdą instrukcję i przekazuje sterowanie do debuggera pozwalając mu na wykonywanie pojedynczego kroku przez aplikację. Jeśli ten bit jest wyczyszczony ,wtedy 80x86 wykonuje instrukcje bez przerwania. CPU 80x86 nie dostarcza żadnej instrukcji ,która bezpośrednio manipuluje tą flagą .Aby ustawić lub wyczyścić tą flagę musimy: • Odłożyć flagi na stos 80x86 • Ściągnąć wartość innego rejestru • Ustawić wartość flagi stanu śledzenia
• Odłożyć wynik na stos a potem • Ściągnąć flagi ze stosu Jeśli wynik jakiegoś obliczenia jest negatywany,80x86 ustawia znacznik znaku. Możemy przetestować tę flagę po operacji arytmetycznej dla sprawdzenia ujemnego wyniku. Pamiętamy ,wartość jest ujemna jeśli jej najbardziej znaczący bit wynosi jeden. Dlatego też operacje na wartościach bez znakowych ustawią flagę znaku jeśli rezultat ma jedynkę na najbardziej znaczącej pozycji. Różne instrukcje ustawiają znacznik zera kiedy generują jako wynik zero. Często będziemy używać tej flagi aby zobaczyć, czy dwie wartości są równe (np. po odjęciu dwóch liczb, są one równe jeśli wynik wynosi zero).Flaga ta jest również użyteczna po różnych operacjach logicznych aby zobaczyć czy wyspecyfikowany bit w rejestrze lub pamięci zawiera zero czy jeden. Znacznik przeniesienia połówkowego wspiera operacje specjalnego systemu dziesiętnego kodowanego dwójkowo (BCD).Ponieważ większość programów nie stosuje liczb BCD, rzadko będziemy używać tej flagi a i nawet wtedy nie uzyskamy do niej bezpośrednio dostępu.. CPU 80x86 nie dostarczają żadnej instrukcji, która pozwalałaby nam bezpośrednio testować, ustawiać i czyścić tą flagę. Tylko instrukcje add, adc, sub, sbb, mul, imul, div, idiv i BCD manipulują tą flagą. Znacznik parzystości jest ustawiany według parzystości najmniej znaczących bitów każdej operacji na danych. Jeśli operacja tworzy parzystą liczbę bitów, CPU ustawia tą flagę. Zeruje tą flagę jeśli operacja przynosi nieparzystą liczbę bitów. Flaga ta jest użyteczna w pewnych programach komunikacyjnych, jednak Intel wprowadził go głównie dla kompatybilności ze starszymi mikroprocesorami 8080. Znacznik przeniesienia ma kilka zadań. Po pierwsze oznacza przepełnienie bez znakowe (podobnie jak znacznik przepełnienia wykrywa przepełnienie znakowe).Możemy go również użyć podczas operacji arytmetycznych i logicznych o dużej dokładności. Pewne bity testowe, ustawiania, zerowania i inwersji w 80386 bezpośrednio wpływają na tą flagę. W końcu, ponieważ możemy łatwo zerować, ustawiać ,odwracać i testować ją, jest użyteczna dla różnych operacji boolowskich. Flaga przeniesienia ma wiele zadań i znajomość kiedy ją użyć i w jakim celu może wprowadzić w zakłopotanie początkującego programistę asemblerowego. Na szczęście, dla większości danych instrukcji, flaga przeniesienia jest zerowana. Używanie tych znaczników stanie się łatwe i oczywiste w przyszłych sekcjach i rozdziałach. Ta sekcja jest głównie formalnym wprowadzeniem do pojedynczych flag w rejestrze zamiast próba dokładnego wyjaśnienia funkcjonowania każdej z flag. 6.2 KODOWANIE INSTRUKCJI 80x86 używa kodowania binarnego dla każdej operacji maszynowej. Podczas gdy jest ważne mieć ogólne pojęcie jak 80x86 koduje instrukcje, nie jest tak ważne wprowadzanie do pamięci kodowania dla wszystkich instrukcji w zbiorze instrukcji. Gdybyśmy pisali asembler lub disasembler (debugger) musielibyśmy koniecznie znać dokładnie kodowanie .Jednak dla ogólnych programów asemblerowych nie musimy znać dokładnego kodowania. Ponieważ stajemy się bardziej doświadczeni w asemblerze prawdopodobnie chcielibyśmy studiować kodowania zbioru instrukcji 80x86.Oczywiście powinniśmy być zaznajomieni z takimi terminami jak opcod ,bajt mod-reg-r/m., wartość przemieszczenia ,i innymi. Chociaż nie musimy zapamiętywać parametrów dla każdej instrukcji, jest zawsze dobrym pomysłem znać długość i czas cyklu dla instrukcji używanych regularnie ponieważ pomoże to nam pisać lepsze programy. Rozdział Trzeci i Rozdział Czwarty dostarczyły szczegółów odnośnie kodowania instrukcji dla różnych instrukcji (80x86 i x86);takie omówienie było ważne ponieważ musimy zrozumieć jak CPU koduje i wykonuje instrukcje. ten rozdział nie zajmuje się takimi szczegółami Ten rozdział przedstawia wysokopoziomowy obraz każdej instrukcji i zakłada, że nie interesuje nas jak maszyna traktuje bity w pamięci. Dla tych kilku razy, kiedy będziemy musieli poznać kodowanie binarne dla poszczególnych instrukcji, kompletny listing kodowania instrukcji zawiera Appendix D. 6.3 INSTRUKCJE PRZENOSZENIA DANYCH Instrukcje przenoszenia danych kopiują wartości z jednej lokacji do innej. Te instrukcje to mov,xchg,lds,lea,les,lfs,lgs,lss,push,pusha,pushad,pushf,pushfd,pop,popa,popad,popf,popfd,lahf i sahf 6.3.1 INSTRUKCJA MOV Instrukcja mov przybiera kilka różnych form: mov reg, reg mov mem ,reg mov reg, mem mov mem, dana bezpośrednia mov reg, dana bezpośrednia mov ax/al., mem mov mem, ax/al.
mov mov mov mov
segreg, mem16 segreg, reg16 mem16, segreg reg16 ,segreg
Ostatni rozdział omawiał instrukcję mov szczegółowo, więc tylko trochę komentarzy godnych uwagi .Po pierwsze, są to warianty instrukcji mov które są szybsze i krótsze niż inne instrukcje mov wykonujące tą samą pracę. Na przykład, obie instrukcje mov ax, mem i mov reg, mem mogą załadować rejestr ax z komórki pamięci. We wszystkich procesorach, pierwsza wersja jest krótsza .We wcześniejszych członkach rodziny 80x86 jest również szybsza. Są dwa bardzo ważne szczegóły dotyczące instrukcji mov. Po pierwsze nie ma operacji przeniesienia z pamięci do pamięci. tryb adresowania bajt mod-reg-r/m. (zobacz Rozdział Czwarty) pozwala na to dwóm operandom rejestrowym lub jednemu operandowi rejestrowemu i jednemu operandowi pamięci.. nie ma formy instrukcji mov, która pozwala nam kodować dwa adresy pamięci w tej samej instrukcji. Po drugie, nie możemy bezpośrednio przenieść danych do rejestru segmentowego. Instrukcje które przenoszą dane do lub z rejestru segmentowego mają bajty mod-reg-r/m. ;nie ma formy która przenosi bezpośrednie wartości do rejestru segmentowego .Dwa powszechne błędy robione przez początkujących programistów to próby przenoszenia z pamięci do pamięci i próby ładowania stałej do rejestru segmentowego. Operandami instrukcji mov mogą być bajty, słowa lub podwójne słowa. Oba operandy muszą być tego samego rozmiaru lub MASM wygeneruje błąd podczas asemblowania naszego programu. To ma zastosowanie do operandu pamięci i rejestru. Jeśli deklarujemy zmienną B, używając byte i próbujemy załadować tą zmienną do rejestru ax ,MASM zgłosi konflikt typów. CPU rozszerza dane bezpośrednie do rozmiaru operandu przeznaczenia (chyba ,że jest zbyt duży do umieszczenia w operandzie przeznaczenia, wtedy mamy błąd).Zauważmy, że możemy przenieść wartości bezpośrednie do komórki pamięci. Ta sama zasada dotyczy stosowanego rozmiaru. Jednak MASM nie może ustalić rozmiaru pewnych operandów pamięci. Na przykład, czy instrukcja mov [bx],0 przechowuje wartość ośmio- szesnasto- czy trzydziesto dwu bitową? MASM nie może powiedzieć wiec zgłasza błąd. Ten problem nie istnieje, jeśli przenosimy dane do zmiennych, zadeklarowanych w naszym programie. Na przykład, jeśli zadeklarujemy B jako zmienną bajtową, MASM wie, że ma przechować osiem bitów zero w B dla instrukcji mov B,0.Tylko te operandy pamięci wymagające wskaźników bez zmiennych operandów cierpią na ten problem. Rozwiązaniem jest wyraźne powiedzenie MASMowi czy operand jest bajtem, słowem czy podwójnym słowem, Możemy tego dokonać w następujących formach instrukcji: mov byte ptr [bx],0 mov word ptr [bx],0 mov dword ptr [bx],0 (3) (3) dostępne tylko na procesorach 80386 i późniejszych Po więcej szczegółów na temat operatora type ptr zajrzyj do Rozdziału Ósmego. Przeniesienia do i z rejestru segmentowego są zawsze 16 bitowe; operand mod-reg-r/m. musi być 16 bitowy albo MASM wygeneruje błąd. Ponieważ nie możemy załadować stałej bezpośrednio do rejestru segmentowego ,popularnym rozwiązaniem jest ładowanie stałej do rejestru ogólnego przeznaczenia a potem skopiowanie go do rejestru segmentowego. Na przykład, następujące dwie instrukcje ładują rejestr es wartością 40h: mov ax,40h mov es, ax Zauważmy ,że prawie każdy rejestr ogólnego przeznaczenia jest wystarczający. Tutaj, ax został wybrany przypadkowo. Instrukcja mov nie wymaga żadnych flag. W szczególności,80x86 zachowuje wartości flag po wykonaniu instrukcji mov. 6.3.2 INSTRUKCJA XCHG Instrukcja xchg (exchange) zamienia miejscami dwie wartości. Ogólna jej forma to xchg operand1, operand2 Są cztery specyficzne formy tej instrukcji w 80x86: xchg reg, mem xchg reg, reg xchg ax, reg16 xchg eax, reg32 (3) (3) dostępne tylko na procesorze 80386 i późniejszych Pierwsze dwie formy ogólne wymagają dwóch lub więcej bajtów na opcody i bajty mod-regr/m.(przemieszczenie, jeśli jest konieczne ,wymaga dodatkowego bajtu).Trzecia i czwarta forma są specjalnymi
formami drugiej, która zamienia dane w rejestrze (e)ax z innym 16- lub 32 bitowym rejestrem. Forma 16 bitowa używa pojedynczego bajtu opcodu, który jest krótszy niż pozostałe dwie formy, które używają jednego bajtu opcodu i bajtu mod-reg-r/m. Powinniśmy zanotować wzorzec konstruowania: rodzina 80x86 często dostarcza krótszych i szybszych wersji instrukcji ,które używają rejestry ax. Dlatego też, powinniśmy próbować tak układać swoje obliczenia aby używać rejestru (e)ax tak często jak to możliwe. Instrukcja xchg jest doskonałym przykładem, forma, która zamienia 16 bitowe rejestry jest długa na jeden bajt. Zauważmy, że porządek operandów xchg nie ma znaczenia. To znaczy możemy wprowadzić xchg mem, reg i otrzymać ten sam wynik jak przy xchg reg, mem. Większość nowoczesnych asemblerów automatycznie emituje opcod dla krótszej instrukcji xchg ax, reg jeśli wyszczególnimy xchg reg, ax. Oba operandy muszą być tego samego rozmiaru .Na procesorach przed 80386, operandy mogą być ośmio- lub szesnasto bitowe. Na procesorach 80386 i późniejszych operandy mogą być długie na 32 bity. Instrukcja xchg nie modyfikuje żadnych flag. 6.3.3 INSTRUKCJE LDS,LES,LFS,LGS I LSS Instrukcje lds ,les, lfs, lgs i lss pozwalają nam załadować 16 bitowy rejestr ogólnego przeznaczenia i rejestr segmentowy pojedyncza instrukcją W 80296 i wcześniejszych, instrukcje lds i les są instrukcjami, które bezpośrednio przetwarzają wartości większe iż 32 bity. Ogólna forma: LxS prez. , źródło Te instrukcje przyjmują takie formy: lds reg16, mem32 les reg16, mem32 lfs reg16, mem32 (3) (3) lgs reg16, mem32 lss reg16, mem32 (3) (3) dostępne na procesorach 80386 lub późniejszych Reg16 jest rejestrem ogólnego przeznaczenia a mem32 jest komórką pamięci podwójnego słowa (zadeklarowaną instrukcją dword). Te instrukcje ładują 32 bitowe spod adresu wyspecyfikowanego przez mem32 do reg 16 i rejestrów ds., es, fs, gs lub ss. ładują one rejestr ogólnego przeznaczenia najmniej znaczącym słowem operandu pamięci a rejestr segmentowy słowem najbardziej znaczącym. Następujący algorytm opisuje dokładnie tą operację:
Ponieważ instrukcje LxS ładują rejestry segmentowe 80x86,nie wolno nam używać tych instrukcji dla przypadkowych zadań. Użycie ich do ustawienia (dalekich) wskaźników do pewnych obiektów danych jest omówione w Rozdziale Czwartym. Inne ich zastosowanie może spowodować problemy z naszym kodem jeśli spróbujemy przenieść go na Windows,OS/2 lub UNIX. Zapamiętajmy, że te instrukcje ładują cztery bajty spod danej komórki pamięci do pary rejestrów; one nie ładują adresów zmiennych do pary rejestrów (tj. ta instrukcja nie ma trybu bezpośredniego).Jeśli chcemy się nauczyć jak ładować adresy zmiennych do pary rejestrów ,zobaczymy to w Rozdziale Ósmym. Instrukcje LxS nie wpływają na żaden bit flagi 80x86.
6.3.4 INSTRUKCJA LEA Instrukcja LEA (Załaduj adres efektywny) jest inną instrukcją używaną do przygotowania wartości wskaźnika. Instrukcja lea przybiera formę: lea prez. , źródło Formy w 80x86 to: lea reg16, mem lea reg32, mem (3) (3) dostępne tylko na procesorach 80386 lub późniejszych Ładuje ona określony 16 lub 32 bitowy rejestr ogólnego przeznaczenia adresem efektywnym określonej komórki pamięci. Adres efektywny jest końcowym adresem pamięci uzyskanym w wyniku końcowego obliczania trybu adresowania .na przykład, lea ax, ds:[1234h] ładuje rejestr ax adresem komórki pamięci 1234h;tu jest ładowna do rejestru ax wartość 1234h Jeśli pomyślimy o tym przez chwilę, to nie jest bardzo ekscytująca operacją końcu instrukcja mov ax, dana_ bezpośrednia może to zrobić. Więc dlaczego zawracamy sobie głowę instrukcją lea? Cóż, jest wiele innych form operandów pamięci poza operandem „tylko przemieszczenie”. Rozważmy następujące instrukcje lea: lea ax, [bx] lea bx, 3[bx] lea ax, 3[bx] lea bx, 4[bp+si] lea ax, -123[di] Instrukcja lea ax, [bx] kopiuje adres wyrażenia [bx] do rejestru ax. Ponieważ adres efektywny jest wartością w rejestrze bx, ta instrukcja kopiuje wartość bx do rejestru ax .Znów, ta instrukcja nie jest bardzo interesująca ponieważ mov może robić to samo ,nawet szybciej. Instrukcja lea bx,3[bx] kopiuje adres efektywny z 3[bx] do rejestru bx. Ponieważ adres efektywny jest równy bieżącej wartości z bx plus trzy, ta instrukcja lea skutecznie doda trzy do rejestru bx Jest instrukcja add, która pozwala nam dodać trzy do rejestru bx, więc znowu, instrukcja lea jest zbyteczna do tego celu. Trzecia instrukcja lea pokazuje gdzie lea rzeczywiście zaczyna błyszczeć. Lea ax, 3[bx] kopiuje adres z komórki pamięci 3[bx] do rejestru ax; tj. dodaje trzy do wartości w rejestrze bx i przenosi tą sumę do ax. Jest to doskonały przykład jak można użyć instrukcji lea do operacji mov i w dodatku w pojedynczej instrukcji. Dwie końcowe instrukcje, lea bx,4[bp+si} i lea ax, -123[di] dostarczają dodatkowych przykładów instrukcji lea, które są bardziej wydajne niż ich odpowiedniki mov /add. W 80386 i późniejszych procesorach, może używać trybu adresowania indeksowego ze skalowaniem do mnożenia przez dwa, cztery lub osiem jak również dodać rejestry i przemieszczenie razem Intel zdecydowanie sugeruje używanie instrukcji lea ponieważ jest ona dużo szybsza niż sekwencja instrukcji obliczająca ten sam rezultat. (Rzeczywistym) celem lea jest ładowanie rejestru adresem pamięci. Na przykład, lea bx, 128[bp+di] ustawia bx adresem bajtu odnoszącym się do bajtu 128[bp+di].Okazuje się, że instrukcja w formie mov al.,[bx] wykonuje się szybciej niż instrukcja mov al.,128[bp+di].Jeśli ta instrukcja wykona się kilka razy, będzie wydajniej załadować adres efektywny ze 128[bp+di] do rejestru bx i użycie trybu adresowania [bx].Jest to powszechna optymalizacja w wysoko wydajnych programach. Instrukcja lea nie wpływa na żaden bit flag 80x86. 6.3.5 INSTRUKCJE PUSH I POP Instrukcje 80x86 PUSH i POP manipulują danymi na stosie sprzętowym 80x86.Jest 19 wariantów instrukcji push i pop, oto i one
(2) dostępne tylko na procesorach 80286 i późniejszych (3) dostępne na procesorach 80386 i późniejszych Pierwsze dwie instrukcje kładą i zdejmują 16 bitowy rejestr ogólnego przeznaczenia. Jest to małych rozmiarów (jedno bajtowa) wersja stworzona specjalne dla rejestrów. Zauważmy, że jest druga forma która dostarcza bajtu mod-reg-r/m., która może kłaść rejestry również ;większość asemblerów używa tylko tamtej formy dla położenia wartości komórki pamięci. Druga para instrukcji kładzie i zdejmuje 32 bitowe rejestry ogólnego przeznaczenia 80386.Jest to rzeczywiście nic więcej niż położenie rejestru instrukcją opisaną w poprzednim paragrafie którego przyrostek ma rozmiar bajta. Trzecia para instrukcji push/pop pozwala nam położyć i zdjąć rejestry segmentowe 80x86.Zauwazmy,że instrukcje takie jak push fs i gs są dłuższe niż te które kładą cs, ds., es i ss. Zobacz Appendix D po więcej szczegółów ,Możemy także położyć rejestr cs (zdjęcie ze stosu rejestru cs co stwarzałoby pewne interesujące problemy kontroli przepływu programu). Czwarta para instrukcji push/pop pozwala nam położyć lub zdjąć zawartość komórki pamięci. W 80286 i wcześniejszych ,musi to być wartość 16 bitowa. Dla operacji pamięci bez wyraźnego typu (np. [bx]) musimy użyć albo mnemonika pushw albo jawnego stanu rozmiaru używanej instrukcji jak push word ptr [bx].W 80386 i późniejszych możemy położyć i zdjąć wartości 16 i 32 bitowe. Możemy użyć operandu pamięci dword, mnemonika pushd lub operator dword ptr do wymuszenia operacji 32 bitowej. Przykłady: push DblWordVar push dword ptr [bx] push dword Instrukcje pusha i popa (dostępne na 80286 i późniejszych) kładą i zdejmują wszystkie 16 bitowe rejestry ogólnego przeznaczenia. Pusha kładzie rejestry w następującym porządku: ax, cx, dx, bx, sp ,bp ,si a potem di. Popa zdejmuje te rejestry w porządku odwrotnym. Pushad i Popad (dostępne na 80386 i późniejszych) robią to samo na zbiorze 32 bitowych rejestrów 80386.Zauważmy,że te „kładące wszystko” i zdejmujące wszystko” instrukcje nie kładą ani nie zdejmują znaczników ani rejestrów segmentowych. Instrukcje pushf i popf pozwalają nam kłaść / zdejmować rejestr stanu procesora (flagi).Zauważmy, że te dwie instrukcje dostarczające mechanizm do modyfikacji znacznika śledzenia stanu. Zobacz opis działania wcześniej w tym rozdziale. Oczywiście możemy ustawić lub wyczyścić tym sposobem również inne flagi. Jednakże, większość innych flag chcielibyśmy modyfikować (kody błędów) dostarczając określonych instrukcji lub innych prostych sekwencji do tego celu. Enter i leave kładą / zdejmują rejestr bp i alokują pamięć dla lokalnych zmiennych na stosie .Dowiemy się więcej o tych instrukcjach w późniejszym rozdziale. Ten rozdział ich nie rozpatruje ponieważ nie są one szczególnie użyteczne w oderwaniu od wejścia i wyjścia z procedury. „Więc co robią te instrukcje?” spytamy prawdopodobnie. Instrukcje push przenoszą dane na stos 80x86 a instrukcje pop przenoszą dane ze stosu do pamięci lub rejestru. Poniżej znajduje się opisany algorytm każdej instrukcji: instrukcja push (16 bitowa): SP:=Sp-2 [SS:SP]:= operand 16 bitowy (przechowuje wynik w lokacji SS:SP) instrukcja pop (16 bitowa): operand 16 bitowy:=[SS:SP] SP:=SP+2 instrukcja push (32 bitowa): SP”=SP-4 [SS:SP]:= 32 bitowy operand instrukcja pop (32 bitowa): 32 bitowy operand:=[SS:SP] SP:=SP+4 Możemy potraktować instrukcje pusha/pushad i popa/popad jako równoważniki odpowiadające sekwencji operacji 16 lub 32 bitowych push i pop (np. push ax, push cx, push dx, push bx itd.). Odnotujmy trzy rzeczy o stosie sprzętowym 80x86.Po pierwsze, jest zawsze w segmencie stosu (gdziekolwiek wskazuje ss).Po drugie, stos maleje w pamięci. To znaczy, jeśli kładziemy wartość na stos CPU przechowuje je w następujących po sobie coraz niższych komórkach pamięci. W końcu, stos sprzętowy 80x86 wskaźnik (ss:sp) zawsze zawiera adres wartości ze szczytu stosu (ostatnia wartość położona na stos).
Możemy użyć stosu sprzętowego 80x86 dla czasowego przechowywania rejestrów i zmiennych, parametrów dla procedur, alokowania pamięci dla lokalnych zmiennych i innych rzeczy .Instrukcje push i pop są niezmiernie cenne dla manipulowania tymi pozycjami na stosie. Dostaniemy szansę zobaczenia jak użyjemy ich później w tym tekście. Większość instrukcji push i po nie wpływa na żadną flagę stanu rejestru w procesorze 80x86.Instrukcje popf’ popfd przez swoją naturę mogą modyfikować wszystkie bity flag rejestru stanu (rejestr flag) procesora 80x86.Pushf i Pushfd odkładają flagi na stos, ale nie zmieniają podczas tego żadnego znacznika. Wszystkie operacje odkładania i zdejmowania są 16 lub 32 bitowe. Nie ma (łatwego) sposobu włożenia pojedynczej ośmiobitowej wartości na stos. Aby włożyć ośmiobitową wartość będziemy musieli załadować ją do bardziej znaczącego bajtu 16 bitowego rejestru, odłożyć rejestr a potem dodać jeden do wskaźnika stosu. Na wszystkich procesorach z wyjątkiem 8088 spowolniło by to przyszły dostęp do stosu ponieważ sp zawiera teraz nieparzysty adres ,który źle rozmieszczałby dalsze zdjęcia / położenia na stos .Dlatego też, większość programów odkłada lub zdejmuje 16 bitów, nawet jeśli mamy do czynienia z ośmioma bitami. Chociaż jest stosunkowo bezpiecznie odłożyć osiem bitów zmiennej pamięci, bądźmy ostrożni kiedy zdejmujemy ze stosu do ośmio bitowej komórki pamięci. Odłożenie zmiennej ośmio bitowej push word ptr ByteVar odkłada na stos dwa bajty, bajt zmiennej ByteVar i bajt bezpośredni następujący po niej. Nasz kod może po prostu zignorować ten dodatkowy bajt tej instrukcji odkładany na stos. Zdejmowanie ze stosu takich wartości nie jest całkiem proste. Generalnie ,nie sprawi to trudności jeśli odłożymy te dwa bajty .Jednak, może być nieszczęście jeśli zdejmiemy wartość i zgubimy gdzieś następny bajt w pamięci. Są tylko dwa rozwiązania tego problemu. Po pierwsze, możemy zdjąć wartość 16 bitową do rejestru takiego jak ax a potem przenieść najmniej znaczący bajt tego rejestru do zmiennej bajtowej. Drugim rozwiązaniem jest zarezerwowanie dodatkowego bajtu uzupełniającego po zmiennej bajtowej do przechowania całego słowa które chcemy odłożyć. 6.3.6 INSTRUKCJE LAHF I SAHF instrukcje LAHF (ładuj ah z flag) i SAHF (przechowaj ah we flagach) są instrukcjami archaicznymi zachowanymi w zbiorze instrukcji 80x86 aby utrzymać zgodność ze starszymi Intelowskimi procesorami typu 8080.jako takie instrukcje te mają bardzo małe zastosowanie w nowoczesnych programach na 80x86.Instrukcja lahf nie wpływa na żaden bit flag. Instrukcja sahf, ze względu na swoją naturę modyfikuje bity S,Z,A,P i C w rejestrze stanu procesora. Te instrukcje nie wymagają żadnego operandu a używamy ich w następujący sposób: sahf lahf Sahf wpływa tylko na osiem najmniej znaczących bitów rejestru flag. Podobnie lahf, ładuje tylko osiem najmniej znaczących bitów rejestru flag do rejestru AH. Instrukcje te nie wpływają na flagi przepełnienia, kierunku, zabronienia przerwania lub stanu śledzenia. Fakt, że te instrukcje nie wpływają na flagę przepełnienia jest ważnym ograniczeniem. Sahf ma jedno ważne zastosowanie. Kiedy używamy procesorów zmienno przecinkowych (8087,80287,80387,80487,Pentium itp.) możemy użyć instrukcji sahf do kopiowania zmiennoprzecinkowych flag rejestru stanu do rejestru flag 80x86.Zobaczymy to zastosowanie w rozdziale o arytmetyce zmienno przecinkowej (zobacz :”Arytmetyka Zmiennoprzecinkowa”). 6.4 KONWERSJA Zbiór instrukcji 80x86 dostarcza kilka instrukcji konwersji .Zaliczają się do nich movzx ,movsx, cbw, cwd, cwde, cdq, bswap i xlat. Większość z tych instrukcji stosuje rozszerzenie całkowite (ze znakiem) o zerowe, ostatnie dwie instrukcje konwertują pomiędzy formatami pamięci tłumaczeniem wartości przez tablicę połączeń .Instrukcje te przybierają formę: movzx przez, źródło ;przeznaczenie musi być dwa razy większe niż źródło movsx przez, źródło ;przeznaczenie musi być dwa razy większe niż źródło cbw cwd cwde cdq bswap reg 32 xlat 6.4.1 INSTRUKCJE MOVZX,MOVSX,CBW,CWD,CWDE I CDQ Te instrukcje rozszerzają wartości poprzez powielanie zer lub znaków .Instrukcje CBW i CWD są dostępne na procesorach 80x86.Instrukcje movzx, movsx, cwde i cdq są dostępne na procesorach 80386 i późniejszych.
Instrukcja cbw (konwertuj bajt na słowo) powiela znak ośmiobitowej wartości w al. i umieszcza ją w ax. To znaczy kopiuje siódmy bit AL. do bitów 8-15 AX. Ta instrukcja jest ważna zwłaszcza przed wykonaniem ośmiobitowego dzielenia. Ta instrukcja nie wymaga operandów a używamy ją jak następuje: cbw Instrukcja cwd (konwertuj słowo na podwójne słowo) powiela znak 16 bitowej wartości w ax do 32 bitów i umieszcza wynik w dx:ax. kopiuje bit 15 AX do wszystkich bitów dx. jest dostępna na wszystkich procesorach 80x86,co wyjaśnia dlaczego nie powiela znaku wartości do eax. podobnie jak instrukcja cbw, instrukcja ta jest bardzo ważna przy operacjach dzielenia. Nie wymaga operandu a używamy ją : cwd Instrukcja cwde powiela znak 16 bitowej wartości w ax do 32 bitów i umieszcza wynik w eax poprzez kopiowanie bitu 15 do wszystkich bitów 16..31 eax. Instrukcja ta jest dostępna tylko na procesorach 80386 lub późniejszych. Podobnie jak cbw i cwd nie wymaga operandu a używamy ja jak następuje Cwde Instrukcja cdq powiela znak 32 bitowej wartości w eax do 64 bitów i umieszcza wynik w edz:eax przez kopiowanie bitu 31 eax do wszystkich bitów 0..31 edx .Instrukcja ta jest dostępna na procesorach 80386 lub późniejszych. Normalnie będziemy używać tej instrukcji przed operacją długich liczb Podobnie jak cbw, cwd i cwde nie ma operandu a stosujemy ją tak: Cdq Jeśli chcemy powielić znak wartości ośmiobitowej do 32 lub 64 bitów używając tych instrukcji, możemy zastosować następującą sekwencję: ;powielenie znaku al. do dx:ax cbw cwd ;powielenie znaku al. do eax cbw cwde ;powielenie znaku al. do edx:eax cbw cwde cdq Możemy również użyć movsx dla powielania znaku z ośmiu do szesnastu lub trzydziestu dwóch bitów. Instrukcja movsx jest uogólnioną formą instrukcji cbw, cwd i cwde. Powiela znak wartości ośmio bitowej do szesnastu lub trzydziestu dwóch bitów, lub powiela znak wartości szesnasto bitowej do 32 bitów. Ta instrukcja używa bajtu mod-reg-r/m. do wyspecyfikowani dwóch operandów .dostępne formy dla tej instrukcji to movsx reg16,mem8 movsx reg16,reg8 movsx reg32,mem8 movsx reg32,reg8 movsx reg32,mem16 movsx reg32,reg16 Zauważmy, że wszystko co możemy zrobić instrukcjami cbw i cwde, możemy zrobić instrukcją movsx; movsx ax,al. ;cbw movsx eax,ax ;cwde movsx eax,al. ;cbw następujące po cwde Jednak, instrukcje cbw i cwde są krótsze i czasami szybsze. Ta instrukcja jest dostępna tylko na procesorach 80386 i późniejszych. zauważmy też, że nie ma bezpośredniego ekwiwalentu movsx dla instrukcji cwd i cdq. Instrukcja movzx działa podobnie jak movsx, z wyjątkiem tego, że rozszerza wartości bez znaku przez powielanie zer zamiast wartości ze znakiem przez powielanie znaku. Składnia jest taka sam jak dla movsx, z wyjątkiem oczywiście użycia mnemonika movzx zamiast movsx. Jeśli chcemy powielić zero ośmiobitowej wartości do 16 bitów (np. al. do ax) prosta instrukcja mov jest szybsza i krótsza niż movzx. Na przykład, mov bh,0 jest krótsze niż movzx bx, bl Oczywiście, jeśli przenosimy dane do różnych 16 bitowych rejestrów (np. movzx bx ,al.) instrukcja movzx jest lepsza. Podobnie jak instrukcja movsx instrukcja movzx jest dostępna tylko na procesorach 80386 lub późniejszych. Powielanie zer lub znaków nie wpływa na żadną z flag.
6.4.2 INSTRUKCJA BSWAP Instrukcja bswap dostępna jest tylko na 80486 (tak,486) i późniejszych procesorach, konwertuje pomiędzy 32 bitowymi wartościami little endian i big endian. Ta instrukcja akceptuje tylko pojedynczy 32 bitowy operand rejestr. Wymienia pierwszy bajt z czwartym a drugi bajt z trzecim. Składnia dla instrukcji to; bswap reg 32 Gdzie reg32 jest 32 bitowym rejestrem ogólnego przeznaczenia. Procesory z rodziny Intela używają organizacji pamięci znanej jako little endian byte organization (LEBO) W LEBO ,najmniej znaczący bajt wielobajtowej sekwencji staje się najniższym adresem w pamięci. Na przykład ,bity zero do siedem 32 bitowej wartości pojawiają się jako drugi adres w pamięci; bity 16 do 23 pojawiają się w trzecim bajcie, a bity 24 do 31 w czwartym bajcie. Inną popularną organizacją pamięci jest big endian. W schemacie big endian bity 24 do 31 pojawiają się w pierwszym (najniższym adresie),bity 16 do 23 w drugim bajcie, bity 8 do 15 w trzecim bajcie a bity o do 7 w czwartym bajcie. CPU takie jak rodzina Motoroli 68000 używane przez Apple w Macintoshach i wielu chipach RISC, stosuje schemat big endian. Normalnie nie musimy martwić się o organizację bajtów w pamięci, ponieważ pisane programy dla procesorów Intela w asemblerze, nie pracują na procesorach 68000.Jednak,jest dosyć powszechna wymiana danych między maszynami z różnymi organizacjami bajtów. Niestety, 16 i 32 bitowe wartości w maszynie big endian nie tworzą prawidłowych wyników kiedy używamy ich na maszynach little endian. Tam wchodzi instrukcja bswap. Pozwala ona nam łatwo skonwertować 32 bitową wartość big endian do 32 bitowej wartości little endian. Jednym interesującym zastosowaniem instrukcji bswap jest uzyskanie dostępu do drugiego zbioru rejestrów ogólnego przeznaczenia.. Jeśli używamy 16 bitowych rejestrów ogólnego przeznaczenia w naszym kodzie, możemy zdublować liczbę dostępnych rejestrów poprzez użycie instrukcji bswap do wymiany danych z 16 bitowego rejestru z bardziej znaczącym słowem rejestru 32 bitowego. Na przykład, możemy trzymać dwie 16 bitowe wartości w eax i przenieść odpowiednią wartość do ax jak następuje: < Jakieś obliczenia, które zostawiają wynik w AX > bswap eax < Jakieś dodatkowe obliczenia wymagające AX > bswap eax < Jakieś obliczenia wymagające oryginalnej wartości AX > 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 <= B mov ax, A ; zakładamy, że A i B są wartościami całkowitymi ze znakiem cmp ax, B setle Bool ; Bool musi być zmienną bajtową. Ponieważ instrukcje setcc zawsze tworzą zero lub jeden, możemy użyć wyników logicznego and i or do obliczenia złożonych wartości boolowskich: ; Bool := ((A <=B) and (D = E) )or (F<>G) mov ax, A cmp ax, B setle bl mov ax, D cmp ax, E sete bh and bl, bh mov ax, F cmp ax, G setne bh or bl, bh mov Bool, bh Po więcej szczegółów zobacz „Wyrażenia Logiczne (Boolowskie)” Instrukcje setcc zawsze tworzą ośmio bitowy wynik ponieważ bajt jest najmniejszym operandem na którym działa 80x86.Jednakże możemy łatwo użyć instrukcji przesunięcia i obrotu do upakowania ośmio bitowej wartości w pojedynczym bajcie. Następujące instrukcje porównują osiem różnych wartości z zerem i kopiują „flagę zera” z każdego porównania do odpowiednich bitów w al: cmp Val7, 0 setne al. ;wprowadza pierwszą wartość w bicie #0 cmp val6, 0 ;testuje wartość dla bitu #6 setne ah ;kopiuje flagę zera do rejestru ah shr ah, 1 ;kopiuje flagę zera do przeniesienia rcl al., 1 ;przesuwa przeniesienie do bajtu wyniku cmp Val5, 0 ;testuje wartość dla bitu #5 setne ah shr ah, 1 rcl al., 1
cmp Val4, 0 setne ah shr ah, 1 rcl al., 1 cmp Val3, 0 setne ah shr ah, 1 rcl al.,1 cmp Val2, 0 setne ah shr ah, 1 rcl al., 1 cmp Val1, 0 setne ah shr ah, 1 rcl al., 1 cmp Val0, 0 setne ah shr ah, 1 rcl al., 1 ;Teraz AL. zawiera flagę zera z ośmiu porównań
;test wartości dla bitu #4
;test wartości dla bitu #3
;test wartości dla bitu #2
;test wartości dla bitu #1
;test wartości dla bitu #0
6.7 INSTRUKCJE I/O 80x86 wspiera dwie instrukcje I/O: in i out. Przybierają one formę: in eax/ax/al., port in eax/ax/al., dx out port, eax/ax/al. out dx, eax/ax/al. port jest wartością między 0 a 255. 80x86 dostarcza 65,536 różnych portów I/O (wymagających 16 bitowych adresów).Wartość powyższego portu jest wartością pojedynczego bajtu. Dlatego też, możemy tylko bezpośrednio adresować pierwsze 256 portów I/O w przestrzeni adresowej I/O. Aby zaadresować wszystkie 65,536 różnych portów I/O, musimy załadować adres żądanego portu do rejestru dx i uzyskać dostęp do portu pośrednio. Instrukcja in odczytuje dane z wyszczególnionego portu i kopiuje je do rejestru akumulatora. instrukcja out zapisuje wartość z rejestru akumulatora do wyszczególnionego portu. Proszę zauważyć ,że nie ma nic magicznego w instrukcjach in i out 80x86.Są one po prostu inną formą instrukcji mov która uzyskuje dostęp do innej przestrzeni pamięci (przestrzeń adresowa I/O) zamiast normalnej 1 Megabajtowej przestrzeni adresowej pamięci. Instrukcje in i out nie wpływają na żadną flagę 80x86 Przykłady instrukcji I/O 80x86: in al., 60h ;odczytuje port klawiatury mov dx, 378h ;wskazuje na LPT1: port danych in al., dx ;odczytuje dane z portu drukarki inc ax ;zmniejsza ASCII kod o jeden out dx, al. ;zapisuje dane w AL. do portu drukarki 6.8 INSTRUKCJE ŁAŃCUCHOWE 890x86 dostarcza dwanaście instrukcji łańcuchowych: • movs (przesuń łańcuch) • lods (ładuj element łańcucha do rejestru akumulatora) • stos (przechowaj akumulator w elemencie łańcucha) • scas (szukanie łańcucha i sprawdzanie podobieństwa wartości w rejestrze akumulatora • cmps (porównaj dwa łańcuchy) • ins (wprowadzenie łańcucha z portu I/O) • outs (wprowadzenie łańcucha do poru I/O) • rep (powtarzaj operacje łańcuchowe) • repz (powtarzaj dopóki zero) • repe (powtarzaj dopóki równe) • repnz (powtarzaj dopóki nie zero)
•
repne (powtarzaj dopóki nie różne)
Możemy użyć instrukcji movs, stos, scas ,cmps ,ibns i outs do manipulowania pojedynczym elementem (bajt, słowo lub podwójne słowo) w łańcuchu, lub przetwarzać cały łańcuch. generalnie, moglibyśmy używać instrukcji lods do manipulowania pojedyncza pozycją. Instrukcje te mogą działać na łańcuchach bajtów, słów lub podwójnych słów .Dla wyspecyfikowania rozmiaru obiektu, po prostu dodajemy b, w lub d na koniec mnemonika instrukcji, np. lodsb, movsw, cmpsd itp. Oczywiście, formy podwójnego słowa są tylko dostępne na procesorach 80386 i późniejszych. Instrukcje movs i cmps zakładają, że ds:si zawiera adres segmentowy łańcucha źródłowego a es:di zawiera adres segmentowy łańcucha przeznaczenia. Instrukcja lods zakłada, że ds:si wskazuje łańcuch źródłowy, akumulator (al./ax/eax0 jest lokacja przeznaczenia. Instrukcje scas i stos zakładają, że es:di wskazuje łańcuch przeznaczenia a akumulator zawiera wartość źródłową. Instrukcja mobs przesuwa jeden element łańcucha (bajt, słowo lub podwójne słowo) z komórki pamięci ds.:si do es:di.Po przesunięciu danych, instrukcja zwiększa lub zmniejsza si i di o jeden, dwa lub cztery jeśli przetwarzamy odpowiednio bajt, słowo lub podwójne słowo. CPU zwiększa te rejestry jeśli flaga kierunku jest wyzerowana, zmniejsza jeśli flag kierunku jest ustawiona. Instrukcja movs może przesuwać bloki danych w pamięci. Możemy użyć jej do przesuwania łańcuchów, tablic i innych wielobajtowych struktur danych. movs {b, w, d}: es:[di] := ds.:[si] if flaga_kierunku= 0 then si := si + rozmiar; di := di + rozmiar; else si := si – rozmiar; di := di – rozmiar; endif; Notka: rozmiar to jeden dla bajtu, dwa dla słowa i cztery dla podwójnego słowa. Instrukcja cmps porównuje bajt, słowo lub podwójne słowo z lokacji ds.:si i es:di i ustawia odpowiednio flagi procesora. Po porównaniu, cmps zwiększa lub zmniejsza si i di o jeden, dwa lub cztery w zależności od rozmiaru instrukcji i stanu flagi kierunku w rejestrze flag. cmps{b,w,d}:
cmp ds.:[si], es:[di] if flaga_kierunku = 0 then si :=si+ rozmiar; di := di + rozmiar; else si := si – rozmiar; di := di – rozmiar; endif; Instrukcja lods przesuwa bajt, słowo lub podwójne słowo spod ds.:si do rejestru al.,ax lub eax. Potem zwiększa lub zmniejsza rejestr si o jeden, dwa lub cztery w zależności od rozmiaru instrukcji i wartości flagi kierunku. Instrukcja lods jest użyteczna dla pobierania sekwencji bajtów, słów lub podwójnych słów z tablicy, wykonując jakieś operacje na tych wartościach a potem przetwarza następny element z łańcucha. lods {b,w,d}: eax,ax,al. := ds.:[si] if flaga _kierunku = 0 then si := si+rozmiar; else si := si – rozmiar; endif; Instrukcja stos przechowuje al ,ax lub eax pod adresem wyszczególnionym przez es:di. Di jest zwiększane lub zmniejszane według rozmiaru instrukcji i wartości flagi kierunku. Instrukcja stos ma kilka zastosowań. W parze z instrukcją lods może ładować (przez lods),manipulować i przechowywać elementy łańcucha. Sama, instrukcja stos może szybko przechować pojedynczą wartość w całej wielobajtowej strukturze danych. stos{b,w,d}: es:[di] ;+ eax/ax/al if flaga_kierunku = 0 then di := di +rozmiar; else di := di-rozmiar;
endif; Instrukcja scas porównuje al., ax lub eax z wartością spod lokacji es:di i potem modyfikuje odpowiednio di. Instrukcja ta ustawia flagi w rejestrze stanu procesora podobnie jak instrukcje cmp i cmps. instrukcja scas jest dobra dla szukania szczególnych wartości w całej wielobajtowej strukturze danych. scas{b,w,d}: cmp eax/ax/al., es:[di] if flaga_kierunku = 0 then di := di+ rozmiar; else di := di – rozmiar; endif; Instrukcja ins wprowadza bajt, słowo lub podwójne słowo z portu I/O wyszczególnionego w rejestrze dx. Przechowuje potem wartość wprowadzoną w komórce pamięci es:di i zwiększa lub zmniejsza di odpowiednio. Instrukcja ta jest dostępna tylko na procesorach 80286 i późniejszych. ins {b,w,d}: es:[di] := port(dx) if flaga_kierunku = 0 wtedy di := di+ rozmiar; else di := di + rozmiar endif; Instrukcja outs pobiera bajt, słowo lub podwójne słowo spod adresu ds:si, zwiększając lub zmniejszając odpowiednio si, a potem wprowadza wartość do portu wyszczególnionego w rejestrze dx. outs{b,w,d]: port(dx) := ds.:[si] if flaga_kierunku = 0 then si := si + rozmiar; else si := si – rozmiar; endif; Jak wyjaśniliśmy tu ,instrukcje łańcuchowe są użyteczne, ale może być jeszcze lepiej .Kiedy łączymy je z przedrostkiem rep ,repz, repe, repnz i repne, pojedyncza instrukcja może działać na całym łańcuchu. 6.9 INSTRUKCJE STEROWANIA PRZEPŁYWEM DANYCH W PROGRAMIE Instrukcje omawiane do tej pory wykonywały się sekwencyjnie; to znaczy, CPU wykonuje każdą instrukcję w tej kolejności w jakiej pojawiają się w naszym programie. Napisanie rzeczywistych programów wymaga kilku struktur sterujących, nie sekwencyjnych. Przykłady obejmują instrukcję if, pętle i wywołanie podprogramu. Ponieważ kompilatory redukują wszystkie inne języki do języka asemblera, nie powinno być dla nas niespodzianką ,że asembler wspiera niezbędne instrukcje do implementacji tych struktur sterujących. Instrukcje sterujące programem 80x86 należą do trzech grup: bezwarunkowego przekazania sterowania ,warunkowego przekazania sterowania i wywołania podprogramu i instrukcji powrotu Następna sekcja omawia te instrukcje. 6.9.1 SKOKI BEZWARUNKOWE Instrukcja jmp (skok) przenosi sterowanie bezwarunkowo do innego punktu w programie. Jest sześć form tej instrukcji: międzysegmentowy /skok bezpośredni, dwa wewnątrz segmentowe/ skoki bezpośrednie, międzysegmentowy /skok pośredni, dwa wewnątrzsegmentowe /skoki pośrednie. Skoki wewnątrzsegmentowe są zawsze pomiędzy instrukcjami w tym samym segmencie kodu. Skoki międzysegmentowe mogą przenosić sterowanie do instrukcji do różnych segmentów kodu. Instrukcje te używają tej samej składni: jmp cel Asembler rozróżnia je po ich operandach: jmp disp8 ;bezpośrednio wewnątrz segmentu, 8 bitowe przesunięcie jmp disp16 ;bezpośrednio wewnątrz segmentu,16 bitowe przesunięcie jmp adrs32 ;bezpośrednio między segmentami, 32 bitowy adres segmentowy jmp mem16 ;pośrednio wewnątrz segmentu, 16 bitowy operand pamięci jmp reg16 ;pośrednio wewnątrz segmentu jmp mem32 ;pośrednio między segmentami, 32 bitowy operand pamięci. Międzysegmentowy jest synonimem dla daleko, wewnątrzsegmentowy jest synonimem dla blisko. Dwa bezpośrednie skoki wewnątrzsegmentowe różnią się tylko ich długościami. Pierwsza forma skalda się z opcodu i pojedyncze bajtowe przesunięcie. CPU powiela znak tego przesunięcia do 16 bitów i dodaje go do rejestru ip. Instrukcja ta może rozgałęziać się do lokacji –128..+127 od początku następującej instrukcji następującej po niej.
Druga forma wewnątrzsegmentowego skoku jest długa na trzy bajty z dwu bajtowym przesunięciem. Instrukcja ta pozwala na faktyczny zakres –32,768..+32,767 bajtów i może sterować przepływem gdzieś w bieżącym segmencie kodu .CPU po prostu dodaje dwa bajty przesunięcia do rejestru ip. Te pierwsze dwa skoki używają względnego schematu adresowania. Offset kodowany jako część bajtu opcodu nie jest adresem docelowym w bieżącym segmencie kodu, ale odległością od adresu docelowego. Na szczęście MASM obliczy tą odległość za nas automatycznie, więc nie musimy obliczać wartości tego przesunięcia osobiście. Pod wieloma względami te instrukcje są niczym więcej niż instrukcjami add ip, disp. Bezpośredni skok międzysegmentowy jest długości pięciu bajtów ,ostatnie cztery bajty zawierają adres segmentowy(offset w drugim i trzecim bajcie, segment w czwartym i piątym bajcie)Instrukcja ta kopiuje offset do rejestru ip a segment do rejestru cs. Wykonywanie następnej instrukcji zaczyna się od nowego adresu w cs:ip. W odróżnieniu od poprzednich dwóch skoków, adres opcodu jest absolutnym adresem pamięci instrukcji docelowej; ta wersja nie używa względnego adresowania .Instrukcja ta ładuje cs:ip 32 bitową wartością bezpośrednią. Dla tych trzech skoków bezpośrednich opisanych powyżej, zwykle wyszczególniamy adres docelowy używając etykiety instrukcji. Etykieta instrukcji jest zazwyczaj identyfikatorem następującym przed dwukropkiem, zazwyczaj w tej samej linii jak wykonywana instrukcja maszynowa. Asembler określa offset instrukcji po etykiecie i automatycznie oblicza odległość od instrukcji skoku do etykiety instrukcji. Dlatego też, nie musimy martwić się o ręczne obliczanie przesunięcia. Na przykład, następująca krótka pętla stale odczytuje dane portu równoległego drukarki i odwraca najmniej znaczący bit. Tworzy to falę prostokątną sygnału elektrycznego na jednej z linii wyjściowych portu drukarki: mov dx, 378h ;adres portu równoległego drukarki LoopForever: in al., dx ;odczytanie znaku z portu wyjściowego xor al., 1 ;odwrócenie najmniej znaczącego bitu out dx, al. ;dana wyjściowa wraca do portu jmp LoopForever ;powtórka Czwarta forma instrukcji skoku bezwarunkowego jest to instrukcja skoku pośredniego wewnątrzsegmentowego. Wymaga ona 16 bitowego operandu pamięci. Forma ta steruje przepływem do dresu wewnątrz offsetu danego przez dwa bajty operandu pamięci. Na przykład, WordVar word AdresDocelowy jmp WordVar steruje przepływem danych do adresu wyszczególnionego przez wartość w 16 bitowej komórce pamięci WordVar. Nie jest to skok do instrukcji pod adresem WordVar, skacze do instrukcji pod adresem mieszczącym się w zmiennej WordVar. Zauważmy, że ta forma instrukcji jmp jest mniej więcej odpowiednikiem: mov ip, WordVar Chociaż powyższy przykład używa zmiennej z pojedynczym słowem zawierającym adres pośredni, możemy użyć każdego ważnego trybu adresowania pamięci, a nie tylko trybu adresowania „tylko przemieszczenie”. Możemy użyć pośredniego trybu adresowania pamięci jak: jmp DispOnly ;zmienna Słowo jmp Disp[bx] ;disp jest tablicą słów jmp Disp[bx][si] jmp [bx] itd. Rozważymy indeksowy tryb adresowania za chwilę (disp[bx])Ten tryb adresowania pobiera słowo z lokacji disp+bx i kopiuje tą wartość do rejestru ip; pozwala to nam stworzyć tablicę wskaźników i skoczyć do wyszczególnionego wskaźnika używając indeksu tablicy. Rozważmy następujący przykład: AdrsArray word stmt1,stmt2,stmt3,stmt4 mov bx, I ;I jest z zakresu 0..3 add bx, bx ;indeks do tablicy słów jmp AdrsArray[bx] ;skok do stmt1,stmt2 itd., w zależności od wartości I Ważną rzeczą do zapamiętania jest to ,że bliski skok pośredni pobiera słowo z pamięci i kopiuje go do rejestru ip; nie skacze do wyszczególnionej komórki pamięci, skacze pośrednio przez 16 bitowy wskaźnik spod wyszczególnionej komórki pamięci.
Piąta instrukcja jmp steruje przepływem offsetu danego w 16 bitowym rejestrze ogólnego przeznaczenia. Zauważmy ,że możemy użyć każdego rejestru ogólnego przeznaczenia, nie tylko bx ,si, di lub bp. Instrukcja w formie jmp ax jest mniej więcej odpowiednikiem mov ip, ax Zauważmy, że poprzednie dwie formy (rejestr lub pamięć pośrednia) są rzeczywiście tymi samymi instrukcjami. Pole mod and r /m. bajtu mod-reg-r/m. wyszczególniają adres pośredni rejestru lub pamięci. Zobacz Appendix D po więcej szczegółów. Szósta forma instrukcji jmp, pośredni skok międzysegmentowy, ma operand pamięci który zawiera wskaźnik na podwójne słowo. CPU kopiuje podwójne słowo spod tego adresu do pary rejestrów cs:ip. Na przykład, FarPointer dword AdresDocelowy jmp FarPointer steruje przepływem danych do adresu segmentowego wyszczególnionego przez cztery bajty adresu FarPointer. Instrukcja ta jest semantycznie identyczna do (mitycznej) instrukcji lcs ip, FarPointer ;ładuje cs, ip z FarPointer Ponieważ dla bliskiego skoku pośredniego omówionego wcześniej, ten daleki skok pośredni pozwala nam wyszczególnić każdy ważny tryb adresowania. Nie jesteśmy ograniczenia to trybu adresowania „tylko przemieszczenie” używanych przez powyższe przykłady. MASM używa bliskiego pośredniego i dalekiego pośredniego trybu adresowania w zależności od typu komórki pamięci którą wyszczególnimy .Jeśli zmienna którą wyszczególniamy jest zmienna słowo ,MASM automatycznie wygeneruje bliski skok pośredni; jeśli zmienna jest podwójnym słowem, MASM wyemituje opcod dla dalekiego skoku pośredniego. Pewne formy adresowania pamięci niestety nie wyszczególniają samoistnie rozmiaru. Na przykład [bx] jest zdecydowanie operandem pamięci, ale czy punkt bx jest zmienną słowa czy podwójnego słowa? Może wskazywać jeden i drugi. Dlatego też MASM odrzuci instrukcję w tej formie: jmp [bx] MASM nie może powiedzieć czy to powinien być skok bliski pośredni czy daleki pośredni. Aby rozwiązać tę dwuznaczność, będziemy musieli użyć operatora sprawdzania zgodności typów. Rozdział Osiem w pełni opisuje operator sprawdzania zgodności typów, ale teraz ,użyjemy jednej z następujących dwóch instrukcji dla bliskiego lub dalekiego skoku, odpowiednio: jmp word ptr [bx] jmp dword ptr [bx] Tryb adresowania pośredniego przez rejestr nie jest jedynym, który mógłby być typem dwuznacznym. Możemy również podejść do tego problemu z trybem adresowania indeksowym i bazowym indeksowym: jmp word ptr 5[bx] jmp dword ptr 9 [bx][si] Więcej o operatorach sprawdzania zgodności typów znajdziesz w Rozdziale Ósmym. Teoretycznie, możemy użyć instrukcji skoku pośredniego i instrukcji setcc do warunkowego sterowania przesyłaniem danych kilku danych lokacji. Na przykład następujący kod steruje przesyłaniem danych do iftrue jeśli zmienna słowa X jest równa zmiennej słowa Y.W przeciwnym razie steruje przesyłaniem danych do iffalse. JmpTbl word iffalse, iftrue mov ax, X cmp ax, Y sete bl movzx ebx, bl jmp JmpTbl [ebx*2] Jak zobaczymy wkrótce jest dużo lepszy sposób zrobienia tego używając instrukcji skoku warunkowego. 6.9.2 INSTRUKCJE CALL I RET Instrukcje call i ret obsługują wywołania podprogramów i powroty. Jest pięć różnych instrukcji call i sześć różnych form instrukcji powrotu:
call call call call call
disp16 adrs32 mem16 reg16 mem32
;bezpośrednio wewnątrz segmentu ;bezpośrednio między segmentami, 32 bitowy adres segmentowy ;pośrednio wewnątrz segmentu,16 bitowy wskaźnik pamięci ;pośrednio wewnątrz segmentu,16 bitowy wskaźnik rejestru ;pośrednio między segmentamil,32 bitowy wskaźnik pamięci
ret ;bliski lub daleki powrót retn ;bliski powrót retf ;daleki powrót ret disp ;bliski lub daleki powrót i zdjęcie ze stosu retn disp ;bliski powrót i zdjęcie ze stosu retf disp ;daleki powrót i zdjęcie ze stosu Instrukcje call przybierają takie same formy jak instrukcje jmp z wyjątkiem tego, że nie są krótsze (dwa bajty) wywołania wewnątrzsegmentowego. Daleka instrukcja call robi co następuje: • Odkłada rejestr cs na stos • Odkłada 16 bitowy offset następnej instrukcji po wywołaniu na stos • Kopiuje 32 bitowy adres efektywny do rejestrów cs:ip. Ponieważ instrukcja call pozwala na to, że te same tryby adresowania jak jmp, call mogą uzyskiwać adres docelowy używając względnego, pamięci lub rejestru, trybu adresowania. • Kontynuuje się wykonywanie pierwszej instrukcji podprogramu. Ta pierwszą instrukcją jest opcod adresu docelowego obliczonego w poprzednim kroku. Bliska instrukcja call robi co następuje: • Odkłada 16 bitowy offset następnej instrukcji po wywołaniu na stos • Kopiuje 16 bitowy adres efektywny do rejestru ip. Ponieważ instrukcja call pozwala na to, że te same tryby adresowania jak jmp, call mogą uzyskiwać adres docelowy używając względnego, pamięci lub rejestru, trybu adresowania. • Kontynuowane jest wykonywanie pierwszej instrukcji podprogramu. Tą pierwszą instrukcją jest opcod adresu docelowego obliczonego w poprzednim kroku. Instrukcja call disp16 używa adresowania względnego. Możemy obliczyć adres efektywny poprzez dodanie tego 16 bitowego przesunięcia z adresem powrotnym (podobnie jak względna instrukcja jmp, przesunięcie jest odległością od instrukcji następującej po call do adresu docelowego). Instrukcja call disp32 używa bezpośredniego trybu adresowania.32 bitowy adres segmentowy bezpośrednio następuje po opcodzie call. Forma ta instrukcji call kopiuje ta wartość bezpośrednio do pary rejestrów cs:ip. Pod wieloma względami, jest to odpowiednik bezpośredniego trybu adresowania ponieważ wartość tej instrukcji jest kopiowana do pary rejestrów bezpośrednio następujących po tej instrukcji. Call mem16 używa pośredniego trybu adresowania pamięci. Podobnie jak instrukcja jmp, ta forma instrukcji call pobiera słowo spod wyszczególnionej komórki pamięci i używa wartości tego słowa jako adresu docelowego. Pamiętamy, że możemy użyć każdego trybu adresowania pamięci z tą instrukcją. Tryb adresowani „tylko przesunięcie” jest najbardziej powszechna formą, ale inne są równie ważne: call CallTbl [bx] ;indeks do tablicy wskaźników call word ptr [bx] ;BX wskazuje słowo do użycia call WordTbl [bx][si] ;itd. Zauważmy, że wybieranie trybu adresowania tylko wpływa na obliczenie adresu efektywnego dla podprogramu docelowego. Te instrukcje call odkładają offset następnej instrukcji następującej po call na stos. Ponieważ są to bliskie wywołania (uzyskują swój adres docelowy z 16 bitowej komórki pamięci) odkładają 16 bitowy adres powrotny na stos. Call reg16 pracuje jak powyższe pośrednie wywołanie pamięci ,z wyjątkiem tego, że używamy 16 bitowej wartości w rejestrze dla adresu docelowego. Instrukcja ta jest tą samą instrukcją jak instrukcja call mem16.Obie formy wyszczególniają swój adres efektywny używając bajtu mod-reg-r/m. Dla postaci call reg16,bit mod zawiera 11b więc pole r/m. specyfikuje rejestrowy zamiast pamięciowy tryb adresowania. Oczywiście ,instrukcja ta również odkłada 16 bitowy offset następnej instrukcji na stos jako adres powrotny. Instrukcja call mem32 jest pośrednim dalekim wywołaniem. Adres pamięci wyszczególniony przez tą instrukcję musi być wartością podwójnego słowa. Ta forma instrukcji call pobiera 32 bitowy adres segmentowy, oblicza adres efektywny i kopiuje tą wartość podwójnego słowa do pary rejestrów cs:ip. Instrukcja ta również kopiuje 32bitowy adres segmentowy następnej instrukcji na stos (odkłada najpierw wartość segmentu a
potem offset).Podobnie jak instrukcją call mem16,możemy użyć każdego ważnego trybu adresowania pamięci z tą instrukcją: call DWordVar call DwordTbl [bx] call dword ptr [bx] itd. Jest względnie łatwo zsyntetyzować instrukcję call używając dwóch lub trzech innych instrukcji 80x86.Możemy stworzyć odpowiednik bliskiego wywołania używając instrukcji push i jmp: push jmp podprogram Dalekie wywołanie będzie podobne, będziemy musieli dodać instrukcję push cs przed tymi dwoma instrukcjami aby odłożyć daleki adres powrotu na stos. Instrukcja ret (powrót) jest instrukcją powrotu z podprogramu. Robi to przez zdjęcie adresu powrotu ze stosu i steruje przesyłaniem danych do instrukcji spod tego adresu powrotu. Wewnątrzsegmentowy (bliski) powrót zdejmuje 16 bitowy adres powrotu ze stosu do rejestru ip. Międzysegmentowy (daleki) powrót zdejmuje 16 bitowy offset do rejestru ip a potem 16 bitową wartość segmentu do rejestru cs. Instrukcje te są faktycznie równe następującym: retn pop ip retf popd cs:ip Musimy wyraźnie dopasować bliskie wywołanie podprogramu z bliskim powrotem a dalekie wywołanie podprogramu z odpowiadającym mu dalekim powrotem. Jeśli wymieszamy bliskie wywołanie z dalekim powrotem lub vice versa, zostawimy stos w niespójnym stanie i prawdopodobnie nie powrócimy do właściwej instrukcji po wywołaniu. Oczywiście, inną ważną kwestia kiedy używamy instrukcji call i ret jest to, że musimy upewnić się, że nasz podprogram nie odkłada czegoś na stos a potem spotka go niepowodzenie przed próbą powrotu z wywołania. Problemy ze stosem są główną przyczyną błędów w podprogramach języka asemblera. Rozważmy następujący kod: Podprogram: push ax push bx pop bx ret call Podprogram Instrukcja call odkłada na stos adres powrotu na stos a potem steruje przepływem danych do pierwszej instrukcji Podprogramu. pierwsze dwie instrukcje push odkładają rejestry ax i bx na stos, przypuszczalnie żeby zachować ich wartości ponieważ Podprogram je modyfikuje. Niestety ,istnieje błąd programistyczny w powyższym programie, Podprogram tylko zdejmuje bx ze stosu ,ale zapomina zdjąć również ax. To znaczy, że kiedy podprogram próbuje wrócić do wywołania, wartość ax prędzej niż inne zwróci adres powrotu, który siedzi na szczycie stosu. Dlatego też, ten Podprogram zwraca sterowanie do adresu wyszczególnionego przez wartość początkową rejestru ax zamiast prawdziwego adresu powrotu .Ponieważ jest 65,536 różnych wartości jakie może mieć ax, jest jedna szansa na 65,536 ,że nasz kod powróci do prawdziwego adresu powrotu, Bardziej prawdopodobne, że kod taki jak ten zawiesi naszą maszynę. Morał z tej historii – zawsze upewniaj się, że adres powrotu jest usadowiony na stosie przed wykonaniem instrukcji powrotu. Podobnie jak instrukcja call, jest bardzo łatwo do symulowania instrukcji ret używając dwóch instrukcji 80x86.Wszystko co musimy zrobić to zdjąć adres powrotu ze stosu a potem skopiować go do rejestru ip .Dla bliskiego powrotu, jest to bardzo prosta operacja, zdejmujemy bliski adres powrotu ze stosu a potem skaczemy pośrednio do tego rejestru.: pop ax jmp ax Symulacja dalekiego powrotu jest trochę bardziej trudniejsza ponieważ musimy załadować cs:ip w pojedynczej operacji. Jedyną instrukcją która to robi jest instrukcja jmp mem32. Są dwie różne formy instrukcji ret. Są one identyczne do tych powyższych z wyjątkiem 16 bitowego przesunięcia ich opcodów. CPU dodaje te wartości do wskaźnika stosu bezpośrednio po zdjęciu adresu powrotu ze stosu. Mechanizm ten usuwa parametry włożone na stos przed powrotem z wywołania. Asembler pozwala nam pisać bez przyrostków „f” lub „n” .jeśli to zrobimy, asembler wykombinuje czy powinien wygenerować bliski czy daleki powrót.
6.9.3 INSTRUKCJE INT,INTO,BOUND I IRET Instrukcja int (wywołanie programu obsługi przerwania) jest bardzo specjalną formą instrukcji call. Podczas gdy instrukcja call wywołuje podprogram wewnątrz naszego programu, instrukcja int wywołuje system podprogramów i inne specjalne podprogramy. Główna różnica między podprogramem obsługi przerwań a standardowymi procedurami jest taka ,że możemy mieć każdą liczbę różnych procedur w programie asemblerowym, podczas gdy system wspiera maksimum 256 różnych podprogramów obsługi przerwań. Program wywołuje podprogram poprzez wyszczególnienie adresu tego podprogramu; wywołuje podprogram obsługi przerwań przez wyszczególnienie numeru przerwania dla poszczególnego podprogramu obsługi przerwań. ten rozdział omawia tylko jak wywołać podprogram obsługi przerwań, używając instrukcji int, into i bound, i jak powrócić z podprogramu obsługi przerwań używając instrukcji iret. Są cztery różne formy instrukcji int. Pierwszą formą jest int nm (gdzie „nm” jest wartością między 0 a 256).Pozwala ona nam wywołać jedno z 256 różnych podprogramów przerwań. Forma ta instrukcji int jest długa na dwa bajty. Pierwszym bajtem jest opcod int. Drugim bajtem jest dana bezpośrednia zawierająca numer przerwania. Chociaż możemy używać instrukcji int do wywołania procedur (podprogram obsługi przerwań),pierwszym celem tej instrukcji jest wywołanie funkcji systemowej. Funkcja systemowa jest procedurą dostarczaną przez system taki jak DOS, mysz PC-BIOS lub kilka innych programów rezydujących w maszynie przed tym zanim program zacznie się wykonywać. Ponieważ zawsze odwołujemy się do wyszczególnionej funkcji systemowej przez jej numer przerwania zamiast adres, nasz program nie musi znać aktualnego adresu podprogramu w pamięci. Instrukcja int dostarcza łączenia dynamicznego do naszego programu. CPU określa aktualny adres podprogramu obsługi przerwań w czasie wykonania poprzez szukanie tego adresu w tablicy wektora przerwań. Pozwala to autorowi takiego systemu podprogramów do zmiany kodu (wliczając w to punkt wejścia) bez obawy o starsze programy które wywołują podprogram obsługi przerwań .Tak długo jak funkcja systemowa używa tego samego numeru przerwania, CPU automatycznie wywołuje podprogram obsługi przerwań spod nowego adresu. Jedyny problem z instrukcją int jest taki, że wspiera tylko 256 różnych podprogramów obsługi przerwań. MS-DOS sam używa ponad 100 różnych wywołań. BIOS i inne oprogramowanie systemowe dostarcza tysiące innych. Te i wszelkie inne wszystkie przerwania zarezerwowane przez Intel dla przerwań sprzętowych i pułapek. Powszechnym rozwiązaniem w większości funkcji systemowych jest stosowanie pojedynczego numeru przerwania dla danej klasy wywołania a potem podanie numery funkcji w jednym z rejestrów 80x86 (typowo rejestrze ax).Na przykład, MS-DOS używa tylko pojedynczego numeru przerwania, 21h.Wybierając szczególną funkcję DOS, ładujemy kod funkcji DOS do rejestru ah przed wykonaniem instrukcji int 21h.Na przykład, przerwanie programu i przywrócenie sterowania do MS-DOS wymaga załadowania 4Ch i wywołania DOSa instrukcją int 21h: mov ah,4ch int 21h Przerwanie klawiatury BIOS jest dobrym przykładem. Przerwanie 16h jest odpowiedzialne za testowanie i odczytywanie danych z klawiatury .ten podprogram BIOS dostarcza kilku wywołań do odczytu znaków i kodu klawisza klawiatury, aby zobaczyć czy jakieś klawisze są dostępne w systemowym buforze, sprawdza stan klawiatury modyfikując flagi, i wiele innych. Wybierając poszczególne operacje ładujemy numer funkcji do rejestru ah przed wykonaniem int 16h.Następująca tabela wymienia możliwe funkcje:
Tabela 31: Funkcje wspierające klawiaturę BIOS Na przykład odczytując znak z bufora systemowego pozostawiamy kod ASCII w al., możemy użyć kodu: mov ah, 0 ;czekanie na dostępny kod ,a potem int 16h ;odczyt tego klawisza mov znak, al. ;zachowanie odczytanego znaku Podobnie, jeśli chcielibyśmy przetestować bufor aby zobaczyć czy klawisz jest dostępny bez odczytywania naciśniętego klawisza, możemy użyć następującego kodu: mov ax, 1 ;testujemy aby zobaczyć czy klawisz jest dostępny int 16h ;ustawiamy flagę zera jeśli klawisz nie jest dostępny Po więcej informacji o PC-BIOS i MS-DOS zobacz w „MS-DOS,PC-BIOS i pliki I/O” Drugą formą instrukcji int jest specjalny przypadek: Int 3 Int 3 jest specjalną formą instrukcji przerwania, która jest długości jednego bajta. CodeView i inne debuggery używają jej jako instrukcji programowego przerwania. Kiedykolwiek ustawimy punkt przerwania na instrukcji w naszym programie, debugger zastąpi pierwszy bajt opcodu instrukcji instrukcją int 3.Kiedy nasz program wykona instrukcję int 3,odwoła się do „funkcji systemowej’ debuggera, więc debugger odbierze sterowanie z CPU. Kiedy się to stanie, debugger zastąpi instrukcję int 3 oryginalnym opcodem. Kiedy działamy wewnątrz debuggera, możemy używać instrukcji int 3 do zatrzymania wykonywania programu i zwrócenia sterowania do debuggera. Nie jest to normalny sposób zatrzymywania programu.. Jeśli spróbujemy wykonać instrukcję int 3 podczas działania pod DOSem, zamiast pod kontrolą debuggera, możemy łatwo załatwić system. Trzecią formą instrukcji int jest into .Into będzie powodowało programowe przerwanie jeśli flag przepełnienia będzie ustawiona. Możemy użyć tej instrukcji do szybkiego testowania dla arytmetycznego przepełnienia po wykonaniu instrukcji arytmetycznych .Semantycznie instrukcja ta jest odpowiednikiem: if overflow = 1 then int 4 Nie powinniśmy używać tej instrukcji jeżeli nie dostarczymy odpowiedniej procedury pułapki (podprogram obsługi przerwań).Robiąc to spowodujemy prawdopodobnie zawieszenia systemu. Czwartym przerwaniem programowym dostarczonym przez procesory 80286 i późniejsze, jest instrukcja bound. Instrukcja ta przyjmuje formę: bound reg, mem i wykonuje następujący algorytm: if (reg < [mem]) or (reg > [mem+sizeof(reg)]) then int 5 [mem] oznacza zawartość komórki pamięci a sizeof (reg0 to dwa lub cztery w zależności od tego czy rejestr jest szeroki na 16 czy 32 bity. Operand pamięci musi mieć podwójny rozmiar w stosunku do operandu rejestru. Instrukcja bound porównuje wartości używając całkowitego porównania ze znakiem.
Projektanci Intela dodali instrukcję bound pozwalając szybko sprawdzić zakres wartości w rejestrze. Jest to użyteczne w Pascalu, który sprawdza zakres tablicy a po sprawdzeniu sprawdza czy ciąg liczb integer jest w poprawnej granicy. Są dwa problemy z tą instrukcją. Na procesorach 80486 i Pentium/586 instrukcja bound jest generalnie wolniejsza niż instrukcji które podmienia: cmp reg, LowerBound jl OutOfBound cmp reg, UpperBound jg OutOfBound Na chipach 80486 i Pentium/586,powyższa sekwencja wymaga tylko czterech cykli zegarowych ,zakładając ,że możemy użyć bezpośredniego trybu adresowania i rozgałęzienia nie są pobierane; instrukcja bound wymaga 7-8 cykli zegarowych w podobnych okolicznościach i również zakładając, że operandy pamięci są w pamięci podręcznej. Drugi problem z instrukcją bound jest taki, że wykonuje się jako int 5 jeśli wyszczególniony rejestr jest poza zakresem. IBM, w swojej nieskończonej mądrości, postanowił używać przerwania int 5 do drukowania ekranu. Dlatego też jeśli wykonujemy instrukcję bound i wartość jest poza zakresem, system, domyślnie ,zacznie drukować zawartość ekranu na drukarce. Jeśli zamienimy procedurę int 5 na jeden z naszych w własnych, naciskając klawisz PrtSc przekażemy sterowanie do podprogramu naszej instrukcji bound. Chociaż są sposoby ominięcia tego problemu, większość ludzi nie martwi się tym ponieważ instrukcja bound jest zbyt wolna. Jakąkolwiek instrukcję int wykonujemy, wystąpi następująca sekwencja zdarzeń: • 80x86 odkłada rejestr flag na stos • 80x86 odkłada cs a potem ip na stos • 80x86 używa numeru przerwania (into jest przerwaniem #4,bound #5) razy cztery jako indeks do tablicy wektora przerwań i kopiuje podwójne słowo z punktu w tablicy do cs:ip. Instrukcje int różnią się od call w dwóch głównych punktach .Po pierwsze, instrukcje call różnią się w długości od dwóch do sześciu bajtów długości ,podczas gdy instrukcje int są generalnie długie na dwa bajty (int 3,into i bound są wyjątkami)Po drugie, i bardzo ważne, instrukcja int odkłada flagi i adres powrotu na stos podczas gdy instrukcja call odkłada tylko adres powrotu. Zauważmy również, że instrukcja int zawsze odkłada daleki adres powrotu (tj. wartość cs i offset wewnątrz segmentu kodu),gdy dalekie wywołanie odkłada podwójne słowo adresu powrotu. Ponieważ int odkłada flagi na stos musimy użyć specjalnej instrukcji powrotu, iret (powrót z przerwania),powrót z podprogramu wywołującego przez instrukcję int. Jeśli wracamy z procedury przerwania używając instrukcji ret ,flagi pozostaną na stosie by powrócić do tego wywołania który je przywołał .Instrukcja iret jest odpowiednikiem sekwencji dwóch instrukcji: ret, popf(zakładając, że wykonaliśmy popf przed zwróceniem sterowania pod adres wskazywany przez podwójne słowo na szczycie stosu). Instrukcje int czyszczą flagę śledzenia (T) w rejestrze flag. nie wpływają na żadną inna flagę. Instrukcja iret, przez wzgląd na swoją naturę, może wpływać na wszystkie flagi ponieważ zdejmuje flagi ze stosu. 6.9.4 INSTRUKCJE SKOKÓW WARUNKOWYCH Chociaż instrukcje jmp, call i ret dostarczają sterowania przepływem danych, nie pozwalają nam na podjęcie poważnych decyzji. Instrukcje skoków warunkowych wykonują to zadanie .Instrukcje skoków warunkowych są podstawowymi narzędziami dla tworzenia pętli i innych warunkowo wykonywanych instrukcji takich jak if...then. Skoki warunkowe testują jedną z wielu flag w rejestrze flag aby zobaczyć czy pasują do wzorca (podobnie jak instrukcje setcc).Jeśli wzór pasuje, sterownie przepływem danych jest przekazywane do lokacji docelowej .Jeśli wzór nie odpowiada, CPU ignoruje skok warunkowy i kontynuuje wykonywanie od następnej instrukcji. Niektóre instrukcje, testują warunki flag znaku, przepełnienia, przeniesienia i zera. Na przykład, po wykonaniu instrukcji przesunięcia w lewo, możemy sprawdzić flagę przeniesienia aby ustalić czy przeniesiono jeden poza bardziej znaczący bit operandu. podobnie, możemy sprawdzić warunek flagi zera po instrukcji test aby zobaczyć czy wyszczególniony bit był jedynką. Większość czasu jednak, będziemy prawdopodobnie wykonywać skoki warunkowe po instrukcji cmp. Instrukcja cmp ustawia flagi ,więc możemy sprawdzić dla mniejsze niż, większe niż, równe itp. Notka: Dokumentacja Intelowska definiuje kilka synonimów lub aliasów instrukcji dla wielu instrukcji skoków warunkowych. Następujące tablice wyliczają wszystkie aliasy dla poszczególnych instrukcji. Tablice te również wyliczają skoki przeciwne Zobaczymy wkrótce cel przeciwnych rozgałęzień.
Tablica 32: Instrukcje Jcc, które testują flagi
Tablica 33:Instrukcje Jcc dla porównań bez znakowych
Tablica 34 Instrukcje Jcc dla porównań ze znakiem W procesorach 80286 i wcześniejszych, instrukcje te wszystkie są dwubajtowe. Pierwszy bajt jest jednym bajtem opcodu następujący po jednym bajcie przesunięcia. Chociaż to prowadzi do instrukcji o bardzo małych wymiarach, pojedynczy bajt przesunięcia pozwala tylko na zakres +/- 138 bajtów .Jest prosta sztuczka, którą możemy zastosować aby przezwyciężyć to ograniczenie na wcześniejszych procesorach: • Jakikolwiek skok użyjemy ,zamieniamy na formę przeciwną (podaną w powyższych tabelach) • Jeśli wybraliśmy skok przeciwny, używamy instrukcji jmp której adres docelowy jest oryginalnym adresem docelowym. Na przykład konwersja: jc Cel używa sekwencji instrukcji: jnc SkiJmp jmp Cel SkiJmp: Jeśli flaga przeniesienia jest czysta (NC= no carry – żadnego przeniesienia),wtedy sterowanie jest przenoszone do etykiety SkiJmp, do tego samego miejsca, gdybyśmy użyli instrukcji jc. Jeśli flaga przeniesienia jest ustawiona kiedy napotykamy tą sekwencję, sterownie zostanie przekazane poprzez instrukcję jnc do instrukcji jmp ,która przeniesie sterowanie do celu .Ponieważ instrukcja jmp pozwala na 16 bitowe przesunięcie i daleki operand, możemy skoczyć gdziekolwiek w pamięci używając tej sztuczki, Krótki komentarz do kolumny „przeciwne” Jak wspomniano powyżej, kiedy musimy ręcznie rozszerzyć rozgałęzienie z +/- 128 powinniśmy wybrać rozgałęzienie przeciwne by móc wykonać skok do lokacji docelowej. Jak już widzieliśmy w kolumnie „aliasy” ,wiele instrukcji skoków warunkowych ma aliasy. To znaczy, że będą również aliasy dla skoków przeciwnych. Nie używamy żadnych aliasów kiedy rozszerzamy rozgałęzienia które są poza zakresem.Za wyjątkiem dwóch wyjątków, bardzo prosta zasada opisuje jak wygenerować rozgałęzienie przeciwne: • Jeśli druga litera instrukcji jcc nie jest „n”, wprowadzamy „n” po „j”. Np. je staje się jne a jl jnl • Jeśli druga litera instrukcji jcc jest „n” wtedy usuwamy to „n” z instrukcji. Np. jng staje się jg a jne je Te dwa wyjątki od tej zasady to jpe (skok przy parzystości ) i jpo (skok przy braku parzystości).Wyjątki te powodują kilka problemów ponieważ (a) prawie zawsze musimy sprawdzać flagę parzystości (b) możemy użyć aliasów jp i jnp jao synonimów dla jpe i jpo. Zasada „N /No N” stosuje się do jp i jnp. Mimo, że wiemy iż jge jest przeciwne jl, miejmy w zwyczaju używanie jnl zamiast jge. Jest zbyt łatwo w ważnej sytuacji zacząć myśleć „większe jest w opozycji do mniejsze” i zastąpić jg. Możemy uniknąć tej pomyłki zawsze używając zasady „N / No N” MASM 6x i wiele innych nowoczesnych asemblerów 80x86 automatycznie konwertuje poza zakresem rozgałęzienia dla tej sekwencji. Jest opcja która pozwala nam zablokować tą cechę. Dla wykonania krytycznego kodu pracującego na 80286 i wcześniejszych procesorach ,możemy chcieć zablokować tą cechę więc możemy
poprawić własne rozgałęzienie. Powód jest bardzo prosty, ta prosta poprawka zawsze uniknie potoku niezależnie jest prawdziwa ponieważ CPU skacze w obu przypadkach. Jedna miła rzecz skoków warunkowych jest taka, że nie opróżniamy potoku i kolejki rozkazów jeśli nie wykonujemy rozgałęzienia. Jeśli jeden warunek jest prawdziwy dużo bardziej niż inny, możemy chcieć użyć skoku warunkowego do przeniesienia sterowania do pobliskiego jmp ,więc możemy kontynuować tak jak przedtem Na przykład, jeśli mamy instrukcję je cel i cel jest poza zakresem ,możemy skonwertować ja na następujący kod: je GotoTarget GotoTarget: jmp Cel Chociaż rozgałęzienie do celu wymaga teraz dwóch skoków, jest to bardziej wydajne niż standardowa konwersja jeśli flaga zera jest normalnie wyczyszczona kiedy wykonujemy instrukcję je. Procesor 80386 i późniejsze, dostarcza rozszerzonej formy skoku warunkowego który jest czterobajtowy ,z ostatnimi dwoma bajtami zawierającymi 16 bitowe przesunięcie. Te skoki warunkowe mogą przekazywać sterowanie gdziekolwiek wewnątrz bieżącego segmentu kodu. Dlatego też, nie musimy się martwić o ręczne rozszerzanie zakresu skoku. Jeśli powiemy MASMowi, że używamy 80386 lub późniejszych procesorów, on automatycznie wybierze dwa lub cztery bajty jeśli będzie to konieczne. Zobacz Rozdział Ósmy, uczący jak powiedzieć MASMowi ,że używamy procesora 80386 lub późniejszych. Instrukcje skoków warunkowych 80x86 dają nam możliwość podziału przebiegu programu na jedną z dwóch części w zależności kilku warunków logicznych. Przypuśćmy, że chcemy zwiększyć rejestr ax jeśli bx jest równy cx. Możemy osiągnąć to za pomocą następującego kodu: cmp bx, cx jne SkipStmts inc ax SkipStmts: Sztuczką jest użycie rozgałęzienia „przeciwnego” aby przeskoczyć nad instrukcją którą chcemy wykonać jeśli warunek jest prawdziwy. Zawsze używamy zasady „rozgałęzienia przeciwnego (N /no N)” podanej wcześniej do wyboru rozgałęzienia przeciwnego. Możemy zrobić ten sam błąd wybierając rozgałęzienie przeciwne kiedy mogliśmy to zrobić poza zakresem skoków. Możemy również użyć instrukcji skoków warunkowych do połączenia pętli. Na przykład, następująca sekwencja kodu odczytuje sekwencję znaków od użytkownika i przechowuje każdy znak w kolejnych elementach tablicy do chwili kiedy użytkownik naciśnie klawisz ENTER (powrót karetki): mov di, 0 ReadLnLoop: mov ah, 0 ;INT 16h czyta opcod klawisza int 16h mov Input[di], al. inc di cmp al., 0dh ;kod ASCII powrotu karetki jne ReadLnLoop mov Input [di-1],0 Po więcej informacji dotyczącej połączenia instrukcji IF, pętli i innych struktur sterujących zobacz „Struktury Sterujące” Podobnie jak instrukcje setcc, instrukcje skoków warunkowych dzielą się na dwie podstawowe kategorie – te które testują wyszczególnione wartości flag (np. jz, jc, jno) i te ,które testują inne warunki(mniejszy niż, większy niż itp.).kiedy sprawdzamy warunek, instrukcje skoków warunkowych prawie zawsze występują po instrukcji cmp. Instrukcja cmp ustawia flagi, więc możemy używać instrukcji ja, jae, jb, jbe, je lub jne do sprawdzania bez znakowego mniejszego niż, mniejszego lub równego, równości, nierówności, większego niż lub większego niż lub równego. Równocześnie instrukcja cmp ustawia flagi więc możemy również zrobić porównania ze znakiem używając instrukcji jl, jle, je, jne, jg i jge. Instrukcje skoków warunkowych tylko sprawdzają flagi, nie wpływają na żadną z flag 80x86. 6.9.5 INSTRUKCJE JCXZ/JECXZ Instrukcja jcxz(skocz jeśli cx wynosi zero) rozgałęzia do adresu docelowego jeśli cx zawiera zero. Chociaż możemy użyć jej zawsze musimy wiedzieć, że czy cx zawiera zero, moglibyśmy normalnie użyć jej przed pętlą skonstruowaną w oparciu o instrukcję loop. Instrukcja loop może powtarzać sekwencję operacji cx razy .Jeśli cx równa się zero, loop będzie powtarzał operację 65,536 razy. Możemy użyć jcxz do przeskoczenia takiej pętli kiedy cx wynosi zero.
Instrukcja jecxz, dostępna tylko na 80386 i późniejszych procesorach, robi zasadniczo taką samą prace jak jcxz ,z wyjątkiem tego, że testuje pełny rejestr ecx. Zauważmy ,że instrukcja jcxz sprawdza tylko cx ,nawet na 80386 w 32 bitowym trybie. Nie ma „przeciwnych” instrukcji do jcxz i jecxz. Dlatego też ,nie możemy użyć zasady „N /No N” do rozszerzenia instrukcji jcxz i jecxz. Najłatwiejszym sposobem rozwiązania tego problemu jest rozbicie instrukcji na dwie instrukcje które wykonują to samo zadanie: jcxz Cel test cx, cx ;ustawienie flagi zera jeśli cx = 0 je Cel teraz możemy już łatwo rozszerzyć instrukcję je używając techniki z poprzedniej sekcji. Instrukcja test ustawia flagę zera jeśli i tylko jeśli cx zawiera zero. W końcu, jeśli są jakieś niezerowe bity w cx, logiczne dodanie ich z nimi samymi stworzy nie zerowy wynik. Jest to doby sposób aby sprawdzić czy 16 lub 32 bitowy rejestr zawiera zero. Faktycznie ta sekwencja tych instrukcji jest szybsza niż instrukcja jcxz na 80486 i późniejszych procesorów. Intel rekomenduje użycie tej sekwencji zamiast instrukcji jcxz jeśli zależy nam na szybkości .Oczywiście, instrukcja jcxz jest krótsza niż sekwencja dwóch instrukcji, ale nie jest szybsza. Jest to dobry przykład wyjątku od zasady „krótsze jest zazwyczaj szybsze”. Instrukcja jcxz nie wpływa na żadną z flag. 6.9.6 INSTRUKCJA LOOP Ta instrukcja zmniejsza rejestr cx a potem rozgałęzia do lokacji docelowej jeśli rejestr cx nie zawiera zera. Ponieważ instrukcja ta zmniejsza cx, wtedy sprawdza dla zera czy cx początkowo zawierał zero, każda pętla jaką stworzymy używa instrukcji loop 65,536 razy. Jeśli nie chcemy wykonywać pętli kiedy cx zawiera zero ,użyjemy jcxz do przeskoczenia pętli. Nie ma „przeciwnych” form instrukcji loop, i podobnie jak instrukcje jcxz /jecxz zakres jej jest ograniczony do +/- 128 bajtów na wszystkich procesorach. Jeśli chcemy rozszerzyć zakres tej instrukcji, będziemy musieli ją rozbić: ;”loop lbl”: dec cx jne lbl Możemy łatwo rozszerzyć jne na każdą odległość. Nie ma instrukcji loop która zmniejsza ecx i rozdziela jeśli nie zero (jest instrukcja loope, ale robi ona całkowicie coś innego).Powód jest całkiem prosty. Wraz z 80386,projektanci Intela całkowicie wstrzymali rozwój instrukcji loop. Oh, jest ona dla zapewnienia zgodności ze starszym kodem ,ale okazuje się, że instrukcje dec /jne są rzeczywiście szybsze na procesorach 32 bitowych. Problemy w dekodowaniu instrukcji i działaniach potoku odpowiadają za ten dziwny zbieg zdarzeń. Chociaż nazwa instrukcji loop sugeruje, że możemy normalnie tworzyć z nią pętle, musimy pamiętać ,że wszystko co rzeczywiście możemy z nią zrobić to zmniejszanie cx i rozgałęzienie do adresu docelowego ,jeśli cx nie zawiera zera po zmniejszeniu. Możemy użyć tej instrukcji gdzie chcemy zmniejszyć cx a potem sprawdzić wynik dla zera .Pomimo to, jest to bardzo dogodna instrukcja do używania jeśli po prostu chcemy powtórzyć sekwencję instrukcji jakąś ilość razy. Na przykład. następująca inicjuje 256 elementów tablicy bajtów wartościami1,2,3... mov ecx, 255 ArrayLp: mov Array[ecx], sl loop ArrayLp mov Array[0], 0 Ostania instrukcja jest konieczna ponieważ pętla nie powtarza się kiedy cx wynosi zero. Dlatego też, w związku z ostatnią instrukcją ,ostatnim elementem tablicy na której działa loop jest Array[1]. Instrukcja loop nie wpływa na żadną z flag. 6.9.7 INSTRUKCJE LOOPE/LOOPZ Loope /loopz rozgałęziają do adresu docelowego jeśli cx nie jest równe zero a flaga zera jest ustawiona. Instrukcja ta jest całkiem użyteczna po instrukcjach cmp i cmps, i jest odrobinę szybsza niż porównywalne instrukcje 803886/496 jeśli używamy wszystkich cech tej instrukcji .Jednakże instrukcja ta dokonuje spustoszenia w potoku i operacjach superskalarnych Pentium, więc prawdopodobnie przyzwyczaić się do instrukcji dyskretnych zamiast tej instrukcji. Instrukcja ta robi co następuje: cx := cx -1 ix Flaga_Zera = 1 and cx ≠ 0 , goto cel
Instrukcja loope zawodzi jeśli występuje jeden z dwóch warunków. Albo flaga zera jest wyzerowana albo instrukcja zmniejszyła cx do zera.. Poprzez testowanie flagi zera po instrukcji loop (instrukcją je lub jne na przykład),możemy określamy przyczynę zatrzymania. Instrukcja ta jest użyteczna jeśli musimy powtórzyć pętle jeśli jakaś wartość jest równa innej, ale jest maksymalna liczba iteracji na jaką możemy pozwolić. Na przykład następująca pętla przeszukuje tablicę szukając pierwszego nie zerowego bajtu, ale nie szuka poza końcem tablicy: mov cx, 16 ;max. 16 elementów tablicy mov bx, -1 ;indeks do tablicy (następne inc) SerachLp: inc bx ;przesuwa się na następny element tablicy cmp Array[bx], 0 ;zobacz czy ten element to zero loope SearchLp ;powtórz jeśli jest je AllZero ;skocz jeśli wszystkie elementy były zerami Zauważmy, że instrukcja ta nie jest przeciwieństwem loopnz / loopne .Jeśli musimy rozszerzyć ten skok poza +/- 128 bajtów, będziemy musieli połąćzyć tą instrukcję używając instrukcji dyskretnych. Na przykład, jeśli cel loope jest poza zakresem, będziemy musieli użyć sekwencji instrukcji podobnej do tej: jne quite dec cx je quite2 jmp cel quite: dec cx ; loope zmniejsza cx, nawet jeśli ZF = 0 quite2: Instrukcja loope /loopz nie wpływają na żadną flagę. 6.9.8 INSTRUKCJA LOOPNE/LOOPNZ Instrukcja ta jest podobna do instrukcji loope/ loopz z poprzedniej sekcji z wyjątkiem loopne /loopnz powtarza gdy cx nie jest zerem i flaga zera jest wyczyszczona. Algorytm: cx := cx – 1 if Flaga_Zera = 0 and cx ≠ 0 , goto cel Możemy określić czy instrukcja loopne zakończy ponieważ cx było zerem lub czy flaga zera była ustawiona przez testowanie flagi zera bezpośrednio po instrukcji loopne. Jeśli flaga zera jest wyczyszczona w tym momencie, instrukcja loopne nie dochodzi do skutku ponieważ zmniejsza ona cx do zera. W przeciwnym razie nie dochodzi do skutku, ponieważ flaga zera była ustawiona. Instrukcja ta nie jest przeciwieństwem instrukcji loope / loopz. Jeśli adres docelowy jest poza zakresem, będziemy musieli użyć sekwencji instrukcji podobnej do tej: je quit dec cx je Quit2 jmp Cel quit: dec cx ;loopne zmniejsza cx ,nawet jeśli ZF = 1 quit2: Możemy użyć instrukcji loopne do powtarzania określonej ilości skoków podczas czekania na jeden warunek by był prawdziwy .Na przykład, możemy przeszukać tablicę aż do wyczerpania liczby elementów tablicy lub aż do znalezienia pewnego bajtu używając pętli jak następuje: mov cx, 16 ;Maksimum elementów tablicy mv bx, -1 ;indeks do tablicy LoopWhlNot0: inc bx ;Przesunięcie na następny element cmp Array[bx], 0 ;czy ten element zawiera zero? loopne LoopWhlNot0 ;wyjście jeśli tak lub więcej niż 16 bajtów Chociaż instrukcja loope/ loopz i loopne/ loopnz są wolniejsze niż pojedyncze instrukcje z których mogłyby być połączone, jest jedno główne zastosowanie dla tych form instrukcji gdzie szybkość nie jest tak ważna: bycie szybszym powoduje ich mniejszą użyteczność – pętle z licznikiem podczas operacji I/O. Przypuśćmy ,że bit #7 na wejściu portu 379h zawiera jeden jeśli urządzenie jest zajęte i zawiera zero jeśli urządzenie nie jest zajęte. Jeśli chcemy wysłać dane do portu możemy użyć następującego kodu: mov dx, 379h WaitNoBusy: in al., dx ;pobiera port test al., 80h ;patrzy czy bit #7 jest jedynką jne WaitNoBusy ;Czekaj jeśli :nie zajęty” Jedyny problem z tą pętlą jest taki, że jest niewykluczone iż będzie to pętla nieskończona. W prawdziwym systemie, przewód może być wyłączony, ktoś mógł wyłączyć urządzenie peryferyjne, i wiele innych rzeczy może się wydarzyć złych, które spowodują zawieszenie systemu. Dobry program zwykle nakłada
limit czasowy na pętlę taka jak ta. Jeśli urządzenie wydaje się być zajęte przez określoną ilość czasu, wtedy pętla kończy się i wzbudza warunek błędu .Realizuje to następujący kod: mov dx, 379h ;adres portu wejściowego mov cx, 0 ;Pętla 65,536 razy a potem wyjście WaitNo Busy: in al., dx ;Pobieranie danych z portu test al., 80h ;zobacz czy zajęty loopne WaitNoBusy ;powtórz jeśli zajęty i licznik nie wyzerowany jne TimeOut ;rozgałęzienie jeśli CX=0 ponieważ skończył się czas Możemy użyć instrukcji loope/ loopz jeśli bit był zerem a nie jedynką. Instrukcja loopne /loopnz nie wpływają na żadną flagę 6.10 INSTRUKCJE RÓŻNORODNE Jest kilka różnorodnych instrukcji w 80x86 które nie podpadają pod żadną z wyżej wymienionych kategorii .generalnie są to instrukcje które manipulują pojedynczymi flagami ,dostarczają specjalnej obsługi procesora lub operują operacjami uprzywilejowanego trybu. Jest kilka instrukcji które bezpośrednio manipulują flagami w rejestrze flag 80x86. Są to: * clc Wyzerowanie flagi przeniesienia * stc Ustawienie flagi przeniesienia * cmc Zamiana wartości flagi przeniesienia na przeciwną * cld Wyzerowanie flagi kierunku * std Ustawienie flagi kierunku * cli Wyzerowanie flagi zezwolenia na przerwanie * sti Ustawienie flagi zezwolenia na przerwanie Notka: powinniśmy być ostrożni kiedy używamy instrukcji cli w naszych programach .Niewłaściwe zastosowanie może zawiesić maszynę do momentu wyłączenia z prądu Instrukcja nop nie robi nic za wyjątkiem marnowania kilu cykli procesora i zajmowania bajtu pamięci. Programiści często używają jej jako wypełniacz lub wspomaganie debuggowania .Okazuje się, że nie jest to wyjątkowa instrukcja, jest synonimem instrukcji xchg ax, ax. Instrukcja hlt, zatrzymuje procesor aż do resetu, przerwań nie maskowalnych lub innych przerwań (zakładając, że przerwania są odblokowane) przychodzących .generalnie, nie powinniśmy używać tej instrukcji na IBM PC chyba, że rzeczywiście wiemy co robimy. Instrukcja ta nie jest odpowiednikiem instrukcji halt w x86.Nie używajmy jej do zatrzymywania naszych programów! 80x86 dostarcza innego przedrostka instrukcji ,lock, który podobnie jak instrukcja rep, wpływa na następujące instrukcje. jednakże. instrukcja ta ma trochę znaczeń na większości systemów PC .Jej celem jest koordynowanie systemów, które mają wiele CPU. Ponieważ systemy z wieloma CPU stają się dostępne ,ten przedrostek może w końcu okazać się cenny. Nie będziemy jednak zbytnio się tu na nim koncentrować. Pentium dostarcza dwóch dodatkowych instrukcji interesujących programistów w trybie rzeczywistym DOS. Instrukcje te to cpuid i rdtsc .Jeśli ładujemy eax zerem i wykonujemy instrukcję cpuid, Pentium (i późniejsze procesory) zwróci wartość maksymalną cpuid przeznaczoną jako parametr w eax. Dla Pentium, tą wartością jest jeden. Jeśli ładujemy rejestr eax jedynką i wykonujemy instrukcję cpuid, Pentium zwróci informację identyfikującą CPU w eax. Druga instrukcja Pentium interesująca to instrukcja rdtsc. Pentium utrzymuje 64 bitowy licznik, który liczy cykle zegarowe startując przy resecie. Instrukcja rdtsc kopiuje bieżącą wartość licznika do pary rejestrów edx:eax. Możemy użyć tej instrukcji do dokładnego odmierzania czasu sekwencji kodu. Poza instrukcjami przedstawionym do tej pory,80286 i późniejsze procesory dostarczają zbioru instrukcji trybu chronionego. Tekst ten nie będzie zajmował się nimi ponieważ są one użyteczne dla tych którzy piszą systemy operacyjne. Nie będziemy musieli nawet używać tych instrukcji w naszych aplikacjach kiedy uruchomimy w trybie chronionym systemu operacyjnego jak Windows, UNIX lub OS/2.Instrukcje te są zarezerwowane dal tych, którzy chcą pisać takie systemy operacyjne i sterowniki do nich. 6.14 PODSUMOWANIE Rodzina procesorów 80x86 dostarcza bogatego zbioru instrukcji CISC (komputer z pełną liczbą rozkazów).Członkowie rodziny procesorów 80x86 są generalnie zgodne z przyszłymi wersjami, w znaczeniu kolejnych procesorów wykonujących wszystkie instrukcje z poprzednich chipów. Programy pisane dla 80x86 generalnie pracują na wszystkich członkach rodziny, programy używające nowych instrukcji na 80286 będą działały na 80286 i późniejszych procesorach, ale nie na 8086.Podobnie programy które wykorzystują nowe instrukcje na 80386 będą działały na 80386 i późniejszych procesorach ale nie na wcześniejszych. I tak dalej. Procesory opisane w tym rozdziale zawierają 8086/8088, 80286,80386,80486 i Pentium(80586).Intel również stworzył 80186,ale ten procesor nigdy nie był używany intensywnie w komputerach osobistych.
Zbiór instrukcji 80x86 jest bardzo łatwo podzielić na osiem kategorii * Instrukcje przenoszenia danych * Konwersja * Instrukcje arytmetyczne * Instrukcje logiczne, przesunięcia, obrotu i bitowe * Instrukcje I/O * Instrukcje łańcuchowe * Instrukcje sterowania strumieniem danych w programie * Instrukcje różne Wiele instrukcji wpływa na kilka bitów flag w rejestrze flag 80x86.Pewne instrukcje mogą testować te flagi ponieważ są one wartościami boolowskimi. Flagi również oznaczają znaki po porównaniu takie jak równość, mniejszy niż i większy niż .Aby naumieć się o tych flagach i jak testujemy je w programie, zajrzyj • Rejestr Stanu Procesora (Flagi) • Instrukcje Warunkowe • Instrukcje skoków warunkowych Jest kilka instrukcji które przesyłają dane między rejestrami a pamięcią. Instrukcje te są jedynymi, których programista asemblerowy wzywa bardzo często.80x86 dostarcza wiele takich instrukcji które pomagają nam pisać szybsze i wydajniejsze programy. O szczegółach czytaj: • Instrukcje przenoszenia danych • Instrukcja MOV • Instrukcja Xchg • Instrukcje LDS,LES
Typowym zastosowaniem tych instrukcji jest uzyskiwanie dostępu do rejestrów sprzętowych urządzeń peryferyjnych. • Instrukcje I/O Rodzina 80x86 dostarcza dużego repertuaru instrukcji które manipulują łańcuchami danych. Instrukcje te to movs, lods, stos, scas, cmps ,ins, outs, rep, repz, repe, repnz i repne. • Instrukcje łańcuchowe Instrukcje sterowania przesyłaniem danych w 80x86 pozwalają nam tworzyć pętle, podprogramy ,sekwencje warunków i wiele innych testów. • Instrukcje sterowania przebiegiem programu • Skoki Bezwarunkowe • Instrukcje CALL i RET • Instrukcje INT,INTO,BOUND i IRET • Instrukcje skoków warunkowych • Instrukcje JCXZ/JECXZ • Instrukcja LOOP • Instrukcja LOOPE/LOOPZ • Instrukcja LOOPNE?LOOPNZ Na końcu rozdział ten omawia kilka różnorodnych instrukcji. Instrukcje te bezpośrednio manipulują flagami w rejestrze flag, dostarczają specjalnej obsługi procesora lub wykonują operacje trybu chronionego. Ten rozdział tylko wspomina o instrukcjach trybu chronionego. Ponieważ zwykle nie będziemy ich używać w programach (nie –O/S) użytkowych, nie musimy o nich wiedzieć • Instrukcje Różnorodne
6.15 PYTANIA 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19.
Dostarcz przykładu który pokazuje że wymagane jest n+11 bitów do przechowania sumy dwóch n-bitowych wartości ADC i SBB mogą być wykorzystane do zachowania się jak ADD I SUB przez włożenie kilku innych instrukcji między ADC i SBB. Jaka instrukcja musi być zastosowana przed ADC aby zachowywała się jak ADD a jaka przed SBB aby zachowywała się jak SUB? Przypuśćmy że możemy manipulować pozycjami danych na szczycie stosu używając PUSH i POP. Wyjaśnij jak możemy zmodyfikować adres powrotu na stosie żeby instrukcja RET spowodowała w 80x86 powrót dwóch bajtów poza oryginalny adres powrotu. Dostarcz czterech różnych sposobów na dodanie dwóch wartości w rejestrze BX. Żaden sposób nie powinien wymagać więcej niż dwóch instrukcji(podpowiedź, jest co najmniej sześć sposobów zrobienia tego) Załóżmy ,że adres docelowy dla następujących skoków warunkowych jest poza zakresem krótkiego rozgałęzienia. Zmodyfikuj każdą z tych instrukcji aby wykonywała właściwe działania a) JS Label1 b)JE Label2 c)JZ Label3 d)JC Label4 e)JBE There f)JG Label5 Wyjaśnij różnice pomiędzy flagą przeniesienia a flagą przepełnienia Kiedy używamy instrukcji CBW i CWD do powielenia wartości znaku? Jaka jest różnica pomiędzy instrukcjami „MOV reg, dana bezpośrednia” a „LEA reg, adres” Co robi instrukcja INT nm odkładając na stos to czego instrukcja CALL FAR nie robi? Jakie jest typowe zastosowanie instrukcji JCXZ Wyjaśnij działanie instrukcji LOOP,LOOPE/LOOPZ i LOOPNE/LOOPNZ Na jaki rejestr (inny niż rejestr flag) wpływają instrukcje MUL,IMUL,DU|IV i IDIV? Wypisz trzy różnice pomiędzy DEC AX i SUB AX,1 Która z instrukcji przesunięcia, obrotu i logiczna nie wpływa na flagę zera? Dlaczego instrukcja SAR zawsze zeruje flagę przepełnienia Na 80386 instrukcja IMUL jest prawie całkowicie ortogonalna. Prawie. Daj kilka przykładów form dozwolonych dla instrukcji ADD dla których nie ma porównywalnych instrukcji IMUL Dlaczego Intel uogólnił instrukcję IDIV a nie zrobił tego z instrukcją IMUL? Jakiej instrukcji będziemy musieli użyć aby odczytać ośmio bitową wartość spod adresu I/O 378h?Proszę podać określoną instrukcję do zrobienia tego. Której flagi(flag0 używa 80x86 do sprawdzenia bez znakowego przepełnienia arytmetycznego?
20. Jaka flaga (-i) pozwalają nam sprawdzić przepełnienie ze znakiem? 21. Jakiej flagi (flag) używa 80x86 do testowania następujących warunków bez znakowych? Ile musi być ustawionych flag aby warunek był prawdziwy? a) równy b) nie równy c) mniejszy niż d)mniejszy niż lub równy e)większy niż f) większy niż lub równy 22. Powtórz powyższe pytanie dla porównania ze znakiem 23. Wyjaśnij działanie instrukcji CALL i RET 80x86.Opisz krok po kroku każdy wariant tych instrukcji 24. Następująca sekwencja wymienia wartości pomiędzy dwoma zmiennymi pamięci I i J: xchg ax ,i xchg ax, j xchg ax, i Na 80486,instrukcje „MOV reg, mem” i „MOV mem, reg” zabierają tylko jeden cykl podczas gdy „Xchg reg, mem” zabiera trzy cykle. Pokaż szybszą sekwencję dla ‘486 niż powyższa. 25. Na 80386 instrukcja „MOV reg ,mem” wymaga czterech cykli ,”MOV mem ,reg” wymaga dwóch cykli a „Xchg reg, mem” wymaga pięciu cykli. Pokaż szybszą sekwencję problemu wymiany pamięci w pytaniu 24 dla 80386 26. Na 80486 instrukcje „MOV acc, mem” i „MOV reg, mem” wszystkie zabierają tylko jeden cykl do wykonania. Zakładając ,że wszystko inne jest dobre, dlaczego nie chcemy użyć „MOV acc, mem” zamiast „MOV reg, mem” dla załadowania wartości do AL./AX?EAX? 27. Jakie instrukcje wykonują ładowanie 32 bitów na procesorach przed-80386? 28. Jak możemy użyć instrukcji PUSH i POP dla zachowania rejestru AX pomiędzy dwoma punktami naszego kodu? 29. Jeśli bezpośrednio na wejściu do podprogramu wykonamy instrukcję pop ax, jaką wartość będziemy mieli w rejestrze AX? 30. Jaka jest główna zastosowanie dla instrukcji SAHF? 31. Jaka jest różnica między CWD i CWDE? 32. Instrukcja BSWAP skonwertuje 32 bitową wartość big endian na 32 bitową wartość little endian .Jakiej instrukcji możemy użyć do konwersji 16 bitowego big endian na 16 bitowy little endian? 33. Jaka instrukcja może być użyta do konwersji wartości 32 bitowego little endian na 32 bitowa wartość big endian? 34. Wyjaśnij jak możemy zastosować instrukcję XLAT do konwersji znaku alfabetycznego w rejestrze AL. z małej do dużej litery (zakładając ,że jest tam mała litera) i pozostawić wszystkie inne wartość w AL. niezmienione. 35. Jak instrukcja jest bardzo podobna do CMP? 36. Jak instrukcja jest bardzo podobna do TEST? 37. Co robi instrukcja NEG? 38. Pod jakimi warunkami zawiodą instrukcje DIV i IDIV? 39. Jaka jest różnica między RCL i ROL? 40. Napisz krótki segment kodu, używając instrukcji LOOP, który wywołuje „podprogram „CallMe” 25 razy. 41. Na 80486 i Pentium instrukcja LOOP nie jest tak szybka jak instrukcje dyskretne, które wykonują te same operacje .Przepisz powyższy kod do stworzenia szybciej wykonującego się programu na chipach 80486 i Pentium. 42. Jak możemy określić „przeciwny skok” dla skoku warunkowego. Dlaczego ten algorytm jest bardziej pożądany? 43. Podaj przykład instrukcji BOUND. Wyjaśnij co twój przykład będzie robił. 44. Jak jest różnica między instrukcjami IRET i RET (daleki)? 45. Instrukcja BT (Test Bitu) kopiuje określony bit do flagi przeniesienia. Jeśli określony bit to jedynka, ustawia ona flagę przeniesienia, jeśli ten bit to zero. zeruje flagę przeniesienia. Przypuśćmy ,że chcemy wyzerować flagę przeniesienia jeśli bit był zero, w przeciwnym razie ustawiony. Jak instrukcja może się wykonać po osiągnięciu tego przez BT? 46. Możemy zasymulować instrukcję dalekiego powrotu używając zmiennej podwójnego słowa i dwóch instrukcji 80x86.jak jest sekwencja tych dwóch instrukcji do osiągnięcia tego?
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK konsultacja naukowa: NEKRO [email protected]
[email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ SIÓDMY: STANDARDOWA BIBLIOTEKA UCR Większość języków programowania dostarcza kilku „wbudowanych” funkcji redukując wysiłek potrzebny do napisania programu. Tradycyjnie, programiści asemblerowi nie mieli dostępu do standardowego zbioru powszechnie używanych podprogramów dla swoich programów; w związku z tym, wydajność programistów asemblerowych była całkiem niska ponieważ stale „wymyślali koło” w każdym programie który napisali. Standardowa biblioteka UCR dla programistów 80x86 dostarcza takiego zbioru podprogramów .Rozdział ten omawia mały podzbiór podprogramów dostępnych w tej bibliotece. Po przeczytaniu tego rozdziału powinniśmy przestudiować dokumentację towarzyszącą podprogramom biblioteki standardowej. 7.0 WSTĘP Rozdział ten dostarczy podstawowego wprowadzenia do funkcji dostępnych w Standardowej Bibliotece UCR dla programistów asemblerowych 80x86.Ten krótki wstęp obejmuje następujące tematy: • Standardowa Biblioteka UCR dla Programistów Języka Asemblera • Podprogramy zarządzania pamięcią • Procedury wejściowe • Procedury wyjściowe • Konwersja • Predefiniowane stałe i makra
7.1 WSTĘP DO STANDARDOWEJ BIBLIOTEKI UCR „Standardowa biblioteka UCR dla Programistów Języka Asemblera 80x86” jest zbiorem asemblerowych podprogramów wzorowanych na standardowej bibliotece „C”. Pośród innych rzeczy, biblioteka standardowa zawiera procedury do operowaniu na danych wejściowych, wyjściowych, konwersji, kilku porównań i sprawdzeń, manipulowania łańcuchem, zarządzaniem pamięcią, zbiór operatorów znakowych, operacje zmiennoprzecinkowe, manipulowania listą, portami szeregowymi I/O ,współbieżność i współprogramy i dopasowanie do wzorca. Ten rozdział nie będzie próbował opisać każdego podprogramu w bibliotece. Przede wszystkim Biblioteka jest stale zmieniana, więc taki opis szybko mógłby się stać przestarzały. Po drugie, kilka z tych podprogramów z biblioteki jest tylko dla zaawansowanych programistów, więc jest poza zasięgiem tego tekstu. W końcu, jest sto podprogramów w tej bibliotece. Zakładając, że opiszemy je tu wszystkie, byłoby poważnym zakłócenie dla prawdziwej pracy jaką mamy do wykonania – nauczenie się asemblera. Dlatego też, ten tekst omawia kilka niezbędnych podprogramów, które działają przy najmniejszym wysiłku. Zauważmy, że pełna dokumentacja biblioteki, jak również kody źródłowe i kilka przykładowych plików znajduje się na dyskietce dołączonej do tego tekstu. Odnośny przewodnik znajduje się w dodatkach do tego tekstu. Możemy również znaleźć ostatnią wersję Standardowej Biblioteki UCR na wielu serwisach on-line, BBSach i wieku innych miejscach. Jest również dostępna przez anonimowe FTPy w Internecie. Kiedy używamy Standardowej Biblioteki UCR, powinniśmy zawsze używać pliku SHELL.ASM dostarczanego jako „szkielet” nowego programu. Plik ten zakłada konieczne segmenty, dostarcza właściwych dyrektyw include i inicjuje dla nas konieczne podprogramy Biblioteki.
Nie powinniśmy próbować tworzyć nowego programu z podprogramów przypadkowych, chyba ,że jesteśmy bardzo dobrze zaznajomieni z wewnętrznymi działaniami Biblioteki Standardowej. Zauważmy, że większość podprogramów Biblioteki Standardowej używa makr zamiast instrukcji call dla wywołania. Nie możemy, na przykład, bezpośrednio wywołać podprogramu putc. Faktycznie wywołujemy makro putc które zawiera wywołanie do procedury sl_putc („SL” - Standard Library). Jeśli nie wybierzemy do używania pliku SHELL.ASM ,nasz program musi zawierać kilka instrukcji do uruchomienia biblioteki standardowej i zaspokojenia pewnych wymagań biblioteki standardowej. Dopóki korzystamy z doświadczeń programowania w asemblerze, powinniśmy zawsze używać pliku SHELL.ASM jako punktu startowego dla naszych programów. 7.1.1 PODPROGRAMY ZARZĄDZANIA PAMIĘCIĄ: MEMINIT,MALLOC I FREE Biblioteka Standardowa dostarcza kilku podprogramów ,które zarządzają wolną pamięcią na stercie. Dają one programistom asemblerowym zdolność do dynamicznego alokowania pamięci podczas wykonywania programu i powrót tej pamięci do systemu kiedy program kiedy nie potrzebujemy dłużej programu. Poprzez dynamiczne alokowanie i zwalnianie bloków pamięci możemy wydajniej używać pamięci na PC. Podprogram meminit inicjuje menadżera pamięci a my musimy wywołać go przed każdym podprogramem, który używa tego menadżera pamięci. Ponieważ wiele podprogramów Biblioteki Standardowej używa menadżera pamięci, powinniśmy wywoływać tą procedurę wcześniej w programie .Plik SHELL.ASM wykonuje takie wywołanie dla nas. Podprogram malloc alokuje pamięć na stercie i zwraca wskaźnik do bloku, umieszczając go w rejestrach es:di. Przed wywołaniem malloc, musimy załadować rozmiar bloku (w bajtach) do rejestru cx. Przy powrocie, malloc ustawia flagę przeniesienia jeśli wystąpił błąd (niewystarczająca pamięć).Jeśli przeniesienie jest wyzerowane ,es:di wskazuje na blok bajtów, którego rozmiar wyszczególniliśmy: mov cx, 1024 ;przejęcie 1024 bajtów na stertę malloc ;wywołanie MALLOC jc MllocError ;jeśli błąd pamięci mov word ptr PNTR, DI ; wskaźnik do zapisu bloku mov word ptr PNTR+2, ES Kiedy wywołujemy malloc ,menadżer pamięci obiecuje, że blok, który nam dał jest wolny i wyzerowany i że nie realokuje tego bloku do momentu aż go wyraźnie nie zwolnimy. Zwracając blok pamięci z powrotem do menadżera pamięci, możemy (być może) użyć go ponownie w przyszłości, używając podprogramu free z Biblioteki. Free oczekuje, że podamy wskaźnik powrotny poprzez malloc: les di, PNTR ;pobieranie wskaźnika do zwolnienia free ;zwolnienie bloku jc BadFree Jak zwykle przy większości podprogramów Biblioteki Standardowej, jeśli podprogram free ma kilka rodzajów trudności zwróci flagę przeniesienia aby zaznaczyć, ze wystąpił błąd 7.1.2 PODRPOGRAMY STANDARDOWEGO WEJŚCIA:GETC,GETS,GETSM Biblioteka Standardowa dostarcza kilku podprogramów wejścia, są trzy, które w szczególności będziemy używali cały czas: getc (pobierz znak),gets (pobierz łańcuch) i getsm (pobierz łańcuch malloc). Getc odczytuje pojedynczy znak z klawiatury i zwraca ten znak w rejestrze al. Zwraca ona stan końca pliku (EOF) w rejestrze ah (zero oznacza, że EOF nie wystąpił, jeden znaczy, że EOF wystąpił)Nie modyfikuje ona innych rejestrów. Jak zwykle, flaga przeniesienia zwraca stan błędu. Nie musimy przekazywać getc żadnej wartości w rejestrze. Getc nie potwierdza znaku wejściowego do wyświetlania na ekranie .Musimy wyraźnie wydrukować znak jeśli chcemy aby pojawił się na wyjściu monitora. Następujący program przykładowy wykonuje nieustanną pętlę dopóki użytkownik nie naciśnie klawisza Enter: ;Notka: „CR” jest symbolem. ,który pojawia się w pliku nagłówkowym „consts.a”. Jest to wartość 13,która jest ;kodem ASCII dla znaku powrotu karetki Wait4Enter: getc cmp al., cl jne Wait4Enter Podprogram gets odczytuje całą linię tekstu z klawiatury. Przechowuje każdy kolejny znak linii wejściowej w tablicy bajtów której adres bazowy znajduje się w parze rejestrów es:di. Ta tablica musi mieć miejsce na przynajmniej 128 bajtów. Podprogram gets będzie odczytywał każdy znak i umieszczał go w tablicy z wyjątkiem dla znaku powrotu karetki. Gets kończy linię wejściową bajtem zerowym (który jest zgodny z podprogramem obsługi łańcucha Biblioteki Standardowej).Gets potwierdza każdy znak wypisywany na urządzeniu wyświetlającym, również operuje prostymi funkcjami edycyjnymi takimi jak backspace. Jak zwykle, gets zwraca ustawienie przepełnienia jeśli wystąpi błąd. Następujący przykład odczytuje linię tekstu ze
standardowego urządzenia wejściowego a potem zlicza liczbę wypisywanych znaków. Kod ten jest skomplikowany, zauważmy, że inicjuje licznik i wskaźnik do –1 uprzednio wprowadzając pętlę a potem bezpośrednio zwiększa je o jeden .Ustawia to licznik na zero i modyfikuje wskaźnik, żeby wskazywać pierwszy znak w łańcuchu. To uproszczenie tworzy kod mniej wydajny niż proste rozwiązanie:
Podprogram getsm również odczytuje łańcuch z klawiatury i zwraca wskaźnik do tego łańcucha w es:di. Różnica między gets a getsm jest taka, że nie musimy podawać adresu bufora wejściowego w es:di. Getsm automatycznie alokuje pamięć na stercie z wywołaniem malloc i zwraca wskaźnik do bufora w es:di .Nie zapomnijmy ,że musimy wywołać meminit na początku naszego programu jeśli używamy tego podprogramu. Plik szkieletowy SHELL.ASM wywołuje meminit za nas. Również, nie zapomnijmy wywołać free aby ściągnąć pamięć ze sterty. Getsm ;zwraca wskaźnik w ES:DI free ;Zwraca pamięć ze sterty 7.1.3 STANDARDOWE PODPROGRAMY WYJŚCIA:PUTC,PUTCR,PUTS,PUTH,PUTIPRINT I PRINTF Biblioteka Standardowa dostarcza szerokiego wachlarza podprogramów wyjścia, dużo więcej niż zobaczymy tu. Podprogramy te są reprezentatywne dla podprogramów które znajdziemy w Bibliotece. Putc wyprowadza pojedynczy znak na urządzenie wyświetlające ,Wyprowadzony znak pojawia się w rejestrze al. Nie wpływa na inny rejestr, chyba, że wystąpi błąd na wyjściu (flaga przeniesienia oznacza błąd /brak błędu jak zwykle)..Zobacz dokumentację Biblioteki po więcej szczegółów. Putcr wyprowadza „nową linię” (kombinację powrotu karetki CR /przesunięcia o jedną linię LF) na standardowe wyjście. Jest ona odpowiednio równoważna następującemu kodowi: mov al., cr ;CR i LF są stałe putc ;pojawiają się w pliku mov al., lf ;nagłówkowym const.a putc Podprogram puts (wprowadź łańcuch) drukuje łańcuch zakończony zerem który wskazuje es:di. Zauważmy, że puts automatycznie nie wyprowadza nowej linii po wydrukowaniu łańcucha. Musimy albo wprowadzić znak CR/LF na koniec łańcucha albo wywołać putcr po wywołaniu puts jeśli chcemy wydrukować nową linię po łańcuchu. Puts nie wpływa na żaden rejestr (chyba że wystąpi błąd).W szczególności, nie zmienia wartości rejestrów es:di. Następująca sekwencja kodu korzysta z tego faktu: getsm ;odczyt łańcucha puts ;drukuje go putcr ;druk nowej linii
free ;zwolnienie pamięci dla łańcucha Ponieważ powyższy podprogram zachowuje es:di (z wyjątkiem oczywiście getsm),wywołanie free dealokuje zaalokowaną pamięć przez wywołanie getsm. Podprogram puth drukuje wartość z rejestru al. jako dokładnie dwie heksadecymalne cyfry wliczając w to czołowy bajt zero jeśli wartość jest z zakresu 0..Fh.Następująca pętla odczytuje sekwencję klawiszy klawiatury i drukuje ich wartości ASCII do chwili kiedy użytkownik nie naciśnie klawisza ENTER KeyLoop: getc cmp al., cr je done puth putcr jmp KeyLoop done: Podprogram puti drukuje wartość z ax jako 16 bitową wartość całkowitą ze znakiem. Następujący kod jest fragmentem wydruku sumy I i J : mov ax, I add ax, J puti putcr Putu jest podobne do puti z wyjątkiem tego, że wyprowadza całkowitą wartość bez znaku zamiast całkowitej ze znakiem. Podprogramy jak puti i putu zawsze wyprowadzają liczby używając minimalnej liczby możliwych pozycji drukowania .Na przykład ,puti używa trzech pozycji drukowania w łańcuchu drukującym wartość 123.Czasami.możemy chcieć zmusić te podprogramy wyjściowe do drukowania ich wartości używając stałej liczby pozycji drukowania, uzupełniając każdą ekstra pozycję spacją. Podprogramy putisize i putusize dostarczają takiej możliwości. Podprogramy te oczekują wartości liczbowych w ax i wyszczególnionej szerokości pola w cx. Drukują one liczbę w polu szerokości przynajmniej pozycji cx .Jeśli wartość w cx jest większa niż liczba drukowanych pozycji o wymaganych wartościach, podprogramy te wyrównują do prawej liczbę w polu cx drukowanej pozycji. Jeśli liczba w cx jest mniejsza niż liczba drukowanych pozycji wymaganych wartości. podprogramy te zignorują wartość w cx i użyją jednak wielu pozycji drukowania wymaganych liczb. Podprogram print jest jednym z wielu, często wywoływanych procedur w bibliotece. Drukuje ona łańcuch zakończony zerem, który występuje bezpośrednio po wywołaniu print: print byte „drukuj ten łańcuch na wyświetlaczu”,cr,lf,0 Powyższy przykład drukuje łańcuch „drukuj ten łańcuch na wyświetlaczu” poprzedzony przez nową linie. Zauważmy ,że print będzie drukował jakikolwiek znak bezpośrednio następujący po wywołaniu print aż do spotkania pierwszego bajtu zerowego. W szczególności, możemy drukować sekwencje nowej linii i każdy inny znak sterujący jak pokazano powyżej. Również zauważmy, że nie jesteśmy ograniczeni do drukowania jednej linii tekstu z podprogramem print: print byte „To przykład podprogramu PRINT”,cr.lf byte „drukującego kilka linii tekstu. ”,cr, lf byte cr, lf byte 0 Uzyskamy cos takiego: To przykład podprogramu PRINT Drukującego kilka linijek tekstu. Jest niezwykle ważne abyśmy nie zapominali o bajcie kończącym zerem. Podprogram print zaczyna wykonywanie pierwszej maszynowej instrukcji 80x86 z bajtem zakończonym zerem. jeśli zapomnimy wprowadzić bajt zakończony zerem po naszym łańcuchu, podprogram print chętnie pożre kolejne bajty instrukcji naszego łańcucha (wydrukuje je),chyba że znajdzie bajt zero (bajty zerowe są powszechne w programach asemblerowych).Będzie to przyczyną, że nasz program będzie się źle zachowywał a jest to duży błąd początkujących programistów, kiedy stosują podprogram print. Zawsze o tym pamiętaj. Printf, podobnie jak jego imienniczka w „C”, dostarcza zdolności do formatowania danych wyjściowych dla pakietu Biblioteki Standardowej. Typowe wywołanie printf zawsze przybiera następującą formę: printf byte „łańcuch formatowany:,0 dword operand1,operand2,......opernadn
Łańcuch formatowany jest porównywalny do dostarczanego w języku „C”. Dla większości znaków, printf po prostu drukuje znaki w łańcuchu formatowanym aż do momentu natrafienia na bajt zakończony zerem. Dwa wyjątki to znaki poprzedzone przez backslash (‘\”) i znak procent („%).Podobnie jak printf z C, printf Biblioteki Standardowej używa backslasha jako znaku sterującego i znaku procenta jako obowiązującego przy formatowaniu łańcucha. Printf używa „\” do drukowania znaków specjalnych ,podobnie do, ale nie identycznie jak printf w C. Printf Biblioteki Standardowej wspiera następujące znaki specjalne: • \r Drukowanie powrotu karetki ale nie przesunięcia o jedną linię • \n Drukowanie znaku nowej linii (powrót karetki /przesunięcie o jedną linie) • \b Drukowanie znaku backspace • \t Drukowanie znaku tab • \l Drukowanie znaku przesunięcia o jedną linię (ale nie powrotu karetki) • \f Drukowanie znaku przesunięcia strony • \\ Drukowanie znaku backslash • \% Drukowanie znaku procenta • \0xhh Drukowanie kodu ASCII hh, reprezentowanego przez dwie cyfry heksadecymalne Użytkownicy C powinni zauważyć, że parę różnic pomiędzy Biblioteką Standardową a C. Po pierwsze użycie \% drukuje znak procenta wewnątrz formatowanego łańcucha, nie „%%”.C nie pozwala używać \% ponieważ kompilator C przetwarzając „\%” podczas kompilacji programu (pozostawi pojedynczy„%” w kodzie obiektu) podczas gdy printf przetwarza łańcuch formatowany podczas wykonywania. Widzi pojedynczy „%” i traktuje go jako znak doprowadzający do formatowania. Printf Standardowej Biblioteki, z drugiej strony, przetwarza oba „\” i „%” w czasie wykonywania, dlatego też można rozpoznać „\%”. Łańcuchu w formie „\0xhh” muszą zawierać dokładnie dwie cyfry heksadecymalne. Bieżący podprogram printf nie jest dość silny aby operować sekwencjami formy „=oxh” która zawiera tylko pojedynczą cyfrę heksadecymalną. Zapamiętajmy, gdy odkryjemy, że printf będzie „odrąbywać” znaki po napisaniu wartości Nie ma absolutnie żadnego powodu aby używać heksadecymalnego znaku sterującego z wyjątkiem „\0x00”.Printf przechwytuje wszystkie znaki występujące po wywołaniu printf aż do bajtu zakończonego zerem (który jest po to abyśmy nie musieli stosować „\0x00” jeśli chcemy wydrukować znak null, printf nie wydrukuje takiej wartości).Printf Standardowej Biblioteki nie martwi się jak te znaki się tam znajdą W szczególności, nie jesteśmy ograniczeni do używania pojedynczego łańcucha po wywołaniu printf. To jest zupełnie poprawne: printf byte „To jest łańcuch”,13,10 byte „jest w nowej lini”,13,10 byte :drukuje backspace na końcu tej linii:” byte 8,13,10,0 Nasz kod będzie działał szybciej troszkę, jeśli unikniemy stosowania sekwencji znaków sterujących. Co ważniejsze, sekwencja znaków sterujących zajmuje przynajmniej dwa bajty. Możemy zakodować większość z nich jako pojedyncze bajty przez po prostu osadzenie kodu ASCII dla tego bajtu bezpośrednio do strumienia kodu. nie zapomnijmy, nie możemy wprowadzić bajtu zero do strumienia kodu. Bajt zerowy kończy łańcuch formatowany. zamiast tego użyjemy „\0x00”. Sekwencje formatowe zawsze zaczynają się od „%”.Dla każdej sekwencji formatowej musimy dostarczyć daleki wskaźnik dla powiązania danej bezpośredniej występującej w łańcuchu formatowany. Np. printf byte „%i, %1”, 0 dword i,j Sekwencja formatowa przyjmuje ogólną formę „%s\cn^f” gdzie: • % jest zawsze znakiem „%”.Używamy „\%” jeśli chcemy wydrukować znak procenta • s jest albo niczym albo znakiem minus („-„) • „\c” jest również opcjonalny, może lub nie musi pojawiać się na pozycji formatowej ,”c” przedstawia każdy znak drukowalny • „n” przedstawia łańcuch z jedną lub więcej cyfrą dziesiętną • „^” jest znakiem karetki • „f’ przedstawia jeden ze znaków formatowania: i,d,x,h,u,c,s,ld,li,lx lub lu
Pozycje „s”,”\c”,”n” i „^” są opcjonalne, pozycje „%” i „f” musza być. Ponadto ,porządek tych pozycji w pozycjach formatowych jest bardzo ważny. Pozycja „\c”, na przykład nie może poprzedzać pozycji „s”. Podobnie ,znak „^” może następować za wszystkimi z wyjątkiem znaku „f” Znaki formatowe: i,d,x,h,u,c,s,ld,li,lx i lu sterują formatem wyjściowym dla danej. Znaki formatowe i i d wykonują identyczne funkcje, mówią printf żeby drukował wartości jako 16 bitowe dziesiętne całkowite ze znakiem. znaki formatowe x i h instruują printf o drukowaniu wyszczególnionych wartości jako 16 bitowych lub 8 bitowych wartości heksadecymalnych .Jeśli wyspecyfikujemy u, printf wydrukuje wartość jako 16 bitową dziesiętną bez znaku. Używając c mówi printf aby drukował wartość jako pojedynczy znak. S mówi printf, że dostarczamy adres łańcucha zakończonego znakiem zera., printf drukuje ten łańcuch. Pozycje ld,li,lx i lu są długimi (32 bitowymi) wersjami d/l,x i u. Odpowiedni adres wskazuje na 32 bitową wartość, którą printf sformatuje i wydrukuje na standardowym wyjściu. Następujący przykład demonstruje te pozycje formatowe: printf byte „I = %1, U= %u, HexC= %h,HexI =%x,C =%c „ dbyte „S = %s”,13,10 byte „L= %ld”,13,10,0 dword i,u,c,i,c,s,l Liczba dalekich adresów (wyszczególniony przez operand „dd” pseudo- opcodu) musi zgadzać się z liczbą pozycji formatowej „%” w łańcuchu formatowego. Printf liczy liczbę pozycji formatowych „%” w łańcuchu formatowanym i pomija wiele dalekich adresów będących za formatem łańcucha .Jeśli liczba pozycji nie zgadza się, adres powrotny dla printf będzie nieprawidłowy a program prawdopodobnie zawieszony lub będzie źle funkcjonował. Podobnie (jak podprogram print),łańcuch formatowany musi kończyć się bajtem zerowym. Adresy kolejnych pozycji łańcucha formatowanego musi wskazywać bezpośrednio na komórkę pamięci gdzie leży wyszczególniona dana. Kiedy używamy powyższego formatu, printf zawsze drukuje wartości używając minimalnej liczby pozycji drukowania dla każdego operandu. Jeśli chcemy wyszczególnić minimalną szerokość pola, możemy zrobić to używając opcji formatowej „n”. Pozycja formatowa z formatu „%10d” drukuje wartość całkowitą dziesiętną używając przynajmniej dziesięć pozycji drukowania. Podobnie „%16” drukując łańcuch używając przynajmniej 16 pozycji drukowania. Jeśli wartość drukowania wymaga więcej niż wyszczególniona liczba pozycji drukowania, printf użyje tyle ile trzeba. Jeśli wartość drukowania wymaga mniej, printf zawsze będzie drukował wyszczególnioną liczbę ,uzupełniając wartość pustymi polami .Printf będzie drukował wartości wyrównane do prawej strony w polu drukowania (bez względu na typ danych).Jeśli chcemy drukować wartość wyrównaną do lewej strony w pliku wyjściowym, używamy znaku formatowego „-„ jako przedrostka w polu szerokości np. printf byte „%-17s”,0 dword łańcuch W tym przykładzie, printf drukuje łańcuch używając 17 znaków pola szerokości wyrównanego do lewej strony w polu wyjściowym. Domyślnie, printf wypełnia na pusto pole wyjściowe jeśli wartość drukowania wymaga mniej pozycji drukowania niż wyszczególnione przez pozycję formatową. Pozycja formatowa „\c” pozwala nam zmienić znak wypełnienia .Na przykład drukując wartości, wyrównane do prawej, używając „*”,jako znak wypełnienia, użyjemy pozycji formatowej „%\10d”.Drukowanie jako wyrównanego do lewej strony będziemy używać pozycji formatowej „%-\10d”.Zauważmy,że „-„ musi poprzedzać „\”.Jest to ograniczenie bieżącej wersji oprogramowania. Operandy muszą pojawiać się w tym porządku. Normalnie kolejny adres(y) łańcucha formatowego printf muszą być dalekimi wskaźnikami do aktualnej danej do drukowania. Czasami, zwłaszcza kiedy alokujemy pamięć na stercie (używając malloc),możemy nie znać adresu obiektu, który chcemy drukować. Możemy mieć tylko wskaźnik do danej ,którą chcemy drukować. Opcja formatowania „^” mówi printf, że kolejny daleki wskaźnik łańcucha formatowego jest adres wskaźnika do danych zamiast adresu samej danej. ta opcja pozwala nam uzyskać dostęp do danej pośrednio. Notka: w odróżnieniu od C, podprogram printf Biblioteki Standardowej nie wspiera danych wyjściowych zmienno przecinkowych. Wprowadzenie wartości zmiennoprzecinkowej do printf zwiększy rozmiar tego podprogramu o ogromna ilość. Ponieważ większość ludzi nie potrzebuje wartości zmienno przecinkowych ,nie będą się tu pojawiać. Jest oddzielny podprogram printf ,który zajmuje się operacjami zmiennoprzecinkowymi Podprogram printf Biblioteki Standardowej jest złożoną bestią. Jednakże jest bardzo elastyczny i niezmiernie użyteczny. Powinniśmy spędzić trochę czasu na opanowanie jego głównych funkcji. Będziemy używać tego podprogramu dosyć często w naszych programach.
Pakiet standardowego wyjścia dostarcza wielu dodatkowych podprogramów oprócz tych omówionych tutaj. Po prostu nie ma tyle miejsca aby omówić je wszystkie w tym rozdziale .po więcej szczegółów można sięgnąć do dokumentacji Biblioteki Standardowej. 7.1.4 PODPROGRAMY FORMATOWANIA WYJŚCIOWEGO:PUTISIZE,PUTUSIZE,PUTLSIZE I PUTULSIZE Podprogramy wyjściowe puti, putu i putl łańcuchów liczbowych używają minimalnej liczby koniecznych pozycji drukowania .Na przykład, puti używa trzech znaków pozycji do drukowania wartości – 12.Czasami,możemy potrzebować wyszczególnić różne pola szerokości więc możemy wyrównywać kolumny liczb lub osiągać inne zadania formatowania. Chociaż możemy użyć printf do osiągnięcia tego celu, printf ma dwie główne wady – drukuje tylko wartości w pamięci (tj. nie może drukować wartości w rejestrze) a pole szerokości wyszczególnione dla printf musi być stałe. Podprogramy putisize, putusize i putlsize przezwyciężają te ograniczenia. Podobnie jak ich odpowiedniki puti, putu i putl, te podprogramy drukują wartości całkowite ze znakiem, całkowite bez znaku i 32 bitowe wartości całkowite ze znakiem .Oczekują wartości do drukowania w rejestrze ax (putisize i putusize) lub parze rejestrów dx:ax(putlsize).Oczekują również minimalnego pola szerokości w rejestrze. Aczkolwiek printf, jeśli wartość w rejestrze cx jest mniejsza niż liczba pozycji drukowania ta liczba w rzeczywistości potrzebuje drukować, putisize ,putusize i putlsize ignorują tą wartość w cx i drukuje wartość używając minimalną konieczną liczbę pozycji drukowania. 7.1.5 PODPROGRAMY ROZMAIRU PÓL WYJŚCIOWYCH: ISIZE,USIZE I LSIZE Raz na jakiś czas, możemy chcieć znać liczbę pozycji drukowania wartości wymaganej przed właściwym drukowaniem tej wartości .Na przykład, możemy chcieć obliczyć maksymalną szerokość zbioru liczb więc możemy drukować je w formacie kolumnowym automatycznie modyfikować szerokość pola dla największej liczby w zbiorze. Podprogramy isize, usize i lsize zrobią to za nas. Podprogram isize oczekuje wartości całkowitej ze znakiem w rejestrze ax. Zwraca minimalną szerokość pola tej wartości (zawierającą pozycję dla znaku minus, jeśli to konieczne) w rejestrze ax. Usize oblicza rozmiar wartości całkowitej bez znaku w ax i zwraca minimalną szerokość pola w rejestrze ax. Lsize oblicza minimalną szerokość wartości całkowitej ze znakiem w dx:ax (zawierającą pozycję dla znaku minus, jeśli to konieczne) i zwraca tą szerokość w rejestrze ax. 7.1.6 PODPROGRAMY KONWERSJI :ATOx I xTOA Biblioteka Standardowa dostarcza kilku podprogramów do konwersji między łańcuchem a wartościami liczbowymi. Są to atoi, atoh, atou ,itoa, htoa, wtoa i utoa (plus inne).Podprogramy ATOx konwertują łańcuch ASCII w stosownym formacie do wartości liczbowej i zostawia tą wartość w ax lub al. .Podprogramy ITOx konwertują wartość w al. ./ax do łańcucha cyfr i przechowuje ten łańcuch w buforze którego adres jest w es:di. Jest kilka wariantów każdego podprogramu który operuje w różnych przypadkach .Następny paragraf opisuje każdy podprogram. Podprogram atoi zakłada, że es:di wskazuje na łańcuch zawierający cyfry całkowite (i być może znak minus).Konwertuje ten łańcuch na wartość całkowitą i zwraca wartość całkowitą do ax. Po powrocie es:di wskazuje jeszcze na początek łańcucha. Jeśli es:di nie wskazuje na łańcuch cyfr na wejściu lub jeśli wystąpiło przepełnienie, atoi zwraca ustawienie flagi przeniesienia. Atoi zachowuje wartość pary rejestrów es:di. Wariant atoi,atoi2 również konwertuje łańcuch ASCII na wartość całkowitą z wyjątkiem tego, że nie zachowuje wartości w rejestrze di. Podprogram atoi2 jest szczególnie użyteczny jeśli musimy skonwertować sekwencję liczb pojawiającą się w tym samym łańcuchu Każde wywołanie atoi2 pozostawia rejestr di wskazując na pierwszy znak poza łańcuchem cyfr. Możemy łatwo przeskoczyć każdą spację ,przecinek lub inne znaki ograniczające dopóki nie dotrzemy do następnej liczby w łańcuchu. Potem możemy wywołać atoi2 do konwersji tego łańcucha na liczbę. Możemy powtórzyć to działanie dla każdej liczby w linii. Atoh pracuje podobnie jak podprogram atoi, z wyjątkiem tego, że oczekuje łańcucha zawierającego cyfry heksadecymalne .Po powrocie ax zawiera skonwertowaną 16 bitową wartość i flagę przeniesienia oznaczającą błąd. brak błędu. Podobnie jak atoi, podprogram atoh zachowuje wartości w parze rejestrów es:di .Możemy wywołać atoh2 ,jeśli chcemy aby rejestr es:di wskazywał na pierwszy znak poza końcem łańcucha cyfr heksadecymalnych. Atou konwertuje łańcuch ASCII cyfr dziesiętnych w zakresie 0..65,536 do wartości całkowitej i zwraca tą wartość w ax. Z wyjątkiem tego, że nie jest dozwolony, ten podprogram zachowuje się jak atoi. jest również podprogram atou2 który nie przechowuje wartości w rejestrze di; di wskazuje na pierwszy znak poza łańcuchem cyfr dziesiętnych. Ponieważ nie ma podprogramów geti, geth lub getu dostępnych w Bibliotece Standardowej ,będziemy musieli stworzyć je sami. Poniższy kod demonstruje jak odczytać wartość całkowitą z klawiatury: print
byte „Wprowadź wartość całkowitą:”,0 getsm atoi ;konwersja łańcucha na wartość całkowitą w AX free ;powrót alokowanej pamięci przez getsm print byte „Wprowadziłeś” . 0 puti ;drukuj wartość zwróconą przez ATOI putcr Podprogramy itoa utoa, htoa i wtoa są logicznymi odwróceniami podprogramu atox. Konwertują one wartości liczbowe do całkowitych, bez znakowych i heksadecymalnych łańcuchów. Jest kilka wariantów tych podprogramów w zależności od tego czy chcemy automatycznie alokować pamięć dla łańcucha lub czy chcemy je zachować w rejestrze di. Itoa konwertuje 16 bitowe wartości całkowite ze znakiem w ax do łańcucha i przechowuje znaki tego łańcucha poczynając od lokacji es:di. Kiedy wywołujemy itoa ,musimy zapewnić, że es:di wskazuje na tablicę znaków dość dużą do przetrzymywania łańcuchów wynikowych. Itoa wymaga maksymalnie siedmiu bajtów dla tej konwersji :pięciu cyfr ,znaku i bajtu zakończonego zerem. Itoa zachowuje wartości w parze rejestrów es:di, więc na powrót es:di wskazuje na początek łańcucha stworzonego przez itoa. Czasami możemy nie chcieć przechowywać wartości w rejestrze di kiedy wywołujemy podprogram itoa. Na przykład, jeśli chcemy stworzyć pojedynczy łańcuch zawierający kilka skonwertowanych wartości, byłoby miło gdyby itoa opuścił di wskazujący na koniec łańcucha zamiast na początek. Podprogram itoa2 robi to dla nas; pozostawia rejestr di wskazując na bajt zakończony zerem na końcu łańcucha. Rozważmy następujący segment kodu który stworzy łańcuch zawierający reprezentację ASCII dla trzech zmiennych całkowitych Int1,Int2 i Int3: ;zakładamy, że es:di już wskazuje na lokację początkową przechowująca skonwertowane wartości całkowite mov ax, Int1 itoa2 ;konwersja Int1 do łańcucha mov byte ptr es:][di], ‘ ‘ inc di ,Konwersja drugiej wartości mov ax, Int2 itoa2 mov byte ptr es:[di] inc di ;Konwersja trzeciej wartości mov ax, Int3 itoa2 ;w tym miejscu di wskazuje na koniec łańcucha zawierającego skonwertowane wartości .Szczęśliwie jeszcze ;wiemy gdzie zaczyna się łańcuch więc możemy nim manipulować! Inny wariant podprogramu itoa, itoam, nie wymaga inicjacji pary rejestrów es:di .Podprogram ten wywołuje malloc automatycznie alokując pamięć. Zwraca wskaźnik do skonwertowanego łańcucha na stercie w parze rejestrów es:di. Kiedy skończymy z łańcuchem, powinniśmy wywołać free aby zwrócić pamięć ze sterty. ;następujący fragment kodu konwertuje wartość całkowitą w AX do łańcucha i drukuje ten łańcuch. Oczywiście, możemy zrobić to samo z użyciem PUTI, ale ten kod demonstruje jak wywołać itoam itoam ;konwertuje wartość całkowitą do łańcucha puts ;drukuje łańcuch free ;zwraca pamięć ze sterty Podprogramy utoa,utoa2 i utoam pracują dokładnie tak jak itoa,itoa2 i itoam., z wyjątkiem tego, że konwertują wartości całkowite bez znaku w ax do łańcucha. Zauważmy, że utoa i utoa2 wymagają sześciu bajtów ponieważ nigdy nie przetwarzają znaku ze znakiem. Wtoa,wtoa2 i wtoam konwertują 16 bitową wartość w ax do łańcucha z dokładnie czterema znakami heksadecymalnymi plus bajt zakończony zerem. W przeciwnym razie, zachowują się dokładnie jak itoa,itoa2 i i itoam. Zauważmy, że te podprogramy przetwarzają zero początkowe więc wartość jest zawsze długa na cztery cyfry. Podprogramy htoa,htoa2 i htoam są podobne do wtoa,wtoa2 i wtoam. Jednakże ,podprogramy htoax konwertują ośmio bitową wartość w al. do łańcucha z dwoma znakami heksadecymalnymi plus bajt zakończony zerem. Biblioteka Standardowa dostarcza kilku innych podprogramów konwersji poza tymi wymienionymi w tej sekcji. 7.1.7 PODPROGRAMY TESTUJĄCE ZNAKI DLA PRZYANALEŻNOSCI DO ZBIORU
Biblioteka Standardowa UCR dostarcza wiele podprogramów ,które testują znak w rejestrze al. aby sprawdzić czy należy do pewnego zbioru znaków. Te podprogramy wszystkie zwracają stan we fladze zera .Jeśli warunek jest prawdziwy, ustawiają flagę zera (więc możemy przetestować warunki instrukcją je). Jeśli warunek jest fałszywa, zerują flagę zera (testujemy instrukcją jne),Te podprogramy to: * IsAlNumSprawdza czy al. zawiera znaki alfanumeryczne * IsXDigitSprawdza al. czy zawiera znaki cyfr heksadecymalnych * IsDigitSprawdza al. czy zawiera znaki cyfr dziesiętnych * IsAlpha Sprawdza al. czy zawiera znaki alfabetu * IsLower Sprawdza al. czy zawiera znaki małych liter * Is Upper Sprawdza al. czy zawiera znaki dużych liter 7.1.8 PODPROGRAMY KONWERSJI ZNAKÓW: TOUPPER,TOLOWER Podprogramy ToLower i ToUpper sprawdzają znak w rejestrze al. Skonwertują znak w al. do właściwej wielkości znaku. Jeśli al. zawiera znak alfabetyczny małej litery, ToUpper skonwertuje go do odpowiedniego znaku dużej litery. Jeśli al zawiera jakiś inny znak, ToUpper zwróci go niezmienionym. Jeśli al zawiera znak alfabetyczny dużej litery, ToLower skonwertuje go do odpowiedniego znaku małej litery .Jeśli wartość nie jest znakiem alfabetycznym dużej litery ToLower pozostawi go niezmienionym. 7.1.9 GENEROWANIE LICZB LOSOWYCH: RANDOM,RANDOMIZE Podprogram Random Standardowej Biblioteki generuje sekwencje pseudo – losowych liczb. Zwraca wartość losową w rejestrze ax w każdym wywołaniu. Możemy potraktować tą wartość jako wartość ze znakiem lub bez, ponieważ Random manipuluje wszystkimi 16 bitami rejestru ax. Możemy użyć instrukcji div lub i div do zmuszenia do przetwarzania random w wyszczególnionym zakresie. Podzielenie wartości losowej zwraca jakąś liczbę n a reszta tego dzielenia będzie wartością z zakresu 0..n-1.Na przykład, obliczenie liczby losowej z zakresu 1..10,może wymagać kodu jak następuj: random ;pobiera losową liczbę z zakresu 0..65,536 sub dx, dx ;powielenie zera do 16 bitów mov bx, 10 ;chcemy wartości z zakresu 1..10 div bx ;reszta idzie do dx! Inc dx ;konwersja 0..9 do 1..10 ;w tym punkcie, liczba losową z zakresu 1..10 znajduje się w rejestrze dx. Podprogram random zawsze zwraca tą samą sekwencję wartości kiedy program ładuje się z dysku i wykonuje. Random używa wewnętrznej tablicy wartości ziarnistej która przechowuje część swojego kodu. Ponieważ wartości te są stałe i zawsze ładują do pamięci z programu, algorytm którego używa random zawsze stworzy tą samą sekwencję wartości kiedy program zawiera go ładując z dysku i zaczynając program. Może to nie wyglądać „losowo”, ale, faktycznie ,jest to miła cecha ponieważ jest bardzo trudno przetestować program który naprawdę używa wartości losowych. Jeśli generator liczb losowych tworzy tą samą sekwencję liczb, żaden test uruchomiony w tym programie nie będzie powtarzany Niestety, jest wiele przykładów programów które możemy chcieć napisać (np. gry) gdzie posiadanie powtarzalnego wyniku nie jest do przyjęcia. Dla takich aplikacji możemy wywołać podprogram randomize. Randomize używa bieżącej wartości zegara systemowego do generowania prawie losowej sekwencji startowej. Więc jeśli potrzebujemy (prawie) unikalnej sekwencji liczb losowych, w każdym czasie kiedy program zaczyna się wykonywać, wywołujemy podprogram randomize przed każdym wywołaniem podprogramu random. Zauważmy, że jest mały profit z wywołania podprogramu randomize więcej niż jeden raz w programie. Raz random ustala punkt startowy random, dalsze wywołanie randomize nie poprawi jakości (przypadkowości) liczb przez niego generowanych. 7.1.10 STAŁE,MAKRA I INNE RÓŻNOŚCI Kiedy stosujemy plik nagłówkowy „stdlib.a”, możemy zdefiniować pewne makra (zobacz Rozdział Ósmy opis makr) i powszechnie stosujemy stałe. Zawiera się to następująco: NULL = 0 ;jakiś pewien kod ASCII BELL = 07 ;znak dzwonka(??????chyba) bs = 08 ;znak backspace tab = 09 ;znak tabulatora lf = 0ah ;znak przesunięcia do nowej linii cr = odh ;powrót karetki W dodatku do powyższych stałych,”stdlib.a” również definiuje kilka użytecznych makr zawierających ExitPgm,lesi i ldxi.Makra te zawierają następujące instrukcje:
;ExitPgm – zwraca sterowanie do MS-DOS ExitPgm macro mov ah, 4ch ;opcod zakończenia programu DOSowskiego int 21h ,wywołanie DOS endm ;LESI ADR – ładuje ES:DI adresem wyszczególnionym przez operand lesi macro adrs mov di, ser adrs mov es, di mov si, offset adrs endm ;LDXI ADRS – ładuje DX:SI adresem wyszczególnionym przez operand ldxi macro adrs mov dx, seg adrs mov si, offset adrs endm Makra lesi i ldxi są zwłaszcza użyteczne dla ładowania adresów do es:di lub dx:si przed wywołaniem kilku podprogramów standardowej biblioteki. 7.1.11 PLUS WIĘCEJ! Biblioteka Standardowa zawiera wiele ,wiele podprogramów których ten rozdział nie omawiał .Większość można znaleźć w dokumentacji Biblioteki Standardowej. Podprogramy omówione w tym rozdziale są podprogramami, których będziemy używali najczęściej. 7.5 PODSUMOWANIE Rozdział ten wprowadził kilka dyrektyw asemblera i pseudo opcodów wspieranych przez MASM. Omówił również krótko podprogramy w Standardowej Bibliotece UCR dla Programistów Języka Asemblera 80x86.Nie znaczy to, że rozdział ten omówił komplet tego co MASM lub Biblioteka Standardowa nam oferują. Dostarczył tylko dość informacji dla rozpoczęcia działań. Aby pomóc w pisaniu programów asemblerowych z minimalnym zamieszaniem, tekst ten korzysta w znacznym stopniu z kilku podprogramów Biblioteki Standardowej. Chociaż rozdział ten z pewnością nie omówił wszystkich jej podprogramów, omówił wiele często używanych. 7.6 PYTANIA 1. 2. 3. 4. 5. 6. 7. 8.
Jakiego pliku powinniśmy używać na początku naszego programu kiedy piszemy kod używający Standardowej Biblioteki UCR? Jaki podprogram alokuj pamięć na stercie Jakiego podprogramu będziemy używać do drukowania pojedynczego znaku? Jaki podprogram pozwala nam drukować łańcuch stałych znakowych na wyświetlaczu? Biblioteka Standardowa nie dostarcza podprogramu do odczytu wartości całkowitych od użytkownika. Opisz, jak zastosować podprogramy GETS i ATOI do wykonania tego zadania. Jak jest różnica między podprogramem GETS a GETSM? Jaka jest różnica między podprogramem ATOI a ATOI2? Co robi podprogram ITOA? Opisz wartości wejściowe i wyjściowe
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK konsultacja naukowa: NEKRO [email protected]
[email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ ÓSMY : MASM: DYREKTYWY I PSEUDO-OPCODY Instrukcje takie jak mov ax,0 i add ax, bx są niezrozumiałe dla mikroprocesora. W takiej postaci w jakiej te instrukcje się pojawiają, są one jeszcze czytelnymi dla ludzi postaciami instrukcji 80x86.80x86 reaguje na polecenia takie jak B80000 i 03C3.Assembler jest programem który konwertuje łańcuchy takie jak mov ax, 0 do kodu maszynowego 80x86 „B80000”.Każdy program w języku asemblera zawiera instrukcje takie jak mov ax, 0.Assembler konwertuje każdy asemblerowy plik źródłowy do kodu maszynowego – binarny odpowiednik programu asemblerowego. Pod tym względem program asemblerowy jest jak kompilator, odczytuje plik źródłowy ASCII z dysku i tworzy na wyjściu program języka maszynowego. Główna różnica między kompilatorem dla języka wysokiego poziomu (HLL) takim jak Pascal i asemblerem jest taka, że kompilator zwykle emituje kilka instrukcji maszynowych dla każdej instrukcji pascalowskiej .Asembler generalnie emituje pojedynczą instrukcję maszynową dla każdej instrukcji języka asemblera. Próba napisania programu w języku maszynowym (tj. w binarnym) nie jest szczególnie miła. Proces ten jest bardzo nużący, skłonny do błędów i nie proponujący żadnych korzyści przy programowaniu w języku asemblera .Jedyną główną wadą języka asemblera przy czystym kodzie maszynowym jest to ,że musimy najpierw assemblować i linkować program przed jego wykonaniem. Jednakże, próba asemblowania kodu ręcznie będzie trwała dłużej niż mała ilość czasu jaką poświęci asembler na wykonanie tego za nas. Jest inna wada nauki języka asemblera .Asembler taki jak Microsoft’s Macro Assembler (MASM) dostarcza dużej liczby możliwości dla programisty asemblerowego. Chociaż nauka o tych możliwościach zabiera sporo czasu, są one bardzo użyteczne i warte włożonego wysiłku. 8.0 WSTĘP Podobnie jak w Rozdziale Szóstym, dużo informacji w tym rozdziale jest materiałami odnośnymi. Podobnie jak każda sekcja odnośna, jakaś wiedza jest niezbędna, inny materiał jest przydatny ale opcjonalny, a niektórych materiałów możemy nigdy nie używać podczas pisania programów. Poniższa lista przedstawia informacje w tym tekście. Symbol „•” oznacza materia niezbędny, symbol „⊗” oznacza opcjonalny i mniej użyteczny temat. • Format źródłowy instrukcji języka asemblera ⊗ Licznik lokacji • Symbole i identyfikatory • Stałe • Deklaracje procedur ⊗ Segmenty w programie języka asemblera • Zmienne • Typy symboli • Wyrażenia adresowe (późniejsze podsekcje zawierają materiał zaawansowany) ⊗ Warunkowe asemblowanie ⊗ Makra ⊗ Dyrektywy listowania ⊗ Oddzielne asemblowanie
8.1 INSTRUKCJE JĘZYKA ASSEMBLERA Instrukcje w języku assemblera w pliku źródłowym używają następującego formatu: {Etykieta} {Mnemonik {Operand}} {;Komentarz} Każda powyższa jednostka jest polem Cztery powyższe pola są to pole etyliety, pole mnemonika, pole operandu i pole komentarza. Pole etykiety jest (zazwyczaj) polem opcjonalnym zawierającym etykietę symboliczną dla bieżącej instrukcji. etykiety są używane w języku assemblera, podobnie jak w HLL, do oznaczania linii jako celu skoków GOTO .Możemy również wyszczególnić nazwę zmiennej, nazwę procedury i inne jednostki używające etykiet symbolicznych. Przez większość czasu pole etykiety jest opcjonalne, w znaczeniu, że etykieta może być obecna tylko, jeśli chcemy etykietę na tej szczególnej linii. Niektóre mnemoniki, jednak wymagają etykiety, inne nie. Generalnie ,powinniśmy zawsze zaczynać nasze etykiety w pierwszej kolumnie (uczyni to nasz program łatwiejszym w czytaniu). Mnemonik jest nazwą instrukcji (np. mov, add itp.)Słowo mnemonik oznacza wspomaganie pamięci. mov jest dużo łatwiejsze do zapamiętania niż binarny odpowiednik instrukcji mov! Nawiasy klamrowe oznaczają, że ta pozycja jest opcjonalna. Zauważmy jednak, że nie możemy mieć operandu bez mnemonika. Pole mnemonika zawiera instrukcję assemblera .Instrukcje są dzielone na trzy klasy :instrukcje maszynowe 80x86,dyrektywy assemblera i pseudo opcody .Instrukcje 80x86 są oczywiście mnemonikami assemblera ,które odpowiadają rzeczywistym instrukcjom 80x86 wprowadzonym w Rozdziale Szóstym. Dyrektywy assemblera są instrukcjami specjalnymi które dostarczają informacji do assemblera ale nie generują żadnego kodu. Przykłady obejmują dyrektywę segment, equ, assume i end. Te mnemoniki nie są ważnymi instrukcjami 80x86.Są one tylko informacjami dla assemblera ,niczym więcej. Pseudo-opcody są wiadomością dla assemblera, podobnie jak dyrektywy, jednak pseudo-opcod wyemituje bajt kodu wynikowego.. Przykłady pseudo-opcodów obejmują byte ,word, dword, qword i tbyte. Instrukcje te emitują bajty danych wyszczególnione przez ich operandy ale nie są prawdziwymi instrukcjami maszynowymi 80x86. Pole operandu zawiera operandy lub parametry ,dla instrukcji wyszczególnionej w polu mnemonika. Operandy nigdy nie pojawiają się w wierszu same z siebie. Typ i liczba operandów (zero, jeden, dwa lub więcej) zależy wyłącznie od określonej instrukcji. Pole komentarza pozwala nam opisać każdą linie kodu źródłowego w naszym programie. .Zauważmy ,że pole komentarza zawsze zaczyna się od średnika. Kiedy asembler przetwarza linię tekstu, kompletnie ignoruje wszystko w linii źródłowej występujące za średnikiem. Każda instrukcja języka assemblera pojawia się we własnym wierszu w pliku źródłowym. nie możemy mieć wielu instrukcji w pojedynczej linii. Z drugiej strony, ponieważ wszystkie pola w instrukcji języka assemblera są opcjonalne, linie puste są w porządku. Możemy użyć pustych linii gdziekolwiek w naszym pliku źródłowym. Linie puste są użyteczne przy rozmieszczaniu pewnych sekcji kodu, czyniąc je łatwiejszymi do czytania. Microsoft Macro Assembler jest asemblerem o swobodnej formie. Różne pola instrukcji języka assemblera mogą pojawiać się w każdej kolumnie (chociaż lepiej ,żeby pojawiały się we właściwym porządku) Każda liczba spacji lub tabulatorów może oddzielać różne pola w instrukcji. Dla assemblera, te dwie sekwencje kodu są identyczne ---------------------------------------------------------mov ax, 0 mov bx, ax add ax ,dx mov cx, ax ----------------------------------------------------------mov ax, 0 mov bx, ax add ax, dx mov cx,ax ------------------------------------------------------------Pierwsza sekwencja kodu jest dużo łatwiejsza do odczytu niż druga (jeśli tak nie sądzisz, być może powinieneś zobaczyć się z lekarzem!) Umieszczenie etykiety w kolumnie jeden, mnemoników w kolumnie 17 (dwa tabulatory),pola operandu w kolumnie 25 (trzy tabulatory) i komentarza około kolumny 41 lub 49 (pięć lub sześć tabulatorów) tworzy najlepiej wyglądający listing. Programy w języku assemblera, takie jak ten , są dosyć trudne do odczytania. formatowanie naszego listingu pomoże uczynić go łatwiejszym do odczytu i uczyni łatwiejszym do pielęgnacji.
Możemy mieć sam komentarz w linii. W takim przypadku, umieszczamy średnik w kolumnie jeden i używać całej linii dla komentarza ,przykłady: ;Następująca sekcja kodu pozycjonuje kursor w górnej, lewej części ekranu: mov X, 0 mov Y, 0 8.2 LICZNIK LOKACJI Przypomnijmy, że wszystkie adresy w przestrzeni pamięci 80x86 składa się z adresu segmentowego i offsetu wewnątrz segmentu. Assembler, w trakcie konwertowania naszego pliku źródłowego do kodu wynikowego, musi zachować ścieżkę offsetu wewnątrz bieżącego segmentu. Licznik lokacji jest zmienną assemblera która to obsługuje. Kiedy tworzymy segment w naszym pliku źródłowego języka assemblera, assembler kojarzy z nim wartość bieżącego licznika lokacji. Licznik lokacji zawiera bieżący offset do tego segmentu. Pierwotnie (kiedy assembler po raz pierwszy napotyka segment) licznik lokacji jest ustawiony na zero. Kiedy napotyka instrukcję lub pseudo-opcodu, MASM zwiększa licznik lokacji dla każdego bajtu zapisanego w pliku kodu wynikowego. Na przykład, MASM zwiększa licznik lokacji o dwa po napotkaniu mov ax, bx ponieważ instrukcja ta jest dwubajtowa. Wartość licznika lokacji zmienia się w całym procesie asemblacji. Zmienia się dla każdej linii kodu w naszym programie, kiedy tworzymy kod wynikowy .Będziemy używać terminu licznik lokacji w znaczeniu wartości licznika lokacji przy poszczególnej instrukcji przed wygenerowaniem jakiegoś kodu. Rozważmy następujące instrukcje języka assemblera: 0: 3: 6: 9: A: C: D: E: F: 10:
or ah, 9 and ah,0c9h xor ah,40 pop cx mov al.,cl pop bp pop cx pop dx pop ds. ret Instrukcje or, and i xor ,wszystkie są długości trzech bajtów, instrukcja mov jest dwubajtowa; pozostałe instrukcje wszystkie są jednobajtowe. Jeśli te instrukcje pojawią się na początku segmentu, licznik lokacji będzie taki sam jak liczby które pojawiają się bezpośrednio na lewo od każdej powyższej instrukcji. Na przykład, instrukcja or zaczyna się od offsetu zero, ponieważ instrukcja or jest trzybajtowa ,następna instrukcja (and) wystąpi pod offsetem trzy. Podobnie and jest trzybajtowa ,więc xor wystąpi od offsetu sześć itd. 8.3 SYMBOLE Rozważmy na chwilę instrukcję jmp. Instrukcja ta przyjmuje formę: jmp cel Cel jest adresem przeznaczenia. Wyobraźmy sobie jak żmudne byłoby gdybyśmy musieli w rzeczywistości wyszczególnić adres docelowy pamięci jako wartość liczbową. Jeśli kiedyś programowaliśmy w BAISICu, doświadczylibyśmy około 10% problemów jakie mielibyśmy w języku assemblera gdybyśmy musieli wyszczególniać cel dla jmp poprzez adres. Dla ilustracji, przypuśćmy, że chcielibyśmy skoczyć do pewnej grupy instrukcji którą jeszcze piszemy.. Jaki jest adres instrukcji docelowej? Jak możemy powiedzieć że napisaliśmy jakąś instrukcję przed instrukcją docelową? Co się zdarzy jeśli zmienimy program (pamiętajmy, wprowadzenie i kasowanie instrukcji powoduje, że wartość licznika lokacji dla wszystkich następnych instrukcji wewnątrz tego segmentu zmienia się).Na szczęście ,wszystkie te problemy są zmartwieniem programistów języka maszynowego. Programiści języka assemblera mogą zająć się adresami w dużo bardziej rozsądny sposób – poprzez użycie adresów symbolicznych. Symbol, identyfikator lub etykieta, jest nazwą powiązana z jakąś szczególną wartością. Wartość ta może być offsetem wewnątrz segmentu, stałą, łańcuchem, adresem segmentowym, offsetem wewnątrz rekordu lub nawet operandem dla instrukcji. W każdym razie, etykieta zapewnia nam możliwości do przedstawiania niezrozumiałych wartości ,jako dobrze znanej mnemonicznej nazwy. Nazwa symboliczna składa się z sekwencji liter, cyfr i znaków specjalnych, z następującymi ograniczeniami: • Symbol nie może zaczynać się od cyfry
• • • •
Nazwa może mieć każdą kombinację dużych i małych liter. Assembler traktuje duże i małe litery równorzędnie. Symbol może zawierać każdą liczbę znaków, jednak tylko pierwsze 31 jest używanych. Assembler ignoruje wszystkie znaki po trzydziestym pierwszym Symbole „_,$,? I @” mogą się pojawiać gdziekolwiek wewnątrz symbolu. jednak ,$ i ? są symbolami specjalnymi; nie możemy tworzyć symbolu składającego się wyłącznie z tych dwóch symboli. Symbol nie może pokrywać się z żadną z nazw ,które są zarezerwowanymi symbolami. Następujące symbole są zarezerwowane:
W dodatku, wszystkie poprawne nazwy instrukcji 80x86 i nazwy rejestrów są również zarezerwowane. Zauważmy ,że lista ta stosuje się dla MASMa w wersji 6.0.Wcześnejsze wersje tego assemblera mają mniej zarezerwowanych słów. Późniejsze wersje mogą mieć więcej Kilka przykładów poprawnych symboli: L1 Bletch RightHere Right_Here Item1 _Special $1234 @Home $_@1 Dollar$ WhereAmI @1234 $1234 i @1234 są zupełnie poprawne, chociaż mogą wydawać się dziwne. Kilka przykładów niewłaściwych symboli: 1TooMany - zaczyna się cyfrą
Hello.There -zawiera kropkę w środku symbolu $ -nie może być samodzielnego znaku $ i > LABEL -zarezerwowane słowo assemblera Right Here -Symbol nie może zawierać spacji Hi,There - lub innego znaku specjalnego poza _,?,$ i @ Symbolom, jak wspomniano wcześniej, można przydzielić wartości liczbowe (takie jak wartości licznika lokacji),łańcuchy lub nawet całe operandy. Wyjaśnijmy sobie jedną rzeczy, assembler przydziela typ do każdego symbolu. Przykłady typów near, far, byte, word, double word, quad word, text i string. jak zadeklarować etykiety pewnych typów jest tematem reszty tego rozdziału. Zauważmy, że assembler zawsze przydziela jakiś typ do etykiety i będzie dozorował czy próbujemy użyć etykiety w miejscu gdzie nie jest dozwolony taki typ etykiety. 8.4 STAŁE ZNAKOWE MASM jest zdolny do przetwarzania pięciu różnych typów stałych: całkowite, całkowite upakowane dziesiętne kodowane binarnie, liczby rzeczywiste, łańcuchy i tekst. W rozdziale tym zajmiemy się tylko stałymi całkowitymi, rzeczywistymi, łańcuchami i tekstem. Po więcej informacji o wartościach całkowitych upakowanych BCD, proszę zgłosić się do Przewodnika MASM. Stała znakowa jest jedną której wartość jest ukryta pod znakami, które stanowią stałą. Przykłady stałych znakowych: • 123 • 3.14159 • „Łańcuchowa Stała Znakowa” • 0FABCh • ‘A’ • Oprócz ostatniego przykładu, większość stałych znakowych powinny być dobrze znane każdemu kto pisał programy w językach takich jak Pascal lub C++. Stałe tekstowa są specjalnymi formami łańcuchów które pozwalają na zastąpienie tekstowe podczas asemblacji. Przedstawianie stałych znakowych odpowiada temu co normalnie moglibyśmy oczekiwać dla „wartości rzeczywistego słowa” .Stałe znakowe są również znane jako „stałe nie symboliczne” ponieważ używają wartości rzeczywistych ,zamiast jakichś nazw symbolicznych, wewnątrz naszego programu. MASM również pozwala nam zdefiniować symboliczną lub jawną stałą w programie, ale więcej o tym później. 8.4.1 STAŁE CAŁKOWITE Stała całkowita jest wartością liczbową, która może być określona jako binarna, dziesiętna lub heksadecymalna. Wybór bazy (lub podstawy systemu liczbowego) należy do nas. Poniższa tablica pokazuje poprawne cyfry dla każdej podstawy:
Tablica 35 Cyfry używane dla każdej podstawy systemu liczbowego Dla odróżnienia pomiędzy liczbami w kilku systemach liczbowych, używamy znaku przyrostka. Jeśli, kończymy liczbę „b” lub „B”, wtedy MASM zakłada, że jest to liczba binarna. Jeśli zawiera każdą inną cyfrę niż zero lub jeden assembler wygeneruje błąd .Jeśli przyrostek to „t”, ”T” ,”d” lub „D”, wtedy assembler zakłada, że jest to wartość dziesiętna (o podstawie 10).Przyrostek „h” lub „H” wybierze podstawę heksadecymalną. Wszystkie stałe całkowite muszą zaczynać się cyfrą dziesiętną, wliczając w to stałe heksadecymalne. Przedstawienie wartości „FDED” musi wyszczególnić 0FDEDh.Czołowa cyfra dziesiętna jest wymagana przez assembler ,żeby mógł rozróżniać pomiędzy symbolami i stałymi numerycznymi; pamiętajmy ,”FDED” jest zupełnie poprawnym symbolem MASMa. Przykłady: 0F000h 12345d 0110010100b
1234h 100h 08h Jeśli nie wyszczególnimy przyrostka po stałej liczbowej, assembler użyje bieżącej, domyślnej podstawy .Domyślną podstawą jest podstawa dziesiętna. dlatego też, możemy zazwyczaj wyszczególnić wartości dziesiętne bez dokładania znaku „D”. Dyrektywa assemblera radix może być użyta do zmiany domyślnej podstawy systemu liczbowego na inną bazę .instrukcja .radix przyjmuje następującą formę: .radix base ;opcjonalnie komentarz Base jest domyślną wartością między 2 a 16. Instrukcja .radix skutkuje jak tylko MASM napotka ją w pliku źródłowym .Wszystkie instrukcje przed instrukcją .radix będą używały poprzedniej domyślnej podstawy dla stałej liczbowej .Poprzez odrobinę wielokrotności instrukcji .radix w całym naszym pliku źródłowym, możemy przełączać domyślną podstawę pomiędzy kilkoma wartościami zależnie od tego jaki jest najbardziej dogodny w danym punkcie programu. Generalnie, dziesiętna podstawa jest dobra jako podstawa domyślna, więc instrukcja .radix nie używa jej często .Jednakże, w obliczu wejścia do gigantycznej tablicy wartości heksadecymalnych, możemy zachować wiele typów poprzez czasowe przełączenie do podstawy 16 przed tablicą i przełączenia z powrotem do 10 po tablicy .Notka: jeśli domyślna podstawa jest heksadecymalna, powinniśmy użyć przyrostka „I” do oznaczenia wartości dziesiętnej ponieważ MASM może pomylić przyrostek „D” z cyfrą heksadecymalną. 8.4.2 STAŁE ŁAŃCUCHOWE Stała łańcuchowa jest sekwencją znaków otoczonych przez apostrofy lub cudzysłowy. Przykłady: „to jest łańcuch” ‘Więc i to’ Możemy swobodnie umieszczać apostrofy wewnątrz stałej łańcuchowej otoczonej cudzysłowem i vice versa. Jeśli chcemy umieścić apostrof wewnątrz łańcucha ograniczonego przez apostrofy, musimy umieścić następną parę apostrofów w łańcuchu. Np. ‘Co się ‘’ stało?’ Cudzysłów pojawia się wewnątrz łańcucha ograniczonego przez cudzysłów muszą również zdublowane np. „Microsoft twierdzi „”Nasz software jest bardzo szybki.”” Czy im wierzysz?” Chociaż możemy zdublować apostrofy i cudzysłowy jak pokazano w powyższych przykładach, łatwiejszym sposobem zawarcia tych znaków w łańcuchu jest użycie innego znaku jako ogranicznika łańcucha.: „Co się stało?” ‘Microsoft twierdzi „Nasz software jest bardzo szybki.” Czy im wierzysz?’ Jedyny raz kiedy zdublowanie cudzysłowu lub apostrofu jest absolutnie konieczne w łańcuchu jest to, kiedy łańcuch zawiera oba symbole. Rzadko się to zdarza w rzeczywistych programach. Podobnie jak języki programowania C i C++, istnieje subtelna różnica pomiędzy wartością znaku a wartością łańcucha. Pojedynczy znak (to znaczy, łańcuch o długości jeden) może pojawiać się gdziekolwiek MASM pozwala na stałe całkowite lub łańcuch. jeśli wyszczególnimy stałą znakową tam gdzie MASM spodziewa się stałej całkowitej, MASM użyje kodu ASCII tego znaku jako wartości całkowitej .Łańcuchy (których długość jest większa niż jeden) są dozwolone tylko wewnątrz pewnych kontekstów 8.4.3 STAŁE RZECZYWISTE Wewnątrz pewnych kontekstów możemy użyć stałych zmienno przecinkowych .MASM pozwala nam wyrazić stałą zmiennoprzecinkową na jedną z dwóch postaci :notacji dziesiętnej lub naukowej. Formy te są całkiem podobne dla formatu dla liczb rzeczywistych używanych przez Pascala, C i inne HLL’e . Forma dziesiętna jest sekwencją cyfr zawierającą punkt dziesiętny w odpowiedniej pozycji liczby: 1.0 3.14159 625,25 -128.0 0.5 Notacja naukowa jest również identyczna do form używanych przez różne HLL’e: 1e5 1.576e-2 -6.02e-10 5.34e+12 Dokładny zakres precyzji liczb zależy od naszego pakietu zmiennoprzecinkowego .Jednakże MASM generalnie emituje dane binarne dla powyższych stałych które są kompatybilne z koprocesorem arytmetycznym 80x87.Forma ta odpowiada formatowi liczbowemu wyspecyfikowanemu przez standard IEEE dla wartości zmienno przecinkowych .W szczególności stała 1.0 nie jest binarnym odpowiednikiem jedynki całkowitej. 8.4.4 STAŁE TEKSTOWE Stałe tekstowe nie są tym samy co stałe łańcuchowe. Stała tekstowa jest zastępowana dosłownie podczas procesu asemblacji. Na przykład, znaki 5[bx] mogą być stałą tekstową powiązaną z symbolem VAR1.Podczas asemblacji, instrukcja w postaci mov ax,VAR1,będzie skonwertowana do instrukcji mov ax, 5[bx].
Tekstowe porównania są całkiem użyteczne w MASM ponieważ MASM często upiera się przy długich łańcuchach tekstu dla prostego operandu języka assemblera. Używając tekstowych porównań pozwala nam na upraszczanie takich operandów przez zastąpienie łańcucha tekstu przez pojedynczy identyfikator w instrukcji. Stała tekstowa skalda się z sekwencji znaków otoczonych przez symbole „<” i „>”.Na przykład stała tekstowa 5[bx] mogłaby być normalnie zapisana jako <5[bx]>.kiedy wystąpi zastąpienie tekstu, MASM usunie znaki ograniczające „<” i „”>”. 8.5 DEKLAROWANIE STAŁYCH UŻYWAJĄCYCH DYREKTYW RÓWNOŚCI Stała jawna jest nazwą symbolu która przedstawia jakąś stałą wartość podczas procesu asemblacji. To znaczy ,jest to nazwa symboliczna która przedstawia jakąś wartość. Dyrektywy równości to mechanizm MASM używany do deklaracji stałych symbolicznych. Dyrektywy równości przybierają trzy podstawowe formy: symbol equ wyrażenie symbol = wyrażenie symbol tekstequ wyrażenie Operand wyrażenia jest wyrażeniem liczbowym lub łańcuchem tekstu. Symbol jest daną wartością i typem wyrażenia. Dyrektywy equ i „=” były w MASM, ponieważ początkowo Microsoft dodał dyrektywę textequ do MASM 6.0 Celem dyrektywy „=” jest zdefiniowanie symboli, które mają całkowitą (lub pojedynczy znak) wartość z nimi związaną. Dyrektywy te nie zezwalają na operandy rzeczywiste, łańcuchowe lub tekstowe. Jest to podstawowa dyrektywa jaką powinniśmy używać do tworzenia stałych liczbowych symbolicznych w naszych programach .Kilka przykładów: NumElements
= 16 . . . Array byte NumElements dup (?) . . . mov cx, NumElements mov bx, 0 ClrLoop: mov Array[bx],0 inc bx loop ClrLoop Dyrektywa textequ definiuje symbol zastępujący tekst. Wyrażenie w polu operandu musi być stałą tekstową ograniczoną między symbolami „<” i „>”.Ilekroć MASM napotka te symbole wewnątrz instrukcji, zastąpi tekst w polu operandu dla tego symbolu. Programiści zazwyczaj używają tego dyrektywy równości do zachowania typu lub uczynienia jakiegoś kodu bardziej czytelnym: Count textequ <6[bp]> DataPtr textequ <8[bp]> les bx, DataPtr ;to samo co les bx, 8[bp] mov cx, Count ;to samo co mov cx, 6[bp] mov al., 0 ClrLp: mov es:[bx], al inc bx loop ClrLp Zauważmy, że jest to zupełnie poprawna dyrektywa typu - symbol z pustym operandem używającym dyrektywy równości jak następuje: BlankEqu textequ <> Celem takiej dyrektywy równości będzie czyszczenie w segmencie asemblacji warunkowej i makrach. Dyrektywa equ dostarcza prawie nadzbioru zdolności dyrektyw „=” i textequ. Pozwala na operandy, które są numeryczne ,tekstowe lub stałymi literałami łańcuchowymi. Poniżej pokazano poprawne zastosowanie dyrektywy equ: One equ 1 Minus1 equ -1
TryAgain StringEqu TxtEqu
equ ‘Y’ equ „hello there” equ <4[si]> HTString byte StrinEqu ;to samo co HTString equ „hello there” mov ax, TxtEqu ;to samo co mov ax, 4[si] mov bl, One ;to samo co mov bl, 1 cmp al.,TryAgain ;to samo co cmp al., ‘Y’ Stałe jawne zadeklarowane z dyrektywami równości pomogą nam sparametryzować program. Jeśli użyjemy tych samych wartości, łańcuchów lub tekstu wiele razy wewnątrz programu, używając symbolicznych dyrektyw równości uczynimy go dużo łatwiejszym na zmiany tych wartości w przyszłości modyfikując ten program .Rozważmy następujący przykład: Array byte 16 dup (?) mov cx, 16 mov bx,0 ClrLoop: mov Array[bx],0 inc bx loop ClrLoop Jeśli zdecydujemy, że chcemy mieć Array 32 elementowy zamiast 16,będziemy musieli poszukać w całym naszym programie i zlokalizować każde odniesienie do tej danej i zmodyfikować odpowiednio stałą znakową. Wtedy istnieje możliwość, że nie zmodyfikujemy jakiejś sekcji kodu, wprowadzając błąd do naszego programu. Z drugiej strony, jeśli użyjemy stałej symbolicznej NumElements pokazanej wcześniej, będziemy musieli tylko zmienić pojedynczą instrukcję w naszym programie ,zreassemblować go, i jesteśmy w domu; MASM automatycznie uaktualni wszystkie odnośniki używające NumElements. MASM pozwala nam przedefiniować symbole zadeklarowane dyrektywą „=”.To znaczy, że następujące przykłady są poprawne: JakiśSymbol = 0 JakiśSymbol = 1 Ponieważ możemy zmieniać wartości stałych w programie ,zasięg symbolu (gdzie symbol ma szczególną wartość) staje się ważny. Jeśli nie moglibyśmy redefiniować symbolu, moglibyśmy się spodziewać, że symbol ma tą stałą wartość wszędzie w programie. Zważywszy, że możemy redefiniować stałą, zasięg symbolu nie może obejmować całego programu. MASM używa jednego rozwiązania, zasięg stałej jawnej jest od punktu gdzie jest ona zdefiniowana do punktu gdzie jest ona redefiniowana. Ma to jedną ważna konsekwencję – musimy zadeklarować wszystkie stałe jawne dyrektywą „=” zanim użyjemy tych stałych. Oczywiście zaraz po tym jak przedefiniujemy stałą symboliczną, poprzednia wartość tej stałej jest zapominana. Zauważmy, że nie możemy przedefiniować symboli zadeklarowanych dyrektywami textequ i equ. 8.6 DYREKTYWY PROCESORA Domyślnie, MASM będzie assemblował tylko instrukcje które są dostępne na wszystkich członkach rodziny 80x86.W szczególności ,to znaczy, że nie będą asemblowane instrukcje które nie są dostępne na mikroprocesorach 8086 i 8088.Przez generowanie błędów dla instrukcji nie-8086,MASM zapobiega przypadkowemu zastosowaniu tych instrukcji nie są dostępne na różnych procesorach. Jest to dobre o ile, rzeczywiście chcemy użyć tych instrukcji dostępnych na procesorach poza 8086 i 8088.Dyrektywy procesora pozwalają nam zastosować instrukcje asemblacji dostępnych na późniejszych procesorów. Dyrektywy procesora to: .8086 .8087 .186 .286 .287 .286P .386 .387 .386P .486 .486P .586 .586P
Żadna z tych dyrektyw nie akceptuje żadnych operandów. Dyrektywy procesora aktywują wszystkie instrukcje dostępne na danym procesorze. Ponieważ rodzina 80x86 jest kompatybilna w górę, wyszczególniając poszczególne dyrektywy procesora aktywując wszystkie instrukcje na tym procesorze i również wszystkich wcześniejszych procesorach. Dyrektywy .8087, .287, i .387 aktywują zbiór instrukcji zmiennoprzecinkowych dla danego koprocesora zmiennoprzecinkowego. Jednakże dyrektywa .8086 również włącza zbiór instrukcji 8087;podobnie .286 aktywuje zbiór instrukcji 80286 a .386 aktywuje zbiór instrukcji zmiennoprzecinkowych 80387.jedynym celem dla tych dyrektyw FPU (koprocesor arytmetyczny) jest zezwolenie na współpracę instrukcji 80287 z 8087 lub zbioru instrukcji 80186 lub 80387 ze zbiorem instrukcji 8086,80186 lub 80286. Dyrektywy procesora zakończone „P” pozwalają assemblować w trybie uprzywilejowanym instrukcji. Instrukcje trybu uprzywilejowanego są użyteczne tylko dla pisania systemów operacyjnych, pewnych sterowników i innych zaawansowanych podprogramów systemowych. Ponieważ ten tekst nie omawia instrukcji trybu uprzywilejowanego, omówimy trochę tych instrukcji później. Procesory 80386 i późniejsze wspiera dwa typy segmentów kiedy działamy w trybie chronionym – 16 bitowe segmenty i 32 bitowe segmentowe. W trybie rzeczywistym. te procesory wspierają tylko segmenty 16 bitowe. Assembler musi wygenerować subtelnie różne opcody dla 16 i 32 bitowych segmentów .Jeśli wyszczególnimy 32 bitowy procesor używając .386,.486 lub .586,MASM domyślnie generuje instrukcje dla 32 bitowego segmentu. Jeśli spróbujemy uruchomić taki kod w trybie rzeczywistym pod MS-DOSem , prawdopodobnie spowodujemy krach systemu. Są dwa rozwiązania tego problemu. Pierwszym jest wyszczególnienie use16 jako operandu dla każdego segmentu jaki tworzymy w naszym programie .Drugie rozwiązanie jest odrobinę bardziej praktyczne, po prostu wstawimy następującą instrukcję po 32 bitowej dyrektywy procesora: option segment:use16 Ta dyrektywa mówi MASMowi aby wygenerował domyślnie segmenty 16 bitowe zamiast 32 bitowych segmentów. Zauważmy, że MASM nie wymaga procesora 80486 lub Pentium jeśli wyszczególniamy dyrektywy .486 lub .586.Sam assembler jest napisany w kodzie 80386 (począwszy od wersji 6.1) więc potrzebujemy tylko procesora 80386 dla asemblacji każdego programu MASMem. Oczywiście, jeśli używamy określonych instrukcji procesorów 80486 lub Pentium, będziemy musieli zastosować procesor 80486 lub Pentium dla uruchomienia zassemblowanego kodu. Możemy selektywnie aktywować lub dezaktywować różne zbiory instrukcji w całym naszym programie. Na przykład możemy włączyć instrukcje 80386 w kilku liniach naszego kodu a potem powrócić do instrukcji 8086.Następująca sekwencja to demonstruje: .386 ;zaczynamy używać instrukcji 80386 ,ten kod może mieć instrukcje 80386 .8086 ;wracamy do instrukcji 8086 ;ten kod może mieć tylko instrukcje 8086 Możliwe jest napisanie programu, który wykrywa, podczas wykonywani programu, na jakim procesorze rzeczywiście jest wykonywany program. Dlatego też, możemy wykryć procesor 80386 i używać instrukcji 80386.Jeśli nie wykryjemy procesora 80386 ,możemy wetknąć instrukcje 8086.Poprzez selektywne włączanie instrukcji 80386 w tych segmentach naszego programu, który wykonujemy jeśli jest obecny procesor 80386,możemy wykorzystać dodatkowe instrukcje. Podobnie, poprzez wyłączanie zbioru instrukcji 80386 w inne sekcji naszego programu, możemy zabezpieczyć się przed nieumyślnym zastosowaniem instrukcji 80386 w części programu dla 8086. 8.7 PROCEDURY W odróżnieniu od HLL’i, MASM nie wprowadza surowych zasad co do tego jak składać procedury. Możemy wywołać procedurę spod każdego adresu w pamięci. Pierwsza instrukcja ret napotkana podczas wykonywania programu kończy procedurę. Taka pełna wyrazu swoboda, często jest nadużywana przez programy, które są bardzo trudne do odczytu i pielęgnacji. Dlatego też MASM dostarcza udogodnień przy deklaracji procedur wewnątrz naszego kodu. Podstawowym mechanizmem dla deklaracji jest: Procname proc {NEAR or FAR} procname endp
Jak widzimy, definicja procedury wygląda podobnie do tej dla segmentu. Jedyna różnica jest taka ,że procname (to znaczy nazwa definiowanej procedury) musi być unikalnym identyfikatorem wewnątrz naszego programu. Nasz kod wywołuje tą procedurę używając jej nazwy, nie zrobi tego jeśli będzie miał inną procedurę o tej samej nazwie; jak program może określić który podprogram wywołać? Proc może mieć kilka operandów, mimo, że możemy rozważać tylko trzy: pojedyncze słowo kluczowe near, pojedyncze słowo kluczowe far lub puste pole operandu. MASM używa tych operandów do określenia czy wywołaliśmy tą procedurę instrukcją bliskiego lub dalekiego wywołania. Określają one również jaki typ instrukcji ret MASM wyemituje wewnątrz procedury. Rozważmy następujące dwie procedury: NProc NProc FProc FProc
proc mov ret endp
near ax, 0
proc mov ret endp
far ax, 0FFFFH
i: call NPROC call FPROC Assembler automatycznie generuje trzybajtowe (bliskie) wywołanie dla pierwszej instrukcji call, ponieważ wie ,że NProc jest procedurą bliską. generuje również pięciobajtową (daleką) instrukcję call dla drugiego wywołania, ponieważ FProc jest procedurą daleką. Wewnątrz samej procedury, MASM automatycznie konwertuje wszystkie instrukcje ret do bliskiego lub dalekiego powrotu w zależności od typu podprogramu. Zauważmy, że jeśli nie zakończymy sekcji proc /endp ret’em lub inną instrukcją sterowania przesyłaniem danych a przebieg programu wykonuje się do dyrektywy endp, wykonywanie będzie kontynuowane od następnej wykonywalnej instrukcji następującej po endp .Na przykład rozważmy coś takiego: Proc1 proc mov ax, 0 Proc1 endp Proc2 proc mov bx, 0FFFFH ret Proc2 endp Jeśli wywołamy Proc1, sterowanie przejdzie do Proc2 z instrukcją mov bx, 0FFFFh.W odróżnieniu od procedur języków wysokiego poziomu, procedury język assemblera nie zwierają ukrytych instrukcji powrotu przed dyrektywą endp. Więc zawsze musimy być świadomi jak pracują dyrektywy proc / endp. Nie jest nic specjalnego w deklaracjach procedur. Są one wygodą dostarczoną przez assembler, niczym więcej. Możemy pisać programy w języku assemblera prze resztę swojego życia i nigdy nie użyć dyrektyw proc i endp. Jednak robiąc tak, bylibyśmy kiepskimi programistami. Proc i endp są dobrze udokumentowanymi cechami, które, kiedy są użyte poprawnie, mogą pomóc nam uczynić nasze programy dużo łatwiejsze do odczytu i pielęgnacji. MASM w wersji 6.0 i późniejszych traktuje wszystkie etykiety instrukcji wewnątrz procedury jako lokalne. To znaczy, nie możemy odwołać się bezpośrednio do tych symboli z zewnątrz procedury. 8.8 SEGMENTY Wszystkie programy składają się z jednego lub więcej segmentów. Oczywiście, kiedy nasz program jest uruchomiony, rejestry segmentowe 80x86 wskazują na bieżący aktywny segment. W 80286 i wcześniejszych procesorach, możemy mieć do czterech aktywnych segmentów na raz. (kodu, danych, specjalny i stosu);w 80386 i późniejszych procesorach są dwa dodatkowe rejestry segmentowe s i gs. Chociaż nie możemy uzyskać dostępu do danych w więcej niż czterech lub sześciu segmentach w danej chwili, możemy modyfikować rejestry segmentowe 80x86 aby wskazywały na inne segmenty w pamięci pod kontrolą programu. To znaczy, że program może uzyskać dostęp do więcej niż czterech lub sześciu segmentów. Pytanie :”jak tworzymy te różne segmenty w programie i jak możemy uzyskać dostęp do nich w czasie wykonywania?”. Segmenty, w naszym asemblerowym pliku źródłowym, są definiowane dyrektywami segment i ends. Możemy wprowadzić tak dużo segmentów ile chcemy w naszym programie. Cóż, w rzeczywistości jesteśmy ograniczeni do 65,536 różnych segmentów przez procesory 80x86 a MASM prawdopodobnie nawet nie pozwoli
na tyle, ale prawdopodobnie nigdy nie będziemy przekraczać liczby segmentów na jakie pozwala nam MASM do wprowadzenia w naszym programie. Kiedy MS-DOS zaczyna wykonywanie naszego ,inicjuje dwa rejestry segmentowe. Wskazuje cs jako segment zawierający nasz program główny i ss jako nasz segment stosu. Z tego punktu widzenia, jesteśmy odpowiedzialni za utrzymanie swoich rejestrów segmentowych. Aby uzyskać dostęp do danych w szczególnym segmencie ,rejestry segmentowe 80x86 muszą zawierać adres tego segmentu. Jeśli uzyskujemy dostęp do danych w kilku różnych segmentach, nasz program będzie musiał załadować rejestr segmentowy adresem segmentowym przed uzyskaniem dostępu do niego. Jeśli często uzyskujemy dostęp do danych w różnych segmentach, spędzimy dużo czasu przeładowując rejestry segmentowe. Na szczęście, większość programów wykazuje lokalność odniesienia kiedy uzyskuje dostęp do danych. To znaczy, że kawałek kodu będzie uzyskiwał dostęp do tej samej grupy zmiennych wiele razy podczas danego okresu czasu .Łatwo jest zorganizować tak nasze programy, aby zmienne do których często chcemy uzyskać dostęp, pojawiły się w tym samym segmencie. Przez aranżowanie naszych w taki sposób, możemy zminimalizować liczbę razy, jakiej będziemy potrzebować. Do ładowania rejestrów segmentowych. W tym sensie, segment nie jest niczym więcej niż buforem dla często używanych danych. W trybie rzeczywistym, segment może być długi na 65Kilobajty.Większość programów czystego języka assemblera używa mniej niż 64K kodu,64K danych globalnych i 64K przestrzeni stosu .Dlatego też możemy często korzystać z nie więcej niż trzech lub czterech segmentów w naszym programie. Faktycznie ,plik SHELL.ASM (zawierający szkielet programu asemblerowego) definiuje cztery segmenty a generalnie możemy użyć tylko trzech z nich. Jeśli użyjemy pliku SHELL.ASM jako podstawy naszych programów, rzadko będziemy musieli się martwić o dzielenie na segmenty w 80x86.Z drugiej strony, jeśli chcemy pisać złożone programy, będziemy musieli zrozumieć segmentację. Segment w naszym pliku powinien przybrać formę: segmentname segment {READONLY} {align} {combine} {use} {‘class’} Instrukcje segmentname ends Następna sekcja opisuje każdy z tych operandów dyrektywy segment Notka: segmentacja jest koncepcją, która dla wielu początkujących programistów asemblerowych wydaje się trudną do zrozumienia .Zauważmy ,że nie musimy zrozumieć dogłębnie segmentacji aby zacząć pisać programy asemblerowe 80x86.Jeśli będziemy robić kopię pliku SHELL.ASM do każdego programu jaki piszemy ,możemy zignorować sprawę segmentacji. Głównym zadaniem pliku SHELL.ASM jest zaopiekowanie się szczegółami segmentacji. Tak długo jak nie będziemy pisać niezmiernie długich programów lub używać olbrzymiej ilości danych, powinniśmy móc użyć SHELL.AM i zapomnieć o segmentacji. Pomimo to, ostatecznie możemy chcieć napisać większe programy asemblerowe, lub możemy chcieć napisać podprogram asemblerowy dla języka wysokiego poziomu takiego jak Pascal lub C++.W tym miejscu będziemy musieli poznać troszkę segmentację 8.8.1 NAZWY SEGMENTÓW Dyrektywa segmentu wymaga etykiety w polu etykiety. Etykieta ta jest nazwą segmentu. MASM używa nazw segmentów dla trzech celów: łączenia segmentów ,określenia czy prefiks przesłonięcia segmentu jest konieczny uzyskanie adresu segmentu. Musimy również wyszczególnić nazwę segmentu w polu etykiety dyrektywy ends która kończy segment. Jeśli nazwa segmentu nie jest unikalna (tj. zdefiniowaliśmy ją gdzieś w programie.),inne użycie musi również być definicją segmentu. Jeśli jest inny segment o tej samej nazwie, wtedy assembler traktuje tą definicję segmentu jako kontynuację poprzedniego segmentu używającego tej samej nazwy. Każdy segment ma swoją własną wartość licznika lokacji z nim powiązaną. Kiedy zaczynamy nawy segment(to znaczy, segment ,którego nazwa nie pojawiła się jeszcze w pliku źródłowym) MASM stworzy nową zmienną licznika lokacji, początkowo zero dla segmentu. Jeśli MASM napotka definicję segmentu która jest kontynuacją poprzedniego segmentu, wtedy MASM używa wartości licznika lokacji na końcu poprzedniego segmentu. Np. CSEG CSEG DSEG Item1 Item2 DSEG CSEG
segment mov ax,bx ret ends segment byte 0 word 0 ends segment mov ax,10
add ax,Item1 ret CSEG ends End Pierwszy segment (CSEG) zaczyna się wartością zero licznika lokacji .Instrukcja mov ax ,bx jest dwubajtowa a instrukcja ret jest jednobajtowa, więc licznik lokacji wynosi trzy na końcu segmentu. DSEG jest innym trzybajtowym segmentem, więc licznik lokacji powiązany z DSEG również zawiera trzy na końcu segmentu. Trzeci segment również ma tą samą nazwę jak segment pierwszy (CSEG),dlatego też assembler zakłada, że są one tym samym segmentem , w drugim wystąpieniu ,jako proste przedłużenie pierwszego .Dlatego też, kod umieszczony w drugim segmencie CSEG będzie assemblowany poczynając od offsetu trzy wewnątrz CSEG – faktyczne kontynuowanie kodu w pierwszym segmencie CSEG. Kiedykolwiek wyszczególnimy nazwę segmentu jako operand instrukcji, MASM użyje bezpośredniego trybu adresowania i zastąpi adres tego segmentu dla jego nazwy. Ponieważ nie możemy załadować wartości bezpośredniej do rejestru segmentu pojedynczą instrukcją, ładujemy adres segmentowy do rejestru segmentowego dwoma instrukcjami. Na przykład, następujące trzy instrukcje pojawiają się na początku pliku SHELL.ASM, inicjują one rejestry ds i es więc wskazują one segment dseg: mov ax, dseg ;ładuje ax adres segmentowy dseg mov ds.,ax ;ds. wskazuje dseg mov es,ax ;es wskazuje dseg Innym celem dla nazwy segmentu jest dostarczenie części składowej nazwie zmiennej. Pamiętamy ,adresy 80x86 zawierają dwie części składowe :segment i offset. Ponieważ 80x86 domyślnie większość danych odnosi do segmentu danych, jest powszechną praktyką wśród programistów asemblerowych, robić to samo, to znaczy nie zawracają sobie głowy wyszczególnianiem nazwy segmentu kiedy uzyskują dostęp do zmiennych w segmencie danych. Faktycznie, pełne odniesienie do zmiennej składa się z nazwy segmentu, dwukropka i nazwy offsetu: mov ax, dseg : Item1 mov dseg : Item2, ax Technicznie, powinniśmy wstawić przedrostek z nazwą segmentu przed wszystkimi zmiennymi w ten sposób. Jednak ,większość programistów nie martwi się tym specjalnym wymaganiem. Większość czasu będziemy się obchodzić bez tego ;jednak jest kilka momentów, kiedy rzeczywiście będziemy musieli wyszczególnić nazwę segmentu. Na szczęście, sytuacje te są rzadkie i tylko występują w bardzo złożonych programach. Ważne jest ,żeby zdawać sobie sprawę, że wyspecyfikowanie nazwy segmentu przed nazwą zmiennej nie znaczy, że możemy uzyskać dostęp do danej w segmencie bez posiadania jakiegoś rejestru segmentowego wskazującego na ten segment. Z wyjątkiem instrukcji jmp i call nie ma instrukcji 80x86 które pozwalają nam wyspecyfikować pełny 32 bitowy bezpośredni adres segmentowy. Wszystkie inne odniesienia do pamięci używają rejestru segmentowego dla dostarczenia części składowych adresu segmentowego 8.8.2 PORZĄDEK ŁADOWANIA SEGMENTÓW Normalnie segmenty są ładowane do pamięci w kolejności w jakiej pojawiają się w naszym pliku źródłowym W powyższym przykładzie, DOS będzie ładował segment CSEG do pamięci przed segmentem DSEG .Jeśli nawet segment CSEG pojawia się w dwóch częściach, przed i po DSEG, deklaracja CSEG przed wystąpieniem DSEG mówią DOSowi aby załadował cały segment CSEG do pamięci przed DSEG. Aby załadować DSEG przed CSEG, możemy użyć następującego programu: DSEG segment public DSEG ends CSEG segment public mov ax ,bx ret CSEG ends DSEG segment public Item1 byte 0 Item2 byte 0 DSEG ends CSEG segment public mov ax, 10 add ax, Item1 ret CSEG ends end
Pusty segment deklarowany jako DSEG nie emituje żadnego kodu. Wartość licznika lokacji dla DSEG wynosi zero na końcu definicji segmentu. W związku z tym mam zero na początku następnego segmentu DSEG, dokładnie jakby był kontynuacją poprzedniej wersji programu. Jednak ponieważ deklaracja DSEG pojawia się pierwszy w programie, DOS załaduje go do pamięci jako pierwszy. Porządek pojawiania się jest tylko jednym z czynników porządku ładowania. Na przykład, jeśli użyjemy dyrektywy .alpha, MASM będzie organizował segmenty alfabetycznie zamiast według pierwszeństwa pojawiania. Opcjonalne operandy dyrektyw segmentu również sterują porządkiem ładowania. Te operandy są tematem następnej sekcji. 8.8.3 OPERANDY SEGMENTU Dyrektywa segment pozwala na sześć różnych pozycji w polu operandu: operand wyrównania, operand łączenia, operand klasy, operand tylko do odczytu ,operand „uses” i operand rozmiaru .Trzy z tych operandów sterują jak DOS ładuje segment do pamięci, pozostałe trzy sterują generowaniem kodu. 8.8.3.1 TYP WYRÓWNANIA Parametr wyrównania jest jednym z następujących słów: byte, word, dword, para lub page. Te słowa kluczowe instruują assembler, linker i DOS do załadowania segmentu do granicy bajtu, słowa, podwójnego słowa, paragrafu lub strony. Parametr wyrównania jest opcjonalny .Jeśli jedno z powyższych słów kluczowych nie pojawia się jako parametr dyrektywy segmentu, domyślnym wyrównaniem jest paragraf (paragraf jest wielokrotnością 16 bajtów) Wyrównywanie segmentu do granicy słowa ładuje segment do pamięci począwszy od pierwszego dostępnego bajtu po ostatnim segmencie. Wyrównanie do granicy słowa zaczyna segment od pierwszego bajtu parzystego adresu po ostatnim segmencie. Wyrównanie od granicy dword lokuje bieżący segment od pierwszego adresu który jest parzystą wielokrotnością cztery po ostatnim segmencie. Na przykład, jeśli segment #1 jest zadeklarowany pierwszy w naszym pliku źródłowym a segment #2 bezpośrednio po nim i jest wyrównany bajtem, segment będzie przechowywany w pamięci jak następuje (zobacz rysunek 8.1)
Rysunek 8.1:Segment wyrównany bajtem
Rysunek 8.2: Segment wyrównany słowem seg1
seg1 seg2
segment ends segment -
byte
seg2 ends Jeśli segmenty jeden i dwa są zadeklarowane tak jak poniżej ,a segment #2 jest wyrównany słowem ,segmenty pojawią się w pamięci jak pokazano na rysunku 8.2 seg1
seg1 seg2
seg2
segment ends segment ends
word
Inny przykład: jeśli segmenty jeden i dwa są takie jak poniżej. A segment #2 jest wyrównany podwójnym słowem, segmenty będą przechowywane w pamięci tak jak pokazano na rysunku 8.3 seg1
seg1 seg2
seg2
segment ends segment ends
dword
Rysunek 8.3: Segment wyrównany podwójnym słowem
Rysunek 8.4: Segment wyrównany Paragrafem Ponieważ rejestry segmentowe 80x86 zawsze wskazują adres paragrafu, większość segmentów jest wyrównywanych do 16 bajtowej granicy paragrafu (para).Przeważnie nasze segmentu powinny zawsze być wyrównywane do granicy paragrafu, chyba, że mamy dobry powód innego wyboru.
Na przykład, jeśli segmenty jeden i dwa są zadeklarowane tak jak poniżej ,a segment #2 jest wyrównany paragrafem, DOS będzie przechowywał segment w pamięci jak pokazano na rysunku 8.4 seg1
segment seg1 ends seg2 segment para seg2 ends Granica strony wymusza wyrównanie segmentu zaczynającego się od następnego adresu który jest równy wielokrotności 256 bajtów. Pewne bufory danych mogą wymagać wyrównania do granicy 256 (lub 512) bajtów. Opcja wyrównania strony może być w tej sytuacji użyteczna. Na przykład, jeśli segmenty jeden i dwa są zadeklarowane jak poniżej a segment #2 jest wyrównany stroną ,segmenty będą przechowywane w pamięci jak pokazano na rysunku 8.5 eg1 segment seg1 ends seg2 segment page seg2 ends
Rysunek 8.5: Segment z wyrównaniem strony
Rysunek 8.6: Wyrównanie segmentu do paragrafu Jeśli wybierzemy jakieś wyrównanie inne niż bajt, assembler, linker i DOS mogą wprowadzić kilka fikcyjnych bajtów pomiędzy dwa segmenty ,żeby segment był właściwie wyrównany. Ponieważ rejestry segmentowe muszą zawsze wskazywać adres paragrafu(to znaczy, muszą być wyrównane paragrafem) możemy się zastanawiać jak procesor może adresować segment, który jest wyrównany do granicy bajtu, słowa lub podwójnego słowa. To jest łatwe. Kiedykolwiek wyszczególnimy wyrównanie segmentu, które wymusza segment zaczynający się spod adresu, który nie jest granicą paragrafu, assembler /linker założą, że rejestr
segmentowy wskazuje na poprzedni adres paragrafu a licznik lokacji zaczyna się jakimś offsetem do tego segmentu innego niż zero. Na przykład przypuśćmy ,że segment #1 kończy się na adresie fizycznym 10F87h a segment #2 jest wyrównany bajtem. Kod dla segmentu #2 będzie się zaczynał pod adresem segmentowym 10F80h.Jednakże,będzie nachodził na segment #1 na osiem bajtów. Aby przezwyciężyć ten problem, licznik lokacji dla segmentu #2 będzie zaczynał się od 8,więc segment będzie ładowany do pamięci tuż poza segmentem #1. Jeśli segment #2 jest wyrównany bajtem a segment #1 nie kończy się na parzystym adresie paragrafu ,MASM poprawi pozycję startową licznika lokacji dla segmentu #2,aby mógł użyć poprzedniego adresu paragrafu przy dostępie do niego. (zobacz Rysunek 8.6) Ponieważ 80x86 wymaga wszystkich segmentów do rozpoczęcia granicy paragrafu w pamięci, MASM (domyślnie) zakłada, że chcemy wyrównania paragrafem dla naszego segmentu. Poniższa definicja segmentu jest zawsze wyrównywana do granicy paragrafu: CSEF segment mov ax,bx ret CSEG ends end 8.8.3.2 TYP ŁĄCZENIA Typ łączenia steruje porządkiem w jakim segmenty o tej samej nazwie są wypisywane w kodzie wynikowym pliku tworzonego przez assembler. Dla wyspecyfikowania typu łączenia używamy jedno ze słów kluczowych public, stack, common, memory lub at .Memory jest synonimem dla public utrzymywane z powodu kompatybilności; powinniśmy zawsze używać public zamiast memory. Common i at są zaawansowanymi typami łączenia, które nie będą rozpatrywane w tym tekście. Typ łączenia stack powinien być używany z naszym segmentem stosu. Typ łączenia public powinien być używany wszędzie indziej. Typy łączenia public i stack w gruncie rzeczy wykonują te same operacje. Łączą one segmenty o tej samej nazwie do pojedynczego ,ciągłego segmentu, jak opisano wcześniej. Różnice pomiędzy nimi jest sposób w jaki DOS manipuluje inicjacją rejestrów segmentu stosu i wskaźnikiem stosu. Wszystkie programy powinny mieć przynajmniej jeden typ stack (albo linker wygeneruje ostrzeżenie);reszta powinna być publiczna. MS-DOS automatycznie wskazuje rejestr segmentu stosu w segmencie zadeklarowanym typem łączenia stack kiedy ładuje program do pamięci. Jeśli nie wyszczególnimy typu łączenia. wtedy assembler nie połączy segmentów kiedy tworzy plik kodu wynikowego praktyce, brak jakiegoś słowa kluczowego typu łączenia tworzy domyślnie typ łączenia private. Chyba, że typy klas są takie same (zobacz następną sekcję),każdy segment będzie emitowany jeśli MASM napotka go w pliku źródłowym. Na przykład rozważmy następujący program: CSEG segment public mov ax, 0 mov VAR1, ax CSEG ends DSEG segment public I word ? DSEG ends CSEG segment public mov bx, ax ret CSEG ends DSEG segment public J word ? DSEG ends end Ta część programu przynosi ten sam kod co: CSEG segment public mov ax, 0 mov VAR1, ax mov bx,ax ret CSEG ends DSEG segment public I word ? J word ?
DSEG
ends end
Assembler automatycznie łączy wszystkie segmenty które mają takie same nazwy i są publiczne .Przyczyną dla której assembler pozwala nam oddzielać segmenty jak te ,jest wygoda. Przypuśćmy, że mamy kilka procedur, z których każda wymaga pewnych zmiennych. Możemy zadeklarować wszystkie zmienne gdzieś w jednym segmencie, ale jest to często zakłóceniem. Większość ludzi jednak deklaruje swoje zmienne przed procedurą która ich używa. Poprzez używanie typu łączeniowego public z deklarowaniem segmentu, możemy zadeklarować nasze zmienne przed użyciem ich a assembler automatycznie przesunie te zmienne do właściwego segmentu kiedy assembluje program. Na przykład CSEG segment public ;To jest procedura #1 DSEG segment public ;lokalne zmienne dla procedury #1 VAR1 word ? DSEG ends mov AX,0 mov VAR1,AX mov BX,AX ret ;To jest procedura #2 DSEG segment public I word ? J word ? DSEG ends mov ax,I add ax,J ret CSEG ends ens Zauważmy ,że możemy zagnieździć segmenty w zadawalający sposób Niestety w MASM zakres działania zmiennych nie pracuje w ten sam sposób jak w HLL’ach takich jak Pascal .Normalnie ,raz zdefiniowany symbol wewnątrz naszego programu, jest widzialny gdziekolwiek w programie. 8.8.4 TYP KLASY Końcowym operandem dyrektywy segment jest zazwyczaj typ klasy. Typ klasy wyszczególnia porządek segmentów, które nie mają takich sam nazw segmentów. Ten operand składa się z symbolu zamkniętego przez apostrofy (cudzysłów nie jest tam dozwolony).generalnie powinniśmy używać następujących nazw: DODE (dla segmentu zawierającego kod programu);DATA(dla segmentu zawierającego zmienne, dane stałe i tablice);CONST (dla segmentu zawierającego dane stałe i tablice);i STACK (dla segmentu stosu),Poniższy fragment programu ilustruje ich użycie: CSEG segment public ‘CODE’ mov ax, bx ret CSEG ends DSEG Item1 Item2 DSEG CSEG
CSEG
segment byte byte ends segment mov add ret ends
SSEG STK SSEG
segment word ends
public ‘DATA’ 0 0 public ‘CODE’ ax, 10 Ax, Item1
stack ‘STACK’ 4000 dup (?)
C2SEG
segment public „CODE’ ret C2SEG ends end Rzeczywiste ładowanie procedury jest realizowane jak następuje. Assembler lokuje pierwszy segment w pliku{Ponieważ jest on segmentem łączonym public, MASM łączy wszystkie inne segmenty CSEG na końcu tego segmentu. W końcu ponieważ jest łączona klasa ‘CODE’,MASM dołączy wszystkie segmenty (C2SEG) o tych samych nazwach klas później. Po działaniu na tych segmentach, MASM szuka pliku źródłowego Dla następnego niepołączonego segmentu i powtarza cały proces. W powyższym przykładzie ,segmenty będą ładowane w następującym porządku: CSEG, CSEG(drugie wystąpienie),C2SEG.DSEG a potem SSEG. Ogólna zasada dotycząca jak nasz plik będzie ładowany do pamięci jest następująca • (1)Assembler łączy wszystkie segmenty publiczne, które mają taką samą nazwę • (2)Już połączone, segmenty są wysyłane do pliku z kodem wynikowym w porządku swojego pojawiania się w pliku źródłowym. Jeśli nazwa segmentu pojawia się dwa razy wewnątrz pliku źródłowego (a jest public),wtedy segment łączony będzie wysłany do pliku z kodem wynikowym na pozycji określonej przez pierwsze wystąpienie segmentu wewnątrz pliku źródłowego. • (3)Linker odczytuje plik z kodem wynikowym stworzony przez assembler i przestawia segmenty kiedy tworzy plik wykonywalny. Linker zaczyna przez przypisanie pierwszego znalezionego segmentu w pliku kodu wynikowego do pliku .EXE. Potem szuka w całym pliku z kodem wynikowym każdego segmentu z taką samą nazwą klasy. Takie segmenty są sekwencyjnie przypisywane do pliku .EXE. • (4)Kiedy wszystkie segmenty o tej samej nazwie klas jak segment pierwszy zostały wyemitowane do pliku .EXE, linker szuka pliku z kodem wynikowym dla następnego segmentu który, nie należy do tej samej klasy co poprzedni segment (y).Zapisuje ten segment do pliku .EXE a i powtarza krok (3) dla każdego segmentu należącego do tej klasy. • (5)W końcu, linker powtarza krok (4) aż do chwili kiedy zlinkuje wszystkie segmenty w pliku z kodem wykonawczym.
8.8.5 OPERAN TYLKO DO ODCZYTU Jeśli tylko do odczytu jest pierwszym operandem dyrektywy segment, assembler będzie generował błąd jeśli napotka jakąś instrukcję, która próbuje zapisać coś do tego segmentu. Jest to bardzo użyteczne dla kodu segmentu. Opcja ta w rzeczywistości nie zapobiega zapisaniu do tego segmentu w czasie wykonania. Jest bardzo łatwa sztuczka assemblera aby zapisać do tego segmentu tak czy tak.. Jednakże, przez wyszczególnienie readonly, możemy wyłapać jakieś powszechne błędy programistyczne ,które inaczej moglibyśmy przegapić. Ponieważ, rzadko będziemy umieszczać zmienne programowalne w naszym segmencie kodu, jest prawdopodobnie dobrym pomysłem ,uczynić nasze segmenty kodu readonly. Przykład operandu READONLY: seg1 segment readonly para public ‘DATA’ seg1 ends 8.8.6 OPCJE USE16,USE32 I FLAT Kiedy pracujemy z procesorem 80386 lub późniejszym, MASM generuje różne kody dla 16 a 32 bitowych segmentów. Kiedy piszemy kod do wykonywania w trybie rzeczywistym pod DOS, musimy zawsze używać segmentów 16 bitowych.32 bitowe segmenty mają zastosowanie do programów uruchomionych w trybie chronionym. Niestety MASM często domyślnie ustawia tryb 32 bitowy, kiedy tylko wybierzemy procesor 80386 lub późniejszy używając dyrektywy .386,.486 lub .586 w programie. Jeśli chcemy użyć 32 bitowych instrukcji, będziemy musieli jasno powiedzieć MASMowi o użyciu 16 bitowych segmentów. Operandy use16,use32 i flat dyrektywy segment pozwalają nam wyszczególnić rozmiar segmentu. Dla większości programów DOS’owskich ,zawsze będziemy używać operandu use16.Mówi on MASMowi ,że segment jest segmentem 16 bitowym i aby zasemblował go odpowiednio. Jeśli używamy jednej z dyrektyw do aktywacji zbioru instrukcji 80386 lub późniejszych, powinniśmy użyć use16 w całym naszym segmencie kodu lub MASM wygeneruje zły kod. Przykład użycia operandu use16: seg1 segment para public use16 ‘data’ -
seg1
ends Operandy use32 i flat mówią MASMowi aby wygenerował kod dla 32 bitowego segmentu. Ponieważ ten tekst nie zajmuje się programowanie w trybie chronionym, nie będziemy rozważać tych opcji. Jeśli chcemy wymusić use16 jako domyślny w programie, który zezwala na instrukcje 80386 i późniejsze, jest jeden sposób na zrobienie tego. Umieszczenie następującej dyrektywy w naszym programie przed każdym segmentem: .option segment:use16 8.8.7 DEFINICJE TYPOWYCH SEGMENTÓW Czy powyższe omówienie pozostawiło cię całkiem zakłopotanym? Nie martw się tym. Dopóki nie będziemy pisali wyjątkowo długich programów, nie będziemy musieli martwić się operandami powiązanymi z dyrektywą segment. Dla większości programów, następujące trzy segmenty powinny okazać się wystarczające: DSEG segment para public ‘DATA’ ;tu wprowadzamy definicje naszych zmiennych DSEG ends CSEG segment para public use16 ‘CODE’ ;tu wprowadzamy instrukcje naszego programu CSEG ends SSEG segment para stack ‘STACK’ stk word 1000h dup (?) EndStk equ this word SSEG ends end Plik SHELL.ASM automatycznie deklaruje te trzy segmenty. Jeśli zawsze będziemy kopiować pliki SHELL.ASM przy pisaniu nowego programu prawdopodobnie nie będziemy musieli martwić się o deklaracje segmentów i segmentację. 8.8.8 DLACZEGO BĘDZIEMY CHCIELI STEROWAĆ PORZĄDKIEM ŁADOWANIA Pewne wywołania DOS wymagają aby podać długość naszego programu jako parametr. Niestety, obliczanie długości programu zawierającego kilka segmentów jest bardzo trudnym procesem. Jednakże ,kiedy DOS ładuje nasz program do pamięci, załaduje cały program do zawartego bloku RAMu. Dlatego do obliczanie długości programu potrzebujemy znać tylko adres startowy i końcowy naszego programu .Po prostu różnica tych dwóch wartości stanowi o długości naszego programu. W programie, który zawiera wiele segmentów, będziemy musieli znać który segment będzie ładowany jako pierwszy, a który ostatni, żeby obliczyć długość naszego programu. Okazuje się, że DOS zawsze ładuje Prefiks Segmentu Programu lub PSP do pamięci, przed pierwszym segmentem naszego programu. Musimy brać pod uwagę długość PSP kiedy obliczamy długość programu. MS-DOS odkłada adres segmentowy PSP w rejestrze ds. Więc obliczenie różnicy pomiędzy ostatnim bajtem w programie a PSP da nam długość naszego programu. Następujący kod oblicza długość programu w paragrafie: CSEG segment public ‘CODE’ mov ax, ds. ;pobierz adres segmentowy PSP sub ax, seg LASTSEG ;oblicza różnicę ;AX zawiera teraz długość tego programu (w paragrafie) CSEG ends ;Wstawiamy wszystkie inne segmenty tutaj. LASTSEG segment para public „LASTSEG’ LASTSEG ends end 8.8.9 PRZEDROSTKI SEGMENTU Kiedy 80x86 odnosi się do operandu pamięci, zazwyczaj odnosi się do lokacji wewnątrz bieżącego segmentu danych .jednak, możemy poinstruować mikroprocesor do odniesienia danych w jednym z innych segmentów używając przedrostka segmentu przed wyrażeniem adresowym. Prefiksem segmentu jest ds:,cs:,ss:,es:,fs:, lub gs: .Kiedy używamy go przed wyrażeniem adresowym, prefiks segmentu instruuje 80x86 do pobrania operandu pamięci z wyszczególnionego segmentu zamiast segmentu domyślnego. na przykład, mov ax ,cs:I[bx] ładuje akumulator adresem I+bx wewnątrz bieżącego
segmentu kodu. Jeśli prefiks cs: byłby nieobecny, instrukcja normalnie załadowała by dane z bieżącego segmentu danych. Podobnie mov ds:[bp], ax przechowa akumulator w komórce pamięci wskazywanej przez rejestr bp w bieżącym segmencie danych (pamiętajmy ,kiedykolwiek używamy bp jako rejestry bazowego, wskazuje on na segment stosu). Prefiksy segmentu są opcodami instrukcji. Dlatego też, kiedykolwiek ich używamy, zwiększamy długość (i zmniejszamy szybkość) instrukcji wykorzystujących prefiksu segmentu. Dlatego też, nie chcemy używać prefiksów segmentów chyba, że mamy ku temu dobry powód. 8.8.10 SEGMENTY STERUJĄCE DYREYKTYWY ASSUME 80x86 generalnie odnoszą się do pozycji danych w stosunku do rejestru segmentowego ds (lub segmentu stosu).Podobnie, wszystkie odwołania kodu (skoki, wywołania ,itp.) są zawsze w stosunku do bieżącego segmentu kodu. Jest tylko jedna kwestia – jak assembler wie, który segment to segment danych a który jest segmentem kodu (lub innym segmentem)?Dyrektywa segment nie mówi nam jakiego typu segment pojawi się w programie. Pamiętajmy ,segment danych jest segmentem danych ponieważ wskazuje na niego rejestr ds. Ponieważ rejestr ds może być zmieniony w czasie wykonywania programu (przy użyciu instrukcji jak mov ds., ax),każdy segment może być segmentem danych. Ma to pewne interesujące konsekwencje dla assemblera. Kiedy wyszczególniamy segment w naszym programie, nie tylko musimy powiedzieć CPU, że segment jest segmentem danych, ale musimy również powiedzieć asemblerowi gdzie i kiedy ten segment jest segmentem danych (lub kodu. stosu /specjalnym/ F/G).Dyrektywa ASSUME dostarcza tej informacji assemblerowi. Dyrektywa assume przybiera następującą formę: assume {CS:seg} {DS.:seg} {ES:seg} {FS:seg} {GS:seg} {SS:seg} Nawiasy klamrowe otaczające pozycje opcjonalne, nie są częścią tych operandów. Zauważmy ,że musi być przynajmniej jeden operand. Seg jest albo nazwą segmentu (zdefiniowaną dyrektywą segment) albo zarezerwowanym słowem nothing. Wiele operandów w polu operandu dyrektywy assume musi być oddzielonych przecinkiem. Przykłady poprawnych dyrektyw assume: assume DS.:DSEG assume CS:CSEG, DS.:DSEG, ES:DSEG, SS:SSEG assume CS:CSEG,DS.:NOTHING Dyrektywa assume wskazuje assemblerowi, że musimy załadować wyszczególniony rejestr(y) segmentowe wyspecyfikowaną wartością adresu segmentowego. Zauważmy ,że ta dyrektywa nie modyfikuje żadnego rejestru segmentowego, po prostu mówi assemblerowi, żeby założył rejestry segmentowe wskazujące pewne segmenty w programie. Podobnie jak dyrektywy wyboru procesor i równości, dyrektywa assume modyfikuje zachowanie assemblera od gdzie napotyka ją MASM, aż do innej dyrektywy assume zmieniając podane założenia. Rozważmy następujący program: DSEG1 segment para public ‘DATA’ var1 word ? DSEG1 ends DSEG2 var2 DSEG2
segment word ends
para public ‘DATA’ ?
CSEG
segment assume mov mov mov mov mov mov assume mov mov mov -
para public ‘CODE’ CS:CSEG, DS.:DSEG1, ES:DSEG2 ax, seg DSEG1 ds.,ax ax, seg DSEG2 es, ax var1, 0 var2, 0
DS.:DSEG2 ax, seg DSEG2 ds., ax var2, 0
CSEG ends end Kiedykolwiek assembler napotka nazwę symboliczną, sprawdza ,który segment zawiera ten symbol .W powyższym programie,var1 pojawia się w segmencie DSEG1 a var2 pojawia się w segmencie DSEG2.Pamiętamy,że mikroprocesor 80x86 nie wie o zadeklarowanych segmentach wewnątrz programu, może uzyskać dostęp do danych wskazywanych przez rejestry segmentowe cs,ds.,es,ss,fs i gs .Instrukcja assume w tym programie mówi assemblerowi, że rejestr ds. wskazuje na DSEG1 w pierwszej części programu, a DSEG2 w drugiej części programu. Kiedy assembler napotyka instrukcję w postaci mov var1,0,pierwszą rzeczą jest określenie segmentu var1.Potem porównuje ten segment ponownie z listą uczynionych przez assembler założeń co do rejestrów segmentowych. Jeśli nie zadeklarujemy var1 w jednym z tych segmentów, wtedy assembler wygeneruje błąd stwierdzający, że program nie może uzyskać dostępu do tej zmiennej .Jeśli symbol (var1 w naszym przykładzie) pojawia się w jednym obecnie założonym segmencie, wtedy assembler sprawdza czy jest to segment danych. Jeśli tak, wtedy instrukcja jest assemblowana jak opisano w dodatkach. Jeśli symbol pojawia się w segmencie innym niż ten ,który zakłada assembler ,że wskazuje ds. ,wtedy assembler emituje bajt przedrostka przesłonięcia segmentu, specyfikujący faktyczny segment który zawiera dane. W przykładowym powyższym programie, MASM będzie assemblował pierwsze wystąpienie instrukcji mov var2,0 z bajtem prefiksu segmentu es: ponieważ assembler, zakłada że es zamiast ds wskazuje DSEG2.MASM będzie assemblował drugie wystąpienie tej instrukcji bez bajtu prefiksu segmentu es ponieważ assembler zakłada ,że ds. wskazuje na DSEG2.Zapamiętajmy,ze jest bardzo i łatwo zmylić assembler. Rozpatrzmy następujący kod: CSEG segment para public ‘CODE’ assume CS:CSEG,DS:DSEG1,ES:DSEG2 mov ax, seg DSEG1 mov ds,ax jmp SkipFixDS assume DS.:DSEG2 FixDS: mov ax, seg DSEG2 mov ds, ax SkipFixDS: CSEG ends End Zauważmy, że ten program skacze do kodu który ładuje rejestr ds wartością segmentu DSEG2.To znaczy, że pod etykietą SkipFixDS rejestr ds zawiera wskaźnik do DSEG1 ,nie dSEG2.Jednakże assembler nie jest dość błyskotliwy dla rozwiązania tego problemu, więc ślepo zakłada, że ds wskazuje na DSEG2 zamiast na DSEG1To będzie klęska .Ponieważ assembler zakłada. że uzyskujemy dostęp do zmiennych w DSEG2 podczas gdy rejestr ds w rzeczywistości wskazuje na DSEG1,taki dostęp będzie się odnosił do komórki pamięci w DSEG1 pod taki sam offset jaki ma zmienna w DSEG2.Zmieni to dane w DSEG1 (lub nasz program będzie czytał nieprawidłowe wartości dla zmiennych założonych w segmencie DSEG2). Dla początkujących programistów najlepszym rozwiązaniem tego problemu jest unikanie stosowania wielu segmentów (danych) wewnątrz swoich programów jeśli tylko to możliwe. Zachowajmy dostęp do wielu segmentów do dnia kiedy będziemy gotowi do zajęcia się takim problemem jak ten. Jako początkujący programista asemblerowy po prostu używaj jednego segmentu kodu, jednego segmentu danych i jednego segmentu stosu i pozostaw rejestry segmentowe wskazujące każdy na ten segment do którego jest przeznaczony. Dyrektywa assume jest całkiem złożona i może być przyczyną niezłych kłopotów jeśli będzie niewłaściwie używana. Lepiej nie zawracać sobie głowy używaniem assume do chwili kiedy lepiej zapoznamy się z całą ideo programowania w języku assemblera i segmentacji w 80x86. Słowo zarezerwowane nothing mówi assemblerowi, że nie mieliśmy najmniejszego pojęcia gdzie ma wskazywać rejestr segmentowy. Mówi również assemblerowi ,że nie powinien uzyskiwać dostępu do danych w stosunku do tego rejestru segmentowego ,chyba, że wyraźnie dostarczy prefiks segmentu do adresu. Powszechną konwencją programistyczna jest umiejscawianie dyrektywy assume przed wszystkimi procedurami w programie.
Ponieważ wskaźnik segmentu deklarujący segment w programie rzadko zmienia punkty wejścia i wyjścia procedury, jest to idealne miejsce do wstawienia dyrektywy assume: assume ds:P1Dseg, cs:cseg, es:nothing Procedura1 proc near push ds ;zachowanie ds push ax ;zachowanie ax mov ax, P1Dseg ;pobiera wskaźnik do P1dseg mov ds, ax ;do rejestry ds pop ax ;przywrócenie wartości ax pop ds ;przywrócenie wartość ds ret Procedura1 endp Jedyne problem z tym kodem jest to ,że MASM jeszcze zakłada, że ds wskazuje P1Dseg kiedy napotka kod po Procedura1Najlepszym rozwiązaniem jest wprowadzenie drugiej dyrektywy assume po dyrektywie endp ,która powie MASMowi, że nic nie wie o wartości w rejestrze ds: ret Procedura1 endp assume ds:nothing Chociaż następna instrukcja w programie prawdopodobnie będzie inną dyrektywą assume daną przez assembler jako nowe założenie co do ds (na początku procedury, która następuje powyżej),jest lepszym pomysłem zaadoptować tą koncepcję. Jeśli opuścimy wprowadzenie dyrektywy assume przed następną procedurą w naszym pliku źródłowym, instrukcja assume ds:nothing będzie utrzymywała assembler w założeniu, że możemy uzyskać dostęp do zmiennych w P1Dseg. Prefiks przesłonięcia segmentu zawsze ignoruje założenia czynione przez assembler. Mov ax, cs:var1 zawsze ładuje rejestr ax słowem spod offsetu var1 wewnątrz bieżącego segmentu kodu ,bez względu na to gdzie jest zdefiniowane var1.Głównym celem prefiksów przesłonięcia segmentów jest manipulacja pośrednimi odniesieniami. Jeśli mamy instrukcję w postaci mov ax,[bx] assembler zakłada, że bx wskazuje na segment danych. Jeśli rzeczywiście musimy uzyskać dostęp w różnych segmentach, możemy użyć przesłonięcia segmentu, w ten sposób mov ax, es:[bx]. Generalnie, jeśli mamy zamiar użyć wielu segmentów danych wewnątrz naszego programu powinniśmy użyć pełnej nazwy segment:offset dla naszej zmiennej. Np. mov ax,DSEG1:I i mov bx,DSEG2:J.Nie eliminuje to potrzeby ładowania rejestrów segmentowych lub uczynienia właściwego użytku z dyrektywy assume, ale uczyni to nasz program łatwiejszy do odczytu i pomoże MASMowi zlokalizować możliwe błędy w naszym programie. Dyrektywa assume jest w rzeczywistości całkiem użyteczna dla innych spraw poza ustawieniem segmentu domyślnego. Zobaczymy więcej zastosowań dla tej dyrektywy trochę później w tym rozdziale. 8.8.11 SEGMENTY ŁĄCZONE: DYREKTYWA GROUP Większość segmentów w typowym programie asemblerowym jest mniejsza niż 64 Kilobajty. Istotnie, większość segmentów jest dużo mniejszych niż 64KB.Kiedy MS-DOS ładuje segment programu do pamięci, kilka z segmentów może znaleźć się w pojedynczym 64KB regionie pamięci. W praktyce, możemy łączyć te segmenty w pojedynczym segmencie w pamięci. Może to umożliwić poprawę wydajności naszego kodu. Więc dlaczego po prostu nie połączyć takich segmentów w naszym assemblerowym kodzie? Cóż, jak wskazała poprzednia sekcja, utrzymanie oddzielnych segmentów może pomóc nam skonstruować nasze programy lepsze i pomóc uczynić je bardziej modularne. Ta modularność jest bardzo ważna w naszych programach. Jak zwykle, poprawa struktury i modularności naszych programów może spowodować, że staną się mniej wydajne. Na szczęście, MASM dostarcza dyrektywy group, która pozwala nam traktować dwa segmenty jako ten sam segment fizyczny bez porzucania struktury i modularności naszego programu. Dyrektywa group pozwala nam tworzyć nową nazwę segmentu, który obejmuje segmenty zgrupowane razem. Na przykład, jeśli mamy dwa segmenty nazwane „Module1Data” i „Module2Data”,które życzymy sobie połączyć do pojedynczego segmentu fizycznego, możemy użyć dyrektywę group jak następuje: ModuleData group Module1Data, Module2Data Jedynym ograniczeniem jest to, że koniec drugiego modułu danych musi być nie większy niż 64 kilobajty od początku pierwszego modułu w pamięci .MASM i linker nie będą łączyć automatycznie tych segmentów i
umiejscowić je razem w pamięci. Jeśli są inne segmenty pomiędzy tymi dwoma w pamięci, wtedy suma wszystkich segmentów musi być mniejsza niż 64kilobajtów.Dal zredukowanie tego problemu, możemy użyć operandu klasy dyrektywy segment. który powie linkerowi aby połączył te dwa segmenty w pamięci poprzez użycie takiej samej nazwy klasy: ModuleData group Module1Data, Module2Data Module1Data segment para public ‘MODULES’ Module ends Module2Data segment byte public ‘MODULES’ Module2Data ends Z taką deklaracją jak ta powyżej, może użyć „ModuleData” gdziekolwiek MASM pozwoli na nazwę segmentu ,jako operandu instrukcji mov, jako operandu dyrektywy assume, itp. .Poniższy przykład demonstruje zastosowanie nazwy segmentu ModuleData: ModuleProc
assume proc push push mov
ds:ModuleDAta near ds ;zachowanie wartości ds ax ;zachowanie wartości ax ax, ModuleData ;ładowanie ds segmentem
adress mov ds, ax ;ModuleData pop ax ;przywrócenie ax i ds pop ds ret ModuleProc end assume ds:nothing Oczywiście, używanie dyrektywy group w ten sposób nie możemy poprawić naszego kodu. Faktycznie użycie różnych nazw dla segmentu danych, może kłócić się z zastosowaniem group w ten sposób, właściwie zaciemnia kod. jednakże przypuśćmy ,że mamy sekwencję kodu ,która musi uzyskać dostęp do zmiennych w obu segmentach Module1Data i Module2Data.Jeśli te segmenty były fizycznie i logicznie oddzielone będziemy musieli załadować dwa rejestry segmentowe z adresami tych dwóch segmentów żeby jednocześnie uzyskać do nich dostęp .Będzie to kosztowało nas prefiks przesłonięcia segmentu na wszystkich instrukcjach uzyskujących dostęp do jednego z tych segmentów. Jeśli nie możemy użyczyć dodatkowego rejestru segmentowego, sytuacja będzie nawet gorsza, będziemy musieli stale ładować nową wartość do pojedynczego rejestru segmentowego dla uzyskania dostępu do danych w dwóch segmentach. Możemy uniknąć tego obciążenia poprzez połączenie dwóch logicznych segmentów w pojedynczy fizyczny segment i uzyskiwać dostęp bezpośrednio do grupy zamiast do pojedynczej nazwy segmentu. Jeśli grupujemy dwa lub więcej segmentów razem, wszystko co robimy to tworzenie pseudo – segmentu który obejmuje segmenty pojawiające się w polu operandu dyrektywy group. Grupowanie segmentów nie zapobiega uzyskiwaniu dostępu do pojedynczych segmentów z pogrupowanej listy .Poniższy kod jest zupełnie poprawny: assume ds:Module1Data mov ax, Module1Data mov ds, ax -
assume ds:Module2Data mov ax,Module2Data mov ds, ax assume ds:ModuleData mov ax,ModuleData mov ds, ax Kiedy assembler tworzy segmenty, zazwyczaj zaczyna od wartości licznika lokacji ustawionej na segment zero. Jednak jeśli grupujemy zbiór segmentów pojawiają się niejasności; zgrupowanie dwóch segmentów powoduje, że MASM i linker łączą zmienne z jednego lub więcej segmentów na końcu pierwszego segmentu z listy. Osiągają to poprzez modyfikowane offsetów wszystkich symboli w łączonych segmentach mimo, że były one symbolami w tym samym segmencie. Dwuznaczność występuje ponieważ MASM pozwala nam odniesienie do symbolu w segmencie lub grupie segmentów .Symbol ma różne offsety w zależności od wybranego segmentu. Rozwiązaniem tej dwuznaczności jest następujący algorytm: • Jeśli MASM nie wie, że rejestr segmentowy wskazuje na symbol segmentu lub grupę zawierającą segment, MASM wygeneruje błąd. • Jeśli dyrektywa assume powiązana jest z rejestrem segmentowym nazwy segmentu ale nie powiązana z rejestrem segmentowym grupy nazw, wtedy MASM używa offsetu symbolu wewnątrz segmentu. • Jeśli dyrektywa powiązana jest z rejestrem segmentowym grupy nazw ale nie powiązana z rejestrem segmentowym nazwy segmentowej symbolu, MASM używa offsetu symbolu grupy. • Jeśli dyrektywa assume dostarcza rejestru segmentowego powiązanego z oboma symbolami segmentu i grupą, MASM wybierze offset, który nie będzie wymagał prefiksu przesłonięcia segmentu. Na przykład, jeśli dyrektywa assume wyszczególni, że ds wskazuje nazwę grupy a es wskazuje nazwę segmentu, MASM użyje offset grupy jeśli domyślnym rejestrem segmentowym będzie ds ponieważ nie będzie to wymagało aby MASM wyemitował opcod prefiksu przesłonięcia segmentu Jeśli oba wyniki emitują prefiks przesłonięcia segmentu, MASM wybierze offset (i prefiks przesłonięcia offsetu) powiązany z symbolem segmentu. MASM używa powyższego algorytmu jeśli wyszczególnimy nazwę zmiennej bez prefiksu segmentu. Jeśli wyszczególnimy prefiks przesłonięcia rejestru segmentowego, wtedy MASM może wybrać offset przypadkowy. Często okazuje się być to offset grupy .Poniższa sekwencja instrukcji, bez dyrektywy assume powie MASMowi,że symbol BadOffset w seg1 może tworzyć zły kod wynikowy: DataSegs group Data1, Data2, Data3 Data2 segment BadOffset word ? Data2 ends assume ds:nothing, es:nothing, fs:nothing, gs:nothing mov ax, Data2 despite mov ds, ax mov ax, ds:BadOffset
DataSegs Jeśli chcemy wymusić prawidłowy offset ,używamy nazwy zmiennej zawierającej kompletny adres segment:offset : ;wymusimy użycie offsetu wewnątrz grupy DataSegs uzywając instrukcji takich jak ta: mov ax, DataSegs:BadOffset ;wymusimy użycie offsetu wewnątrz Data2, używając: mov ax, Data2:BadOffset Musimy roztoczyć specjalną troskę kiedy pracujemy z grupą wewnątrz naszego programu asemblerowego. Jeśli zmusimy MASM do użycia offsetu wewnątrz jakiegoś szczególnego segmentu (lub grupy) a rejestr segmentowy nie wskazuje na na ten szczególny segment lub grupę, MASM może nie wygenerować informacji o błędzie a program nie wykona się prawidłowo Odczytując offsety MASM nie pomoże nam znaleźć tego błędu. MASM zawsze wyświetli offset wewnątrz symbolu segmentu w listingu asemblacji. Jedyny rzeczywisty sposób wykrycia, że MASM i linker używają złego offsetu jest zastosowanie debuggera takiego jak CodeView i spojrzenie na aktualne bajty kodu maszynowego stworzonego przez linker i loader. 8.8.12 DLACZEGO ZAWRACMY SOBIE GŁOWĘ SEGMENTAMI? Po przeczytaniu poprzedniej sekcji, prawdopodobnie zastanawiamy się jakie możliwe są pożytki z zastosowania segmentów w naszych programach. Byłoby doskonale, gdybyśmy zastosowali plik SHELL.ASM jako szkielet dla naszych programów asemblerowych, wtedy możemy mieć łatwiej bez martwienia się o segmenty, grupy, prefiksy przesłonięcia segmentów i pełne nazwy segment:offset. Dla początkujących programistów asemblerowych jest to dobry pomysł, aby zignorować wiele z tego omówienia segmentacji. Jednak istnieją trzy powody dla nauczenia się o segmentacji, jeśli chcemy kontynuować pisanie programów assemblerowych o różnych długościach: ograniczenie segmentu do 64K w trybie rzeczywistym ,modularność programów ,i łączenie z językami wysokiego poziomu. Kiedy działamy w trybie rzeczywistym, segmenty mogą być długie maksymalnie na 64 kilobajty. Jeśli musimy uzyskać dostęp do więcej niż 64K danych lub kodu w programie, będziemy musieli użyć więcej niż jeden segment. Ten fakt, bardziej niż inne powody, odrzuca programistów od świata segmentacji. Niestety wielu programistów odchodzi od segmentacji. Oni rzadko uczą się wystarczająco o segmentacji, aby pisać programy które uzyskują dostęp do więcej niż 64K danych. W rezultacie ,kiedy wystąpią problemy z segmentacją, ponieważ nie do końca zrozumieli tą koncepcję, winią segmentację za swoje problemy i unikają stosowania segmentacji jak tylko jest to możliwe. Jest to bardzo złe ponieważ segmentacja jest silnym narzędziem zarządzania pamięcią, które pozwala nam organizować nasze programy w logiczne jednostki (segmenty) które są, w teorii, niezależne od innych. dziedzin inżynierii oprogramowania naucza jak pisać poprawne, duże programy. Modularność i niezależność są dwoma podstawowymi narzędziami inżynierii oprogramowania używanymi do pisania dużych programów które są poprawne i łatwe do pielęgnacji. Rodzina 80x86 dostarcza ,w sprzęcie ,narzędzia do implementacji segmentacji. Na innych procesorach, segmentacja jest wprowadzana wyłącznie programowo .W rezultacie, łatwiej jest pracować z segmentami na procesorach 80x86. Chociaż ten tekst nie zajmuje się programowaniem w trybie chronionym, warte jest wskazanie, że kiedy działamy w trybie chronionym na procesorach 80286 i późniejszych, sprzęt 80x86 może rzeczywiście zapobiegać aby jeden moduł uzyskiwał dostęp do danych innego modułu (istotnie termin „tryb chroniony” znaczy, że segmenty są chronione przed nieuprawnionym dostępem)Wiele debuggerów dostępnych dla MSDOS działa w trybie chronionym pozwalając nam na naruszenie obszaru tablicy i segmentu. Soft-ICE i Bounds Checker firmy NuMega są przykładami takich produktów. Większość ludzi którzy pracowali z segmentacją w środowisku trybu chronionego (np. OS/2 czy Windows) ceni sobie zalety oferowane przez segmentację. Innym powodem dla studiowania segmentacji na 80x86 jest to, że możemy chcieć pisać funkcje asemblerowe które może wywołać program w języku wysokopoziomowy. Ponieważ kompilator HLL czyni pewne założenia o organizacji segmentów w pamięci, będziemy musieli trochę wiedzieć o segmentacji żeby napisać taki kod. 8.9 DYREKTYWA END Dyrektywa end kończy plik źródłowy języka assemblera. W dodatku mówi MASMowi, że dotarł do końca pliku źródłowego ,operand opcjonalny dyrektywy end mówi MS-DOSowi gdzie ma przekazać sterowanie kiedy program zacznie się wykonywać; to znaczy, wyszczególniamy nazwę procedury głównej jako operand dyrektywy end. Jeśli nie ma operandu dyrektywy end, MS-DOS zacznie wykonywanie począwszy od pierwszego bajtu w pliku .exe. Ponieważ jest to często niepewna gwarancja, że nasz główny program zacznie się od pierwszego bajtu kodu wynikowego w pliku .exe, większość programistów wyszczególnia lokację startową jako operand dyrektywy end. Jeśli używamy pliku SHELL.ASM jako szkieletu dla naszych programów assemblerowych, zauważymy, że dyrektywa end już wyszczególniła procedurę main jako punkt startowy dla programu.
Jeśli nie stogujemy oddzielnej asemblacji i łączymy razem kilka różnych plików kodów wynikowych (zobacz ;”Zarządzanie Dużymi Programami”) tylko jeden moduł może mieć program główny. Podobnie ,tylko jeden moduł powinien wyszczególnić lokację startową programu. Jeśli wyszczególnimy więcej niż jedną lokację startową zmylimy linker i wygeneruje on błąd. 8.10 ZMIENNE Dla zadeklarowania zmiennych globalnych stosujemy pseudo-opcody byte/sbyte/ds,word/sword/dw, dword, sdword/dd, qword/dq i tbyte/dt. Chociaż możemy umieścić nasze zmienne w każdym segmencie (\wliczając w to segment kodu),większość początkujących programistów assemblerowych umieszcza wszystkie swoje zmienne globalne w pojedynczym segmencie danych. Typowa deklaracja zmiennych przybiera postać: varname byte wartość_inicjująca Varname jest nazwą zmiennej którą deklarujemy a wartość_inicjująca jest to wartość ,jaką chcemy aby ta zmienna miała w chwili kiedy program zacznie się wykonywać.”?” Jest specjalną wartością inicjującą Oznacza ona, że nie chcemy dać zmiennej wartości inicjującej. Kiedy DOS ładuje program zawierający takie zmienne do pamięci, nie inicjuje tej zmiennej żadną szczególną wartością. Powyższa deklaracja rezerwuje pamięć dla pojedynczego bajtu. Może to zmienić jakiś inny typ zmiennej poprzez prostą zmianę mnemonika byte na jakiś inny, właściwy pseudo-opcod. Przeważnie ten tekst będzie zakładał, że deklarujemy wszystkie zmienne w segmencie danych, to znaczy segment na który wskazuje rejestr ds 80x86.W szczególności większość programów umiejscawia wszystkie zmienne w segmencie DSEG (CSEG jest dla kodu, DSEG jest dla danych a SSEG dla stosu). Ponieważ Rozdział Piąty omawiał deklarację zmiennych, typów danych ,struktur, tablic i wskaźników, więc ten rozdział nie będzie marnował czasu na omawianie tych tematów. 8.11 TYPY ETYKIET Jedną niezwykła cechą składni assemblera Intela (takiego jak MASM) jest ścisła kontrola typów. Assembler ze ścisłą kontrolą typów kojarzy pewien typ z deklarowanymi symbolami pojawiającymi się w pliku źródłowym i generuje ostrzeżenie lub informację o błędzie jeśli spróbujemy zastosować ten symbol w kontekście na który nie pozwala jego szczególny typ .Chociaż, jest to niezwykłe w assemblerze, większość HLL’i stosuje pewne zasady typowania do deklaracji symboli w pliku źródłowym. Pascal oczywiście jest znany jako język ze ścisłą kontrola typów. Nie możemy w Pascalu przydzielić łańcucha do zmiennej liczbowej lub próbować przydzielić wartość całkowitą do procedury etykiety. Intel, projektując składnię dla assemblera 80x86,zdecydował,że wszystkie przyczyny stosowania języka o ścisłej kontroli typów stosują się również do języka asemblera równie dobrze jak do Pascala. Dlatego też, standardowa składnia assemblera 80x86,np. MASM, narzuca pewne ograniczenia typów na stosowanie symboli wewnątrz programów asemblerowych. 8.11.1 JAK NADAĆ SYMBOL POSZCZEGÓLNYM TYPOM Symbole, w programie assemblerowym 80x86,mogą być jednym z ośmiu podstawowych typów: byte, word, dword, qword ,tbyte, near, far i abs (stała).zawsze kiedy definiujemy etykietę pseudo-opcodem byte, word, dword, qword lub tbyte,MASM łączy typ tego pseudo-opcodu z etykietą. Na przykład ,następująca deklaracja zmiennej stworzy symbol typu bajt: Bvar byte ? Podobnie, definiowanie symbolu dword: DWVar dword ? Typy zmiennej nie są ograniczone do podstawowych typów wbudowanych w MASM’a .Jeśli tworzymy własny typ używając dyrektyw typedef lub struct MASM połączy te typy z dołączonymi deklaracjami zmiennych. Możemy zdefiniować bliskie symbole (znane również jako etykiety instrukcji) na parę różnych sposobów .Po pierwsze, wszystkie deklarowane symbole procedur dyrektywą proc (albo z pustym polem operandu albo near w polu operandu) są bliskimi symbolami. Etykiety instrukcji są również bliskimi symbolami. Etykiety instrukcji przybierają następującą postać: etykieta: instrukcja Instrukcja, reprezentuje instrukcję 80x86.Zauważmy,że dwukropek musi wystąpić po symbolu. Nie jest to część symbolu, dwukropek informuje assembler, że ten symbol jest etykietą instrukcji i powinien być potraktowany jako typ bliskiego symbolu. Etykiety instrukcji są często celami instrukcji skoków lub pętli. Na przykład rozważmy następującą sekwencję kodu.: mov cx, 25 Loop1: mov ax, cx
call PrintInteger loop Loop1 Instrukcja loop zmniejsza rejestr cx i przekazuje sterowanie do instrukcji oetykietowanej jako Loop1 dopóki cx nie będzie miało wartości zero. Wewnątrz procedury ,etykiety instrukcji są lokalne. To znaczy, że zakres etykiet instrukcji wewnątrz procedury jest dostępny tylko dla kodu wewnątrz tej procedury. Jeśli chcemy uczynić symbol globalnym dla procedury, umieścimy dwa dwukropki po nazwie symbolu. W powyższym przykładzie, jeśli musimy odnieść Loop1 na zewnątrz załączonej procedury, powinniśmy użyć kodu: mov cx, 25 Loop1:: mov ax,cx call PrintInteger loop Loop1 Ogólnie, dalekie symbole są celami instrukcji skoku i wywołania. Najpowszechniejszą metodą programistyczną użytą do stworzenia dalekiej etykiety jest umieszczenie far w polu operandu dyrektywy proc. Symbole, które są stałymi są normalnie definiowane z dyrektywą equ. Możemy również zadeklarować symbole z różnymi typami używając dyrektyw equ i extm/extem/extemdef. Wyjaśnienie dyrektywy extm pojawi się w sekcji „Zarządzanie Dużymi Programami” Jeśli zadeklarujemy stałą liczbową używając równania ,MASM przydzieli typ abs (wartość bezwzględna lub stała) do systemu. Dyrektywy równości text i string są dane jako typ text. Możemy również przydzielić dowolnie typ do symbolu używając dyrektywy equ. 8.11.2 WARTOŚCI ETYKIET Kiedykolwiek zdefiniujemy etykietę używając dyrektywy lub pseudo-opcodu, MASM nada jej typ i wartość. Wartość nadana przez MASM etykiecie jest zazwyczaj wartością bieżącą licznika lokacji. Jeśli zdefiniujemy symbol w dyrektywie równania jako jego operand zazwyczaj wyszczególniamy wartość symbolu. Kiedy napotyka etykietę w polu operandu, jak na przykład w instrukcji loop ,powyżej, MASM podstawia wartość etykiety za tą etykietę. 8.11.3 KONFLIKTY TYPÓW Ponieważ 80x86 wspiera symbole ścisłej kontroli typów, następne zadane pytanie to:” Po co są one używane? ”W dużym skrócie, symbole ścisłej kontroli typów mogą pomóc zweryfikować poprawną operację naszego programu. Rozpatrzmy następującą sekcję kodu: DSEG segment public ‘DATA’ I byte ? DSEG ends CSEG segment public ‘CODE’ mov ax, I CSEG ends end Instrukcja mov w tym przykładzie próbuje załadować rejestr ax (16 bitów) zmienną o rozmiarze bajta. Teraz mikroprocesor 80x86 jest zupełni zdolny do tej operacji. Załaduje rejestr al z komórki pamięci związanej z I i załaduje rejestr ah z następnej sąsiadującej komórki pamięci (która jest prawdopodobnie najmniej znaczącym bajtem innej zmiennej) Jednakże nie było to pierwotną intencją. Osoba ,która przeczyta ten kod prawdopodobnie zapomni, że I jest zmienną o rozmiarze bajta i założy, że jest to zmienna słowa – co jest zdecydowanym błędem w logice tego programu. MASM nigdy nie powinien pozwolić instrukcji takiej jak ta powyższa na zasemblowanie bez wygenerowania instrukcji diagnostycznej. Może to pomóc nam zaleźć błąd w programie. Czasami zaawansowani programiści assemblerowi mogą chcieć wykonać instrukcje jak te powyższe. MASM dostarcza
pewnych operatorów sprawdzania zgodności typów, które obchodzą mechanizmy zabezpieczeń MASMa i pozwalają na niepoprawne operacje. 8.12 WYRAŻENIA ADRESOWE Wyrażenie adresowe jest wyrażeniem algebraicznym, które tworzy wynik liczbowy, które MASM scala do pola przemieszczenia instrukcji. Stała całkowita jest prawdopodobnie najprostszym przykładem wyrażenia adresowego. Assembler po prostu zastępuje wartość stałej liczbowej dla wyszczególnionego operandu. Na przykład, następująca instrukcja wypełnia pole danej bezpośredniej instrukcji mov zerem: mov ax, o Inną prostą postacią trybu adresowania jest symbol. Po napotkaniu symbolu, MASM zamienia wartość tego symbolu. Na przykład poniższe dwie instrukcje emitują taki sam kod wynikowy jak instrukcja powyższa: Value equ 0 mov ax, Value Wyrażenia adresowe mogą być bardziej złożone niż te. Możemy użyć różnych arytmetycznych i logicznych operatorów dla modyfikacji podstawowej wartości jakiegoś symbolu lub stałej. Zapamiętajmy, że MASM oblicza wyrażenie adresowe podczas assemblacji a nie w czasie wykonywania. Na przykład, następująca instrukcja nie załaduje ax z lokacji Var i doda jeden do niej: mov ax, Var+1 Zamiast tego, instrukcja ta załaduje rejestr al bajtem przechowywanym pod adresem Var1 plus jeden a potem załaduje rejestr ah bajtem przechowywanym pod Var1 plus dwa. Początkujący programiści często mylą obliczanie robione w czasie assemblacji z tym robionym w trakcie czasu wykonywania. Zapamiętajmy, że MASM oblicza wszystkie wyrażenia adresowe w czasie assemblowania!!!! 8.12.1 TYPY SYMBOLI A TRYBY ADRESOWANIA Rozważmy następującą instrukcję: jmp lokacja W zależności od tego jak etykieta lokacja jest zdefiniowana, ta instrukcja jmp będzie wykonywała jedną z kilu różnych operacji. Jeśli zajrzymy do rozdziału o zbiorze instrukcji 80x86,zauważymy,że instrukcja jmp przybiera kilka form. rekapitulując, oto one: jmp label (krótka) jmp label (bliska) jmp label (daleka) jmp reg (pośredni bliski ,poprzez rejestr) jmp mem/reg (pośredni bliski, poprzez pamięć) jmp mem/reg (pośredni daleki, poprzez pamięć) Zauważmy, że MASM używa takiego samego mnemonika (jmp) dla każdej z tych instrukcji; jak on je odróżnia? Sekret leży w operandzie. Jeśli operand jest etykietą instrukcji wewnątrz bieżącego segmentu ,assembler wybiera jedną z pierwszych dwóch postaci w zależności od odległości do instrukcji docelowej. Jeśli operand jest etykietą instrukcji wewnątrz innego segmentu, wtedy assembler wybiera etykietę jmp (daleką).Jeśli operand występujący po instrukcji jmp jest rejestrem, wtedy MASM używa jmp bliskiego pośredniego a program skacze pod adres z rejestru. Jeśli wybrana jest komórka pamięci, assembler używa jednego z następujących skoków: • NEAR jeśli zmienna była zadeklarowana word/sword/dw • FAR jeśli zmienna była zadeklarowana dword/sdword/dd Otrzymamy błędny wynik jeśli użyjemy byte/sbyte/db,qword/dq lub tbyte/dt lub innego typu. Jeśli wyszczególnimy adres pośredni np. jmp [bx],assembler wygeneruje błąd ponieważ nie może określić czy bx wskazuje na słowo lub podwójne słowo. Po szczegóły jak wyspecyfikować rozmiar, zajrzyj do sekcji o sprawdzaniu zgodności typów w tym rozdziale. 8.12.2 OPERATORY ARYTMETYCZNE I LOGICZNE MASM rozpoznaje kilka operatorów arytmetycznych i logicznych. Poniższa tablica pokazuje listę takich operatorów:
Tablica 36 Operatory Arytmetyczne
Tablica 37 Operatory Logiczne
Tablica 38 Operatory Porównania Nie wolno nam pomylić tych operatorów z instrukcjami 80x86!!!Operator dodawania dodaje dwie wartości razem, ich suma staje się operandem instrukcji. To dodawanie jest wykonywane kiedy assemblujemy program a nie podczas jego wykonywania. Jeśli musimy wykonać dodawanie w czasie wykonywania, użyjemy instrukcji add lub adc. Prawdopodobnie zadamy sobie pytanie ”Po co są stosowane te operatory? ”Prawda nie jest skomplikowana. Operator dodawania jest używany czasami, odejmowania w mniejszym stopniu, porównania raz na jakiś czas, a reszta nawet mniej. ponieważ dodawanie i odejmowanie są jedynie operatorami stosowanymi w programowaniu w miarę regularnie, to omówienie rozważać będzie tylko te dwa operatory a inne jeśli będzie to wymagane w tym tekście. Operator dodawania przybiera dwie formy: wyraż + wyraż lub wyraż[wyraż].Na przykład, poniższe instrukcje ładują akumulator ,nie z komórki pamięci COUNT ale z bardzo bliskiej lokacji w pamięci: mov al, COUNT+1
Assembler, po napotkaniu tej instrukcji, obliczy sumę adresu COUNT plus jeden. Wartość wyniku jest adresem pamięci dla tej instrukcji. Instrukcja mov al, pamięć jest trzybajtowa i przybiera postać: OPCODE | Mniej znaczący bajt przesunięcia | Bardziej znaczący bajt przesunięcia | Dwa bajty przesunięcia tej instrukcji zawierają sumę COUNT+1. Forma wyraż[wyraż] operacji dodawania stosowana jest do uzyskania dostępu do elementów tablicy. Jeśli AryData jest symbolem który reprezentuje adres pierwszego elementu tablicy, AryData[5] przedstawia adres piątego bajtu w AryData. Wyrażenie AryData+5 tworzy taki sam wynik, i może być stosowana zamiennie, jednak dla tablic postać wyraż[wyraż] jest trochę bardziej samodokumentująca. Unikniemy pułapki: wyraż1[wyraż2][wyraż3] nie indeksuje (prawidłowo) automatycznie dwu wymiarowej tablicy. Po prostu oblicza sumę wyraż1+wyraż2+wyraż3. Operator odejmowania pracuje podobnie jak operator dodawania, z wyjątkiem tego, że oblicza różnicę zamiast sumy. Operator ten stanie się ważniejszy kiedy zajmiemy się zmiennymi lokalnymi w Rozdziale 11 Uważajmy, kiedy używamy wielu symboli w wyrażeniach adresowych. MASM ogranicza operacje, które możemy wykonać na symbolach do dodawania i odejmowania i pozwala tylko na następujące formy: Wyrażenie: Typ wyniku: reloc + const reloc, pod wyszczególniony adres reloc – const reloc, pod wyszczególniony adres reloc – reloc Stała, której wartość jest liczbą bajtów pomiędzy pierwszym a drugim operandem .Obie zmienne musza fizycznie pojawić się w tym samym segmencie w bieżącym pliku źródłowym. Reloc oznacza symbol przemieszczalny lub wyrażenie. Może to być nazwa zmiennej, etykieta instrukcji ,nazwa procedury lub każdy inny symbol związany z komórką pamięci w programie. Może to być również wyrażenie tworzące przemieszczalny wynik. MASM nie pozwala na żadną operację inną niż dodawanie lub odejmowanie wyrażeń, której typ wyniku jest przemieszczalny. Na przykład nie możemy my obliczyć iloczynu dwóch przemieszczalnych symboli. Pierwsze dwie z powyższych form są bardzo popularne w programach assemblerowych .takie wyrażenia adresowe często składają się z pojedynczego przemieszczalnego symbolu i pojedynczej stałej (np. var+1).nie będziemy mogli użyć trzeciej formy bardzo często ,ale jest bardzo użyteczna raz na jakiś czas. Możemy użyć tej formy wyrażenia adresowego do obliczenia odległości, w bajtach między dwoma punktami w naszym programie. Symbol procsize w następującym kodzie oblicza rozmiar Proc1: Proc1 proc near push ax push bx push cx mov cx, 10 lea bx, SomeArray mov ax, 0 ClrArray mov [bx],ax add bx,2 loop ClrArray pop cx pop bx pop ax ret Proc1 endp Procsize = $ - Proc1 „$” jest specjalnym symbolem, którego MASM używa do określania bieżącego offsetu wewnątrz segmentu (np. licznik lokacji)jest to symbol przemieszczalny ,więc powyższe równanie oblicza długość procedury Proc1,w bajtach. Operand operatora innego niż dodawanie lub odejmowanie musi być stała lub wyrażeniem dającym stałą (np. „$-Proc1” daje stałą wartość).Głównie używamy tych operatorów w makrach i z dyrektywami asemblacji warunkowej. 8.12.3 KOERCJA Rozważmy następujący segment programu: DSEG segment public ‘DATA’ I byte ?
J DSEG CSEG
byte ? ends segment mov al, I mov ah, J CSEG ends Ponieważ I i J są przyległe ,nie musimy używać dwóch instrukcji mov do załadowania al i ah, prosta instrukcja mov ax, I zrobiłaby to samo. Niestety, assembler będzie się wzdragał przed mov ax ,I ponieważ I jest bajtem. Assembler będzie narzekał jeśli spróbujemy potraktować to jako słowo.. Jak zobaczymy, prawdopodobnie będzie kilka sytuacji kiedy potraktujemy zmienną bajtową jako słowo (lub słowo jako bajt lub podwójne słowo, lub potraktować podwójne słowo jako coś innego). Czasowa zmiana typu etykiety dla jakiegoś szczególnego wystąpienia to koercja. Wyrażenie może być sprowadzone do innego typu przez zastosowanie operatora ptr. Użyjemy operatora ptr jak następuje: type PTR wyrażenie Type jest jednym z typów byte ,word ,dword, tbyte ,near ,far lub innego a wyrażenie jest ogólnym wyrażeniem, które jest adresem jakiegoś obiektu .Operator koercji zwraca wyrażenie o takiej samej wartości jak wyrażenie, ale z typem wyszczególnionym przez type. Rozwiązaniem powyższego problemu będzie użycie instrukcji języka assemblera: mov ax, word ptr I Informuje to assembler, żeby wyemitował kod, który załaduje rejestr ax słowem spod adresu I .Oczywiście załaduje al I a ah J. Kod który używa wartości podwójnego słowa często robi się rozległy poprzez zastosowanie operacji koercji .Ponieważ lds i lea są jedynymi 32 bitowymi instrukcjami na pre –procesorach 80386,nie możemy (bez koercji) przechować wartości całkowitych w 32 bitowych zmiennych używając instrukcji mov na tych wcześniejszych CPU. jeśli zadeklarujemy DBL używając pseudo-opcodu dword, wtedy instrukcja w postaci mov DBL, ax wygeneruje błąd ponieważ próbujemy przenieść 16 bitową wielkość do 32 bitowej zmiennej. Przechowywanie wartości w zmiennej podwójnego słowa wymaga użycia operatora ptr. Poniższy kod demonstruje jak przechowywać rejestry ds i bx w zmiennej podwójnego słowa DBL: mov word ptr DBL, bx mov word ptr DBL+2, ds Będziemy używać tej techniki często przy Standardowej Bibliotece UCR i wywołaniach MS-DOS zwracających wartość podwójnego słowa w parze rejestrów. Ostrzeżenie: Jeśli wykonujemy koercje instrukcji jmp wymagającą skoku far do etykiety near, (far jmp dłużej się wykonuje),nasz program będzie pracował dobrze. Jeśli wykonamy koercję call wymagającą dalekiego wywołania do bliskiego podprogramu, kierujemy się prosto do kłopotów. Pamiętajmy, że dalekie wywołanie odkłada rejestr cs na stos (z adresem powrotnym)Kiedy wykonamy bliską instrukcję ret, stara wartość cs nie będzie ściągnięta ze stosu ,pozostawiając śmieci na stosie Bardzo blisko położone instrukcje pop i ret nie działają poprawnie ponieważ zdejmują wartość cs ze stosu zamiast oryginalną wartość odłożoną na stos. Wyrażenie poddane koercji może przydać się czasami. Innym razem jest niezbędne. Jednakże nie powinniśmy unikać koercji ponieważ sprawdzanie typu danych jest silnym narzędziem wbudowanym w MASM. Poprzez użycie koercji, możemy zlekceważyć to zabezpieczenie dostarczane przez assembler. Dlatego też, zawsze uważajmy kiedy przesłaniamy typ symbolu operatorem ptr. Jedyne miejsce gdzie potrzebujemy koercji jest instrukcja mov pamięć, dana bezpośrednia. Rozważmy następującą instrukcję: mov [bx], 5 Niestety, assembler nie ma sposobu aby dowiedzieć się czy bx wskazuje pozycję bajtu, słowa lub podwójnego słowa w pamięci. Wartość operandu bezpośredniego nie jest używana .Pomimo, że pięć jest wielkością bajtu ,instrukcja ta może przechowywać wartość 0005 w zmiennej word, lub 00000005 w zmiennej podwójnego słowa. Jeśli spróbujemy zasemblować tę instrukcję, assembler wygeneruje błąd ,w skutek tego, że musimy wyszczególnić rozmiar operandu pamięci. Możemy łatwo to osiągnąć używając operatorów byte ptr, word ptr i dword ptr jak następuje: mov byte ptr [bx],5 ;dla zmiennej bajtowej mov word ptr [bx],5 ;dla zmiennej słowa mov dword ptr [bx], 5 ;dla zmiennej podwójnego słowa
Leniwy programista może narzekać, że pisanie łańcuchów jak „word ptr” lub „far ptr” wymaga zbyt dużo pracy. Czyż nie byłoby milej ,gdyby Intel wybrał pojedynczego symbolu znaku zamiast tych długich fraz? Cóż, przestańmy narzekać i pamiętajmy o dyrektywie textequ. Z dyrektywami równania możemy zastąpić długi łańcuch taki jak „word ptr” na krótszy symbol. Znajdziemy takie dyrektywy równania w wielu programach: byp textequ ;pamiętaj ,”bp” jest zarezerwowanym symbolem wp textequ dp textequ np. textequ fp textequ Z dyrektywami równań, jak powyższe, możemy użyć instrukcji jak pokazano poniżej: mov byp [bx], 5 mov ax, wp I mov wp DBL, bx mov wp DBL+2, ds 8.12.4 TYPY OPERATORÓW Operator koercji „xxxx ptr” jest przykładem operatora. Wyrażenie MASM posiadają dwa główne atrybuty: wartość i typ. Operatory arytmetyczne ,logiczne i relacyjne zmieniają wartość wyrażenia. Operatory typów zmieniają ich typ. Poprzednia sekcja demonstrowała jak operator ptr może zmienić typ wyrażenia. Jest kilka dodatkowych operatorów typów.
Tablica 39: Operatory typu Operator short pracuje wyłącznie z instrukcją jmp .Pamiętamy, że są dwie instrukcje jmp bliskie bezpośrednie, jedna która ma zakres 128 bajtów, druga ma 32,768 bajtów. MASM automatycznie generuje krótki skok jeśli adres docelowy jest większy niż 128 bajtów przed bieżącą instrukcją. Operator ten jest głównie obecny ze względu na kompatybilność ze starszymi wersjami MASMa. Operatora this tworzy wyrażenie z wyszczególnionym typem którego wartość jest bieżącym licznikiem lokacji. Na przykład instrukcja mov bx, this word, załaduje rejestr bx wartością 8B1Eh,opcodem dla mov bx, pamięć. Adres this word jest adresem opcodu dla tej właśnie instrukcji!. Operatora this używamy głównie z dyrektywą equ dającą symbol typu innego niż stała .Na przykład, rozważmy następującą instrukcję: HERE equ this near Ta instrukcja przydziela bieżąca wartość licznika lokacji do HERE i ustawia typ HERE na near, Oczywiście mogłoby to być zrobione dużo łatwiej poprzez umiejscowienie etykiety HERE: w tej samej linii .Rozważmy coś takiego: Warray equ this word Barray byte 200 dup (?) W tym przykładzie symbol Barray jest typu byte. Dlatego też, instrukcje uzyskujące dostęp do Barray musi zawierać operand byte .MASM sygnalizuje instrukcję mov ax,BArray+8 jako błąd .Jednak zastosowanie symbolu WArray pozwala nam uzyskać dostęp dokładnie do tej samej komórki pamięci (ponieważ Warray ma wartość licznika lokacji bezpośrednio przed napotkaniem pseudo-opcodu byte) więc mov ax,WArray+8 uzyskuje dostęp do BArray+8.Zauważmy,że następujące dwie instrukcje są podobne: mov ax, word ptr BArray+8 mov ax, WArray+8 Operator seg robi dwie rzeczy. Po pierwsze wydziela część segmentową wyszczególnionego adresu, po drugie konwertuje typ wyszczególnionego wyrażenia z adresu do stałej. Instrukcja w postaci mov ax, ser symbol zawsze ładuje akumulator stałą odpowiadającą adresowi części segmentu symbol. Jeśli symbol jest nazwą segmentu. MASM automatycznie zastępuje adres paragrafu segmentu dla nazwy. Jednakże, jest całkiem poprawne zastosowanie operatora seg również. Poniższe dwie instrukcje są identyczne jeśli dseg jest nazwą segmentu: mov ax, dseg mov ax, seg dseg Offset pracuje podobnie jak seg, z tym, że zwraca offset części wyszczególnionego wyrażenia zamiast część segmentu. jeśli VAR1 jest zmienną słowa, mov ax ,VAR1 zawsze załaduje dwa bajty spod adresu wyszczególnionego przez VAR1 do rejestru ax. Instrukcja mov ax, offset VAR1 ładuje offset (adres) VAR1 do rejestru ax. Zauważmy, że możemy użyć instrukcji lea lub instrukcji mov z operatorem offset do załadowania adresu zmiennej skalarnej do rejestru 16 bitowego. Poniższe dwie instrukcje, obie ładują bx adresem zmiennej J; mov bx, offset J mov bx, J Instrukcja lea jest bardziej elastyczna ponieważ możemy wyszczególnić każdy tryb adresowania pamięci, operator offset pozwala nam na pojedynczy symbol (tj. tryb adresowania „tylko przemieszczenie”)Większość programistów używa formy mov dla zmiennych skalarnych a instrukcji lea dla innych trybów adresowania. Jest tak ponieważ instrukcja mov będzie szybsza na wcześniejszych procesorach. Jednym bardzo popularnym zastosowaniem operatorów seg i offset jest inicjacja rejestru segmentu i wskaźnika adresem segmentowym jakiegoś obiektu. Na przykład ładując es:di adresem SomeVar, możemy użyć poniższego kodu: mov di, seg SomeVar mov es, di mov di, offset SomeVar Ponieważ nie możemy załadować stałej bezpośrednio do rejestru segmentowego, powyższy kod kopiuje część segmentową adresu do di a potem kopiuje di do es przed skopiowaniem offsetu do di. Kod ten używa rejestru di do kopiowania części segmentowej adresu do es, więc nie będzie wpływał na kilka innych rejestrów Opattr zwraca 16 bitową wartość dostarczając określonej informacji o wyrażeniu występującym po nim. Operator .type jest starsza wersją opattr, która zwraca mniej znaczące osiem bitów tej wartości. Każdy bit wartości tego operatora ma następujące znaczenie:
Tablica 40: Zwracane wartości OPATTR/.TYPE Bity języka są dla programistów, którzy piszą kody dla języków wysokopoziomowych takich jak Pascal czy C++. Takie programy używają uproszczonych dyrektyw segmentowych i cech HLLi MASMa. Będziemy mogli normalnie używać tych wartości z dyrektywami asemblacji warunkowej MASMa i makrami. Pozwoli to nam wygenerować różne sekwencje instrukcji w zależności od typu parametrów makra lub bieżącej konfiguracji assemblacji. Operatory size ,sizeof, length i lengthof obliczają rozmiar zmiennych (wliczając w to tablice) i zwracają te rozmiary i ich wartości. Zwykle nie powinniśmy używać size i length. Operatory sizeof i lengthof zastąpiły te operatory. Size i length nie zawsze zwracają sensowne wartości dla różnych operandów. MASM 6.x zawiera je tylko dla kompatybilności ze starszymi wersjami assemblera. Jednakże, później w rozdziale zobaczymy przykład gdzie używamy tych operatorów. Operator sizeof zmiennej zwraca liczbę bajtów bezpośrednio alokowanych w wyszczególnionej zmiennej. Ilustruje to poniższy przykład: a1 byte ? ;sizeof (a1)=1 a2 word ? ;sizeof(a2)=2 a4 dword ? ;sizeof)(a4)=4 a8 real8 ? ;sizeof(a8)=8 ary0 byte 10 dup (0) ;sizeof (ary0) = 10 ary1 word 10 dup(10 dup (0) ;sizeof(ary1) = 200 Możemy również użyć operatora sizeof do obliczenia rozmiaru, w bajtach, struktury innych typów danych .Jest to bardzo użyteczne dla obliczania indeksów w tablicy używając formuły z Rozdziału Czwartego: Adres_Elelmntu := adres_bazowy+indeks*Rozmiar_Elementu Możemy otrzymać rozmiar elementu tablicy lub struktury używając operatora sizeof. Więc jeśli mamy tablicę struktury ,możemy obliczyć indeks do tablicy jak następuje: .286 ;dozwolone instrukcje 80286 s struct s ends array
s imul
16 dup ({})
;tablica 16 elementów „s”
bx, I, sizeof s
;obliczenie BX :=I * rozmiar elementu
mov al, array[bx].nazwa pola Możemy również zastosować operator sizeof do innych typów danych aby uzyskać ich rozmiary w bajtach .Na przykład ,sizeof byte zwraca 1, sizeof word zwraca dwa a sizeof dword zwraca 4.Oczywiście zastosowanie tego operatora dla wbudowanych w MASM typów danych jest wątpliwe ponieważ rozmiar tych obiektów jest stały. Jednakże, jeśli stworzymy swój własny typ danych używając typedef, nabierze sens obliczanie rozmiaru tego obiektu przez użycie operatora sizeof: Integer typedef word Array integer 16 dup (?) imul bx, bx, sizeof integer W powyższym kodzie, sizeof integer będzie zwracał dwa podobnie jak sizeof word. Jednak jeśli zmienimy instrukcję typedef tak aby integer wskazywał dword zamiast word, operand sizeof integer automatycznie zmieni jego wartość na cztery ,odzwierciedlając nowy rozmiar integer. Operator lengthof zwraca całkowitą liczbę elementów w tablicy. Dla powyższej zmiennej Array,lengthof Array zwróci 16.jeśli mamy dwuwymiarową tablicę, lengthof zwróci całkowitą liczbę elementów w tej tablicy. Kiedy użyjemy operatorów lengthof i sizeof z tablicami, musimy zapamiętać, że jest możliwe zadeklarowanie tablic w sposób w który MASM może źle zinterpretować. Na przykład, poniższe instrukcje deklarują tablice zawierające osiem słów: A1 word 8 dup (?) A2 word 1,2,3,4,5,6,7,8 ;Notka: „\” jest symbolem „kontynuowania linii” Mówi MASMowi aby dołączył następną linie do końca bieżącej ;linii A3 word 1,2,3,4 \ 5,6,76,8 A4 word 1,2,3,4 word 5,6,7,8 Zastosowanie operatorów sizeof i lengthof dla A1,A2 i A3 daje 16 (sizeof) i 8 (lengthof).Jednakże sizeof(A4) daje osiem a lengthof(A4) daje cztery. Dzieje się tak ponieważ MASM sądzi, że tablice zaczynają się i kończą pojedynczymi deklaracjami danych .Chociaż A4 deklaruje rezerwację w pamięci osiem kolejnych słów, podobnie jak trzy inne powyższe deklaracje, MASM sądzi, że dwie dyrektywy word deklarują dwie oddzielne tablice zamiast tablicy pojedynczej. Więc jeśli chcemy inicjować elementy dużej tablicy lub tablicy wielowymiarowej i również chcemy móc zastosować operatory lengthof i sizeof do tej tablicy ,powinniśmy użyć postaci A3 dla deklaracji zamiast A4 Operator type zwraca stałą, która jest liczbą bajtów wyszczególnionego operandu. Na przykład, type(word) zwraca wartość dwa Ta rewelacja nie jest szczególnie interesująca ponieważ operatory size i sizeof również zwracają tą wartość jednak, kiedy używamy operatora type do porównania operatorów (np,. ne,le,lt,gt i ge) porównanie to daje wynik prawdziwy tylko jeśli typy operandów są takie same. rozważmy następujące definicje: Integer typedef word J word ? K sword ? L integer ? M. word ? byte type (J) eq word ;wartość = 0FFh byte type (J) eq sword ;wartość = 0 byte type (J) eq type (L) ;wartość = 0FFh byte type (J) eq type (M) ;wartość = 0FFh byte type (L) eq integer ;wartość = 0FFh byte type (K) eq dword ;wartość = 0 Ponieważ powyższy kod zmienia integer na word, MASM traktuje wartości całkowite i słowa jako ten sam typ. Zauważmy, że z wyjątkiem ostatniego przykładu, wartość po obu stronach operatora eq to dwa. Dlatego też, kiedy używamy operatorów porównania z operatorem type MASM porównuje więcej niż tą wartość .Dlatego też, type i sizeof nie są synonimami. Np. byte type (J) eq type (K) ;wartość = 0
byte
(sizeof J) equ (sizeof K)
;wartość = 0FFh
Operator type jest zwłaszcza użyteczny kiedy używamy warunkowych dyrektyw assemblacji MASMa. Powyższe przykłady demonstrują również inną interesującą cechę MASMa. Jeśli użyjemy typu name wewnątrz wyrażenia, MASM potraktuje go jak gdybyśmy wprowadzili „typ(name)” gdzie name jest symbolem danego typu .W szczególności, wyszczególniona nazwa typu zwraca rozmiar, w bajtach, obiektu danego typu. Rozważmy następujący przykład: Integer typedef word s struct d dword ? w word ? b byte ? s ends byte word ;wartość = 2 byte sword ,wartość = 2 byte byte ;wartość = 1 byte dword ;wartość = 4 byte s ;wartość = 7 byte word eq word ;wartość = 0FFh byte word eq sword ;wartość = 0 byte b eq dword ;wartość = 0 byte s eq byte ;wartość = 0 byte word eq Integer ;wartość = 0FFh Operatory high i low, podobnie jak offset i seg zmieniają typ wyrażenia z jakiejkolwiek na stałą. Operatory te rózwniż wpływają na wartość – rozkładają go na bardziej i mniej znaczące bajty. Operator high ekstrahuje bity wyrażenia od osiem do piętnaście, operator low ekstrahuje i zwraca bity od zera do siedem. Highword i lowword ekstrahuje bardziej i mniej znaczące 16 bitów wyrażenia zobacz rysunek 8.7) Możemy wyciągnąć bity 16 – 23 i 24-31 używając wyrażeń w postaci low (highword (expr)) i high(highword(expr)),odpowiednio. 8.12.5 OPERATOR PIERSZEŃSTWA Chociaż będziemy rzadko musieli używać złożonych wyrażeń adresowych stosując dwa lub więcej niż dwa operandy i pojedynczy operator, czasami potrzebujemy je zastosować .MASM wspiera pojedynczy operator konwencji pierwszeństwa oparty na następujących zasadach: • MASM wykonuje operatory od najwyższego priorytetu
• •
Rysunek 8.7:Operatory HIGHWORD,LOWWORD,HIGH i LOW Operatory równania są lewo łączne i obliczane od lewej do prawej Nawiasy unieważniają normalne pierwszeństwa
Tablica 41: Operatory pierwszeństwa Nawiasy powinny tylko otaczać wyrażenia. Operatory takie jak sizeof i lengthof wymagaj nazwy typu, nie wyrażeń .Nie pozwalają nam otoczyć nawisami nazw. Dlatego też „sizeof X” jest poprawne, ale „sizeof(X)” nie .Zapamiętajmy kiedy używamy nawiasów do unieważnienia operatorów pierwszeństwa w wyrażeniu. Jeśli MASM generuje błąd, możemy będziemy musieli przestawić w naszym wyrażeniu. Podobnie jak dla wyrażeń w językach wysokiego poziomu, dobrym pomysłem jest używanie zawsze nawiasów okrągłych do wyraźnego określenia stanu pierwszeństwa we wszystkich złożonych wyrażeniach adresowych (złożony w znaczeniu ,że wyrażenie ma więcej niż jeden operator).Uczyni to wyrażenia bardziej czytelnymi i pozwoli uniknąć błędów pierwszeństwa. 8.13 ASSEMBLACJA WARUNKOWA MASM dostarcza bardzo silnych udogodnień assemblacji warunkowej. Z asemblacją warunkową, możemy zadecydować ,w oparciu o pewne warunki, czy MASM będzie assemblował kod. Jest kilka dyrektyw assemblacji warunkowej, poniższa sekcja omówi większość z nich. Ważne jest aby uświadomić sobie, że dyrektywy te wyliczają wyrażenia w czasie assemblacji a nie w czasie wykonania .Dyrektywa assemblacji warunkowej nie jest tym samym co instrukcja „if” Pascala lub C. Jeśli jesteśmy zaznajomieni z C, dyrektywa #ifdef w C jest pobieżnym odpowiednikiem dyrektyw assemblacji warunkowej MASMa. Dyrektywy assemblacji warunkowej MASMa są ważne ponieważ pozwalają nam generować różne kody wynikowe dla różnych operacji środowiska i różnych sytuacji. Na przykład przypuśćmy, że chcemy napisać program, który będzie pracował na wszystkich maszynach, ale chcielibyśmy zoptymalizować kod dla 80386 i późniejszych procesorów. Oczywiście nie możemy wykonać kodu 80386 na procesorze 8086,więc jak możemy rozwiązać ten problem? Jednym z możliwych rozwiązań jest określenie typu procesora w czasie wykonania i wykonanie różnych sekcji kodu w programie w zależności od obecności lub absencji 386 lub późniejszych CPU. Problem w takim podejściu jest taki, że nasz program musi zawierać dwie sekwencje kodu – optymalna sekwencję dla 80386 i kompatybilną sekwencję dla 8086.Na każdym danym systemie, CPU wykona tylko jedną z tych sekwencji w programie, więc druga sekwencja będzie marnowała pamięć i może mieć niekorzystny wpływa na cache w systemie. Drugą możliwością jest napisanie dwóch wersji kodu, jeden, który używa tylko instrukcji 8086 i jeden, który stosuje pełen zbiór instrukcji. Podczas instalacji, użytkownik (lub program instalacyjne) wybiera wersję 80386 jeśli mamy procesor 80386 lub późniejszy. W przeciwnym wypadku wybiera wersję 8086.Jest to nieznaczne zwiększenie kosztów programu, ponieważ wymaga to więcej przestrzeni na dysku, program będzie zużywał mniej pamięci podczas działania. Problem jaki napotkamy to to, że będziemy musieli utrzymywać dwie
odrębne wersje programu. Jeśli poprawimy błąd w wersji kodu dla 8086,prawdopodbnie będziemy musieli poprawić ten sam błąd w programie dla 80386.utrzeymywanie wielu plików źródłowych jest trudnym zadaniem. Trzecim rozwiązaniem jest zastosowanie asemblacji warunkowej. Z asemblacją warunkową możemy połączyć wersje kodu dla 8086 i 80386 w tym samym pliku źródłowym. Podczas asemblacji możemy warunkowo wybierać czy MASM assembluje wersję kodu dla 8086 czy 80386.Poprzez dwukrotną asemblacje kodu ,możemy stworzyć wersje kodu dla 8086 i 80386.Ponieważ obie wersje kodu pojawiają się w tym samym pliku źródłowym, program będzie dużo łatwiejszy do utrzymania ponieważ nie będziemy musieli poprawiać tych samych błędów w dwóch oddzielnych plikach źródłowych. Musimy poprawić ten sam błąd dwa razy w dwóch oddzielnych sekwencjach kodu w programie, ale generalnie błąd pojawi się w dwóch sąsiadujących sekwencjach kodu, więc jest mniej prawdopodobne, że zapomnimy dokonać zmian w obu miejscach. Dyrektywy asemblacji warunkowej MASMa są szczególnie użyteczne wewnątrz makr. Mogą one pomóc nam stworzyć sprawnie działający kod kiedy makro normalnie tworzy sub -optymalny kod .Po więcej informacji o makrach i jak używać asemblacji warunkowej wewnątrz makr zajrzyj do „Makra”. Makra i asemblacja warunkowa właściwie dostarczają „języka programowania wewnątrz języka programowania” .Makra i asemblacja warunkowa pozwalają nam pisać programy (w „języku makra”) które tworzą część kodu assemblera. Wprowadza to niezależny sposób generowania błędów w naszych aplikacjach. Nie tylko można tworzyć błędy w naszym kodzie assemblerowym, ale możemy również wprowadzać błędy w naszym kodzie makra (np. asemblacja warunkowa),co kończy się tworzeniem błędów w kodzie asemblerowym. Zapamiętajmy ,że jeśli dostajemy kod zbyt wyrafinowany kiedy u używamy asemblacji warunkowej ,tworzymy programy, które są zbyt trudne do odczytania ,zrozumienia i zdebuggowania. 8.13.1 DYREKTYWA IF Dyrektywa if używa następującej składni: if wyrażenie else ;to jest opcjonalne endif MASM oblicza wyrażenie. Jeśli nie jest to wartość zerowa, wtedy MASM zasembluje instrukcje pomiędzy dyrektywami if i else (lub endif, jeśli else nie jest obecna).Jeśli wyliczone wyrażenie jest zerem (fałsz) a sekcja else występuje, MASM zasembluje instrukcje pomiędzy dyrektywami else a endif. Jeśli else nie występuje, a wartość wyrażenia jest fałszem, wtedy MASM nie zasembluje żadnego kodu pomiędzy dyrektywami if i endif. Ważną rzeczą do zapamiętania jest to ,że wyrażenie ma być wyrażeniem, które MASM może wyliczyć w czasie asemblacji. To znaczy musi obliczyć stałą. Stałą jawna i wartość, które tworzą typ operatorów MASMa są dostępne w wyrażeniach dyrektywy if. Na przykład przypuśćmy ,że chcemy zasemblować kod dla dwóch różnych procesorów jak opisano powyżej. Możemy użyć instrukcji podobnych do tych: Procesor = 80386 ;ustawia 8086 dla kodu tylko-8086 if Procesor eq 80386 shl ax, 4 else ,musi być procesor 8086 mov cl, 4 shl ax, cl endif Są inne sposoby osiągnięcia tej samej rzeczy. MASM dostarcza zmiennych wbudowanych które mówią nam czy assemblujemy kod dla wyszczególnionego procesora. Ale o tym później. 8.13.2 DYREKTYWA IFE Dyrektywa ife jest używana dokładnie jak dyrektywa if, z wyjątkiem tego, że assembluje kod po dyrektywie ife tylko jeśli wyliczona wartość wyrażenia to zero (fałsz),zamiast prawda (nie – zero) 8.13.3 IFDEF I IFNDEF Te dwie dyrektywy wymagają pojedynczego symbolu jako operandu. Ifdef będzie assemblować powiązany kod jeśli symbol jest zdefiniowany. Ifndef będzie assemblował powiązany kod jeśli symbol nie jest zdefiniowany. Użycie else i endif kończy sekwencję assemblacji warunkowej.
Dyrektywy te są zwłaszcza popularne dla zawartego lub nie zawartego kodu w programie asemblerowym operującym w pewnych specjalnych przypadkach. Na przykład, możemy użyć instrukcji takich jak poniższe zawierające instrukcje debuggujące w naszym kodzie: ifdef DEBUG endif Aby uruchomić kod debuggujący po prostu definiujemy symbol DEBUG gdzieś na początku naszego programu (przed pierwszym odwołaniem ifdef do DEBUG).Automatyczne wyeliminowanie kodu debuggującego polega po prostu na skasowaniu definicji DEBUG .Możemy zdefiniować DEBUG używając prostej instrukcji: DEBUG = 0 Zauważmy, że wartość przydzielona do DEBUG jest nieistotna. Tylko fakt, że mamy (lub nie mamy) zdefiniowanego tego symbolu jest ważny. 8.13.4 IFB,IFNB Dyrektywy te, są użyteczne głównie w makrach do sprawdzania czy operand jest pusty (ifb) lub nie pusty (ifnb).Rozważmy następujący kod: Blank textequ <> NotBlank textequ ifb Blank endif ifb NotBlank endif Ifnb pracuje przeciwnie do ifb. To znaczy, assemblowałby instrukcje powyższe, których nie assembluje ifb i vice versa. 8.13.5 IFIDN,IFDIF,IFIDNI I IFDIFI Te dyrektyw asemblacji warunkowej pobierają dwa operandy i przetwarzają powiązany kod jeśli operandy są identyczne (ifidn),różne (ifdif),identyczne bez rozróżniania liter(ifidni) lub różne bez rozróżniania liter. Ich składnia: ifidn op1, op2 = > endif ifdif op1, op2 ≠ > endif ifidni op1,op2 = > endif ifdifi op1,op2 ≠ > endif Różnice pomiędzy powyższymi instrukcjami Ifxxx a IfxxxI jest taka ,że instrukcje IfxxxI ignorują różnice wielkości liter alfabetu przy porównaniu operandów. 8.14 MAKRA Makro jest jak procedura, która wstawia blok instrukcji w różnych punktach w naszym programie podczas asemblacji. Są trzy główne typy makr w MASM : makra proceduralne ,makra funkcjonalne i makra pętli. Wraz z asemblacją warunkową, narzędzia te dostarczają tradycyjnych konstrukcji if, loop, procedur i funkcji, znanych z wielu języków wysokiego poziomu. W odróżnieniu od instrukcji asemblacji, asemblacja warunkowa i konstrukcje języka makr wykonują się podczas asemblacji, Asemblacja warunkowa i instrukcje makr nie występują kiedy nasz program jest uruchomiony. Celem tych instrukcji jest kontrolowanie ,które instrukcje MASM assembluje do końcowego pliku „.exe” Podczas gdy dyrektywy asemblacji warunkowej wybierają lub pomijają pewne instrukcje do asemblacji, dyrektywy makra pozwalają emitować powtarzające się
sekwencje instrukcji w pliku asemblerowym podobnie jak procedury języków wysokiego poziomu i pętle pozwalające nam powtarzać wykonywanie sekwencji instrukcji języków wysokiego poziomu. 8.14.1 MAKRA PROCEDURALNE Następująca sekwencja definiuje makro: nazwa macro {parametr1 {parametr2 {,...}}} endm Nazwa musi być poprawnym i unikalnym symbolem w pliku źródłowym. Będziemy używać tego identyfikatora przy wywoływaniu makra. (Opcjonalne) nazwy parametrów są symbolami zastępczymi dla wartości wyszczególnionych kiedy wywołujemy makro; powyższe nawiasy klamrowe oznaczają pozycje opcjonalne, które nie powinny pojawić się w rzeczywistości w naszym kodzie źródłowym. Te nazwy parametrów są lokalne w makrze i mogą się pojawić gdzie indziej w programie. Przykład definicji makra: COPY macro Przez, Źródło mov ax, Źródło mov Dest, ax endm Makro to kopiuje słowo spod adresu źródłowego do słowa pod adresem przeznaczenia. Symbole Przez i Źródło są lokalne w makrze i mogą pojawiać się gdziekolwiek indziej w programie. Zauważmy, że MASM nie assembluje bezpośrednio instrukcji między dyrektywami macro a endm kiedy MASM napotyka makro. Zamiast tego ,asembler przechowuje teks odpowiadający makru w specjalnej tablicy (nazywanej tablicą symboli).MASM wstawia te instrukcje do naszego programu kiedy wywołujemy makro. Wywołanie (użycie) makra, polega na wyszczególnieniu nazwy makra jako mnemonika MASMa. Kiedy to zrobimy, MASM wstawi instrukcje pomiędzy dyrektywy macro i endm do naszego kodu a punkcie wywołania makra. jeśli makro ma parametry MASM zastąpi rzeczywiste parametry pojawiające się jako operandy na parametry formalne pojawiające się w definicji makra.. MASM robi bezpośrednie podstawienie tekstowe mimo że stworzyliśmy przyrównanie tekstowe dla parametrów. Rozważmy następujący kod który używa makra COPY zdefiniowanego powyżej: call SetUpX copy Y,X add Y,5 Ta część programu będzie wywoływała SetUpX (która ,przypuszczalnie, robi coś ze zmienną X) potem wywołuje makro COPY, które kopiuje wartość w zmiennej X do zmiennej Y.W końcu, dodaje pięć do wartości zawartej w zmiennej Y. Zauważmy ,że ta sekwencja instrukcji jest absolutnie podobna do: call SetUpX mov ax, X mov Y, ax add Y,5 W takim przypadku użycie makra może zaoszczędzić znaczną ilość pisaniny w naszym programie. Na przykład przypuśćmy ,że chcemy uzyskać dostęp do elementów różnych dwuwymiarowych tablic. Jak możemy sobie przypomnieć, formuła obliczania adresu rzędowego pozycjonowania elementów dla tablicy to: Adres elementu = adres bazowy+(Pierwszy Indeks*Rozmiar Rzędu+ Drugi Indeks)* rozmiar elementu Przypuśćmy, że chcemy napisać kod asemblerowy, który osiągnie ten sam wynik jak następujący kod C: int a[16][7], b[16][7],x[7][16]; int i,j; for (i=0; i<16; i=i+1) for (j=0; j<7; j=j+1) x[j][i] = a[i][j]*b[15-i][j]; Kod 8086 dla tej sekwencji jest raczej złożony poprzez liczbę dostępów do tablic. Kompletny kod to: .386 ;używamy instrukcji 286 lub 386 option segment:use16 ;wymagane dla programów trybu rzeczywistego a sword 16 dup (7 dup (?)) b sword 16 dup (7 dup (?)) x sword 7 dup (16 dup (?))
textequ textequ
;przechowanie I w rejestrze cx ;przechowanie J w rejestrze dx
ForILp:
mov cmp jnl
I, 0 I, 16 ForIDone
;inicjacja I indeksem pętli zerem ;czy I jest mniejsze niż 16? ;jeśli tak skok do treści pętli
ForJLp:
mov cmp jnl
J, 0 J, 7 ForJDone
;inicjacja J indeksem pętli zerem ;czy J jest mniejsze iż 7 ;jeśli tak skok do treści pętli J
imul add add mov
bx,I,7 bx, J bx, bx ax, A[bx]
;Oblicza indeks dla a[i][j]
mov sub imul add add imul
bx, 15 bx, I bx, 7 bx, J bx, bx ax,b[bx]
;obliczenie indeksu dla b[15-I][j]
imul add add mov
bx, J, 16 bx, I bx, bx X[bx], ax
inc jmp inc Jmp
J ForILp I ForILp
i j
ForJDone:
;Rozmiar elementu jest dwubajtowy ;pobranie a [i][j]
;rozmiar elementu jest dwubajtowy ;oblicza a[i][j] * b[16-i][j] ;oblicza indeks dla X[j][i] ;przechowuje wynik ;następna iteracja pętli ;następna iteracji pętli I
ForIDone: Jest to dużo kodu w porównaniu z pięcioma instrukcjami C/C++! Jeśli spojrzymy na tez kod zauważymy, że duża liczba instrukcji po prostu oblicza indeks do trzech tablic. Ponadto sekwencje kodu która oblicza te indeksy tablic są bardzo podobne. Jeśli byłyby takie same, byłoby oczywistym napisanie makra zastępującego obliczanie indeksów trzech tablic. Ponieważ te obliczane indeksy nie są identyczne byłoby świetnie gdyby możliwe było stworzenie makra które uprościłoby ten kod. Odpowiedź brzmi tak :poprzez użycie parametrów makra jest bardzo łatwo napisać takie makro. Rozważmy następujący kod: i textequ ;przechowanie I w rejestrze cx j textequ ;przechowanie J w rejestrze dx NDX2
macro imul add add endm
Indeks1,Indeks2,RowSize bx, Indeks1, RowSize bx, Indeks2 bx. Bx
ForILp:
mov cmp jnl
I, 0 I, 16 ForIDone
;inicjacja pętli I indeksem zero ;czy I jest mniejsze niż 16 ;jeśli tak, skok do treści pętli I
ForJLp:
mov cmp jnl
J, 0 J, 7 ForJDone
;inicjacja pętli J indeksem zero ;czy J jest mniejsze niż 7? ;jeśli tak, skok do treści pętli J
NDX2 mov mov sub NDX2 imul
I,J,7 ax, A[bx] bx, 15 bx, I bx,J,7 ax,b[bx]
NDX2 J,I,16 mov X[bx], ax
ForJDone:
inc jmp inc Jmp
J ForJLp I ForILp
;pobranie a[i][j] ;obliczanie indeksu dla b[15-I][j] ;Oblicza a[i][j]*b[15-i][j] ;przechowanie wyniku ;następna iteracja pętli J ;następna iteracja pętli I
ForIDone: Jeden problem z makrem NDX2 jest taki, że musimy znać rozmiar wiersza tablicy (ponieważ jest to parametr makra).W krótkim przykładzie, jak ten, nie jest to duży problem. Jednak, jeśli piszemy duży program możemy łatwo zapomnieć rozmiary i musimy szukać ich, lub co gorsza, ”zapamiętać” je niepoprawnie i wprowadzić błąd do naszego programu. jednym sensownym pytaniem jest czy MASM może wykombinować rozmiar wiersza tablicy automatycznie. Odpowiedź brzmi tak. Operator MASMa length jest przypuszczalnie zwraca liczbę elementów w tablicy. Jednakże wszystkie rzeczywiste zwroty odnoszą się do pierwszej wartości pojawiającej się w polu operandu tablicy. Na przykład, (length a) zwróci 16 z danej powyższej definicji. MASM poprawi ten problem przez wprowadzenie operatora lengthof, który. Właściwie zwróci całkowitą liczbę elementów w tablicy.(Lengthof a),na przykład, właściwie zwróci 112 (16*7).Chociaż operator (length a) zwraca błędną wartość dla naszego celu (zwraca rozmiar kolumny zamiast wiersza),możemy użyć tej zwracanej wartości do obliczenia rozmiaru wiersza używając wyrażenia (lengthof a)/(length a).Z taką wiedzą rozważmy następujące dwa makra: ;LDAX- Jest to makro ładujące ax słowem spod adresu Array[Index1][Index2] ; Założenie: Zadeklarujemy tablicę używając instrukcji takiej jak ; Array word Colsize dup (Rowsize dup (?)) ; a tablica jest przechowana w rzędowym przechowywaniu elementów. ; Jeśli wyszczególnimy (opcjonalnie) czwarty parametr, jest to instrukcja maszynowa 8086 zastępowana ; przez instrukcję MOV ,która ładuje AX z Array[bx] LDAX macro Array, Index1,Index2,Instr imul bx,Index1, (lengthof Array) / (length Array) add bx, Index2 add bx, bx ;zobaczmy czy jest dostarczony czwarty parametr ifb mov ax, Array[bx] ;Jeśli nie, emituj instrukcję mov else instr ax, Array[bx] ;jeśli tak, emituj instrukcję użytkownika endif endm ;STAX –jest to makro przechowujące ax w słowie spod adresu Array[Index1][Index2] ; Założenie: Takie jak powyżej STAX macro Array,Index1,Index2 imul bx, Index1, (lengthof Array)/(length Array) add bx, Index2 add bx, bx mov Array[bx], ax endm Z powyższymi makrami, oryginalny program będzie się przedstawiał tak: i j
textequ textequ
;przetrzymanie I w rejestrze cx ;przetrzymanie J w rejestrze dx
ForILp:
mov cmp jnl
;inicjacja pętli I indeksem zero ;czy I jest mniejsze niż 16? ;jeśli tak skocz do treści pętli I
I,0 I, 16 ForIDone
ForJLp:
ForJDone:
mov cmp jnl
J ,0 J, 7 ForJDone
;inicjacja pętli J indeksem zero ;czy J jest mniejsze niż 7? ;jeśli tak, skocz do treści pętli J
ldax mov sub ldax stax
A,I, J bx,16 bx,I b, bx,J, imul x, J, I
;pobranie A[I][J]
inc jmp inc Jmp
J ForJLp I ForILp
;obliczanie 16-I ;mnożenie B[16-I][J] ;przechowanie X[J][I] ;następna iteracja pętli ;następna iteracja pętli I
ForIDone: Jak widać wyraźnie, kod z powyższymi pętlami jest krótszy poprzez użycie tych makr. Oczywiście, cała sekwencja kodu jest w rzeczywistości dłuższa ponieważ makra przedstawiają więcej linii kodu, który zachowują w oryginalnym programie. jednakże, jest to artefakt tego szczególnego programu. Generalnie ,prawdopodobnie będziemy mieli więcej niż trzy tablice; co więcej, możemy zawsze dołożyć LDAX i STAX do pliku bibliotecznego i automatycznie wprowadzać je zawsze tam gdzie zajmujemy się dwuwymiarowymi tablicami. Chociaż, technicznie ,nasz program może w rzeczywistości zawierać więcej instrukcji assemblera, jeśli wprowadzamy te makra w naszym kodzie, musimy tylko napisać te makra jeden raz. Zresztą zajmuje niewiele wysiłku wprowadzenie makr do każdego nowego programu. Możemy skrócić tę sekwencję kodu nawet bardziej używając kilku dodatkowych makr. Jednakże, jest kilka dodatkowych tematów do omówienia zanim będziemy mogli to zrobić. 8.14.2 MAKRA A PROCEDURY 80x86 Początkujący programiści asemblerowi często mylą makra i procedury. Procedura jest pojedynczą częścią kodu, którą wywołujemy z różnych punktów programu. Makro jest sekwencją instrukcji, które MASM kopiuje w naszym programie ilekroć używamy makra. Rozważmy dwa fragmenty kodu: Proc_1 proc near mov ax, 0 mov bx, ax mov cx, 5 ret Proc_1 endp Macro_1
macro mov mov mov endm call call Macro_1 Macro_1
ax, 0 bx, ax cx, 5 Proc_1 Proc_1
Chociaż makro i procedura dają ten sam wynik, robią to na różne sposoby. Procedura generuje kod kiedy assembler napotka dyrektywę proc. Wywołanie tej procedury wymaga tylko trzech bajtów. W czasie wykonania,80x86: • napotyka instrukcję call • odkłada adres powrotu na stos
• • • •
skacze do Proc_1 w tym wykonuje kod zdejmuje ze stosu adres powrotu wraca do kodu wywołującego
Makro, z drugiej strony, nie emituje żadnego kodu kiedy przetwarza instrukcje pomiędzy dyrektywami macro i endm. Jednak, po napotkaniu Macro_1 w polu mnemoniku ,MASM będzie assemblował każdą instrukcję pomiędzy dyrektywami macro i endm i emitował ten kod do pliku wyjściowego. W czasie wykonywania, CPU wykonuje te instrukcje bez obciążenia call /ret. Wykonanie makrorozwinięcia jest zazwyczaj szybsze niż wykonanie tego samego kodu implementowanego w procedurze. Jednakże, jest to inny przykład klasycznego problemu szybkość/ miejsce. Makra wykonują się szybciej poprzez wyeliminowanie sekwencji call /return. jednak asembler kopiuje kod makro do naszego programu przy każdym wywołaniu makra. Jeśli mamy dużo wywołań makra wewnątrz programu, będzie on dużo większy niż ten sam program ,który używa procedur. Wywołania makr i wywołania procedur znacznie się różnią. Aby wywołać makro, musimy po prostu wyszczególnić nazwę makra jak gdyby była instrukcją lub dyrektywą. Do wywołania procedury musimy użyć instrukcji call. W wielu wypadkach jest nieszczęśliwie ,że używamy dwóch oddzielnych mechanizmów wywołań dla tak podobnych operacji. Rzeczywisty problem występuje jeśli chcemy przełączyć makro na procedurę lub vice versa. Może być tak, że używaliśmy makrorozwinięcia dla szczególnej operacji, ale teraz rozwinęliśmy makro tak wiele razy, że większego sensu nabiera zastosowanie procedury. Być może właśnie przeciwieństwo jest prawdą, użyliśmy procedury, ale chcemy rozszerzyć kod wplatany do poprawienia jej wydajności. Problem z jedną i drugą konwersja jest taki, że będziemy musieli znaleźć każde wywołanie makro lub procedury i zmodyfikować je. Modyfikacja makra lub procedury jest łatwa, ale zlokalizowanie i zmiana wszystkich wywołań może dać trochę pracy. Na szczęście, jest bardzo prosta technika, którą możemy użyć jako wywołania procedury dzieląc składnię z wywołaniem makra. Sztuczka ta to stworzenie makra lub równania tekstowego dla każdej procedury jaką piszemy, która rozszerza ją o wywołanie tej procedury. Na przykład przypuśćmy, że napiszemy procedurę ClearArray, która zeruje tablice. Kiedy napiszemy kod, możemy zrobić to następująco.: ClearArray textequ $$ClearArray proc near $$ClearArray endm Wywołanie procedury ClearArray używa po prostu instrukcji takiej jak ta: ClearArray Jeśli kiedyś zmienimy procedurę $$ClearArray na makro, wszystko co musimy zrobić to nazwać ClearArray i pozbyć się textequ dla procedury. Odwrotnie, jeśli już mamy makro i chcemy skonwertować je na procedurę, po prostu nazywamy procedurę $$procname i tworzymy przyrównanie tekstowe, które emituje wywołanie do tej procedury. Pozwala to nam na zastosowanie tej samej składni wywołania dla procedur i makr. Ten tekst nie używa normalnie omówionych powyżej technik, z wyjątkiem podprogramów Standardowej Biblioteki UCR. Jest tak ponieważ nie jest to dobry sposób na wywoływanie procedur. Niektórzy ludzie mają problemy w odróżnieniu makr i procedur, więc ten tekst będzie używał wyraźnych wywołań pomagając unikać takich pomyłek .Wywołania Biblioteki Standardowej są wyjątkiem ponieważ stosowanie wywołań makr jest standardowym sposobem wywołania tych podprogramów. 8.14.3 DYREKTYWA LOCAL Rozważmy następującą definicję makra: LJE macro Przez jne SkipIt jmp Przez SkipIt: Endm
To makro robi „długi skok jeśli równe”. Jednakże, jest jeden problem z nim. Ponieważ MASM kopiuje teks makra dosłownie, symbol SkipIt będzie przedefiniowany za każdym razem, kiedy makro LJE się pojawia. Kiedy to się wydarzy, asembler wygeneruje wielokrotnie błąd definicji. Przezwyciężeniem tego problemu jest dyrektywa local, która może być zastosowana do zdefiniowania lokalnego symbolu wewnątrz makra. Rozważmy następującą definicję makra: LJE
macro local jne jmp
Przez SkipIt SkipIt Przez
SkipIt: Endm W tej definicji makra, SkipIt jest symbolem lokalnym. Dlatego też, asembler wygeneruje nową kopię SkipIt za każdy razem ,kiedy jest wywoływane makro. Zapobiega to generowaniu przez MASM błędów. Dyrektywa local, jeśli pojawi się wewnątrz naszej definicji makra, musi pojawić się bezpośrednio po dyrektywie macro .Jeśli potrzebujemy wielorakich lokalnych symboli ,możemy wyszczególnić kilka z nich w polu operandu dyrektywy local, po prostu oddzielając każdy symbol przecinkiem: IFEQUAL
macro local mov jne inc jmp dec
ElsePosition: Done:
a,b ElsePosition, Done ax , a ElsePosition bx Done bx
endm 8.14.4 DYREKTYWA EXITM Dyrektywa exitm bezpośrednio zakańcza makro, dokładnie jak gdyby MASM napotkał endm. MASM ignoruje cały tekst od dyrektywy exitm do endm. Prawdopodobnie zastanawiacie się dlaczego ktoś miałby używać dyrektywy exitm .W końcu jeśli MASM ignoruje cały tekst między exitm i endm, dlaczego zawracać sobie głowę umieszczaniem dyrektywy exitm w naszym makrze na pierwszym miejscu? Odpowiedzią jest asemblacja warunkowa. Asemblacja warunkowa może być zastosowana do warunkowego wykonania dyrektywy exitm, tym samym pozwalając na dalsze makrorozwinięcia pod pewnymi warunkami, rozważmy coś takiego: Bytes macro Count byte Count if Count eq 0 exitm endif byte Count dup (?) endm Oczywiście ten prosty przykład może być zakodowany bez zastosowania dyrektywy exitm, ale on demonstruje jak dyrektywa exitm może być użyta wewnątrz sekwencji asemblacji warunkowej do sterowania jego oddziaływaniem. 8.14.5 PARAMETRY MAKROROZWINIĘCIA I OPERATORY MAKRA Ponieważ MASM wykonuje tekstowe zastąpienie dla parametrów makra, kiedy wywołujemy makro, są chwile, kiedy wywołanie makra może nie przynieść wyniku jakiego oczekujemy. Na przykład rozpatrzmy następującą (co prawda głupią) definicję makra: Index ;Problem ;
= 8 Makro to próbuje załadować AX elementem tablicy słów, wyszczególnionej przez parametr makra. Parametr ten musi być stałą w czasie asemblacji
Problem
macro mov endm
Parametr ax, Array[Parametr*2]
Problem 2 Problem Index+2 Kiedy MASM rozwija pierwsze wywołanie Problem ,tworzy instrukcje: mov ax, Array[2*2] Okay ,jak dotąd wszystko dobrze. Kod ten ładuje element dwa tablicy Array do ax. Jednak, rozważmy rozwinięcie drugiego wywołania Problem: mov ax, Array[Index+2*2] ponieważ wyrażenia adresowe MASMa wspierają operatory pierwszeństwa ,to makrorozwinięcie nie da prawidłowego wyniku. Uzyskamy dostęp do szóstego elementy Array (pod indeksem 12) zamiast do elementu dziesiątego spod indeksu 20. Powyższy problem wystąpi ponieważ MASM po prostu zastąpi parametr formalny przez tekst rzeczywistego parametru a nie wartość rzeczywistego parametru. Taki mechanizm przekazywanie przez nazwę powinien być znany od dawna programistom C i C++, którzy używają instrukcji #define. Jeśli sądzimy, że parametry makra (przekazywanie przez nazwę) pracuje tak jak Pascalowskie lub C przekazywanie parametrów przez wartość, musimy nastawić się na końcowe nieszczęście. Jednym możliwym rozwiązaniem, które działa dla makr takich jak powyższe, jest wstawienie nawiasów okrągłych wokół parametrów makra, które występują wewnątrz wyrażenia w środku makra. Rozpatrzmy następujący kod: Problem macro Parametr mov ax, Array[{Parametr)*2] endm Problem Index+2 Makro to wywołuje rozwinięcie Mov ax, Array[(Index+2)*2] To daje nam spodziewany rezultat. Parametr tekstowy jest zastępowany ale jest jeden problem w działaniu kiedy używamy makr. Innym problemem występuje ponieważ MASM ma dwa typy wartości czasu asemblacji: numeryczne i tekstowe. Niestety MASM oczekuje wartości numerycznych w pewnym kontekście i wartości tekstowych w innym. Nie są one w pełni zamienne. Na szczęście MASM dostarcza zbioru operatorów, które pozwalają nam konwertować pomiędzy jedną a drugą formą (jeśli jest to możliwe do wykonania).Aby zrozumieć subtelne różnice pomiędzy tymi dwoma typami wartości spójrzmy na poniższe instrukcje: Numeryczne = 10+2 Tekstowe textequ <10+2> MASM ocenia wyrażenie numeryczne „10+2” i łączy wartość dwanaście z symbolem Numeryczny. Dla symbolu Tekstowy po prostu przechowuje łańcuch „10+2” i zastępuje go dla Tekstowy gdziekolwiek użyjemy go w wyrażeniu. W wielu kontekstach możemy użyć jednego z dwóch symboli. Na przykład, poniższe dwie instrukcje obie ładują dwanaście do ax: mov ax, Numeryczny ;to samo co mov ax, 12 mov ax, Tekstowy ;to samo co mov ax,10+2 Jednak rozważmy poniższe dwie instrukcje: mov ax, Numeryczny*2 ;to samo co mov ax, 12*2 mov ax, Tekstowy*2 ;to samo co mov ax, 10+2*2 Jak widzimy, zastąpienie tekstu, które występuje z przyrównaniem tekstowym może prowadzić do tego samego problemu, który napotykamy przy zastępowaniu tekstowym parametrów makr. MASM automatycznie konwertuje obiekt tekstowy na wartość numeryczną, jeśli konwersja jest konieczna. Jeśli omówiliśmy problem zastępowania tekstowego, możemy użyć wartości tekstowej (jeśli ciąg przedstawia wielkość liczbową) gdziekolwiek MASM wymaga wartości liczbowej. Idąc w przeciwnym kierunku, konwersja wartości liczbowej na tekstową nie jest automatyczna. Dlatego też MASM dostarcza operatora, którego możemy użyć do konwersji danej liczbowej do danej tekstowej:
operator „%”.Ten operator rozszerzenia wymusza bezpośrednie wyliczenie wyrażenia potem konwertuje wynik wyrażenia do ciągu cyfr. Spójrzmy na wywołanie makra Problem: Problem 10+2 ;Parametrem jest „10+2” Problem %10+2 ;Parametrem jest „12” W drugim powyższym przykładzie operator rozwinięcia tekstu instruuje MASM aby wyliczył wyrażenie „10+2” i konwertuje wynik wartości liczbowej do wartości tekstowej składającej się z cyfr, które przedstawiają wartość dwanaście. Dlatego też, te dwa makra rozwijają się (odpowiednio) w następujące instrukcje: mov ax, Array[10+2*2] mov ax, Array[12+2] MASM dostarcza drugiego operatora, operatora zastąpienia, który pozwala nam rozwinąć nazwy parametrów makra gdzie MASM normalnie nie oczekuje symbolu. Operator zastąpienia to znak „&” (ampersand) .Jeśli otoczymy nazwę parametru makra ampersandami wewnątrz makra ,MASM zastąpi parametr tekstowy bez względu na lokację symbolu. Pozwala to nam rozwinąć parametry nazw których nazwy pojawiają się wewnątrz innych identyfikatorów lub wewnątrz ciągów literowych. Poniższe makro demonstruje zastosowanie tego operatora: DebugMsg macro Point,String Msg&String& byte „At point &Point&: &String&” endm DebugMsg 5, Makro wywołuje bezpośrednio instrukcję: Msg5 byte „At point 5: Twierdzenie błędne” Zauważmy, że operator zastąpienia pozwolił temu makru na połączenie :Msg” i „5” w etykietę dyrektywy bajtowej. Zauważmy również ,że operator rozwinięcia pozwala nam rozwinąć identyfikator makra nawet jeśli pojawia się on w stałym ciągu literowym. Bez ampersandu w ciągu ,MASM wyemitowałby instrukcję: Msg5 byte „At point : String” Innym ważnym operatorem aktywnym wewnątrz makr jest operator dosłownego znaku, wykrzyknik „!”.Symbol ten instruuje MASM, żeby przekazał znak bez żadnych modyfikacji. Normalnie będziemy używać tego symbolu jeśli musimy objąć jeden z następujących symboli jako znaków wewnątrz makra: ! & > % Na przykład, mamy ochotę wyświetlić w ciągu makra DebugMsg ampersand, użyjemy definicji: DebugMsg macro Point,String Msg&String& byte „At point !&Point!&: &String&” Endm „debug 5, dostarczy następującej instrukcji: Msg5 byte „At point &Point&: &String&” Użycie symboli „<” i „>” ogranicza dane tekstowe wewnątrz MASMa. Poniższe dwa wywołania makra PutData pokazują jak możemy zastosować te ograniczniki w makrze: PutData macro PD_&Nazwa& byte endm PutData PutData
Nazwa, Dane Dane
MDane, 5,4,3 MDane <5,4,3>
,emituje „PD_Mdane byte 5” ;emituje „PD_Mdane byte 5,4,3
Możemy użyć ograniczników tekstu do otoczenia obiektów, które życzymy sobie traktować jako pojedynczy parametr zamiast jako listę wielu parametrów. W przykładzie PutData, pierwsze wywołanie przekazuje cztery parametry do PutData (PutData ignoruje ostatnie dwa)W drugim wywołaniu, są dwa parametry, drugi składa się z tekstu 5,4,3. Ostatnim interesującym nas operatorem makra jest operator „::”.operator ten zaczyna komentarz makra. Zwykle MASM kopiuje cały tekst z makra do treści programu podczas asemblacji, wliczając w to wszystkie komentarze. Jednak, jeśli zaczniemy komentarz od „ ;;” zamiast pojedynczego średnika, MASM nie rozwinie tego komentarza jako części kodu podczas makrorozwinięcia. Zwiększa to szybkość asemblacji a co ważniejsze, nie zaśmieca listingu programu kopiami tych samych komentarzy (zobacz „Kontrola Listingu” aby nauczyć się więcej o listingach programu)
Tablica 42: Operatory Makra 8.14.6 PRÓBKA MAKRA IMPLEMENTUJĄCEGO PĘTLĘ FOR Pamiętamy operacje dla pętli i macierzy używane w poprzednim przykładzie? W konkluzji tej sekcji był krótki komentarz, że możemy „poprawić” ten kod bardziej, używając makr, ale na przykład musimy poczekać. Korzystając z opisu operatorów makra, możemy skończyć teraz to omówienie. Makra, które są zaimplementowane dla pętli for: ;Po pierwsze trzy makra, które pozwolą nam skonstruować symbole poprzez łączenie innych. Jest to konieczne ;ponieważ kod ten musi rozszerzyć kilka komponentów w tekście przyrównywanym wiele razy aby dojść do ;poprawnego symbolu ; ;MakeLbl - Emituje tworzenie etykiety prze połączenie dwóch parametrów przekazanych do tego makra. MakeLbl macro FirstHalf,SecondHalf &FirstHalf&&SecondHalf&: endm jgDone macro FirstHalf,ScondHAlf jg &FirstHAlf&&SecondHAlf& endm ;ForLp - Makro to pojawia się na początku pętli for. Wywołując to makro, używamy instrukcji: ; ; ForLp LoopCtrlVar, StartVal, StopVal ; ;Notka ”FOR” jest w MASM słowem zarezerwowanym, dlatego to makro nie używa tej nazwy. ForLp macro LCV,Start,Stop ;Musimy wygenerować unikalny globalny symbol dla każdej pętli for jaką tworzymy. Symbol te musi być ;globalny, ponieważ będziemy musieli odnosić się do niego u podstawy pętli. Generując unikalny symbol makro ;to łączy „FOR” z nazwą zmiennej sterującej pętli i unikalną wartością liczbową, którą to makro zwiększa za ;każdym razem, kiedy użytkownik konstruuje pętlę for z takiej samej zmiennej sterującej pętli. ifndef $$For&LCV& ;;Symbol =$$FOR połączony z LCV $$For&LCV& = 0 ;;Jeśli jest to pierwsza pętla / LCV używa else ;;zera, inaczej zwiększa wartość $$For&LCV& = $$For&LCV&+1 endif ;Emisja instrukcji do inicjacji zmiennej sterującej pętli: mov ax, Start mov LCV, ax ;etykieta wyjściowa na szczycie pętli .Przybiera postać $$FOR LCV x ;gdzie LCV jest nazwą zmiennej sterującej pętli a X jest unikalnym numerem, które to makro zwiększa dla ;każdej pętli for, która używa takiej samej zmiennej sterującej pętli. MakeLbl $$For&LCV&, %$$For&LCV& ;Okay, kod wyjściowy dla tej pętli for jest kompletny. ;Makro jgDone generuje skok (jeśli większy) do etykiety Next makro emituje poniższy kod mov ax, LCV cmp ax, Stop jgDone $$Next&LCV&, %$$For&LCV&
;Makro Next kończy pętlę for. Makro to zwiększa zmienną sterującą pętli a potem przekazuje sterowanie powrotnie do etykiety na szczycie pętli for Next
macro LCV inc LCV jmpLoop $$For&LCV&, %$$For&LCV& MakeLbl $$Next&LCV&, %$$For$LCV& endm Z tymi makrami i makrami LCAX/STAX,kod prezentowany wcześniej do manipulowania tablicą ,staje się bardzo prosty ForLp I,0,15 ForLp J,0,6 ldax mov sub ldax stax
A,I,J bx, 15 bx, I b, bx,J, imul x, J,I
Next Next
J I
;pobiera A[I]{J} ;oblicza 16 - I ;mnoży w B[15-i][J} ;przechowuje X [J][I]
Chociaż kod ten nie jest całkiem tak krótki jak oryginalny przykład w C/C++, jest już całkiem znośny. Podczas gdy program główny stał się dużo prostszy, jest pytanie o same makra. Makra ForLp i Next są niezmiernie złożone! Gdybyśmy musieli wkładać taki wysiłek za każdym razem, kiedy chcemy stworzyć makro, program asemblerowy byłby dziesięć razy cięższy do napisania, jeśli zdecydowalibyśmy się na użycie makr. Na szczęście ,musimy tylko raz napisać (i zdebuggować) makro takie jak to. Wtedy możemy go używać tak wiele razy jak chcemy, w wielu różnych programach bez każdorazowego martwienia się o jego implementację. Mając złożoność makr For i Next, prawdopodobnie jest dobrym pomysłem ostrożnie opisać co każda instrukcja w tych makrach robi. Jednak, przed omówieniem makr powinniśmy omówić dokładnie jak można zaimplementować pętle for / next w języku asemblera. Ten tekst w pełni bada pętlę for trochę później ,ale możemy z pewnością przejść tutaj podstawy. Rozważmy następującą Pascalowska pętle for: for zmienna := Start to End do Jakieś_Instrukcje; Pascal zaczyna od obliczenia wartości Start. Wtedy przydziela tą wartość do zmiennej sterującej pętli (zmienna).Potem oblicza End i zachowuje tą wartość w lokacji tymczasowej. Wtedy instrukcja pascalowska for jest wprowadzana do treści pętli. Pierwszą rzeczą jaką robi pętla jest porównanie wartości zmienna z wartością obliczoną dla End.Jeśli wartość zmienna jest większa niż wartość End,Pacal przechodzi do pierwszej instrukcji po pętli for, w przeciwnym razie wykonuje Jakieś_Instrukcje. Po wykonaniu przez pętlę for Jakieś_Instrukcje, dodawana jest jedynka do zmienna i wykonywany skok do punktu gdzie porównywana jest wartość zmienna z obliczoną wartością End. Konwertując ten kod bezpośrednio na język asemblera mamy następujący kod: ;Notka: Ten kod zakłada że Start i End są prostymi zmiennymi. Jeśli tak jest, obliczamy wartość dla tych wyrażeń ;i umieszczamy je w tych zmiennych mov ax, Start mov Zmienna, ax ForLoop: mov ax, Zmienna cmp ax, End jg ForDone inc jmp
Zmienna ForLoop
ForDone: Aby implementować to jako zbiór makr, będziemy musieli napisać krótki kawałek kodu który napisze powyższe instrukcje asemblera dla nas. Na pierwszy rzut oka, wydawałoby się to łatwe, dlaczego nie użyć następującego kodu? ForLp
macro mov
Zmienna, Start, Stop ax, Start
ForLoop:
Next
mov mov cmp jg endm macro inc jmp
Zmienna, ax ax, Zmienna ax, Stop ForDone Zmienna Zmienna ForLoop
ForDone: Endm Makra te stworzyłyby poprawny kod – dokładnie raz. jednak problem rozwinąłby się gdybyśmy spróbowali użyć tych makr po raz drugi .jest to szczególnie widoczne kiedy używamy pętli zagnieżdżonych: ForLp I, 1, 10 ForLp J, 1, 10 Next J Next I Powyższe makra emitują kod 80x86: mov ax, 1 mov I, ax ForLoop mov ax, I cmp ax, 10; jg ForDone
ForLoop
mov mov mov cmp jg inc jmp
ax, 1 J, ax ax, J ax, 10 ForDone
;makro ForLp I, 1 ,10 emituje te ;instrukcje ; ; ; ; ;Makro ForLp J, 1 ,10 emituje te ;instrukcje ; ; ; -
J ForLp
;makro Next emituje te instrukcje ;
inc jmp
I ForLp
;makro Next J emituje te instrukcje ;
ForDone: ForDone: Problem, widoczny w powyższym kodzie, jest taki za każdym razem kiedy używamy makra ForLp, emitujemy etykietę „ForLoop” do kodu. Podobnie, za każdym razem, kiedy używamy makra Next, emitujemy etykietę „ForDone” do strumienia kodu. Dlatego też, jeśli używamy tych makr więcej niż raz (wewnątrz tej samej procedury) dostaniemy powielony symbol błędu. Aby zapobiec temu błędowi, makra musza wygenerować unikalne etykiety za każdym razem kiedy ich używamy. Niestety, dyrektywa local nie robi tego. Dyrektywa local definiuje unikalny symbol wewnątrz pojedynczego wywołania makra. Jeśli spojrzymy uważnie na powyższy kod, zobaczymy, że makro ForLp emituje symbol do którego odnosi się kod w makrze Next. Podobnie, makro Next emituje etykietkę do której odnosi się makro ForLp .Dlatego też, nazwa etykiety musi być globalna ponieważ dwa makra mogą odnosić się wzajemnie do etykiet. Rzeczywistym rozwiązaniem zastosowania makr ForLp i Next jest wygenerowanie znanych globalnie etykiet w postaci „$$For”+”nazwa zmiennej” + „jakiś unikalny numer” i „$$Next+”nazwa zmiennej”+”jakiś unikalny numer”. Dla przykładu podanego wyżej, rzeczywiste makra ForLp i Next generują następujący kod: mov ax, 1 ;Makro ForLp I, 1, 10 emituje te instrukcje mov I,ax ; $$ForIO: mov ax, I ; cmp ax, 10 ; jg $$NextIO ; mov ax, 1 ;Makro ForLp J, 1 ,10 emituje te instrukcje mov J, ax ;
mov cmp jg inc jmp
ax, J ax, 10 $$NextJO
; ; ;
J $$ForJO
;Makro Next emituje te instrukcje ;
inc jmp
I $$ForIO
;Makro I emituje te instrukcje
$$NextJO: $$NextIO: Pozostaje pytanie „Jak wygenerować takie etykiety?” Budowa symbolu w postaci „$$ForI” lub „$$NextJ” jest stosunkowo łatwa. Tworzymy ten symbol poprzez połączenie ciągu „$$For” lub „$$Next” z nazwą zmiennej sterującej pętli .Problem wystąpi wtedy, kiedy spróbujemy dołączyć wartość liczbową na koniec tego ciągu. rzeczywisty kod ForLp i Next osiągnie to stworzenie nazwy zmiennej w postaci „$$For nazwa zmiennej” w czasie asemblacji i zwiększy tą zmienną dla każdej pętli z daną nazwą zmiennej sterującej. Poprzez wywołanie makr MakeLbl,jgDone i jmpLoop,ForLp i Next otrzymamy właściwe etykiety i pomocnicze instrukcje. Makra ForLp i Next są dosyć złożone. Daleko bardziej złożone, niż te które znajdziemy w programach. Demonstrują one jednak siłę i możliwości makr MASMa. Nawiasem mówiąc, dużo lepszym sposobem do tworzenia tych symboli jest zastosowanie funkcji makra. 8.14.7 FUNKCJE MAKRA Funkcja makra jest makrem ,którego jedynym celem jest zwracanie wartości do użycia w polu operandu jakiejś innej instrukcji. Chociaż jest oczywiste podobieństwo pomiędzy procedurami a funkcjami w językach wysokiego poziomu a makrami proceduralnymi i funkcjonalnymi, analogia jest daleko bardziej zupełna. Funkcje makra nie pozwalają nam na stworzenie sekwencji kodu ,który emituje jakieś instrukcje, które obliczają wartości, kiedy program się wykonuje. Zamiast tego, funkcje makra po prostu obliczają jakąś wartość w czasie asemblacji, kiedy MASM może używać operandu. Dobrym przykładem funkcji makra jest funkcja Date.Makro to pakuje wartość pięciu bitów dnia, czterech bitów miesiąca i siedem bitów roku do wartości 16 bitów i zwraca tą wartość 16 bitową jako wynik .Jeśli musielibyśmy stworzyć zainicjowaną tablicę dat, moglibyśmy użyć poniższego kodu: DateArray word Date (2, 4, 84) word Date (1, 1, 94) word Date (7, 20, 60) word Date (7, 19, 69) word Date (6, 18, 74) Funkcja Date spakuje dane a dyrektywa word wyemituje 16 bitową spakowaną wartość dla każdej daty pliku wynikowego. Wywołujemy funkcje makra poprzez zastosowanie ich nazw ,gdzie MASM oczekuje jakiegoś rodzaju wyrażenia tekstowego. Jeśli funkcja makra wymaga parametrów musimy otoczyć je wewnątrz nawiasami ,podobnie jak parametry Date. Funkcje makra wyglądają dokładnie jak makra standardowe z dwoma wyjątkami: nie zawierają instrukcji, które generują kod i zwracają wartość tekstową poprzez operand do dyrektywy exitm .Zauważmy, że nie możemy zwrócić wartości liczbowej z funkcji makra. Jeśli musimy zwrócić wartość liczbową ,najpierw musimy skonwertować ją do wartości tekstowej. Następująca funkcja makra implementuje Date używając 16 bitowego formatu danych podanych w Rozdziale Pierwszym. Date Value
macro local = Exitm Endm
month, day, year Value (month shl 12) or (day shl 7) or year %Value
Operator wyrażenia („%”) jest konieczny w polu operandu dyrektywy exitm ponieważ funkcje makra zawsze zwracają dane tekstowe a nie numeryczne. Operator wyrażenia konwertuje wartość liczbową do ciągu cyfr do przyjęcia przez exitm. Jeden drobny problem z powyższym kodem jest taki, że funkcja zwraca śmieci jeśli data jest nieprawidłowa. Lepszym rozwiązaniem będzie wygenerowanie błędu jeśli dane wejściowe będą niepoprawne. Możemy użyć dyrektywy „.err” i zrobić asemblację warunkową. Poniższa implementacja Date sprawdza wartości miesiąca, dnia i roku aby zobaczyć czy są one sensowne: Date macro monyh,day, year local Value if
(month gt 12) or (month lt 1) or \ (day gt 31) or (day lt 1) or \ (year gt 99) or (year lt 1) .err exitm <0> ;;musi zwrócić coś! endif = (month shl 12) or (day shl 7) or year exitm %Value endm
Value
W tej wersji, każda próba wprowadzenia nieprawidłowej daty wywoła dyrektywę .err, która wywoła błąd w czasie asemblacji. 8.14.8 MAKRA PRZEDEFINIOWANE,FUNKCJE MAKRA I SYMBOLE MASM dostarcza czterech wbudowanych makr i czterech odpowiednich funkcji makra. W dodatku, MASM również dostarcza dużej liczby uprzednio zdefiniowanych symboli, do których możemy uzyskać dostęp podczas asemblacji. Chociaż będziemy rzadko używali tych makr ,funkcji i zmiennych dla w miarę złożonych makr, są one niezbędne, kiedy ich potrzebujemy
Tablica 43: Predefiniowane Makra MASM Makra substr i catstr zwracają dane tekstowe. Pod wieloma względami są one podobne do dyrektywy textequ ponieważ używamy ich do przydzielenia danej tekstowej do symbolu w czasie asemblacji. Instr i sizestr są podobne do dyrektywy „ = „ o tyle, że zwracają one wartość liczbową. Makro catstr może wyeliminować potrzebę znalezienia makra MakeLbl w makrze ForLp. Porównajmy następującą wersję ForLp z poprzednia wersją (zobacz „Przykład makra do implementacji pętli for” ForLp $$For&LCV&
macro local ifndef =
LCV, Start,Stop ForLoop $$For&LCV& 0
else = endif mov mov
$$For&LCV&
ForLoop &ForLoop&:
textequ
$$For&LCV&+1 ax, Start LCV, ax @catstr($for&LCV&,%$$For&LCV&)
mov ax, LCV cmp ax, Stop jgDone $$Next&LCV&, %$$For&LCV& endm MASM dostarcza również funkcji makra dla form catstr, instr, sizestr i substr .Dla rozróżnienia tych funkcji makra od odpowiadających predefiniowanych makr. MASM używa nazw @catstr,@instr,@sizestr i @substr. Oto odpowiedniki dla tych operacji: Symbol Symbol
catstr textequ
String1, String2,.... @catstr(String1,String2,...)
Symbol Symbol
substr textequ
SomeStr, 1, 5 @substr(SomeStr, 1, 5)
Symbol Symbol
instr =
1, SomeStr,SearchStr @substr(1, SomeStr, SearchStr)
Symbol Symbol
sizestr =
SomeStr @sizestr(SomeStr)
Tablica 44: Predefiniowane funkcje makra dla MASMa Ostatni przykład pokazał jak pozbyć się makr jgDone i jmpLoop w makrze ForLp .Końcowa, poprawiona wersja makr ForLp i Next, wyeliminowała trzy makra i pracująca bez błędów w MASM, może wyglądać tak jak poniżej: ForLp
$$For&LCV& $$For&LCV&
ForLoop &ForLoop&:
macro local
LCV, Start,Stop ForLoop
ifndef` = else = endif mov mov textequ
$$For&LCV& 0
ax, Start LCV, ax @catstr($For&LCV&, %$$For&LCV&)
mov cmp jg
ax, LCV ax, Stop @catstr($$Next&LCV&, %$$For&LCV&)
$$For&LCV& + 1
Next
NextLbl &NextLbl&:
endm macro local inc jmp textequ
LCV NextLbl LCV @catstr($$For&LCV&, %$$For&LCV&) @catstr($Next&LCV$, %$$For&LCV&)
endm MASM dostarcza również dużej liczby wbudowanych zmiennych, które zwracają informacje o bieżącej asemblacji. Poniższa tablica opisuje te wbudowane zmienne czasu asemblacji.
Tablica 45:Przedefiniowane zmienne czasu asemblacji MASMA Chociaż jest niewystarczająco miejsca do omówienia w szczegółach o możliwych zastosowaniach dla każdej z tych zmiennych, kilka przykładów można zademonstrować jako możliwości. Inne zastosowania tych zmiennych będą pojawiały się w całym tekście, jednak największe wrażenie zrobi samodzielne ich odkrywanie. Zmienna @CPU jest całkiem użyteczna jeśli chcemy zasemblować różne sekwencje kodu w programie dla różnych procesorów. Sekcja o asemblacji warunkowej w tym rozdziale opisuje jak można stworzyć symbol określający czy assemblujemy kod dla procesora 80386 lub późniejszych lub procesora 8086.Symbol @CPU dostarcza nam symbolu ,który mówi dokładnie jakie instrukcje są dozwolone w danym punkcie naszego programu. Poniżej jest przedstawiony przykład zastosowania zmiennej @CPU: if @CPUand 100b ;Potrzebujemy procesora 80286 lub późniejszych shl ax, 4 ;dla tych instrukcji else ;musi być procesor 8086 mov cl, 4 shl ax, cl endif Możemy użyć dyrektywy @Line do wprowadzenia specjalnej wiadomości diagnostycznej w naszym kodzie. następujący kod wydrukuje informację o błędzie wliczając w to numer linii w pliku źródłowym naruszonego twierdzenia, jeśli wykryje błąd w czasie wykonania: mov ax, ErrorFlag cmp ax, 0 je NoError mov ax, @Line ;ładuje AX z bieżącej linii # call Printerror ;drukuje info o błędzie i Linię # jmp Quit ;Kończy program 8.14.9 MAKRA A PRZYRÓWNYWANIE TEKSTU Marka, funkcje makra i przyrównywanie tekstu wszystkie zastępują tekst w programie. Chociaż jest jakieś podobieństwo miedzy nimi, w rzeczywistości służą one różnym celom w programie języka asemblera. Przyrównywanie tekstu wykonuje zastąpienie pojedynczego tekstu w linii. Nie pozwala na żadne parametry. Jednakże, możemy zastąpić tekst gdziekolwiek w linii przyrównaniem tekstu. Możemy rozszerzyć przyrównanie tekstu na etykietę, mnemonik, operand lub nawet pole komentarza. Ponadto możemy zastąpić pola wielokrotne, nawet w całej linii pojedynczym symbolem. Funkcje makra są poprawne w tylko w polu operandu. Jednak możemy przekazać parametry do funkcji makra czyniąc je bardziej ogólnym niż po prostu przyrównaniem tekstu. Makra proceduralne pozwalają nam emitować sekwencje instrukcji (z przyrównywaniem tekstu, możemy emitować ,najwyżej, jedną instrukcję). 8.14.10 MAKRA: DOBRE I ZŁE WIEŚCI
Makra oferują znaczną wygodę. Pozwalają nam na wprowadzenie kilku instrukcji do naszego pliku źródłowego poprzez wpisanie pojedynczej komendy. Może to zaoszczędzić niewiarygodną ilość pisaniny kiedy wprowadzamy ogromną tablicę, której każda linia zawiera jakieś dziwaczne ale powtarzalne obliczenia. .Jest użyteczne (w pewnych przypadkach) dla wspomożenia uczynienia naszych programów bardziej czytelnymi.. Niewielu będzie się sprzeczało, że ForLp I,1 ,10 nie jest bardziej czytelne niż odpowiadający kod 8086.Niestety,jest łatwo stworzyć kod, który jest niewydolny, trudny do czytania i trudny do pielęgnacji. Wielu tak zwanych „zaawansowanych” programistów assemblerowych poniósł pomysł, że mogą tworzyć swoje własne instrukcje przez definicję makra i zaczynają tworzyć makra dla każdej wyobrażalnej funkcji pod słońcem. Makro COPY przedstawione wcześniej jest dobrym przykłądem.8086 nie wspiera operacji przesunięcia z pamięci do pamięci. Spoko, stworzymy makro, które zrobi tę robotę dla nas. Wkrótce program asemblerowy nie będzie wcale wyglądał jak asembler 8086.Zamiast tego będzie duża liczba instrukcji będących wywołaniami makra. Być może teraz jest dobrze programiście który stworzył wszystkie te makra i gruntownie zrozumiał ich działanie. Programista 8086,który nie jest zaznajomiony z tymi makrami jest to wszystko bzdurą .Utrzymywanie programu ,który ktoś napisał, który zawiera „nowe” instrukcje implementowane przez makro, jest zadaniem strasznym .Dlatego też ,powinniśmy rzadko stosować makra jako środka do tworzenia nowych instrukcji na 80x86 Inny problem z makrami, to to, że mają one tendencję do ukrywania skutków ubocznych .Rozważmy makro COPY prezentowane wcześniej. Jeśli napotkamy instrukcję w postaci COPY VAR1, VAR2 w programie asemblerowym, będziemy myśleć, że jest to niewinna instrukcja, która kopiuje VAR2 do VAR1.Błąd!Niszczy ona również zawartość rejestru ax pozostawiając kopię wartości VAR2 w rejestrze ax. Wywołanie tego makra nie czyni tego wyraźnie. Rozważmy taką sekwencję kodu: mov ax, 5 copy Var2, Var1 mov Var1, ax Ta sekwencja kodu kopiuje Var1 do Var2 a potem (podobno) przechowuje pięć w Var1.Niestety,makro COPY zmiotło wartość w ax (pozostawiając samą pierwotną wartość zawartą w Var1),więc sekwencja instrukcji nie modyfikuje wcale Var1! Innym problemem z makrami jest wydajność. Rozważmy następujące wywołania makra COPY: copy Var3, Var1 copy Var2, Var1 copy Var0, Var1 Te trzy instrukcje generują kod: mov ax, Var1 mov Var3, ax mov ax, Var1 mov Var2, ax mov ax, Var1 mov Var0, ax wyraźnie widać ,że ostatnie dwie instrukcje mov ax, Var1 są zbyteczne. rejestr ax już zawiera kopie Var1,więc nie musimy ponownie ładować ax tą wartością. Niestety, ta niedogodność jest zupełnie oczywista w kodzie rozszerzonym ,nie jest oczywista wcale w wywołaniu makra. Innym problemem z makrami jest złożoność. Żeby wygenerować wydajny kod, musimy stworzyć niezmiernie złożone makro używając asemblacji warunkowej (zwłaszcza ifb, ifidn ,itp.),pętli reapet (opisanej trochę później) i innych dyrektyw. Niestety ,makra te są małymi programikami same w sobie. Możemy mieć błędy w naszych makrach, kiedy mamy błąd w naszym programie asemblerowym. Przez większa złożoność naszych makr staje się bardziej prawdopodobne, że zawarte w nich błędy staną się błędami w naszych programach, kiedy wywołamy makro. Nadużywanie makr, zwłaszcza złożonych, tworzy trudny do odczytania kod ,który jest trudny do pielęgnacji. Mimo entuzjastycznych zapewnień, tych, którzy kochają makra ,niepohamowane stosowanie makr wewnątrz programu generalnie powoduje więcej błędów niż pomaga. Jeśli będziemy stosować makra, bądźmy ostrożni. Jednak jest i dobra strona makr. Jeśli ujednolicimy zbiór makr i udokumentujemy wszystkie nasze programy przez stosowanie makr, mogą one nam pomóc uczynić nasze programy bardziej czytelnymi. Zwłaszcza jeśli te makra mają łatwo identyfikowalne nazwy. Standardowa Biblioteka UCR dla Programistów Języka Assemblera 80x86 stosuje makra dla większości wywołań biblioteki. Przeczytamy więcej o Standardowej Bibliotece UCR w następnym rozdziale. 8.15 OPERACJE REPEAT
Innym formatem makra (przynajmniej zdefiniowanym przez Microsoft) jest makro repeat. Makro repeat jest niczym więcej niż pętlą, która powtarza instrukcje wewnątrz pętli określona ilość razy .Są trzy typy makra repeat dostarczane przez MASM: repeat/rept.for/irp i forc/irpc .Makro repeat/rept używa następującej składni: repeat wyrażenie endm Wyrażenie musi być wyrażeniem liczbowym, które oblicza stałą bez znaku. Dyrektywa repeat powiela wszystkie instrukcje pomiędzy repeat a endm wiele razy. Poniższy kod generuje tablicę 26 bajtową, zawierającą 26 dużych znaków: ASCIICode = ‘A’ repeat 26 byte ASCIICode ASCIICode = ASCIICode+1 endm Symbol ASCIICode jest powiązany z kodem ASCII dla ‘A’. Pętla powtarza się 26 razy, za każdym razem emitując bajt z wartością ASCIICode. Również jest zwiększany symbol ASCIICode po każdej powtórce ,tak aby zawierał kod ASCII następnego znaku w tabeli ASCII. Faktycznie są generowane następujące instrukcje: byte ‘A’ byte ‘B’ byte ‘Y’ byte ‘Z’ ASCIICode = 27 Zauważmy, że pętla repeat wykonuje się w czasie asemblacji a nie w czasie wykonania. Repeat nie jest mechanizmem dla tworzenia pętli wewnątrz naszych programów; używamy jej dla replikowania części kodu wewnątrz programu. Jeśli chcemy stworzyć pętlę, która wykonuje się pewną liczbę razy wewnątrz programu, użyjemy instrukcji loop. Chociaż poniższe dwie sekwencje kodu dają ten sam wynik, nie są one takie same: ;sekwencja kodu używająca pętli czasu wykonania: mov cx, 10 AddLp: add ax, [bx] add bx, 2 loop AddLp ;Sekwencja kodu używająca pętli czasu asemblacji: repeat 10 add ax, [bx] add bx, 2 endm Pierwsza, powyższa sekwencja kodu emituje cztery instrukcje maszynowe do pliku kodu wynikowego. W czasie asemblacji, CPU 80x86 wykonują instrukcje pomiędzy AddLp a instrukcją loop dziesięć razy pod kontrolą instrukcji loop. Druga sekwencja kodu emituje 20 instrukcji do pliku kodu wynikowego. W czasie wykonania, CPU 80x86 po prostu wykonuje te 20 instrukcji sekwencyjnie ,bez żadnej transmisji sterowania. Druga forma będzie szybsza ponieważ 80x86 nie musi wykonywać instrukcji loop co trzecią instrukcję. Z drugiej strony, druga wersja jest również dużo większa ponieważ replikuje treść pętli dziesięć razy w pliku kodu wynikowego. W odróżnieniu od makr standardowych, nie definiujemy i wywołujemy makr repeat oddzielnie. MASM emituje kod pomiędzy dyrektywami repeat i endm po napotkaniu dyrektywy repeat. Nie jest to oddzielna faza wywołania. Jeśli chcemy stworzyć makro repeat, które może być wywoływane w całym naszym programie, rozważmy cos takiego: REPTMakro
macro Cout repeat Cout endm endm Poprzez umieszczenie makra repeat wewnątrz makra standardowego, możemy wywołać makro repeat gdziekolwiek w naszym programie poprzez wywołanie makra REPTMakro. Zauważmy, że potrzebujemy dwóch dyrektyw endm ,jedną do zakończenia makra repeat, jedną do zakończenia makra standardowego.
Rept jest synonimem dla repeat .Repeat jest nowszą forma, MASM wspiera rept dla kompatybilności ze starszymi plikami źródłowymi. Zawsze powinniśmy używać formy repeat. 8.16 MAKRO OPERACJE FOR I FORC Inną postacią makra repeat jest makro for. Makro to przybiera następującą postać: for PARAMETR, endm Nawiasy ostre są wymagane wokół pozycji w polu operandu dyrektywy for. Nawiasy otaczają opcjonalne pozycje ,nawiasy nie powinny się pojawiać w polu operandu. Dyrektywa for replikuje instrukcje pomiędzy for i endm raz dla każdej pozycji pojawiającej się w polu operandu. Co więcej dla każdej iteracji pierwszemu symbolowi w polu operandu jest przypisywana wartość kolejnej pozycji drugiego parametru. Rozważmy następującą pętle: for value, <0,1,2,3,4,5> byte value endm Pętla ta emituje sześć bajtów zawierających wartości zero, jeden ,dwa....pięć. Jest to absolutnie identyczne z sekwencją instrukcji byte 0 byte 1 byte 2 byte 3 byte 4 byte 5 Pamiętajmy, że pętla for, podobnie jak pętla repeat wykonuje się w czasie asemblacji a nie w czasie wykonania. Drugi operand for nie musi być stałą tekstową; możemy dostarczyć parametr makra, wynik funkcji makra lub przyrównywanie tekstowe do tej wartości. Zapamiętajmy, mimo że ten parametr musi być rozszerzony do wartości tekstowej z ogranicznikami tekstu wokół niej. Irp jest starym ,przestarzałym, synonimem dla for. MASM pozostawił irp dla kompatybilności ze starszymi plikami źródłowymi. Jednak zawsze powinniśmy stosować dyrektywę for. Trzecią formą makra pętli jest makro forc .Różni się od makra for w tym, że powtarza pętlę ilość razy wyszczególnioną przez długość ciągu znaków zamiast przez liczbę przedstawionych operandów. Składnia dla dyrektywy forc: forc parametr, endm Instrukcje w pętli są powtarzane raz dla każdego znaku w operandzie ciągu. Nawiasy ostre muszą pojawić się wokół ciągu. Rozważmy następującą pętlę: forc value, <012345> byte value endm Pętla ta tworzy taki sam kod jak przykład dla powyższej dyrektywy for. Irpc jest starym synonimem dla forc dostarczanym dla kompatybilności. Jednak zawsze powinniśmy używać forc w naszych kodach. 8.17 OPERACJE MAKRA WHILE Makro while pozwala nam powtarzać sekwencję kodu w naszym pliku asemblerowym nieokreśloną ilość razy .Czas asemblacji wyrażenia, które while oblicza przed emitowaniem kodu dla każdej pętli, determinuje czy ją powtarza. Składnia dla tego makra: while wyrażenie endm Makro to oblicza wyrażenie w czasie asemblacji; jeśli wartość tego wyrażenia jest zero, makro while ignoruje instrukcje aż do odpowiadającej mu dyrektywy endm. Jeśli obliczone wyrażenie jest nie zerowe (prawda) wtedy MASM assembluje instrukcje do dyrektywy endm i oblicza ponownie wyrażenie aby zobaczyć czy powinno assemblować treść pętli while ponownie. Zwykle ,dyrektywa while powtarza instrukcje pomiędzy while i endm tak długo jak obliczone wyrażenie jest prawdą. Jednakże, możemy również użyć dyrektywy exitm do wcześniejszego zakończenia rozszerzania się treści loop .Zapamiętajmy, że musimy dostarczyć jakiś warunek, który kończy pętlę ,w
przeciwnym wypadku, MASM będzie kontynuował pętlę nieskończoną i emitował kod do pliku kodu wynikowego dopóki dysk się nie zapełni (lub po prostu będzie wykonywał pętlę nieskończoną gdy pętla nie emituje żadnego kodu). 8.18 PARAMETRY MAKRA Standardowe makra MASMa są bardzo elastyczne. Jeśli liczba rzeczywistych parametrów (tych obecnych w polu operandu wywołania makra) nie odpowiada liczbie parametrów formalnych (tych pojawiających się w polu operandu definicji makra),MASM nie koniecznie będzie narzekał. jeśli jest więcej parametrów rzeczywistych niż formalnych .MASM zignoruje te parametry dodatkowe i wygeneruje ostrzeżenie. Jeśli będzie więcej parametrów formalnych niż rzeczywistych zastąpi pusty ciąg („<>”) dla dodatkowych parametrów Poprzez zastosowanie dyrektyw asemblacji warunkowej ifb i ifnb, możemy przetestować te ostatnie warunki. Podczas gdy ta technika zastępowania parametrów jest elastyczna pozostawia ona otwarte możliwości błędów. Jeśli chcemy, żeby programista dostarczył dokładnie trzy parametry ,a dostarczył mniej, MASM nie będzie generował błędu. jeśli zapomnimy przetestować obecność każdego parametru używając ifb, możemy wygenerować zły kod. Do przezwyciężenia tego ograniczenia MASM dostarczył również zdolność do wyszczególniania tych pewnych parametrów makr, które są wymagane. Możemy również przydzielić wartość domyślną do parametru jeśli jest to wymagane. W końcu MASM dostarcza również możliwości pozwalających na zmienne liczbowe argumentów makra. Jeśli wymagamy aby programista dostarczył szczególnych parametrów makra, po prostu dopisujemy „:req” po parametrze makra w definicji makra. W czasie asemblacji MASM wygeneruje błąd jeśli to szczególne makro zaginie. Needs2Param
macro param1:req, param2:req endm Needs2Param ax ;generuje błąd Needs2Param ;generuje błąd Needs2Param ax, bx ;pracuje dobrze Inną możliwością jest mieć makro dostarczające wartość domyślną do makra jeśli zaginął on z listy parametrów rzeczywistych robiąc to po prostu używamy operatora „:=text>” bezpośrednio po nazwie parametru w liście parametrów formalnych .Na przykład, funkcja BIOS int 10h dostarcza różnych usług video. Jednym z bardziej powszechnych zastosowań usług video jest funkcja ah=0eh,która wyprowadza znak z al. Na wyświetlacz. Poniższe makro pozwala wywołać tą funkcję której chcemy użyć, i domyślnie funkcje 0eh jeśli nie wyszczególnimy parametru: Video macro service := <0eh> mov ah, service int 10h endm Ostatnia cech makr MASMa wspierana jest zdolnością do przetwarzania parametrów jako zmiennych liczbowych. Robiąc to, po prostu umieszczamy operator „:vararg” po ostatnim formalnym parametrze w liście parametrów. MASM wiąże pierwsze n parametrów rzeczywistych z odpowiadającymi parametrami formalnymi pojawiającymi się przed argumentem zmiennej, tworząc potem przyrównanie tekstu wszystkich pozostałych parametrów do parametru formalnego z przyrostkiem operatora „:vararg”. Możemy zastosować makro for do wydzielenia każdego parametru z tej listy argumentów zmiennej. Na przykład poniższe makro pozwala nam zadeklarować przypadkową liczbę dwuwymiarowych tablic, wszystkie o tych samych rozmiarach. Pierwsze dwa parametry wyszczególniają liczbę wierszy i kolumn, pozostałe opcjonalne parametry wyszczególniają nazwę tych tablic: MkArrays macro Numrow:req,NumCols:req,Names:vararg for AryName,Names word NumRows dup (Numcols dup (?)) endm endm -
MkArrays
8, 12, a, b, x, y
8.19 KONTROLA LISTINGU MASM dostarcza kilku dyrektyw asemblerowych ,które są użyteczne przy kontroli asemblera. Dyrektywy te to echo ,%out, title, subttl ,page, .list, .nolist i .xlist. jest kilka innych, ale te są najważniejsze 8.19.1 DYREKTYWY ECHO I %OUT Dyrektywy echo i %out po prostu drukują cokolwiek pojawi się w ich polu operandu na wyświetlaczu podczas asemblacji. Pewne przykłady echo i %out pojawiły się w sekcji o asemblacji warunkowej i makrach. Zauważmy, że %out jest starszą postacią echo dostarczoną dla kompatybilności ze starymi kodami źródłowymi. Powinniśmy używać echo we wszystkich nowych kodach. 8.19.2 DYREKTYWA TITLE Dyrektywa asemblera title przydziela tytuł do naszego pliku źródłowego. Tylko jedna dyrektywa title może pojawić się w naszym programie. Składani tej dyrektywy to: title tekst MASM będzie drukował wyszczególniony tekst na górze każdej strony listingu asemblera 8.19.3 DYEREKTYWA SUBTTL Dyrektywa subttl jest podobna do dyrektywy title, z z wyjątkiem wielu napisów pojawiających się wewnątrz naszego pliku źródłowego. podtytuły pojawiają się bezpośrednio poniżej tytułu na górze każdej strony w listingu asemblera. składnia dla dyrektywy subttl : Subttl tekst Wyszczególniony tekst stanie się nowym podtytułem. Zauważmy ,że MASM nie wydrukuje nowego podtytułu aż do pojawienia się nowej strony. Jeśli życzymy sobie umieścić podtytuł na tej samej stronie na jakiej bezpośrednio następuje kod po dyrektywie, użyjemy dyrektywy page (omówionej poniżej) do wymuszenia pojawienia się strony 8.19.4 DYREKTYWA PAGE Dyrektywa page spełnia dwie funkcje – może wymusić pojawianie się strony w asemblowanym listingu i może ustawić szerokość i długość na urządzeniu wyjściowym .Do wymuszenia pojawienia strony używamy następującej formy dyrektywy page: Page Jeśli umieścimy znak plus ,”+”,w polu operandu, wtedy MASM wykona przerwanie, zwiększenie numeru sekcji i przestawienie numeru strony o jeden. MASM drukuje liczbę stron używając formatu Section-page Jeśli chcemy wykorzystać udogodnienie numeru sekcji, będziemy musieli ręcznie wprowadzić przerwanie strony (z operandem „+”) przed każdą nową sekcja Druga forma polecenia page pozwala nam ustawić wartość długości i szerokości wydruku strony. Przyjmuje postać: page length, width gdzie długość jest liczbą linii na stronę (domyślnie 50,ale 50-60 jest lepszym wyborem dla większości drukarek) a szerokość jest liczbą znaków na linię. Domyślna szerokość to 80 znaków .Jeśli nasza drukarka jest zdolna do drukowania 132 kolumn, powinniśmy zmienić tą wartość na 132,więc nasz listing będzie łatwiejszy do czytania. Zauważmy, że niektóre drukarki ,nawet jeśli karetka jest szeroka na 8-1/2” ,wydrukujemy przynajmniej 132 kolumny w trybie zagęszczonym. Typowo jakiś znak sterujący musi być wysłany do drukarki i umieszczony w trybie zagęszczonym. Możemy wprowadzić taki znak sterujący w komentarzu na początku naszego listingu źródłowego. 8.19.5 DYREKTYWY .LIST, .NOLIST I .XLIST Dyrektywy .list, .nolist i .xlist mogą być używane do wybranej części listy naszego pliku źródłowego podczas asemblacji.. List włącza listing, .Nolist wyłącza listing. .Xlist jest przestarzałą formą .Nolist dla starszych kodów. Poprzez dodanie szczypty tych trzech dyrektyw w całym naszym pliku źródłowym, możemy listować tylko te sekcje kodu, które nas interesują. Żadna z tych dyrektyw nie akceptuje żadnego operandu Przybierają następujące formy: .list .nolist .xlist
8.19.6 INNE DYREKTYWY LISTINGU MASM dostarcza kilku innych dyrektyw kontrolnych listingu, których ten rozdział nie omawia. Pozwalają nam kontrolować makra, segmenty asemblacji warunkowej i inne w pliku listingu. Proszę zobaczyć dodatki po więcej szczegółów o tych dyrektywach. 8.20 ZARZĄDZANIE DUŻYMI PROGRAMAMI Większość programów asemblerowych nie jest zupełnie autonomicznymi programami. Generalnie rzecz biorąc, wywołujemy różne biblioteki standardowe lub inne podprogramy które nie są zdefiniowane w naszym głównym programie. Na przykład, prawdopodobnie zauważyliśmy, że 80x86 nie dostarcza żadnych instrukcji takich jak „read”, ”write” lub „printf” dla wykonania operacji I/O .Faktycznie, jedynymi instrukcjami dla I/O, jakie zawiera 80x86 są instrukcje in i out, które są w rzeczywistości specjalnymi instrukcjami mov, i dyrektywy echo /%out, które wykonują się w czasie asemblacji, a nie jak chcemy w czasie wykonania. Czy nie ma żadnego sposobu wykonania I/O w asemblerze? Oczywiście jest. Możemy napisać procedury, które wykonują operacje I/O takie jak „read”, „write”. Niestety napisanie takich podprogramów jest zadaniem złożonym ,a początkujący programiści asemblerowi nie są przygotowani do takich zadań. Wtedy z pomocą przychodzi Standardowa Biblioteka UCR dla Programistów Języka Asemblera Dla 80x86.Są to upakowane procedury, które możemy wywoływać do wykonania prostych operacji I/O, takich jak „printf” Standardowa Biblioteka UCR zawiera tysiące linii kodu źródłowego. Wyobraźmy sobie jak trudne byłoby programowaniem gdybyśmy musieli włączyć te tysiące linii kodu do naszych prostych programów. Na szczęście nie musimy. Dla małych programów, pracujących z pojedynczym plikiem źródłowym wszystko jest dobrze. Dla dużych programów staje się to nieporęczne (rozpatrzmy powyższy przykład musząc wprowadzić cała Bibliotekę UCR do każdego naszego programu).Co więcej, kiedy zdebaggujemy i przetestujemy dużą część naszego kodu, kontynuacja asemblacji tego samego kodu, kiedy zrobimy małą zmianę jakiejś innej części naszego kodu jest stratą czasu. Na przykład Standardowej Bibliotece UCR asemblacja zajmuje kilka minut, nawet na szybkich maszynach. Wyobraźmy sobie ,że musimy czekać pięć lub dziesięć minut na szybkim Pentium na asemblację programu, w którym zmieniliśmy jedną linijkę! Podobnie jak w HLL’ach, rozwiązaniem jest oddzielna kompilacja (lub oddzielna asemblacja w przypadku MASMa ).Po pierwsze, rozkładamy nasze duże pliki źródłowe na wykonywalne kawałki. Potem assemblujemy oddzielne pliki do modułów kodu wynikowego. W końcu, linkujemy moduły wynikowe razem do postaci kompletnego programu. Jeśli musimy dokonać małej zmiany w jednym z modułów, musimy tylko zreassemblować jeden z modułów, nie musimy reasemblować całego programu. Standardowa Biblioteka UCR pracuje w ten sposób dokładniej. Biblioteka Standardowa jest już zasemblowana i gotowa do użycia. po prostu wywołujemy podprogram z Biblioteki Standardowej i linkujemy nasz kod z Biblioteką Standardową używając programu linkera. Oszczędza to ogromną ilość czasu kiedy wykorzystujemy program używający kodu Biblioteki Standardowej. Oczywiście ,możemy łatwo stworzyć własny moduły wynikowe i zlinkować je razem z naszym kodem. Możemy nawet dodawać nowe podprogramy do biblioteki Standardowej aby były dostępne do zastosowania w przyszłych programach, które napiszemy w przyszłości. „Programming in the large” jest terminem inżynierów oprogramowania ukutym na potrzeby opisania procesów, metodologii i narzędzi przy rozwoju dużych projektów programistycznych. W zasadzie każdy ma swój własny pomysł co to jest „duży”, oddzielna kompilacja, i jakaś konwencja stosowania oddzielnej kompilacji, są jednym z większych technik „programming in large”. Poniższa sekcja opisuje narzędzia MASMa dla oddzielnej kompilacji i jak wydajniej stosować te narzędzia w naszych programach. 8.20.1 DYREKTYWA INCLUDE Dyrektywa include. kiedy znajduje się w pliku źródłowym, przełącza program z pliku bieżącego na plik wyszczególniony w liście parametrów include. Pozwala to nam na zbudowanie pliku tekstowego zawierającego identyfikatory ,makra, kod źródłowy i inne pozycje asemblerowe, i plik nagłówkowy do asemblacji kilku oddzielnych programów. Składnia dyrektywy include include nazwa pliku Nazwa pliku musi być poprawną nazwą DOS’a. MASM łączy wyszczególniony plik do asemblacji w punkcie dyrektywy include .Zauważmy, że możemy zagnieżdżać instrukcje include wewnątrz plików,. To znaczy, plik zawarty wewnątrz innego pliku podczas asemblacji może wywołać trzeci plik. Stosowanie dyrektywy include prze nią samą nie dostarcza oddzielnej kompilacji. Możemy użyć dyrektywy include do podzielenia dużego pliku na oddzielne moduły i łączyć te moduły razem, kiedy assemblujemy nasz plik. Poniższy przykład łączy pliki PRINTF.ASM i PUTC.ASM podczas asemblacji naszego programu: include printf.asm include putc.asm
end Teraz nasz program będzie przynosił korzyść z modularności. Niestety ,nie oszczędzimy na czasie konstruowania programu. Dyrektywa include wkłada plik źródłowy w punkcie include podczas asemblacji, dokładnie jak gdybyśmy napisali ten kod sami. MASM musi jeszcze zasemblować ten kod a to zabiera czas. Gdybyśmy włożyli wszystkie pliki podprogramów Biblioteki Standardowej ,nasza asemblacja trwałaby wiecznie. Generalnie, nie powinniśmy używać dyrektywy include do włączania pliku źródłowego jak pokazano powyżej. zamiast tego powinniśmy używać dyrektywy include do wprowadzania zbioru stałych, makr, deklaracji zewnętrznych procedur, i innych takich pozycji do programu. Typowo, asembler zawiera pliki, nie zawierające żadnego kodu maszynowego (na zewnątrz makr).Cel stosowania plików include w ten sposób, stanie się jaśniejszy po zobaczeniu jak pracują deklaracje public i external. 8.20.2 DYREKTYWY PUBLIC,EXTERN I EXTRN Technicznie, dyrektywa include dostarcza nam wszystkich udogodnień potrzebnych do stworzenia programów modularnych .Możemy zgromadzić bibliotekę modułów, każda zawierająca jakiś specyficzny podprogram i zawrzeć niezbędne moduły do programu asemblera używając odpowiedniej komendy include. MASM (i towarzyszący mu program LINK) dostarczają lepszego sposobu: symboli external i public. Jeden ważny problem z mechanizmem include jest to ,że zdebuggowanie podprogramu, wprowadzonego do asemblacji jest marnotrawstwem czasu ponieważ MASM musi zreassemblować wolny od błędów kod za każdym razem ,kiedy asembujemy główny program. Dużo lepszym rozwiązaniem będzie wcześniejsza asemblacja zdebuggowanego modułu i zlinkowanie razem z modułem wynikowym zamiast reasemblacja całego programu za każdym razem ,kiedy zmieniamy pojedynczy moduł. To jest to czego dostarczają dyrektywy public i extern. Extrn jest starszą dyrektywa, która jest synonimem extern. Dołączona jest dla kompatybilności ze starszymi plikami źródłowymi.. powinniśmy zawsze stosować dyrektywę extern w nowym kodzie źródłowym. Aby skorzystać z public i extern musimy stworzyć przynajmniej dwa pliki źródłowe .Jeden plik zawiera zbiór zmiennych i procedur używanych przez drugi. Drugi plik używa tych zmiennych i procedur bez wiedzy jak są one implementowane. Dla zademonstrowania rozważymy następujące dwa moduły: ;Module #1: DSEG Var1 Var2 DSEG CSEG Proc1
Proc1 CSEG
public segment word word ends
Var1,Var2,Proc1 para public ‘data’ ? ?
segment assume proc mov add mov ret endp ends End
para public ‘code’ cs:cseg, ds.:dseg near ax, Var1 ax, var2 Var1, ax
extern segment mov mov call ends
Var1:word,Var2:word, Proc1:near para public ‘code’
;Module #2: CSEG
CSEG
Var1, 2 Var2, 3 Proc1
end Module #2 odnosi się do Var1,Var2 i Proc1,ale mimo to symbole te są zewnętrzne dla modułu #2. Dlatego też musimy zadeklarować je dyrektywa extern. Dyrektywa ta przyjmuje postać: Extern nazwa: typ {nazwa:typ...} Nazwa jest nazwą symbolu zewnętrznego, a typ jest typem tego symbolu. Typ może być near, far, proc, byte, word, dword, qword ,tbyte, abs (wartość bezwzględna, która jest stałą),lub innym zdefiniowanym typem przez użytkownika. Bieżący moduł używa tego typu deklaracji. Ani MASM ani linker nie sprawdzają deklaracji typu w stosunku do modułu definiującego nazwę aby zobaczyć czy typ jest zgodny. Dlatego też musimy korzystać ostrożnie kiedy definiujemy symbole zewnętrzne. Dyrektywa public pozwala nam eksportować wartość symbolu do modułów zewnętrznych. Deklaracja przybiera postać:
Rysunek 8.8: Użycie pojedynczego pliku Include do implementacji i stosowania modułów Public name {name...} Każdy symbol pojawiający się w polu operandu instrukcji public jest dostępny jako symbol zewnętrzny do innego modułu. Podobnie, wszystkie zewnętrzne symbole wewnątrz modułu muszą pojawić się wewnątrz instrukcji public w jakimś innym module. Kiedy stworzymy moduł źródłowy ,powinniśmy najpierw zasemblować plik zawierający deklaracje public. Z MASMem 6.x będziemy stosować komendę taka jak ML/c.pubs.asm Opcja „/c” mówi MASMowi aby wykonał asemblację „tylko kompilacja” To znaczy ,nie będzie próbował linkować kodu po skutecznej asemblacji. Tworzy moduł wynikowy „pubs.obj” Następnie asembujemy plik zawierający definicje zewnętrzne i linkujemy kod stosując komendę MASMa: ML exts.asm pubs.obj Zakładając, że nie ma żadnych błędów, stworzymy plik „exts.exe”, który jest zlinkowaną i wykonywalną postacią programu. Zauważmy, ze dyrektywa extern definiuje symbol w naszym pliku źródłowym. Każda próba przedefiniowania symbolu gdzieś w programie stworzy błąd „symbolu powtórzonego” To, okazuje się, jest źródłem problemów, które Microsoft rozwiązuje dyrektywa externdef 8.20.3 DYREKTYWA EXTERNDEF Dyrektywa externdef jest kombinacją public i extern połączonych w jedno. Używa tej samej składni jak dyrektywa extern to znaczy umieszczamy całą listę nazwa: typ w polu operandu. Jeśli MASM nie napotka innej definicji symbolu w bieżącym pliku źródłowym, externdef będzie się zachowywać dokładnie jak instrukcja extern. Jeśli symbol nie pojawi się w pliku źródłowym, wtedy externdef zachowuje się jak komenda public. Z dyrektywą externdef rzeczywiście nie musimy stosować instrukcji public lub extern, chyba, że czujemy się jakoś zmuszeni do zrobienia tego. Wielką korzyścią dyrektywy externdef jest to ,że pozwala nam minimalizować powielanie wysiłku w naszym pliku źródłowym .Przypuśćmy na przykład, że chcemy stworzyć moduł z grupą wspomagających podprogramów dla innych programów. Oprócz współdzielenia jakichś podprogramów i jakichś zmiennych, przypuśćmy chcemy również dzielić stałe i makra.. Mechanizm pliku include dostarcza doskonałego sposobu do uczynienia tego. po prostu tworzymy plik include zawierający stałe, makra i definicje externdef i zawieramy ten plik w module, który implementuje nasze podprogramy i w module, który używa tych podprogramów (zobacz rysunek 8.8)
Zauważmy, że extern i public nie działają w tym przypadku ponieważ implementacja modułu potrzebuje dyrektywy public a zastosowanie modułu potrzebuje dyrektywy extern. będziemy musieli stworzyć dwa oddzielne pliki nagłówkowe. Utrzymywanie dwóch oddzielnych plików nagłówkowych, które zawierają przeważnie identyczne definicje nie jest dobrym pomysłem .Rozwiązania dostarcza dyrektywa externdef. `Wewnątrz naszych plików nagłówkowych powinniśmy stworzyć segment definicji, które odpowiadają tym zawartym w modułach. Trzeba by włożyć dyrektywę externdef do wewnątrz tego samego segmentu w którym symbol jest w rzeczywistości zdefiniowany. Wiążemy wartość segmentu z symbolem, żeby MASM mógł właściwie zrobić stosowna optymalizację i inne obliczenia w oparciu o pełen adres symbolu: ;Plik ”HEADER.A”: cseg
segment para public ‘code’ externdef Routine1:near, Routine2:far cseg ends dseg segment para public ‘data’ externdef i:word, b:byte, flag:byte dseg ends Tekst ten adoptuje konwencję z Biblioteki Standardowej UCR stosowania przyrostka „.a” dla plików nagłówkowych asemblera innym popularnymi przyrostkami są „.inc” i „.def” 8.21 PLIKI MAKE Chociaż zastosowanie oddzielnej kompilacji redukuje czas asemblacji i promuje kod wielokrotnego stosowania i modularność, nie jest bez wad. Przypuśćmy, że mamy program, który składa się z dwóch modułów: pgma.asm i pgmb.asm. Przypuśćmy również, że mamy już zasemblowane oba moduły, więc istnieją pliki pgma.obj i pgmb.obj.W końcu robimy zmianę w pgma.asm i pgmb.asm i assemblujemy pgma.asm ale zapominamy zasemblować plik pgmb.asm. Dlatego też plik pgmb.obj będzie nieaktualny ponieważ ten plik obiektowy nie odzwierciedla zmian dokonanych w pliku pgmb.asm. Jeśli zlinkujemy razem moduły programu, wynikowy plik .exe będzie zawierał zmiany pliku pgma.asm, nie będzie mógł uaktualnić kodu obiektowego powiązanego z pgmb.asm. Jeśli projekt staje się większy ,zawiera więcej modułów z nim związanych, i więcej programistów zaczyna pracę nad projektem, staje się bardzo trudne śledzenie które moduły obiektowe są aktualne. Ta złożoność normalnie powoduje reasemblację (lub rekompilację) wszystkich modułów w projekcie, nawet jeśli wiele z plików .obj jest aktualnych, po prostu może wydać się zbyt trudne do śledzenia ,które moduły są aktualne a które nie .Robiąc to wyeliminujemy wiele korzyści jakie oferuje oddzielna kompilacja. Na szczęście, jest narzędzie, które może pomóc nam zarządzać dużym projektem – nmake. Program nmake, z małą naszą pomocą, może wyliczyć, które pliki muszą być reasemblowane a które pliki maja aktualne pliki .obj .Z właściwie zdefiniowanym plikiem make, możemy łatwo assemblować tylko te moduły, które absolutnie muszą być zasemblowane dla wygenerowania spójnego programu. Plik make jest plikiem teekstowym, który wymienia zależności czasu asemblacji pomiędzy plikami. Plik .exe, na przykład, jest zależny od kodu źródłowego którego asemblacja tworzy plik wykonywalny. Jeśli uczynimy zmianę w kodzie źródłowym,(prawdopodobnie) będziemy musieli reasemblować lub rekompilować kod źródłowy tworząc nowy plik .exe. Typowe zależności obejmują: • Plik wykonywalny (.exe) generalnie zależy tylko od zbioru plików obiektowych (.obj),które linker łączy do postaci wykonywalnej • Dany plik kodu wynikowego (.obj) zależy od pliku źródłowego asemblera, który zasemblowany dał plik obiektowy. Obejmuje to plik źródłowy asemblera (.asm) i inne pliki obejmowane podczas tej asemblacji (generalnie pliki .a) • Pliki źródłowe i pliki include nie zależą od niczego Plik make ogólnie składa się z instrukcji zależności określonych przez zbiór komend operujących tymi zależnościami. instrukcja zależności przybiera następującą formę: dependent-file : lista plików Przykład: Pgm.exe: pgma.obj pgmb.obj Ta instrukcja mówi,że”pgm.exe” jest zależny od pgma.obj i pgmb.obj. Każda zmiana która wystąpi w pgma.obj lub pgmb.obj będzie wymagała wygenerowania nowego pliku pgm.exe. Program nmake.exe stosuje oznaczenia czas /data do określenia czy plik zależny jest nieaktualny w stosunku do pliku od którego zależy. W każdej chwili możemy dokonać zmiany pliku. MS-DOS i Windows uaktualnią zmodyfikowany czas i datę powiązanych z plikiem. program nmake.exe porównuje zmodyfikowane
oznaczenie czas/ data pliku zależnego z modyfikacją oznaczenia czas /data pliku od którego zależy. Jeśli modyfikacja czas/ data pliku zależnego jest wcześniejsza niż jednego lub więcej plików od których zależy, lub jeden plik od których zależy nie jest obecny, wtedy nmake.exe zakłada, że jakaś operacja musi być konieczna dla uaktualnienia pliku zależnego Kiedy uaktualnienie jest konieczne,nmake.exe wykonuje zbiór poleceń (MS-DOS) instrukcji zależności .Przypuszczalnie te polecenia będą robiły co konieczne do wytworzenia pliku uaktualnionego. Instrukcje zależności muszą zaczynać się w kolumnie jeden. Każde polecenie, które musi my wykonać dla rozwiązania zależności musi zaczynać się od linii bezpośrednio następującej po instrukcji zależności a każde polecenie musi zaczynać się od akapitu jednym tabem. Instrukcja Pgm.exe wyglądałaby prawdopodobnie następująco: Pgm.exe:pgma.obj pgmb.obj ml /Fepgm.exe pgma.obj pgmb.obj (opcaj /Fepgm.exe podaje MASMowi nazwę pliku wykonywalnego „pgm.exe”) Jeśli musimy wykonać więcej niż jedno polecenie rozwiązujące zależności, możemy umieścić kilka poleceń po instrukcji zależności we właściwym porządku. Zauważmy, że musimy zacząć jednym tabem od akapitu wszystkie polecenia.Nmake.exe ignoruje każdą pustą linię w pliku make .Dlatego też, możemy dodać puste linie czyniąc plik łatwiejszym do czytania i zrozumienia Może być więcej niż jedna instrukcja zależności w pliku make. W powyższym przykładzie na przykład,pgm.exe zależy od plików pgma.obj i pgmb.obj. Oczywiście pliki .obj zależą od plików źródłowych które je generują. Dlatego też przed próbą rozwiązania zależności dla pgm.exe,nmake.exe najpierw zweryfikuje resztę plików make aby zobaczyć czy pgma.obj i pgmb.obj zależą od jakiegoś. Jeśli tak,nmake.exe rozwiąże te zależności najpierw. Rozpatrzmy poniższy plik make: pgm.exe: pgma.obj pgmb,obj ml /Fepgm.exe pgma.obj pgmb.obj pgma.obj:pgma.asm ml /c pgma.asm pgmb.obj”pgmb.asm ml /c pgmb.asm Program nmake.exe najpierw przetworzy zależności liniowe, które znajduje w pliku. Jednak, pliki pgm.exe zależą od nich samych maja zależności liniowe. Dlatego też,nmake.exe najpierw zapewnia, że pgma.obj i pgmb.obj są aktualne, przed próba wykonania MASM zlinkuje te pliki razem. Dlatego ,jeśli jedyną zmianę uczyniono w pgmb.asm,nmake.exe podejmie następujące kroki (zakładając, że pgma.obj istnieje i jest aktualny). 1. Nmake.exe przetwarza najpierw instrukcje zależności. Zauważa, że linie zależności istnieją dla pgm.exe (pliki od których zależy pgm.exe)Więc przetwarza najpierw te instrukcje. 2. Nmake.exe przetwarza linię zależności pgma.obj. Zauważa że plik pgma.obj jest nowszy niż plik pgma.asm, więc nie wykonuje następnych poleceń tej instrukcji zależności 3. Nmake przetwarza linię zależności pgmb.obj. Zauważa ,że pgmb.obj jest starszy niż pgma.asm (ponieważ właśnie zmieniliśmy plik źródłowy pgmb.asm)Dlatego też,nmake.exe wykonuje następne polecenie DOS w następnej linii. Generuje to nowy plik pgmb.obj, który jest teraz aktualny. 4. Mając przetworzone zależności pgma.obj i pgmb.obj,nmake.exe teraz zwraca swoją uwagę do pierwszej linii zależności. Ponieważ nmake.exe stworzył nowy plik pgmb.obj, jego oznaczenie czas/ data będzie nowsze niż pgm.exe. Dlatego nmake.exe wykona polecenie ml, które linkuje pgma.obj i pgmb.obj razem do postaci nowego pliku pgm.exe Zauważmy ,że właściwie napisany plik make poinformuje nmake.exe do asemblacji tylko tych modułów absolutnie koniecznych do wytworzenia spójnego pliku wykonywalnego. W powyższym przykładzie,nmake.exe nie przeszkadza zasemblować pgma.asm ponieważ jego plik wynikowy już był aktualny. Jest jedna końcowa rzecz godna podkreślenia w związku z zależnościami. Często, pliki wynikowe są zależne nie tylko od pliku źródłowego ,który tworzy pliki wynikowe ,ale każdego pliku, który również zawiera plik źródłowy. W poprzednim przykładzie (najwyraźniej) nie było takich plików include .Nie ma często takich przypadków. Bardziej typowy plik make może wyglądać jak poniższy: pgm.exe: pgma.obj pgmb.obj ml /Fepgm.exe pgma.obj pgmb.obj pgma.obj: pgma.asm pgm.a ml /c pgma.asm pgmb.obj: pgmb.asm pgm.a ml /c pgmb.asm
Zauważ, że każda zmiana pliku pgm.a wymusza na nmake.exe reasemblację obu pgma.asm i pgmb.asm ponieważ pliki pgma.obj i pgmb.obj oba zależą od pliku dołączanego pgm.a. Pozostawienie pliku dołączanego z listą zależności jest powszechnym błędem programistów, co może tworzyć wewnętrznie sprzeczne pliki .exe. Zauważmy ,że zwykle nie musimy wyszczególniać plików dołączanych Biblioteki Standardowej UCR ani plików Biblioteki Standardowej .lib w liście zależności. Prawda, nasz wynikowy plik .exe nie zależy od tego kodu, ale Biblioteka Standardowa rzadko się zmienia więc bezpiecznie możemy ją wyrzucić z naszej listy zależności. Czynimy modyfikację Biblioteki standardowej po prostu usuwając każdy stary plik .exe i .obj i wymuszając reasemblację całego systemu. Nmake.exe, domyślnie, zakłada, że będzie przetwarzany plik make nazwany „make-file”. Kiedy uruchamiamy nmake.exe, szuka on „makefile” w bieżącym folderze. Jeśli go nie znajdzie, kończy .Dlatego, jest dobrym pomysłem kolekcjonowanie plików dla każdego projektu nad którym pracujemy w jego własnym podkatalogu i nadanie każdemu projektowi własny makefile. Potem tworząc plik wykonywalny, potrzebujemy tylko zamienić stosowny podkatalog i uruchomić program nmake.exe. Chociaż ta sekcja omawia program nmake w wystarczających szczegółach do operowania większością projektów nad którymi pracujemy, pamietajmy, że nmake.exe dostarcza znacznej funkcjonalności ,której ten rozdział nie omawia .Dla nauczenia się więcej o programie nmake zajrzyj do dokumentacji, której dostarcza MASM. 8.25 PODSUMOWANIE Rozdział ten wprowadził kilka dyrektyw asemblerowych i pseudo-opcodów wspieranych przez MASM .Rozdział ten, w żaden sposób nie jest kompletnym opisem tego co MASM może zaoferować. Instrukcje języka asemblera są swobodnego formatu i jest zwykle jedna instrukcja na linie w pliku źródłowym Chociaż MASM pozwala na wprowadzenie swobodnego formatu powinniśmy ostrożnie konstruować nasz plik źródłowy czyniąc go łatwiejszym do odczytu. *Zobacz „Instrukcje Języka Asemblera” MASM śledzi offset instrukcji lub zmiennej w segmencie używając licznika lokacji. MASM zwiększa licznik lokacji o jeden dla każdego bajtu kodu wynikowego napisanego dla pliku wyjściowego. *Zobacz „Licznik Lokacji” Podobnie jak HLL’e ,MASM pozwala nam stosować nazwy symboliczne dla zmiennych i etykiet instrukcji. Działać z symbolami jest dużo prościej niż offsetami liczbowymi w programie asemblerowym. Symbole MASMa wyglądają wszystkie jak w HLL’ach z wyjątkiem kilku rozszerzeń. *Zobacz „Symbole” MASM dostarcza kilku różnych typów stałych literowych wliczając w to binarne, dziesiętne i heksadecymalne stałe całkowite ,stałe łańcuchowe i stałe tekstowe *Zobacz „Stałe Literowe” *Zobacz ”Stałe Całkowite” *Zobacz „Stałe Łańcuchowe” *Zobacz „Stałe Tekstowe” Do pomocy przy manipulowaniu segmentami wewnątrz naszego programu MASM dostarcza dyrektyw segment /ends. Z dyrektywą segment możemy sterować porządkiem ładowania i ustawieniem modułów w pamięci. *Zobacz „Segmenty” *Zobacz „Nazwy Segmentów” *Zobacz „Porządek Ładowania Segmentów” *Zobacz „Operandy Segmentów” *Zobacz „Typ CLASS” *Zobacz „Definicje typowych segmentów” *Zobacz „Dlaczego chcemy sterować porządkiem ładowania” MASM dostarcza dyrektyw proc /endp dla deklarowania procedur wewnątrz naszych asemblerowych programów Chociaż ,nie ściśle koniecznie, dyrektywy proc/ endp czynią nasz program dużo łatwiejszym do odczytu i pielęgnacji. Dyrektywy proc /endp również pozwalają nam stosować nazwy i lokalnych instrukcji wewnątrz naszych procedur *Zobacz „Procedury” Dyrektywy równości pozwalają nam zdefiniować stałe symboliczne różnego rodzaju w naszym programie .MASM dostarcza trzech dyrektyw dla definiowania takich stałych: „=”,equ i textequ. Podobnie jak przy HLL’ach, rozsądne stosowanie dyrektyw równań może pomóc uczynić nasz program dużo łatwiejszym do czytania *Zobacz „Deklarowanie Jawnych Stałych Używając dyrektyw równań” Jak widzieliśmy w Rozdziale Czwartym .MASM daje nam zdolność deklarowania zmiennych w segmencie danych stosując byte, word, dword i inne dyrektywy. MASM jest asemblerem ze ścisłą kontrolą typów i
przyłącza również typ jako lokację do nazwy zmiennej (większość asemblerów dołącza tylko lokację) To pomaga MASMowi lokalizować niejasne błędy w naszych programach *Zobacz „Zmienne” *Zobacz „Typ Label” *Zobacz „Jak nadać symbol poszczególnym typom” *Zobacz „Wartości etykiet” *Zobacz „konflikty typów” MASM wspiera wyrażenia adresowe, które pozwalają nam stosować operatory arytmetyczne do budowania stałych wartości adresów w czasie asemblacji. Pozwala to również nam przesłonić typ wartości adresu i wyodrębnić róże kawałki informacji o symbolu. *Zobacz „Wyrażenia adresowe” *Zobacz „Typy symboli i tryby adresowania” *Zobacz „Operatory Arytmetyczne i Logiczne” *Zobacz „Koercja” *Zobacz :Operatory typu’ *Zobacz „Operatory Pierwszeństwa” MASM dostarcza kilka udogodnień dla powiadomienia asemblera który segment powiązać z którym rejestrem segmentowym. Daje to nam również zdolność do uchylenia domyślnego wyboru .Pozwala to naszemu programowi zarządzać kilkoma segmentami od razu z minimalnym zamieszaniem *zobacz „Przedrostki Segmentu” *Zobacz „Sterowanie Segmentami dyrektywa Assume” MASM dostarcza nam z „asemblacją warunkową” zdolność, która pozwala nam wybierać które segmenty kodu są w rzeczywistości asemblowane podczas procesu asemblacji. Jest to użyteczne przy wprowadzaniu kodu debuggującego do naszego programu (który możemy łatwo usunąć pojedynczą instrukcją) i dla pisania programów, które muszą działać w różnych środowiskach (poprzez wprowadzanie i usuwanie różnych sekwencji kodu) *Zobacz „Asemblacja warunkowa” *Zobacz „Dyrektywa IF” *Zobacz „Dyrektywa IFE’ *Zobacz „IFDEF i IFNDEF” *Zobacz IFB,IFNB” *Zobacz „IFIDN,IFDIF,IFIDNI i IFDIFI’ MASM silnych udogodnień w postaci makr. Makra są częściami kodu, który możemy replikować poprzez proste umieszczanie nazw makr w naszym kodzie. Makro ,zastosowane właściwie, może pomóc napisać krótszy, łatwiejszy do odczytu i bardziej silniejsze programy. Niestety ,niewłaściwie zastosowane, makra tworzą trudne do pielęgnacji, niewydajne programy. *Zobacz „Makra” *Zobacz „Makra proceduralne” *Zobacz „Dyrektywa LOCAL” *Zobacz „Dyrektywa EXITM” *Zobacz „Makra: Dobre i Złe wieści” *Zobacz „Operacje repeat” MASM dostarcza kilku dyrektyw, które możemy zastosować do tworzenia „listingu zassemblowanego” lub wydruków z naszego programu z dużą ilością generowanych (użytecznych) informacji asemblera. Dyrektywy te pozwalają nam włączać lub wyłączać operacje listingu ,wyświetlają informacje na wyświetlaczu podczas asemblacji ustawiają tytuły na wydrukach wyjściowych *Zobacz „Sterowanie listingiem” *Zobacz „Dyrektywy ECHO i %OUT” *Zobacz „Dyrektywa TITLE” *Zobacz „:Dyrektywa SUBTTL” *Zobacz „Dyrektywa PAGE” *Zobacz „Dyrektywy .LIST, .NOLIST i .XLIST” *Zobacz „ Inne Dyrektywy Listingu” Manipulowanie dużymi projektami („Programming in the Large”) wymaga oddzielnej kompilacji (lub oddzielnej asemblacji w przypadku MASMa). MASM dostarcza kilku dyrektyw które pozwalają nam łączyć pliki źródłowe podczas asemblacji, oddzielnie assemblować moduły i przekazywać procedury i nazwy zmiennych pomiędzy modułami. *Zobacz „Zarządzanie Dużymi Programami” *Zobacz „Dyrektywa INCLUDE” *Zobacz „dyrektywy PUBLIC,EXTERN i EXTRN”
*Zobacz „Dyrektywa EXTERNDEF” 8.26 PYTANIA 1) Jaka jest różnica między następującymi sekwencjami instrukcji? MOV AX, VAR+1 a MOV AX, VAR INC AX 2) Jaka jest format linii źródłowej dla instrukcji asemblera 3) Jakie jest zadanie dyrektywy ASSUME? 4) Co to jest licznik lokacji? 5) Który z poniższych symboli są poprawne? a) ThisIsASymbol b) This_Is_A_Symbol c) This.Is.A.Symbol d) .Is_This_A_symbol? e) ----------------------f) @_$?_To_You g) 1WayToGo h) %Hello i) F000h j) ?A_0$1 k) $1234 l) Hello there 6) Jak wyszczególniamy segment w porządku ładowania? 7) Jaki jest typ symboli zadeklarowanych poniżej instrukcji? a) symbol1 equ 0 b) symbol2: c) symbol3 proc d) symbol4 db ? e) symbol5 dw ? f) symbol6 proc far g) symbol7 equ this word h) symbol8 equ byte ptr symbol7 i) symbol9 dd ? j) symbol10 macro k) symbol11 segment para public ‘data’ l) symbol12 equ this near m) symbol13 equ ‘ABCD’ n) symbol14 equ 8) Którym z symboli z pytania 7 nie są przypisane wartości z bieżącego licznika lokacji? 9) Wyjaśnij zadania poniższych operatorów; a) PTR b) SHORT c)THIS d) HIGH e)LOW f) SEG g) OFFSET 10) Jak jest różnicą między makrem REPEAT a operatorem DUP? 11) Jak jest różnica między wartościami ładowanymi do rejestru BX w poniższej sekwencji kodu? mov bx, offset Table lea bx, Table 12) W jakiej kolejności będą ładowane poniższe segmenty do pamięci? CSEG segment para public ‘CODE’ CSEG ends DSEG segment para public ‘DATA’ DSEG ends ESEG segment para public ‘CODE’ ESEG ends 13) Które z następujących wyrażeń adresowych nie tworzy takiego samego wyniku jak inne: a) Var1[3][5] b) 15 [Var1] c) Var1[8] d) Var1+2[6] e) Var1*3*5 f) Var1 +3+5
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA AG HTTP://WWW.ASMPAGE.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK konsultacja naukowa: NEKRO [email protected]
[email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ DZIEWIĄTY: OPERACJE ARYTMETYCZNE I LOGICZNE Dużo więcej jest potrzebne do posługiwania się asemblerem niż znajomość mnogich operacji. Musimy nauczyć się jak je stosować i co one robią, Wiele instrukcji jest użytecznych dla operacji, które mają mało pracy z ich matematycznymi lub oczywistymi funkcjami .Rozdział ten omawia jak skonwertować wyrażenia z języka wysokiego poziomu do asemblera, Omawia również zaawansowane operacje arytmetyczne i logiczne wliczając w to wielokrotnej precyzji i sztuczki ,którymi możemy zabawić się z różnymi instrukcjami. 9.0 WSTĘP Rozdział ten omawia sześć głównych tematów: konwertowanie wyrażeń arytmetycznych HLL’a na język asemblera, wyrażenia logiczne, arytmetyczne i logiczne operacje o podwyższonej precyzji, działanie na operandach o różnych rozmiarach, styl maszynowy i arytmetyczny i operacje maskowania. Podobnie jak poprzednie rozdziały, rozdział ten zawiera obszerny materiał, którego musimy się nauczyć natychmiast jeśli jesteśmy początkującymi programistami asemblerowymi. Poniższe części, które mają przedrostek „•” są niezbędne. te części z „⊗” omawiają zaawansowane tematy, które możemy odłożyć na jakiś czas. • Wyrażenia arytmetyczne • Proste przypisania • Proste wyrażenia • Wyrażenia złożone • Operatory przemienności • Wyrażenia logiczne • Operacje wielokrotnej precyzji • Operacje dodawania o wielokrotnej precyzji • Operacje odejmowania o wielokrotnej precyzji • Porównania o podwyższonej precyzji ⊗ Mnożenie o podwyższonej precyzji ⊗ Dzielenie o podwyższonej precyzji ⊗ Negacja o rozszerzonej precyzji • AND, OR, XOR i NOT o rozszerzonej precyzji ⊗ Operacje przesunięcia i obrotu o podwyższonej precyzji ⊗ Działanie na operandach o różnych rozmiarach • Mnożenie bez MUL i IMUL ⊗ Dzielenie bez DIV i IDIV ⊗ Zastosowanie AND do obliczania reszt ⊗ Licznik Modulo – n z AND ⊗ Testowanie dla 0FFFF...FFFh • Operacje testowe ⊗ Znaki testujące z instrukcją XOR ⊗ Operacje maskowania ⊗ Maskowanie z instrukcją AND
⊗ ⊗ ⊗
Maskowanie z instrukcją OR Pakowanie i rozpakowywanie typów danych Tablice połączeń
Żaden z tych materiałów nie jest szczególnie trudny do zrozumienia. Jednak, jest tu wiele nowych tematów a zabranie się za nie po kilka na raz pozwoli nam lepiej wchłonąć materiał. Te tematy z przedrostkiem „•” są jedynymi, stosowanymi przez nas często; Stąd jest dobrym pomysłem zacząć studiować je jako pierwsze. 9.1 WYRAŻENIA ARYTMETYCZNE Prawdopodobnie największym szokiem dla początkujących odkrywających asembler po raz pierwszy jest brak dobrze znanych wyrażeń arytmetycznych. Wyrażenia arytmetyczne, w większości języków wysokiego poziomu ,wygląda podobnie do tego algebraicznego równania: X := Y*Z; W asemblerze, potrzebujemy kilku instrukcji do wykonania tego samego zadania np. mov ax, y imul z mov x, ax Oczywiście wersja dla HLLi jest dużo łatwiejsza do napisania, czytania i zrozumienia. Punkt ten, bardziej niż inne, jest odpowiedzialny za odstraszanie ludzi od asemblera. Chociaż jest wiele zawiłości, konwersja wyrażeń arytmetycznych do języka asemblera nie jest wcale trudne. Przez zaatakowanie problemu krok po kroku, chociaż może rozwiązalibyśmy problem ręcznie, możemy łatwo podzielić każde arytmetyczne wyrażenie na odpowiednią sekwencję instrukcji asemblerowych. Ucząc się jak skonwertować takie wyrażenia na asembler w trzech krokach ,odkryjemy, że jest to trochę trudniejsze zadanie. 9.1.1 PROSTE PRZYPISANIA Najłatwiejszymi wyrażeniami do konwersji na język asemblera są proste przypisania. Proste przypisania kopiują pojedynczą wartość do zmiennej i przybierają jedną z dwóch form: zmienna := stała lub zmienna := zmienna Jeśli zmienna pojawia się w bieżącym segmencie danych (np. DSEG), konwersja pierwszej postaci na język asemblera jest łatwe, po prostu używamy instrukcji asemblera: mov zmienna , stała Ta instrukcja move bezpośrednio kopiuje stałą do zmiennej. Drugie powyższe przypisanie jest nieco bardziej skomplikowane ponieważ 80x86 nie dostarcza instrukcji mov pamięć do pamięci. Dlatego też, aby skopiować jedną zmienną pamięci do innej, musimy przesunąć dane przez rejestr. Jeśli spojrzymy na kodowanie dla instrukcji mov w dodatku, zauważymy ,że instrukcje mov ax, pamięć i mov pamięć, ax są krótsze niż przesuwanie wymagającego innych rejestrów. Na przykład, var1 := var2 staje się mov ax, var2 mov var1, ax Oczywiście, jeśli zastosujemy rejestry ax do czegoś innego, wystarczy jeden z innych rejestrów. Mimo to musimy zastosować rejestr do przeniesienia jednej komórki pamięci do innej. Omówienie to oczywiście zakłada, że obie zmienne są w pamięci. Jeśli to możliwe, powinniśmy spróbować stosować rejestr do przechowywania wartości zmiennej. 9.1.2 PROSTE WYRAŻENIA Wyższym stopniem złożoności od prostego przypisania jest proste wyrażenie. Proste wyrażenie przybiera formę: var := term1 op term2; Var jest zmienną,term1 i term2 są zmiennymi lub stałymi a op jest jakimś arytmetycznym operatorem (dodawania odejmowania, mnożenia itp.) Większość wyrażeń przyjmuje taką formę. Nie powinno być to dla nas niespodzianka, że architektura 80x86 została zoptymalizowana właśnie dla takiego typu wyrażeń. Typowa konwersja dla tego typu wyrażenia przyjmuje postać: mov ax, term1
op ax, term2 mov var, ax Op jest mnemonikiem, który odpowiada wyszczególnionej operacji (np. „+” = add, „-„ = sub, itp.) Jest kilka niekonsekwencji, których musimy być świadomi. Po pierwsze, instrukcje {i}mul 80x86 nie pozwalają na operandy bezpośrednie na procesorach wcześniejszych niż 80286.Dalej,żaden procesor nie pozwala na bezpośredni operand z {i}div. Dlatego też, jeśli operacją jest mnożenie lub dzielenie a jeden z warunków jest wartością stałą, będziemy musieli załadować tą stałą do rejestru lub komórki pamięci a potem pomnożyć lub podzielić ax przez tą wartość. Oczywiście, kiedy zajmujemy się mnożenia i dzielenia na 8086/8088,musimy stosować rejestry ax i dz. Nie możemy zastosować przypadkowego rejestru jak możemy uczynić to z inna operacją .Również, nie zapomnijmy instrukcji rozszerzenia znaku jeśli wykonujemy operację dzielenia i dzielimy jedną 16/32 bitową liczbę przez inna. W końcu, nie zapomnijmy, że pewne instrukcje mogą powodować przepełnienie .Możemy chcieć sprawdzić warunek przepełnienia (lub niedomiaru) po operacji arytmetycznej. Przykłady prostych wyrażeń: X := Y + Z; mov ax, Y add ax, Z mov x, ax X := Y – Z; mov ax, y sub ax, z mov x, ax X := Y * Z; {bez znaku} mov ax, y mul z ;stosujemy IMUL dla arytmetyki znakowej mov x, ax ;nie zapomnijmy tym zmieść dx X := Y div Z; {dzielenie bez znaku} mov ax, y mov dx, 0 ;rozszerzenie zerem AX do DX div z mov x, ax X := Y div Z {div ze znakiem} mov ax, y cwd ; rozszerzenie znakiem AX do DX idiv z mov x, ax X := Y mod Z {reszta bez znaku} mov ax, y mov dx, 0 ;rozszerzenie zerem AX do DX div z mov x, dx ;reszta jest w DX X := Y mod Z { reszta ze znakiem} mov ax, y cwd ;rozszerzenie znakiem AX do DX idiv z mov x, dx ;reszta jest w DX Ponieważ jest możliwe wystąpienie błędu arytmetycznego, powinniśmy ogólnie testować wynik każdego wyrażenia na błąd przed lub po ukończonej operacji .Na przykład, bez znakowe dodawanie, odejmowanie i mnożenie ustawiają flagę przeniesienia jeśli wystąpi przepełnienie. Możemy zastosować instrukcje bezpośrednio po odpowiedniej sekwencji instrukcji dla testu na przepełnienie. Podobnie, możemy użyć instrukcji jo lub jno po tej sekwencji do testowania dla przepełnienia przy arytmetyce ze znakiem .Następne dwa przykłady demonstrują jak zrobić to dla instrukcji add: X := Y + Z; {bez znaku} mov ax, y add ax, z mov x, ax jc uOverflow X := Y + Z; {ze znakiem}
mov ax, y add ax, z mov x, ax jo sOverflow pewne operacje jednoargumentowe również kwalifikują się jako proste wyrażenia. Dobrym przykładem operacji jednoargumentowej jest negacja .W językach wysokiego poziomu negacja przyjmuje jedną z dwóch możliwych form: var := - var or var1 := - var2 Zauważmy, że var := - stała jest w rzeczywistości prostym przypisaniem, nie prostym wyrażeniem. Możemy wyszczególnić stałą ujemną jako operand instrukcji mov: mov var , -14 Przy operowaniu pierwszą formą operacji negacji stosujemy pojedynczą instrukcją asemblerową: neg var Jeśli są stosowane dwie różne zmienne, wtedy stosujemy jak następuje: mov ax, var2 neg ax mov var2, ax Przepełnienie wystąpi wtedy gdy spróbujemy zanegować największą wartość (-128 dla wartości ośmio bitowej, -32768 dla szesnasto bitowej .itd.).W tym przypadku 80x86 ustawia flagę przepełnienia ,więc możemy testować dla przepełnienia arytmetycznego stosując instrukcje jo lub jno We wszystkich innych przypadkach 80x86 zeruje flagę przepełnienia. Flaga przeniesienia nie ma znaczenia po wykonaniu instrukcji neg ponieważ neg (oczywiście) nie stosuje operandu bez znakowego. 9.1.3 WYRAŻENIA ZŁOŻONE Wyrażenie złożone jest każdym wyrażeniem arytmetycznym zawierającym więcej niż dwa warunki i jeden operator. Takie wyrażeniami są powszechnie znajdowane w programach pisanych w językach wysokiego poziomu. Wyrażenia złożone może zawierać nawiasy z operatorami pierwszeństwa ,wywołania funkcji, dostęp do tablic itp. Konwersja jakiegoś złożonego wyrażenia na język asemblera jest dosyć proste, inne wymagają wysiłku. Ta sekcja naszkicuje zasady jakich używamy przy konwersji takich wyrażeń. Złożona funkcja która jest łatwa do konwersji na asembler jest funkcją, która zawiera trzy warunki i dwa operatory ,na przykład: W := W- Y – Z; Konwersja tej instrukcji na język asemblera wymaga dwóch instrukcji sub. Jednakże, nawet w wyrażeniu tak prostym jak to, konwersja nie jest banalna. Faktycznie są dwa sposoby konwersji powyższej instrukcji na asembler: mov ax, w sub ax, y sub ax, z mov w, ax i mov ax, y sub ax, z sub w, ax Druga konwersja, ponieważ jest krótsza ,wygląda lepiej. Jednakże, tworzy ona niepoprawny wynik (zakładając Pascalową składnię dla oryginalnej instrukcji).Problemem jest prawo łączności. Druga z powyższych sekwencji oblicza W:=W-(Y-Z),która nie jest tym samym co W:=(W-Y)-Z .Jak umieścimy nawiasy wokół podwyrażeń możemy wpływać na wynik. Zauważmy, że jeśli jesteśmy zainteresowani krótszą forma, możemy zastosować poniższą sekwencję: mov ax, y add ax, z sub w, ax Oblicza to W:=W-(Y+Z).Jest to odpowiednik W:=(W-Y)-Z. Inną kwestią jest pierwszeństwo. Rozważmy wyrażenie Pascalowskie: X := W*Y+Z; Znowu mamy dwa sposoby w jaki możemy ocenić to wyrażenie: X := (W*Y)+Z; lub X := W*(Y+Z) Prawdopodobnie teraz myślisz, że ten tekst jest szalony. Każdy wie, że poprawnym sposobem oceny tych wyrażeń jest druga forma przedstawiona w tym drugim przykładzie. Jednakże ,mylisz się, jeśli myślisz w ten
sposób .Język programowania APL ,na przykład, ocenia wyrażenia wyłącznie od prawej do lewej i nie daje pierwszeństwa jednemu operatorowi przed innym. Większość języków wysokiego poziomu używa stałego zbioru zasad pierwszeństwa opisujących porządek oceniania w wyrażeniach wymagających dwóch lub więcej różnych operatorów. Większość języków programowania oblicza mnożenie i dzielenie przed dodawaniem i odejmowaniem. Te które wspierają podnoszenie do potęgi (np. FORTRAN i BASIC) zazwyczaj obliczają je przed mnożeniem i dzieleniem. Zasady te są intuicyjne ponieważ prawie każdy uczył się o nich w szkole. Rozważmy wyrażenie: X op1 Y op2 Z Jeśli op1 posiada pierwszeństwo przed op2,wtedy wyliczamy to (X op1 Y) op2 Z w przeciwnym razie jeśli op2 ma pierwszeństwo przed op1 wtedy wyliczamy to jako X op1 (Y op2 Z).W zależności od tego czy są wymagane operatory i operandy te dwa obliczenia mogą tworzyć różne wyniki. Kiedy konwertujemy wyrażenie tej postaci do języka asemblera, musimy najpierw obliczyć podwyrażenie z najwyższym priorytetem. Poniższy przykład demonstruje tą technikę: ; W := X+Y *Z; mov bx, x mov ax, y ;musimy najpierw obliczyć Y*Z ponieważ „*” ma najwyższy mul z ;priorytet. add bx, ax ;teraz dodajemy iloczyn i wartość Xa mov w, bx ;zachowujemy wynik Ponieważ dodawanie jest operacją przemienną ,możemy zoptymalizować powyższy kod tworząc: ; W := X+Y*Z; mov ax, y ;musimy obliczyć najpierw Y*Z ponieważ „*” ma najwyższy mul z ;priorytet. add ax, x ;teraz dodajemy iloczyn i wartość Xa mov w, ax ;zachowujemy wynik Jeśli dwa operatory pojawiające się wewnątrz wyrażenia mają taki sam priorytet, wtedy określamy porządek wyliczania używając zasady łączności .Większość operatorów jest lewo łącznych w znaczeniu, że są wyliczane od lewej do prawej Dodawanie, odejmowanie, mnożenie i dzielenie wszystkie są lewo łączne. Prawo łączne operatory są wyliczane od prawej do lewej. Operator potęgowania w FORTRANie i BASICu jest dobrym przykładem operatora prawo łącznego: 2^2^3 jest równy 2^(2^3) nie (2^2)^3 Zasady pierwszeństwa i łączności określają porządek wyliczania. Pośrednio te zasady mówią nam gdzie umieścić nawiasy w wyrażeniu dla określenia pierwszeństwa i łączności. Jednak, ostatecznie nasz kod asemblerowy musi ukończyć pewne działania przed innymi aby poprawnie wyliczyć wartość danego wyrażenia. Poniższy przykład demonstruje ta zasadę: ; W := X-Y-Z mov ax, x ;wszystkie operatory są takie same, więc musimy obliczać sub ax, y ;od lewej do prawej ponieważ wszystkie maja taki sub ax, z ;sam priorytet mov w, ax ;W := X+Y*Z mov ax,y ,najpierw musimy obliczyć Y*Z ponieważ mnożenie ma imul z ;wyższy priorytet niż dodawanie add ax, x mov w, ax ;W := X/Y –Z mov ax, x ;tu musimy najpierw obliczyć dzielenie ponieważ ma wyższy cwd ;priorytet idiv y sub ax, z mov w, ax ;W := X*Y*Z mov ax, y ;dodawanie i mnożenie są przemienne, dlatego porządek obliczania imul z ;nie ma znaczenia imul x mov w, ax Jest jeden wyjątek od zasady łączności. Jeśli wyrażenie wymaga mnożenia i dzielenia, zawsze lepiej najpierw wykonać mnożenie .Na przykład, dane jest wyrażenie w postaci: W := X/Y *Z
Lepiej jest obliczyć X*Z a potem podzielić wynik przez Y zamiast dzielić X przez Y i pomnożyć iloraz przez Z. Są dwa powody, że takie podejście jest lepsze. Po pierwsze, pamiętajmy, że instrukcja imul zawsze tworzy wynik 32 bitowy (zakładając 16 bitowy operand)Przez wykonanie najpierw mnożenia automatycznie powielamy znak iloczynu do rejestru dx aby nie musieć powielać znaku ax wcześniejszego dzielenia. To zapewnia wykonanie instrukcji cwd. Drugi powód wykonania najpierw mnożenia ,to zwiększenie precyzji obliczeń .Pamiętamy, że dzielenia (całkowite) często tworzy wynik niedokładny. Na przykład, jeśli obliczymy 5/2 otrzymamy wartość dwa a nie 2.5.Obliczenie (5/2)*3 da nam sześć. Jednakże, jeśli obliczamy (5*3)/2 dostaniemy wartość siedem, która jest trochę zbliżona do rzeczywistej wartości (7.5).Dlatego też, jeśli napotkamy wyrażenie w postaci: W := X/Y*Z Zazwyczaj możemy ją skonwertować do kodu asemblerowego: mov ax, x imul z idiv z mov w, ax Oczywiście, jeśli algorytm jakim kodujemy zależy od efektu zaokrąglenia operacji dzielenia, nie możemy zastosować tej sztuczki do poprawienia algorytmu. Morał z tej historii: zawsze upewniajmy się ,czy dokładnie zrozumieliśmy dane wyrażenia, które chcemy skonwertować do języka asemblera. Oczywista, że jeśli semantyka dyktuje, że najpierw musimy wykonać dzielenie, zróbmy to. Rozważmy poniższa instrukcję pascalowską: W := X – Y *Z; Jest ona podobna do poprzedniego przykładu z wyjątkiem tego, że stosujemy odejmowanie zamiast dodawania. Ponieważ odejmowanie nie jest przemienne, nie możemy obliczyć Y*Z a potem odjąć X od tego wyniku. Trochę to nam skomplikuje nieco konwersję .Zamiast prostej sekwencji mnożenia i dodawania ,musimy załadować X do rejestru, pomnożyć Y i Z pozostawiając iloczyn w innym rejestrze a potem odjąć ten iloczyn od X np. mov bx, x mov ax, y imul z sub bx, ax mov w, bx Jest to trywialny przykład, który demonstruje potrzebę stosowania zmiennych tymczasowych w wyrażeniu. Kod stosujący rejestr bx tymczasowo przechowuje kopię X dopóki obliczany jest iloczyn Y i Z. Jeśli nasze wyrażenia stają się coraz bardziej złożone, rośnie potrzeba na tymczasowość rośnie. Rozważmy poniższą instrukcję Pascalowską: W := (A+B)*(Y+Z); Stosując zwykłe zasady oceniania, obliczamy najpierw podwyrażenia wewnątrz nawiasów (tj. dwa podwyrażenia z najwyższymi priorytetami) i rezerwujemy miejsce w pamięci dla ich wartości. Kiedy obliczymy wartości dla obu podwyrażeń, możemy obliczyć ich sumę. Jedyny sposób zajęcia się złożonym wyrażeniem takim jak to jest zredukowanie go do sekwencji prostych wyrażeń, których wyniki przetrzymuje się w zmiennych tymczasowych. Na przykład, możemy skonwertować powyższe pojedyncze wyrażenie do następującej sekwencji: Temp1 := A+B;; Temp2 := Y+Z; W := Temp1 * Temp2; Ponieważ konwertowanie prostych wyrażeń do języka asemblera jest całkiem łatwe, teraz dopasowujemy obliczenia pierwszego, złożonego wyrażenia w asemblerze. Kod : mov ax, a add ax, b mov temp1, ax mov ax, y add ax, z mov temp2, ax mov ax, temp1 imul temp2 mov w, ax Oczywiście, kod ten jest rażąco niewydajny i wymaga zadeklarowania pary zmiennych tymczasowych w segmencie danych. Jednakże jest łatwy sposób zoptymalizowania tego kodu przez przetrzymanie zmiennych tymczasowych, tak długo jak to możliwe w rejestrach 80x86.Poprzez zastosowanie rejestrów 80x86, przechowujących wyniki tymczasowe te kod staje się taki;
mov ax, a add ax, b mov bx, y add bx, z imul bx mov w, ax Jeszcze inny przykład: X := (Y+Z)*(A-B) / 10 Będzie to skonwertowane po ustaleniu czterech prostych wyrażeń: Temp1 := (Y+Z) Temp2 := (A-B) Temp1 := Temp1 * Temp2 X := Temp1 / 10 Możemy skonwertować te cztery proste wyrażenia na instrukcje języka asemblera: mov ax, y ;oblicza AX := Y+Z add ax, z mov bx, a ;oblicza BX := A-B sub bx, b mul bx ;oblicza AX := AX*BX, również powiela znak mov bx, 10 ;AX do DX dla iciv idiv bx ;oblicza AX := AX / 10 mov x, ax ;przechowuje wynik w X Najważniejsza rzecz jaka jest do zapamiętania to taka ,że wartości tymczasowe, jeśli to możliwe, powinny być trzymane w rejestrach. Pamiętamy, że dostęp do rejestrów 80x86 jest dużo bardziej wydajny niż dostęp do komórek pamięci .Stosujemy komórki pamięci do przechowywania tymczasówek, tylko jeśli korzystamy już z rejestrów. Ostatecznie, konwertowanie złożonych wyrażeń na język asemblera, trochę różni się od rozwiązywania wyrażeń ręcznie. Zamiast właściwego obliczania wyniku w każdej fazie obliczeń, po prostu piszemy kod asemblerowy, który oblicza wyniki Ponieważ nauczyliśmy się obliczać tylko jedno działania na raz, to znaczy, że ręczne obliczenia pracują nad „prostym wyrażeniem”, które istnieje w wyrażeniu złożonym. Oczywiście konwertowanie tych prostych wyrażeń na asembler jest banalnie proste. Dlatego też, każdy kto może rozwiązać złożone wyrażenie ręcznie, może skonwertować go na język asemblera korzystając z zasad dla prostych wyrażeń. 9.1.4 OPERATORY PRZEMIENNOŚCI Jeśli „@” przedstawia jakiś operator, ten operator jest przemienny, jeśli następujący związek zawsze jest prawdziwy: (A @ B) = (B @ A) Jak widzieliśmy w poprzedniej sekcji, operatory przemienności są przyjemne, ponieważ porządek ich operandów jest nieistotny a to pozwala nam przestawiać obliczenia, często czyniąc to obliczenie łatwiejszym lub bardziej wydajnym. Często przestawianie obliczeń pozwala nam na zastosowanie mniej zmiennych tymczasowych. Kiedykolwiek napotkamy operator przemienności w wyrażeniu, powinniśmy zawsze sprawdzić czy istnieje lepsza sekwencja, którą można zastosować do poprawy szybkości naszego kodu. Poniższa tabela wylicza operatory przemienności i nie przemienne jakie zwykle znajdujemy w językach wysokiego poziomu:
Tablica 46 : Popularne binarne operatory przemienności
Tablica 47 : Popularne nie przemienne operatory binarne 9.2 WYRAŻENIA LOGICZNE (BOOLOWSKIE) Rozważmy następujące wyrażenie z programu pascalowskiego: B:= ((X=Y) and (A <= C)) or ((Z-A) <> 5); B jest zmienną boolowską a pozostałe zmienne są całkowite. Jak przedstawimy zmienne boolowskie w języku asemblera? Chociaż, przyjmują tylko jeden bit dla przedstawienia wartości boolowskiej, większość programistów asemblerowych przeznacza cały bajt lub słowo na ten cel. Z bajtem, jest 256 możliwych wartości, jakie możemy zastosować dla przedstawienia dwóch wartości prawda i fałsz. Wiec która z dwóch wartości (lub które dwa zbiory wartości) stosujemy do przedstawienia tych wartości boolowskich? Z powodu architektury maszyny, jest dużo łatwiej testować dla warunków takich jak zero lub nie-zero i dodatnich lub ujemnych zamiast dla jednej lub dwóch szczególnych wartości boolowskich. Większość programistów (a nawet języki programowani takie jak C) wybiera zero do przedstawiania fałszu i coś innego dla przedstawiania prawdy .niektórzy ludzie wolą przedstawiać prawdę i fałsz jako jeden i zero (odpowiednio) i nie pozwalają na inne wartości. Inni wybierają 0FFFFh dla prawdy i 0 dla fałszu. Możemy również zastosować wartości dodatnie dla prawdy i ujemne dla fałszu. wszystkie te mechanizmy mają zalety i wady. Używanie tylko zera i jedynki dla przedstawiania fałszu i prawdy oferuje jedną bardzo dużą zaletę: instrukcje logiczne 80x86 (and, or, xor i , w mniejszym stopniu, not) działają na tych wartościach dokładnie tak jak od nich oczekujemy. to znaczy, jeśli mamy dwie zmienne boolowskie A i B, wtedy poniższe instrukcje wykonują podstawowe operacje logiczne na tych dwóch zmiennych: mov ax, A and ax, B mov C, ax ;C := A and B mov or mov
ax, A ax, B C, ax
;C := A or B
mov xor mov
ax, A ax, B C, ax
;C := A xor B
mov not and mov
ax, A ax ax, 1 B, ax
;B := not A
mov ax, A ;inny sposób zrobienia B := NOT A xor ax, 1 mov B, ax ;B := not A Zauważ, jak wskazano powyżej, że instrukcja not nie dokładnie oblicza logiczną negację. Not zera na poziomie bitowym to 0FFh a not jeden na poziomie bitowym to 0FEh.Żaden wynik nie jest zerem lub jedynką. Jednakże,
przez dodanie jeden do wyniku, uzyskamy wynik poprawny. Zauważmy, że możemy uczynić operację not bardziej wydajną używając instrukcji xor ax,1 ponieważ wpływa ona tylko na najmniej znaczący bit. Okazuje się, że stosując zero dla fałszu i jakiejś innej wartości dla prawdy mamy dużo subtelnych zalet .Ściśle, test dla prawdy lub fałszu jest często ukryty w wykonywaniu każdej logicznej instrukcji. Jednak ten mechanizm cierpi na bardzo dużą niedogodność :nie możemy używać instrukcji and ,or, xor i not 80x86 do implementacji działań boolowskich tej samej nazwy. Rozpatrzmy dwie wartości 55h i 0Aah.Obie są niezerowe, więc obie przedstawiają wartość prawdy .Jednak, jeśli logicznie zandujemy 55h i 0Aah razem, stosując instrukcję and 80x86,wynikiem będzie zero.(Prawda i prawda) powinny tworzyć prawdę ,nie fałsz. System, który stosuje wartości niezerowych do przedstawiania prawdy i zera dla przedstawiania fałszu jest arytmetycznym systemem logicznym. System, który stosuje dwie odrębne wartości, takie jak zero i jeden do przedstawiania fałszu i prawdy jest nazywany boolowskim systemem logicznym, lub po prostu, systemem boolowskim. Możemy zastosować inny system, równie dogodny .Rozważmy znów wyrażenie boolowskie: B := ((X=Y) and (A<=D)) or ((Z-A) <>5); Proste wyrażenia wynikłe z tego wyrażenia mogą być następujące: Temp2 := X=Y Temp := A <= D Temp := Temp and Temp2 Temp2 := Z-A Temp2 := Temp2 <> 5 B := Temp or Temp2 Kod asemblerowy dla tego wyrażenia może być taki:
L1: L2:
ST1: L3:
ST2: L4:
mov cmp jnz mov jmp mov
ax, x ax, y L1 al., 1 L2 al., o
;patrzy czy X + Y i ładuje zero lub jeden do AX ;oznaczający wynik tego porównania.
mov cmp jle mov jmp mov
bx, A bx, D ST1 bl, 0 L3 bl, 1
;patrzy czy A <= D i ładuje zero lub jeden do BX ;wynik tego porównania
and
bl, al.
;Temp := Temp and Temp2
mov sub cmp jnz mov jmp mov
ax, Z ax, A ax, 5 ST2 al., 0 short L4 al., 1
;czy (Z-A) <>5 ;Temp2 := Z-A ;Temp2 := Temp2 <>5
;X=Y ;X <>Y
or al., bl ;Temp := Temp or Temp2 mov B, al. ;B := Temp Jak możemy zobaczyć jest to dosyć nieporęczna sekwencja instrukcji. Jedną niewielką optymalizacja jaką możemy zastosować, jest założenie ,że wynik będzie prawdą lub fałszem i zainicjowanie odpowiedniego wyniku boolowskiego przed czasem: mov bl, 0 ;Zakładamy X <> Y mov ax, x cmp ax, Y jne L1 mov bl, 1 ;X jest równe Y więc jest to prawda L1: mov bh, 0 ;zakładamy ,że nie (A <=D) mov ax, A cmp ax, D
jnle mov
L2 bh, 1
;A <= D więc jest to prawda
and
bl, bh
;obliczamy wynik logicznego AND
mov mov sub cmp je mov
bh,0 ax, Z ax, A ax, 5 L3: bh, 1
;zakładamy ,że (Z-A) = 5
L2:
;(Z-A) <> 5
L3: or bl, bh ; wynik logicznego OR mov B, bl ;zachowujemy wynik boolowski Oczywiście, jeśli mamy 80386 lub późniejszy procesor ,możemy zastosować instrukcje setcc do uproszczenia tego trochę: mov ax, x cmp ax, y sete al. ;Temp2 := X=Y mov cmp setle and mov sub cmp setne or mov
bx, A bx, D bl bl, al. ax, Z ax, A ax, 5 al. bl, al. B, bl
;Temp := A <= D ;Temp := Temp and Temp2 ;Temp2 := Z-A ;Temp2 := Temp2 <> 5 ;Temp:=Temp or Temp2 ;B := Temp
Ta sekwencja kodu jest oczywiście dużo lepsza niż poprzednie, ale wykonuje się tylko na pocesorach80386 i późniejszych. Innym sposobem zajęcia się wyrażeniami boolowskimi jest przedstawianie wartości boolowskich poprzez stany wewnątrz naszego kodu. Podstawową ideą jest zapomnieć o utrzymaniu zmiennych boolowskich przez całe wykonywanie sekwencji kodu i zastosowanie lokalizacji wewnątrz kodu do określenia wyniku boolowskiego. Rozważmy następująca implementację powyższego wyrażenia .Po pierwsze, poprzestawiamy wyrażenie: B := ((Z-A) <> 5) or ((X=Y) and (A <=D)); Jest to zupełnie poprawne ponieważ operator or jest przemienny teraz rozważmy poniższą implementację: mov B, 1 ;zakładamy ,że wynik jest prawdą mov ax, Z ;sprawdzamy czy (Z-A) <> 5 sub ax, A ;jeśli ten warunek jest prawdziwy, wynik jest zawsze cmp ax, 5 ;prawdą i nie musimy sprawdzać reszty jne Done mov cmp jne
ax, X ax, Y SetBtoFalse
;jeśli X <> Y wynik jest fałszem ;bez względu co zawiera A i D
mov cmp jle mov
ax, A ax, D Done B, 0
;czy A <= D
;jeśli tak, to wyjście SetBtoFalse: Done: Zauważmy, że ta sekcja kodu jest dużo krótsza iż pierwsza wersja powyżej ( i działa na wszystkich procesorach).Poprzednia robiła wszystko obliczeniowo. Ta wersja stosuje logikę działań programu do poprawy kodu. Zaczyna od założenia prawdziwości wyniku i ustawia zmienną B na prawda. potem sprawdza czy (Z-A) <>5.Jeśli jest to prawda, kod rozgałęzia się do tablicy done ponieważ B jest prawdą bez względu na to co się wydarzy. jeśli program nie dochodzi do instrukcji mov ax, X, wiemy, że wynik poprzedniego porównania jest
fałszem. Nie musimy zachowywać tego wyniku w zmiennej tymczasowej ponieważ pośrednio znamy wynik poprzez fakt, że wykonujemy instrukcję mov ax, X. Podobnie ,druga grupa instrukcji sprawdza czy X jest równe Y .Jeśli nie, już wiemy, że wynik jest fałszem, więc ten kod skacze do etykiety SetBtoFalse .Jeśli program zaczyna wykonywanie trzeciego zbioru instrukcji ,wiemy ,że pierwszy wynik był fałszem a drugi wynik był prawdą: położenie kodu to gwarantuje. Dlatego nie musimy utrzymywać tymczasowej zmiennej boolowskiej, która śledzi stan tego obliczania. Rozważmy inny przykład; B := ((A=E) or (F <>D) and ((A<>B) or (F=D) Obliczeniowo, to wyrażenie daje znaczną ilość kodu. Jednak ,poprzez użycie sterowania strumieniem danych możemy zredukować go jak następuje: mov b, 0 ;zakładamy wynik fałsz mov ax, a ;czy A = E cmp ax, e je test2 ;jeśli tak, pierwsze podwyrażenie jest prawdą
Test2:
SetBtol: Done:
mov cmp je mov cmp jne
ax, f ax, d Done ax, a ax, b SetBtol
;jeśli nie sprawdź drugie podwyrażenie ;czy F <> D ;jeśli tak przechodzimy do drugiego testu ;czy A <> B?
mov cmp jne mov
ax, f ax, d Done b, 1
;jeśli nie, zobacz czy F = D
;jeśli tak, zrobiono
Jest jedna różnica pomiędzy stosowaniem sterowaniem przebiegiem programu a logiką obliczeniowa: kiedy stosujemy metody sterowania przebiegiem programu, możemy pominąć większość instrukcji, które implementuje formuła boolowska. Jest to znane jako obliczenie częściowe. kiedy stosujemy model obliczeniowy, nawet z instrukcją setcc, kończymy wykonywanie większości instrukcji. Zapamiętajmy, że nie jest to koniecznie wada. Na procesorach potokowych, można dużo szybciej wykonać kilka dodatkowych instrukcji zamiast opróżniać potok i kolejkę rozkazów. może musimy poeksperymentować z naszym kodem dla określenia najlepszego rozwiązania. Kiedy pracujemy z wyrażeniami boolowskimi, nie zapomnijmy, że można zoptymalizować nasz kod poprzez upraszczanie tych wyrażeń boolowskich (zobacz „Upraszczanie Funkcji Boolowskich”).możemy zastosować transformację algebraiczną (zwłaszcza teorie DeMorgana) i metodę mapowania do pomocy przy redukcji złożonych wyrażeń. 9.3 OPERACJE WIELOKROTNEJ PRECYZJI Jedną wielką zaletą języka asemblera nad HLL’ami jest to, że asembler nie ogranicza wielkości liczb całkowitych. Na przykład, język C definiuje maksymalnie trzy różne wielkości całkowite: short int, int i long int .Na PC są często 16 lub 32 bitowe liczby całkowite .Chociaż instrukcje maszynowe 80x86 ograniczają nas do działania na ośmio- szesnasto lub trzydziesto dwu bitowych liczbach całkowitych z pojedynczą instrukcją, możemy zawsze zastosować więcej niż jedną instrukcję do działania na liczbach całkowitych o każdej wielkości jaką sobie życzymy. Jeśli chcemy wartość całkowitą 256 bitową, żaden problem. Poniższa sekcja opisuje jak rozszerzyć różne arytmetyczne i logiczne operacje z 16 lub 32 bitów do tak wielu bitów jaka nas zadowala. 9.3.1 OPERACJE DODAWANIA O WIELKOKTROTNEJ PRECYZJI Instrukcja add 80x86 dodaje dwa 8, 16 lub 32 bitowe liczby. Po wykonaniu instrukcji dodawania, flaga przeniesienia jest ustawiana jeśli wystąpi przepełnienie z najbardziej znaczącego bitu sumy.
Rysunek 9.1 dodawanie wielokrotnej precyzji (48 bitowe) Możemy użyć tych informacji dla operacji dodawania wielokrotnej precyzji. Rozważmy sposób ręcznego wykonania operacji dodawania wielocyfrowego (wielokrotnej precyzji): Krok1: Dodajemy razem najmniej znaczące cyfry :
Krok2: Dodajemy następne znaczące cyfry plus przeniesienie:
Krok 3: Dodajemy najbardziej znaczące cyfry plus przeniesienie:
80x86 operuje rozszerzoną precyzją arytmetyki w identycznej formie, z wyjątkiem tego, że zamiast dodawania lic cyfr, dodaje bajt lub słowo. Rozważmy operację dodawania trój słowa (48 bitów) z rysunku 8.1. Instrukcja add dodaje najmniej znaczące słowa razem. Instrukcja adc (dodawanie z przeniesieniem) dodaje inne pary słów razem. Instrukcja adc dodaje dwa operandy plus flaga przeniesienia razem tworząc wartość słowa i (możliwe) przeniesienie. Na przykład przypuśćmy, że mamy dwie trzydziesto dwu bitowe wartości, które życzymy sobie dodać razem, zdefiniowane jak następuje: X dword ? Y dword ? Przypuśćmy też, że chcemy przechować sumę w trzeciej zmiennej, Z, która jest podobnie zdefiniowana dyrektywą dword. poniższy kod 80x86 realizuje to zadanie:
mov ax, word ptr X add ax, word ptr Y mov word ptr Z, ax mov ax, word ptr X+2 adc ax, word ptr Y+2 mov word ptr Z+2, ax Pamiętamy, że te zmienne są deklarowane dyrektywą dword. Dlatego też asembler nie zaakceptuje instrukcji w postaci mov ax, X ponieważ instrukcja ta próbuje załadować 32 bitową wartość do 16 bitowego rejestru. Dlatego też kod ten stosuje operator koercji word ptr do sprowadzenia symboli X,Y i Z do szesnastu bitów. Pierwsze trzy instrukcje dodają razem najmniej znaczące słowa X i Y i przechowują wynik w najmniej znaczącym słowie Z. Ostatnie trzy instrukcje dodają najbardziej znaczące słowa X i Y razem z przeniesieniem z mniej znaczącego słowa, i przechowują wynik w bardziej znaczącym słowie Z. Pamiętajmy, że wyrażenia adresowe w postaci „X+2” uzyskuje dostęp do bardziej znaczącego słowa 32 bitowej jednostki. To z powodu faktu, że przestrzeń adresowa 80x86 jest adresowana bajtami i zabiera dwa kolejne bajty na słowo. Oczywiście, jeśli mamy 80386 lub późniejsze procesory, nie musimy przechodzić przez wszystkie dodawania dwóch 32 bitowych wartości razem ,ponieważ 80386 bezpośrednio wspiera 32 bitowe operacje .jednakże, jeśli chcielibyśmy dodać razem dwie 64 bitowe wartości całkowite na 80386,będziemy musieli zastosować tę technikę. Możemy rozszerzyć to na każdą liczbę bitów poprzez zastosowanie instrukcji adc dla dodawania w wyższym porządku słów w wartości. Na przykład, dodanie razem dwóch 128 bitową wartość ,możemy użyć kod, który wygląda podobnie jak poniższy:: BigVal1 dword 0,0,0,0 ;cztery podwójne słowa BigVal2 dword 0,0,0,0 BigVal3 dword 0,0,0,0 mov eax, BigVal1 ;nie potrzebujemy operatora dword ptr ponieważ są to add eax, Bigval2 ; zmienne dword mov BigVal3, eax mov adc mov
eax,BigVal+4 eax,BigVal2+4 BigVal3+4, eax
mov adc mov
eax,BigVal1+8 eax,BigVal2+8 BigVal3+8, eax
mov adc mov
eax,BigVal1+12 eax,bigVal2+12 BigVal3+12, eax
9.3.2 OPERACJE ODEJMOWANIE WIELOKROTNEJ PRECYZJI Podobnie jak dodawanie,80x86 wykonuje odejmowanie wielobajtowe, w ten sam sposób ręcznie, z wyjątkiem kiedy odejmuje całe bajty, słowa lub podwójne słowa na raz zamiast cyfr dziesiętnych. Mechanizm jest podobny do tego dla operacji dodawania.. Stosujemy instrukcje sub na mniej znaczącym bajcie /słowie /podwójnym słowie a instrukcję sbb na wartościach bardziej znaczących cyfr .Poniższy przykład demonstruje 32 bitowe odejmowanie stosując 16 bitowe rejestry na 8086: var1 dword ? var2 dword ? diff dword ? mov ax, word ptr var1 sub ax, word ptr var2 mov word ptr diff, ax mov ax, word ptr var1+2 sbb ax, word ptr var2+2 mov word ptr diff+2, ax Poniższy przykład demonstruje 128 bitowe odejmowanie stosując zbiór 32 bitowych rejestrów 80386:
BigVal1 BigVal2 BigVal3
dword dword dword mov sub mov
0,0,0,0 0,0,0,0 0,0,0,0
eax,BigVal1 eax, BigVal2 BigVal3, eax
,nie potrzebujemy operatora dword ptr ponieważ są to ;zmienne podwójnego słowa
mov sbb mov
eax,BigVal1+4 eax, BigVal2+4 BigVal3+4, eax
;odejmujemy wartości od mniej do bardziej znaczącej ;jednostki stosując instrukcje SUB i SBB
mov sbb mov
eax, BigVal1+8 eax, BigVal2+8 BigVal3+8,eax
mov sbb mov
eax, BigVal1+12 eax,BigVal2+12 BigVal3+12, eax
9.3.3 PORÓWNANIA O ROZSZERZONEJ PRECYZJI Niestety nie ma instrukcji „porównania z pożyczką”, która mogła by być zastosowana dla porównania o rozszerzonej precyzji. Ponieważ instrukcje cmp i sub wykonują takie same operacje, przynajmniej jeśli chodzi o flagi ,prawdopodobnie domyślamy się, że można użyć instrukcji sbb do syntezy porównań o rozszerzonej precyzji; Jednakże jest to częściowa prawda .Jest lepszy sposób. Rozważmy dwie wartości bez znaku 2157h i 1293h.Mniej znaczące bajty tych dwóch wartości nie wpływają na wynik porównania. Po prostu porównujemy 21h z 12h,które mówią nam ,że pierwsza wartość jest większa niż druga. Faktycznie, jedyny raz kiedy musimy spojrzeć na oba bajty tych wartości jest wtedy czy bardziej znaczące bajty są równe. We wszystkich innych przypadkach porównania bardziej znaczących bajtów mówią nam wszystko co musimy wiedzieć o wartościach. Oczywiście ,jest to prawda dla każdej liczby bajtów, nie tylko dwóch. Poniższy kod porównuje dwie 64 bitowe liczby całkowite ze znakiem na 80386 i późniejszych procesorach: ;Jest to przekazanie sterowania do lokacji „IsGreater” jeśli QwordValue > QwordValue2 ;Przekazuje sterowanie do „IsLess” jeśli Qword Value < QwordValue2.nie dochodzi do wykonania ;tych instrukcji jeśli qwordValue = QwordValue2.Test dla nierówności zmienia operandy „IsGreater” i ;”IsLess” na „NotEquaL” w tym kodzie. mov eax, dword ptr QwordValue+4 ;pobiera bardziej znaczący dword cmp eax, dword ptr QwordValue2+4 jg IsGreater jl IsLess mov eax, dword ptr QwordValue cmp eax, dword ptr QwordValue2 jg IsGreater jl IsLess Dla porównania wartości bez znakowych stosujemy instrukcje ja i jb w miejsce jg i jl Możemy łatwo syntetyzować każde możliwe porównanie z powyższą sekwencją, poniższy przykład pokazuje jak to zrobić te przykłady robisz porównanie ze znakiem, zastępując ja ,jae, jb i jbe za jg, jge i jle (odpowiednio) dla porównania bez znakowego QW1 QW2
qword ? qword ?
dp
textequ
;testujemy 64 bity aby zobaczyć czy QW! < QW2 (ze znakiem) ;przekazuje sterownie do etykiety „IsLess’ jeśli QW1 < QW2..Nie dochodzi do skutku następna instrukcja jeśli ;nie jest to prawdą mov eax, dp QW1+4 ;pobiera bardziej znaczący dword
cmp jg jl mov cmp jl NotLess:
eax, dp QW2+4 NotLess IsLess eax, dp QW1 eax, dp QW2 IsLess
;nie dochodzi do skutku jeśli bardziej znaczące dwordy ;są równe
;testujemy 64 bity aby zobaczyć czy QW1 <= QW2 (ze znakiem) mov eax, dp QW1+4 ;pobieramy bardziej znaczący dword cmp eax, dp QW2+4 jg NotLessEq jl IsLessEq mov eax, dp QW1 cmp eax, dword ptr QW2 jle IsLessEq NotLessEQ: ;testujemy 64 bity aby zobaczyć czy QW1 > QW2 (ze znakiem) mov eax, dp QW1+4 ;pobranie bardziej znaczącego dworda cmp eax, dp QW2+4 jg IsGtr jl NotGtr mov eax, dp QW1 ;nie dochodzi do skutku jeśli bardziej znaczące dwordy cmp eax, dp QW2 ;są równe jg IsGtr NotGtr: ;testujemy 64 bity aby zobaczyć czy QW1 >= QW2 (ze znakiem) mov eax, dp QW1+4 ;pobranie bardziej znaczącego dworda cmp eax, dp QW2+4 jg IsGtreq jl NotGtrEq mov eax, dp QW1 cmp eax, dword ptr QW2 jge IsGtrEq NotGtrEq: ;testujemy 64 bity aby zobaczyć czy QW1 = QW2 (ze znakiem lub bez znaku).kod ten rozgałęzia się do etykiety ;”IsEqual” jeśli QW1 = QW2.Nie dochodzi do następnej instrukcji jeśli nie są one równe mov eax, dp QW1+4 cmp eax, dp QW2+4 jne NotEqual mov eax, dp QW1 cmp eax, dword ptr QW2 je IsEqual NotEqual: ;testujemy 64 bity aby zobaczyć czy QW1 <> QW2 (ze znakiem lub bez znaku)Ten kod rozgałęzia się do ;etykiety „NotEqual” jeśli QW1 <> QW2.Nie dochodzi do skutku następna instrukcja jeśli są równe mov eax, dp QW1 +4 ;pobranie bardziej znaczącego dworda cmp eax, dp QW2+4 jne NotEqual mov eax, dp QW1 cmp eax, dword ptr QW2 jne NotEqual
9.3.4 MNOZENIE O ROZSZERZONEJ PRECYZJI Chociaż mnożenie 16x16 lub 32x32 jest wystarczające, są chwile, kiedy chcemy pomnożyć razem. Większe wartości .Zastosujemy pojedynczy operand 80x86 instrukcji mul i imul dla mnożenia o rozszerzonej precyzji. Bez zaskoczenia (zważywszy na to jak pracują adc i sbb), zastosujemy tą samą technikę dla wykonania mnożenia o rozszerzonej precyzji na 80x86,które stosujemy kiedy ręcznie mnożymy dwie wartości. Rozważmy uproszczoną postać sposobu w jaki wykonujemy wielocyfrowe mnożenie ręcznie: 1) Mnożymy pierwsze dwie liczby:
2) Mnożymy 5*2:
3) Mnożymy 5*1
4)
5) Mnożymy 4*2
6) 4*1
7)dodajemy wszystkie części razem:
Rysunek 9.2 Mnożenie o zwielokrotnionej precyzji 80x86 robi emnożeni o wielokrotnej precyzji w ten sam sposób z wyjątkiem, tego ,że pracuje z bajtami, słowami i podwójnymi słowami zamiast cyframi. Rysunek 8.2 pokazuje jak to robi. Prawdopodobnie najważniejszą rzeczą do zapamiętania kiedy wykonujemy mnożenie o rozszerzonej precyzji jest taka, że musimy również wykonać dodawanie o wielokrotnej precyzji w tym samym czasie. Dodanie wszystkich części iloczynu wymaga kilku dodawań .które stworzą wynik. poniższy listing demonstruje właściwy sposób mnożenia dwóch 32 bitowych wartości na szesnastobitowym procesorze: Notka:Multiplier i Multiplicand są 32 bitowymi zmiennymi zadeklarowanymi w segmencie danych ,przez dyrektywę dword .Iloczyn jest 64 bitową zmienną zadeklarowaną w segmencie danych przez dyrektywę qword.
Jedną rzecz dotyczącą tego kodu musimy zapamiętać, pracuje tylko dla operandów bez znakowych. 9.3.5 DZIELENNIE O ROZSZERZONEJ PRECYZJI Nie możemy zastosować ogólnej operacji dzielenia n-bitów /m.-bitów stosując instrukcji div i idiv. Taka operacja musi być wykonana przy zastosowaniu sekwencji instrukcji przesunięć i odejmowania. Tak operacja jest niezmiernie niechlujna. Mniej ogólna operacja, dzielenie n bitową wielkość przez 32 bitową (na 80386 lub późniejszych) lub 16 bitową wielkość jest łatwiejsza do zrobienia stosując instrukcję div .Poniższy kod demonstruje jak podzielić 64 bitową wielkość przez 16 bitowy dzielnik, tworząc 64 bitowy iloraz i 16 bitową resztę: dseg segment para public ‘DATA’ divident dword 0FFFFFFFFh, 12345678h divisor word 16 Qoutient dword 0,0 Modulo word 0 dseg ends cseg
segment para public ‘CODE’ assume cs:cseg, ds.:dseg ;Dzielimy wielkość 64 bitową przez wielkość 16 bitową: Divide64
proc mov div mov mov div mov mov div mov
near ax, word ptr dividend+6 divisor word ptr Quotient+6, ax ax, word ptr dividend+4 divisor word ptr Quotient+4, ax ax, word ptr dividend+2 divisor word ptr Quotient+2, ax
Divide64 cseg
mov div mov mov ret endp ends
ax, word ptr dividend divisor word ptr Quoyient, ax Modulo, dx
Kod ten może być rozszerzony do każdej liczby bitów przez proste dodanie dodatkowych instrukcji mov / div/ mov na początku sekwencji. Oczywiście, na 80386 i późniejszych procesorach możemy dzielić przez wartości 32 bitowe stosując edx i eax w powyższej sekwencji (z kilkoma innymi stosownymi regulacjami) Jeśli musimy zastosować dzielnik większy niż 16 bitów (32 bity na 80386 i późniejszych) będziemy musieli zaimplementować dzielenie stosując strategię przesunięć i odejmowania. Niestety takie algorytmy są bardzo powolne. W tej sekcji rozwiniemy dwa algorytmy dzielenia, które działają na dowolnej liczbie bitów. Pierwszy jest wolny, ale łatwiejszy do zrozumienia, drugi jest trochę szybszy (ogólnie rzecz biorąc) Podobnie jak dla mnożenia, najlepszy sposób zrozumienia jak komputer wykonuje dzielenie jest przestudiowanie jak nauczyć się wykonuje się długie dzielenie ręcznie .rozważmy działanie 3456 / 12 i krok po kroku wykonamy ręcznie tą operację:
Algorytm ten jest w rzeczywistości łatwiejszy w systemie binarnym, ponieważ w każdym kroku nie musimy się domyślać ile razy 12 mieści się w reszcie oraz czy musimy mnożyć 12 przez domyślną liczbę odejmowań. Przy każdym kroku w algorytmie binarnym ,dzielnik zawiera resztę dokładnie zero lub jeden raz jako przykład rozpatrzmy dzielenie 27 (11011) przez trzy (11):
Jest to nowatorski sposób implementacji tego algorytmu binarnego dzielenia, który oblicza iloraz i resztę w tym samym czasie. Algorytm jest następujący:
NumberBits jest liczbą bitów w zmiennych Remainder, Quotient,Divisor i Dividend .Zauważmy, że instrukcja Quotient := Quotient +1 ustawia najmniej znaczący bit Quotient na jeden ponieważ ten poprzedni algorytm przesunął Quotient jeden bit w lewo .Kod 80x86 implementuje ten algorytm tak:
Ten kod wygląda na krótki i prostym ale, jest kilka problemów a nim .Po pierwsze, nie sprowadza dzielenia przez zero (tworzy wartość 0FFFFFFFFh jeśli próbujemy dzielić przez zero),działa tylko z wartościami bez znakowymi i jest bardzo wolny .Dzielenie przez zero jest bardzo proste, sprawdzamy czy dzielnik jest zerem podczas wcześniejszego wykonywania kodu i zwracamy właściwy kod błędu jeśli dzielnik jest zerem. Działanie z wartościami ze znakiem jest równie proste, co zobaczymy trochę później. Wydajność tego algorytmu pozostawia wiele do życzenia .Zakładając że jedno przejście przez pętlę zabiera 30 cykli zegarowych, ten algorytm wymagałby prawie 1000 cykli zegarowych! To jest porządny rozmiar, gorszy niż instrukcje DIV/IDIV na 80x86,które są wśród najwolniejszych instrukcji 80x86 Jest technika, którą możemy zastosować do zwiększenia wydajności tego dzielenia przy dużej ilości danych :sprawdzamy czy zmienna dzielnika rzeczywiście używa 32 bitów. Często nawet mimo, że dzielnik jest zmienną 32 bitową, jej wartość mieści się w 16 bitach.(np. bardziej znaczące słowo Divisora jest zerem)W tym specjalnym przypadku, który występuje często, możemy zastosować instrukcję DIV, która jest dużo szybsza. 9.3.6 OPERACJA NEG O ROZSZERZONEJ PRECYZJI. Chociaż jest kilka sposobów zanegowania wartości o rozszerzonej precyzji, najkrótszym sposobem jest zastosowanie instrukcji neg lub sbb. technika ta korzysta z faktu, że neg odejmuje swój operand od zera. W szczególności, ustawia flagi w ten sam sposób jak instrukcja sub jeśli odjęlibyśmy wartość przeznaczenia od zera. Kod przyjmuje następującą postać: neg dx neg ax, sbb dx, 0 Instrukcja sbb zmniejsza dx jeśli wystąpi pożyczka z mniej znaczącego słowa operacji negacji (która zawsze wystąpi o ile ax nie jest zerem)
Rozszerzenie tej operacji do dodatkowego słowa lub podwójnego słowa jest łatwe; wszystko co musimy zrobić to zacząć od bardziej znaczącej komórki pamięci obiektu, który chcemy zanegować i działać w kierunku mniej znaczącego bajtu. Poniższy kod oblicza 128 bitową negację na procesorze 80386:
Niestety, kod ten ma tendencje do stawania się rzeczywiście dużym i wolnym ponieważ musimy rozszerzyć przeniesienie przez wszystkie bardziej znaczące słowa po każdej operacji negacji. Prostszym sposobem zanegowania dużej wartości jest po prostu odjęcie tej wartości od zera:
9.3.7 OPERACJA AND O ROZSZERZONEJ PRECYZJI Wykonanie operacji and na n-słowach jest bardzo proste – po prostu andujemy odpowiadające sobie słowa pomiędzy dwoma operandami, zachowujemy wynik. Na przykład, wykonanie operacji and gdzie wszystkie trzy operandy są długości 32 bitów może wymagać takiego kodu:
Technika ta łatwo rozszerza się na każdą liczbę słów, wszystkie muszą zrobić logiczne andowanie odpowiadających bajtów, słów lub podwójnych słów w odpowiadających operandach. 9.3.8 OPERACJA OR O ROZSZERZONEJ PRECYZJI
Wielosłowna operacja logiczna or wykonuje się w ten sam sposób jak wielosłowna operacja and .Po prostu Orujemy odpowiadające sobie słowa w dwóch operandach. Na przykład, dla logicznego xorowania dwóch 48 bitowych wartości możemy zastosować taki kod:
9.3.9 OPERACJA XOR O ROZSZERZONEJ PRECYZJI Operacja xor o rozszerzonej precyzji wykonywana jest w sposób identyczny jak and /or, po prostu xorujemy odpowiadające sobie słowa w dwóch operandach uzyskując wynik o rozszerzonej precyzji. Poniższa sekwencja kodu działa na dwóch 64 bitowych operandach, oblicza ich exclusive-or i przechowuje wynik w 64 bitowej zmiennej Przykład ten stosuje 32 bitowe rejestry dostępne na 80386 i późniejszych
9.3.10 OPERACJA NOT O ROZSZERZONEJ PRECYZJI Instrukcja not odwraca wszystkie bity w wyszczególnionym operandzie. Nie wpływa na żadną flagę (dlatego też, stosowanie skoku warunkowego po instrukcji not nie ma znaczenia)Not o rozszerzonej precyzji jest wykonywane przez proste wykonanie instrukcji not na wszystkich operandach. Na przykład, dla wykonania 32 bitowej operacji not na wartości w (dx:ax),wszystko co musimy zrobić to wykonać instrukcje: Not ax lub not dx Not dx not ax Zapamiętajmy, że jeśli wykonujemy instrukcję not dwa razy, kończymy z oryginalną wartością. Zauważmy również ,że xorownaie wartości wszystkich jedynek (0FFh,0FFFFh lub 0FF...FFh) wykonuje tą samą operację jak instrukcja not 9.3.11 OPERACJE PRZESUNIĘCIA O PODWYŻSZONEJ DOKŁADNOŚCI Operacje przesunięcia o podwyższonej dokładności wymagają instrukcji przesunięcia i obrotu. Rozważmy co musi zdarzyć się dla implementacji 32 bitowego shl stosując 16 bitowe operacje 1) Zero musi być przesunięte do bitu zero 2) Bity od zera do 14 są przesuwane do następnego wyższego bitu 3) Bit 15 jest przesuwany do bitu 16 4) Bity 16 do 30 muszą być przesunięte do następnego wyższego bitu 5) Bit 31 jest przesuwany do flagi przeniesienia
Rysunek 9.3 Operacja 32 bitowego przesunięcia w lewo Dwie instrukcje możemy zastosować do implementacji tego 32 bitowego przesunięcia to shl i rcl. Na przykład, dla przesunięcia 32 bitowej wielkości w (dx:ax) o jedną pozycję w lewo, zastosujemy takie instrukcje; shl ax, 1 rcl dx, 1 Zauważmy, że możemy tylko przesunąć wartość o podwyższonej dokładności jeden bit na raz. Nie możemy przesunąć operandu o podwyższonej dokładności o kilka bitów stosując rejestr cl lub wartość bezpośrednią większą niż jeden ponieważ liczymy stosując tą technikę. Dla zrozumienia jak ta sekwencja instrukcji pracuje rozważmy działanie tych instrukcji na osobnych podstawach. Instrukcja shl przesuwa zero do bitu zero 32 bitowego operandu a bit 15 do flagi przeniesienia Instrukcja rcl wtedy przesuwa flagę przeniesienia do bitu 16 a bit 31 do flagi przeniesienia. Wynik jest dokładnie tym czego oczekujemy. Wykonanie operacji przesunięcia w lewo na operandzie większym niż 32 bity, to po prostu dodanie dodatkowej instrukcji rcl. Operacja przesunięcia w lewo o podwyższonej dokładności zawsze zaczyna się od najmniej znaczącego słowa a każda następna instrukcja rcl działa na następnym bardziej znaczącym słowie. Na przykład ,dla wykonania operacji 48 bitowego przesunięcia w lewo na komórce pamięci zastosujemy poniższe instrukcje: shl word ptr Operand, 1 rcl word ptr Operand+2, 1 rcl word ptr Operand +4, 1 Jeśli musimy przesunąć nasze dane o dwa lub więcej bitów ,możemy albo powtórzyć powyższą sekwencję żądaną liczbę razy (dla stałej liczby przesunięć) albo możemy umieścić instrukcje w pętli do powtarzania jakąś liczbę razy, Na przykład, poniższy kod przesuwa wartość 48 bitową Operand w lewo o liczbę bitów wyszczególnioną w cx: ShiftLoop: shl word ptr Operand, 1 rcl word ptr Operand+2, 1 rcl word ptr Operand+4, 1 loop ShiftLoop Implementujemy shr i sar w podobny sposób z wyjątkiem tego, że musimy zacząć od bardziej znaczącego słowa operandu i pracować w kierunku mniej znaczącego słowa: DblSAR: sar word ptr Operand+4, 1 rcr word ptr Operand+2, 1 rcr word ptr Operand, 1 DblSHR: shr word ptr Operand+4, 1 rcr word ptr Operand+2, 1 rcr word ptr Operand, 1 Jest jedna główna różnica między przesunięciami o podwyższonej dokładności opisanym tu a ich 8/16 bitowymi odpowiednikami – przesunięcia o podwyższonej dokładności ustawiają flagi inaczej niż operacja o pojedynczej dokładności. Na przykład, flaga zera jest ustawiana jeśli ostatnia instrukcja obrotu tworzy wynik zerowy, a nie jeśli cała operacja przesunięcia tworzy wynik zerowy. dla operacji przesunięcia w prawo, flagi przepełnienia i znaku nie są ustawiane poprawnie (są one ustawiane poprawnie dla przesunięcia w lewo).Dodatkowe testowanie będzie wymagane jeśli musimy przetestować jedną z tych flag po operacji przesunięcia o podwyższonej dokładności. Na szczęście, flaga przeniesienia jest flagą bardzo często testowaną po operacji przesunięcia a instrukcje przesunięcia o podwyższonej dokładności właściwie ustawiają te flagi. Instrukcje shld i shrd pozwalają nam wydajniej implementować przesunięcia o zwielokrotnionej dokładności kilku bitów na 80386 i późniejszych procesorach. Rozważmy poniższą sekwencję kodu:
Pamiętajmy, że instrukcja shld przesuwa bity ze swego drugiego operandu do pierwszego operandu. Dlatego też, pierwsza powyższa instrukcja shld przesuwa bity od ShiftMe+4 do ShiftM+8 bez wpływania na wartość w ShiftMe+4.Druga instrukcja shld przesuwa bity od ShiftMe do ShiftMe+4.W końcu, instrukcja shl przesuwa mniej znaczące podwójne słowo o stosowną wielkość. Są dwie ważne rzeczy do odnotowania o tym kodzie. Po pierwsze, w odróżnieniu od innych operacji przesunięcia w lewo o podwyższonej dokładności, ta sekwencja pracuje od bardziej znaczącego podwójnego słowa do mniej znaczącego słowa. Po drugie, flaga przeniesienia nie zawiera przeniesienia z bardziej znaczącej operacji przesunięcia. Jeśli musimy zachować flagę przeniesienia w tym punkcie ,będziemy musieli przenieść na stos flagi po pierwszej instrukcji shld i ściagnąć ze stosu po instrukcji shl. Możemy wykonać operację przesunięcia w prawo o podwyższonej dokładności stosując instrukcję shrd. Pracuje prawie w ten sam sposób jak powyższa sekwencja kodu z wyjątkiem działania od mniej znaczącego podwójnego słowa do bardziej znaczącego podwójnego słowa 9.3.12 OPERACJE OBROTU O PODWYŻSZONEJ DOKŁADNOŚCI Rozszerzone operacje rcl i rcr działają w sposób prawie identyczny jak shl i shr .Na przykład dla wykonanie 48 bitowej operacji rcl i rcr, stosujemy poniższe instrukcje:
Jedyna różnica pomiędzy tym kodem a kodem dla operacji przesunięcia o podwyższonej dokładności jest taka, że pierwszą instrukcją jest rcl lub rcr zamiast instrukcje shl lub shr .Wykonywanie instrukcji rol i ror o podwyższonej dokładności nie jest tak prostą operacją. Na procesorach 80386 i późniejszych, możemy zastosować instrukcje bt, shld i shrd dla łatwiejszej implementacji instrukcji ror i rol o podwyższonej dokładności. Poniższy kod pokazuje jak użyć instrukcji shld do wykonania rol o podwyższonej dokładności: ;Oblicza ROL EDX:EAX, 4 mov ebx, edx shld edx,eax, 4 shld eax, ebx, 4 bt eax, 0 ;ustawienie flagi przeniesienia Instrukcja ror o podwyższonej dokładności jest podobna; zapamiętajmy, że pracuje najpierw na najmniej znaczącym końcu obiektu, a na bardziej znaczącym jako ostatnim. 9.4 DZIAŁANIA NA OPERANDACH O RÓZNYCH WYMIARACH Czasami możemy musieć obliczać jakieś wartości pary operandów, które nie są tego samego rozmiaru. Na przykład, możemy chcieć dodać słowo i podwójne słowo razem lub odjąć wartość bajtu z wartością słowa. Rozwiązanie jest proste: rozszerzenie małego operandu do rozmiaru operandu dużego a potem wykonanie operacji na dwóch rozmiarowo podobnych operandach.. Dla operandów ze znakiem ,powielimy znak mniejszego operandu do rozmiaru większego operandu; dla wartości bez znakowej, powielimy zero mniejszego operandu .Działa to dla każdej operacji, chociaż poniższy przykład demonstruje to dla operacji dodawania. Dla rozszerzenia mniejszego operandu do wielkości dużego operandu stosujemy operację rozwinięcie znaku lub zera (w zależności czy dodajemy wartości ze znakiem czy bez znaku).Kiedy poszerzymy mniejszą wartość do rozmiaru większej, możemy kontynuować dodawanie .Rozważmy poniższy kod, który dodaje wartość bajtu do wartości słowa: var1 var2
byte word
Dodawanie bez znakowe: mov al., var1 mov ah,0 add ax, var2
? ? Dodawanie ze znakiem: mov al., var1 cbw add ax, var2
W obu przypadkach, zmienna bajtowa została załadowana do rejestru al., rozszerzona do 16 bitów i potem dodana do operandu word. Kod ten powiedzie się rzeczywiście dobrze, jeśli możemy wybrać porządek operacji (np. dodanie wartości ośmiobitowej do wartości szesnastobitowej) Czasami ,nie możemy wyszczególnić. Być może szesnasto bitowa wartość jest już w rejestrze ax a my chcemy dodać do niej wartość ośmiobitową. Dla dodawania bez znakowego, możemy zastosować poniższy kod: mov ax, var2 ;ładujemy 16 bitową wartość do AX ;robimy jakieś inne operacje pozostawiając ;16 bitową wielkość w AX add al., var1 ;dodajemy wartość ośmio bitową adc ah, 0 ;dodajemy przeniesienie do bardziej znaczącego słowa Pierwsza instrukcja add w tym przykładzie dodaje bajt var1 do mniej znaczącego bajtu wartości w akumulatorze. instrukcja adc dodaje przeniesienie z miej znaczącego bajtu do bardziej znaczącego bajtu akumulatora. Musimy być pewni, że instrukcja adc jest obecna .Jeśli ją opuścimy, możemy nie otrzymać prawidłowego wyniku. Dodanie ośmiobitowego operandu ze znakiem do wartości szesnastobitowej ze znakiem jest trochę trudniejsze. Niestety, nie możemy dodać wartości bezpośrednio (jak powyżej) do bardziej znaczącego słowa w ax. Jest tak ponieważ bardziej znaczący rozszerzony bajt może być albo 00h albo 0FFh.Jeśli rejestr jest wolny ,najlepszą rzecz jaka to zrobi to: mov bx, ax ,BX jest wolnym rejestrem mov al., var1 cbw add ax, bx Jeśli dodatkowy rejestr nie jest wolny, możemy spróbować poniższego kodu: add al., var1 cmp var1, 0 jge add0 adc ah, 0FFh jmp addedFF add0: adc ah, 0 addedFF: Oczywiście ,jeśli inny rejestr nie jest wolny, możemy zawsze odłożyć go na stos i zapisać go podczas wykonywania operacji np. push bx mov bx, ax mov al.,var1 cbw add ax, bx pop bx Inną alternatywą jest przechowanie 16 bitowej wartości z akumulatora w komórce pamięci a potem kontynuowanie jak przedtem: mov temp, ax mov al., var1 cbw add ax, temp Wszystkie powyższe przykłady dodają wartość bajtową do wartości słowa .Poprzez rozszerzenie zera lub znaku mniejszego operandu do rozmiaru operandu większego, możemy łatwo dodać każde dwie różnorozmiarowe zmienne razem. Rozważmy poniższy kod, który dodaje operand bajtowy ze znakiem do podwójnego słowa ze znakiem: var1 var2
byte dword
? ?
mov al., var1 cbw cwd ;rozszerzenie do 32 bitów w DX add ax, word ptr var2 adc dx, word ptr var2+2 Oczywiście, jeśli mamy procesor 80386 lub późniejszy ,możemy zastosować poniższy kod: movsx eax, var1
add eax, var2 Przykładem bardziej odpowiednim dla 80386 jest dodawanie wartości ośmiobitowej do poczwórnego słowa (64 bity),rozważmy poniższy kod: Bval Qval
byte qword
-1 1
movsx cdq add adc
eax, Bval eax, dword ptr Qval edx, dword ptr Qval+4
9.5 IDIOMY MASZYNOWE I ARYTMETYCZNE Idiom jest dziwactwem. Kilka operacji arytmetycznych i instrukcji 80x86 ma dziwactwa, które możemy wykorzystać kiedy piszemy kod języka asemblera. Niektórzy odnoszą się do stosowania maszynowych i arytmetycznych idiomów jako „skomplikowanego programowania”, którego powinniśmy zawsze unikać w dobrze napisanych programach. Podczas gdy mądrze jest unikać sztuczek, właśnie przez wzgląd na sztuczki, wiele maszynowych i arytmetycznych idiomów jest dobrze znanych i powszechnie znajdowanych w programach asemblerowych. Niektóre z nich rzeczywiście mogą być skomplikowane, ale większość to proste kuglarskie sztuczki. Ten tekst nawet nie może zacząć przedstawiać wszystkich idiomów ,które znajdują się dzisiaj w użyciu.; są one zbyt liczne ,a lista ich jest stale zmieniana .Niemniej jednak, jest kilka bardzo ważnych idiomów, które będziemy widzieli cały czas, wiec jest sens aby je omówić. 9.5.1 MNOŻENIE BEZ MUL I IMUL Jeśli spojrzymy na czas wykonania dla instrukcji mnożenia, zauważymy, że czas wykonania tych instrukcji jest dosyć długi. Tylko instrukcje div i idiv wykonują się dłużej na 8086.kiedy mnożymy przez stałą, możemy uniknąć spadku wydajności instrukcji mul i imul przez użycie przesunięć, dodawań i odejmowań dla wykonania mnożenia. Pamiętamy, że operacja shl wykonuje tą samą operację jak mnożenie wyszczególnionego operandu przez dwa. Przesunięcie w lewo o dwa bity mnoży operand przez cztery .Przesunięcie w lewo o trzy bity mnoży operand przez osiem. Ogólnie, przesunięcie operandu w lewo o n bitów mnoży go przez 2n.Kazda wartość może być pomnożona przez jakąś stałą, używając serii przesunięć i dodawań lub przesunięć i odejmowań. .Na przykład, pomnożenie rejestru ax przez dziesięć ,potrzebujemy tylko pomnożyć go przez osiem a potem dodać dwa razy pomnożoną wartość oryginalną. To znaczy 10*ax =8*ax+2*ax.Kod który to wykonuje: shl ax, 1 ,mnożenie ax przez dwa mov bx, ax ;zachowanie 2 * AX na później shl ax, 1 ,mnożenie ax przez cztery shl ax, 1 ;mnożenie AX przez osiem add ax, bx ;dodanie 2*AX aby otrzymać 10*AX Rejestr ax (albo inny przeznaczony do tego celu) może być pomnożony przez wartości stałe dużo szybciej stosując shl niż przez stosowanie instrukcji mul .Może się to wydawać trudne do uwierzenia ponieważ zabiera ona tylko dwie instrukcje do obliczenia wyniku: mov bx, 10 mul bx Jednak, jeśli spojrzymy na czas wykonania ,przykład z przesunięciami i dodawaniem wymaga mniej cykli zegarowych na większości procesorów z rodziny 80x86 niż instrukcja mul .Oczywiście, kod jest dłuższy (o kilka bajtów),ale poprawa wydajności jest zazwyczaj warta tego. Oczywiście na procesorach późniejszych 80x86,instrukcja mul jest trochę szybsza niż na procesorach wcześniejszych, ale schemat przesunięcie i dodawanie jest generalnie szybszy również na tych procesorach. Możemy również zastosować odejmowanie z przesunięciem dla wykonania operacji mnożenia. Rozważmy mnożenie przez siedem: mov bx, ax ;zachowanie AX*1 shl ax, 1 ;AX := AX*2 shl ax, 1 ;AX := AX*4 shl ax, 1 ;AX := AX*8 sub ax, bx ;AX :=AX*7 To wynika bezpośrednio z faktu, że ax*7 = (ax*8)-ax Powszechnym błędem robionym przez początkujących studentów języka asemblera jest odejmowanie lub dodawanie jeden lub dwa zamiast ax*1 lub ax*2.To poniżej nie obliczy ax*7: shl ax, 1
shl ax, 1 shl ax, 1 sub ax, 1 Obliczymy (8*ax)-1,coś całkowicie innego (chyba, że ax = 1). Wystrzegajmy się takich pułapek, kiedy stosujemy przesunięcia, dodawanie i odejmowanie dla wykonani operacji mnożenia. Możemy również zastosować instrukcję lea do obliczenia pewnych iloczynów na procesorze 80386 i późniejszych. Sztuczką jest zastosowanie trybu skalowanego z indeksowaniem. Poniższy przykład demonstruje kilka prostych przypadków: lea eax, [ecx][ecx] ;EAX := ECX*2 lea eax, [eax][eax*2] ;EAX := EAX*3 lea eax, [eax*4] ;EAX := EAX*4 lea eax, [ebx][ebx*4] ;EAX := EBX*5 lea eax, [eax*8] ;EAX := EAX*8 lea eax, [edx][edx*8] ;EAX := EDX*9 9.5.2 DZIELENIE BEZ DIV I IDIV Podobnie jak instrukcja shl może być zastosowana dla zasymulowanie mnożenia przez jakąś potęgę dwójki, tak instrukcje shr i sar mogą być zastosowane do zasymulowania dzielenia przez potęgę dwójki. Niestety, nie możemy zastosować przesunięcia, dodawania i odejmowania dla wykonania dzielenia przez dowolną stałą, tak łatwo jak można zastosować te instrukcje do wykonania operacji mnożenia. Innym sposobem wykonania dzielenia jest użycie instrukcji mnożenia. możemy podzielić jakąś wartość przez pomnożenie przez jej odwrotność .Instrukcja mnożenia jest odrobinę szybsza niż instrukcja dzielenia; mnożenie przez odwrotność jest prawie zawsze szybsze niż dzielenie. Teraz prawdopodobnie zastanawiamy się „jak pomnożyć przez odwrotność kiedy wartości ,którymi się Zajmujemy wszystkie są całkowite? ”Odpowiedź, oczywiście jest taka, że musimy zrobić to oszukańczo. Jeśli chcemy pomnożyć przez jedną dziesiątą, nie mam sposobu załadowania wartości 1/10 do rejestru 80x86 przed wykonaniem dzielenia .Jednakże, możemy pomnożyć 1/10 przez 10,wykonując mnożenie a potem dzieląc wynik przez 10 uzyskamy wynik końcowy .Oczywiście, to nie doprowadziłoby do niczego, faktycznie rzeczy zrobiły się gorsze ponieważ teraz robimy mnożenie przez dziesięć również jako dzielenie przez dziesięć. Jednak przypuśćmy, że mnożymy 1/10 przez 65,536(6553),wykonujemy mnożeni a potem dzielimy przez 65,536.To będzie jeszcze poprawnie wykonana operacja i ,okazuje się, jeśli prawidłowo założymy problem, możemy otrzymać operację dzielenia za darmo. Rozważmy poniższy kod, który dzieli ax przez dziesięć: mov dx, 6554 ;zaokrąglenie (65,536 / 10) mul dx Kod ten pozostawi ax/10 w rejestrze dx. Dla zrozumienia jak to działa, rozpatrzmy co wydarzy się ,kiedy pomnożymy ax przez 65,536 (10000h).Po prostu ax jest przenoszone do dx i ustawiane na zero. Pomnożenie przez 6,554 (65,536 \dzielone przez dziesięć) wkłada ax podzielone przez 10 do rejestru dx. Ponieważ mul jest tylko nieznacznie szybsza niż div, ta technika działa trochę szybciej kiedy używamy prostego dzielenia. Mnożenie przez odwrotność pracuje dobrze kiedy musimy dzielić przez stałą. możemy nawet zastosować ją do dzielenia przez zmienną, ale koszty obliczania odwrotności opłacą się jeśli wykonanym dzielenie wiele, wiele razy (przez tą samą wartość) 9.5.3 STOSOWANIE AND DO OBLICZANIA RESZTY Instrukcja and może być zastosowana do szybkiego obliczania reszt z postaci: przez. := przez. MOD 2n Dla obliczenia reszt stosując instrukcję and, po prostu andujemy operand z wartością 2n-1.Na przykład, dla obliczenia ax = ax mod 8 po prostu używamy instrukcji and ax, 7 Dodatkowe przykłady: and ax, 3 ;AX := AX mod 4 and ax, 0Fh ;AX := AX mod 16 and ax, 1Fh ;AX := AX mod 32 and ax, 3Fh ;AX := AX mod 64 and ax, 7Fh ;AX := AX mod 128 mov ah, 0 ;AX := AX mod 256 ;(to samo co ax and 0FFH) 9.5.4 IMPLEMENTACJA LICZNIKA MODULO –n Z AND
Jeśli chcemy zaimplementować licznik zmiennej, który zlicza w górę do 2n-1 a potem resetuje do zera, po prostu użyjemy następującego kodu: inc CounterVAr and CounterVAr, nBits gdzie nBits jest binarną wartością zawierającą n bitów jedynek wyrównanych w liczbie do prawej .na przykład, dla stworzenie licznika ,który obraca się od zera do piętnastu, użyjemy poniższy kod inc CounterVAr and CounterVAr, 00001111b 9.5.5 TESTOWANIE WARTOŚCI O PODWYŻSZONEJ DOKŁADNOŚCI DLA 0FFFF..FFh Instrukcja and może być zastosowana do szybkiego sprawdzenia wartości wielosłowa aby zobaczyć czy zawiera jedynki na wszystkich pozycjach bitów. Po prostu ładujemy pierwsze słowo do rejestru ax a potem logicznie andujemy rejestr ax ze wszystkimi pozostałymi słowami w strukturze danych. Kiedy operacja and jest skończona ,rejestr ax będzie zawierał 0FFFFh jeśli i tylko jeśli wszystkie słowa w strukturze zawierały 0FFFFh,N.p: mov ax, word ptr var and ax, word ptr var+2 and ax, word ptr var+4 and ax, word ptr var+n cmp ax, 0FFFFh je Is0FFFFh 9.5.6 OPERACJE TEST Pamiętamy, że instrukcja test jest instrukcją and, która nie zachowuje wyniku operacji (inaczej niż ustawianie flag).Dlatego też, wiele uwag dotyczących operacji and (zwłaszcza ze względu na sposób wpływania na flagi) również dotyczy instrukcji test. Jednakże, ponieważ instrukcja test nie wpływa na operand przeznaczenia, wielokrotne testowanie bitów może być wykonywane na tej samej wartości. Rozważmy poniższy kod: test ax, 1 jnz Bit0 test ax, 2 jnz Bit1 test ax, 4 jnz Bit3 itd. Kod ten może być zastosowany do następującego po sobie testowania każdego bitu rejestrze ax (lub każdego innego operandu dla tego celu. Zauważmy, że nie możemy zastosować pary instrukcji test /cmp do testowania dla określonej wartości wewnątrz ciągu bitów (jednak możemy użyć instrukcje and /cmp).Ponieważ test nie usuwa żadnych niepotrzebnych bitów, instrukcja cmp w rzeczywistości będzie porównywała wartości oryginalne zamiast wartości usuniętych. Z tego powody, zwykle stosujemy instrukcję test aby zobaczyć czy pojedynczy bit jest ustawiony lub czy jeden lub więcej bitów z grupy bitów są ustawione. Oczywiście, jeśli mamy procesor 80386 lub późniejsze, możemy również użyć instrukcji bt do testowania pojedynczych bitów operandzie. Innym ważnym zastosowaniem instrukcji test jest efektywne porównanie rejestru z zerem. Poniższa instrukcja test ustawia flagę zera jeśli i tylko jeśli ax zawiera zero test ax, ax Instrukcja test jest krótsza niż cmp ax, 0 lub cmp eax, 0 chociaż nie jest lepsza niż cmp al.,0 Zauważmy, że możemy zastosować instrukcje and i or do testowania dla zera w sposób identyczny jak test. Jednakże, na procesorach potokowych, takich jak 80486 i Pentium, przy instrukcji test jest mniejsze prawdopodobieństwo do stworzenia ryzyka ponieważ nie przechowuje wyniku w swoim rejestrze przeznaczenia.
9.5.7 TESTOWANIE ZNAKÓW INSTRUKCJĄ XOR Pamiętamy ból związany z operacją mnożenia ze znakiem o zwielokrotnionej precyzji? Potrzebujemy określić znak wyniku, bierzemy wartości absolutne operandów, potem je mnożymy, a potem poprawiamy znak wyniku na określony przed operacją mnożenia. Znak iloczynu dwóch liczb jest po prostu exclucsive-or ich znaków przed wykonaniem mnożenia. Dlatego też, możemy użyć instrukcji xor do określenia znaku iloczynu dwóch liczb o podwyższonej dokładności. Np. 32x32 Mnożenie: mov al., byte ptr Oprnd1+3 xor al., byte ptr Oprnd2+3 mov cl, al. ;zachowaj znak ;Tu robimy mnożenie (nie zapomnijmy wziąć wartości tych dwóch operandów przed wykonaniem mnożenia) ;teraz ustalamy znak cmp cl, 0 ;sprawdzamy bit znaku jns resultIsPos ;Negujemy tu iloczyn ResultIs Pos: 9.6 OPERACJE MASKOWANIA Maska jest wartością używaną do wymuszenia pewnych bitów na zero lub jeden wewnątrz jakiejś innej wartości. Maska typowo wpływa na pewne bity w operandzie i pozostawiają inne bity nienaruszone. odpowiednie zastosowanie maski pozwala nam wyekstrahować bity z wartości, wprowadzić bity do wartości i zapakować i wypakować upakowany typ danych. Poniższa sekcja opisuje te operacje szczegółowo 9.6.1 OPERACJE MASKOWANIA Z INSTRUKCJĄ AND Jeśli spojrzymy na tablicę prawdy dla operacji and w Rozdziale Pierwszym, zauważymy, że jeśli ustalimy operand na zero, wynik jest zawsze zerem. Jeśli ustawimy ten operand na jeden, wynik jest zawsze wartością drugiego operandu, Możemy zastosować tą właściwość instrukcji and do selektywnego wymuszania pewnych bitów na zero w wartości bez wpływania na pozostałe bity. Nazywa się to maskowaniem bitów. Dla przykładu rozpatrzymy kody ASCII dla cyfr „0”..”9”.Ich kody są odpowiednio z zakresu 30h..39h.Aby skonwertować cyfry ASCII do ich odpowiednich wartości numerycznych musimy odjąć30h od kodu ASCII. Jest to łatwe do wykonania poprzez logiczne dodanie wartości 0Fh.To ustawia wszystko na zero, ale cztery mniej znaczące bity tworzą wartość liczbową. Może zastosujemy instrukcje odejmowania ,ale większość ludzi do tego celu stosuje instrukcję and. 9.6.2 OPERACJE MASKOWANIA Z INSTRUKCJĄ OR Podobnie jak możemy zastosować instrukcję and do wymuszenia wybranych bitów na zero, możemy użyć instrukcji or do wymuszenia wybranych bitów na jeden. Pamiętamy operację maskowania popisaną wcześniej przy instrukcji and? W tamtym przykładzie chcieliśmy skonwertować kod ASCII do cyfr jako jej liczbowego ekwiwalentu .Możemy zastosować instrukcję or dla odwrócenia tego procesu. To znaczy ,konwertujemy wartość liczbową z zakresu 0..9 do kodu ASCII odpowiadającemu cyfrze tj. ‘0’..’9’.Zrobimy to orując logicznie wyszczególnioną wartość liczbową z 30h. 9.7 PAKOWANIE I WYPAKOWYWANIE TYPÓW DANYCH Jednym z podstawowych zastosowań instrukcji przesunięcia i obrotu jest pakowanie i wypakowywanie danych. Typy danych bajtu i słowa są wybierane dużo częściej niż inne ponieważ 80x86 wspiera te dwa rozmiary danych sprzętowo. jeśli nie potrzebujemy dokładnie ośmiu lub 16 bitów, stosowanie bajtu lub słowa do przechowywania danych może być rozrzutnością. Poprzez upakowanie danych możemy zredukować pamięć wymaganą dla naszej danej poprzez wstawienie dwóch lub więcej wartości do pojedynczego bajtu lub słowa .Kosztem takiej redukcji w pamięci jest niższa wydajność. Zabiera czas pakowanie i wypakowywanie danych .Pomimo to, dla aplikacji, dla których szybkość nie jest krytyczna (lub dla tych części aplikacji, dla których szybkość nie jest krytyczna),oszczędność pamięci może uzasadniać zastosowanie danych upakowanych. Typ danych, który oferuje największe oszczędności kiedy stosujemy technikę pakowania, jest boolowski typ danych. Do przedstawienia prawdy lub fałszu wymaga pojedynczego bitu. Dlatego też, osiem różnych wartości boolowskich może być upakowanych w pojedynczym bajcie. To przedstawia współczynnik
kompresji 8:1,dlatego upakowana tablica wartości boolowskich wymaga tylko jedną ósmą miejsca tablicy nie upakowanej(gdzie każda boolowska zmienna zużywa jeden bajt).Na przykład pascalowska tablica B;packed arreay[0..31] of boolean Wymaga tylko czterech bajtów, kiedy pakujemy jedną wartość na bit. Kiedy pakujemy jedną wartość na bajt, tablica ta wymaga 32 bajtów. Zajęcie się spakowaną tablicą boolowską wymaga dwóch operacji. Będziemy musieli wprowadzić wartość do pakowanej zmiennej (często zwanej polem upakowania) i będziemy musieli wyciągnąć wartość z pola upakowania . Aby wprowadzić wartości do upakowanej tablicy boolowskiej musimy ustawić bit źródłowy na jego pozycji w operandzie przeznaczenia a potem przechować ten bit w operandzie przeznaczenia .Możemy to zrobić sekwencją instrukcji and, or i przesunięć. Pierwszym krokiem jest zamaskowanie odpowiednich bitów w operandzie przeznaczenia. Do tego celu używamy instrukcję and. Potem operand źródłowy jest przesuwany, żeby był ustawiony na pozycji przeznaczenia, w końcu operand źródłowy jest orowany z operandem przeznaczenia. Na przykład, jeśli chcemy wstawić bit zero rejestru ax do bitu pięć rejestru cx, zastosujemy poniższy kod: and cl, 0DFh ;zerujemy bit pięć (bit przeznaczenia) and al.,1 ;zeruje wszystkie bity za wyjątkiem bitu źródłowego ror al., 1 ;przesuwa do bitu 7 shr al., 1 ;przesuwa do bitu 6 shr al., 1 ;przesuwa do bitu 5 or cl, al.
Rysunek 8.4 Dane upakowane Kod ten jest nieco skomplikowany .Obraca dane w prawo zamiast przesuwać je w lewo ponieważ wymaga to mniej instrukcji przesunięć i obrotów. Aby wyciągnąć wartość boolowską, po prostu odwracamy ten proces. Po pierwsze, przesuwamy żądany bit do bitu zero a potem maskujemy wszystkie inne bity. Na przykład, dla wyciągnięcia danych z bitu pięć rejestru cx, pozostawiamy pojedynczą wartość boolowską w bicie zero rejestru ax, stosujemy poniższy kod: mov al. , cl shl al., 1 ;Bit 5 do bitu 6 shl al., 1 ;Bit 6 do bitu 7 rol al., 1 ;Bit 7 do bitu 0 and ax, 1 ;zerujemy wszystkie bity z wyjątkiem zero Dla testowania zmiennych boolowskich w upakowanej tablicy, nie potrzebujemy wyciągać bitu a potem go testować ,możemy przetestować go na miejscu. Na przykład dla przetestowania wartości w bicie pięć, dla sprawdzenia czy jest tam zero czy jeden ,zastosujemy poniższy kod: test cl, 00100000b jnz BitIsSet Inne typy danych upakowanych mogą być obsługiwane w podobny sposób z wyjątkiem, kiedy musimy pracować z dwoma lub więcej bitami. Na przykład przypuśćmy, że upakowaliśmy pięć różnych trzybitowych pól do sześciobitowej wartości jak pokazano na rysunku 8.4. Jeśli rejestr ax zawiera dane pakowane do value3,możemy użyć poniższego kodu do wprowadzenia tej danej do pola trzy: mov ah, al. ;robi shl przez 8 shr ax, 1 ;repozycjonowanie do bitów 6..8 shr ax, 1 and ax, 11100000b ;usunięcie niepożądanych bitów and DATA, 0FE3Fh ;ustawienie żądanego pola na zero or DATA, ax ;przyłączenie nowej danej do pola wyłuskanie jest dokonywane w podobny sposób Najpierw usuwamy niepotrzebne bity a potem uzyskujemy wynik: mov ax, DATA
and ax, 1Ch shr ax, 1 shr ax, 1 shr ax, 1 shr ax, 1 shr ax, 1 shr ax, 1 Kod ten może być poprawiony poprzez zastosowanie poniższej sekwencji kodu: mov ax, DATA shl ax, 1 shl ax, 1 mov al. Ah and ax, 07h Dodatkowe zastosowanie danych upakowanych będzie zgłębiane przez całą książę. 9.8 TABLICE Termin „tablice” ma różne znaczenia dla różnych programistów .Dla większości programistów asemblerowych, tablica nie jest niczym więcej niż tablicą, która jest inicjowana jakąś daną. Programista asemblerowy często stosuje tablice do obliczania złożonych lub inaczej wolnych funkcji. Bardzo wiele języków wysokiego poziomu (np SNOBOLA4 i Icon) bezpośrednio wspierają tablicowy typ danych. Tablice w tych językach są zasadniczo tablicami, do elementów których możemy uzyskać dostęp z wartościami nie całkowitymi (np. zmienno przecinkowymi, ciągami lub innymi typami danych).W sekcji tej ,zaadoptujemy spojrzenie programistów asemblerowych na tablice. Tablica jest tablicą zawierającą preinicjowane wartości, które nie zmieniają się podczas wykonywania programu. Tablica może być porównana do tablicy w ten sam sposób, jak stała całkowita może być porównana do zmiennej całkowitej .W asemblerze, możemy zastosować tablice do różnych celów; obliczania funkcji, sterownia przepływem danych .Ogólnie, tablice dostarczają szybkiego mechanizmu dla wykonani jakichś operacji kosztem przestrzeni w naszym programie (dodatkowa przestrzeń przechowuje dane tablicowe).W poniższej sekcji, zgłębimy możliwości zastosowania tablic w programach asemblerowych. 9.8.1 OBLICZANIE FUNKCJI PRZEZ PRZEKSZTAŁCENIE TABLICOWE Tablice mogą robić różne rzeczy w asemblerze. W HLL’ach, takich jak Pascal, łatwo jest w rzeczywistości stworzyć formułę, która oblicza jakąś wartość .Proste wyrażenie arytmetyczne jest odpowiednikiem znacznej ilości kodu języka asemblerowego 80x86.Programiści języka asemblera mają tendencję do obliczania wielu wartości poprzez przekształcenia tablicowe zamiast przez wykonanie jakiejś funkcji ..Ma to zaletę bycia łatwiejszym i często również bardziej wydajnym. Rozważmy poniższą instrukcję pascalowską: if (znak >= ‘a’) and (znak <= ‘z’) then znak := chr(ord(znak) – 32); Ta Pascalowska instrukcja if konwertuje zmienną znak z małej litery do dużej litery jeśli znak jest z zakres ‘a’..’z’.Kod asemblerowy 80x86,który wykonuje to samo jest taki: mov al., znak cmp al., ‘a’ jb NowLower cmp al., ‘z’ ja NotLower and al., 05fh ;tak sama operacja jak SUB AL., 32 NotLower: mov znak, al. Możemy schować ten kod w pętli zagnieżdżonej, jednak będzie trudno uzyskać poprawę szybkości kodu bez użycia przekształcenia tablicowego. Stosowanie przekształcenie tablicowe pozwala nam na zredukowanie tej sekwencji kodu tylko do czterech instrukcji: mov al., znak lea bx, CnvrtLower xlat mov znak, al. CnvrtLower jest 256 bajtową tablicą, która zawiera wartości 0..60h pod indeksami 0..60h,41h..5Ah pod indeksami 61h..7Ah i 7Bh..0FFh.Często,stosujac to udogodnienie przekształcenia tablicowego zwiększymy szybkość naszego kodu. Ponieważ zwiększa się złożoność funkcji, korzyści z metody przekształcenia tablicowego wzrastają gwałtownie. Kiedy prawie wcale nie stosujemy tablicy wyszukiwań dla konwersji małych liter na duże ,rozpatrzmy co się stanie jeśli chcemy zamienić przypadek:
Przez obliczenie:
NotLower:
mov cmp jb cmp ja and jmp cmp jb cmp ja or
al., znak al., ‘a’ NotLower al., ‘z’ Notlower al., 05fh ConvertDone al., ‘A’ ConvertDone al., ‘Z’ ConvertDone al., 20h
mov
znak, a;
ConvertDone: Kod przekształcenia tablicowego oblicza tą sama funkcję tak: mov al., znak lea bx, SwapUl xlat mov znak, al. Jak widać, kiedy obliczamy funkcję przez przekształcenie tablicowe, bez względu jak jest funkcja ,zmienia się tablica, nie kod wykonujący wyszukiwanie. Przekształcenie tablicowe cierpi tylko z jednego głównego powodu – funkcje obliczane przez przekształcenia tablicowa mają ograniczony zakres działania. Zakresem działania funkcji jest zbiór możliwych wartości wejściowych (parametry),które akceptuje. Na przykład, powyższa funkcja konwersji duże/ małe litery ma 256 znakowy zbiór znaków ASCII jako swój zakres działania. Funkcje takie jak SIN czy COS akceptują zbiór liczb rzeczywistych jako możliwe wartości wejściowe. Jasne ,że zakres działania dla SIN i COS jest dużo większy niż dla funkcji konwersji duże/małe litery. Jeśli mamy zamiar zrobić obliczenia przez przekształcenie tablicowe ,musimy ograniczyć zakres działania funkcji do małego zbioru. Jest tak ponieważ każdy element z zakresu działania funkcji wymaga wejścia w tablicy wyszukiwań. Nie będziesz uważał za bardzo praktyczną implementację funkcji która korzysta z przekształceń tablicowych ,której zakresem działania jest zbiór liczb rzeczywistych. Większość tablic wyszukiwań jest całkiem małych, zazwyczaj 10 do 128 wejść. Rzadko tablica przekształceń wzrasta poza 1000 wejść. Większość programistów nie ma cierpliwości do tworzenia (i weryfikacji poprawności) 1000 wejść do tablicy. Innym ograniczeniem funkcji opartych o tablice wyszukiwań jest to, że elementy w zakresie działania funkcji muszą być dość przyległe. Przekształcenie tablicowe pobiera wartość wejściową dla funkcji, używając tej wartości wejściowej jako indeksu tablicy i zwraca wartość do tego wejścia w tablicy. Jeśli nie przekazujemy funkcji żadnej wartości innej niż 0,100,1000 i 10 000,będzie się wydawała idealnym kandydatem do implementacji przez przekształcenie tablicowe, jej zakres działania składa się tylko z czterech pozycji. Jednakże ,tablica w rzeczywistości będzie wymagała 10 001 różnych elementów należących do zakresu wartości wejściowych. Dlatego też, nie możemy sprawnie tworzyć takiej funkcji przez przekształcenie tablicowe. W całej sekcji o tablicach ,zakładamy ,że zakres działania funkcji jest dosyć ciągłym zbiorem wartości. Najlepszymi funkcjami, które mogą być implementowana poprzez przekształcenia tablicowe są te, których zakres działania jest zawsze 0..255 (lub jakiś podzbiór tego zakresu).Takie funkcje są wydajniej implementowane na 80x86 przez instrukcje XLAT. Podprogram konwersji duże/małe litery przedstawiony wcześniej jest dobrym przykładem takiej funkcji. Każda funkcja w tej klasie (te których zakres jest od 0 do 255) może być obliczana przez zastosowanie tak samo dwóch instrukcji (lea bx, table / xlat).jedyną rzeczą która zawsze się zmienia jest tablica wyszukiwań. Instrukcja xlat nie może być (dogodnie) zastosowana do obliczenia wartości funkcji ,której zakres działania lub zakres wychodzi poza zakres 0..255.Są trzy sytuacje do rozpatrzenia: • Obszar stosowania wychodzi poza 0..255 ale zakres jest wewnątrz 0..255 • Obszar stosowani jest wewnątrz0..255 ale zakres jest poza 0.255, i • Oba, obszar stosowania i zakres funkcji mają wartości poza 0..255 Rozpatrzymy każdy z tych przypadków z osobna Jeśli obszar stosowania funkcji wychodzi poza 0.255 ale zakres funkcji mieści się wewnątrz tego zbioru wartości, nasza tablica wyszukiwań będzie wymagała więcej niż 256 wejść bale możemy przedstawić
każde wejście pojedynczym bajtem. Dlatego też, tablica wyszukiwań może być tablicą bajtów. Obok połączenia wymagającego instrukcji xlat ,funkcje mieszczące się w tej klasie są bardziej wydajne. Poniższa Pascalowska funkcja B: = Func(X); Gdzie Func to function Func (X:word):byte składa się z następującego kodu 80x86 mov bx, X mov al., FuncTable [bx] mov B, al. Kod ten ładuje parametr funkcji do bx ,stosując tą wartość (z zakresu 0..??),jako indeks do tablicy FuncTable, pobierając bajt spod tej lokacji i przechowując wynik w B. Oczywiście tablica musi zawierać poprawne wejścia dla każdej możliwej wartości X. Na przykład przypuśćmy, że chcieliśmy zmapować pozycję kursora na wyświetlaczu w zakresie 0.1999 (są 2 000 pozycje znaki na wyświetlaczu 80x25) do koordynaty X lub Y na ekranie. Możemy łatwo obliczyć koordynatę X przez funkcję X:= Posn mod 80 i koordynatę Y z formuły Y:= Posn div 80 (gdzie Posn jest pozycją kursora na ekranie)Jest to łatwe do obliczenia stosując kod 80x86: mov bl, 80 mov ax, Posn div bx ; X jest teraz w AH,Y jest w AL. Jednakże, instrukcja div na 80x86 jest bardzo wolna. Jeśli musimy robić to obliczenie dla każdego znaku napisanego na ekranie, poważnie zmniejszymy szybkość kodu naszego wyświetlacza .poniższy kod, który realizuje te dwie funkcje przez przekształcenie tablicowe, poprawi znacznie wydajność naszego kodu : mov bx, Posn mov al., Ycoord[bx] mov ah, Xcoord[bx] Jeśli obszar stosowania funkcji jest wewnątrz 0.255 ale zakres jest poza tym zbiorem, tablica wyszukiwań będzie zawierała 256 lub mniej wejść ale każde wejście będzie wymagało dwóch lub więcej bajtów. Jeśli oba zakres i obszar stosowania funkcji są poza 0.255,każde wejście będzie wymagało dwóch lub więcej bajtów a tablica będzie zawierała więcej niż 256 wejść. Przypomnijmy sobie z Rozdziału Czwartego formułę dla indeksowania jednowymiarowej tablicy (której tablica jest specjalnym przypadkiem): Adres: = Baza + indeks* rozmiar Jeśli elementy z zakresu funkcji wymagają dwóch bajtów, wtedy indeks musi być pomnożony przez dwa przed indeksowaniem tablicy. Podobnie, jeśli każde wejście wymaga trzech ,czterech lub więcej bajtów ,indeks musi być pomnożony przez rozmiar każdego wejścia tablicy przez zastosowaniem jako indeksu do tablicy .Na przykład przypuśćmy, że mamy funkcję F(x),zdefiniowaną przez (pseudo) pascalowską deklarację: function F(x:0..999):word Możemy łatwo stworzyć ta funkcję stosując poniższy kod 80x86 (i oczywiście, odpowiednia tablicę): mov bx, X ;pobranie wartości wejściowej funkcji i konwersja shl bx, 1 ; indeksu słowa do F mov ax, F[bx] Instrukcja shl mnoży indeks przez dwa, dostarczając właściwego indeksu do tablicy której elementami są słowa. Każda funkcja, której obszar stosowania jest mały i głownie zwartym jest dobrym kandydatem dla obliczenia przez przekształcenia tablicowe. W takim przypadku, nie- zwarte obszary stosowania są również do przyjęcia, tak długo jak obszar stosowania może być sprowadzony do właściwego zbioru wartości. Takie operacje są nazywane uzależnianiem i są tematem następnej sekcji. 9.8.2 UZALEŻNIANIE OBSZARÓW STOSOWANIA Uzależnianie obszarów stosowania jest pobraniem zbioru wartości w obszarze stosowani funkcji i przetworzenie ich ,tak, żeby były bardziej akceptowalne jako dane wejściowe do tej funkcji .rozważmy poniższą funkcję:
Mówi ona, że (komputerowa) funkcja SIN(x) jest odpowiednikiem (matematycznej) funkcji sin x gdzie
Jak wszyscy wiemy, sinus jest funkcją cykliczną, która akceptuje każdą wejściową wartość rzeczywistą Formuła stosuje obliczenie sinusa, jednak, tylko akceptuje mały zbiór tych wartości. To ograniczenie zakresu nie przedstawia rzeczywistego problemu ,poprzez proste obliczenie SIN(X mod (2*pi)) możemy obliczyć sinus każdej wartości wejściowej. Modyfikacja wartości wejściowej tak ,żebyśmy mogli łatwo obliczyć funkcję jest nazywana uzależnieniem wartości wejściowej. W powyższym przykładzie obliczyliśmy X mod 2*pi i stosując wynik jako daną wejściowa funkcji sin. Zaokrągla X do obszaru stosowania sin bez wpływania na wynik .Możemy zastosować uzależnienie wejścia ,możemy również zastosować przekształcenie tablicowe .Faktycznie skalowanie indeksu posługując się wejściami słowa jest formą uzależnienia wejścia .rozważmy poniższą funkcję Pascalowską: function val (x:word):word; begin case x of 0: val :=1; 1: val :=1; 2: val :=4; 3: val := 27; 4: val := 256; inne val :=0; end; end; Funkcja ta oblicza jakąś wartość dla x z zakresu 0..4 i zwraca zero jeśli x jest poza zakresem. Ponieważ x może przybrać 65 536 różnych wartości (będących 16 bitowym słowem).stworzenie tablicy zawierającej 65 536 słów gdzie tylko pierwsze pięć wejść jest nie zerowych ,będzie całkiem nieekonomiczne. Jednakże możemy jeszcze obliczyć tą funkcję stosując przekształcenie tablicowe jeśli zastosujemy uzależnienie wejścia. poniższy kod asemblerowy przedstawia tą zasadę: xor ax, ax ;AX = 0, zakładamy X > 4 mov bx, x cmp bx, 4 ja ItsZero shl bx, 1 mov ax, val [bx] ItsZero: Kod ten sprawdza czy x jest poza zakresem 0..4.jeśli tak, fizycznie ustawia ax na zero, w innym przypadku odszukuje wartość funkcji w tablicy val. z uzależnieniem wejścia ,możemy zaimplementować kilka funkcji ,które w innym wypadku byłyby niepraktyczne do zrobienia przez przekształcenie tablicowe. 9.8.3 GENEROWANIE TABLIC Jednym sporym problemem, z zastosowaniem przekształcenia tablicowego jest tworzenie tablicy. jest to szczególnie prawdziwe jeśli jest duża liczba wejść w tablicy. Obliczanie danych do umieszczenia w tablicy, potem mozolne wprowadzanie danych, a w końcu sprawdzanie tych danych aby upewnić się, że są poprawne, jest czasochłonnym i nudnym procesem. Dla różnych tablic jest lepszy sposób – użyjemy komputera do wygenerowania tablicy dla nas. Przykład jest dużo lepszy iż tylko opis .Rozważmy poniższą zmodyfikowaną funkcję sinus:
To świadczy, że x jest wartością całkowitą z zakresu 0..359 i r jest wartością całkowitą. Komputer może łatwo obliczyć to z poniższego kodu: mov bx, X shl bx, 1 mov ax, Sinus [bx] ;pobranie SIN(X)*1000 mov bx, R ;obliczanie R*(SIN(X)*1000) mul bx mov bx, 1000 ;obliczanie (R*(SIN(X)*1000))/ 1000 div bx Zauważmy, że mnożenie całkowite i dzielenie nie są łączne .nie możemy usunąć mnożenia przez 1000 i dzielenia przez 1000 ponieważ wywołałoby to anulowanie jedno drugiego .Co więcej ten kod musi obliczyć tą funkcję dokładnie w ten sposób. To co otrzymujemy na koniec tej funkcji jest tablica zawierająca 360 różnych
wartości odpowiadających sinusowi kąta (w stopniach razy 1000.Wprowadzanie tablicy do programu asemblerowego zawierającą takiej wartości jest niezmiernie nudne i prawdopodobnie popełnimy kilka błędów wprowadzając i weryfikując te dane .Jednak możemy mieć program generujący taką tablicę dla nas. Rozważmy poniższy program Turbo Pascala: program maketable; var i:integer; r:integer; f::text begin assign (f,’sinus.asm’); rewrite (f); for i := 0 to 359 do begin r:= round(sin(I*2.0*pi / 360.0)*1000.0); if (i mod 8) = 0 then begin writeln(f) write (f, ‘dw’, r); end else write(f,’,’,r) end; close(f); end. Program ten tworzy poniższe dane wyjściowe:
Oczywiście jest dużo łatwiej napisać program w Turbo Pascalu, który generuje te dane niż wprowadzać (i weryfikować) te dane ręcznie. Ten krótki przykład pokazuje jak może być użyteczny Pascal dla programisty asemblerowego. 9.12 PODSUMOWANIE Rozdział ten omówił arytmetyczne i logiczne operacje na CPU 80x86.Przedstawił instrukcje i techniki konieczne do wykonania arytmetyki całkowitej na podobną modę jak języki wysokiego poziomu. Rozdział ten również omówił operacje o zwielokrotnionej precyzji, jak wykonać operacje arytmetyczne stosując nie arytmetyczne instrukcje i jak użyć instrukcji arytmetycznych do wykonania operacji nie arytmetycznych. Wyrażenia arytmetyczne są dużo prostsze w językach wysokiego poziomu niż w języku asemblera. Istotnie ,pierwotnym celem Języka FORTRAN było dostarczanie FORmula TRANslator (Tłumacz Formuł) dla wyrażeń arytmetycznych. Chociaż zabiera trochę więcej wysiłku konwertowanie formuł arytmetycznych na asembler niż powiedzmy, Pascalowi ,tak długo jak będziemy postępować według bardzo prostych reguł, konwersja nie będzie trudna. Po opis krok po kroku zobacz: *Wyrażenia arytmetyczne *Proste przypisania *Proste wyrażenia *Wyrażenia złożone *Operatory przemienności *Wyrażenia Logiczne (Boolowskie) Jedną dużą zaletą języka asemblera jest to ,że jest łatwo wykonać prawie nie ograniczone dokładnością operacje arytmetyczne i logiczne. Rozdział ten opisuje jak wykonać operacje o podwyższonej dokładności dla większości powszechnych działań. Po komplet instrukcji zobacz; *Operacje o zwielokrotnionej dokładności *Operacje dodawania o zwielokrotnionej dokładności *Operacje odejmowania o zwielokrotnionej dokładności *Porównania o podwyższonej dokładności *Mnożenie o podwyższonej dokładności *Dzielenie o podwyższonej dokładności *Operacje NEG o podwyższonej dokładności *Operacje AND o podwyższonej dokładności *Operacje OR o podwyższonej dokładności *Operacje NOT o podwyższonej dokładności *Operacje przesunięcia o podwyższonej dokładności *operacje obrotu o podwyższonej dokładności W pewnym momencie możemy musieć działać na dwóch operandach, które są różnych typów .Na przykład, możemy musieć dodać wartość bajtową z wartością słowa. Ogólną ideą jest poszerzenie mniejszego operandu tak, żeby był tego samego rozmiaru co operand większy a potem obliczyć wynik tych operandów .Po więcej szczegółów zajrzyj *Operacje na operandach o różnych rozmiarach Chociaż zbiór instrukcji 80x86 dostarcza prosty sposób osiągania wielu zadań, możemy często wykorzystać różne idiomy w zbiorze instrukcji lub w związku z pewnymi operacjami arytmetycznymi tworzyć kod, który jest szybszy lub krótszy niż kod oczywisty. Rozdział ten wprowadził kilka tych idiomów. *Idiomy Maszynowe i arytmetyczne *Mnożenie bez MUL i IMUL *Dzielenie bez DIV i IDIV *Stosowanie AND do obliczania reszty *Implementowanie licznika Modulo-n z AND *Testowanie wartości o podwyższonej dokładności dla 0FFFF..FFh *Operacje0 TEST *Testowanie znaków instrukcją XOR Dla manipulowania danymi upakowanymi potrzebujemy zdolności do wyciągania pola z upakowanego rekordu i wprowadzania pola do upakowanego rekordu. Możemy użyć logicznych instrukcji and i or do maskowania pól, którymi chcemy manipulować; możemy zastosować instrukcje shl i shr do pozycjonowania danych do ich właściwych pozycji przed wprowadzeniem lub po wyciągnięciu danej. po naukę jak to robić zajrzyj *Operacje maskowania *Operacje maskowania instrukcją AND *Operacje maskowania instrukcją OR *Upakowane i rozpakowane typy danych
9.13 PYTANIA 1) Opisz jak możemy dodać zmienną bez znakowego słowa do zmiennej bez znakowego bajtu, tworząc wynik bajtowy. Wyjaśnij okoliczności wystąpienia błędu i jak poradzić sobie z nim 2) Odpowiedz na pytanie jeden dla wartości ze znakiem 3) Zakładamy, że var1 jest słowem a var2 i var3 są podwójnymi słowami .Jaki jest kod asemblerowy 80x86,który doda var1 do var2 pozostawiając wynik w var 3 jeśli: a) var1,var2 i var3 są wartościami bez znaku b) var1,var2 i var 3 są wartościami ze znakiem 4) „ADD BX, 4” jest bardziej wydajne niż :LEA BX,4[BX].Podaj przykład instrukcji LEA, która jest bardziej wydajna niż odpowiadająca jej instrukcja ADD 5) Dostarcz pojedynczej instrukcji LEA 80386,która mnoży EAX przez pięć 6) Zakładamy, że VAR1 i VAR2 są 32 bitowymi zmiennymi zadeklarowanymi z pseudo-opcodem DWORD. Napisz sekwencję kodu, która testuje co następuje: a)VAR1 = VAR2 b) VAR1 <> VAR2 c) VAR1 < VAR2 (wersje ze znakiem i bez znaku dla każdej z nich) d) VAR1 <= VAR2 e) VAR1 > VAR2 f) VAR1 >= VAR2 7) Skonwertuj poniższe wyrażenia do języka asemblera używając przesunięć, dodawań, odejmowań w miejsce mnożenia: a) AX*15 b) AX*129 c) AX*1024 d) AX*20000 8) Jaki jest najlepszy sposób podzielenia rejestru AX przez poniższe stałe? a) 8 b)255 c)1024 d) 45 9) opisz jak można pomnożyć wartość ośmio bitową w AL. Przez 256 (pozostawiając wynik w AX) stosując instrukcje MOV 10) Jak można logicznie zANDować wartość w AX przez 0FFh stosując instrukcję MOV? 11) Przypuśćmy, że rejestr AX zawiera parę upakowanych wartości binarnych z najmniej znaczącymi czterema bitami zawierającymi wartość z zakresu 0..15 i 12 bardziej znaczącymi bitami zawierającymi wartość z zakresu 0.4095.teraz przypuśćmy, że chcemy zobaczyć czy porcja 12 bitów zawiera wartość 295.Wyjaśnij jak można to wykonać za pomocą dwóch instrukcji. 12) Jak można użyć instrukcję TEST (lub sekwencję instrukcji TEST) aby zobaczyć czy bit zero i cztery w rejestrze AL oba są ustawione na jeden? Jak zastosować instrukcję TEST aby zobaczyć czy jeden albo drugi bit jest ustawiony? Jak użyć instrukcję TEST aby zobaczyć czy żaden bit nie jest ustawiony? 13) Dlaczego rejestr CL nie może być użyty jako operand licznika kiedy przesuwamy operand o zwielokrotnionej precyzji. Tj. dlaczego poniższe instrukcje nie przesuną wartości w (DX,AX) trzy bity w lewo? mov cl, 3 shl ax, cl rcl dx, cl 14) Dostarcz sekwencji instrukcji ,które wykonają operacje ROL i ROR (32 bitowe) o podwyższonej dokładności stosując tylko instrukcje 8086 15) Dostarcz sekwencji instrukcji, które implementują 64 bitową operację ROR stosując instrukcje 80386 SHRF i BT 16) Dostarcz kodu 80386 do wykonania poniższych 64 bitowych obliczeń .Zakładamy, że obliczamy X := Y op Z, z X,Y i Z zdefiniowanymi jak następuje: X dword 0, 0 Y dword 1, 2 Z dword 3, 4 a) dodawanie b)odejmowanie c) mnożenie d) logiczne AND e) logiczne OR f) logiczne XOR g) negacja h) logiczne NOT
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK konsultacja naukowa: NEKRO [email protected] [email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ DZIESIĄTY: STRUKTURY STERUJĄCE Rozdział ten omawia dwa podstawowe typy struktur sterujących : decyzje i powtórzenia (iteracje).Omawia jak skonwertować instrukcje języka wysokiego poziomu takie jak if...then..else, case (switch), while, for itp do odpowiedników sekwencji języka asemblera. rozdział ten również omawia techniki jakie możemy zastosować do poprawienia osiągów tych struktur sterujących. Sekcje poniżej, które mają przedrostek „•” są niezbędne .Te sekcje, z „⊗” omawiają zaawansowane tematy, które może odłożymy na później. • Wprowadzenie do Decyzji • Sekwencje IF..THEN..ELSE • Instrukcje CASE ⊗ Stany maszynowe i skoki pośrednie • Kod spaghetti • Pętle • Pętle WHILE • Pętle REPEAT..UNTIL • LOOP..ENDLOOP • Pętle FOR • Wykorzystanie rejestru a pętle ⊗ Poprawianie wydajności ⊗ Przenoszenie warunku zakończenia na koniec pętli ⊗ Wykonywanie pętli wstecznych ⊗ Pętla niezmienników ⊗ Pętle rozwijane ⊗ Indukcja zmiennych 10.1 WPROWADZENIE DO DECYZJI W swoje podstawowej formie, decyzja jest rodzajem krótkiego skoku wewnątrz kodu ,który przełącza pomiędzy dwoma możliwymi wykonaniami ścieżkami wykonania opartymi na jakimś warunku. Normalnie,(chociaż nie zawsze),sekwencje instrukcji warunkowych są implementowane z instrukcjami skoków warunkowych. Instrukcje warunkowe odpowiadają instrukcjom w Pascalu; IF (warunek jest prawdziwy) THEN instr1 else instr2; Język asemblera, jak zwykle, oferuje dużo większą elastyczność, kiedy zajmujemy się instrukcjami warunkowymi. Rozważmy poniższą instrukcję pascalowską: IF ((XT)) or (A <>B) THEN instr1; Podchodząc do konwertowania tej instrukcji na asembler metodą „brute force” możemy stworzyć:
Jak możemy zobaczyć ,w przetwarzaniu tego powyższego wyrażenia bierze udział znaczna liczba instrukcji warunkowych. Pobieżnie odpowiada to (odpowiednikowi) Pascalowych instrukcji: cl := true; IF (X>=Y) then cl := fałsz; IF (Z<=T) then cl := fałsz; IF (A<>B) then cl := true; IF (CL = true) then stmt1; Teraz porównajmy to z poniższym „poprawionym” kodem: mov ax, A cmp ax, B jne DoStmt mov ax, X cmp ax, Y jnl SkipStmt mov ax, Z cmp ax, T jng SkipStmt DoStmt: SkipStmt: Dwie rzeczy powinny być oczywiste z powyższej sekwencji kodu: po pierwsze, pojedyncza instrukcja warunkowa w Pascalu może wymagać kilku skoków warunkowych w asemblerze; po drugie, organizacja złożonego wyrażenia w sekwencji warunkowej może wpływać na wydajność kodu. Dlatego też ,powinniśmy ostrożnie ćwiczyć kiedy zajmujemy się sekwencjami warunkowymi w asemblerze. Instrukcje warunkowe mogą być podzielone na trzy podstawowe kategorie: instrukcje if..then..else ,instrukcje case i skoki pośrednie. Poniższa sekcja opisze te struktury programu ,jak je zastosować i jak napisać je w języku asemblera. 10.2 SEKWENCJE IF..THEN..ELSE Najpowszechniejszym zastosowaniem instrukcji warunkowych jest instrukcja if..then lub if..then..else. Te dwie instrukcje przybierają formę jak pokazano na rysunku 10.1 Instrukcja if..then jest specjalnym przypadkiem instrukcji if..then..else ( z pustym blokiem ELSE).Dlatego też, będziemy rozpatrywać bardziej ogólną postać if..then..else. Podstawowa implementacja instrukcji if..then..else w języku asemblera 80x86 wygląda podobnie jak to:
Rysunek 10.1 Instrukcje IF..THEN i IF..THEN..ELSE {sekwencja instrukcji testujących jakiś warunek} jcc JakiśKod {sekwencja instrukcji odpowiadających blokowi THEN} jmp EndOfIf ElseCode: {sekwencja instrukcji odpowiadających blokowi ELSE} EndOfIf: Notka: Jcc przedstawia jakąś instrukcję skoku warunkowego. Na przykład do konwersji instrukcji pascalowskie: IF (a=b) then c:=d else b:= b+1; Na język asemblera, możemy zastosować poniższy kod 80x86: mov ax, a cmp ax, b jne ElseBlk mov ax, d mov c, ax jmp EndOfIf ElseBlk: inc b EndOf If: Dla prostego wyrażenia takiego jak (A=B) generowanie właściwego kodu dla instrukcji if..then..else jest prawie trywialne. Kiedy wyrażenie staje się bardziej złożone, również wzrasta złożoność powiązanego kodu asemblerowego. Rozważmy poniższą instrukcję if prezentowaną wcześniej:
IF ((X > Y) and (Z B) THEN C :=D; Kiedy przetwarzamy złożone instrukcje if takie jak to, zadanie konwersji stanie się łatwiejsze jeśli podzielimy tą instrukcję if na sekwencję trzech różnych instrukcji if jak następuje: IF (A<>B) THEN C :=D IF (X >Y) THEN IF (Z< T) THEN C := D; Konwersja ta pochodzi z Pascalowskich odpowiedników: IF (wyraż1 AND wyarż2) THEN instr; Jest odpowiednikiem IF (wyraż1) THEN IF (wyraż2) THEN instr; a IF (wyraż1 OR wyraż2) THEN instr; jest odpowiednikiem IF (wyraż1) THEN instr; IF (wyraż2) THEN instr; W języku asemblera, dawna instrukcja if staje się : mov ax, A cmp ax, B jne DOIf mov ax, X cmp ax, Y jng EndOfIf mov ax, Z cmp ax, T jnl RndOfIf DoIf: mov ax, D mov C, ax EndOfIf: Jak już prawdopodobnie mówiliśmy, kod konieczny do sprawdzenia warunku może łatwo stać się bardziej złożony niż instrukcje pojawiające się w blokach else i then. Chociaż wydaje się to nieco paradoksalne, że można włożyć większy wysiłek w testowanie warunku niż działanie na wyniku tego warunku, zdarza się to cały czas .Dlatego też powinniśmy być przygotowani na taką sytuację. Prawdopodobnie największym problem z implementacją złożonych instrukcji warunkowych w asemblerze jest próbowanie obliczenia co zrobimy po napisaniu kodu. Największą zaletą oferowaną przez język wysokiego poziomu nad asemblerem jest to że wyrażenia są dużo łatwiejsze do odczytu i pojęcia w języku wysokiego poziomu. Wersja HLL’a jest samodokumentująca podczas gdy język asemblera ma tendencje do ukrywania prawdziwej natury kodu. Dlatego też dobrze napisane komentarze są niezbędnym składnikiem implementacji przez asembler instrukcji if..then..else. Elegancka implementacja powyższego przykładu: ;IF ((X >Y) AND (Z < T)) OR (A<> B) THEN C:=D; ;Implementujemy jako: ;IF (A<>B) THEN GOTO DoIf; mov ax, A cmp ax, B jne DoIf ;IF NOT (X>Y) THEN GOTO EndOfIf; mov ax, X cmp ax, Y jng EndOf If ;IF NOT (Z < T) THEN GOTO EndOfIf; mov ax, Z cmp ax,T jnl EndOfIf ; Blok THEN: mov ax, D mov C, ax
;Koniec instrukcji IF EndOfIf: Trzeba przyznać, że takie przedstawianie jest popadaniem w przesadę dla tak prostego przykładu poniższe byłoby prawdopodobnie wystarczające: ;IF ((X > Y) AND (Z < T) OR (A<>B) THEN C:=D; ;test wyrażenia boolowskiego: mov ax, A cmp ax, B jne DoIF mov ax, X cmp ax, Y jng EndOfIf mov ax, Z cmp ax, T jnl EndOfIf ;Blok THEN: mov ax, D mov C, ax ;Koniec instrukcji IF EndOfIf: Jednakże jeśli nasze instrukcje if stają się złożone, gęstość (i jakość) naszych komentarzy staje się coraz bardziej ważna. 10.3 INSTRUKCJE CASE Pascalowa instrukcja case przybiera poniższą formę: CASE zmienna OF stała1: instr1; stała2:instr2; stałan:instrn END; Kiedy wykonuje się ta instrukcja, sprawdza wartość stałej const1..constn.Jeśli została znaleziona, wtedy wykonuje się odpowiednia instrukcja. standardowy Pascal umieszcza kilka ograniczeń na instrukcję case. Po pierwsze, wartość zmiennej nie jest na liście stałych, wynik instrukcji case jest niezdefiniowany. Po drugie, wszystkie stałe pojawiające się po etykiecie CASE muszą być unikalne. Powód tych ograniczeń stanie się jasny za chwilę. Większość wstępnych tekstów o programowaniu wprowadza instrukcję case poprzez wyjaśnienie jej jako sekwencji instrukcji if..then..else. Można twierdzić że poniższe dwa kawałki kodu pascalowego są sobie odpowiednie: CASE I OF 0: Writeln(‘I=0’); 1:Writeln(‘I=1’); 2:Writeln(‘I=2’); END; IF I = 0 THEN writeln (‘I=0’) ELSE IF I = 1 THEN Writeln (‘I= 1’) ELSE IF I= 2 THEN Writeln (‘I=2’); Podczas gdy semantycznie te dwie części kodu mogą być takie same, ich implementacja jest zwykle różna. Podczas gdy łańcuch if..then..else wykonuje porównanie dla każdej instrukcji warunkowej w sekwencji, instrukcja case normalnie używa skoku pośredniego dla sterowania przesyłaniem danych do jednej z kilku instrukcji pojedynczego obliczenia. Rozważmy dwa przedstawione powyżej przykłady ,które mogą być napisane w asemblerze w poniższym kodzie:
Dwie rzeczy powinny stać się łatwo zauważalne: im więcej (kolejnych) case’ów mamy, tym bardziej wydajna staje się implementacja tablicy skoków (pod względem przestrzeni i szybkości).z wyjątkiem banalnych przypadków ,instrukcja case jest prawie zawsze szybsza i zwykle z dużym marginesem. Tak długo jak etykiety case są kolejnymi wartościami, wersja instrukcji case jest zazwyczaj również mniejsza. Co stanie się jeśli musimy zawrzeć nie po kolei etykiety case lub nie możemy być pewni, czy zmienna case nie wyjdzie poza zakres? Wiele Pascali ma rozszerzoną definicję instrukcji case zawierającą klauzulę otherwise .Taka instrukcja case przyjmuje postać: CASE zmienna OF stała:instr; stała:instr; • • • stała:instr; OTHERWISE instr END; Jeśli zwartość zmiennej zgadza się z jedną ze stałych stanowiącą etykietę case, wtedy jest wykonywana powiązana instrukcja .Jeśli wartość zmiennej nie zgadza się z żadną z etykiet case wtedy wykonywana jest instrukcja
po klauzuli otherwise. Klauzula otherwise jest implementowana w dwóch fazach .najpierw musimy wybrać wartości minimalną i maksymalną, która pojawia się w instrukcji case. W poniższej instrukcji case ,najmniejsza etykieta case to pięć a największa 15: CASE I OF 5:instr1; 8:instr2; 10:instr3; 12:instr4; 15:instr5; OTHERWISE instr6 END;
Przed wykonaniem skoku przez tablicę skoków,80x86 implementuje tą instrukcję case , sprawdzając zmienną case aby być pewnym ,że jest w zakresie 5..15.Jeśli nie, sterowanie powinno być przekazane do instr6: mov bx, I cmp bx, 5 jl Otherwise cmp bx, 15 jg Otherwise shl bx, 1 jmp cs:JmpTbl-10[bx] Jedyny problem z tą formą instrukcji case jaki występuje, jest taki, że niepoprawnie działa w sytuacji gdzie I jest równe 6,7,9,11,13 lub 14.Zamiast dorzucić ekstra kod przed skok warunkowy, możemy dołączyć ekstra wejście w tablicy skoków jak następuje: mov bx, I cmp bx, 5 jl Otherwise cmp bx, 15 jg Otherwise shl bx, 1 jmp cs:JmpTbl-10[bx] Otherwise: {wkładamy instr6 tutaj} JmpTbl
word instr1, Otherwise,Otherwise,instr2,Otherwise word instr3,Otherwise,instr4,otherwise,otherwise word instr5 itp. Zauważmy ,że wartość 10 jest odejmowana od adresu tablicy skoków. Pierwsze wejście w tablicy ma zawsze offset zero podczas gdy najmniejsza wartość stosowana do indeksowania tej tablicy to pięć (która jest mnożona przez dwa do stworzenia 10).Wejścia dla 6,7,89,11,13 i 14 wszystkie wskazują na kod klauzuli Otherwise, więc jeśli te wartości zawierają jeden, będzie wykonywana klauzula Otherwise. Jest problem z tą implementacją instrukcji case. Jeśli etykieta case zawiera nie-kolejne wejścia, które są bardziej przestrzenne, poniższa instrukcja case wygeneruje niezmiernie duży kod: CASE I OF 0:instr1; 100:instr2; 1000:instr3; 10000:instr4 Otherwise instr5 END; W takiej sytuacji nasz program będzie dużo mniejszy, jeśli zaimplementujemy instrukcję case sekwencją instrukcji if zamiast stosując instrukcje skoku. Jednak, zapamiętajmy jedną rzecz - wielkość tablicy skoków normalnie nie wpływa na szybkość wykonywania programu .Jeśli tablica skoków zawiera dwa wejścia lub dwa tysiące, instrukcja case wykona wielokrotny skok stałą ilość razy. Implementacja instrukcji if wymaga rosnącej
liniowo ilości czasu dla każdej etykiety case pojawiającej się w instrukcji case. Prawdopodobnie, największą zaletą stosowania języka asemblera nad HLL’ami takimi jak Pascal jest to, że możemy wybrać rzeczywistą implementację. W takim przypadku, możemy zaimplementować instrukcję case jako sekwencję instrukcji if..then..else .lub możemy zaimplementować ją jako tablicę skoków lub zastosujemy ich skrzyżowanie ich dwóch: CASE I OF 0:instr1; 1:instr2; 2:instr3; 100:instr4; Otherwise instr5 END; może stać się: mov bx, I cmp bx, 100 je Is100 cmp bx, 2 ja Otherwise shl bx, 1 jmp cs:JmpTbl[bx] itd. Oczywiście, możemy zrobić to w Pascalu, w poniższym kodzie: IF I = 100 then instr4 ELSE CASE I OF 0:instr1; 1:instr2; 2:instr3; otherwise instr5 END; Ale powoduje to zakłócenie czytelności programu pascalowskiego. Z drugiej strony, dodatkowy kod do testowania dla 100 w kodzie asemblera niekorzystnie wpływa na czytelność programu (być może dlatego ,że już jest trudny do odczytu) Dlatego też ,większość ludzi doda dodatkowy kod aby uczynić swój program bardziej wydajnym. Instrukcja switch C/C++ jest bardzo podobna do pascalowskiej instrukcji case. Jest tylko jedna semantyczna różnica; programista musi wyraźnie umieścić instrukcję break w każdej klauzuli dla przekazania sterowania do pierwszej instrukcji za switch. To break odpowiada instrukcji jmp na końcu każdej sekwencji case w powyższym kodzie asemblera .Jeśli odpowiednie break nie jest obecny ,C/C++ przekazuje sterowanie do kodu następującego po case .Jest to odpowiednik pozostawienia jmp na końcu sekwencji case: switch (1) { case 0: instr1; case 1: instr2; case 2: instr3; break; case 3: instr4; break; default: instr5; } Tłumaczymy to na kod 80x86: mov bx, 1 cmp bx, 3 ja DefaultCase
JmpTbl[bx] case0:
shl bx, 1 jmp cs:JmpTbl[bx] word case0, case1, case2, case3
case1: case2: case3: DefalutCase: EndCase:
jmp EndCase jmp EndCase < kod instr5>
;emitowana dla instrukcji break ;emitowany dla instrukcji break
10.4 STANY MASZYNOWE I SKOKI POŚREDNIE Inną strukturą sterująca powszechnie stosowaną w programach asemblerowych jest „stan maszynowy”. Stan maszynowy używa zmienną stanu do sterowania przebiegiem programu. Język FORTRAN dostarcza tej możliwości z powiązaną instrukcją goto. Pewne warianty C (np. GNU’s GCC z Free Software Foundation) dostarczają podobnych cech. W języku asemblera, skok pośredni dostarcza mechanizmu do łatwej implementacji stanów maszynowych Więc co to jest ten stan maszynowy? W bardzo podstawowej terminologii ,jest to część kodu który śledzi historię wykonania poprzez wprowadzanie i pozostawiania pewnych „stanów”. Dla potrzeb tego rozdziału ,nie będziemy stosować bardzo formalnej definicji stanu maszynowego. Założymy ,że stan maszynowy jest kawałkiem kodu który (jakoś) zapamiętuje historię wykonywania (stan) i wykonuje część kodu opartego na tej historii. W sensie rzeczywistym, wszystkie programy są stanami masynowymi. Rejestry CPU i wartości w pamięci stanowią „stan” tej maszyny. Jednakże, my zastosujemy dużo bardziej wymuszony pogląd. Istotnie, dla większości potrzeb tylko pojedyncza zmienna (lub wartość w rejestrze IP) będzie oznaczała bieżący stan. Teraz rozpatrzymy konkretny przykład. Przypuśćmy ,że mamy procedurę, która wykonuje jedną operacje pierwszy raz kiedy ją wywołujemy, różne operacje za drugim wywołaniem, jeszcze coś innego za trzecim wywołaniem a potem znowu coś nowego za czwartym wywołaniem. Po czwartym wywołaniu powtarza te cztery różne operacje w kolejności. Na przykład przypuśćmy że chcemy aby procedura dodała ax i bx za pierwszym razem, odjęła je za drugim wywołaniem, pomnożyła je za trzecim i podzieliła za czwartym. Możemy zaimplementować tą procedurę jak następuje: State byte 0 StateMatch proc cmp stan, 0 jne TryState1 ;jeśli jest to stan 0, dodaje BX do AX i przełącza do stanu 1: add ax, bx inc State ;ustawia go na stan 1 ret ;Jeśli jest to stan 1, odejmuje BX od AX i przełącza na stan 2 TryState1: cmp Stan, 1 jne TryState2 sub ax, bx inc State ret ;jeśli jest to stan 2, mnoży Ax i BX i przełącza do stanu 3: TryState2: cmp Stan, 2 jne MustBeState3 push dx mul bx pop dx inc State ret ;jeśli żaden z powyższych, zakładamy, że to stan 4.Więc dzielimy AX przez BX MustBeState3: push dx xor dx, dx ;powielenie zera AX do DX div bx pop dx mov State, 0 ;przełączenie z powrotem do stanu 0 ret
StateMatch
endp
Technicznie procedura ta nie jest stanem maszyny. Zamiast tego, jest zmienna State i instrukcje cmp/jne, które stanowią stan maszyny. Nie ma niczego specjalnego w tym kodzie .Jest trochę bardziej niż instrukcja case implementowany przez konstrukcję if..then..else. Jedyna specjalna rzecz w tej procedurze jest to, że zapamiętuje ile razy została wywołana i zachowuje się różnie w zależności od liczby wywołania. Chociaż jest to poprawna implementacja żądanego stanu maszynowego, nie jest ona szczególnie wydajna. Bardziej powszechną implementacją stanu maszynowego w asemblerze jest zastosowanie skoku pośredniego .Zamiast stosować zmienną stanu zawierającą wartości takie jak zero, jeden, dwa lub trzy, możemy załadować zmienną stanu adresem kodu do wykonania na wejście do procedury. Poprzez prosty skok do tego adresu, stan maszynowy może zapisać powyższe testy potrzebne do właściwego wykonania fragmentu kodu. Rozważmy poniższą implementację stosującą skok pośredni:
Instrukcja jmp na początku procedury StateMatch przekazuje sterowanie do lokacji wskazywanej przez zmienna State. Przy pierwszym wywołaniu StateMAtch wskazuje na etykietę State0.Od tego czasu każda podsekcja kodu ustawia zmienną State aby wskazywała właściwy następny kod. 10.5 KOD SPAGHETTI Jednym z głównych problemów z językiem asemblera jest to, że zabiera on kilka instrukcji dla realizacji prostej idei ujętej przez pojedynczą instrukcję HLL’a. Zbyt często programista zauważa, że może zaoszczędzić kilka bajtów lub cykli poprzez skok do środka jakiejś struktury programowej. Po kilku takich obserwacjach ( i odpowiednich modyfikacjach),kod zawiera całą sekwencję skoków do lub z części kodu. Jeśli namalujemy linię każdego skoku do jego przeznaczenia, listing końcowy będzie się kończył tak jak gdyby ktoś rzucił miską spaghetti w nasz kod., stąd termin „spaghetti kod”
Spaghetti kod cierpi z powodu jednej głównej wady - jest trudny (w najlepszym razie) do odczytania taki program i dojścia do tego co on robi. Dużo programów zaczynających się w postaci „strukturalnej” staje się kodem niestrukturalnym (spaghetti) na ołtarzu wydajności. Niestety kod spaghetti rzadko jest wydajny .Ponieważ, trudno jest obliczyć dokładnie, co się stanie, jest bardzo trudno określić czy możemy zastosować lepszy algorytm do poprawy systemu. W związku z tym kod spaghetti może okazać się mniej wydajny. Chociaż jest prawdą ,że stworzenie jakiegoś kodu spaghetti w naszym programie może poprawić jego wydajność, powinniśmy zawsze robić to w ostateczności (kiedy wypróbowaliśmy wszystkie inne możliwości a jeszcze nie osiągnęliśmy tego co chcieliśmy),nigdy automatycznie. Zawsze zaczynajmy pisanie naszych programów z prostymi instrukcjami if i case .Oczywiście ,nigdy nie powinniśmy zacierać struktury naszego kodu chyba, że wynikające z tego korzyści są tego warte. Znane powiedzenie z kręgu programowani strukturalnego mówi: „Po goto , wskaźniki są kolejnym, niebezpiecznym elementem w języku programowania” .Podobne powiedzenie mówi: „Wskaźniki są strukturą danych a goto’a są strukturami sterującymi”. Innymi słowy, unikajmy nadmiernego stosowania wskaźników. Jeśli wskaźniki i goto’a są złe ,wtedy skok pośredni musi być najgorszą ze wszystkich ponieważ wymaga obu goto i wskaźników! Jednak serio ,instrukcji skoku pośredniego powinniśmy unikać poza sporadycznym zastosowaniem .Mogą one uczynić program trudniejszym do odczytania. W końcu, skok pośredni może (teoretycznie) przekazać sterowanie danymi do każdej etykiety wewnątrz programu. Wyobraźmy sobie jak trudno byłoby kontrolować przepływ w programie gdybyśmy nie mieli pojęcia co zawiera wskaźnik i napotkany skok pośredni stosujący ten wskaźnik .Dlatego też ,powinniśmy zawsze być ostrożni stosując instrukcje skoku pośredniego. 10.6 PĘTLE Pętle przedstawiają końcowe podstawowe struktury sterujące (sekwencje, decyzje i pętle) ,które stanowią typowy program. Podobnie jak wiele innych struktur w asemblerze, znajdziemy zastosowanie pętli w miejscach o których nam się nie śniło, że można zastosować pętle. Większość HLLi implikuje strukturę pętli jako ukrytą. Na przykład, rozważmy instrukcję BASICa : IF A$ = B$ THEN 100. Ta instrukcja if porównuje dwa łańcuchy i skacze do instrukcji 100 jeśli są równe. W języku asemblera, będziemy musieli napisać pętlę dla porównania każdego znaku w A$ z odpowiednim znakiem w B$ a potem skok do instrukcji 100 jeśli i tylko jeśli wszystkie znaki się zgadzają. W BASICu nie jest widoczna żadna pętla w programie. W języku asemblera ta bardzo prosta instrukcja if wymaga pętli. Ten mały przykład pokazuje, że pętle wydają się pojawiać wszędzie. Program z pętlami składa się z trzech składników: opcjonalny składnik inicjacyjny, test zakończenia pętli i ciała pętli. Porządek w jakim te składniki są asemblowane może radykalnie zmienić sposób działania pętli. Trzy kombinacje tych składników pojawiają się wielokrotnie. Z powodu ich częstotliwości, tym strukturom pętli zostały nadane specjalne nazwy w HLL’ach: pętle while, pętle repeat..until ( do..while w C/C++) i pętle loop..endloop 10.6.1 PĘTLE WHILE Najbardziej ogólną pętlą jest pętla while. Przybiera ona następującą postać: WHILE wyrażenie boolowskie DO instrukcja; Są dwa ważne punkty do odnotowania jeśli chodzi o pętlę while. Po pierwsze, test zakończenia pojawia się na początku pętli. Po drugie bezpośrednią konsekwencją umieszczenia testu zakończenia, ciało pętli może nigdy się nie wykonać. Jeśli warunek zakończenia zawsze istnieje, ciało pętli będzie zawsze przeskakiwane. Rozważmy poniższą pascalowską pętlę loop: I := 0; WHILE (I <100) do I := I=1; I:=0; jest kodem inicjującym dla tej pętli. I jest zmienną sterującą pętli ,ponieważ steruje wykonaniem ciała pętli (I<100) jest warunkiem zakończenia pętli To znaczy, pętla nie zakończy się tak długo dopóki I będzie mniejsze niż 100. I := I+1 jest ciałem pętli. To jest kod, który wykonuje się w każdym przebiegu pętli. Możemy skonwertować go do języka asemblera 80x86 jak następuje: mov I, 0 WhileLp: cmp I, 100 jge WhileDone inc I jmp WhileLp WhileDone: Zauważmy ,że pascalowska pętla while może być łatwo zsyntetyzowana przez zastosowanie instrukcji if i goto. Na przykład powyższa Pascalowska pętla while może być zastąpioną przez: I := 0;
1:
IF (I < 100) THEN BEGIN I := I + 1; GOTO 1;
END; Bardziej ogólnie, każda pętla while może być zbudowana według poniższego schematu: Opcjonalny kod inicjujący 1: Jeśli warunek zakończenia nie spełniony THEN BEGIN ciało pętli GOTO 1; END; Dlatego możemy użyć tej techniki dla wcześniejszych konwersji, w tym rozdziale, instrukcji if do języka asemblera. Wszystko co potrzebujemy to dodatkowa instrukcja jmp (goto) 10.6.2 PĘTLE REPEAT..UNTIL Pętla repeat..until (do..while) testuje warunek zakończenia na końcu pętli zamiast na jej początku. W Pascalu, pętla repeat..until przybiera poniższą postać: Opcjonalny kod inicjujący REPEAT treść pętli UNTIL warunek zakończenia Sekwencja ta wykonuje kod inicjujący treść pętli, wtedy testuje jakiś warunek aby zobaczyć czy pętla powinna być powtórzona. Jeśli wyrażenie boolowskie przyjmuje wartość fałsz, pętla jest powtarzana, w przeciwnym razie kończy się. Dwiema rzeczami do zapamiętania o pętli repeat..until jest to, że test zakończenia pojawia się na końcu pętli i, bezpośrednia konsekwencja tego, treść pętli wykonuje się przynajmniej raz. Podobnie jak pętla while, pętla repeat..until może być zsyntetyzowana instrukcją if i goto. Zastosowalibyśmy coś takiego: Kod inicjujący 1: treść pętli IF warunek zakończenia nie spełniony THEN GOTO Opierając się na materiale przedstawionym w poprzedniej sekcji ,możemy łatwo zsyntetyzować pętlę repeat..until w języku asemblera. 10.6.3 PĘTLE LOOP..ENDLOOP Jeśli pętle while testują warunek zakończenia na początku pętli, a pętle repeat..until sprawdzają ten warunek na końcu pętli, jedynym miejscem pozostawionym do testowania warunku zakończenia jest środek pętli. Chociaż Pascal i C/C++ bezpośrednio nie wspierają takiej pętli, struktura loop..endloop może być znajdowania w HLLach takich jak Ada. Pętla loop..endloop przyjmuje następującą postać: LOOP Treść pętli ENDLOOP; Zauważmy że nie ma wyraźnego warunku zakończenia. Chyba że uwzględniono budowę loop..endloop jako prostej formy pętli nieskończonej. Warunek pętli jest obsługiwany przez instrukcje if i goto. Rozważmy poniższy (pseudo) pascalowski kod, który zawiera budowę loop..endloop: LOOP READ (ch) IF ch = ‘.’ THEN BREAK; WRITE(ch); ENDLOOP; W rzeczywistym Pascalu, zastosowalibyśmy poniższy kod do wykonania tego: 1: READ(ch); IF ch = ‘.’ THEN GOTO 2; (*Turbo Pascal wspiera BRAEK!*) WRITE(ch); GOTO 1 2:
W języku asemblera skończymy tak: LOOP1: getc cmp je putc jmp EndLoop:
al., ‘.’ EndLoop LOOP1
10.6.4 PĘTLE FOR Pętla for jest specjalną formą pętli while ,która powtarza treść pętli określoną ilość razy. W Pascalu pętla for wygląda podobnie jak ta: FOR var := wartość początkowa TO wartość końcowa DO instrukcja lub FOR var := wartość początkowa DOWNTO wartość końcowa DO instrukcja Tradycyjnie, w Pascalu pętla for bywa zastosowana do przetwarzania tablic i innych obiektów dostępnych w porządku sekwencji numerycznej. Pętle te mogą być konwertowane bezpośrednio do języka asemblera jak poniżej: W Pascalu: FOR var := star TO stop DO instrukcje; W Asemblerze: mov var, start FL: mov ax, var cmp ax, stop jg EndFor ; kod odpowiadający instrukcji przychodzi tutaj inc var jmp FL EndFor: Na szczęście, większość pętli for powtarza jakieś instrukcje stałą ilość razy. Na przykład, FOR I := 0 to 7 do write(ch); W sytuacji takiej jak ta lepiej jest zastosować instrukcję loop 80x86 zamiast symulowania pętli for: mov cx, 7 LP: mov al., ch call putc loop LP Zapamiętajmy ,że normalnie instrukcja loop pojawia się na końcu pętli podczas gdy pętla for testuje warunek zakończenia na początku pętli. Dlatego też, powinniśmy się zabezpieczyć aby zapobiec pętli nieskończonej w przypadku gdy cx jest równe zero (kiedy będzie to przypadek pętli loop powtarzającej się 65,536 razy) lub gdy wartość zatrzymania jest mniejsza niż wartość początkowa. W przypadku: FOR var := start TO stop DO instrukcja; Zakładając ,że nie stosujemy wartości var wewnątrz pętli, prawdopodobnie będziemy chcieli zastosować kod asemblerowy: mov cx, stop sub cx, start jl SkipFor inc cx LP: instrukcja Loop LP SkipFor: Pamiętamy, że instrukcje sub i cmp ustawiają flagi i identyczny sposób. Dlatego też, pętla ta będzie pominięta jeśli stop będzie mniejsze niż start. Będzie ona powtarzana (stop-start)+1 raz w przeciwnym razie. Jeśli musimy odnieść się do wartości var wewnątrz pętli, możemy użyć poniższego kodu: mov ax, star mov var, ax mov cx, stop
LP:
sub cx, ax jl SkipFor inc cx instrukcje inc var loop LP
SkipFor: 10.7 WYKORZYSTANIE REJESTRÓW A PĘTLE Zważywszy ,że uzyskujemy dostęp do rejestrów 80x86 dużo szybciej niż do komórek pamięci, rejestry są idealnym miejscem do umieszczenia zmiennych sterujących pętli (zwłaszcza dla małych pętli). Punkt ten jest podkreślony, ponieważ instrukcja loop wymaga zastosowania rejestru cx. Jednakże, jest kilka problemów związanych z zastosowaniem rejestrów wewnątrz pętli. Podstawowym problemem z zastosowaniem rejestrów jako zmiennej sterującej pętli jest to ,że rejestry są ograniczonym zasobami. W szczególności, jest tylko jeden rejestr cx. Dlatego też, poniższy kod nie będzie pracował poprawnie: mov cx, 8 Loop1: mov cx, 4 Loop2: instrukcje loop Loop2 instrukcje loop Loop1 Intencją tu było stworzenie zbioru pętli zagnieżdżonych, to znaczy, jedna pętla wewnątrz innej. Pętla wewnętrzna (Loop2) powinna powtarzać się cztery razy dla każdego z ośmiu wykonań pętli zewnętrznej (Loop1). Niestety, obie pętle stosują instrukcję loop. Dlatego też będzie to pętla nieskończona ponieważ cx będzie ustawiane na zero (które loop traktuje jako 65,536) na końcu pierwszej instrukcji loop. Ponieważ cx jest zawsze po napotkaniu drugiej instrukcji loop. Sterowanie zostanie zawsze przekazane do etykiety Loop1.Rozwiązanie tu jest zapisanie i odzyskanie rejestru cx lub użycie różnych rejestrów w miejsce cx dla pętli zewnętrznej: mov cx, 8 Loop1: push cx mov cx, 4 Loop2: instrukcje loop Loop2 pop cx instrukcje loop Loop1 lub: mov bx, 8 Loop1: mov cx, 4 Loop2: instrukcje Loop Loop2 Instrukcje dec bx jnz Loop1 Niepoprawny rejestr jest jedną z głównych źródeł błędów w pętlach programów asemblerowych. 10.8 POPRAWIANIE WYDAJNOŚĆI Mikroprocesory 80x86 wykonują sekwencje instrukcji z oślepiająca szybkością Rzadko będziemy spotykać program który jest wolny nie zawierający pętli. Ponieważ pętle są głównym źródłem problemów z wydajnością wewnątrz programów ,są one miejscem na które spoglądamy, kiedy próbujemy przyspieszyć nasze oprogramowanie. Rozważania na temat jak pisać bardziej wydajne programy są poza zakresem tego rozdziału, jednak jest parę rzeczy, na które powinniśmy uważać kiedy projektujemy pętle w naszych programach. Namierzają one i usuwają wszystkie niepotrzebne instrukcje z pętli, aby zredukować czas jaki zabiera im wykonanie jednej iteracji pętli. 10.8.1 PRZENOSZENIE WARUNKU ZAKOŃCZENIA NA KONIEC PĘTLI Rozważmy poniższych wykres przepływu dla trzech typów pętli przedstawionych wcześniej:
Pętla repeat..until : Kod inicjujący Treść pętli Testowanie warunku zakończenia Kod następujący po pętli Pętla while: Kod inicjujący Test zakończenia pętli Treść pętli Skok do testu Kod następujący po pętli Pętla loop..endloop: Kod inicjujący Treść pętli, cześć jeden Test zakończenia pętli Treść pętli część druga Skok do treści pętli część 1 Kod występujący po pętli Jak możemy zobaczyć, pętla repeat ..until jest najprostsza. Ma to odzwierciedlenie w kodzie asemblerowym wymaganym do implementacji tych pętli .Rozważmy poniższe pętle repeat..until i while, które są identyczne: SI := DI - 20; SI := DI - 20; while (SI <= DI) do repeat begin instrukcje instrukcje SI := SI +1; SI := SI +1; End; until SI > DI; Kod w języku asemblera dla tych dwóch pętli: mov si, di mov si, di sub si, 20 sub si, 20 WL1: cmp si, di U: instrukcje jnle QWL inc si instrukcje cmp si, di inc si jng RU jmp WL1 QWL: Jak widzimy, testowanie warunku zakończenia na końcu pętli pozwala nam usunąć instrukcję jmp z pętli .To może być znaczące jeśli pętla ta jest zagnieżdżona wewnątrz innej pętli. W poprzednim przykładzie nie było problemu z wykonaniem treści przynajmniej raz W poddanej definicji pętli łatwo możemy zobaczyć ,że pętla będzie wykonywana dokładnie 20 razy. Zakładając ,że cx jest dostępny, łatwo zredukujemy tą pętlę do: lea si, -20[di] mov cx, 20 WL1: instrukcje inc si loop WL1 Niestety, nie jest to zawsze takie łatwe. Rozważmy poniższy Pascalowski kod: WHILE (SI <= DI) DO BEGIN Instrukcje SI := SI +1; END; W tym szczególnym przykładzie, mamy niewielkie pojecie co zawiera si na wejściu do pętli. dlatego też, nie możemy zakładać, że treść pętli zostanie wykonana przynajmniej raz. Dlatego też, musimy zrobić badanie przed wykonaniem treści pętli .Test może być umieszczony na końcu pętli włącznie z pojedynczą instrukcją jmp: jmp short Test
RU:
instrukcje inc si Test: cmp si, di jle RU Chociaż kod jest tak długi jak oryginalna pętla while, instrukcja jmp wykonuje się tylko raz zamiast przy każdym powtarzaniu pętli. Zauważmy, że ten drobny zysk na wydajności jest uzyskany kosztem utraty czytelności. Druga sekwencja kodu jest bliska kodu spaghetti, niż oryginalnej implementacji. Taka jest często cena małego wzrostu wydajności. Dlatego też, powinniśmy ostrożnie analizować nasz kod, dla zapewnienia, że wzrost wydajności jest wart utraty przejrzystości. Dużo częściej programiści asemblerowi poświęcają przejrzystość dla wątpliwej zyskowności w wydajności, tworząc niemożliwe do zrozumienia programy. 10.8.2 WYKONYWANIE PĘTLI WSTECZNYCH Z powodu właściwości flag na 80x86, pętle z zakresem od jakiejś liczby w dół do (lub w górę do) zera są bardziej wydajne niż inne .Porównajmy poniższe pętle pascalowskie i kodach, które generują: for I := 1 to 8 do for I := 8 downto 1 do K := K+I-J; K := K+I - J; mov I, 1 mov I, 8 mov ax, K FLP: mov ax, K add ax, I add ax, I sub ax, J sub ax, J mov K, ax mov K, ax inc I dec I cmp I, 8 jnz FLP jle FLP Zauważmy że poprzez uruchomienie pętli od osiem do jeden (kod po prawej) zachowujemy porównanie przy każdym powtórzeniu pętli. Niestety nie możemy uczynić wszystkich pętli wstecznymi. Jednak dzięki lekkiemu wysiłkowi i koercji możemy doprowadzić ,żeby większość pętli pracowała jako wsteczne. Jeśli raz uczynisz pętle wsteczna jest dobrym kandydatem instrukcja loop (która będzie poprawiała wydajność pętli na pre-procesorach 486). Powyższy przykład powiedzie się dobrze ponieważ pętla działa od osiem do jeden. Pętla kończy się kiedy zmienna sterująca pętli ma wartość zero. Co się stanie jeśli musimy wykonać pętlę kiedy zmienna sterująca pętli idzie do zera. Na przykład przypuśćmy, że powyższa pętla ma zakres od siedmiu w dół do zera. Tak długo jak górna granica jest dodatnia, możemy zamienić instrukcję jns w miejsce powyższej instrukcji jnz dla powtarzania pętli jakąś określoną ilość razy: mov I, 7 FLP: mov ax, K add ax, I sub ax, J mov K, ax dec I jns FLP Pętla ta będzie powtarzana osiem razy przyjmując wartości od siedem do zera w każdym wykonaniu pętli. Kiedy zostanie zmniejszona z zera do minus jeden, zostanie ustawiona flaga znaku i pętla się zakończy. Zapamiętajmy, że jakieś wartości mogą wyglądać na dodatnie ale są ujemne. Jeśli zmienna sterująca pętli jest bajtowa, wtedy wartość z zakresu 128.255 jest ujemna. Podobnie 16 bitowa wartość z zakresu 32768..65535 jest ujemna. Dlatego też zainicjowanie zmiennej sterującej pętli wartością z zakresu 129.. 255 lub 32769..65535 (lub oczywiście zerem) spowoduje, że pętla zakończy się po jednokrotnym wykonaniu. Może to sprawić nam dużo kłopotu jeśli nie będziemy ostrożni FLP:
10.8.3 OBLICZANII NIEZMIENNIKÓW PĘTLI Obliczanie niezmienników pętli jest jakimś obliczeniem, które pojawia się wewnątrz pętli, które zawsze przynosi ten sam wynik. Nie potrzebujemy robić takiego obliczania wewnątrz pętli. Poniższy Pascalowski kod demonstruje pętlę, która zawiera obliczanie niezmienników: FOR I := 0 TO N DO K := K+(I+J-2);
Ponieważ J nigdy nie zmienia się w trakcie całego wykonywania tej pętli, podwyrażenie „J-2” może być obliczone na zewnątrz pętli a jej wartość jest używana w wyrażeniu wewnątrz pętli: temp := J-2; FOR I := 0 TO N DO K := K+ (I+temp); Oczywiście ,jeśli jesteśmy faktycznie zainteresowani poprawianiem wydajności tej szczególnej pętli,dużo lepszym sposobem obliczenia K jest stosowanie formuły
To obliczenie dla K jest oparte na formule:
Jednakże, proste obliczanie takie jak to nie jest zawsze możliwe. poza tym pokazuje to, że lepszy algorytm jest prawie zawsze lepszy niż skomplikowany kod jaki możemy wykombinować. W asemblerze, obliczenia niezbędników są równie skomplikowane. Rozważmy taką konwersję powyższego kodu pascalowego: mov ax, J add ax, 2 mov temp, ax mov ax, n mov I, ax FLP: mov ax, K add ax, I sub ax, temp mov K, ax dec I cmp I, -1 jg FLP Oczywiście, pierwszym udoskonaleniem jakie możemy zrobić, jest przeniesienie zmiennej sterującej pętli (I) do rejestru. To daje nam poniższy kod: mov ax, J inc ax inc ax mov temp, ax mov cx, n FLP: mov ax, K add ax, cx sub ax, temp mov K, ax dec cx cmp cx, -1 jg FLP Działanie to przyspiesza pętlę poprzez usuniecie dostępu do pamięci przy każdym powtórzeniu pętli. Idąc krok dalej, dlaczego nie zastosować rejestru do podtrzymania wartości temp zamiast komórki pamięci:
FLP:
mov inc inc mov mov add
bx, J bx bx cx, n ax, K ax,cx
sub ax, bx mov K, ax dec cx cmp cx. -1 jg FLP Co więcej, dostęp do zmiennej K może być również usunięty z pętli: mov bx, J inc bx inc bx mov cx, n mov ax, K FLP: add ax, cx sub ax, bx dec cx cmp cx, -1 jg FLP Końcową poprawką jest zastąpienie instrukcji loop przez instrukcje dec cx / cmp cx, -1 / JG FLP. Niestety pętla ta musi być powtarzana kiedykolwiek zmienna sterująca pętli trafia na zero, instrukcja loop nie może tego zrobić. Jednakże, możemy rozwinąć ostatnie wywołanie pętli (zobacz następną sekcję) i zrobić to obliczenie na zewnątrz pętli jak pokazano poniżej: mov bx, J inc bx inc bx mov cx, n mov ax, K FLP: add ax, cx sub ax, bx loop FLP sub ax, bx mov K, ax Jak widzimy, te udoskonalenia redukują liczbę instrukcji wykonywanych wewnątrz pętli a te instrukcje, które pojawiają się wewnątrz pętli są bardzo szybkie ponieważ wszystkie odnoszą się do rejestrów zamiast do komórek pamięci. Usuwanie obliczeń niezmienników i niepotrzebnych komórek pamięci z pętli (szczególnie pętli wewnętrznej w zbiorze pętli zagnieżdżonej) może stworzyć radykalną poprawę wydajności w programie. 10.8.4 PĘTLE ROZWIJANE Dla małych pętli, to znaczy takich, których treść składa się z kilku instrukcji ,koszt wymagany dla przetwarzania pętli może stanowić znaczący procent całego czasu przetwarzania. Na przykład, spójrzmy na poniższy kod pascalowski i powiązany kod asemblera 80x86: FOR I := 3 DOWNTO 0 DO A[I} := 0; mov I, 3 mov bx, I shl bx, 1 mov A[bx], 0 dec I jns FLP Każde wykonanie pętli wymaga pięciu instrukcji. Tylko jedna instrukcja wykonuje żądaną operację (przesuniecie zero do elementu A) Pozostałe cztery instrukcje konwertują zmienną sterującą pętli na indeks do A i sterują powtarzaniem pętli .Dlatego też, zabiera 20 instrukcji zrobienie operacji logicznej wymaganej przez cztery. Ponieważ jest wiele poprawek które możemy uczynić w tej pętli opierając się na informacjach przedstawionych do tej pory, rozważmy ostrożnie dokładnie co jest takiego, że ta pętla się wykonuje - jest to proste przechowanie czterech zer od A[0] do A[3]. Bardziej wydajnym podejściem jest zastosowanie czterech instrukcji mov dla wykonania tego samego zadania. Na przykład, jeśli A jest tablicą słów wtedy poniższy kod inicjuje A dużo szybciej niż kod powyższy: FLP:
mov A, 0 mov A+2, 0 mov A+4, 0 mov A+6, 0 Możemy poprawić szybkość wykonania i rozmiar kodu przez zastosowanie rejestru ax do przechowywania zera: xor ax, ax mov A, ax mov A+2, ax mov A+4, ax mov A+6, ax Chociaż jest to przykład banalny, pokazuje korzyści pętli rozwijanej. Jeśli ta prosta pętla pojawi się zamknięta wewnątrz zbioru pętli zagnieżdżonych, będzie możliwa redukcja instrukcji 5:1 podwójnej wydajności tej sekcji naszego programu. Oczywiście nie możemy rozwijać wszystkich pętli. Pętle które wykonują się zmienną liczbę razy nie mogą być rozwinięte ponieważ jest to rzadki sposób określania (w czasie asemblacji) liczby razy, ile pętla będzie się wykonywała. Dlatego rozwinięcie pętli jest procesem najlepszym dla pętli, które wykonują się znaną liczbę razy. Nawet jeśli powtarzamy pętlę jakąś stałą liczbę iteracji, może ona nie być dobrym kandydatem na pętlę rozwijaną. Pętle rozwijane tworzą imponującą poprawę wydajności kiedy liczba instrukcji wymaganych do sterowania pętlą (i działania na innych kosztownych operacjach) przedstawia znaczący procent całkowitej liczby instrukcji w pętli. Mając powyższą pętle zawierającą 36 instrukcji w treści pętli (wyłączając cztery instrukcje nadmiarowe), wtedy poprawa wydajności będzie ,w najlepszym razie, tylko 10% (porównując z 300-400% ). Dlatego tez, koszt pętli rozwijanej , tj wszystkie dodatkowe kody, które muszą być wprowadzone do naszego programu, szybko osiąga małego zwrotu ponieważ treść pętli rośnie bardziej lub zwiększa się liczba iteracji. Co więcej, wprowadzanie tego kodu do naszego programu może stać się całkiem przykre .Dlatego też pętla rozwijana jest najlepszą techniką stosowaną w małych pętlach. Zauważ, chipy superskalarne x86 (Pentium i późniejsze) mają sprzętowe przewidywanie rozgałęzień i stosują inne techniki dla poprawy wydajności. Pętle rozwijane na takich systemach w rzeczywistości spowalnia kod ponieważ te procesory są optymalizowane do wykonywania krótkich pętli 10.8.5 INDUKCJA ZMIENNYCH Poniżej jest przedstawiona lekko zmodyfikowana pętla przedstawiona w poprzedniej sekcji: FOR I := 0 TO 255 DO A[I] := 0; mov I, 0 FLP: mov bx, I shl bx, 1 mov A[bx], 0 inc I cmp I, 255 jbe FLP Chociaż rozwinięcie tego kodu stworzy olbrzymią poprawę wydajności, zajmuje 257 instrukcji do wykonania tego zadania. Jednakże, możemy sporo zredukować czas wykonania treści pętli stosując indukcje zmiennych. Zmienna indukcyjna jest zmienną, której wartość zależy wyłącznie od wartości jakiejś innej zmiennej, W powyższym przykładzie , indeks do tablicy A śledzi zmienna sterująca pętli (jest zawsze równa wartości zmiennej sterującej pętli razy dwa)Ponieważ I nie pojawia się nigdzie indziej w pętli ,nie ma sensu wykonywanie wszystkich obliczeń na I. Dlaczego nie działać bezpośrednio na wartościach indeksu tablicy? Poniższy kod demonstruje tą technikę: mov bx, 0 FLP: mov A[bx], 0 inc bx inc bx cmp bx, 510 jbe FLP Tu, kilka instrukcji uzyskuje dostęp do pamięci, gdzie były zastąpione instrukcjami które uzyskują dostęp tylko do rejestrów. Inną poprawą jaką możemy uczynić jest skrócenie instrukcji MOV A][bx],0 stosując poniższy kod:
lea bx, A xor ax, ax FLP: mov [bx], ax inc bx inc bx cmp bx, offset A+510 jbe FLP Ten stransformowany kod poprawia wydajność pętli nawet bardzo. Jednakże, możemy wydajność poprzez zastosowanie instrukcji loop i rejestru cx do wyeliminowania instrukcji cmp: lea bx, A xor ax, ax mov cx, 256 FLP: mov [bx], ax inc bx inc bx loop FLP Ta końcowa transformacja tworzy najszybszy wykonywalną wersję tego kodu.
poprawić
10.8.6 INNE SPOSBY POPRAWY WYDAJNOŚCI Jest wiele innych sposobów poprawy wydajności pętli wewnątrz naszego programu asemblerowego. Dodatkowa sugestia, dobry tekst o kompilatorach taki jak „Compilers, Principles,Techniques and Tools”Aho, Sethi i Ullmana będzie doskonałym dodatkiem. 10.9 INSTRUKCJE ZAGNIEŻDŻONE Tak długo jak się będziemy trzymali szablonów dostarczonych w przykładach prezentowanych w tym rozdziale ,bardzo łatwo jest zagnieździć instrukcje jedną wewnątrz drugiej. Tajemnicą uczynienia naszej sekwencji asemblerowej zagnieżdżonej jest zapewnienie, że każda konstrukcja ma jeden punkt wejścia i jeden punkt wyjścia .Jeśli jest to ten przypadek ,wtedy przyjdzie nam z łatwością połączyć instrukcje. Wszystkie instrukcje omawiane w tym rozdziale są zgodne z tą zasadą. Być może najpowszechniej zagnieżdżanymi instrukcjami są instrukcje if..then..else. Aby zobaczyć jak łatwo zagnieździć te instrukcje w asemblerze rozważmy poniższy kod pascalowski: if (x = y) then if (I >= J) then writeln (‘At point 1’) else writeln (‘At point 2’) else write (‘Error condition’); Konwersja tego zagnieżdżenia do języka asemblera zaczyna się od znajdującego się na zewnątrz if, konwertujemy go do asemblacji, potem pracujemy na wewnętrznym if: ; if (x = y) then mov ax, X cmp ax, Y jne Else0 ;kładziemy wewnętrzne if tutaj jmp IfDone0 ;Else write (‘Error condition’); Else0:
print byte „Error condition”,0
IfDone0: Jak możemy zobaczyć, powyższy kod działa z instrukcją „if (X=Y)” pozostawiając miejsce dla drugiego if. Teraz dodamy drugie if jak następuje: ; if (x=y) then mov ax ,X cmp ax, Y jne Else0 ; If (I >=J) then writeln (‘At point 1’)
mov cmp jnge print byte jmp ; Else writeln (‘At point2’); Else1:
print byte
ax, I ax, J Esle1 „At point1”,cr,lf,0 IfDone1
„At point 2”,cr,lf,0
IfDone1: Jmp ; Else write (‘Error condition’); Else0:
print byte
IfDone0
„Error condition”,0
IfDone0: Jest to oczywista optymalizacja, której nie chcemy rzeczywiście robić dopóki szybkość nie stanie się prawdziwym problemem. Zauważmy w wewnętrznej instrukcji if, że instrukcja JMP IFDONE1 po prostu skacze do instrukcji jmp, która przekazuje sterowanie do IfDone0 Jest bardzo kuszące zastąpić pierwszy jmp przez inny który skacze bezpośrednio do IfDone0.Istotnie,kiedy wchodzimy do środka i optymizujemy nasz kod, będzie to dobrze zrobiona optymalizacja. Jednakże, nie powinniśmy robić takiej optymalizacji naszego kodu, chyba, że rzeczywiście potrzebujemy szybkości. Robiąc tak uczynimy nasz kod trudniejszym do odczytania i zrozumienia. Pamiętajmy ,że chcemy aby wszystkie nasze struktury sterujące mają jedno wejście i jedno wyjście. Zmieniając ten skok jak opisano, dostaniemy dwa punkty wyjścia z wewnętrznej instrukcji if. Pętla for jest innym powszechną zagnieżdżoną strukturą sterującą. Jeszcze raz, kluczem do zbudowania struktury zagnieżdżonej jest skonstruowanie najpierw obiektu zewnętrznego i wypełnienie go później obiektami wewnętrznymi .jako przykład rozważmy poniższą zagnieżdżoną pętlę for, która dodaje elementy pary dwu wymiarowych tablic razem for i := 0 to 7 do for k := 0 to 7 do A[i,j]:= B[i,j] + C[i,j]; Jak przedtem zaczniemy od skonstruowania najpierw najbardziej zewnętrznej pętli. Kod ten zakłada, że dx będzie znamienną sterującą pętli dla pętli zewnętrznej (to znaczy dx jest odpowiadające „i”): ;for dx := 0 to 7 do mov dx, 0 cmp dx, 7 jnle EndFor0 ; tu wkładamy wewnętrzną pętle FOR inc dx jmp ForLp0 EndFor0: ForLp0:
Teraz dodamy kod dla zagnieżdżonej pętli loop. Zauważmy że zastosowano rejestr cx dla sterującej pętli w wewnętrznej pętli for tego kodu ;for dx := 0 to 7 do mov ForLp0: cmp jnle ; for cx := 0 to 7 do mov
dx,0 dx, 7 EndFor0 cx,0
zmiennej
ForLp1:
cmp cx, 7 jnle EndFor1 ; Wkładamy tu kod dla A[dx,cx] := b[dx,cx]+C [dx,cx] inc cx jmp ForLp1 EndFor1: inc dx jmp ForLp0 EndFor0: Końcowym krokiem jest dodanie kodu, który wykonuje to rzeczywiste obliczenie. 10.10 PĘTLE OPÓŹNIENIA CZASOWEGO Większość czasu komputer pracuje zbyt wolno jak dla większości ludzi. Jednakże ,są sytuacje, kiedy on w rzeczywistości pracuje zbyt szybko .Jednym popularnym rozwiązaniem jest stworzenie pustej pętli dla tracenia małej ilości czasu. W Pascalu powszechnie występują pętle takie jak; for i := 1 to 10000 do; W asemblerze, możemy zobaczyć porównywalne pętle: mov cx, 8000h DelayLp: loop DelayLp Przez ostrożny wybór liczby iteracji, możemy uzyskiwać stosunkowo dokładne przedziały opóźnienia jest jednak małe „ale”. To stosunkowo dokładne przedziały opóźnienia są dokładne tylko na Twojej maszynie. Jeśli przeniesiemy nasz program na inną maszynę z innym CPU, szybkością zegara, liczbą stanów oczekiwania, innym rozmiarem pamięci cache lub innymi cechami, odkryjemy, że nasz pętla opóźniająca zajmuje kompletnie inną ilość czasu. Ponieważ jest lepiej niż sto do jednego różnic w szybkości pomiędzy dzisiejszymi PC wysokiej klasy a niskiej klasy, powinniśmy przyjąć żadnej niespodzianki, że powyższa pętla będzie wykonywała się 100 razy szybciej na jednych maszynach niż na innych. Fakt, że jeden CPU działa 100 razy szybciej niż inny nie oznacza, że nie musimy mieć pętli opóźniającej wykonującej się stałą ilość razy. Istotnie, czyni to problem dużo bardziej poważniejszym. Na szczęście, PC dostarczają sprzętowej matrycy tajmera, które działają z tą samą szybkością bez względu na szybkość CPU. Ten tajmer utrzymuje czas dzienny dla systemu operacyjnego, więc jest bardzo ważne, że pracuje z ta samą szybkością niezależnie czy pracuje na 8088 czy Pentium. W rozdziale o przerwaniach nauczymy się o rzeczywistych łatkach do tych urządzeń wykonujących różne zadania. Teraz po prostu wykorzystamy fakt, że ten układ tajmera wymusza z CPU zwiększenie 32 bitowej komórki pamięci (40:6ch) o 18.2 razy na sekundę .Poprzez patrzenie na tą zmienną możemy określić szybkość CPU i zmodyfikować licznik wartości dla pustej pętli odpowiednio. Podstawową ideą poniższego kodu jest obserwacja zmiennej tajmera BIOSA dopóki się zmienia. jedna zmiana, zaczyna liczyć liczbę iteracji przez jakąś część pętli dopóki zmienna tajmera BIOSA nie zmieni się ponownie. Jeśli wykonujemy podobną pętlę tą sam ilość razy będzie wymaga około 1/18.2 sekundy na wykonanie. Poniższy program demonstruje jak stworzyć taki podprogram Delay
10.14 PODSUMOWANIE Rozdział ten omówił implementację różnych struktur sterujących w programach języka asemblera zawierających instrukcje warunkowe (instrukcje if..then..else i case), stany maszynowe i iteracje (pętle while repeat..until (do/while), loop..endloop i for).Podczas gdy asembler daje nam elastyczność w tworzeniu własnych struktur sterujących, robiąc to często tworzymy programy które są trudne do odczytania i zrozumienia. Chyba że sytuacja wymaga absolutnie czegoś innego ,powinniśmy spróbować wzorować nasze struktury sterujące asemblera na tych z języków wysokiego poziomu jeśli to możliwe. Najbardziej powszechna struktura sterująca pojawiająca się w HLL’ach to instrukcja IF..THEN..ELSE. Możemy łatwo zsyntetyzować instrukcje if..then i if..then..else w asemblerze stosując instrukcję cmp, skoków
warunkowych, i instrukcji jmp. Aby zobaczyć jak skonwertować HLL’ową instrukcję if..then..else do języka asemblera zajrzyj do *Sekwencja IF..THEN..ELSE” Drugą popularną instrukcją warunkową HLLi jest instrukcja case (switch).Instrukcja case dostarcza wydajnego sposobu dla sterowania przepływem danych do jednej lub wielu różnych instrukcji w zależności od wartości jakiegoś wyrażenia. podczas gdy jest wiele sposobów implementacji instrukcji case w asemblerze, najpowszechniejszym sposobem jest użycie tablicy skoków .Dla instrukcji case z przyległymi wartościami, jest to prawdopodobnie najlepsza implementacja. Dla instrukcji case, która ma znaczną przestrzeń nie przylegających wartości, implementacja if..then..else lub jakaś inna technika są lepsze szczegóły zajrzyj: *Instrukcja CASE Stany maszynowe dostarczają użytecznego paradygmatu w pewnych programistycznych sytuacjach. Sekcja kodu która implementuje stany maszynowe utrzymuje historię wcześniejszego wykonania wewnątrz zmiennej stanu .Późniejsze wykonanie kodu pobiera historię różnych „stanów” w zależności od wcześniejszego wykonania. Skoki pośrednie dostarczają wydajnego mechanizmu dla implementacji stanów maszynowych w asemblerze. Rozdział ten dostarcza krótkiego wprowadzenia do stanów maszynowych. Aby zobaczyć jak implementować stany maszynowe za pomocą skoków pośrednich zobacz: *Stany maszynowe i skoki pośrednie Język asemblera dostarcza bardzo mocnych elementów podstawowych dla konstruowania szerokiego wyboru struktur sterujących. Chociaż rozdział ten skupia się na symulowaniu konstrukcji HLL, możemy zbudować jakieś zawiłe struktury sterujące korzystając z instrukcji cmp 80x86 i skoków warunkowych. Niestety, wynik może być bardzo różny do zrozumienia, zwłaszcza przez kogoś innego niż samego autora. Chociaż asembler daje nam wolność do robienia tego co chcemy, dojrzały programista ćwiczy umiar i wybiera tylko taki przebieg sterowania który jest łatwy do odczytu i zrozumienia ;nigdy nie zadawala się kodem zagmatwanym, chyba, że jest to absolutnie konieczne. *Kod Spaghetti Iteracja jest jednym z trzech podstawowych składników języka programowania budowanym dla maszyn Von Neumanna. Struktura pętli sterującej dostarcza mechanizmu podstawowej iteracji w większości HLLi. Język asemblera nie dostarcza żadnej pętli podstawowej. Nawet instrukcja 80x86 loop nie jest pętlą rzeczywistą ,jest instrukcją zmniejszania, porównania i odgałęzienia. Pomimo to jest łatwo zsyntetyzować popularne struktury pętli sterujących w asemblerze. Poniższe sekcje omawiają jak zbudować HLL’owską strukturę pętli sterującej w asemblerze: *Pętle *Pętla WHILE *Pętle Repeat..Until *Pętle LOOP..ENDLOOP Pętla FOR Pętle programowe często pochłaniają większość czasu CPU w typowym programie .dlatego też, jeśli chcemy poprawić wydajność naszego programu, pętle są na pierwszym miejscu któremu chcemy się przyjrzeć. Rozdział ten dostarczył kilku sugestii pomocnych przy poprawie wydajności pewnych typów pętli w programach asemblerowych Podczas gdy nie dostarczają kompletnego przewodnika do optymalizacji, poniższe sekcje dostarczają popularnych technik stosowanych przez kompilatory i doświadczonych programistów języka asemblera: *Stosowanie rejestrów i Pętli *Poprawianie wydajności *Przesuwanie warunku zakończenia na koniec pętli *Wykonywanie pętli wstecznych *Obliczanie pętli niezmienników *Pętle rozwijane *Zmienne wywoływane *Inne sposoby poprawiania wydajności 10.15 PYTANIA 1) Skonwertuj poniższe Pascalowskie instrukcje do asemblera: (zakładamy, że wszystkie zmienne są dwubajtowymi liczbami całkowitymi ze znakiem) a) IF (X=Y) then A := B; b) IF (X<=Y) then X:=X+1 ELSE Y:= Y-1;
2)
3)
4) 5) 6) 7) 8) 9)
10)
c) IF NOT ((X=Y) and (Z<>T)) then Z :=T else X := T; d) IF (X=0) and ((Y-2)>1) then T:=Y-1; Skonwertuj poniższą instrukcję CASE na asembler: CASE I OF 0: I:=5; 1: J:=J+1; 2: K:=I+J; 3: K:=I-J; Otherwise I:=0; END; Która z metod implementacji dla instrukcji CASE (tablica skoków lub forma IF) tworzy najmniejszą ilość kodu (wliczając w to tablicę skoków ,jeśli użyjemy) dla poniższych instrukcji CASE? a) CASE I OF 0:instr; 100:instr; 1000:instr; END; b) CASE I OF 0:instr; 1:instr; 2:instr; 3:instr; 4:instr; END; Dla pytani trzy ,która forma tworzy najszybszy kod? Zaimplementuj instrukcje CASE z pytania trzy używając język asemblera 80x86 Jakie są trzy składniki tworzące pętlę? Jakie są trzy główne różnice pomiędzy pętlami WHILE, REPEAT..UNTIL i LOOP..ENDLOOP? Co to jest zmienna sterująca pętli? Skonwertuj poniższe pętle WHILE do asemblera :(Notka: nie optymalizuj tych pętli,)
Skonwertuj poniższe pętle REPEAT ..UNTIL do języka asemblera.
11) Skonwertuj poniższe pętle LOOP..ENDLOOP do języka asemblera
12) Jakie są różnice ,jeśli, pomiędzy pętlami z pytań 4 ,5 i 6? Czy wykonują one takie same operacje? Która wersja jest bardziej wydajna? 13) Przepisz dwie pętle przedstawione w poprzednim przykładzie w asemblerze, tak sprawnie jak możesz 14) Poprze proste dodanie instrukcji JMP, skonwertuj dwie pętle z pytania cztery do pętli Repeat..Until 15) Poprzez proste dodanie instrukcji JMP, skonwertuj dwie pętle z pytania pięć do pętli WHILE 16) Skonwertuj poniższe pętle FOR na asembler (Notka: masz wolną rękę w użyciu podprogramów dostarczonych w Bibliotece Standardowej UCR):
17) Słowo zarezerwowane DOWNTO używane wraz z Pascalowską pętlą FOR, uruchamia licznik pętli od najwyższej liczby w dół do najmniejszej liczby. Pętla FOR ze słowem zarezerwowanym DOWNTO jest odpowiednikiem poniższej pętli WHILE:
18) 19) 20) 21) 22) 23) 24) 25)
Zaimplementuj poniższe pętle pascalowskie FOR w asemblerze: a) FOR I := start downto stop do WriteLn(I); b) FOR I := 7 downto 0 do FOR J := 0 to 7 do K:=K*(I-J); c) FOR I := 255 downto 16 do A[I] := A[240-I]-I; Przepisz pętlę z pytania 11b utrzymując I w BX, J w CX a K w AX Jak przesunąć test zakończenia pętli na koniec pętli poprawiając wydajność tej pętli? Co to jest obliczanie niezbędników pętli? Jak wykonać pętlę wsteczną poprawiając wydajność pętli? Co oznacza rozwinięcie pętli? Jak rozwinąć pętlę poprawiając wydajność pętli? Daj przykład pętli która nie może być rozwinięta Daj przykład pętli, która może być, ale nie powinna być rozwinięta
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK konsultacja naukowa: NEKRO [email protected]
[email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ JEDENASTY: PROCEDURY I FUNKCJE Budowa modularna jest jednym z kamieni węgielnych programowania strukturalnego. Program modularny zawiera bloki kodu z pojedynczym punktem wejścia i wyjścia. Możemy użyć ponownie dobrze napisaną sekcję kodu w innych programach lub innej części istniejącego programu. Jeśli użyjemy ponownie istniejącą część kodu ,nie musimy tworzyć kodu, ani też debuggować tej części kodu ponieważ (przypuszczalnie) zostało to już zrobione. Przy rosnących kosztach projektowania oprogramowania, budowa modularna staje się bardzo ważna ponieważ oszczędza czas. Podstawową jednostką program u modularnego jest moduł. Moduły mają różne znaczenia dla różnych ludzi ,tutaj możemy założyć, że terminy moduł, podprogram, podprocedura, jednostka programowa ,procedura i funkcja wszystkie są synonimami. Procedura jest podstawowa dla stylu programowania. Języki proceduralne to Pascal, BASIC,C++,FORTRAN,PL/I i ALGOL Przykłady języków nie proceduralnych to APL,LISP,SNOBOL4 ICON,FORTH,SETL,PROLOG i inne ,które są oparte o inne konstrukcje programistyczne takie jak funkcjonalna abstrakcja lub dopasowanie do wzorca .Język asemblera jest zdolny pełnić obowiązki języka proceduralnego lub nieproceduralnego .ponieważ prawdopodobnie jesteśmy dużo bardziej zapoznani z paradygmatami programowania proceduralnego, ten tekst trzymać się będzie stymulowania konstrukcji proceduralnych w języku asemblera 80x86 11.0 WSTĘP Rozdział ten przedstawia wprowadzenie do procedur i funkcji w asemblerze. Omawia podstawowe zasady, przekazywanie parametrów, wyników funkcji, zmiennych lokalnych i rekurencje Zastosujemy większość technik, które ten rozdział omawia w typowych programach asemblerowych. Omawianie procedur i funkcji będzie kontynuowane w następnym rozdziale; ten rozdział omawia zaawansowane techniki, które nie są powszechnie stosowane w programach asemblerowych. poniższa część, która ma przedrostek „•” jest niezbędna. Te części z „⊗” omawiają zaawansowane tematy które możemy zechcieć odłożyć na później. • Procedury ⊗ Procedury bliskie i dalekie • Funkcje • Zachowywanie stanów maszyny • Parametry • Przekazywanie parametrów przez wartość • Przekazywanie parametrów przez referencję ⊗ Przekazywanie parametrów przez wartość zwrotną ⊗ Przekazywanie parametrów przez wynik ⊗ Przekazywanie parametrów przez nazwę • Przekazywanie parametrów w rejestrach • Przekazywanie parametrów w zmiennych globalnych • Przekazywanie parametrów na stos • Przekazywanie parametrów w strumieniu kodu
⊗ Przekazywanie parametrów przez blok parametrów • Wyniki funkcji • Zwracanie wyniku funkcji w rejestrach • Zwracanie wyniku funkcji na stos • Zwracanie wyniku funkcji w komórkach pamięci • Efekty uboczne ⊗ Przechowywanie zmiennych lokalnych ⊗ Rekurencja
11.1 PROCEDURY W środowisku proceduralnym, podstawową jednostką kodu jest procedura. Procedura jest zbiorem instrukcji, które obliczają jaką wartość lub wykonują jakąś czynność (taką jak drukowanie lub odczytywanie wartości znaku). Definicja procedury jest bardzo podobna do definicji algorytmu. Procedura jest zbiorem zasad następujących po sobie, które jeśli są zakończone tworzą jakiś wynik. Algorytm jest również taką sekwencją, ale algorytm gwarantuje zakończenie podczas gdy procedura takiej gwarancji nie daje. Wiele języków proceduralnych implementuje procedury stosując mechanizm call / ret. To znaczy, jakiś kod wywołuje procedurę, procedura robi swoje a potem wraca do miejsca skąd została wywołana. Instrukcje call i return dostarczają mechanizmy wywołania procedury 80x86.Kod wywołujący wywołuje procedurę instrukcją call, procedura wraca do miejsca wywołania instrukcją ret .Na przykład, poniższa instrukcja 80x86 wywołuje podprogram Biblioteki Standardowej UCR sL_putcr: call sl_putcr sl_putcr drukuje sekwencję carriage return /line feed na wyświetlaczu i zwraca sterowanie do instrukcji bezpośrednio po instrukcji call sl_putrc. Niestety, Biblioteka Standardowa UCR nie dostarcza wszystkich podprogramów jakich będziemy potrzebowali .Większość czasu, będziemy musieli pisać swoje własne procedury. Prosta procedura może składać się z niczego więcej niż tylko sekwencji instrukcji zakończenia z instrukcją ret. Na przykład, poniższa „procedura’ zeruje 256 bajtów zaczynając od adresu w rejestrze bx: ZeroBytes; xor ax, ax mov cx, 128 ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret Przez załadowanie rejestru bx adresem bloku 256 bajtów i wykonanie instrukcji call ZeroBytes, możemy wyzerować określony blok. Jako generalna zasada nie możemy zdefiniować własnej procedury w ten sposób, zamiast tego powinniśmy użyć dyrektyw MASM’a proc i endp. Podprogram ZeroByte stosujący dyrektywy proc i endp: ZeroBytes; proc xor ax,ax mov cx, 128 ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret ZeroBytes endp Zapamiętajmy, że proc i endp są dyrektywami asemblera. Nie generują żadnego kodu .Są one mechanizmami pomagającymi uczynić nasz program łatwiejszym do czytania. Dla 80x86 dwa ostatnie przykłady są identyczne; jednakże dla istoty ludzkiej, ostatni jest wyraźnie samodzielną procedurą, inna może być po prostu przypadkowym zbiorem instrukcji wewnątrz jakiejś innej procedury. Rozważmy teraz poniższy kod: ZeroBytes: xor ax, ax jcxz DoFFs ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret DoFFs:
mov mov
cx, 128 ax, 0ffffh
FFLoop:
mov [bx], ax sub bx, 2 loop FFLoop ret Czy są to dwie procedury czy tylko jedna? Innymi słowy, możemy wywoływać program wprowadzając ten kod przy etykiecie ZeroBytes i DoFFs lub tylko ZeroBytes? Zastosowanie dyrektyw proc i endp może pomóc usunąć tę niejasność: Traktujemy jako pojedynczy podprogram: ZeroBytes: proc xor ax, ax jcxz DoFFs ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret DoFFs: FFLoop:
ZeroBytes:
mov mov mov sub loop ret endp
cx, 128 ax, 0ffffh [bx], ax bx, 2 FFLoop
Traktujemy jako dwa oddzielne podprogramy: ZeroBytes: proc xor ax, ax jcxz DoFFs ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ret ZeroBytes: endp DoFFs: proc mov cx, 128 mov ax, 0ffffh FFLoop: mov [bx], ax sub bx, 2 loop FFLoop ret DoFFs endp Zawsze pamiętajmy, że dyrektywy proc i endp są logicznymi separatorami procedury. Mikroprocesor wraca z procedury wykonując instrukcję ret a nie poprzez napotkanie dyrektywy endp .Poniższy kod nie jest odpowiednikiem kodu powyższego: ZeroBytes proc xor ax, ax jcxz DoFFs ZeroLoop: mov [bx], ax add bx, 2 loop ZeroLoop ; zagubiona instrukcja RET ZeroBytes
endp
DoFFs
proc mov mov mov sub
FFLoop:
cx, 128 ax, 0ffffh [bx[, ax bx, 2
loop
FFLoop
; zaginiona instrukcja RET DoFFs
endp Bez instrukcji ret na końcu każdej procedury,80x86 przejdzie do następnego podprogramu zamiast wrócić do miejsca wywołania. Po wykonaniu ZeroBytes, 80x86 zacznie wykonywać podprogram DoFFs (zaczynając od instrukcji mov cx, 128).Zaraz potem, 80x86 zacznie kontynuowanie wykonywania następnych instrukcji aż do dyrektywy endp DoFFs Procedura 80x86 przyjmuje formę: ProcName proc {near | far} ;wybierz near lub far ProcName endp Operand near lub far jest opcjonalny, następna sekcja omawia ich cele. Nazwa procedury musi być zarówno w linii proc jaki i endp. Nazwa procedury musi być unikalna w programie. Każda dyrektywa proc musi mieć dopasowaną dyrektywę endp. Błędne dopasowanie dyrektyw proc i endp wywoła błąd zagnieżdżenia bloku. 11.2 PROCEDURY BLISKIE I DALEKIE 80x86 wspiera podprogramy. Wywołanie bliskie i powrót przekazują sterowanie danymi pomiędzy procedurami w tym samym segmencie kodu. Dalekie wywołanie i powrót przekazują sterowanie między różnymi segmentami. Te dwa mechanizmy wywołania i powrotu odkładają i ściągają adresy powrotne. Generalnie nie stosujemy bliskiej instrukcji call do wywołania dalekiej procedury lub dalekiej instrukcji call do wywołania bliskiej procedury. Opierając się na tej zasadzie, powstaje pytanie :jak możemy sterować emisją bliskiego lub dalekiego call lub ret?” Większość czasu, instrukcja call stosuje następującą składnię: call ProcName a instrukcja ret : ret lub ret przemieszczenie Niestety, instrukcje te nie mówią MASMowi czy wywołujemy bliską czy daleką procedurę lub czy wracamy z dalekiej czy bliskiej procedury. Dyrektywa proc zajmuje się tym. dyrektywa proc ma opcjonalny operand, który jest albo bliski albo daleki. Bliski jest domyślny ,jeśli pole operandu jest puste .Assembler przydziela typ procedury (bliska lub daleka) do symbolu. Kiedykolwiek MASM asembluje instrukcję call, emituje bliskie lub dalekie wywołanie w zależności od operandu .Dlatego też deklarowanie symbolu proc lub proc near wymusza bliskie wywołanie. Podobnie zastosowanie proc far wymusza dalekie wywołanie. Poza sterowaniem generowania bliskiego lub dalekiego wywołania, operand proc również steruje generowaniem kodu ret. Jeśli procedura ma bliski operand, wtedy wszystkie powroty instrukcji wewnątrz tej procedury będą bliskie.. MASM emituje dalekie powroty wewnątrz dalekich procedur. 11.2.1 WYMUSZANIE BLISKICH LUB DALEKICH WYWOŁAŃ I POWROTÓW Raz na jakiś czas możemy chcieć zastąpić mechanizm deklaracji near / far. MASM dostarcza mechanizmu, który pozwala nam wymusić zastosowanie wywołań bliskie/ dalekie i powrotów. Stosujemy operatory near ptr i far ptr do zastąpienia automatycznie przydzielonego wywołania bliskiego lub dalekiego. Jeśli NearLbl jest bliską etykietą a FarLbl jest etykietą daleką, wtedy następująca instrukcja call wygeneruje odpowiednio bliskie i dalekie wywołanie: call NearLbl ;generowanie bliskiego wywołania call FarLbl ;generowanie dalekiego wywołania Przypuśćmy, że musimy wykonać dalekie wywołanie do NearLbl lub bliskie wywołanie do FarLbl .Możemy to wykonać stosując poniższe instrukcje: call far ptr NearLbl ;generowanie dalekiego wywołania call near ptr FarLbl ;generowanie bliskiego wywołania Wywołując bliską procedurę stosujemy daleki call lub wywołując daleką procedurę stosując bliski call, to nie jest coś co będziemy normalnie robić. jeśli wywołamy bliską procedurę stosując daleką instrukcję call, bliski powrót pozostanie wartością cs na stosie. Generalnie, zamiast: call far ptr NearProc powinniśmy zastosować jaśniejszy kod: push cs call NearProc.
Wywoływanie dalekiej procedury bliskim call jest niebezpieczną operacja. Jeśli spróbujemy takiego wywołania, bieżąca wartość cs musi być na stosie. Pamiętajmy, że daleki ret zdejmuje segmentowy adres powrotu ze stosu .Bliska instrukcja call odkłada tylko offset a nie segmentową część adresu powrotnego. Poczynając od MASM v5.0, są jasne instrukcje, które możemy użyć dla wymuszenia bliskiego lub dalekiego ret. Jeśli ret pojawia się wewnątrz procedury deklarowanej przez proc i endp, MASM automatycznie wygeneruje właściwą instrukcję bliskiego lub dalekiego powrotu. Wykonując to użyjemy instrukcji retn i retf .Te dwie instrukcje generują odpowiednio bliski i daleki ret. 11.2.2 PROCEDURY ZAGNIEŻDŻONE MASM pozwala nam zagnieżdżać procedury. To znaczy, definicja jednej procedury może być całkowicie otoczoną wewnątrz innej. Poniżej jest przykład takiej pary procedur: OutsideProc: InsideProc
EndofOutside: OutsideProc
proc jmp proc mov ret endp call mov ret
near EndofOutside near ax, 0 InsideProc bx,0 endp
W odróżnieniu od języków wysokiego poziomu, zagnieżdżanie procedur w asemblerze 80x86 ni służy żadnemu użytecznemu celowi. Jeśli zagnieżdżamy procedurę (jak powyższa InsideProc), będziemy musieli wyraźnie zakodować jmp wokoło zagnieżdżonej procedury. Umieszczając procedury zagnieżdżonej po całym kodzie procedury zewnętrznej (ale jeszcze pomiędzy dyrektywami zewnętrznymi proc /endp) nie osiągamy niczego. Dlatego też, to nie jest dobry powód zagnieżdżania procedur w ten sposób. Kiedy zagnieżdżamy jedną procedurę wewnątrz innej, musi być całkowicie zawarta wewnątrz procedury zagnieżdżającej. To znaczy, instrukcje proc i endp dla procedury zagnieżdżanej muszą leżeć pomiędzy dyrektywami proc i endp zewnętrznymi zagnieżdżającej procedury. Poniższy kod nie jest poprawny: OutsideProc
proc near InsideProc proc near OutsideProc endp InsideProc endp Procedury OutsideProc i InsideProc zachodzą na siebie, nie są zagnieżdżone. Jeśli spróbujemy stworzyć zbiór procedur takich jak ta, MASM zaraportuje „błąd zagnieżdżanego bloku”. Rysunek 11 demonstruje to graficznie:
Rysunek 11.1 Niepoprawne zagnieżdżanie procedur
Rysunek 11.2 Poprawne zagnieżdżanie procedur
Rysunek 11.3 poprawne zagnieżdżanie Procedura/Segment Jedyną akceptowalną formą przez MASM jest ta pojawiająca się na rysunku 11.2 Poza dopasowaniem wewnątrz otaczającej procedury, grupa proc/endp musi dopasować się całkowicie wewnątrz segmentu. Dlatego poniższy kod jest niepoprawny: cseg segment MyProc proc near ret cseg ends MyProc endp Dyrektywa endp musi pojawić się przed instrukcją cseg ends ponieważ MyProc zaczyna się wewnątrz cseg. Dlatego też procedury wewnątrz segmentów muszą zawsze przybierać formę pokazaną na rysunku 11.3. Nie tylko możemy gnieździć procedury wewnątrz innych procedur i segmentów ,ale możemy zagnieżdżać również segmenty wewnątrz procedur i segmentów. Jeśli lubimy symulować procedury C lub Pascala w asemblerze, możemy stworzyć zmienną deklarującą sekcje na początku każdej procedury jaka tworzymy, podobnie jak w Pascalu: cgroup
group
cseg1, cseg2
cseg1 cseg1
segment para public ‘code’ ends
cseg2 cseg2
segment para public ‘code’ ends
Rysunek 11.4 Przykład rozmieszczenia pamięci dseg dseg
segment ends
para public ‘data’
cseg1
segment para public ‘code’ assume cs:cgroup, ds.:dseg
MainPgm proc near ;deklaracja danych dla głównego programu: dseg I J dseg
segment para public ‘data’ word ? word ? ends
; Procedury które są lokalne w głównym programie: cseg2
segment para public ‘code’
ZeroWords
proc
near
;zmienne lokalne dla ZeroBytes: dseg AXSave BXSave CXSave dseg
segment para public ‘data’ word ? word ? word ? ends
;kod dla procedury ZeroBytes: mov AXSave,ax mov CXSave, cx mov BXSave, bx xor ax, ax ZeroLoop: mov [bx], ax inc bx inc bx loop ZeroLoop mov ax, AXSave mov bx, BXSave mov cx, CXSave ret ZeroWords endp cseg2
ends
; rzeczywisty główny program zaczyna się tutaj: mov bx, offset Array mov cx, 128 call ZeroWords ret MainPgm endp cseg1 ends end System załaduje ten kod do pamięci jak pokazano na rysunku 11.4 ZeroWords występuje w programie głównym ponieważ należy do innego segmentu (cseg2) niż MainPgm (cseg1).Pamiętamy, że asembler i linker łączą segmenty o tej samej nazwie klasy w pojedynczy segment przed załadowaniem do pamięci. Możemy zastosować tą cechę asemblera do „pseudo-paskalizacji” naszego kodu w powyżej pokazany sposób. Jednakże ,prawdopodobnie nie uczynimy naszego programu bardziej czytelnym niż stosując proste podejście nie zagnieżdżania. 11.3 FUNKCJE Różnica pomiędzy funkcją a procedurą w języku asemblera jest głównie w sposobie definiowania. Celem dla funkcji jest zwrócenie jakiejś konkretnej wartości, podczas gdy celem funkcji jest wykonanie jakiegoś działania. Dla deklarowania funkcji w asemblerze stosujemy dyrektywy proc/ endp. Wszystkie zasady i techniki jakie stosujemy dla procedury stosujemy również dla funkcji. tekst ten będzie omawiał później funkcje w tym rozdziale w sekcji o wynikach funkcji. Na razie procedury będą znaczyły procedura lub funkcja 11.4 ZACHOWANIE STANÓW MASZYNY Spójrzmy na ten kod: mov cx, 10 call PrintSpace putcr loop Loop0 PrintSpace proc near mov al., ‘ ‘ mov cx, 40 PSLoop: putc loop PSLoop ret PrintSpaces endp Ta część kodu próbuje wydrukować dziesięć linii każda po 40 spacji. Niestety jest subtelny błąd, który powoduje, że drukuje się 40 spacji na linię w pętli nieskończonej. Program główny stosuje instrukcję loop do wywołania PrintSpaces 10 razy. PrintSpaces używa cx do zliczania 40 spacji do druku. PrintSpaces wraca kiedy cx zawiera zero. Program główny drukuje wtedy, carriage return / line feed , zmniejsza cx i zaczyna powtórkę ponieważ cx nie jest zerem (zawsze będzie zawierać 0FFFFh w tym miejscu) Tu problem jest taki, że podprogram PrintSpaces nie przechowuje rejestru cx. Zachowanie rejestru znaczy, że zachowujemy go na wejściu do podprogramu i przywracamy przed wyjściem. Mając podprogram PrintSpaces zachowujący zawartość rejestru cx, powyższy program funkcjonowałby poprawnie. Stosujemy instrukcje 80x86 push i pop do przechowania wartości rejestru, podczas gdy używamy ich do czegoś innego. Rozważmy poniższy kod dla PrintSpaces: Loop0:
PrintSpaces
PSLoop:
PrintSpaces
proc push push mov mov putc loop pop pop ret endp
near ax cx al., ‘ ‘ cx, 40 PSLoop cx ax
Zauważmy, że PrintSpaces zachowuje i przywraca ax i cx (ponieważ procedura ta modyfikuje te rejestry). Również zauważmy, że ten kod zdejmuje rejestry ze stosu w odwrotnej kolejności, niż je tam odkładał. Operacje na stosie narzucają taką kolejność. Albo wywołujący (kod zawierający instrukcję call) albo wywoływany (podprogram) mogą wziąć odpowiedzialność za zachowanie rejestrów .W powyższym przykładzie wywoływany zachowuje rejestry. Poniższy przykład pokazuje jak może wyglądać ten kod, jeśli wywołujący zachowuje rejestry: mov cx, 10 Loop0: push ax push cx call PrintSpaces pop cx pop ax putcr loop Loop) PrintSpaces proc near mov al., ‘ ‘ mov cx, 40 PSLoop: putc loop PSLoop ret PrintSpaces endp Są dwie korzyści z zachowywania przez wywoływanego: miejsce i pielęgnowalność. Jeśli wywoływany przechowuje wszystkie wykorzystywane rejestry, wtedy jest tylko jedna kopia instrukcji push i pop, które zawiera ta procedura. Kiedy wywołujący zachowuje wartości w rejestrach, program potrzebuje zbioru instrukcji push i pop przy każdym wywołaniu. To nie tylko czyni nasze programy dłuższymi, czyni je również trudniejszymi do pielęgnacji. Pamiętanie które rejestry odkładamy i zdejmuje przy każdym wywołaniu procedury nie jest czymś łatwym do zrobienia. Z drugiej strony, podprogram może niepotrzebnie przechować jakieś rejestry, jeśli przechowuje wszystkie rejestry które modyfikuje. W powyższym przykładzie, kod nie potrzebuje przechowywać ax. Chociaż PrintSpaces zmienia al., nie wpłynie to na działanie programu. Jeśli wywołujący przechowuje rejestry, nie musi zachowywać rejestrów nie troszcząc się o to: mov cx, 10 Loop0: push cx call PrintSpaces pop cx putcr loop Loop0 putcr putcr call PrintSpaces
Loop1:
PrintSpaces PSLoop:
mov mov putc push push call pop pop putc putcr loop proc mov mov putc
al., ‘*’ cx, 100 ax cx PrintSpaces cx ax Loop1
near al., ‘ ‘ cx, 40
PrintSpaces
loop ret endp
PSLoop
Przykład ten dostarcza trzech różnych przypadków. Pierwsza pętla (Loop0) zachowuje tylko rejestr cx. Modyfikacja rejestru al. Nie wpływa na działanie tego programu. Bezpośrednio po pierwszej pętli, kod ten znowu wywołuje PrintSpaces. Jednakże, ten kod nie zachowuje ax lub cx ponieważ nie troszczy się czy PrintSpaces je zmienia. Ponieważ pętla końcowa (Loop1) stosuje ax i cx, zachowuje je obie. Jednym dużym problemem z zachowywaniem rejestrów przez wywołującego, jest to ,że nasz program może się zmieniać. Możemy zmodyfikować kod wywołujący lub procedurę, żeby stosowały dodatkowy rejestr. Takie zmiany, oczywiście, mogą zmieniać zbiór rejestrów, które musimy przechować. Gorzej jeszcze, jeśli modyfikowany jest sam podprogram, będziemy musieli zlokalizować każde wywołanie programu i zweryfikować, który podprogram nie zmienia żadnego rejestru stosując kod wywołujący. Przechowywanie rejestrów nie jest tym co zachowanie otoczenia. Możemy również położyć i zdjąć zmienne i inne wartości które podprogram może zmienić. Ponieważ, 80x86 pozwala nam odłożyć i zdjąć komórki pamięci, możemy łatwo zachować również te wartości. 11.5 PARAMETRY Chociaż jest dużo klas procedur które są całkowicie zamknięte, większość procedur wymaga jakichś danych wejściowych i zwraca jakieś dane do wywoływującego. Parametry są wartościami które możemy przekazać z i do procedury. Jest wiele aspektów parametrów. Pytania związane z parametrami: *gdzie jest dana przychodząca? *jak przekazujemy i zwracamy dane? *jaka jest ilość danych do przekazania? Jest sześć głównych mechanizmów dla przekazywania danych do i z procedury, są to: *przekazywanie przez wartość *przekazywanie przez referencję *przekazywanie przez wartość/powrót *przekazywanie przez wynik, i *przekazywanie przez nazwę *przekazywanie przez leniwe wartościowanie Możemy również martwić się gdzie mamy przekazać parametry. Popularne miejsca to: *do rejestrów *do globalnych komórek pamięci *na stos *do strumienia kodu lub *do bloku parametrów odnosząc się przez wskaźnik W końcu, ilość danych ma bezpośredni związek na to gdzie i jak się je przekazuje. Poniższa sekcja zajmuje się tymi tematami. 11.5.1 PRZEKAZYWANIE PRZEZ WARTOŚĆ Parametr przekazywany przez wartość to znaczy – program wywołujący przekazuje wartość do procedury. Parametry przekazywane przez wartość są tylko wartościami wejściowymi. To znaczy, możemy przekazać je do procedury, ale procedura nie może ich zwrócić. W HLLach, takich jak Pascal, idea przekazywania parametru przez wartość tylko parametru wejściowego stanowi większy sens .Mamy daną procedurę call; CallProc(I); Jeśli przekażemy I przez wartość, CallProc nie zmieni wartości I, bez względu na to co będzie się działo z parametrem wewnątrz CallProc. Ponieważ musimy przekazać kopię danej do procedury, powinniśmy tylko stosować tą metodę dla przekazywania małych obiektów, takich jak bajty, słowa, i podwójne słowa. Przekazywanie tablic i ciągów znaków jest bardzo nieefektywne (ponieważ musimy stworzyć i przekazać kopię struktury do procedury) 11.5.2 PRZEKAZYWANIE PRZEZ REFERENCJĘ Przy przekazywaniu parametrów przez referencję, musimy przekazać adres zmiennej zamiast jej wartości. Innymi słowy, musimy przekazać wskaźnik do danej. Procedura musi odnieść się przez ten wskaźnik aby uzyskać dostęp do danej. Przekazywanie parametrów przez referencję jest użyteczne kiedy musimy zmodyfikować parametr aktualny lub kiedy przekazujemy duże struktury danych pomiędzy procedurami. Przekazywanie parametrów przez referencję może tworzyć jakieś osobliwe wyniki. Poniższa procedura pascalowska dostarcza przykład jednego problemu jaki możemy napotkać: program main (input, output) ; var m.: integer;
begin
procedure bletch (var i, j: integer); begin i := i+2; j := j-1; writeln (i, ‘ ‘, j); end; {main} m. := 5; bletch (m., m.);
end. Ta szczególna sekwencja kodu będzie drukowała „00” bez w względu na wartość m. Jest tak ponieważ parametry i i j są wskaźnikami do aktualnej danej i oba wskazują na ten sam obiekt. Dlatego też, instrukcja j := j+i; zawsze daje zero ponieważ i i j odnoszą się do tej samej zmiennej. Przekazywanie przez referencję jest zazwyczaj mniej wydajne niż przekazywanie przez wartość. Musimy osiągnąć wartość obiektu dla wszystkich parametrów przekazywanych przez referencję przy każdym dostępie; jest to wolniejsze niż proste zastosowanie wartości. Jednakże, kiedy przekazujemy duże struktury danych,, przekazywanie przez referencje jest szybsze ponieważ nie robimy kopii tej struktury przed wywołaniem procedury. 11.5.3 PRZEKAZYWANIE PRZEZ WARTOŚĆ / POWRÓT Przekazywanie przez wartość / powrót (znane również jako wartość – wynik) jest kombinacją cech przekazywania przez wartość i przekazywania przez referencję. Przekazujemy parametry przez wartość/ powrót poprzez adres, podobnie jak przy przekazywaniu parametrów przez referencję. Jednakże, na wejściu, procedura czyni czasową kopię tego parametru i stosuje kopię podczas wykonywania procedury. Kiedy procedura się kończy, kopiuje kopię czasową do oryginalnego parametru. Pascalowski kod przedstawiony w poprzedniej sekcji działałby właściwie z przekazywaniem parametrów przez wartość/ powrót. Oczywiście, kiedy Bletch wraca do kodu wywołującego, m może zwierać tylko jedną z dwóch wartości, ale kiedy Bletch jest wykonywana, i i j mogą zawierać różne wartości. W pewnych przypadkach przekazywanie przez wartość/ powrót jest bardziej wydajne niż przekazywanie przez referencję, w innych mniej wydajne. Jeśli procedura odwołuje się do parametrów parę razy, kopiowanie danych parametrów jest kosztowne. Z drugiej strony, jeśli procedura stosuje ten parametr często, procedura amortyzuje stały koszt kopiowania danych poprzez niekosztowny dostęp do lokalnej kopii. 11.5.4 PRZEKAZYWANIE PRZEZ WYNIK Przekazywanie przez wynik jest prawie identyczne do przekazywania przez wartość – powrót. Przekazujemy wskaźnik do żądanego obiektu a procedura stosuje lokalną kopię zmiennej a potem przechowuje wynik we wskaźniku kiedy wraca. Jedyna różnica pomiędzy przekazywaniem przez wartość – powrót a przekazywaniem przez wynik polega na tym, że kiedy przekazujemy parametry przez wynik nie kopiujemy danych na wejściu do procedury. Parametry przekazywanie przez wynik są wartościami zwracanymi, nie danymi przekazywanymi do procedury. Dlatego też, przekazywanie przez wynik jest odrobinę bardziej wydajne niż przekazywanie przez wartość – powrót ponieważ oszczędzamy na kopiowaniu danych do lokalnej zmiennej. 11.5.5 PRZEKAZYWANIE PRZEZ NAZWĘ Przekazywanie przez nazwę jest mechanizmem przekazywania parametrów stosowanym przez makra , przyrównywania tekstu i macro #define w języku C. Ten mechanizm przekazywania parametrów stosuje zastępowanie tekstowe parametrów. Rozważmy poniższe makro MASMa: PassByName macro Param1, param2 mov ax, param1 add ax, Param2 endm Jeśli mamy wywołanie makra w postaci: PassByName bx, I MASM wyemituje poniższy kod, zastępując bx za param1 i I za Param2: mov ax, bx add ax, I Niektóre języki wysokiego poziomu takie jak ALGOL-68 i Panacea, wspierają przekazywane parametrów przez nazwę. Jednakże, implementacja przekazywania przez nazwę stosując zastępowanie tekstowe w językach kompilowanych (jak ALGOL68) jest bardzo trudne i niewydajne. W zasadzie, musimy rekompilować funkcje za każdym razem kiedy ją wywołujemy. Wiec
języki kompilowane które wpierają przekazywanie przez nazwę generalnie stosują różne techniki dla przekazywania tych parametrów. Rozważmy poniższą procedurę Panacei: PassByName: procedure(name item; integer ; var index: integer); begin PassByName; foreach index in 0..10 do item := 0; endfor; end PassByName; Zakładając, że wywołujemy ten podprogram instrukcją PassByName(A[i], i); gdzie A jest tablicą liczb całkowitych mającą (przynajmniej) elementy A[0]..A[10].Po zastąpienia przekazywania przez nazwę parametru item dostalibyśmy poniższy kod: begin PassByName; foreach index in 0..10 do A[I] := 0 (*Zauważmy, że index i I są aliasami*) endfor; end PassByName; Kod ten zeruje elementy 0..10 tablicy A Języki wysokiego poziomu jak ALGOL-68 i Panacea kompilują przekazywanie parametrów przez nazwę do funkcji, która zwraca adres danego parametru. Pod tym względem przekazywanie parametrów przez nazwę jest podobne do przekazywania parametrów przez referencję ponieważ przekazujemy adres obiektu. Główna różnica jest taka ,że przy przekazywaniu parametrów przez referencję obliczamy adres obiektu przed wywołaniem podprogramu; przy przekazywaniu przez nazwę podprogram sam wywołuje jakąś funkcję obliczając adres parametru. Więc co różni te wykonania? Cóż, spójrzmy ponownie na powyższy kod. Mamy przekazane A[I] przez referencję zamiast przez nazwę, kod wywołujący obliczył adres A[I] tuż przed wywołaniem i przekazał tym samym ten adres. Wewnątrz procedury PassByName zmienna item odnosiła się zawsze do pojedynczego adresu, a nie do adresu ,który zmienił się wraz z I. Przy przekazywaniu przez nazwę item jest rzeczywiście funkcją, która oblicza adres parametru, do którego procedura przekazuje wartość zero. Funkcja taka może wyglądać jak poniższa: Itemthunk
proc near mov bx, I shl bx, 1 lea bx, A[bx] ret ItemThunk endp Skompilowany kod wewnątrz procedury PassByName może wyglądać czasami jak poniższy: ; item := 0; call mov
ItemThunk word ptr [bx], 0
Thunk jest historycznym terminem dal tej funkcji, która oblicza adres parametru przekazywanego przez nazwę. Nie jest nic warte, że większość HLLi wspiera przekazywanie parametrów przez nazwę nie wywołując bezpośrednio thunks’ów (jak powyższy call). Ogólnie, program wywołujący przekazuje adres thunk a podprogram wywołuje thunk pośrednio . Pozwala to tej samej sekwencji instrukcji wywoływać kilka różnych thunks’ów (odpowiadającym różnym wywołaniom podprogramu) 11.5.6 PRZEKAZYWANIE PRZEZ LENIWE WARTOŚCIOWANIE Przekazywanie przez nazwę jest podobne do przekazywania przez referencję ponieważ procedura uzyskuje dostęp do parametru stosując adres parametru. Pierwszą różnicą pomiędzy nimi dwom jest to , że program wywołujący przekazuje bezpośrednio adres na stos kiedy przekazujemy przez referencję, przekazuje adres funkcji, która oblicza adres parametru kiedy przekazujemy parametry przez nazwę. Mechanizm przekazywania parametrów przez leniwe wartościowanie dzieli te same związki z przekazywaniem parametrów przez wartość – program wywołujący przekazuje adres funkcji która oblicza wartość parametru jeśli pierwszym dostępem do tego parametru jest operacja odczytu. Przekazywanie przez leniwe wartościowanie jest użyteczną techniką przekazywania parametrów jeśli koszt obliczenia wartości parametru jest bardzo wysoki a procedura może nie używać wartości. Rozważmy poniższą nagłówkową procedurę Panacei: PassByEval1: procedure (eval a:integer; eval b:integer; eval c:integer);
Kiedy wywołujemy funkcję PassByEval nie oblicza aktualnego parametru i przekazuje swoje wartości do procedury,. Zamiast tego kompilator generuje thunksy, które będą obliczały wartość parametru przynajmniej raz. Jeśli pierwszym dostępem do parametru eval jest odczyt, thunk obliczy wartość parametru i przechowa ją w lokalnej zmiennej. Ustawi również flagi żeby wszystkie przyszłe dostępy nie wywoływały thunka (ponieważ już jest obliczona wartość parametru). Jeśli pierwszy dostęp do parametru eval to zapis, wtedy kod ustawia flagi a przyszły dostęp wewnątrz tej samej aktywowanej procedury zastosuje zapisaną wartość i zignorowanie thunka. Rozważmy powyższą procedurę PassByEval. Przypuśćmy, że zabiera kilka minut obliczenie wartości dla parametrów a ,b i c (które mogą być na przykład trzema możliwymi różnymi ścieżkami w grze Szachy). Być może procedura PassByEval zastosuje tylko wartość jednego z tych parametrów. Bez przekazanie przez leniwie wartościowanie, kod wywołujący musiałby tracić czas na obliczenie wszystkich trzech parametrów, nawet jeśli procedura stosować będzie tylko jedną wartość. Z przekazaniem przez leniwe wartościowanie jednakże, procedura spędzi czas na obliczeniu jednego potrzebnego parametru. Leniwe wartościowanie jest popularną techniką sztucznej inteligencji (AI) i systemów operacyjnych używających poprawy wydajności. 11.5.7 PRZEKAZYWANIE PARAMETRÓW DO REJESTRACH Mając już poruszony temat jak przekazywać parametry do procedury, następną rzeczą do omówienia jest to gdzie przekazywać parametry. Gdzie są przekazywane parametry zależy w dużej mierze do rozmiaru i liczby tych parametrów. Jeśli przekazujemy małą liczbę bajtów do procedury, wtedy rejestry są doskonałym miejscem do przekazania parametrów. Rejestry są idealnym miejscem do przekazania wartości parametrów do procedury. Jeśli przekazujemy pojedynczy parametr do procedury powinniśmy użyć poniższych rejestrów dla stowarzyszonych typów danych: Rozmiar Danej Przekazanie do Rejestru Bajt: al Słowo: ax Podwójne Słowo: dx: ax lub eax (jeśli 80386 lub lepszy) Taka jest zasada. Jeśli dogodniej będzie przekazać 16 bitowa wartość do rejestru si lub bx, jak najbardziej zróbmy to. Jednak większość programistów stosuje powyższe rejestry do przekazania parametrów. Jeśli przekazujemy kilka parametrów do procedury do rejestrów 80x86, powinniśmy zastosować rejestry w następującym porządku: Pierwszy Ostatni ax, dx, si, di, bx, cx Generalnie powinniśmy unikać stosowania rejestru bp. Jeśli potrzebujemy więcej niż sześć słów, być może powinniśmy przekazać nasze wartości gdzie indziej Pakiet Standardowej Biblioteki UCR dostarcza kilku dobrych przykładów procedur, które przekazują parametry przez wartość do rejestrów. Putc, która wysyła kod znaku ASCII na wyświetlacz, spodziewa się wartości ASCII w rejestrze al. Podobnie jak puti spodziewa się wartości liczby całkowitej ze znakiem w rejestrze ax. Jako inny przykład, rozważmy poniższy podprogram putsi , który wysyła wartość do al jako liczbę całkowitą ze znakiem: putsi
proc push ax ;przechowuje wartość AH cbw ;rozszerzenie znaku AL -> AX puti pop ax ;przywrócenie AH ret putsi endp Pozostałe cztery mechanizmy przekazywania parametrów (przekazywanie przez referencję, wartość/powrót, wynik i nazwę) generalnie wymagają aby przekazać wskaźnik do żądanego obiektu (lub do thunka w przypadku przekazywania przez nazwę). Kiedy przekazujemy takie parametry do rejestrów, musimy rozważyć czy przekazujemy offset czy pełny adres segmentowy. Szesnastobitowy offset może być przekazany do każdego 16 bitowego rejestru ogólnego przeznaczenia 80x86. Si, di i bx są najlepszymi miejscami do przekazania offsetu ponieważ będziemy musieli prawdopodobnie załadować go i tak do jednego z tych rejestrów. Możemy przekazać 32 bitowy adres segmentowy do dx:ax podobnie inne parametry podwójnego słowa. Jednakże możemy również przekazać je do ds.:bx, ds.:si, ds.:di, es:bx, es:si lub es:di i zastosować je bez kopiowania do rejestru segmentowego. Podprogram puts z UCR Stdlib, który drukuje ciąg znaków na wyświetlaczu jest dobrym przykładem podprogramu który stosuje przekazywanie przez referencję. Chce adres ciągu w parze rejestrów es:di. Przekazuje parametry w ten sposób nie dlatego, że modyfikuje parametry ale dlatego, że ciągi są dosyć długie i przekazywanie ich w jakiś inny sposób byłoby nieefektywne. Jako inny przykład rozważmy strfill(str,c) który kopiuje znak c (przekazywany przez wartość do al) na każdą pozycję znaku w str (przekazywaną przez referencję do es:di) aż do zerowego bajtu kończącego: ; strfill -
kopiuje wartość do al ciągu wskazywanego przez es:di
;
aż do zerowego bajtu kończącego.
byp
textequ
strfill
proc pushf cld push jmp stosb cmp jne
sfLoop: sfStart:
di sfStart ;es:[di] := al, di := di+1; byp es:[di], 0 sfLoop
pop di popf ret strfill endp Kiedy przekazujemy parametry przez wartość/powrót lub wynik do podprogramu, możemy przekazać adres do rejestru. Wewnątrz procedury skopiujemy wartość wskazywaną przez ten rejestr do zmiennej lokalnej (tylko wartość/powrót). Dopiero przed powrotem do programu wywołującego, możemy przekazać wynik końcowy z powrotem jako adres do rejestru. Poniższy kod wymaga dwóch parametrów. Pierwszy jest przekazywany przez wartość/powrót a podprogram oczekuje adresu parametru aktualnego w rejestrze bx. Drugi jest przekazywany przez wynik, którego adres jest w si. Ten podprogram zwiększa parametr przekazywany przez wartość/powrót i przechowuje poprzedni wynik w parametrze przekazywanym przez wynik: ;CopyAndInc; ; CopyAndInc
BX zawiera adres zmiennej. Podprogram ten kopiuje tą zmienną do lokacji wyszczególnionej w SI a potem zwiększa zmienną BX. Notka: AX i CX przechowują lokalne kopie tych parametrów podczas wykonywania proc push ax ;przechowanie AX push cx ;przechowanie CX mov ax, [bx] ;pobranie lokalnej kopii pierwszego parametru mov cx, ax ;przechowanie w zmiennej lokalnej drugiego parametru inc ax ;zwiększenie pierwszego parametru mov [si], cx ;przechowanie parametru przekazanego przez wart /powrót pop cx ;przywrócenie wartości CX pop ax ;przywrócenie wartości AX ret CopyAndInc endp Czyniąc wywołanie CopyAndInc (I,J) zastosujemy kod podobny do tego: lea bx, I lea si, J call CopyAndInc Jest to oczywiście przykład banalny, którego implementacja jest bardzo niewydajna. Niemniej jednak, pokazuje jak są przekazywane parametry przez wartość/powrót i wynik w rejestrach 80x86. Jeśli chętnie wymienimy trochę przestrzeni za szybkość, jest inny sposób osiągnięcia tego samego rezultatu jak przekazanie przez wartość/powrót lub wynik kiedy przekazujemy parametry do rejestrów. Rozważmy poniższą implementację CopyAndInc: CopyAndInc
proc mov cx, ax ;robi kopię pierwszego parametru, inc ax ;potem zwiększa go o jeden. ret CopyAndInc endp Czyniąc wywołanie CopyAndInc(I, J),jak poprzednio, zastosujemy poniższy kod 80x86: mov ax, I call CopyAndInc mov I, ax mov J, cx Zauważmy, że kod ten nie przekazuje wcale żadnego adresu; a mimo to ma taką samą semantykę (to znaczy wykonuje te same operacje) jak wersja poprzednia. Obie wersje zwiększają I i przechowują pre-inkrementowaną wersję w J. Wyraźnie ostatnia
wersja jest szybsza, chociaż nasz program będzie odrobinę większy jeśli będzie wiele wywołań CopyAndInc w programie (sześć lub więcej). Możemy przekazać parametry przez nazwę lub leniwe wartościowanie do rejestru poprzez proste załadowanie tego rejestru adresem thunka wywołującego. Rozważmy procedurę Panacei PassByName. Jedna z implementacji tej procedury może wyglądać następująco
Możemy wywołać ten podprogram kodem, który wygląda następująco: lea si, I lea dx, Thunk_A call PassByName Thunk_A proc mov bx, I shl bx, 1 lea bx, A[bx] ret Thunk_A endp Korzyścią z tego schematu jest to,że możemy wywoływać różne thunksy , nie tylko podprogram pojawiający się we wcześniejszym przykładzie
ItemThunk
11.5.8 PRZEKAZYWANIE PARAMETRÓW DO ZMIENNYCH GLOBALNYCH. Kiedy już skończyliśmy z rejestrami, inną jedyną (rozsądną) alternatywą jaką mamy jest pamięć główna. Jednym z łatwiejszych miejsc do przekazania parametrów jest zmienna globalna w segmencie danych. Poniższy kod dostarcza przykładu: mov ax, xxxx ;przekazanie tego parametru przez wartość mov Value1Proc1, ax mov ax, offset yyyy ;przekazanie tego parametru przez referencję mov word ptr Ref1Proc1, ax mov ax, seg yyyy mov word ptr Ref1Proc1+2, ax call ThisProc -
Rysunek 11.5 Ułożenie na stosie CallProc dla bliskiej procedury proc near push es push ax push bx les bx, Ref1Proc1 ;pobranie adresu parametru referencyjnego mov ax, Value1Proc1 ;pobranie parametru przez wartość mov es:[bx], ax ;przechowanie w lokacji wskazywanej przez parametr referencyjny pop bx pop ax pop es ret ThisProc endp Przekazywanie parametrów przez globalne lokacje jest mało elegancji i nie wydolne. Co więcej, jeśli stosujemy zmienne globalne w ten sposób do przekazywania parametrów, podprogramy, które piszemy nie mogą stosować rekurencji. Na szczęście jest lepszy schemat przekazywania parametrów dla przekazywania danych w pamięci wiec nie musimy poważnie rozważać tego schematu. ThisProc
11.5.9 PRZEKAZYWANIE PARAMETRÓW NA STOS Wiele HLLi używa stosu do przekazywania parametrów ponieważ metoda ta jest dosyć wydajna. Przekazując parametry na stos, odkładamy je bezpośrednio przed wywołaniem podprogramu. Wtedy podprogram odczytuje je ze stosu pamięci i działa na nich odpowiednio. Rozważmy poniższą pascalowską procedurę call: CallProc(i, j,k+4); Większość kompilatorów Pascala odkłada swoje parametry na stos, w kolejności w jakiej się pojawiają na liście parametrów. Dlatego też, kod 80x86 typowo emituje dla tego podprogramu wywołania (zakładając, ze mamy przekazywanie parametrów przez wartość): push i push j mov ax, k add ax, 4 push ax call CallProc Na wejściu do CallProc, stos 80x86 wygląda jak ten pokazany na rysunku 11.5 (dla bliskiej procedury) lub Rysunku 11.6 (dla dalekiej procedury). Możemy zyskać na dostępie do parametrów przekazywanych na stos przez usunięcie danych ze stosu (Zakładając wywołanie bliskiej procedury):
Rysunek 11.6 : Położenie CallProc na stosie dla Dalekiej procedury
Rysunek 11.7 : Dostęp do parametrów na stosie CallProc
CallProc
proc pop pop pop pop push ret end
near RtnAdrs kParam jparam iParam RtnAdrs
Jednakże jest lepszy sposób. Architektura 80x86 pozwala nam na stosowanie rejestru bp (wskaźnik bazowy) przy dostępie do parametrów przekazywanych na stos. Jest to jeden z powodów dla którego tryby adresowania disp[bp], [bp][di], [bp][si], disp[bp][si] i disp[bp][di] używają segment stosu zamiast segmentu danych. Poniższy fragment kodu daje nam kod standardowego wejścia i wyjścia procedury: StdProc
proc near push bp mov bp, sp pop bp ret ParamSize StdProc endp ParamSize jest liczbą bajtów parametrów odkładanych na stos przed wywołaniem procedury. W procedurze CallProc było sześć bajtów parametrów odkładanych na stos więc ParamSize będzie maił sześć. Spójrzmy na stos bezpośrednio po wykonaniu mov bp, sp w StdProc. Zakładając, że odłożyliśmy trzy parametry słowa na stos, powinien on wyglądać jak na rysunku 11.7
Rysunek 11.8 Dostęp do parametrów na stosie w dalekiej procedurze Teraz parametry mogą być pobierane poprzez indeksowanie rejestru bp: mov ax, 8[bp] ;dostęp do pierwszego parametru mov ax, 6[bp] ;dostęp do drugiego parametru mov ax, 4[bp] ;dostęp do trzeciego parametru Kiedy wracamy do kodu wywołującego, procedura musi usunąć te parametry ze stosu. Dla osiągnięcia tego zdejmujemy starą wartość bp ze stosu i wykonujemy instrukcję ret6. To zdejmuje adres powrotny ze stosu i dodaje sześć do wskaźnika stosu, skutecznie usuwając parametry ze stosu. Przesunięcia dane powyżej są tylko dla bliskiej procedury. Kiedy wywołujemy daleką procedurę. *0[BP] będzie wskazywał na starą wartość BP *2[BP] będzie wskazywał na przesunięcie części adresu powrotu *4[BP] będzie wskazywał na segment części adresu powrotu a *6[BP] będzie wskazywał ostatni parametr na stosie Zawartość stosu kiedy wywołujemy daleką procedurę jest pokazana na rysunku 11.8 Zbiór parametrów, adres powrotu, rejestry zachowane na stosie i inne pozycje to tzw. Ramka stosu lub rekord aktywacji. Kiedy zachowujemy inne rejestry na stosie, zawsze upewniajmy się, że zachowaliśmy i ustawili bp przed odłożeniem innych rejestrów na stos. Jeśli odłożymy inne rejestry na stos przed ustawieniem bp, offset ramki stosu zmieni się. Na przykład, poniższy kod zakłóca porządek prezentowany powyżej: FunnyProc
proc near push ax push bx push bp mov bp, sp pop bp pop bx pop ax ret FunnyProc endp Ponieważ kod ten odkłada ax i bx przed odłożeniem bp i skopiowaniem sp do bp, ax i bx pojawią się w rekordzie aktywacji przed adresem powrotu (który zastartowałby normalnie z pod lokacji ]bp+2]). Jako wynik, wartość bp pojawi się w lokacji [bp+2] a wartość ax pojawi się w lokacji [bp+4]. Takie odłożenie adresu powrotu i innych parametrów na stosie jest pokazane na rysunku 11.9
Rysunek 11.9: Popsucie offsetu przez odłożenie na stos innych rejestrów przed BP
Rysunek 11.10: Zachowanie stałego offsetu przez odłożenie na stos pierwszego BP Chociaż jest to bliska procedura, parametry nie zaczynają się od offsetu osiem w rekordzie aktywacji. Po odłożeniu rejestrów ax i bx po ustawieniu bp, offset parametrów będzie wynosił cztery (zobacz rysunek 11.10) FunnyProc
FunnyProc
proc push mov push push pop pop pop ret endp
near bp bp, sp ax bx
bx ax bp
Dlatego też, instrukcje push bp i mov bp, sp powinny być pierwszymi instrukcjami każdego wykonywanego podprogramu, kiedy ma swoje parametry na stosie. Dostęp do parametrów stosując wyrażenie takie jak [bp+6] może uczynić nasz program bardzo trudnym do czytania i pielęgnacji. Jeśli chcielibyśmy użyć sensownych nazw, jest kilka sposobów zrobienia tego. Jeden sposób do odniesienia się do parametrów przez nazwę jest zastosowanie przyrównań. Rozważmy poniższą Pascalową procedurę i jej odpowiednik w kodzie języka asemblera 80x86: procedure xyz (var i: integer; j:integer); begin
i :=j + k; end; Sekwencja wywołania: xyz(a, 3, 4); Kod języka asemblera: xyz_i xyz_j xyz_k xyz
xyz
equ equ equ proc push mov push push push les mov add mov pop pop pop pop ret endp
8[bp] 6[bp] 4[bp] near bp bp, sp es ax bx bx, xyz_i ax, xyz_j ax, xyz_k es:[bx], ax bx ax es bp 8
;Stosujemy przyrównanie więc możemy odnieść się do nazwy ;symbolicznej w treści procedury
;pobranie adresu I do ES:BX ;pobranie parametru J ;dodanie parametru K ;przechowanie wyniku w parametrze I
Sekwencja wywołania: mov ax, seg a ;parametr ten jest przekazany przez referencję, więc przekazujemy push ax ; jego adres na stos mov ax, offset a push ax mov ax, 3 ;to jest drugi parametr push ax mov ax, 4 ;to jest trzeci parametr push ax call xyz Na procesorach 80186 i późniejszych możemy zastosować poniższy kod w miejsce powyższego: push seg a push offset a push 3 push 4 call xyz Na wejściu do procedury xyz, przed wykonaniem instrukcji les, stos wygląda jak pokazano na rysunku 11.11 Ponieważ przekazujemy I przez referencję, musimy odłożyć jego adres na stos. Ten kod przekazuje parametr referencyjny stosując 32 bitowy adres segmentowy. Zauważmy, że kod ten używa ret 8. Chociaż są trzy parametry na stosie, referencyjny parametr I używa czterech bajtów ponieważ jest to daleki adres. Dlatego też jest osiem bajtów parametrów na stosie wymaganych przez instrukcję ret8 Gdybyśmy przekazali I przez referencję stosując bliski wskaźnik zamiast dalekiego wskaźnika, kod mógłby wyglądać tak: xyz_i equ 8[bp] ;stosujemy przyrównanie więc możemy odnieść się do nazwy xyz_j equ 6[bp] ;symbolicznej w treści procedury xyz_k equ 4[bp] xyz proc near push bp mov bp, sp push ax push bx mov bx, xyz_i ;pobranie adresu I do BX
xyz
mov add mov pop pop pop ret endp
Rysunek 11.11: Stos na wejściu do procedury XYZ ax, xyz_j ;pobranie parametru J ax, xyz_k ;dodanie parametru K [bx], ax ;przechowanie wyniku w parametrze I bx ax bp 6
Zauważmy, że ponieważ adres I na stosie jest tylko dwu bajtowy (zamiast cztero), ten podprogram zdejmuje tylko sześć bajtów kiedy wraca. Sekwencja wywołania: mov ax, offset a ;przekazanie bliskiego adresu a push ax mov ax, 3 ;to jest drugi parametr push ax mov ax, 4 ;to jest trzeci parametr push ax call xyz Na procesorze 80286 i późniejszych możemy zastosować poniższy kod w miejsce powyższego: push offset a ;przekazanie bliskiego adresu a push 3 ;przekazanie drugiego parametru przez wartość push 4 ;przekazanie trzeciego parametru przez wartość call xyz ramkę stosu powyższego kodu pokazano na Rysunku 11.12 Kiedy przekazujemy parametry przez wartość/powrót lub wynik ,przekazujemy adres do procedury, dokładnie jak przekazując parametry przez referencję. Jedyna różnica jest taka, że stosujemy lokalną kopię zmiennej wewnątrz procedury zamiast pośredniego dostępu do zmiennej przez wskaźnik. Poniższa implementacja dla xyz pokazuje jak przekazać I przez wartość-powrót i przez wynik: ; wersja xyz stosująca przekazanie przez wartość –powrót dla xyz_i xyz_i equ 8[bp] ;stosujemy przyrównanie więc możemy odnieść się do nazw xyz_j equ 6[bp] ;symbolicznych w treści procedury xyz_k equ 4[bp] xyz
proc push mov push push
near bp bp, sp ax bx
Rysunek 11.12 :Przekazanie parametrów przez referencję stosując bliski wskaźnik zamiast wskaźnika dalekiego push cx ;Tu lokalna kopia mov bx, xyz_i ;pobranie adresu I do BX mov cx, [bx] ;pobranie lokalnej kopii parametru I mov ax, xyz_j ;pobranie parametru J add ax, xyz_k ;dodanie parametru K mov cx, ax ;przechowanie wyniku w lokalnej kopii mov bx, xyz_i ;pobranie wskaźnika do I, znowu mov [bx], cx ;przechowanie wyniku pop cx pop bx pop ax pop bp ret 6 xyz endp Jest parę niekoniecznych instrukcji mov w tym kodzie. Są one obecne tylko dla dokładnej implementacji przekazywania parametrów przez wartość-powrót. Łatwo jest poprawić ten kod stosując przekazanie parametrów przez wynik. Kod zmodyfikowany: ;wersja xyz stosująca przekazywanie przez wynik dla xyz_i xyz_i xyz_j xyz_k
equ equ equ
8[bp] 6[bp] 4[bp]
xyz
proc push mov push push push mov add mov mov mov pop pop pop pop ret endp
near bp bp, sp ax bx cx ax, xyz_i ax, xyz_k cx, ax bx, xyz_i [bx], cx cx bx ax bp 6
xyz
;stosujemy przyrównanie więc możemy odnieść się do nazwy ;symbolicznej w treści procedury
;zachowanie lokalnej kopii ;pobranie parametru J ;dodanie parametru K ;przechowanie wyniku w kopii lokalnej ;pobranie wskaźnika do I, znowu ;przechowanie wyniku
Z przekazywaniem parametrów do rejestrów przez wartość-powrót i wynik, możemy poprawić wydajność tego kodu stosując zmodyfikowaną postać przekazania przez wartość. Rozważmy poniższą implementację xyz: ;wersja xyz stosująca zmodyfikowane przekazywanie przez wartość-powrót dla xyz_i xyz_i xyz_j xyz_k
equ equ equ
8[bp] 6[bp] 4[bp]
;stosujemy przyrównanie więc możemy odnieść się ;do nazwy symbolicznej w treści procedury
xyz
proc near push bp mov bp, sp push ax mov ax, xyz_j ;pobrane parametru J add ax, xyz_k ;dodanie parametru K mov xyz_i, ax ;przechowanie wyniku w lokalnej kopii pop ax pop bp ret 4 ;zauważmy, że nie zdejmujemy parametru I xyz endp Sekwencja wywołania dla tego kodu; push a ;przekazanie a jako wartości do xyz push 3 ;przekazanie drugiego parametru przez wartość push 4 ;przekazanie trzeciego parametru przez wartość call xyz pop a Zauważmy, że wersja z przekazaniem przez wynik nie byłaby praktyczna ponieważ musimy odłożyć coś na stos aby uczynić miejsce dla lokalnej kopii I wewnątrz xyz. Możemy również odłożyć wartość a na wejściu pomimo, że procedura xyz ignoruje ją. Procedura ta zdejmuje tylko cztery bajty ze stosu na wyjściu. Pozostawia wartość parametru I na stosie, żeby kod wywołujący mógł przechować ją dalej dla właściwego miejsca przeznaczenia. Przekazując parametr przez nazwę na stos, po prostu odkładamy adres thunka. Rozważmy poniższy pseudo – pascalowy kod: Procedure swap (name Item1, Item2: integer); var temp: integer; begin temp := Item1; Item1 := Item2; Item2 := temp; End; Jeśli swap jest bliską procedurą, kod 80x86 dal tej procedury może wyglądać jak poniższy (zauważmy, że ten kod został zoptymalizowany odrobinę i nie następuje dokładna sekwencja jak powyżej): ;swap;
zamienia dwa parametry przekazywane przez nazwę na stos Item1 jest przekazywany pod adres [bp+6], Item2 jest przekazywany pod adres [bp+4]
wp swap_Item1 swap_Item2
textequ equ [bp+6] equ [bp+4]
swap
proc push mov push push call mov call xchg call mov
near bp bp, sp ax bx wp swap_Item1 ax, [bx] wp swap_Item2 ax, [bx] wp swap_Item1 [bx], ax
;zachowanie wartości temp ;zachowanie bx ;pobranie adresu Item1 ;zachowanie w temp (AX) ;pobranie adresu Item2 ;zamiana temp <-> Item1 ;pobranie adresu Item11 ;zachowanie temp w Item1
pop pop ret endp
swap
bx ax 4
;przywrócenie bx ;przywrócenie ax
Kilka przykładów wywołań swap: ;swap (A[i], i) -- wersja 8086 lea ax, thunk1 push ax lea ax, thunk2 push ax call swap ;swap (A[i],i) -- wersja 80186 i wersje późniejsze push offset thunk1 push offset thunk2 call swap ;Notka: kod ten zakłada, że A jest dwubajtową tablicą liczb całkowitych thunk1
thunk1
proc mov shl lea ret endp
near bx, 1 bx, 1 bx, A[bx]
thunk2
proc near lea bx, 1 ret thunk2 endp Powyższy kod zakłada, że thunksy są bliskimi procedurami które tkwią w tym samym segmencie co podprogram swap. Jeśli thunksy są dalekimi procedurami, program wywołujący musi przekazać daleki adres na stos a podprogram swap musi manipulować dalekim adresem. Demonstruje to poniższa implementacja swap, thunk1 i thunk2 ;swap; swap_Item1 swap_Item2 dp swap
zamienia dwa parametry przekazane przez nazwę na stos. Item1 jest przekazany pod adres [bp+10], Item2 przekazany pod adres [bp+6] equ equ textequ proc push mov push push push call mov call xchg call mov pop pop pop ret
[bp+10] [bp+6] far bp bp, sp ax bx es dp swap_Item1 ax, es:[bx] dp swap_Item2 ax, es:[bx] dp swap_Item1 es:[bx], ax es bx ax 8
;zachowanie wartości temp ;zachowanie bx ;zachowanie es ;pobranie adresu Item1 ;zachowanie w temp (AX) ;pobranie adresu Item2 ;zamiana temp <-> Item2 ;pobranie adresu Item1 ;zachowanie temp w Item1 ;przywrócenie es ;przywrócenie bx ;przywrócenie ax ;powrót i zdjęcie Item1 i Item2
swap
endp
Kilka przykładów wywołania swap: ;swap(A[i], i) -- wersja 8086 mov push lea push mov push lea push call
ax, seg thunk1 ax ax, thunk1 ax seg, thunk2 ax ax, thunk2 ax swap
;swap(A[i], i) –wersja 80186 i wersje późniejsze push seg thunk1 push offset thunk1 push seg thunk2 push offset thunk2 call swap ;Notka: kod ten zakłada, że A jest tablicą dwubajtową liczb całkowitych. Zauważmy również, że ; jaki segment(y) zawierają A i I thunk1
proc far mov bx, seg A ,musimy zwrócić seg A w ES push bx ;zachowujemy na później mov bx, seg i ;potrzebujemy segment I, żeby mieć do niego dostęp mov es, bx mov bx, es:i ;pobranie wartości I shl bx, 1 lea bx, A[bx] pop es ;zwrócenie segmentu A[I] w es ret thunk1 endp thunk2 proc near mov bx, seg i ;potrzebujemy zwrócić segment I w es mov es, bx lea bx, i ret thunk2 endp Dodatkowe informacje o rekordzie aktywacji i ramce stosu pojawią się później w tym rozdziale w sekcji o zmiennych lokalnych 11.5.10 PRZEKAZYWANIE PARAMETRÓW W STRUMIENIU KODU innym miejscem gdzie możemy przekazać parametry jest strumień kodu bezpośrednio po instrukcji call. Podprogram print w pakiecie Standardowej Biblioteki UCR dostarcza doskonałego przykładu: print byte „Ten parametr jest w strumieniu kodu.:,0 Normalnie podprogram zwraca sterowanie do pierwszej instrukcji bezpośrednio następującej po instrukcji call. Ale co wydarzyłoby się tu gdyby 80x86 próbował zinterpretować kod ASCII dla „To.....” jako instrukcję. Dało by to nam niewłaściwy wynik. Na szczęście, możemy przeskoczyć ten ciąg kiedy wracamy z podprogramu Więc jak korzystamy z dostępu do tych parametrów? Łatwo. Adres powrotu na stosie wskazuje na nie. Rozważmy poniższą implementację print: MyPrint
proc push
near bp
mov bp, sp push bx push ax mov bx, 2[bp] ,Ładowanie adresu powrotu do BX PrintLp; mov al, cs:[bx] ;pobranie nowego znaku cmp al, 0 ;sprawdzenie czy koniec ciągu jz EndStr putc ;jeśli nie koniec, drukujemy ten znak inc bx ;przejście do nowego znaku jmp PrintLp EndStr; inc bx ;wskazuje pierwszy bajt poza zerem mov 2[bp], bx ;zachowanie jako nowego adresu powrotu pop ax pop bx pop bp ret MyPrint endp Procedura ta zaczyna się od odłożenia wszystkich zmienianych rejestrów na stos. Pobierany jest adres powrotu pod offset 2[BP] i drukowany każdy kolejny znak aż do napotkania bajtu zerowego. Odnotujmy obecność przedrostka przesłonięcia segmentu cs: w instrukcji mov al, cs:[bx]. Ponieważ dane pochodzą z segmentu kodu, prefiks t gwarantuje, że MyPrint pobierze dany znak z właściwego segmentu.. Po napotkaniu bajtu zerowego, MyPrint wskaże bx jako pierwszy bajt po zerze. Jest to adres pierwszej instrukcji występującej po zerowym bajcie kończącym. CPU stosuje tą wartość jako nowy adres powrotu. Teraz instrukcja ret zwróci sterowanie do instrukcji następnej po ciągu. Powyższy kod pracuje dobrze jeśli MyPrint jest bliską procedurą. Jeśli musimy wywołać MyPrint z różnych segmentów, musimy stworzyć daleką procedurę. Oczywiście, główna różnica jest taka, że daleki adres powrotu w tym momencie będzie na stosie – będziemy musieli zastosować daleki wskaźnik zamiast wskaźnika bliskiego. Poniższa implementacja MyPrint pokazuje ten przypadek. MyPrint
proc far push bp mov bp, sp push bx ;zachowanie ES, AX i BX push ax push es les bx,2[bp] ;załadownie adresu powrotu do ES:BX mov al, es:[bx] ;pobranie nowego znaku cmp al, 0 ;sprawdzenie czy koniec ciągu jz EndStr putc ;jeśli nie koniec, drukuj ten znak inc bx ;przejście do następnego znaku jmp PrintLp EndStr: inc bx ;wskazuje na pierwszy bajt po zerze mov 2[bp], bx ;zachowanie jako nowy adres powrotu pop es pop ax pop bx pop bp ret MyPrint endp Zauważmy, że ten kod nie przechowuje es w lokacji [bp+4]. Powód jest całkiem prosty – es nie zmienia się podczas wykonywania tej procedury; przechowanie es w lokacji [bp+ 4] nie zmienia wartości pod tą lokacją. Zauważmy, że ta wersja MyPrint pobiera każdy znak z lokacji es:[bx] zamiast cx:[bx]. Jest tak ponieważ ciąg jaki drukujemy jest w części wywołującej, a to może nie być ta sama część zawierająca MyPrint. Poza pokazaniem jak przekazać parametry w strumieniu kodu, podprogram MyPrint również wykazuje inne pojęcie: parametry o zmiennej długości. Ciąg następny po call może być każdej praktycznie długości . Zerowy bajt kończący oznacza koniec listy parametrów. Są dwa łatwe sposoby operowania parametrami o zmiennej długości. Albo zastosować jaką specjalną wartość kończącą (jak zero) lub możemy przekazać specjalną wartość długości która powie podprogramowi ile parametrów jest przekazywanych. Obie metody mają swoje zalety i wady. Stosowanie wartości specjalnej dla zakończenia listy parametrów wymaga, żebyśmy wybrali wartość, która nigdy nie pojawia się na liście. Na przykład, MyPrint używa zera jako wartości kończącej, więc nie może wydrukować znaku NULL (którego kodem ASCII jest zero). Czasami to nie jest ograniczeniem.
Wyszczególnienie specjalnej długości parametru jest innym mechanizmem, który możemy użyć do przekazania listy parametrów o zmiennej długości .Chociaż nie jest wymagane żaden specjalny kod lub ogranicznik zakresu możliwych wartości, które mogą być przekazane do podprogramu, ustawienie długości parametrów i pielęgnacja kodu wynikowego może być prawdziwym koszmarem. Chociaż przekazanie parametrów w strumieniu kodu jest idealnym sposobem do przekazania listy parametrów o zmiennej długości, możemy przekazać również listę parametrów o stałej długości. Strumień kodu jest doskonałym miejscem do przekazania stałych (podobnie jak ciąg stałych przekazanych do MyPrint) i parametrów referencyjnych, Rozważmy poniższy kod, który oczekuje trzech parametrów przez referencję: Sekwencja wywołania: call AddEm word I, J, K Procedura: AddEm proc near push bp mov bp, sp push si push bx push ax mov si, [bp+2] ,Pobranie adresu powrotu mov bx, cs:[si+2] ;pobranie adresu J mov ax, [bx] ;pobranie wartości J mov bx, cs:[si+4] ;pobranie adresu K add ax, [bx] ;dodanie wartości K mov bx, cs:[si] ;pobranie adresu I mov [bx], ax ;przechowanie wyniku add si, 6 ;skok poza parametry mov [bp+2], si ;zachowanie adresu powrotu pop ax pop bx pop si pop bp ret AddEm endp Podprogram ten dodaje J i K razem a wynik przechowuje w I. Zauważmy, że kod ten stosuje 16 bitowy bliski wskaźnik dla przekazania adresów I, J i K do AddEm. Dlatego też I, J i K muszą być w bieżącym segmencie danych. W powyższym przykładzie, AddEm jest bliską procedurą. Aby była daleką procedurą musielibyśmy pobrać czterobajtowy wskaźnika ze stosu zamiast dwubajtowego wskaźnika. Poniżej jest daleka wersja AddEm
W obu wersjach AddEm, wskaźniki do I, J i K przekazane w strumieniu kodu są bliskimi wskaźnikami. Obie wersje zakładają, że I, J i K wszystkie są w bieżącym segmencie danych. Jest możliwe przekazanie dalekich wskaźników do tych zmiennych, lub nawet bliskich wskaźników do tych dalekich wskaźników do innych, w strumieniu kodu. Poniższy przykład nie jest całkiem taki ambitny, to jest bliska procedura, która oczekuje dalekiego wskaźnika, ale nie pokazuje jakichś znaczących różnic Sekwencja wywołania: call AddEm dword I, J, K Kod:
Zauważmy, że jest 12 bajtów parametrów w strumieniu kodu tym razem. Jest tak dlatego, że kod ten zawiera instrukcję add si, 12 zamiast add si, 6 pojawiającą się w poprzednich wersjach. W przykładach podanych do tego momentu, MyPrint oczekiwał przekazania parametrów przez wartość, drukował aktualne znaki następujące po call a AddEm oczekuje trzech parametrów przekazanych przez referencję – ich adresy występują w strumieniu kodu. Oczywiście, możemy również przekazać parametry przez wartość – powrót, przez wynik, przez nazwę lub leniwe wartościowanie w strumieniu kodu. Następny przykład jest modyfikacją AddEm, który stosuje przekazywanie przez wynik dla I, przekazywanie przez wartość-powrót dla J i przekazywanie przez nazwę dla K. Wersja ta jest odrobinę różna ponieważ modyfikuje zarówno J jak I, żeby uzasadnić stosowanie parametru wartość-powrót.
Przykład sekwencji wywołania:
Notka: przekazaliśmy I przez referencję zamiast przez wynik , w tym przykładzie wywołując AddEm(I, J, A[i]) stworzymy różne wyniki. Czy można wytłumaczyć dlaczego? Przekazanie parametrów w strumieniu kodu pozwala nam wykonać naprawdę zmyślne zadania. Poniższy przykład jest znacznie bardziej złożony niż inne w tej sekcji, ale demonstruje
Rysunek 11.13: Stos na wejściu do procedury ForStmt
Rysunek 11.14 Stos przed opuszczeniem procedury ForStmt potęgę przekazywania parametrów w strumieniu kodu i ,mimo złożoności tego przykładu, jak można uprościć nasze zadania programistyczne. Poniższe dwa podprogramy implementują instrukcje for /next, podobnie jak w BASICu, w języku asemblera. Sekwencja wywołania dla tych podprogramów jest następująca: call ForStmt word , , call Next Kod ten ustawia zmienną sterującą pętli (której bliski adres przekazujemy jako pierwszy parametr, przez referencję) na wartość początkową (przekazywaną przez wartość jako drugi parametr). Wtedy zaczyna się wykonywanie treści pętli. Po wykonaniu wywołania Next, program ten zwiększy zmienną sterującą pętli i potem porówna ją z wartością końcową. Jeśli jest mniejsza lub równa wartości końcowej , sterowanie zwracane jest na początek treści pętli (pierwsza instrukcja po dyrektywie word). W innym przypadku jest kontynuowane wykonywanie pierwszej instrukcji po wywołaniu Next. Teraz prawdopodobnie zastanawiamy się „Jak przekazać sterowanie do początku treści pętli?” W końcu nie ma etykiety przy instrukcji i nie ma instrukcji przekazania sterowania, która skacze do pierwszej instrukcji po dyrektywie word. Cóż, okazuje się, ze możemy zrobić to trochę skomplikowaną manipulacją stosu. Rozważmy jak będzie wyglądał stos na wejściu do podprogramu ForStmt, po odłożeniu bp na stos (zobacz rysunek 11.13) Normalnie, podprogram ForStmt zdjąłby bp i zwrócił instrukcją ret, która usunęła rekord aktywacji ForStmt ze stosu. Przypuśćmy, że zamiast tego ForStmt wykonuje poniższe instrukcje: add word ptr 2[bp], 2 ;przeskok parametrów push [bp+2] ;zrobienie kopii adresu powrotu mov bp, [bp] ;przywrócenie wartości bp ret ;powrót do programu wywołującego Właśnie przed powyższą instrukcją ret, stos ma wejścia pokazane na rysunku 11.14
Rysunek 11.15: Stos na wejściu do procedury Next Po wykonaniu instrukcji ret, Forstmt będzie zwracał właściwy adres powrotu ale pozostawi swój rekord aktywacji n stosie! Po wykonaniu instrukcji w treści pętli, program wywoła podprogram Next. Na początkowym wejściu do Next (i ustawieniu bp), stos zawiera wejścia pojawiające się na rysunku 11.15 Ważną rzeczą do ujrzenia tutaj jest to, że adres powrotu ForStmt, który wskazuje na pierwszą instrukcję po dyrektywie word, jest jeszcze na stosie i dostępny Next pod offsetem [bp+6]. Next może użyć tego adresu powrotu do wzmocnienia dostępu do parametrów i powrotu do właściwego miejsca jeśli to konieczne. Next zwiększa zmienną sterującą pętli i porównuje ją z wartością końcową. Jeśli wartość zmiennej sterującej pętli jest mniejsza lub równa wartości końcowej, Next zdejmuje swój adres powrotu i wraca przez adres powrotu ForStmt. Jeśli wartość zmiennej sterującej pętli jest większa niż wartość końcowa, Next wraca przez swój własny adres powrotu i usuwa rekord aktywacji ForStmt ze stosu. Poniżej mamy kod dla Next i ForStmt:
Kod przykładowy w głównym programie pokazuje, że pętle for zagnieżdżają się dokładnie jak oczekiwalibyśmy w językach wysokiego poziomu, takich jak BASIC, Pascal lub C. Oczywiście ,nie jest to szczególnie dobry sposób konstruowania pętli for w języku asemblera. Jest wiele razy wolniejszy niż zastosowanie standardowej techniki generowania pętli. Oczywiście, jeśli nie martwimy się o szybkość, jest to doskonały sposób do implementacji pętli. Jest to z pewnością łatwiejsze do odczytu i zrozumienia niż tradycyjne metody tworzenia pętli loop. Dla innej (bardziej wydajnej) implementacji pętli loop, sprawdźmy makro ForLp w Rozdziale Ósmym . Strumień kodu jest bardzo dogodnym miejscem do przekazywania parametrów. Standardowa Biblioteka UCR czyni istotnym użycie tego mechanizmu przekazywania parametrów czyniąc go łatwym dla wywoływania pewnych podprogramów. Printf jest, być może, najbardziej złożonym przykładem, ale inne przykłady ( zwłaszcza w bibliotece string) też obfitują. Pomimo wygody, są wady przekazywania parametrów w strumieniu kodu. Po pierwsze, zapomnimy dostarczyć stosowną liczbę parametrów wymaganych przez procedurę, podprogram może się pogubić. Rozpatrzmy podprogram print z Biblioteki Standardowej UCR. Drukuje łańcuch znaków aż do zerowego bajtu zakończenia a potem zwraca sterowanie do pierwszej instrukcji po zerowym bajcie zakończenia. Jeśli opuścimy zerowy bajt zakończenia, podprogram print wesoło wydrukuje następne bajty opcodów jako znaki ASCII dopóki nie znajdzie bajtu zero. Ponieważ bajty zero często pojawiają się w środku instrukcji, podprogram print może zwrócić sterowanie w trakcie jakiejś innej instrukcji. To prawdopodobnie zrobi krach sprzętu. Wprowadzenie dodatkowego zera, które może wystąpić dużo częściej niż można sobie pomyśleć, jest innym problemem jaki programiści mają z podprogramem print. W takim przypadku, podprogram print będzie zwracał po napotkaniu pierwszego bajtu zero i próbował wykonać następny znak ASCII jako kod maszynowy. I ponownie mamy krach sprzętu. Innym problemem z przekazywaniem parametrów w strumieniu kodu jest taki, że mamy trochę dłuższy dostęp do takich parametrów. Przekazanie parametrów w rejestrach, w zmiennych globalnych lub na stos jest odrobinę bardziej wydajne, zwłaszcza w krótkich podprogramach. Niemniej jednak, uzyskiwanie dostępu do parametrów w strumieniu kodu nie jest nadzwyczaj wolne, więc wygoda takich parametrów może przeważyć nad kosztami. Co więcej, wiele podprogramów (print jest dobrym przykładem) jest tak wolnych, że kilka dodatkowych mikrosekund nie robi już różnicy 11.5.11 PRZEKAZYWANIE PARAMETRÓW PRZEZ BLOK PARAMETRÓW
Innym sposobem przekazywania parametrów w pamięci jest przekazywanie przez blok parametrów. Blok parametrów jest to zbiór przyległych komórek pamięci zawierających parametry. Aby uzyskać dostęp do takich parametrów, przekazujemy podprogramowi wskaźnik do bloku parametrów. Rozważmy podprogram z poprzedniej sekcji, który oddawał razem J i K, przechowując wynik w I; kod który przekazuje te parametry przez blok parametrów może być następujący: Sekwencja wywołania: ParamBlock I J K
AddEm
AddEm
dword word word word les call proc push mov add mov pop ret endp
I ? ? ?
; I, J i K muszą pojawić się w tym porządku
bx, ParamBlock AddEm
near ax ax, es:2[bx] ax, es:4[bx] es:[bx], ax ax
;pobranie wartości J ;dodanie wartości K ;przechowanie wyniku w I
Zauważmy, że musimy zaalokować trzy parametry w sąsiednich komórkach pamięci. Ta postać przekazywania parametrów pracuje dobrze kiedy przekazujemy kilka parametrów przez referencję, ponieważ możemy zainicjalizować wskaźniki do parametrów bezpośrednio wewnątrz asemblera. Na przykład przypuśćmy, że chcielibyśmy stworzyć podprogram rotate do którego przekazujemy cztery parametry przez referencję. Podprogram ten kopiowałby drugi parametr do pierwszego, trzeci do drugiego, czwarty do trzeciego a pierwszy do czwartego. Łatwy sposób wykonania tego w asemblerze to:
Przy wywołaniu tego podprogramu przekazujemy mu wskaźnik do grupy czterech dalekich wskaźników w rejestrze bx. Na przykład przypuśćmy ,że chcielibyśmy obrócić pierwsze elementy czterech różnych tablic, drugie elementy tych czterech tablic i trzecie elementy tych czterech tablic. Możemy do tego zastosować poniższy kod: lea bx, RotateGrp1 call Rotate lea bx, RotateGrp2 call Rotate lea bx, RotateGrp3 call Rotate RotateGrp1 dword ary1[0], ary2[0], ary3[0], ary4[0] RotateGrp2 dword ary1[2], ary2[2], ary3[2], ary4[2] RotateGrp3 dword ary1[4], ary2[4], ary3[4], ary4[4] Zauważmy, że wskaźnik do bloku parametrów sam jest parametrem. Przykłady w tej sekcji przekazują ten wskaźnik w rejestrach. Jednakże, możemy przekazać ten wskaźnik gdziekolwiek gdzie przekazujemy inne parametry referencyjne – w rejestrach, w zmiennych globalnych, na stos, w strumieniu kodu, nawet w innym bloku parametrów! Takie są wariacje na ten temat, jednak, zostawmy naszą wyobraźnię. Najlepszym miejscem do przekazania wskaźnika do bloku parametrów są rejestry. Ten tekst generalnie przyjmuje tą zasadę. Chociaż początkujący programiści asemblerowi rzadko używają bloku parametrów, one z pewnością mają swoje miejsce. Niektóre funkcje IBM PC BIOS i MS-DOS używają tego mechanizmu przekazywania parametrów. Bloki parametrów, ponieważ możemy inicjalizować ich wartości podczas asemblacji (stosując byte, word itd.) dostarczają szybkiego, wydajnego sposobu przekazywania parametrów do procedury. Oczywiście, możemy przekazać parametry przez wartość, referencję, wartość-powrót, wynik lub nazwę w bloku parametrów. Poniższy kawałek kodu jest modyfikacją powyższej procedury Rotate, gdzie pierwszy parametr jest przekazywany przez wartość (jego wartość pojawia się wewnątrz bloku parametrów), drugi jest przekazywany przez referencję, trzeci przez wartość-powrót a czwarty przez nazwę (nie ma przekazywania przez wynik ponieważ Rotate odczytuje i zapisuje wszystkie wartości) . Dla uproszczenia kod ten używa bliskich wskaźników i zakłada, że wszystkie zmienne pojawiają się w segmencie danych:
Przykład wywołania tego podprogramu może być taki: I
word
10
J K RotateBlk
Kthunk Kthunk
word word word lea call proc lea ret endp
15 20 25, I, J, Kthunk
di, RotateBlk Rotate
near bx, K
11.6 WYNIKI FUNKCJI Funkcje zwracają wynik, który jest niczym więcej niż wynikiem parametru. W języku asemblera jest kilka różnic pomiędzy procedurą a funkcją. Jest tak prawdopodobnie dlatego że nie ma dyrektyw „func” i „endf” Funkcje i procedury są zazwyczaj różne w HLLach, wywołanie funkcji pojawia się tylko w wyrażeniach, podprogramy wywoływane są jako instrukcje. Język asemblera nie rozróżnia pomiędzy nimi. Możemy zwrócić wynik funkcji i w to samo miejsce przekazać i zwrócić parametry. Typowo jednakże funkcja zwraca tylko pojedynczą wartość (lub pojedyncza strukturę danych) jako wynik funkcji. Metody i lokacje używane do zwracania wyniku funkcji są tematem następnych trzech sekcji. 11.6.1 ZWRACANIE WYNIKU FUNKCJI W REJESTRRZE Podobnie jak przy parametrach, rejestry 80x86 są najlepszym miejscem do zwracania wyniku funkcji. Podprogram getc ze Standardowej Biblioteki UCR jest dobrym przykładem funkcji, która zwraca wartość w jednym z rejestrów CPU. Odczytuje znak z klawiatury i zwraca kod ASCII dla tego znaku w rejestrze al. Ogólnie, funkcje zwracają swój wynik w następujących rejestrach:
Jeszcze raz, tablica ta przedstawia ogólne wskazówki. Jeśli mamy skłonności do robienia czegoś innego, możemy zwrócić wartość podwójnego słowa w (cl, dh, al ,bh). Jeśli zwracamy wynik funkcji w jakichś rejestrach, nie powinniśmy zachowywać i przywracać tych rejestrów. Robiąc tak możemy zniszczyć cały cel tej funkcji. 11.6.2 ZWRACANIE WYNIKU FUNKCJI NA STOS Innym dobrym miejscem gdzie możemy zwrócić wynik funkcji jest stos. Pomysłem jest odłożenie jakichś fikcyjnych wartości na stos dla stworzenia przestrzenia dla wyniku funkcji. Funkcja, przed opuszczeniem, przechowuje swój wynik w tej lokacji. Kiedy funkcja wraca do programu wywołującego, zdejmuje wszystko ze stosu za wyjątkiem tego wyniku funkcji. Wiele HLLi stosuje tą technikę 9chociaż większość HLLi na IBM PC zwraca wynik funkcji w rejestrach). Poniższa sekwencja kodu pokazuje jak wartości mogą być zwracane na stos:
Sekwencja wywołująca: push mov push push push call pop
ax ax, 2 ax n l PasFunc ax
;miejsce dla zwracanego wyniku funkcji
;pobranie zwracanego wyniku funkcji
Na 80286 lub późniejszych procesorach możemy również użyć kodu: push ax ;miejsce dla zwracanego wyniku funkcji push 2 push n push l call PasFunc pop ax ;pobranie zwracanego wyniku funkcji Chociaż program wywołujący odkłada osiem bajtów danych na stos, PasFunc usuwa tylko sześć. Pierwszy „parametr” na stosie jest wynikiem funkcji. Funkcja musi zostawić tą wartość na stosie kiedy wraca. 11.6.3 ZWRACANIE WYNIKU FUNKCJI W KOMÓRKACH PAMIĘCI Innym sensownym miejscem zwracania wyniku funkcji są komórki pamięci. Możemy zwracać wartość funkcji w zmiennych globalnych lub możemy zwrócić wskaźnik (przypuszczalnie w rejestrze lub parze rejestrów) do bloku parametrów. Proces ten jest praktycznie identyczny do przekazywania parametrów do procedury lub funkcji w zmiennych globalnych lub przez blok parametrów. Zwracanie parametrów przez wskaźnik do bloku parametrów jest doskonałym sposobem zwracania dużych struktur danych jako wyniku funkcji. Jeśli funkcja zwraca całą tablicę, najlepszym sposobem do zwrócenia tej tablicy jest przydzielenie pamięci, przechowanie danych w tym obszarze i wychodząc do podprogramu wywołującego zwolnienie tej pamięci. Większość języków wysokiego poziomu, które pozwalają nam na zwracanie dużych struktur danych jako wyniku funkcji stosuje tą technikę. Oczywiście jest bardzo mała różnica pomiędzy zwracaniem wyniku funkcji w pamięci a mechanizmem przekazania parametru przez wynik. 11.7 EFEKT UBOCZNY
Efekt uboczny jest jakimś obliczeniem lub działaniem procedury, które nie jest pierwszoplanowe dla tej procedury. Na przykład, jeśli wybierzemy nie chronione wszystkie rejestry wewnątrz procedury, modyfikacja tych rejestrów jest efektem ubocznym tej procedury. Programowy efekt uboczny, to znaczy praktyczne zastosowanie efektów ubocznych procedury jest bardzo niebezpieczne. Wszyscy programiści zbyt często polegają na efekcie ubocznym procedury. Późniejsze modyfikacje mogą zmienić efekt uboczny, unieważniając cały kod polegający na tym efekcie ubocznym. Może uczynić to nasz program trudnym do zdebuggowania i pielęgnacji. Dlatego też powinniśmy unikać programowania efektów ubocznych. Być może niektóre przykłady programowych efektów ubocznych będą pomocne przy rozjaśnianiu trudności jakie możemy napotkać. Poniższa procedura wyzerowuje tablicę. Z powodu wydajności, czynimy program wywołujący odpowiedzialnym za zachowanie niezbędnych rejestrów. W wyniku jednym efektem ubocznym tej procedury jest to, że rejestry bx i cx są modyfikowane. W szczególności rejestr cx zawiera zero przy zwracaniu.
Jeśli nasz kod oczekuje aby cx zawierał zero po wykonaniu tego podprogramu, będziemy polegać na efekcie ubocznym procedury ClrArray. Głównym celem tego kodu jest wyzerowanie tablicy a nie ustawienie rejestru cx na zero. Później, jeśli zmodyfikujemy procedurę ClrArray do następnej, nasz kod zależny od cx zawierającego zero , już nie będzie pracowała poprawnie:
Więc jak możemy uniknąć pułapek programowych efektów ubocznych w naszych procedurach? Poprzez ostrożne budowanie naszego kodu i zwrócenie uwagi dokładnie jak nasz kod wywołujący i podporządkowana procedura wzajemnie ze sobą oddziałują. Poniższe zasady pomogą uniknąć nam problemów z programowymi efektami ubocznymi: • Zawsze właściwie opisuj warunki wejściowe i wyjściowe procedury. Nigdy nie polegaj na innych warunkach wejściowych i wyjściowych jak tylko tych operacji opisanych • Dziel swoje procedury tak, żeby obliczały pojedynczą wartość lub wykonywały pojedynczą operację. Podprogramy, które robią dwa lub więcej zadań są , z definicji, twórcami efektów ubocznych ,chyba ,że każde wywołanie tego podprogramu wymaga wszystkich obliczeń i operacji • Kiedy aktualizujemy kod w procedurze, upewnijmy się ,że spełniliśmy warunki wejścia i wyjścia. Jeśli nie, albo modyfikujemy program, żeby to robił albo uaktualniamy dokumentację tej procedury odzwierciedlającą nowe warunki wejścia i wyjścia • Zawsze zachowujmy i zwracajmy wszystkie rejestry modyfikowanej procedury • Unikajmy przekazywania parametrów i wyników funkcji w zmiennych globalnych. • Unikajmy przekazywania parametrów przez referencję ( z intencją modyfikowania ich dla użytkowania przez kod wywołujący) Zasady te, podobnie jak wszystkie inne zasady, pewnie będą złamane. Dobre praktyki programistyczne często są poświęcane na ołtarzu wydajności. Nie ma nic złego w łamaniu tych zasad tak często jak to wydaje się konieczna. Jednakże nasz kod będzie trudny do zdebuggowania i pielęgnacji jeśli naruszamy te zasady często. Ale taka jest cena wydajności. Dopóki nie nabierzemy dosyć doświadczenia aby uczynić rozsądny wybór o użyciu efektów ubocznych w naszych programach powinniśmy ich unikać. Dużo częściej zastosowanie efektów ubocznych powoduje więcej problemów niż daje rozwiązań
11.8 ZMIENNA PAMIĘCI LOKALNEJ Czasami procedura będzie wymagała czasowego przechowania, które nie jest wymagane, kiedy procedura wraca. Możemy łatwo alokować taka zmienną pamięci lokalnej na stosie 80x86 wspiera zmienną pamięci lokalnej takim samym mechanizmem jaki stosuje dla parametrów – używa rejestrów bp i sp przy dostępie i alokacji takich zmiennych. Rozważmy poniższy program pascalowski;
Pascal zwykle alokuje zmienne globalne w segmencie danych a lokalne zmienne w segmencie stosu. Dlatego też powyższy program alokuje 50,002 słów pamięci lokalnej (30,001 słów dla Proc1 i 20,001 dla Proc2). Ponieważ 50,002 słów zajmuje 100,004 bajtów pamięci mamy mały problem – CPU 80x86 w trybie rzeczywistym ogranicza segment stosu do 65,536 bajtów. Pascal unika tego problemu przez dynamiczną alokację pamięci lokalnej przy wchodzeniu do procedury i dealokowanie pamięci lokalnej przy powrocie. Chyba ,że Proc1 i Proc2 są obie aktywne (co może zdarzyć się jeśli Proc1 wywołuje Proc2 lub vice versa), jest wystarczająco pamięci dla tego programu. Nie potrzebujemy 30,001 słów dla Proc1 i 20,001 słów dla Proc2 w tym samym czasie. Więc Proc1 alokuje i używa 60,002 bajtów pamięci, potem dealokuje tą pamieć i zwraca (zwalnia 60,002 bajty). Następnie Proc2 alokuje 40,002 bajty pamięci, używa jej, dealokuje ją i wraca do programu wywołującego. Zauważmy ,że Proc1 i Proc2 dzielą wiele tych samych komórek pamięci. Jednakże robią to w różnym czasie. Tak długo jak te zmienne są tymczasowe, czyli wartości nie potrzebujemy zachować od jednego wywołania procedury do drugiego, ta postać alokacji pamięci lokalnej pracuje dobrze. Poniższe porównanie pomiędzy procedurą pascalowską a jej odpowiednikiem w języku asemblera daje nam kod, który jest dobrym pomysłem jak alokować pamięć lokalną na stosie: procedure LocalStuff (i, j,k:integer); var l,m,n:integer; {zmienna lokalna} begin i := i+2; j := l*k+j; n := j-l; m := l+j+n; end; Sekwencja wywołująca: LocalStuff (1,2,3); Kod języka asemblera:
Instrukcja sub sp, 6 czyni miejsce dla trzech słów na stosie. Możemy zaalokować l, m i n w tych trzech słowach. Możemy odnieść się do tych zmiennych poprzez indeksowanie rejestrem bp stosując offset ujemny (zobacz kod powyżej) Przy osiągnięciu instrukcji przy etykiecieL0, stos wygląda tak jak na rysunku 11.15. Kod ten używa dopasowującej instrukcji add sp, 6 na końcu procedury dla dealokowania pamięci lokalnej. Wartość , którą dodajemy do wskaźnika stosu musi dokładnie zgadzać się z wartością jaką odjęliśmy, kiedy alokowaliśmy tą pamięć. Jeśli te dwie wartości się nie zgadzają, wskaźnik stosu na wejściu do podprogramu nie zgadza się ze wskaźnikiem stosu na wyjściu; jest tak jak odłożenie i zdjęcie zbyt wielu pozycji wewnątrz procedury W odróżnieniu od parametrów, które mają stały offset w rekordzie aktywacji, możemy alokować zmienne lokalne w dowolnym porządku. Tak długo jak jesteśmy zgodni z naszą przydzieloną lokacja, możemy alokować je w wybrany przez nas sposób. Zapamiętajmy jednak ,że 80x86 wspiera dwie formy trybu adresowania disp[bp]. Stosuje jednobajtowe przemieszczenie kiedy jest to zakres –128..+127. Stosuje dwubajtowe przemieszczenie dla wartości z zakresu –32,786...+32,767. Dlatego też, powinniśmy umieszczać wszystkie elementarne typy danych i inne małe struktury blisko wskaźnika bazowego, aby można było użyć jednobajtowego przemieszczenia. Powinniśmy umieszczać duże tablice i inne struktury danych poniżej mniejszych zmiennych na stosie. Nie musimy się martwić większość czasu o alokowanie zmiennych lokalnych na stosie. Większość programów nie wymaga więcej niż 64Kb pamięci. CPU działa na zmiennych globalnych szybciej niż zmiennych lokalnych. Są dwie sytuacje, gdzie alokowanie zmiennych lokalnych jako globalnych w segmencie danych nie jest praktyczne: kiedy łączymy język asemblera z HLLem takim jak Pascal i kiedy piszemy kod rekurencyjny. Kiedy łączymy z Pascalem, nasz kod asemblerowy może nie mieć segmentu danych, który może używać, rekurencja często wymaga mnożenia egzemplarzy tej samej zmiennej lokalnej.
Rysunek 11.16 Stos na wejściu do procedury Next 11.9 REKURENCJA Rekurencja występuje wtedy kiedy procedura wywołuje samą siebie. Poniżej mamy na przykład procedurę rekurencyjną: Recursive
proc call Recursive ret Recursive endp Oczywiście, CPU nigdy nie będzie wykonywał instrukcji ret na końcu tej procedury. Na wejściu do Recursive, procedura ta będzie bezpośrednio wywoływała samą siebie znowu i sterownie nigdy nie zostanie przekazane do instrukcji ret. W tym szczególnym przypadku, wynik rekurencji ucieknie nam w pętlę nieskończoną Pod wieloma względami rekurencja jest podobna do iteracji (to znaczy wykonywanie się powtarzających pętli0 Poniższy kod również tworzy pętlę nieskończoną: Recursive Recursive
proc Jmp Ret endp
Recursive
Jest jednakże jedna znacząca różnica pomiędzy tymi implementacjami.. ta pierwsza wersja Recursive odkłada na stos adres powrotu przy każdym wywołaniu podprogramu. To nie dzieje się w powyższym przykładzie (ponieważ instrukcja jmp nie ma wpływu na stos).
Podobnie jak struktury pętlujące, rekurencja wymaga warunku zakończenia aby zatrzymać nieskończoną rekurencję. Recursive może być przepisana z warunkiem zakończenia jak następuje: Recursive
proc dec ax jz QuitRecursive call Recursive QuitRecursive ret Recursive endp Ta modyfikacja podprogramu powoduje, że Recursive wywołuje się liczbę razy pojawiającą się w rejestrze ax Przy każdym wywołaniu, Recursive zmniejsza rejestr ax o jeden i wywołuje się znowu. Ewentualnie Recursive zmniejsza ax do zera i powraca. Jedno co się wydarzy, to CPU wykona łańcuch instrukcji ret aż do przekazania sterowania do oryginalnego wywołania Recursive. Dotychczas jednakże nie było rzeczywistej potrzeby dla rekurencji. W końcu możemy wydajnie zakodować tą procedurę jak poniżej pokazano: Recursive proc RepaetAgain dec ax jnz RepeatAgain ret Recursive endp Oba przykłady będą powtarzały treść procedury ilość razy przekazaną w rejestrze ax. Okazuje się, że jest kilka algorytmów rekurencji, których nie można zaimplementować w trybie iteracyjnym. Jednakże wiele implementacji algorytmów rekurencyjnych jest bardziej wydajnych niż ich iteracyjne odpowiedniki i o wiele bardziej postać algorytmu rekurencyjnego jest dużo łatwiejszy do zrozumienia. Algorytm szybkiego sortowania jest chyba najbardziej znanym algorytmem, który prawie zawsze pojawia się w formie rekurencyjnej. Pascalowska implementacja tego algorytmu:
Podprogram sort jest podprogramem rekurencyjnym. Rekurencja pojawia się przy ostatnich dwóch instrukcjach if w procedurze sort. W języku asemblera , podprogram sort wygląda podobnie jak to:
Inaczej niż w podstawowej optymalizacji, (przechowanie kilku zmiennych w rejestrach) kod ten jest zawsze tłumaczeniem kodu pascalowskiego. Zauważmy ,że zmienne lokalne i i j nie są konieczne w tym kodzie asemblerowym (możemy użyć rejestrów do przechowania ich wartości) Ich zastosowanie demonstruje alokację zmiennych lokalnych na stosie. Jest jedna ważna rzecz jaką musimy zapamiętać kiedy używamy rekurencji – podprogramy rekurencyjne mogą „zjadać” znaczną przestrzeń stosu. Dlatego też, kiedy piszemy podprogram rekurencyjny, zawsze alokujmy dostateczną pamięć w naszym segmencie stosu. Powyższy przykład ma niezwykle anemiczną 512 bajtową przestrzeń stosu, jednak, sortuje on tylko 32 liczby dlatego też 512 bajtów stosu jest wystarczające. Ogólnie nie będziemy znali głębokości naszej rekurencji więc alokowanie dużego bloku pamięci na stos może być właściwe. Jest kilka wydajnych czynników, które nakładają się na procedury rekurencyjne. Na przykład, drugie (rekurencyjne) wywołanie sort w powyższym kodzie asemblera nie musi być wywołaniem rekurencyjnym. Poprzez ustawienie pary zmiennych i rejestrów, prosta instrukcja jmp może zastąpić odkładanie i wywołanie rekurencji. Poprawi to wydajność podprogramu szybkiego sortowania (rzeczywiście trochę) i zredukuje ilość pamięci wymaganą przez stos. Dobra książka o algorytmach, tak jak „The Art. Of Computer Programming, Tom III” D.E. Knutha jest doskonałym dodatkowym źródłem o szybkim sortowaniu .Inne teksty na temat złożonych algorytmów, teorii rekurencji i algorytmów będą dobrym miejscem do szukania pomysłów na wydajne implementowanie algorytmów rekurencji. 11.13 PODSUMOWANIE W programie języka asemblera, wszystko co musimy zrobić to wykorzystać instrukcje call i ret dla implementacji procedur i funkcji. Rozdział Siódmy omawiał podstawowe zastosowanie procedur w programie asemblerowym 80x86; ten rozdział omówił jak zorganizować program podobnie do procedur i funkcji, jak przekazać parametry, zaalokować i uzyskać dostęp do zmiennych lokalnych i powiązane z tym tematy. Rozdział ten zaczął się od spojrzenia co to jest procedura, jak zaimplementować procedurę z MASMem i różnic pomiędzy bliskimi i dalekimi procedurami w 80x86. Po szczegóły zajrzyj do poniższych sekcji: *Procedury *Bliskie i Dalekie Procedury *Wymuszanie bliskich i dalekich wywołań i powroty *Procedury zagnieżdżone Funkcje są bardzo ważnymi konstrukcjami w językach wysokiego poziomu takich jak Pascal. Jednakże, nie ma rzeczywistych różnic pomiędzy funkcją a procedurą w programie asemblerowym. Logicznie, funkcja zwraca wynik a procedura nie; ale deklarujemy i wywołujemy procedury i funkcje identycznie w programie asemblerowym Zobacz: *Funkcje Procedury i funkcje często tworzą „efekt uboczny”. To znaczy, modyfikują one wartości rejestrów i nielokalnych zmiennych. Często, te efekty uboczne są niepożądane. Na przykład procedura może modyfikować rejestr , który kod wywołujący potrzebował zachować. Są dwa podstawowe mechanizmy dla zachowania takich wartości: zachowanie przez kod wywołany i zachowanie przez kod wywołujący. Po szczegóły dotyczące tych schematów zachowania i innych ważnych kwestii zobacz *Zachowanie stanów maszynowych *Efekty uboczne Jedną ze znaczących korzyści stosowania proceduralnych języków takich jak Pascal lub C++ jest to, że możemy łatwo przekazać parametry do i z procedury i funkcji. Chociaż jest to trochę więcej pracy, możemy również przekazać parametry do naszych asemblerowych funkcji i procedur. Ten rozdział omówił jak i gdzie przekazać parametry. Omówił również jak uzyskać dostęp do parametrów wewnątrz procedury i funkcji. Poczytamy o tym w sekcjach: *Parametry *Przekazywanie przez wartość *Przekazywanie przez referencję
*Przekazywanie przez wartość-Powrót *Przekazywanie przez nazwę *Przekazywanie przez leniwe wartościowanie *Przekazywanie parametrów do rejestrów *Przekazywanie parametrów do zmiennych globalnych *Przekazywanie parametrów na stos *Przekazywanie parametrów w strumieniu kodu *Przekazywanie parametrów przez blok parametrów Ponieważ język asemblera w rzeczywistości nie wspiera zapisu funkcji jako takiego, implementacja funkcji składa się z napisania procedury z parametrem zwracanym. Jako takie, wynik funkcji są całkiem podobne do parametrów pod wieloma względami. *Wyniki funkcji *Zwracanie wyniku funkcji do rejestru * Zwracanie wyniku funkcji na stos * Zwracanie wyniku funkcji do komórek pamięci Większość HLLi dostarcza zmiennej lokalnej pamięci powiązanej z aktywacją i deaktywacją procedury lub funkcji. Chociaż kilka programów asemblerowych stosuje lokalne zmienne w identyczny sposób, łatwo jest zaimplementować dynamiczną alokacje zmiennych lokalnych na stosie *Zmienne lokalnej pamięci Rekurencja jest innym HLLowym udogodnieniem, który jest bardzo łatwy do implementacji w asemblerze. Rozdział ten omawia techniki rekurencji i przedstawia prosty przykład użycia algorytmu Szybkiego sortowania 11.14 PYTANIA 1) Wyjaśnij jak działają instrukcje CALL i RET. 2) Jakie są operandy dla dyrektywy asemblerowej PROC? Jaka jest jej funkcja? 3) Przepisz poniższy kod stosując PROC i ENDP
4) Zmodyfikuj swoją odpowiedź pytania 3 tak aby wszystkie wymagane rejestry były przechowane przez procedurę FillMem 5) Co się wydarzy jeśli opuścimy instrukcję przekazania sterowani (taką jak JMP lub RET) bezpośrednio przed dyrektywą ENDP w procedurze? 6) Jak asembler określa czy CALL jest bliski czy daleki? Jak określa czy instrukcja RET jest bliska czy daleka? 7) Jak możemy zastąpić domyślną decyzję asemblera czy zastosować bliski czy daleki CALL lub RET? 8) Czy zawsze musimy zagnieżdżać procedury w programie asemblerowym? Jeśli tak daj przykład. 9) Daj przykład dlaczego możemy chcieć zagnieździć segment w procedurze. 10) Jaka jest różnica pomiędzy funkcją a procedurą ? 11) Dlaczego podprogramy powinny przechowywać rejestry które modyfikują? 12) Jakie są zalety i wady wartości przechowywanych przez kod wywołujący a wartości przechowywanych przez kod wywoływany? 13) Co to są parametry? 14) Jak działają poniższe mechanizmy przekazywania parametrów a) przekazanie przez wartość b) przekazanie przez referencję c) przekazanie przez wartość – powrót d) przekazanie przez nazwę 15) Gdzie jest najlepsze miejsce do przekazania parametrów do procedury? 16) Wypisz pięć lokacji/ metod dla przekazania parametrów z lub do procedury 17) Jakie są parametry, które są przekazane na stos, dostępne wewnątrz procedury? 18) Jaki jest najlepszy sposób dealokacji parametrów przekazanych na stos, kiedy procedura kończy wykonywanie?
19) Podaj definicję poniższej pascalowskiej procedury: procedure PascalProc(i,j,k:integer) 20) Powtórz pytanie 19 zakładając, że procedura jest daleką procedurą 21) Jak wygląda stos podczas wykonywania procedury z pytania 19 ? Pytania 20? 22) Jak procedury w asemblerze uzyskują dostęp do parametrów przekazanych w strumieniu kodu? 23) Jak 80x86 przeskakuje parametry przekazane w strumieniu kodu i kontynuuje wykonywanie programu poza nimi kiedy procedura wraca do kodu wywołującego? 24) Jakie są zalety przekazywania parametrów przez blok parametrów? 25) Gdzie typowo są zwracane wyniki funkcji? 26) Co to jest efekt uboczny? 27) Gdzie typowo są alokowane zmienne lokalne (czasowe)? 28) Jak zaalokować lokalną (czasową) zmienną wewnątrz procedury? 29) Zakładając, że mamy trzy parametry przekazane przez wartość na stos i cztery różne lokalne zmienne, jak wygląda rekord aktywacji po zaalokowaniu zmiennych lokalnych (zakładamy bliską procedurę i żadnych rejestrów innych niż BP odłożonych na stos) 30) Co to jest rekurencja?
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
WYŁĄCZNOŚĆ DO PUBLIKOWANIA TEGO TŁUMACZENIA POSIADA RAG HTTP://WWW.R-AG.PRV.PL „THE ART OF ASSEMBLY LANGUAGE” tłumaczone by KREMIK konsultacja naukowa: NEKRO [email protected] [email protected] &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ROZDZIAŁ DWUNASTY: PROCEDURY: ZAAWANSOWANE TEMATY Ostatni rozdział omawiał jak tworzyć procedury, przekazywać parametry i alokować oraz uzyskać dostęp do zmiennych lokalnych. Ten rozdział omawia jak uzyskać dostęp w innych procedurach, przekazywanie procedur jako parametrów i implementacje jakichś zdefiniowanych przez użytkownika struktur sterujących. 12.0 WSTĘP Rozdział ten całkowicie omawia procedury, parametry i zmienne lokalne rozpoczęte w poprzednim rozdziale. Rozdział ten opisuje jak języki o strukturze blokowej takie jak Pascal, Odula-2, Algol i Ada uzyskują dostęp do lokalnych i nielokalnych zmiennych. Omawia również jak zaimplementować zdefiniowane przez użytkownika strukturę sterującą, iterator. Większość materiału w tym rozdziale będzie interesująca dla piszących kompilatory i tych którzy chcą nauczyć się jak kompilator generuje kod dla pewnych typów konstrukcji programowych.. Kilka czystych programów języka asemblera będzie używać technik, które opisuje ten rozdział. Dlatego niewiele z tego materiału tego rozdziału nie jest szczególnie ważne dla tych ,którzy chcą tylko nauczyć się języka asemblera. Jednakże, jeśli mamy zamiar pisać kompilatory, lub chcemy zrozumieć jak kompilator generuje kod, żeby pisać wydajne programy w HLL’ach, będziemy musieli nauczyć się tego materiału wcześniej czy później. Rozdział ten zaczyna się od omówienia pojęcia zasięg i jak HLL’e jak Pascal uzyskują dostęp do zmiennych w zagnieżdżonych procedurach. Pierwsza sekcja omawia koncepcję zagnieżdżenia leksykalnego i zastosowania wiązań statycznych i terminali do uzyskania dostępu do zmiennych nielokalnych. Następnie ten rozdział opisuje jak przekazać zmienne spod różnych poziomów leksykalnych jako parametry. Trzecia sekcja omawia jak przekazać parametry z jednej procedury jako parametry do innej procedury. Czwarta zasadniczy temat tego rozdziału opisuje przekazywanie procedur jako parametrów. Rozdział kończy się opisaniem iteratorów, zdefiniowanej przez użytkownika struktury danych. Rozdział ten zakłada znajomość języków o strukturze blokowej takich jak Pascal czy Ada. Jeśli nasze doświadczenia z HLL’ami są związane z językami o strukturze nie blokowej takimi jak C, C++, BASIC czy FORTRAN, niektóre z koncepcji przedstawianych w tym rozdziale może być zupełnie nowych i możemy mieć problemy z ich zrozumieniem. Jakiś wstępny tekst o Pascalu lub Adzie bezie pomocny przy wyjaśnieniu niezrozumiałych koncepcji, które ten rozdział zakłada jako warunek wstępny . 12.1 ZAGNIEŻDŻENIA LEKSYKALNE, ŁĄCZENIA STATYCZNE I DISPLAY W języku o strukturze blokowej, takim jak Pascal możliwe jest zagnieżdżanie procedur i funkcji. Zagnieżdżanie jednej procedury wewnątrz innej ogranicza dostęp do zagnieżdżonej procedury; nie możemy uzyskać dostępu do zagnieżdżonej procedury z zewnątrz otaczającej procedury. Podobnie zmienne zadeklarowane wewnątrz procedury są widoczne wewnątrz tej procedury i dla wszystkich procedur zagnieżdżonych wewnątrz tej procedury. Jest to standardowe pojęcie zakresu języka o strukturze blokowej, które powinno być dobrze znane, każdemu kto pisał programy w Pascalu lub Adzie. Jest wiele złożoności ukrytej za koncepcją zakresu, lub leksykalnego zagnieżdżenia, w języku o strukturze blokowej. Podczas gdy dostęp do zmiennych lokalnych w bieżącym rekordzie aktywacji jest wydajny, dostęp do
zmiennych globalnych w języku o strukturze blokowej może być bardzo niewydajny. Sekcja ta będzie opisywać jak HLL’e jak Pascal zajmują się nielokalnymi identyfikatorami i jak uzyskują dostęp do zmiennych globalnych i wywołują nielokalne procedury i funkcje. 12.1.1 ZASIĘG Zasięg w większości języków wysokiego poziomu jest pojęciem statycznym lub wykonywanym w czasie kompilacji. Zasięg jest pojęciem ,kiedy nazwa jest widzialna lub dostępna wewnątrz programu. Ta zdolność do ukrywania nazw jest użyteczna w programach ponieważ jest to często dogodne przy wielokrotnym używaniu pewnych (nie opisowych) nazw. Zmienna i stosowana do sterowania większości pętli for w językach wysokiego poziomu jest doskonałym przykładem. W całym rozdziale będziemy widzieli coś takiego jak xyz_i, xyz_j itp. Powód dla wyboru takich nazw jest taki ,że MASM nie wspiera koncepcji zasięgu nazw jak języki wysokiego poziomu. NA szczęście MASM 6.x i późniejsze wspierają zasięg nazw. Domyślnie MASM 6.x traktuje etykiety instrukcji (te z dwukropkiem po nich) jako lokalne dla procedury. To znaczy, możemy tylko odnosić się do takich etykiet wewnątrz procedury w której są zadeklarowane. Jest to prawda nawet jeśli zagnieździmy jedną procedurę wewnątrz innej. Na szczęście, nie ma dobrego powodu aby chcieć zagnieżdżać procedury w programie MASM; Posiadanie lokalnych etykiet wewnątrz procedury jest miłe. Pozwala to nam na ponowne użycie etykiety instrukcji (np. etykieta pętli) bez martwienia się o konflikt nazw z innymi procedurami. Czasami jednakże, możemy chcieć wyłączyć zasięg nazw w procedurze; dobrym przykładem jest to kiedy mamy instrukcję case, której tablica skoków pojawia się na zewnątrz procedury. Jeśli etykiety instrukcji case są lokalne w tej procedurze, nie będą widzialne na zewnątrz procedury i nie można ich użyć przy tablicy skoków instrukcji case. Są dwa sposoby w jaki możemy wyłączyć zasięg etykiet w MASM 6.x. Pierwszy sposób to zawarcie instrukcji w naszym programie: option nonscoped To pozwoli wyłączyć zasięg zmiennych od tego punktu w przód w naszym pliku źródłowym programu. Możemy z powrotem włączyć zasięg poprzez instrukcję w postaci option scoped Poprzez umieszczenie tych instrukcji wokół naszej procedury możemy wybiórczo sterować zasięgiem. Inny sposób do sterowania zasięgiem pojedynczych nazw jest umieszczenie podwójnego dwukropka („::”) po etykiecie. Informuje to asembler ,że ta szczególna nazwa powinna być globalna dla otaczającej procedury. MASM podobnie jak język C, wspiera trzy poziomy zasięgu: publiczny, globalny (lub statyczny) i lokalny. Symbole lokalne są widoczne tylko wewnątrz procedury, w której są zdefiniowane. Symbole globalne są dostępne w całym pliku źródłowym, ale nie są widoczne w innych modułach programu. Symbole publiczne są widoczne w całym programie, w modułach. MASM używa następujących domyślnych zasad zasięgu: *Domyślnie etykieta instrukcji pojawia się w procedurze jako lokalna dla tej procedury *Domyślnie wszystkie nazwy procedur są publiczne *Domyślnie większość innych symboli jest globalna Zauważmy, że te zasady dotyczą tylko MASMa 6.x. Inne asemblery i wcześniejsze wersje MASMa korzystają z różnych zasad. Przesłonięcie domyślności pierwszej z powyższych zasad jest łatwe - albo zastosujemy instrukcję option nonscoped albo zastosujemy podwójny dwukropek dla uczynienia globalnej etykiety. Powinniśmy być świadomi, że nie możemy uczynić lokalnej etykiety publicznej stosując dyrektyw public lub externdef. Możemy uczynić symbol globalnym (stosując obojętnie jaką technikę) przed uczynieniem ją publiczną. Posiadanie wszystkich nazw procedur publicznymi domyślnie zazwyczaj nie jest dużym problemem. Jednakże może się okazać, że chcemy zastosować tą samą (lokalna) nazwę procedury w kilku różnych modułach. Jeśli MASM automatycznie uczyni takie nazwy publicznymi, linker da nam błąd ponieważ są to wielokrotne publiczne procedury o tej samej nazwie. Możemy włączyć lub wyłączyć tą domyślną akcję stsoując poniższe instrukcje: option proc:private ;procedury są globalne
Rysunek 12. Identyfikacja zasięgu Option proc:export ;procedury są publiczne Zauważmy ,że jakieś debuggery tylko dostarczają informacji symbolicznych jeśli nazwa procedury jest publiczna. Jest tak dlatego, że MASM 6.x domyślnie ustawia je na nazwy publiczne. Problem ten nie istnieje w CodeView; więc możemy zastosować którykolwiek jest bardziej dogodny. Oczywiście, jeśli wybierzemy prywatne nazwy procedur (tylko globalne), wtedy będziemy musieli użyć dyrektyw public lub externdef dla uczynienia żądanej nazwy procedury publiczną. To omówienie lokalnych, globalnych i publicznych symboli stosuje się głownie do instrukcji i etykiet procedur. Nie ma zastosowania do zmiennych zadeklarowanych w segmencie danych, przyrównań, makr typedefów lub większości innych symboli. Takie symbole są zawsze globalne bez względu na to gdzie je zdefiniujemy. Jedyny sposób uczynienia ich publicznymi jest wyszczególnienie ich nazw dyrektywami public lub externdef Jest sposób deklaracji nazw parametrów i zmiennych lokalnych, alokowanych na stosie, taki ,że ich nazwy są lokalne dla danej procedury. Zobacz do dyrektywy proc w podręczniku do MASMa po szczegóły Zakres nazwy ogranicza swoją widzialność wewnątrz programu. To znaczy, program ma dostęp do nazwy zmiennej tylko wewnątrz tego zasięgu nazw. Na zewnątrz zasięgu program nie ma dostępu do tej nazwy. Wiele języków programowania, takich jak Pascal i C++ pozwalają nam na ponowne użycie identyfikatorów jeśli zasięg tych wielokrotnych użyć nie zachodzi na siebie. Jak widzieliśmy MASM dostarcza minimalnych cech zasięgu etykiet instrukcji. Jest jednak inna kwestia związana z zasięgiem: powiązanie adresu i czas życia zmiennej. Powiązanie adresu jest to działanie kojarzenia adresu pamięci z nazwą zmiennej. Czas życia zmiennej jest tą częścią wykonywanego programu podczas którego komórka pamięci jest ograniczana do zmiennej. Rozważmy poniższą procedurę Pascalowską:
Rysunek 12.1 pokazuje zasięg identyfikatorów One, Two, Entry, i , j i Param. Lokalna zmienna j w Two maskuje identyfikator j w procedurze One wewnątrz Two 12.1.2 AKTYWACJA ELEMENTU, POWIĄZANIE ADRESU I CZAS ŻYCIA ZMIENNEJ
Aktywacja elementu jest procesem wywoływania procedury lub funkcji. Kombinacja rekordu aktywacji i jakiegoś kodu wykonywalnego jest uważane za przypadek podprogramu. Kiedy występuje aktywacja elementu podprogram wiąże adres maszynowy do swoich lokalnych zmiennych. Adres powiązany (dla zmiennych lokalnych) występuje wtedy kiedy podprogram modyfikuje wskaźnik stosu aby zrobić miejsce dla zmiennych lokalnych. Czas życia tych zmiennych jest od tego punktu do momentu kiedy podprogram niszczy rekord aktywacji eliminując pamięć dla zmiennych lokalnych. Chociaż zasięg ogranicza widzialność nazw do pewnej części kodu i nie pozwala na powtarzanie nazw wewnątrz tego samego zasięgu, nie znaczy to, że jest tylko jeden adres graniczny dla nazwy. Jest całkiem możliwe, że ma kilka adresów granicznych dla tej samej nazwy w tym samym czasie. Rozważmy wywołanie rekurencyjne procedury. Przy każdej aktywacji procedura buduje nowy rekord aktywacji. Ponieważ poprzednia instancja jeszcze istnieje , teraz są dwa rekordy aktywacji na stosie zawierające zmienne lokalne dla tej procedury. Ponieważ dodatkowo wystąpiła rekurencyjna aktywacja, system buduje więcej rekordów aktywacji, każdy z adresem granicznym do tej samej nazwy. Rozwiązanie tej możliwej dwuznaczności (który adres jest dostępny kiedy działamy na zmiennej?), system zawsze manipuluje zmienną w ostatnim rekordzie aktywacji. Zauważmy, że procedury One i Two w poprzedniej sekcji są pośrednio rekurencyjne. To znaczy, obie wywołują podprogramy które ,po kolei , wywołuje je same. Zakładając, że parametr One jest mniejszy niż 10 przy inicjalizacji wywołania, kod ten będzie generował wiele rekordów aktywacji ( i dlatego też wiele kopii zmiennych lokalnych) na stosie. Na przykład, mamy wywołanie One(9),wtedy stos wygląda tak jak na rysunku 12.2 po pierwszym napotkaniu end związanego z procedurą Two. Jak możemy zobaczyć, jest kilka kopii I i J na stosie w tym punkcie. Procedura Two (bieżący wykonywany podprogram) uzyskuje dostęp do J w ostatnim rekordzie aktywacji, który jest na samym dole rysunku 12.2. Poprzednia instancja Two uzyskiwała dostęp do zmiennej J tylko w swoim rekordzie aktywacji, kiedy bieżąca instancja wracała do One a potem cofała do Two. Czas życia instancji zmiennych jest od punktu stworzenia rekordu aktywacji do punktu usunięcia rekordu aktywacji. Zauważmy ,że pierwsza instancja J ,powyżej, (na szczycie powyższego diagramu) ma najdłuższy czas życia ze wszystkich zachodzących na siebie instancji J. 12.1.3 ŁĄCZENIA STATYCZNE Pascal pozwala procedurze Two uzyskać dostęp do I w procedurze One. Jednakże, kiedy istnieje możliwość rekurencji może być kilka instancji I na stosie. Pascal, oczywiście, pozwoli tylko procedurze Two na dostęp do ostatniej instancji I. Na diagramie stosu na rysunku 12.2, odpowiada to wartości I w rekordzie aktywacji, który zaczyna się z „parametrem One(9+1)”. Jedynym problemem jest to „jak się dowiedzieć gdzie znajduje się rekord aktywacji zawierający I”? Szybką ale kiepską myślą, jest odpowiedź, że jest to po prostu indeks wsteczny do stosu. W końcu możemy łatwo zobaczyć na powyższym diagramie, że I jest offsetem osiem z rekordu aktywacji Two. Niestety, nie zawsze jest taki przypadek. Zakładamy, że procedura Three również wywołuje procedurę Two a poniższa instrukcja pojawia się wewnątrz procedury One: If (Entry <5) then Three(Entry*2) else Two(Entry); Z tą instrukcją w tym miejscu jest całkiem możliwe jest posiadanie dwóch różnych ramek stosu na wejściu do Procedury Two: jeden z rekordem aktywacji dla procedury Three wciśnięty między rekordy aktywacji One i Two i jeden z rekordami aktywacji dla procedur One i Two przylegającymi jeden do drugiego. Najwyraźniej stały offset z rekordu aktywacji Two nie zawsze wskazuje na zmienną I w ostatnim rekordzie aktywacji.
Rysunek 12.2 Pośrednia rekurencja Przebiegły czytelnik może zauważyć, że zachowana wartość bp w rekordzie aktywacji Two wskazuje na wywołujący rekord aktywacji. Można pomyśleć, że możemy użyć tego jako wskaźnika do rekordu aktywacji One. Ale ten schemat jest zawodny z tego samego powodu że stały offset zawodzi technicznie. A stara wartość Bp, łączona dynamicznie, wskazuje na wywołujący rekord aktywacji. Ponieważ kod wywołujący nie jest konieczni otoczony procedurą, łączenie dynamiczne może nie wskazywać na otaczający rekord aktywacji procedury Jaka jest rzeczywista potrzeba wskaźnika do otaczającego rekordu aktywacji procedury. Wiele kompilatorów w językach o strukturze blokowej tworzy takie wskaźniki, łączenie (linkowanie) statyczne. Rozważmy poniższy Pascalowski kod: Procedure Parent; var i, j: integer; procedure Child1; var j :integer; begin for j := 0 to 2 do writeln(i); end {Child1}; procedure Child2; var i: integer; begin for i := 0 to 1 do Child1; end {Child2};
Rysunek 12. 3 Rekord aktywacji po kilku zagnieżdżonych wywołaniach begin {Parent} Child2; Child2; end; Po wprowadzeniu po raz pierwszy Child1, stos będzie wyglądał jak na rysunku 12.3.Kiedy Child1 spróbuje uzyskać dostęp do zmiennej i w Parent, potrzebny będzie wskaźnik, statyczne łącze, do rekordu aktywacji Parent. Niestety nie ma sposobu dla Child1, na wejściu, ,wykombinować gdzie leży w pamięci rekord aktywacji Parent. Byłoby to konieczne dla kodu wywołującego (Child2 w tym przykładzie) dla przekazania łącza statycznego do Child1. Ogólnie, kod wywoływany może traktować łącze statyczne jako inny parametr; zazwyczaj odkładany na stos bezpośrednio przed wykonaniem instrukcji call. Aby w pełni zrozumieć jak przekazujemy łącze statyczne z wywołania do wywołania, musimy najpierw zrozumieć koncepcję poziomów leksykalnych. Poziomy leksykalne w Pascalu odpowiadają statycznemu zagnieżdżeniu poziomów procedur i funkcji. Wielu piszących kompilatory wyszczególnia lex poziom zero jako główny program. To znaczy wszystkie symbole ,deklarowane w głównym programie istnieją jako lex poziom zero. Nazwy procedur i funkcji pojawiających się w programie głównym definiują lex poziom jeden, nie zależnie jak dużo procedur i funkcji pojawia się w programie głównym. Wszystkie one zaczynają nową kopię lex poziomu jeden. Dla każdego poziomu zagnieżdżenia Pascal wprowadza nowy lex poziom. Rysunek 12.4 pokazuje to. Podczas wykonywania, program może tylko uzyskać dostęp do zmiennych na lex poziomie mniejszym lub równym poziomowi bieżącego podprogramu. Ponadto tylko jeden zbiór wartości na danym lex poziomie jest dostępny w jednym czasie a wartości te są zawsze w ostatnim rekordzie aktywacji przy tym lex poziomie. Przed zamartwianiem się o to jak uzyskać dostęp do nie lokalnych zmiennych używając łącza statycznego musimy wykombinować jak przekazać łączę statyczne jako parametr. Kiedy przekazujemy łącze statyczne jako parametr do jednostki programowej (procedury lub funkcji), są trzy typu sekwencji wywołania: • jednostka programowa wywołuje „potomka” procedury lub funkcji. Jeśli bieżący lex poziom jest n, wtedy procedura lub funkcja „potomna” jest na lex poziomie n+1 i jest lokalna
Rysunek 12.4 Schematyczne pokazanie poziomów leksykalnych procedury
Rysunek 12.5 Ogólny rekord aktywacji dla bieżącej jednostki programowej. Zauważmy, że większość języków o strukturze blokowej nie pozwala na wywoływanie procedur lub funkcji spod lex poziomów większych niż n+1 • •
Jednostka programowa wywołuje równorzędne procedury lub funkcje. Równorzędna procedura lub funkcja jest na tym samym poziomie leksykalnym jak bieżący kod wywołujący a pojedyncza jednostka programowa łączy obie jednostki programowe Jednostka programowa wywołuje przodka procedury lub funkcji. Jednostka przodka jest albo jednostką macierzystą, rodzicem jednostki przodka lub równorzędną jednostką przodka
Sekwencje wywoływania dla pierwszych dwóch typów powyższych wywołań są bardzo proste. Przez wzgląd na ten przykład założymy, że rekord aktywacji dla tej procedury przybiera ogólną postać jak na rysunku 12.5. Kiedy procedura lub funkcja macierzysta wywołują jednostkę programową potomka, łącze statyczne jest niczym więcej jak tylko wartością w rejestrze bp bezpośrednio przed wywołaniem. Dlatego też, przekazanie łącza statycznego do jednostki potomnej, jedynie odkładając bp przed wykonaniem instrukcji call:
Rysunek 12.6: Łącze statyczne push bp call ChildUnit Oczywiście jednostka programowa potomka przetwarza łącze statyczne za stosem podobnie jak inne parametry. W tym przypadku tak statyczne jak i dynamiczne łącze są takie same. Ogólnie jednak nie jest to prawda. Jeśli jednostka programowa wywołuje równorzędną procedurę lub funkcję, bieżąca wartość w bp nie jest łączem statycznym. Jest wskaźnikiem do kodu wywołującego zmienne lokalne a równorzędna procedura nie może uzyskać dostępu do tych zmiennych. Jednakże, kod wywołujący i wywoływany dzielą tą samą macierzystą jednostkę programową, wiec kod wywołujący może po prostu odłożyć kopię swojego łącza statycznego na stos przed wywołaniem równorzędnej procedury lub funkcji. Poniższy kod robi to, zakładając że wszystkie procedury i funkcję są bliskie.: push [bp+4] ;odłożenie łącza statycznego na stos call PeerUnit Jeśli procedura lub funkcja jest daleka, łącze statyczne będzie dwa bajty dalej na stosie, więc będzie trzeba zastosować poniższy kod: push [bp+6] ;odłożenie łącza statycznego na stos call PeerUnit Wywoływanie przodka jest trochę bardziej złożone. Jeśli jesteśmy obecnie na lex poziomie n i życzymy sobie wywołać przodka z lex poziomu m (m
mov bx, ss:[bx+4] ;do Lex Poziomu 3 mov bx, ss:[bx+4] ;do Lex Poziomu 2 push ss:[bx+4] call ProcAtLL2 Zauważmy ,że prefiks ss: w powyższych instrukcjach. Pamiętamy ,że rekordy aktywacji wszystkie są w segmencie stosu a indeksy bx domyślnie w segmencie danych. 12.1.4 UZYSKANIE DOSTĘPU DO ZMIENNYCH NIE LOKALNYCH STOSUJĄC ŁACZA STATYCZNE Żeby uzyskać dostęp do nie lokalnych zmiennych, musimy przeglądnąć łańcuch łącz statycznych aż do uzyskania wskaźnika do żądanego rekordu aktywacji. Operacja ta jest podobna do lokalizowania łącz statycznych dla wywoływania procedur naszkicowana w poprzedniej sekcji, z wyjątkiem tego, że przeglądamy tylko n-m łącz statycznych zamiast (n-m) + 1 łącz uzyskujących wskaźnik do odpowiedniego rekordu aktywacji. Rozważmy poniższy kod Pascalowski:
Procedura Inner uzyskuje dostęp do globalnych zmiennych spod lex poziomu n-1 i n-2 (gdzie n jest lex poziomem procedury ) Procedura Middle uzyskuje dostęp do pojedynczej zmiennej globalnej spod lex poziomu m-1 (gdzie m jest lex poziomem procedury Middle) Poniższy kod jeżyka asemblera implementuje te trzy procedury:
Jak widzimy, dostęp do zmiennych globalnych może być bardzo nieefektywny. Zauważmy, że ponieważ różnice pomiędzy rekordami aktywacji rosną, stają się mniej i mniej wydajne przy uzyskiwaniu dostępu do zmiennych globalnych. Uzyskanie dostępu do zmiennych globalnych w poprzednim rekordzie aktywacji wymagało tylko jednej dodatkowej instrukcji przy dostępie, przy dwóch lex poziomach potrzebujemy dwóch dodatkowych instrukcji, itd. Jeśli przeanalizujemy dużą liczbę programów pascalowskich, odkryjemy że większość z nich nie zagnieżdża procedur i funkcji a w tych gdzie są zagnieżdżone jednostki programowe, rzadko uzyskują dostęp do zmiennych globalnych. Jest jednak jeden ważny wyjątek. Chociaż procedury i funkcje Pascala rzadko uzyskują dostęp do lokalnych zmiennych wewnątrz innych procedur i funkcji, one często uzyskują dostęp do zmiennych globalnych zadeklarowanych w programie głównym. Ponieważ takie zmienne pojawiają się w lex poziomie zero, dostęp do takich zmiennych może być tak niewydajny jak możliwy kiedy używamy łącz statycznych. Rozwiązując ten drobny problem, większość opartych na 80x86 języków o strukturze blokowej alokuje zmienne na lex poziomie zero bezpośrednio w segmencie danych i uzyskując dostęp do nich bezpośrednio. 12.1.5 Display (Wyświetlanie) Po przeczytaniu poprzedniej sekcji mogliśmy nabrać pewności, że nigdy nie powinniśmy stosować nie lokalnych zmiennych lub ograniczyć nielokalny dostęp do tych zmiennych zadeklarowanych na lex poziomie zero. W końcu jest często dosyć łatwo włożyć wszystkie dzielone zmienne na lex poziom zero. Jeśli jesteśmy projektantami języka programowania, możemy zaadoptować filozofię projektowania języka C i po prostu nie dostarczać struktur blokowych. Taki kompromis okazuje się być niepotrzebny. Jest struktura danych, display (wyświetlanie), która dostarcza wydajnego dostępu do każdego zbioru nie lokalnych zmiennych.
Rysunek 12.7 Display Display jest po prostu tablicą wskaźników do rekordów aktywacji. Display[0] zawiera wskaźnik do ostatniego rekordu aktywacji dla lex poziomu zero. Display[1] zawiera wskaźnik do ostatniego rekordu aktywacji dla poziomu jeden i tak dalej. Zakładając ,że zachowujemy tablicę Display w bieżącym segmencie danych (zawsze dobre miejsce do jej trzymania), to tylko dzięki dwóm instrukcjom uzyskujemy dostęp do nie lokalnych zmiennych. Display pracuje tak jak pokazano na rysunku 12.7 Zauważmy ,że wejście w display’u zawsze wskazuje ostatni rekord aktywacji dla procedury na danym lex poziomie. Jeśli nie ma żadnego aktywnego rekordu aktywacji dla poszczególnego lex poziomu (np. powyżej lex poziom sześć) wtedy wejście w display’u zawiera śmieci. Maksymalne zagnieżdżenie poziomów leksykalnych w naszym programie określa jak dużo elementów musi być w display’u. Większość programów ma tylko trzy lub cztery zagnieżdżone procedury więc display jest zazwyczaj bardzo mały. Ogólnie, rzadko będziemy wymagali więcej niż 10 lub więcej elementów w display’u. Inną zaletą zastosowania display’a jest to ,że każdy pojedyncza procedura może zachować informację display’a ,kod wywołujący nie musi być zawiły. Kiedy stosujemy łącza statyczne kod wywołujący musi obliczyć i przekazać właściwe łącze statyczne do procedury. To nie tylko jest wolne ale kod który to robi musi pojawiać się przed każdym wywołaniem. Jeśli nasz program używa stosuje display’a kod wywoływany zamiast wywołującego zachowuje display więc potrzebujemy jednej kopii kodu na procedurę. Co więcej, jak pokazują następne przykłady kod działający z display’em jest krótszy i szybszy. Utrzymanie display’a jest bardzo proste. Na wejściu inicjalizującym do procedury musimy najpierw zapisać zawartość tablicy display spod bieżącego lex poziomu a potem przechować wskaźnik do bieżącego rekordu aktywacji do tego samego miejsca. Zakładając ,że nie lokalna zmienna wymaga tylko dwóch instrukcji, jedną do załadowania elementu display do rejestru i drugą dla uzyskania dostępu zmiennej. Poniższy kod implementuje procedury z przykładowymi łączami statycznymi.
Chociaż ten kod nie wygląda szczególnie lepiej niż dawny kod, zastosowanie display’a jest często dużo bardziej wydajne niż zastosowanie łącz statycznych 12.1.6 INSTRUKCJE ENTER I LEAVE 80286. Kiedy projektowano 80286, projektanci CPU Intela zadecydowali, że dodadzą dwie instrukcje do pomocy przy utrzymaniu display’i. Niestety, chociaż ich praca jest bardzo ogólna i wymaga tylko danych w segmencie
stosu, jest bardzo powolna; dużo wolniejsza niż zastosowanie technik z poprzedniej sekcji. Chociaż wiele nie optymalizujących kompilatorów stosuje te instrukcje, najlepsze kompilatory unikają ich stosowania ,jeśli to możliwe. Instrukcja leave jest bardzo łatwa do zrozumienia. Wykonuje takie same operacje jak dwie instrukcje: mov sp, bp pop bp Dlatego też możemy użyć tej instrukcji dla kodu standardowej procedury exit jeśli mamy procesor 80286 lub późniejszy. Na 80386 lub wcześniejszych procesorach instrukcja leave jest krótsza i szybsza niż odpowiadająca jej sekwencja move i pop. Jednak instrukcja leave jest wolniejsza na procesorach 80486 i późniejszych. Instrukcja enter posiada dwa operandy. Pierwszy jest liczbą bajtów pamięci lokalnej wymaganą przez bieżącą procedurę, drugi jest lex poziomem bieżącej procedury. Instrukcja enter robi co następuje: ; ENTER Locals, LexLevel push bp ;zachowanie łącza dynamicznego mov tempreg, sp ;zachowanie na później cmp LexLevel, 0 ;zrobione jeśli jest to lex poziom zero je Lex0 lp: dec LexLevel jz Done ;wyjście jeśli w końcu lex poziom sub bp, 2 ;indeks do display w poprzednim rekordzie aktywacji push [bp] ;i odłożenie każdego elementu jmp lp ;powtórzenie dla każdego wejścia Done: push tempreg ;dodanie wejścia dla bieżącego lex poziomu Lex0: mov bp, tempreg ;wskaźnik do bieżącego rekordu aktywacji sub sp, Locals ;alokowanie pamięci lokalnej Jak możemy zobaczyć w tym kodzie, instrukcja enter kopiuje display z rekordu aktywacji do rekordu aktywacji. Może to stać się bardzo kosztowne jeśli zagnieżdżamy procedury na dowolną głębokość. Większość HLLi ,jeśli używa instrukcji enter , zwykle określa poziom zagnieżdżenia zero unikając kopiowania display’a w całym stosie. Instrukcja enter wkłada wartość dla wejścia display[n[ pod lokację BP-(n*2). Instrukcja enter nie kopiuje wartości dla display[0] do każdej ramki stosu. Intel założył, że będziemy trzymać zmienne globalne programu głównego w segmencie danych. Oszczędza to czas i pamięć, nie przeszkadzają kopiowaniu wejścia display[0]. Instrukcja enter jest bardzo wolna, szczególnie na procesorach 80486 i późniejszych. Jeśli rzeczywiście chcemy skopiować display z rekordu aktywacji do rekordu aktywacji jest prawdopodobnie inny sposób ich odłożenia. Poniższy strzępek kodu pokazuje jak to zrobić:
Jeśli wierzyć cyklom Intelowskim ,widzimy, że instrukcja enter nie jest prawie nigdy szybsza niż prosta sekwencja instrukcji, która wykonuje to samo zadanie. Jeśli interesuje nas oszczędność miejsca zamiast pisanie szybkiego kodu, instrukcja enter ogólnie jest lepszą alternatywą. Tak samo, ogólnie rzecz biorąc jest prawdą również dla instrukcji leave. To jest tylko jeden długi bajt, ale jest wolniejszy niż odpowiadające mu instrukcje mov bp, sp i pop bp. 12.2 PRZEKAZYWANIE ZMIENNYCH SPOD RÓŻNYCH LEX POZIOMÓW JAKO PARAMETRY Uzyskanie dostępu spod różnych lex poziomów w programie o strukturze blokowej wprowadza do programu kilka złożoności. Poprzednia sekcja wprowadziła nas do złożoności przy dostępie do nie lokalnych zmiennych. Problem ten staje się nawet gorszy kiedy próbujemy przekazać takie zmienne jako parametry do innych jednostek programowych. Poniższa podsekcja omawia strategię dla każdego ważnego mechanizmu przekazywania parametrów. Dla celów tego omówienia, poniższa sekcja będzie zakładała, ze „lokalna” odnosi się do zmiennych w bieżącym rekordzie aktywacji, „globalna” odnosi się do zmiennych w segmencie danych a „pośredni” odnosi się do zmiennych w jakimś rekordzie aktywacji innym niż bieżący rekord aktywacji. Zauważmy że poniższa sekcja nie zakłada, że ds. jest równe ss. Te sekcje również przekazują również wszystkie parametry na stos. Możemy łatwo zmodyfikować szczegóły przekazywania tych parametrów gdzie indziej. 12.2.1 PRZEKAZYWANIE PARAMETRÓW PRZEZ WARTOŚĆ W JĘZYKU O STRUKTURZE BLOKOWEJ Przekazywanie wartości parametrów do jednostki programowej nie jest trudniejsze niż dostęp do odpowiadających zmiennych; wszystko co musimy zrobić to odłożyć wartość na stos przed wywołaniem skojarzonej procedury. Przekazując zmienne globalne przez wartość do innej procedury możemy zastosować kod podobny do poniższego: push GlobalVar ;zakładamy że GlobalVar jest w DSEG call Procedure Przekazując lokalną zmienną przez wartość do innej procedury możemy użyć takiego kodu: push [bp-2] ;lokalna zmienna w bieżącym rekordzie aktywacji call Procedure Przekazując zmienną pośrednią jako wartość parametru, musimy najpierw zlokalizować rekord aktywacji tej zmiennej pośredniej a potem odłożyć jego wartość na stos. Dokładny mechanizm jaki zastosujemy zależy od tego czy używamy łącz statycznych lub display’a do śledzenia rekordu aktywacji zmiennej pośredniej. Jeśli stosujemy łącza statyczne możemy zastosować kod podobny do poniższego do przekazania zmiennej z dwóch lex poziomów z bieżącej procedury: mov bx, [bp+4] ;zakładamy, że S.L jest pod offsetem 4 mov bx, ss:[bx+4] ;przechodzimy dwa łącza statyczne push ss:[bx-2] ;odkładamy wartość zmiennych call Procedure Przekazywanie zmiennej pośredniej przez wartość kiedy stosujemy display jest nieco łatwiejsze. Możemy użyć kodu takiego jak poniższy dla przekazania zmiennej pośredniej z lex poziomu jeden: mov bx, Display[1*2] ;pobranie wejścia Display[1] push ss:[bx-2] ;odłożenie wartości zmiennej
call
Procedure
12.2.2 PRZEKAZYWANIE PARAMETRÓW PRZEZ REFERENCJĘ,WARTOŚĆ I WARTOŚĆ-WYNIK W JĘZYKU O STRUKTURZE BLOKOWEJ Mechanizmy przekazywania przez referencję, wynik i wartość -wynik ogólnie przekazują adres parametru na stos. Jeśli globalna zmienna rezyduje w segmencie danych, wszystkie rekordy aktywacji istnieją w segmencie stosu a ds ≠ ss wtedy musimy przekazać daleki wskaźnik dla uzyskania dostępu do wszystkich możliwych zmiennych. Przekazując daleki wskaźnik musimy odłożyć wartość segmentu występującą przed wartością offsetu na stos. Dla globalnych zmiennych, wartość segmentu znajduje się w rejestrze ds.; dla wartości nie globalnych, ss zawiera wartość segmentu Dla obliczenia offsetowej części adresu normalnie użylibyśmy instrukcji lea. Poniższa sekwencja kodu przekazuje zmienne globalne przez referencję: push ds. ;odkładamy najpierw adres segmentu lea ax, GlobalVar ;obliczanie offsetu push ax ;odłożenie offsetu GlobalVar Zmienne globalne są specjalnym przypadkiem ponieważ asembler może obliczyć ich czas wykonania podczas czasu asemblacji. Dlatego też, tylko dla skalarnych zmiennych globalnych, możemy skrócić powyższą sekwencję kodu do: push ds ;odłożenie adresu segmentu push offset GlobalVar ;odłożenie części offsetu call Procedure Dla przekazania zmiennej lokalnej poprzez referencję nasz kod musi najpierw odłożyć wartość ss na stos a potem odłożyć offset. Ten offset jest offsetem zmiennej wewnątrz segmentu stosu, nie offsetem wewnątrz rekordu aktywacji! Poniższy kod przekazuje adres lokalnej zmiennej poprzez referencję: push ss ;odłożenie adresu segmentowego lea ax, [bp-2] ;obliczenie offsetu lokalnej zmiennej push ax ;i odłożenie go call Procedure Dla przekazania zmiennej pośredniej poprzez referencję musimy najpierw zlokalizować rekord aktywacji zawierający zmienną żeby obliczyć adres efektywny w segmencie stosu. Kiedy stosujemy łącza statyczne, kod przekazujący adres parametru może wyglądać następująco: push ss ;odłożenie części segmentowej mov bx, [bp+4] ;Zakładamy ,że S.L jest pod offsetem 4 mov bx, ss:[bx+4] ;przechodzimy dwa łącza statyczne lea ax, [bx-2] ;obliczanie adresu efektywnego push ax ;odłożenie części offsetowej call Procedure Kiedy stosujemy display, sekwencja wywołująca mogłaby wyglądać następująco: push ss ;odłożenie części segmentowej mov bx, Display[1*2] ;pobranie wejścia Display[1] lea ax, [bx-2] ;pobranie offsetu zmiennej push ax ;o odłożenie go call Procedure Jak możemy sobie przypomnieć z poprzedniego rozdziału, są dwa sposoby przekazywania parametrów przez wartość-wynik. Możemy odłożyć wartość na stos a potem, kiedy wracamy z procedury, zdejmujemy tą wartość ze stosu i przekazujemy z powrotem do zmiennej. To jest właśnie specjalny przypadek mechanizmu przekazywania przez wartość opisanego w poprzedniej sekcji. 12.2.3 PRZEKAZYWANIE PARAMETRÓW PRZEZ NAZWE I LENIWE WARTOŚCIOWANIE W JĘZYKU O BLOKOWEJ STRUKTURZE Ponieważ przekazujemy adres thunka kiedy przekazujemy parametry przez nazwę lub leniwe wartościowanie, obecność globalnych, pośrednich i lokalnych zmiennych nie wpływa na sekwencję wywołującą procedury. Zamiast tego, thunk musi zając się rozróżnianiem lokacji tych zmiennych. Poniższe przykłady przedstawia thunki, które mogą łatwo zmodyfikować te thunki dla parametrów leniwego wartościowania. Największy problem thunk ma z lokalizacją rekordu aktywacji zawierającego zmienną której adres zwraca. W ostatnim rozdziale nie było to zbyt dużym problemem ponieważ zmienne istniały albo w bieżącym rekordzie
aktywacji albo globalnej przestrzeni danych. W obecności zmiennych pośrednich zadanie to staje się co nieco bardziej złożone. Najłatwiejszym rozwiązaniem jest przekazanie dwóch wskaźników kiedy przekazujemy zmienną przez nazwę. Pierwszy wskaźnik powinien być adresem thunka, drugi wskaźnik powinien być offsetem rekordu aktywacji zawierającego zmienną do której thunk musi uzyskać dostęp. Kiedy procedura wywołuje thunka, musi przekazać ten offset rekordu aktywacji jako parametr do thunka. Rozpatrzmy poniższą procedurę Panacei: TestThunk: procedure (name item: integer; var j : integer); begin TestThunk; for j in 0..9 do item := 0; end TestThunk; CallThunk: procedure; var A: array [0..9] : integer; I: integer; endvar; begin CallThunk TestThunk (A[I], I); end CallThunk; Kod asemblerowy dla powyższego kodu mógłby wyglądać tak:
12.3 PRZEKAZYWANIE PARAMETRÓW JAKO PARAMETRÓW DO INNYCH PROCEDUR Kiedy procedura przekazuje jeden ze swoich własnych parametrów jako parametr do innej procedury, rozwiną się pewne problemy, które nie istniały kiedy przekazywaliśmy zmienne jako parametry. Istotnie, w pewnych (rzadkich) przypadkach jest logicznie niemożliwe przekazanie jakiegoś typu parametru do innej procedury. Sekcja ta zajmuje się problemami przekazywania parametrów jednej procedury do innej procedury. Przekazywanie parametrów przez wartość zasadniczo niczym się nie różni niż zmiennych lokalnych . wszystkie techniki z poprzedniej sekcji mają zastosowanie do przekazywaniu parametrów przez wartość. Poniższa sekcja zajmuje się przypadkami gdzie wywoływana procedura przekazuje parametr przekazany do niej przez referencję, wartośćwynik, wynik i leniwe wartościowanie. 12.3.1 PRZEKAZANIE PARAMETRÓW REFERENCYJNYCH DO INNYCH PROCEDUR Przekazywanie parametrów referencyjnych do innej procedury jest tam gdzie zaczyna się złożoność. Rozważmy następującą (pseudo) Pascalowski szkielet procedury: procedure HasRef (var refparm:integer); procedure ToProc (???? Parm:integer); begin end; begin {HasRef} TpProc(refParm); end;
„????” w liście parametrów ToProc wskazuje, że wypełnimy we właściwym mechanizmie przekazywania parametrów jak gwarantuje omówienie Jeśli ToProc wymaga przekazania parametrów przez wartość (???? Jest pustym ciągiem), wtedy HasRef musi pobrać wartość parametru refparm i przekazać tą wartość do ToProc. Wykonuje to poniższy kod: les bx, [bp+4] ;pobranie adresu refparm push es:[bx] ;odłożenie liczby całkowitej wskazującej na refparm call ToProc Przekazanie parametrów referencyjnych poprzez referencję, wartość-wynik lub wynik jest łatwe - wystarczy skopiować parametr wywołujący na stos. To znaczy, jeśli parametr parm w ToProc jest parametrem referencyjnym, parametrem wartość-wynik lub parametrem wyniku, zastosujemy poniższą sekwencję wywołującą: push [bp+6] ;odłożenie części segmentowej ref param push [bp+4] ;odłożenie części offsetowej ref parm call ToProc Przekazanie parametrów referencyjnych przez nazwę jest dosyć proste. Piszemy thunka, który chwyta adres parametru referencyjnego i zwraca tą wartość. W powyższym przykładzie wywołanie ToProc może wyglądać tak jak poniżej: jmp SkipThunk Thunk0 proc near les bx, [bp+4] ;zakładamy, że BP wskazuje AR HasRef ret Thunk0 endp SkipTunk
push push call
offset Thunk0 bp ToProc
;adres thunka
Wewnątrz ToProc, referencja do parametru może wyglądać jak następuje: push bp ;zachowanie wskaźnika do rekordu aktywacji mov bp, [bp+4] ;wskaźnik do rekordu aktywacji Parm call near ptr [bp+6] ;wywołanie thunka pop bp ;odzyskanie naszego wskaźnika do AR mov ax, es:[bx] ;dostęp do zmiennej Przekazanie parametru referencyjnego przez leniwe wartościowanie jest bardzo podobne do przekazania go przez nazwę. Jedyna różnica (w sekwencji wywołania ToProc) jest taka, że thunk musi zwrócić wartość zmiennej zamiast jej adresu. Możemy łatwo wykonać to z poniższym thunkiem: Thunk1 proc near push es push bx les bx, [bp+4] ;zakładamy, że BP wskazuje AR HasRef mov ax, es:[bx] ,zwracana wartość ref parm w ax pop bx pop es ret Thunk1 endp 12.3.2 PRZEKAZANIE PARAMETRÓW WARTOŚĆ-WYNIK I WYNIK JAKO PARAMETRÓW Zakładając ,że stworzyliśmy zmienną lokalną która przechowuje wartość parametru wartość-wynik lub wynik, przekazujemy jeden z tych parametrów do innej procedury nie różni się od przekazywania wartości parametru do innego kodu. Ponieważ procedura robi lokalną kopię parametru wartość-wynik lub alokuje pamięć dla parametru wyniku, możemy traktować zmienną podobnie jak parametr wartość lub zmienną lokalną pod względem przekazywania jej do innej procedury. Oczywiście, nie ma sensu stosować wartości parametru wynik do przechowywania wartości w pamięci lokalnego
parametru. Dlatego też, uważajmy kiedy przekazujemy parametry wynikowe do innych procedur aby zainicjalizować parametr wyniku przed użyciem jego wartości. 12.3.3 PRZEKAZYWANIE PARAMETRÓW NAZW DO INNYCH PROCEDUR Ponieważ thunk przekazywania parametrów przez nazwę zwraca adres parametru, przekazywanie parametrów nazw do innej procedury jest bardzo podobne do przekazywania parametrów referencyjnych do innej procedury. Podstawowa różnica występuje kiedy przekazujemy parametr jako parametr nazwy. Kiedy przekazujemy parametr nazwy jako parametr wartości, po pierwsze wywołujemy thunk, wyłuskujemy adres jaki zwraca thunk a potem przekazujemy wartość do nowej procedury. Poniższy kod demonstruje takie wywołanie kiedy thunk zwraca adres zmiennej w es:bx (zakładając przekazanie parametru przez nazwę wskaźnik AR jest pod adresem bp+4 a wskaźnik do thunka jest pod adresem bp+6):
Przekazywanie parametru nazw do innej procedury przez referencję jest bardzo proste. Wszystko co musimy zrobić to odłożyć adres zwracanego thunka na stos. Poniższy kod, wykonujący to jest bardzo podobny do powyższego:
Przekazanie parametru nazw do innej procedury jako przekazanie przez parametr nazwy jest bardzo łatwe; wszystko co trzeba zrobić to przekazać thunk ( i powiązane wskaźniki) do nowej procedury. Wykonuje to poniższy kod: push [bp+6] ;przekazanie adresu thunka push [bp+4] ;przekazanie adresu AR thunka call ToProc Aby przekazać parametr nazwy do innej procedury przez leniwe wartościowanie musimy stworzyć thunk dla parametru leniwego wartościowania, który wywołuje thunk przekazywania parametru przez nazwę, usuwając wskaźnik i potem zwracając tą wartość. 12.3.4 PRZEKAZYWANIE PARAMETRÓW LENIWEGO WARTOSCIOWANIA JAKO PARAMETRÓW Parametry leniwego wartościowania typowo składają się z trzech części: adresu thunku, lokacji dla trzymania wartości powrotu thunka i zmiennej boolowskiej, która określa czy procedura musi wywołać thunka dla pobrania wartości parametru lub czy może po prostu użyć wartości wcześniej zwróconej przez thunk. Kiedy przekazujemy parametr przez leniwe wartościowanie do innej procedury, kod wywołujący musi najpierw sprawdzić zmienną boolowską aby zobaczyć czy wartość pola jest prawidłowa. Jeśli pole boolowskie jest prawdą, kod wywołujący może po prostu zastosować dane w polu wartości. W innym przypadku, ponieważ pole wartości ma dane, przekazanie tej danej do innej procedury nie różni się od przekazania zmiennej lokalnej lub parametru wartości do innej procedury. 12.3.5 PODSUMOWANIE PRZEKAZYWANIA PARAMETRÓW
Tablica 48 Przekazywanie parametrów jako parametrów do innych procedur 12.4 PRZEKAZYWANIE PROCEDUR JAKO PARAMETRÓW
Wiele języków programowania pozwala nam przekazać nazwę procedury lub funkcji jako parametr, To pozwala kodowi wywołującemu przekazać różne działania dla wykonania wewnątrz procedury. Klasycznym przykładem jest procedura plot, która rysuje wykres jakiejś ogólnej funkcji matematycznej przekazywanej jako parametr do plot. Pascal Standardowy pozwala nam przekazać procedury i funkcje poprzez deklarowanie ich jak następuje: procedure DoCall (procedure x); begin x; end; Instrukcja DoCall(x,y,z); wywołuje DoCall, która po kolei wywołuje procedurę xyz. Przekazywanie procedury lub funkcji jako parametru może wydawać się łatwym zadaniem - przekazanie adresu funkcji lub procedury demonstruje poniższy przykład: procedure PassMe; begin writeln (‘Pass me została wywołana’); end; procedure CallPassMe (procedure x); begin x; end; begin {main} CallPassMe(PassMe); end. Kod 8086 implementujący powyższe mógłby wyglądać tak: PassMe
proc print byte ret endp
PassMe
near „PassMe została wywołana” ,cr, lf, 0
CallPassMe
proc near push bp mov bp, sp call word ptr [bp+4] pop bp ret 2 CallPassMe endp Main proc near lea bx, PassMe ;przekazanie adresu PassMe do CallPassMe push bx call CallPassMe exitPgm Main endp Dla przykładu tak prostego jak powyższy, ta technika pracuje dobrze. Jednakże nie zawsze pracuje poprawnie jeśli PassMe musi uzyskać dostęp do nie lokalnych zmiennych. Poniższy kod Pascalowski demonstruje problem, który mógłby wystąpić: program main; procedure dummy; begin end; procedure Recurse1 (i: integer; procedure x); procedure Print;
begin writeln(i); end; procedure Recurse2 (j: integer, procedure y); begin if (j = 10 then y else if (j = 5) then Recurse1 (j-1, Print) else Recurse1 (j-1,y); end; begin {Recurse1} Recurse2(i, x); End; begin {Main} Recurse1(%, dummy); end. Kod ten tworzy poniższą sekwencję wywołań:
Print będzie drukowało wartość Recurse1 zmiennej i do standardowego wyjścia. Jednakże jest kilka rekordów aktywacji obecnych na stosie które podnoszą oczywiste pytanie ,”którą kopię i Print wyświetli?” Bez podania tego, po długim namyśle możemy dojść do wniosku, że powinniśmy drukować wartość „1” ponieważ Recurse2 wywołuje Print kiedy wartość Recurse1 dla i to jeden. Zauważmy, że kiedy Recurse2 przekazuje adres Print do Recurse1, wartość i to cztery. Pascal , podobnie jak większość języków o strukturze blokowej, będzie stosował wartość i w momencie przekazywania przez Recurse2 adresu Print do Recurse1.W związku z tym, powyższy kod powinien drukować wartość cztery a nie wartość jeden. To stwarza różne implementacje problemu. W końcu Print nie może po prostu uzyskać dostępu do display’a aby skorzystać z dostępu do zmiennej globalnej i - wejście do display’a dla Recurse1 wskazuje ostatnią kopię rekordu aktywacji Recurse1, , nie wejścia zawierającego wartość cztery , która jest tym czego chcemy. Większość popularnych rozwiązań w systemowym zastosowaniu display’a jest zrobienie lokalnej kopii każdego display’a kiedykolwiek wywołujemy procedurę lub funkcję. Kiedy przekazujemy procedurę lub funkcję jako parametr, kod wywołujący kopiuje display wraz z adresem procedury lub funkcji. Jest tak dlatego, że Intelowska instrukcja enter robi kopię display’a kiedy budujemy rekord aktywacji. Jeśli przekazujemy funkcję albo procedurę jak parametry, możemy rozważyć zastosowanie łącza statycznego zamiast display’a. Kiedy stosujemy łącze statyczne musimy tylko przekazać pojedynczy wskaźnik (łącze statyczne) wraz z adresem podprogramu. Oczywiście jest więcej pracy przy dostępie do nie lokalnych zmiennych, ale musimy kopiować display’a przy każdym wywołaniu, co jest bardzo kosztowne. Poniższy kod 80x86 dostarcza implementacji powyższego kodu przy zastosowaniu łącz statycznych:
Jest kilka sposobów poprawy tego kodu. Oczywiście, ten szczególny program w rzeczywistości nie potrzebuje pielęgnacji display’a lub łącza statycznego ponieważ tylko PrintIt uzyskuje dostęp do nie lokalnych zmiennych; jednakże zignorujemy ten fakt na razie i pominiemy to. Ponieważ wiemy, że tylko PrintIt uzyskuje dostęp do zmiennych przy określonym lex poziomie i tylko program wywołuje PrintIt pośrednio, możemy przekazać wskaźnik do właściwego rekordu aktywacji; to jest to co powyższy kod robi, chociaż zachowuje również pełne łącza statyczne. Kompilatory muszą zawsze zakładać najgorszy przypadek i często generować nieefektywny kod. Jeśli przestudiujemy swoje potrzeby, możemy poprawić efektywność naszego kodu poprzez unikanie dużych kosztów pielęgnowania łączy statycznych lub kopiowania display’a. Zapamiętajmy, że thunki są specjalnymi przypadkami funkcji, które możemy wywołać pośrednio Cierpimy na ten same problemy i wady przy procedurach i funkcjach jako parametrach w związku z dostępem do nie lokalnych zmiennych. Jeśli taki podprogram uzyskuje dostęp do nie lokalnych zmiennych (a thunki prawie zawsze) wtedy musimy zachować ostrożność kiedy wywołujemy taki podprogram. Na szczęście thunki nigdy nie powodują pośredniej rekurencji( która jest odpowiedzialna za problemy w przykładzie Recurse1 /Recurse1) więc możemy użyć display’a do uzyskania dostępu do nie lokalnych zmiennych pojawiających się wewnątrz thunka. 12.5 ITERATORY Iterator jest czymś pomiędzy strukturą sterującą a funkcją. Chociaż popularne języki wysokiego poziomu niezbyt często wspierają iteratory, są one obecne w wielu językach wysokiego poziomu. Iteratory dostarczają połączenia mechanizmu stanu maszynowego / wywołania funkcji. Iteratory są również częścią pętli struktury sterującej, iterator dostarcza wartości dla zmiennej sterowania pętli na każdą iterację. Aby zrozumieć co to jest iterator rozważmy poniższą pascalowską pętlę for: for I := 1 to 10 do Kiedy uczyliście się Pascala, prawdopodobnie myśleliście, że ta instrukcja inicjalizuje i jedynką, porównuje i z 10 i wykonuje instrukcje jeśli i jest mniejsze niż lub równe 10. Po wykonaniu instrukcji, instrukcja for zwiększa i i porównuje go z 10 ponownie., powtarzając ten proces szereg razy dopóki I nie będzie większe niż 10. Podczas gdy ten opis jest semantycznie poprawny ,i rzeczywiście jest to sposób w jaki większość kompilatorów Pascala implementuje pętlę for, nie jest to jedyny punkt widzenia, który opisuje jak działa pętla for. Przypuśćmy, zamiast tego , że potraktujemy słowo zastrzeżone „to” jako operator. Operator który oczekuje dwóch parametrów (jeden i dziesięć w tym przypadku) i zwraca zakres wartości po każdym wykonaniu. To znaczy, przy pierwszym wywołaniu operator „to” zwróciłby jeden, po drugim dwa i tak dalej. Po dziesiątym wywołaniu operator „to” będzie niepoprawny i zakończy pętlę. Jest to dokładnie opis iteratora Generalnie rzecz biorąc, iterator steruje pętla Różne języki używają różnych nazw dla pętli sterowanych iteratorem, ten tekst będzie używał nazwy foreach jak następuje: foreach zmienna in iterator() do instrukcje; endfor; Zmienna jest zmienną której typ jest kompatybilny z typem zwracanym przez iterator. Iterator zwraca dwie wartości: boolowskie sukces lub porażka i wynik funkcji. Tak długo jak iterator zwraca sukces, instrukcja foreach przydziela inną wartość zwracaną do zmiennej i wykonuje instrukcje. Jeśli iterator zwraca porażka, pętla foreach się kończy i wykonuje następne instrukcje sekwencyjne po treści pętli foreach. W przypadku porażki, instrukcja foreach nie wpływa na wartość zmiennej.
Iteratory są dużo bardziej złożone niż normalne funkcje. Typowa funkcja wywołuje dwie podstawowe operacje: wywołanie i powrót. Wezwanie iteratora wywołuje cztery podstawowe operacje: 1) Pierwsze wywołanie iteratora 2) Dostarczenie wartości 3) Wznowienie iteratora 4) Zakończenie iteratora Aby zrozumieć jak działa operator, rozważmy poniższy krótki przykład z języka programowania Panacea: Range: iterator (start, stop : integer): integer; begin range; while (start <= stop) do yield start; start := start +1; endwhile; end Range; W języku Panacea iterator wywołany może pojawić się w instrukcji foreach Za wyjątkiem powyższej instrukcji yield,, każda dobrze znana z Pascala lub C++ powinna móc decydować o podstawowej logice tego iteratora Iterator w Panacei może wracać do swojego kodu wywołującego stosując jeden lub dwa oddzielne mechanizmy, może wracać do kodu wywołującego poprzez wyjście przez end Range; instrukcje lub może zwrócić wartość poprzez wykonanie instrukcji yield. Iterator powiedzie się jeśli wykonamy instrukcję yield, nie powiedzie się jeśli po prostu wróci do kodu wywołującego. Dlatego też, instrukcja foreach będzie tylko wykonywała odpowiednią instrukcję jeśli wyjdziemy z iteratora przez yield. Instrukcja foreach zakończy się jeśli po prostu wrócimy z iteratora. W powyższym przykładzie, iterator zwraca wartość start.. stop poprzez yield a potem iterator kończy się. Pętla foreach i in Range (1, 10) do Write(1); endfor; jest porównywalna do instrukcji pascalowskiej for i := 1 to 10 do write(1); Kiedy program Panacea wykonuje po raz pierwszy instrukcję foreach, czyni początkowe wywołanie iteratora. Iterator działa dopóki wykonuje yield lub zwraca. Jeśli wykonuje instrukcję yield zwraca wartość wyrażenia następującego po yield jako wynik iteratora i zawraca sukces. Jeśli po prostu zwraca, iterator zwraca porażkę i żadnego wyniku iteratora. W bieżącym przykładzie przy początkowym wywołaniu iterator zwraca sukces i wartość jeden. Zakładając pomyślny powrót (jak w bieżącym przykładzie), instrukcja foreach przypisuje wartość powrotną iteratora do zmiennej sterującej pętli i wykonuje treść pętli foreach. Po wykonaniu treści pętli, instrukcja foreach wywołuje ponownie iterator. Jednakże, tym razem instrukcja foreach wznawia iterator zamiast czynić początkowe wywołanie. Iterator wznawia kontynuowanie z pierwszą instrukcją po ostatnim wykonanym yieldzie. W przykładzie range wykonywanie będzie kontynuowane od instrukcji start := start + 1; Przy pierwszym wznowieniu, iterator Range dodaje jeden do start, tworząc wartość dwa. Dwa jest mniejsze niż dziesięć (wartość stop) wiec pętla while będzie powtórzona a iterator dostarczy wartość dwa. Proces ten będzie powtarzany tak długo dopóki iterator dostarczy dziesięć. Po wznowieniu po dostarczeniu dziesięć, iterator zwiększy start do jedenastu i zwróci, zamiast dostarczyć, ponieważ ta nowa wartość nie jest mniejsza lub równa dziesięć. Kiedy iterator range zwróci (błąd), pętla foreach zakończy się. 12.5.1 IMPLEMENTOWANIE ITERATORÓW PRZY ZASTOSOWANIU ROZWINIĘCIA IN-LINE Implementacja iteratora jest raczej złożona. Przede wszystkim rozważmy pierwszą próbę asemblerowej implementacji powyższej instrukcji foreach: push push
1 10
;zakładamy 286 lub lepszy ;a parametry przekazujemy na stos
ForLoop:
call jc puti call jnc
Range_Initial Failure
;robimy początkowe wywołanie iteratora ;C=0, 1 znaczy sukces, błąd ;zakładamy, że wynik jest w AX Range_Resume ;wznowienie iteratora ForLoop ;wyzerowane przeniesienie to sukces!
Failure: Chociaż wygląda to jak prosto zaimplementowany projekt, jest kilka kwestii do rozważenia. Po pierwsze, wywołanie Range_Resume wygląda dosyć prosto, ale nie ma stałego adresu, który adres wznowienia. Chociaż jest to prawdą, że ten przykład Range ma tylko jeden adres wznowienia, generalnie możemy mieć tak dużo instrukcji yield jak jest w iteratorze. Na przykład poniższy iterator zwraca wartości 1,2,3 i 4:
Początkowe wywołanie wykona instrukcję yield 1l Pierwsze wznowienie wykona instrukcje yield 2, drugie wznowienie wykona instrukcje yield 3; itd. Oczywiście nie ma pojedynczego adresu wznowienia, który może wyliczyć kod wywołujący. Jest parę dodatkowych szczegółów do rozpatrzenia. Po pierwsze iterator może wywołać procedurę lub funkcję. Jeśli tak procedura lub funkcja wykonuje instrukcję yield wtedy wznowienie przez instrukcję foreach kontynuuje wykonywanie wewnątrz procedury lub funkcji która wykonywała yield. Po drugie, semantyka iteratora wymaga zachowania wartości wszystkich lokalnych zmiennych i parametrów aż do zakończenia iteratora. To znaczy, zwracanie wartości nie dealokuje zmiennych lokalnych i parametrów .Podobnie, każdy adres wznowienia opuszczający stos (na przykład wywołanie procedury lub funkcji, które wykonują instrukcję yield) nie może być zagubione kiedy kawałek kodu zwraca wartość a odpowiednia instrukcja foreach wznawia iterator. Generalnie, znaczy to ,że nie możemy zastosować standardowej sekwencji wywołania i powrotu do yield lub wznowienia iteratora ponieważ musimy zachować zawartość stosu. Podczas gdy jest kilka sposobów implementacji iteratorów w asemblerze, być może najpraktyczniejszą metodą jest mieć wywołanie iteratorem sterowania pętlą poprzez iterator i powrót pętli do iteratora funkcją. Niektóre języki wysokiego poziomu wspierają iteratory. Na przykład Metaware’s Professional Pascal Compiler for PC wspiera iteratory. Stworzy on sekwencję kodu podobną do poniższej iterator OneToFour: integer begin yield 1; yield 2; yield 3; yield 4; end; i wywoła ją w programie głównym jak następuje: for i in OneToFour do writeln(i); Professional Pascal kompletnie przestawi nasz kod. Zamiast tego stworzymy funkcję asemblerową i wywołamy tą funkcję z wnętrza treści pętli, kod przekaże treść pętli for do funkcji, rozszerzając iterator in-line (podobnie jak makro) i wywołuje treść pętli for funkcji przy każdym yield. To znaczy Professional Pascal prawdopodobnie stworzy kod asemblerowy podobny do tego:
Metoda dla implementacji iteratorów jest dogodna i tworzy stosunkowo wydajny (szybki) kod .Robi to, jednakże, jest parę wad. Po pierwsze ponieważ musimy poszerzyć iterator in-line gdziekolwiek go wywołujemy, bardziej niż makro, nasz program może zwiększyć się znacznie jeśli iterator nie jest krótki i stosujemy go często. Po drugie, metoda implementacji iteratora kompletnie skrywa odpowiednią logikę kodu i czyni nasz program trudnym do odczytu i zrozumienia. 12.5.2 IMPLEMENTACJA ITERATORÓW RAMKAMI WZNOWIEŃ Poszerzanie in-line nie jest jedynym sposobem implementacji iteratorów. Jest inna metoda, która zachowuje strukturę naszego programu kosztem odrobinę bardziej złożonej implementacji. Kilka języków wysokiego poziomu, w tym Icon i CLU, używają tej implementacji. Po pierwsze potrzebujemy innej ramki stosu: ramki wznowień. Ramka wznowień zawiera dwa wejścia; adres powrotu yield (to znaczy adres następnej instrukcji po instrukcji yield) i łącze dynamiczne, które jest wskaźnikiem do rekordu aktywacji iteratora. Zazwyczaj łącze dynamiczne jest wartością rejestru bp w czasie wykonywania instrukcji yield. Wersja ta implementuje cztery części iteratora jak następuje: 1) Wywołanie instrukcji dla początkowego wywołania iteratora 2) Wywołanie instrukcji dla instrukcji yield 3) Instrukcja ret dla wznowienia działania, i 4) Instrukcja ret dla zakończenia iteratora Przede wszystkim iterator wymaga dwóch adresów powrotu zamiast jednego, jaki jest normalnie oczekiwany. Pierwszy adres powrotny jaki pojawia się na stosie jest adresem powrotnym zakończenia. Drugi adres powrotny jest tam gdzie podprogram [przekazuje sterowanie do operacji yield. Kod wywołujący musi odłożyć te dwa adresy powrotne przy początkowym wywołaniu iteratora.. Stos, na początkowym wejściu do iteratora powinien wyglądać jak na rysunku 12.8 Jako przykład rozważmy iterator Range przedstawiany wcześniej. Iterator ten wymaga dwóch parametrów, wartości początkowej i wartości końcowej: foreach i in Range (1, 10) do writeln(1);
Rysunek 12.8: Rekord aktywacji iteratora Kod który robi następujący: push push push call
początkowe wywołanie iteratora Range, tworzący stos jak ten powyżej, może być 1 10 offset ForDone Range
;odłożenie wartości parametru start ;odłożenie wartości parametru stop ;odłożenie adresu zakończenia
ForDone jest pierwszą instrukcją bezpośrednio następującą po pętli foreach, to znaczy, instrukcją do wykonania kiedy iterator zwróci porażkę. Treść pętli foreach musi zacząć się pierwszą instrukcją następującą po wywołaniu Range. Na końcu pętli foreach, zamiast skoku na początek pętli lub, lub wywołać ponownie iterator, kod ten powinien wykonać instrukcję ret. Powód stanie się jasny za chwilę. Więc implementacja powyższej instrukcji foreach może być następująca: push 1 push 10 push offset Fordone call Range mov bp, [bp] puti putcr ret ForDone: Przyznajmy, nie wygląda to wcale jak pętla. Jednakże, stosując jakieś sztuczki ze stosem, zobaczymy, że ten kod rzeczywiście powtarza treść pętli (puti i putcr) jak zaplanowano. Teraz rozważmy iterator Range,:
Rysunek 12.9 Rekord aktywacji Range Chociaż ten podprogram jest raczej krótki, jest całkiem złożony. Najlepszy sposób opisania jak ten iterator działa, jest pobranie kilku instrukcji po kolei. Pierwsze dwie instrukcje są standardową sekwencją wejść do procedury. Po wykonaniu tych instrukcji stos wygląda jak na rysunku 12.9 Następne trzy instrukcje w iteratorze Range, przy etykiecie RangeLoop, implementują test zakończenia pętli while. Kiedy parametr Start zawiera wartość większą niż parametr Stop ,sterowanie jest przekazane do etykiety RangeDone, przy której wskazywany kod zdejmuje wartość bp ze stosu, zdejmuje adres powrotny yield ze stosu (ponieważ ten kod nie będzie wracał do treści pętli iteratora) a potem wraca przez adres powrotny zakończenia, który jest bezpośrednio powyżej adresu powrotnego yield na stosie. Instrukcja powrotu również zdejmuje ze stosu dwa parametry.
Rzeczywista praca iteratora ma miejsce w treści pętli while. Instrukcje push, call i pop implementują instrukcje yield. Instrukcje push i call budują ramkę wznowień a potem zwracają sterowanie do treści pętli foreach. Instrukcja call nie wywołuje podprogramu. To ,co tu się robi to wykańcza ramkę wznowień (przez przechowanie adresu powrotnego yield w ramce wznowień) a potem zwrócenie sterownia z powrotem do treści pętli foreach poprzez skok pośredni przez adres powrotny yield odłożony na stos przez początkowe wywołanie iteratora. Po wykonaniu tego wywołania, ramka stosu wygląda tak jak na rysunku 12.9. Zauważmy również, że rejestr ax zawiera wartość powrotną dla iteratora. Ax jest dobrym miejscem do zwracania wyniku zwracanego iteratora. Bezpośrednio po powrocie yield do pętli foreach, kod musi przeładować bp oryginalną wartością przed wywołaniem iteratora. Pozwala to kodowi wywołującemu poprawnie uzyskać dostęp do parametrów i zmiennych lokalnych w swoim własnym rekordzie aktywacji zamiast rekordzie aktywacji iteratora. Ponieważ bp tak się zdarzyło, wskazuje na oryginalną wartość bp dla kodu wywołującego, wykonanie instrukcji mov bp, [bp] przeładuje bp odpowiednio. Oczywiście, w tym przykładzie przeładowanie bp nie jest konieczne ponieważ treść pętli foreach nie odnosi się do komórek pamięci z rejestru bp, ale generalnie rzecz biorąc, będziemy musieli przywrócić bp. Na końcu treści pętli foreach instrukcja ret wznawia iterator. Instrukcja ret zdejmuje adres powrotny ze stosu, który zwraca sterowanie do iteratora bezpośrednio po wywołaniu. Instrukcja w tym momencie zdejmuje bp ze stosu, zwiększa zmienną Start a potem powtarza całą pętlę.
Rysunek 12.10 Rekord wznowienia Range Oczywiście, jest to dużo pracy aby stworzyć część kodu, która po prostu powtarza pętlę 10 razy. Prosta pętla for byłaby dużo łatwiejsza i całkiem bardziej wydajniejsza niż implementacja foreach opisaną w tej sekcji. Sekcja ta stosuje iterator Range ponieważ było łatwo pokazać jak działa iterator stosując Range, a nie dlatego, że w rzeczywistości implementacja Range jako iteratora jest dobrym pomysłem. 12.9 PODSUMOWANIE Języki o strukturze blokowej ,takie jak Pascal dostarczają dostęp do nie lokalnych zmiennych spod różnych lex poziomów. Dostęp do nie lokalnych zmiennych jest złożonym zadaniem wymagającym specjalnych struktur danych takich jak łańcuch łącz statycznych lub display. Display jest prawdopodobnie najbardziej efektywnym sposobem dostępu do zmiennych nie lokalnych. 80286 i późniejsze procesory dostarczają specjalnych instrukcji enter i leave dla utrzymania listy display, ale instrukcje te są zbyt wolne dla większości powszechnych zastosowań.
Po szczegóły zajrzyj: • • • • • • • • •
„Leksykalne zagnieżdżenia, Łącza statyczne i Display’e „Zasięg” „Łącza statyczne” „Uzyskanie dostępu do zmiennych nie lokalnych stosując łącza statyczne” „Display” „Instrukcje ENTER i LEAVE 80286” „Przekazywanie zmiennych spod różnych Lex Poziomów jako Parametrów” „Przekazywanie parametrów jako parametrów do innych procedur” Przekazywanie procedur jako parametrów”
Iteratory są skrzyżowaniem funkcji i konstrukcji pętli. Są one bardzo potężną programistyczną konstrukcją dostępną w wielu językach wysokiego poziomu. Efektywna implementacja iteratorów wymaga ostrożnego manipulowania stosem w czasie wykonania .Aby zobaczyć jak implementować iteratory odczytaj poniższe sekcje: • „Iteratory” • „Implementacja Iteratorów stosując poszerzenie in-line’ • „Implementacja iteratorów z ramką wznowień”
12.10 PYTANIA 1) 2) 3) 4) 5) 6) 7) 8) 9) 10) 11) 12)
13)
Co to jest iterator? Co to jest ramka wznowień? Jak iteratory w tym rozdziale implementują wynik sukces lub porażka? Jak wygląda stos kiedy wykonujemy treść pętli sterowanej przez iterator? Co to jest łącze statyczne? Co to jest display? Opisz jak uzyskujemy dostęp do zmiennych nie lokalnych kiedy używamy łącz statycznych. Opisz jak uzyskujemy dostęp do zmiennych nie lokalnych kiedy używamy display’a Jak uzyskamy dostęp do nie lokalnych zmiennych kiedy stosujemy display utworzony przez instrukcję ENTER 80286? Namaluj obraz rekordu aktywacji dla procedury spod lex poziomu 4, który używa instrukcji ENTER dla zbudowania display’a Wyjaśnij dlaczego łącza statyczne pracują lepiej niż display kiedy przekazujemy procedury i funkcje jako parametry. Przypuśćmy, że chcemy przekazać pośrednią zmienną przez wartość-wynik używając techniki gdzie odkładamy wartość przed wywołaniem procedury a potem zdejmujemy wartość (przechowując z powrotem w zmiennej pośredniej) przy powrocie z procedury Dostarcz dwóch przykładów, jeden stosujący łącza statyczne i jeden stosujący display, które implementują przekazywanie wartość -wynik w ten sposób. Skonwertuj poniższy (pseudo) kod pascalowski na język asemblera 80x86. Zakładamy, że Pascal wspiera przekazywanie przez nazwę i przekazywanie przez leniwe wartościowanie parametrów, jak wskazuje na to poniższy kod.
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
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Ł TRZYNASTY: MS-DOS, PC-BIOS i I/O PLIKÓW Typowy system PC składa się z wielu innych składników poza CPU 80x86 i pamięci. MS-DOS i BIOS PC dostarczają połączenia pomiędzy naszą aplikacją a wykorzystywanym sprzętem. Chociaż czasami jest konieczność oprogramować sprzęt bezpośrednio, najczęściej lepiej jest pozostawić to oprogramowaniu systemowemu (MS-DOS i BIOS) wykonującemu to za nas. Co więcej dużo łatwiej jest nam wywołać po prostu podprogram wbudowany w nasz system niż pisać taki podprogram samemu. Możemy uzyskać dostęp do sprzętu IBM PC na jednym z trzech ogólnych poziomów z języka asemblera. Możemy oprogramować sprzęt bezpośrednio, możemy użyć podprogramów ROM BIOS do uzyskania dostępu do sprzętu lub możemy uczynić wywołanie MS-DOS aby uzyskać dostęp do sprzętu. Każdy z poziomów systemu ma swój własny zbiór zalet i wad. Oprogramowanie bezpośrednie sprzętu oferuje dwie zalety w stosunku do pozostałych metod.: sterowanie i wydajność. Jeśli będziemy sterowali trybami sprzętu możemy uzyskać poprawę wydajności systemu przez wykorzystanie specjalnych sztuczek sprzętowych lub innych takich, których nie potrafią podprogramy ogólnego przeznaczenia. Dla niektórych programów, takich jak edytory ekranowe (które muszą mieć szybki dostęp do karty video) bezpośredni dostęp sprzętowy jest jedynym sposobem osiągnięcia rozsądnej poprawienia poziomów. Z drugiej strony, bezpośrednie programowanie sprzętu ma również swoje wady. Edytory ekranowe, mające bezpośredni dostęp do pamięci video, mogą nie działać jeśli pojawi się nowy typ kart video w IBM PC. Dla takich programów mogą być konieczne złożone sterowniki, zwiększające ilość pracy przy tworzeniu i pielęgnacji takiego programu. Co więcej, mając napisanych kilka programów, uzyskujących dostęp do pamięci ekranowej bezpośrednio i zakładając, że IBM wyprodukował nowe, niekompatybilne rozszerzenie, będziemy musieli przepisać wszystkie nasze programy tak, aby działały z nowymi kartami video. Nasza praca stałaby się znacznie łatwiejsza gdyby IBM dostarczył, w stałej, znanej lokacji jakiś podprogramy, które wykonywałby wszystkie operacje ekranowe I/O za nas. Nasze wszystkie programy mogłyby wywoływać te podprogramy. Kiedy producent wprowadziłby na rynek nowe rozszerzenie do kart, dostarczałby nowego zbioru podprogramów dla wyświetlacza z kartą rozszerzenia. Te nowe podprogramy zastąpią stare lub je uaktualnią, tak aby wywołanie starych podprogramów wywoływało tez podprogramy nowsze. Jeśli interfejs programu jest taki sam pomiędzy dwoma zbiorami podprogramów, nasze programy będą działać z nowszymi podprogramami. IBM ma zaimplementowany taki mechanizm z oprogramowaniu systemowym. W najwyższym obszarze jedno megabajtowej przestrzeni w PC znajdują się specjalistyczne adresy dla przechowywania danych ROM. Chip pamięci ROM zawiera specjalne oprogramowanie Basic Input Output System (Podstawowy System Wejścia – Wyjścia) lub BIOS. Podprogramy BIOS dostarczają niezależnego od sprzętu interfejsu dla różnych urządzeń w systemie IBM PC. Na przykład jedną z usług BIOS jest sterowanie wyświetlaniem .Poprzez różne wywołania podprogramu video BIOS, nasz program będzie mógł wypisać znaki na ekranie bez względu na rzeczywistą , zainstalowaną kartę graficzną. Jednym z wyższych poziomów jest MS-DOS. Podczas gdy BIOS pozwala nam manipulować urządzeniami na bardzo niskim poziomie, MS-DOS dostarcza wysokopoziomowego interfejsu dla wielu urządzeń. Na przykład jeden z podprogramów BIOS pozwala nam uzyskać dostęp do dyskietki. Dzięki temu podprogramowi BIOS’a możemy odczytać lub zapisać bloki na dyskietce. Niestety BIOS nie wie nic o takich rzeczach jak pliki czy katalogi. O wie tylko o blokach. Jeśli chcesz uzyskać dostęp do pliku na dyskietce
stosując wywołanie BIOS’a musisz wiedzieć dokładnie gdzie ten plik pojawia się na powierzchni dyskietki. Z drugiej strony wywołanie MS-DOS pozwala nam działać na nazwie pliku zamiast na adresie dyskowym pliku. MS-DOS śledzi gdzie na powierzchni dyskietki są pliki i wywołując ROM BIOS odczytuje właściwy blok dla nas. Ten wysokopoziomowy interfejs znacznie redukuje ilość wysiłku jaki nasz program musi wydatkować na uzyskanie dostępu do danych na dyskietce. Celem tego rozdziału jest dostarczenie krótkiego wprowadzenia do różnych usług BIOS’a i DOS’a dostępnych dla nas. Rozdział ten nie próbuje opisywać wszystkich podprogramów lub opcji dostępnych przy każdym podprogramie. Jest kilka tekstów większych od tego, które próbują opisać tylko BIOS lub tylko DOS. Zatem każda próba dostarczenia opisu MS-DOS lub BIOS w pojedynczym tekście jest skazana na porażkę już na starcie – oba są ruchomymi celami, zmieniającymi specyfikację przy każdej nowej wersji. Więc zamiast wyjaśniać wszystko, rozdział ten po prostu będzie próbował prezentować smaczki.
13.0 WSTĘP Rozdział ten przedstawia materiał, który jest specyficzny dla PC. Informacje te o BIOS’ie i DOS’ie nie są konieczne jeśli chcesz nauczyć się programowania w języku assemblera; jednakże informacje te są ważne dla każdego chcącego pisać programy assemblerowe ,które działają pod MS-DOS na kompatybilnych maszynach z PC. W wyniku tego, większość informacji w tym rozdziale jest opcjonalna dla tych , którzy chcą nauczyć się ogólnego programowania w assemblerze. Z drugiej strony, informacje te informacje są przydatne dla tego kto chce pisać aplikacje w assemblerze na PC. Te części ,które mają prefiks „•” są niezbędne. Te części z „⊗” omawiają zaawansowane tematy, które możemy odłożyć na później. • BIOS IBM PC ⊗ Zrzut ekranu • Usługi video ⊗ Instalowanie sprzętu ⊗ Dostępność pamięci ⊗ Usługi nisko poziomowe • Złącze szeregowe We/Wy ⊗ Usługi różnorodne • Usługi klawiatury • Usługi drukowania ⊗ Działanie BASIC ⊗ Ponowne uruchomienie komputera ⊗ Zegar czasu rzeczywistego • Sekwencja wywołania MS-DOS • Funkcje znakowe MS-DOS ⊗ Polecenia napędu MS-DOS ⊗ Funkcje czasu i daty MS-DOS ⊗ Funkcje zarządzania pamięcią MS-DOS ⊗ Funkcje sterowania procesem MS-DOS • „Nowe” segregowanie wywołań MS-DOS • Otwieranie pliku • Tworzenie pliku • Zamykanie pliku • Czytanie z pliku • Zapis do pliku ⊗ Przeszukiwanie ⊗ Ustawienie adresu przekazania dysku ⊗ Znajdowanie pierwszego pliku ⊗ Znajdowanie kolejnego pliku • Usuwanie pliku • Zmiana nazwy pliku ⊗ Zmiana / pobranie atrybutu pliku ⊗ Pobranie / ustawienie daty i czasu pliku ⊗ Inne wywołania DOS
• Przykłady plików I/O • Zblokowane pliki I/O ⊗ Program Segment Prefix ⊗ Dostęp do parametrów linii poleceń ⊗ ARGC i ARGV • Pliki podprogramów I/O Standardowej Biblioteki UCR • FOPEN • FCREATE • FCLOSE • FFLUSH • FGETC • FREAD • FPUTC • FWRITE ⊗ Przekierowanie na porty I/O przez podprogramy plików I/O STDLIB 13.1 BIOS IBM PC Zamiast umieścić podprogramy BIOS’u w stałych komórkach pamięci w ROM, IBM użył dużo bardziej elastycznego podejścia przy projektowaniu BIOS’u. Dla wywołania podprogramu BIOS’a, używamy jednej z instrukcji przerwania programowego int 80x86. Instrukcja int używa następującej składni: int wartość Wartość jest jakąś liczbą z zakresu 0..255. Wykonanie instrukcji int będzie powodowała w 80x86 przekazanie sterowania do jednej z 256 różnych procedur obsługi przerwań. Tablica wektorów przerwań zaczyna się w fizycznej komórce pamięci pod adresem 0:0, przechowując adresy tych procedur obsługi przerwań. Każdy adres jest pełnym adresem segmentowym, wymagającym czterech bajtów więc jest 400h bajtów w tablicy wektorów przerwań – jeden adres segmentowy dla każdego z 256 możliwych przerwań programowych. Na przykład, int 0 przekazuje sterowane do podprogramu którego adres znajduje się w komórce 0:0, int 1 przekazuje sterowanie do podprogramu, którego adres jest pod 0:4, int 2 pod 0:8, int 3 pod 0:C a int 4 pod 0:10. Kiedy resetujemy PC jedną z pierwszych czynności jest zainicjalizowanie kilku z tych wektorów przerwań aby wskazały podprogramy usług BIOS. Później, kiedy wykonujemy odpowiednią instrukcje int, sterownie jest przekazane do właściwego kodu BIOS. Jeśli tylko wywołujemy podprogramy BIOS ( w przeciwieństwie do ich napisania) możemy zobaczyć, że instrukcje int są niczym więcej niż specjalnymi instrukcjami call. 13.2 Wprowadzenie do usług BIOS’a BIOS IBM PC używa przerwań programowych 5 i 10h..1Ah dla realizacji różnych działań. Dlatego też instrukcje int 5 i int 10h.. int 1ah dostarczają interfejsu do BIOS’a. Poniższa tablica streszcza usługi BIOS: INT 5h 10h 11h 12h 13h 14h 15h 16h 17h 18h 19h 1Ah
Funkcja Operacja zrzutu ekranowego Obsługa monitora Konfiguracja komputera określenie rozmiaru pamięci Usługa obsługi dysków Obsługa złącza szeregowego I/O Dodatkowe funkcje Obsługa klawiatury Obsługa drukarki BASIC Gorący restart systemu Usługa zegara czasu rzeczywistego
Większość z tych podprogramów wymaga różnych parametrów w rejestrach 80x86. Niektóre wymagają dodatkowych parametrów w pewnych komórkach pamięci. Poniższe sekcje opisują dokładnie działanie wielu z podprogramów BIOS’a.
13.2.1 INT 5 –WYDRUK ZAWARTOŚCI EKRANU Instrukcja Działanie BIOS Parametry
int 5h wydruk bieżącej zawartości ekranu brak
Jeśli wykonujemy instrukcję int 5h, PC wyśle dokładna kopię obrazu monitora na drukarkę jak gdybyś nacisnął klawisz PrtSc na klawiaturze. W rzeczywistości BIOS wywołuje instrukcję int 5h kiedy naciskasz PrtSc, więc te dwie operacje są dokładnie identyczne (jedynie jedna jest sterowana programowo zamiast ręcznie). Zauważmy, że 80286 i późniejsze procesory również używają int 5h dla pułapki BOUNDS. 13.2.2 INT 10h - USŁUGI VIDEO Instrukcja Działanie BIOS Parametry
int 10h Usługi video Kilka, przekazywane w rejestrach ax, bx, cx, dx i es:bp
Instrukcja int 10h wykonuje kilka pokrewnych funkcji ekranowych. Możemy zastosować ja do inicjalizacji monitora, ustawienie rozmiaru i pozycji kursora, odczyt pozycji kursora, manipulacje piórem świetlnym, odczyt i zapis aktywnej strony, przesuwanie danych na ekranie w górę i w dół, odczyt i zapis znaków, odczyt i zapis pikseli w trybie graficznym i zapis łańcucha znaków na ekranie. Wybierasz poszczególne funkcje poprze ustawienie wartości w rejestrze ah. Usługi video przedstawiają jeden z większych zbiorów dostępnych wywołań BIOS. Jest wiele różnych kart graficznych wyprodukowanych dla PC , każda z pomniejszymi zmianami i często mająca swój własny, unikalny zbiór funkcji BIOS.. BIOS odnosi się w tej dodatkowej liście do większości dostępnych funkcji, ale jak powiedziałem wcześniej, lista ta jest niekompletna i przestarzała, nie nadążając za szybkimi zmianami w technologii. Prawdopodobnie najbardziej powszechnym zastosowaniem wywołania usługi video jest podprogram wypisujący znaki: Nazwa: Parametry:
Zapis znaku na ekranie w trybie TTY ah = 0Eh, al. = kod ASCII (w trybie graficznym, bl = numer strony)
Podprogram ten zapisuje pojedynczy znak na ekranie. MS-DOS wywołuje ten podprogram do wyświetlania znaków na ekranie. Standardowa Biblioteka UCR również dostarcza wywołania , które pozwala nam zapisać znak bezpośrednio na ekranie przy użyciu wywołania BIOS. Większość podprogramów ekranowych BIOS jest napisanych kiepsko. Są one niezmiernie wolne i nie dostarczają, w pewnym sensie, poprawy funkcjonalności. Z tego powodu większość programistów (którzy potrzebują wysoko wydajnych sterowników ekranowych) kończy na napisaniu swoich własnych kodów ekranowych. Uwzględnia to szybkość przy nakładach na przenośność oprogramowania. Niestety, rzadko mammy inny wybór. Jeśli chcemy funkcjonalności zamiast szybkości, powinniśmy rozważyć zastosowanie sterownika ekranowego ANSI.SYS dostarczanego z MS-DOS. Sterownik ten dostarcza wszystkich rodzajów użytecznych usług, takich jak czyszczenie końca linii, czyszczenie końca ekranu, itp. TABLICA 49: FUNKCJE VIDEO BIOS (Lista częściowa) AH 0 1
Parametry wejściowe al = tryb wyświetlanie ch - linia początkowa cl - linia końcowa
2
bh – numer strony dh - wiersz dl – kolumna
3
bh – numer strony
Parametry wyjściowe
ch – linia początkowa cl – linia końcowa dl – kolumna dh – wiersz
Opis Ustawianie trybu wyświetlania Ustawienie kształtu i rozmiaru kursora. Wartości linii są w zakresie 0..15. Możemy ukryć kursor poprzez ustawienie ch = 20h Ustawienie pozycji kursora (x,y) na ekranie. Zazwyczaj określimy stronę zerową. BIOS zachowa oddzielny kursor dla każdego kursora. Pobranie pozycji i rozmiaru kursora
4 5
6
7
8 9
0Ah 0Bh 0Eh
Al. – numer strony
Al– liczba linii do przewinięcia Bh – atrybut ekranowy dla wyzerowanej przestrzeni cl – kolumna lewego górnego rogu okna ch – wiersz lewego górnego rogu okna dl - kolumna prawego dolnego rogu dh – wiersz prawego dolnego rogu Al.– liczba linii do przewinięcia Bh – atrybut ekranu dla czyszczonej przestrzeni cl – kolumna lewego górnego rogu okna ch – wiersz lewego górnego rogu okna dl – kolumna prawego dolnego rogu okna dh - wiersz prawego dolnego rogu okna bh – numer strony Al. – kod znaku Ah – atrybut znaku Al. – znak Bh – numer strony bl – atrybut cx – liczba znaków do zapisania Al. – znak Bh – numer strony Bh - 0 bl – kolor tła Al. – kod znaku Bh – numer strony
0Fh
Przestarzałe (obsługa pióra świetlnego) Ustawienie aktywnej strony. Zmiana aktywnej strony na stronę o wyszczególnionym numerze. Strona zerowa jest standardową stroną tekstu. Większość kart graficznych wspiera do ośmiu stron tekstu (0..7) Czyści lub przesuwa okno. Jeśli al Zawiera zero, funkcja czyści prostokątną część ekranu wyszczególnioną przez cl / ch (lewy górny róg) i dl / dh (dolny prawy róg). Jeśli al. Zawiera jakąś inną wartość, prostokątne okno będzie przewijane w dół o liczbę linii określoną w al.
Czyści lub przesuwa okno. Jeśli al zawiera zero, funkcja czyści prostokątną część ekranu wyszczególnioną przez cl / ch (lewy górny róg) i dl / dh (dolny prawy róg). Jeśli al Zawiera jakąś inną wartość, prostokątne okno będzie przewijane w dół o liczbę linii określoną w al
Odczyt bajtu kodu i atrybuty znaku ASCII spod bieżącej pozycji kursora Zapisuje cx kopii znaku i atrybutu z al./bl zaczynając spod bieżącej pozycji kursora na ekranie. Nie zmienia pozycji kursora.
Zapisuje znak z al. w bieżącej pozycji ekranu stosując istniejący atrybut. Nie zmienia pozycji kursora Ustala paletę kolorów dla wyświetlania tekstu Wypisuje znak na ekranie. Używa istniejącego atrybutu i zmienia pozycję kursora po zapisaniu ah – liczba znaków w wierszu Zwraca informacje o trybie video al. – tryb pracy bh – numer strony
Zauważmy, że jest wiele innych podfunkcji BIOS 10h. W większości te inne funkcje zajmują się trybem graficznym (BIOS jest zbyt wolny aby zajmować się grafiką, więc nie powinniśmy używać tych wywołań) i rozszerzonym cechami pewnych kart graficznych. 13.2.3 INT 11h – Konfiguracja komputera Instrukcja: Działanie BIOS: Parametry:
int 11h zwraca listę wyposażenia komputera na wejściu: żadnych, na wyjściu: AX zawierający listę wyposażenia
Przy zwracaniu z 11h, rejestr AX zawiera kodowaną bitowo listę wyposażenia komputera ,poniższymi wartościami: Bit 0 Bit 1 Bity 2 , 3 Bity 4, 5
Bity 6, 7 Bit 8 Bity 9, 10, 11 Bit 12 Bit 13 Bity 14, 15
zainstalowana dyskietka zainstalowany koprocesor matematyczny zainstalowana płyta RAM (przestarzałe) tryby video 00 – żaden 01 – 40x25 10 – 80x25 11 – 80x25 Liczba dysków twardych Obecność DMA Liczba zainstalowanych złącz szeregowych RS-232 zainstalowany port gier I/O Przyłączony szeregowy port drukarki Liczba dołączonych drukarek
Zauważmy, że tą usługa BIOS’a zaprojektowano dla oryginalnych IBM PC, z bardzo ograniczonymi możliwościami rozszerzenia sprzętu. Zwracane bity przez to wywołanie jest prawie zawsze niezrozumiałe dzisiaj. 13.2.4 INT 12h - OKREŚLENIE ROZMAIRU PAMIĘCI Instrukcja Działanie BIOS: Parametry:
int 12h Określanie rozmiaru pamięci Rozmiar pamięci jest zwracany w AX
Kiedy wrócimy do dni kiedy pecety IBM miały zainstalowane na płycie głównej 64kb pamięci, to wywołanie miało jakieś znaczenie. Jednakże dzisiejsze pecety mogą działać na pamięci 64 megabajtowej lub większych. Wyraźnie to wywołanie BIOS jest troszkę przestarzałe. Niektóre PS używają tego wywołania dla różnych celów, ale my nie możemy polegać na takim wywołaniu na maszynie. 13.2.5 INT 13h – NISKO POZIOWA OBSŁUGA DYSKÓW Instrukcja: Działanie BIOS: Parametry:
int 13h Obsługa dysków ax, es:bx, cx, dx (zobacz poniżej)
Funkcja int 13h dostarcza kilku różnych nisko poziomowych usług dyskowych: Reset systemu dyskowego, pobranie statusu dysku, odczyt sektorów dysku , zapis sektorów dysku, weryfikacja sektorów dysku, format dysku i wiele, wiele innych. Oto inny przykład podprogramu BIOS , który zmieniał się przez lata. Kiedy ten program został stworzony, 10 megabajtowy dysk twardy był uważany za duży Dzisiaj wysoko wydajne gry wymagają 20 do 30 MB pamięci AH 0
1
Parametry wejściowe dl – urządzenie (0..7fh to dyskietka,80h..ffh dysk twardy dl – urządzenie (jak powyżej)
Parametry wyjściowe ah – stan (0 i flaga przeniesienie wyzerowana jeśli brak błędu, kod błędu jeśli błąd) Ah – 0 Al. – stan poprzedniej operacji dyskowej
Opis Reset określonego napędu dyskowego. Zresetowanie dysku twardego również resetuje dyskietkę To wywołanie zwraca poniższe wartości stanu w al: 0 – żadnego błędu 1 – niepoprawne polecenie 2 – nie znaleziono znacznika adresu 3 – chroniony zapis dysku 4 – nie można znaleźć sektora 5 – błąd resetu 6 – nośnik usunięty 7 – zły parametr tablicy
8 – przepełnienie DMA 9 – operacja DMA przekroczyła 64k granicę 10 – niedozwolona flaga sektora 11 – niedozwolona flaga ścieżki 12 – niedozwolony nośnik 13 - niepoprawna liczba sektorów 14 – napotkano znacznik adresu danej sterującej 15 – błąd DMA 16 – błąd danej CRC 17 – błąd prawidłowej danej ECC 32 – niedozwolony sterownik dysku 64 – błąd ustawienia głowic dysku 128 – błąd czasu oczekiwania 170 – dysk nie odczytany 187 – błąd niezdefiniowany 204 – błąd zapisu 224 – błąd stanu 225 – niedozwolony odczyt 2
3 4 0Ch 0Dh
Al. – liczba sektorów do odczytu Es:bx – adres bufora cl – bity 0..5 sektor # cl – bity 6/7 ścieżka bitów 8 i9 ch – ścieżka bitów 0..7 dl - urządzenie (jak powyżej) dh – bity 0..5 głowica # dh – bity 6 i 7 : ścieżka bitów 10 i 11 Tak samo jak (2) powyżej
ah - zwraca stan al. – błąd sekwencji długości przeniesienia – 0: sukces, 1: błąd
Odczytuje określoną liczbę z 512 bajtowych sektorów z dysku. Dana musi być 64 kilobajtowa lub mniejsza.
Tak samo jak (2) powyżej
Tak samo jak (2) powyżej z wyjątkiem tego, że nie poturbujemy bufora. Tak samo jak (4) powyżej z wyjątkiem tego, że nie potrzebujemy sektor # Dl – urządzenie # (80h lub 81h)
Tak samo jak (2) powyżej
Zapisuje określoną liczbę z 512 bajtowych sektorów na dysk. Długość danej nie może przekraczać 64 kilobajtów Weryfikacja danych w określonej liczbie 512 bajtowych sektorów na dysku.
Tak samo jak (4) powyżej
Wysłanie głowicy dyskowej do określonej ścieżki na dysku.
ah – zwraca stan Reset sterownika twardego dysku przeniesienia – 0: żadnego błędu 1: błąd
Tablica 50: Popularne wywołania dyskowych subsystemów BIOS Notka: zobacz właściwą dokumentację BIOS’a po dodatkową informację o wsparcie BIOSa dla dyskowych podsystemów 13.2.6 INT 14h – OBSŁUGA ZŁĄCZA SZERGOWEGO I/O Instrukcja: Działanie BIOS: Parametry:
int 14h dostęp do szeregowych portów komunikacyjnych ax, dx
IBM BIOS utrzymuje cztery różne szeregowe porty komunikacyjne (sprzętowo utrzymuje osiem). Ogólnie, większość PC ma jeden lub dwa zainstalowane porty szeregowe (COM1: i COM2) Int 14h posiada cztery pod funkcje – inicjalizacja, przesłanie znaku, odbiór znaku i status. Dla wszystkich czterech usług, numer
portu szeregowego (wartość w zakresie 0..3) znajduje się w rejestrze dx (0 = COM1, 1 = COM2 itd.) Int 14h oczekuje i zwraca inne dane w rejestrze al. lub ax. 13.2.6.1 AH = 0: INICJALIZACJA PORTU SZEREGOWEGO podfunkca zero inicjalizuje port szeregowy. Wywołanie to pozwala nam ustawić szybkosć transmisji danych, wybrać tryb parzystości, wybrać liczbę bitów stopu i liczbę bitów przekazywanych poprzez linie szerwgową. Parametry te są wszystkie określone przez wartosci w rejestrze al. uzywając następujacego kodowania bitowego: Bity 5..7
3..4
2
Funkcja Wybór szybkości transmisji 000 – 110 bodów 001 – 150 010 – 300 011 – 600 100 – 1200 101 – 2400 110 – 4800 111 – 9600 Wybór parzystości 00 – żadnej parzystości 01 – nieparzyste 10 – żadnej parzystości 11 – parzyste Bity stopu 0 – jeden bit stopu 1 – dwa bity stopu
0..1
Rozmiar znaku 10 – 7 bitów 11 – 8 bitów
Chociaż standardowy sprzętowy port szeregowy wspiera 19 600 bodów, niektóre BIOS’y mogą nie wspierać tej szybkości . Przykład: Inicjalizacja COM1: 2400 bodów, żadnej parzystości, osiem bitów danych i dwa bity stopu – mov ah, 0 ;opcod inicjalizujący mov al., 10100111b ;parametr danych mov dx, 0 ;port COM1: int 14h po wywołaniu kodu inicjalizującego, status portu szeregowego jest zwracany w ax (zobacz poniżej Status Portu Szeregowego, ah = 3) 13.2.6.2 AH=1: PRZESYŁANIE ZNAKU DO PORTU SZEREGOWEGO Funkcja ta przesyła znak z rejestru al do portu szeregowego wyszczególnionego w rejestrze dx. Jeśli ah zawiera zero wtedy znak zostanie przesłany właściwie. Jeśli bit 7 ah zawiera 1, wtedy pojawi się jakiś rodzaj błędu. Pozostałe siedem bitów zawiera wszystkie stany błędów zwracanych przez wywołanie GetStatus z wyjątkiem limitu czasu błędu (który jest zwracany w bicie siedem) Jeśli został zakomunikowany błąd powinniśmy użyć podfunkcji trzy do ustawienia rzeczywistej wartości błędu ze sprzętowego portu szeregowego. Przykład: Przesyłanie znaku do portu COM1: mov dx, 0 ;wybranie COM1: mov al., ‘a’ ;Znak do przesłania mov ah, 1 ;przesłanie opcodu int 14h test ah, 80h ;sprawdzenie błędu jnz SerialError
Funkcja ta będzie czekała dopóki port szeregowy nie skończy przesyłania ostatniego znaku a potem przechowa znak w rejestrze przesyłowym. 13.2.6.3 AH=2 : ODBIÓR ZNAKU Z PORTU SZEREGOWEGO Podfunkcja dwa jest używana do odczytu znaku z portu szeregowego. Na wejściu dx zawiera numer portu szeregowego. Na wyjściu al. zawiera znak odczytany z portu szeregowego i bit siedem ah zawierający status błędu. Kiedy ten podprogram jest wywołany, nie wraca do kodu wywołującego dopóki znak jest odbierany z portu szeregowego. Przykład: Odczyt znaku z portu COM1: mov mov int test jnz
dx, 0 ah, 2 14h ah, 80 SerialError
;wybór COM1: ;opcod odbioru ;sprawdzenie błędu
< Odebrany znak jest teraz w AL.> 13.2.6.4 AH=3: STATUS PORTU SZEREGOWEGO To wywołanie zwraca informację o statusie portu szeregowego wliczając w to czy błąd wystąpił czy nie, czy znak został odebrany do bufora odbiorczego, czy bufor przesyłowy jest pusty i inne różne użyteczne informacje. Na wejściu tego podprogramu rejestr dx zawiera numer portu szeregowego, na wyjściu rejestr ax zawiera poniższe wartości: AX: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Znaczenie bitu Błąd limitu czasu Pusty rejestr przesuwu Pusty rejestr bufora Błąd wykrycia przerwy Błąd ramki Błąd parzystości Błąd przepełnienia Dostępne dane Wykryty sygnał na linii Żądanie odbioru Gotowość do odbioru Gotowość do nadawania Zmiana znacznika sygnału na linii Zmiana znacznika żądania odbioru Zmiana znacznika gotowości do odbioru Zmiana znacznika gotowości do nadawania
Jest parę użytecznych bitów , nie odnoszących się do błędów, zwracanych w tej informacji o statusie. Jeśli bit dostępnych danych jest ustawiony (bit 8), wtedy port szeregowy odbiera dane a my powinniśmy je odczytać z portu szeregowego. Pusty rejestr bufora (bit 13) mówi nam, czy operacja przesyłu zostanie opóźniona podczas oczekiwania na bieżący znak do przesłania lub czy następny znak będzie bezpośrednio przesłany. Przez testowanie tych dwóch bitów możemy wykonać inne operacje podczas oczekiwania na to, że rejestr bufora stanie się dostępny lub ,że rejestr nadawczy zawiera znak. Jeśli interesujesz się komunikacją szeregową powinieneś nabyć kopię Joe Campbell’s C Programmer’s Guide to Serial Communications. Chociaż napisana specjalnie dla programistów C, książka ta zawiera wiele pożytecznych informacji dla programistów pracujących w różnych językach programowania. 13.2.7 INT 15h - DODATKOWE FUNKCJE Pierwotnie int 15h dostarczał usług do odczytu i zapisu kaset. Prawie bezpośrednio, każdy zdawał sobie sprawę, że kasety to historia, więc IBM zaczął używać int 15h dla wielu innych usług. Dzisiaj, int 15h jest używane dla szerokiego wachlarza usług, wliczając w to dostęp do pamięci rozszerzonej, odczyt karty
rozszerzeń gier / joystik i wielu, wielu innych działań. Z wyjątkiem wywołania joystika, większość z tych usług sięga poza zakres tego tekstu. 13.2.8 INT 16h - OBSŁUGA KLAWIAITURY Instrukcja: Działanie BIOS: Parametry:
int 16h Odczyt klawisza, test dla klawisza lub pobranie statusu klawiatury al.
IBM PC BIOS dostarcza kilku funkcji wywołujących działających z klawiaturą. Aczkolwiek jest wiele podprogramów PC BIOS, liczba funkcji zwiększa się co roku. Sekcja ta opisuje trzy wywołania, które były dostępne na oryginalnym IBM PC. 13.2.8.1 AH=0: ODCZUYT KLAWISZA Z KLAWIATURY Jeśli int 16h jest wywoływane z ah równym zero, BIOS nie zwraca sterowania do programu wywołującego dopóki klawisz jest na początku bufora. klawaitury Przy zwracaniu, al. zawiera kod ASCII dla odczytywanego klawisza z bufora a ah zawiera kod klawisza klawiatury. Kody klawiszy klawiatury są opisane w dodatkach. Pewne klawisze na klawiaturze PC nie mają odpowiadających kodów ASCII. Klawisze funkcyjne Home, PgUp, End PgDn, klawisze strzałek i klawisze Alt są tego dobrym przykładem. Kiedy jest naciśnięty taki klawisz, int 16h zwraca zero w al. i kod klawisza w ah. Dlatego też kiedy kod ASCII równa się zero musimy sprawdzić rejestr ah dla określenia, który klawisz został naciśnięty. Zauważmy, że odczytując klawisz z klawiatury stosując BIOS int 16h nie wywołujemy echa naciśniętego klawisza na ekranie. Musimy wywołać putc lub użyć int 10h do wydruku znaku kiedy go odczytamy jeśli chcemy potwierdzić go na monitorze Przykład: Odczyt sekwencji naciśniętych klawiszy z klawiatury do czasu naciśnięcia klawisza ENTER ReadLoop : mov ah, 0 ;Odczyt opcodu klawisza int 16h cmp al., 0 ;Funkcja specjalna jz ReadLoop ;Jeśli tak ,nie potwierdzaj tego naciśnięcia putc cmp al., 0dh ;powrót karetki (ENTER) jne ReadLoop 13.2.8.2 AH=1: SPRAWDZENIE CZY KLAWISZ JEST DOSTĘPNY SPOD KLAWIATURY Ta szczególna podfunkcja int 16h pozwala nam sprawdzić aby stwierdzić czy klawisz jest dostępny w systemowym buforze klawiatury. Nawet jeśli klawisz nie jest dostępny, sterowanie jest zwracane (natychmiast!) do kodu wywołującego. Z tym wywołaniem możemy czasami monitorować klawiaturę aby zobaczyć czy klawisz jest dostępny i kontynuować działanie jeśli klawisz nie był naciśnięty (w przeciwieństwie do zamrożenia komputera do czasu naciśnięcia klawisza) Nie ma żadnych parametrów wejściowych do tej funkcji. Przy zwracaniu, flaga zera będzie wyzerowana jeśli klawisz jest dostępny, ustawiona jeśli nie ma żadnego klawisza w buforze klawiatury. Jeśli klawisz jest dostępny, wtedy ax będzie zawierał kod i kod ASCII dla tego klawisza. Jednakże, funkcja ta nie usuwa tego klawisza z bufora klawiatury. Podfunkcja #0 musi być zastosowana do usunięcia znaków. Poniższy przykład demonstruje jak zbudować losowy generator liczb używając funkcji testowania klawiatury : Przykład: generowanie liczb losowych podczas oczekiwania na naciśnięcie klawisza: ;Po pierwsze czyścimy bufor klawiatury ze wszystkich znaków ClrBuffer:
BufferIsClear:
mov int 16h jz mov int jmp mov
ah, 1
;czy klawisz jest dostępny?
BufferIsClear ah, 0 16h ClrBuffer cx, 0
;Jeśli nie przerywamy opróżnianie bufora ; Opróżniamy ten znak z bufora i próbujemy dalej ;inicjalizacja liczby „ losowej”
GenRandom:
inc cx mov ah, 1 ;zobacz czy klawisz jest jeszcze dostępny int 16h jz GenRandom xor cl, ch mov ah, 0 ;Odczyt znaku z bufora int 16h ;Liczba losowa jest teraz w Cl, klawisz naciśnięty przez użytkownika w AX Podczas oczekiwania na klawisz podprogram ten stale zwiększa rejestr cx. Ponieważ człowiek nie może odpowiedzieć bardzo szybko (przynajmniej pod względem mikrosekund) rejestr cl będzie przepełniany wiele razy, nawet przy najszybszych piszących na klawiaturze. W wyniku tego, cl będzie zawierał losowe wartości ponieważ użytkownik nie będzie mógł sterować tym (przynajmnie około 2 ms) kiedy klawisz jest naciśnięty. 13.2.8.3 AH=2: PYTANIE O STAN KLAWIATURY Funkcja ta zwraca stan różnych klawiszy klawiatury PC w rejestrze al. Wartości zwracane są następujące: Bit 7 6 5 4 3 2 1 0
Znaczenie Stan INSERT (przełączany po naciśnięciu klawisza INS) CAPS LOCK (1 = Caps lock włączony) NUM LOCK (1 = Num lock włączony) SCROLL LOCK (1 = Scroll lock włączony) ALT (1 = klawisz Alt aktualnie wciśnięty) CTRL (1 = klawisz Ctrl aktualnie wciśnięty) Lewy SHIFT (1 = klawisz lewy Shift wciśnięty) Prawy SHIFT (1 – klawisz prawy Shift wciśnięty)
Z powodu błędu w kodzie BIOS’a , bity te tylko odzwierciedlają bieżący status tych klawiszy, niekoniecznie za to odzwierciedlają status tych klawiszy, kiedy następny klawisz odczytany z bufora będzie naciśnięty. Żeby zapewnić, że te bity statusu odpowiadają stanowi tych klawiszy kiedy kod znaku jest czytany z bufora klawiatury, musimy wyczyścić bufor, poczekać na naciśnięcie klawisza a potem bezpośrednio sprawdzić status klawiatury. 13.2.9 INT 17h – USŁUGI DRUKARKI Instrukcja: Działanie BIOS: Parametry:
int 17h Drukuje dane i testuje stan drukarki ax, dx
Int 17h steruje złączem równoległym drukarki na IBM PC w taki sam sposób jak int 14h steruje portem szeregowym. Ponieważ programowanie portu równoległego jest znacznie łatwiejsze niż sterowanie portem szeregowym, używając podprogramu int 17h jest nieco łatwiejsze niż używanie podprogramu int 14h. Int 17h dostarcza trzech podfunkcji, wyszczególnionych przez wartości w rejestrze ah. Te podfunkcje to: 0 – wydruk znaku z rejestru AL. 1 – inicjalizacja drukarki 2 – zwracanie statusu drukarki Każda z tych funkcji jest opisana w poniższych sekcjach. Podobnie jak usługi dla portu szeregowego, usługi dla portu drukarki pozwalają nam wyszczególnić, który z portów drukarki zainstalowanych w systemie życzymy sobie użyć (LPT1:, LPT2: lub LPT3: ). Wartość w rejestrze dx (0..2) określa który port drukarki będzie używany. Jedna końcowa uwaga – pod DOS’em jest możliwe przekierowanie wszystkich danych wyjściowych drukarki do portu szeregowego. Jest to całkiem użyteczne jeśli używamy drukarki szeregowej . Usługi drukarki BIOS tylko przekazują do kontrolera drukarki równoległej. Jeśli potrzebujemy wysłać dane do drukarki szeregowej używając BIOS musimy użyć int 14h do przesłania danych do portu szeregowego.
13.2.9.1 AH=0: WYDRUK ZNAKU Jeśli ah wynosi zero kiedy wywołujemy int 17h, wtedy BIOS będzie drukował znaki z rejestru al. Dokładnie jak kod znaku w rejestrze al. jest traktowany całkowicie zależy od sterownika drukarki jakiego używamy. Większość drukarek jednak. Uwzględnia drukowalny zbiór znaków ASCII i kilka znaków sterujących. Wiele drukarek również będzie drukowało wszystkie symbole w zbiorze znaków IBM/ASCII (wliczając Europejskie, kreślenie lini i inne specjalne symbole. Wiele drukarek traktuje znaki sterujące (zwłaszcza sekwencję ESC) w kompletnie różny sposób. Dlatego też, jeśli zamierzasz drukować całkiem inne znaki niż standardowe znaki ASCII, bądź przygotowany, że twoje programy mogą nie działać na innych niż te na których rozwijasz swoje programy. Po powrocie z podprogramu zero podfunkcji int 17h, rejestr ah zawiera bieżący status. Wartości w rzeczywistości zwracane są opisane w sekcji o podfunkcji numer dwa. 13.2.9.2 AH=1: INICJALIZACJA DRUKARKI Wykonując to wywołanie wysyłamy impuls elektryczny do drukarki mówiący o inicjalizacji. Przy zwracaniu, rejestr ah zawiera status drukarki jaki pokazuje funkcja numer dwa. 13.2.9.3 AH=2: ZWRACANIE STATUSU DRUKARKI Wywołując tą funkcję sprawdzamy status drukarki i zwracamy go w rejestrze ah. Wartości zwracane to: AH: 7 6 5 4 3 2 1 0
Znaczenie bitów 1 = drukarka zajęta, 0 = drukarka nie zajęta 1 = potwierdzenie z drukarki 1 = brak papieru 1 = drukarka włączona 1 = błąd I/O Nie używane Nie używane Przekroczenie czasu
Potwierdzenie z drukarki jest , w gruncie rzeczy, zbędnym sygnałem (ponieważ drukarka zajęta / nie zajęta daje nam taką samą informację). Tak długo jak drukarka jest zajęta, nie zaakceptuje dodatkowej danej. Dlatego też, wywołanie funkcji wydruku znaku (ah=0) będzie wykonywane z opóźnieniem. Sygnał braku papieru wystąpi zawsze kiedy drukarka wykryje brak papieru. Sygnał ten nie jest implementowany na wielu kontrolerach drukarek. W takich kontrolerach jest zawsze zaprogramowane zero logiczne (nawet jeśli drukarce brakuje papieru). Dlatego też pojawiające się zero na tej pozycji bitu, nie zawsze gwarantuje, że w drukarce mamy papier. Jednak patrząc tu, zdecydowanie znaczy, że w drukarce brakuje papieru. Bit włączenia drukarki tak długo zawiera jeden, jak drukarka jest włączona. Jeśli użytkownik wyłączy drukarkę, bit ten zostanie wyzerowany. Bit błędu I/O zawiera jeden jeśli wystąpił jakiś ogólny błąd I/O. Bit przekroczenia czasu zawiera jeden, jeśli podprogram BIOS oczekiwał dłuższy okres czasu na drukarkę aby stała się „nie zajęta”, a drukarka wciąż pozostaje „zajęta” Zauważmy ,że inne urządzenia peryferyjne (inne niż drukarka ) również są przyłączane do portu równoległego, często w dodatku do portu drukarki. Niektóre z tych urządzeń używają sygnałów lini błąd/status do zwracania danych do PC. Oprogramowanie sterujące takimi urządzeniami często przejmuje podprogram int 17h (przez technikę której pomówimy później) i zawsze zwraca stan „żadnego błędu” lub :przekroczenie czasu” jeśli wystąpi błąd na urządzeniu drukującym. Dlatego też powinniśmy zadbać aby nie uzależnić się w dużym stopniu od zmian tych sygnałów, kiedy wywołujemy int 17h BIOS’a. 13.2.10 INT 18h – URUCHOMIENIE BASICA Instrukcja: Działanie BIOS: Parametry:
int 18h aktywacja ROM BASIC Żadne
Wykonując int 18h, aktywujemy interpretera ROM BASIC w IBM PC. Jednak, nie powinieneś używać tego mechanizmu uruchamiania BASIC’a ponieważ wiele kompatybilnych PC nie ma BASIC’a w ROM i wynik wykonania int 18h jest zdefiniowany. 13.2.11 INT 19h – GORĄCY RESTART SYSTEMU Instrukcja: Działanie BIOS: Parametry:
19h Restart systemu żadne
Wykonanie tego przerwania daje taki sam efekt jak naciśniecie klawiszy Ctrl+Alt+Del na klawiaturze. Z oczywistych powodów to przerwanie powinno być stosowane ostrożnie. 13.2.12 INT 1Ah – ZEGAR CZASU RZECZYWISTEGO Instrukcja: Działanie BIOS: Parametry:
int 1ah usługi czasu rzeczywistego ax, cx, dx
Są dwie usługi dostarczone przez podprogram BIOS – odczyt zegara i ustawianie zegara. Zegar czasu rzeczywistego PC utrzymuje licznik, który zlicza liczby co 1/18 sekundy, które zdarzyły się od północy. Kiedy odczytujemy zegar, pobieramy liczbę „taktów”, które wystąpiły. Kiedy ustawiamy zegar, określamy liczbę „taktów”, które wystąpiły od północy. Jak zwykle poszczególne usługi są wybierane przez wartość w rejestrze ah. 13.2.12.1 AH=1: ODCZYT ZEGARA CZASU RZECZYWISTEGO Jeśli ah = 0, wtedy int 1ah zwraca 33 bitową wartość w al:cx:dx jak następuje: Rejestr dx cx al.
Wartość zwracana Mniej znaczące słowo licznika zegara bardziej znaczące słowo licznika zegara zero jeśli zegar nie działał dłużej niż 24 godziny W innym wypadku wartość nie zerowa 32 bitowa wartość w cx:dx przedstawia liczbę z okresem 55 milisekund, która upłynęła od północy.
13.2.12.2 AH=1: USTAWIANIE ZEGARA CZASU RZECZYWSITEGO Wywołanie to pozwala nam ustawić bieżącą wartość czasu systemowego, cx:dx zawiera bieżącą liczbę (z 55 milisekundowym zwiększaniem) od ostatniej północy. Cx zawiera bardziej znaczące słowo, dx zawiera mniej znaczące słowo. 13.3 WPROWADZENIE DO MS-DOS™ MS-DOS dostarcza wszystkich podstawowych funkcji zarządzających plikami i urządzeniami wymaganymi przez większość programów użytkowych uruchamianych na IBM PC. MS-DOS obsługuje pliki I/O, znaki I/O, zarządzanie pamięcią i inne różne funkcje w stosunkowo spójny sposób. Jeśli poważnie myślisz o pisaniu programów na PC, będziesz musiał przyjaźnie nastawić się do MS-DOS’a. Tytuł tej sekcji to „ Wprowadzenie do MS-DOS”. I jest to dokładnie to co znaczy. Nie ma sposobu aby MS-DOS został poznany kompletnie w jednym rozdziale. Danych jest wiele różnych książek, które zajmują się wyłącznie tym problemem. Microsoft napisał 1600 stronicową książkę na ten temat i nawet on nie wyczerpał tematu w pełni. Wszystko to prowadzi do prób wymigania się. Nie ma mowy aby ten temat nie mógł być potraktowany powierzchownie w pojedynczym rozdziale. Jeśli poważnie myślisz o pisaniu programów w języku assemblera na PC, musisz połączyć ten tekst z paroma innymi. Dodatkowe książki na temat MS-DOS to: MSDOS Programmer’s Reference (również zwany MS-DOS Technical Reference Manual). Peter Norton’s Programmer’s Guide to the IBM PC, The MS-DOS Encyclopedia i MS-DOS Developper’s Guide. To czywiście jest tylko część listy książek dostępnych dzisiaj. Bez wątpienia MS-DOS Technical Reference Guide jest najważniejszym tekstem ,który warto przejrzeć jest to oficjalny opis wywołań MS-DOS i parametrów. MS-DOS ma długa i barwną historię. Przez cały czas swojego życia przechodził kilka zmian, każda po to aby był lepszy niż poprzedni. Początków MS-DOS trzeba szukać w systemie operacyjnym CP/M.-80,
napisanym dla mikroprocesora Intel 8080. Faktycznie, MS-DOS v1.0 był niczym więcej niż klonem CP/M-80 dla mikroprocesora Intel 8088. Niestety, zdolność CP/M. do obsługi plików była straszna, mówiąc krótko. Dlatego też, DOS przewyższył CP/M. Nowe zdolności do obsługi plików , zgodne z Xenix i Unix, dodane do DOS stworzyły MS-DOS v. 2.0. Dodatkowe funkcje zostały dodane , do późniejszych wersji MS-DOS. Nawet wprowadzenie OS/2 i Windows NT ,Microsoft pracuje nad polepszeniem MS-DOS, który może pojawić się nawet w późniejszych wersjach. Każda nowa cech dodana do DOS wprowadza nowe funkcje DOS, które zachowują całą funkcjonalność poprzednich wersji DOS. Kiedy Microsoft napisał podprogramy obsługi plików w wersji dwa, nie zmienił starych funkcji, po prostu zostały dodane nowe. Zachowanie oprogramowania kompatybilnego z programami, które działały ze starszymi wersjami DOS spowodowało, że DOS posiada dwa zbiory usług plikowych o identycznej funkcjonalności ale zupełnie nie kompatybilnych. W tym rozdziale skoncentrujemy się na małym podzbiorze dostępnych poleceń DOS. Zupełnie zignorujemy przestarzałe polecenia, które zostały powiększone o nowsze, lepsze polecenia w późniejszych wersjach DOS. Co więcej pominiemy opisywanie tych funkcji , które mają małe zastosowanie w codziennym programowaniu. Dla kompletnego szczegółowego opisu poleceń nie opisanych w tym rozdziale powinieneś nabyć jedną z wyżej wymienionych książek. 13.3.1 SEKWENCJA WYWOŁANIA MS-DOS MS-DOS jest wywoływany poprzez instrukcję int 21h. Po wybraniu właściwej funkcji DOS, ładujesz do rejestru ah numer funkcji przed instrukcją int 21h. Większość funkcji DOS wymaga również innych parametrów. Ogólnie te inne parametry są przekazywane w zbiorze rejestrów CPU. Określone parametry będą omawiane wraz z każdą funkcją. Chyba, że MS-DOS zwraca jakąś określoną wartość w rejestrze, wszystkie rejestry CPU są zachowywane po wywołaniu DOS. 13.3.2 FUNKCJKE MS-DOS ZORIENTOWANE ZNAKOWO DOS dostarcza 12 funkcji zorientowanych znakowo I/O. Większość z nich zajmuje się zapisem i odczytem danych do / z klawiatury, monitora, portu szeregowego i portu drukarki. Wszystkie te funkcje mają swoje odpowiedniki w usługach BIOS. Faktycznie, DOS zazwyczaj wywołuje stosowną funkcję BIOS wykonującą operację I/O. Jednak wskutek tego, że DOS przekierowywuje I/O i sterowniki urządzeń, funkcje te nie zawsze wywołują podprogramy BIOS. Dlatego nie powinniśmy wywoływać podprogramów BIOS (zamiast DOS) po prostu dlatego, że DOS kończy wywoływanie BIOS. Robiąc tak możemy zapobiec temu aby nasz program pracował z pewnymi wspieranymi przez DOS urządzeniami. Z wyjątkiem funkcji siedem, wszystkie funkcje zorientowane znakowo sprawdzają urządzenia wejściowe konsoli (klawiatura) przez control-C Jeśli użytkownik naciśnie control-C, DOS wykona instrukcję int 23h. Zazwyczaj instrukcja ta powoduje, że program jest przerywany a sterowanie zwracane do DOS. Zapamiętajmy to, kiedy będziemy korzystać z tych funkcji. Microsoft uznał te funkcje za przestarzałe i nie gwarantował, że będą dostarczone w przyszłych wersjach DOS. Więc ujmiemy te 12 funkcji w pigułce. Zauważ ,że Standardowa Biblioteka UCR dostarcza funkcjonalności wielu z tych funkcji i posiada właściwe funkcje DOS, które nie są przestarzałe. Numer funkcji (AH) 1
2 3 4 5 6
Parametry wejściowe
Parametry wyjściowe al. – znak odczytany
Opis Odczyt pojedynczego znaku z klawiatury i wyświetlenie wypisanego znaku na ekranie dl - znak do wypisania Wypisuje pojedynczy znak na monitorze al. – znak do odczytu Odczyt pojedynczego znaku z portu szeregowego dl – znak do wypisania Zapis pojedynczego znaku do portu wyjściowego dl – znak do wypisania Zapis pojedynczego znaku do drukarki dl – znak wyjściowy 9jeśli nie al. – znak do odczytu (jeśli Na wejściu, jeśli dl 0FFh dl = 0FFh) zawiera 0FFh, funkcja ta próbuje odczytać znak z klawiatury. Jeśli znak jest
dostępny, zwracane jest wyzerowana flaga zera i znak w al. Jeśli żaden znak nie jest dostępny, zwracana jest ustawiona flaga zera. Jeśli dl zawiera wartość inną niż 0FFh, podprogram wysyła znak na ekran. Ten podprogram nie sprawdza ctrl-C 7 al. – znak odczytany Odczytuje znak z klawiatury. Nie daje echa znaku na wyświetlacz. Ta funkcja nie robi sprawdzenia dla ctrl-C 8 al. –znak odczytany Podobnie jak funkcja 7, z wyjątkiem sprawdzania dla ctrl-C 9 ds.:dx – wskaźnik do łańcucha Funkcja ta wyświetla zakończonego „$” znaki spod lokacji ds.:dx w górę (ale nie zawiera) kończącego znaku „$” 0Ah ds.:dx – wskaźnik do bufora Funkcja odczytuje linię wejściowego tekstu z klawiatury i przechowuje ją w buforze wejściowym pod ds.:dx. Pierwszy bajt buforu musi zawierać liczbę pomiędzy 0 a 255, która zawiera maksymalną liczbę dozwolonych znaków w buforze wejściowym. Podprogram przechowuje rzeczywista liczbę odczytanych znaków w drugim bajcie. Rzeczywiste znaki wejściowe zaczynają się w trzecim bajcie 0Bh al. – status ( 0 = nie Określa czy znak jest gotowe, 0FFh = gotowe) dostępny z klawiatury 0Ch al. – Dosowy opcod 0,1, 6, 7 al. - znak wejściowy jeśli Ta funkcja opróżnia lub 8 opcod 1,6,7 lub 8 systemowy bufor klawiatury a potem wykonuje polecenia DOS określone w rejestrze al. (jeśli al. = 0, brak akcji) Tablica 51: Funkcje DOS zorientowane znakowo Funkcje 1,2,3,4,5,9 i 0Ah są przestarzałe i nie powinniśmy ich używać. Zamiast nich używajmy DOS owych funkcji plików I/O 9opcody 3Fh i 40h).
13.3.3 POLECENIA NAPĘDÓW MS-DOS MS-DOS dostarcza kilka poleceń, które pozwalają nam na ustawienie domyślnego napędu, określenie który napęd jest domyślny i wykonanie innych działań. Poniższa tabela pokazuje te funkcje. Numer funkcji (AH Parametry wejściowe Parametry wyjściowe Opis 0Dh Opróżnianie wszystkich bufor ów plikowych na dysku. Ogólnie
0Eh
dl – numer napędu
19h 1Ah
al. – numer domyślnego napędu ds.:dx - adres DTA
1Bh
1Ch
1Fh
Al. – numer napędu logicznego
al. – sektory /klastry cx – bajty / sektor dx – # klastrów ds.:bx – wskazuje bajt deskryptora nośnika
Dl – numer nośnika
Zobacz powyżej
al. –zawiera 0FFH jeśli błąd, 0 jeśli nie ma błędu ds.:bx – wskaźnik do DPB
wywoływana przez ctrl-C lub sekcję kodu, który musi zagwarantować zgodność plików, ponieważ może wystąpić błąd Ustawia domyślny napęd według określonych wartości (0=A,1=B,2=C itd.). Zwraca liczbę logicznych napędów, chociaż może nie być ciągłości od 0 –al. Zwraca numer bieżącego domyślnego napędu (0=A,1=B,2=C, itp.) Ustawia adres, którego MS-DOS używa dla przestarzałych plików I/O i poleceń Find First / Find Next Zwraca informację o dysku w domyślnym nośniku. Zobacz również funkcję 36h. Typowe wartości dla deskryptora nośnika: 0F0h - 3.5” 0F8h – dysk twardy 0F9h – 720k 3.5” lub 1.2m. 5.25” 0FAh – 320k 5.25” 0FBh - 640k 3.5” 0FCh – 180k 5.25” 0FDh – 360k 5.25” 0FEh – 160k 5.25” 0FFh – 320k 5.25” To samo co powyżej z wyjątkiem tego, ze możemy określić numer nośnika w rejestrze dl (0=domyślnie, 1=A,2=B,3=C, itd.). Ustawia domyślny DPB: jeśli z powodzeniem, funkcja ta zawraca wskaźnik do poniższych struktur; Dysk (bajt) – Numer nośnika (0-A, 1-B itd. Jednostka (bajt0 –liczba jednostkowa dla napędu Rozmiar Sektora (słowo) - # bajt / sektor ClusterMask (bajt) – sektor/ klaster minus jeden Cluster2 (bajt) – 2 klaster /sektor. PierwszyFAT(słwo) – adres sektora gdzie zaczyna się FAT LicznikFAT (bajt) – liczba FAT’ów RootEntries(słowo) – liczba wejść w katalogu głównym PierwszySektor (słowo) – pierwszy sektor pierwszego klastra MaxCluster(słwo) – liczba klastrów na dysku plus jeden RozmiarFAT (słowo) – rozmiar FAT w sektorach DirSector (dword) – adres nagłówka urządzenia Nośnik(bajt) – bajt deskryptora nośnika PierwszyDostęp (bajt) – ustawia dostęp do urządzenia NextDPB (dword) link do następnego
DPB na liście NextFree (słowo) ostatni zaalokowany klaster FreeCnt (słowo) – liczba wolnych klastrów 2EH
2Fh 32h
33h 36h
54h
al. – sygnalizator weryfikacji (0 = nie zweryfikowane, 1= zweryfikowane
Włącza lub wyłącza weryfikację po zapisie. Zazwyczaj wyłącza ponieważ jest wolną operacją, ale może ją włączyć ,kiedy wykonuje się krytyczne I/.O es:bx wskaźnik do DTA To samo co 1Fh
Zwraca wskaźnik do bieżącego DTA w es:bx dl – numer napędu To samo co funkcja 1Fh z wyjątkiem tego, ze możesz pobrać określony numer napędu (0 = domyślny, 1=A,2=B, 3=C itd.). al. –05 (kod podfunkcji) Zwraca numer urządzenia używanego do inicjowania DOS (1=A, 2=B itd.) dl – numer urządzenia ax – sektory / klastry Raportuje o ilości wolnej przestrzeni. bx – dostępne klastry Funkcja ta wypiera funkcje 1Bh i 1Ch, cx – bajty / sektor które wspierają dyski do 32MB. Ta dx – klastry całkowite funkcja działa z dużymi dyskami,. Możemy obliczyć wielkość wolnego miejsca ( w bajtach) poprzez bx*cx*ax. Jeśli wystąpi błąd, funkcja zwróci 0FFFFh w ax. al. – weryfikacja stanu Zwraca bieżący stan sygnalizatora weryfikacji (al. = 0 jeśli wyłączony, al. =1 jeśli włączony) Tablica 52: Funkcje DOS dla napędów dyskowych
13.3.4 „PRZESTARZAŁE” FUNKCJE KATALOGUJĄCE MS-DOS Funkcje DOS 0Fh – 18h, 1Eh, 20-24h i 26h – 29h są zapomnianymi funkcjami od czasu CP/M.-80. Generalnie nie powinniśmy sobie zawracać głowy tymi funkcjami ponieważ MS-DOS v.2.0 i późniejsze dostarczają dużo lepszych sposobów do wykonania tych operacji niż te funkcje.
13.3.5 FUNKCJE DOS DOTYCZĄCE DATY I CZASU Funkcje daty i czasu MS-DOS zwracają bieżącą datę i czas w oparciu o wewnętrzne wartości podtrzymywane przez zegar czasu rzeczywistego (RTC). Funkcje dostarczone przez DOS zawierają odczyt i ustawianie daty i czasu. Te wartości daty i czasu są używane do elektronicznego oznaczania daty i czasu plików , które są tworzone na dysku. Dlatego też jeśli zmieniamy datę lub czas ,zapamiętajmy, że ma to wpływ na pliki, stworzone od tego czasu. Zauważmy, że Standardowa Biblioteka UCR również dostarcza zbioru funkcji daty i czasu, które w wielu przypadkach, są dużo łatwiejsze do zastosowania niż funkcje DOS. Numer funkcji (AH) 2Ah
2Bh
Parametry wejściowe
cx – rok (1980 – 2099) dh – miesiąc (1 –12)
Parametry wyjściowe Opis al. – dzień (0= Niedz., 1 = Zwraca aktualną datę w Poniedx. itd.) zegarze systemowym cx – rok dh – miesiąc (1 = Stycz, 2 = luty itd.) dl – dzień miesiąca (1 – 31) Ustawia aktualną datę w zegarze systemowym
dl – dzień (1-31) 2Ch
2Dh
ch – godziny (24) cl – minuty dh – sekundy dl – setne sekundy ch – godziny cl – minuty dh – sekundy dl – setne sekundy
Odczytuje aktualny czas zegara systemowego. Zauważ, że pole setnej sekundy ma rozdzielczość 1/18 sekundy Ustawia bieżący czas w zegarze systemowym.
Tablica 53; Funkcje daty i czasu
13.3.6 FUNKCJE ZARZĄDZANIA PAMIĘCIĄ MS-DOS dostarcza trzech funkcji zarządzania pamięcią – przydzielanie , zwalnianie i zmiana rozmiaru (weryfikacja). Dla większości programów te trzy funkcje przydzielania pamięci nie są używane. Kiedy DOS wykonuje program, dostarcza całej dostępnej pamięci od startu tego programu do końca RAM w procesie wykonywania. Każda próba alokowania pamięci bez zwrócenia nie używanej pamięci do systemu wywoła błąd „niewystarczającej pamięci”. Skomplikowane programy, kończące się i pozostające w pamięci, uruchamiające inne programy lub wykonujące złożone zadania zarządzania pamięcią mogą wymagać użycia tych trzech funkcji zarządzania pamięcią . Ogólnie tego typu programy bezpośrednio zwalniają całą nie wykorzystaną pamięć a potem zaczynają przydzielać i zwalniać pamięć taką jak jest potrzebna. Ponieważ są to złożone funkcje, nie powinny być stosowane, chyba ,ż mamy dl nich specjalne zastosowanie. Niewłaściwe stosowanie tych poleceń może spowodować, że zagubimy pamięć w systemie, którą można będzie odzyskać tylko przez restart systemu. Każda z poniższych funkcji zwraca stan błędu we fladze przeniesienia. Jeśli przeniesienie jest wyzerowane przy zwrocie, wtedy operacja zakończyła się sukcesem, Jeśli flaga ta jest ustawiona, kiedy DOS powraca, wtedy rejestr ax zawiera jeden z poniższych kodów błędu: 7 – Zniszczony blok sterujący pamięci 8 – Niewystarczająca ilość pamięci 9 – Nieprawidłowy blok adresowy pamięci Dodatkowo odnotujmy, że o tych błędach będziemy mówili w stosownym momencie 13.3.6.1 PRZYDZIELANIE PAMIĘCI Funkcja (ah): Parametry wejściowe: Parametry wyjściowe:
48h bx – żądany rozmiar bloku ( w paragrafach) Jeśli nie ma błędu (przeniesienie wyzerowane) Ax:0 wskazuje na przydzielony blok pamięci Jeśli wystąpił błąd (przeniesienie ustawione) bx – maksymalny możliwy przydzielony rozmiar ax – kod błędu (7 lub 8)
Funkcja ta jest stosowana do przydzielania bloku pamięci. Na wejściu do DOS, bx zawiera rozmiar żądanego bloku w paragrafach (grupa 16 bitowa). Na wyjściu, zakładając brak błędu, rejestr ax zawiera adres segmentowy początku przydzielonego bloku. Jeśli błąd wystąpi ,blok nie jest przydzielany a rejestr ax zawraca zawarty kod błędu. Jeśli wystąpi brak dostatecznej ilości pamięci, rejestr bx zwróci maksymalną liczbę rzeczywiście dostępnych paragrafów. 13.3.6.2 ZWALNIANIE PAMIĘCI Funkcja (ah): Parametry wejściowe: Parametry wyjściowe
49h es:0 - Adres segmentowy zwalnianego bloku Jeśli przeniesienie jest ustawione, ax zawiera kod błędu (7,9) Funkcja ta używa zwalnianej pamięci do przydzielenia jej przez powyższą funkcję 48h. Rejestr es nie może zawierać przypadkowego adresu pamięci. Musi zawierać wartość zwracaną przez funkcję przydzielania
pamięci. Nie możemy użyć tej funkcji do zwalniania części bloku. Do tej operacji służy zmodyfikowana funkcja przydzielania. 13.3.6.3 ZMODYFIKOWANA FUNKCJA PRZYDZIELANIA PAMIĘCI Funkcja (ah): Parametry wejściowe:
4Ah es:0 – adres bloku do modyfikacji bx – rozmiar nowego bloku Parametry wyjściowe: jeśli przeniesienie jest ustawione, wtedy ax zawiera kod błędu 7,8 lub 9 bx zawiera maksymalny możliwy rozmiar (jeśli błąd 8) Ta funkcja jest używana do zmiany rozmiaru przydzielanego bloku. Na wejściu es musi zawierać adres segmentowy przydzielanego bloku zwracany przez funkcję przydzielania pamięci. Bx musi zawierać nowy rozmiar tego bloku w paragrafach. Możemy prawie zawsze zredukować rozmiar, nie możemy normalnie zwiększać rozmiaru tego bloku jeśli inne bloki zostały przydzielone po bloku , na którym dokonujemy modyfikacji. Pamiętajmy o tym, kiedy stosujemy tą funkcję. 13.3.6.4 ZAAWANSOWANE FUNKCJE ZARZĄDZANIA PAMIĘCIĄ Opcod 58h MS-DOS pozwala programiście modyfikować strategię alokowania pamięci MS-DOS i sterować blokiem wyższej pamięci (UMB) Są cztery podfunkcje do wykonania tego, wartości tych podfunkcji pojawiają się w rejestrze al. Poniższa tablica opisuje te funkcje: Numer funkcji (AH) Parametry wejściowe Parametry wyjściowe Opis 58h al. - 0 ax – strategia Zwraca bieżącą strategię alokacji w ax. (zobacz tablicę poniżej po szczegóły) 58h al. – 1 bx - strategia Ustawia strategię alokacji MS-DOS na wartość określoną w bx (zobacz poniższą tabele po szczegóły) 58h al. - 2 al.- flaga łączenia Zwraca prawda/fałsz (1/0) w al. do określenia czy program może alokować pamięć w wyższym bloku pamięci 58h al. – 3 bx –flaga Łączy lub rozłącza obszar wyższej łączenia (0 = żadnego pamięci. Kiedy jest połączona, łączenia, 1 =łączenie aplikacja może alokować pamięć w OK) UMB (używając zwykłej funkcji alokującej DOS Tablica 54: Funkcje zaawansowanego zarządzania pamięcią Wartość 0
Nazwa Pierwsze Niższe Dopasowanie
1
Najlepsze Niższe Dopasowanie
2
Ostatnie Niższe Dopasowanie
80h
Pierwsze Wyższe Dopasowanie
81h
Najlepsze Wyższe Dopasowanie
82h
Ostatnie Wyższe Dopasowanie
Opis Przeszukuje pamięć konwencjonalna po pierwszy wolny blok pamięci dostatecznie duży aby spełnić żądanie alokacji Przeszukuje pamięć konwencjonalną po najmniejszy blok spełniający żądanie alokacji. Przeszukuje pamięć konwencjonalna po najwyższy adres zstępujący dla pierwszego dość dużego bloku spełniającego żądanie. Przeszukuje wyższą pamięć, potem konwencjonalną, po pierwszy dostępny blok, który może spełnić żądanie alokacji Przeszukuje wyższą pamięć, potem konwencjonalną, po najmniejszy blok, dosyć duży który może spełnić żądanie alokacji. Przeszukuje wyższą pamięć od adresów wyższych do niższych, potem pamięć konwencjonalną od adresów wyższych do niższych szukając pierwszego bloku dość
40h 41h 42h
Pierwsze Wyższe (tylko) Dopasowanie Najlepsze Wyższe (tylko) Dopasowanie Ostatnie Wyższe (tylko ) Dopasowanie
aby spełnić żądanie alokacji Przeszukuje tylko wyższą pamięć po pierwszy blok, dosyć duży aby spełnić żądanie alokacji Przeszukuje tylko wyższą pamięć po najmniejszy blok, dosyć duży aby spełnić żądanie alokacji Przeszukuje tylko wyższą pamięć od końca pamięci w dół, po pierwszy blok dosyć duży aby spełnić żądanie alokacji
Tablica 55: Strategie Alokacji Pamięci Te różne strategie alokacji pamięci mogą mieć wpływ na wydajność systemu. Po analizę różnych strategii zarządzania pamięcią sięgnij po dobry teoretyczny tekst o systemach operacyjnych. 13.3.7 FUNKCJE MS-DOS STERUJĄCE PROCESEM DOS dostarcza kilku usług zajmujących się ładowaniem, wykonywaniem i kończeniem programów. Wiele z tych funkcji zostało uznanych za przestarzałe w późniejszych wersjach DOS’a Tu mamy trzy funkcje ogólnie zajmujące się – zakończeniem programu, wstrzymaniem i pozostaniem w pamięci i wykonaniem programu. Te trzy funkcje będą omówione w poniższych sekcjach. 13.3.7.1 ZAKOŃCZENIE WYKONYWANIA PROGRAMU Funkcja : Parametry wejściowe: Parametry wyjściowe:
4Ch al. – kod powrotu Nie zwraca nic do programu
Jest to wywołanie funkcji, która zazwyczaj jest używana do kończenia programu. Zwraca sterowanie do procesu wywołującego (normalnie, ale nie koniecznie, DOS) Zwracany kod może być przekazany do procesu wywołującego w rejestrze al. Ten kod powrotu może być sprawdzony przez DOS’owskie polecenie „kod powrotu IF ERRORLEVEL” w pliku wsadowym. Wszystkie pliki otwarte przez bieżący proces będą automatycznie zamknięte przy zakończeniu programu. Zauważmy, że funkcja „ExitPgm” Standardowej Biblioteki UCR jest po prostu makrem, które wykonuje to szczególne wywołanie DOS. Jest to zwykły sposób zwracania sterowania do MS-DOS lub innego programu, który działa jako aktywna aplikacja. 13.3.7.2 ZAKOŃCZENIE, ALE POZOSTAWANIE W PAMIĘCIA Funkcja(9ah): Parametry wejściowe: Parametry wyjściowe:
31h al. – kod powrotu dx - rozmiar pamięci , w paragrafach nie zwracane do programu
Ta funkcja również powoduje zakończenie wykonywania programu, ale po powrocie do DOS , pamięć używana w procesie nie jest zwracana do DOS, do jego wolnego obszaru pamięci. Zasadniczo program pozostaje w pamięci. Programy, które pozostają rezydentne z pamięci po zwróceniu do DOS, często są nazywane TSR’ami (terminate and stay resident - oprogramowanie rezydentne). Kiedy jest wykonywane to polecenie, dx zawiera liczbę paragrafów pamięci pozostawionych w pamięci. Wartość ta jest odmierzana od początku „program segment prefix” ,segment oznacza początek pliku w pamięci. Adres PSP jest przekazywany do programu w rejestrze ds., kiedy program jest wykonywany po raz pierwszy. Będziemy musieli zachować tą wartość jeśli jest to program TSR. Programy które są TSR, muszą dostarczyć kilka mechanizmów do restartowania. Ponieważ wracają do DOS, nie mogą być restartowane normalnie. Większość TSR’ów wstawiana jest do jednego z wektorów przerwań (takich jak klawiatura, drukarka lub szeregowy wektor przerwań), żeby zostać restartowanymi gdy tylko wystąpi zdarzenie powiązane ze sprzętem (takim jak naciśnięcie klawisza) .Jest to jak programy „pop – up” taki jak SmartKey. Ogólnie programy TSR są pop – up lub specjalnymi sterownikami urządzeń. Mechanizm TSR dostarcza dogodnego sposobu do ładowania własnych podprogramów dla zamiany lub zwiększenia podprogramów BIOS. Nasz program załadowany do pamięci, wstawiany jest do właściwego wektora przerwań, żeby wskazywał wewnętrzny program obsługi przerwań naszego programu a potem zakończył i pozostał w
pamięci. Teraz , kiedy jest wykonywana właściwa instrukcja przerwania, będzie wywoływany nasz kod zamiast podprogram BIOS. Jest daleko więcej szczegółów dotyczących TSR’ów zawierających różne kwestie. DOS poruszy te kwestie, i jak działają przerwania tutaj. Dodatkowe szczegóły pojawia się w późniejszych rozdziałach. 13.3.7.3 WYKONANIE PROGRAMU Funkcja (ah): Parametry wejściowe:
Parametry wyjściowe:
40h ds.:dx – wskaźnik do ścieżki dostępu programu do wykonania es:bx – wskaźnik do bloku parametrów al. –0 = załadowanie i wykonanie, 1 = tylko załadowanie, 3 =załadowanie nakładki Jeśli przeniesienie jest ustawione, ax zawiera jeden z poniższych kodów: 1 – niewłaściwa funkcja 2 – plik nie znaleziony 5 – dostęp zastrzeżony 8 – nie wystarczająca pamięć 10 – złe otoczenie 11 – zły format
Funkcja wykonania (exec) jest niezmiernie złożoną, ale jednocześnie, bardzo użyteczną operacją. Polecenie to pozwala nam załadować i wykonać program z dysku. Na wejściu do funkcji exec , rejestry ds.:dx zawierają wskaźnik do łańcucha zakończonego zerem z nazwą pliku będącego ładowanym lub wykonywanym, es:bx wskazuje na blok parametrów a al. zawiera zero lub jeden w zależności czy chcemy załadować i wykonać program, czy tylko załadować go do pamięci. Przy powrocie, jeśli przeniesienie jest wyzerowane, wtedy DOS właściwie wykona polecenie. Jeśli flaga przeniesienia jest ustawiona, wtedy DOS napotka błąd podczas wykonania polecenia. Parametr nazwy pliku może być pełną ścieżką dostępu wliczając w to informacje o napędzie i podkatalogach. „B:\DIR1\DIR2\MYPGM.EXE” jest doskonałą ,poprawną nazwą pliku (pamiętajmy jednak, że musi być zakończona zerem) Adres segmentowy tej ścieżki dostępu jest przekazany w rejestrach ds.:dx Rejestry es:bx wskazują blok parametrów dla funkcji exec. Ten blok parametrów przybiera trzy różne formy w zależności od tego czy .program jest załadowany i wykonywany (al. = 0),tylko załadowany do pamięci (al. =1) lub załadowany jako nakładka (al. = 3) Jeśli al. = 0, wtedy funkcja exec ładuje i wykonuje program. W tym przypadku rejestry es:bx wskazuje blok parametrów zawierający poniższe wartości: Offset Opis 0 2 6 0Ah
Wartość słowa zawierająca adres segmentowy domyślnego otoczenia (zazwyczaj jest ustawione na zero jeśli standardowym środowiskiem jest DOS) Wskaźnik podwójnego słowa zawierający adres segmentowy łańcucha lini poleceń. Wskaźnik podwójnego słowa do domyślnego FCB pod adresem 5Ch Wskaźnik podwójnego słowa do domyślnego FCB pod adresem 6Ch
Obszar środowiska jest zbiorem łańcuchów zawierających domyślne ścieżki dostępu i inne informacje (informacje te są dostarczane przez DOS przy użyciu poleceń PATH, SET i innych). Jeśli te parametry wejściowe zawierają zero, wtedy exec przekaże standardowe środowisko DOS do nowej procedury. Jeśli nie – zero, wtedy ten parametr zawiera adres segmentowy bloku środowiska, do którego twoje działanie przekazuje program do wykonania. Generalnie powinniśmy przechowywać zero pod tym adresem. Wskaźnik do łańcucha poleceń powinien zawierać adres segmentowy długości prefiksu łańcucha, który również jest zakończony przez znak powrotu karetki (znak powrotu karetki nie pojawia się w długości łańcucha). Łańcuch ten odpowiada danej, która jest zazwyczaj wypisywana po nazwie programu w lini poleceń DOS. Na przykład, jeśli wykonujemy automatyczne linkowanie, możemy przekazać polecenie łańcuchowe w poniższej formie: CmdStr byte 16, „MyPgm+Podprogram /m.”, 0dh Druga pozycja w bloku parametrów musi zawierać adres segmentowy tego łańcucha
Trzecia i czwarta pozycja w bloku parametrów wskazują domyślne FCB’y. FCB’y są używane przez przestarzałe polecenia zapisu danych do pliku, wiec są rzadko używane w nowszych aplikacjach. Ponieważ struktury danych tych dwóch wskaźników wskazują na rzadkie stosowanie, możemy zaznaczyć je jako grupę 20 zer. Przykład: Formatowanie dyskietki w napędzie A: używamy polecenia FORMAT.EXE mov ah, 4Bh mov al., 0 mov dx, seg PathName mov ds., dx lea dx, PathName mov bx, seg ParmBlock mov es, bx lea bx, ParmBlock int 21h PathName byte ‘C:\DOS\FORMAT.EXE’, 0 ParmBlock word 0 ;domyślne środowisko dword CmdLine ;łańcuch lini poleceń dword Dummy, Dummy ;fikcyjny FCB CmdLine Dummy
byte byte
3, ‘ A;’, 0dh 20 dup (?)
Wersje wcześniejsze MS-DOS niż 3.0 nie zabezpieczały żadnych rejestrów z wyjątkiem cs:ip kiedy wykonywana byłą funkcja exec. W szczególności, ss:sp nie był zabezpieczony. Jeśli używasz DOS’a v 2.x lub wcześniejszego, musisz użyć poniższego kodu: ;Przykład: Formatowanie dyskietki w napędzie A: przy użyciu polecenia FORMAT.EXE < odłożenie wszystkich rejestrów jakie musisz zachować> mov cs :SS_save, ss ;zachowanie SS:SP w komórce mov cs: SP_save, sp ;do której mamy dostęp później mov ah, 4Bh ;opcod DOS’owy EXEC mov al., 0 ;załadowanie i wykonanie mov dx, seg PathName ;pobranie nazwy pliku do DS.:DX mov ds., dx lea dx, PathName mov bx, seg ParamBlock ;wskazuje ES:BX jako blok parametrów mov es, bx lea bx, ParamBlock int 21h mov ss, cs: SS_save ;przywrócenie SS:SP z lokacji mov sp, cs: SP_save SS_Save word ? SP_Save word ? PathName byte ‘C:\DOS\FORMAT.EXE’, 0 ParmBlock word 0 ;domyślne środowisko dword CmdLine ;łańcuch lini poleceń dword Dummy, Dummy; Dummy ;FCB’y CmdLine byte 3, ‘A:’, 0dh Dummy byte 20 dup (?)
SS_Save i SP_Save muszą być zadeklarowane wewnątrz naszego segmentu kodu. Jedynie zmienne mogą być zadeklarowane gdziekolwiek. Polecenie exec automatycznie alokuje pamięć dla programu będącego wykonywanym .Jeśli nie będziemy mieli wolnej nieużywanej pamięci przed wykonaniem tego polecenia, możemy otrzymać błąd niewystarczającej pamięci/ Dlatego też powinniśmy używać poleceń dealokacji pamięci DOS dla uwolnienia nieużywanej pamięci przed przystąpieniem do używania polecenia exec. Jeśli al. = 1 kiedy wykonuje się funkcja exec, DOS załaduje określony plik, ale go nie wykona. Funkcja ta jest ogólnie używana do ładowania programu do wykonania w pamięci ale przekazuje kodowi wywołującemu sterownie i pozwala mu zacząć ten kod. Kiedy jest dokonywane wywołanie tej funkcji, es:bx wskazuje na jeden z poniższych bloków parametrów: Offset 0 2 6 0Ah 0Eh 12h
Opis Wartość słowa zawierającego adres segmentowy bloku środowiska dla nowego procesu. Jeśli chcemy używać macierzystego procesu bloku środowiska, ustawiamy to słowo na zero. Wskaźnik dword polecenia stopu do dla tej operacji. Polecenie stopu jest łańcuchem lini poleceń, który będzie się pojawiał pod lokacją PSP:80 Adres domyślnego FCB #1. Dla większości programów, powinien wskazywać blok 20 zer (chyba , że uruchamiamy program przy użyciu FCB) Adres domyślnego FCB #2. Również powinien wskazywać blok 20 zer Wartość SS:SP . Musimy załadować te cztery bajty do SS i SP przed zastartowaniem aplikacji Wartość CS:IP. Te cztery bajty zawierają adres startowy programu
Pola SSSP i CSIP są wartościami wyjściowymi. DOS wypełnia te pola i zwraca je w ładowanej strukturze. Wszystkie inne pola są wejściowe i musimy je wypełnić przed wywołaniem funkcji exec z al. =1. Kiedy wykonujemy polecenie exec z al. =3, DOS po prostu .ładuje nakładkę do pamięci. Nakładki ogólnie składają się z pojedynczego segmentu kodu, który zawiera jakieś funkcje które chcemy wykonać. Ponieważ nie tworzymy nowego procesu, blok parametrów dla tego ładowanego typu jest dużo prostsze niż dla pozostałych dwóch typów operacji ładowania, Na wejściu es:bx musi wskazywać poniższy blok parametrów w pamięci: Offset 0 2
Opis Wartość słowa zawierającą adres segmentu, pod który zostanie załadowany plik. Plik będzie załadowany pod offsetem zero wewnątrz tego segmentu Wartość słowa zawierająca stałą przemieszczenia dla tego pliku
W odróżnieniu od funkcji ładowania i wykonania, funkcja nakładki automatycznie nie alokuje pamięci dla pliku będącego ładowanym .Nasz program musi alokować wystarczająco pamięci a potem przekazać adres tego bloku pamięci do polecenia exec (przez powyższy blok parametrów). Do polecenia exec jest przekazywany tylko adres segmentu, offset jest zawsze zakładany jako zero. Stała przemieszczenia również powinna zawierać adres segmentu dla plików „.EXE”. Dla plików „.COM” parametr stałej przemieszczenia powinien być zero. Polecenie nakładki jest bardziej użyteczne przy ładowaniu nakładek z dysku do pamięci. Nakładka jest segmentem kodu, który rezyduje na dysku dopóki program w rzeczywistości będzie musiał wykonać ten kod. Wtedy kod jest ładowany do pamięci i wykonywany. Nakładki mogą zredukować ilość pamięci programu poprzez przyjęcie założenia o ponownym użyciu tej samej części pamięci dla różnych procedur nakładki( rzecz jasna , tylko jedna taka procedura może być aktywna w tym samym czasie. Poprzez umieszczenie rzadko używanego kodu i kodu inicjalizującego w plikach nakładkowych, możemy zredukować ilość pamięci używanej przez pliki naszego programu. Jednak, słowo uwagi, obsługa nakładek jest bardzo skomplikowanym zadaniem. Nie jest to coś do czego początkujący programiści asemblerowi zabierają się w pierwszej kolejności. Kiedy ładujemy plik do pamięci 9jako przeciwieństwo ładowania i wykonania pliku), DOS nie zmienia wszystkich rejestrów, więc nie musimy specjalnie dbać o przechowanie ss:sp i innych rejestrów. „Encyklopedia MS-DOS’ zawiera doskonały opis użycia funkcji exec.
13.3.8 „NOWE” FUNKCJE ZAPISUJĄCE DANE MS-DOS
Poczynając od DOS v .2.0 Microsoft wprowadził zbiór plików działających procedur, które (ostatecznie) uzyskują dostęp do plików dyskowych znośnych pod MS-DOS. Nie tylko znośnych, ale i łatwych do użycia! Poniższe sekcje opisują użycie tych poleceń dostępu do plików na dysku. Polecenia plikowe, które zajmują się nazwami plików (Create, Open, Delete, Rename i inne) przekazują adres ścieżki odstępu zakończonej zerem. Te , które w rzeczywistości otwierają plik (Create i Open) zwracają jako wynik logiczny numer pliku (zakładając, oczywiście, że nie wystąpił błąd ) Ten logiczny numer pliku jest używany z innymi funkcjami (read, write, seek, close, itp.) aby zwiększyć dostęp do otwartego pliku. Pod tym względem, logiczny numer pliku jest podobny do zmiennej plikowej w Pascalu. Rozważmy poniższy kod Microsoft/Turbo Pascal: program DemoFile; var F:TEXT; begin assign (f, ‘FileName.TXT’); rewrite(f); writeln (f, ‘Hello there’); close (f); end. Zmienna plikowa f jest używana w tym przykładzie Pascalowskim w taki sam sposób w jaki logiczny numer pliku jest używany w programie asemblerowym – polepszenie dostępu do pliku, który został stworzony w programie. Wszystkie poniższe polecenia DOS’a zwracają stan błędu we fladze przeniesienia. Jeśli flaga przeniesienia jest wyzerowana kiedy DOS wraca do naszego programu, wtedy operacja kończy się powodzeniem. Jeśli flaga przeniesienia jest ustawiona przed powrotem, wtedy wystąpi jakiś rodzaj błędu a jego numer zawiera rejestr AX. Rzeczywiste błędy wartości zwracanych będziemy omawiali wraz z każdą funkcją w poniższych sekcjach. 13.3.8.1 OTWIERANIE PLIKU Funkcja (ah): Parametry wejściowe:
Parametry wyjściowe:
3Dh al. – wartość dostępu do pliku 0 – plik otwarty do odczytu 1 – plik otwarty do zapisu 2 – plik otwarty do odczytu i zapisu ds.:dx - wskazuje łańcuch zakończony zerem zawierający nazwę pliku Jeśli przeniesienie jest ustawione, ax zawiera jeden z poniższych kodów błędów: 2 – plik nie znaleziony 4 – zbyt wiele otwartych plików 5 – dostęp zastrzeżony 12 – dostęp nieprawidłowy Jeśli przeniesienie jest wyzerowane, ax zawiera wartość logicznego numeru pliku powiązanego przez DOS
Plik musi być otwarty przed tym zanim uzyskamy do niego dostęp. Polecenie open, otwiera plik, który już istnieje. Powoduje to, że jest to trochę podobne do procedury pascalowskiej Reset. Próbując otworzyć plik , który nie istnieje, wywołamy błąd. Przykład: lea dx, FileName ;zakładamy, że DS. wskazuje segment mov ah, 3dh ; z nazwą pliku mov al., 0 ;otwieramy do czytania int 21h jc OpenErroe mov FileHandle, ax jeśli wystąpi błąd podczas otwierania pliku, plik nie zostanie otwarty. Powinniśmy zawsze sprawdzać błędy przed wykonaniem DOS’owskiego polecenia open, ponieważ kontynuowanie działania na pliku, który nie został poprawnie otwarty doprowadzi do katastrofalnych konsekwencji. Dokładnie jak operowanie błędem otwarcia jest naszym zadaniem, przynajmniej powinniśmy wydrukować informację o błędzie aby dąć sposobność użytkownikowi do określenia inne nazwy pliku.
Jeśli polecenie open zakończy się bez wygenerowania błędu, DOS zwróci logiczny numer pliku dla tego pliku w rejestrze ax. Zazwyczaj powinniśmy zachować tą wartość gdzieś, aby można było jej użyć, kiedy będziemy chcieli uzyskać dostęp do pliku później. 13.3.8.2 TWORZENIE PLIKU Funkcja (ah) Parametry wejściowe: Parametry wyjściowe:
3Ch ds.:dx – adres ścieżki dostępu zakończonej zerem cx – Atrybut pliku Jeśli przeniesienie jest ustawione, ax zawiera jeden z poniższych kodów błędów: 3 – Ścieżka nie znaleziona 4 – zbyt wiele otwartych plików 5 – dostęp zastrzeżony Jeśli przeniesienie jest wyzerowane, ax zwraca zawartość logicznego numeru pliku
Tworzy nowy plik. Tak jak przy poleceniu OPEN, ds.:dx wskazuje łańcuch zakończony zerem zawierający nazwę pliku. Ponieważ funkcja ta tworzy nowy plik, DOS zakłada, że otworzyliśmy plik tylko do zapisu. Innym parametrem, przekazany w cx, jest początkowe ustawienie atrybutu pliku. Najmniej znaczące sześć bitów cx zawiera poniższe wartości: Bit Znaczenie 0 Plik jest plikiem tylko do odczytu 1 Plik jest plikiem ukrytym 2 Plik jest plikiem systemowym 3 Plik jest identyfikatorem dysku 4 Plik jest podkatalogiem 5 Plik był archiwizowany Generalnie nie powinniśmy ustawiać żadnego z tych bitów. Większość zwykłych plików powinno być stworzonych z atrybutem zero. Dlatego też rejestr cx powinien być załadowany wartością zero przed wywołaniem funkcji Create. Na wyjściu flaga przeniesienia jest ustawiona, jeśli wystąpi błąd. Błąd „Ścieżka nie znaleziona” wymaga dodatkowych wyjaśnień. Błąd ten jest generowany nie wtedy kiedy plik nie został znaleziony (co może występować cały czas ponieważ polecenie to jest zazwyczaj używane do tworzenia nowych plików), ale wtedy kiedy podkatalog w ścieżce dostępu nie może być znaleziony. Jeśli flaga przeniesienia jest wyzerowana, kiedy DOS wraca do naszego programu, wtedy plik będzie poprawnie otwarty a rejestr ax zawiera logiczny numer pliku dla tego pliku. 13.3.8.3 ZAMYKANIE PLIKU Funkcja: Parametry wejściowe: Parametry wyjściowe:
3Eh bx – logiczny numer pliku Jeśli flaga przeniesienia jest ustawiona, ax zawiera 6, jedyny możliwy błąd, który jest niewłaściwym błędem obsługi.
Funkcja ta jest używana do zamykania otwartego pliku powyższymi poleceniami Open lub Create. W rejestrze bx jest przekazywany logiczny numer pliku, i zakładając, że logiczny numer pliku jest poprawny, zamyka określony plik. Powinniśmy zamykać wszystkie pliku w używanym programie jeśli tylko mamy z nimi połączenie, aby uniknąć uszkodzenia plików dyskowych przy wyłączeniu zasilania systemu lub resetu maszyny, podczas gdy plik są jeszcze otwarte. Zauważmy, ze wyjście do DOS (lub przerwanie DOS przez naciśnięcie control – C lub control – break) automatycznie zamyka wszystkie otwarte pliki. Jednakże, nigdy nie powinniśmy polegać na tej cesze ponieważ pokazuje to złą praktykę programowania. 13.3.8.4 ODCZYT Z PLIKU Funkcja (ah): Parametry wejściowe:
3Fh bx - logiczny numer pliku
cx – liczba bajtów do odczytu ds.:dx - adres tablicy do przetrzymania odczytanych bajtów Parametry wyjściowe: Jeśli flaga przeniesienia jest ustawiona, ax zawiera jeden z poniższych kodów błędów 5 – dostęp zastrzeżony 6 – nieprawidłowy uchwyt Jeśli flaga przeniesienia jest wyzerowana, ax zawiera liczbę bajtów rzeczywiście odczytanych z pliku Funkcja read jest używana do odczytu pewnej liczby bajtów z pliku. Rzeczywista liczba bajtów jest określona przez rejestr cx na wejściu do DOS’a. Logiczny numer pliku, który określa z którego pliku będą czytane bajty, jest przekazywany w bx. Rejestr ds.:dx. Zawiera adres bufora w którym odczytane bajty będą przechowywane. Przy zwrocie, jeśli nie wystąpił błąd, rejestr ax zawiera liczbę bajtów rzeczywiście odczytanych.. Chyba, że osiągniemy koniec pliku (EOF), wtedy liczba ta będzie się zgadzała z wartością przekazaną do DOS’a w rejestrze cx. Jeśli zostanie osiągnięty koniec pliku, wartość zwracana w ax będzie gdzieś pomiędzy zero a wartością przekazaną do DOS w rejestrze cx. Jest to jedynie test dla warunku EOF. Przykład: Przykład otwarcia pliku i odczytu do końca pliku mov ah, 3dh ;otwarcie pliku mov al., 0 ;otwarcie do odczytu lea dx, Filename ;zakładamy, że DS. wskazuje nazwę pliku int 21h jc BadOpen mov FHndl, ax ,zachowanie numeru logicznego pliku LP: mov ax, 3fh ;odczyt danej z pliku lea dx, Buffer ;adres danej bufora mov cx,1 ;odczyt jednego bajtu mov bx, FHndl ;pobranie wartości logicznego numeru pliku int 21h jc ReadError cmp ax, cx ;czy osiągnięty EOF? jne EOF mov al., Buffer ;pobranie odczytanego znaku putc ;wydrukowanie go jmp LP ;odczytaj następny bajt EOF: mov bx, FHndl mov ah, 3eh ;zamykanie pliku int 21h jc CloseError Ta część kodu będzie odczytywała cały plik, którego (zakończona zerem) nazwa pliku została znaleziona pod adresem „Filename” w bieżącym segmencie danych i zapisuje każdy znak do pliku standardowego urządzenia wyjściowego, stosując podprogram putc z UCR StdLib. Przestrzegam, że takie jednoznakowe działanie jest zdecydowanie wolne. Będziemy omawiali lepsze sposoby szybkiego odczytu pliku trochę później w tym rozdziale 13.3.8.5 ZAPIS DO PLIKU Funkcja (ah): Parametry wejściowe:
40h bx – numer logiczny pliku cx – liczba bajtów do zapisu ds.:dx – adres bufora zawierającego dane do zapisu Parametry wyjściowe: Jeśli flaga przeniesienia jest ustawiona, ax zawiera poniższy kod błędu 5 – Dostęp zastrzeżony 6 – nieprawidłowy uchwyt Jeśli flaga przeniesienia jest wyzerowana, ax zawiera liczbę bajtów rzeczywiście zapisanych do pliku. Funkcja ta jest prawie odwrotnością polecenia read przedstawianego wcześniej. Zapisuje określoną liczbę bajtów pod ds.:dx do pliku zamiast go odczytać. Przy zwracaniu, jeśli liczba bajtów zapisanych do pliku nie jest równa pierwotnej liczbie w rejestrze cx, dysk jest pełny i powinno to być traktowane jako błąd.
Jeśli cx zawiera zero, kiedy ta funkcja jest wywoływana, DOS skraca plik do bieżącej pozycji pliku *tj, wszystkie dane z bieżącej pozycji w pliku będą skasowane) 13.3.8.6 POZYCJONOWANIE (PRZESUWANIE WSKAŹNIKA PLIKU) Funkcja: Parametry wejściowe:
42h al. – sposób przesuwania 0 – offset określony od początku pliku 1 – offset określony od bieżącego wskaźnika pliku 2 – Wskaźnik jest przesuwany od końca pliku minus określony offset bx – logiczny numer pliku cx:dx – odległość na jaką trzeba przesunąć, w bajtach Parametry wyjściowe: Jeśli przeniesienie jest ustawione, ax zawiera jeden z poniższych kodów błędów 1 – Nieprawidłowa funkcja 6 – niepoprawny uchwyt Jeśli przeniesienie jest wyzerowane, dx:ax zawiera nową pozycję pliku Polecenie to jest używane do przesuwanie wskaźnika pliku w pliku o dostępie swobodnym. Są trzy metody przesuwania wskaźnika pliku na odległość absolutną wewnątrz pliku (jeśli al. =0), jakąś dodatnią odległość od bieżącej pozycji wskaźnika pliku (jeśli al. = 1), lub jakąś odległość od końca pliku (jeśli al. =2). Jeśli AL. nie zawiera 0,1 lub 2 DOS zwraca błąd niepoprawnej funkcji. Jeśli funkcja zakończy się powodzeniem. Następny bajt do zapisania lub odczytu będzie występował pod określoną lokacją. Zauważ, że DOS traktuje cx:dx jako liczbę całkowitą bez znaku. Dlatego też pojedyncze polecenie seek nie może być używane do przesuwania wstecz w pliku. Zamiast tego musi być użyta metoda #0 do pozycjonowania wskaźnika pliku pod jakąś absolutną pozycją w pliku. Jeśli nie wiemy gdzie jesteśmy obecnie i chcemy cofnąć 256 bajtów możemy użyć poniższego kodu: mov ah, 42h ;polecenie seek mov al., 1 ;przesunięcie z bieżącej lokacji xor cx, cx ;zerowanie cx i dx xor dx, dx mov bx, Filename int 21h jc SeekError sub ax, 256 ;DX:AX zawiera teraz bieżącą pozycję sbb dx, 0 ;pliku, więc obliczamy pozycję 256 bajtów mov cx, dx mov dx, ax mov ah, 42h mov al., 0 ;absolutna pozycja pliku int 21h ;BX zawiera jeszcze uchwyt 13.3.8.7 USTAWIENIE BUFORA ROBOCZEGGO OPERACJI DYSKWYCH (DTA) Funkcja: Parametry wejściowe: Parametry wyjściowe;
1Ah ds.:dx – wskaźnik do DTA żadnych
Polecenie to jest nazywane „ustawieniem bufora roboczego operacji dyskowych „ ponieważ było (jest) używane z oryginalną funkcją pliku DOS v.1.0. Zazwyczaj nie rozpatrujemy tej funkcji, z wyjątkiem faktu, że jest również używane przez funkcję 4Eh i 4Fh 9opisane poniżej) do ustawienia wskaźnika do 43 –bajtowego obszaru bufora. Jeśli ta funkcja nie jest wykonywana przed wykonaniem funkcji 4Eh lub 4Fh, DOS będzie używał domyślnej przestrzeni bufora spod PSP:80h 13.3.8.8 ZNAJDOWANIE PIERWSZEGO PLIKU W KATALOGU Funkcja (ah): Parametry wejściowe:
4EH cx – atrybuty ds.:dx – wskaźnik do nazwy pliku
Parametry wyjściowe:
Jeśli przeniesienie jest ustawione, ax zawiera jeden z następujących kodów błędów: 2 – plik nie znaleziony 18 – nie ma więcej plików Funkcje Find First File i Find Next File (opisana poniżej) są używane do wyszukiwania plików, określonych przez zastosowanie niejednoznacznego odniesienia do pliku. Niejednoznaczne odniesienie do pliku jest nazwa pliku zawierającą znaki wieloznaczności „*” i „?”. Funkcja Find First File jest używana do znalezienie pierwszej takiej nazwy pliku wewnątrz określonego katalogu. Funkcja Find Next File jest używana do znajdowania następujących po sobie wejść w katalogu Ogólnie, kiedy używamy niejednoznacznego odniesienia do pliku, polecenie Finf First File jest stosowane do zlokalizowania pierwszego wystąpienia pliku ,potem jest wywoływana pętla, Find Next File dla zlokalizowania wszystkich innych wystąpień pliku, dopóki nie będzie więcej plików ( błąd numer 18). Gdziekolwiek Find First File jest wywoływana, ustawia określone informacje o DTA: Offset Opis 0 Zarezerwowane do użycia przez Find Next File 21 Atrybut znalezionego pliku 22 Czas ostatniej modyfikacji 24 Data ostatniej modyfikacji 26 Rozmiar pliku w bajtach 30 Nazwa pliku i rozszerzenie w kodzie ASCIIZ (Offsety są pisane dziesiętnie) Zakładając że Find First File nie zwraca żadnego rodzaju błędu, dopasowana nazwa pierwszego pliku do opisu pliku niejednoznacznego pojawi się pod offsetem 30 w DTA. Notka: jeśli określona ścieżka dostępu nie zawiera żadnego znaku wieloznaczności, wtedy Find First File będzie zwracał dokładnie określoną nazwę pliku, jeśli taka istnieje. Każde późniejsze wywołanie Find First File będzie zwracało błąd. Rejestr cx zawiera atrybut dla tego pliku. Zazwyczaj, cx powinien zawierać zero. Jeśli nie – zero, Find First File (i Find Next File) będzie obejmował nazwy plików mających określony atrybut, również wszystkie zwykłe nazwy plików. 13.3.8.9 ZNAJDOWANIE NASTĘPNEGO PLIKU W KATALOGU Funkcja (ah): Parametry wejściowe: Parametry wyjściowe;
4Fh żadne Jeśli przeniesienie jest ustawione, wtedy nie ma więcej innych plików, a ax będzie zwracał 18.
Funkcja Find Next File jest używana do wyszukiwania dodatkowych nazw plików dopasowanych do niejednoznacznego odwołania do pliku po wywołaniu Find First File. DTA musi wskazywać rekord danych ustawiony przez funkcję Find First File. Przykład: Poniższy kod listuje nazwy wszystkich plików w bieżącym katalogu, które kończą się „.EXE”. Przypuszczalnie zmienna „DTA” znajduje się w bieżącym segmencie danych:
DirLoop: PrtName:
mov lea int xor lea mov int jc lea cld lodsb test jz putc jmp
ah, 1Ah dx, DTA 21h cx, cx dx, FileName ah, 4Eh 21h NoMoreFiles si, DTA+30 al., al. NextEntry PrtName
;ustawienie DTA ;Żadnych atrybutów ;Find First File ;robimy jeśli błąd ;Adres nazwy pliku ;zero bajtów?
NextEntry:
mov int jnc
ah, 4Fh 21h DirLoop
;Find Next File ;drukuj następną nazwę
13.3.8.10 USUWANIE PLIKU Funkcja (ah0; Parametry wejściowe: Parametry wyjściowe:
41h ds.:dx – adres ścieżki do usunięcia Jeśli przeniesienie jest ustawione, ax zawiera jeden z poniższych kodów błędów 2 – Plik nie znaleziony 5 – Dostęp zastrzeżony Funkcja ta usuwa określony plik z katalogu. Nazwa pliku musi być nazwą nie niejednoznaczną (tj. nie może zawierać znaków wieloznacznych) 13.3.8.11 ZMAINA NAZWY PLIKU Funkcja (ah0: Parametry wejściowe: Parametry wyjściowe :
56h ds.:dx – wskaźnik do ścieżki istniejącego pliku es:di – wskaźnik do nowej ścieżki dostępu Jeśli przeniesienie jest ustawione, ax zawiera jeden z poniższych kodów błędów 2 – plik nie znaleziony 5 – dostęp zastrzeżony 17 – nie takie samo urządzenie
Polecenie to służy dwóm celom: pozwala ono zmieniać nazwę pliku i pozwala na przenoszenie pliku z jednego katalogu do innego (tak długo jak te dwa podkatalogi są na tym samym dysku) Przykład: Zmaian nazwy z „MYPGM.EXE” na „YPURPGM.EXE” ; Zakładamy, że ES i DS. wskazują na bieżący segment danych ; zawierający nazwy plików. lea dx, OldName lea di, NewName mov ah, 56h int 21h jc BadRename OldName byte „MYPGM.EXXE”, 0 NewName byte „YOURPGM.EXE”,0 Przykład numer 2: Przenoszenie nazwy pliku z jednego katalogu do innego: ;Zakładamy, że ES i DS. wskazują na bieżący segment danych ; zawiera nazwę pliku lea dx, OldName lea di, NewName mov ah, 56h int 21h jc Badrename OldName byte „\DIR1\MYPGM.EXE”, 0 NewName byte „\DIR2\MYPGM.EXE”,0 13.3.8.12 ZMIANA / POBRANIE ATRYBUTU PLIKU Funkcja (ah):
43h
Parametry wejściowe:
al. – kod podfunkcji 0 – zwracany atrybut pliku w cx 1 – ustawienie atrybutu w cx cx – nowy atrybut jeśli AL. = 01 ds.:dx – adres ścieżki dostępu Jeśli przeniesienie jest ustawione, ax zawiera jeden z poniższych kodów błędu 1 – niepoprawna funkcja 3 – Ścieżka nie znaleziona 5 – Dostęp zastrzeżony Jeśli przeniesienie jest wyzerowane, a podfunkcja byłą zerem, cx będzie zawierał atrybut pliku.
Parametry wyjściowe:
Funkcja ta jest użyteczna do ustawiania / przestawiania i odczytywania bitów atrybutu pliku. Może być użyta do ustawienia pliku jako tylko do odczytu, ustawienia wyzerowania bitu archiwizacji. 13.3.8.13 POBRANIE/ USTAWIENIE DATY I CZASU MODYFIKACJI PLIKU Funkcja (ah): Parametry wejściowe:
57h al. – kod podfunkcji 0 – pobranie daty i czasu 1 – ustawienie daty i czasu bx – logiczny numer pliku cx – czas do ustawienia (jeśli AL. = 01) dx – data do ustawienia (jeśli AL. = 01) Parametry wyjściowe: Jeśli przeniesienie jest ustawione, ax zawiera jeden z poniższych kodów błędów 1 – Nieprawidłowa funkcja 6 – nieprawidłowy uchwyt Jeśli przeniesienie jest wyzerowane, cx/dx ustawiają czas/ datę jeśli al. = 00 Funkcja ta ustawia „ostatni zapis” daty / czasu dkla określonego pliku. Plik musi być otwarty (używając open lub create) przed zastosowaniem tej funkcji. Data nie będzie zapisana dopóki plik jest zamknięty. 13.3.8.14 INNE FUNKCJE DOS Poniższe tablice pokrótce przedstawiają wiele innych funkcji DOS. Po więcej informacji na temat zastosowania tych funkcji zajrzyj do Microsoft MS-DOS Programmer’s Reference lub MS-DOS Technical Reference Funkcja (AH) 39h 3Ah
3Bh 45h
Parametry wejściowe
Parametry wyjściowe
ds.:dx – wskaźnik do ścieżki dostępu zakończonej znakiem zero ds.:ds. wskaźnik do ścieżki dostępu zakończonej znakiem zera ds.:dx – wskaźnik do ścieżki dostępu zakończonej znakiem zera bx – logiczny numer pliku ax – nowy uchwyt
Opis Tworzy nowy katalog o określonej nazwie Usuwa katalog z określonej ścieżki dostępu. Wystąpi błąd jeśli katalog nie jest pusty lub jest katalogiem bieżącym Zmienia domyślny katalog na określony w ścieżce dostępu Tworzy kopię logicznego numeru pliku, tak aby program mógł mieć dostęp używając dwóch oddzielnych zmiennych plikowych. Pozwala programowi zamykać plik
z jednym uchwytem a kontynuować z innym 46h
bx –logiczny numer pliku cx – powtórny uchwyt
47h
ds.:si – wskaźnik do bufora dl – napęd
5Ah
cx – atrybuty ds.:dx – wskaźnik do ścieżki tymczasowej
5Bh
cx – atrybuty ax – uchwyt ds.:dx – wskaźnik do ścieżki zakończonej zerem
67h
bx - uchwyt
68h
bx - uchwyt
ax - uchwyt
Tablica 56: Różne funkcje plikowe DOS
Podobnie jak funkcja 45h, z wyjątkiem określenia, który uchwyt (w cx) chcemy odnieść do istniejącego pliku (określony przez bx) Przechowuje łańcuch zawierający bieżącą ścieżkę dostępu (zakończoną zerem) zaczynającą się pod ds.:si. Rejestry te muszą wskazywać na bufor zawierający przynajmniej 64 bajty. Rejestr dl zawiera określony numer napędu ( 0 = domyślny, 1 =A, 2 =B, 3=C ) Tworzy plik o unikalnej nazwie, w katalogu określonym przez łańcuch zakończony zerem, który jest wskazany przez ds.:dx. Musi być przynajmniej 13 zero bajtowy poza końcem ścieżki dostępu ponieważ funkcja ta będzie przechowywała wygenerowaną nazwę pliku na końcu ścieżki dostępu. Atrybuty są takie same jak dla funkcji Create. Podobna jak funkcja call, ale ta funkcja upiera się, że plik nie istnieje. Zwraca błąd jeśli plik istnieje 9zamaist usunąć stary plik) Funkcja ustawia maksymalną liczbę uchwytów, których program może użyć w danym czasie Opróżnia wszystkie dane do pliku bez jego zamykania, zapewniając, że dane pliku są bieżące i spójne
Funkcja (AH) 25h
Parametry wejściowe
Parametry wyjściowe
Al. – numer przerwania Ds.:dx – wskaźnik do podprogramu obsługi programu
Przechowuje określony adres w ds.:dx tablicy wektora przerwań, przy wejściu określonym przez rejestr al. al. – wersja główna Zwraca bieżący numer ah – wersja pomocnicza wersji DOS’a (lub wartość bh – znacznik wersji ustawioną przez bl:cx – 24 bitowy numer SETVER) seryjny dl – znacznik break Zwraca stan znacznika (0 = off, 1= on) break MS-DOS. Jeśli on, MS-DOS sprawdza CtrlC kiedy wykonujemy jakieś polecenie; jeśli off, MS-DOS sprawdza tylko funkcje 1 –0Ch Ustawia znacznik break MS-DOS wedle wartości w dl bl – wersja główna Zwraca „rzeczywisty” bh – wersja pomocnicza numer wersji, nie tylko dl – powtórka ustawioną przez polecenie dh – znacznik wersji SETVER. Bity trzy i cztery znacznika wersji są jedynkami jeśli, odpowiednio, DOS jest w ROM lub DOS jest w wysokiej pamięci. es:bx – wskaźnik do Zwraca adres znacznika znacznika InDOS InDOS. Znacznik ten pomaga zapobiegać es:bx – wskaźnik do Zwraca wskaźnik do podprogramu obsługi podprogramu obsługi przerwań przerwań dla określonego numeru przerwania. Jest to cała rodzina dodatkowych funkcji DOS dla sterowania różnymi urządzeniami. al. – wartość zwracana Zwraca ostatni kod ah – metoda zakończenia wynikowy z podprogramu potomnego w al. rejestr ah zawiera metodę zakończenia, która jest jedną z następujących wartości: 0 –normalna, 1 – ctrl-C, 2 – krytyczny błąd urządzenia, 3 – zakończenie i pozostanie w pamięci Ustawia bieżący DOS’owski adres PSP wartością określoną w rejestrze bx bx – adres PSP Zwraca wskaźnik do bieżącego PSP w rejestrze bx
30h
33h
Ah – 0
33h
al. –1 dl – znacznik break
33h
al. - 6
34h 35h
al. – numer przerwania
44h
al. – podkod Inne parametry!
4Dh
50h
51h
bx – adres PSP
Opis
59h
5Dh
ax- rozszerzony kod błędu Zwraca dodatkowe bh –klasa błędu informacje kiedy wystąpi bl – reakcja na błąd błąd w wywołaniu DOS ch – miejsce błędu al. –0Ah Kopiuje dane z ds.:si –wskaźnik do rozszerzonej struktury rozszerzonej struktury błędu do wewnętrznego błędu DOS’owego rekordu Tablica 57: Różne funkcje DOS
W dodatku do powyższych poleceń, jest kilka dodatkowych poleceń DOS, które działają z Siecią i międzynarodowym zbiorem znaków.. 13.3.9 PRZYKŁADY PLIKÓW I/O Oczywiście , jednym z głównych powodów dla funkcji DOS jest manipulowanie plikami na urządzeniach pamięci masowej. Poniższe przykłady demonstrują kilka zastosowań znaków I/O używających DOS. 13.3.9.1 PRZYKŁAD 1: NARZĘDZIE DO ZRZUTU HEKSADECYMALNEGO Program ten zrzuca plik w postaci heksadecymalnej. Nazwa pliku musi być ustalona w pliku. include stdlib.a includelib stdlib.lib cseg segment byte public ‘CODE’ assume cs :cseg, ds.: dseg, es: dseg, ss: sseg MainPgm proc far ;właściwe ustawienie rejestrów segmentowych: mov ax, seg dseg mov ds., ax mov es, ax mov ah, 3dh mov al., 0 ;otwarcie pliku do odczytu lea dx, FileName ;plik do otwarcia int 21h jnc GoodOpen print byte ‘Nie można otworzyć pliku., program przerwany...’, cr, 0 jmp PgmExit GoodOpen: ReadFileLp:
NotNewLine:
mov mov mov and jnz putcr mov xchg puth xchg puth print byte
FileHandle, ax Position, 0 al., byte ptr Position al., 0Fh NotNewLine
inc mov mov lea
Position bx, FileHandle cx, 1 dx, buffer
;zachowanie uchwytu plik u ;inicjalizacja pozycji licznika pliku ;obliczanie (Position MOD 16) ;początek nowej linie każdych 16 bajtów
ax, Position al., ah al., ah ‘: ‘, 0 ;zwiększenie licznika znaków ;odczyt 1 bajtu ;miejsce dla przechowania tego bajtu
mov int jc cmp jnz mov puth mov putc jmp BadRead:
AtEOF:
print byte byte byte mov mov int
ah, 3Fh 21h BadRead ax, 1 AtEOF al., Buffer al., ‘ ‘
;operacja odczytu ;osiągniecie EOF? ;pobranie odczytanego znaku ;i wydruk w hex ;wydruk spacji między wartościami
ReadFileLp cr, lf ‘Błąd odczytu danych z pliku, przerwanie’ cr, lf, 0 bx, FileHandle ;zamknięcie pliku ah, 3Eh 21h
PgmExit: MainPgm:
ExitPgm endp
cseg dseg
ends segment byte public ‘data’
Filename FileHandle Buffer Position
byte word byte word
dseg
ends
sseg stk sseg zzzzzzseg LastBytes Zzzzzzseg
segment byte stack ‘stack’ word 0ffh dup (?) ends segment para public ‘zzzzzz’ byte 16 dup (?) ends End MainPgm
‘hexdump.asm’, 0 ? ? 0
;nazwa pliku do zrzutu
13.3.9.2 PRZYKŁAD 2: KONWERSJA NA DUŻE LITERY Poniższy program odczytuje jeden plik, konwertuje wszystkie małe litery na duże , i zapisuje dane do innego pliku wyjściowego. include stdlib.a includelib stdlib.lib cseg segment byte public ‘CODE’ assume cs: cseg, ds : dseg, es: dseg, ss: sseg MainPgm proc far ;właściwe ustawienie rejestrów segmentowych : mov ax, seg dseg mov ds., ax mov es, ax ;--------------------------------------------------------------------------------------------------------------------------------------; ; Konwersja UCCONVERT.ASM na duże litery ; ; Otwarcie pliku wejściowego: mov ah, 3dh
GoodOpen:
mov lea int jnc print byte jmp
al., 0 dx, Filename 21h GoodOpen
;otwarcie pliku do odczytu ;Plik do otwarcia
„Nie można otworzyć pliku, przerwanie programu...’, cr, lf, 0 PgmExit
mov
FileHandle1, ax
;zachowanie uchwytu pliku wejściowego
;Otwarcie pliku wyjściowego: mov ah, 3Ch ;funkcja tworzenia pliku mov cx, 0 ;normalny atrybut lea dx, OutFileName ;plik do otwarcia int 21h jnc GoodOpen2 print byte ‘Nie można otworzyć pliku wyjściowego , przerwanie programu...’ byte cr, lf, 0 mov ah, 3Eh ;zamknięcie pliku wejściowego mov bx, FileHandle1 int 21h jmp PgmExit ;ignoruj błąd GoodOpen2:
mov
FileHandle2, ax
ReadFileLp:
mov mov lea mov int jc cmp jz jmp
bx, FileHandle1 cx, 1 dx, buffer ah, 3Fh 21h BadRead ax, 1 ReadOK AtEOF
ReadOK:
mov al., Buffer cmp al., ‘a’ jb NotLower cmp al., ‘z’ ja NotLower and al., 5Fh NotLower: mov Buffer, al. ;teraz zapisujemy dane do pliku wyjściowego mov bx, FileHandle2 mov cx, 1 lea dx, buffer mov ah, 40h int 21h jc BadWrite cmp ax, 1 jz ReadFileLp BadWrite
BadRead:
print byte byte byte jmp print byte
;zachowanie uchwytu pliku wyjściowego ;odczyt jednego bajtu ;miejsce do przechowania tego bajtu ;operacja odczytu ;osiągnięcie EOF?
;pobranie odczytanego znaku i ;konwersja do znaku dużego
;ustawienie bitu 5 na zero
;odczyt jednego bajtu ;miejsce do przechowania tego bajtu ;operacja zapisu ;upewnienie czy dysk nie jest pełny
cr, lf ‘Błąd zapisu danych do pliku, operacja przerwana’ cr, lf, 0 short AtEOF cr, lf
byte ‘Błąd odczytu danych z pliku, przerwanie ’ byte ‘operacji’, cr, lf, 0 AtEOF: mov bx, FileHAndle1 ;zamknięcie pliku mov ah, 3Eh int 21h mov bx, FileHandle2 mov ah, 3Eh int 21h ;--------------------------------------------------------------------------------------------------------------------------------------PgmExit: MainPgm cseg
ExitPgm endp ends
dseg
segment byte public ‘data’
Filename OutFileName FileHandle1 FielHandle2 Buffer Position
byte byte word word byte word
dseg
ends
sseg stk sseg
segment byte stack ‘stack’ word offh dup (?) ends
zzzzzzseg LastBytes Zzzzzzseg
segment para public ‘zzzzzz’ byte 16 dup (?) ends end MainPgm
‘unconvrt.asm’, 0 ‘output.txt’, 0 ? ? ? 0
;nazwa pliku do konwersji ;nazwa pliku wyjściowego
13.3.10 ZBLOKOWANE PLIKI I/O Przykłady w poprzedniej sekcji cierpiały z powodu jednej wady, były zbyt wolne. Problem wydajności w powyższym kodzie spowodowane są wyłącznie DOS’em. Czyniąc wywołania DOS powinniśmy wiedzieć, że nie są one najszybszymi operacjami na świecie. Wywołując DOS za każdym razem kiedy chcemy odczytać lub zapisać pojedynczy znak z / do pliku, będziemy rzucać system na kolana. Jak się okazuje, nie robi zabiera (praktycznie) więcej czasu DOS’owi odczyt lub zapis dwóch znaków niż odczyt lub zapis jednego znaku. Ponieważ ilość czasu jaką (zazwyczaj) spędzamy przetwarzając dane jest nieistotna w porównaniu z ilością czasu jaki DOS pobiera do zwrotu lub zapisu danych, odczyt dwóch znaków po kolei, w gruncie rzeczy podwoi prędkość programu. Jeśli odczyt dwóch znaków podwaja szybkość przetwarzania, jak odczytać cztery znaki? Z pewnością czterokrotnie zwiększy prędkość przetwarzania. Podobnie przetwarzanie dziesięciu znaków po kolei prawie zwiększa prędkość przetwarzania według kolejności wag. Niestety, ten postęp nie będzie trwał wiecznie. Nadchodzi kiedyś punkt zmniejszania, kiedy bierzemy stanowczo za dużo pamięci dla uzasadnienia (bardzo) małej poprawy wydajności (zapamiętajmy, że odczyt 64K w pojedynczej operacji wymaga 64K bufora pamięci dla przechowania danych). Dobrym kompromisem jest 256 lub 512 bajtów. Odczyt większej ilości danych w rzeczywistości nie poprawia wiele wydajności, ale mimo to bufor 256 bajtowy lub 52\12 bajtowy jest łatwiejszy w postępowaniu niż bufory większe. Odczyt danych w grupach lub blokach jest nazywany zblokowanym I/O . Zblokowany I/O jest często rzędu jeden do dwóch razy szybsze niż pojedynczy znak I/O, więc oczywiście powinniśmy używać zblokowanych I/O gdziekolwiek to możliwe. Jest jedna pomniejsza wada zblokowanych I/O – jest trochę bardziej złożone do oprogramowania niż pojedynczy znak I/O. Rozważmy przykład przedstawiony w sekcji o poleceniu DOS’a Read: Przykład: Ten przykład otwiera plik i odczytuje go do EOF mov ah,3dh ;otwarcie pliku mov al., 0 ;otwarcie do odczytu
lea
dx, Filename
;zakładamy, że wskazuje DS.
int jc mov mov lea mov mov int jc cmp jne mov putc jmp mov mov int jc
21h BadOpen FHndl, ax ah, 3Fh dx, Buffer cx, 1 bx, FHndl 21h ReadError ax, cx EOF al., Buffer
;segment
Filename:
LP:
EOF:
LP bx, FHndl ah, 3Eh 21h CloseError
;zachowanie uchwytu pliku ;odczyt danych z pliku ;adres bufora danych ;odczyt jednego bajtu ;pobranie wartości uchwytu pliku ;osiągnięte EOF? ;pobranie odczytanego znaku ;wydruk go (wywołanie IOSHELL) ;odczyt następnego bajtu ;zamknięcie pliku
Teraz przepiszmy ten program przy użyciu zblokowanych I/O: Przykład: Ten przykład otwiera plik i odczytuje go do EOF używając zblokowanych I/O mov ah, 3dh ;otwórz plik mov al., 0 ;otwarcie do odczytu lea dx, Filename ;zakładamy, że wskazuje DS. Filename int 21h ;segment jc BadOpen mov FHndl, ax ;zachowanie uchwytu pliku LP:
PrtLP:
EOF: Finis:
EOF2:
mov lea mov mov int jc cmp jne mov mov putc inc loop jmp mov jcxz mov mov putc inc loop mov mov int jc
ah, 3fh dx, Buffer cx, 256 bx, FHndl 21h ReadError ax, cx EOF si, 0 al., Buffer[si] si PrtLp LP cx, ax EOF2 si, 0 al., Buffer [si] si Finis bx, FHndl ah, 3eh 21h CloseError
;odczyt danych z pliku ;adres bufora danych ;odczyt 256 bajtów ;pobranie wartości uchwytu pliku ;osiągnięte EOF? ;CX =256 w tym punkcie ;pobranie odczytanego znaku ;jego wydruk ;odczyt następnego bloku ;jeśli CX = 0 , rzeczywiście robimy ;przetwarzanie ostatniego bloku danych ;odczytanych z pliku, który zawiera ;1..255 poprawnych danych
;zamknięcie pliku
Przykład ten demonstruje jeden główny kłopot ze zblokowanymi I/O – kiedy osiągamy koniec pliku, nie musimy koniecznie przetworzyć wszystkie dane w pliku. Jeśli rozmiar bloku ma 256 a jest 255 bajtów opuszczonych w pliku, DOS będzie zwracała warunek EOF (liczba bajtów odczytanych nie pasuje do
zgłoszenia). W tym przypadku, musimy jeszcze przetwarzać znaki, które zostały odczytane. Powyższy kod robi to raczej w prosty sposób, używając drugiej pętli, kończącą kiedy osiągnięty zostanie EOF. Zauważyliśmy, że dwie pętle print są praktycznie identyczne. Program ten może być zredukowany rozmiarowo, przez zastosowanie kodu, który jest trochę bardziej złożony: Przykład: ten przykład otwiera plik i odczytuje go do EOF używając zblokowanych I/O mov mov lea
ah, 3dh al., 0 dx, FileName
;otwarcie pliku ;otwarcie do odczytu ;zakładamy, że DS. wskazuje
int jc mov
21h BadOpen FHndl, ax
;segment
mov lea mov mov int jc mov mov jcxz mov mov putc inc loop cmp je
ah, 3fh dx, Buffer cx, 256 bx, FHndl 21h ReadError bx, ax cx, ax EOF si, 0 al., Buffer [si]
;odczyt danej z pliku ;adres bufora danych ;odczyt 256 bajtów ;pobranie wartości uchwytu pliku
FileName
LP:
PrtLp;
si PrtLp bx, 256 LP
;zachowanie uchwytu pliku
;zachowanie na później ;CX=256 w tym punkcie ;pobranie odczytanego znaku ;jego wydruk ;osiągnięte EOF?
EOF:
mov bx, FHndl mov ah, 3eh ;zamknięcie pliku int 21h jc CloseError Zblokowane I/O pracują najlepiej na plikach sekwencyjnych. To znaczy, plik te są otwierane tylko do odczytu lub zapisu (żadnego pozycjonowania). Kiedy działamy na plikach o dostępie swobodnym, powinniśmy odczytywać lub zapisywać cały rekord używając polecenie odczyt / zapis DOS’a do działania na całym rekordzie. Jest to znacznie szybsze niż manipulowanie danymi jedno bajtowymi. 13.3.11 PRZEDROSTEK SEGMENTU PROGRAMU (PSP) Kiedy program jest ładowany do pamięci dla wykonania, DOS buduje najpierw przedrostek segmentu programu bezpośrednio przed programem, który jest ładowany do pamięci. PSP zawiera wiele informacji, jedne są użyteczne, inne przestarzałe. Zrozumienie rozkładu PSP jest niezbędne dla programistów piszących programy w assemblerze PSP ma długość 256 bajtów i zawiera następujące informacje: Offset 0 2 4 5 0Ah 0Eh 12h 16h 2Ch 2Eh 50h
Długość 2 2 1 5 4 4 4 22 2 34 3
Opis Tu jest przechowywana instrukcja 20h Pułap pamięci programu Nieużywane, zarezerwowane przez DOS Wywołanie funkcji DOS obsługującej funkcje systemowe Adres procedury obsługi przerwania 22h Adres procedury obsługi przerwania 23h Adres procedury obsługi przerwania 24h Zarezerwowane przez DOS Adres segmentowy środowiska systemowego Zarezerwowane przez DOS INT 21h, instrukcja RETF
53h 5Ch 6Ch 80h 81h
9 16 20 1 127
Zarezerwowane przez DOS Domyślny FCB 1 Domyślny FCB 2 Długość łańcucha lini poleceń Łańcuch lini poleceń
Notka: lokacje 80h..FFh są używane przez domyślny DTA. Większość informacji w PSP jest używana w nowoczesnych programach asemblerowych MS-DOS. Ukryte w PSP są jednak warte wiedzy. My jednak będziemy się przyglądać wszystkim polom w PSP. Pierwsze pole w PSP zawiera instrukcję int 20h. Int 20h jest przestarzałym mechanizmem używanym do zakańczania wykonywania programu. We wcześniejszych dniach DOS v. 1.0, program wykonywał jmp do tej lokacji żeby zakończyć. Obecnie oczywiście, mamy funkcję DOS 4Ch, która jest dużo łatwiejsza (i bezpieczniejsza) niż skakanie do lokacji zero w PSP. Dlatego to pole jest przestarzałe. Pole numer dwa zawiera wartość, która wskazuje ostatni paragraf alokowany w naszym programie. Przez odjęcie adresu PSP od tej wartości, możemy określić ilość pamięci alokowanej dla naszego programu (i opuścić jeśli jest dostępna niewystarczająca ilość pamięci) Trzecie pole jest pierwszą z wielu „dziur” pozostawionych przez Microsoft w PSP. Niech zgadnie ktoś dlaczego są one tu. Czwarte pole jest to wywołanie funkcji DOS obsługującej funkcje systemowe. Celem tego (teraz przestarzałego) mechanizmu wywołania DOS było zezwolenie na dodatkową kompatybilność z programami CP/M.-80. W nowoczesnych programach DOS’a nie musimy się martwić o to pole. Następne trzy pola są używane do przechowywania specjalnych adresów podczas wykonywania programów. Pola te zawierają domyślny wektor zakończenia, wektor przerwania i wektor krytycznych błędów. Są to wartości normalnie przechowywane w wektorach przerwań dla int 22h, int 23h i int 24h. Poprzez przechowywanie kopii tych wartości w wektorach dla tych przerwań, możemy zmienić te wektory aby wskazywały na nasz własny kod.. Kiedy program się kończy, DOS przywraca te trzy wektory z tych trzech pól w PSP. Ósme pole w PSP jest innym zarezerwowanym polem, aktualnie niedostępnym do użytkowania przez programy. Pole dziewiąte jest innym prawdziwym skarbem. Jest to adres segmentowy środowiska systemowego. Jest to dwu bajtowy wskaźnik, który zawiera adres segmentu obszaru pamięci środowiskowej. Łańcuchy środowiska zawsze zaczynają się od offsetu zero wewnątrz segmentu. Obszar łańcuchów środowiska składa się z sekwencji łańcuchów zakończonych zerem. Używa następującego formatu: string 1 0, string2 0, string3 0....0, stringn 0 0 To znaczy , obszar środowiska składa się z listy łańcuchów zakończonych zerem , sama lista kończy się łańcuchem o długości zero (tj same zera w nim , albo dwa zera w wierszu). Łańcuchy są (zazwyczaj) umieszczone w obszarze środowiska poprzez polecenia DOS takie jak PATH, SET, itp. Ogólnie łańcuch przybiera w obszarze środowiska postać: nazwa = parametry Na przykład polecenie „SET IPATH = C:\ASSEMBLY\INCLUDE”kopiuje łańcuch „IPATH=C:\ASSEMBLY\INCLUDE” do obszaru pamięci środowiska systemowego. Wiele języków przeszukuje obszar pamięci środowiska aby znaleźć domyślną nazwę pliku w ścieżce dostępu i inne fragmenty domyślnej informacji ustawionej przez DOS.. Nasze programy mogą również to wykorzystywać. Następne pole w PSP jest innym blokiem zarezerwowanej pamięci, obecnie nie zdefiniowanej przez DOS. Pole 11 w PSP jest inną procedurą DOS’a obsługującą funkcje systemowe. Dlaczego istnieje ta funkcja (kiedy pod lokacją 5 w PSP już istnieje i nikt rzeczywiście nie używa innego mechanizmu wywołania DOS) jest interesującym pytaniem. To pole powinno być ignorowane przez nasze programy. Pole 12 jest innym blokiem nie używanych bajtów w PSP, który powinien być ignorowany. Pola 13 i 14 w PSP są domyślnymi FCB’ami (Blok Kontrolny Pliku –FCB). Bloki kontrolne pliku są inną archaiczną strukturą danych przeniesiona z CP/M.-80. Są używane tylko wtedy, kiedy używamy przestarzałych podprogramów obsługi plików DOS’a 1.0 , więc są nieco interesujące dla nas. Ignorujemy jednak FCB’y w PSP. Lokacja 80h na końcu PSP zawiera bardzo ważną część informacji – wpisane parametry lini poleceń DOS wraz z nazwą programu. Jeśli w lini poleceń jest wpisane: MYPGM parametr1, parametr 2 W polu parametrów lini poleceń przechowujemy: 23, „parametr1, parametr2”, 0Dh
Lokacja 80h zawiera 2310 ,długość parametrów następujących po nazwie programu. Lokacja 81h do 97h zawierają znaki stanowiące łańcuch parametrów . Lokacja 98h zawiera powrót karetki. Zauważmy, że znak powrotu karetki nie figuruje w długości łańcucha lini poleceń. Przetwarzanie łańcucha lini poleceń jest takim ważnym aspektem programowania języku asemblera, że proces ten będzie omówiony w następnej sekcji. Lokacje 80h..FFh w PSP również stanowią domyślny DTA. Dlatego też nie używajmy funkcji DOS 1Ah do zmiany DTA i wykonujmy FIND FIRST FILE, informacja o nazwie pliku będzie przechowana w początkowej lokacji 80h w PSP. Ważnym szczegółem jaki pominęliśmy jest to jak dokładnie uzyskujemy dostęp do danych w PSP. Chociaż PSP jest ładowane do pamięci bezpośrednio przed programem, nie znaczy to ,ze pojawia się koniecznie 100h bajtów przed kodem. Nasz segment danych może być załadowany przed segmentem kodu, tym samym obala metodę umiejscawiania PSP. Adres segmentu PSP jest przekazany do programu w rejestrze ds. Przechowując adres PSP w segmencie danych , nasz program powinien zaczynać się od takiego kodu: push ds. ;zachowanie wartości PSP mov ax, seg DSEG ;DS. i ES wskazują na nasz segment mov ds., ax ;danych mov es, ax pop PSP ;przechowanie wartości PSP w zmiennej ;PSP Innym sposobem uzyskania adresu PSP w DOS 5.0 i późniejszych, jest wykonanie wywołania DOS’a. Jeśli załadujemy do ah 51h i wykonamy instrukcję int 21h, MS-DOS zwróci adres segmentu aktualnego PSP w rejestrze bx. Jest wiele sztuczek umożliwiających pracę z danymi w PSP. Peter Norton;s Progerammer’s Guide dla IBM PC wylicza wszystkie rodzaje sztuczek. Takie działania nie będą tu omawiane ponieważ wychodzą one poza zakres tego materiału 13.3.12 DOSTĘP DO PARAMETRÓW LINI POLECEŃ Większość programów takich jak MASM i LINK pozwala nam określić parametry lini poleceń, kiedy program jest wykonywany. Na przykład pisząc ML MYPGM.ASM Możemy poinstruować MASM aby zasemblował MYPGM bez żadnych dalszych interwencji z klawiatury. „MYPGM.ASM” jest dobrym przykładem parametru lini poleceń Kiedy DOS’owskie polecenie COMMAND.COM interpretuje analizę lini poleceń, kopiuje większość następującego tekstu nazwy programu do lokacji 80h w PSP jak opisano w poprzedniej sekcji. Na przykład powyższa linia poleceń będzie przechowana w PSP:80h tak 11, ‘MYPGM.ASM”, 0 Przechowywany tekst w obszarze pamięci lini poleceń w PSP jest zazwyczaj dokładną kopią danej pojawiającej się w lini poleceń. Jest jednak parę wyjątków. Przede wszystkim parametry przekierowania I/O nie są przechowywane w buforze wejściowym. Inną sprawą pojawiającą się w lini poleceń, która jest nieobecna w danych pod PSP:80h jest nazwa programu. Jest to raczej nieszczęśliwe, ponieważ mając dostępną nazwę programu, możemy określić katalog zawierający program. Pomimo to, jest dużo użytecznych informacji przedstawionych w lini poleceń. Informacje w lini poleceń mogą być użyte do prawie każdego celu jak uznamy za stosowny. Jednakże, większość programów, oczekuje dwóch typów parametrów w buforze parametrów lini poleceń – nazwy pliku i przełączników. Cel nazwy pliku jest raczej oczywisty, pozwala programowi uzyskać dostęp do pliku. Przełączniki, z drugiej strony, są dowolnymi parametrami programu. Przez konwencję, przełączniki są poprzedzone ukośnikiem lub łącznikiem w lini poleceń. Zastanówmy się co robić z informacjami w lini poleceń zwanymi analizą składniową lini poleceń. Jeśli programy manipulują danymi w lini poleceń, musimy zanalizować linię poleceń wewnątrz kodu. Zanim linia poleceń zostanie zanalizowana, każda pozycja lini poleceń musi być oddzielona od innych. To znaczy, każde słowo (lub bardziej poprawnie – leksem) musi być zidentyfikowane w lini poleceń. Oddzielenie leksemów w lini poleceń jest stosunkowo łatwe, wszystko co musimy zrobić to wyszukać separatory ograniczające w lini poleceń. Separatory są specjalnymi symbolami używanymi do oddzielania znaczników w lini poleceń. DOS wspiera sześć różnych znaków ograniczników: spacja, przecinek, średnik, znak równości, tabulator lub powrót karetki.
Generalnie każda liczba znaków ograniczających może pojawić się pomiędzy dwoma znacznikami w lini poleceń. Dlatego też, wszystkie takie wystąpienia muszą być pominięte, kiesy przeszukujemy linie poleceń. Poniższy kod asemblerowy przeszukuje całą linie poleceń i drukuje wszystkie znaczniki tam się pojawiające: include stdlib.a includelib stdlib.lib cseg segment byte public ‘CODE’ assume cs: cseg, ds :dseg, es : dseg, ss: sseg ;Przyrównywanie w lini poleceń CmdLnLen equ byte ptr es: [80h] ;długość lini poleceń CmdLn equ byte ptr es:[81h] ;dane lini poleceń Tab
equ
09
MainPgm proc far ;właściwe ustawienie rejestrów segmentowych: push ds. ;zachowanie PSP mov ax, seg dseg mov ds., ax pop PSP ;--------------------------------------------------------------------------------------------------------------------------------------print byte byte
PrintLoop: PrtLoop2:
EndofToken:
cr, lf ‘Pozycja w tej lini:’, cr, lf, lf, 0
mov lea print byte call mov call jz putc inc jmp
es, PSP bx, CmdLn cr, lf, ’Pozycja: ‘, 0 SkipDelimiters al., es:[bx] TestDelimiter EndofToken
cmp jne
al., cr PrintLoop
bx PrtLoop2
;ES wskazuje PSP ;wskazuje linię poleceń ;przeskoczenie głównych ograniczników ;pobranie następnego znaku ;czy jest ogranicznik? ;wyjście z pętli jeśli jest ;wydruk znaku jeśli nie ;przesunięcie na następny znak ;powrót karetki? ;powtórz jeśli nie koniec lini
print byte cr, lf, lf byte ‘Koniec lini poleceń’, cr, lf,lf ,0 ExitPgm MainPgm endp ;Poniższy podprogram ustawia flagę zera jeśli znak w rejestrze AL. to jeden z sześciu ograniczników ;DOS’owych, w przeciwnym razie flaga zera jest zerowana. Pozwala to nam później na użycie instrukcji ;JE/JNE do testowania ograniczników TestDelimiter
proc cmp jz cmp jz cmp jz cmp jz cmp
near al., ‘ ‘ ItsOne al., ‘ , ‘ ItsOne al., Tab ItsOne al., ‘ ; ‘ ItsOne al., ‘ = ‘
ItsOne: TestDelimiter
jz cmp ret endp
ItsOne al., cr
;SkipDelimiters przeskakuje główne ograniczniki w lini poleceń. Nie przeskakuje jednak powrotu karetki i ;końca lini ponieważ znak ten jest używany jako kończący program główny SkipDelimiters SDLoop:
QuitSD: SkipDelimiters cseg dseg PSP dseg sseg stk sseg zzzzzzseg LastBytes Zzzzzzseg
proc near dec bx inc bx mov al., es;[bx] cmp al., 0dh jz QuitSD call TestDelimiter jz SDLoop ret endp ends segment byte public ‘data’ word ? ends segment byte stack ‘stack’ word 0ffh dup (?) ends
;Offset BX poniżej ;przesunięcie na następny znak ;pobranie następnego znaku ;nie przeskakuj jeśli CR ;zobacz czy jest jakiś inny ;ogranicznik i powtórz
;przedrostek segmentu programu
segment para public ‘zzzzzz’ byte 16 dup (?) ends End MainPgm
Ponieważ możemy przeszukać linie poleceń (to znaczy, oddzielić leksemy), następnym krokiem jest jego analiza składniowa. Dla większości programów analiza składniowa lini poleceń jest nadzwyczajnie trywialnym działaniem. Jeśli pogram akceptuje tylko pojedynczą nazwę pliku, wszystko co musimy zrobić jest uchwycenie pierwszego leksema w lini poleceń, dodanie zerowego bajta na jego koniec (być może przesuniecie go w segmencie danych) i użycie jako nazwy pliku. Poniższy program asemblerowy modyfikuje podprogram zrzutu hex prezentowanego wcześniej, żeby pobrać jego nazwę pliku z lini poleceń zamiast ustalenia nazwy pliku w programie include stdlib.a includelib stdlib.lib cseg segment byte public ‘CODE’ assume cs: cseg, ds.: dseg, es : dseg, ss :sseg ;zanotuj, że CR i LF są już zdefiniowane w STDLIB.A tab equ 09h MainPgm proc far ;właściwe ustawienie rejestrów segmentowych: mov ax, seg dseg mov es, ax ;DS. wskazuje na PSP ;--------------------------------------------------------------------------------------------------------------------------------------; ; Najpierw analizujemy linie poleceń aby pobrać nazwę pliku: mov si, 81h ;wskazuje na linię poleceń lea di, FileName ;wskazuje bufor FileName SkipDelimiters: lodsb ;pobranie następnego znaku call TestDelimiter je SkipDelimiters ; Zakładamy, że to co nastąpi jest faktyczną nazwą pliku
dec si ;wskazuje pierwszy znak nazwy lodsb cmp al., 0dh je GotName call TestDelimiter je GotName stosb ;zachowanie znaku nazwy pliku jmp GetFName ;Jesteśmy na końcu pliku, wiec jest wymagane przez DOS zakończenie zerem. GetFName:
GotName:
mov mov mov
byte ptr es:[di], 0 ax, es ds., ax
mov mov lea int jnc print byte jmp mov mov mov and jnz putcr mov xchg puth xchg puth print byte
ah, 3dh al., 0 dx, FileName 21h GoodOpen
;teraz działamy na pliku
GoodOpen: ReadFileLp:
NotNewLn:
BadRead:
AtEOF:
;otwarcie pliku do odczytu ;Plik do otwarcia
„Nie można otworzyć pliku...program przerwany...’, cr, 0 PgmExit FileHandle, ax ;zachowanie uchwytu pliku Position, 0 ;inicjalizacja pozycji pliku al., byte ptr Position al., 0Fh ;obliczanie (Position MOD 16) NotNewLn ;każde 16 bajtów zaczyna się w lini ax, Position al., ah
;wydruk offsetu pliku
al., ah ‘ : ‘,0
inc mov mov lea mov int jc cmp jnz mov puth mov putc jmp
Position bx, FileHandle cx, 1 dx, buffer ah, 3Fh 21h BadRead ax, 1 AtEOF al., Buffer
ReadFileLp
print byte byte byte
cr, lf ‘Błąd odczytu danej z pliku, przerwanie’ cr, lf, 0
al., ‘ ‘
;zwiększenie licznika znaków ;odczyt jednego bajta ;miejsce do przechowywania tego bajta ;operacja odczytu ;osiągnięty EOF? ;pobranie odczytanego znaku i ;jego wydruk w hex ;wydruk spacji między wartościami
mov bx, FileHandle ;zamknięcie pliku mov ah, 3Eh int 21h ;---------------------------------------------------------------------------------------------------------------------------------------
PgmExit: MainPgm
ExitPgm endp
TestDelimiter
xit: TestDelimiter cseg
proc cmp je cmp je cmp je cmp je cmp ret endp ends
dseg
segment byte public ‘data’
PSP Filename FileHandle Buffer Position
word byte word byte word
dseg
ends
sseg stk sseg
word
near al., ‘ ‘ xit al., ‘ , ‘ xit al., Tab xit al., ‘ ; ‘ xit al., ‘ = ‘
? 64 dup (?) ? ? 0
;plik do zrzutu
segment byte stack ‘stack’ 0ffh dup (?) ends
zzzzzzseg LastBytes zzzzzzseg
segment para public ‘zzzzzz’ byte 16 dup (?) ends end MainPgm Poniższy przykład demonstruje kilka koncepcji działania z parametrami lini poleceń. Program ten kopiuje jeden plik do innego.. jeśli przełącznik „/U” jest (gdzieś) w lini poleceń włączony, wszystkie małe litery w pliku są konwertowane na duże przed zapisaniem do pliku przeznaczenia. Inna cecha tego kodu to to ,że zachęca użytkownika do wprowadzenia zaginionej nazwy pliku, podobnie jak programy MASM i LINK zachęcają nas do wprowadzenia nazwy pliku jeśli nie dostarczyliśmy żadnej. include stdlib.a includelib stdlib.lib cseg segment byte public ‘CODE’ assume cs: cseg, ds: nothing, es: dseg, ss: sseg ;Notka: Stałe CR (0dh) i LF (0ah) pojawiają się wewnątrz pliku stdlib.a tab
equ
09h
MainPgm proc far ;właściwe ustawienie rejestrów segmentowych: mov ax, seg dseg mov es, ax ;DS. wskazuje na PSP ;--------------------------------------------------------------------------------------------------------------------------------------;Najpierw analizujemy linię poleceń aby pobrać nazwę pliku: mov es: GotName1, 0 ;inicjalizacja flag, które mówią nam czy mov es: GotName2, 0 ;analizujemy nazwę pliku mov es: ConvertLc, 0 ;i włączony jest przełącznik „/U” ;Okay, zaczynamy przeszukiwanie i analizowanie lini poleceń mov si, 81h ;wskazuje linię poleceń
SkipDelimiters: lodsb ;pobranie następnego znaku call TestDelimiter je SkipDelimiters ;Określenie czy to jest nazwa pliku lub przełącznik /U cmp al., ‘/ ‘ jnz MustBeFN ;zobacz czy jest tu „/U” lodsb and al., 5fh ;konwersja „u” do „U” cmp al., ‘U’ jnz NotGoodSwitch lodsb ;upewnij się, że następny znak cmp al., cr ;jest ograniczony jakoś jz GoodSwitch call TestDelimiter jne NotGoodSwitch ;Okay, tu jest ”/U’ GoodSwitch: mov es: ConvertLC , 1 ;konwersja LC na UC dec si jmp SkipDelimiters ;przesunięcie na następną pozycję ;jeśli został znaleziony zły przełącznik w lini poleceń, wydruk błędu i przerwanie NotGoodSwitch: print byte cr, lf byte ‘Nieprawidłowy przełącznik, dozwolony jest tylko /U!’, cr, lf byte ‘Przerwane wykonywanie programu’, cr. lf, 0 jmp PgmExit ;jeśli to nie jest przełącznik zakładamy, że jest to poprawna nazwa pliku i dany tu uchwyt MustBeFN: cmp al., cr ;zobacz czy koniec lini cmd je EndOfCmdLn ;Zobacz czy jest jedna, dwie lub więcej nazw plików , które zostały określone cmp es: GotName1, 0 jz Is1stName cmp es: GotName2 jz Is2ndName ;więcej niż dwa wprowadzone pliki, wydruk błędu i przerwanie print byte cr, lf byte ‘Zbyt wiele określono nazw plików.’, cr, lf byte ‘Program przerwany...’, cr, lf, lf, 0 jmp PgmExit ;skok tu jeśli to jest pierwsza przetwarzana nazwa pliku Is1stName: lea dl, FileName1 mov es: GotName1, 1 jmp ProcessName Is2ndName: lea dl, FileName2 mov es: GotName2, 1 ProcessName: stosb ;przechowanie znaku z nazwy lodsb ;pobranie kolejnego znaku z lini cmd cmp al., cr je NameIsDone call TestDelimiter jne ProcessName NameIsDone:
mov stosb dec jmp
al., 0
;nazwa pliku zakończona zerem
si SkipDelimiters
;wskazuje na poprzedni znak ;spróbuj ponownie
;kiedy osiągnięto koniec lini poleceń, zobacz czy obie nazwy pliku zostały określone assume ds.: dseg EndOfCmdLn:
mov ax, es ;DS. wskazuje DSEG mov ds., ax ;Jesteśmy na końcu pliku wiec przez DOS jest wymagane zakończenie zerem GotName: mov ax, es ;DS. wskazuje DSEG mov ds., ax ;zobacz czy nazwa pliku została dostarczona do lini poleceń ;jeśli nie zachęć użytkownika do podania i odczytaj ją z klawiatury cmp GotName1, 0 ;dostarczono nazwę numer 1? jnz HasName1 mov al., ‘1’ ;nazwa pliku numer 1 lea si, FileName1 call GetName ;pobranie pliku numer 1 HasName1: cmp GotName2, 0 ;dostarczono nazwę pliku numer 2? jnz HasName2 mov al., ‘2’ ;jeśli nie odczyt z klawiatury lea si, FileName2 call GetName ;Okay, mamy obie nazwy plików, teraz otwieramy pliki i kopiujemy plik źródłowy do pliku przeznaczenia HasName2: mov ah, 3dh mov al., 0 ;otwarcie pliku do odczytu lea dx, FileName1 ;plik do otwarcia int 21h jnc Goodopen1 print byte ‘Nie można otworzyć pliku, program przerwany...’, cr, lf, 0 jmp PgmExit ;jeśli plik źródłowy został otwarty z powodzeniem, zachowujemy uchwyt pliku GoodOpen1: mov FileHandle1, ax ;zachowanie uchwytu pliku ;Otwarcie (właściwie CREATE) drugiego pliku mov ah, 3ch ;tworzenie pliku mov cx, 0 ;standardowy atrybut lea dx, Filename2 ;plik do otwarcia int 21h jnc GoodCreate ;Notka: poniższe kody błędów zależą od tego, że DOS automatycznie zamyka każdy otwarty plik źródłowy ;kiedy program się kończy print byte cr, lf byte ‘Nie można stworzyć nowego pliku, operacja przerwana’ byte cr, lf, lf, 0 jmp PgmExit GoodCreate: mov FileHandle2, ax ;zachowanie uchwytu pliku ;teraz przetwarzamy pliki CopyLoop: mov ah,3Fh ;DOS’owy opcod odczytu mov bx, fileHandle1 ;odczyt z pliku numer 1 mov cx,512 ;odczyt 512 bajtów lea dx, buffer ;bufor dla pamięci int 21h jc BadRead mov bp, ax ; zachowanie # odczytanego bajtu cmp ConvertLC, 0 jz NoConversion ;Konwersja wszystkich małych liter w buforze na duże mov cx, 512 lea si, Buffer
;opcja konwersji aktywna?
mov
di, si
ConvertLC2UC:
NoConv:
lodsb cmp jb cmp ja and stosb loop
al., ‘a’ NoConv al., ‘z’ NoConv al., 5fh ConvertLC2UC
NoConversion: mov mov mov lea int jc cmp jnz cmp jz jmp jDiskFull: jmp ;różne komunikaty o błędach BadRead print byte byte byte jmp BadWrite:
DiskFull: AtEOF
PgmExit: MainPgm TestDelimiter
xit: TestDelimiter
print byte byte byte jmp
ah, 40h bx, FileHandle2 cx, bp dx, buffer 21h BadWrite ax, bp jDiskFull bp, 512 CopyLoop AtEOF DiskFull
;DOS’owy opcod zapisu ;zapis do pliku numer 2 ;zapis jednak wielu bajtów ;bufor na pamięć ;zapisano wszystkie bajty? ;czy odczytano te 512 bajtów
cr, lf ‘Błąd podczas odczytu pliku źródłowego, przerwanie ‘ ‘operacji ‘, cr, lf, 0 AtEOF cr, lf „Błąd podczas zapisu pliku przeznaczenia, przerwana ‘ ‘operacja’, cr, lf, 0 AtEOF
print byte cr, lf byte „Błąd, dysk pełny. Operacja przerwana’, cr, lf,0 mov bx, FileHandle1 ;zamknięcie pierwszego pliku mov ah, 3Eh int 21h mov bx, FileHandle2 ;zamknięcie drugiego pliku mov ah, 3Eh int 21h ExitPgm endp proc cmp je cmp je cmp je cmp je cmp ret endp
near al., ‘ ‘ xit al., ‘ , ‘ xit al., Tab xit al., ‘ ; ‘ xit al., ‘ = ‘
;GetName – odczytuje nazwę pliku z klawiatury, Na wejściu, AL. zawiera numer nazwy pliku a DI wskazuje ;bufor w ES gdzie musi być przechowywany plik zakończony zerem. GetName proc near print byte ‘Wprowadź numer nazwy pliku:’ , 0 putc mov al., ‘ : ‘ putc gets ret GotName endp cseg ends dseg
segment byte public ‘data’
PSP FileName1 FileName2 FileHandle1 FileHandle2 GotName1 GotName2 ConvertLC Buffer
word byte byte word word byte byte byte byte
dseg sseg stk sseg
ends segment byte stack ‘stack’ word 0ffh dup(?) ends
zzzzzzseg LastBytes Zzzzzzseg
segment para public ‘zzzzzz’ byte 16 dup (?) ends end MainPgm
? 128 dup (?) 128 dup (?) ? ? ? ? ? 512 dup (?)
;nazwa pliku źródłowego ;nazwa pliku przeznaczenia
Jak można zauważyć, jest wymagającej więcej wysiłku przetwarzanie parametrów lini poleceń niż rzeczywiste kopiowanie plików. 13.3.13 ARGC I ARGV Standardowa Biblioteka UCR dostarcza dwóch podprogramów, argc i argv, które pozwalają na łatwy dostęp do parametrów lini poleceń. Argc (licznik argumentów) zwraca liczbę pozycji w lini poleceń. Argv (wektor argumentów0 zwraca wskaźnik do określonej pozycji w lini poleceń Podprogramy te rozrywają linię poleceń w leksemach używając standardowych ograniczników. W konwencji przed MS-DOS , argc i argv traktowane były jako łańcuchy otoczone cudzysłowami w lini poleceń, jako pojedyncze pozycje lini poleceń. Argc zwróci w cx liczbę pozycji lini poleceń. Ponieważ MS-DOS nie wprowadza nazwy programu do lini poleceń, ten licznik również nie wlicza nazwy programu. Co więcej, operandy przekierowania („>nazwa pliku” i „
dseg ArgCnt dseg cseg
segment para public ‘data’ word 0 ends segment para public ‘code’ assume cs: cseg, ds.: dseg Main proc mov ax, dseg mov ds., ax mov es, ax ;Musimy wywołać podprogram inicjalizacyjny menadżera pamięci jeśli używamy jakiegoś podprogramu który ;wywołuje malloc! ARGV jest dobrym przykładem podprogramu który wywołuje malloc. meminit
PrintCmds:
argc jcxz mov printf byte dword
Quit ArgCnt , 1
;pobrania licznika argumentów lini poleceń ;Wyjście jeśli nie ma argumentów lini poleceń ;inicjalizacja licznika lini poleceń ;drukuj pozycję
„\n%2d: „, 0 ArgCnt
Quit: Main cseg
mov ax, ArgCnt argv puts inc ArgCnt loop PrintCmds putcr ExitPgm endp ends
sseg stk sseg
segment para stack ‘ stack’ byte 1024 dup („stack”) ends
;pobranie następnego argumentów ;Przesunięcie na następny argument ;powtarzanie dla każdego argumentu ;makro DOS dla wyjścia z programu
;zzzzzzseg jest wymagany przez podprogram standardowej biblioteki zzzzzzseg LastBytes Zzzzzzseg
segment para public ‘zzzzzz’ byte 16 dup (?) ends end Main
13.4 PODPROGRAMY PLIKÓW I/O STANDARDOWEJ BIBLIOTEKI UCR Chociaż programy plików I/O MS-DOS nie są złe, Standardowa Biblioteka UCR dostarcza pakietu plików I/O, które czynią zblokowanie sekwencyjne tak łatwym jak symbole w plikach I/O. Ponadto , przy małym wysiłku możemy użyć wszystkich podprogramów StdLib takich jak printf, print, puti, puth, putc, getc, gets itd. Kiedy wykonujemy plik I/O. Znacznie upraszcza to działanie na plikach tekstowych w języku asemblera. Zauważmy, że zapis ukierunkowany lub binarny I/O jest prawdopodobnie najlepsze co zostało po czystym DOS’ie, jeśli w dowolnym czasie chcesz wykonać dostęp swobodny wewnątrz pliku. Podprogramy Biblioteki Standardowej w rzeczywistości tylko wspomagają sekwencyjne pliki tekstowe I/O. Niemniej jednak jest to najpowszechniejsza postać plików I/O, więc podprogramy Biblioteki Standardowej są rzeczywiście całkiem użyteczne Biblioteka Standardowa. UCR dostarcza ośmiu podprogramów plików I/O: fopen, fcreate, fgetc, fread, fputc i fwrite. Fgetc i fputc odtwarzają po kolei znaki I/O, fread i fwrite pozwalają nam odczytać i zapisać bloki danych, pozostałe cztery funkcje wykonują oczywiste działania DOS Standardowa Biblioteka UCR używa specjalnych zmiennych plikowych do śledzenia operacji pliku. Jest to specjalne typy rekordowe, FileVar zadeklarowany w stdlib.a. Kiedy używamy podprogramów
plików I/O StdLib musimy stworzyć zmienną typu FileVar dla każdego pliku, który musimy otworzyć w tym samym czasie. Jest to bardzo proste, używamy definicji w postaci: MyFileVar FileVar {} Proszę zauważyć, że zmienna plikowa Biblioteki Standardowej nie jest tym samym co uchwyt pliku DOS’a. Jest to struktura, która zawiera uchwyt pliku DOS, bufor 9dla zblokowanych I/O i różne indeksy i zmienne stanu. Wewnętrzna struktura tego typu nie jest interesująca (pamiętajmy o hermetyzacji danych!) z wyjątkiem implementatora podprogramów pliku. Będziemy przekazywali adres tej zmiennej plikowej do różnych podprogramów plików I/O Standardowej Biblioteki.
13.4.1 FOPEN Parametry wejściowe:
ax – tryb otwarcia pliku 0 – plik otwarty do odczytu 1 – plik otwarty do zapisu dx:si – wskazuje łańcuch zakończony zerem zawierający nazwę pliku es:di – wskazuje na zmienną plikową StdLib Parametry wyjściowe: Jeśli flaga przeniesienia jest ustawiona, ax zawiera zwracany przez DOS kod błędu Fopen otwiera sekwencyjny plik tekstowy do odczytu lub zapisu. W odróżnieniu od DOS’a , nie możemy otworzyć pliku do zapisu lub odczytu. Co więcej, jest to sekwencyjny plik tekstowy , który nie wspiera dostępu swobodnego. Zauważmy, że plik musi istnieć lub fopen zwróci błąd. Jest to prawda, nawet kiedy otworzymy plik do zapisu. Zauważ, że jeśli otworzymy plik do zapisu a plik ten już istnieje , dane zapisane do tego pliku napiszą dane istniejące Kiedy zamykamy plik, dane pojawiające się w pliku po zapisanych danych będą tam jeszcze. Jeśli chcemy usunąć istniejące plik przed zapisaniem danych do niego, użyjemy funkcji fcreate. 13.4.2 FCREATE Parametry wejściowe: Parametry wyjściowe:
dx:si - wskazuje łańcuch zakończony zerem zawierający nazwę pliku es:di – wskazuje zmienną plikową StdLib jeśli flaga przeniesienia jest ustawiona, ax zawiera zwracany przez DOS kod błędu
Fcreate tworzy nowy plik i otwiera go do zapisu. Jeśli plik już istnieje, fcreate usuwa istniejący plik i tworzy nowy. Inicjalizuje zmienną plikową wyjściową, ale poza tym jest identyczny z funkcją fopen 13.4.3 FCLOSE Parametry wejściowe: Parametry wyjściowe:
es:di – wskazuje zmienną plikową StdLib Jeśli flaga przeniesienia jest ustawiona, ax zawiera zwracany przez DOS kod błędu
Fclose zamyka plik i uaktualnia informacje porządkowe.. jest bardzo ważne aby zamknąć wszystkie pliki otwarte przez fopen lub fcreate używając tej funkcji. Kiedy robimy wywołanie pliku DOS, i zapomnimy zamknąć plik, DOS automatycznie zrobi to za nas kiedy nasz program się skończy. Jednak, podprogramy StdLib chowają dane w buforach wewnętrznych, a funkcja fclose automatycznie czyści te bufory na dysku. Jeśli zakończymy program bez wywołania fclose, możemy zgubić jakieś zapisane dane w pliku ale jeszcze nie przekazane z wewnętrznego bufora na dysk. Jeśli jesteśmy w środowisku gdzie jest możliwość przerwania przez kogoś programu ,bez dania nam możliwości zamknięcia pliku, powinniśmy wywołać podprogram fflush (następna sekcja)c dla regularnego unikania gubienia dużej ilości danych. 13.4.4 FFLUSH Parametry wejściowe: Parametry wyjściowe:
es:di – wskazuje zmienną plikową StdLib jeśli flaga przeniesienia jest ustawiona, ax zawiera zwracany przez DOS kod błędu
Podprogram ten bezpośrednio zapisuje dane w wewnętrznym buforze na dysku. Zauważmy, że powinniśmy tylko używać tego podprogramu wraz z plikami otwartymi do zapisu (lub otwartymi przez fcreate). Jeśli zapiszemy dane do pliku a potem musimy zostawić plik otwarty, ale nieaktywny, w tym samym okresie czasu powinniśmy wykonać operację opróżnienia w przypadku programu zamkniętego nienormalnie. 13.4.5 FGETC
Parametry wejściowe: Parametry wyjściowe:
es:di – wskazuje zmienną plikową StdLib jeśli flaga przeniesienia jest wyzerowana, al. zawiera odczytany znak z pliku. Jeśli flaga przeniesienia jest ustawiona, ax zawiera zwracany przez DOS kod błędu Ax będzie zawierał zero jeśli spróbujemy odczytać poza końcem pliku.
Fgetc odczytuje pojedynczy znak z pliku i zwraca ten znak w rejestrze al. w odróżnieniu od funkcji DOS, pojedynczy znak I/O używający fgetc jest względnie szybszy ponieważ podprogramy StdLib używają zblokowanych I/O. Oczywiście wielokrotne wywołanie fgetc nie będzie szybsze niż wywołanie fread, ale wydajność nie jest zła. Fgetc jest bardzo elastyczna. Jak zobaczymy, możemy przekierować podprogramy wejściowe StdLib ,aby odczytywały dane z pliku używając fgetc. Pozwala to nam zastosować podprogramy wyższego poziomu takie jak gets i getsm kiedy odczytujemy dane z pliku. 13.4.6 FREAD Parametry wejściowe: Parametry wyjściowe:
es:di – wskazuje zmienną plikową StdLib dx:si – wskazuje bufor danych wejściowych cx - zawiera bajt licznika Jeśli flaga przeniesienia jest wyzerowana, ax zawiera rzeczywistą liczbę bajtów odczytanych z pliku. Jeśli flaga przeniesienia jest ustawiona, ax zawiera zwracany przez DOS kod błędów.
Fread jest bardzo podobna do DOS’owego polecenia read. Pozwala nam odczytać blok bajtów, zamiast jednego bajtu, z pliku. Zauważmy, że jeśli wszystko co chcemy zrobić to odczytanie bloku bajtów z pliku, DOS’owska funkcja jest odrobinę wydajna niż fread. Jednak jeśli mamy do odczytania mieszankę bajtów pojedynczych i wielu bajtów, kombinacja fread i fgetc działa bardzo dobrze. Podobnie jak przy operacji odczytu DOS, jeśli liczony bajt w ax nie zgadza się z wartością przekazaną w rejestrze cx, wtedy odczytamy pozostałe bajty w pliku. Jeśli tak się zdarzy , następne wywołanie fread lub fgetc zwróci błąd końca pliku (flaga przeniesienia będzie ustawiona a ax będzie zawierał zero). Zauważmy, ze fread nie zwraca EOF, chyba że zostało odczytane zero bajtów z pliku. 13.4.7 FPUTC Parametry wejściowe: Parametry wyjściowe:
es:di – wskazuje zmienna plikową StdLib al. – zawiera znak do zapisania w pliku Jeśli flaga przeniesienia jest ustawiona, ax zawiera zwracany przez DOS kod błędu
Fputc zapisuje pojedynczy znak (a al) do pliku określonego przez zmienną plikową , której adres jest w es:di. Wywołanie to po prostu dodaje znak w al. do wewnętrznego bufora (część zmiennej plikowej) dopóki bufor nie będzie pełny. Gdy tylko bufor jest wypełniony lub wywołujesz fflush ( zamykamy plik fclose), podprogramy plików I/O zapisują dane na dysk. 13.4.8 FWRITE Parametry wejściowe:
es:di – wskazuje zmienną plikową StdLib dx:si – wskazuje bufor danych wyjściowych cx – zawiera zliczony bajt Parametry wyjściowe: Jeśli flaga przeniesienia jest wyzerowana, ax zawiera rzeczywistą liczbę bajtów zapisanych do pliku Jeśli flaga przeniesienia jest ustawiona, ax zawiera zwracany przez DOS, kod błędów Podobnie jak fread, fwrite działa na blokach bajtów. Pozwala nam zapisać blok bajtów do pliku otwartego do zapisu przez fopen lub fcreate .
13.4.9 PRZEKIEROWANIE NA PORTY I/O PRZEZ PODPROGRAMY PLIKÓW I/O STDBLIB Standardowa biblioteka UCR dostarcza niewielu podprogramów plików I/O .Na przykład Fputc i fwrite są jedynie dwoma podprogramami wyjściowymi .Standardowa biblioteka języka C (na której oparta jest Standardowa Biblioteka UCR) dostarcza wiele podprogramów takich jak fprintf, fputs, fscanf itp. Żaden z nich nie jest potrzebny w Standardowej Bibliotece UCR , ponieważ biblioteka UCR dostarcza mechanizmu przekierowania na porty I/O , który pozwala nam na ponowne użycie wszystkich istniejących podprogramów I/O dla wykonania pliku I/O.
Podprogram putc Standardowej Biblioteki UCR składa się z pojedynczej instrukcji jmp. Instrukcja ta przekazuje sterowanie do jakiegoś rzeczywistego podprogramu wyjściowego poprzez pośredni adres wewnętrzny do kodu putc. Zwykle ta zmienna wskaźnikowa wskazuje kawałek kodu który zapisuje znak w rejestrze al. do standardowego urządzenia wyjściowego DOS. Jednak Biblioteka Standardowa dostarcza również czterech podprogramów, które pozwalają nam manipulować tym pośrednim wskaźnikiem . Przez zmianę tego wskaźnika, możemy przekierować wyjście z jego aktualnego podprogramu do podprogramu jaki wybraliśmy, Wszystkie podprogramy wyjściowe Biblioteki Standardowej (tj. printf, puti, puth, puts) wywołują putc do wyprowadzenia pojedynczego znaku. Dlatego też przekierowanie podprogramu putc wpływa na wszystkie podprogramy wyjściowe. Podobnie podprogram getc nie jest niczym innym jak skokiem pośrednim jmp, którego zmienna wskaźnikowa zwykle wskazuje kawałek kodu który odczytuje dane ze standardowego wejścia. Ponieważ wszystkie podprogramy wejściowe Biblioteki Standardowej wywołują funkcję getc do odczytu każdego znaku, możemy przekierować plik wejściowy w sposób identyczny do pliku wyjściowego. GetOutAdrs, SetOutAdrs, PushOutAdrs i PopOutAdrs z Bibliotek Standardowej są czterema głównymi podprogramami, które manipulują wskaźnikiem przekierowania wyjścia. GetOutAdrs zwraca adres bieżącego podprogramu wyjściowego w rejestrach es:di. SetOutAdrs odwrotnie, oczekuje, że przekażemy mu adres nowego podprogramu wyjściowego w rejestrach es:di i przechowuje ten adres we wskaźniku wyjściowym. PushOutAdrs i PopOutAdrs odkładają i zdejmują wskaźnik z wewnętrznego stosu. Nie używają one stosu sprzętowego 80x86. Jesteśmy ograniczeni do małej ilości odłożeń i zdjęć. Generalnie nie powinniśmy liczyć na możliwość odłożenia więcej niż czterech tych adresów na wewnętrznym stosie bez jego przepełnienia. GetInAdrs, SetInAdrs, PushInAdrs i PopInAdrs są uzupełniającymi podprogramami dla wektora wejściowego. Pozwalają nam one na manipulowanie wskaźnikiem podprogramów wejściowych. Zauważmy, że stos dla PushInAdrs / PopInAdrs nie jest taki sam jak stos dla PushOutAdrs / PopOutAdrs. Odkładanie i zdejmowanie z tych dwóch stosów jest niezależne jedno od drugiego. Zazwyczaj, wskaźnik wyjściowy (do którego będziemy się odtąd odnosili jako do łącza wyjściowego) wskazuje podprogram PutcStdOut Biblioteki Standardowej. Dlatego też możemy przywrócić łącze wyjściowe do jego normalnego stanu inicjalizującego w każdej chwili poprzez wykonanie instrukcji: mov di, seg SL_PutcStdOut mov es, di mov di, offset SL_PutcStdOut SetOutAdrs Podprogram PutcStdOut zapisuje znak z rejestru al. do standardowego wyjścia DOS’a, które samo może być przekierowane do jakiegoś pliku lub urządzenia (używając DOS’owego operatora przekierowania „>”). Jeśli chcemy upewnić się, że naszym urządzeniem wyjściowym jest monitor, możemy zawsze wywołać podprogram PutcBIOS, który wywołuje BIOS bezpośrednio dla znaku wyjściowego. Możemy zmusić wszystkie wyjścia Biblioteki Standardowej do urządzenia standardowego błędu używając takiej sekwencji kodu: mov di, seg SL_PutcBIOS mov es, di mov di, offset SL_PutcBIOS SetOutAdrs Generalnie nie możemy oswobodzić łącza wyjściowego poprzez przechowanie wskaźnika do podprogramu na szczycie jakiegoś wskaźnika ,który tam był a potem przywrócić łącze do PutcStdOut po ukończeniu. Kto wie czy łącze przede wszystkim wskazywałoby na PutcStdOut.? Najlepszym rozwiązaniem jest użycie podprogramów Biblioteki Standardowej PushOutAdrs i PopOutAdrs do przechowania i przywrócenia poprzedniego łącza. Poniższy kod demonstruje łagodniejszy sposób modyfikacji łącza wyjściowego: PushOutAdrs ;zachowanie bieżącego podprogramu wyjściowego mov di, seg Output_Routine mov es, di mov di, offset Output_Routine SetOutAdrs PopOutAdrs ;przywrócenie poprzedniego podprogramu wyjścia Obsługa wejścia w podobny sposób używa odpowiednich łączy wejściowych dla dostępu do podprogramu i podprogramów SL_GetStdOut i SL_GetBIOS. Pamiętajmy zawsze, że jest ograniczona liczba wejść do łączy stosu wejścia i wyjścia , i ile pozycji odłożymy na te stosy bez zdejmowania czegokolwiek.
Dla przekierowania wyjścia do pliku (lub przekierowania wejścia z pliku) musimy najpierw napisać krótki podprogram, który zapisze (odczyta) pojedynczy znak z (do) pliku. Jest to bardzo łatwe.. Kod dla podprogramu dla danych wyjściowych do pliku opisany przez zmienną plikową OutputFile to: ToOutput proc far push es push di ;Ładuje ES:DI adresem zmiennej OutputFile. Kod ten zakłada, że OutputFile jest typu FileVar, a nie ;wskaźnikiem do zmiennej typu FileVar mov di, seg OutputFile mov es, di mov di, offset OutputFile ;wprowadzany znak z AL. do pliku jest opisany przez „OutputFile” fputc pop pop ret endp
ToOutput
di es
Teraz, tylko z jednym dodatkowym kawałkiem kodu, możemy zacząć zapisywać dane do pliku wyjściowego używając podprogramów wyjściowych Biblioteki Standardowej. Poniżej mamy krótki fragment kodu, który przekierowywuje łącze wyjściowe do powyższego podprogramu „ToOutput”: SetOutFile
proc push push
es di
PushOutAdrs mov di, seg ToOutput mov es, di mov di, offset ToOutput SetOutAdrs
;zachowanie bieżącego łącza wyjściowego
pop di pop es ret SetOutFile endp Nie ma potrzeby oddzielnego podprogramu przywracającego łącze wyjściowe do jego poprzedniej wartości; PopOutAdrs wykona to zadanie. 13.4.10 PRZYKŁAD PLIKU I/O Poniższy przykład łączy wszystko razem z poprzednich kilku sekcji .Jest to prosty program , który dodaje numery lini do pliku tekstowego. Program ten wymaga dwóch parametrów lini poleceń: pliku wejściowego i pliku wyjściowego. Kopiuje plik wejściowy do pliku wyjściowego dołączając numery lini na początku każdej lini w pliku wyjściowym. Kod ten demonstruje zastosowanie argc, argv, podprogramów plików I/O Biblioteki Standardowej i przekierowania I/O ;Program ten kopiuje plik wejściowy do pliku wyjściowego i dodaje numery lini podczas kopiowania pliku include stdlib.a includelib stdlib.lib dseg
segment para public ‘data’
ArgCnt LineNumber DOSErrorCode InFile OutFile
word word word dword dword
0 0 0 ? ?
;wskaźnik do nazwy pliku wejściowego ;wskaźnik do nazwy pliku wyjściowego
InputLine OutputFile InputFile
byte 1024 dup (0) FileVar {} FileVar {}
dseg
ends
;bufor danych Wejścia / Wyjścia
cseg
segment para public ‘code’ assume cs : cseg, ds. : dseg ;ReadLn – Odczyt lini tekstu z pliku wejściowego i przechowanie danych w buforze InputLine: ReadLn
proc push push push push push
ds. es di si ax
mov mov mov lesi
si, dseg ds., si si, offset InputLine InputFile
fgetc jc cmp je mov inc cmp jne dec cmp jne dec
RdLnDone ah, 0 RdLnDone ds.:[si], al. si al., lf GetLnLp si byte ptr ds.:[si –1], cr RdLnDone si
GetLnLp:
RdLnDone:
;jeśli jakiś dziwaczny błąd ;sprawdzenie dla końca pliku ;Notka: przeniesienie jest ustawione ;EOLN? ;cofnięcie przed LF ;CR przed LF? ;jeśli tak, przeskocz także
mov byte ptr ds.:[si], 0 ;zakończenie zerem pop ax pop si pop di pop es po ds. ret ReadLn endp ;MyOutput – zapis pojedynczego znaku z AL. do pliku wyjściowego MyOutput proc far push es push di lesi OutputFile fputc pop di pop es ret MyOutput endp ;Program główny , który wykonuje całą pracę Main proc mov ax, dseg mov ds., ax mov es, ax
;Musimy wywołać podprogram inicjalizujący menadżera pamięci, jeśli używamy jakiegoś podprogramu, który ;wywołuje malloc! ARGV jest dobrym przykładem podprogramu wywołującego malloc meminit ;Oczekujemy, że program będzie wywoływany jak następuje ; fileio file1, file2 ;w przeciwnym razie mamy błąd argc cmp cx, 2 ;musimy mieć dwa parametry je Got2Parms BadParms: print byte „Użycie : FILEIO infile, outfile”, cr, lf, 0 jmp Quit ;Okay, mamy dwa parametry, szczęśliwie mają poprawne nazwy. ‘Pobieramy kopię nazw plików i przechowujemy jako wskaźnik do nich Got2Parms: mov ax, 1 ;pobranie nazwy pliku wejściowego argv mov word ptr InFile, di mov word ptr InFile+2, es mov ax, 2 ;pobranie nazwy pliku wyjściowego argv mov word ptr OutFile, di mov word ptr OutFile+2, es ;Wyprowadzenie nazwy pliku na standardowe urządzenie wyjściowe printf byte „Plik wejściowy: %^s\n” byte „plik wyjściowy: %^s\n”, 0 dword InFile, OutFile ;Otwarcie pliku wejściowego: lesi InputFile mov dx, word ptr InFile+2 mov si, word ptr InFile mov ax, 0 fopen jnc GoodOpen mov DOSErrorCode, ax printf byte „Nie można otworzyć pliku wejściowego, DOS: %d\n”, 0 dword DOSErrorCode jmp Quit ;Stworzenie nowego pliku dla wyjścia: GoodOpen: lesi OutputFile mov dx, word ptr OutFile+2 mov si, word ptr OutFile fcreate jnc GoodCreate mov DOSErrorCode, AX printf byte „Nie można otworzyć pliku wyjściowego, DOS : %d\n”, 0 dword DOSErrorCode jmp Quit ;Okay, zachowamy łącze wyjściowe i przekierowanie wyjścia GoodCreate: PushOutAdrs lesi MyOutput SetOutAdrs WhlNotEOF: inc LineNumber ;Okay odczytujemy linię wejściową od użytkownika: call ReadLn jc BadInput ;okay, przekierowanie wyjścia do naszego pliku wyjściowego i zapis ostatniej odczytanej lini
;poprzedzonej numerem lini printf byte „%4d: %s\n”, 0 dword LineNumber, InputLine jmp WhlNotEOF BadInput: push ax ;zachowanie kodu błędu PopOutAdrs ;przywrócenie łącza wyjściowego pop ax ;odzyskanie kodu błędu test ax, ax ;błąd EOF? (AX = 0) jz CloseFiles mov DOSErrorCode, ax printf byte „Błąd wejścia, DOS: %d\n”, 0 dword LineNumber ;okay, zamykamy plik i wychodzimy: CloseFiles: lesi OutputFile fclose lesi InputFile fclose Quit: Main cseg
ExitPgm endp 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
;makro DOS’a do zamknięcia programu
13.8 PODSUMOWANIE MS-DOS i BIOS dostarczają wiele usług systemowych, które sterują sprzętem na PC. Dostarczają one niezależnego od sprzętu i elastycznego interfejsu. Niestety, PC rozwinął się trochę od dni oryginalnego 5 Mhz 8088 IBM PC. Wiele funkcji DOS i BIOS jest teraz przestarzałych, wypieranych przez nowsze funkcje. Zakładając wsteczną kompatybilność, MS-DOS i BIOS generalnie wspierają wszystkie starsze i przestarzałe funkcje tak samo jak nowsze. Jednakże nasz program nie powinien używać funkcji przestarzałych. BIOS dostarcza wiele usług powiązanych ze sterowaniem urządzeniami takimi jak monitor; port drukarki, klawiatura, port szeregowy, zegar czasu rzeczywistego itp. Opis usług BIOS dla tych urządzeń pojawia się w poniższych sekcjach: • „INT 5 - Zrzut Ekranowy” • „INT 10h - Usługi Video” • INT 11h – Konfiguracja komputera’ • „INT 12h – Określenie rozmiaru pamięci” • „INT 13h – Usługa obsługi dysków” • „INT 14h – Obsługa złącza szeregowego I/O” • „INT 15h – Usługi Dodatkowe” • „INT 16h – Obsługa klawiatury” • „INT 17h – Obsługa drukarki” • „INT 18h – BASIC” • „INT 19h – Gorący restart systemu” • „INT 1Ah – Usługa zegara czasu rzeczywistego” MS-DOS dostarcza kilka różnych typów usług. Ten rozdział koncentrował się na usługach plików I/O dostarczonych przez MS-DOS. W szczególności rozdział ten zajmował się implementacją działania wydajnych plików I/O używających zblokowanych I/O. Uczy jak wykonać pliki I/O i inne operacje MS-DOS.
• • • • • • • • •
„Sekwencja wywołania MS-DOS” „Funkcje zorientowane znakowo MS-DOS” „Przestarzałe funkcje zapisujące dane MS-DOS” „Funkcje daty i czasu MS-DOS” „Funkcje zarządzania pamięcią MS-DOS’ „Funkcje sterowania procesem MS-DOS” „Nowe funkcje zapisujące dane MS-DOS” „Przykłady pliku I/O” „Zblokowane pliki I/O”
Dostęp do parametrów lini poleceń jest ważną operacją wewnątrz aplikacji MS-DOS. DOS’owy PSP zawiera linię poleceń i kilka innych kawałków ważnych informacji. Uczy o różnych polach w PSP i pokazuje jak uzyskać dostęp do parametrów lini poleceń. • • •
„Program Segment Prefix” „Dostęp do parametrów lini poleceń” „ARGC i ARGV”
Oczywiście, Standardowa Biblioteka UCR dostarcza również kilka podprogramów pliku I/O. Rozdział ten zamyka się przez opisanie kilku podprogramów pliku I/O StdLib wraz z ich zaletami i wadami • „Fopen” • „Fcreate” • „Fclose” • „Fflush” • „Fgetc” • „Fread” • „Fputc • „Fwrite” • „Przekierowanie I/O przez podprogramy I/O StdLib • „Przykład pliku I/O”
13.9 PYTANIA 1) Jak są wywoływane podprogramy BIOS’a? 2) Jaki podprogram BIOS jest używany do zapisu znaku do: a) monitora b) portu szeregowego c) portu drukarki 3) Kiedy usługa transmisji szeregowej lub odbioru wraca do kodu wywołującego, stan błędu jest zwracany w rejestrze AH. Jednakże jest problem z wartością zwracaną. Co to za problem? 4) Wyjaśnij jak można przetestować klawiaturę, aby zobaczyć czy klawisz jest dostępny. 5) Co jest złego w stanie funkcji przesunięcia klawiatury/ 6) Jak specjalne kody kluczy (nie zwracające kodów ASCII) są zwracane przez funkcję odczytu klawiatury ? 7) Jak wysyłamy znak do drukarki? 8) Jak odczytujemy czas rzeczywisty zegara systemowego? 9) Podano, że RTC zwiększa 32 bitowy licznik co 55 ms, jak długo będzie działał system zanim wystąpi przepełnienie licznika? 10) Dlaczego powinniśmy resetować zegar ,kiedy odczytujemy zegar, określając, że licznik został przepełniony? 11) Jak program asemblerowy wywołuje MS-DOS 12) Gdzie generalnie są przekazywane parametry do MS-DOS? 13) Dlaczego są dwa zbiory funkcji zapisujących dane na dysku w MS-DOS? 14) Gdzie mogą być znaleziona linia poleceń DOS’a? 15) Jakie jest zadanie środowiska obszaru ciągów? 16) Jak można określić ilość pamięci dostępnej do zastosowania przez nasz program? 17) Co jest bardziej wydajne : znakowe I/O lub zblokowane I/O? Dlaczego? 18) Jaki jest dobry rozmiar bloku dla zblokowanych I/O? 19) Dlaczego nie możemy użyć zblokowanych I/O w plikach o dostępie swobodnym? 20) Wyjaśnij jak zastosować polecenie SEEK do przesunięcia wskaźnika pliku 128 bajtó wstecz w pliku z bieżącej pozycji pliku.
21) Gdzie zazwyczaj zwracany jest stan błędu po wywołaniu DOS? 22) Dlaczego jest trudno użyć zblokowanych I/O w plikach o dostępie swobodnym? 23) Opisz jak można zaimplementować zblokowane I/O w plikach otwartych z sekwencyjnym dostępem do odczytu i zapisu. 24) Jakie są dwa sposoby w jaki możemy uzyskać adres PSP? 25) Jak możemy określić, że osiągnęliśmy koniec pliku kiedy używamy funkcji pliku I/O MS-DOS? Kiedy używamy funkcji Biblioteki Standardowej UCR?
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
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Ł CZTERNASTY: ARYTMETYKA ZMIENNO PRZECINKOWA Chociaż liczby całkowite dostarczają dokładnej reprezentacji dla wartości liczbowych, cierpią one z powodu dwóch głównych wad: niemożność przedstawiania liczb ułamkowych i ograniczenie zakresu dynamiki. Arytmetyka zmienno przecinkowa rozwiązuje te dwa problemy kosztem dokładności i, na niektórych procesorach . szybkości. Większość programistów jest świadomych utraty szybkości związanej z arytmetyką zmienno przecinkową; jednakże są beztrosko nieświadomi problemów z dokładnością. Dla wielu aplikacji korzyści ze zmienno przecinkowości przeważają nad wadami. Jednakże dla właściwego zastosowania arytmetyki zmienno przecinkowej w każdym programie musimy nauczyć się jak działa arytmetyka zmienno przecinkowa. Intel , rozumiejąc znaczenie arytmetyki zmienno przecinkowej w nowoczesnych programach dostarczył wsparcia dla arytmetyki zmienno przecinkowej w najwcześniejszym swoim projekcie 8086 – FPU 8087 (jednostka zmienno przecinkowa lub koprocesor matematyczny). Jednak na procesorach wcześniejszych niż 80486 (lub na 80486sx) procesor zmienno przecinkowy jest urządzeniem opcjonalnym; to znaczy, że nieobecne urządzenie musi być zasymulowane programowo. Rozdział ten zawiera cztery główne sekcje. Pierwsza sekcja omawia arytmetykę zmienno przecinkową z punktu widzenia matematycznego. Druga sekcja omawia binarną postać zmienno przecinkowości powszechnie używanej w procesorach Intela. Trzecia omawia zmienno przecinkowość programową i podprogramy matematyczne ze Standardowej Biblioteki UCR. Czwarta omawia chip 80x87 FPU. 14.0 WSTĘP Ten rozdział zawiera cztery główne sekcje: opis formatu zmienno przecinkowego i operacji (dwie sekcje), omówienie wsparcia zmienno przecinkowości w Bibliotece Standardowej UCR i omówienie 80x87 FPU (jednostki zmienno przecinkowej). Poniższe sekcje które maja znak „•” są niezbędne. Sekcje z „⊗” omawiają tematy zaawansowane, które możemy chcieć opuścić. • Matematyczna arytmetyka zmienno przecinkowa • Formaty zmienno przecinkowe IEEE • Podprogramy zmienno przecinkowe Biblioteki Standardowej UCR • Koprocesor zmienno przecinkowy 80x87 • Instrukcje przesuwania danych FPU ⊗ Konwersje • Instrukcje arytmetyczne • Instrukcje porównań ⊗ Instrukcje stałe ⊗ Instrukcje transcendentalne ⊗ Instrukcje różnorodne ⊗ Operacje całkowite ⊗ Dodatkowe operacje trygonometryczne
14.1
MATEMATYCZNA ARYTMETYKA ZMIENNO PRZECINKOWA
Dużym problem z arytmetyką zmienno przecinkową jest taki, że nie stosuje standardowych zasad. Niemniej jednak wielu programistów stosuje normalne zasady algebraiczne kiedy stosuje arytmetykę zmienno przecinkową. Jest to źródło błędów w wielu programach. Jednym z podstawowych celów tej sekcji jest opisanie ograniczeń arytmetyki zmienno przecinkowej, więc zrozumiemy jak stosować ją właściwie. Zwykłe zasady algebraiczne stosują tylko arytmetykę o nieskończonej precyzji. Rozważmy prostą instrukcję x: = x +1, x jest liczbą całkowitą. Na nowoczesnych komputerach ta instrukcja będzie korzystała ze zwykłych zasad algebry , tak długo dopóki nie wystąpi przepełnienie. To znaczy ta instrukcja jest poprawna tylko
Rysunek 14.1: Prosty format zmienno przecinkowy dla pewnych wartości x (minint <= x < maxint). Większość programistów nie ma z tym problemu, ponieważ są świadomi faktu, że liczby całkowite w programie nie stosują się do standardowych zasad algebraicznych (np. 5 / 2 ≠ 2.5). Liczby całkowite nie stosują się do standardowych zasad algebry ponieważ ich komputerowa reprezentacja ma skończoną liczbę bitów. Nie możemy przedstawić żadnej (całkowitej) wartości powyżej maksymalnej liczby całkowitej lub poniżej minimalnej liczby całkowitej. Wartości zmienno przecinkowe cierpią na ten sam problem, tylko gorzej. Jednak liczby całkowite są podzbiorem liczb rzeczywistych. Dlatego też wartości zmienno przecinkowe muszą przedstawiać taki sam nieskończony zbiór liczb całkowitych. Jednak jest nieskończona liczba wartości pomiędzy dwoma wartościami rzeczywistymi, więc ten problem jest nieskończenie gorszy. Dlatego też mając ograniczone wartości pomiędzy zakresem maksymalnym a minimalnym, nie możemy przedstawić również wszystkich wartości pomiędzy tymi dwoma zakresami. Do przedstawienia liczb rzeczywistych większość formatów zmienno przecinkowych stosuje notację naukową i używa jakiejś liczby bitów do przedstawiania mantysy i mniejszej liczby bitów do przedstawienia wykładnika. Końcowy rezultat jest taki, że liczba zmienno przecinkowa może tylko przedstawiać liczby z określoną liczbą znaczących cyfr. Ma to duży wpływ na to jak działa arytmetyka zmienno przecinkowa. Łatwo zobaczymy wpływ arytmetyki o ograniczonej precyzji, kiedy przyjmiemy uproszczony format dziesiętny zmienno przecinkowy dla naszych przykładów. Nasz format zmienno przecinkowy dostarczy mantysy z trzema znaczącymi cyframi i dziesiętny wykładnik z dwoma cyframi. Mantysy i wykładniki są wartościami ze znakiem (zobacz rysunek 14.1) Kiedy dodajemy i odejmujemy dwie liczby w notacji naukowej, musimy zmodyfikować dwie wartości, żeby ich wykładniki były takie same. Na przykład, kiedy dodajemy 1.23e1 i 4,56e0, musimy zmodyfikować wartości tak, żeby miały takie same wykładniki. Jednym sposobem zrobienia jest tego jest skonwertowanie 4.56e0 do 0.456e1 a potem je dodać. Todaje1.686e1. Niestety wynik nie mieści się w trzech znaczących cyfrach, wiec musimy zaokrąglić lub skrócić wynik do trzech znaczących cyfr. Zaokrąglanie, generalni, tworzy bardziej precyzyjny wynik, więc zaokrąglamy wynik do 1.69e1. Jak widzimy, brak precyzji (liczba cyfr lub bitów w obliczeniu) wpływa na dokładność (poprawność obliczenia) W poprzednim przykładzie mogliśmy zaokrąglić wynik ponieważ mieliśmy cztery znaczące cyfry podczas obliczania. Jeśli nasze obliczenia zmienno są ograniczone do trzech znaczących liczb podczas obliczania, musimy obciąć ostatnią cyfrę mniejszej liczby uzyskując 1.86e1, które jest mniej poprawne. Dodatkowa cyfra dostępna podczas obliczania jest znana jako pozycja chroniona wyniku (lub bit zabezpieczenia w przypadku formatu bitowego). One wielce podnoszą dokładność podczas długiego szeregu obliczeń. Strata dokładności podczas pojedynczego obliczenia zazwyczaj nie jest taka aby się martwić , chyba ,że martwisz się rzeczywiście precyzją swoich obliczeń. Jednakże jeśli obliczamy wartość , która jest wynikiem sekwencji operacji zmienno przecinkowych, błąd może się gromadzić wielce wpływając na sam wynik. Przypuśćmy na przykład, że dodamy 1.23e3 z 1.00e0. Modyfikując liczby tak, żeby ich wykładniki były takie same przed dodawaniem uzyskujemy 1.23e3 + 0.001e3 . Suma tych dwóch wartości , nawet po zaokrągleniu to 1.23e3. To może wydać się nam zupełnie racjonalne; w końcu możemy tylko zajmować się trzema znaczącymi cyframi, dodanie małej wartości nie powinno wcale wpłynąć na wynik. Jednak przypuśćmy, że będziemy dodawać 1.00e0 do 1.23e3 dziesięć razy. Pierwszy raz dodając 1.00e0 do 1.23e3 dostajemy 1.23e0. Podobnie uzyskamy taki sam wynik za drugim, trzecim, czwartym...dziesiątym razem. Z drugiej strony dodając 1.00e0 do samej siebie dziesięć razy , a potem dodając wynik (1.00e1) do 1.23e3 otrzymamy inny wynik , 1.24e3. Jest ważna rzecz do zapamiętania w arytmetyce o ograniczonej precyzji : Porządek wyliczenia może wpływać na precyzję wyniku Uzyskamy bardziej dokładny wynik jeśli odpowiednie wielkości (to znaczy wykładniki) są bliżej jeden drugiego Jeśli wykonujemy szereg operacji wymagających dodawania i odejmowania, powinniśmy zgrupować właściwe wartości.
Inny problem z dodawaniem i odejmowaniem jest taki, że możemy skończyć z fałszywą precyzją. Rozpatrzmy obliczenie 1.23e0 – 1.22e0. Daje to 0.01e0. Chociaż jest to równoważne matematycznie 1.00e-2, ta druga postać wskazuje na to, że ostatnie dwie cyfry są dokładnie zerami. Niestety mamy tylko pojedynczą znaczącą cyfrę tym razem. Rzeczywiście, niektóre FPU lub pakiet oprogramowania zmienno przecinkowego może rzeczywiście mogą wprowadzać losowe cyfry lub bity na najmniej znaczące pozycje. Jest to druga ważna zasada dotycząca arytmetyki o ograniczonej precyzji: Kiedykolwiek odejmujemy dwie liczby z takimi samymi znakami lub dodajemy dwie liczby z różnymi znakami, precyzja wyniku może być mniejsza niż precyzja dostępna w formacie zmienno przecinkowym. Mnożenie i dzielenie nie cierpi z tego samego powodu co dodawanie i odejmowanie ponieważ nie musimy modyfikować wykładników przed tymi działaniami; wszystko co musimy zrobić to dodać wykładniki i pomnożyć mantysy (lub odjąć wykładniki i podzielić mantysy). Mnożenie i dzielenie nie tworzą szczególnie słabych wyników. Jednakże mają one skłonności do zwielokrotniania błędów , które już istnieją w wartości. Na przykład, jeśli pomnożymy 1.23e0 przez dwa, kiedy powinniśmy pomnożyć 1.24e0 przez dwa, wynik jest tym bardziej niedokładny. To daje nam trzecią ważną zasadę kiedy pracujemy z arytmetyką o ograniczonej precyzji: Kiedy wykonujemy łańcuch obliczeń wymagających dodawania, odejmowania mnożenia i dzielenia, próbujemy najpierw wykonać mnożenie i dzielenie. Często, przez zastosowanie normalnych transformacji algebraicznych, możemy ułożyć obliczenia tak, że mnożenie i dzielenie wystąpią najpierw. Na przykład ,przypuśćmy, że chcemy obliczyć x* (y+z). Zwykle dodajemy razem y i z a potem ich sumę mnożymy przez z. Jednakże uzyskamy trochę bardziej dokładny wynik jeśli przetransformujemy x*(y+z) do x*y + x*z i obliczymy wynik, najpierw obliczając mnożenie. Mnożenie i dzielenie też nie są pozbawione problemów. Kiedy mnożymy dwie bardzo duże lub bardzo małe liczby, jest całkiem możliwe wystąpienie przepełnienia lub niedomiaru. Taka sama sytuacja wystąpi kiedy dzielimy małą liczbę przez dużą lub dużą przez małą. Daje to nam czwartą zasadę , którą powinniśmy próbować stosować kiedy mnożymy lub dzielimy wartości: Kiedy mnożymy lub dzielimy zbiór liczb, próbujmy ułożyć mnożenia tak, żeby mnożyć duże i małe liczby razem; podobnie próbujmy dzielić liczby, które maja takie same względne wartości. Porównywanie liczb zmienno przecinkowych jest bardzo niebezpieczne. Ze względu na nieścisłości obecne w obliczeniach (wliczając konwersję ciągu wejściowego na wartość zmienno przecinkową) nie powinniśmy nigdy porównywać dwóch wartości zmienno przecinkowych aby zobaczyć czy są równe. W binarnym formacie zmienno przecinkowym , różne obleczenia , które tworzą taki sam wynik (matematyczny) mogą różnić się w swoich najmniej znaczących bitach. Na przykład dodając 1.31e0 + 1.69e0 powinniśmy otrzymać 3.00e0. podobnie dodając 2.50e0 + 0.50e0 powinniśmy otrzymać 3.00e0. Jednakże porównując (1.31e0+1.69e0) i (2.50e0 + 0.50e0) możemy odkryć, że sumy te nie są równe jedna drugiej. Test dla równości jest pozytywny wtedy i tylko wtedy kiedy wszystkie bity (lub cyfry) w dwóch argumentach są takie same. Ponieważ nie jest to koniecznie prawda ,po dwóch różnych obliczeniach zmienno przecinkowych , które powinny tworzyć taki sam wynik, prosty test na równość może nie działać. Standardowym sposobem dla sprawdzenia równości miedzy liczbami zmienno przecinkowymi jest określenie na jaki błąd (lub jaką tolerancję) pozwolimy w porównaniu i sprawdzamy czy jedna wartość znajduje się wewnątrz zakresu błędu innej. Prostym sposobem zrobienia tego jest użycie testu takiego jak poniższy: if Value1 >= (value2 – błąd) i Value1 <= (Value2 + błąd) then .... innym popularnym sposobem wykonania tego samego porównania jest zastosowanie instrukcji w postaci: if abs (Value1 – Value2) <= błąd then.... Większość tekstów które omawiają porównania zmienno przecinkowe zatrzymuje się bezpośrednio po omówieniu problemu równości , zakładając, że inne formy porównań są zupełnie OK. dla liczb zmienno przecinkowych. To nie jest prawda! Jeśli założymy, że x = y i jeśli x jest wewnątrz y ± błąd, wtedy proste porównanie na poziomie bitowym x i y określi, że x < y jeśli y jest większe niż x ale mniejsze niż y ± błąd. Jednakże w takim przypadku x powinno być potraktowane rzeczywiście jako równe y , nie mniejsze niż y. Dlatego też musimy zawsze porównywać dwie liczby zmienno przecinkowe przy użyciu zakresów, bez względu na rzeczywiste porównanie jakie chcemy wykonać. Próbując porównywać dwie liczby zmienno przecinkowe bezpośrednio może prowadzić do błędu. Dla porównania dwóch liczb zmienno przecinkowych xi y, powinniśmy użyć jedną z poniższych form: = ≠ < ≤
if abs(x-y) <= błąd then.... if abs(x-y) > błąd then..... if (x-y) < błąd then..... if (x-y) <= błąd then...
> if (x-y) > błąd then.... ≥ if (x-y) >= błąd then... Musimy postępować ostrożnie kiedy wybieramy wartość błędu. Powinna to być wartość odrobinę większa niż największa ilość błędów, jakie wkradną się do naszych obliczeń. Dokładna wartość będzie zależała od określonego formatu zmienno przecinkowego jakiego używamy, ale więcej o tym, trochę później. Końcową zasadą tej sekcji jest: Kiedy porównujemy dwie liczby zmienno przecinkowe, zawsze porównujemy jedną wartość aby zobaczyć czy jest ona w zakresie danym przez drugą wartość, plus minus jakaś mała wartość błędu. Jest wiele innych problemów, które mogą wystąpić kiedy stosujemy wartości zmienno przecinkowe. Ten tekst może tylko wskazać główne problemy i uświadomić na fakt, że nie możemy traktować arytmetyki zmienno przecinkowej tak jak rzeczywistej arytmetyki – niedokładności obecne w arytmetyce o ograniczonej precyzji mogą spowodować wiele kłopotów jeśli nie będziemy ostrożni. Dobry tekst o analizie numerycznej lub obliczeniach naukowych może pomóc wypełnić szczegóły, które są poza zakresem tego tekstu. Jeśli będziemy pracowali z arytmetyką zmienno przecinkowa, w jakimś języku, powinniśmy znaleźć czas na przestudiowanie wpływu arytmetyki o ograniczonej na nasze obliczenia. 14.2 FORMAT ZMIENNO PRZECINKOWY IEEE Kiedy Intel planował wprowadzenie koprocesora zmienno przecinkowego dla swoich nowych mikroprocesorów 8086, dość elegancko zrealizowali to inżynierowie elektrycy i fizycy, którzy zaprojektowali chipy, być może nie najlepsi ludzie do wykonania koniecznej analizy numerycznej i do wybrania najlepszej możliwie binarnej reprezentacji dla formatu zmienno przecinkowego. Więc Intel wynajął najlepszego analityka numerycznego, który mógł znaleźć pomysł na format zmienno przecinkowy dla ich FPY 8087.Osoba ta wynajęła innych ekspertów w terenie a trzech z nich (Kahn, Coonan i Stone ) zaprojektowali Intelowski format zmienno przecinkowy. Wykonali tak dobrą robotę projektując Standard Zmienno Przecinkowy KCS, że organizacja IEE zaadoptowała go dla formatu zmienno przecinkowego IEEE. Do wymaganego działania w szerokim zakresie wydajności i precyzji Intel w rzeczywistości wprowadził trzy formaty zmienno przecinkowe: o pojedynczej precyzji, podwójnej precyzji i precyzji podwyższonej. Pojedyncza i podwójna precyzja odpowiadają typom float i double z C lub typom real i double z FORTRAN’a. Intel zmierzał do użycia precyzji podwyższonej dla długiego łańcucha obliczeń. Podwyższona precyzja zawiera 16 dodatkowych bitów które
Rysunek 14.2: Bity formatu pojedynczej precyzji zmienno przecinkowej dla obliczenia mogą być używane jako bity zabezpieczenia przed zaokrąglaniem w dół do wartości podwójnej precyzji, kiedy przechowują wynik. Format pojedynczej precyzji używa uzupełnionej jedynkami 24 bitowej mantysy i ośmiu bitów nadwyżki – 128 wykładnika. Mantysa zazwyczaj przedstawia wartość pomiędzy 1.0 a 2.0. Najbardziej znaczący bit mantysy jest zazwyczaj jedynką i przedstawia wartość na lewo od przecinka dwójkowego. Pozostałe 23 bity mantysy pojawiają się na prawo od przecinak dwójkowego. Dlatego mantysa reprezentuje wartość : 1.mmmmmmm mmmmmmmm mmmmmmmm Znaki „mmmmmm....” przedstawiają 23 bity mantysy. Zapamiętajmy, że pracujemy tu z liczbami binarnymi. Dlatego każda pozycja na prawo od przecinka binarnego przedstawia wartość (zero lub jeden) razy następujące po sobie ujemne potęgi dwójki. Jeden założony bit jest zawsze mnożony przez 20, czyli jeden. Jest tak dlatego, że mantysa jest zawsze większa niż lub równa jeden. Nawet jeśli wszystkie bity mantysy są zerami, założony jeden bit zawsze daje nam zero. Oczywiście, nawet jeśli mieliśmy prawie nieskończoną liczbę bitów jeden po przecinku binarnym nie mogą one zsumować się do dwóch. Jest tak ponieważ mantysa może reprezentować wartości w zakresie od jeden do dwóch.
Chociaż jest nieskończona liczba wartości pomiędzy jeden i dwa ,możemy tylko przedstawić osiem milionów z nich ponieważ mamy 23 bity mantysy ( 24 bity to zawsze jeden). To jest powód niedokładności w arytmetyce zmienno przecinkowej – jesteśmy ograniczeni do 23 bitowej precyzji w obliczeniach wymagających wartości o pojedynczej precyzji zmienno przecinkowej. Mantysa używa formatu uzupełnienia jedynkami zamiast uzupełnienia do dwóch. To znaczy, że 24 bitowa mantysy jest po prostu bez znakową liczbą binarną a bit znaku określa czy wartość ta jest dodatnia czy ujemna. Liczby uzupełnione jedynkami mają niezwykła właściwość, którą są dwie reprezentacje zera 9 z ustawionym bitem znaku i wyzerowanym) . generalnie jest to ważne tylko dla osób projektujących oprogramowanie zmienno przecinkowe lub system sprzętowy. My założymy, że wartość zera ma zawsze wyzerowany znak bitu. Przy przedstawianiu wartości poza zakresem 1.0 do 2.0, wchodzi do gry część wykładnika formatu zmienno przecinkowego. Format zmienno przecinkowy podnosi dwa do potęgi określonej przez wykładnik a potem mnoży mantysę przez tą wartość. Wykładnik jest ośmio bitowy i jest przechowywany w formacie nadmiarowym-127. W formacie tym wykładnik 20 jest reprezentowany przez wartość 127 (7fh). Dlatego też konwersja wykładnika do formatu nadmairu-127 to po prostu dodanie 127 do wartości wykładnika. Stosowanie formatu nadmiaru-127 czyni łatwiejszym porównywanie wartości zmienno przecinkowych. Format pojedynczej precyzji zmienno przecinkowej przybiera postać pokazaną na rysunku 14.2 Z 24 bitową mantysą możemy uzyskać w przybliżeniu uzyskać precyzję 6- ½ cyfr (połówka precyzji cyfry oznacza, że pierwsze sześć cyfr może być w zakresie 0..9 ale siódma cyfra może być tylko w zakresie 0...x gdzie x < 9 i generalnie jest blisko pięć). Z ośmio bitowym wyk³adnikiem nadmiaru-128
Rysunek 14.3: Format 64 bitowej podwójnej precyzji zmienno przecinkowej
Rysunek 14.4: Format 80 bitej podwyższonej precyzji zmienno przecinkowej zakres dynamiki liczb zmienno przecinkowych o pojedynczej precyzji to w przybliżeniu 2±128 do 10±38 . Chociaż liczby zmienno przecinkowe o pojedynczej precyzji są odpowiednie dla wielu aplikacji, zakres dynamiki jest czasami za mały dal wielu aplikacji naukowych a duże ograniczenie dokładności jest nie odpowiednie dla wielu finansowych, naukowych i innych aplikacji. Co więcej w długim łańcuchu obliczeniowym ograniczenie dokładności formatu pojedynczej precyzji może wprowadzać poważne błędy. Format podwójnej precyzji pomaga przezwyciężyć problemy pojedynczej precyzji zmienno przecinkowej. Używając dwóch obszarów, format podwójnej precyzji ma 11 bitów nadmiar-1023 wykładnika i 53 bity mantysy ( z niejawnym bardziej znaczącym bite jako jedynką) plus znak bitu. Dostarcza to dynamiki zakresu od około 10±308 i 14-1/2 precyzji cyfr, wystarczającą dal większości aplikacji. Wartości zmienno przecinkowe o podwójnej precyzji przybierają postać jak pokazano na rysunku 14.3 Żeby móc zapewnić dokładność podczas długiego łańcucha obliczeń liczb zmienno przecinkowych o podwójnej precyzji. Intel zaprojektował format o podwyższonej precyzji. Format podwyższonej dokładności używa 80 bitów. Dwanaście z szesnastu dodatkowych bitów jest połączonych z mantysą, cztery z dodatkowych bitów jest dołączonych na koniec wykładnika. W odróżnieniu od wartości od pojedynczej i podwójnej precyzji, format podwyższonej precyzji nie ma niejawnego bardziej znaczącego bitu, który jest zawsze jedynką. Dlatego format podwyższonej precyzji dostarcza 64 bitowej mantysy, 15 bitów wykładnika nadmiar – 16383 i jednego bitu znaku. Format dla wartości zmienno przecinkowej o podwyższonej precyzji jest pokazany na rysunku 14.4 W FPU 80x87 i CPU 80486 wszystkie obliczenia są robione przy użyciu postaci o podwyższonej precyzji. Kiedykolwiek ładujemy wartość o pojedynczej lub podwójnej precyzji, FPU automatycznie konwertuje je do wartości o rozszerzonej precyzji. Podobnie, kiedy przechowujemy wartość o pojedynczej lub podwójnej
precyzji w pamięci, FPU automatycznie zaokrągla wartość w dół do właściwego rozmiaru przed jej zachowaniem. Przez pracę z formatem o podwyższonej precyzji Intel gwarantuje dużą liczbę bitów zabezpieczenia obecnych dla zapewnienia dokładności naszego obliczenia. Niektóre teksty mylnie stwierdzają, ze nie powinniśmy nigdy używać formatu o rozszerzonej precyzji ponieważ Intel gwarantuje dokładność obliczeń tylko dla formatów o pojedynczej i podwójnej precyzji. To jest głupie. Przez wykonywanie wszystkich obliczeń używając 80 bitów , Intel zapewnia (ale nie gwarantuje) ,że uzyskamy pełną 32 lub 64 bitową precyzję w naszych obliczeniach. Ponieważ FPU 80x87 i CPU 80486 nie dostarcza dużej liczby bitów zabezpieczenia w 80 bitowych obliczeniach , pewne błędy nieuchronnie pojawią się w mniej znaczących bitach obliczenie o podwyższonej precyzji. Jednakże, jeśli nasze obliczenie jest poprawne dla 64 bitów , obliczenie 80 bitowe będzie zawsze dostarczało przynajmniej 64 poprawnych bitów. Większość czasu będzie to nawet więcej. Ponieważ nie możemy założyć, że uzyskamy prawidłowe 80 bitowe obliczenie, możemy zazwyczaj zrobić lepiej niż 64 kiedy stosujemy format o podwyższonej precyzji . Zachowując maksimum dokładności podczas obliczenia, większość obliczeń używa wartości znormalizowanych. Znormalizowana wartość zmienno przecinkowa jest wartością, którą ma najbardziej znaczący bit mantysy równy jeden. Prawie każda nie znormalizowana wartość może być znormalizowana przez przesunięcie bitów mantysy w lewo i zmniejszanie wykładnika o jeden , dopóki nie pojawi się jeden w najbardziej znaczącym bicie mantysy. Pamiętajmy, że wykładnik jest wykładnikiem binarnym. Za każdym razem kiedy zwiększamy wykładnik mnożymy wartość zmienno przecinkową przez dwa. Podobnie kiedy zmniejszamy wykładnik, dzielimy wartość zmienno przecinkową przez dwa. Z tych samych powodów, przesuniecie mantysy w lewo o jeden bit mnoży wartość zmienno przecinkową przez dwa; podobnie przesunięcie mantysy w prawo dzieli wartość zmienno przecinkową przez dwa. Dlatego też przesunięcie mantysy w lewo i zmniejszenie wykładnika nie zmienia wcale wartości liczby zmienno przecinkowej. Utrzymywanie znormalizowanych liczb zmienno przecinkowych jest korzystne ponieważ zachowuje maksymalną liczbę bitów precyzji dla obliczenia jeśli bardziej znaczące bity mantysy, wszystkie są zerami, mantysa ma o wiele mniej bitów precyzji dostępnych dla obliczenia. Dlatego też obliczenia zmienno przecinkowe będą o wiele bardziej dokładne jeśli wykorzystamy tylko wartości znormalizowane. Są dwa ważne przypadki gdzie wartości znormalizowane nie mogą być znormalizowane. Wartość 0.0 jest specjalnym przypadkiem. Oczywiście nie może być znormalizowana ponieważ przedstawianie zera w postaci zmienno przecinkowej nie ma bitów jeden w mantysie. To jednak nie jest problemem ponieważ możemy dokładnie przedstawić wartość zero tylko pojedynczym bitem. Drugi przypadek to wtedy kiedy mamy jakieś bardziej znaczące bity w mantysie są zerami ale przesunięty wykładnik również jest zerem ( i nie możemy zmniejszyć go do znormalizowania mantysy). Zamiast odrzucać pewne małe wartości , kiedy bardziej znaczące bity mantysy i przesunięty wykładnik są zerami (większość możliwych ujemnych wykładników), standard IEE pozwala na specjalne nieznormalizowane wartości dla przedstawienia tych mniejszych wartości. Chociaż użycie nieznormalizowanych wartości pozwala obliczeniom zmienno przecinkowym IEEE na uzyskiwanie lepszych wyników niż jeśli wystąpił by niedomiar, zapamiętajmy, że wartości nieznormalizowane proponują mniejszą precyzję bitów i są z natury mniej dokładne. Ponieważ FPU 80x87 i CPU 80486 zawsze konwertują wartości o pojedynczej i podwójnej precyzji na podwyższoną precyzję, arytmetyka podwyższonej precyzji jest rzeczywiście szybsza niż precyzji pojedynczej lub podwójnej.. Dlatego oczekiwane osiągnięcie korzyści z zastosowania mniejszych formatów nie jest obecne na tych chipach. Jednak kiedy projektowani Pentium /586, Intel przeprojektował wbudowaną jednostkę zmienno przecinkową dla lepszego współzawodnictwa z chipami RISC. Większość chipów RISC wspiera własny 64 bitowy format podwójnej precyzji, który jest szybszy niż Intelowski format podwyższonej precyzji. Dlatego też Intel wprowadził własne 64 bitowe operacje w Pentium dla lepszego konkurowania z chipami RISC. Dlatego też, format podwójnej precyzji jest najszybszy na Pentium i późniejszych chipach. 14.3 PODPROGRAMY ZMIENNO PRZECINKOWE STANDARDWOEJ BIBLIOTEKI UCR W większości tekstów o języku asemblera , które zajmują się arytmetyką zmienno przecinkową, sekcja ta jest zazwyczaj opisywana jako sposób zaprojektowania własnych podprogramów dodawania, odejmowania, mnożenia i dzielenia. Ten tekst nie robi tego z kilku powodów .Po pierwsze, stworzenie dobrej biblioteki zmienno przecinkowej wymaga solidnych podstaw analizy numerycznej; warunki wstępne tego tekstu nie zakładają, że zna je czytelnik.. Po drugie Standardowa Bibliotek UCR dostarcza już sensownego zbioru podprogramów zmienno przecinkowych w formie kodu źródłowego: dlaczego marnować miejsce w tekście kiedy są dostępne łatwo źródła? Po trzecie jednostki zmienno przecinkowe szybko stają się standardowym wyposażeniem we wszystkich nowoczesnych CPU lub płytach głównych.; nie ma większego sensu opisywać jak ręcznie wykonać obliczenie zmienno przecinkowe niż opisywanie jak ręcznie wykonać obliczenia całkowite. Dlatego też ta sekcja będzie opisywała jak zastosować podprogramy Biblioteki Standardowej UCR jeśli nie mamy dostępnego FPU; późniejsze sekcje opiszą zastosowanie jednostki zmienno przecinkowej.
Standardowa Biblioteka UCR dostarcza dużej liczby podprogramów wspierających obliczenia zmienno przecinkowe i I/.O. Biblioteka ta używa takiego samego formatu pamięci dla liczb zmienno przecinkowych FPU 80x87 32, 64 i 80 bitowych. Podprogramy zmienno przecinkowe biblioteki Standardowej UCR niezbyt dokładnie spełniają wymagania IEEE pod względem warunków błędu i innych przypadków i mogą tworzyć odrobinę inne wyniki niż FPU 80x87 ale wyniki będą bardzo bliskie. Ponieważ Biblioteka Standardowa używa takiego samego formatu pamięci dla liczb 32, 64 i 80 bitowych jak FPU 80x87, możemy swobodnie połączyć obliczenia wymagające wartości zmienno przecinkowych, pomiędzy FPU a podprogramami Biblioteki Standardowej. Biblioteka Standardowa UCR dostarcza licznych podprogramów do manipulowania liczbami zmienno przecinkowymi. Poniższe sekcje omówią każdy z tych podprogramów kategoriami. 14.3.1 ŁADOWNIE I PRZECHOWYWANIE PODPROGRAMÓW Ponieważ COU 80c86 bez FPU nie dostarczają 80 bitowych rejestrów, Bibliotek Standardowa UCR musi użyć zmiennych bazujących na pamięci dla przechowania wartości zmienno przecinkowych podczas obliczenia. Podprogramy Standardowej Biblioteki UCR używają dwóch pseudo rejestrów, rejestru akumulatora i rejestru argumentów, kiedy wykonujemy operacje zmienno przecinkowe. Na przykład podprogram dodawania zmienno przecinkowego dodaje wartości w rejestrze argumentów zmienno przecinkowych do rejestru akumulatora zmienno przecinkowego, pozostawiając wynik w akumulatorze. Ładowanie i przechowywanie podprogramów pozwala nam ładować wartości zmienno przecinkowe do akumulatora zmienno przecinkowego i rejestrów argumentów jak również przechowywać wartości z akumulatora zmienno przecinkowego w pamięci. Do podprogramów z tej kategorii zaliczają się: accop, xacoop, lsfpa , ssfpa, ldfpa, sdfpa, lefpa, sefpa, lefpal, lsfpo, lefpo i lefpol Podprogram accop kopiuje wartość z akumulatora zmienno przecinkowego do rejestru argumentów zmienno przecinkowych. Podprogram ten jest użyteczny kiedy chcemy użyć wyniku jednego obliczenia jako argumentu dla drugiego obliczenia. Podprogram xacoop zamienia wartości w akumulatorze zmienno przecinkowym i rejestrze argumentów. Zauważmy, że wiele obliczeń zmienno przecinkowych niszczy wartości w rejestrze argumentów zmienno przecinkowych, więc nie możemy ślepo zakładać, że podprogramy zachowają rejestr argumentów. Dlatego też wywołanie tego podprogramu tylko ma sens po wykonaniu jakichś obliczeń, o których wiemy, że nie wpłyną na rejestr argumentów zmienno przecinkowych. Lsfpa, ldfpa, i lefpa ładują do akumulatora, odpowiednio, wartość z pojedynczą, podwójną lub podwyższoną precyzją. Biblioteka Standardowa UCR używa swojego własnego wewnętrznego formatu dla obliczeń. Podprogramy te konwertują określone wartości do tego wewnętrznego formatu podczas ładowania. Na wejściu do każdego z tych podprogramów, es:di musi zawierać adres zmiennej, którą chcemy załadować do akumulatora zmienno przecinkowego. Poniższy kod demonstruje jak wywołać te podprogramy: rVar real4 1.0 drVar real8 2.0 xrVar real10 3.0 lesi rVar lsfpa lesi drVar ldfpa lesi xrVar lefpa Podprogramy lsfpo, ldfpo i lefpo są podobne do podprogramów lsfpa, ldfpa i lefpa z wyjątkiem oczywiście tego, że ładują one rejestr argumentów zmienno przecinkowych zamiast zmienno przecinkowego akumulatora wartościami spod adresu es:di
Lefpal i lefpol ładują akumulator zmienno przecinkowy lub rejestr argumentów dosłowną 80 bitową stałą zmienno przecinkową pojawiającą się w strumieniu kodu. Używając tych dwóch podprogramów następuje wywołanie z dyrektywą real10 i właściwą stałą, np. lefpal real10 1.0 lefpol real10 2.0e5 podprogramy ssfpa, sdfpa i sefpa przechowują wartość w akumulatorze zmienno przecinkowym w zmiennej zmienno przecinkowej bazującej na pamięci której adres pojawia się w es:di. Nie odpowiadają one podprogramom ssfpo, sdfpo lub sefpo ponieważ wynik jaki chcemy przechować nie powinien pojawić się nigdy w rejestrze argumentów zmienno przecinkowych. Jeśli zdarzy się nam pobrać wartość argumentu zmienno przecinkowego , który chcemy przechować w pamięci po prostu użyjemy podprogramu xaccop do zamiany rejestrów akumulatora i argumentów, potem użyjemy podprogramów przechowania akumulatora do przechowania wyniku. Poniższy kod demonstruje użycie tych podprogramów: rVar drvar xrVar
real4 real8 real10 lesi ssfpa lesi sdfpa lesi sefpa
1.0 2.0 3.0
rVar
drVar
xrVar
14.3.2 KONWERSJA WARTOŚCI CAŁKOWITE / WARTOŚCI ZMIENNO PRZECINKOWE Biblioteka Standardowa UCR zawiera kilka podprogramów do konwersji pomiędzy binarnymi liczbami całkowitymi a wartościami zmienno przecinkowymi. Te podprogramy to itof, utof, ltof, ftoi, ftou, ftol i ftoul. Pierwsze cztery podprogramy konwertują liczby całkowite ze znakiem i bez znaku na format zmienno przecinkowy, ostatnie cztery obcinają wartości zmienno przecinkowe i konwertują je na wartości całkowite. Itof konwertuje 16 bitową wartość ze znakiem w ax na wartość zmienno przecinkową i pozostawia wynik w akumulatorze zmienno przecinkowym. Ten podprogram nie wpływa na rejestr argumentów zmienno przecinkowych. Utof konwertuje wartości całkowite bez znaku w ax w podobny sposób. Ltof i ultof konwertują 32 bitowe ze znakiem (ltof) lub bez znaku (ultof) wartości całkowite w dx:ax do wartości zmienno przecinkowych pozostawiając wartość w akumulatorze zmienno przecinkowym . Te podprogramy zawsze kończą się sukcesem. Ftoi konwertuje wartość z akumulatora zmienno przecinkowego do wartości całkowitej ze znakiem, pozostawiając wynik w ax. Konwersja odbywa się przez obcięcie; podprogram zatrzymuje część całkowitą i odrzuca część ułamkową. Jeśli wystąpi przepełnienie ponieważ część całkowita nie mieści się w 16 bitach, ftoi ustawia flagę przeniesienia. Jeśli konwersja wystąpi bez błędu , ftoi wyzeruje flagę przeniesienia. Ftou działa w podobny sposób, z wyjątkiem konwersji wartości zmienno przecinkowej do wartości całkowitej bez znaku w ax, i ustawia flagę przeniesienia jeśli wartość zmienno przecinkowa byłą ujemna. Ftol i ftoul konwertują wartości z akumulatora zmienno przecinkowego do 32 bitowej wartości całkowitej pozostawiając wynik a dx:ax. Ftol działa na wartościach ze znakiem, ftoul na wartościach bez znaku. Podobnie jak ftoi i ftou podprogramy te zwracają ustawioną flagę przeniesienia jeśli wystąpi błąd konwersji. 14.3.3 ARYTMETYKA ZMIENNO PRZECINKOWA
Arytmetyka zmienno przecinkowa jest wykonywana przez podprogramy fpadd, fpsub, fpcmp, fpmul i fpdiv. Fpadd dodaje wartości w akumulatora zmienno przecinkowego do akumulatora zmienno przecinkowego. Fpsub odejmuje wartość argumentu zmienno przecinkowego od akumulatora zmienno przecinkowego. Fpmul mnoży wartość z akumulatora przez rejestr argumentów zmienno przecinkowych. Fpdiv dzieli wartość z akumulatora zmienno przecinkowego przez wartość w rejestrze argumentów zmienno przecinkowych. Fpcmp porównuje wartość w akumulatorze zmienno przecinkowym z rejestrem argumentów zmienno przecinkowych. Podprogramy arytmetyczne Biblioteki Standardowej UCR wykonują trochę kontroli błędów. Na przykład, jeśli przepełnienie arytmetyczne wystąpi podczas dodawania, odejmowania ,mnożenia lub dzielenia, Biblioteka Sztandarowa po prostu ustawia wynik na największą poprawną wartość i ją zwraca.. Jest to jedno z głównych odchyleń od standardu zmienno przecinkowego IEEE. Podobnie, kiedy wystąpi niedomiar, podprogramy po prostu ustawiają wynik na zero i zwracają. Jeśli podzielimy jakąś wartość przez zero, podprogramy biblioteki Standardowej ustawią wynik na największą możliwą wartość i zwracają. Możemy musieć zmodyfikować podprogramy biblioteki standardowej jeśli musimy skontrolować przepełnienie, niedomiar lub dzielenie przez zero w naszych programach. Podprogram porównania zmienno przecinkowego (fpcmp) porównuje akumulator zmienno przecinkowy z rejestrem argumentów zmienno przecinkowych i zwraca –1, 0 lub 1 w rejestrze ax jeśli akumulator jest mniejszy niż, równy lub większy niż rejestr argumentów. Porównuje również ax z zerem bezpośrednio przed zwróceniem, więc ustawia flagi aby można było użyć instrukcji jg, jge, jl, jle, je i jne bezpośrednio po wywołaniu fpcmp. W odróżnieniu od fpadd, fpsub, fpmul i fpdiv, fpcmp nie niszczy wartości w akumulatorze zmienno przecinkowym lub rejestrze argumentów zmienno przecinkowym. Pamiętajmy o problemach związanych z porównywaniem liczb zmienno przecinkowych! 14.3.4 KONWERSJA WARTOŚCI ZMIENNO PRZECINLKOWYCH NA TEKST I PRINTFF Standardowa Biblioteka UCR dostarcza trzech podprogramów, ftoa, etoa i atof, które pozwalają nam konwertować liczby zmienno przecinkowe na ciągi ASCII i vice versa; dostarcza również specjalnej wersji printf, printff, która zawiera zdolność do drukowania wartości zmienno przecinkowych tak jak innych typów danych. Ftoa konwertuje liczbę zmienno przecinkową na ciąg ASCII, który jest dziesiętnym odpowiednikiem tej liczby zmienno przecinkowej. Na wejściu, akumulator zmienno przecinkowy zawiera liczbę, którą chcemy skonwertować do ciągu. Para rejestrów es:di wskazuje bufor w pamięci gdzie ftoa będzie przechowywać ciąg. Rejestr al. zawiera pole szerokości (liczbę pozycji do druku) Rejestr ah zawiera liczbę pozycji do wyświetlenia na prawo od punktu dziesiętnego. Jeśli ftoa nie może wyświetlić liczby używając formatu druku określonego przez al. i ah, stworzy ciąg znaków „#”, długi na ah znaków. Es:di musi wskazywać tablicę bajtów zawierającą przynajmniej al.+1 znaków i al. powinien zawierać przynajmniej pięć. Pole szerokości i dziesiętna wartość długości w rejestrach al. i ah są podobne do wartości pojawiających się po liczbach zmienno przecinkowych w Pascalowskiej instrukcji write, np.: write (floatVar :al.: ah); Etoa wyprowadza liczby zmienno przecinkowe w postaci wykładniczej. Podobnie jak przy ftoa, es:di wskazuje bufor gdzie etoa będzie przechowywać wynik. Rejestr al. musi zawierać przynajmniej osiem i jest polem szerokości dla liczby. Jeśli al. zawiera mniej niż osiem , etoa wyprowadzi ciąg znaków „#”. Ciąg , który wskazuje es:di musi zawierać przynajmniej al.+1 znaków. Ten podprogram konwersji jest podobny do pascalowskiej procedury write , kiedy zapisujemy wartości rzeczywiste z wyspecyfikowanym pojedynczym polem szerokości : write (realVar:al.) Podprogram Biblioteki Standardowej printff dostarcza wszystkich możliwości standardowego podprogramu printf plus umiejętność wyprowadzania wartości zmienno przecinkowych. Podprogram printff wprowadza kilka nowych specyfikacji formatu dla druku liczb zmienno przecinkowych w postaci dziesiętnej lub przy użyciu notacji naukowej. Te specyfikacje to: * %x.y F Wydruk 32 bitowej liczby zmienno przecinkowej w postaci dziesiętnej * %x.yGF Wydruk 64 bitowej liczby zmienno przecinkowej w postaci dziesiętnej * %x.yLF Wydruk 80 bitowej liczby zmienno przecinkowej w postaci dziesiętnej * %zE Wydruk 32 bitowej liczby zmienno przecinkowej używając notacji naukowej * %zGE Wydruk 64 bitowej liczby zmienno przecinkowej używając notacji naukowej * %zLE Wydruk 80 bitowej liczby zmiennoprzecinkowej używając notacji naukowej W powyższych formatach ciągów, x i z są stałymi całkowitymi, które oznaczają pole szerokości liczby do wydruku. Pozycja y jest również stałą całkowitą , która określa liczbę pozycji do druku po przecinku dziesiętnym. Wartości x y są porównywalne z wartościami przekazywanymi do ftoa w al. i ah. Wartość z jest porównywalna z wartością etoa oczekiwaną w rejestrze al.
Z wyjątkiem tych sześciu nowych formatów, podprogram printff jest identyczny z podprogramem printf. Jeśli użyjemy podprogramu printff w programie asemblerowym, nie powinniśmy już używać podprogramu printf. Printff duplikuje wszystkie udogodnienia printf i używanie obu byłoby marnotrawieniem pamięci 14.4 KOPROCESOR ZMIENNO PRZECINKOWY 80x87 Kiedy latach siedemdziesiątych pojawiły się pierwsze CPU 8086, technologia półprzewodnikowa nie była w takim punkcie, gdzie Intel mógł włożyć instrukcje zmienno przecinkowe bezpośrednio do CPU 8086. Dlatego też, podzielili oni projekt, dzięki czemu mogli użyć drugiego chipu dla wykonania obliczeń zmienno przecinkowych – jednostka zmienno przecinkowa (lub FPU). Swój oryginalny chip zmienno przecinkowy 80x87 udostępnili w 1980 roku. Ten szczególny FPU działał z CPU 8086, 8088,80186 i 80188. Kiedy Intel wprowadził CPU 80286, udostępnili przeprojektowany chip FPU 80287. Chociaż 80287 był kompatybilny z CPU 80386 , Intel zaprojektował lepszy, 80387, dla stosowania z 80386. CPU 80486 był pierwszym CPU Intela zawierający FPU zintegrowany z układem. Krótko po udostępnieniu 80486, Intel wprowadził CPU 80486sx, który był 80486 ale bez wbudowanego FPU Dla uzyskania możliwości zmienno przecinkowych na tym chipie, musiano dodać chip 80487, chociaż w rzeczywistości 80487 był niczym więcej niż pełnym 80486, który przejął chip „sx” w systemie. Chipy Intela Pentium/ 586 dostarczyły jednostkę zmienno przecinkową o dużej wydajności bezpośrednio w CPU. Nie ma koprocesora zmienno przecinkowego dostępnego dla chipu Pentium. Zbiorczo będziemy się odnosić do wszystkich tych chipów jak FPU 80x87. Ze względu na zestarzenie się chipów 8086, 80286,8087 i 80287, ten tekst skoncentruje się na chipie 80386 i późniejszych. Jest kilka różnic pomiędzy jednostkami zmienno przecinkowymi 80387 / 80487/ Pentium a wcześniejszymi FPU. Jeśli musisz napisać kod, który będzie wykonywany na tych wcześniejszych maszynach, powinieneś sprawdzić właściwą Intelowską dokumentację dla tego urządzenia. 14.4.1 REJESTRY FPU FPU 80x87 dodał 13 rejestrów do 80386 i późniejszych procesorów : osiem rejestrów danych zmienno przecinkowych, rejestr sterujący, rejestr stanu, rejestr znaczników, wskaźnik instrukcji i wskaźnik danych. Rejestry danych są podobne do zbioru rejestrów ogólnego przeznaczenia 80x86 na tyle na ile obliczenia zmienno przecinkowe mają miejsce w tych rejestrach. Rejestr sterujący zawiera bity , które pozwalają nam decydować jak radzić będzie sobie 80x87 w pewnych przypadkach takich jak zaokrąglanie niedokładnych obliczeń, kontroli precyzji itd. Rejestr stanu jest podobny do rejestru flag 80x86 : zawiera bity kodu warunkowego kilka innych flag zmienno przecinkowych , które opisują stan chipu 80x87 . Rejestr znaczników zawiera kilka grup bitów , które określają stan wartości w każdym z ośmiu rejestrów ogólnego przeznaczenia. Rejestry wskaźników instrukcji i danych zawierają pewne informacje o stanie
Rysunek 14.5 Rejestr stosu zmienno przecinkowego 80x87 O ostatnio wykonanej instrukcji zmienno przecinkowej. Nie będziemy rozpatrywać ostatnich trzech rejestrów w tym tekście. 14.4.1.1. REJESTRY DANYCH FPU FPU 80x87 dostarcza ośmiu 80 bitowych rejestrów danych zorganizowanych w formie stosu. Jest to znaczące odejście od organizacji rejestrów ogólnego przeznaczenia w CPU 80x86, które składają się na standard
zbioru rejestrów ogólnego przeznaczenia. Intel odnosi się do tych rejestrów jako ST(0), ST(1), ..., ST(7). Większość asemblerów zaakceptuje ST jako skrót dla ST(0) Największą różnicą pomiędzy zbiorem rejestrów FPU a zbiorem rejestrów 80x86 jest organizacja stosu. W CPU 80x86, rejestr ax jest zawsze rejestrem ax , bez względu na to co się dzieje. W 80x87 jednak, zbiór rejestrów jest ośmio elementowym stosem 80 bitowych wartości zmienno przecinkowych (zobacz rysunek 14.5). ST(0) odnosi się do pozycji na szczycie stosu, ST(1) odnosi się do następnej pozycji na stosie i tak dalej. Wiele instrukcji zmienno przecinkowych odkłada i zdejmuje coś na stos; dlatego też, ST(1) będzie się odnosić do poprzedniej zawartości ST(0) po odłożeniu przez nas czegoś na stos. Trzeba będzie przywyknąć myślowo i praktycznie do faktu, że rejestry są zmieniane przez nas, ale ten problem jest łatwy do przezwyciężenia. 14.4.1.2 REJESTR STERUJĄCY FPU Kiedy Intel zaprojektował 80x87 (i, zasadniczo, standard zmienno przecinkowy IEEE), nie było standardu zmienno przecinkowego sprzętu. Różne (duże i mini) fabryki komputerów , wszystkie miały różne i niekompatybilne formaty zmienno przecinkowe. Niestety wiele aplikacji zostało napisanych biorąc pod uwagę cechy tych różnych formatów zmienno przecinkowych. Intel chciał zaprojektować FPU, który mógłby pracować z większością tego oprogramowania (pamiętajmy, że IBM był trzy, cztery lata w tyle, kiedy Intel zaczynał projektowanie 8087, nie mogli polegać na tej „górze” oprogramowania dostępnego na PC czyniąc ich chip popularnym). Niestety wiele cech znajdowanych w tych starszych formatach zmienno przecinkowych było wzajemnie wykluczających. Na przykład w jakimś systemie zmienno przecinkowym występowało zaokrąglanie kiedy była niewystarczająca precyzja, w innych występowało obcięcie. Niektóre aplikacje będą pracowały z jednym systemem zmienno przecinkowym a z innym nie. Intel chciał, żeby tak wiele aplikacji jak to możliwe pracowało z niewielkimi zmianami na FPU 80x87, więc dodał specjalny rejestr, rejestr sterujący FPU, który pozwala użytkownikowi wybrać jeden z kilku możliwych trybów działania 80x87 Rejestr sterujący 80x87 jest zorganizowany w 16 bitów jak pokazano na rysunku 14.6 Bit 12 tego rejestru jest obecny tylko w chipach 8087 i 80287. Steruje on tym jak 80x87 reaguje na nieskończoność. 80387 i późniejsze chipy zawsze używa postaci znanej jako zamknięcie afiniczne ponieważ jest to jedyna postać wspierana przez standardy IEEE 754/ 854.
Rysunek 14.6: Rejestr sterujący 80x87 Dalej będziemy ignorować ten bit i założymy ,że zawsze jest zaprogramowany na jeden. Bity 10 i 11 dostarczają kontroli zaokrąglania według poniższej tabeli: Bity 10 i 11 00 01 10
Funkcja Najbliżej lub parzyście Zaokrąglanie w dół Zaokrąglanie w górę
11 Obcięcie Tablica 58: Sterowanie zaokrąglaniem Ustawienie „00” jest domyślne. 80x87 zaokrągla wartości powyżej połówkowego najmniej znaczącego bitu w górę. Zaokrągla w dół poniżej połówkowego najmniej znaczącego bitu. Jeśli wartość poniżej najmniej znaczącego bitu jest dokładnie połówkowym najmniej znaczącym bitem, 80x87 zaokrągla wartość w kierunku wartości, której najmniej znaczący bit jest zerem. Dla długiego łańcucha obliczeń, dostarcza sensownego, automatycznego sposobu uzyskania maksimum precyzji. Opcje zaokrąglania w dół i w górę są obecne dla tych obliczeń, gdzie ważne jest śledzenie dokładności podczas obliczeń. Przez ustawienie kontroli sterowania na zaokrąglanie w dół i wykonanie działania, powtarzanie działania z kontrolą zaokrąglania ustawioną w górę, możemy określić minimalny i maksymalny zakres pomiędzy którym padnie prawdziwy wynik. Opcja obcinania wymuszana wszystkich obliczeniach, obcinania nadmiarowych bitów podczas obliczania. Będziemy rzadko używali tej opcji, jeśli ważna dla nas jest dokładność. Jednakże, jeśli przenosimy starsze oprogramowanie pod 80x87, możemy użyć tej opcji pomagającej przy przenoszeniu oprogramowania. Bity osiem i dziewięć rejestru sterującego sterują precyzją podczas obliczenia. Ta zdolność jest dostarczona głównie ze względu na kompatybilność ze starszym oprogramowaniem wedle standardu IEEE 754. Bity sterowania precyzją używają następujących wartości: Bity 8 i 9 00 01 10 11
Sterowanie precyzją 24 bity Zarezerwowane 53 bit 64 bity
Tabela 59: Mantysa bitów sterujących precyzją Dla nowoczesnych aplikacji, bity sterowania precyzją powinny zawsze być ustawione na „11” dl uzyskania 64 bitowej precyzji. Stworzy to najbardziej dokładny wynik podczas obliczeń numerycznych. Bity od zero do pięć są maskami wyjątków. Są one podobne do bitu zezwalającego na przerwania w rejestrze flag 80x86. Jeśli te bity zawierają jedynki, odpowiedni warunek jest ignorowany przez FPU 80x87. Jednakże jeśli jakiś bit zawiera zero i wystąpi odpowiedni warunek, wtedy FPU natychmiast generuje przerwanie, aby program mógł obsłużyć ten warunek. Bit zero odpowiada błędowi niewłaściwej operacji. Generalnie występuje jako wynik błędu programistycznego. Problem, który zgłasza wyjątek niepoprawnej operacji zawiera odłożenie więcej niż ośmiu pozycji na stos lub próba zdjęcia czegoś z pustego stosu, wyliczenia pierwiastka kwadratowego z liczby ujemnej, lub załadownia nie pustego rejestru. Bit jeden maskuje nieznormalizowane przerwanie, które wystąpi jeśli tylko spróbujemy manipulować wartościami nieznormalizowanymi. Wartości nieznormalizowane generalnie pojawiają się wtedy kiedy lądujemy dowolne wartości o podwyższonej precyzji do FPU lub pracujemy z bardzo małymi liczbami z poza zakresu zdolności FPU. Zwykle , prawdopodobnie nie będziemy aktywować tego wyjątku. Bit dwa maskuje wyjątek dzielenia przez zero. Jeśli ten bit zawiera zero, FPU wygeneruje przerwani jeśli spróbujemy podzielić wartość niezerową przez zero. Jeśli nie włączymy wyjątku dzielenia przez zero , FPU będzie tworzył NaN (not a number – nie liczbę), kiedy będziemy wykonywali dzielnie przez zero. Bit trzy maskuje wyjątek przepełnienia. FPU zgłasza wyjątek przepełnienia jeśli nastąpi przepełnienie przy obliczeniu lub jeśli spróbujemy przechować wartość, która jest zbyt duża do przechowania w argumencie przeznaczenia (tj. przechowanie dużej wartości o podwyższonej precyzji w zmiennej o pojedynczej precyzji). Bit cztery, jeśli jest ustawiony, maskuje wyjątek niedomiaru. Niedomiar wystąpi kiedy wynik jest zbyt mały do przechowania w argumencie przeznaczenia. Podobnie jak przepełnienie, wyjątek ten może wystąpić, kiedy przechowujemy małą wartość o rozszerzonej precyzji w mniejszej zmiennej ( pojedynczej lub podwójnej precyzji)lub kiedy wynik obliczenia jest zbyt mały dla rozszerzonej precyzji. Bit pięć kontroluje czy może wystąpić wyjątek precyzji. Wyjątek ten wystąpi kiedy FPU tworzy nieprecyzyjny wynik, generalni wynik wewnętrznej operacji zaokrąglania. Chociaż wiele operacji będzie tworzyło dokładny wynik , o wiele więcej nie. Na przykład , dzieląc jeden przez dziesięć uzyskujemy wynik nie dokładny. Dlatego też ten bit jest zazwyczaj jedynką , ponieważ wyniki niedokładne są bardzo powszechne. Bity sześć i od trzynaście do piętnaście w rejestrze sterującym są aktualnie niezdefiniowane i zarezerwowane dla późniejszego zastosowania. Bit siedem jest maską zezwolenia n przerwanie, ale jest tylko czynna na FPU 80x87; zero w tym bicie zezwala na przerwania 80x87 a jeden nie. 80x87 dostarcza dwóch instrukcji, FLDCW (ładuj słowo sterujące) i FSTCW (przechowaj słowo sterujące), które pozwalają nam załadować i przechować zawartość rejestru sterującego. Pojedynczy argument
tych instrukcji musi być 16 bitową komórką pamięci. Instrukcja FLDCW ładuje rejestr sterujący z określonej komórki pamięci, FSTCW przechowuje zawartość rejestru sterującego w określonej komórce pamięci.
Rysunek 14.7: Rejestr stanu FPU
14.4.1.3 REJESTR STANU FPU Rejestr stanu FPU dostarcza stanu koprocesora w chwili jego odczytu. Instrukcja FSTSW przechowuje 16 bitowy zmienno przecinkowy rejestr stanu w argumencie mod/ reg / rm. Rejestr stanu jest 16 bitowym rejestrem, a jego układ jest pokazany na rysunku 14.7. Bity od zero do pięć są znacznikami wyjątków. Bity te pojawiają się w takim samym porządku jak maski wyjątków w rejestrze sterującym. Jeśli odpowiedni warunek istnieje, wtedy bit jest ustawiony. Bity te są niezależne od masek wyjątków w rejestrze sterującym. 80x87 ustawia i zeruje te bity bez względu na odpowiadające ustawienia masek. Bit sześć (czynny tylko w procesorach 80386 i późniejszych) wskazuje zakłócenie stosu. Zakłócenie stosu wystąpi kiedykolwiek stos jest przepełniony lub niedopełniony. Kiedy ten bit jest ustawiony, bit kodu błędu C1 określa czy było przepełnienie stosu (C1 = 1) lub niedomiar (C1=0). Bit siedem rejestru stanu jest ustawiony jeśli jest ustawiony jakikolwiek bit warunku wystąpienia błędu. Jest to logiczne OR bitów od zero do pięć. Program może przetestować ten bit szybko określając czy istnieje warunek wystąpienia błędu. Bity osiem, dziewięć, dziesięć i czternaście są bitami kodów warunkowych koprocesora. Różne instrukcje ustawiają bity kodów zakończenia tak jak pokazano w poniższej tablicy:
Instrukcje fcom, fcomp, fcompp, ficom, ficomp ftst
fxam
Bity kodu zakończenia C3 C2 C1 C0 0 0 X 0 0 0 X 1 1 0 X 0 1 1 X 1 X = nie ważne 0 0 X 0 0 0 X 1 1 0 X 0 1 1 X 1 0 0 0 0 0 0 1 0
Warunek ST > źródła ST < źródła ST = źródło ST lub niezdefiniowane źródło ST jest dodatnie ST jest ujemne ST to zero (+ lub -) ST jest nieporównywalne + Nieznormalizowane - Nieznormalizowane
fucom, fucomp; fucompp
0 1 0 1 1 0 1 0 1 1 1 1 0 0 0 0 0 1 0 1 1 X 0 0 0 0 1 0 1 1 X = nie ważne
0 1 0 1 0 1 0 1 0 1 X X X X X
0 1 0 0 0 0 1 1 1 1 1 0 1 0 1
+ Znormalizowane - Znormalizowane +0 -0 +Deznormalizowane -Deznormalizowane +NaN -Nan +Nieskończoność -Nieskończoność Rejestr pusty ST > źródła ST < źródła ST = źródło Nieuporządkowane
Tablica 60: Bity kodów warunkowych FPU Instrukcja(-e) fcom, fcomp, fcmpp, ftst, fucom, fucomp, fucompp, ficom, ficomp
C0 Wynik porównania. Zobacz powyższą tabelę
C3 Wynik porównania. Zobacz powyższą tabelę
C2 Argument nie jest porównywalny.
fxam
Zobacz poprzednią tabelę
Zobacz poprzednią tabelę
Zobacz poprzednią tabelę
fprem, fprem1
Bit 2 reszty
Bit 0 reszty
0 – skrócenie zrobione 1- skrócenie niekompletne
fist, fbstp, frndint, fst, fstp, fadd, fmul, fdiv, fdivr, fsub, fsubr, fscale, fsqrt, fpatan, f2xm1, fyl2x,fyl2xp1
Niezdefiniowany
Niezdefiniowany
Niezdefiniowany
fptan, fsin, fcos, fsincos
Niezdefiniowany
Niezdefiniowany
0 – skrócenie zrobione 1- skrócenie niekompletne
fchs, fabs, fxch, fincstp,fdecstp, Stałe ładowane ,fxtract, fld, fild, fbld, fstp (80 bit) fldenv, fstor
Niezdefiniowany
Niezdefiniowany
Niezdefiniowany
Przywrócony z operandu pamięci fldcw, fstenv ,fstcw, Niezdefiniowany
Przywrócony z operandu pamięci Niezdefiniowany
Przywrócony z operandu pamięci Niezdefiniowany
C1 Wynik porównania ( zobacz powyższą tabelę) lub przepełnienie / niedomiar stosu (jeśli bit wyjątku stosu jest ustawiony Znak wyniku , lub przepełnienie /niedomiar stosu (jeśli bit wyjątku stosu jest ustawiony) Bit 1 reszty lub przepełnienie / niedomiar stosu (jeśli bit wyjątku stosu jest ustawiony) Wystąpienie zaokrąglenia górę lub przepełnienie / niedomiar (jeśli bit wyjątku stosu jest ustawiony) Wystąpienie zaokrąglenia w górę lub przepełnienie / niedomiar stosu (jeśli bit wyjątku stosu jest ustawiony) Wynik zero lub przepełnienie / niedomiar stosu (jeśli bit wyjątku stosu jest ustawiony) Przywrócony z operandu pamięci Niezdefiniowany
fstsw, fclex finit, fsave
Wyczyszczony do zera
Wyczyszczony do zera
Wyczyszczony do zera
Wyczyszczony do zera
Tablica 61: Interpretacja kodów warunkowych
Rysunek 14.8: Format zmienno przecinkowy 80x87
Rysunek 14.9: Format całkowity 80x87 Bity 11-13 rejestru stanu FPU dostarczają liczy rejestrów na szczycie stosu. Podczas obliczenia, 80x87 dodaje (modulo osiem) logiczną liczę rejestrów dostarczonych przez programistę do tych trzech bitów, określając fizyczną liczbę rejestrów w czasie wykonania. Bit 15 rejestru stanu jest bitem zajętości. Jest ustawiony wtedy kiedy FPU jest zajęty Wiele programów będzie miało powód do uzyskania dostępu do tego bitu. 14.4.2 TYPY DANYCH FPU FP 80x87 wspiera siedem różnych typów danych: trzy typy całkowite, upakowany typ dziesiętny i trzy typy zmienno przecinkowe. Ponieważ CPU 80x87 już wspiera całkowite typy danych, jest niewiele powodów dla których używać będziemy typów całkowitych 80x87. Typ upakowanej liczby dziesiętnej dostarcza 17 cyfrową liczę całkowitą ze znakiem (BCD) Jednakże pominiemy arytmetykę BCD w tym tekście, więc
zignorujemy ten typ danych w FPU 80x87.Pozstałe trzy typy danych są 32 bitowymi, 64 bitowymi i 80 bitowymi typami zmienno przecinkowymi. Typy danych 80x87 pojawiają się na rysunku 14.8, rysunku 14.9 i rysunku 14.10
Rysunek 14.10: Format upakowanej liczby dziesiętnej 80x87 FPU 80x87 generalnie przechowuje wartości w formacie znormalizowanym. Kiedy liczba zmienno przecinkowa jest znormalizowana, najbardziej znaczący bit jest zawsze jeden. W formacie zmienno przecinkowym 32 i 64 bitowym, 80x87 właściwie nie przechowuje tych bitów,80x87 zawsze zakłada, że jest to jeden. Dlatego też, liczby zmienno przecinkowe 32 i 64 bitowe są zawsze znormalizowane. W 80 bitowym formacie o podwyższonej precyzji, 80x87 nie zakłada, że najbardziej znaczący bit mantysy to jeden, najbardziej znaczący bit pojawia się jako część ciągu bitów. Wartości znormalizowane dostarczają największej precyzji dla danej liczby bitów. Jednakże jest duża liczba wartości nie znormalizowanych, które mogą być przedstawiane w formacie 80 bitowym. Wartości te są bardzo bliskie zeru i przedstawiają zbiór wartości, których mantysa bardziej znaczącego bitu nie jest zerem. FPU 80x87 wspiera specjalną formę 80 bitową znaną jako wartości denormalizowane. Wartości denormalizowane pozwalają 80x87 zakodować bardzo małe wartości, których nie może zakodować używając wartości znormalizowanych, ale za wysoką cenę. Wartości denormalizowane oferują mniejszą precyzję bitową niż wartości znormalizowane. Dlatego też używając wartości denormalizowanych w obliczeniach możemy wprowadzić drobną niedokładność do obliczenia. Oczywiście, jest to zawsze lepsze niż zbliżenie wartości denormalizowanej do zera (co może uczynić obliczenie jeszcze mniej dokładnym), ale musimy zapamiętać, że jeśli pracujemy z bardzo małymi wartościami, możemy zgubić pewną dokładność w naszych obliczeniach. Zauważmy, że rejestr stanu 80x87 zawiera bit, który możemy użyć do wykrycia kiedy FPU używa wartości denormalizowanych w obliczeniu. 14.4.3 ZBIÓR INSTRUKCJI FPU FPU 80387 ( i późniejsze) dodał ponad 80 nowych instrukcji do zbioru instrukcji 80x86. Możemy zaklasyfikować te instrukcje jako instrukcje przesuwania danych, konwersji, instrukcje arytmetyczne, porównania, instrukcje stałe, instrukcje przestępne i instrukcje różne. Poniższe sekcje omawiają każą z tych instrukcji w tych kategoriach. 14.4.4 INSRUKCJE PRZSUNIĘCIA DANYCH FPU Instrukcje przesunięcia danych przenoszą dane pomiędzy wewnętrznymi rejestrami FPU a pamięcią. Instrukcje w tej kategorii to fld, fst, fstp, i fxch. Instrukcja fld zawsze odkłada swoje operandy na stos zmienno przecinkowy. Instrukcja fstp zawsze zdejmuje szczyt stosu po zachowaniu szczytu stosu (tos) w jej operacji. Pozostałe instrukcje nie wpływają na liczbę pozycji na stosie. 14.4.4.1 INSTRUKCJA FLD Instrukcja fld ładuje 32, 64 lub 80 bitową wartość zmienno przecinkową na stos. Instrukcja ta konwertuje 32 i 64 bitowy operand na80 bitową wartość o podwyższonej precyzji przed odłożeniem wartości na stos zmienno przecinkowy Instrukcja fld zmniejsza wskaźnik tos bity 11 – 13 rejestru stanu) a potem zachowuje 80 bitową wartość w rejestrze fizycznym określonym przez nowy wskaźnik tos. Jeśli argument źródłowy instrukcji fld jest rejestrem danych zmienno przecinkowych ST(i), wtedy rzeczywisty rejestr 80x87 używany do operacji ładowania jest numerem rejestru przed zmniejszeniem wskaźnika tos. Dlatego też, fld st lub fld st(0) duplikują wartość na szczyt stosu. Instrukcja fld ustawia bit zakłócenia stosu jeśli wystąpi przepełnienie stosu. Ustawia bit wyjątku denormalizacji jeśli załadujemy 80 bitową wartość denormalizowaną. Ustawia bit nieprawidłowej operacji jeśli próbujemy załadować pusty rejestr zmienno przecinkowy na szczyt stosu (lub wykonujemy jakąś inną nieprawidłową operację)
Przykład: fld fld fld fl
st(1) mem_32 MyRealVar mem_64[bx]
14.4.4.2 INSTRUKCJE FST I FSTP Instrukcje fst i fstp kopiują wartość ze szczytu rejestru stosu zmienno przecinkowego do innego rejestru lub 32, 64 lub 80 bitowej zmiennej pamięciowej. Kiedy kopiujemy dane do 32 lub 64 bitowej zmiennej pamięciowej, wartość 80bitowa o podwyższonej precyzji na szczycie stosu jest zaokrąglana do mniejszego formatu, określonego przez bit kontroli zaokrąglania w rejestrze sterującym FPU. Instrukcja fstp zdejmuje wartość ze szczytu stosu kiedy przenosimy ją do lokacji przeznaczenia. Robi to przez zwiększenie wskaźnika szczytu stosu w rejestrze stanu po uzyskaniu dostępu do danych w st(0). Jeśli operand przeznaczenia jest rejestrem zmienno przecinkowym, FPU przechowa wartość pod określonym numerem rejestru przed zdjęciem danych ze szczytu stosu. Wykonując instrukcję fstp st(0) faktycznie zdejmuje dane ze szczytu stosu bez transferu danych. Przykład: fst mm_32 fstp mem_64 fstp mem_64 [ebx*8] fst mem_80 fst st (2) fstp st (1) Ostatni przykład faktycznie zdejmuje st (1) podczas gdy st(0) pozostaje na stosie. Instrukcje fst i fstp ustawiają bit wyjątku stosu jeśli wystąpi niedomiar stosu (próbując przechować wartość pustego rejestru stosu). Ustawiają bit precyzji jeśli jest stracona precyzja podczas operacji przechowania (to nastąp, na przykład, kiedy przechowujemy wartość 80 bitową o podwyższonej precyzji w zmiennej pamięciowej 32 lub 64 bitowej i są gubione jakieś bity podczas konwersji). Ustawiają bit wyjątku niedomiaru kiedy przechowujemy wartość 80 bitową w zmiennej pamięciowej 32 lub 64 bitowej, ale wartość jest zbyt mała do przechowania w operandzie przeznaczenia. Podobnie, instrukcje te ustawia bit wyjątku przepełnienia jeśli wartość na szczycie stosu jest zbyt duża aby przechować ją w 32 lub 64 bitowej zmiennej pamięciowej. Instrukcje fst i fstp ustawiają flagę denormalizacji, kiedy próbujemy przechować wartość denormalizacyjną w 80 bitowym rejestrze lub zmiennej. Ustawiają flagę niepoprawnej operacji jeśli wystąpi niepoprawna operacja (taka jak przechowanie pustego rejestru). W końcu, instrukcje te ustawiają bit warunkowy C1 jeśli wystąpi zaokrąglanie podczas operacji przechowania (wystąpi tylko kiedy przechowujmy w zmiennej pamięciowej 32 lub 64 bitowej i musimy zaokrąglić mantysę aby zmieściła się w miejscu przeznaczenia). 14.4.4.3 INSTRUKCJA FXCH Instrukcja fxch wymienia wartość ze szczytu stosu z jednym z rejestrów FPU. Instrukcja ta przybiera dwie postacie: jedną z pojedynczym rejestrem FPU jako argumentem, drugą bez żadnych argumentów. Pierwsza postać wymienia szczyt stosu z określonym rejestrem. Druga postać fxch wymienia szczyt stosu z st(1). Wiele instrukcji FPU, np. fsqrt działa tylko ze szczytem rejestru stosu. Jeśli chcemy wykonać taką operację na wartości , która nie jest na szczycie stosu, możemy użyć instrukcji fxch do wymiany tos z oryginalnym rejestrem. Poniższy przykład pobiera pierwiastek kwadratowy z st(2): fxch st (2) fsqrt fxch st (2) Instrukcja fxch ustawia bit wyjątku stosu jeśli stos jest pusty. Ustawia bit niepoprawnej operacji jeśli określimy pusty rejestr jako argument. Ta instrukcja zawsze zeruje bit kodu warunku C1. 14.4.5 KONWERSJE Chip 80x87 wykonuje wszystkie arytmetyczne operacje na rzeczywistych 80 bitowych wartościach. W takim sensie instrukcje fld i fst/ fstp są instrukcjami konwersji w równym stopniu jak instrukcjami przesuwania danych ponieważ one automatycznie konwertują pomiędzy wewnętrznym 80 bitowym rzeczywistym formatem a 32 lub 64 bitowym formatem pamięci. Pomimo to będziemy je klasyfikować jako operacje przesunięcia danych , zamiast konwersji, ponieważ przesuwają on wartości rzeczywiste do i z pamięci. FPU 80x87 dostarcza pięciu
podprogramów, które konwertują do lub z formatu liczb całkowitych lub BCD kiedy przesuwamy dane. Instrukcje te to fild, fist ,fistp, fbld i fbstp. 14.4.5.1 INSRUKJA FILD Instrukcja fild (ładuj liczbę całkowitą) konwertuje 16 , 32 lub 64 bitową liczbę całkowitą uzupełnioną dwójkowo do 80 bitowego formatu o podwyższonej precyzji i odkłada wynik na stos. Instrukcja ta zawsze oczekuje pojedynczego operandu. Operand ten musi być adresem słowa, podwójnego słowa lub poczwórnego słowa zmiennej całkowitej. Chociaż format instrukcji dla fild używa dobrze znanych pól mod / rm, operand musi być zmienną pamięciową, nawet dla liczb całkowitych 16 i 32 bitowych. Nie możemy wyszczególnić jednego z 16 lub 32 bitowych rejestrów ogólnego przeznaczenia. Jeśli chcemy odłożyć rejestr ogólnego przeznaczenia 80x86 na stos FPU musimy najpierw przechować go w zmiennej pamięciowej a potem użyć fild do odłożenia tej wartości z tej zmiennej pamięciowej Instrukcja fild ustawia bit wyjątku stosu. i C1 (odpowiednio) jeśli wystąpi przepełnieni stosu podczas odkładania skonwertowanej wartości. Przykład: fild mem_16 fild mem_32 [ecx*4] fild mm_64 [ebx + ecx*8] 14.4.5.2 INSTUKCE FIST I FISTP Instrukcje fist i fistp konwertują zmienną 80 bitową o podwyższonej precyzji ze szczytu stosu do 16, 32 lub 64 bitowej liczby całkowitej i przechowują wynik w zmiennej pamięciowej określonej przez pojedynczy operand. Instrukcje te konwertują wartość z tos do liczby całkowitej według ustawień zaokrąglania w rejestrze sterującym FPU (bity 10 i 11). Instrukcje fist i fistp nie pozwalają nam określić jednego z rejestrów 16 lub 32 bitowych ogólnego przeznaczenia 80x86 jako operandów przeznaczenia Instrukcja fist konwertuje wartość ze szczytu stosu do liczby całkowitej a potem przechowuje wynik; inaczej ni wpływa na rejestr stosu zmienno przecinkowego. Instrukcja fistp zdejmuje wartość z rejestru stosu zmienno przecinkowego po przechowaniu wartości skonwertowanej. Instrukcje te ustawiają bit wyjątku stosu jeśli rejestr stosu zmienno przecinkowego jest pusty (również zeruje C1). Ustawiają bity precyzji (operacje nieprecyzyjne) i C1 jeśli wystąpi zaokrąglenie (to znaczy jeśli jest jakaś część ułamkowa wartości w st(0) ). Instrukcje te ustawiają bit wyjątku niedomiaru jeśli wynik jest zbyt mały (tj. mniejszy niż jeden ale większy niż zero lub mniejszy niż zero ale większy niż –1). Przykład: fist mem_16 [bx] fist mem_64 fistp mem_32 Nie zapomnijmy, że instrukcje te używają ustawień sterujących zaokrąglaniem określających jak będą konwertowane dane zmienno przecinkowe do liczb całkowitych podczas operacji zapamiętywania. Domyślnie, kontrola zaokrąglania jest zazwyczaj ustawiona na tryb „round”; mimo to większość programistów oczekuje od fist/ fistp obcinania części dziesiętnych podczas konwersji. Jeśli chcemy aby fist/ fistp obcinały wartości zmienno przecinkowe kiedy konwertują j do wartości całkowitych , musimy ustawić właściwie bity sterownia zaokrąglaniem w rejestrze sterującym zmienno przecinkowym. 14.4.5.3 INSTRUKCJE FBLD I FBSTP Instrukcje fbld i fbstp ładują i przechowują 80 bitowe wartości BCD. Instrukcja fbld konwertuje wartość BCD do jej 80 bitego odpowiednika o podwyższonej precyzji i odkłada wynik na stos. Instrukcja fbstp zdejmuje rzeczywiste wartości o podwyższonej precyzji z tos, konwertuje na 80 bitową wartość BCD (zaokrąglając według bitów w rejestrze sterującym zmienno przecinkowym) i przechowuje skonwertowany wynik pod adresem określonym przez operand przeznaczenia pamięci. Zauważmy, że nie ma instrukcji fbst, która przechowuje na tos bez zdejmowania jej. Instrukcja fbld ustawia bit wyjątku stosu i C1 jeśli wystąpi przepełnienie stosu. stawia bit niepoprawnej operacji jeśli próbujmy załadować niepoprawną wartość BCD. Instrukcja fbstp ustawia bit wyjątku stosu i zeruje C1 jeśli wystąpi niedomiar stosu (stos jest pusty). Ustawia flagę niedomiaru na takich samych warunkach jak fist i fistp. Przykłady: ;Zakładając mniej niż osiem pozycji na stosie, poniższa sekwencja kodu jest odpowiednikiem instrukcji fbst: fld st (0) ;duplikowanie wartości na TOS fbstp mem_80 ;poniższy przykład łatwo konwertuje 80 bitową wartość BCD do 64 bitowej liczby całkowitej:
fbld fist
bcd_80 mem_64
;pobranie wartości BCD do konwersji ;przechowanie jako liczby całkowitej
14.4.6 INSTRUKCJE ARYTMETYCZE Instrukcje arytmetyczne stanowią mały ale ważny podzbiór zbioru instrukcji 80x87. Instrukcje te dzielą się na dwie kategorie: te które działają na wartościach rzeczywistych i te które działają na wartościach rzeczywistych i całkowitych 14.4.6.1 INSTRUKCJE FADD I FADDP Te dwie instrukcje przybierają następujące postacie: fadd faddp fadd st (1), st (0) fadd st (0), st (1) faddp st(1), st(0) fadd mem Pierwsze dwie formy są równoważne. Zdejmują dwie wartości ze stosu, dodają je i odkładają ich sumę z powrotem na stos. Następne dwie formy instrukcji fadd, te z dwoma argumentami rejestrów FPU, zachowują się jak instrukcja add 80x86. Dodają wartość z argumentu drugiego rejestru do wartości argumentu pierwszego rejestru. Zauważmy, że jednym z tych rejestrów musi być st (0). Instrukcja faddp z dwoma operandami dodaje st (0) (które mus być zawsze drugim operandem) do operandu przeznaczenia (pierwszego) a potem zdejmuje st(0). Operand przeznaczenia musi być jednym z innych rejestrów FPU. Ostatnia, powyższa postać fadd z operandem pamięci dodaje 32 lub 64 bitową zmienną zmienno przecinkową do wartości w st(0). Instrukcja ta skonwertuje 32 lub 64 bitowy operand do 80 bitowej wartości o podwyższonej precyzji przed wykonaniem dodawania. Zauważmy, że instrukcja ta nie wpływa na 80 bitowy operand pamięci.. Instrukcje te mogą zgłosić wyjątki stosu, precyzji, niedomiaru, przepełnienia, denormalizacji i niepoprawnej operacji. Jeśli wystąpi wyjątek zakłócenia stosu, C1 oznacza przepełnienie lub niedomiar. 14.4.6.2 INSTRUKCJE FSUB, FSUBP, FSUBR I FSUBRP Te cztery instrukcje przybierają następujące formy: fsub fsubp fsubr fsubrp fsub fsub fsubp fsub
st(1), st(0) st(0), st(1) st(1), st(0) mem
fsubr fsubr fsubrp fsubr
st(1), st(0) st(0), st(1) st(1), st(0) mem
Instrukcje fsub i fsubp bez żadnego operandu są identyczne. Zdejmują one st0) i st(1) z rejestru stosu, obliczają st0) – st(1) i odkładają różnicę z powrotem na stos. Instrukcje fsubr i fsubrp (odwrócone odejmowanie) działają w prawie identyczny sposób z wyjątkiem tego, że obliczają st(1) – st(0) i odkładają tą różnicę na stos. Z dwoma argumentami rejestrowymi (przeznaczenie i źródło), instrukcja fsub oblicza przeznaczenie := przeznaczenie – źródło. Jednym z tych dwóch rejestrów musi być st(0). Z dwoma rejestrami jako operandami, fsubp również oblicza przeznaczenie := przeznaczenie – źródło a potem zdejmuje st(0) ze stosu po obliczeniu różnicy. Dla instrukcji fsubp, operandem źródłowym musi być st(0). Z dwoma operandami rejestrów instrukcje fsubr i fsubrp działają w podobny sposób do fsub i fsubp, z wyjątkiem tego, że obliczają przeznaczenie := źródło – przeznaczenie.
Instrukcje fsub mem i fsubr mem akceptują 32 i 64 bitowe operandy pamięci. Konwertują one operand pamięci do 80 bitowej wartości o podwyższonej precyzji i odejmują tą postać st(0) (fsub) lub odejmują st(0) z tą wartością (fsubr) i przechowują wynik w st(0). Instrukcje te mogą zgłaszać wyjątek stosu, precyzji, niedomiaru, przepełnienia, denormalizacji i niepoprawnej operacji. Jeśli wystąpi wyjątek zakłócenia stosu, C1 oznacza przepełnienie lub niedomiar stosu. 14.4.6.3 INSTRUKCJE FMUL I FMULP Instrukcje fmul i fmul mnożą dwie wartości zmienno przecinkowe. Instrukcje te mają następujące formy: fmul fmulp fmul fmul fmul
st(0), st(1) st(1), st(0) mem
fmulp
st(1), st(0)
Bez żadnego operandu, fmul i fmulp robią to samo – zdejmują st(0) i st(1), mnożą te wartości i odkładają iloczyn z powrotem na stos. Instrukcja fmul z dwoma operandami rejestrowymi oblicza przeznaczenie := przeznaczenie * źródło. Jeden z rejestrów (źródło lub przeznaczenie)musi to być st(0). Instrukcja fmulp st(i), st(0) oblicza st(i) := st(i) * st(0) a potem zdejmuje st(0). Instrukcja ta używa wartości dla i przed zdjęciem st(0). Instrukcja fmul mem wymaga 32 lub 64 bitowego operandu pamięci. Konwertuje określoną zmienną pamięciową do 80 bitowej wartości o podwyższonej precyzji i mnoży st(0) przez tą wartość. Instrukcje te mogą zgłaszać wyjątki stosu, precyzji, niedomiaru, przepełnienia , denormalizacji i niepoprawnej operacji. Jeśli wystąpi zaokrąglenie podczas obliczania, instrukcje te ustawią bit kodu warunkowego C1. Jeśli wystąpi wyjątek zakłócenia stosu, C1 oznacza przepełnienie lub niedomiar. 14.4.6.4 INSTRUKCJE FDIV, FDIVP, FDIVR I FDIVRP Te cztery instrukcje przybierają następujące formy: fdiv fdivp fdivr fdivrp fdiv fdiv fdivp
st(0), st(1) st(1), st(0) st(1), st(0)
fdivr fdivr fdivrp
st0), st(1) st(1), st(0) st(1), st(0)
fdiv fdivr
mem mem
Instrukcje fdiv i fdivp bez operandów zdejmują st(0) i st(1), obliczają st(0)/st(1) i odkładają wynik z powrotem na stos. Instrukcje fdivr i fdivrp również zdejmują st(0) i st(1) ale obliczają st(1)/st(0) przed odłożeniem ilorazu na stos. Z dwoma operandami rejestrowymi instrukcje te obliczają poniższe ilorazy: fdiv st(0), st(1) ;st(0) := st(0) / st(1) fdiv st(1), st(0) ;st(1) := st(1) / st(0) fdivp st(1), st(0) ;st(1) := st(1) / st(0) fdivr st(1), st(1) ;st(0) := st(0) / st(1) fdivrp st(1), st(0) ;st(0) := st(0) / st(1)
Instrukcje fdivp i fdivrp również zdejmują st(0) po wykonaniu operacji dzielenia. Wartość dla i w tych dwóch instrukcjach jest obliczana przed zdjęciem st0) Instrukcje te mogą zgłosić wyjątki stosu, precyzji, niedomiaru, przepełnienia, denormalizacji, dzielenia przez zero i niepoprawnej operacji. Jeśli wystąpi zaokrąglani podczas obliczeń, instrukcje te ustawią bit kodu warunkowego C1. Jeśli wystąpi wyjątek zakłócenia stosu, C1 oznacza przepełnienie lub niedomiar stosu. 14.4.6.5 INSTRUKCJA FSQRT Podprogram fsqrt nie pozwala na żadne operandy. Oblicza pierwiastek kwadratowy z wartości na tos i zastępuje st(0) tym wynikiem. Wartość na tos musi być zerem lub dodatnia, w innym przypadku fsqrt wygeneruje wyjątek niepoprawnej operacji. Instrukcja ta zgłasza wyjątki stosu, precyzji, denormalizacji i niepoprawnej operacji. Jeśli wystąpi zaokrąglenie podczas obliczenia, fsqrt ustawi bit C1. Jeśli wystąpi wyjątek zakłócenia stosu, C1 oznacza przepełnienie lub niedomiar stosu. Przykład: ; Obliczenie Z:= sqrt (x** + y**2); fld x ;ładuje X fld st(0) ;duplikuje X na TOS fmul ;obliczenie x**2 fld fld fmul fadd fsqrt fst
y st(0)
Z
;załadowanie Y ;duplikowanie Y na TOS ;obliczenie Y**2 ;obliczenie x**2 + y **2 ;obliczenie sqrt (x**2+y**2) ;przechowanie wyniku w Z
14.4.6.6 INSTRUKCJA FSCALE Instrukcja fscale zdejmuje dwie wartości ze stosu. Mnoży st(0) przez 2st(1) i odkłada wynik z powrotem na stos. Jeśli wartość w st(1) nie jest całkowita, fscale obcina ją w stronę zera przed wykonaniem tej operacji. Instrukcja ta zgłosi wyjątek stosu jeśli nie ma obecnie dwóch pozycji na stosie (również wyzeruje C1 ponieważ wystąpi przepełnienie stosu). Zgłosi wyjątek precyzji jeśli zagubimy precyzję należąca do tj operacji to wystąpi jeśli st(1) zawiera dużą, ujemną wartość).Podobnie ta instrukcja ustawi bit wyjątku przepełnienia lub niedomiaru jeśli pomnożymy st(0) przez dużą dodatnią lub ujemną potęgę dwójki. Jeśli wynik mnożenia jest bardzo mały, fscale może ustawić bit denormalizacji. Instrukcja to może również ustawić bit niepoprawnej operacji jeśli próbujemy użyć niewłaściwej wartości. Fscale ustawi C1 jeśli wystąpi zaokrąglenie w skądinąd poprawnym obliczeniu. Przykład: fild Sixteen ;odłożenie sixteen na stos fld X ;obliczenie x*(2**16) fscale Sixteen word 16 14.4.6.7 INSTRUKCJE FPEM I FPEM1 Instrukcje fprem i fpem1 obliczają resztę częściową. Intel zaprojektował instrukcję fprem przed tym zanim IEEE sfinalizował swój standard zmienno przecinkowy. W końcowym projekcie standardu zmienno przecinkowego IEE, definicja fprem trochę różniła się od Intelowskiego projektu. Niestety, Intel musiał utrzymać zgodność z istniejącym oprogramowaniem, które używało instrukcji fprem, więc zaprojektowali nową wersję obsługującą operację częściowej reszty IEEE, fpem1. Powinniśmy zawsze używać fprem1 w programach jakie piszemy, dlatego też tylko będziemy omawiać tutaj fpem1, chociaż fprem używamy w podobny sposób. Fprem1 oblicza częściową resztę z st(0) / st(1). Jeśli różnica pomiędzy wykładnikami st(0) i st(1) jest mniejsza niż 64, fprem1 może obliczyć dokładną resztę w jednej operacji. W innym przypadku będziemy musieli wykonać fprem1 dwa lub więcej razy dla uzyskania poprawnej wartości reszty. Bit kodu warunkowego C2 określa kiedy obliczenie jest ukończone. Zauważmy, że fprem1 nie zdejmuje dwóch operandów ze stosu;
pozostawia częściową resztę w st(0) a oryginalny dzielnik w st(1) w przypadku kiedy musimy obliczyć inny cząstkowy iloraz dla kompletnego wyniku. Instrukcja fpem1 ustawia flagę wyjątku stosu jeśli nie ma dwóch wartości na szczycie stosu. Ustawia bity wyjątku przepełnienia i denormalizacji jeśli wynik jest zbyt mały .Ustawia bit nieoprawnej operacji jeśli wartości na tos są niewłaściwe. Ustawia bit C2 jeśli operacja reszty cząstkowej nie jest ukończona. W końcu, ładuje C3,C1 i C0 bitami zero, jeden i dwa ilorazu, odpowiednio . Przykład: ;Obliczamy Z := X mod Y fld y fld x PartialLp: fprem1 fstsw ax test ah, 100b jnz PartialLp fstp Z fstp st(0)
;pobranie bitu warunku w ax ;zobacz czy C2 jest ustawione ;powtórz jeśli jeszcze nie zrobione ;przechowanie reszty ;zdjęcie starej wartości y
14.4.6.8 INSTRUKCJA FRNDINT Instrukcja frndint zaokrągla wartość na tos do najbliższej wartości całkowitej używając algorytmu określonego w rejestrze sterującym. Instrukcja ta ustawia flagę wyjątku stosu jeśli nie ma wartości na tos (również zeruje C1 w tym przypadku). Ustawia bity wyjątków precyzji i denormalizacji jeśli była utrata precyzji. Ustawia flagę niepoprawnej operacji jeśli wartość na tos nie jest poprawną liczbą. 14.4.6.9 INSTRUKCJA FXTRACT Instrukcja fxtract jest uzupełnieniem instrukcji fscale. Zdejmuje wartość ze stosu i odkłada wartość która jest całkowitym odpowiednikiem wykładnika ( w 80 bitowej rzeczywistej postaci), a potem odkłada mantysę z zerowym wykładnikiem (3fffh w formie obarczonej błędem). Instrukcja zgłasza wyjątek stosu jeśli jest niedomiar stosu kiedy zdejmujemy wartość oryginalną lub przepełnienie stosu kiedy odkładamy dwa wyniki (C1 określa czy wystąpiło przepełnienie czy niedomiar). Jeśli pierwotnie szczyt stosu był zerem, fxtract ustawia flagę wyjątku dzielenia przez zero. Flaga denormalizacji jest ustawiana jeśli wynik to gwarantuje; a flaga niepoprawnej operacji jest ustawiana jeśli mamy niepoprawne wartości wejściowe, kiedy wykonujemy fxtract. Przykład: ;Poniższy przykład wyciąga wykładnik binarny z X i przechowuje go w 16 bitowej zmiennej całkowitej ;Xponent fld x fxtract fstp st(0) fistp Xponent 14.4.6.10 INSTRUKCJA FABS Fabs oblicza wartość bezwzględną st(0) przez wyzerowanie bitu znaku st(0). Ustawia bit wyjątku stosu i niepoprawnej operacji jeśli stos jest pusty. Przykład: ;Obliczamy X := sqrt(abs(x)); fld fabs fsqrt fstp 14.4.6.11 INSTRUKCA FCHS
x x
Fchs zmienia znak wartości st(0) przez odwrócenie jego bitu znaku. Ustawia bit wyjątku stosu i niepoprawnej operacji jeśli stos jest pusty. Przykład: ;Obliczamy X := -X jeśli X jest dodatnie, X := X jeśli x jest ujemne fld x fabs fchs fstp x 14.7 INSTRUKCJE PORÓWNAŃ 80x87 dostarcza kilku instrukcji dla porównywania wartości rzeczywistych. Instrukcje fcom, fcomp, fcompp, fucom, fucomp i fucompp porównują dwie wartości na szczycie stosu i ustawiają właściwe kody warunkowe. Instrukcja ftst porównuje wartość ze szczytu stosu z zerem. Instrukcja fxam sprawdza wartość na tos i przekazuje informacje o znaku, normalizacji i znaczniku. Generalnie, większość programów testuje bity kodów warunków bezpośrednio po porównaniu. Niestety, nie ma żadnych instrukcji skoków warunkowych , które wykonywały by rozgałęzienia w oparciu o kody warunków FPU. Zamiast tego możemy użyć instrukcji fstsw do skopiowani rejestru stanu do rejestru ax; potem możemy użyć instrukcji sahf do skopiowania rejestru ah do bitów kodów warunków 80x86. Po wykonaniu tego, możemy użyć instrukcji skoków warunkowych do testowania jakiegoś warunku. Ta technika kopiuje C0 do flagi przeniesienia, C2 do flagi parzystości a C3 do flagi zera. Instrukcja sahf nie kopiuje C1 do żadnego bitu flagi 80x86. Ponieważ instrukcja sahf nie kopiuje żadnego bitu stanu procesora 80x87 do flagi znaku lub przepełnienia, nie możemy użyć instrukcji jg, jl, jge lub jle . Zamiast tego, użyjemy instrukcji ja, jae, jb, jbe, je i jz kiedy testujemy wyniki porównań zmienno przecinkowych. Tak , te skoki warunkowe zwykle testują wartości bez znaku a liczby zmienno przecinkowe są wartościami ze znakiem. Jednakże, używamy bez znakowych rozgałęzień warunkowych; instrukcje fstsw i sahf ustawiają rejestr flag 80x86 do stosowania skoków bez znaku 14.4.7.1 INSTRUKCJE FCOM, FCOMP I FCOMPP Instrukcje fcom, fcomp i fcompp porównują st(0) do określonego operandu i ustawiają odpowiedni bit warunkowy 80x87 w wyniku porównania. Poprawne formy tych instrukcji to: fcom fcomp fcompp fcom st(1) fcomp st(1) fcom mem fcomp mem Bez żadnych operandów, fcom, fcomp i fcompp porównują st(0) z st(1) i ustawiają stosownie flagi procesora. Dodatkowo fcomp zdejmuje st(0) ze stosu a fcompp zdejmuje ze stosu i st(0) i st(1). Z pojedynczym operandem rejestrowym, fcom i fcomp porównują st(0) z określonym rejestrem. Fcomp również zdejmuje st(0) po porównaniu. Z 32 lub 64 bitowymi operandami pamięci, instrukcje fcom i fcomp konwertują zmienną pamięciową do wartości 80 bitowej o podwyższonej precyzji a potem porównuje st(0) z tą wartością, ustawiając odpowiednio bity kodów warunkowych. Fcomp również zdejmuje st90) po porównaniu. Instrukcje te ustawiają C2 ( który kończy się we fladze parzystości) jeśli dwa operandy są nieporównywalne (np. NaN). Jeśli jest możliwe dla niepoprawnej wartości zmienno przecinkowej zakończenie porównania, powinniśmy sprawdzić flagę parzystości na obecność błędu przed sprawdzeniem żądanego warunku. Instrukcje te ustawiają bit zakłócenia stosu jeśli nie ma dwóch pozycji na szczycie rejestru stosu. Ustawiają bit wyjątku denormalizacji jeśli któryś lub oba operandy są całkowicie NaN. Instrukcje te zawsze zerują kod warunkowy C1. 14.4.7.2 INSTRUKCJE FUCOM, FUCOMP I FUCOMPP Instrukcje te są podobne do instrukcji fcom, fcomp i fcompp, chociaż przybierają tylko takie formy: fucom
fucomp fucompp fucom st(1) fucomp st(1) Różnica pomiędzy fcom/fcomp/fcompp a fucom/fucomp/fucompp jest stosunkowo mała. Instrukcje fcom/fcomp/fcompp ustawiają bit wyjątku niepoprawnej operacji, jeśli porównuje dwa NaN’y. Instrukcje fucom/fucomp/fucompp nie. W pozostałych przypadkach te dwa zbiory instrukcji działają identycznie. 14.4.7.3 INSTRUKCJA FTST Instrukcja ftst porównuje wartość w st(0) z 0.0. Działa podobnie jak instrukcja fcom, jeśli st(1) zawierałoby 0.0.Zauważmu, że instrukcja ta nie rozróżnia -0.0 od +0.0. Jeśli wartość w st(0) jest jedną z tych wartości, ftst ustawi C3 oznaczającą równość. Jeśli potrzebujemy rozróżnić miedzy –0.0 a +0.0 użyjemy instrukcji fxam. Zauważmy ,że instrukcja ta nie zdejmuje st(0) ze stosu. 14.4.7.4 INSTRUKCJA FXAM Instrukcja fxam bada wartość w st(0) i odnotowuje wynik w bitach kodu warunkowego (zobacz „Rejestr stanu FPU aby zobaczyć jak fxam ustawia te bity), Instrukcja ta nie zdejmuje st(0) ze stosu. 14.4.8 INSTRUKCJE STAŁE FPU 80x87 dostarcza kilku instrukcji, które pozwalają nam załadować powszechnie używane stałe do rejestru stosu FPU. Instrukcje te ustawiają flagi zakłócenia stosu, niepoprawnej operację i C1 jeśli wystąpi przepełnienie stosu; w innym przypadku nie wpływają na flagi FPU. Do instrukcji z tej kategorii zaliczamy: fldz ;odłożenie +0.0 fldl ;odłożenie +1.0 fldpi ;odłożenie π fldl2t ;odłożenie log2 (10) fldl2e ;odłożenie log2 (e) fldlg2 ;odłożenie log10 (2) fldln2 ;odłożenie ln (2)
14.4.9 INSTUKCJE PRZESTĘPNE 80387 i późniejsze FPU dostarcza osiem instrukcji przestępnych (logarytmicznych i trygonometrycznych) do obliczenia częściowego tangensa, arctangensa, 2x –1, y* log2(x), i y* log2(x+1).Używając różnych algebraicznych identyfikatorów, łatwo jest obliczyć większość z pozostałych powszechnych funkcji przestępnych używając tych instrukcji 14.4.9.1 INSTRUKCJA F2XM1 F2xm1 oblicza 2st(0) – 1. Wartość w st(0) musi być z zakresu –1.0 ≤ st(0) ≤ +1.0. Jeśli st(0) jest poza zakresem f2xm1 wygeneruje wynik nieokreślony ale nie zgłosi wyjątku. Wyliczona wartość zamieni wartość w st(0). Przykład: ;Obliczamy 10x używając identyfikatora : 10x = 2x*lg (10) (lg = log2) fld x fldl2t fmul f2xm1 fldl fadd Zauważmy, że f2xm1 oblicza 2x – 1, a powyższy kod dodaje 1.0 do wyniku na końcu obliczenia. 14.4.9.2 INSTRUKCJE FSIN, FCOS I FSINCOS
Instrukcje te zdejmują wartość ze szczytu rejestru stosu i obliczają sinus, cosinus lub oba i odkłada wynik(i) z powrotem na stos. Fsincos odkłada sinus po którym następuje cosinus z oryginalnego argumentu, i przechowuje cos(st(0)) w st(0) a sin(st(0)) w st(1). Instrukcje te zakładają, że st(0) określa kąt w radianach, a ten kąt musi być w zakresie –263 < st(0) 63 <+2 . Jeśli oryginalny operand jest poza zakresem, instrukcje te ustawiają flagę C2 i pozostawiają st(0) nie zmienione. Możemy użyć instrukcji fprem1 z dzielnikiem 2π , do zredukowania argumentu do stosownego zakresu . Instrukcje te ustawiają flagi zakłócenia stosu/ C1, precyzji, niedomiaru, denormalizacji i niepoprawnej operacji według wyniku obliczenia. 14.4.9.3 INSTRUKCA FPTAN Fptan oblicza tangens z st(0) i odkłada tą wartość a potem odkłada 1.0 na stos. Podobnie jak instrukcje fsin i fcos, wartość st(0) jest przyjmowana w radianach i musi być w zakresie –263 < st(0) <+263 . Jeśli wartość jest poza zakresem, fptan ustawia C2 aby wskazywał ,że konwersja nie miał miejsca. Podobnie jak przy instrukcjach fsin, fcos i fsincos, możemy zastosować instrukcję fprem1 do zredukowania tego argumentu do właściwego zakresu używając dzielnika 2π. Jeśli argument jest niewłaściwy (tj. zero lub π radianów, które powodują dzielnie przez zero) wynik jest niezdefiniowany a instrukcja ta nie zgłasza żadnych wyjątków. Fptan ustawi zakłócenie stosu, precyzję, niedomiar, denormalizację, niepoprawną operację , bity C2 i C1 jakie są wymagane przy operacji. 14.4.9.4 INSTRUKCJA FPATAN Instrukcja ta oczekuje dwóch wartości na szczycie stosu. Zdejmuje je i oblicza co następuje: st(0) = tan-1(st(1) / st(0)) Wartość wynikowa jest arctangensem współczynnika na stosie wyrażonym w radianach. Jeśli mamy wartość z jakiej życzymy sobie obliczyć tangens, użyjemy fld1 do stworzenia właściwego współczynnika a potem wykonamy instrukcję fpatan. Instrukcja ta wpływa na bity zakłócenia stosu / C1, precyzji , niedomiaru, denormalizacji i niepoprawnej operacji jeśli wystąpi problem podczas obliczenia. Ustawia bit kodu warunkowego C1 jeśli musi zaokrąglić wynik. 14.4.9.4 INSTRUKCJE FYL2X I FYL2XP1 Instrukcje fyl2x i fyl2xp1 obliczają odpowiednio st(1)* log2(st(0)) i st(1)* log2(st(0)+1). Fyl2x wymaga, żeby st(0) był większy od zera, fyl2xp1wymaga aby st(0) było z zakresu:
Fyl2x jest użyteczne przy obliczaniu logarytmów opartych na podstawie innej niż dwa; fyl2xp1 jest użyteczne dla obliczenia procentów składanych przy zachowaniu maksimum precyzji podczas obliczenia. Fyl2x może wpływać na wszystkie flagi wyjątków. C1 oznacza zaokrąglanie jeśli nie ma innych błędów, przepełnieni / niedomiar stosu jeśli jest ustawiony bit zakłócenia stosu. Instrukcja fyl2xp1 nie wpływa na flagi wyjątków przepełnienia i dzielenia przez zero. Wyjątki te występują jeśli st(0) jest bardzo małe lub wynosi zero. Ponieważ fyl2xp1 dodaje jeden do st(0) przed obliczeniem funkcji, ten warunek nigdy się nie spełni. Fyl2xp1 wpływa na inne flagi w sposób identyczny jak fyl2x. 14.4.10 INSTRUKCJE RÓŻNE FPU 80x87 zawiera kilka dodatkowych instrukcji, które sterują FPU, synchronizują działania i pozwalają nam testować i ustawiać różne bity stanu. Instrukcje te zawierają finit / fninit, fdisi / fndisi, feni / fneni , fldcw, fstcw / fnstcw, fclex / fnclex, fsave / fnsave , frstor, frstpm, fstsw / fnstsw, fstenv/ fnstenv, fldenv, fincstp, fdecstp, fwait, fnop i ffree. Fdisi / fndisi , feni / fneni i frstpm są aktywne tylko na FPU wcześniejszych niż 80387, więc nie będziemy ich rozpatrywać tutaj.
Wiele z tych instrukcji ma dwie formy. Pierwszą formą jest Fxxxx a drugą Fnxxxx. Wersja bez „N” wysyła wcześniej opcodu instrukcji fwait (która jest standardem dla większości instrukcji koprocesora). Wersja z „N” nie wysyła opcodu fwait („N” oznacza „no wait” –żadnego czekania) 14.4.10.1 INSTRUKCJE FINIT I FNINIT Instrukcja finit inicjalizuje FPU dla właściwej operacji. Nasza aplikacja powinna wykonać tą instrukcję przed wykonaniem każdej innej instrukcji FPU. Instrukcja ta inicjalizuje rejestr sterujący 37Fh, rejestr stanu zerem a znacznik słowa 0FFFFh. Inne rejestry są niezmienione. 14.4.10.2 INSTRUKCJA FWAIT Instrukcja fwait pauzuje system dopóki nie zakończy się wykonywanie bieżącej instrukcji FPU. Jest to wymagane ponieważ FPU na 80486 i wcześniejszych związkach CPU/FPU może wykonywać instrukcje równolegle z CPU. Dlatego też instrukcje FPU które odczytują lub zapisują do pamięci mogą cierpieć n zagrożenie danych jeśli główne CPU uzysk dostęp do tej samej komórki pamięci przed odczytem lub zapisem tej komórki przez FPU. Instrukcja fwait pozwala nam zsynchronizować działania z FPU poprzez oczekiwani dopóki nie zakończy się bieżąca instrukcja FPU. O rozwiązuje zagrożenie danych przez, skuteczne wprowadzenie „opóźnienia’ do wykonywanego strumienia. 14.4.10.3 INSTRUKCJE FLDCW I FSTCW Instrukcje fldcw i fstcw wymagają pojedynczego 16 bitowego operandu pamięci: fldcw mem_16 fstcw mem_16 Te dwie instrukcje ładują rejestr sterujący z komórki pamięci (fldcw) lub przechowują słowo sterujące w 16 bitowej komórce pamięci (fstcw) Kiedy używamy instrukcji fldcw do włączenia jednego z wyjątków, jeśli odpowiadająca flaga wyjątku jest ustawiona kiedy dopuszczamy ten wyjątek, FPU będzie generował natychmiastowe przerwanie przed wykonaniem przez CPU następnej instrukcji. Dlatego też powinniśmy używać instrukcji fclex do wyzerowania każdego przerwania w toku przed zmienieniem bitów dopuszczających wyjątki FPU. 14.4.10.5 INSTRUKCJE FLDENV, FSTENV I FNSTENV fstenv mem_14b fnstenv mem_14b fldenv mem_14b Instrukcje fstenv / fnstenv przechowują 14 bitowy rekord środowiskowy FPU w określonym operandzie pamięci. Kiedy działamy w trybie rzeczywistym (jedyny tryb rozważany w tym tekście), rekord środowiskowy przybiera postać jak na rysunku 14.11 Musimy wykonywać instrukcje fstenv i fnstenv przy wyłączonych przerwaniach CPU. Co więcej powinniśmy zawsze być pewni ,że FPU nie jest zajęta przed wykonaniem tej instrukcji. Jest to łatwe do wykonania przy zastosowaniu następującego kodu: pushf ;przechowanie flagi I cli ;wyłączenie przerwań fstenv mem_14b ;niejawne oczekiwanie czy nie zajęty fwait ;oczekiwanie na koniec operacji popf ;przywrócenie flagi I Instrukcja fldenv ładuje środowisko FPU z określonej komórki pamięci. Zauważmy, że instrukcja ta pozwala nam załadować słowo stanu. Nie ma żadnej jasnej instrukcji, takiej jak fldcw do wykonania tego.
Rysunek 14.11 Rekord środowiskowy FPU (16 bitowy tryb rzeczywisty) 14.4.10.6 INSTRUKCJE FSAVE, FNSAVE I FRSTOR fsave mem_94b fnsave mem_94b frstor mem_94b Instrukcje te zachowują i przywracają stan FPU. Wymaga to zachowania wszystkich wewnętrznych rejestrów sterujących, stanu i danych. Lokacja przeznaczenia dla fsave / fnsave (lokacja źródłowa dla fstor) musi by długa na 94 bajty. Pierwsze 14 bajtów odpowiada użyciu rekordów środowiska instrukcji fldenv i fstenv; pozostałe 80 bajtów przechowuje dane z rejestru stosu FPU wypisanych od st(0) do st(7). Frstor przeładowuje rekord środowiska i rejestry zmienno przecinkowe z określonego operandu pamięci. Instrukcje fsave / fnsave i frstor są głównie przeznaczone dla przełączania zadań. Możemy również użyć fsave / fnsave i frstor jako sekwencji „zdejmij wszystko” i „odłóż wszystko” dl zachowania stanu FPU. Podobnie jak przy instrukcjach fstenv i fldenv przerwania powinny być wyłączone podczas zachowywania lub przywracania stanu FPU. W przeciwnym razie inny podprogram obsługi przerwań może manipulować rejestrami FPU i unieważnić działanie operacji fsave / fnsave i frstor. Poniższy kod właściwie ochrania dane środowiskowe podczas zachowywania i odtwarzania statusu FPU: ;Zachowanie stanu FPU, zakładamy, że di wskazuje na rekord środowiska w pamięci. pushf cli fsave [si] fwait popf pushf cli frstor [si] fwait popf
14.4.10.7 INSTRUKCE FSTSW I FNSTSW fstsw fnstsw fstsw fnstsw
ax ax mem_16 mem_16
Instrukcje te przechowują rejestr stanu FPU w 16 bitowej komórce pamięci lub rejestrze ax. Instrukcje te są niezwykłe w tym sensie, że mogą one kopiować wartość FPU do jednego z rejestrów ogólnego przeznaczenia 80x86. Oczywiście, celem poza zezwoleniem na przesłanie rejestru stanu do ax jest zezwolenie CPU na łatwe testowani rejestru kodów warunku instrukcją sahf. 14.4.10.9 INSTRUKCJE FINCSTP I FDECSTP Instrukcje fincstp i fdecstp nie pobierają żadnych argumentów. Po prostu zwiększają i zmniejszają bit wskaźnika stosu (mod 8) w rejestrze stanu FPU. Te dwie instrukcje zerują flagę C1 ale nie wpływają na inne bity kodu warunkowego w rejestrze stanu FPU. 14.4.10.9 INSTRUKCJA FNOP Instrukcja fnop jest po prostu synonimem dla fst st, st(0). Nie wykonuje żadnych działań na FPU 14.4.10.10 INSTRUKCJA FFREE ffree
st (1)
Instrukcja ta modyfikuje bity znacznika dla rejestru i w rejestrze znaczników oznaczając określony rejestr jako pusty. Wartość jest nienaruszona przez tą instrukcję, ale FPU już nie może uzysk dostępu do tej danej (bez przestawienia właściwych bitów znacznika) 14.4.11 OPERACJE CAŁKOWITE FPU 80x7 dostarcza specjalnych instrukcji, które łączą liczby całkowite przekształcone do podwyższonej precyzji wraz z różnymi operacjami arytmetycznymi i porównań. Instrukcje te to: fiadd int fisub int fisubr int fimul int fidiv int fidivr int ficom int ficomp int Instrukcje te konwertują swoje 16 lub 32 bitowe argumenty całkowite na 80 bitową wartość zmienno przecinkową o podwyższonej precyzji a potem używają tej wartości jako operandu źródłowego dla określonych działań. Instrukcje te używają st(0) jako argumentu przeznaczenia. 14.8 POSUMOWANIE Dla wielu aplikacji arytmetyka liczb całkowitych ma dwie wady nie do pokonania – nie jest łato przedstawiać wartości ułamkowe z liczbami całkowitymi i liczby całkowite mają ograniczony zakres dynamiki. Arytmetyka zmienno przecinkowa dostarcza przybliżenia do rzeczywistej arytmetyki, która pokonuje te dwa ograniczenia. Arytmetyka zmienno przecinkowa jednakże też ni jest pozbawiona własnych problemów. Arytmetyka zmienno przecinkowa cierpi z powodu ograniczonej precyzji. W wyniku tego może wkraść się niedokładność do obliczeń. Dlatego też, arytmetyka zmienno przecinkowa nie do końca stosuje zasady zwykłej algebry. Jest pięć ważnych zasad do zapamiętania, kiedy używamy arytmetyki zmienno przecinkowej: (1) Kolejność wyliczenia może wpływać na wynik (2) Kiedykolwiek dodajemy lub odejmujemy liczby, dokładność wyniku może być mniejsza niż precyzja dostarczona przez format zmienno przecinkowy (3) Kiedy wykonujemy łańcuch obliczeń dodawania, odejmowania, mnożenia i dzielenia, próbujmy wykonywać najpierw mnożenie i dzielenie (4) Kiedy mnożymy i dzielimy wartości, próbujemy mnożyć duże i małe liczby razem najpierw, a potem próbujmy dzielić liczby według tych samych względnych rozmiarów (5) Kiedy porównujemy dwie liczy zmienno przecinkowe, zawsze pamiętajmy że błąd może wkraść się do obliczenia, dlatego też powinniśmy sprawdzać czy jedna wartość mieści się wewnątrz pewnego zakresu drugiej. * Matematyczna arytmetyka zmienno przecinkowa Intel wcześnie rozpoznał potrzebę sprzętowej jednostki zmienno przecinkowej. Wynajęli trzech matematyków do zaprojektowania bardzo dokładnego formatu zmienno przecinkowego i algorytmów dla ich
rodziny FPU 80x87. Formaty te, z drobnymi modyfikacjami, stały się standardami zmienno przecinkowymi IEEE 754 i IEEE 854. Standard IEEE w rzeczywistości dostarcza trzech różnych formatów: 32 bitowego formatu o standardowej precyzji, 64 bitowego formatu o podwójnej precyzji i formatu o podwyższonej precyzji. Intel zaimplementował format o podwyższonej precyzji używając 80 bitów. 32 bitowy format używa 24 bitowej mantysy (najbardziej znaczący bit jest niejawną jedynką i nie jest przechowywany w 32 bitach), ośmio bitowego stałego 127 wykładnika i jednego bitu znaku. 64 bitowy format dostarcza 53 bitowej mantysy (ponownie najbardziej znaczący bit jest zawsze jedynką i nie jest przechowywany w wartości 64 bitowej), 11 bitów dodatkowego 1023 wykładnika i jednego bitu znaku. 80 bitowy format o podwyższonej precyzji używa 64 bitowego wykładnika, 15 bitów dodatkowego 16363 wykładnika i pojedynczego bitu znaku. * Format zmienno przecinkowy IEEE Chociaż FPU 80x87 i CPU z wbudowanym FPU (80486 i Pentium) stają się bardzo popularne, jest całkiem możliwe, że będziemy musieli wykonać kod, który używa arytmetyki zmienno przecinkowej na maszynie bez FPU. W takim przypadku będziemy potrzebować wsparcia podprogramów dla wykonania arytmetyki zmienno przecinkowej. A szczęście Standardowa Biblioteka UCR dostarcza zbioru podprogramów zmienno przecinkowych, które możemy wywołać. Biblioteka Standardowa wprowadza podprogramy do ładowania i przechowywania wartości zmienno przecinkowych, konwersji pomiędzy liczbami całkowitymi a formatem zmienno przecinkowym, dodawania, odejmowania, mnożenia i dzielenia wartość zmienno przecinkowych, konwersji pomiędzy wartościami ASCII a zmienno przecinkowymi i wyprowadzania wartości zmienno przecinkowych. Nawet jeśli mamy zainstalowany FPU, podprogramy konwertujące i wyjściowe Biblioteki Standardowej są całkiem użyteczne. * Podprogramy zmienno przecinkowe Biblioteki Standardowej UCR Dla szybkiej arytmetyki zmienno przecinkowej, oprogramowanie nie ma szans przeciwko sprzętowi. FPU 80x87 dostarczają szybkich operacji zmienno przecinkowych poprzez rozszerzenie zbioru instrukcji 80x86 do działania z arytmetyką zmienno przecinkową. Dodatkowo do tych nowych instrukcji, FPU 80x87 dodaje osiem nowych rejestrów danych, rejestr sterujący, rejestr stanu i kilka innych rejestrów wewnętrznych. Rejestry danych FPU, w odróżnieniu od rejestrów ogólnego przeznaczenia 80x86 są zorganizowane w stos. Chociaż jest możliwe działanie na tych rejestrach tak jak by były one rejestrami standardowymi, większość aplikacji FPU używa mechanizmu stosu kiedy oblicza wynik zmienno przecinkowy. Rejestr sterujący FPU pozwala nam inicjalizować FPU 80x87 jednym z kilku różnych trybów. Rejestr sterujący pozwala nam ustawić nam sterowani zaokrąglaniem, precyzję dostępną podczas obliczania, i wybór tego jaki wyjątek przyczyni się do przerwania. Rejestr stanu 80x87 raportuje bieżący stan FPU. Rejestr ten dostarcza bitów które określają czy FPU jest aktualnie zajęty, określa czy poprzednia instrukcja wygenerowała wyjątek, określa liczę fizycznych rejestrów na szczycie rejestru stosu i dostarcza kodów warunkowych FPU * Koprocesor zmienno przecinkowy 80x87 * Rejestry FPU * Rejestry danych FPU * Rejestr sterujący FPU * Rejestr stanu FPU W dodatku do typów danych IEEE o pojedynczej, podwójnej i podwyższonej precyzji, FPU 80x87 również wspiera różne całkowite i BCD typy danych. FPU będzie automatycznie konwertować do i z tych typów danych kiedy ładuje i przechowuje takie wartości. * Typy danych FPU FPU 80x87 dostarcza szerokiego zakresu operacji zmienno przecinkowych poprzez zwiększenie zbioru instrukcji 80x86. Możemy zaklasyfikować instrukcje FPU w osiem kategorii: przesunięcia danych, konwersji, instrukcji arytmetycznych, instrukcji porównań, instrukcji stałych, instrukcji przestępnych, instrukcji różnych i instrukcji całkowitych. * Zbiór instrukcji FPU * Instrukcje przesunięcia danych FPU * Konwersje * Instrukcje arytmetyczne * Instrukcje porównań * Instrukcje stałe
* Instrukcje przestępne * Instrukcje różne * Operacje całkowite Chociaż 80387 i późniejsze FPU dostarczają bogaty zbiór funkcji przestępnych, jest wiele funkcji trygonometrycznych , inwersji trygonometrycznych , wykładniczych i logarytmicznych nieobecnych w tym zbiorze instrukcji. Jednakże, brakujące funkcje lato jest zsyntetyzować używając identyfikatorów algebraicznych. Ten rozdział dostarczył kodu źródłowego dla wielu z tych podprogramów jako przykład programowania FPU. 14.9 PYTANIA 1) Dlaczego nie można zastosować zwykłych zasad algebry do arytmetyki zmienno przecinkowej? 2) Oda przykład sekwencji operacji w których kolejność obliczeń tworzy różne wyniki arytmetyki o skończonej precyzji 3) Wyjaśnij dlaczego operacje dodawania i odejmowania o ograniczonej precyzji mogą powodować zagubienie precyzji podczas obliczeń. 4) Dlaczego powinniśmy ,jeśli to możliwe, wykonywać najpierw mnożenie i dzielenie w obliczeniach zawierających mnożenie i dzielenie jak również dodawanie i odejmowanie? 5) Wyjaśnij różnice pomiędzy wartością zmienno przecinkową znormalizowana, nieznormalizowaną i denormalizowaną 6) Używając Biblioteki Standardowej UCR skonwertuj następujące wyrażenia do kodu asemblerowego 80x86 zakładając ,że wszystkie zmienne są wartościami 64 bitowej podwójnej precyzji). Aby być pewnym wykonania koniecznych manipulacji załóż maksymalną dokładność. Możesz założyć, że wszystkie zmienne są z zakresu ±1e-110...± e+10: a) Z := X*X + Y*Y b) Z:= (X-Y)* Z c) Z := X*Y - X/Y d) Z:= (X+Y)/(X-Y) e) Z:= (X*X)/(Y*Y) f) Z:=X*X + Y +1.0 7) Skonwertuj powyższe instrukcje do kodu FPU 80x87 8) Następujących problemów dostarczają definicje hiperbolicznych funkcji trygonometrycznych. Zakoduj każdą z nich używając instrukcji FPU 80x87 i podprogramów exp(x) i ln(x) pokazanych w tym rozdziale
9) Stwórz funkcję log (x,y), która obliczy logy x. Algebraiczna tożsamość dla tego to:
10) Przedział arytmetyczny wymaga wykonania obliczeń, z każdym wynikiem zaokrąglonym w dół a potem powtarzaniem obliczeń z każdym wynikiem zaokrąglonym w górę. Na końcu tych dwóch obliczeń, wiesz, że prawdziwy wynik musi leżeć pomiędzy tymi dwoma obliczonymi wynikami. Bit sterowania zaokrąglaniem w rejestrze sterującym FPU pozwala Ci wybrać tryb zaokrąglania w górę i w dół. Powtórz pytanie sześć stosując przedział arytmetyczny i oblicz dwie granice dla każdego z tych problemów (a-f) 11) Mantysa bitu sterującego precyzją w rejestrze sterującym FPU steruje po prostu kiedy FPU zaokrągla wynik. Wybierając mniejszą precyzję nie poprawiamy wydajności FPU. Dlatego też, każdy nowy , napisany program powinien ustawić te dwa bity na jedynki uzyskując 64 bitową precyzję, kiedy wykonujemy obliczenia. Czy możesz podać jeden powód dlaczego możemy chcieć ustawić precyzję inną niż 64 bity? 12) Przypuśćmy, że masz dwie 64 bitowe wartości X i Y, które chcesz porównać aby sprawdzić czy są równe. Jak wiesz nie powinieneś porównywać ich bezpośrednio aby zobaczyć czy są równe, ale raczej zobaczyć czy są mniejsze niż jakąś mała , oddzielna wartość. Przyjmijmy, że ε, stały błąd, to 1e-300. Dostarcz kod, który załaduj ax zerem jeśli X=Y i załaduje ax jedynką jeśli X ≠ Y. 13) Powtórz problem 12 zakładając test dla: a) X ≤ Y b) X < Y c) X ≥ Y d) X > Y e) X ≠ Y 14) Jaką instrukcję możemy zastosować aby zobaczyć czy wartość w st(0) jest zdenormalizowana? 15) Zakładając brak przepełnienia lub niedomiaru stosu, jakie jest zazwyczaj zastosowanie dla bitu kodu warunku C1? 16) Wiele tekstów, kiedy opisuje chip FPU, sugeruje, że możesz użyć FPU dla wykonania arytmetyki całkowitej. Podawanym argumentem jest to, że FPU może wspierać 64 bitowe wielkości całkowite podczas gdy CP spiera tylko 16 lub 32 bitowe wielkości całkowite. Co jest nie tak w tej argumentacji? Dlaczego nie będziemy chcieli zastosować FPU do wykonania arytmetyki całkowitej? Dlaczego FPU nie dostarcza nawet instrukcji całkowitych? 17) Przypuśćmy, że ,masz w pamięci 64 bitową wartość zmienno przecinkową o podwójnej precyzji. Opisz jak możesz pobrać wartość bezwzględną tej zmiennej bez stosowania FPU (tj. przez użycie tylko instrukcji 80x86) 18) Wyjaśnij jak zmienić znak zmiennej z pytania 17 19) Wyjaśnij możliwy problem z następującą sekwencją kodu: stp xor
mem_64 byte ptr mem_64+7, 80h
;zmień bitu znaku
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
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ĘTNASTY: CIĄGI ZNAKÓW I ZESTAWY ZNAKÓW Ciąg jest zbiorem obiektów przechowywanych w przylegających do siebie komórkach pamięci. Ciągi są zazwyczaj tablicami bajtów, słów lub (w 80386 i późniejszych procesorach) podwójnych słów Procesory z rodziny 80x86 wspierają kilka instrukcji specjalnie zaprojektowanych do kopiowania ciągów. Rozdział ten zgłębi kilka zastosowań tych instrukcji ciągów. 8088, 8086, 80186 i 80286 mogą przetwarzać dwa typy ciągów: ciągi bajtów i ciągi słów. 80386 i późniejsze procesory działają również na ciągach podwójnych słów. Mogą one przenosić ciągi, porównywać ciągi, szukać określonej wartości wewnątrz ciągu, inicjalizować ciąg stałą wartością i inne elementarne operacje na ciągach. Instrukcje ciągów 80x86 również są użyteczne dla manipulowania tablicami, tabelami i rekordami. Możemy łatwo przypisać lub porównać takie struktury danych używając instrukcji ciągów. Używając tych instrukcji możemy znacznie przyspieszyć kod działający na tablicach. 15.0 WSTĘP Rozdział ten przedstawia przegląd operacji instrukcji ciągów 80x86. Potem omawia jak przetwarzać ciągi znaków używając tych instrukcji . W końcu, omawia instrukcje ciągów dostępne w Bibliotece Standardowej UCR. Poniższe sekcje, które mają prefiks „ •” są niezbędne. Te sekcje z „⊗” omawiają zaawansowane tematy, które możemy odłożyć na później. • Instrukcje ciągów 80x86 • Ciągi znaków • Funkcje ciągów znaków • Funkcje ciągów w Bibliotece Standardowej UCR ⊗ Zastosowanie instrukcji ciągów w innych typach danych
15.1 INSTRUKCJE CIĄGÓW 80X86 Wszyscy członkowie rodziny 80x86 wspierają pięć różnych instrukcji ciągów: movs, cmps, scas, lods i stos. Są one ciągami elementarnymi ponieważ możemy zbudować większość innych operacji ciągów z tych pięciu instrukcji. Jak używać tych pięciu instrukcji , jest tematem następnych kilku sekcji. 15.1.1 JAK DZIAŁAJĄ INSTUKCJE CIĄGÓW Instrukcje ciągów działają na blokach (tablicach o liniowej ciągłości) pamięci. Na przykład, instrukcja movs przesuwa sekwencję bajtów z jednej lokacji pamięci do innej. Instrukcja cmps porównuj dwa bloki w pamięci. Instrukcja scas przeszukuje blok pamięci pod katem określonej wartości. Te instrukcje ciągów często wymagają trzech operandów, adresu bloku przeznaczenia, adresu bloku źródłowego i (opcjonalnie) elementu zliczającego. Na przykład, kiedy używamy instrukcji movs do kopiowania ciągu, potrzebujemy adresu źródłowego i licznika (liczby elementów ciągu do przesunięcia) W odróżnieniu od innych instrukcji które działają na pamięci, instrukcje ciągów są instrukcjami jednobajtowymi, które nie maja żadnych jasnych operandów. Argumenty dla instrukcji ciągów to:
• • • • •
Rejestr si (indeks źródła) Rejestr di (indeks przeznaczenia Rejestr cx (licznik) Rejestr ax i Flaga kierunku w rejestrze FLAGS
A przykład jeden wariant instrukcji movs (przenieś ciąg) kopiuje ciąg spod adresu określonego przez ds:si do adresu określonego przez es:di, o długości cx. Podobnie instrukcja cmps porównuje ciąg wskazywany przez ds:si o długości cx z ciągiem wskazywanym przez es:di Nie wszystkie instrukcje mają operand źródłowy i przeznaczenia (tylko movs i cmps) . Na przykład instrukcja scas (przeszukaj ciąg) porównuje wartość w akumulatorze z wartością w pamięci. Pomimo różnic, instrukcje ciągów wszystkie mają jedną rzecz wspólną – używanie ich wymaga abyśmy działali na dwóch segmentach, segmencie danych i dodatkowym segmencie. 15.1.2 PRZDROSTKI REP / REPE / REPZ I REPNZ / REPNE Instrukcje ciągów same z siebie nie działają na ciągach danych. Instrukcja movs na przykład będzie przesuwała pojedynczy bajt, słowo lub podwójne słowo. Kiedy będzie się wykonywała, instrukcja movs zignoruje wartość z rejestru cx. Przedrostki powtórki mówią 80x86 aby wykonał wielobajtową operację na ciągach. Składnia dla przedrostków powtórki to: Pole: Etykieta
repeat
mnemonik
argument
rep
movs
{argumenty}
repe repz repne repnz
cmps cmps cmps cmps
{argumenty} {argumenty} {argumenty} {argumenty}
repe repz repne repnz
scas scas scas scas
{argumenty} {argumenty} {argumenty} {argumenty}
rep
stos
{argumenty}
;komentarz
Dla MOVS: Dla CMPS:
Dla SCAS:
Dla STOS: Zazwyczaj nie będziemy używali przedrostków powtórzenia z instrukcją lods. Jak widzimy, obecność przedrostków powtórzenia wprowadza nowe pole w lini źródłowej – pole przedrostka powtórzenia. Pole to pojawia si tylko w lini źródłowej zawierającej instrukcje ciągów. W naszym pliku źródłowym: • etykieta pola powinna zawsze zaczynać się w kolumnie jeden • pole powtórzenia powinno zaczynać się od pierwszego miejsca tabulacji , i • pole mnemonika powinno zaczynać się od drugiego miejsca tabulacji Kiedy określamy przedrostek powtórzenia przed instrukcją ciągu, instrukcja ta jest powtarzana cx razy. Bez tego przedrostka, instrukcja działa tylko na pojedynczym bajcie, słowie lub podwójnym słowie. Możemy użyć przedrostka powtórzenia do przetwarzania całego ciągu pojedyncza instrukcją. Możemy użyć instrukcji ciągów, bez przedrostka powtórzenia, jako elementarnych operacji na ciągach do zsyntetyzowania bardziej złożonych operacji na ciągach. Pole operacji jest opcjonalne. Jeśli jest obecne, MASM po prostu używa go do określenia rozmiaru ciągu na jakim będzie działał. Jeśli pole operandu jest nazwą zmiennej bajtowej, instrukcja ciągu będzie działała na bajtach. Jeśli argument jest adresem słowa, instrukcja działa na słowie. Podobnie z podwójnym słowem. Jeśli pole argumentu nie jest obecne, musimy dołączyć „B”, „W” lub „D” na końcu instrukcji ciągu dla oznaczenia rozmiaru np. movsb, movsw lub movsd. 15.1.3 FLAGA KIERUNKU
Oprócz rejestrów si, di si i ax, jednym z rejestrów sterujących instrukcjami ciągów 80x86 jest rejestr flag. Ściśle, flaga kierunku w rejestrze sterującym flag, jeśli CPU przetwarza ciągi. Jeśli flaga kierunku jest wyzerowana, CPU zwiększa si i di po operacji na każdym elemencie ciągu. Na przykład, jeśli flaga kierunku jest wyzerowana, wtedy wykonanie movs przeniesie bajt, słowo lub podwójne słowo spod ds:si do es:di i zwiększy si i di o jeden, dwa lub cztery. Kiedy wyspecyfikujemy przedrostek rep przed tą instrukcją, CPU zwiększy si i di dla każdego elementu w ciągu. Po zakończeniu, rejestry si i di będą wskazywały pierwszą pozycję poza ciągiem. Jeśli flag kierunku jest ustawiona, wtedy 80x86 zmniejsza si i di po przetworzeniu każdego elementu ciągu. Po powtórzeniu operacji na ciągu, rejestry si i di będą wskazywały na pierwszy bajt lub słowo przed ciągiem, jeśli flaga kierunku była ustawiona. Flaga kierunku może być ustawiona lub zerowana przy użyciu instrukcji cld (zeruj flagę kierunku) i std (ustaw flagę kierunku). Kiedy używamy tych instrukcji wewnątrz procedury, zapamiętajmy, że modyfikują one stan maszynowy. Dlatego też musimy zachować flagę kierunku podczas wykonywania tej procedury. Poniższy przykład wskazuje rodzaje problemów jakie możemy napotkać: StringStuff: cld call Str2 proc near Std ret endp
Str2
Str2
Kod ten nie pracuje właściwie. Kod wywołujący zakłada, że flaga kierunku jest wyzerowana po powrocie Str2.Jednakże, nie jest to prawda. Dlatego też, operacje na ciągach wykonywane po wywołaniu Str2 nie funkcjonują poprawnie. Jest parę sposobów zadziałania z tym problemem. Pierwszy, i prawdopodobnie najbardziej oczywisty, to zawsze wprowadzać instrukcje cld i std bezpośrednio przed wykonywaniem instrukcji na ciągach. Inną alternatywą jest zachowanie i przywrócenie flagi kierunku używając instrukcji pushf i popf. Po zastosowaniu tych dwóch technik powyższy kod będzie wyglądał jak następuje: Zawsze wstawimy cld i std przed instrukcje ciągów: StringStuff:
Str2
Str2
cld call Str2 cld proc near std ret endp Zachowanie i przywrócenie rejestr flag:
StringStuff: cld call Str2
Str2
Str2
proc near pushf std popf ret endp
Jeśli użyjemy instrukcji pushf i popf do zachowania i przywrócenia rejestru flag, zapamiętajmy, że zachowujmy i przywracamy wszystkie flagi. Dlatego też, taki podprogram nie może zwracać żadnych informacji we flagach. Na przykład, nie będziemy mogli zwrócić warunku błędu we fladze przeniesienia jeśli użyliśmy pushf i popf. 15.1.4 INSTRUKCJA MOVS Instrukcja movs przybiera cztery podstawowe formy. Movs przenosi bajty, słowa lub podwójne słowa, movsb przesuwa ciąg bajtowy, movsw przenosi ciąg słów a movsd przenosi ciąg podwójnego słowa ( na 80386 lub późniejszych procesorach). Te cztery instrukcje używają następującej składni: {REP} MOVSB {REP} MOVSW {REP} MOVSD ;dostępna na 80386 lub późniejszych procesorach {REP} MOVS Przeznaczenie, Źródło Instrukcja movsb (przenieś ciąg bajtowy) pobiera bajt spod adresu ds.:si, przechowuje go pod adresem es:di, a potem zwiększa lub zmniejsza rejestry si i di o jeden. Jeśli jest obecny przedrostek rep, CPU sprawdza cx aby zobaczyć czy zawiera zero. Jeśli nie, wtedy przenosi bajt z ds.:si do es:di i zmniejsza rejestr cx. Ten proces będzie się powtarzał dopóki cx nie będzie zawierał zero. Instrukcja movsw (przenieś ciąg słów) pobiera słowo spod adresu ds.:si, przechowuje go pod adresem es:di a potem zwiększa lub zmniejsza si i di o dwa. Jeśli jest przedrostek rep, wtedy CPU powtarza tą procedurę tak długo jak jest to określone w cx. Instrukcja movsd działa w podobny sposób na podwójnych słowach. Zwiększa lub zmniejsza si i di o cztery dla każdego przesunięcia danych. MASM automatycznie wylicza rozmiar instrukcji movs poprzez „rzut oka” na rozmiar określonych argumentów. Jeśli zdefiniujemy dwa argumenty dyrektywą byte (lub porównywalną), wtedy MASM wygeneruje instrukcję movsb. Jeśli zadeklarujemy dwie etykiety przez word (lub porównywalne), MASM wygeneruje instrukcję movsw. Jeśli zadeklarujemy dwie etykiety jako dword, MASM wygeneruje instrukcję movsd. Asembler również sprawdzi segmenty argumentów aby zapewnić, że pasują one do aktualnie założonych (przez dyrektywę assume) rejestrów es i ds. Zawsze powinniśmy używać postaci movsb, movsw i movsd i zapomnieć o postaci movs. Chociaż teoretycznie, forma movs wydaje się być eleganckim sposobem manipulowania instrukcjami przenoszenia ciągów, w praktyce tworzy więcej problemów niż jest to warte. Co więcej ta forma instrukcji przenoszenia ciągów sugeruje, że movs ma jawne argumenty, kiedy w rzeczywistości rejestry si i di pośrednio określają argumenty. Z tego powodu zawsze używajmy instrukcji movsb, movsw i movsd. Kiedy używamy przedrostka rep, instrukcja movsb przeniesie liczę bajtów określoną w rejestrze cx. Poniższy fragment kodu kopiuje 384 bajty z String1 do String2:
rep
String1 String2
cld lea lea mov movsb byte byte
si, String1 di, String2 cx, 384
384 dup (?) 384 dup (?)
Kod ten oczywiście zakłada, że String1 i String2 są w tym samym segmencie i oba rejestry ds. i es wskazują ten segment. Jeśli zastąpimy movsb przez movsw, wtedy powyższy kod przeniesie384 słowa (768 bajtów) zamiast 384 bajtów:
rep
String1 String2
cld lea lea mov movsw word word
si, String1 di, String2 cx, 384
384 dup (?) 384 dup (?)
Pamiętamy, że cx zawiera element liczącym nie licznik bajtów. Kiedy używamy instrukcji movsw, CPU przesuwa liczbę słów określonych w rejestrze. Jeśli ustawimy flagę kierunku przed wykonaniem instrukcji movsb / movsw / movsd, CPU zmniejszy rejestry si i di po przeniesieniu każdego elementu ciągu. To znaczy, że si i di muszą wskazywać koniec ich właściwych ciągów przed wywołaniem instrukcji movsb, movsw lub movsd. Na przykład
rep
String1 String2
std lea lea mov movsb byte byte
si, String1+383 di,String2+383 cx, 384
384 dup (?) 384 dup (?)
Chociaż są chwile kiedy przetwarzanie ciągu od końca do początku jest użyteczne (zobacz opis cmps w następnej sekcji), generalnie będziemy przetwarzać ciągi w kierunku do przodu ponieważ jest to dużo prostsze do zrobienia Jest jedna klasa operacji na ciągach gdzie przetwarzanie ciągów w obu kierunkach jest całkowicie obowiązkowe: przetwarzanie ciągów kiedy blok źródłowy i przeznaczenia zachodzą na siebie. Rozważmy co zdarzy się w następującym kodzie: cld lea si, String1 lea di, String2 mov cx, 384 rep movsb String1 byte ? String2 byte 384 dup (?)
Rysunek 15.1 :Nadpisywanie danych podczas operacji przenoszenia bloków Ta sekwencja instrukcji traktuje String1 i String2 jako parę 384 bajtowych ciągów. Jednak ostanie 383 bajty w tablicy String1 nachodzi na pierwsze 383 bajty w tablicy String2. Prześledźmy działanie tego kodu bajt po bajcie. Kiedy CPU wykonuje instrukcje movsb, kopiuje bajt spod ds.:si (string1) do bajtu wskazywanego przez es:di (String2). Potem zwiększa si i di, zmniejsza cx o jeden i powtarza ten proces. Teraz rejestr si wskazuje na String1+1 (który jest adresem String2) a rejestr di wskazuje na String2+1. Instrukcja movsb kopiuje bajt wskazywany przez si do bajtu wskazywanego przez di. Jednak jest to bajt pierwotnie skopiowany z lokacji String1. Więc instrukcja movsb kopiuje pierwotną wartość z lokacji String1 do obu lokacji String2 i String2+1. Ponownie, CPU zwiększa si i di, zmniejsza cx i powtarza tą operację. Teraz instrukcja movsb kopiuje bajt z lokacji String1+2 (String2+1) do lokacji String2+2. Ale znowu, jest to wartość, która pierwotnie pojawiła się w lokacji String1. Każde powtórzenie pętli kopiuj następny element w String1 do następnej dostępnej lokacji w tablicy String2. Wygląda to tak jak na rysunku 15.1
Rysunek 15.2 : Poprawny sposób przenoszenia danych przy operacji przenoszenia bloku Końcowy wynik jest taki, że X zostaje zreplikowany w całym ciągu. Instrukcja przeniesienia kopiuje argument źródłowy do komórki pamięci, która staje się operandem źródłowym dla następnych operacji przenoszenia, które powodują replikację. Jeśli rzeczywiście chcemy przesunąć jedną tablicę do innej kiedy nachodzą n siebie, powinniśmy przenieść każdy element ciągu źródłowego do ciągu przeznaczenia poczynając od końca dwóch ciągów jak pokazano na rysunku 15.2 Ustawienie flagi kierunku i wskazywanie si i di na koniec ciągów pozwoli nam (poprawnie) przenieść jeden ciąg do innego, kiedy dwa ciągi zachodzą na siebie a ciąg źródłowy zaczyna się od mniejszego adresu niż ciąg przeznaczenia. Jeśli dwa ciągi nachodzą na siebie a ciąg źródłowy zaczyna się od wyższego adresu niż ciąg przeznaczenia, wtedy zerujemy flagę kierunku a si i di wskazują początek dwóch ciągów. Jeśli dwa ciągi nie zachodzą na siebie, wtedy możemy łatwo użyć techniki przeniesienia ciągów w pamięci. Ogólnie działanie przy wyzerowanej fladze kierunku jest łatwiejsze. Nie powinniśmy używać instrukcji movs do wypełniania tablicy wartością pojedynczego bajtu, słowa lub podwójnego słowa. Inna instrukcja ciągu, stos, jest dużo lepsza do tego celu. Jednakże, dla tablic, których elementy są większe niż cztery bajty, możemy zastosować instrukcję movs do inicjalizacji całej tablicy zawartością pierwszego elementu. 15.1.5 INSTRUKCJA CMPS Instrukcja cmps porównuje dwa ciągi. CPU porównuje ciąg odnosząc się przez es:di do ciągu wskazywanego przez ds.:si. Cx zawiera długość dwóch ciągów (kiedy używamy przedrostka rep). Podobnie jak instrukcja movs, MASM pozwala na kilka różnych form tej instrukcji: {REPE} CMPSB {REPE} CMPSW {REPE} CMPSD ;dostępna tylko na 80386 i późniejszych
{REPE} {REPNE} {REPNE} {REPNE} {REPNE}
CMPS przeznaczenie źródło CMPSB CMPSW CMPSD CMPS przeznaczenie, źródło
;dostępna tylko na 80386 i późniejszych
Podobnie jak przy instrukcji movs, argumenty obecne w polu argumentów instrukcji cmps określają rozmiar argumentu. Określamy rzeczywisty adres argumentu w rejestrach si i di. Bez przedrostka powtarzania, instrukcja cmps odejmuj wartość spod lokacji es:di od wartości spod ds.:si i uaktualnia flagi. Poza uaktualnieniem flag CPU nie używa różnicy stworzonej przez to odejmowanie. Po porównaniu dwóch komórek, cmps zwiększa lub zmniejsza rejestry si i di o jeden, dwa lub cztery ( odpowiednio dla cmpsb / cmpsw / cmpsd ). Cmps zwiększa rejestry si i di jeśli flaga kierunku jest wyzerowana i zmniejsza je w przeciwnym wypadku. Oczywiście nie wykorzystamy rzeczywistej siły instrukcji cmps używając jej do porównywania pojedynczych bajtów lub słów w pamięci. Instrukcja ta bryluje kiedy używamy jej do całych ciągów. Z cmps, możemy porównywać kolejne elementy ciągu dopóki nie znajdziemy dopasowania lub dopóki nie dopasujemy kolejnych elementów. Aby porównać dwa ciągi aby zobaczyć czy są równe lub nie równe, musimy porównywać odpowiadające sobie elementy w ciągu dopóki się nie skończą. Rozważmy następujące ciągi: „String1” „String1” Jedyny sposób określenia czy te dwa ciągi są równe jest porównanie każdego znaku z pierwszego ciągu z odpowiadającym mu znakiem w drugim. W końcu drugi ciąg mógłby to być „String2” który zdecydowanie nie jest równy „String1”. Oczywiście, napotkawszy znak w ciągu przeznaczenia, który nie jest równy odpowiedniemu znakowi w ciągu źródłowym, porównanie jest zatrzymane. Nie potrzebujemy porównywać żadnego innego znaku w tych ciągach . Przedrostek repe realizuje to zadanie. Będzie porównywał następujące po sobie elementy w ciągu tak długo jak są one równe a cx jest większe od zera. Możemy porównać dwa powyższe ciągi używając następującego kodu asemblerowego 80x86: ;Zakładamy, że ciągi są w tym samym segmencie a ES i DS. , ba wskazują na ten segment cld lea si, AdrsSring1 lea di, AdrsString2 mov cx, 7 repe cmpsb Po wykonaniu instrukcji cmpsb, możemy przetestować flagi używając standardowych instrukcji skoków warunkowych. Pozwoli to nam sprawdzenie równości , nierówności, mniejsze niż, większe niż itp. Ciągi znaków są zazwyczaj porównywane przy zastosowaniu porządku leksykograficznego. W tym porządku najmniej znaczący element ciągu ma największą wagę. Jest to w bezpośrednim kontraście ze standardowym porównaniem całkowitym gdzie najbardziej znacząca część liczby niesie największą wagę. Ponadto, długość ciągu wpływa na porównanie tylko jeśli dwa ciągi są identyczne do długości krótszego ciągu. Na przykład, „Zebra” jest mniejsza niż „Zebras” ponieważ jest krótszym z dwóch ciągów, jednak „Zebra” jest większa niż „AAAAAAAAAAAH!”, chociaż jest krótsza. Porównania leksykograficzne porównują odpowiednie elementy dopóki nie napotkają nie pasującego znaku lub końca krótszego ciągu. Jeśli para odpowiednich znaków nie pasuje, wtedy algorytm ten porównuje dwa ciągi opierając się n tym pojedynczym znaku. Jeśli te dwa ciągi pasują do długości krótszego ciągu, musimy porównać ich długości. Dwa ciągi są równe jeśli , i tylko jeśli, ich długości są równe i każda odpowiadająca sobie para znaków w tych ciągach jest identyczna. Porządek leksykograficzny jest standardowym porządkiem alfabetycznym z jakim dorastaliśmy. Dla ciągów znakowych używamy instrukcji cmps w następujący sposób: • Flaga kierunku musi być wyzerowana przed porównaniem ciągów • Używamy instrukcji cmpsb do porównania ciągów na podstawie bajt przez bajt Nawet jeśli ciągi zawierają parzystą liczbę znaków, nie możemy użyć instrukcji cmpsw. Ona ni porównuje ciągów w porządku leksykograficznym. • Rejestr cx musi być załadowany długością krótszego ciągu • Używamy przedrostka repe • Rejestry ds.:si i es:di muszą wskazywać absolutnie pierwszy znak w tych dwóch ciągach, które chcemy porównać
Po wykonaniu instrukcji cmps, jeśli dwa ciągi były równe, ich długości muszą być porównane żeby zakończyć porównanie. Poniższy kod porównuje parę ciągów znakowych: lea si, źródło lea di, przeznaczenie mov cx, lengthSource mov ax, lenghtDest cmp cx, ax ja Noswap xchg ax, cx NoSwap: repe cmpsb jne NotEqual mov ax, lengthSource cmp ax, lengthDest NotEqual: Jeśli używamy bajtów do przetrzymania długości ciągów, powinniśmy zmodyfikować stosownie ten kod. Możemy również użyć instrukcji cmps do porównania wartość wielo słowa całkowitego (to jest wartość całkowita o podwyższonej precyzji). Z powodu ilości ustawień wymaganych dla porównania ciągów, nie jest to praktyczne dla wartości całkowitych mniejszych niż o długości trzech lub czterech słów, ale dla dużych wartości całkowitych jest to doskonały sposób porównania takich wartości. W odróżnieniu od ciągów znaków, nie możemy porównać ciągów całkowitych przy użyciu porządku leksykograficznego. Kiedy porównujemy ciągi, porównujemy znaki od najmniej znaczącego bajtu do najbardziej znaczącego bajtu. Kiedy porównujmy wartości całkowite musimy porównać wartości od najbardziej znaczącego bajtu (lub słowa / podwójnego słowa) w dół do najmniej znaczącego bajtu, słowa lub podwójnego słowa. Więc dla porównania dwóch 128 bitowych wartości całkowitych użyjemy następującego kodu na 80286: std lea si, SourceInteger+14 lea di, estInteger+14 mov cx, 8 repe cmpsw Kod ten porównuje wartości całkowite od ich najbardziej znaczących słów w dół do najmniej znaczącego słowa. Instrukcja cmpsw kończy się kiedy dwie wartości są nie równe lub przy zmniejszeniu cx do zera (zakładając ,że dwie wartości są równe). Ponownie flagi uwzględniają wynik porównania . Przedrostek repne instruuje instrukcję cmps aby porównywała kolejne elementy ciągu tak długo dopóki nie będą pasować. Flagi 80x86 są używane po wykonaniu tej instrukcji. Albo rejestr cx zawiera zero ( w takim przypadku dwa ciągi są całkowicie różne lub zawiera liczbę elementów porównywanych aż do dopasowania. Ta forma instrukcji cmps nie jest szczególnie użyteczna przy porównywaniu ciągów, jest użyteczna do zlokalizowania pierwszej pary dopasowanych pozycji w parz tablic bajtowych lub słów. Ogólnie jednak będziemy rzadko używali przedrostka repne z cmps. Ostatnią rzeczą do zapamiętania przy stosowaniu instrukcji cmps – wartość w rejestrze cx określa liczbę elementów do przetworzenia a nie liczbę bajtów. Dlatego też, kiedy używamy cmpsw, cx określa liczbę słów do porównania. To oczywiście jest dwukrotna liczba bajtów do porównania. 15.1.6 INSTRUKCA SCAS Instrukcja cmps porównuje dwa ciągi ze sobą. Nie możemy użyć jej do wyszukania określonego elementu wewnątrz ciągu. Na przykład nie możemy zastosować instrukcji cmps do szybkiego wyszukania zera w jakimś innym ciągu. Możemy użyć instrukcji scas (przeszukaj ciąg) dla tego zadania. W odróżnieniu od instrukcji movs i cmps, instrukcja scas wymaga tylko ciągu przeznaczenia (es:di) zamiast obu ciągów źródłowego i przeznaczenia. Operand źródłowy jest wartością w rejestrze al. (scasb), ax (scasw) lub eax (scasd) .Instrukcja scas, porównuje wartość z akumulatora (al., ax lub eax) z wartością wskazywaną przez es:di a potem zwiększa (lub zmniejsza) di o jeden, dwa lub cztery. CPU ustawia flagi stosownie do wyniku porównania. Chociaż to może być użyteczna czasami, scas jest dużo bardziej użyteczna kiedy stosujmy przedrostki repe i repne Kiedy jest obecny przedrostek repe (powtarzaj kiedy równe), scas przeszukuje ciąg poszukując elementu, który nie pasuje do wartości w akumulatorze. Kiedy użyjemy przedrostka repne (powtarzaj kiedy nie równe) ,scas przeszukuje ciąg poszukując pierwszego elementu ciągu, który jest równy wartości w akumulatorze.
Możemy być zdumieni „dlaczego te przedrostki robią dokładnie co innego niż powinny robić?”. Paragraf powyżej nie całkiem poprawnie wyraził działanie instrukcji scas. Kiedy używamy przedrostka repe ze scas, 80x86 przeszukuje cały ciąg dopóki wartość w akumulatorze jest równa argumentowi ciągu. Jest to odpowiednik przeszukiwania całego ciągu do pierwszego elementu, który nie jest dopasowany do wartości w akumulatorze. Instrukcja scas z repne przeszukuje cały ciąg dopóki akumulator nie jest równy argumentowi ciągu. Oczywiście ta postać poszukuje pierwszej wartości w ciągu dopasowanej do wartości w rejestrze akumulatora. Instrukcja scas przybiera następujące formy: {REPE} SCASB {REPE} SCASW {REPE} SCASD ;dostępny tylko na 80386 i późniejszych procesorach {REPE} SCAS przeznaczenie {REPNE} SCASB {REPNE} SCASW {REPNE} SCASD ;dostępny tylko na 80386 i późniejszych procesorach {REPNE} SCAS przeznaczenie Podobnie jak przy instrukcjach cmps i movs., wartość w rejestrze cx określa liczę elementów do przetworzenia, nie bajtów, kiedy stosujemy przedrostek powtórzenia. 15.1.7 INSTRUKCA STOS Instrukcja stos przechowuje wartość z akumulatora pod lokacją określoną przez es:di. Po przechowaniu tej wartości, CPU zwiększa lub zmniejsza di w zależności od stanu flagi kierunku. Chociaż instrukcja stos ma wiele zastosowań, podstawowym zastosowaniem jest inicjalizowanie tablic i ciągów stałymi wartościami. Na przykład jeśli mamy 265 bajtową tablicę i chcemy wypełnić ją zerami , użyjemy następującego kodu: ;Przypuszczalnie rejestr ES już wskazuje segment zawierając DestString
rep
cld lea mov xor stosw
di, DestString cx, 128 ax, ax
;256 bajtów to 128 słów ; AX := 0
Kod ten zapisuje 128 słów zamiast 256 bajtów ponieważ pojedyncza operacja stosw jest szybsza niż dwie operacje stosb. Na 80386 lub późniejszych kod ten może zapisać 64 podwójnych słów wykonując to samo szybciej. Instrukcja stos przybiera cztery formy. Oto one: {REP} {REP} {REP} {REP}
STOSB STOSW STOSD STOS przeznaczenie
Instrukcja stosb przechowuje wartość z rejestru al. W określonej komórce(-ach) pamięci, instrukcja stosw przechowuje rejestr ax w określonej komórce –ach) pamięci a instrukcja stosd przechowuje eax w określonej lokacji. Instrukcja stos jest instrukcją albo instrukcją stosb, stosw lub stosd w zależności od rozmiaru określonego argumentu. Zapamiętajmy, że instrukcja stos jest użyteczna tylko dla inicjalizowania tablic bajtu, słowa lub podwójnego słowa stałymi wartościami. Jeśli musimy zainicjalizować tablicę różnymi wartościami, nie możemy użyć instrukcji stos. Możemy użyć movs w takiej sytuacji. 15.1.8 INSTRUKCJA LODS Instrukcja lods jest unikalna pośród instrukcji ciągów. Nigdy nie będziemy używali przedrostka powtórzenia z tą instrukcją. Instrukcja lods kopiuje bajt lub słowo wskazywane przez ds.:si do rejestru al., ax lub eax, poczym zwiększa lub zmniejsza rejestr si o jeden , dwa lub cztery. Powtarzanie tej instrukcji poprzez przedrostek powtórzenia nie służyć będzie jakimkolwiek celom ponieważ rejestr akumulatora będzie nadpisywany za każdym razem, kiedy będzie powtarzana instrukcja lods. Na koniec operacji powtarzania akumulator będzie zawierał ostatnią wartość odczytaną z pamięci.
Zamiast tego używamy instrukcję lods do pobierania bajtów (lodsb) , słów (lodsw) lub podwójnych słów (lodsd) z pamięci do dalszego przetwarzania. Przez użycie instrukcji stos możemy zsyntetyzować silniejsze operacje na ciągach. Podobnie jak instrukcja stos, instrukcja lods przybiera cztery formy: {REP} LODSB {REP} LODSW {REP} LODSD ;dostępna na 80386 lub późniejszych {REP} LODS przeznaczenie Jak wspomniano wcześniej, będziemy rzadko, jeśli w ogóle, używali przedrostka rep z tymi instrukcjami. 80x86 zwiększa lub zmniejsza si o jeden, dwa lub cztery w zależności od flagi kierunku i czy użyliśmy instrukcji lodsb, lodsw lub lodsd. 15.1.9 BUDOWANIE ZŁOŻONYCH FUNKCJI CIĄGÓW Z LODS I STOS 80x86 utrzymuje tylko pięć różnych instrukcji na ciągach: movs, cmps, scas, lods i stos. Nie są to z pewnością jedyne operacje na ciągach jakie chcielibyśmy stosować. Jednak możemy użyć instrukcji lods i stos do łatwego wygenerowania jakiejś szczególnej operacji na ciągach. Na przykład przypuśćmy, że chcemy operacji na ciągach, która konwertuje wszystkie duże znaki w ciągu na małe. Możemy użyć następującego kodu: ;Przypuszczalnie ES i DS. zostały ustawione aby wskazywały ten sam segment zawierający ciąg do konwersji
Convert2Lower:
NotUpper
lea mov mov lodsb cmp jb cmp ja or stosb loop
si, String2Convert di, si cx, LenghtOfString al., ‘A’ NotUpper al., ‘Z’ NotUpper al., 20h
;pobranie następnego znaku z ciągu ;czy duży znak?
;konwersja na małą literę ;przechowanie w przeznaczeniu
Convert2Lower
Zakładając, że chcemy zmarnować 256 bajtów na tablicę, ta konwersja może być przyśpieszona przez użycie instrukcji xlat: ;Przypuszczalnie, ES i DS. zostały ustawione tak, aby wskazywały ten sam segment, zawierający ciąg do ;konwersji cld lea si, String2Convert mov di, si mov cx, LengthOfString lea bx, ConversionTable Convert2Lower: lodsb ;pobranie następnego znaku w ciągu xlat ;właściwa konwersja stosb ;przechowanie w przeznaczeniu loop Convert2Lower Tablica konwersji oczywiście będzie zawierała indeks do tablicy do każdej lokacji z wyjątkiem offsetów 41h....5Ah. Pod tymi lokacjami tablica konwersji będzie zawierała wartości 61h....7Ah (tj. indeksy ‘A’...’Z’ tablica zawierałaby kody dla ‘a’...’z’) Ponieważ instrukcje lods i stos używają akumulatora jako pośrednika ,możemy użyć każdej operacji akumulatora do szybkiego manipulowania elementami ciągu. ---------------------------------------------------------------------------------------------------------------------------------------15.1.10 PRZEDROSTKI I INSTRUCKJE CIĄGÓW Instrukcje ciągów akceptują przedrostki segmentów, przedrostki blokady i przedrostki powtórzenia. Faktycznie można określić wszystkie trzy typy przedrostków instrukcji które są pożądane. Jednakże, wskutek tego wystąpią błędy we wcześniejszych chipach 80x86 (przed 80386), wic nie powinniśmy używać więcej niż pojedynczego przedrostka (powtórzenia, blokady lub przesłonięcia segmentu) w instrukcjach ciągów, chyba ,że nasz kod będzie działał na późniejszych procesorach; prawdopodobnie nawet w obecnych dniach. Jeśli
koniecznie musimy użyć dwóch lub więcej przedrostków i uruchomić na wcześniejszych procesorach, upewnijmy się , ze wyłączyliśmy przerwania podczas wykonywania instrukcji na ciągach. 15.2 CIĄGI ZNKÓW Ponieważ będziemy natykali się na ciągi znaków częściej niż inne typy ciągów, zasługują one na specjalną uwagę. Poniższa sekcja opisuje ciągi znaków i różne typy operacji na ciągach. 15.2.1 TYPY CIĄGÓW Na większości podstawowych poziomów, instrukcje ciągów 80x86 działają tylko na tablicach znaków. Jednakże, ponieważ wiele typów danych ciągów zawiera tablicę znaków jako składnik, instrukcje ciągów są przydatne do manipulowania częścią ciągu. Prawdopodobnie największą różnicą pomiędzy ciągiem znaków a tablicą znaków jest atrybut długości. Tablica znaków zawiera stałą liczbę znaków. Ani mniej ani więcej. Ciąg znaków ma dynamiczną długość podczas wykonania, to znaczy, liczba znaków zawartych w ciągu w programie. Ciągi znaków w odróżnieniu od tablic znaków, mają zdolność do zmiany swojego rozmiaru podczas wykonywania (wewnątrz pewnego zakresu oczywiście). Komplikuje sprawy to, że są dwa ogólne typy ciągów: ciągi alokowane statycznie i ciągi alokowane dynamicznie. Ciągi alokowane statycznie są dane jako stałe, o maksymalnej długości tworzonej w czasie programu. Długość ciągu może różnić się w czasie wykonania ale tylko między zero a tą maksymalną długością. Większość systemów alokuje i dealokuje ciągi alokowane dynamicznie w obszarze pamięci kiedy używamy ciągów. Takie ciągi mogą być dowolnej długości (do jakiejś sensownej maksymalnej granicy). Dostęp do takich ciągów jest mniej wydajny niż dostęp do ciągów alokowanych statycznie. Ponadto odzyskiwanie pamięci może zabrać trochę czasu. Pomimo to, ciągi alokowane dynamicznie są dużo bardziej wydajne przestrzennie niż ciągi alokowane statycznie i , w tym przypadku, dostęp do ciągów alokowanych dynamicznie jest również szybszy. Większość z przykładów tego rozdziału będzie stosowało ciągi alokowane statycznie. Ciągi o dynamicznej długości potrzebują jakiegoś sposobu na śledzenie tej długości. Podczas gdy jest kilka możliwych sposobów przedstawiania długości ciągów, da najbardziej popularne to ciąg z przedrostkiem długości i ciąg zakończony zerem. Ciąg z przedrostkiem długości skała się z pojedynczego bajtu lub słowa, które zawiera długość ciągu. Bezpośrednio po wartości długości są znaki tworzące ciąg. Zakładając użycie bajtowego przedrostka długości możemy zdefiniować ciąg „HELLO” jak następuje: HelloStr
byte
5, „HELLO”
Ciągi z przedrostkiem długości są nazywane często ciągami Pascalowymi ponieważ jest to typ zmiennej ciągu wspierana przez większość wersji Pascala. Innym popularnym sposobem określania długości ciągu jest użycie ciągu zakończonego zerem. Ciąg zakończony zerem składa się z ciągu znaków zakończonych bajtem zerowym. Ten typ ciągów jest często nazywany ciągami C ponieważ są one typem używanym przez C/C++. Ponieważ Standardowa Biblioteka UCR naśladuje standardową bibliotekę C, również ciągów zakończonych zerem. Ciągi Pascalowe są dużo lepsze niż ciągi C/C++ z kilku powodów. Po pierwsze , obliczenie długości ciągu pascalowego jest banalne. Musimy pobrać tylko pierwszy bajt (słowo) ciągu i możemy obliczyć jego długość. Obliczanie długości ciągu C/C++ jest zdecydowanie mniej wydajne. Musimy przeszukać cały ciąg (np. używając instrukcji scasb) aż do bajtu zero. Jeśli ciąg C/C++ jest długi, może to zająć jakiś czas. Co więcej ciąg C/C++ nie może zawierać znaku NULL Z drugiej strony , ciągi C/C++ mogą być każdej długości, ale wymagają obciążającego dodatkowego pojedynczego bajtu. Ciągi Pascalowe jednak nie mogą być dłuższe niż 255 znaków kiedy używamy tylko długości pojedynczego bajtu. Dla ciągów dłuższych niż 255 bajtów, będziemy potrzebowali dwóch bajtów do przechowania długości ciągu Pascalowego. Ponieważ większość ciągów jest mniejsza niż 256 znaków długości, to nie jest wielką wadą. Zaletą ciągów zakończonych zerem jest to ,że są łatwe do użycia w programach języka asemblera. Jest to szczególnie prawdziwe dla ciągów, które są tak długie, że wymagają wielu lini kodu źródłowego w programach asemblerowych. Zliczanie każdego znaku w ciągu jest tak nużące, ze nie jest warte. Jednakże możemy napisać makro, które łatwo zbuduj dla nas ciąg Pascalowski: Pstring macro String local StringLength, StringStart byte StringLength StringStart byte String StringLength = $ - StringStart endm
Pstring „Ten ciąg ma przedrostek długości” Tak długo jak ciąg mieści się całkowicie w jednej lini źródłowej, możemy zastosować to makro do generowania ciągów w stylu Pascalowskim. Popularne funkcje ciągów. Takie jak konkatenacji, długości, odejmowani, indeks i inne są dużo łatwiejsze do napisania kiedy używamy ciągów z przedrostkiem długości. Więc będziemy używali ciągów Pascalowskich. Co więcej, Standardowa Biblioteka UCR dostarcza dużej liczby funkcji ciągów C/C++, wic nie ma potrzeby powielania tych funkcji tutaj. 15.2.2 PRZYPISYWANIE CIĄGÓW Możemy łatwo przypisać jeden ciąg do innego przy użyciu instrukcji movsb. Na przykład, jeśli chcemy przypisać ciąg z przedrostkiem długości String1 do String2 użyjemy czegoś takiego: ;Przypuszczalnie ES i DS są już ustawione
rep
lea lea mov mov inc movsb
si, Sring1 di, String2 ch, 0 cl, String1 cx
;rozszerzamy len do 16 bitów ;pobieranie długości ciągu ;obejmujemy długość bajtu
Kod ten zwiększa cx o jeden przed wykonaniem movsb ponieważ długość bajtu zawiera długość ciągu zawierającą samą długość bajtu. Ogólnie zmienna napisowa może być zainicjalizowana stałymi przez użycie makra Pstring opisanego wcześniej. Jednak, jeśli musimy ustawić zmienną napisową na jakąś wartość stałą, możemy napisać podprogram StrAssign, który przypisuje ciąg bezpośrednio po call. Poniższa procedura właśnie to robi :
cseg
include includelib
stdlib.a stdlib.lib
segment assume
para public ‘code’ cs: cseg, ds.: dseg, es: dseg, ss: sseg
;procedura przypisania ciągu MainPgm
proc mov mov mov
far ax , seg dseg ds., ax es, ax
lea di, ToString call StrAssign byte „To jest przykład jak „ byte „jest używany podprogram StrAssign” nop ExitPgm MainPgm StrAssign
proc push mov pushf push push push
near bp bp, sp ds. si di
push cx push ax push di push es cld ;Pobranie adresu ciągu źródłowego mov ax, cx mov es, ax mov di, 2[bp] mov cx, 0ffffh mov al., 0 repne scasb neg cx dec cx dec cx ;Teraz kopiujemy ciągi pop es pop di mov al., cl stosb ;Teraz kopiujemy ciąg źródłowy mov ax, cs mov ds ,ax mov si, 2[bp] rep movsb ;Uaktualniamy adres powrotny i zostawiamy: inc si mov 2[bp], si
StrAssign
pop pop pop pop pop popf pop ret endp
cseg
ends
dseg ToString dseg
segment para public ‘data’ byte 255 dup (0) ends
sseg
segment para stack ‘stack’ word 256 dup (?) ends end MainPgm
sseg
;zachowanie do późniejszego użycia
;pobranie adresu powrotu ;szukanie tak długo jak pobiera ;szukanie zera ;obliczanie długości ciągu ;konwersja długości do liczby dodatniej ;ponieważ zaczynamy od –1 nie 0 ;przeskakujemy bajt zakończony zerem ;pobranie segmentu przeznaczenia ;pobranie adresu przeznaczenia ;przechowanie bajtu długości
;przeskakujemy bajt zerowy
ax cx di si ds. bp
Kod ten używa instrukcji scas do określenia długości ciągu bezpośrednio następującego po instrukcji call. Ponieważ kod określa długość, przechowuje tą długość w pierwszym bajcie ciągu przeznaczenia a potem kopiuje tekst następujący po call do zmiennej napisowej. Po skopiowaniu ciągu, kod ten modyfikuje adres powrotny aby wskazywał poza bajt zakończony zerem. Potem procedura przekazuje sterowanie do kodu wywołującego. Oczywiście ta procedura przypisywania ciągów nie jest bardzo wydajna, ale jest bardzo łatwa do użycia. Ustawienie es:di jest takie, że wszystko co trzeba zrobić to użyć tej procedury. Jeśli potrzebujemy szybszego przypisywania ciągów po prostu użyjemy instrukcji movs: ;Przypuszczalnie, DS. i ES już były ustawione
LengthSource
lea lea mov movsb byte byte byte =
LengthSource –1 „To jest przykład jak „ „ jest używany podprogram StrAssign „ $ - SourceString
DestString
byte
256 dup (?)
rep
SourceString
si, SourceString di, DestString cx, LengthSource
Zastosowanie instrukcji wbudowanych wymaga znacznie więcej ustawień (i pisania!), ale jest dużo szybsze niż procedura StrAssign. Jeśli nie lubisz pisania, możesz zawsze napisać makro, które przypisze ciąg za ciebie. 15.2.3 PORÓWNANIE CIĄGÓW Porównywanie dwóch ciągów znakowych było już omawiane w sekcji o instrukcji cmps. Dostarczymy konkretnego przykładu, tak, że nie będzie powodu aby rozpatrywać tego tematu później. Notka: wszystkie poniższe przykłady zakładają, że es i ds wskazują właściwe segmenty zawierające ciągi przeznaczenia i źródłowy. Porównanie Str1 z Str2: lea lea
si, Str1 di, Str2
;pobranie minimalnej długości dwóch ciągów mov al., Str1 mov cl, Str2 cmp al., Str2 jb CmpStrs mov cl, Str2 ;porównanie dwóch ciągów CmpStrs” repe
mov ch, 0 cld cmpsb jne StrsNotEqual
;Jeśli CMPS stwierdzi, że są równe, porównuje ich długości, dla pewności cmp al., Str2 StrsNotEqual: Przy etykiecie StrsNotEqual, flagi będą zawierały wszystkie adekwatne informacje o miejscach tych dwóch ciągów, Możemy użyć instrukcji skoków warunkowych do przetestowania wyników tego porównania. 15.3 FUNKCJE CIĄGÓW ZNKÓW Większość jeżyków wysokiego poziomu takie jak Pascal, BASIC, C i PL/I dostarcza kilku funkcji i procedur (albo wbudowanych w język albo jako część biblioteki standardowej). Inaczej niż przy pięciu ,powyższych ,operacjach na ciągach , 80x86 nie wspiera żadnej funkcji na ciągach. Dlatego też jeśli potrzebujemy określonej funkcji ciągu, będziemy musieli napisać ją sami. Poniższe sekcje opisują wiele z popularnych funkcji ciągów i jak zaimplementować je w asemblerze. 15.3.1 SUBSTR
Funkcja Substr kopiuje część jednego ciągu do innego. W języku wysokopoziomowym funkcja ta zazwyczaj przybiera postać: DestStr := Substr (SrcStr, Index, Length); gdzie: • DestStr jest nazwą zmiennej napisowej gdzie chcemy przechować pod- ciąg • SrcStr jest nazwą ciągu źródłowego ( z którego jest pobierany podciąg) • Index jest pozycją startową znaku wewnątrz ciągu (1..length(SrcStr)), i • Length jest to długość pod-ciągu jaki chcemy skopiować do DestStr Poniższy przykład pokazuje jak działa Substr. SrcStr := ‘This is an example of a string’; DestStr := Substr (SrcStr, 11,7); write(DestStr); Wydrukuje to „example”. Wartość indeksu to jedenaście, więc funkcja Substr zacznie kopiowanie danych poczynając od jedenastego znaku w ciągu. Jedenasty znak to „e” w „example”. Długość tego ciągu to siedem. To wywołanie kopiuje siedem znaków „example” do DestStr SrcStr := ‘This is an example of a string’; DestStr := Substr (SrcStr, 1,10); write(DestStr); To wydrukuje „This is an”. Ponieważ indeks to jeden, to wystąpienie funkcji Substr zacznie kopiowanie 10 znaków poczynając od pierwszego znaku w ciągu. SrcStr := ‘This is an example of a string’; DestStr := Substr (SrcStr, 20,11); write(DestStr); To wydrukuje ‘of a string’. To wywołanie Substr wydobędzie ostatnie jedenaście znaków z ciągu. Co zdarzy się jeśli indeks i wartość długości znajdą się poza zakresem? Na przykład, co zdarzy się jeśli index będzie zerem lub większy niż długość ciągu? Co zdarzy się jeśli index jest OK., ale suma index i length jest większa niż długość ciągu źródłowego? Możemy zadziałać w tych nienormalnych sytuacjach na jeden z trzech sposobów: (1) zignorować możliwość błędu ; (2) przerwać program z błędem wykonania; (3) przetworzyć jakąś sensowna liczbę znaków w odpowiedzi na to zgłoszenie. Pierwsze rozwiązanie działa przy założeniu, że program wywołujący nigdy nie popełni pomyłki przy obliczaniu wartości dla parametrów funkcji Substr. Ślepo zakłada, że wartości przekazywane do funkcji Substr są poprawne i przetwarza ciąg w oparciu o te założenia. Może to dać dziwaczny efekt. Rozważmy następujący przykład, który używa ciągu z przedrostkiem długości: SourceStr := ‘1234567890ABCDEFGHJIKLMNOPQRSTUVWXYZ’; DestStr := Substr (SrcStr, 0,5); write(‘DestStr’); wydrukuje ‘$1234’. Powodem jest oczywiście to, że SourceStr jest ciągiem z przedrostkiem długości. Dlatego też długość 36 pojawi się pod offsetem zero wewnątrz ciągu. Jeśli Substr używa nielegalnego indeksu zero, wtedy długość ciągu będzie zwrócona jako pierwszy znak. W tym szczególnym przypadku, długość ciągu ,36, odpowiada kodowi znaku ASCII ‘$’. Sytuacja jest znacznie gorsza jeśli wartość określona dla index jest ujemna lub większa niż długość ciągu. W takim przypadku funkcja Substr zwracałaby podciąg zawierający znaki pojawiające się przed lub po ciągu źródłowym. Nie jest to wynik odpowiedni. Pomimo ignorowani problemów możliwych błędów w funkcji Substr, jest jedna duża zaleta przetwarzania podciągów w ten sposób: kod wynikowy Substr jest bardziej wydajny jeśli nie musimy wykonywać sprawdzania danych w czasie wykonywania. Jeśli wiemy, że wartości index i length są zawsze wewnątrz akceptowalnego zakresu, wtedy nie musimy wykonywać sprawdzania wewnątrz funkcji Substr. Jeśli możemy zagwarantować, ze błąd ni wystąpi, nasze programy będą działały (w pewnym stopniu)szybciej poprze wyeliminowania kontroli podczas wykonania. Ponieważ większość programów rzadko jest wolnych od błędów, mówimy o dużym ryzyku zakładając, ze wszystkie wywołania podprogramu Substr przekazują odpowiednie wartości. Dlatego też pewna porcja kontroli w czasie wykonania jest często konieczna do wyłapania błędów w programie. Błędy wystąpią przy następujących warunkach: • Parametr indeksowy (index) jest mniejszy niż jeden • Index jest większy niż długość ciągu • Parametr długości Substr (length) jest większy niż długość ciągu • Suma index i length jest większa niż długość ciągu
Alternatywa dla ignorowania każdego z tych błędów jest przerwanie informacją o błędzie. Jest to prawdopodobnie dobre podczas fazy rozwoju programu, ale jeśli nasz program jest już w rękach użytkowników, może to być rzeczywista klęska. Nasi klienci nie byliby zbytnio szczęśliwi gdyby musieli spędzać wiele dni wprowadzając dane do programu, a on przerwałby działanie powodując zgubienie danych które wprowadzali. Alternatywą dla przerwania jeśli wystąpi błąd, jest posiadanie funkcji Substr zwracającej warunek błędu. Wtedy pozostawiamy to kodowi wywołującemu, określenie czy wystąpił błąd. Technika ta dział dobrze z trzecią alternatywą obsługiwania błędów: przetwarzaniem podciągów jak najlepiej można. Trzecia alternatywa obsługi błędów jak najlepiej można jest prawdopodobnie najlepszą alternatywą. Obsługuje warunki błędu w następujący sposób: •
• • •
Parametr indeksowy (index) jest mniejszy niż. Są dwa sposoby operowania tym warunkiem błędu. Pierwszy sposób to automatyczne ustawienie parametru index na jeden i powrót do podciągu zaczynając od pierwszego znaku ciągu źródłowego. Inną alternatywą jest zwrot pustego ciągu, długość ciągu zero jako podciąg. Są również możliwe wariacje na ten temat. Możemy zwrócić podciąg zaczynając od pierwszego znaku jeśli index to zero a ciąg pusty jeśli index jest ujemny. Jeszcze inną alternatywą jest użycie liczb bez znaku. Wtedy musimy się martwić o przypadek kiedy index jest równy zero. Liczba ujemna, powinna w kodzie wywołującym przypadkiem wygenerować jeden, wyglądała by jak duża liczba dodatnia. Index jest większy niż długość ciągu .Jeśli jest taki przypadek, wtedy funkcja Substr powinna zwrócić pusty ciąg. Intuicyjnie jest to właściwa odpowiedź na tą sytuację. Parametr długości (Length) Substr jest większy niż długość ciągu - lub Suma Index i Length jest większa niż długość ciągu. Punkty trzeci czwarty maja taki sam problem, długość żądanego podciągu wychodzi poza koniec ciągu źródłowego. W tym przypadku Substr powinna zwracać podciąg składający się ze znaków zaczynających się pod Index aż do końca ciągu źródłowego.
Poniższy kod dla funkcji Substr oczekuje czterech parametrów: adresów ciągów źródłowego i przeznaczenia, indeksu startowego i długości żądanego podciągu. Substr oczekuje tych parametrów w następujących rejestrach: ds:si es:di ch cl
Adres ciągu źródłowego Adres ciągu przeznaczenia Indeks startowy Długość podciągu
Substr zwraca następujące wartości: • Podciąg pod lokacją es:di • Substr zeruje flagę przeniesienia jeśli nie było błędów. Ustawia flagę przeniesienia jeśli był błąd • Zachowuje wszystkie rejestry Jeśli wystąpi błąd, wtedy kod wywołujący musi zanalizować wartości w si, di i cx aby określić dokładny powód powstania błędu (jeśli jest to konieczne). W przypadku błędu, funkcja Substr zwróci następujące podciągi: • Jeśli parametr index (ch) to zero, Substr użyje jeden • Oba parametry Index i Length są wartościami bajtowymi bez znaku, dlatego też nigdy nie są ujemne • Jeśli parametr index jest większy niż długość ciągu źródłowego Substr zwróci pusty ciąg • Jeśli suma parametrów Index i Length jest większa niż długość ciągu źródłowego, Substr zwróci tylko znaki od Index do końca ciągu źródłowego. Poniższy kod realizuje funkcję Podciągów ;funkcja Podciąg ; ;postać HLL ; ;procedure substring (var Src :string; ; Index, Length: integer; ; var Dest : string; ; ;Src – Adres ciągu źródłowego
;Index – Indeks do ciągu źródłowego ;Length – długość podciągu do wyekstrahowania ;Dest – adres ciągu przeznaczenia ; ;Kopiuje ciąg źródłowy z adresu [Src+index] długości ;Length do ciągu przeznaczenia ; ;Jeśli wystąpi błąd, flaga przeniesienia jest zwracana jako ustawiona, w przeciwnym razie jako wyzerowana ; ;Parametry są przekazywane jak następuje: ; ;DS:SI – Adres ciągu źródłowego ;ES:DI – Adres ciągu przeznaczenia ;CH – Indeks do ciągu źródłowego ;CL – długość ciągu źródłowego ; ;Notka: ciągi wskazywane przez rejestry SI i DI są ciągami z przedrostkiem długości. To znaczy, pierwszy bajt ;każdego ciągu zawiera długość tego ciągu. Substring
proc near push ax push cx push di push si clc pushf ;Sprawdzamy poprawność parametrów cmp ch, [si] ja ReturnEmpty mov al., ch dec al. add al., cl jc TooLong cmp al., [si] jbe OkaySoFar
;zakładamy brak błędu ;zachowujemy stan flagi kierunku ;czy index jest poza długością ciągu źródłowego? ;zobacz czy suma index i length jest poza końcem ;ciągu ;błąd jeśli > 255 ;poza długością źródła?
;Jeśli pociąg nie jest całkowicie zawarty wewnątrz ciągu źródłowego, obcina go: TooLong:
OkaySoFar:
rep SubStrDone:
popf stc pushf mov sub inc mov mov inc mov mov mov add cld movsb popf pop pop pop pop ret
;zwrot flagi błędu al., [si] al., ch al. cl, al. es:[di], cl di al., ch ch, 0 ah, 0 si, ax
;pobranie maksymalnej długości ;odjęcie wartości index ;modyfikacja jeśli właściwe ;zachowanie nowej długości ;zachowanie długości ciągu przeznaczenia ;pobranie indeksu do źródła ;wartość długości poszerzonej zerem do CX ;indeks rozszerzony zerem do AX ;obliczanie adresu podciągu ;kopiowanie podciągu
si di cx ax
;Zwracanie pustego ciągu: ReturnEmpty:
SubStrDone:
mov popf stc jmp endp
byte ptr es:[di], 0 SubStrDone
15.3.2 INDEX Funkcja ciągu index przeszukuje pierwszego wystąpienia jednego ciągu wewnątrz innego i zwraca offset do tego wystąpienia. Rozważmy następującą postać HLL’a: SourceStr := ‘Hello world’; TestStr : = ‘world’; I := INDEX(SourceStr, TestStr); Funkcja index szuka w całym ciągu źródłowym pierwszego wystąpienia ciągu testowego. Jeśli znajdzie, zwraca indeks do ciągu źródłowego gdzie zaczyna się ciąg testowy. W powyższym przykładzie funkcja INDEX zwróci siedem ponieważ podciąg ‘world’ zaczyna się od siódmej pozycji znaku w ciągu źródłowym. Jedyny możliwy błąd wystąpi jeśli Index nie znajdzie ciągu testowego w ciągu źródłowym. W takiej sytuacji większość implementacji zwróci zero Nasza wersja będzie robiła podobnie. Funkcja Index wykonuje następujące działania w następujący sposób: 1) Porównuje długość ciągu testowego z długością ciągu źródłowego. Jeśli ciąg testowy jest dłuższy, Index natychmiast zwróci zero ponieważ nie ma sposobu aby ciąg testowy mógłby być znaleziony w ciągu źródłowym w takiej sytuacji 2) Funkcja index działa jak następuje: i := 1 while (i < (długość(źródło) – długość(test)) i test <> substr (źródło, i, długość (test) do i := i+1; Kiedy ta pętla się koczy, jeśli (i < długość(źródło) –długość(test) wtedy zawiera indeks do źródła gdzie zaczyna się test. W przeciwnym razie test nie jest podciągiem źródła. Używając poprzedniego przykładu, pętla ta porównuje test ze źródłem w następujący sposób: i=1 test: źródło:
world Hello world
Nie dopasowane
i=2 test: źródło:
world Hello world
Nie dopasowane
i=3 test: źródło:
world Hello world
Nie dopasowane
i=4 test: źródło:
world Hello world
Nie dopasowane
i=5 test: źródło:
world Hello world
Nie dopasowane
i=6 test: źródło:
world Hello world
Nie dopasowane
i=7 test: źródło:
world Hello world
Dopasowane
Są (algorytmiczne) lepsze sposoby wykonania tego porównania, jednakże powyższy algorytm pozwala na stosowanie instrukcji ciągów 80x86 i jest bardzo łatwy do zrozumienia. Kod Index jest następujący: ;INDEX – oblicza offset jednego ciągu w innym ; ;Na wejściu: ; ;ES: DI Wskazują na ciąg testowy ,którego szuka INDEX w ciągu źródłowym ; ;DS:SI Wskazują na ciąg źródłowy który (przypuszczalnie) zawiera ciąg szukany ; przez INDEX ; ;Na wyjściu: ; ;AX Zawiera offset do ciągu źródłowego gdzie znaleziono ciąg testowy ; INDEX proc near push si push di push bx push cx pushf ;zachowanie wartości flagi kierunku cld mov al., es:[di] ;pobranie długości ciągu testowego cmp al., [si] ;zobacz czy jest dłuższy niż długość ciągu ja NotThere ;źródłowego ;Obliczenie indeksu ostatniego znaku potrzebnego do wyliczenia ciągu testowego w ciągu ;źródłowym mov al., es:[di] ;długość ciągu testowego mov cl, al. ;zachowanie na później mov ch, 0 sub al., [si] ;długość ciągu źródłowego mov bl, al. ;liczba powtórzeń pętli inc di ;przeskoczenie długości bajtu xor ax, ax ;inicjalizacja indeksu zerem CmpLoop: inc ax ;indeks na jeden inc si ;przeniesienie na olejny znak w źródle push si ;zachowani wskaźnika ciągu i długości ciągu testowego push di push cx rep cmpsb ;porównanie ciągów pop cx ;przywrócenie wskaźnika ciągu pop di ;i długości pop si je Foundindex ;jeśli znaleziono podciąg dec bl jnz CmpLoop ;spróbujmy następne wejście w ciągu źródłowym ;Jeśli jesteśmy tutaj, ciąg testowy nie pojawił się wewnątrz ciągu źródłowego NotThere:
xor
ax, ax
;zwraca INDEX = 0
;Jeśli podciąg został znaleziony w powyższej pętli usuwamy śmiecie pozostawione na stosie Foundindex:
popf
INDEX
pop pop pop pop ret endp
cx bx di si
15.3.3 REPEAT funkcja ciągów Repeat oczekuje trzech parametrów: adresu ciągu, długości i znaku. Konstruuje ona ciąg określonej długości zawierający „długość” kopiowanego , określonego znaku. Na przykład , Repeat (STR,5,’*’) przechowa ciąg ‘*****’ w zmiennej napisowej STR. Jest to bardzo łatwa do napisania, dzięki instrukcji stosb: ;REPEAT ; ; ;Na wejściu: ; ;ES:DI ;CX ;AL. ;
Konstruuje ciąg o długości CX, gdzie każdy element jest inicjalizowany znakiem przekazywanym w Al.
Wskazują na ciąg do stworzenia Zawiera długość ciągu Zawiera znak, którym będzie inicjalizowany każdy element ciągu
REPEAT
rep
REPEAT
proc push push push pushf cld mov mov inc stosb popf pop pop pop ret endp
near di ax cx ;zachowanie wartości flagi kierunku es:[di],cl ch, 0 di
;zachowanie długości ciągu ;start ciągu od następnej lokacji
cx ax di
15.3.4 INSERT Funkcja ciągu Insert wprowadza jeden ciąg do innego. Oczekuje trzech parametrów, ciągu źródłowego, ciągu przeznaczenia i indeksu. Insert wprowadza ciąg źródłowy do ciągu przeznaczenia poczynając od offsetu określonego przez parametr indeksowy. HLL’e zazwyczaj wywołują procedurę Insert jak następuje: Źródło := ‘ there’; Przezn := ‘Hello world’; INSERT (źródło, przezn, 6); Powyższe wywołanie Insert zmieni zawartość źródła, zawierając ciąg ‘Hello there world’. Jest to zrobione przez wstawienie ciągu ‘there’ przed szóstym znakiem w ‘Hello world’. Procedura wstawiania używa następującego algorytmu:: Insert (Src, przezn, index) ; 1) Przenosi znaki z lokacji przezn + index bezpośrednio na koniec ciągu przeznaczenia length(Src) bajtów w pamięci 2) Kopiuje znaki z ciągu Src do lokacji przezn + index
3) Modyfikuje długość ciągu przeznaczenia więc jest to suma długości przeznaczenia i źródła. Poniższy kod implementuje ten algorytm: ;INSERT – Wprowadza jeden ciąg do innego ; ;Na wejściu: ; ;DS:SI Wskazują ciąg źródłowy do wstawienia ; ;ES:DI Wskazują ciąg przeznaczenia do którego będzie wstawiany ciąg źródłowy ; ;DX zawiera offset w ciągu przeznaczenia gdzie ciąg źródłowy będzie wstawiony ; ; ;Wszystkie rejestry są zachowane ; ;Warunek błędu ; ;Jeśli długość nowo stworzonego ciągu jest większa iż 255, operacja wstawiania nie będzie wykonana ;a flaga przeniesienia będzie ustawiona ; ;Jeśli indeks jest większy niż długość ciągu przeznaczenia, wtedy ciąg źródłowy będzie dołączony do końca ;ciągu przeznaczenia INSERT
proc push push push push push push clc pushf mov
near si di dx cx bx ax ;zakładamy brak błędu dh, 0
;dla bezpieczeństwa
;Najpierw zobaczymy czy nowy ciąg nie będzie zbyt długi mov ch, 0 mov ah, ch mov bh, ch mov al., es:[di] ;AX = długość ciągu przeznaczenia mov cl. [si] ;CX = długość ciągu źródłowego mov bl, al. ;BX = długość nowego ciągu add bl, cl jc TooLong ;przerwanie jeśli zbyt duży mov es:[di], bl ;uaktualnienie długości ;zobaczymy czy wartość index jest zbyt duża cmp dl, al. jbe IndexIsOK mov dl, al. IndexIsOK: ;Teraz zrobimy miejsce dla ciągu który będzie wstawiony push si ;zachowanie na później push cx mov add add std
si, di si, ax di, bx
;SI wskazuje koniec bieżącego ;ciągu przeznaczenia ;DI wskazuje koniec nowego ciągu
rep movsb ;otwarcie miejsca na nowy ciąg ;Teraz kopiujemy ciąg źródłowy do otwartej przestrzeni pop cx pop si add si, cx ;wskazanie końca ciągu źródłowego rep movsb jmp INSERTDone TooLong:: popf stc pushf INSERTDone: popf pop ax pop bx pop cx pop dx pop di pop si ret INSERT endp 15.3.5 DELETE Delete usuwa znaki z ciągu Oczekuje trzech parametrów – adres ciągu, indeksu do tego ciągu i liczby znaków do usunięcia z tego ciągu. HLL’e zazwyczaj wywołują Delete za pomocą : Delete (Str , index, length) Na przykład, Str := ‘Hello there world’; Delete(str, 7,6); To wywołanie Delete pozostawi ciąg ‘Hello world’. Algorytm dla operacji usuwania jest następujący: 1) Odejmujemy wartość parametru długości od długości ciągu przeznaczenia i uaktualniamy długość ciągu przeznaczenia tą nową wartością. 2) Kopiujemy kolejne znaki usuwanego ciągu nad krańcem usuwanego pod ciągu Jest parę błędów, które mogą wystąpić kiedy używamy procedury usuwającej. Wartość indeksu może by zerem lub większa niż rozmiar określonego ciągu. W tym przypadku procedura Delete nie powinna robić niczego z tym ciągiem. Jeśli suma parametrów index i length jest większa niż długość ciągu, wtedy procedura Delete powinna usnąć wszystkie znaki do końca ciągu. Poniższy kod implementuje procedurę Delete ;DELETE – usuwa podciągi z ciągu ; ;Na wejściu: ; ;DS.:SI Wskazują ciąg źródłowy ;DX Indeks do ciągu zaczynającego podciąg do usunięcia ; ;CX Długość podciągu do usunięcia ; ;Warunki błędu ; ;Jeśli DX jest większy niż długość ciągu, wtedy operacja jest przerywana ; ;Jeśli DX+CX są większe niż długość ciągu, DELETE usunie tylko te znaki z DX do końca ciągu DELETE
proc push push push
near es si di
push ax push cx push dx pushf ;zachowanie flagi kierunku mov ax, ds. ;ciągi przeznaczenia i źródłowy mov es, ax ;są takie same mov ah, 0 mov dh, ah ;dla bezpieczeństwa mov ch, ah ;Zobaczymy czy istnieje warunek błędu mov al., [si] ;pobranie długości ciągu cmp dl, al. ;czy indeks jest zbyt długi ja TooBig mov al., dl ;teraz zobaczmy czy INDEX + LENGTH add al., cl ;są zbyt duże jc Truncate cmp al., [si] jbe LengthIsOK ;Jeśli podciąg jest zbyt duży obcinamy go do odpowiedniego Truncate:
mov cl, [si] sub cl, dl inc cl ;Obliczamy długość nowego ciągu
,obliczamy maksymalną długość
LengthIsOK:
mov al., [si] sub cl, cl mov [si], al. ;OK., teraz usuwamy określony podciąg add si, dx mov di, si add di, cx cld rep movsb TooBig: popf pop dx pop cx pop ax pop di pop si pop es ret DELETE endp
;obliczamy adres podciągu do usunięcia ;i adres pierwszego znaku po nim ;usunięcie ciągu
15.3.6 KONKATENCJA Operacja konkatenacji bierze dwa ciągi i dołącza jeden na koniec drugiego. Na przykład, Concat(‘Hello ‘, ‘world’) stworzy łańcuch ‘Hello world’. Niektóre języki wysokiego poziomu traktują konkatenację jako wywołanie funkcji, inne jako wywołanie procedury. Ponieważ w asemblerze wszystko jest wywołaniem procedury, zaadoptujemy składnie proceduralną. Nasza procedura Concat przybierze postać jak następuje: Concat(źróło1.źróło2, przeznaczenie) Procedura ta skopiuje źródło1 do przeznaczenia, potem dołączy źródło2 na koniec przeznaczenia. ;Concat ; ; ;Na wejściu ; ;DS:SI ;DS.:BX
Kopiuje ciąg wskazywany przez SI do ciągu wskazywanego przez DI a potem łączy ciąg wskazywany przez BX do ciągu przeznaczenia
wskazuje na pierwszy ciąg źródłowy wskazuje na drugi ciąg źródłowy
;ES:DI wskazuje na ciąg przeznaczenia ; ;Warunki błędu ; ;Suma długości dwóch ciągów jest większa niż 255. W takim razie, drugi ciąg będzie obcięty aby cały ciąg był ;mniejszy niż 256 znaków. CONCAT
proc near push si push di push cx push ax pushf ;Kopiujemy pierwszy ciąg do ciągu przeznaczenia: mov al., [si] mov cl, al. mov ch, 0 mov ah, ch add al., [bx] ;oblicza sumę długości ciągów adc ah, 0 cmp ax, 256 jb SetNewLength mov ah,[si] ,zachowanie oryginalnej długości ciągu mov al., 255 ;ustalenie długości ciągu na 255 SetNewLength: mov es:[di], al. ;zachowanie nowej długości ciągu inc di ;przeskok długości bajtu inc si rep movsb ;skopiowanie źróło1 do ciągu przeznaczenia ;Jeśli suma dwóch ciągów jest zbyt duża, drugi ciąg musi być obcięty
LengthAreOK: rep
CONCAT
popf pop pop pop pop ret endp
mov cmp jb mov neg
cl, [bx] ax, 256 LengthAreOK cl, ah cl
;pobrani długości drugiego ciągu
lea
si, 1 bx]
;wskazuje drugi ciąg i pominięcie ;długości ciągu
cld movsb
;obliczanie obciętej długości ;CL := 256 – Length(Str1)
;wykonanie konkatenacji
ax cx di si
15.4 FUNCKJE CIĄGÓW W BIBLIOTECE STNDARDOWEJ UCR Standardowa Biblioteka UCR dostarcza bardzo bogatego zbioru funkcji ciągów jakie możemy użyć. Te podprogramy, przeważnie, trochę podobne do funkcji ciągów Standardowej Biblioteki C. Funkcje te wspierają ciągi zakończone zerem zamiast ciągów z przedrostkiem długości wspierane przez funkcje z poprzedniej sekcji. Ponieważ jest wiele różnych podprogramów ciągów UCR StdLib, a kody źródłowe dla wszystkich tych podprogramów są public domain ( i są zamieszczone na dołączonym do tego tekstu CD-ROM’ie), następne sekcje nie będą omawiały implementacji każdego z tych podprogramów. Zamiast tego poniższe sekcje skoncentrują się na tym jak użyć tych podprogramów bibliotecznych. Biblioteka UCR często dostarcza kilu wariantów tego samego podprogramu. Ogólnie przyrostek „l”, „m” lub „ml” pojawi się na końcu nazwy tych wariantów podprogramów. Przyrostek ‘l’ określa „stałą
literalną”, Podprogram z przyrostkiem „l” (lub „ml”) wymaga dwóch argumentów ciągów. Pierwszy jest wskazywany przez es:di a drugi występuje bezpośrednio po wywołaniu w strumieniu kodu. Większość podprogramów ciągów StdLib dział n określonych ciągach (lub jednym z ciągów jeśli funkcja ma dwa argumenty). Przyrostek „m” (lub „ml”) instruuje funkcję ciągu aby zaalokowała pamięć na stercie (używając malloc, stąd przyrostek „m”) dla nowego ciągu i przechowała tam zmodyfikowany wynik zamiast zmieniać ciąg (i) źródłowy. Podprogramy te zawsze zwracają wskaźnik do nowo stworzonego ciągu w rejestrach es:di. W przypadku błędu alokacji pamięci (niewystarczająca pamięć) podprogramy te z przyrostkiem „m” lub „ml” zwracaj ustawioną flagę przeniesienia. Zwracają one wyzerowaną tą flagę jeśli operacja zakończyła się sukcesem. 15.4.1 STRBDEL, STRBDELM Te dwa podprogramy usuwają czołowe spacje z ciągu StrBDel usuwa każdą spację czołową z ciągu wskazywanego przez es:di. W rzeczywistości modyfikuje ciąg źródłowy. StrBDelm robi kopię ciągu na stercie z usuniętymi spacjami czołowymi. Jeśli nie ma żadnych spacji czołowych, wtedy podprogram StrBDel zwróci oryginalny ciąg bez modyfikacji. Zauważmy, że te podprogramy wpływają tylko na spacje czołowe (te pojawiające się na początku ciągu). Nie usuwają spacji końcowych i spacji w środku ciągu. Do usuwania spacji końcowych zajrzyj po Strtrim. Przykład: MyString MyStrPtr
byte „ Hello there, this is my string”, 0 dword MyString les di, MyStrPtr strbdelm ;tworzy nowy ciąg bez czołowych spacji jc error ;wskaźnik do ciągu jest zwracany w ES:DI puts ;drukuje ciąg wskazywany przez ES:DI free ;dealokuje pamięć zaalokowaną przez strbdelm
;Zauważmy, że „MyString” zawiera jeszcze czołowe spacje. Poniższe wywołanie printf wydrukuje ciąg wraz z ;tymi spacjami. Powyższe „strbdelm” nie zmienia MyString printf byte „MyString = ‘%s’ \n”, 0 dword MyString les di, MyStrPtr strbdel ;Teraz rzeczywiście usuwamy czołowe spacje z „MyString” printf byte „MyString = ‘%s’\n”, 0 dword MyString Dane wyjściowe tego fragmentu kodu: Hello there, this is my string MyString = ‘ Hello there, this is my string’ MyString = ‘Hello there, this is my string’ 15.4.2 STRCAT, STRCATL, STRCATM, STRCATML Podprogramy strcat(xx) wykonują konkatenację ciągów. Na wejściu es:di wskazuje na pierwszy ciąg a dla strcat / strcatm dx:si wskazuje drugi ciąg. Dla strcatl i strcatlm drugi ciąg następuje po wywołaniu w strumieniu kodu. Podprogramy te tworzą nowy ciąg przez dołączenie drugiego ciągu na końcu pierwszego. W przypadku strcat i strcatl, drugi ciąg jest dołączany bezpośrednio do końca pierwszego ciągu (es:di) w pamięci. Musimy upewnić się ,że jest wystarczająca ilość pamięci przy końcu pierwszego ciągu, aby można było
dołączyć nowe znaki. Strcatm i strcatml tworzą nowy ciąg na stercie (używając malloc) przechowując tam wynik połączenia. Przykład: String1 String2
byte byte byte
„Hello „, 0 16 dup (0) ‘World”, 0
;miejsce dla konkatenacji
;Poniższe makro ładuje ES:DI adresem określonym w argumencie lesi
macro operand mov di , seg operand mov es, di mov di, offset operand endm ;Poniższe makro ładuje DX:SI adresem określonego operandu ldxi
macro operand mov dx, seg operand mov si, offset operand endm lesi String1 ldxi String2 strcatm ;Tworzy „Hello world” jc error ;jeśli nie wystarczająca pamięć print byte „strcatm: „,0 puts ;Drukuje „Hello world” putcr free ;dealokacja pamięci ciągu lesi String1 ;tworzenie ciągu strcatml ;’Hello world” jc error ;jeśli niewystarczająca pamięć byte „there”, 0 print byte „strcatml:”, 0 puts ;drukuje „Hello world” putcr free lesi String1 ldxi String2 strcat ;tworzenie „Hello world” printf byte „strcat: %s\n”, 0 ;Notka: ponieważ strcat w rzeczywistości modyfikuje String1, poniższe wywołanie strcatl dołączy ;”there” na koniec ciągu „Hello world” lesi String1 strcatl byte „there”, 0
printf byte „strcatl: %s\n”, 0 Powyższy kod stworzy następujące dane wyjściowe: strcatm: Hello world strcatml: Hello there strcat: Hello world strcatl: Hello world there 15.4.3 STRCHR Strchr poszukuje pierwszego wystąpienia pojedynczego znaku wewnątrz ciągu .Jest w tym podobna trochę do instrukcji scasb. Jednak nie musimy określać wyraźnie długości kiesy używamy tej funkcji jak było to przy scasb. Na wejściu es:di wskazują ciąg jaki chcemy przeszukać, al. Zawiera wartość szukana. Przy zwracaniu, flaga przeniesienia oznacza sukces (C = 1 znaczy, że znak nie jest obecny w ciągu C =0 znaczy, że znak jest w ciągu). Jeśli znak został znaleziony w ciągu, cx zawiera indeks do ciągu gdzie strchr ulokował znak. Zauważmy, że pierwszy znak ciągu jest pod indeksem zero. Więc strchr zwróci zero jeśli al pasuje do pierwszego znaku ciągu. Jeśli flaga przeniesienia jest ustawiona, wtedy wartość w cx nie ma znaczenia. Przykład: ;Zauważmy, że poniższy ciąg ma kropkę pod lokacją „HasPeroiod +24” HasPeriod
byte „This string has period.:, 0 lesi HasPeriod ;zobacz strcat dla definicji lesi mov al., ‘.’ ;poszukiwanie kropki strchr jnc GotPeriod print byte „ Żadnej kropki w ciągu ”,cr,lf, 0 jmp Done ;Jeśli kropka jest znaleziona, wyprowadzamy offset do ciągu: GotPeriod:
print byte mov puti putcr
„Kropka znaleziona pod offsetem „, 0 ax, cx
Done: Ten kod tworzy dane wyjściowe; Znaleziono kropkę pod offsetem 24 15.4.4 STRCMP, STRCMPL,STRICMP, STRICMPL Podprogramy te porównują ciągi używając porządku leksykograficznego. Na wejściu do strcmp lub stricmp, es:di wskazuje pierwszy ciąg a dx:si wskazuje drugi ciąg. Strcmp porównuje pierwszy ciąg z drugim i zwraca wynik porównania we fladze rejestrów. Strcmpl działa w podobny sposób, z wyjątkiem tego, że drugi ciąg jest wywoływany w strumieniu kodu. Podprogramy stricmp i stricmpl różnią się od swoich odpowiedników w tym, że ignorują wielkość liter podczas porównania. Podczas gdy strcmp zwróciłby „nie równe” kiedy porównuje ”Strcmp” z „strcmp”, podprogramy stricmp (i stricmpl) zwrócą „równe’ ponieważ jedyną różnicą jest duża litera kontra mała litera. „i” w stricmp i stricmpl oznacza „ignoruj wielkość liter”. Przykład:
String1 String2 String3
IsGtrEql:
byte byte byte lesi ldxi strcmp jae printf byte dword jmp printf byte dword
„Hello world’, 0 „hello world”, 0 „Hello there’, 0
String1 String2 IsGtrEql „%s jest mniejszy niż %s\n’, 0 String1, String2 Try1 „%s jest większy niż %s\n”, 0 String1, String2
Try1:
lesi String2 strcmpl byte „hi world!”, 0 jne NotEql printf byte „Hmm..., %s jest równe ‘hi world!’\n”, 0 dword String2 jmp Try1
NotEql:
printf byte „%s nie jest równe ‘hi world!’ \n”, 0 dword String2
Try1:
lesi ldxi stricmp jne printf byte dword jmp
String1 String2 BadCmp „Ignoruj wielkość liter. %s równe %s \n”, 0 String1, String2 Tryil
BadCmp:
printf byte „Wow, stricmp nie działa! %s <> %S\n”,0 dword String1, String2
Tryil:
lesi String2 stricmpl byte „hELLO THERE”, 0 jne BadCmp2 print byte „Stricmpl zadziałał”, cr, lf, 0 jmp Done
BadCmp2:
print Byte
„Stricmp nie zadziałał”, cr, lf, 0
Done: 15.4.5 STRCPY,STRCPYL,STRDUP,STRDUPL
Podprogramy strcpy i strdup kopiują jeden ciąg do innego. Nie ma podprogramów strcpym lub strcpyml . Strdup i strdupl odpowiadają tym operacjom. Standardowa Biblioteka UCR używa nazw strdup i strdupl zamiast strcpym i strcpyml, więc używa tych samych nazw jak standardowa biblioteka C. Strcpy kopiuje ciąg wskazywany przez es:di do komórki pamięci zaczynającej się pod adresem dx:si. Nie ma żadnej kontroli błędów; musimy zapewnić, że jest odpowiednia ilość wolnej przestrzeni pod lokacją dx:si przed wywołaniem strcpy. Strcpy zwraca pod es:di wskazujący ciąg przeznaczenia (to znaczy oryginalną wartość dx:si). Strcpyl działa w podobny sposób, z wyjątkiem ciągu źródłowego następującego po wywołaniu. Strdup duplikuje ciąg , który wskazuje es:di i zwraca wskaźnik do nowego ciągu na stercie. Strdupl działa w podobny sposób, z wyjątkiem tego, że ciąg występuje po wywołaniu. Jak zwykle, flaga przeniesienia jest ustawiona jeśli wystąpi błąd alokacji pamięci kiedy używamy strdup lub strdupl. Przykład: String1 String2 String3 StrVar1 StrVar2
byte byte byte dword dword lesi ldxi strcpy
„Copy this string”, 0 32 dup (0) 32 dup (0) 0 0
String1 String2
ldxi String3 strcpyl byte „This string, too!”, 0 lesi strdup jc mov mov
String1 error word ptr StrVar1, di word ptr StrVar1+2, es
strdupl jc byte mov mov
error „Also this string”, 0 word ptr StrVar2, di word ptr StrVar2+2, es
printf byte byte byte byte dword
„strcpy: %s\n” „strcpyl: %s\n” „strdup: %^s\n” „strdupl: %^s\n”, 0 String2, String3, StrVar1, StrVar2
;jeśli niewystarczająca pamięć ;zachowanie wskaźnika do ciągu
15.4.6 STRDEL, STRDELM Strdel i strdelm usuwają znaki z ciągu. Strdel usuwa określone znaki z ciągu , strdelm tworzy nową kopię ciągu źródłowego bez określonych znaków. Na wejściu, es:di wskazują ciąg do obróbki, cx zawiera indeks do ciągu gdzie zaczyna się usuwanie a ax zawiera liczbę znaków do usunięcia z ciągu. Przy zwrocie, es:di wskazują nowy ciąg (który jest na stercie jeśli wywołaliśmy strdelm). Tylko dla strdelm ,jeśli flaga przeniesienia jest ustawiona przy zwrocie, będzie błąd alokacji pamięci. Tak jak dla wszystkich podprogramów ciągów StdLib UCR, wartości indeksu dla ciągów są oparte na zerze. To znaczy zero jest indeksem pierwszego znaku w ciągu źródłowym. Przykład: String1
byte -
„Hello there, how are you?”, 0
lesi mov mov strdelm jc print byte puts putcr lesi mov mov strdel printf byte dword Kod ten daje nam co następuje:
String1 cx, 5 ax, 6
;start od pozycji pięć ( „ there”) ;usunięcie szóstego znaku ;stworzenie nowego ciągu ;jeśli niewystarczająca pamięć
error „New string:”, 0
String1 ax, 11 cx, 13 „Zmodyfikowany ciąg; %s\n”, 0 String1
New string: Hello, how are you? Modified string: Hello there 15.4.7 STRINS, STRINSL, STRINSM, STRINSML Funkcje strins(xx) wprowadzają jeden ciąg do innego. Dla wszystkich czterech podprogramów es:di wskazuje ciąg źródłowy do którego chcemy wprowadzić inny ciąg .Cx zawiera punkt wprowadzenia (0...długość ciągu źródłowego) Dla strins i strinsm, dx:si wskazuje ciąg jaki życzymy sobie wprowadzić. Dla strinsl i strinslm ciąg do wprowadzenia pojawia się jako stałą literalna w strumieniu kodu. Strins i strinsl wprowadzają drugi ciąg bezpośrednio do ciągu wskazywanego przez es:di. Strinsm i strinsml robią kopię ciągu źródłowego i wprowadzają drugi ciąg do tej kopii. Zwracają one wskaźnik do nowego ciągu w es:di. Jeśli wystąpi błąd alokacji pamięci wtedy strinsm / strinsml ustawiają flagę przeniesienia przy powrocie. Dla strins i strinsl, pierwszy ciąg musi mieć odpowiednio zaalokowaną pamięć dla przechowania nowego ciągu. Przykład: InsertInMe InsertStr StrPtr1 StrPtr2
byte byte byte dword dword lesi ldxi mov strinsm mov mov
„Insert >< 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 „<”
word ptr StrPtr1, di word ptr StrPtr1+2, es
„insert that’, 0 word ptr StrPtr2, di word ptr StrPtr2+2, es
„ „, 0
;dwie spacje
lesi ldxi mov strins printf byte byte byte dword
InsertInMe InsertStr cx, 9
;przed pierwszą ze spacji
„Pierwszy ciąg: %^s\n” „Drugi ciąg: %^s\n” „Trzeci ciąg: %s\n”, 0 StrPtr1, StrPtr2, InsertInMe
Zauważmy, że powyższe operacje strins i strinsl, obie wprowadzają ciągi do tego samego ciągu przeznaczenia. Dane wyjściowe powyższego kodu: Pierwszy ciąg: Insert >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 „>” 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 <[bp+6]> textequ <[bp+4]>
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 cx dx
mov
dx, MyPortAdrs
;adres bazowy LPT1:
mov out
al, 0Fh dx, al.
;zapis wartości “wszystkie jedynki” do ;naszego portu wyjściowego
mov inc in dec and loopne je clc pop pop ret
cx, 0 dx al., dx dx al., 78h Wait40s Got0s
Wait40s:
Got0s:
stc pop pop
;zwraca powodzenie dx cx
;wskazuje port wejściowy ;odczyt portu stanu ;wskazuje ponownie port danych ;maska bitów wejściowych ;Pętla dopóki ustawiamy zera ;wszystkie zera? Jeśli tak, skok ;zwraca nie powodzenie
dx cx ;zwraca powodzenie dx cx
Send1s
ret endp
; Synchronizacja- Procedura ta zapisuje wszystkie zera i wszystkie jedynki do swojego portu wyjściowego i ; sprawdza stan portu wejściowego aby zobaczyć czy węzeł odbiorczy został zsynchronizowany. ; Kiedy jest zsynchronizowany, zapisze wartość 05h do swojego portu wyjściowego. Więc kiedy ; ten węzeł zobaczy wartość 05h na swoim porcie wejściowym, oba węzły zostaną ; zsynchronizowane. Zwraca ustawioną flagę przeniesienia jeśli operacja zakończyła się ; powodzeniem, wyczyszczoną jeśli niepowodzeniem Synchronize
SyncLoop:
proc print byte byte
near
mov
dx, MportAdrs
mov call jc
cx, TimeOutConst Send0s Got1s
„Srynchronizing with receiver program” cr, lf, 0
;opóźnienie przekroczenia czasu ;wysłanie bitów zero, czekanie na jedynki ; (przeniesienie ustawione = jedynki)
; Jeśli nie mamy to na co oczekiwaliśmy, zapisujemy jedynki w tym punkcie i zobaczymy czy poza ; wprowadzeniem węzła odbiorczego Retry0:
call jc
Send1s SyncLoop
;wysyłamy jedynki, czekamy na zera ;przeniesienie ustawione = zera
;Cóż, nie dostaliśmy jeszcze odpowiedzi, zobaczmy czy użytkownik nacisnął ctrl-C aby przerwać program DoRetry:
call
TestAbort
;Okay, węzeł odbiorczy już odpowiedział. Wracamy i próbujemy ponownie loop
SyncLoop
; Jeśli przekroczyliśmy czas, drukuje informację o błędzie i zwraca wyczyszczoną flagę przeniesienia ; (oznaczającą błąd przekroczenia czasu) print byte byte clc ret
„Transmit: Timeout error waiting for receiver” cr, lf, 0
;Okay, zapisaliśmy zera I mamy jedynki. Zapiszmy jedynki i zobaczymy czy mamy zera. Jeśli nie ponawiamy ; pętlę Got1s: call jnc
Send1s DoRetry
;wysyłamy bity jedynek, czekamy na zera ;(przeniesieni ustawione = zera)
;Dobrze, wydaje się być zsynchronizowane. Dla pewności zróbmy to raz jeszcze call jnc call jnc
Send0a Retry0 Snd1s DoRetry
;wysyłamy zera , czekamy na jedynki ;wysyłamy jedynki, czekamy na zera
; Zsynchronizowaliśmy .Wyślijmy wartość 05h do węzła odbiorczego co pozwoli poznać ,że wszystko jest w ; porządku: mov out
al., 05h dx, al.
;wysłanie sygnału do odbiorcy ;aby powiedzieć, że jest synchronizacja
FinalDelay:
xor loop
cx, cx FinalDelay
;długie opóźnienie dające czas odbiorcy ; na przygotowanie
Synchronize
print byte byet stc ret endp
„Synchronized with receiving site” cr, lf, 0
; Podprogramy I/O plików: ; ; GetFileInfoOtwiera plik określony przez użytkownika i przekazuje nazwę pliku i jego rozmiar do ; węzła odbiorczego. Zwraca ustawioną flagę przeniesienia jeśli operacja zakończyła się ; powodzeniem, wyczyszczoną jeśli niepowodzeniem GetFileInfo
proc
near
; Pobranie nazwy pliku z lini poleceń DOS: mov argv mov mov
ax, 1 word ptr FileNamePtr, di word ptr FileNamePtr+2, es
printf byte “Opening %^s\n”,0 dword FileNamePtr ;Otwieramy plik: push mov lds int pop jc mov
ds ax, 3D00h dx, FileNamePtr 21h ds. BadFile FileHandle, ax
;otwarcie do odczytu
;obliczamy rozmiar tego pliku (robimy to poprzez pozycjonowanie na ostatniej pozycji w pliku i ; użycie pozycji powrotnej jako długości pliku): mov mov xor xor int jc
bx, ax ax, 4202h cx, cx dx, dx 21h BadFile
;potrzebna obsługa w BX ;pozycja końca pliku ;umieszczenie na pozycji zero ; z końca pliku
; Zachowujemy końcową pozycję jako długość pliku: mov
word ptr FileSize, ax
mov
word ptr FileSize+2, dx
;Musimy przewinąć plik na początek (ustawienie pozycji zero): mov mov xor xor int jc
bx, FileHandle ax, 4202h cx, cx dx, dx 21h BadFile
;pozycja początku pliku ;ustawienie pozycji zero
;Okay, przekazujemy do węzła odbiorczego:
odbiorcy SendName:
BadFile:
mov call mov call mov call mov call
al., byte ptr FileSize SendByte al., byte ptr FileSie+1 SendByte al, byte ptr FileSize+2 SendByte al, byte ptr FileSize+3 SendByte
;wysłanie rozmiaru pliku
les
bx, FileNamePtr
;wysyłamy
mov call inc cmp jne stc ret
al., es;[bx] SendByte bx al., 0 SendName
;dopóki nie trafimy na bajt zero
znaki
nazwy
;zwraca powodzenie
GetFileInfo
print byte puti putcr clc ret endp
;GetFileData-
procedura ta odczytuje dane z pliku I transmituje je do odbiorcy jako bajt w czasie
GetFileData
proc mov mov mov lea int jc
near ah, 3Fh cx, 512 bx, FileHandle dx, FileBuffer 21h GFDError
mov jcxz lea mov call inc loop jmp
cx, ax GFDone bx, FileBuffer al., [bx] SendByte bx XmitLoop GetFileData
XmitLoop:
pliku
„Error transmitting file infroamtion:”, 0
;opcod odczytu DOS ;odczyt 512 bajtów w czasie ;plik do odczytu ;bufor do przechowywania danych ;odczyt danej ;wyjście jeśli błąd odczytu danych ; zachowanie # bajta odczytanego ;wyjście jeśli EOF ;wysłanie bajtów bufora plików do odbiorcy
;odczyt reszty pliku
do
GFDError:
GFDone: GetFileData
print byte puti print byte ret endp
„DOS error #”, 0 ‚ while reading file“, cr, lf, 0
;Okay, tu mamy program główny, któ®y steruje wszystkim Main
proc mov ax, dseg mov ds, ax meminit
;Najpierw pobieramy adres LPT1: z obszaru zmiennych BIOS mov mov mov mov
ax, 40h es, ax ax, es:[PrtrBase] MyPortAdrs, ax
; Zobaczmy czy mamy parametr nazwy pliku: argc cmp je print byte jmp GotName:
call jnc
cx, 1 GotName „Usage: transmit ”, cr,lf,0 Quit Synchronize Quit
Quit: Main
call GetFileData ExitPgm endp
cseg
ends
ssego stk sseg
segment para stack ‘stack’ byte 1024 dup (“stack”) ends
zzzzzzseg LastBytes Zzzzzzseg
segment para public ‘zzzzzz’ byte 16 dup (?) ends end Main
;czekanie na program przekaźnika ;pobieranie danych h pliku
Tu mamy program odbiornika, który akceptuje i przechowuje dany wysyłane przez powyższy program : ; RECEIVE.ASM ; ;Program ten jest odbiorczą częścią programu, który przekazuje pliki przez kompatybilny kabel równoległy z ; Laplink ; ; Program ten zakłada, że użytkownik chce używać LPT1: dla transmisji. Modyfikuje przyrównywanie lub czyta ; port z lini poleceń jeśli jest niewłaściwy
.286 .xlist include includelib .list
stdlib.a stdlib.lib
dseg
segment para public ‘data’
TimeOutConst Prtrbase
equ equ
100 8
;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 FileName
dword ? byte 128 dup (0)
dseg
ends
cseg
segment para public ‘code’ assume cs:cseg, ds:dseg
;TestAbort-
Odczytuje klawiaturę i podaje użytkownikowi sposobność do włączenia klawiszy ctrl-C
TestAbort
TestAbort
proc push mov int je mov int pop ret endp
; GetByte-
odczytuje pojedynczy bajt z portu równoległego (cztery bity w czasie). Zwraca bajt w AL.
GetByte
proc push push
NoKeyPress:
near ax ah, 1 16h NoKeypress ah, 8 21h ax
;rozmiar nadchodzącego pliku ; przechowuje nazwę pliku
;zobacz czy coś naciśnięto ;odczyt znaku, sprawdzenie na ctrl-C
near cx dx
; odbiór mniej znaczącego nibble’a
W4DLp: Wait4Data:
mov mov out
dx, MyPortAdrs al., 10h dx, al.
;sygnał nie zajęty
inc
dx
;wskazuje port stanu
mov in test loopne je call jmp
cx, 10000 al., dx al., 80h Wait4Data DataIsAvail TestAbort W4DLp
;zobacz czy dana dostępna ; (bit 7 = 0 jeśli dana dostępna) ;czy dana jest dostępna? ;jeśli nie sprawdzamy ctrl-C
DataIsAvail:
shr and mov
al., 3 al., 0Fh ah, al.
;zachowujemy pakiet czterech bitów ; (to jest mniej znaczący nibble naszego bajtu)
dec mov out
dx al., 0 dx, al.
;wskazuje rejestr danych
inc dx mov cx, 10000 in al., dx test al., 80h loope Wait4Ack jne NextNibble call TestAbort jmp W4Alp ; odbiór bardziej znaczącego nibble’a: W4Alp: Wait4Ack:
NextNibble:
;wskazuje rejestr stanu ;czeka aż przekaźnik cofnie dostępną daną ;pętla dopóki pętla niedostępna ;skok jeśli dana niedostępna ;zezwala użytkownikowi na ctrl-C
dec mov out inc mov in test loopne je call jmp
dx al., 10h dx, al. dx cx, 10000 al., dx al., 80h Wait4Data2 DataAvail2 TestAbort W4D2Lp
shl and or dec mov out
al., 1 al., 0FH ah, al. dx al., 0 dx, al.
;dzielimy ten bardziej znaczący nibble z ; istniejącym mniej znaczącym nibble’m
inc mov in test loope jne call jmp
dx cx, 10000 al, dx al., 80h Wait4Ack2 ReturnData TestAbort WrA2Lp
;wskazuje rejestr stanu
al., ah dx cx
GetByte
mov pop pop ret endp
; Synchronize; ; ;
Procedura ta oczekuje dopóki widzie same zera w bitach wejściowych jakie odebraliśmy z węzła transmisji. Ponieważ odebrano same zera, zapisuje same jedynki do portu wyjściowego . Kiedy mamy same jedynki, zapisujemy same zera. Powtarzamy ten proces dopóki węzeł transmisji zapisze wartość 05h
Synchronize
proc
W4D2Lp: Wait4Data2:
DataAvail2:
W4A2Lp: Wait4Ack2:
ReturnData:
near
;wskazuje rejestr danych ; sygnał nie zajęty ;wskazuje stan portu ;zobaczmy czy dana jest dostępna ;(bit 7 = 0 jeśli dostępna) ;pętla dopóki data jest dostępna ;skok jeśli dostępna ;sprawdzamy ctrl-C
;wskazuje rejestr danych ;sygnał danej pobrany
;czeka na przekaźnik aż cofnie dostępną daną ;czekamy aż dana niedostępna ;skok jeśli nie dostępna ;sprawdzenie ctrl-C ;włożenie danej do al
SyncLoop: SyncLoop0:
print byte byte
„Synchronizing with transmitter program” cr, lf, 0
mov mov out mov
dx, MyPortAdrs al, 0 dx, al. bx, TimeOutConst
mov inc in dec and cmp je cmp loopne
cx, 0 dx al., dx dx al., 78h al., 78h Got1s al. 0 SyncLoop0
;inicjalizujemy nasz port wyjściowy ; zapobiegając pomyłce ;warunek przekroczenia czasu ;dla celów przekroczenia czasu ;wskazuje port wejściowy ;odczyt naszych bitów wejściowych ;trzymamy tylko bity danej ;sprawdzamy dla wszystkich jedynek ;skok jeśli same jedynki ;zobacz czy same zera
;Ponieważ widzieliśmy zero, zapisujemy same jedynki do portu wyjściowego mov out
al., 0FFh dx, al.
;zapisz wszystkie jedynki
;teraz czekamy aż same jedynki nadejdą z węzła transmisji SyncLoop1:
inc in dec and cmp loopne je
dx al., dx dx al., 78h al., 78h SyncLoop1 Got1s
;Wskazuje rejestr stanu ;odczyt stanu portu ;wskazuje z powrotem rejestr danych ;trzymamy tylko bity danej ;czy wszystkie są jedynkami?
;jeśli przekroczyliśmy czas, sprawdzamy aby zobaczyć czy użytkownik nacisnął ctrl-C aby przerwać call dec jne
TestAbort dx SyncLoop
;sprawdzamy ctrl-C ;zobaczmy czy przekroczyliśmy czas ;powtarzamy jeśli tak
print byte byte clc ret
„Receive: connected time out during synchronization” cr, lf, 0 ;sygnał przekroczenia czasu
; Skaczemy tu jeśli widzieliśmy i zero i jeden. Wysyłamy je dwa w kombinacji dopóki nie dostaniemy 05h z ; węzła transmisji lub użytkownik nie naciśnie ctrl-C Got1s:
inc in dec shr and cmp je not out call
dx al., dx dx al. 3 al., 0Fh al., 05h Synchronized al. dx, al. TestAbort
;wskazuje rejestr stanu ;kopiujemy cokolwiek pojawi się w naszym porcie ;wejściowym do portu wyjściowego dopóki węzeł ;transmisji nie wyśle nam wartości 05h
;trzymamy odwrócone to co dostaliśmy i wysyłamy ; to do tarnsmittera ;sparwdzamy ctrl-C
jmp
Got1s
;Okay, zsynchronizowaliśmy, wracamy do kodu wywołującego Synchronized:
Synchronize ;GetFileInfoGetFileInfo
and out print byte byte stc ret endp
al., 0Fh dx, al.
;upewniamy się czy bit busy to jeden ; (bit 4 = 0 dla busy = 1)
“Synchronized with transmitting site” cr, lf, 0
Program przekaźnika wysyła nam długość pliku i nazwę pliku zakończoną zerem proc near mov dx, MyPortAdrs mov al, 10h ;ustawiamy bit busy na zero out dx, al.
;Pierwsze cztery bajty zwierają rozmiar pliku: call mov call mov call mov call mov
GetByte byte ptr FileSize, al GetByte byte ptr FileSize+1, al GetByte byte ptr FileSize+2, al GetByte byte ptr FileSize+3, al
; kolejne n bajtów (do bajtu zakończonego zerem) zawierają nazwę pliku GetFileName:
mov call mov call inc cmp jne
bx, 0 GetByte FileName[bx], al TestAbort bx al, 0 GetFileName
GetFileInfo
ret endp
;GetFileData-
Odbiera plik danych z węzła transmisji I zapisuje go do pliku wyjściowego
GetFileData
proc
near
;najpierw, zobaczymy czy mamy więcej niż 512 bajtów cmp jne cmp jbe
word ptr FileSize+2, 0 MoreThan512 word ptr FileSize, 512 LastBlock
;jeśli bardziej znaczące słowo nie jest ;zerem, więcej niż 512. ;jeśli bardziej znaczące słowo jest zerem, ;sprawdzamy mnij znaczące słowo
; mamy więcej niż 512 bajtów w tym pliku, odczytujemy 512 bajtów w tum punkcie MoreThan512:
mov lea
cx, 512 bx, FileBuffer
;odbieramy 512 bajtów ;z przekaźnika
ReadLoop:
call mov inc loop
GetByte [bx], al bx ReadLoop
;odczyt bajtu ;zachowujemy bajt ;przechodzimy do kolejnego elementu ;bufora
;Okay, zapisujemy dane do pliku: mov mov mov lea int jc
ah, 40h bx, FileHandle cx, 512 dx, FileBuffer 21h BadWrite
;opcod zapisu DOS ;zapis do tego pliku ;zapis 512 bajtów ; z tego adresu ;wyjście jeśli błąd
;zmniejszenie rozmiaru pliku do 512 bajtów: sub sbb jmp
word ptr FileSize, 512 word ptr FileSize, 0 GetFileData
;32 bitowe odejmowanie z 512
;Przetwarzamy ostatni blok, który zawiera 1..511 bajtów LastBlock: ReadLB:
BadWrite:
mov lea call mov inc loop mov mov mov lea int jnc print byte put1 print byte
cx, word ptr FileSize bx, FileBuffer GetByte [bx], al bx ReadLB ah, 40h bx, FileHandle cx, word ptr FileSize dx, FileBuffer 21h Closefile
;odbiór ostatnich 1..511 bajtów z ;przekaźnika
;zapis ostatniego bloku do pliku
“DOD error #”, 0 “ while writing data.”, cr, lf,0
;Tu zamykamy plik CloseFile:
GetFileData
mov mov int ret endp
bx, FileHandle ah, 3Eh 21h
; Tu jest program główny Main
proc mov ax, dseg mov ds, ax meminit
;najpierw pobieramy adres LPT1: z obszaru zmiennych BIOS
;zamykamy ten plik ;opcod zamykania DOS
mov mov mov mov
ax, 40h es, ax ax, es:[PrtrBase] MyPortAdrs, ax
;wskazuje segment zmiennych BIOS
call jnc
Synchronize Quit
;oczekujemy na program przekaźnika
call
GetFileInfo
;pobieramy nazwę i rozmiar pliku
printf byte „Filename: %s\nFile size: %ld\n”, 0 dword FileName, FileSize mov mov lea int jnc print byte jmp GoodOpen:
ah, 3Ch cx, 0 dx, Filename 21h GoodOpen
;stworzenie pliku ;atrybuty standardowe
„Error opening file”, cr,lf,0 Quit
Quit: Main cseg
mov FileHandle, ax call GetFileData ExitPgm endp 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
;pobranie danych pliku
21.5 PODSUMOWANIE Port równoległy PC , chociaż pierwotnie zaprojektowany do sterownia drukarkami równoległymi , jest uniwersalnym ośmio bitowym portem wyjściowym z kilkoma liniami uzgodnień, jakie możemy zastosować do sterowania różnymi innymi urządzeniami niż drukarki. Teoretycznie, komunikacja równoległa powinna być wiele razy szybsza niż komunikacja szeregowa. W praktyce, jednak ograniczenia świata rzeczywistego i ekonomia uniemożliwiają taki przypadek. Niemniej jedna możemy połączyć wysoko wydajne urządzenia do portu równoległego PC. Porty równoległe PC dzielą się na jednokierunkowe i dwukierunkowe. Wersje dwukierunkowe są dostępne tylko na PS/2, pewnych laptopach i kilku innych maszynach. Podczas gdy osiem lini danych jest wyjściowych tylko w portach jednokierunkowych, możemy oprogramować je jako wejściowe lub wyjściowe w porcie dwukierunkowym. Chociaż operacje dwukierunkowe są skierowane do drukarki, można poprawić wydajność innych urządzeń połączonych z portem równoległym, takie jak dysk i streamer, łącza sieciowe, SCSI i tak dalej. Kiedy system komunikuje si z innym urządzeniem przez port równoległy, potrzebuje jakiegoś sposobu przekazania urządzeniu, że dana jest dostępna na lini danych. Podobnie, urządzenie potrzebuje sposobu w jaki przekaże systemowi, że nie jest zajęte i zaakceptowało dane. Wymaga to dodatkowych sygnałów w porcie równoległym znanych jako linie uzgodnienia. Typowy port równoległy PC dostarcza trzech sygnałów uzgodnienia: strobowanie dostępnej danej, sygnał potwierdzenia pobrania danej i linia zajętości urządzenia. Linie te łatwo sterują przepływem danych pomiędzy PC a jakimś urządzeniem zewnętrznym.
Dodatkowo do lini uzgodnień port równoległy PC dostarcza kilku innych pomocniczych linii I/O. W sumie jest 12 lini wyjściowych i pięć wejściowych w porcie równoległym PC Są trzy porty I/O w przestrzeni adresowej PC powiązane z każdym portem I/O. Pierwszy z nich (pod adresem bazowym portu) jest rejestrem danych. Jest to ośmio bitowy rejestr wyjściowy w porcie jednokierunkowym, jest rejestrem wejściowym / wyjściowym w porcie dwukierunkowym. Drugi rejestr , pod adresem bazowym plus jeden jest rejestrem stanu. Rejestr stanu jest portem wejściowym. Pięć z tych bitów odpowiada pięciu liniom wejściowym w porcie równoległym PC. Rejestr trzeci (pod adresem bazowym plus dwa) jest rejestrem sterującym. Cztery z tych bitów odpowiadają dodatkowym czterem bitom wyjściowym w PC, jeden z tych bitów steruje linią IRQ w porcie równoległym, a szósty bit steruje kierunkiem danych w porcie dwukierunkowym. *”Podstawowe informacje o porcie równoległym” *”Sprzęt portu równoległego” Chociaż wielu producentów używa portu równoległego do sterowania wieloma różnymi urządzeniami, drukarka równoległa jest urządzeniem najczęściej przyłączanym do portu równoległego. Są trzy sposoby do wysyłania danych do drukarki: poprzez wywołanie DOS do wydruku znaku, poprzez wywołanie ISR’a int 17h BIOS do wydruku znaku, lub poprzez komunikację bezpośrednią do portu równoległego. Powinniśmy unikać tej ostatniej techniki z powodu możliwych niekompatybilności programowych z innymi urządzeniami podłączonymi do portu równoległego. *”Sterowanie drukarką poprzez port równoległy” *”Drukowanie poprzez DOS” *”Drukowanie poprzez BIOS” *”Podprogram obsługi przerwania INT 17h” Popularnym zastosowaniem portu równoległego jest komunikacja pomiędzy dwoma komputerami; na przykład transfer danych między maszyną stacjonarną a laptopem.. Dla zademonstrowania jak używać portu równoległego do sterowani innym urządzeniem poza drukarką, rozdział ten przedstawia program do transferu danych pomiędzy komputerami poprzez jednokierunkowy port równoległy (działa również z portem dwukierunkowym) *”Miedzy komputerowa komunikacja portem równoległym”
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 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 DRUGI: PORTY SZEREGOWE Standard komunikacji szeregowej RS-232 jest prawdopodobnie najpopularniejszym schematem komunikacji szeregowej na świecie. Chociaż cierpi z powodu wielu wad, szybkość jest jedną z nich , jego zastosowanie jest rozpowszechnione i są dosłownie tysiące urządzeń jakie możemy podłączyć do PC przy użyciu interfejsu RS-232. PC wspiera do czterech urządzeń kompatybilnych z RS-232 używając COM1:, COM2:, COM3: i COM4: dla tych, którzy potrzebują więcej urządzeń szeregowych (np. sterowanie elektronicznej komputerowej tablicy ogłoszeń [BBS], możemy kupić urządzenia, które pozwalają dodać 16 lub więcej ,portów szeregowych do PC. Ponieważ większość PC ma tylko jeden lub dwa porty szeregowe, skoncentrujemy się w tym rozdziale jak użyć COM1: i COM2:. Chociaż, teoretycznie, oryginalnie zaprojektowany PC pozwolił projektantom systemu zaimplementować porty komunikacji szeregowej używając żądanego sprzętu, wiele z dzisiejszego oprogramowania wykonuje komunikację szeregową poprzez bezpośredni kontakt z chipem komunikacji szeregowej 8250 SCC. Wprowadza to takie same komplikacje z kompatybilnością jakie były kiedy komunikowaliśmy się bezpośrednio ze sprzętem portu równoległego. Jednakże, podczas gdy BIOS dostarcza doskonałego interfejsu dla portu równoległego, wspierając wszystko co życzylibyśmy sobie zrobić poprzez bezpośrednie dotarcie do sprzętu, wsparcie szeregowe nie jest dobre. Dlatego też jest powszechną praktyką omijanie funkcji BIOS 1h i sterowanie chipem 8250 SCC bezpośrednio tak aby program mógł uzyskać dostęp do każdego bitu każdego rejestru w 8250. Być może największym problemem z kodem BIOS jest to, że nie wspiera przerwań. Chociaż oprogramowanie sterujące portami równoległymi rzadko używa układów I/O sterowanych przerwaniami, często znajduje się oprogramowanie, które dostarcza podprogramów obsługi przerwań dla portów szeregowych. Ponieważ BIOS nie dostarcza takich podprogramów, oprogramowanie które zechce użyć układów I/O sterowanych przerwaniami będzie musiało komunikować się bezpośrednio z 8250 i omijać BIOS. Dlatego też pierwsza część tego rozdziału będzie omawiała chip 8250. Manipulowanie portem szeregowym nie jest trudne. Jednakże, 8250 SCC zawiera wiele rejestrów i dostarcza wielu cech. Dlatego też, zabiera wiele kodu sterowanie każdą cechą tego chipu. Na szczęście nie musimy pisać tego kodu sami. Biblioteka Standardowa UCR dostarcza doskonałego zbioru podprogramów, które pozwalają sterować 8250. Druga część tego rozdziału będzie przedstawiać kod z Biblioteki Standardowej jako przykład jak oprogramować każdy rejestr w 8250 SCC. 22.1 CHIP KOMUNIKACJI SZEREGOWEJ 8250 8250 i kompatybilne chipy (jak urządzenia 16450 i 16550) dostarczają dziewięciu rejestrów I/O. Pewne urządzenia kompatybilne w górę(np. 16450 i 16550) dostarczają również dziesięciu rejestrów. Rejestry te konsumują osiem portów I/O adresowanych w przestrzeni adresowej PC. Sprzęt i lokacje adresów dla tych urządzeń jest następująca: Port
Fizyczny adres bazowy (w hex)
COM1: COM2:
3F8 2F8
Zmienna BIOS zawierająca adres fizyczny 40:0 40:2
Tablica 81; Adresy portów COM
Podobnie jak w portach równoległych PC możemy wymieniać COM1: i COM2: z poziomu programu poprzez wymianę ich adresów bazowych w zmiennych BIOS 40:0i 40:2. Jednakże, oprogramowanie które zmierza bezpośrednio do sprzętu, zwłaszcza podprogramy obsługi przerwań dla portów szeregowych, muszą działać z adresami sprzętowymi, nie adresami logicznymi. Dlatego też zawsze będziemy oznaczać adres bazowy I/O 3F8h kiedy omawiamy COM1: w tym rozdziale. Podobnie zawsze oznaczamy adres bazowy 2F8h kiedy omawiamy COM2: w tym rozdziale. Adres bazowy jest pierwszym z ośmiu lokacji I/O konsumowanych przez 8250 SCC. Dokładne zadanie tych ośmiu lokacji I/O pojawiają się w poniższej tablicy: Adresy I/O (w hex) 3F8/2F8 3F9/2F9 3FA/2FA 3FB/2FB 3FC/2FC 3FD/3FD 3FE/2FE 3FF/2FF
Opis Rejestr danych odbiór/ przekazanie. Również najmniej znaczący bajt rejestru zatrzasku dzielenia szybkości transmisji Rejestr zezwalający na przerwania/ Również bardziej znaczący bajt rejestru zatrzasku dzielenia szybkości transmisji Rejestr identyfikacji przerwań (tylko odczyt) Rejestr sterowania łączem Rejestr sterowania modemem Rejestr stanu linii (tylko odczyt) Rejestr stanu modemu (tylko odczyt) Rejestr rzutowania odbioru (tylko odczyt, nie dostępny na oryginalnym PC)
Tablica 82: Rejestry 8250 SCC Kolejne sekcje opisują zadania każdego z tych rejestrów 22.1.1 REJESTR DANYCH (REJESTR PRZESŁANIA / ODBIORU) Rejestr danych jest w rzeczywistości dwoma oddzielnymi rejestrami: rejestr przesłania i rejestr odbioru. Wybieramy rejestr przesłania poprzez wpisanie adresu I/O 3F8h lub 2F8h, wybieramy rejestr odbioru poprzez odczyt z tych adresów. Zakładając, że rejestr przesłania jest pusty zapis do rejestru przesłania zaczynamy transmisję danych poprzez linie szeregową. Zakładając, że rejestr odbioru jest pełny, odczytanie rejestru odbioru zwraca tą daną. Aby określić czy nadajnik jest pusty lub odbiornik jest pełny, zobacz rejestr stanu lini. Zauważmy, że rejestr dzielenia szybkości transmisji dzieli ten adres I/O z rejestrami odbioru i przesłania. Proszę zobaczyć „Dzielenie szybkości transmisji” i „Rejestr sterowania łączem” po więcej informacji o podwójnym zastosowaniu tych lokacji I/O. 22.1.2 REJESTR ZEZWALAJĄCY NA PRZERWANIA (IER) Kiedy działamy w trybie przerwań, 8250 SCC dostarcza czterech źródeł przerwań: przerwanie odbioru znaku, przerwanie pustego nadajnika, przerwanie błędu komunikacji i przerwanie zmiany statusu. Możemy indywidualnie zezwalać lub blokować te źródła przerwań poprzez wpisanie jedynek lub zer do 8250 IER (Rejestr zezwalający na przerwania). Wpisując zero do odpowiedniego bitu blokujemy to szczególne przerwanie. Wpisując jedynkę zezwalamy na to przerwanie. Rejestr ten jest do odczytu / zapisu, więc możemy przepytywać aktualne ustawienie w dowolnym czasie (na przykład, jeśli chcemy zamaskować poszczególne przerwanie bez wpływania na inne). Rozkład tego rejestru jest następujący :
Lokacja rejestru zezwalającego na przerwania jest również wspólna z rejestrem dzielenia szybkości transmisji. 22.13 DZIELNIK SZYBKOŚCI TRANSMISJI Rejestr dzielnika szybkości transmisji jest 16 bitowym rejestrem który dzieli lokacje I/O 3F8h/2F8h i 3F9h/2F9h z rejestrem danych i zezwolenia na przerwania. Bit siedem rejestru sterowania łączem wybiera rejestr dzielenia lub rejestr danych / zezwolenia na przerwanie. Rejestr dzielenia szybkości transmisji pozwala nam wybrać szybkość transmisji danych (poprawnie nazywanej bity na sekundę lub bps lub baud) Poniższa tablica pokazuje wartości jakie powinniśmy zapisać do tych rejestrów aby sterować szybkością transmisji / odbioru: Bity na sekundę 110 300 600 1200 1800 2400 3600 4800 9600 19,2K 38,4K 56K
Wartość 3F9/2F9 4 1 0 0 0 0 0 0 0 0 0 0
Wartość 3F8/2F8 17h 80h C0h 60h 40h 30h 20h 18h 0Ch 6 3 1
Tablica 83 Wartości rejestru dzielenia szybkości transmisji Powinniśmy działać przy szybkościach większych niż 19,2K na szybkich PC z wysoko wydajnym SCC’esem (np. 16450 lub 16550). Na szczęście powinniśmy użyć kabli najwyższej jakości i trzymać nasze kable bardzo krótkie kiedy działamy przy wysokich szybkościach. 22.1.4 REJESTR IDENTYFIKACJI PRZERWANIA Rejestr identyfikacji przerwania jest rejestrem tylko do odczytu, który określa czy przerwanie oczekuje i które z czterech źródeł wymaga uwagi. Ten rejestr ma następujący rozkład:
Ponieważ IIR może tylko raportować o jednym przerwaniu w czasie, a jest prawdopodobieństwo, że są dwa lub więcej przerwania oczekujące, 8250 SCC wprowadza priorytety przerwań. Przerwanie źródła 00 (zmiana statusu) ma najniższy priorytet a przerwanie źródła 11 (błąd lub przerwa) mają priorytet najwyższy ,tj .numer źródła przerwania dostarcza priorytetu (z trzema będącymi najwyższego priorytetu) Poniższa tablica opisuje źródła przerwań i jak „wyczyścić” wartość przerwania w IIR. Jeśli dwa przerwania oczekują a my obsługujemy najwyższe przerwanie, 8250 SCC zamienia wartość IIR z identyfikatorem kolejnego źródła przerwania o najwyższym priorytecie Priorytet Najwyższy
Wartość ID 11b
Kolejny z najwyższych Kolejny z najniższych
10b
Najniższy
00
01b
Przerwanie Powód Wyzerowanie Błąd lub przerwanie Błąd przepełnienia, Odczytanie rejestru parzystości, stanu łącza synchronizacji ramki lub przerwanie break Dostępna dana Odczyt rejestru odbioru Pusty przekaźnik Odczytanie IIR (z ID przerwania 01b) lub zapis rejestru danych Stan modemu Odczyt rejestru stanu modemu
Tablica 84: Powody przerwania i funkcje zwalniające Jest jeden interesujący punkt do odnotowania o organizacji IIR: rozkład bitów dostarcza dogodnego sposobu do przekazania sterowania do właściwej sekcji podprogramu obsługi przerwania SCC. Rozważmy poniższy kod:
HandlerTbl
in mov mov jmp word
al., dx ;odczyt IIR bl, al. bh, 0 HnadlerTbl [bx] RLSHandler, RDHandler,TEHandler, MSHandler
Kiedy wystąpi przerwanie, bit zero IIR będzie zerem. Kolejne dwa bity zawierają numer źródła przerwania a bardziej znaczące pięć bitów jest zerami. Pozwala to nam użyć wartości IIR jako indeksu do tablicy wskaźników właściwego podprogramu obsługi, jak demonstruje powyższy kod. 22.1.5 REJESTR STEROWANIA ŁĄCZEM
Rejestr sterowani łączem pozwala nam określić parametry transmisji dla SCC. Obejmuje to ustawienie rozmiaru danej, liczby bitów zatrzymujących, parzystości, wymuszenia break i wybrania rejestru dzielnika szybkości transmisji . Rejestr sterowania łączem wygląda następująco:
8250 SCC może transmitować daną szeregową jako grupy pięciu, sześciu, siedmiu lub ośmiu bitów. Większość nowoczesnych systemów komunikacji używa siedmiu lub ośmiu bitów do transmisji ( my potrzebujemy siedmiu bitów do przekazania ASCII, ośmiu bitów do przekazania danej binarnej). Domyślnie, większość aplikacji przekazuje dane używając ośmio bitowej danej. Oczywiście, zawsze odczytujemy osiem bitów z rejestru odbiorczego; 8250 SCC ustawia wszystkie bardziej znaczące bity na zero jeśli odbieramy mniej niż osiem bitów. Zauważmy, że jeśli przekazujemy tylko znaki ASCII,, komunikacja szeregowa będzie działała około 10% szybciej z siedmio bitową transmisją zamiast transmisji ośmio bitowej. Jest to ważna rzecz do zapamiętania, jeśli sterujemy oboma końcami kabla szeregowego . Z drugiej strony, zazwyczaj będziemy łączyć urządzenia które maja stałą długość słowa, więc będziemy musieli oprogramować SCC specjalnie do dopasowania tego urządzenia. Szeregowa transmisja danych składa się z bitu startowego, pięciu do ośmiu bitów danych i jednego lub dwóch bitów stopu. Bit startu jest specjalnym sygnałem, który informuje SCC (lub inne urządzenie), że dana przebywa na lini szeregowej. Bity stopu są, w gruncie rzeczy, pod nieobecność bitu startowego, dostarczając małą ilość czasu pomiędzy przybyciem kolejnych znaków na linię szeregową .Przez wybranie dwóch bitów stopu wprowadzamy dodatkowy czas pomiędzy transmisję kolejnego znaku. Niektóre inne starsze urządzenia mogą wymagać tego dodatkowego czasu lub się pogubią. Jednakże, prawie wszystkie nowoczesne urządzenia szeregowe zadowalają się pojedynczym bitem stopu. Dlatego też, powinniśmy zawsze oprogramować chip tylko jednym bitem stopu. Dodatkowy druki bit stopu zwiększa czas transmisji o około 10%. Bity parzystości pozwalają nam włączyć lub wyłączyć parzystość lub wybrać tryb parzystości. Parzystość jest schematem detekcji błędu. Kiedy włączmy parzystość, SCC dodaje dodatkowy bit (bit parzystości) do transmisji. Jeśli wybieramy kontrolę nieparzystości, bit parzystości zawiera zero lub jeden aby suma mniej znaczącego bitu danej i bitu parzystości dało jeden. Jeśli wybieramy kontrolę parzystości, SCC tworzy bit parzystości taki, że mniej znaczący bit sumy parzystości i bitu danej to zero Wartość „parzystości zablokowanej” (10b i 11b) zawsze tworzy bit parzystości na zero lub jeden. Głównym celem bitu parzystości jest wykrycie możliwego błędu transmisji. Jeśli mamy długi, hałaśliwy lub zły kanał komunikacji szeregowej, jest możliwe zgubienie informacji podczas transmisji. Kiedy się to zdarzy, jest mało prawdopodobne, że suma bitów będzie pasowała do wartości parzystości. Węzeł odbiorczy może wykryć ten „błąd parzystości” i zaraportować błąd transmisji. Możemy również użyć wartości zablokowanej parzystości (10b i 11b)do usunięcia tych ośmiu bitów i zawsze zamienić na zera lub jedynki podczas transmisji. Na przykład, kiedy przesyłamy osiem bitów znaków PC/ASCII do różnych systemów komputerowych, jest możliwe ,że zbiór rozszerzonych znaków PC (znaki których kod to 128 i większy) nie odwzorowywuje takiego samego znaku na maszynie przeznaczenia. Istotnie, wysyłając takie znaki możemy stworzyć problem na tej maszynie. Poprzez ustawienie rozmiaru słowa na
siedem bitów i włączenia parzystości i zablokowania dla zera możemy automatycznie usunąć wszystkie bardziej znaczące bity podczas transmisji, zamieniając je na zera. Oczywiście jeśli pojawiają się rozszerzone znaki, SCC będzie odwzorowywał je do możliwie nie związanych znaków ASCII, ale jest to użyteczna sztuczka, czasami. Bit break transmisji, przerywa sygnał systemu zdalnego tak długo jak jest oprogramowany bit na tej pozycji. Nie powinniśmy pozostawić włączonego break podczas prób transmisji danej. Sygnał break pochodzi z czasów dalekopisów .Break jest podobne do ctrl-C lub ctrl-break z klawiatury PC. Powinien przerwać program uruchomiony na systemie zdalnym. Zauważmy, że SCC może wykryć nadchodzący sygnał break i wygenerować właściwe przerwanie, ale ten sygnał break pochodzi z systemu zdalnego, nie jest (bezpośrednio) połączony do wyjściowego sygnału break LCR. Bit siedem LCR jest bitem zatrzasku rejestru dzielnika szybkości transmisji. Kiedy bit ten zawiera jeden, lokacje 3F8h/2F8h i 3F9h/2F9h stają się rejestrem dzielnika szybkości transmisji. Kiedy zawiera zero, te lokacje I/O odpowiadają rejestrowi danych i rejestrowi zezwolenia na przerwania. Powinniśmy zawsze oprogramować ten bit zerem poza tym kiedy inicjalizujemy szybkość SCC LCR jest rejestrem odczyt / zapis. Odczytując ten rejestr, LCR zwraca ostatnią wartość zapisaną do niego. 22.1.6 REJESTR STEROWANIA MODEMEM Rejestr sterowania modemem 8250 zawiera pięć bitów, które pozwalają nam bezpośrednio sterować różnymi końcówkami wyjściowymi w 8250, jak również włączyć tryb pętli sprzężenia zwrotnego. Poniżej mamy ten rejestr:
8250 wyznacza drogę bitów DTR i RTS bezpośrednio do liń DTR i RTS w chipie 8250. Kiedy bity te są jedynkami, odpowiednie wyjścia są aktywne. Linie te są dwoma oddzielnymi liniami uzgodnień dla komunikacji RS-232. Sygnał DTR jest porównywalny do sygnału busy. Kiedy węzeł lini DTR jest nie aktywny, inny węzeł nie ma prawa transmitować danej do niego. Linia DTR jest linią ręcznego uzgodnienia. Pojawia się jako linia DSR (gotowość do odbioru danych) po drugiej stronie kabla. Inne urządzenia musza wyraźnie sprawdzić swoje linie DSR aby zobaczyć czy mogą przesyłać dane. Schemat DTR / DSR jest głównie zaplanowany do wymiany potwierdzeń pomiędzy komputerami a modemami. Linia RTS dostarcza drugiej postaci uzgodnienia. Odpowiadający jej sygnał to CTS (gotowość nadawcza). Protokół uzgodnień RTS/CTS jest zaplanowany głównie do bezpośredniego połączenia urządzeń takich jak komputery i drukarki. Możemy zapytać „dlaczego są dwa oddzielne, ale ortogonalne protokoły uzgodnień?” Powód jest taki, że RS-232C rozwija się od ponad stu lat (od dnia pierwszego telegrafu) i jest wynikiem łączenia różnych schematów przez te lata. Out1 jest wyjściem ogólnego przeznaczenia w SCC, które mam bardzo małe zastosowanie w IBM PC. Niektóre złącza płyt łączą ten sygnał , inne pozostawiają go niepołączonymi. Generalnie bit ten nie ma funkcji w PC. Bit zezwolenia na przerwania jest specyficzną pozycją w PC Normalnie jest to wyjście ogólnego przeznaczenia (OUT2) w 8250 SCC. Jednakże, IBM zaprojektował to wyjście jako przyłączone do zewnętrznej bramki włączającej lub wyłączającej wszystkie przerwania z SCC. Bit ten musi być oprogramowany jedynka, włączającą przerwania .Podobnie, musimy zapewnić, że ten bit zawiera zero jeśli nie używamy przerwań. Bit pętli sprzężenia zwrotnego łączy rejestr przesyłający z rejestrem odbiorczym. Wszystkie dane wysyłane z przekaźnika bezpośrednio przychodzą do rejestru odbiorczego .Jest to użyteczne przy diagnostyce,
testowaniu oprogramowania i wykrywaniu chipu szeregowego. Zauważmy, niestety, że obwód sprzężenia zwrotnego nie będzie generował żadnego przerwania. Możemy użyć tej techniki tyko z odpytywaniem I/O. Pozostałe bity w MCR są zarezerwowane, powinny zawsze zawierać zero. Przyszłe wersje SCC (lub chipów kompatybilnych) mogą używać tych bitów do innych celów, ze stanem zero domyślnie (symulacja 8250). MCR jest rejestrem odczyt/zapis. Odczytując ten rejestr, MCR zwraca ostatnią ,zapisaną do niego wartość. 22.1.7 REJESTR STANU ŁĄCZA (LSR) Rejestr stanu łącza (LSR) jest rejestrem tylko do odczytu, który zwraca bieżący stan komunikacji. Rozkład bitów rejestrów:
Bit dostępnej danej jest ustawiony jeśli jest dostępna dana w rejestrze odbiorczym. Również generuje przerwanie. Odczyt danej w rejestrze odbioru ,czyści ten bit. Rejestr odbiorczy 8250 może tylko przechowywać jedne bajt w czasie. Jeśli nadszedł bajt a program go nie odczytał a potem nadszedł drugi bajt, 8250 zatrze pierwszy bajt drugim .8250 SCC ustawi bit błędu przepełnienia kiedy to nastąpi/ Odczyt LSR czyści ten bit (po odczytaniu LSR). Błąd ten będzie generował błąd przerwania najwyższego priorytetu 8250 ustawia bit parzystości jeśli wykryje błąd parzystości kiedy odbiera bajt. Ten błąd wystąpi tylko jeśli mamy włączoną operację parzystości w LCR. 8250 resetuje ten bit po odczytaniu LSR. Kiedy wystąpi ten błąd, 8250 wygeneruje błąd przerwania. Bit trzy jest bitem błędu ramkowania. Błąd ramkowania wystąpi jeśli 8250 odbiera znak bez prawidłowego bitu stopu. 8250 czyści ten bit po odczytaniu LSR. Błąd ten będzie generował błąd przerwania o najwyższym priorytecie. 8250 ustawia bit przerwania break kiedy odbiera sygnał break z urządzenia przesyłającego. Również generuje błąd przerwania. Odczyt LSR czyści ten bit. 8250 ustawia bit piąty, przekaźnik przechowuje bit pustego rejestru, kiedy można zapisać inny znak do rejestru danych. Zauważmy, że 8250 w rzeczywistości ma dwa rejestry związane z przekaźnikiem. Rejestr przesunięcia przekaźnika zawiera dane aktualnie przesuwane na linię szeregową. Rejestr przechowujący przekaźnika przechowuje wartości, które 8250 zapisuje do rejestru przesunięcia, kiedy ten kończy przesunięcie znaku. Bit pięć wskazuje ,że rejestr przechowujący jest pusty a 8250 może zaakceptować inny bajt. Zauważmy, że 8250 może jeszcze przesuwać znak równolegle z tą operacją. 8250 może generować przerwanie kiedy rejestr przechowujący przekaźnika jest pusty. Odczyt LSR lub zapis do rejestru danych czyści ten bit. 8250 ustawia bit sześć kiedy oba, rejestr przechowujący i rejestr przesunięcia są puste. Bit ten jest czyszczony kiedy inny rejestr zawiera daną 22.1.8 REJESTR STANU MODEMU (MSR) Rejestr stanu modemu (MSR) raportuje stan sygnału uzgodnienia i innych sygnałów modemu. Cztery bity dostarczają wartości natychmiastowych tych sygnałów, 8250 ustawia inne cztery bity jeśli któryś z tych sygnałów zmienił się od ostatniego czasu odpytania MSR przez CPU. Rozkład MSR:
Bit gotowości do wysłania (bit # 4) jest sygnałem uzgodnienia. Jest zazwyczaj podłączony do sygnału RTS (zgłoszenie gotowości do nadawania) w urządzeniu odbiorczym. Kiedy to zdalne urządzenie zaznaczy swoją linię RTS, transmisja danych może mieć miejsce. Bit gotowości do odbioru (bit # 5) jest jedynka jeśli urządzenie zdalne nie jest zajęte. To wejście jest generalnie podłączone do lini gotowości do wysłania danych (DTR) w urządzeniu zdalnym Chip 82250 ustawia bit wskaźnika pierścieniowego (bit #6) kiedy modem zaznacza linię wskaźnika pierścieniowego. Będziemy rzadko używali tego sygnału, chyba ,że piszemy nowoczesne oprogramowanie sterujące, które automatycznie odpowiada na telefon. Bit wykrywania sygnału nośnego (DCD, bit # 7) jest innym specyficznym sygnałem modemu. Bit ten zawiera jeden podczas gdy modem wykrywa sygnał nośny na lini telefonicznej. Bity od zera do trzy MSR są bitami „delta”. Bity te zawierają jeden, jeśli ich odpowiadający sygnał stanu modemu zmienia się. Takie wystąpienie również generuje stan przerwania modemu. Odczyt MSR będzie czyścił te bity. 22.1.9 POMOCNICZE REJESTR WEJŚCIOWY Pomocniczy rejestr wejściowy jest dostępny tylko na późniejszych modelach 8250 kompatybilnych urządzeń. Jest to rejestr tylko odczyt, który zwraca taką samą wartość jaka odczytuje rejestr danych. Różnica pomiędzy odczytem tego rejestru a odczytem rejestru danych jest taka, że odczyt pomocniczego rejestru wejściowego nie wpływa na bit dostępnej danej w LSR. Pozwala to nam przetestować nadchodzące wartości danej bez usuwania jej z rejestru wejściowego. Jest to użyteczne, na przykład kiedy tworzymy łańcuch podprogramów obsługi przerwań chipu szeregowego i chcemy obsłużyć pewne „gorące” wartości w jednym ISR’ze i przekazać wszystkie inne znaki do różnych szeregowych ISR’ów. 22.2 PROGRAMY WSPIERAJĄCE KOMUNIKACJĘ SZEREGOWĄ BIBLIOTEKI STANDARDOWEJ UCR Chociaż oprogramowanie 8250 SCC nie wydaje się być rzeczywiście dużym problemem, niezmiennie jest trudnym ,niewdzięcznym (i nużącym) pisanie całego koniecznego oprogramowania uzyskując działający system komunikacji szeregowej. Jest to szczególnie prawdziwe kiedy używamy układów szeregowych I/O sterowanych przerwaniami. Na szczęście nie musimy pisać tego oprogramowania od podstaw, Biblioteka Standardowa UCR dostarcza 21 podprogramów, które trywializują zastosowanie portów szeregowych w PC. Jedyną wadą tych podprogramów jest to ,że zostały napisane specjalnie dla COM1:, chociaż nie jest zbyt dużo pracy z modyfikacją ich do współpracy z COM2: Poniższa tablica pokazuje dostępne podprogramy: Nazwa Wejścia Wyjścia Opis Ustawia szybkość komunikacji ComBaud AX: bps (baud rate) = portu szeregowego. ComBaud 110,150, 300, 600, 1200, wspiera tylko określone 2400, 4800, 9600 lub szybkości. Jeśli ax zawiera jakąś 19200 inną wartość na wejściu, ComStop
AX: 1 lub 2
ComBaud ignoruje ją Ustawia liczbę bitów stopu. Rejestr ax zawiera liczbę bitów
ComSize
AX: rozmiar słowa (5,6,7 lub 8)
ComParity
AX: selektor parzystości. Jeśli bit zero jest zerem, parzystość wyłączona, jeśli jeden, bity jeden i dwa to: 00-kontorla nieparzystości 01-kontroal parzystości 10-zablokowana parzystość przy 0 11-zablokowana parzystość przy 1
ComRead
ComWrite
stop (1 lub 2) Ustawia liczbę bitów danej. Rejestr ax zawiera liczbę bitów do przesłania dla każdego bajtu na lini szeregowej. Ustawia parzystość dla komunikacji szeregowej
AL- Odczyt znaku z portu
AL – znak do zapisu
ComTstIn
AL.=0 jeśli brak znaku AL.=1 jeśli znak dostępny
ComTstOut
AL.=0 jeśli przekaźnik zajęty, AL.=1 jeśli nie zajęty
ComGetLSR
AL= Bieżąca wartość LSR
ComGetMSR
AL.= bieżąca wartość MSR AL= bieżąca wartość MCR
ComGetMCR ComSetMCR
AL.= nowa wartość MCR
ComGetLCR ComSetLCR
AL= bieżąca wartość LCR AL.= nowa wartość LCR
ComGetIIR
AL.= bieżąca wartość IIR
ComGetIER
AL.= bieżąca wartość IER
ComSetIER ComInitIntr ComDisIntr ComIn ComOut
AL. = nowa wartość IER
Czeka dopóki znak jest dostępny z rejestru danych i zwraca ten znak. Używana dla odpytywania I/O na porcie szeregowym. Ni używane jeśli aktywujemy przerwania szeregowe. Oczekuje dopóki rejestr przechowujący przekaźnika jest pusty, wtedy zapisuje znak w al. do rejestru wyjściowego. Używana przy odpytywaniu I/O w porcie szeregowym. Nie używamy przy aktywnych przerwaniach. Testuje by sprawdzić czy znak jest dostępny w porcie szeregowym. Używamy tylko do odpytywania I/O , nie używamy z aktywnymi przerwaniami Testuje by sprawdzić czy można zapisać znak do rejestru wyjściowego. Używany tylko z odpytywaniem I/O Zwraca bieżącą wartość LSR w rejestrze al. Zwraca bieżącą wartość MSR w rejestrze al. Zwraca bieżącą wartość MCR w rejestrze al. Przechowuje wartość al. w rejestrze MCR Zwraca bieżącą wartość LCR w rejestrze al. Przechowuje wartość al. w rejestrze LCR Zwraca bieżącą wartość IIR w rejestrze al. Zwraca bieżącą wartość IER w rejestrze al. Przechowuje wartość al. w rejestrze IER Inicjalizuje system wspierając układy szeregowe I/O sterowane przerwaniami. Resetuje system z powrotem do szeregowego odpytywania I/O Czyta znak portu szeregowego kiedy działa z układami I/O sterowanymi przerwaniami Zapisuje znak do portu szeregowego używając układów I/O sterowanych przerwaniami
Tablica 85: Wsparcie portu szeregowego przez Bibliotekę Standardową Cecha podprogramów Biblioteki Standardowej , sterowanie układami I/O poprzez przerwania zasługuje na późniejsze wyjaśnienia. Kiedy wywołujemy podprogram ComInitIntr, aktualizujemy wektor przerwań COM1: (int 0Ch), włączamy IRQ 4 w PIC 8259A i włączamy odczyt i zapis przerwań w 8250 SCC. Jedyna rzecz jakiej to wywołanie nie robi to to, że powinniśmy zaktualizować wektory wyjątków break i błędu krytycznego (int 23h i int 24h) obsługujące przerwanie każdego programu jaki nadchodzi. Kiedy nasz program kończy się, albo normalnie , albo przez jeden z powyższych wyjątków , musi wywołać ComDisIntr do zablokowania przerwań .W przeciwnym wypadku, następnym razem znak przychodzący do portu szeregowego maszyny może spowodować krach ponieważ będzie próbował skoczyć do podprogramu obsługi przerwania, którego może tam nie być. Podprogramy ComIn i ComOut obsługują szeregowe układy I/O sterowane przerwaniami. Biblioteka Standardowa dostarcza stosownych buforów wejściowego i wyjściowego (podobnych do bufora roboczego klawiatury), abyśmy nie musieli martwić się o zgubione znaki chyba ,że program jest rzeczywiście, rzeczywiście wolny lub rzadko odczytuje dane z portu szeregowego. Pomiędzy funkcjami ComInitIntr a ComDisIntr nie powinniśmy wywoływać żadnego innego podprogramu szeregowego z wyjątkiem ComIn i ComOut. Inne podprogramy są zaplanowane do odpytywania I/O i inicjalizacji. Oczywiście powinniśmy wykonać konieczną inicjalizację przed włączeniem przerwań i nie wolno odpytywać I/O podczas gdy działają przerwania. Zauważmy, że nie ma odpowiedników dla ComTstIn i ComTstOut w czasie działania w trybie przerwania. Podprogramy te są łatwe do napisania, instrukcja pojawi się w następnej sekcji. 22.3 OPROGRAMOWANIE 8250 (PRZYKŁADY Z BIBLIOTEKI STANDARDOWEJ) Podprogramy komunikacji szeregowej Biblioteki Standardowej UCR dostarczają doskonałego przykładu jak oprogramować bezpośrednio 8250 SCC, ponieważ używają prawie wszystkich cech tego chipu z PC. Dlatego też, sekcja ta wylistuje każdy z tych podprogramów i opisze dokładnie co dany podprogram robi. Poprzez studiowanie tego kodu możemy nauczyć się o wielu szczegółach związanych z SCC i odkryć jak rozszerzyć lub zmodyfikować podprogramy Biblioteki Standardowej. ;Użyteczne równania: BIOSvars Com1Adrs Com2Adrs
= = =
40h 0 2
;adres segmentu BIOS’a ;offset adresu COM1: zmiennych BIOS ;offset adresu COM2: zmiennych BIOS
BufSize
=
256
;# bajtów w buforze
; Przyrównania portu szeregowego. Jeśli chcemy wesprzeć COM2: zamiast COM1:, po prostu zmieniamy ; przyrównanie na 2F8h, 2F9h.... ComPort ComIER ComIIR ComLCR ComMCR ComLSR ComMSR
= = = = = = =
3F8h 3F9h 3FAh 3FBh 3FCh 3FDh 3Feh
; Zmienne itd., Kod ten zakłada, że DS=CS. To znaczy, wszystkie zmienne są w segmencie kodu. ; ;Wskaźnik do wektora przerwań dla int 0Ch w tablicy wektorów przerwań. Notka: zmieniamy te wartości na ; 0Bh*4 i 0Bh*4+2 jeśli chcemy wesprzeć port COM2:
int0Cofs int0Dseg
equ equ
es:[0Ch*4] es:[0Ch*4+2]
OldInt0C
dword ?
;Bufor wejściowy dla nadchodzących znaków (tylko działanie przerwań). Zobacz rozdział o strukturach danych i ; opis kolejki cyklicznej po szczegóły jak działa bufor. Działa w sposób inny niż bufor roboczy klawiatury. InHead InTail InpBuf InpBufEnd
word word byte equ
InpBuf InpBuf BufSize dup (?) this byte
;Bufor wyjściowy dla znaków oczekujących na przesłanie OutHead OutTail OutBuf OutBufEnd
word word byte equ
OutBuf OutBuf BufSize dup (?) this byte
; Zmienna 18259a przechowuje kopię PIC IER’a więc możemy przywrócić ją po usunięciu naszego ; podprogramu obsługi przerwań z pamięci. 18259a
byte
0
;rejestr zezwalający na przerwania 8259a
;Zmienna TestBuffer mówi nam czy mamy bufor pełen znaków lub czy możemy przechować kolejny znak ; bezpośrednio w rejestrze wyjściowym 8250 TestBuffer
db
0
Pierwszy zbiór podprogramów dostarczony przez Bibliotekę Standardową pozwala nam inicjalizować 8250 SCC. Podprogramy te dostarczają interfejsu „przyjaznego programiście” rejestrów dzielnika szybkości transmisji i starowania łączem .Pozwalają nam ustawić szybkość transmisji, rozmiar danej liczbę bitów stopu i opcję parzystości w SCC Podprogram ComBaud ustawia szybkość transferu 8250 (w bitach na sekundę). Dostarcza przyjemnego „interfejsu programisty” 8250 SCC. Zamiast obliczania dzielnika szybkości transmisji samemu, możemy po prostu załadować ax wartością bps jaką chcemy i po prostu wywołujemy ten podprogram. Oczywiście jeden problem jest taki, że musimy wybrać wartość bps, jaką wspiera ten podprogram lub zignoruje żądanie zmiany szybkości transmisji. Na szczęście podprogram ten wspiera wszystkie popularne szybkości bps; jeśli potrzebujemy jakiejś innej, łatwo jest zmodyfikować ten kod na pozwalający na inne szybkości. Kod ten skalda się z dwóch części. Pierwsza część porównuje wartość w ax ze zbiorem poprawnych wartości bps. Jeśli dopasuje, ładuje ax odpowiednią 16 bitową stałą dzielnika. Druga część tego kodu przełącza rejestry dzielnika szybkości transmisji i przechowuje wartość ax w tych rejestrach. W końcu przełącza pierwsze dwa rejestry I/O 8250 z powrotem na rejestr danych i zezwolenia na przerwanie. Notka: Ten podprogram wywołuje kilka podprogramów, zwłaszcza ComSetLCR i ComGetLCR, które zdefiniujemy trochę później. Podprogramy te wykonują oczywiste funkcje, odczyt i zapis rejestru LCR ComBaud
proc push push cmp ja je cmp ja je cmp ja je cmp
ax dx ax, 9600 Set19200 Set9600 ax, 2400 Set4800 Set2400 ax, 600 Set1200 Set600 ax, 150
Set150: Set300: Set600: Set1200: Set2400 Set4800: Set9600: Set19200 SetPort:
ComBaud
ja je mov jmp
Set300 Set150 ax, 1047 SetPort
mov jmp mov jmp mov jmp mov jmp mov jmp mov jmp mov jmp mov mov call push ot call mov mov out inc mov out pop call pop pop ret endp
ax, 768 SetPort ax, 384 SetPort ax, 192 SetPort ax, 96 SetPort ax, 48 SetPort ax, 24 SetPort ax, 12 SetPort ax, 6 dx, ax GetLCRCom ax al., 80h SetLCRCom ax, dx dx, ComPort dx, al. dx al., ah dx, al. ax SetLCRCom1 dx ax
;domyślnie 110 bps ;wartość dzielnika dla 150 bps ;wartość dzielnika dla 300 bps ;wartość dzielnika dla 600 bps ;wartość dzielnika dla 1200 bps :wartość dzielnika dla 2400 bps ;wartość dzielnika dla 4800 bps ;wartość dzielnika dla 9600 bps ;wartość dzielnika dla 19,2 kbps ;zachowanie wartości szybkości ;pobranie wartości LCR ;zachowanie starej wartości bitu dzielnika ;ustawienie bitu wyboru dzielnika ;zapis wartości LCR ;Pobranie wartości dzielnika szybkości transmisji ;wskazuje mniej znaczący bajt rejestru dzielnika ;wyprowadzenie mniej znaczącego bajtu dzielnika ;wskazuje bardziej znaczący bajt ;włożenie bardziej znaczącego bajtu do AL. ;wyprowadzenie bardziej znaczącego bajtu ;przywrócenie starej wartości LCR ;przywrócenie wartości bitu dzielnika
Podprogram ComStop oprogramowuje LCR dostarczając określoną liczbę bitów stopu. Na wyjściu, ax powinien zawierać albo jeden albo dwa (liczbą żadnych bitów stopu) Kod ten konwertuje to do zera lub jeden i zapisuje wynikowy mniej znaczący bit do pola bitu stopu LCR. Zauważmy ,że kod ten ignoruje inne bity w rejestrze ax. Odczytuje LCR, maskuje pole bitu stopu a potem odwraca wartość kodu wywołującego określonego w tym polu. Odnotujmy zastosowanie instrukcji shl ax, 2; wymaga to procesora 80286 lub późniejszego. ComStop
proc push push dec and shl mov call and or call pop pop ret
ax dx ax al., 1 ax, 2 ah, al. ComGetLCR al., 11111011b al., ah ComSetLCR dx ax
;konwersja 1lub 2 na 0 lub 1 ; usuwamy inne bity ;pozycja w bicie #2 ;zachowujemy naszą wartość wyjściową ;odczyt wartości LCR ;maskowanie bitu stopu ;podział nowych # bitów stopu ;zapis wyniku ponownie do LCR
ComStop
endp
ComSize ustawia rozmiar słowa dla transmisji danych. Zazwyczaj kod ten dostarcza „przyjacielskiego programiście” interfejsu do 8250 SCC. Na wejściu określamy liczbę bitów (5,6,7 lub 8) w rejestrze ax, nie musimy martwić się o właściwy wzorzec bitów dla rejestru LCR 8250. Podprogram ten oblicza właściwy wzorzec bitów dla nas. Jeśli wartość w rejestrze ax nie jest właściwa, kod ten domyślnie ustawi rozmiar słowa na osiem bitów. ComSize
Okay:
ComSize
proc push push sub cmp jbe mov mov call and or call pop pop ret endp
ax dx al., 5 al., 3 Okay al., 3 ah, al. ComGetLCR al., 11111100b al., ah ComSetLCR dx ax
;odwzorowanie 5..8 → 00b, 01b, 10b, 11b ;domyślnie osiem bitów ;zachowanie nowego rozmiaru bitów ;odczyt bieżącej wartości LCR ;maskowanie starego rozmiaru słowa ;dzielenie nowego rozmiaru słowa ;zapis nowej wartości LCR
Podprogram ComParity inicjalizuje opcje parzystości w 8250. Niestety jest mniejsza możliwość „przyjaznego programiście” interfejsu dl tego podprogramu, więc ten kod wymaga aby przekazać mu jedna z następujących wartości w rejestrze ax: Wartość w AX 0 1 3 5 7
Opis Parzystość wyłączona Włączona kontrola nieparzystości Włączona kontrola parzystości Włączony bit zablokowanej parzystości wartość jeden Włączony bit zablokowanej parzystości wartość zero
Tablica 86: Parametry wejœciowe ComParity ComParity
ComParity
proc push push shl and mov call and or call pop pop ret endp
ax dx al ,3 al., 00111000b ah, al. ComGetLCR al., 11000111b al., ah ComSetLCR dx ax
;przesunięcie na końcową pozycję w LCR ;maskowanie innej danej ;zachowanie na później ;pobranie bieżącej wartości LCR ;maska istniejących bitów parzystości ;podział nowych bitów ;zapis wyniku do LCR
Kolejny zbiór podprogramów komunikacji szeregowej dostarcza wsparcia dla odpytywania I/O. Podprogramy te pozwalają nam odczytać znaki z portu szeregowego, zapisać znaki do portu szeregowego i sprawdzić czy jest dostępna dana w porcie wejściowym lub zobaczyć czy można zapisać dane do portu
wyjściowego. Pod żadnym pozorem nie powinniśmy używać tych podprogramów kiedy mamy aktywny system przerwań szeregowych. Robiąc to możemy zamieszać w systemie i stworzyć niepoprawne dane lub zagubić je. Podprogram ComRead jest porównywalny z getc – oczekuje dopóki dana jest dostępna w porcie szeregowym, odczytuje tą dana i zwraca ją w rejestrze al. Ten program zaczyna się poprzez upewnienie się, ze możemy uzyskać dostęp do rejestru odbioru danych (poprzez wyczyszczenie bitu zatrzasku dzielnika szybkości transmisji w LCR) ComRead
WaitForChar:
ComRead
proc push call push and call call test jz mov in mov pop call mov pop ret endp
dx GetLCRCom ax al., 7fh SetLCRCom GetLSRCom al., 1 WaitForChar dx, ComPort al., dx dl, al. ax SetLCRCom al., dl dx
;zachowanie bitu zatrzasku dzielnika ;wybór normalnego portu ;zapis LCR wyłączając rejestr dzielnika ;pobranie bitu dostępności danej z LSR ;dana dostępna? ;pętla dopóki dana dostępna ;odczyt danej z portu wejściowego ;zachowanie znaku ;przywrócenie bitu dostępu dzielnika ;zapis z powrotem do LCR ;przywrócenie wyprowadzonego znaku
Podprogram ComWrite wyprowadza znak z al. do portu szeregowego. Najpierw czeka dopóki rejestr przechowywani przekaźnika jest pusty, potem zapisuje daną wyjściową do rejestru wyjściowego ComWrite
WaitForXmtr:
ComWrite
proc push push mov call push and call call test jz mov mov out pop call pop pop ret endp
dx ax dl, al. GetLCRCom ax al., 7fh SetLCRCom GetLSRCom al., 00100000b WaitForXmtr al., dl dx, ComPort dx, al ax SetLCRCom ax dx
;zachowanie znaku do wyprowadzenia ;przełączenie na rejestr wyjściowy ;zachowanie bitu zatrzasku dzielnika ;wybranie zwykłego portu wejścia / wyjścia ;zamiast rejestru dzielnika ;odczyt LSR z bitu pustego transmitera ;bufor przekaźnika pusty? ;pętla dopóki pusty ;pobranie znaku do wyprowadzenia ;przechowanie w porcie wyjściowym ;przywrócenie bitu dzielnika
Podprogramy ComTstIn I ComTstOut pozwalają nam sprawdzić czy znak jest dostępny w porcie wejściowym (ComTstIn) lub czy można wysłać znak do portu wyjściowego (ComTstOut) . ComTstIn zwraca zero lub jeden w al., odpowiednio, jeśli dana nie jest dostępna lub jest dostępna. ComTstOut zwraca zero lub jeden w al., odpowiednio, jeśli rejestr przekaźnika jest pełny lub pusty ComTstIn
proc call and ret
GetComLSR ax, 1
;przetrzymujemy tylko bit dostępności
ComTstIn ComTstOut
tocl: ComTstOut
endp proc push call test mov jz inc ret endp
dx ComGetLSR al., 00100000b al., 0 tocl ax
;pobranie stanu łącza ;maskujemy bit pustego przekaźnika ;zakładamy, że nie pusty ;skok jeśli nie pusty ;ustawiamy jeden jeśli pusty
Kolejny zbiór podprogramów Biblioteki Standardowej dostarcza ładowania i przechowywania różnych rejestrów w 8250 SCC. Chociaż są to trywialne podprogramy, pozwalają programiście uzyskać dostęp do tego rejestru poprzez nazwę bez poznania jego adresu. Co więcej podprogramy te zachowują wszystkie wartości w rejestrze dx, zachowując jakiś kod w programie wywoływanym jeśli rejestr dx jest już używany. Poniższe podprogramy pozwalają nam odczytać („Get”) wartość w rejestrach LSR, MSR, MCR, IIR i IER, zwracając wartość w rejestrze al. Pozwalają nam zapisać („Set”) wartość z al do każdego z rejestrów LCR, MCR i IER. Ponieważ te podprogramy są tak proste nie ma potrzeby ich omawiania ich pojedynczo. Zauważmy, że powinniśmy unikać wywoływania tych podprogramów na zewnątrz SCC ISR podczas trybu przerwań, ponieważ robiąc to możemy wpłynąć na system przerwań 8250 SCC. ComGetLSR
ComGetLSR ComGetMSR
ComGetMSR ComSetMCR
ComSetMCR ComGetMCR
ComGetMCR ComGetLCR
proc push mov in pop ret endp proc push mov in pop ret endp proc push mov out pop ret endp proc push mov in pop ret endp proc push mov in pop
;zwraca wartość LSR w rejestrze AL. dx dx, ComLSR al., dx dx
;wybór rejestru LSR ;odczyt i zwrot wartości LSR
;zwraca wartość MSR w rejestrze AL. dx dx, ComMSR al, dx dx
;wybór rejestru MSR ;odczyt i zwrot wartości MSR
;zachowanie wartości AL. w rejestrze MCR dx dx, ComMCR dx, al dx
;wskazuje rejestr MCR ;wyprowadzenie wartości z AL. do MCR
;przechowanie wartości AL. w rejestrze MCR dx dx, ComMCR al, dx dx
;wybór rejestru MCR ;odczyt wartości z rejestru MCR do AL
; zwraca wartość LCR w rejestrze AL. dx dx, ComLCR al., dx dx
;wskazuje rejestr LCR ;odczyt i zwrot wartości LCR
ComGetLCR ComSetLCR
ComSetLCR ComGetIIR
ComGetIIR ComGetIER
ComGetIER ComSetIER
ComSetIER
ret endp proc push mov out pop ret endp proc push mov in pop ret endp proc push call push and call mov in mov pop call mov pop ret endp proc push push mov call push and call mov mov out pop call pop pop ret endp
;zapis nowej wartości do LCR dx dx, ComLCR dx, al. dx
;wskazuje rejestr LCR ;zapis wartości w AL do LCR
;zwraca wartość w IIR dx dx, ComIIR al, dx dx
;wybór rejestru IIR ;odczyt wartości IIR do AL. i powrót
;zwraca wartość IER w AL. dx ComGetLCR ax al., 7fh ComSetLCR dx, ComIER al, dx dl, al. ax ComSetLCR al., dl dx
;musimy wybrać rejestr IER poprzez zachowanie ;wartości LCR a potem wyczyszczenie bitu ;zatrzasku dzielnika szybkości transmisji ;adres IER ;odczyt bieżącej wartości IER ;zachowanie teraz ;odzyskanie starej wartości LCR ;przywrócenie zatrzasku dzielnika ;przywrócenie wartości IER
;zapis wartości AL. do IER dx ax ah, al. ComGetLCR ax al., 7fh ComSetLCR al., ah dx. ComIER dx, al. ax ComSetLCR ax dx
;zachowanie wartości AX ; zachowanie wartości IER do wyprowadzenia ;pobranie i zachowanie bitu dzielnika ;czyszczenie bitu dzielnika ;odzyskanie nowej wartości IER ;wybór rejestru IER ; wartość wyjściowa IER ;przywrócenie bitu dzielnika
Ostatni zbiór szeregowych podprogramów pojawiających się w Bibliotece Standardowej dostarcza wsparcia dla układów I/O sterowanych przerwaniami. Jest pięć podprogramów w tej sekcji: ComInitIntr, ComDisIntr, ComIntISR, ComIn i ComOut. ComInitIntr inicjalizuje system przerwań portu szeregowego . Zachowuje stary wektor [przerwania 0Ch, inicjalizuje wektor wskazywanym przez ComIntISR podprogramem obsługi przerwania i stosownie inicjalizuje PIS 8259A i 8250 SCC dla operacji opartych na przerwaniu. ComDisIntr niszczy wszystko co ustawi ComDisIntr; musimy wywołać ten podprogram wyłączając przerwania zanim nasz pogram się skończy. ComOut i ComIn przekazują dane do i z bufora opisanego w sekcji zmiennych;
Podprogram ComIntISR jest odpowiedzialny za usunięcie danych z kolejki transmisji i wysłanie przez linię szeregową , jak również buforowanie nadchodzących danych z lini szeregowej. Podprogram ComInitIntr inicjalizuje 8250 SCC i PIC 8259A przerwaniami szeregowym I/O. Również inicjalizuje wektor 0Ch wskazujący podprogram ComIntISR. Jedynej rzeczy jakiej ten kod nie robi to dostarczenie obsługi wyjątków break i błędu krytycznego. Pamiętajmy, jeśli użytkownik naciśnie ctrl-C (lub ctrl-Break) lub wybierze przerwanie przy błędzie I/O, domyślny program obsługi wyjątków po prostu wróci do DOS’a bez przywracania wektora 0Ch. Ważne jest aby nasz program dostarczył obsługi wyjątku, który wywoła ComDisIntr zanim zezwoli systemowi zwrócić sterowanie do DOS’a . W przeciwnym razie system może mieć krach, kiedy DOS załaduje kolejny program do pamięci. ComInitIntr
proc pushf push es push ax push dx ; Wyłączamy przerwania
;zachowanie flagi wyłączenia przerwań
cli ;Zachowujemy stary wektor przerwań. Oczywiście musimy zmienić poniższy kod zachowując i ustawiając ;wektor 0Bh. Jeśli chcemy uzyskać dostęp do COM2: zamiast do portu COM1; xor mov mov mov mov mv
ax, ax ;wskazuje wektor przerwań es, ax ax, Int0Cofs word ptr OldInd0C, ax ax, Int0Seg word ptr OldInt0C+2, ax
;wskazujemy wektor 0Ch naszego programu obsługi przerwań mov mov mov mov
ax, cs Int0Cseg, ax ax, offset ComIntISR Int0Cofs, ax
;zerujemy oczekujące przerwania: call call call mov in
ComGetLSR ComGetMSR ComGetIIR dx, ComPort al, dx
;zerujemy stan łącza odbiorczego ;zerujemy przerwania CTS/DSR/RI ;zerujemy przerwanie pustego transmitera ;zerujemy przerwanie dostępnej danej
; Zerujemy bit dostępu dzielnika/. PODCZAS OPEROWANIA W TRYBIE PRZERWAŃ, BIT TEN MUSI ; BYĆ ZEREM. Jeśli z jakiegoś dziwnego powodu musimy zmienić szybkość transmisji w środku transmisji ; lub kiedy przerwania są włąćzone) zerujemy flagę przerwań, zerujemy bit zatrzasku dzielnika i w końcu ; przywracamy przerwania. call and call
ComGetLCR al, 7fh ComSetLCR
;Pobranie LCR ;zerowanie bitu zatrzasku dzielnika ;zapisanie nowej wartości LCR
;Włączamy przerwania odbiornika i nadajnika. Zauważmy, że kod ten ignoruje błędy i przerwania ; zmiany stanu modemu mov call
al., 3 SetIERCom
;włączenie przerwań odb./nad.
;Musimy ustawić linie OUT2 do pracy z przerwaniami. Również ustawiamy aktywne DTR i RTS mov call
al., 00001011b ComSetMCR
;Aktywacja bitu COM1 (int 0Ch) w chipie 8259A. Notka: musimy zmienić poniższy kod dla ; wyczyszczenia bitu trzy (zamiast cztery) dla zastosowania tego kodu z portem COM2:.
ComInitIntr
in mov and out
al.,21h 18259a, al. al., 0efh 21h, al
pop pop pop popf ret endp
dx ax es
;pobranie wartości zezwolenia na przerwanie 8259A ;zapisanie bitów włączenia przerwania ;bit 4 = IRQ 4 = INT 0Ch ;włączenie przerwań
;przywrócenie wyłączonej flagi przerwań
Podprogram ComDisIntr blokuje przerwania szeregowe. Przywraca oryginalną wartość rejestru zezwolenia na przerwania 8259A, przywraca wektor przerwań int 0Ch i maskuje przerwania w 8250 SCC. Zauważmy, że kod ten zakłada, że nie musimy zmieniać bitów zezwolenia na przerwania w PIC 8259 ponieważ wywołujemy ComInitIntr. Przywraca rejestr zezwolenia na przerwanie 8259A wartością z rejestru zezwolenia na przerwanie 8259A kiedy pierwotnie wywołujemy ComInitIntr. Byłoby kompletną katastrofą wywołanie tego programu bez wcześniejszego wywołania ComInitIntr. Robiąc to, zaktualizowalibyśmy wektor 0Ch śmieciami i , podobnie, przywrócilibyśmy rejestr zezwolenia na przerwania 8259A ze śmieciami. Upewnijmy się, ze wywołaliśmy ComInitIntr przed wywołaniem tego podprogramu. Generalnie powinniśmy wywoływać ComInitIntr raz, na początku naszego programu, i wywołać ComDisIntr raz, albo na końcu programu albo wewnątrz podprogramu wyjątku break lub błędu krytycznego. ComDisIntr
proc pushf push push push
es dx ax
cli xor ax, ax mov es, ax ; Najpierw wyłączamy źródła przerwań w chipie 8250: call and call
ComGetMCR al., 3 ComSetMCR
;nie zezwalamy na przerwania ;ES wskazuje tablicę wektorów przerwań ;pobranie bitu OUT2 ;zamaskowanie bitu OUT2 ;zapis wyniku do MCR
; Teraz przywracamy bit IRQ 4 w PIC 8259A. Zauważmy, że musimy zmodyfikować ten kod , jeśli chcemy ; wesprzeć COM2: zamiast COM1: in and mov and or out
al., 21h al., 0efh ah, 18259a ah, 1000b al., ah 21h, al.
; przywracanie wektora przerwań: mov
ax, word ptr OldInt0C
;pobranie bieżącej wartości IER 8259A ;zerowanie bitu IRQ 4 -zmiana na COM2: ;pobranie naszej zapisanej wartości ;zamaskowanie bitu COM1 (IRQ 4) ;odłożenie bitu z powrotem
ComDisIntr
mov mov mov
Int0Cofs, ax ax, word ptr OldInt0C+2 Int0Cseg, ax
pop pop pop popf ret endp
ax dx es
Poniższy kod implementuje podprogram obsługi przerwania dla 8250 SCC. Kiedy wystąpi przerwanie, kod ten odczyta IIR 8250 aby określić źródło przerwania. Podprogramy Biblioteki Standardowej dostarczają tylko bezpośredniego starcia dla przerwania dostępnej danej i przerwania pustego rejestru przechowywania nadajnika. Jeśli kod ten wykryje błąd lub zmianę stanu przerwania, zeruje stan przerwania ale nie podejmuje innej akcji. Jeśli wykryje przerwanie odbiornika lub nadajnika, przekazuje sterowanie do właściwego programu obsługi Program obsługi przerwania odbiornika jest bardzo łatwy do implementacji. Wszystko co ten kod musi robić to odczytać znak z rejestru odbiorczego i dodać ten znak do bufora wejściowego. Jedyny ból, to to, że kod ten musi ignorować każdy nachodzący znak jeśli bufor wejściowy jest pełny. Aplikacja może uzyskać dostęp do tej danej przy zastosowaniu podprogramu ComIn, który usuwa dane z bufora wejściowego. Pogram obsługi nadajnika jest nieco bardziej złożony. 8250 SCC przerywa 80x86 kiedy możliwe jest zaakceptowanie więcej danych do transmisji. Jednakże, fakt, że 8250 jest gotowy na więcej danych nie gwarantuje, że te dane są gotowe do przesłania. Aplikacja tworzy dane swoją własną szybkością, nie koniecznie z szybkością jaką chce 8250 SCC. Dlatego też, jest całkiem możliwe przy 8250 powiedzenie „daj mi więcej danych” ale aplikacja nie tworzy żadnej. Oczywiście nie powinniśmy przesyłać niczego w tym momencie. Zamiast tego, musimy czekać aby aplikacja stworzyła więcej danych przed ponowieniem transmisji. Niestety , to komplikuje nieco sterowanie kodem przesyłu. Przy odbiorniku, przerwanie zawsze wskazywało, ze ISR może przesunąć dane z 8250 do bufora. Aplikacja może usunąć tą dana a proces jest zawsze taki sam.: oczekiwanie na nie pusty bufor odbiorczy a potem usunięcie pierwszej pozycji z bufora. Niestety nie możemy po prostu odwrócić działania kiedy przesyłamy dane. To znaczy nie możemy po prostu przechować danej w buforze przesyłu i pozostawić w ISR’ze aż do usunięcia tej danej. Problem jest taki, że 8250 przerywa tylko raz system, kiedy rejestr przechowujący nadajnika jest pusty. Jeśli w tym momencie nie ma danych do przesłania , ISR musi wrócić bez zapisywania czegokolwiek do rejestru przesyłu. Ponieważ nie ma danej w burze przesyłu, nie będzie generowanych dodatkowych przerwań, nawet jeśli będzie dodana dana do bufora przesyłu. Dlatego też, ISR i podprogramy odpowiedzialne za dodawanie danych do bufora wyjściowego (ComOut) musza skoordynować swoją aktywność. Jeśli bufor jest pusty a nadajnik aktualnie nie przesyła niczego, podprogram ComOut musi zapisać swoje dane bezpośrednio do 8250. Jeśli 8250 aktualnie przesyła dane, ComOut musi dołączyć swoje dane do na koniec bufora wyjściowego. ComIntISR i ComOut używają flag. TestBuffer określa czy ComOut powinien zapisać bezpośrednio do portu szeregowego lub dołączyć dane do bufora wyjściowego. Spojrzyjmy na ten kod i kod dla ComOut ComIntISR
TryAnother:
proc push push push mov in test jnz cmp jnz cmp jnz
far ax bx dx dx, ComIIR al, dx al., 1 IntRtn al., 100b ReadCom1 al., 10b WriteCom1
;pobranie wartości ID przerwania ;inne przerwania ? ;wyjdź ,jeśli inne przerwania nie oczekują ;ponieważ są tylko przerwania nad./odb. ; aktywne, sprawdzamy dla przerwań odb. ;sprawdzenie dla przerwania pustego nad.
; Fałszywe przerwanie? Nie powinniśmy nigdy dopuścić do tego kodu ponieważ nie włączyliśmy przerwań ; błędu lub zmiany stanu. Jednak jest możliwe ,że kod aplikacji może wejść i wkręcić IER w 8250. dlatego też, ; musimy dostarczyć domyślnego programu obsługi przerwań dla tych warunków. Poniższy kod odczytuje ; wszystkie właściwe rejestry dla wyzerowania każdego oczekującego przerwania.
call call jmp
ComGetLSR ComGetMSR TryAnother
;wyzerowanie stanu łącza odbiorczego ;wyzerowanie stanu modemu ;sprawdzenie niższego priorytetu przerwania
; Kiedy nie ma więcej przerwań oczekujących w 8250, wracamy z tego ISR’a IntRtn:
mov al., 20h ;potwierdzenie przerwania sterownika przerwań out 20h, al ;8259A pop dx pop bx pop ax iret ; Tu mamy obsługę nadchodzących danych: ;(Ostrzeżenie: To jest region krytyczny. Przerwania MUSZĄ BYĆ WYŁĄCZONE podczas wykonywania tego ; kodu. Domyślnie, przerwania są wyłączane w ISR’ze. NIE WŁĄCZAMY ICH jeśli modyfikujemy ten kod) ReadCom1:
NoInpWrap:
mov in
dx, ComPort al., dx
;wskazuje rejestr wejściowy danej ;pobranie znaku wejściowego
mov mov
bx, InHead [bx], al.
;wprowadzenie znaku do szeregowego bufora wejściowego
inc cmp jb mov cmp je mov jmp
bx bx, offset InpBufEnd NoInpWrap bx, offset InpBuf bx, InTail TryAnother InHead, bx TryAnother
;zwiększenie wskaźnika bufora
;jeśli bufor jest pełny, ignorujemy ten znak ;wejściowy ;idziemy do programu obsługi innych przerwań ;8250
; Program obsługi danych wychodzących (to również jest region krytyczny): WriteCom1:
mov cmp jne
bx, OutTail bx, OutHead OutputChar
;zobaczmy czy bufor jest pusty ; jeśli nie, wyprowadzamy kolejny znak
;Jeśli głowa i ogon są równe, po prostu ustawiamy zmienną TestBuffer na zero i wychodzimy. Jeśli nie są równe ; wtedy dana jest w buforze i powinniśmy wyprowadzić kolejny znak. mov jmp
TestBuffer, 0 TryAnother
;obsługa innych przerwań oczekujących
;Wskaźniki bufora nie są równe, wyprowadzamy kolejny znak OutputChar:
mov mov out
al., [bx] dx, ComPort dx, al.
;Okay, wskaźnik wyjściowy
NoOutWrap:
inc cmp jb mov mov
bx bx, offset OutBufEnd NoOutWrap bx, offset OutBuf OutTail, bx
;pobrani kolejnego znaku z bufora ;wybór portu wyjściowego ;wyprowadzania znaku
ComIntISR
jmp endp
TryAnoter
Ostanie dwa podprogramy odczytują dane z szeregowego bufora wejściowego i zapisują dane do bufora wyjściowego. Podprogram ComIn, który obsługuje sprawy wejściowe, czeka dopóki bufor wejściowy nie będzie pusty. Potem usuwa pierwszy dostępny bajt z bufora wejściowego i zwraca tą wartość do programu wywołującego ComIn proc pushf ;zachowanie flagi przerwania push bx sti ;upewnijmy się, że przerwania są włączone TstInLoop: mov bx, InTail ;czekamy dopóki jest przynajmniej jeden znak cmp bx, InHead ; w buforze wejściowym je TstInLoop mov al., [bx] ;pobranie kolejnego znaku cli ;wyłączenie przerwań kiedy modyfikujemy inc bx ;wskaźniki bufora cmp bx, offset InpBufEnd jne NoWrap2 mov bx, offset InpBuf NoWrap2: mov InTail, bx pop bx popf ;przywrócenie flagi przerwania ret ComIn endp ComOut musi sprawdzić zmienną TestBuffer aby zobaczyć czy 8250 jest aktualnie zajęty. Jeśli nie (TestBuffer równa się zero) wtedy kod ten musi zapisać znak bezpośrednio do portu szeregowego i ustawić TestBuffer na jeden (ponieważ chip jest teraz zajęty). Jeśli TestBuffer zawiera wartość nie zerową, od ten po prostu dodaje znak z al. na koniec bufora wyjściowego. ComOut
proc pushf cli cmp jnz
far TestBuffer, 0 BufferItUp
;żadnych przerwań ;zapisać bezpośrednio do chipa? ;jeśli nie, włóż do bufora
;Poniższy kod zapisuje bieżący znak bezpośrednio do portu szeregowego ponieważ 8250 nie przekazuje niczego ; teraz i nigdy ponownie nie dostaniemy przerwania pustego rejestru przechowującego nadajnika (przynajmniej ; doki nie zapiszemy danej bezpośrednio do portu) push mov out mov pop popf ret
dx dx, ComPort dx, al. TestBuffer, 1
;wybranie rejestru wyjściowego ;zapis znaku do portu ;bufor przechodzi do następnego znaku ;przywrócenie flagi przerwania
;jeśli 8250 jest zajęty, BufferItUp:
push mov mov
bx bx, OutHead [bx], al.
inc cmp jne mov
bx bx, offset OutBufEnd NoWrap3 bx, offset OutBuf
;wskaźnik do kolejnej pozycji w buforze ;dodanie znaku do bufora
NoWrap3: NoSetTail: ComOut
cmp je mov pop popf ret endp
bx, OutTail NoSetTail OutHead, bx bx
;zobacz czy pełny bufor ;nie dodawaj znaku jeśli bufor jest pełny ;jeśli nie, aktualizuj wskaźnik bufora ;przywrócenie flagi przerwania
Zauważmy ,że Biblioteka Standardowa nie dostarcza żadnego podprogramu , który sprawdza czy dana jest dostępna w buforze wejściowym lub czy bufor wyjściowy jest pełny (porównywalne do podprogramów ComTstIn i ComTstOut). Jednakże jest bardzo łatwo taki podprogram napisać ; wszystko co musimy zrobić to porównać głowę i ogon wskaźników dwóch buforów .Bufory są puste jeśli głowa i ogon wskaźników są równe. Bufory są pełne jeśli głowa wskaźnika jest jeden bajt przed wskaźnikiem ogona (zapamiętajmy ,że wskaźnik zawija na koniec bufora, więc bufor jest również pełny jeśli wskaźnik głowy jest na ostatniej pozycji w buforze a wskaźnik ogona na pierwszej pozycji w buforze). 22.4 PODSUMOWANIE Rozdział ten omawia komunikację szeregową RS-232 w PC. Podobnie jak przy porcie równoległym są trzy poziomy przy jakich możemy uzyskać dostęp do portu szeregowego: poprzez DOS, poprzez BIOS lub oprogramowanie bezpośrednie sprzętu. W odróżnieniu od wsparcia przez DOS i BIOS portu równoległego, wsparcie DOS’a dla szeregowego jest prawie bezwartościowe, a wsparcie BIOS jest dosyć słabe (tj. nie wspiera układów I/O sterowanych przerwaniami) . Dlatego też jest powszechną praktyką programistyczną na PC, sterowanie sprzętem bezpośrednio z aplikacji. Dlatego też , zapoznanie się z chipem komunikacji szeregowej 8250 jest ważne jeśli mamy zamiar korzystać z komunikacji szeregowej . Rozdział ten nie omawia komunikacji szeregowej pod DOS i BIOS, głownie z powodu ich ograniczonego wsparcia. 8250 wspiera 10 rejestrów I/O, które pozwalają nam sterować parametrami komunikacji, sprawdzić stan chipa, sterować potencjałem przerwań i, oczywiście ,wykonania szeregowych I/O, odwzorowania tych rejestrów 8250 jako ośmiu lokacji I/O w przestrzeni adresowej I/O PC. PC wspiera do czterech urządzeń komunikacji szeregowej: COM1:, COM2:, COM3: i COM4:. Jednak większość oprogramowania działa tylko z portami COM1; i COM2:. Podobnie jak przy port równoległy, BIOS rozróżnia logiczny port komunikacyjny i fizyczny port komunikacyjny. BIOS przechowuje adres bazowe COM1:...COM4: w komórkach pamięci 40:0, 40:2. 40:4 i 40:6. Ten adres bazowy jest jst adresem I/O pierwszego rejestru 8250 dla tego szczególnego portu komunikacyjnego. *”Chip komunikacji szeregowej 8250” *”Rejestr danych (rejestr nadawczy / odbiorczy” *”Rejestr zezwolenia na przerwanie (IER)” *”Dzielnik szybkości transmisji” *”Rejestr identyfikacji przerwań (IIR)” *”Rejestr sterowania łączem” *”Rejestr sterowanie modemem” *”Rejestr stanu łącza (LSR)” *”Rejestr stanu modemu (MSR)” *”Pomocniczy rejestr wejściowy” Biblioteka Standardowa UCR dostarcza bardzo rozsądnego zbioru podprogramów jakie możemy użyć do sterowania portem szeregowym w PC. Pakiet ten dostarcza nie tylko zbioru podprogramów odpytujących ,których możemy użyć podobnie jak kodu BIOS’a ale również podprogramów obsługi przerwań wspierających układy I/O sterowane przerwaniami w porcie szeregowym *”Podprogramy wspierające komunikację szeregową biblioteki standardowej UCR” Podprogramy szeregowych I/O Biblioteki Standardowej dostarczają doskonałych przykładów jak oprogramować 8250 SCC. Dlatego też, rozdział ten kończy się przedstawieniem i objaśnieniem podprogramów szeregowych I/O Biblioteki Standardowej. W szczególności, kod ten demonstruje pewne subtelne problemy z komunikacją szeregową sterowaną przerwaniami *”Oprogramowanie 8250 (Przykłady z Biblioteki Standardowej) „
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 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 TRZECI: MONITOR EKRANOWY PC Monitor ekranowy jest bardzo złożonym systemem. Po pierwsze nie jest pojedynczym urządzeniem jakie istnieją dla portów równoległego i szeregowego., lub nawet kilkoma urządzeniami (jak system klawiaturowy PC). Są dosłownie tuziny różnych kart rozszerzeń monitora dostępnych dla PC. Ponadto, każde rozszerzenie wspiera kila różnych trybów wyświetlania . Mając daną dużą liczbę trybów wyświetlania i używanych rozszerzeń wyświetlania, łatwo byłoby napisać grubą książkę o samym monitorze PC. Jednakże to nie jest ten tekst. Ta książka byłaby lekko niekompletna bez przynajmniej wzmianki o monitorze PC, ale nie ma dosyć miejsca aby wgłębiać się w ten temat. Dlatego też, rozdział ten będzie omawiał tryb wyświetlania tekstowego 80x25 , który wspierają prawie wszystkie rozszerzenia wyświetlania. 23.1 ODWZOROWANIE PAMIĘCI VIDEO Wiele urządzeń peryferyjnych w PC używa wejścia / wyjścia odwzorowanego. Program komunikujący się z odwzorowanymi I/O używa instrukcji 80x86 in, out, ins i outs uzyskując dostęp od urządzeń w przestrzeni adresowej I/O PC. Podczas gdy chip kontrolera video, który pojawia się na karcie monitora PC również odwzorowywuje rejestry PC w przestrzenia I/O, karty te również stosują drugą postać I/O wejście/ wyjście odwzorowywane w pamięci.. W szczególności, tryb tekstowy 80x25 jest niczym więcej niż dwu wymiarową tablicą słów, z czego każde słowo w tablicy odpowiada znakowi na ekranie. Tablica ta pojawia się powyżej punktu 649 K w przestrzeni adresowej PC. Jeśli przechowujemy dane w tej tablicy używając standardowych instrukcji adresowania (np. mov), będziemy wpływali na znaki pojawiające się na ekranie. W rzeczywistości są dwie róże tablice o które musimy się martwić. System monochromatyczny (pamiętacie?) lokuje swój start pod lokacją B000:0000 w pamięci. System kolorowy lokuje pod adresem B800:0000 w pamięci. Te lokacje są adresami bazowymi kolumnowej organizacji elementów tablicy jak zadeklarowano poniżej : Display: array [0..79, 0..24] of word; Jeśli wolimy działać z rzędowym pozycjonowaniem elementów tablicy, żaden problem, monitor jest równy poniższej definicji tablicy : Display: array [0..24, 0..79] of word; Zauważmy, że lokacja (0,0) jest to lewy górny róg a lokacja (79, 24) jest to prawy dolny róg monitora (wartości w nawiasach to współrzędne x i y, ze współrzędna poziomą x pojawiająca się najpierw) Mniej znaczący bajt każdego słowa zawiera kod PC/ASCII dla znaku jaki chcemy wyświetlić. Bardziej znaczący bajt każdego słowa jest atrybutem bajtu. Wrócimy do atrybutu bajtu w kolejnej sekcji. Wyświetlenie stron zajmuje odrobinę mniej niż 4Kilobajty w odwzorowaniu pamięci. Karata wyświetlacza kolorowego w rzeczywistości dostarcza 32K dla trybu tekstowego i pozwala wybrać jeden z ośmiu różnych wyświetleń Każde takie wyświetlanie zaczyna się od granicy 4K pod adresami B800:0, B800:1000, B800:2000,...B800:7000. Zauważmy, że większość nowoczesnych kolorowych monitorów w rzeczywistości dostarcza pamięci spod adresu A000:0 do B000:FFFF ( i więcej), ale tryb tekstowy używa tylko 32K od B800:0 do B800:7FFF. W rozdziale tym skoncentrujemy się tylko na pierwszej stronie kolorowego monitora pod adresem B800:0. Jednakże wszystkie omówienia w tym rozdziale odnoszą się również do innych stronic wyświetlania.
Karta monochromatyczna dostarcza tylko pojedynczej strony wyświetlania . Istotnie, najwcześniejsze monitory monochromatyczne miały tylko 4K wbudowanej pamięci (kontrastuje to z kolorowymi monitorami o wysokiej rozdzielczości , które mają do czterech megabajtów wbudowanej pamięci. Możemy zaadresować pamięć monitora jak zwykły RAM. Możemy przechować nawet zmienne programowe lub nawet kod w tej pamięci. Jednakże nigdy takie cos nie jest dobrym pomysłem. Przede wszystkim zapisujemy , w dowolnym czasie , na ekran, więc zniszczymy zmienne przechowywane w aktywnej pamięci monitora. Nawet jeśli przechowujemy taki kod lub zmienne w nieaktywnej stronie wyświetlacza (np. strony od jeden do siedem na kolorowym monitorze) używanie pamięci w ten sposób nie jest dobrym pomysłem ponieważ dostęp do karty jest bardzo wolny. Pamięć główna działa od dwóch do dziesięciu razy szybciej (w zależności od maszyny) 23.2 ATRYBUT BAJTU VIDEO Atrybut video związany jest z każdym znakiem na ekranie sterujący podkreśleniem, intensywnością, odwróceniem video i migotaniem obrazu na ekranie monochromatycznym. Steruje migotaniem i kolorem znaku pierwszoplanowym/ tła na kolorowym wyświetlaczu. Poniższy obrazek pokazuje możliwe wartości atrybutów:
Chcąc uzyskać odwrotność video, po prostu zmieniamy kolory pierwszoplanowy i tła. Zauważmy, że kolor pierwszoplanowy zero z kolorem tła siedem tworząc czarny znak na białym tle, standardowy kolor odwróconego video i takiej samej wartości atrybutu używanego na monitorze monochromatycznym. Musimy być ostrożni ,kiedy wybieramy kolory pierwszoplanowy i tła dla tekstu na kolorowym monitorze. Pewne kombinacje są niemożliwe do odczytu (np. białe znaki na białym tle). Inne kolory razem
mogą spowodować, że trudno jest je odczytać, jeśli nie jest to niemożliwe ( jak jasno zielone litery na zielonym tle?) Musimy być ostrożni wybierając kolory! Znaki migające są doskonałe do przykuwania uwagi do jakiegoś ważnego tekstu na ekranie (jak ostrzeżenie). Jednakże łatwo jest przesadzić z migającym tekstem na ekranie. Nigdy nie powinniśmy mieć więcej niż jednego słowa lub zdania migającego na ekranie w danym czasie. Co więcej, nigdy nie powinniśmy zostawiać migających znaków na monitorze dłuższy czas. Po kilku sekundach, zamieńmy znaki migające na normalne aby uniknąć irytacji użytkownika naszego programu. Zapamiętajmy, że możemy łatwo zmienić atrybuty różnych znaków na ekranie bez wpływania na bieżący tekst. Pamiętajmy, atrybut bajtu pojawia się pod adresem nieparzystym w przestrzeni pamięci dla wyświetlacza. Możemy łatwo wejść i zmienić te bajty pozostawiając same dane kodu znaku. 23.3 OPROGRAMOWANIE TRYBU TEKSTOWEGO Możemy zapytać dlaczego ktoś chce zawracać sobie głowę pracowaniem bezpośrednio z wyświetlaczem odwzorowywanym w pamięci PC. Przecież DOS, BIOS i biblioteka Standardowa dostarczają dużo bardziej dogodnego sposobu wyświetlania tekstu na monitorze. Obsłużenie nowej lini (powrót karetki i przesuniecie o jedną linię) na końcu lini, lub , jeszcze gorzej, przesuwanie ekranu kiedy wyświetlacz jest pełny, to dużo racy. Pracy, którą automatycznie zajmują się wyżej wspomniane podprogramy. Pracy, którą musimy wykonać sami jeśli chcemy uzyskać dostęp bezpośredni do pamięci ekranu. Więc czemu zawracać sobie głowę? Są dwa powody: wydajność i elastyczność. Podprogramy ekranowe BIOS są straszliwie wolne Możemy łatwo uzyskać zwiększenie wydajności od 10 do100razy poprzez zapisanie bezpośrednio do pamięci monitora. Dla typowego projekty informatycznego może nie być to ważne, zwłaszcza jeśli pracujemy na szybkiej maszynie, takiej jak Pentium 150 MHz. Z drugiej strony, jeśli projektujemy program, który wyświetla i usuwa kilka okien lub menu pop-up na ekranie, podprogramy BIOS nie zrobią tego. Chociaż funkcja BIOS int 10h dostarcza dużego zbioru podprogramów video I/O, będzie mnóstwo funkcji jakich nie będziemy chcieli aby BIOS dostarczył. W takim przypadku przejście bezpośrednio do pamięci ekranowej będzie najlepszym rozwiązaniem tego problemu. Inna trudnością z podprogramem BIOS jest to, że nie jest współużywalny. Nie możemy wywołać funkcji wyświetlacza BIOS z podprogramu obsługi przerwania, ani nie możemy swobodnie wywołać BIOS z jednocześnie wykonywanych procesów. Jednakże poprzez napisanie własnego podprogramu obsługi video, możemy łatwo stworzyć okno dla każdego współbieżnego wątku jaki wykonuje aplikacja. Wtedy każdy wątek może wywołać nasz podprogram wyświetlając swoje dane wyjściowe niezależnie od innych wątków wykonywanych w systemie. Program AMAZE.ASM jest dobrym przykładem programu, który bezpośrednio uzyskuje dostęp do trybu tekstowego poprzez bezpośrednie przechowywanie danych w odwzorowywanej w pamięci monitora tablicy wyświetlania. Program ten uzyskuje dostęp bezpośredni do pamięci ponieważ jest bardziej dogodnie to zrobić ( tablica wyświetlacza ekranowego odwzorowuje całkiem dobrze wewnętrzną tablicę labiryntu) .Proste gry video również dobrze odwzorowywują pamięć monitora. Poniższy program dostarcza doskonałego przykładu aplikacji, która musi uzyskać dostęp bezpośredni do pamięci video. Program ten to TSR przechwytywania ekranu. Kiedy naciśniemy lewy shift a potem prawy, program ten skopiuje zawartość bieżącego ekranu do wewnętrznego bufora. Kiedy naciśniemy prawy shift po którym nastąpi lewy shift, program skopiuje bufor wewnętrzny na ekran .Pierwotnie program ten został napisany do przechwytywania ekranów CodeView dla celów laboratoryjnych towarzyszących tej książce. Istnieją komercyjne programy do przechwytywania ekranów (np. HiJak), które dobrze pracują ale są niekompatybilne z CodeView. Ten krótki TSR pozwala przechwycić ekran w CodeView, wyjść z CodeView, wprowadzić z powrotem ekran CodeView na ekran i stosować program taki jak HiJak do przechwytywania wyjścia. ; GRABSCRN.ASM ; ; Krótki TSR przechwytujący bieżący stan ekranu i wyświetlający go później. ; ; Zauważmy, ze ten kod nie aktualizuje int 2Fh (przerwanie równoczesnych procesów) ani nie można usunąć ; tego kodu z pamięci, z wyjątkiem przeładowania. Jeśli chcemy móc zrobić te dwie rzeczy (jak również ; sprawdzenie poprzedniej instalacji) zobacz rozdział o programach rezydentnych. ; ; cseg i EndResident muszą pojawić się przed segmentem biblioteki standardowej ! cseg
segemnt para public ‘code’
OldInt9 ScreenSave ceg
dword ? byte 4096 dup (?) ends
; Oznaczamy segment znajdując koniec sekcji rezydentnej EndResident EndResident
segment para public ‘Resident’ ends .xlist include includelib ;list
RShiftScan LShiftScan
equ equ
stdlib.a stdlib.lib
36h 2ah
; Bity dla klawiszy modyfikujących shift RShfBit LShfBit
equ equ
1 2
KbdFlags
equ
< byte ptr ds.:[17h]>
byp
equ
; Adres segmentowy ekranu. Wartość ta jest tylko dla ekranu kolorowego. Zmień na B000h jeśli chcesz użyć ; tego programu z monitorem monochromatycznym ScreenSeg
equ
0B800h
cseg
segment para public ‘code’
; MyInt9; ; ; ; ; ; ;
ISR INT 9. Podprogram ten odczytuje port klawiatury aby sprawdzić czy nadszedł kod klawiaturowy klawisza shift. Jeśli jest ustawiony bit prawego klawisza w KbdFlags, nadchodzi kod klawiaturowy lewego shift’a, chcemy skopiować dane z naszego wewnętrznego bufora do pamięci ekranowej. Jeśli jest ustawiony bit lewego shift’a i naschodzi kod klawiaturowy prawego shift’a chcemy skopiować pamięć ekranową do naszej lokalnej tablicy. W innym przypadku (żaden z nich nie nadszedł) zawsze przekazujemy sterowanie do oryginalnego programu obsługi INT 9
MyInt9
proc push push mov mov in cmp je cmp Jne
far ds. ax ax, 40h ds., ax al., 60h al., RshiftScan DoRight al, LShiftScan QuitMyInt9
;odczyt portu klawiatury ;wciœniêto prawy shift? ;lewy shift?
;jeśli jest to kod klawiaturowy lewego shift’a, zobaczmy czy prawy jest już wciśnięty test je
KbdFlags, RShfBit QuitMyInt9
;skok jeśli nie
;Okay, prawy shift wciśnięty i wdzieliśmy, że lewy też, kopiujemy nasze lokalne dane do pamięci ekranowej:
pushf push push push push mov mov mov lea mov mov xor jmp
es cx di si cx, 2048 si, cs ds., si si, ScreenSave di, ScreenSeg es, di di, di DoMove
;Okay, mamy już widzieliśmy kod klawiaturowy prawego shift’a, zobaczmy czy lewy shift jest wciśnięty. Jeśli ;tak, zapisujemy bieżącą daną ekranową do naszej lokalnej tablicy DoRight:
DoMove:
test je
KbdFlags, LShfBit QuitMyInt9
pushf push push push push mov mov mov lea mov mov xor
es cx di si cx, 2048 ax, cs es, ax di, ScreenSave si, ScreenSeg ds, si si, si
cld rep movsw pop pop pop pop popf
si di cx es
QuitMuInt9:
MyInt9 Main
pop pop jmp endp
ax ds OldInt9
proc assume ds:cseg mov mov
ax, cseg ds, ax
print byte byte byte byte
“Screen capture TSR”, cr, lf “Pressing left shift, then right shift, captures” “the current screen.”, cr, lf “Pressing right shift , then left shift, dsiplays “
byte byte
“the last aaptured scren.”, cr, lf 0
;Aktualizujemy wektor przerwań INT 9. Zauważmy ,że powyższe instrukcje uczyniły cseg ; bieżącym 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] word ptr OldInt9+2, ax es:[9*4], offset MyInt9 es:[9*4+2], cs ; włązamyprzrwania
; jedyne co musimy zrobić to zakończyć i pozostawić 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
23.4 PODSUMOWANIE System video PC używa tablicy odwzorowania pamięci dla danych ekranowych. Jest to 80x25 kolumnowa organizacja tablicy słów. Każde słowo w tablicy odpowiada pojedynczemu znakowi na ekranie. Tablica zaczyna się pod adresem B000:0 dla wyświetlacza monochromatycznego i B800:0 dla wyświetlacza kolorowego. *”Odwzorowywanie pamięci video” Najmniej znaczący bajt jest kodem znaku PC/ASCII dla tej szczególnej pozycji ekranu, bardziej znaczący bajt zawiera atrybut dla tego . Atrybut wybiera migotanie, intensywność i kolor pierwszoplanowy / tła ( na wyświetlaczu kolorowym) *”Atrybut bajtu video” Jest kilka powodów dla których możemy chcieć trudzić się uzyskaniem bezpośredniego dostępu do pamięci monitora. Szybkość i elastyczność są dwoma podstawowymi powodami dal których ludzie idą
bezpośrednio do pamięci ekranowej. Możemy stworzyć własną funkcję, której nie wspiera BIOS i robi to raz lub dwa razy szybciej niż BIOS poprzez zapisanie bezpośrednio do pamięci ekranowej *”Oprogramowanie trybu tekstowego”
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 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 CZWARTY: ZŁĄCZE GIER PC Ktoś kto zajrzy głębiej wewnątrz kilku popularnych gier PC odkryje, że wielu programistów nie w pełni rozumie jedno z najmniej złożonych urządzeń dołączonych do dzisiejszych PC – analogowego złącza gier. Urządzenie to pozwala użytkownikowi połączyć do czterech potencjometrów rezystancyjnych i czterech przełączników cyfrowych połączonych z PC. Na projekt złącza gier PC wpłynęły oczywiście możliwości wejścia analogowego komputera Applle II, najpopularniejszego komputera dostępnego w czasie prac nad PC. Chociaż IBM dostarczył da razy więcej wejść analogowych niż Apple II, decyzja wsparcia tylko czterech przełączników i czterech potencjometrów (lub „pots”) wydaje się ograniczać dzisiejszych projektantów gier – w ten sam sposób co decyzja IBM o wsparciu 256 K RAM wydaje się dzisiaj ograniczeniem. Niemniej jednak, projektanci gier dają sobie radę tworząc rzeczywiście cudowne produkty, nawet żyjąc z ograniczeniami IBM z 1981 roku. Projekt IBM’owskiego wejścia analogowego, podobnie jak Apple, zaprojektowano za psie pieniądze. Dokładnością i wydajnością nie przejmowano się wcale. Faktycznie możemy kupić elektroniczne części do zbudowania swojej wersji złącza gier, detalicznie, po około trzy dolary. Istotnie możemy dzisiaj zakupić kartę złącza gier z różnymi kupieckimi rabatami za około osiem dolarów. Niestety, mało kosztowny, IBM’owski projekt z 1981 roku stwarza problemy z wydajnością dla szybkich maszyn i wysoko wydajnych gier w 1990 roku. Jednakże nie ma co rozpaczać na d rozlanym mlekiem – zostaniemy przy projekcie oryginalnego złącza gier. Poniższa sekcja opisuje dokładnie jak go wykorzystać. 24.1 TYPOWE URZĄDZENIA DO GIER Złącze gier jest niczym więcej jak sprzęgiem komputerowym dla różnych urządzeń dla gier. Typowa karta złącza gier zawiera złącze DB15 do którego możemy podpiąć urządzenie zewnętrzne Do typowych urządzeń jakie możemy podłączyć do złącza gier zaliczamy paddle, joystick, flight yoke, cyfrowy joystick, rudder pedal, symulator RC i steering wheels. Bez wątpienia jest to krótka lista typowych urządzeń jakie możemy podłączyć do złącza gier. Wiele z tych urządzeń jest o wiele droższych niż sama karta złącza gier. Rzeczywiście, wysokiej jakości konsole symulatorów lotu dla złącza gier kosztują kilka set dolarów. Joystick cyfrowy jest prawdopodobnie najmniej złożonym urządzeniem jakie możemy podłączyć do portu gier PC. Urządzenie to składa się z czterech przełączników i drążka. Przesuwanie drążka w przód lewo, prawo lub w tył, zamyka jeden z tych przełączników Karta złącza gier dostarcza czterech wejść przełączników, wiec możemy wyczuć kierunek (wliczając w to pozostałe pozycji) użytkownika naciskającego joystick. Większość cyfrowych joysticków pozwala również na wyczucie pozycji pośrodku poprzez zwarcie dwóch kontaktów na raz, na przykład poprzez ułożenie drążka pod kątem 45 stopni pomiędzy pozycją w „przód” a „w prawo”. Aplikacje mogą to odczuć i wybrać odpowiednią akcję. Pierwszą korzyścią tych urządzeń jest to ,że są bardzo tanie w wytworzeniu (dlatego pierwsze joysticki znajdowały się w większości domowych maszyn). Jednakże, producenci zwiększając produkcje joysticków analogowych ceny spadły do punktu w którym joysticki cyfrowe nie zdołały zaoferować pokaźniej różnicy cen. Wiec dzisiaj rzadko możemy spotkać takie urządzenia w rękach użytkowników. Paddle jest innym urządzeniem , którego używanie podupadło przez lata. Paddle jest pojedynczym potencjometrem z pojedynczym pokrętłem ( i zazwyczaj z jednym przyciskiem) . Apple dostarczał pary paddle z każdym sprzedanym Apple II. W wyniku tego, gry używające paddle były całkiem popularne kiedy IBM wypuścił PC w 1981 roku. Istotnie klika firm [produkowało paddle dla PC, kiedy został wypuszczony po raz pierwszy. Jednakże, znowu koszt wytworzenia joysticków analogowych spadł do poziomu z którym paddle nie mogło konkurować. Chociaż paddle są odpowiednimi urządzeniami wejściowymi dla wielu gier, joysticki mogą robić wszystko to co paddle i wiele innych. W tej sytuacji stosowanie paddle szybko zamiera. Jest jedna rzecz,
jaką możemy zrobić paddle , a której nie można zrobić joystickiem – możemy umieścić cztery z nich w systemie i stworzyć czterech graczy. Jednak to (oczywiście0 nie jest ważne dal większości projektantów gier, którzy generalnie projektują gry tylko dla jednego gracza. Paddle lub zbiór rudder pedals generalnie dostarczają pojedynczej liczby z zakresu od zera do jakiejś wartości maksymalnej zależnej od systemu
0
Odczyt maksimum Urządzenie wejściowe paddle lub rudder pedal
Rudder pedals są niczym więcej niż specjalnie zaprojektowanym paddle’em, zaprojektowanym tak aby można było aktywować je stopami. Wiele gier symulatorów lotu wykorzystuje to urządzenie wejściowe dostarczając wiele rzeczywistych przeżyć. Ogólnie będziemy używali rudder pedals jako dodatek do joysticka. Joystick zawiera dwa potencjometry połączone z drążkiem. Przesunięcie joysticka analogowego wzdłuż osi x uruchamia jeden z potencjometrów, przesuwając joystick wzdłuż osi y uruchamia inny potencjometr. Poprzez odczyt obu potencjometrów możemy mniej więcej określić absolutną pozycję potencjometrów wewnątrz ich zakresu pracy.
URZĄDZENIE WEJŚCIOWE JOYSTICK Joystick używa dwóch niezależnych potencjometrów dostarczających wartości wejściowych (X,Y). Poziome przesunięcie joysticka wpływa na oś x potencjometru nie zależnie od osi y potencjometru. Podobnie pionowe przesunięcie wpływa na oś y niezależnie od osi x potencjometru. Przez odczytanie obu potencjometrów możemy określić pozycję joysticka w systemie współrzędnych (X,Y). Symulator RC jest niczym więcej niż pudełkiem zawierającym dwa joysticki . Urządzenia yoke i steering wheel są zazwyczaj takimi samymi urządzeniami sprzedawanymi specjalnie do symulatorów lotu lub gier samochodowych. Steering wheel jest podłączony do potencjometru, który odpowiada osi x joysticka. Cofnięcie (lub popchnięcie) na kierownicy aktywuje drugi potencjometr odpowiadający osi y joysticka. Niektóre urządzenia joystickowe, ogólnie znane jako flight sticks zawiera trzy potencjometry . Dwa potencjometry są połączone w sposób standardowego joysticka, trzeci jest podłączone do gałki, której wiele gier używa dla sterowania przepustowością . Inne joysticki, takie jak Thrustnmaster™ lub CH Products’FlightStick Pro, zawierają dodatkowe przełączniki zawierające specjalne „cooley switch”, które dostarczają dodatkowych wejść do gry. Cooley switch jest, w gruncie rzeczy, cyfrowym potencjometrem zamontowanym na szczycie joysticka. Użytkownicy mogą wybrać jedną z czterech pozycji cooley switch’a
używając kciuków. Większość programów symulatorów lotu zgodnych z takimi urządzeniami używa cooley switcha do wyboru różnych widoków z samolotu.
COOLEY SWITCH Cooley switch (pokazany powyżej na urządzeniu podobnym do CH Products’ FlightStick Pro) uruchamia kciukiem cyfrowy joystick. Możemy przesunąć przełącznik w górę, dół, w lewo lub prawo, uruchamiając pojedyncze przełączniki wewnątrz urządzenia. 24.2 SPRZĘTOWE ZŁĄCZE GIER Sprzętowe złącze do gier jest proste. Jest to pojedynczy port wejściowy i pojedynczy port wyjściowy. Rozkład bitów portu wejściowego jest następujący
GAME ADAPTER INPUT PORT
Cztery przełącznik nadchodzą do czterech bardziej znaczących bitów portu I/O 201h. Jeśli użytkownik aktualnie nacisnął przycisk, odpowiednie pozycje bitów będą zawierały zero. Jeśli przycisk jest zwolniony, odpowiednie bity będą zawierały jeden Potencjometr wejściowy może wydawać się dziwny na pierwszy rzut oka. W końcu, jak można przedstawić jedną z dużych liczb potencjalnej pozycji potencjometru (powiedzmy, 256) pojedynczym bitem? Oczywiście nie możemy. Jednakże, bit wejściowy w tym porcie nie zwraca żadnego typu wartości numerycznej określającej pozycję potencjometru. Zamiast tego, każdy z czterech bitów potencjometru jest połączony z wejściem wrażliwego na oporność czterowyjściowego chipu zegarowego 558. Kiedy wyzwalamy chip zegarowy, tworzy on impuls wyjściowy o czasie trwania proporcjonalnym do oporności wejścia do timera. Wyjście tego chipu zegarowego jest traktowane jako bit wejściowy danego portu .Schemat tego układu to
SCHEMAT JOYSTICKA Normalnie bity wejściowe joysticka zawierają zero. Kiedy wyzwalamy chip zegarowy, linie wejściowe potencjometru idą wyżej dla tego samego okresu czasu określonego przez bieżącą oporność potencjometru. Poprzez pomiar jak długo ten bit pozostaje ustawiony, możemy uzyskać wstępne oszacowanie oporności. Aby wyzwolić potencjometr po prostu zapisujemy wartość do portu I/O 201h. Rzeczywista wartość jaką wpiszemy jest nieistotna Poniższy wykres pokazuje sygnał różni się na każdym z bitów wejściowych potencjometru
ANALOGOWY SYGNAŁ WEJŚCIOWY CZĘSTOTLIWOŚCI ZEGARA Pozostaje jedynie pytanie „jak określimy długość impulsu?”. Poniższa krótka pętla demonstruje jeden sposób określenia szerokości tego impulsu:
CntLp:
mov mov out in test loopne neg
cx, -1 dx, 201h dx, al. al., dx al 1 CntLp cx
;Mamy zamiar liczyć wstecz ;wskazujemy port joysticka ;wyzwalamy chip zegarowy ;odczyt portu joysticka ;sprawdzenie wejścia # 0 potencjometru ;powtarzanie dopóki wysoko ;konwertujemy CX do wartości dodatniej
Kiedy ta pętla kończy wykonywanie, rejestr cx będzie zawierał liczbę przejść uczynionych przez tą pętle podczas gdy sygnał wyjściowy timera był logiczną jedynką. Duża wartość w cx, , dłuższy impuls i dlatego większa oporność potencjometru # 0. Jest kilka ważnych problemów z tym kodem. Przede wszystkim, kod ten oczywiście będzie tworzył różne wyniki na różnych maszynach działających przy różnych częstotliwościach cyklu zegara. Na przykład, system Pentium 150 MHz będzie wykonywał ten kod dużo szybciej niż system 8088 5 MHz. Drugi problem jest taki, że joysticki i inne karty adaptera gier tworzą radykalnie różne wyniki częstotliwości zegara. Nawet w tym samym systemie z taką samą kartą adaptera i joystickiem, nie musimy zawsze uzyskiwać spójnych odczytów w różnych dniach. Okazuje się ,że 558 jest w pewnym sensie czuły na temperaturę i będzie tworzył odrobinę różne odczyty przy zmianach temperatury. Niestety, nie ma sposobu stworzenia takiej pętli jak powyższa aby zwracała stały odczyt dla szerokiego wyboru maszyn, potencjometrów i kart adapterów gier. Dlatego też musimy napisać naszą aplikację tak aby była niewrażliwa na szerokie zmiany w wartościach wejściowych z wejść analogowych. Na szczęście, js to bardzo łatwe do zrobienia, ale więcej o tym później. 24.3 ZASTOSOWANIE FUNKCJI BIOS GIER I/O BIOS dostarcza dwóch funkcji do odczytu wejść złącza gier. Obie są podfunkcjami programu obsługi int 15h. Do odczytu przełączników, ładujemy do ah 84h a do dx zero, potem wykonujemy instrukcję int 15h. Przy zwrocie, al będzie zawierał odczytane cztery bardziej znaczące bity przełącznika (zobacz wykres w poprzedniej sekcji). Funkcja ta jest w przybliżeniu odpowiednikiem bezpośredniego odczytu portu 201h. Dla odczytu wejść analogowych, ładujemy do ah 84h a do dx jedynką potem wykonujemy instrukcję int 15h Przy zwrocie AX,BX,CX i DX będą zawierały wartości, odpowiednio, dla potencjometrów zero, jeden, dwa i trzy. W praktyce, funkcja to powinna zwrócić wartości z zakresu 0 –400h, chociaż nie możemy liczyć na to z powodów opisanych w poprzedniej sekcji. Bardzo niewiele programów używa wsparcia joysticka BIOS. Łatwiej odczytać przełączniki bezpośrednio a odczyt potencjometrów nie jest tą pracą, która wywołuje podprogramy BIOS. Kod BIOS jest bardzo wolny. Większość BIOS’ów odczytuje sekwencyjnie cztery potencjometry, wykonując to do czterech
razy dłużej niż program, który odczytuje wszystkie cztery potencjometry równocześnie (zobacz następną sekcję) Ponieważ odczyt potencjometrów może trwać kilkaset mikrosekund do kilku milisekund, większość programistów pisze wysokowydajne gry nie używając funkcji BIOS, piszą oni swoje własne wysoko wydajne podprogramy. To prawdziwy wstyd. Poprzez napisanie określonych sterowników do oryginalnych gier PC złącza gier, ci programiści zmuszają użytkownika do kupowania i użytkowania standardowych kart złącza gier i urządzeń gier. Gdyby byłyby to gry wywołujące funkcje BIOS, trzecia część programistów może stworzyć różne i unikalne sterowniki gier a potem po prostu wesprzeć sterowniki, które zamieniają podprogram int 15h i dostarczyć takiego samego interfejsu programistycznego. Na przykład, Genovation stworzył urządzenie, któ®e pozwala nam podłączyć joystick do portu równoległego PC. Colorado Spectrum stworzyło podobne urządzenie, któ®e pozwala podpiąć joystick do portu szeregowego. Oba urządzenia pozwolą nam na zastosowanie joysticka na maszynie, która nie ma (i być może nie może mieć) zainstalowanego złącza gier. Jednakże, gry, uzyskujące bezpośredni dostęp do joysticka sprzętowego, nie będą kompatybilne z takimi urządzeniami. Jednak projektując gry w oparciu o funkcję int 15h, oprogramowanie byłoby kompatybilne ponieważ zarówno Colorado Spectrum i Genovation wspierają TSR’y int 15h dla przekierowania wywołania joysticka do zastosowania w tych urządzeniach. Aby pomóc w przezwyciężeniu niechęci projektantów gier do stosowania int 15h, tekst ten będzie prezentował wysoko wydajne wersje kodu joysticka BIOS trochę później w tym rozdziale. Programiści, którzy adoptują Standard Game Device Interface stworzą programy, które będą kompatybilne z innymi urządzeniami, które wspierają standard SGDI. Po więcej szczegółów, zobacz do „Standardowy Interfejs Urządzenia Gier (SGDI)”. 24.4 PISANIE WŁASNEGO PODPROGRAMU I/O GIER Rozważmy ponownie kod, który zwraca pewną wartość dla danego ustawienia:
CntLp:
mov mov out in test loopne neg
cx, -1 dx, 201h dx, al. al., dx al, 1 CntLp cx
; mamy zamiar liczyć wstecz ;wskazanie portu joysticka ;wyzwolenie chipu zegarowego ;odczyt portu joysticka ;sprawdzenie wejścia potencjometru # 0 ;powtarzanie dopóki wysoko ;konwersja CX do wartości dodatniej
Jak wspomniano wcześniej, dużym problemem z tym kodem jest to ,że mamy zamiar pobrać gwałtownie wartości o różnych zakresach z różnych kart złączy gier, urządzeń wejściowych i systemów komputerowych. Oczywiście nie może zawsze liczyć na powyższy kod tworząc wartości w zakresie 0 ...180h pod takimi warunkami. Nasze oprogramowanie będzie musiało dynamicznie modyfikować wartości używane w zależności od parametrów systemu. Prawdopodobnie będziemy grac w gry na PC, gdzie oprogramowanie prosić będzie o kalibrację joysticka przed użyciem. Kalibracja generalnie składa się z przesunięcia joysticka do jednego z rogów (na przykład lwy górny róg), naciśnięcia przycisku lub klawisza a potem przesunięcie do przeciwległego rogu (np. dół-prawo) i ponownego naciśnięcia przycisku. Pewne systemy chcą nawet przesunięcia joysticka do pozycji centralnej i naciśnięcia przycisku. Oprogramowanie, które to robi odczytuje minimalną, maksymalną i centralną wartość z joysticka .Mając dana przynajmniej minimalną i maksymalną wartość, możemy łatwo wyskalować odczyt do zakresu jaki chcemy. Poprzez odczyt wartości centralnej, możemy uzyskać odrobinę lepsze wyniki, zwłaszcza na rzeczywiście niedrogich (tanich) joystickach. Ten proces skalowania odczytów do pewnego zakresu jest znany jako normalizacja. Poprzez odczyt wartości minimalnej i maksymalnej od użytkownika i potem normalizacji każdego odczytu, możemy napisać program zakładając, że wartość zawsze się będą znajdować wewnątrz pewnego zakresu, na przykład 0...255.Normalizacja odczytu jest bardzo łatwa, po prostu używamy następującej formuły: (BieżącyOdczyt – OdczytMinimalny) --------------------------------------------------- x WartośćNormalna (OdczytMaksymalny – OdczytMinimalny) Wartości OdczytMaksymalny i Odczyt Minimalny są wartościami minimalną i maksymalną odczytanymi od użytkownika na początku naszej aplikacji. BieżącyOdczyt jest to wartość odczytana ze złącza gier. WartośćNormalna jest to górna granica zakresu, do jakiego chcemy znormalizować odczyt (np. 255), dolną granicą jest zawsze zero
Dla uzyskania lepszych wyników , zwłaszcza kiedy używamy joysticka, powinniśmy uzyskać trzy odczyty podczas fazy kalibracji dla każdego potencjometru – wartość minimalną, wartość maksymalną i wartość centralną. Dla normalizacji odczytu, kiedy uzyskaliśmy te trzy wartości, powinniśmy użyć poniższych formuł:] Jeśli bieżący odczyt jest w zakresie minimalna...centralna , używamy wzoru: (Bieżąca – Centralna) --------------------------------- x WartośćNormalna (Centralna – Minimalna) x2 Jeśli bieżący odczyt jest w zakresie centralna...maksymalna używamy wzoru: (Bieżąca – Centralna) WartośćNormalna ------------------------------------ x WartośćNormalna + ----------------------(Maksymalna – Centralna) x2 2 Duża liczba gier na rynku spełnia wszystkie rodzaje wymagań próbując wymusić odczyt joysticka w rozsądnym zakresie. Zaskakująco niewiele z nich używa tej prostej, powyższej formuły. Niektórzy projektanci gier mogą sprzeczać się, że powyższe formuły są zbyt złożone i pisane dla wysoko wydajnych gier. To nonsens. Zabiera dwukrotnie więcej czasu oczekiwanie na przekroczenie czasu przez joystick niż obliczenie powyższego równania. Więc używajmy ich i uczyńmy nasze programy łatwiejszymi w pisaniu. Chociaż normalizacja naszego odczytu potencjometru zabiera trochę czasu, zawsze jest warta zachodu, odczyt wejść analogowych jest zawsze drogą operacją pod względem cykli CPU. Ponieważ układ zegarowy tworzy relatywnie stały czas opóźnienia dla danej oporności, będziemy nawet marnować więcej cykli CPU na szybkich maszynach niż robiąc to na maszynach wolnych (chociaż odczyt potencjometru zabiera mniej więcej taką samą ilość czasu rzeczywistego na każdej maszynie). Jednym z pewnych sposobów uniknięcia zmarnowania dużej liczby czasu, jest odczyt liku potencjometrów w tym samym czasie; na przykład kiedy odczytujemy potencjometr i jeden uzyskując odczyt joysticka, najpierw odczytujemy potencjometr zero a potem potencjometr jeden. Okazuje się , że możemy łatwo odczytać oba potencjometry równolegle. Robiąc to możemy przyspieszyć odczyt joysticka dwukrotnie. Rozważmy poniższy kod:
CntLp:
mov mov mov mov mov out in and jz shr adc add loop and and
cx, 1000h si, 0 di, si ax, si dx, 201h dx, al. al., dx al, 11b Done ax, 1 si, 0 di, ax CntLp si, 0FFFh di, 0FFFh
;maksymalny czas pętli ;odkładamy odczyt w SI i DI ;ustawiamy AH na zero ;wskazanie portu joysticka ;wyzwolenie chipu zegarowego ;odczyt portu joysticka ; usunięcie niepotrzebnych bitów ;wartość potencjometru 0 do flagi przeniesienia ;zwiększenie wartości pot. 0 jeśli jeszcze aktywny ;zwiększenie wartości pot. 1 jeśli jest aktywny ;powtarzanie dopóki wysoko ;jeśli przekroczono czas, wymuszamy na ;rejestrach zwierających 1000h do zera
Done: Kod ten odczytuje oba potencjometry zero i jeden w tym samym czasie. Działamy w pętli dopóki każdy potencjometr jest aktywny. Przez cały czas w pętli, kod ten dodaje wartości bitów potencjometrów do oddzielnych rejestrów, które akumulują wynik. Kiedy pętla się kończy, si i di zawierają odczyty dla obu potencjometrów zero i jeden. Chociaż ta szczególna pętla zawiera więcej instrukcji niż pętla poprzednia, zabiera tyle samo czasu jej wykonanie. Pamiętajmy, że impuls wyjściowy na timerze 558 określa jak długo ten kod się wykonuje, liczba instrukcji w pętli przyczynia się w niewielkim stopniu do czasu wykonania. Jednakże czas tej pętli potrzebny do wykonania jednej iteracji pętli wpływa na decyzję podprogramu odczytu joysticka. Szybsze wykonanie pętli, więcej iteracji pętli będzie uruchomionych podczas takiego samego okresu czasu i będzie lepszy pomiar.
Generalnie chociaż rozdzielczość powyższego kodu jest większa niż dokładność elektronicznych urządzeń gier, więc nie jest to wielkim problemem. Powyższy kod demonstruje jak odczytać dwa potencjometry. Łatwo jest rozszerzyć ten kod do odczytu trzech lub czterech potencjometrów. Przykład takiego podprogramu pojawi się w sekcji o sterownikach urządzeń SGDI dla standardowej karty rozszerzeń gier. Inne urządzenia gier, przełączniki, wydają się być prostsze w porównaniu z potencjometrami. Zazwyczaj jednak rzeczy nie są tak łatwe jak wydaje się to na pierwszy rzut oka. Przełączniki mają kilka swoich własnych problemów Pierwszy to drganie styków klawisza. Przełączniki w typowym joysticku są prawdopodobnie rząd wielkości gorsze niż klawisze na najtańszej klawiaturze. Drgania styków klawiszy jest faktem z jakim musimy się liczyć kiedy odczytujemy przełączniki joysticka. Generalnie, nie powinniśmy odczytywać przełączników joysticka częściej niż raz na 10 ms. Wiele gier odczytuje przełączniki w czasie przerwania zegarowego 55 ms. Na przykład przypuśćmy, że nasze przerwanie zegarowe odczytuje przełączniki i przechowuje wynik w zmiennej pamięciowej. Główna aplikacja, kiedy chcemy odpalić broń, sprawdza tą zmienną. Jeśli jest ustawiona pogram główny czyści zmienną i odpala broń. 55 milisekund później, timer ustawia zmienną przycisku ponownie a program główny odpali ponownie ,kiedy po raz kolejny sprawdzi zmienną. Taki schemat całkowicie wyeliminowałby problemy z drganiem styków klawiszy. Powyższa technika rozwiązuje inny problem z przełącznikami: śledzenia kiedy przycisk ognia jest w dole .Pamiętajmy, kiedy odczytujemy przełączniki, bity które powróciły mówią nam , że przełącznik jest aktualnie w dole. Nie mówią nam, że przycisk został naciśnięty. Musimy sami to wyśledzić. Jednym z łatwych sposobów wykrycia kiedy użytkownik pierwszy raz nacisnął jest zachowanie poprzedniego odczytu przełącznika i porównanie go z bieżącym odczytem. Jeśli różnią się a bieżący odczyt wskazuje ,że przełącznik jest w dole wtedy jest nowa pozycja dolna. 24.5 STANDARDOWY INTERFEJS URZĄDZENIA GIER (SGDI) Standardowy Interfejs Urządzenia Gier (SGDI) jest specyfikacją dla usługi 15h, która pozwala nam odczytać przypadkową liczbę potencjometrów i joysticków. Napisanie SGDI zgodnych aplikacji jest łatwe i pomaga uczynić nasze aplikacje zgodne ze sterownikami gier, które dostarczają zgodności z SGDI. Poprzez napisanie aplikacji używających SGDI API możemy zapewnić ,że nasze aplikacje będą działały z przyszłymi sterownikami, które dostarczą poszerzonych możliwości SGDI. Rozumiejąc siłę i rozszerzalność SGDI, musimy przyjrzeć się interfejsowi programowemu aplikacji (API) dla SGDI 24.5.1 INTERFEJS PROGRAMOWY APLIKACJI (API) Interfejs SGDI rozszerza BIOS joysticka PC o API int 15h. Wykonamy funkcje SGDI poprzez załadowanie rejestru ah 80x86 84h a dx właściwym kodem funkcji SGDI a potem wykonując instrukcję int 15h. Interfejs SGDI po prostu rozszerza funkcjonalność wbudowanych podprogramów BIOS. Zauważmy, że i program, który wywołuje standardowe podprogramy joysticka BIOS będzie działał ze sterownikiem SGDI. Poniższa tablica pokazuje funkcje SGDI: DH 00
Wejście dl =0
00
dl = 1
01
dl = pot #
02
dl =0 al. = maska potencjometru
03
dl = pot # al. = minimum bx = maximum
Wyjście al – odczyt przełącznika
ax – pot 0 bx – pot 1 cx – pot 2 dx – pot 3 al. = odczyt potencjometru al. = pot 0 ah = pot 1 dl = pot 2 dh = pot 3
Opis Read4Sw. Jest to standardowa podfunkcja BIOS wywołana zerem. Odczytuje stan pierwszych czterech przełączników i zwraca ich wartości górnych czterech bitów rejestru al. Read4Pot. Standardowa podfunkcja BIOS wywoływana jedynką. Odczytuje wszystkie cztery potencjometry (jednocześnie) a zwraca niezmodyfikowanych wartości w ax, bx, cx i dx jako specyfikację BIOS ReadPot. Funkcja ta odczytuje potencjometr i zawraca znormalizowany odczyt w zakresie 0.255 Read4. Podprogram ten odczytuje cztery potencjometry w standardowych kartach rozszerzeń gier podobnie jak powyższa funkcja Read4Pots. Jednakże ten podprogram normalizuje cztery wartości z zakresu 0..255 i zwraca te wartości w al., ah, dl i dh Na wejściu rejestr al. zawiera „maskę potencjometru”, której możemy użyć do wyboru, który z czterech potencjometrów ten podprogram aktualnie odczytuje. Kalibracja. Funkcja ta kalibruje potencjometry dla tych funkcji, które zwracają znormalizowane wartości. Musimy skalibrować potencjometry przed wywołaniem takiej funkcji potencjometru (ReadPot i Read4) Wartości wejściowe muszą być
cx = centralnie 04
dl = pot #
05
dl = pot #
08
dl = przełącznik#
09
al. = 0 jeśli nie skalibrowane, 1 – jeśli skalibrowane ax = wartość niezmodyfikowana ax = wartość przełącznika ax = wartości przełącznika
80h 81h
(ReadPot i Read4) Wartości wejściowe muszą być niezmodyfikowanymi odczytami potencjometru przez Read4Pots lub inną funkcję, które zwracają wartości niezmodyfikowane TestsPotCalibrate. Sprawdza aby zobaczyć czy określony potencjometr jest już skalibrowany. Zwraca stosowną wartość w al. oznaczającą stan kalibracji dla określonego potencjometru. ReadRaw. Odczytuje wartość niezmodyfikowaną dla określonego potencjometru. Możemy użyć tej funkcji do pobrania wartości niezmodyfikowanych wymaganych przez podprogram kalibracji. ReadSw. Odczyt określonego przełącznika i zwraca zero (przełącznik włączony) lub jeden (przełącznik w dole) w rejestrze ax Read16Sw. Funkcja ta pozwala aplikacji odczytu do 16 przełączników sterownika gier w czasie. Bit zero ax odpowiada przełącznika zero, bit 15 ax odpowiada przełącznikowi 15 Remove. Funkcja ta usuwa sterownik z pamięci. Aplikacja generalnie nie wykonują tej funkcji
TestPresence. Ten podprogram zwraca zero w rejestrze ax jeśli sterownik SGDI jest obecny w pamięci. Zwraca wartość w ax niezmienioną w przeciwnym razie (w szczególności, ah będzie jeszcze zawierał 84h) Tablica 87: Funkcje i API SGDI (int 15h, ah = 84h)
24.5.2 READ4SW Wejście: ah = 84h, dx =0 Jest to standardowa funkcja BIOS odczytu przełączników. Zwraca stan przełączników od zera do trzech w joysticku w górnych czterech bitów rejestru al. Bit cztery odpowiada przełącznikowi zero, bit pięć przełącznikowi jeden, bit sześć przełącznikowi dwa a bit siedem przełącznikowi trzy. Bit zero oznacza przełącznik w dole, bit jeden odpowiada przełącznikowi w górnej pozycji. Funkcja ta jest dostarczana dla kompatybilności z istniejącymi podprogramami joysticka BIOS. Do odczyt przełączników joysticka powinniśmy użyć funkcji Read16Sw opisanej później w tym dokumencie. 24.5.3 READ4POTS Wejście: ah = 84h, dx =1 Jest to standardowa funkcja odczytu potencjometrów BIOS. Odczytuje cztery potencjometry w standardowej karcie złącza gier i zwraca ich odczyt w rejestrach ax (oś x /potencjometr 0), bx (oś y / potencjometru 1), cx (potencjometr 2) i dx (potencjometru 3). Są to niezmodyfikowane, nieskalibrowane odczyty potencjometrów , których wartości będą się różnić od maszyny do maszyny i zastosowanej karty I/O/ .Ta funkcja jest dostarczona dla kompatybilności z istniejącymi podprogramami joysticka BIOS. Do odczytu potencjometrów powinniśmy użyć podprogramów ReadPot, Read4 lub ReadRaw opisanych w następnych kilku sekcjach. 24.5.4 READPOT Wejście: ah =84h, dh =1, dl = numer potencjometru Odczytuje określony potencjometr i zwraca znormalizowaną wartość potencjometru w zakresie 0...255 w rejestrze al. Podprogram ten ustawia również ah na zero. Chociaż standard SGDI uwzględnia do 255 różnych potencjometrów, większość rozszerzeń tylko wspiera potencjometry zero, jeden, dwa i trzy. Jeśli spróbujemy odczytać potencjometr nie uwzględniony funkcja ta zwróci zero w ah. Ponieważ wartości są znormalizowane, funkcja ta zwraca porównywalne wartości dla danych ustawień sterownika gier bez względu na maszynę, częstotliwość zegara lub karty gier I/O. Na przykład odczyt 128 odpowiadający (odpowiednio) ustawieniu centralnemu na prawie każdej maszynie. Aby osiągnąć wyniki znormalizowane, musimy skalibrować dany potencjometr przed wykonaniem tej funkcji. Zobacz podprogram CalibratePot po więcej szczegółów. 24.5.5 RAED4
Wejście: ah = 84h, al. = maska potencjometru, dx =0200h Podprogram ten odczytuje cztery potencjometry na karcie rozszerzeń gier, podobnie jak funkcja BIOS (Read4Pots). Jednakże, zwraca wartości znormalizowane w al. (oś x / potencjometr 0), ah (oś y / potencjometr 1), dl (potencjometr 2) i dh (potencjometr 3). Ponieważ ten podprogram zwraca wartości znormalizowane pomiędzy zero i 255, musimy skalibrować potencjometry przed wywołaniem tego kodu. Rejestr al. zawiera wartość „maski potencjometru”. Najmniej znaczące cztery bity al. określają czy ten podprogram będzie rzeczywiście odczytywał każdy potencjometr. Jeśli bit zero, jeden, dwa i trzy są jedynkami, wtedy ta funkcja będzie odczytywała odpowiedni potencjometr; jeśli bity te są zerami, podprogram ten nie będzie odczytywał odpowiedniego potencjometru i zwróci zero do odpowiedniego rejestru. 24.5.6 CALIBRATEPOT Wejście: ah = 84h, dh = 3, dl = potencjometr #, al. = wartość minimalna, bx = wartość maksymalna cx= wartość centralna. Zanim spróbujemy odczytać potencjometr podprogramem ReadPot lub Read4, musimy skalibrować ten potencjometr. Jeśli odczytamy potencjometr bez uprzedniej kalibracji, sterownik SGDI zwróci tylko zero dla odczytu tego potencjometru. Do kalibracji potencjometru będziemy musieli odczytać wartość niezmodyfikowaną dla tego potencjometru w pozycji minimalnej, maksymalnej i centralnej. To musi być odczyt potencjometru niezmodyfikowany. Używamy odczytu uzyskanego z podprogramu Read4Pots. Teoretycznie musimy tylko skalibrować potencjometr tylko po załadowaniu sterownika SGDI. Jednakże temperatura fluktuacji i prąd analogowych obwodów elektrycznych mogą zdekalibrować potencjometr po istotny zastosowaniu. Dlatego też powinniśmy zdekalibrować potencjometry zmierzając do odczytu za każdym razem, kiedy użytkownik uruchamia swoją aplikację. Co więcej powinniśmy dać użytkownikowi opcję rekalibrację potencjometrów wewnątrz naszego programu. 25.5.7 TESTPOTCALIBRAION Wejście: ah =84h, dh =4, dl =potencjometr # Podprogram ten zwraca zero lub jeden w ax, oznaczając odpowiednio nie skalibrowany lub skalibrowany. Możemy użyć tej funkcji aby zobaczyć czy potencjometr jaki zamierzamy użyć, był skalibrowany i możemy przeskoczyć fazę kalibracji. Proszę jednak odnotować komentarz o prądzie w poprzednim paragrafie.
24.5.8 READRAW Wejście: ah = 84h, dh =5, dl = potencjometr # Odczytuje określony potencjometr i zwraca niezmodyfikowaną (nie skalibrowaną) wartość w ax. Możemy użyć tego podprogramu dla uzyskania wartości minimalnej, centralnej i maksymalnej do zastosowania przy wywołaniu podprogramu kalibracji. 24.5.9 READSWITCH Wejście: ah = 84h, dh =8, dl = przełącznik # Ten podprogram odczytuje określony przełącznik i zwraca zero w ax jeśli przełącznik nie jest w dole. Zwraca jeden jeśli przełącznik jest w dole. Zauważmy, że ta wartość jest w przeciwieństwie do ustawień bitów zwracanych przez funkcję Read4Sw. Jeśli próbujemy odczytać numer przełącznika dla wejścia, które nie jest dostępne w bieżącym urządzeniu, sterownik SGDI zwróci zero (przełącznik w górze). Standardowe sterowniki gier wspierają tylko przełączniki od zera od trzech a większość joysticków dostarcza tylko dwóch przełączników Dlatego też, o ile chcemy połączyć naszą aplikację z określonym urządzeniem, nie powinniśmy używać innych przełączników niż zero lub jeden. 24.5.10 READ16SW Wejście: ah =84h, dh =9 Ten podprogram SGDI odczytuje do szesnastu przełączników w pojedynczym wywołaniu. Zwraca wektora bitu w rejestrze ax z bitem zero odpowiadającym przełącznikowi zero, bit jeden odpowiada
przełącznikowi jeden itd. Jedynki oznaczają przełączniki w dole a zera oznaczają przełączniki nie w dole. Ponieważ standardowe złącze gier wspiera tylko cztery przełączniki, tylko bity od zera do trzech al. zawierają znaczące dane (dla tych urządzeń). Wszystkie inne bity będą zawsze zawierały zero. Sterowniki SGDI dla joysticków CH Product’s Flightstick Pro i Thrustmaster będą zwracały bity dla całego zbioru dostępnych przełączników w tych urządzeniach. 24.5.11 REMOVE Wejście: ah =84h, dh =80h Funkcja ta próbuje usunąć sterownik SGDI z pamięci. Generalnie, tylko sam kod SGDI.EXE wywoływał by ten podprogram. Powinniśmy użyć podprogram TestPresence (opisany poniżej) aby zobaczyć czy sterownik był w rzeczywistości usunięty z pamięci przez tą funkcję. 24.5.12 TESTPRESENCE Wejście: ah =84h, dh =81h Jeśli sterownik SGDI jest obecny w pamięci, podprogram ten zwróci ax =0 a wskaźnik do ciągu identyfikującego w es:bx. Jeśli sterownik SGDI nie jest obecny, funkcja ta będzie zwracała ax niezmienione. 24.5.13 STEROWNIK SGDI DLA STANDARDOWEJ KARTY ROZSZERZEŃ GIER Jeśli piszemy program wykorzystujący funkcje SGDI, odkryjemy ,że funkcja TestPresence będzie prawdopodobnie zwracała „nieobecność” kiedy nasz program poszukuje obecnego sterownika SGDI w pamięci. Kod asemblerowy który pojawia się na końcu tej sekcji dostarcza pełnej funkcjonalności, bezpłatnych sterowników SGDI dla standardowej karty rozszerzeń gier (kolejna sekcja przedstawia sterownik SGDI dla CH Products Flightstick Pro). Pozwala to nam napisać naszą aplikację korzystającą tylko z funkcji SGDI. Przez wsparcie TSR SGDI z naszym produktem, nasz odbiorca może użyć naszego oprogramowania ze standardowymi joystickami. Później, jeśli nabędzie określone urządzenie ze swoim własnym sterownikiem SGDI, nasze oprogramowanie będzie automatycznie działał z tym sterownikiem nie zmieniając naszego oprogramowania. Jeśli nie chcemy aby użytkownik uruchamiał TSR przed naszą aplikacją, możemy zawsze wprowadzić poniższy kod wewnątrz naszego kodu programu i aktywować go jeśli funkcja SGDI TestPresence określi ,że żaden inny sterownik SGDI nie jest obecny w pamięci kiedy startujemy nasz program Tu mamy kompletny kod dla sterownika SGDI standardowego złącza gier: .286 page name title subttl
58, 132 SGDI SGDI Driver for Standard Game Adapter Card Ten program jest Public Domain
; SGDI.EXE ; ; Usage: ; SDGI ; ; Program ten ładuje TSR, który aktualizuje INT 15 więc przypadkowy program może odczytać joystick ; w przenośny sposób. ; ; Musimy załadować cseg w pamięci przed każdym innymi segmentami! cseg cseg
segment para public ‘code’ ends
; Kod inicjalizujący, którego nie potrzebujemy z wyjątkiem początkowego ładowania, idzie w poniższym ; segmencie: ; Initialize
segment para public ‘INIT’
;Iinitialize
ends
; Podprogramy Biblioteki Standardowej ,które zostaną pokazane później .xlist include stdlib.a includelib stdlib.lib .list sseg sseg
segment para stack ‘stack’ ends
zzzzzzseg zzzzzzseg
segment para public ‘zzzzzzseg’ ends
CSEG
segment para public ‘CODE’ assume cs:cseg, ds:nothing
wp byp
equ equ
Int15Vect PSP
dword 0 word ?
; Adresy portu dla typowego złącza joysticka: JoyPort JoyTrigger
equ equ
201h 201h
; Struktura danej przechowująca informacje o każdym potencjometrze (głownie do celów kalibracji i ; normalizacji) Pot PotMask DidCal min max center Pot
struc byte byte word word word ends
0 0 5000 0 0
;maska potencjometru dla sprzętu ;czy ten potencjometr jest skalibrowany? ;minimalna wartość potencjometru ;maksymalna wartość potencjometru ;wartość potencjometru po środku
; Zmienne dla każdego potencjometru. Musimy zainicjalizować maski więc maskujemy wszystkie bity z ; wyjątkiem przychodzącego bitu dla każdego potencjometru Pot0 Pot1 Pot2 Pot3
Pot Pot Pot Pot
<1> <2> <4> <8>
; IDString pobiera adres przekazywany z powrotem do kodu wywołującego w funkcji testpresence. Cztery ; bajty przed IDString muszą zawierać numer seryjny i bieżąca liczbę sterownika SerialNumber IDNumber IDString
byte 0,0,0 byte 0 byte “Standard SGDI Driver”, 0 byte “Public Domain Driver Written by Randall L. Hyde”, 0 ;=============================================================================== ; ; ReadPotsAH zawiera bit maski określający , który potencjometr będzie czytany. Bit 0 ma wartość jeden ; kiedy powinniśmy odczytać potencjometr 0, bit 1 jest jedynką jeśli powinniśmy odczytać ; potencjometr 1, bit 2 jest jedynką jeśli powinniśmy odczytać potencjometr 2, bit 3 jest jedynką
; jeśli powinniśmy odczytać potencjometr 3. Wszystkie inne bity powinny być zerami. ; ; Kod ten zwraca wartości potencjometrów w SI, BX, BP i DI dla potencjometrów 0,1,2 i 3 ; ReadPots proc near sub bp, bp mov si, bp mov di, bp mov bx, bp ; Oczekujemy na poprzedni sygnał kończący przed próbą odczytu tego potencjometru .Jest możliwe, że ostatni ; potencjometr jaki odczytaliśmy był bardzo krótki. Jednakże, wyzwolenie sygnału startowego timera działa dla ; wszystkich czterech potencjometrów .Kod ten kończy jeśli tylko skończy się czas bieżącego potencjometru ; Jeśli użytkownik bezpośrednio odczyta inny potencjometr, jest całkiem możliwe, że nowy czas potencjometru ; nie upłynie z poprzedniego odczytu. Poniższa pętla upewnia nas , że nie mierzymy czasu z poprzedniego ; odczytu. mov dx, JoyPort mov cx, 400h Wait4Clean in al, dx and al, 0Fh loopnz Wait4Clean ;Okay, odczytano potencjometry. Poniższy kod wyzwala chip timera 558 a potem umieszcza w pętli dopóki ; wszystkie cztery bity potencjometrów (zamaskowane maską potencjometru w AL.) nie staną się zerami. Za ; każdym razem podczas tej pętli jeśli jeden lub więcej z tych bitów zawiera zero, pętla ta zwiększa odpowiedni ; rejestr(-y) mov out mov mov PortReadLoop: in and jz shr adc shr adc shr adc shr adc loop
PotReadDone: ReadPots
and and and and ret endp
dx, JoyTrigger dx, al. dx, JoyPort cx, 1000h al, dx al, ah potReadDone al, 1 si, 0 al., 1 bx, 0 al., 1 bp, 0 al., 1 di, 0 PotReadLoop si, 0FFFh bx, 0FFFh bp, 0FFFh di, 0FFFh
;wyzwolenie potencjometru
;zwiększamy SI jeśli potencjometr 0 jest aktywny ;zwiększamy BX jeśli potencjometr 1 jest aktywny ;zwiększamy bp jeśli potencjometr 2 jest aktywny ;zwiększamy DI jeśli potencjometr 3 jest aktywny ;Jeśli dojdziemy do tego punktu, jeden lub więcej ;potencjometrów przekroczy czas oczekiwania ( ;dlatego, że zazwyczaj nie są podłączone. Rejestr ;zawiera 4000h, ustawiamy go na 0
;-------------------------------------------------------------------------------------------------------------------------------------; ; NormalizeBX zawiera wskaźnik do struktury potencjometru, AX zawiera wartość potencjometru. ; Normalizujemy tą wartość zgodnie ze skalibrowanym potencjometrem. ; ; Notka: DS. musi wskazywać cseg przed wywołaniem tego podprogramu
Normalize
assume ds.: cseg proc near push cx
; Na zdrowy rozum, upewniamy się czy proces kalibracji przeszedł okay cmp je
[bx].Pot.DidCal, 0 BadNorm
;czy potencjometr jest skalibrowany? ;jeśli nie wychodzimy
mov cmp jbe cmp jae
dx, [bx].Pot.Center dx, [bx].Pot.Min BadNorm dx, [bx].Pot.Max BadNorm
; wykonujemy sprawdzenie wartości minimalnej ;centralnej i maksymalnej aby upewnić się , że ; min < center
; Obcinamy wartość jeśli jest poza zakresem cmp ja mov
ax, [bx].Pot.Min MinOkay ax, [bx].Pot.Min
;jeśli wartość jest mniejsza niż wartość minimum ;ustawiamy na wartość minimalną
cmp jb mov
ax, [bx].Pot.Max MaxOkay ax, [bx].Pot.Max
;jeśli wartość jest większa niż wartość maksymalna ;ustawiamy wartość maksymalną
MinOkay:
MaxOkay: ; Wyskalujemy to wokół centrum: cmp jb
ax, [bx].Pot.Center Lower128
;zobaczymy czy jest mniejsza lub większa ;od wartości centralnej
;Okay, bieżący odczyt jest większy niż wartość centralna, skalujemy odczyt do zakresu 128...255: sub mov mov mov mov shr rcr mov sub jz div add cmp je mov jmp
ax, [bx].Pot.Center dl, ah ah, al. dh, 0 al, dh dl, 1 ax, 1 cx, [bx].Pot.Max cx, [bx].Pot.Center BadNorm cx ax, 128 ah, 0 NormDone ax, 0ffh NormDone
;mnożymy przez 128
;Zabezpieczenie przed dzieleniem przez zero ;obliczamy wartość znormalizowaną ;skalujemy zakres 128...255 ;wynik musi mieścić się w 8 bitach!
; Jeśli odczyt jest poniżej wartości centralnej, skalujemy ją do zakresu 0..127: Lower128:
sub mov mov mov mov
ax, [bx].Pot.Min dl, ah ah, al dh, 0 al, dh
shr rcr mov sub jz div cmp je mov jmp
dl, 1 ax, 1 cx, [bx].Pot.Center cx, [bx].Pot.Min BadNorm cx ah, 0 NormDone ax, 0ffh NormDone
;Jeśli coś poszło źle , zwracamy zero jako wartość znormalizowaną BadNorm:
sub
NormDone
pop cx ret endp assume ds:nothing
Normalize
ax, ax
;=============================================================================== ; Funkcje obsługi INT 15h ;=============================================================================== ; ; Chociaż są zdefiniowane jaki bliskie procedury, nie są w rzeczywistości procedurami. Kod MyInt15 skacze do ; każdej z nich z BX, daleki adres powrotu i flagi są usytuowane na stosie. Każdy z tych podprogramów musi ; obsłużyć właściwie stos. ;-------------------------------------------------------------------------------------------------------------------------------------; ;BIOS- Obsługuje dwie funkcje BIOS, DL =0 odczyt przełączników, DL =1 odczyt potencjometrów. Dla ; podprogramów BIOS, zignorujemy cooley switch i po prostu odczytujemy inne cztery przełączniki ; BIOS
proc cmp jb je
near dl, 1 Read4Sw ReadBIOSPots
;zobacz czy program przełącznika czy potencjometru
; Jeśli nie poprawna funkcja BIOS, skok do oryginalnego programu obsługi INT 15h i zezwolenie na zajęcie się ; tą funkcją pop jmp
bx cs: Int15Vect
;pozwólmy na obsłużenie go!
;BIOS odczytuje funkcję przełącznika Read4Sw:
push mov in and pop pop iret
dx dx, JoyPort al, dx al, 0F0h dx bx
;zwracamy tylko wartości przełączników
; Odczyt funkcji potencjometrów BIOS ReadBIOSPots: pop push
bx si
;wartość zwracana w BX!
BIOS
push push mov call mov mov mov pop pop pop iret endp
di bp ah, 0Fh ReadPots ax, si cx, bp dx, di bp di si
;odczyt wszystkich czterech potencjometrów ;BX już zawiera odczyt z potencjometru 1
;-------------------------------------------------------------------------------------------------------------------------------------; ReadPotNa wejściu DL zawiera numer potencjometru do odczytu. Odczyt i normalizacja tego ; potencjometru i zwracany wynik w AL. ; ReadPot ;;;;;;;;;;;;;;
assume proc push push push push push push push
ds.: cseg near bx ds. cx dx si di bp
;Już na stosie
mov bx, cseg mov ds., bx ; Jeśli dl = 0, odczyt i normalizacja wartości dla potencjometru 0, jeśli nie , próbujemy jakiś inny potencjometr cmp jne mov call lea mov call jmp
dl, 0 Try1 ah, Pot0.PotMask ReadPots bx, Pot0 ax, si Normalize GotPot
; Test dla DL =1 (odczyt i normalizacja potencjometru 1) Try1:
cmp jne mov call mov lea call jmp
dl, 1 Try2 ah, Pot1.PotMask ReadPots ax, bx bx, pot1 Normalize GotPot
;Test dla DL=2 (odczyt I normalizacja potencjometru 2) Try2:
cmp jne mov call
dl, 2 Try3 ah, Pot2.PotMask RaedPots
;pobranie bitu dla tego potencjometru ;odczyt potencjometru 0 ;wskaźnik do danego potencjometru ;pobranie odczytu potencjometru 0 ;normalizacja od 0 do FFh ;powrót do kodu wywołującego
lea mov call jmp
bx, Pot2 ax, bp Normalize GotPot
;Test dla DL=3 (odczyt I normalizacja potencjometru 3) Try3:
cmp dl, 3 jne BadPot mov ah, Pot3.PotMask call ReadPots lea bx, Pot3 mov ax, di call Normalize jmp GotPot ; Złą wartość w DL jeśli doszliśmy do tego miejsca. Standardowe złącze gier wspiera tylko cztery ; potencjometry. BadPot: GotPot:
ReadPot
sub pop pop pop pop pop pop pop iret endp assume
ax, ax bp di si dx cx ds. bx
;Niedostępny potencjometr, zwracane zero
ds.:nothing
;--------------------------------------------------------------------------------------------------------------------------------------; ; ReadRawNa wejściu DL zawiera numer potencjometru do odczytu. Odczytuje ten potencjometr i zwraca ; nieznormalizowany wynik w AX assume ds:cseg ReadRaw ;;;;;;;;;;;;;;;;
proc push push push push push push push
near bx ds. cx dx si di bp
mov mov
bx, cseg ds., bx
;Już na stosie
;kod ten jest prawie identyczny z kodem ReadPot. Jedyna różnica jest taka, że nie musimy martwić się ; normalizacją wyniku i (oczywiście) zwracamy wartość w AX zamiast w Al. cmp jne mov call mov jmp
dl, 0 Try1 ah, Pot0.PotMask ReadPots ax, si GotPot
Try1:
cmp jne mov call mov jmp
dl, 1 Try2 ah, Pot1.PotMask ReadPots ax, bx GotPot
Try2:
cmp je mov call mov jmp
dl, 2 Try3 ah., Pot2.PotMask ReadPots ax, bp GotPot
Try3:
cmp jne mov call mov jmp
dl, 3 BadPot ah, Pot3.PotMask ReadPots ax, di GotPot
BadPot: GotPot:
sub pop pop pop pop pop pop pop iret endp assume
ax, ax bp di si dx cx ds. bx
ReadRaw
;Potencjometr niedostępny, zwracane zero
ds:nothing
;-------------------------------------------------------------------------------------------------------------------------------------; Read4PotsOdczytuje potencjometr zero, jeden , dwa i trzy zwracając ich wartości w AL ,AH ,DL i DH ; ; Na wejściu, AL zawiera maskę potencjometru dla wyboru, który potencjometr powinniśmy ; odczytać (bit 0 =1 dla potencjometru 0, bit 1=1 dla potencjometru 1 itd.) ; Read4Pots ;;;;;;;;;;;;;;;;
proc push push push push push push
near bx ds. cx si di bp
mov mov mov call
dx, cseg ds., dx ah, al. ReadPots
push mov lea call
bx ax, si bx, Pot0 Normalize
;już na stosie
;zachowanie odczytu potencjometru 1 ;pobranie odczytu potencjometru 0 ;bx wskazuje na potencjometr 0 ;normalizacja
Read4Pots
mov
cl, al.
;zachowanie na później
pop lea call mov
ax bx, Pot1 Normalize ch, al.
;odzyskanie odczytu potencjometru 1
mov lea call mov
ax, bp bx, Pot2 Normalize dl, al.
;wartoϾ Pot2
mov lea call mov mov
ax, di bx, Pot3 Normalize dh, al. ax, cx
;wartość Pot3 ;potencjometr 0 I 1
pop pop pop pop pop pop iret endp
bp di si cx ds. bx
;zachowanie znormalizowanej wartości
;-------------------------------------------------------------------------------------------------------------------------------------; CalPotKalibracja potencjometru określonego przez DL. Na wejściu AL. zawiera minimalną ; wartość potencjometru (lepiej żeby była mniejsza niż 256!), BX zawiera maksymalną wartość ; potencjometru a CX zawiera centralną wartość potencjometru ; assume ds.:cseg CalPot
proc pop push push mov mov
near bx ds. si si, cseg ds., si
;odzyskanie wartości maksymalnej
; Sprawdzenie parametrów, sortowanie według rosnącego porządku:
GoodMax: GoodMin:
mov cmp ja xchg cmp jb xchg cmp jb xchg
ah, 0 bx, cx GoodMax bx, cx ax, cx GoodMin ax, cx cx, bx GoodCenter cx, bx
GoodCenter: ;Okay, domyślamy się co skalibrować; lea
si, Pot0
;upewnijmy się ,że center < max ;upewnijmy się, że min < center ;(notka: może okazać się center < max) ;ponownie pewność ,że center < max
DoCal:
CalDone: CalPot
cmp jb lea je lea cmp jb jne lea
dl, 1 DoCal si, Pot1 DoCal si, Pot2 dl, 3 DoCal CalDone si, Pot3
mov mov mov mov pop pop iret endp assume
[si].Pot.Min, ax [si].Pot.Max,bx [si].Pot.Center, cs [si[.Pot,DidDal, 1 si ds
;skok jeśli jest to potencjometr 0 ;skok jeśli jest to potencjometr 1 ;skok jeśli jest to potencjometr 2 ;skok jeśli nie jest to potencjometr 3 ; przechowujemy wartości minimum, maksimum i ; centrum.
ds:nothing
;-------------------------------------------------------------------------------------------------------------------------------------; TestCalSprawdzamy czy określony potencjometr przez DL został już kalibrowany ; assume ds.:cseg TestCal proc near ;;;;;;;;;;;;; push bx ;już na stosie push ds. mov bx, cseg mov ds., bx sub lea
ax, ax bx, Pot0
;zakładamy, że nie było kalibracji (również zero w AH) ;pobranie adresu określonej struktury danych
cmp jb lea je lea cmp jb jne lea
dl, 1 GetCal bx, Pot1 GetCal bx, Pot2 dl, 3 GetCal BadCal bx, Pot3
; do rejestru BX
mov pop pop iret endp assume
al., [bx].Pot.DidCal ds bx
potencjometru
GetCal: BadCal: TestCal
ds:nothing
;-------------------------------------------------------------------------------------------------------------------------------------; ; ReadSwOdczyt przełącznika, którego numer pojawia się w DL ReadSw ;;;;;;;;;;;;;;
proc push push
near bx cx
;już na stosie
NotDown: ReadSw
sub cmp ja
ax, ax dl, 3 NotDown
;założenie ,że brak takiego przełącznika ;zwracane jeśli numer przełącznika jest większa niż ; trzy
mov add mov in shr xor and pop pop iret endp
cl, dl cl, 4 dx, JoyPort al., dx al., cl al., 1 ax, 1 cx bx
;zachowanie przełącznika do odczytu ;przesunięcie z pozycji cztery do zero ;odczyt przełącznika ;przesuwa żądany bit przełącznika do bitu 0 ;odwrócenie w dół = 1 ;usunięcie innego niepotrzebnych bitów
;-------------------------------------------------------------------------------------------------------------------------------------; ; Read16SwOdczyt wszystkich czterech przełączników i zwrot ich wartości w AX Read16Sw proc near ;;;;;;;;;;;;;;;;;; push bx ;już na stosie mov dx, JoyPort in al., dx shr al., 4 xor al., 0Fh ;odwrócenie wszystkich przełączników and ax, 0Fh ;ustawienie pozostałych bitów na zero pop bx iret Read16Sw endp ;***************************************************************************************** *; ; MyInt15Aktualizacja podprogramu BIOS INT 15 dla sterowania odczytem joysticka ; MyInt15
proc push cmp je pop jmp
far bx ah, 84h DoJoystick bx cs: Int15Vect
DoJoystick:
mov mov cmp jae cmp jae shl jmp
bh, 0 bl, dh bl, 80h VendorCalls bx, JmpSize OtherInt15 bx, 1 wp cs: jmptable[bx]
jmptable
word word word word =
BIOS ReadPot, Read4Pots, CalPot, TestCal ReadRaw, OtherInt15, OtherU\Int15 ReadSw, Read16Sw ($-jmptable)/2
OtherInt15:
JmpSize
;kod joysticka
; Obsługa określonej funkcji VendorCalls:
je cmp je pop jmp
RemoveDriver bl, 81h TestPresence bx cs: Int15Vect
; TestPresence- Zwraca zero w AX I wskazuje na ID ciągu w ES:BX TestPresence:
pop sub mov mov lea iret
bx ax, ax bs, cseg es, bx bx, IDString
;pobranie starej wartości ze stosu
; RemoveDriver- Jeśli nie ma innych sterowników załadowanych po tym w pamięci, wyłącza go ; i usuwa z pamięci ;RemoveDriver: push push push push
ds. es ax dx
mov mov
dx, cseg ds., dx
;zobaczmy czy zaktualizowaliśmy ostatni podprogram INT 15h
CantRemove:
mov int cmp jne mov cmp jne
ax, 3515h 21h bx, offset MyInt15 CantRemove bx, es bx, wp seg MyInt15 CantRemove
mov mov push mov mov mov int pop mov int
ax, PSP es, ax es ax, es:[2ch] es, ax ah, 49h 21h es ah, 49h 21h
;zwolnienie pamięci
lds mov int
dx, Int15Vect ax, 2515h 21h
;przywrócenie poprzedniego wektora
pop pop pop pop pop
dx ax es ds bx
;najpierw zwalniamy blok środowiska
;teraz zwalniamy przestrzeń programu
MyInt15 cseg Initialize Main
iret endp ends segment para public ‘INIT’ assume cs: Initialize, ds:cseg proc mov ax, cseg mov es, ax mov es:PSP, ds. mov ds, ax
;pobranie wskaźnika do segmentu zmiennych ;zachowanie wartości PSP
mov ax, zzzzzzseg mov es, ax mov cx, 100h meminit2 print byte byte byte byte byte byte byte byte
„Standard Game Device Interface driver”, cr, lf “PC Compatible Game Adapter Cards “, cr, lf “Written by Randall Hyde”, cr, lf cr, lf cr, lf “’SGDI REMOVE’ usuwa sterownik z pamięci”, cr, lf lf 0
mov ax, 1 argv ;jeśli nie ma parametrów pusty ciąg stricmpl byte „REMOVE”, 0 jne NoRmv mov dh, 81h ;usuwanie opcodu mov ax, 84ffh int 15h ;zobaczmy czy wszystko już załadowane test ax, ax ;pobranie zera? jz Installed print byte „Sterownik SGDI nie jest obecny w pamięci, polecenie” byte „ REMOVE jest ignorowane”, cr, lf, 0 mov ax, 4c01h ;wyjście do DOS’a int 21h Installed:
NotRemoved:
mov mov int mov mov int cmp je print byte byte mov int print byte
ax, 8400h dh, 80h 15h ax, 8400h dh, 81h 15h ax, 0 NotRemoved
;funkcja usuwająca ;wywołanie TestPresence
„Usunięcie sterownika SGDI z pamięci zakończone powodzeniem” cr, lf, 0 ax, 4c00h ;wyjście do DOS 21h „Sterownik SGDI jest nadal obecny w pamięci.”, cr, lf,0
mov int
ax, 4c01h 21h
;wyjście do DOS
;Okay, aktualizujemy INT 15 NoRmv: mov int mov mov
ax, 3515h 21h wp Int15Vect, bx wp Int15Vect+2, es
mov mov mov mov int
dx, cseg ds, dx dx, offset MyInt15 ax, 2515h 21h dx, cseg ds, dx dx, seg Initiazlize dx, ds:psp dx, 2 ax, 3100h 21h
Main
mov mov mov sub add mov int endp
Iniyialize
ends
seg
segment para stack ‘stack’ word 128 dup (0) word ? ends
endstk sseg zzzzzzseg zzzzzzseg
segment para public ‘zzzzzzseg’ byte 16 dup (0) ends end Main
Poniższy program wykonuje kilka różnych typów wywołań sterownika SGDI. Możemy użyć go do przetestowania SGDI TSR: .xlist include includelib .list
stdlib.a stdlib.lib
cseg
segment para public ‘code’ assume cs:cseg, ds:nothing
MinVal0 MinVal1 MaxVal0 MaxVal1
word word word word
;Wait4Button-
Oczekuje dopóki użytkownik nie naciśnie i zwolni przycisku
Wait4Button
proc push push
? ? ? ?
near ax dx
W4BLp:
Delay: W4nBLp:
Delay2:
Wait4Button Main
push
cx
mov mov int cmp je
ah, 84h dx, 900h 15h ax, 0 W4BLp
xor loop mov mov int cmp jne
cx, cx Delay ah, 84h dx, 900h 15h ax, 0 W4nBLp
loop pop pop pop ret endp
Delay2 cx dx ax
proc print byte byte byte
;odczyt mnij znaczących 16 przycisków ;jakiś przycisk w dole? Jeśli nie pętla dopóki jest ;opóźnienie pętli ;teraz czekamy aż użytkownik zwolni wszystkie przyciski
“SGDI Test Program”, cr, lf “Written by Randal Hyde”, cr,lf,lf “Press any key to continue”, cr, lf,0
getc mov mov int cmp je print byte jmp MainLoop0:
print byte
ah, 84h dh, 4 15h ax, 0 MainLoop0
;funkcja Testpresence
“Żaden sterownik SGDI nie jest obecny w pamięci.”,cr,lf,0 Quit „BIOS” 0
;Okay, odczytujemy przełączniki i niezmodyfikowane wartości potencjometrów używając kompatybilnej funkcji ; BIOS mov mov int puth mov putc mov mov int putw mov putc
ah, 84h dx, 0 15h
;kompatybilny z BIOS odczyt przełączników ;wartość wyjściowa przełącznika
al., ‘ ‘ ah, 84h dx, 1 15h al., ‘ ‘
;kompatybilny z BIOS odczyt potencjometrów
mov putw mov putc mov putw mov putc mov putw putcr mov int je getc
ax, bx al., ‘ ‘ ax, cx al., ‘ ‘ ax, dx
ah, 1 16h MainLoop0
;powtarzanie pętli dopóki naciśnięty klawisz
; Odczyt wartości minimalnej i maksymalnej dla każdego potencjometru od użytkownika, więc można ; skalibrować potencjometry print byte byte byte
cr, lf, lf, lf “Przesuwamy joystick w lewy górny róg I naciskamy “ „ jakiś przycisk”,cr,lf,0
call mov mov int mov mov
Wait4Button ah, 84h dx, 1 15h MinVal0, ax MinVal1, bx
print byte byte byte
cr, lf „Przesuwamy joystick w prawy dolny róg „ „ i naciskamy jakiś przycisk”,cr,lf,0
call mov mov int
Wait4Button ah, 84h dx, 1 15h
mov mov
MaxVal0, ax MaxVal1, bx
;odczyt wartości niezmodyfikowanej
;odczyt wartości niezmodyfikowanej
; Kalibracja potencjometrów mov mov mov add shr mov mov int
ax, MinVal0 bx, MaxVal0 cx, bx cx, ax cx, 1 ah, 84h dx, 300h 15h
mov mov mov
ax, MinVal1 bx, MaxVal1 cx, bx
;będzie osiem bitów lub mniej ;obliczamy wartość centralną jako średnią ; z tych dwóch (jest to niebezpieczne, ale ;zazwyczaj działa!) ;kalibracja potencjometru 0 ;będzie osiem lub mnij bitów ;obliczamy wartość centralną jako średnią
MainLoop0:
add shr mov mov
cx, ax cx, 1 ah, 84h dx, 301h
print byte
„ReadSw:”, 0
;z tych dwóch (jest to niebezpieczne, ale ;zazwyczaj działa!) ;kalibrujemy potencjometr 1
;Okay, odczytujemy wartości przełączników i niezmodyfikowanych potencjometrów używając kompatybilnych ; funkcji BIOS mov mov int or putc
ah, 84h dx, 800h 15h al., ‘0’
mov mov int or putc
ah, 84h dx, 801h 15h al., ‘0’
mov mov int or putc
ah, 84h dx, 802h 15h al., ‘0’
mov mov int or putc
ah, 84h dx, 803h 15h al., ‘0’
mov mov int or putc
ah, 84h dx, 804h 15h al., ‘0’
mov mov int or putc
ah, 84h dx, 805h 15h al., ‘0’
mov mov int or putc
ah, 84h dx, 806h 15h al., ‘0’
mov mov int or putc mov
ah, 84h dx, 807h 15h al., ‘0’ al., ‘ ‘
;odczyt przełącznika zero
;odczyt przełącznika jeden
;odczyt przełącznika dwa
;odczyt przełącznika trzy
;odczyt przełącznika cztery
;odczyt przełącznika pięć
;odczyt przełącznika sześć
;odczyt przełącznika siedem ;nie będziemy martwić się ich większą ilością
putc mov mov int putw print byte mov mov int puth mov putc mov puth mov putc mov mov int putw putcr mov int je getc
ah, 84h dh, 9 15h
;odczyt wszystkich 16 przełączników
„ Potencjometry: „, 0 ax, 8403h dx, 200h 15h
;odczyt potencjometru joysticka ;odczyt czterech potencjometrów
al., ‘ ‘ al., ah al., ‘ ‘ ah, 84h dx, 503h 15h
ah, 1 16h MainLoop1
Quit: Main
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
;odczyt niezmodyfikowany, potencjometr 3
;powtarzanie pętli dopóki naciśnięty klawisz
24.6 STEROWNIK SGDI DLA CH PRODUCTS’ FLIGHT STICK PRO™ Joystick CH Products’ Flight Stick Pro jest dobrym przykładem specjalizowanego produktu dla którego sterownik SGDI jest doskonałym rozwiązaniem. Flight Stick Pro dostarcza trzech potencjometrów i pięciu przełączników, piąty przełącznik stanowi specjalną piątą pozycję cooley switch Chociaż potencjometry w Flight Stick Pro mapują do trzech wejść analogowych w standardowej karcie rozszerzeń gier (potencjometr zero, jeden i trzy), jest niewystarczająco wejść cyfrowych do obsługi ośmiu wejść koniecznych dla czterech przycisków i cooley switch Flight Stick Pro. Flight Stick Pro (FSP) używa kilku układów elektronicznych mapujących te osiem pozycji przełącznika do czterech bitów wejściowych. Robiąc to, określają jedno ograniczenie przy stosowaniu przełączników FSP – możemy tylko nacisnąć jeden w jednym czasie. Jeśli przetrzymujemy dwa lub więcej przełączników w tym samym czasie, FSP wybierze jeden z przełączników i zaraportuje tą wartość; zignoruje inne przełączniki dopóki nie zwolnimy przycisku. Ponieważ tylko jeden przełącznik może być odczytany w jednym czasie, FSP wygeneruje wartość czterech bitów, które określą aktualny stan przełączników. Zwraca te
cztery bity jako wartość przełączników w standardowej karcie rozszerzeń gier. Poniższa tablica pokazuje te wartości dla każdego z przełączników: Wartości (binarnie) 0000 0100 1000 1100 1110 1101 1011 0111 1111
Priorytet Najwyższy 7 6 5 4 3 2 Najniższy
Pozycja przełącznika Pozycja górna w cooley switch Prawa pozycja w cooley switch Dolna pozycja w cooley switch Lewa pozycja w cooley switch Wyzwolenie joysticka Najbardziej na lewo przycisk joysticka Najbardziej na prawo przycisk joysticka
Środkowy przycisk na joysticku Żaden przycisk aktualnie nie jest w dole
Tablica 88: Wartości zwraca przełącznika Flight Stick Pro Zauważmy, że przyciski wyglądają jak pojedyncze naciśnięcie przycisku. Pozycja Cooley switch zawiera wartość pozycji w bitach sześć i siedem; bity cztery i pięć zawsze zawierają zero, kiedy cooley switch jest aktywny. Sterownik SGDI dla Flight Stick Pro jest bardzo podobny do standardowej karty rozszerzeń gier sterownika SGDI. Ponieważ Flight Stick Pro dostarcza tylko trzech potencjometrów, kod ten nie zajmuje się próbami odczytu potencjometru 2 (nie istniejący) Oczywiście, przełączniki w FSP różnią się trochę od tych w joysticku standardowym, więc sterownik FSP SGDI mapuje przełączniki FSP do ośmiu logicznych przełączników SGDI. Poprzez odczyt przełączników zero do siedem, możemy przetestować warunki w FSP: Numer przełącznika SGDI: 0 1 2 3 4 5 6 7
Mapowanie do tego przełącznika FSP Wyzwolenie joysticka Lewy przycisk joysticka Środkowy przycisk joysticka Prawy przycisk joysticka Górna pozycja cooley Lewa pozycja cooley Prawa pozycja cooley Dolna pozycja cooley
Tablica 89: Mapowanie przełączników Flight Stick Pro Sterownik FSP SGDI zawiera jedną z nowoczesnych cech, pozwala użytkownikowi zamieniać lewego i prawego przełącznika w joysticku. Wiele gier często przydziela ważne funkcje do spustu i lewego przycisku ponieważ są łatwiejsze do naciśnięcia (prawo ręczny gracz może łatwo nacisnąć lewy przycisk swoim kciukiem). Poprzez wpisanie „LEFT” w lini poleceń, sterownik FSP SGDI zmienimy funkcje lewego i prawego przycisku, aby leworęczny gracz mógł łatwo aktywować tą funkcję kciukiem. Poniższy kod dostarcza kompletnego listingu dla sterownika FSP SGDI. Zauważmy, że możemy zastosować ten sam program testowy z poprzedniej sekcji dla przetestowania tego sterownika .286 page name title
58, 132 FSPSGDI FSPSGDI (CH Products Standard Game Device Interface)
;FSPGDI.EXE ; ; Użycie: ; FSPSGDI {LEFT} ; ; Program ten ładuje TSR, który aktualizuje INT 15 więc program może odczytać joystick CH Products Flight ; Stick w przenośny sposób
wp byp
equ equ
< word ptr > < byte ptr >
; Musimy załadować cseg do pamięci przed innymi segmentami! cseg cseg
segment para public ‘code’ ends
; Kod inicjalizujący Initialize Initialize
segment para public „INIT’ ends
; Podprogramy Biblioteki Standardowej UCR, które pojawią się później .xlist include includelib .list
stdlib.a stdlib.lib
sseg sseg
segemnt para stack ‘stack’ ends
zzzzzzseg zzzzzzseg
segment para public ‘zzzzzzseg’ ends
CSEG
segment para public ‘CODE’ assume cs:cseg, ds:nothing
Int15Vect dword 0 PSP word ? ; Adres portu dla typowego złącza joysticka: JoyPort JoyTrigger
equ equ
201h 201h
CurrentReading word
0
Pot PotMask DidCal min max center Pot
struct byte byte word word word ends
0 0 5000 0 0
Pot0 Pot1 Pot2
Pot Pot Pot
<1> <2> <8>
;maska potencjometru ;czy potencjometr skalibrowany? ;minimalna wartość potencjometru ;maksymalna wartość potencjometru ;wartość potencjometru na środku
; SwapButtons- 0 jeśli powinniśmy użyć normalnego przycisku flightstick pro, ; 1 jeśli powinniśmy zamienić lewy i prawy przycisk SwapButtons
byte
0
; SwBits ;
wartość wejściowa czterech bitów z FlightStick Pro wybierając jeden z poniższych wzorców dla danej pozycji przełącznika
SwBits
byte
10h
;Sw4
SwBitsL
byte byte byte byte byte byte byte
0 0 0 40h 0 0 4
;NA ;NA ;NA ;Sw6 ;NA ;NA ;Sw2
byte byte byte byte byte byte byte byte
80h 0 0 8 20h 2 1 0
;Sw7 ;NA ;NA ;Sw3 ;Sw5 ;Sw1 ;Sw0 ;NA
byte byte byte byte byte byte byte byte
10h 0 0 0 40h 0 0 4
;Sw4 ;NA ;NA ;NA ;Sw6 ;NA ;NA ;Sw2
byte byte byte byte byte byte
80h 0 0 2 20h 8
;Sw7 ;NA ;NA ;Sw3 ;Sw5 ;Sw1
byte byte
1 0
;Sw0 ;NA
; Adres IDString przekazuje ponownie do funkcji wywołującej wywołanie testpresence. Cztery bajty przed ; IDString muszą zawierać liczbę porządkową i bieżącą liczbę urządzenia SerialNumber IDNumber IDString
byte byte byte byte
0, 0, 0 0 “CH Products: FlightStick Pro”, 0 “Written by Randall Hyde” 0
;=============================================================================== ; ; ReadPotsAH zawiera bit maski określający , który potencjometr powinien być odczytany. Bit 0 jest ; jedynką jeśli powinniśmy odczytać potencjometr 0, bit 1 jest jedynką jeśli mamy odczytać ; potencjometr 1, bit 3 jest jedynką jeśli mamy odczytać potencjometr 3/ Wszystkie inne bity ; będą zawierały zero. ; ; Kod ten zwraca wartości potencjometrów w SI, BX, BP i DI dla potencjometrów 0,1,2 i 3 ; ReadPots proc near sub bp, bp mov si, bp mov di, bp mov bx, bp
;Oczekiwanie potencjometru na zakończenie ostatniej akcji:
Wait4Pots;
mov out mov in and loopnz
dx, JoyPort dx, al. cx, 400h al, dx al, 0Fh Wait4Pots
;wyzwolenie potencjometru
;Okay, odczyt potencjometrów:
PotReadLoop:
mov out mov mov in and jz shr adc shr adc shr adc loop
dx, JoyTrigger dx, al. dx, JoyPort cx, 8000h al, dx al, ah PotReadDone al, 1 si, 0 al, 1 bp, 0 al, 2 di, 0 PotReadLoop
;wyzwolenie potencjometru
PotReadDone: ReadPots
Ret endp
;-------------------------------------------------------------------------------------------------------------------------------------; ; NormalizeBX zawiera wskaźnik do struktury potencjometru, AX zawiera wartość potencjometru. ; Normalizujemy tą wartość zgodnie z kalibracją potencjometru. ; ; Notka: DS musi wskazywać cseg zanim wywołamy ten podprogram Normalize
assume ds:cseg proc near push cx
; Sprawdzamy aby upewnić się, że proces kalibracji przeszedł spokojnie cmp je mov cmp jbe cmp jae
[bx].Pot.DidCal, 0 BadNorm dx, [bx].Pot.Center dx, [bx].Pot.Min BadNorm dx,[bx].Pot.Max BadNorm
; Przycinamy tą wartość jeśli jest poza zakresem cmp ja mov
ax, [bx].Pot.Min MinOkay ax, [bx].Pot.Min
cmp
ax, [bx].Pot. Max
MinOkay:
jb mov
MaxOkay ax, [bx].Pot.Max
MaxOkay: ; Skalujemy centralnie: cmp jb
ax, [bx].Pot.Center Lower128
;Skalujemy w zakresie 128..255: sub mov mov mov mov shr rcr mov sub jz div add cmp je mov jmp
ax, [bx].Pot.Center dl, ah ah, al. dh, 0 al, dh dl, 1 ax, 1 cx, [bx].Pot.Max cx, [bx].Pot.Center BadNorm cx ax, 128 ah, 0 NormDone ax, 0ffh NormDone
;mnożenie przez 128
;zapobiegamy dzieleniu przez zero ;obliczamy wartość znormalizowaną ;skalujemy zakres 128.255 ;wynik musi się zmieścić w 8 bitach
;Skalujemy w zakresie 0..127: Lower128:
BadNorm: NormDone:
sub mov mov mov mov shr rcr mov sub jz div cmp je mov jmp
ax,[bx].Pot.Min dl, ah ah, al. dh, 0 al, dh dl, 1 ax, 1 cx, [bx].Pot.Center cx, [bx].Pot.Min BadNorm cx ax, 0 NormDone ax, 0ffh NormDone
;mnożenie przez 128
;obliczenie wartości normalizowanej ;wynik musi się zmienić w 8 bitach!
sub ax, ax pop cx Ret Normalize endp asume ds:nothing ;=============================================================================== ; Funkcja obsługi INT 15h ;=============================================================================== ; Chociaż jest to zdefiniowane jako procedury bliskie, nie są w rzeczywistości procedurami. Kod MyInt15 ; skacze do każdej z nich z BX, daleki adres powrotu i flag usytuowanych na stosie. Każdy z tych ; podprogramów musi właściwie obsłużyć stos. ;
;-------------------------------------------------------------------------------------------------------------------------------------; BIOSobsługuje dwie funkcje BIOS, DL=0 odczytującą przełączniki, DL=1 odczytującą ; potencjometry. Dla podprogramów BIOS zignorujemy cooley switch (the hat) i po prostu ; odczytamy pozostałe cztery przełączniki. BIOS
proc cmp jb je pop jmp
near dl, 1 Read4Sw ReadBIOSPots bx cs:Int15Vect
Read4sw:
push mov in shr mov mov cmp je mov jmp
dx dx, JoyPort al, dx al, 4 bl, al bh, 0 cs: SwapButtons, 0 DoLeft2 al, cs:SwBitsL[bx] SBDone
DoLeft2; SBDone:
mov rol not pop pop iret
al, cs:SwBits[bx] al, 4 al. dx bx
ReadBIOSPots: pop push push push mov call mov mov mov sub pop pop pop iret BIOS endp
bx si di bp ah, 0bh ReadPots ax, si bx, bp dx, di cx, cx bp di si
;zobacz czy podprogram przełącznika czy ;potencjometru
;odłożenie Sw0..3 w górnych bitach I czynimy 0=w dole ;podobnie jak złącze gier
;zwraca wartość w BX
;-------------------------------------------------------------------------------------------------------------------------------------; ; ReadPotNa wejściu, DL zawiera numer potencjometru do odczytu. Odczytujemy i normalizujemy ; potencjometr i zwracamy wynik w AL. ReadPot ;;;;;;;;;;;;;;
assume proc push push push push
ds:cseg near bx ds. cx dx
;już na stosie
push push push
si di bp
mov mov
bx, cseg ds., bx
cmp jne mov call lea mov call jmp
dl, 0 Try1 ah, Pot0.PotMask ReadPots bx, Pot0 ax, si Normalize GotPot
cmp jne mov call lea mov call jmp cmp jne mov call lea mov call jmp
dl, 1 Try3 ah, Pot1.PotMask ReadPots bx, Pot1 ax, bp Normalize GotPot dl, 3 BadPot ah, Pot3.PotMask ReadPots bx, Pot3 ax, di Normalize GotPot
BadPot:
sub
ax, ax
GotPot:
pop pop pop pop pop pop pop iret endp assume
bp di si dx cx ds bx
Try1:
Try3:
ReadPot
;Pytanie: czy powinniśmy to przekazać ;czy zwrócić zero
ds.:nothing
;-------------------------------------------------------------------------------------------------------------------------------------; ; ReadRawNa wejściu DL zawiera numer potencjometru od odczytu. Odczytuje ten potencjometr i zwraca ; nieznormalizowany wynik w AL. ReadRaw ;;;;;;;;;;;;;;;
assume proc push push push push push
ds.:cseg near bx ds. cx dx si
;już na stosie
Try1:
Try3:
BadPot: GotPot:
ReadRaw
push push
di bp
mov mov cmp jne mov call mov jmp
bx. Cseg ds., bx dl, 0 Try1 ah, Pot0.PotMask ReadPots ax, si GotPot
cmp jne mov call mov jmp cmp jne mov call mov jmp
dl, 1 Try3 ah, Pot1.PotMask ReadPots ax, bp GotPot dl, 3 BadPot ah, Pot3.Mask ReadPots ax, di GotPot
sub pop pop pop pop pop pop pop iret endp assume
ax, ax bp di si dx cx ds. bx
;zwracane zero
ds.:nothing
;-------------------------------------------------------------------------------------------------------------------------------------; Read4PotsOdczytuje potencjometry zero, jeden ,dwa I trzy zwracając ich wartości w AL, AH, DL I DH. ; Ponieważ Flighstick Pro nie ma zainstalowanego potencjometr u dwa zwraca zero tej sytuacji. Read4Pots ;;;;;;;;;;;;;;;;
proc push push push push push push
near bx ds. cx si di bp
mov mov mov call
dx, cseg ds., dx ah, 0bh ReadPots
mov lea call mov
ax, si bx, Pot0 Normalize cl, al.
;już na stosie
;odczyt potencjometru 0, 1 i 3
Read4Pots
mov lea call mov mov lea call mov mov mov
ax, bp bx, Pot1 Normalize ch, al ax, di bx, Pot3 Normalize dh, al ax, cx dl, 0
pop pop pop pop pop pop iret endp
bp di si cx ds. bx
;wartość potencjometru 3 ;potencjometry 0 I 1 ;potencjometr 2 nie istnieje
;-------------------------------------------------------------------------------------------------------------------------------------; CalPotKalibrujemy potencjometr określony przez DL. Na wejściu AL. Zawiera minimalną wartość ( ; niemniej niż 256!), BX zawiera wartość maksymalną, a CX zawiera wartość centralną. CalPot
assume proc pop push push mov mov
ds.:cseg near bx ds. si si, cseg ds., si
;odzyskanie wartości maksymalnej
; Sprawdzanie parametrów, sortowanie według rosnącego porządku:
GoodMax: GoodMin:
mov cmp ja xchg cmp jb xchg cmp jb xchg
ah, 0 bx, cx GoodMax bx, cx ax, cx GoodMin ax, cx cx, bx GoodCenter cx, bx
GoodCenter: ;Okay, wymyślimy , kto wspiera kalibrację
DoCal:
lea cmp jb lea je cmp jne lea
si, Pot0 dl , 1 DoCal si, Pot1 DoCal dl, 3 CalDone si, Pot3
mov
[si].Pot.min, ax
CalDone: CalPot
mov mov mov pop pop iret endp assume
[si].Pot.max, bx [si].Pot.center, cx [si].PotDidCal, 1 si ds ds:nothing
;-------------------------------------------------------------------------------------------------------------------------------------; TestCalsprawdzenie czy potencjometr określony w DL nie została już skalibrowany ; assume ds.:cseg TestCal proc near ;;;;;;;;;;;;; push bx ;już na stosie push ds. mov bx, cseg mov ds., bx
GetCal: BadCal: TestCal
sub lea cmp jb lea je cmp jne lea
ax, ax bx, Pot0 dl, 1 GetCal bx, Pot1 GetCal dl, 3 BadCal bx, Pot3
mov mov pop pop iret endp assume
al., [bx].Pot.DidCal ah, 0 ds bx
;zakładamy brak kalibracji
ds:nothing
;-------------------------------------------------------------------------------------------------------------------------------------; ; ReadSwodczyt przełącznika którego numer pojawia się w DL SwTable
byte byte
11100000b, 11010000b, 01110000b, 10110000b 00000000b, 11000000b, 01000000b, 10000000b
SwTableL
byte byte
11100000b, 10110000b, 01110000b, 11010000b 00000000b, 11000000b, 01000000b, 10000000b
ReadSw ;;;;;;;;;;;;
proc push mov mov mov in and cmp je cmp
near bx bl, dl bh, 0 dx, JoyPort al., dx al., 0f0h cs;SwapButtons, 0 DoLeft0 al, cs:SwTableL[bx]
;już na stosie ;zachowanie przełącznika do odczytu
DoLeft0: IsDown: NotDown: ReadSw
jne jmp
NotDown IsDown
cmp jne mov pop iret sub pop iret endp
al, cs:SwTable[bx] NotDown ax, 1 bx ax, ax bx
;--------------------------------------------------------------------------------------------------------------------------------------; ; Read16SwOdczyt wszystkich ośmiu przełączników I zwrot ich wartości w AX Read16Sw ;;;;;;;;;;;;;;;
proc push mov mov in shr mov mov cmp je mov jmp
near bx ah, 0 dx, JoyPort al., dx al., 4 bl, al. bh, 0 cs:SwapButtons, 0 DoLeft1 al, cs:SwBitsL[bx] R8Done
DoLeft1: R8Done:
mov pop iret endp
al, cs:SwBits[bx] bx
Read16Sw
;już na stosie
;***************************************************************************************** * ; ; MyInt15Aktualizacja podprogramu BIOS INT 15 sterującego odczytem joysticka ; MyInt 15 proc far push bx cmp ah, 84h ;kod joysticka? je DoJoystick OyherInt15: pop bx jmp cs:Int15Vect DoJoystick: mov bh, 0 mov bl, dh cmp bl, 80h jae VendorCalls cmp bx, JmpSize jae OtherInt15 shl bx, 1 jmp wp cs:jmptable[bx] jmptable word BIOS word ReadPot, Read4Pots, CalPot, TestCal word ReadRaw, OtherInt15, OtherInt15 word ReadSw, Read16Sw JmpSize = ($-jmptable)/2
;Obsługa określonej funkcji VendorCalls:
je cmp je pop jmp
RemoverDriver bl, 81h TestPresence bx cs: Int15Vect
;TestPresence-
zwraca zero w AX I wskazuje na ID ciągu w ES:BX
TestPresence:
pop sub mov mov lea iret
bx ax, ax bx, cseg es, bx bx, IDString
;pobranie starej wartości ze stosu
;RemoverDriver- Jeśli nie ma żadnych sterowników załadowanych po tym w pamięci, rozłączamy go i usuwamy ; z pamięci. RemoverDriver: push push push push
ds. es ax dx
mov mov
dx, cseg ds., dx
;zobaczmy czy ostatni podprogram zaktualizował INT 15h
CantRemove:
mov int cmp jne mov cmp jne
ax, 3515h 21h bx offset MyInt15h CantRemove bx, es bx, wp seg MyInt15 CantRemove
mov mov push mov mov mov int
ax, PSP es, ax es ax, es:[2ch] es, ax ah, 49h 21h
;zwolnienie pamięci
pop mov int
es ah, 49h 21h
;teraz zwalniamy przestrzeń programu
lds mov int
dx, Int15Vect ax, 2515h 21h
;przywrócenie poprzedniego wektora int
pop pop pop
dx ax es
;najpierw zwalniamy blok środowiska
MyInt15 cseg
pop pop iret endp ends
ds. bx
;Poniższy segment jest odrzucany jeśli ten kod staje się rezydentny Initialize Main
segment para public ‘INIT’ assume cs: Initialize, ds:cseg proc mov ax, cseg mov es, ax mov es: PSP, ds. mov ds., ax
;pobranie wskaźnika do segmentu zmiennych ;zachowanie PSP
mov ax, zzzzzzseg mov es, ax mov cx, 100h meminit2 print byte byte byte byte byte byte byte byte byte
„Standard Game Device Interface driver”, cr, lf “CH Products Flightstick Pro”, cr,lf “Written by Randal Hyde”, cr, lf cr, lf “ ‘FSPSGDI LEFT’ zamienia lewy i prawy przycisk dla „ „lewo ręcznych graczy”, cr, lf „ ‘FSPGDI REMOVE’ usuwa sterownik z pamięci” cr, lf, lf 0
mov ax, 1 argv ;jeśli nie ma parametrów, pusty ciąg stricmpl byte „LEFT”, 0 jne NoLEFT mov SwapButtons, 1 print byte „Lewy i prawy przycisk zmienione”,cr,lf,0 jmp SwappedLeft NoLEFT:
stricmpl byte „REMOVE”, 0 jne NoRmv mov dh, 81h mov ax, 84ffh int 15h ;sprawdzamy czy już nie załadowaliśmy test ax, ax jz Installed print byte “Sterownik SGDI nie jest obecny w pamięci, polecenie REMOVE „ byte „zignorowane.”, cr, lf, 0 mov ax, 4c01h ;wyjście do DOS’a int 21h
Installed:
mov mov int
ax, 8400h dh, 80h 15h
;funkcja usuwania
mov mov int cmp je print byte mov int NotRemoved:
print byte mov int
ax, 8400h dh, 81h 15h ax, 0 NotRemoved
;funkcja TestPresence
“Usunięcie sterownika SGDI z pamięci zakończone pomyślnie.”, cr, lf,0 ax, 4c01h ;wyjście do DOS’a 21h „Sterownik SGDI jest jeszcze obecny w pamięci.” ,cr, lf, 0 ax, 4c01h ;wyjście do DOS’a 21h
NoRmv: ;Okay, aktualizujemy INT 15 i pozostawiamy w pamięci SwappedLeft:
mov int mov mov
ax, 3515h 21h wp Int15Vect, bx wp Int15Vect+2, es
mov mov mov mov int
dx, cseg ds, dx dx, offset MyInt15 ax, 2515h 21h dx, cseg ds, dx dx, seg Initialize dx, ds:psp dx, 2 ax, 3100h 21h
Main
mov mov mov sub add mov int endp
Initialize
ends
sseg
segment para stack ‘ stack’ word 128 dup (0) word ? ends
endstk sseg zzzzzzseg zzzzzzseg
segment para public ‘zzzzzzseg’ byte 16 dup (0) ends end Main
24.7 POPRAWIANIE ISTNIEJĄCYCH GIER Być może nie jesteś gotowy do napisania gry za milion dolarów. Być może chciałbyś uczynić bardziej przyjemniejszą grę którą już posiadasz . Cóż ,sekcja ta dostarczy praktycznej aplikacji z półrezydentnym programem , który aktualizuje grę Lucas Arts Xwing (symulacja Star Wars ). Program aktualizuje grę Xwing wykorzystując specjalne cechy CH Products’ FlightStick Pro. W szczególności pozwala zastosować
potencjometr dławiący w FSP do sterowania szybkością pojazdów kosmicznych. oprogramować każdy z przycisków do czterech ciągów ośmioznakowych każdy.
Pozwala również
Opisując jak zaktualizować istniejącą grę, opiszemy krótko jak ta aktualizacja się rozwinęła. Łatka FSPXW została wywołana przez debugger Soft-ICE. Program ten pozwala nam ustawiać punkty przerwań gdziekolwiek w 80386 lub późniejszych procesorach w określonym porcie I/O. Ustawiając punkt przerwania pod adresem I/O 201h podczas działania xwing.exe, zatrzymujemy program Xwing, kiedy zdecydujemy się odczytać wejścia analogowe i przełączniki. Dizasemblując kod otaczający tworzący kompletny joystick a przycisk odczytuje podprogramy. PO zlokalizowaniu tego podprogramu, było łatwo dość napisać program do przeszukiwania pamięci dla kodu i aktualizacji w skokach kodu w programie FSPXW Zauważmy ,że oryginalny kod joysticka wewnątrz Xwing działa doskonale z FSP Jedynym powodem aktualizacji kodu joysticka jest to ,że nasz kod może odczytać dławik i podjąć stosowną akcję. Podprogram przycisku to zupełnie inna historia. Łatka FSPXW musi pobrać sterowanie z podprogramu przycisku Xwing ponieważ użytkownik FSPXW może chcieć przedefiniować przycisk rozpoznany przez Xwing dla innego celu. Dlatego też, ilekroć XWing wywołuje podprogram przycisku, sterowanie jest przekazywane do podprogramu przycisku wewnątrz FSPXW, który decyduje czy przekazać rzeczywistą informację z powrotem do Xwing lub udawać przycisk w górnej pozycji ponieważ przyciski te są predefiniowane dla innych funkcji. Domyślnie (chyba, że zmienimy kod źródłowy) przyciski są następująco oprogramowane:
Oprogramowanie cooley switcha demonstruje interesującą cechę łatki FSPXW: możemy oprogramować do czterech różnych ciągów na każdym przycisku. Pierwszy raz naciskając przycisk, FSPXW emituje pierwszy ciąg, za drugim razem naciskając przycisk emituje drugi ciąg, potem trzeci a w końcu czwarty. Jeśli ciąg jest pusty, FSPXW przeskakuje ciąg. Łatka FSPXW używa cooley switcha do wyboru widoku z kokpitu. Naciskając cooley switch do przodu, wyświetla się widok z przodu, cofając cooley switch do tyłu mamy widok z tyłu. Jednakże gra Xwing dostarcza trzech lewych i prawych widoków. Naciskając cooley switch w lewo lub prawo po raz pierwszy mamy widok pod kątem 45 stopni. Naciskając po raz drugi mamy widok pod katem 90 stopni. Naciskając w lewo lub w prawo po raz trzeci mamy widok pod kątem 135 stopni. Poniższy diagram pokazuje domyślne oprogramowanie w cooley switch:
Słowo ostrzeżenia dotyczące tej łatki: działa tylko z podstawową wersją gry Xwing. Nie wspiera modułów ponad standardowych (Imperial Pursuit, B-Wing, Tie Fighter, itp.) . Co więcej, łatka ta zakłada, że podstawowy kod Xwing nie zmienił się od lat. Może być tak, że nowsze wersje gry Xwing używają nowszych Podprogramów joysticka i k0d powiązany z tą aplikacją może nie być zdolny do rozpoznania i zaktualizowania tego nowego podprogramu. Łatka ta będzie wykrywała taką sytuację i nie zaktualizuje Xwing jeśli wystąpi taki przypadek Musimy mieć wystarczającą ilość wolnej pamięci RAM dla tej łatki, Xwing i wszystko inne załadowane do pamięci w tym samym czasie (dokładną ilość RAM potrzebną Xwing zależy od zainstalowanych cech, pełna instalacja systemu wymaga nie mniej niż 610 K). Bez dalszych wstępów, mamy tu kod FSPXW: .286 page name title subttl
58, 132 FSPXW FSPXW (Flightstick Pro driver dla XWING) Copyright (C ) 1994 Randall Hyde
; FSPXW.EXE ; ; Usage: ; FSPXW ; ; Program ten wykonuje program XWING.EXE i aktualizuje go do zastosowania z Flightstick Pro byp wp
textequ < byte ptr > textequ < word ptr >
cseg cseg
segment para publick ‘CODE’ ends
sseg sseg
segment para stack ‘STACK’ ends
zzzzzzseg zzzzzzseg
segment para public ‘zzzzzzseg’ ends
include includelib matchfuncs Installation Instalation CSEG
stdlib.a stdlib.lib
ifndef debug segment para public ‘Install’ ends endif segment para public ‘CODE’ assume cs:cseg, ds:nothing
; Wektor przerwania zegarowego Int1Cvect ;PSP-
dword ?
Prefiks Segmentu Programu. Musimy zwolnić pamięć zanim uruchomimy rzeczywisty program
PSP
word
0
;Program ładujący strukturę danych (dla DOS) ExecStruct
LoadSSSP LoadCSIP PgmName
word dword dword dword dword dword dword
0 CmdLine DfltFCB DfltFCB ? ? Pgm
;uzywamy rodzicielskiego bloku środowiska ;dla parametrów linii poleceń
;Zmienne dla potencjometru dławiącego. ; LastThrottle zawiera ostatnio wysłany znak (więc wysyłamy tylko jedną kopię) ;ThrtlCntDn zlicza liczbę razy wywołania podprogramu dławienia. LastThrottle ThrtlCntDn
byte byte
0 10
;ButtonMask;
używamy do maskowania oprogramowanych przycisków kiedy gra odczytuje rzeczywiste przyciski
ButtonMask
byte
0f0h
;Poniższe zmienne pozwalają użytkownikowi przeprogramować przyciski KeyRdf Ptrs ptr2 ptr3 ptr4 Index Cnt Pgmd KeyRdf
struct word word word word word word word ends
? ? ? ? ? ? ?
;pole PTRx wskazuje na cztery możliwe ciągi z 8 ;znakami każdy. Każdy przycisk naciskany jest ;cyklicznie przez te ciągi. ;Indeks do kolejnego ciągu wyjściowego ;Flag = 0 jeśli nie predefiniowane
; Kody Left są wyjściowe jeśli cooley switch jest naciśnięty w lewo. Zauważmy, że ciagi są w ; w rzeczywistości ciągami słów zakończonymi zerami. Left
KeyRdf
Left1 Left2 Left3 Left4
word word word word
‘7’, 0 ‘4’,0 ‘1’, 0 0
; Kody Right są wyjściowe jeśli cooley switch jest przesunięty w prawo Right Right1 Right2 Right3 Right4
KeyRdf word word word word
‘9’, 0 ‘6’, 0 ‘3’, 0 0
; Kody Up są wyjściowe jeśli cooley switch jest przesunięty w górę Up Up1 Up2 Up3 Up4
KeyRdf word word word word
‘8’, 0 0 0 0
;Kody DownKeys są wyjściowe jeśli cooley switch jest przesunięty w dół Down Down1 Down2 Down3 Down3
KeyRdf word word word word
‘2’, 0 0 0 0
; Kody Sw0 są wyjściowe jeśli użytkownik popchnie wyzwalanie.(Ten przełącznik nie jest redefiniowany ) Sw0 Sw01 Sw02 Sw03 Sw04
KeyRdf word word word word
0 0 0 0
; Kody Sw1 są wyjściowe jeśli użytkownik wciśnie Sw1 (lewy przycisk jeśli użytkownik nie zmienił ; lewego i prawego przycisku). Nie redefiniowany Sw1 Sw11 Sw12 Sw13 Sw14
KeyRdf word word word word
0 0 0 0
;Kody Sw2 są wyjściowe jeśli użytkownik naciśnie Sw2 (środkowy przycisk) Sw2 Sw21 Sw22 Sw23 Sw24
KeyRdf word word word word
‘w’, 0 0 0 0
;Kody Sw3 są wyjściowe jeśli użytkownik naciśnie Sw3 (prawy przycisk jeśli użytkownik nie zmienił lewego ; i prawego przycisku) Sw3
KeyRdf
Sw31 Sw32 Sw33 Sw34
word word word word
0 0 0 0
;Przycisk stanu przełącznika: CurSw LastSw
byte byte
0 0
;***************************************************************************************** *; Łatka FSPXW zaczyna się tutaj. Jest to część rezydentna w pamięci Tylko należy włożyć kod, który ma być ; obecny w czasie uruchamiania lub musi pozostać rezydentny po zwolnieniu pamięci. ;***************************************************************************************** * Main
proc mov mov mov
cs:PSP, ds ax, cseg ds., ax
;pobranie wskaźnika do segmentu zmiennych
;Pobranie aktualnego wektora przerwania INT 1Ch mov int mov mov
ax, 351ch 21h wp Int1Cvect, bx wp Int!cVect+2, es
:Poniższe wywołanie MEMINIT zakłada ,że nie występują błędy. Mov ax, zzzzzzseg mov es, ax mov cx, 1024/16 meminit2 ; Wykonujemy inicjalizację przed uruchomieniem gry. To są funkcje kodu inicjalizującego pobierającego kopię ; przed rzeczywistym uruchomieniem XWING call call call
far ptr ChkBIOS15 far ptr Identify far ptr Calibrate
; Jeśli jakiś przełącznik został oprogramowany, usuwamy ten przełącznik z ButtonMask mov cmp je and
al., 0f0h sw0.pgmd, 0 Sw0NotPgmd al., 0e0h
;zakładamy ,że wszystkie przyciski są OK
cmp je and
sw1.pgmd, 0 Sw1NotPgmd al., 0d0h
;usuwamy sw1
cmp je and
sw2.pgdm, 0 Sw2NotPgmd al, 0b0h
;usuwamy Sw2
;usuwamy sw0 ze współzawodnictwa
Sw0NotPgmd:
Sw1NotPgmd:
Sw2NotPgmd: cmp je and
sw3.pgdm, 0 Sw3NotPgmd al, 070h
mov
ButtonMask, al
;usuwmay sw3
Sw3NotPgmd: ;zachowujemy wynik jako maskę przycisku
; Teraz, zwalniamy pamięć z ZZZZZZSEG robiąc miejsce dla XWING. Notka: Absolutnie nie wywołujemy ; podprogramów Biblioteki Standardowej UCR w tym miejscu! (ExitPgm jest OK., jest to makro które ; wywołuje DOS). Zauważmy ,ze po wykonaniu tego kodu, żaden kod i dane z zzzzzzseg są poprawne. mov sub inc mov mov int jnc print byte byte jmp
bx, zzzzzzseg bx, PSP bx es, PSP ah, 4ah 21h GoodRealloc “Memory allocation error” c, lf,0 Quit
GoodRealloc: ;Teraz ładujemy program XWING do pamięci: mov mov mov lds mov int jc
bx, seg ExecStruct es, bx bx, offset ExecStruct dx, PgmName ax, 4b01h 21h Quit
;wskaźnik do rekordu programu ;ładowanie pgm ;jeśli błędnie załadowano plik
;Poszukiwanie kodu joysticka w pamięci: 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
;Łatanie kodu joysticka XWING: mov mov mov ;Znajdoanie kodu Button
byp ds.:[si], 09ah wp ds.:[si+1], offset ReadGame wp ds:[si+3], cs
;dalekie wywołanie
mov mov xor
si, zzzzzzseg ds, si si, si
mov mov mov mov call jc
di, cs es, di di, offset ReadSwCode cx, ButtonLength FindCode Quit
;Łatanie kodu przycisku mov mov mov mov
byp ds:[si], 9ah wp ds:[si+1], offset ReadButtons wp ds:[si+3], cs byp ds:[si+5], 90h
;NOP
: Aktualizacja naszego programu obsługi przerwania zegarowego: mov mov mov mov int
ax, 251ch dx, seg MyInt1C ds, dx dx, offset MyInt1C 21h
;Okay, zaczynamy uruchamianie programu XWING.EXE mov int mov mov mov mov mov mov jmp Quit:
Main
ah, 62h 21h ds., bx es, bx wp ds.:[10], offset Quit wp ds:[12], cs ss, wp cseg:LoadSSSP+2 sp, wp cseg:LoadCSIP dword ptr cseg:LoadCSIP
lds dx, cs:Int1Cvect mov ax, 251ch int 21h ExitPgm endp
;pobranie PSP
;przywrócenie wektora zegarowego
:***************************************************************************************** * ; ; ReadGamePodprogram wywoływany kiedy Xwing odczytuje joystick. Przy co dziesiątym wywołaniu ; będzie odczytywał potencjometru dławiącego i wysyła właściwy znak do bufora ; roboczego, jeśli to konieczne ReadGame
assume proc dec jne mov
ds.:nothing far cs: ThrtlCntDn SkipThrottle cs: ThrtlCntDn, 10
push push
ax bx
;robi to tylko co 10 raz ;XWING wywołuje podprogram joysticka
;nie musimy zachowywać bp, dx lub cx
push
di
;ponieważ XWING je zachowuje
mov mov int
ah,84h dx, 103h 15h
;odczyt potencjometru dławiącego
;Konwersja wartości zwracanych przez podprogram potencjometru do czterech znaków 0..63:”\”, 64..127:”[„ , ; 128..191: „]”, 192..255:, oznaczając odpowiednio zero, 1/3, 2/3 i pełna moc
SetPower:
SkipPIB SkipThrottle:
ReadGame RaedButtons
ReadButtons ; MyInt1Cjakieś ; MyInt1C
mov mov cmp jae mov cmp jae mov cmp jae mov cmp je mov call
dl, al. ax, “\” dl, 192 SetPower ax, „[„ dl, 128 SetPower ax, „]” dl, 64 SetPower ax, 8 al., cs:LastThrottle SkipPIB csL:LastThrottle, al PutIntoBuffer
pop pop pop neg neg sti ret endp
di bx ax bx di
assume ds: nothing proc far mov ah, 84h mov dx, 0 int 15h not al and al, ButtonMask ret endp
;zero mocy ; 1/3 mocy ;2/3 mocy ;BS, pełna moc
;XWING zwraca dane w tych rejestrach. ;aktualizujemy instrukcje NEG i STI
;wyłączamy pgmd przycisków
wywoływana co 1/18 sekundy. Odczytuje przełączniki i decyduje czy powinna zostawić znaki w buforze roboczym assume proc push push push push mov mov mov mov
ds.:cseg far ds ax bx dx ax, cseg ds, ax al, CurSw LastSw, al
mov mov int
dx, 900h ah, 84h 15h
mov xor jz and jz
CurSw, al. al, LastSw NoChanges al., CurSw NoChanges
;odczyt 8 przełączników
;zobacz czy są zmiany ;zobacz czy sw zszedł w dół
; Jeśli przełącznik był zszedł w dół ,wyjście stosownie ustawi kody klawiszy dla niego, jeśli ten klawisz jest ;aktywny. Zauważmy ,ze naciśnięcie * jakiegoś* klawisza zresetuje wszystkie inne indeksy klawiszy
SetSw0:
NoSw0:
SetSw1:
test jz cmp je mov mov mov mov mov mov mov mov mov mov mov add cmp jb mov mov call jmp
al., 1 NoSw0 Sw0.Pgmd, 0 NoChanges ax, 0 Left.Index, ax Right.Index , ax Up.Index, ax Down.Index, ax Sw1.Index, ax Sw2.Index, ax Sw3.Index, ax bx, Sw0.Index ax, Sw0.Index bx, Sw0.Ptrs[bx] ax, 2 ax, Sw0.Cnt SetSw0 ax, 0 Sw0.Index, ax PutStrInBuf NoChanges
test jz cmp je mov mov mov mov mov mov mov mov mov mov mov add cmp jb mov mov call jmp
al, 2 NoSw1 Sw1.Pgmd, 0 NoChanges ax, 0 Left.Index, ax Right.Index, ax Up.Index, ax Down.Index, ax Sw0.Index, ax Sw2.Index, ax Sw3.index, ax bx, Sw1.Index ax, Sw1.Index bx, Sw1.Ptrs[bx] ax, 2 ax, Sw1.Cnt SetSw1 ax, 0 Sw1.Index, ax PutStrInBuf NoChanges
;zobacz czy Sw0 (wyzwalacz) pchnięto
;reset indeksów klawiszy dla wszystkich klawiszy ;z wyjątkiem SW0
;sprawdzamy czy naciśnięto SW1
;reset indeksów klawiszy dla wszystkich ;klawiszy z wyjątkiem Sw1
NoSw1:
SetSw2: NoSw2:
SetSw3:
NoSw3:
test jz cmp je mov mov mov mov mov mov mov mov mov mov mov add cmp jb mov mov call jmp test jz cmp je mov mov mov mov mov mov mov mov mov mov mov add cmp jb mov mov call jmp
al, 4 NoSw2 Sw2.Pgmd, 0 NoChanges ax, 0 Left.Index, ax Right.Index, ax Up.Index, ax Down.Index, ax Sw0.Index, ax Sw2.Index, ax Sw3.index, ax bx, Sw2.Index ax, Sw2.Index bx, Sw2.Ptrs[bx] ax, 2 ax, Sw2.Cnt SetSw2 ax, 0 Sw2.Index, ax PutStrInBuf NoChanges al, 8h NoSw3 Sw3.Pgdm, 0 NoChanges ax, 0 Left.Index, ax Right.Index, ax Up.Index, ax Down.Index, ax Sw0.Index, ax Sw2.Index, ax Sw3.index, ax bx, Sw3.Index ax, Sw3.Index bx, Sw3.Ptrs[bx] ax, 2 ax, Sw3.Cnt SetSw3 ax, 0 Sw3.Index, ax PutStrInBuf NoChanges
test jz cmp je mov mov mov mov mov mov mov mov mov mov
al, 10h NoUp Up.Pgmd, 0 NoChanges ax, 0 Right.Index, ax Left.Index, ax Down.Index, ax Sw0.Index, ax Sw2.Index, ax Sw3.index, ax bx, Up.Index ax, Up.Index bx, Up.Ptrs[bx]
;zobacz czy Sw2 (środkowy sw jest naciśniety)
;reset indeksów klawiszy dla wszystkich ;klawiszy z wyjątkiem Sw2
;zobacz czy Sw3 (prawy sw) jest naciśnięty
;reset indeksów klawiszy dla wszystkich ;klawiszy z wyjątkiem Sw1
;zobacz czy Cooley został naciśnięty w górę
;reset wszystkich przycisków w górze
SetUp:
NoUp:
SetLeft:
NoLeft:
SetRight:
NoRight
add cmp jb mov mov call jmp
ax, 2 ax, Up.Cnt SetUp ax, 0 Up.Index, ax PutStrInBuf NoChanges
test jz cmp je mov mov mov mov mov mov mov mov mov mov add cmp jb mov mov call jmp
al, 20h NoLeft Left.Pgmd, 0 NoChanges ax, 0 Right.Index, ax Up.Index, ax Down.Index, ax Sw0.Index, ax Sw2.Index, ax Sw3.index, ax bx, Left.Index ax, Left.Index bx, Left.Ptrs[bx] ax, 2 ax, Left.Cnt SetLeft ax, 0 Left.Index, ax PutStrInBuf NoChanges
test jz cmp je mov mov mov mov mov mov mov mov mov mov add cmp jb mov mov call jmp
al, 40h ;zobacz czy Cooley został przesunięty w lewo NoRight Right.Pgmd, 0 NoChanges ax, 0 Left.Index, ax ;reset wszystkich klawiszy w Lewo Up.Index, ax Down.Index, ax Sw0.Index, ax Sw2.Index, ax Sw3.index, ax bx, Rightt.Index ax, Right.Index bx, Right.Ptrs[bx] ax, 2 ax, Right.Cnt SetRight ax, 0 Right.Index, ax PutStrInBuf NoChanges
test jz cmp je mov mov mov
al., 80h NoChanges Down.Pgmd, 0 NoChanges ax, 0 Left.Index, ax Up.Index, ax
;zobacz czy Cooley został przesunięty w lewo
;reset wszystkich klawiszy w Lewo
;zobacz czy Cooley został przesunięty w dół
;reset wszystkich klawiszy w Dó³
SetDown:
NoChanges:
MyInt1c
mov mov mov mov mov mov mov add cmp jb mov mov call
Right.Index, ax Sw0.Index, ax Sw2.Index, ax Sw3.index, ax bx, Down.Index ax, Down.Index bx, Down.Ptrs[bx] ax, 2 ax, Down.Cnt SetDown ax, 0 Down.Index, ax PutStrInBuf
pop pop pop pop jmp endp assume
dx bx ax ds cs: Int1Cvect ds:nothing
;PutStrInBuf;
BX zawiera ciąg słów zakończonych zerem. Przesyłamy każde słowo przez wywo³anie PutInBuffer
PutStrInBuf
proc push push mov test jz call add jmp
near ax bx ax, [bx] ax. Ax PutDone PutInBuffer bx, 2 PutLoop bx ax
PutStrInBuf
pop pop ret endp
;PutInBuffer-
znaki wyjściowe i kody znaków w AX do bufora roboczego
PutLoop:
PutDone:
KbdHead KbdTail KbdBuffer EndKbd Buffer PutInBuffer
assume equ equ equ equ equ proc push push mov mov pushf cli mov inc inc
ds.:nothing word ptr ds:[1ah] word ptr ds:[1ch] word ptr ds:[1eh] 3eh 1eh near ds bx bx, 40h ds, bx bx, KbdTail bx bx
;to jest region krytyczny! ;pobranie wskaźnika końca bufora roboczego ;i zrobienie miejsca na ten znak
NoWrap:
PIBDone:
PutInBuffer
cmp jb mov
bx, buffer+32 NoWrap bx, buffer
;fizyczny koniec bufora?
cmp je xchg mov popf pop pop ret endp
bx, KbdHead PIBDone KbdTail, bx ds.:[bx], ax
;przepełnienie bufora?
;zawinięcie z powrotem do 1eh jeśli koniec
;ustawiamy nowy, pobieramy stary wskaźnik ;dane wyjściowe AX do starej lokacji ;przywrócenie przerwań
bx ds.
;***************************************************************************************** *; ; FindCode: Na wejściu, ES:DI wskazuje jakiś kod w * tym *programie który pojawia się w grze ATP. DS.:SI ; wskazuje blok pamięci w grze XWING. FindCode przeszukuje całą pamięć aby znaleźć ; podejrzany kawałek kodu i zwraca w DS.:SI wskazujący początek tego kodu. Kod ten zakłada ; ,że * znajdzie * ten kod!. Zwraca wyzerowaną flagę przeniesienia jeśli 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
pop pop pop stc ret pop pop pop clc ret endp
dx bx ax
repe
FoundCode:
FindCode
cx si di FoundCode si dx CmpLoop si, 1000h ax, ds ah ds, ax ax, 9000h DoCmp
dx bx ax
;zachowujemy wskaźnik do kodu porównywanego ;zachowanie wskaźnika do początku ciągu ;zachowanie licznika
;***************************************************************************************** *; ; Podprogramy joysticka i przycisku, które pojawiają się w grze Xwing. Kod ten jest rzeczywistą daną , ; ponieważ kod aktualizujący INT 21h przeszukuje całą pamięć dla tego kodu po załadowaniu pliku z dysku. JoystickCode
JoystickCode EndJSC:
proc sti neg neg pop pop pop ret mov in mov not and jnz in endp
near bx di bp dx cx bp, bx al., dx bl, al. al al, ah $+11h al, dx
JoyLength
=
EndJSC-JoystickCode
ReadSwCode
proc mov in xor and endp
dx, 201h al, dx al, 0ffh ax, 0f0h
ButtonLength
=
EndRSC-ReadSwCode
cseg
=
ends
ReadSwCode EndRSC:
Segemnt Instalacji ;Przesunęliśmy to tu aby nie zajmowała zbyt dużo miejsca w przestrzeni rezydentnej części tej łatki DfltFCB CmdLine Pgm
byte byte byte Byte
3, „ „, 0,0,0,0,0 2, “ “, 0dh, 126 dup ( “ “) „XWING.EXE”, 0 128 dup (?)
;linia poleceń dla programu ;dla nazwy użytkownika
; ChkBIOS15-
Sprawdzamy aby zobaczyć czy sterownik INT 15 dla FSPro jest obecny w pamięci
ChkBIOS15
proc mov mov int mov strcmpl byte jne ret
NoDriverLoaded:
far ah, 84h dx, 8100h 15h di, bx “CH Products: Fightstick Pro”, 0 NoDriverLoaded
ChkBIOS15
print byte “Sterownik CH Products SGDI dla FlightStick Pro nie jest “ byte “załadowany do pamięci.”, cr, lf byte „Proszę uruchomić FSPSGDI przed uruchomieniem tego programu” byte cr, lf, 0 exitpgm endp
;***************************************************************************************** * ; ; IdentifyDrukowanie zapisanej wiadomości Identify
assume ds.:nothing proc far
;Drukujemy ciąg powitalny. Zauważmy ,że ciąg „VersionStr” będzie zmodyfikowany przez program ; „version.exe” za każdym razem, kiedy zasemblujemy ten kod print byte byte byte byte byte byte Identify
cr, lf,lf “X W I N G P A T C H” ,cr ,lf „CH Products Flightstick Pro”, cr, lf Copyright 1994, Randall Hyde”, cr, lf lf 0
ret endp
;***************************************************************************************** * ; ; Kalibrowanie d³awienia Calibrate
assume proc print byte byte byte byte
ds:nothing far cr, lf, lf “Kalibracja:”, cr, lf, lf „Move the throttle to one extreme and press any „ “button:’, 0
call mov mov int push
Wait4Button ah, 84h dx, 1h 15h dx
print byte byte byte call mov mov int pop mov
cr, lf „Move the throttle to the other extreme and press „ “any button:”, 0 Wait4Button ah, 84h dx, 1 15h bx ax, dx
;zachowanie odczytu potencjometru 3
cmp jb xchg mov sub shr add mov mov int ret endp
ax, bx RangeOkay ax, bx cx, bx cx, ax cx, 1 cx, ax ah, 84h dx, 303h 15h
proc mov mov int and cmp jne
near ah, 84h dx, 0 15h al., 0F0h al., 0F0h Wait4button
mov loop
cx, 0 Delay
Wait4Press:
mov int je getc
ah, 1 16h NoKbd
;zjadany znak z klawiatury który przychodzi ;i obsługa właściwa ctrl-C
NoKbd:
mov mov int and cmp je ret endp ends
ah, 84h dx, 0 15h al., 0F0h al.,0F0h Wait4Press
;teraz oczekujemy na naciśnięcie jakiegoś ;przycisku
RangeOkay:
Calibrate Wait4Button
Delay:
Wait4Button Instalation sseg endstk sseg zzzzzzseg Heap zzzzzzseg
;obliczanie wartości centralnej
;kalibracja potencjometru trzy
;najpierw czekamy aż wszystkie przyciski ;zostaną zwolnione
segment para stack ‘STACK’ word 256 dup (0) word ? ends segment para public ‘zzzzzzseg’ byte 1024 dup (0) ends end Main
24.8 PODSUMOANIE Karta złącza gier PC pozwala nam na podłączenie szerokiej gamy urządzeń wejściowych do naszego PC. Do urządzeń takich zaliczamy cyfrowe joysticki, paddles, joysticki analogowe, steering wheels,yokes i inne. Paddle dostarcza jednego stopnia swobody, joysticki dostarczają dwóch stopni swobody wzdłuż osi (X,Y). Steering wheels i yokes również dostarczają dwóch stopni swobody, chociaż są zaprojektowane dla innych typów gier. *”Typowe urządzenia gier”
Większość wejściowych urządzeń gier jest podłączonych do PC przez kartę złącza gier. Urządzenie to dostarcza do czterech cyfrowych (przełączników) wejść i czterech analogowych (rezystywnych) wejść. Urządzenie to pojawia się jako pojedyncza lokacja I/O w przestrzeni adresowej PC. Cztery z tych bitów pod tym portem odpowiada czterem przełącznikom, cztery z tych wejść dostarcza stanu impulsu zegarowego z chipu 558 dla wejścia analogowego. Przełączniki możemy odczytać bezpośrednio z portu; aby odczytać wejście analogowe musimy stworzyć pętlę czasową zliczającą jak długo pobierany jest impuls powiązany z poszczególnym urządzeniem idąc od wartości największej do najmniejszej *”Sprzętowe złącze gier” Oprogramowanie złącza gier byłoby prostym zadaniem z wyjątkiem tego, że będziemy pobierać różne odczyty dla tych samych pokrewnych pozycji potencjometrów z różnymi kartami złącza gier, urządzeniami wejściowymi gier, systemami komputerowymi i oprogramowaniem. Prawdziwą sztuczką przy programowaniu złącza gier jest stworzenie spójnych wyników bez względu na aktualnie używany sprzęt. Jeśli możemy żyć z niezmodyfikowanymi wartościami wejściowymi, BIOS dostarcza dwóch funkcji do odczytu przełączników i wejść analogowych. Jednakże jeśli musimy znormalizować wartości, prawdopodobnie będziemy musieli napisać swój własny kod. Jednak napisanie takiego kodu jest bardzo łatwe jeśli pamiętamy o podstawowych zasadach algebry. *”Użycie funkcji I/O gier BIOS” *”Pisanie własnego podprogramu gier I/O” Podobnie jak z innymi urządzeniami w PC, jest problem z bezpośrednim dostępem do sprzętowego złącza gier, taki kod nie działa ze sprzętem, który nie stosuje ściśle oryginalnych kryteriów projektowania PC. Wymyślne urządzenia wejściowe gier takie jak joystick Thrustmaster i CH Product’s FlightStick Pro będą wymagały napisania specjalnego sterownika programowego . Co więcej, nasz podstawowy kod joysticka może nawet nie działać z przyszłymi urządzeniami, nawet jeśli dostarczą one minimalnego zbioru cech kompatybilnych ze standardowymi urządzeniami gier. Niestety, usługi BIOS są bardzo wolne i niezbyt dobre., więc niewielu programistów wykonuje wywołania BIOS, pozwalając projektantom dostarcza wymiennych sterowników dla sowich urządzeń gier. Dla złagodzenia tego problemu rozdział ten przedstawia aplikację Standard Game Device Input z interfejsem programowalnym – zbiór funkcji specjalnie zaprojektowanych dla dostarczenia rozszerzalnego, przenośnego systemu dla urządzeń wejściowych. Bieżąca specyfikacja dostarcza do 256 cyfrowych i 256 analogowych urządzeń wejściowych i jest łatwe rozszerzenie obsługi urządzeń wyjściowych i innych urządzeń wejściowych. *”Standardowy Interfejs Urządzenia Gier(SGDI) *”Interfejs Programowy Aplikacji” Rozdział ten kończy się przykładem półrezydentnego programu, który wykonuje wywołanie SGDI. Pogram ten, który aktualizuje popularną grę Xwing, dostarcza pełnego wsparcia dla CH Product’s FlightStick Pro w Xwing. Program ten demonstruje wiele cech sterownika SGDI również dostarczając przykładu jak załatać dostępną komercyjną grę *”Aktualizacja istniejących gier”
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 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 PIĄTY: OPTYMALIACJA NASZYCH PROGRAMÓW
25.0 WSTĘP Ponieważ optymalizacja programu jest generalnie jednym z ostatnich kroków w projektowaniu oprogramowania, jest to tylko przymiarka do omówienia optymalizacji programu w ostatni rozdziale tej książki. Przeszukując inne teksty, które omawiają ten temat, znajdziemy szeroką gamę opinii na ten temat. Niektóre teksty i artykuły ignorują zbiór instrukcji całkowicie i koncentrują się na znajdowaniu lepszych algorytmów. Inne dokumenty zakładają, że już znaleziono najlepszy algorytm i omawiają sposoby wybierania „najlepszej” sekwencji instrukcji realizujących tą pracę. Inne rozważają architekturę CPU i opisują jak „liczyć cykle” i pary instrukcji (zwłaszcza na procesorach superskalarnych lub procesorach z potokami) tworząc szybciej wykonujący się kod. Inne jeszcze rozpatrują architekturę systemu, nie tylko CPU, kiedy próbują zdecydować jak optymalizować mamy nasz program. Niektórzy autorzy spędzają dużo czasu wyjaśniając, że ich metoda jest „jedynie słuszną” dla przyspieszenia programu. Inni jeszcze odbiegają od inżynierii oprogramowania i zaczynają mówić o tym jak czas spędzony nad optymalizacja programu nie jest wart tego z różnych powodów. Cóż, rozdział ten nie ma zamiaru przedstawiać „jedynie słusznego sposobu”, ani też spędzać wiele czasu na sprzeczaniu się o pewne techniki optymalizacji. Po prostu przedstawi kilka przykładów, opcji i sugestii. Ponieważ jesteś sobą po tym rozdziale, czas na Ciebie abyś zaczął podejmować samodzielne decyzje. Miejmy nadzieję, że rozdział ten dostarczy stosownych informacji dal podejmowania poprawnych decyzji. 25.1 KIEDY OPTYMALIZOWAĆ,KIEDY NIE OPTYMALIZOWAĆ Proces optymalizacji nie jest tandetny Jeśli projektujemy program a potem stwierdzimy, że jest zbyt wolny, może będziemy musieli przeprojektować i napisać na nowo główną część tego programu dal uzyskania akceptowalnej wydajności. W oparciu o powyższe świat dzieli się na dwa obozy – tych , którzy optymizują wcześnie i tych , którzy optymalizują później. Obie grupy mają dobre argumenty; obie grupy mają jakieś złe argumenty. Przyjrzymy się obu stronom tych argumentów. Grupa „optymalizujących później” (OL) używa argumentu 90/10: 90% czasu wykonywania programu jest spędzanych w 10% kodu. Jeśli próbujemy optymalizować każdy kawałek kodu jaki napisaliśmy (to jest, optymalizujemy kod zanim dowiemy się ,że musi być zoptymalizowany), 90% wysiłku włożonego pójdzie na marne. Z drugiej strony, jeśli najpierw piszemy kod w zwykły sposób a potem idziemy w optymalizację, możemy poprawić wydajność naszego programu przy mniejszym wkładzie pracy. W końcu jeśli zupełnie usuniemy 90% naszego programu, nasz kod będzie działał tylko 10% szybciej. Z drugiej strony, jeśli zupełnie usuniemy te 10%, program nasz będzie działał 10 razy szybciej Matematyk oczywiście przemawia za tym aby zająć się tymi 10%. Grupa OL twierdzi, że powinniśmy napisać kod zwracając tylko uwagę na wydajność (tj. dać wybór pomiędzy algorytmem O(n2) a O(n lg n), wybierając ten drugi). Ponieważ program działa poprawnie możemy wrócić i skoncentrować wysiłek na tych 10% kodu, który zabiera cały czas. Argumenty OL są przekonywujące. Optymalizacja jest żmudnym i trudnym problemem. Najczęściej nie ma jasnego sposobu przyspieszenia sekcji kodu. Jedyny sposób określa, która z różnych opcji jest lepszym w rzeczywistości kodem i porównać je. Próba zrobienia tego na wejściu programu jest niepraktyczne. Jednakże, jeśli znajdziemy te 10% kodu i zoptymalizujemy go, zredukujemy nasz e obciążenie o 90% istotnie bardzo kusząc .Innym dobrym argumentem grupy )L jest to ,że niewielu programistów jest w stanie określić gdzie najwięcej czasu jest spędzanych w programie. Dlatego też jedynym rzeczywistym sposobem określenia gdzie program spędza czas jest oprzyrządowanie go i pomierzenie, które funkcje zabierają większość czasu. Oczywiście musimy mieć działający program zanim to zrobimy. Ponownie, argumentują, że czas spędzony nad
optymalizacją kodu z wyprzedzeniem jest marnotrawstwem ponieważ prawdopodobnie skończymy na optymalizacji tych 90%, które nie zmieniają niczego. Są jednak bardzo dobre kontr argumenty do powyższych. Po pierwsze, kiedy większość OL zaczyna mówić o zasadzie 90/10, jest to bezgraniczna sugestia, że te 10% kodu pojawi się jako jeden duży fragment w środku naszego programu .Dobry programista, podobnie jak dobry chirurg, może zlokalizować tą złośliwą masę, wyciąć ją i zastąpić czymś dużo szybszym, a zatem zwiększyć szybkość programu niewielkim wysiłkiem. Niestety, nie jest to częsty przypadek w świecie rzeczywistym. W prawdziwych programach, te 10% kodu , które zabiera 90% czasu wykonania jest często porozrzucany po całym naszym programie. Będziemy mieli 1% tu, 0,5% tam, „gigantyczne” 2,5% w jednej funkcji i tak dalej. Gorzej jeszcze, optymalizacja 1% kodu wewnątrz jednej funkcji często wymaga aby zmodyfikować również jakiś inny kod. Na przykład, przepisując funkcję (1%), przyspieszając ją trochę, wymagana jest zmiana sposobu przekazywania parametrów do tej funkcji. Może to wymagać przepisania kilku sekcji kodu na zewnątrz, tych wolnych 10%. Wiec często kończy się na przepisaniu dużo więcej niż tylko 10% kodu aby przyspieszyć te 10 % zajmujące 90% czasu. Inny problem z zasadą 90/10 jest taki, że dział na procentach, i zmienia procenty podczas optymalizacji. Na przykład przypuśćmy, że zlokalizowaliśmy funkcję pożerającą 90% czasu wykonania. Przypuśćmy, że Pan Super Programista i ty daliście sobie radę z przyspieszeniem tego podprogramu dwukrotnie. Nasz program pobierać bezie około 55% czasu wykonania przed optymalizacją. Jeśli potroimy szybkość podprogramu , program pobiera 40% całkowitego czasu wykonania. Jeśli spowodujemy ,że ta funkcja będzie działała dziewięć razy szybciej , program działał będzie teraz w 20% czasu oryginalnego, tj. pięć razy szybciej. Przypuśćmy, że możemy pobrać funkcję działającą dziewięć razy szybciej. Załóżmy, że zasada 90/10 już nie ma zastosowania do naszego programu. 50% czasu wykonania jest spędzanych w 10% kodu, 50% w pozostałych 90% kodu. I jeśli poradziliśmy sobie z przyspieszeniem tej jednej funkcji 0 900%, jest bardzo nieprawdopodobne abyśmy musieli wykluczyć dużo więcej z niego. Czy jest warte zachodu nie traktowanie poważnie tych pozostałych 90% kodu? Zakładamy ,że jest. W końcu, możemy poprawić wydajność naszego programu o 25% jeśli podwoimy szybkość tego pozostałego kodu. Odnotujmy, że jednak mamy tylko 25% wydajność zwiększoną po optymalizacji 10%. Mając zoptymalizowane najpierw 90%, będziemy mieli 5% poprawę wydajności; prawie nic. Pomimo to ujrzymy sytuację gdzie zasada 90/10 wyraźnie nie ma zastosowania i zobaczymy przypadki gdzie optymalizacja tych 90% może stworzyć znaczną poprawę wydajności. Grupa OL uśmiechnie się i powie „hmm, to jest korzyść późnej optymalizacji, możemy optymalizować stopniowo i pobierać poprawną ilość optymalizacji jaką potrzebujemy”. Grupa wczesnej optymalizacji (OE) używa słabości w arytmetyce procentów do wskazania, że prawdopodobnie zakończymy optymalizację dużej części naszego programu tak czy inaczej. Więc dlaczego nie obsłużyć wszystkiego tego w pierwszej kolejności w naszym projekcie? Duży problem ze strategią OL jest taki, że często kończymy projektowanie i pisanie programu dwukrotnie – raz aby uczynić go funkcjonalnym, drugi raz czyniąc go praktycznym. W końcu jeśli mamy zamiar przepisać i tak 90%, dlaczego nie napisać go szybciej za pierwszym razem? Ludzie z OE również wskazują ,ze chociaż programiści notorycznie błądzą przy ustalaniu gzie program spędza większość czasu, są pewne oczywiste miejsca gdzie wiadomo iż wystąpią problemy z wydajnością. Dlaczego czekać do odkrycia tej oczywistości? Dlaczego nie obsłużyć takich obszarów problemu wcześniej co pozwoli spędzić mniej czasu na mierzeniu i optymalizacji tego kodu? Podobnie jak wiele innych argumentów w Inżynierii Oprogramowania, te dwa obozy stały się zupełnie spolaryzowane i stają się zwolennikami całkowicie czystego podejścia w jednym z kierunków (albo OE albo OL). Podobnie jak wiele innych argumentów w Informatyce, prawda leży gdzieś pomiędzy tymi dwoma ekstremami Każdy projekt gdzie programista przystępuje do projektowania perfekcyjnego programu bez martwienia się o wydajność dopóki koniec jest przesądzony. .Większość programistów w tym scenariuszu pisze strasznie wolny kod. Dlaczego? Ponieważ łatwiej jest zrobić to a potem zawsze mogą „rozwiązać problem wydajności podczas fazy optymalizacji.” W wyniku tego, 90% części programu jest często tak wolnych , że nawet jeśli czas pozostałych 10% zostałby zredukowany do zera program byłby jeszcze zbyt wolny. Z drugiej strony, grupa OE próbuje dogonić w pisaniu najlepszego z możliwych kodu opuszczając warunki graniczne , a produkt może nigdy nie zaskoczyć, Jest jeden niezaprzeczalny fakt, który faworyzuje argumentację OL – kod optymalizowany jest trudny do zrozumienia i pielęgnacji. Co więcej, często zawiera błędy, które nie są obecne w kodzie niezoptymalizowanym. Ponieważ kod niepoprawny jest nieakceptowany, nawet jeśli działa szybciej, bardzo dobrym argumentem przeciw wczesnej optymalizacji jest fakt, ze testowanie, debuggowanie i zapewnienie jakości przedstawia dużą część cyklu rozwoju programu. Wczesna optymalizacja może tworzyć wiele dodatkowych błędów programowych, które gubimy obojętnie kiedy ,zachowując przez nie optymalizowanie później w cyklu rozwoju. Poprawnym czasem do optymalizacji programu jest, cóż, odpowiedni czas. Niestety „odpowiedni czas” różni się w programie. Jednakże, pierwszym krokiem jest rozwój wydajności programu wymaganej wraz z innymi specyfikacjami programu. System analizy powinien rozwinąć docelowy czas reakcji dla wszystkich
interakcji użytkownika i przetwarzania. Podczas rozwoju i testowania, programiści maja cel do wypełnienia , więc nie mogą lenić się i czekać na fazę optymalizacji przed napisaniem kodu, wykonującego się rozsądnie. Z drugiej strony mają również na celowniku to, że chociaż kod działa dosyć szybko, nie muszą marnować czasu, lub czynić swój kod mniej pielęgnacyjnym; mogą pójść dalej i pracować nad resztą programu. Oczywiście, system analizy może źle ocenić wymagania wydajności, ale nie zdarza się to często w dobrze zaprojektowanym systemie. Inną okolicznością jest to kiedy co wykonać. Jest kilka typów optymalizacji jakie możemy zastosować. Na przykład. Możemy przestawić instrukcje unikając hazardu dublujących szybkość kawałka kodu. Lub możemy wybrać różne algorytmy, które możemy uruchomi dwa razy szybciej. Jednym wielkim problem z optymalizacją jest taki, że nie jest to proces pojedynczy a wiele typów optymalizacji jest lepiej wykonać później niż wcześniej lub vice versa. Na przykład, wybór dobrego algorytmu jest tym co powinniśmy zrobić wcześniej. Jeśli zdecydujemy się użyć lepszego algorytmu po implementacji kiepskiego, wiele pracy na kodzie implementującym stary algorytm jest stracone. Podobnie szeregowanie instrukcji jest jedną z ostatnich optymalizacji jakie powinniśmy zrobić. Każda zmiana kodu po przestawieniu instrukcji dla wydajności, może wymusić spędzenie później wiele na czasu na przestawianiu ich ponownie. Najwyraźniej najniższy poziom optymalizacji (tj. zależny od CPU lub parametrów systemu) powinien być optymalizowany później. Odwrotnie, poziom najwyższy optymalizacji (tj. wybór algorytmu) powinien być optymalizowany szybciej. We wszystkich przypadkach powinniśmy mieć docelowe wartości wydajności na myśli podczas rozwijania kodu. 25.2 JAK ZNALEŻĆ WOLNY KOD W NASZYM PROGRAMIE? Chociaż są problemy z zasada 90/10, koncepcja jest w zasadzie solidna – programy mają w zwyczaju spędzać duża ilość swojego czasu wykonania tylko w małym procencie kodu. Wyraźnie powinniśmy zoptymalizować najpierw najwolniejszą część kodu. Jedyny problem to ten jak znaleźć ten najwolniejszy kod programu? Są cztery powszechne techniki programistyczne używane do znajdowania „gorących miejsc” (miejsc gdzie programy spędzają większość swojego czasu). Pierwsza to metoda prób i błędów. Druga to optymalizacja wszystkiego. Trzecią jest analiza programu. Czwartą jest użycie profilu lub innego systemowego narzędzia monitorowania wydajności różnych części programu. Po zlokalizowaniu gorących miejsc programista może próbować zanalizować tą część programu. Technika prób i błędów jest ,niestety, najpopularniejszą strategią. Programista przyspiesza różne części programu poprzez uczynienie świadomego odgadywania gdzie spędza się większość czasu. Jeśli programista odgadnie prawidłowo, program będzie działał dużo szybciej po optymalizacji. Eksperymentujący programiści często używają tej techniki szczęśliwie szybko lokalizując i optymalizując program. Jeśli programista odgadnie prawidłowo, technika ta minimalizuje ilość czasu spędzonego na znajdowaniu gorących miejsc w programie. Niestety większość programistów źle odgaduje i kończy na optymalizowaniu złych części kodów. Taki wysiłek często idzie na marne ponieważ optymalizacja złych 10% nie poprawi zdecydowanie wydajności. Jednym z podstawowych powodów błędności tej techniki jest to ,że jest często pierwszym wyborem niedoświadczonych programistów , którzy nie mogą łatwo rozpoznać wolnego kodu. Niestety, są oni nieświadomi innych technik, więc zamiast próbować podejścia strukturalnego, zaczynają stosować (często) odgadywanie nieświadome. Innym sposobem zlokalizowania i optymalizacji wolnej części programu jest optymalizacja wszystkiego. Oczywiście technika ta nie działa dobrze dla dużych programów, ale dla krótkich części kodu działa stosunkowo dobrze. Później w tym tekście będzie dostarczony krótkiego przykładu problemu optymalizacji i będzie stosował tą technikę optymalizacji programu. Oczywiście, dla dużego programu lub podprogramów może nie być podejściem mało opłacalnym. Jednakże gdzie właściwie można zachować czas podczas optymalizacji naszego programu (lub przynajmniej części programu) ponieważ nie będziemy musieli uważnie analizować i mierzyć wydajności naszego kodu. Poprzez optymalizację wszystkiego, jesteśmy pewni optymalizacji wolnego kodu. Metoda analizy jest najtrudniejszą z tych czterech. Przy tej metodzie studiujemy nasz kod i określamy gdzie program spędza większość czasu w oparciu o dane jakich oczekujemy od procesu. Teoretycznie , jest to najlepsza technika. W praktyce, istoty ludzkie generalnie wykazują dystans dla takiej pracy analitycznej. Jako taka, analiza jest często niepoprawna lub zbyt długa do ukończenia. Co więcej, niewielu programistów ma duże doświadczenie w studiowaniu swoich kodów dla określenia gdzie spędza on większość swojego czasu, więc często są bezradni przy lokalizowaniu gorących miejsc przez studiowanie swoich listingów kiedy pojawia się potrzeba. Pomimo problemów z analizą programów, jest to pierwsza technika jakiej powinniśmy zawsze używać, kiedy próbujemy optymalizować program.. Prawie wszystkie programy spędzają większość swojego czasu wykonania w ciele pętli lub rekurencyjnym wywołaniu funkcji.. Dlatego też powinniśmy spróbować zlokalizować wszystkie rekurencyjne wywołania funkcji (zwłaszcza pętle zagnieżdżone) w naszych
programach. Jest bardzo duże prawdopodobieństwo, że program będzie spędzał większość swojego czasu w jednym z tych dwóch obszarach programu. Takie miejsca są do rozważenia w pierwszej kolejności kiedy optymalizujemy nasze programy. Chociaż metoda analityczna dostarcza dobrego sposobu zlokalizowania wolnego kodu w programie, analizowanie programu jest wolnym, nużącym i nudnym procesem. Jest bardzo łatwo zupełnie zgubić najbardziej czasochłonna część programu, zwłaszcza w pośredniej obecności wywołania funkcji rekurencyjnej. Nawet zlokalizowanie czasochłonnych pętli zagnieżdżonych jest często trudne. Na przykład możemy nie uświadamiać sobie, kiedy szukamy pętli wewnątrz procedury, że jest zagnieżdżona pętla z racji faktu, że kod wywołujący wykonuje pętlę kiedy wywołujemy procedurę. Teoretycznie, metoda analityczna powinna zawsze działać. W praktyce, jest to tylko nieznaczny sukces ,kiedy omylni ludzie robią analizę. Niemniej jednak, pewne gorące miejsca są łatwe do odnalezienia poprzez analizę programu, więc naszym pierwszym krokiem, kiedy optymalizujemy program jest analiza. Ponieważ programiści są notorycznie słabi przy analizie programów w celu znalezienia gorących miejsc, można spróbować zautomatyzować ten proces. Jest to dokładnie to co profiler może zrobić dla nas. Profiler jest małym programikiem, który mierzy jak długo nasz kod spędza w jakiejś części programu. Profiler zazwyczaj działa poprzez cykliczne przerywanie naszego kodu i odnotowywanie adresu powrotnego. Profiler buduje histogram przerwań adresów przerwań (generalnie zaokrągla do określonej przez użytkownika wartości). Poprzez studiowanie tego , możemy określić gdzie program spędza większość czasu .Powie to nam jaką część kodu musimy zoptymalizować. Oczywiście, stosując ta technikę, będziemy potrzebowali programu profilera. Borland, Microsoft i kilku innych producentów dostarcza profilerów i innych narzędzi optymalizujących. 25.3 CZY OPTYMALIZACJA JST KONIECZNA? Z wyjątkiem zabawy i edukacji, nigdy nie powinniśmy próbować podejścia do projektu z postawą, że musimy uzyskać maksymalną wydajność naszego kodu. Lata temu, była to ważna postawa ponieważ było to to co pozwalało uzyskiwać przyzwoite działanie na wolnych maszynach tej ery. Redukując czas działania programu z dziesięciu minut do dziesięciu sekund, uczyniono poważny krok dla poprawy wydajności . Z drugiej strony przyspieszenie programu pobierającego 0,1 sekundy do punktu w którym działa on w milisekundę jest często bezcelowe. Zmarnujemy dużo wysiłku poprawiając wydajność, mimo to tylko kilka osób odnotuje różnice. Nie mówię, że przyspieszenie programu z 0,1 sekundy do 0,001 sekundy nie jest nigdy warte zachodu. Jeśli piszemy program do zbierania danych, który wymaga wykonania odczytu co milisekundę, a może tylko obsłużyć dziesięć odczytów na sekundę jako obecnie zapisanych. Co więcej, nawet jeśli nasz program działu już dość szybko, są powody dlaczego chcielibyśmy uczynić go dwa razy szybszym. Na przykład przypuśćmy, że ktoś może używać naszego programu w środowisku wielozadaniowym. Jeśli zmodyfikujemy nasz program do dwukrotnie szybszego, użytkownik będzie mógł uruchomić inny program wraz z naszym i nie zauważyć obniżenia wydajności. Jednakże, sprawą do zapamiętania jest to, że musimy napisać oprogramowanie, które jest dosyć szybkie. Ponieważ program tworzy wyniki natychmiastowe (lub prawie bliskie natychmiastowych) istnieje potrzeba uczynienie jego uruchomienia szybszym. Ponieważ optymalizacja jest procesem kosztownym i skłonnym do błędów, chcemy unikać jej jak to tylko możliwe. Pisanie programów, które działają szybciej niż dosyć szybki jest marnowaniem czasu. Jednakże, jak widać wyraźnie z dzisiejszych rozdętych aplikacji, nie jest to rzeczywisty, większość tworzonych kodów programistycznych jest zbyt wolnych, nie zbyt szybkich. Popularnym podawanym powodem dla tworzenia nie zoptymalizowanego kodu jest złożoność sprzętu. Wielu programistów i menadżerów czuje ze maszyny wyższej klasy dla których projektują oprogramowanie dzisiaj, będą maszynami średniej klasy za dwa lata, kiedy udostępnią końcową wersją oprogramowania .Więc jeśli zaprojektują oprogramowanie działające na dzisiejszych bardzo szybkich maszynach, będą się wykonywały średniej klasy maszynach kiedy udostępnią oprogramowanie Są z tym związane dwa problemy. Po pierwsze system operacyjny działający na tych maszynach za dwa lata pożre znaczną część zasobów maszyny (wliczając w to cykle CPU) Ciekawe jest, że dzisiejsze są sto razy szybsze niż oryginalny 8088, a mimo to wiele aplikacji działa wolniej niż gdyby działał na oryginalnym PC.. Prawda , dzisiejsze oprogramowanie dostarcza wiele cech poza te z oryginalnego PC, ale jest cała masa argumentów – klienci domagają się cech takich jak wiele okienek, GUI, menu rozwijane itd., które wykorzystują cykle CPU. Nie możemy zakładać, ze nowsze maszyny będą dostarczały dodatkowych cykli zegarowych, więc nasz wolny kod będzie działał szybciej. OS lub interfejs użytkownika naszego programu zakończy zjedzenie tych dodatkowych dostępnych cykli zegarowych. Tak więc pierwszym krokiem jest realistyczne określenie żądanej wydajności naszego programu. Potem musimy napisać program spełniający ten cel wydajności. Kiedy rozminiemy się z żądana wydajnością, wtedy
jest czas na optymalizację programu. Jednakże nie powinniśmy marnować dodatkowego czasu optymalizując kod ponieważ nasz program napotyka lub przekracza specyfikację wydajności. 25.4 TRZY TYPY OPTYMALZACJI Są trzy formy optymalizacji jakie może zastosować, kiedy poprawiamy wydajność programu. Wybierają one lepszy algorytm (optymalizacja wysokiego poziomu), implementację lepszego algorytmu (poziom optymalizacji średniego poziomu) i „zliczanie cykli” (optymalizacja niskiego poziomu). Każda technika ma swoje miejsce i, generalnie, stosujemy je w różnych miejscach w procesie projektowania. Wybór lepszego algorytmu jest najbardziej nagłośnioną technika optymalizacji. Niestety jest to technika używana najmniej często. Jest łatwa dla kogoś głoszącego, że powinniśmy zawsze znajdować lepszy algorytm jeśli potrzebujemy większej szybkości; ale znalezienie tego algorytmu jest trochę trudniejsze. Po pierwsze, zdefiniujmy algorytm zmiany ponieważ używamy zasadniczo różnych technik do rozwiązania problemu. Na przykład, przełączając z algorytmu „sortowania bąbelkowego” do algorytmu „szybkiego sortowania” jest dobrym przykładem algorytmu zmiany, Generalnie, chociaż nie zawsze, zmiany algorytmów oznaczają, że używamy programu z lepsza funkcją Big-Oh. Na przykład, kiedy przełączamy z sortowania bąbelkowego do szybkiego sortowania, zmieniamy algorytm z czasem działania O(n2) na algorytm z oczekiwanym czasem działania O(n lg n). Musimy pamiętać o ograniczeniach funkcji Big-Oh kiedy porównujemy algorytmy. Wartość dla n musi być wystarczająco duża do zamaskowania efektu ukrytej stałej. Co więcej, analizy Big-Oh są zazwyczaj najgorsze i mogą nie wpływać na nasz program. Na przykład jeśli życzymy sobie posortować tablicę, która jest „prawie” posortowana najpierw, algorytm sortowania bąbelkowego jest zazwyczaj szybszy niż algorytm szybkiego sortowania, bez względu na wartość dla n. Dla danej, która jest prawie posortowana, sortowanie bąbelkowego działa prawie w czasie O(n) podczas gdy algorytm szybkiego sortowania działa w czasie O(n2). Drugą rzeczą do zapamiętania jest stałość. Jeśli dwa algorytmy mają taką samą funkcję Big-Oh, nie możemy określić żadnej różnicy pomiędzy dwoma analizami Big-Oh. To nie znaczy, że będą one pobierały taką samą ilość czasu działania. Nie zapomnijmy, że w analizie Big-Oh odrzucamy wszystkie nisko poziomowe warunki i stałe mnożne. Trochę bardziej pomocny jest zapis asymptotyczny w tym przypadku. Uzyskanie rzeczywiście poprawy wydajności wymaga zmiany algorytmu naszego programu. Jednakże, zmiana algorytmu O(n lg n) na algorytm O(n2) jest często trudniejsze jeśli rozwiązanie już nie istnieje. Przypuszczalnie , dobrze zaprojektowany program nie będzie zawierał oczywistych algorytmów, które możemy dramatycznie poprawić (jeśli są , nie były dobrze zaprojektowane).Dlatego też, próby znalezienia lepszego algorytmu może nie zakończyć się powodzeniem. Niemniej jednak jest to zawsze pierwszy krok jaki powinniśmy wykonać ponieważ kolejne kroki działają na takim algorytmie jaki mamy. Jeśli wykonujemy te inne kroki na złym algorytmie a potem, później odkryjemy lepszy algorytm, będziemy musieli powtarzać, te czasochłonne kroki ponownie przy nowym algorytmie. Są dwa kroki odkrywania nowych algorytmów: badanie i rozwój. Pierwszy krok służy temu aby zobaczyć czy możemy znaleźć lepsze rozwiązanie w istniejącej literaturze. Ewentualnie, drugi krok jest po to aby zobaczyć czy możemy odkryć lepszy algorytm w naszym własnym. Kluczem do tego jest zabudżetowanie właściwej ilości czasu dla tych dwóch działań badanie jest luźnym procesem . Zawsze możemy przeczytać więcej książek lub artykułów. Więc musimy zadecydować jak wiele czasu mamy zamiar spędzić szukając istniejącego rozwiązania. Może to być kilka godzin, dni m, tygodni lub miesięcy. Jakkolwiek jest to opłacalne. Potem możemy udać się do biblioteki (lub półki z książkami) i poszukać lepszego rozwiązania. Gdy tylko wygasa nasz czas, czas porzucić podejście badawcze chyba, że jesteśmy pewni, że zmierzamy we właściwym kierunku w materiałach jakie studiujemy. Jeśli tak zabudżetujemy trochę więcej czasu i zobaczmy jak to działa. W tym samym miejscu jednak musimy zadecydować czy prawdopodobnie nie można znaleźć lepszego rozwiązania i czy jest czas aby rozwinąć nowe w naszym własnym. Podczas poszukiwania lepszego rozwiązania powinniśmy studiować dokładnie artykuły, teksty itp. Chociaż przestudiowaliśmy ważne testy. Kiedy prawdą jest ,że większość z tego co wystudiowaliśmy nie będzie miało zastosowania do problemu, nauczymy się o rzeczach, które będą użyteczne w przyszłych projektach. Co więcej, podczas gdy ktoś może nie uzyskać potrzebnego rozwiązania, może wykonać pracę , która idzie w tym samym kierunku co nasza i może dostarczyć takich samych dobrych pomysłów, jeśli nie podstawowych dla naszego własnego rozwiązania. Jednakże, zawsze musimy pamiętać, że zadaniem inżynierów jest dostarczenie wydajnych rozwiązań problemu. Jeśli marnujemy zbyt dużo czasu szukając rozwiązań, które mogą się nigdzie nie pojawić w literaturze, spowodujemy kosztowne przedłużenie naszego projektu. Trzeba wiedzieć, kiedy jest czas aby „odłożyć go” i zając się resztą projektu. Rozwijanie nowego algorytmu jako naszego własnego jest również luźne. Możemy spędzić resztę życia próbując znaleźć wydajne rozwiązanie dla trudnego do rozwiązania problemu. Ponownie więc musimy zabudżetować czas w związku z tym dla tego projektu. Spędzajmy czas mądrze próbując rozwijać lepsze
rozwiązanie dla naszego problemu ,ale ponieważ czas jest nieubłagany, czas spróbować różnych podejść zamiast marnować czas na ściganie „świętego grala”. Bądźmy pewni użycia wszystkich zasobów będących w naszej dyspozycji, kiedy próbujemy znaleźć lepszy algorytm. Lokalna biblioteka uniwersytecka może być bardzo pomocna. Również powinniśmy skorzystać z Sieci. Uczęszczając na spotkania miejscowego klubu komputerowego, omawiając nasz problem z innymi, lub rozmawiając z przyjaciółmi, być może ktoś czytał o rozwiązaniu tego czego szukamy. Jeśli mamy dostęp do Internetu, BIX, Compuserv lub innych serwisów on-line bądź BBS’ów, naturalnie możemy wysłać post z informacją o pomoc. Wśród milionów użytkowników, jeśli istnieje lepsze rozwiązanie naszego problemu, ktoś prawdopodobnie już go rozwiązał. Kilka postów może zwiększyć rozwiązanie, którego nie byliśmy w stanie znaleźć lub rozwiązać sami. W tym miejscu musimy przyznać się do niepowodzenia. W rzeczywistości możemy musieć przyznać się do powodzenia – już znaleźliśmy tak dobry algorytm jak można było. Jeśli ten jest jeszcze zbyt wolny dla naszych wymagań, może być czas aby spróbować innych technik poprawy szybkości programu. Kolejnym krokiem jest sprawdzenie czy możemy dostarczyć lepszej implementacji dla algorytmu jakiego używamy. Ten krok optymalizacji, chociaż niezależny od języka, właśnie dla większości programistów asemblerowych tworzy zdecydowaną poprawę wydajności ich kodu. Lepsza implementacja generalnie wymaga kroków takich jak pętle rozwijane, używając przeszukiwania tablicy zamiast obliczeń, eliminując obliczenia z pętli, której wartość nie zmienia się wewnątrz pętli, wykorzystując idiomy maszynowe( tak jak użycie shift lub shift i and zamiast mnożenia), próbując zachować zmienne w rejestrach tak długo jak to możliwe i tak dalej. Jest niespodzianką o ile szybciej program może działać poprzez zastosowanie prostych technik jak te, których opisy pojawiły się w tym tekście. W ostateczności możemy się uciec do zliczania cykli.. Na tym poziomie możemy próbować założyć, że sekwencja instrukcji używa kilku cykli zegarowych. Jest to trudne do optymalizacji do wykonania ponieważ musimy być świadomi jak wiele cykli zegarowych konsumuje każda instrukcja, a to zależy od instrukcji, używanego trybu adresowania, instrukcji dookoła instrukcji bieżącej (tzn. efekty potokowy i superskalarny), szybkości systemu pamięci (stany oczekiwania i pamięci podręcznej) i tak dalej. Rzecz jasna, takie optymalizacje są bardzo nużące i wymagają bardzo ostrożnej analizy programu i systemu na którym działamy. Zwolennicy OL zawsze domagają się aby odłożyć optymalizację na tak długo jak to możliwe .Ludzie ci generalnie mówią o tym jako szybkiej formie optymalizacji. Powód jest prosty: każda zmiana jakiej dokonamy po takiej optymalizacji może zmienić oddziaływanie instrukcji i ich czas wykonania. Jeśli spędzamy znaczna część czasu wybierając sekwencję 50 instrukcji a potem odkrywamy ,ze musimy przepisać ten kod z powodu tego czy innego, cały ten czas spędzamy starannie wybierając te instrukcje unikając hazardu . Z drugiej strony, jeśli czekamy na ostatni możliwy moment do zrobienia takiej optymalizacji naszego kodu, zoptymalizujemy tylko taki kod. Wielu programistów HLL powie, że dobry kompilator może pokonać człowieka przy wybieraniu instrukcji i optymalizacji kodu. To nie jest prawda. Dobry kompilator pokona pośledni program asemblerowy w dużej mierze. Jednakże, dobry kompilator nie starcza na dobrego programistę asemblerowego. W końcu, najgorsze co się może zdarzyć jest to, że dobry programista asemblerowy będzie patrzył na wyjście kompilatora i poprawiał go. „Zliczanie cykli” może poprawić wydajność naszego programu Średnio, możemy przyspieszyć program od 50 do 200% wykonując proste zmiany (jak przestawienie instrukcji). To jest różnica pomiędzy 80486 a Pentium! Więc nie powinniśmy ignorować możliwości zastosowania takiej optymalizacji naszych programów. Zapamiętajmy, że powinniśmy robić taką optymalizację wcześniej zanim nie zakończymy przerabiać ich ponieważ zmienił się nasz kod. Reszta tego rozdziału skoncentruje się na tych technikach poprawy implementacji algorytmu , zamiast projektować lepszy algorytm lub stosować techniki zliczania cykli. Projektowanie lepszego algorytmu wykracza poza temat tego podręcznika. Zliczanie cykli jest jednym z tych procesów, który różni się w zależności od procesora. To znaczy, techniki optymalizacji które działają dobrze na 80386 zawodzą na 486 lub Pentium i vice versa. Ponieważ Intel stale tworzy nowe chipy, wymagające różnych technik optymalizacjnych, listując te techniki tu, materiał ten może okazać się przestarzały .Intel publikuje takie podpowiedzi optymalizacyjne w swoich podręcznikach. Artykuły na temat optymalizacji programów asemblerowych często pojawiają się w magazynach technicznych takich jak Dr. Dobb’s Journal, powinniśmy czytać takie artykuły i uczyć wszystkich technik optymalizacyjnych. 25.5 POPRAWA IMPLEMNTCJI ALGORYTMU Jedynym prostym sposobem częściowego zademonstrowania jak zoptymalizować kawałek kodu jest dostarczenie jakiegoś przykładu i kroków optymalizacyjnych jakie możemy zastosować do tego programu. Sekcja ta przedstawi krótki program, który rozmywa obrazek w ośmiobitowej skali szarości. Potem sekcja ta
poprowadzi prze kilka kroków optymalizacyjnych i pokaże jak uczynić program działającym 16 razy szybciej. Poniższy kod zakłada, że dostarczamy go z plikiem zawierającym zdjęcie w skali szarości 251x256. Struktura danych dla tego pliku jest następująca: Image: array [0..255, 0..255] of byte Każdy bajt zawiera wartości w zakresie 0..255 z zerem oznaczającym czerń, 255 przedstawiającym biel a pozostałe wartości przedstawiają odcienie szarości pomiędzy tymi dwoma wartościami ekstremalnymi. Algorrytm rozmycia uśrednia piksel wraz z jego ośmioma najbliższymi sąsiadami. Pojedyncza operacja rozmycia stosuje tą średnią dla wszystkich wewnętrznych pikseli obrazka (to znaczy, nie stosuje pikseli na granicy obrazka ponieważ nie ma takiej samej liczby sąsiadujących pikseli jak inne piksele) poniższy program Pascalowski implementuje algorytm rozmycia i pozwala użytkownikowi określić ilość rozmyć (poprzez zapętlenie w całym algorytmie ilości określonych przez użytkownika): Program PhotoFilter(input, output); (* Tu jest plik z danymi pierwotnymi stworzony przez program Photoshop *) type image = array [0..255] of array [0..255] of byte (* Zmienne jakich będziemy używać. Zauważmy, że zmienne „datain” i dataout” są wskaźnikami ponieważ *) (* Turbo Pascal nie pozwoli nam zaalokować więcej niż 64K danych w jednym globalnym segmencie danych*) var h ,i, j ,k ,l, sum ,iterations : integer; datain, dataout : ^image; f, g: file of image; begin (* Otwieramy pliki I rzeczywistą daną wejściową*) assign (f, ‘roller1.raw’); assign (f,roller2.raw’); reset(f); rewrite(g); new (datain); new (dataout); read(f, datain^); (* Pobranie liczby iteracji od użytkownika *) write (‘Wprowadź liczbę iteracji: ‘) readln(iterations); writeln (Obliczanie wyniku’); (* Kopiowanie danych z tablicy wejściowej do tablicy wyjściowej. W rzeczywistości jest to kiepski sposób *) (* kopiowania z tablicy wejściowej do tablicy wyjściowej *) for i:=0 to 255 do for j:=0 to 255 do dataout^ [i][j] := datain^ [i][j]; (* Okay, jesteśmy tu gdzie wykonuje się cała praca. Pętla zewnętrzna powtarza operację rozmywania ilość *) (* razy określoną przez użytkownika *) for h:=1 to iterations do begin
(*Dla każdego wiersza pierwotnej z wyjątkiem pierwszej i ostatniej, obliczamy nową wartość*) (*dla każdego elementu *) for i:= 1 to 249 do (* Dla każdej kolumny z wyjątkiem pierwszej i ostatniej obliczamy nową wartość dla każdego*) (* elementu *) for j:= 1 to 254 do begin (*Dla każdego elementu w tablicy, obliczamy nową wartość rozmycia poprzez dodanie ośmiu komórek wokół elementu tablicy osiem razy do bieżącej wartości komórki. Potem dzielimy to przez szesnaście obliczając średnią z dziewięciu komórek tworzących kwadrat wokół bieżącej komórki. Bieżąca komórka ma 50% znaczenie, pozostałe osiem komórek wokół bieżącej komórki dostarcza pozostałych 50% (6,25% każda) *) sum := 0 for k := -1 to 1 do for l:= -1 to 1 do sum :=sum+datain^[I+k] [j+l]; (* Sum aktualnie zawiera sumę dziewięciu komórek, dodanych siedem razy do bieżącej *) (* komórki, więc uzyskujemy całkowicie ośmiokrotnie bieżącą komórkę *) dataout^ [i][j] := (sum +datain^ [i][j]*7) div 16; end; (* Kopiowanie wartości komórki wyjściowej z powrotem do komórki wejściowej, wiec możemy osiągnąć *) (* rozmycie z tą nową daną w kolejnej iteracji *) for i:= 0 to 250 do for j:= 0 to 255 do datain^ [i][j] := dataout^ [i][j]; end; writeln (‘Zapis wyników’); write(g, dataout^); close(f); close(g); end. Powyższy program Pascala skompilowany z Turbo Pascalem v 7.0 , w 45 sekund oblicza 100 iteracji algorytmu rozmycia. Porównywalny program napisany w C i skompilowany pod Borland C++ v 4.02 zajmuje 29 sekund do działania .taki sam plik źródłowy skompilowany z Microsoft C++ v 8.00 działa w 21 sekund. Oczywiście, kompilatory C tworzą lepszy kod niż Turbo Pascal.. Zajęło około 3 godzin uzyskanie wersji pascalowskiej działającej i przetestowanej. Wersja C zajęła około jednej godziny na zakodowanie i przetestowanie. Poniższe dwa obrazki to przykłady „przed” i „po” tej funkcji programu” Przed rozmyciem:
Po rozmyciu (10 iteracji):
Poniżej mamy surowe tłumaczenie z Pascala bezpośrednio do języka asemblera powyższego programu. Wymaga 36 do działania. Tak, kompilatory C wykonują tę pracę lepiej, ale ponieważ widać jak zły to kod, będziemy zdumieni jak wolno działa kod Turbo Pascala. Zajmie około godziny przetłumaczenie wersji
Pascalowej na kod asemblerowy i zdebuggowanie go do miejsca w którym stworzymy takie same dane wyjściowe jak wersja Pascalowa. ; IMGPRCS.ASM ; ; Program przetwarzający obrazek ; ; Jest to program rozmywający obrazek w ośmiobitowej skali szarości poprzez uśrednienie piksela w obrazku z ; ośmioma pikselami wokół. Średnia jest obliczana przez (CurCell*8+ pozostałe 8 komórek)/16, obciążając ; bieżącą komórkę o 50% ; ; Ponieważ rozmiar obrazka (prawie 64k), macierze wejściowe i wyjściowe są w różnych segmentach. ; Wersja #1: Proste tłumaczenie z Pascala na Asembler ; ; Porównanie osiągów (system 66MHz 80486 DX/2) ; ; Ten kod 36 sekund ; Borland Pascal v7.0 45 sekund ; Borland C++ v4.02 29 sekund ; Microsoft C++ v8.00 21 sekund .xlist include includelib .list .286 dseg
stdlib.a stdlib.lib
segment para public ‘data’
;Pętla sterująca zmiennych i inne zmienne: h i j k l sum iterations
word word word word word word word
? ? ? ? ? ? ?
InName OutName
byte byte
“roller.raw”, 0 “roller2.raw”,0
dseg
ends
; Nazwa pliku:
; Tu mamy dane wejściowe na których działamy InSeg DataIn InSeg
segment para public ‘indata’ byte 251 dup (256 dup (?)) ends
; Tu jest tablica wyjściowa, która przechowuje wynik OutSeg DataOut OutSeg
segment para public ‘outdata’ byte 251 dup (256 dup(?)) ends
cseg
segment para public ‘code’
assume cs:cseg, ds: dseg Main
GoodOpen:
GoodRead:
proc mov ax, dseg mov ds, ax meminit mov lea int jnc print byte jmp
ax, 3d00h dx, InName 21h GoodOpen
mov mov mov lea mov mov int cmp je print byte jmp
bx, ax dx, InSeg d, dx dx, DataIn cx, 256*251 ah, 3Fh 21h ax, 256*251 GoodRead
mov mov print byte getsm atoi free mov print byte
ax, dseg ds, ax
;otwarcie pliku wejściowego do odczytu
„Nie można otworzyć pliku .”,cr, lf,0 Quit ;uchwyt pliku ;gdzie pobieramy dane ;rozmiar pliku danych do odczytu ;zobaczmy czy odczytamy daną
„Nie odczytano właściwie pliku”, cr, lf, 0 Quit
“Wprowadź liczbę iteracji: “, 0
iterations, ax „Obliczanie wyniku”, cr, lf, 0
; Kopiowanie danej wejściowej do bufora wyjściowego illop0: jloop0:
mov cmp ja mov cmp ja
i, 0 i, 250 iDone0 j, 0 j, 255 jDone0
mov shl add mov mov mov
bx, I bx, 8 bx, j cx, InSeg es, cx al., es :DataIn[bx]
mov mov mov
cx, OutSeg es, cx es: DataOut[bx], al.
;obliczenie indeksu do obu tablic ;używając formuły i*256+j ;wskazuje segment wejściowy ;Pobranie DataIn[i][j] ; wskazuje segment wyjściowy ;Przechowanie w DataOut [i][j]
jDone0:
inc jmp inc jmp
j jloop0 I iloop0
;kolejna iteracja pętli j ;kolejna iteracja pętli I
iDone0: ; for h :=1 to iterationsmov hloop: mov cmp ja
h, 1 ah, h ax, iterations hloopDone
; for I:=1 to 249 – iloop:
mov cmp ja
I, 1 1, 249 iloopDone
;for j :=1 to 254 – jloop:
mov cmp ja
j, 1 j, 254 jloopDone
;sum :=0; ; for k := -1 to 1 do for l :=-1 to 1 do
kloop:
lloop:
mov mov mov mov cmp jg
ax, InSeg es, ax sum 0 k, -1 k, 1 kloopDone
mov cmp jg
l, -1 l, 1 lloopDone
;uzyskanie dostępu do InSeg
;sum :=sum+datain [I+k][j+l]
lloopDone:
mov add shl add add
bx, I bx, k bx, 8 bx, j bx ,l
mov mov add
al., es: DataIn[bx] ah, 0 Sum, ax
inc jmp inc jmp
l lloop k kloop
;dataout[i][j] := (sum + datain[i][j] * 7) div 16; kloopDone:
mov
bx, 1
;mnożenie przez 256
jloopDone:
shl add mov mov imul add shr
bx, 8 bx, j al, es:DataIn[bx] ah, 0 ax, 7 ax, sum ax, 4
mov mov mov shl add mov
bx ,OutSeg es, bx bx, i bx, 8 bx, j es: DataOut[bx], al
inc jmp inc jmp
j jloop I iloop
;* 256
;div 16
iloopDone: ; Kopiowanie danej wyjściowej do bufora wejściowego iloop:
jDone1: iDone1: hloopDone:
mov cmp ja mov cmp ja
i, 0 i, 250 iDone1 j, 0 j, 255 jDone1
mov shl add
bx, i bx, 8 bx, j
;obliczanie indeksu do obu tablic ;używając formuły i*256+j
mov mov mov
cx, OutSeg es, cx al., es: DataOut[bx]
;wskazuje segment wejściowy
mov mov mov
cx, InSeg es, cx es: DataIn[bx], al.
;wskazuje segment wyjściowy
inc jmp inc jmp inc jmp print byte
j jloop1 I iloop1 h hloop
;kolejna iteracja pętli j
; Pobranie DataIn[i][j]
;przechowanie DataOut[i][j]
;kolejna iteracja pętli I
“Zapisanie wyniku”, cr,lf,0
; Okay, zapisujemy daną do pliku wyjściowego: mov mov lea int jnc print
ah, 3ch cx, 0 dx, OutName 21h GoodCreate
;tworzenie pliku wyjściowego ;normalne atrybuty pliku
GoodCreate:
GoodWrite: Quit: Main cseg sseg stk sseg zzzzzzseg LastBytes zzzzzzseg
byte jmp mov push mov mov lea mov mov int pop cmp je print byte jmp
„Nie można stworzyć pliku wyjściowego”, cr, lf,0 Quit bx, ax ;uchwyt pliku bx dx, OutSeg ;gdzie może być znaleziona dana ds., dx dx, DataOut cx, 256*251 ;rozmiar danej pliku do zapisu ah, 40h ;operacja zapisu 21h bx ;odzyskanie uchwyty dla zamknięcia ax, 256*251 ;zobaczmy czy zapisano daną GoodWrite „Nie zapisano poprawnie pliku”, cr, lf, 0 Quit
mov ah, 3eh Int 21h ExitPgm endp ends
;operacja zamknięcia
segment para stack ‘stack’ byte 1024 dup (“stack”) ends segment para public ‚zzzzzz’ byte 16 dup (?) ends end Main
Ten kod asemblerowy jest bardzo prosty, linia po linii tłumaczy poprzedni kod Pascalowski. Nawet początkujący programista (który przeczytał i zrozumiał Rozdziały Osiem i Dziewięć) powinien łatwo poprawić osiągi tego kodu. Podczas kiedy możemy uruchomić profiler w tym programie określamy gdzie w tym kodzie są „gorące miejsca” , trochę analizy, zwłaszcza w wersji Pascalowej powinno uczynić oczywistym, że jest kilka zagnieżdżonych pętli w tym kodzie .jak wskazuje Rozdział dziesiąty, kiedy optymalizujemy kod powinniśmy zawsze zaczynać od pętli najskrytszych. Ważną zmianą miedzy powyższym kodem a wersją asemblerową jest to, że rozwijamy pętle najskrytsze i zamieniamy obliczony indeks tablicy z jakimś stałym obliczeniem. Ta drobna zmiana przyspiesza wykonywanie sześciokrotnie! Wersja asemblerowa działa teraz sześć sekund zamiast 36. Wersja Microsoft C++ tego samego programu z porównywalna optymalizacja działa osiem sekund. Wymaga blisko czterech godzin wywołanie, testowanie i zdebuggowanie tego kodu. Wymaga dodatkowej godziny zastosowanie tej samej modyfikacji co wersja C ; IMGPRCS2.ASM ; ; Program przetwarzający obrazek ; ; Program ten rozmywa obrazek w ośmiobitowej skali szarości poprzez piksela w tym obrazku z ośmioma ; pikselami wokół. Średnia jest obliczana z (CurCell*8 + pozostałe 8 komórek)/ 16 , obciążając aktualną ; komórkę o 50%. ; ; Ponieważ rozmiar obrazka to prawie 64 K, matryce wejściowa i wyjściowa są w różnych segmentach. ; ; Wersja #1: Proste tłumaczenie z Pascala na Asembler ; Wersja #2: Trzy główne optymalizacje. (1) używa instrukcji movsd zamiast pętli do kopiowania danych z ; DataOut z powrotem do DataIn. (2) Używa formy repeat...until dla wszystkich pętli (3) rozwija najskrytsze ; dwie pętle (które są odpowiedzialne za większość poprawiania wydajności) ; ; Porównania wydajności (system 66 MHz 80486 DX/2)
; ; ; ; ; ; ; ;
Ten kodOryginalny kod ASM Borland Pascal v7.0 Borland C++ v4.02 Microsoft C++ v8.00
6 sekund 36 sekund 45 sekund 29 sekund 21 sekund
< Większość pomijanego kodu znajduje się tutaj, zobacz poprzednią wersję > print byte
„Wyniki obliczane”, cr,lf,0
; dla h := 1 to iterations mov
h, 1
hloop: ; Kopiowanie danych wejściowych do bufora wyjściowego ; Optymalizacja krok 31: Zastąpienie instrukcją movs
rep
push mov mov mov mov lea lea lea mov movsd pop
ds. ax, OutSeg ds., ax ax, InSeg es, ax si, DataOut di, DataIn di, DataIn cx, (251*256) / 4 ds
; Optymalizacja krok #1: Konwersja pętli do postaci repeat…until ; for i := 1 to 249 mov I, 1 iloop: ; for j:=1 to 254 mov j, 1 jloop: ; Optymalizacja. Rozwinięcie dwóch pętli najskrytszych: mov mov
bh, byte ptr i bl, byte ptr j
;i jest zawsze mniejsze niż 256 ; Obliczanie I*256+j!
push mov mov
ds. ax, InSeg ds., ax
; korzyść z dostępu do InSeg
mov mov mov mov add mov add mov add
cx, 0 ah, ch cl, ds.: DataIn[bx – 257] al, ds: dataIn[bx – 256] cx, ax al, ds: DataIn[bx-255] cx, ax al, ds: DataIn[bx-1] cx, ax
;tu obliczamy sumę ;DatIn[I-1][j-1] ;DataIn[I-1][j-1] ;DataIn[I-1][j+1] ;DataIn[I][j-1]
Done: ;
mov add mov add mov add mov add
al, ds: DataIn[bx+1] cx, ax al, ds: DataIn[bx+255] cx, ax al, ds:DataIn[bx+256] cx, ax al, ds:DataIn[bx+257] cx, ax
;DataIn[i][j+1]
mov shl add shr mov mov mov pop
al, ds:DataIn[bx] ax, 3 cx, ax cx, 4 ax, OutSeg ds, ax ds:DataOut[bx], cl ds
;DataIn[I][j] ;DataIn[I][j]*8
inc cmp jbe
j j ,254 jloop
inc cmp jbe
i i, 249 iloop
inc mov cmp jnbe jmp
h ax, h ax, Iterations Done hloop
print Byte
“Zapisywanie wyniku”, cr, lf, 0
;DataIn[I+1][j-1] ;DataIn[I+1][j] ; DataIn[i+1][j+1]
;dzielenie przez 16
Druga ,powyższa wersja używa jeszcze zmiennych pamięciowych dla większości obliczeń. Optymalizacja stosowana do kodu oryginalnego byłą głównie optymalizacją niezależną od języka. Kolejnym krokiem było zastosowanie jakiegoś określonego języka asemblera optymalizującego kod. Pierwsza optymalizacja musiała przesuwać wiele zmiennych do zbioru rejestrów 80x86. Poniższy kod dostarcza takiej optymalizacji. Chociaż poprawa jedynie czas wykonania o 2 sekundy, jest to poprawa o 33% (z sześciu do czterech sekund)! ; IMGPRCS.ASM ; ; Program przetwarzający obrazek ; Program ten rozmywa obrazek w ośmiobitowej skali szarości poprzez piksela w tym obrazku z ośmioma ; pikselami wokół. Średnia jest obliczana z (CurCell*8 + pozostałe 8 komórek)/ 16 , obciążając aktualną ; komórkę o 50%. ; ; Ponieważ rozmiar obrazka to prawie 64 K, matryce wejściowa i wyjściowa są w różnych segmentach. ; ; Wersja #1: Proste tłumaczenie z Pascala na Asembler ; Wersja #2: Trzy główne optymalizacje. (1) używa instrukcji movsd zamiast pętli do kopiowania danych z ; DataOut z powrotem do DataIn. (2) Używa formy repeat...until dla wszystkich pętli (3) rozwija najskrytsze ; dwie pętle (które są odpowiedzialne za większość poprawiania wydajności) ; Wersja #3: Użycie rejestrów dla wszystkich zmiennych. Ustawia rejestry segmentu raz dla wszystkich ; wykonań głównej pętli wiec kod nie musi ponownie ładować ds. za każdym razem. Oblicza indeks do każdego
; wiersza tylko raz (na zewnątrz pętli j) ; ; Porównanie wydajności (system 66MHz 80486 DX/2) ; ; Ten kod4 sekundy ; 1 optymalizacja 6 sekund ; Oryginalny kod ASM 36 sekund ; ; print byte
„Obliczanie wyniku”,cr,lf,0
; Kopiowanie danej wejściowej do bufora wyjściowego hloop:
rep
iloop:
mov mov mov mov lea lea mov movsd
ax, InSeg es, ax ax, OutSeg ds, ax si, DataOut di, DataIn cx, (251*256) / 4
assume mov mov mov mov mov mov mov mov
ds:InSeg, es:OutSeg ax, InSeg ds, ax ax, OutSeg es, ax cl, 249 bh, cl bl, 1 ch, 254
mov mov mov mov add mov add mov add mov add mov add mov add mov add
dx, 0 ah, dh dl, DataIn[bx-257] al, DataIn[bx-256] dx, ax al, dataIn[bx-255] dx, ax al, DataIn[bx-1] dx, ax al, DataIn[bx+1] dx, ax al, DataIn[bx+255] dx, ax al, DataIn[bx+256] dx, ax al, DataIn[bx+257] dx, ax
;tu obliczamy sumę
mov shl add shr mov
al, DataIn[bx] ax, 3 dx ,ax dx, 4 DataOut[bx], dl
;DataIn[i[j] ;DataIn[I][j]*8
;i*256 ; start przy j =1 ; # ilości pętli
jloop: ;DataIn[i-1][j-1] ;DataIn[I-1][j] ;DatIn[i-1][j+1] ;DataIn[I][j-1] ;DataIn[i][j+1] ;DataIn[i+1][j-1] ;DataIn[I+j][j] ;DataIn[I+1][j+1]
;dzielenie przez 16
Done:
inc dec jne
bx ch jloop
dec jne
cl iloop
dec jne print byte
bp hloop “Zapisywanie wyniku”, cr, lf, 0
; < Więcej usuniętego kodu znajduje się tutaj > Zauważmy ,że przy każdej iteracji. Powyższy kod kopiuje daną wyjściową z powrotem jako daną wejściową. Jest to prawie 6 i pól megabajta danych przesuniętych dla 100 iteracji!!! Poniższa wersja programu rozmywającego rozwija dwukrotnie hloop. Pierwsze wystąpienie kopiuje dane z DataIn do DataOut podczas obliczania rozmycia, druga instancja kopiuje dane z DataOut z powrotem do DataIn podczas rozmywania obrazu. Stosując te dwie sekwencje kodu, program zachowuje kopiowanie danych z jednego punktu do innego. Wersja ta również utrzymuje popularne obliczanie pomiędzy dwoma sąsiadującymi komórkami zachowując kilka instrukcji w pętli najskrytszej. Wersja ta układa instrukcje w pętli najskrytszej pomagając uniknąć hazardu danych na procesorze 80486 i późniejszych. Końcowy wynik jest prawie 40% szybszy niż wersji poprzedniej (w dół o 2,5 sekundy z czterech sekund). ; IMGPRCS.ASM ; ; Program przetwarzający obrazek ; Program ten rozmywa obrazek w ośmiobitowej skali szarości poprzez piksela w tym obrazku z ośmioma ; pikselami wokół. Średnia jest obliczana z (CurCell*8 + pozostałe 8 komórek)/ 16 , obciążając aktualną ; komórkę o 50%. ; ; Ponieważ rozmiar obrazka to prawie 64 K, matryce wejściowa i wyjściowa są w różnych segmentach. ; ; Wersja #1: Proste tłumaczenie z Pascala na Asembler ; Wersja #2: Trzy główne optymalizacje. (1) używa instrukcji movsd zamiast pętli do kopiowania danych z ; DataOut z powrotem do DataIn. (2) Używa formy repeat...until dla wszystkich pętli (3) rozwija najskrytsze ; dwie pętle (które są odpowiedzialne za większość poprawiania wydajności) ; Wersja #3: Użycie rejestrów dla wszystkich zmiennych. Ustawia rejestry segmentu raz dla wszystkich ; wykonań głównej pętli wiec kod nie musi ponownie ładować ds. za każdym razem. Oblicza indeks do każdego ; wiersza tylko raz (na zewnątrz pętli j) ; Wersja #4: Eliminuje kopiowanie danych z DataOut do DataIn w każdym kroku. Usuwa hazardy. Utrzymuje ; pdwyrażenia ; Porównanie wydajności (system 66MHz 80486 DX/2) ; ; Ten kod2,5undy ; 2optymalizacja 4 sekund ; 1 optymalizacja 6 sekund ; Oryginalny kod ASM 36 sekund ;
print byte
„Obliczanie wyniku”, cr, lf,0
assume ds.:InSeg, es:OutSeg mov mov
ax, InSeg ds., ax
mov mov
ax, OutSeg es, ax
; Kopiowanie danych raz, więc uzyskujemy brzegi w obu tablicach mov cx, (251*256) / 4 lea si, DataIn lea di, DataOut rep movsd ; „hloop” powtarzana raz dla każdej iteracji hloop: mov mov mov mov
ax, InSeg ds., ax ax, OutSeg es, ax
; „iloop” przetwarza wiersze w matrycach mov cl, 249 iloop: mov bh, cl ;i *256 mov bl, 1 ; zaczynamy od j =1 mov ch, 254/2 mov si, bx mov dh, 0 ;tu obliczamy sumę mov bh, 0 mov ah, 0 ; „jloop” przetwarza pojedyncze elementy tablicy. Pętla ta będzie rozwinięta raz pozwalając podzielić ; na dwie części obliczenia jloop: ; Suma DataIn[i-1[j] + DataIn[i-1][j+1]+DataIn[i+1][j] + DataIn[i+1][j+1] będzie używana w drugiej ; połowie obliczeń. Wiec zachowujemy jej wartość w rejestrze (di) dopóki nie będziemy jej potrzebować mov mov mov add mov add mov add mov
dl, DataIn[si-256] al, DataIn[si-255] bl, DataIn[si+257] dx, ax al, DataIn[si+256] dx, bx bl, DataIn[si+1] dx, ax al, DataIn[si+255]
;[i-1,j] ;[i-1, j+1] ;[I+1, j+1]
mov
di, dx
;Zachowanie częściowych wyników
add mov add mov add mov shl add add shr shr add mov
dx, bx bl, DataIn[si-1] dx ,ax al, dataIn[si] dx, bx bl, DataIn[si-257] ax, 3 dx, bx dx, ax ax, 3 dx, 4 di, ax DataOut[si], di
;[i+1,j] ;[i,j+1] ;[i+1, j-1]
;[i,j-1] ;[i,j] ;[I-1,j-1] ;DataIn[I,j*8 ;przywrócenie DataIn[i,j] ;dzielenie przez 16
; Okay, przetwarzamy kolejną komórkę. Zauważmy, że mamy już częściową sumę usytuowaną w DI. ; Nie zapomnijmy, że nie mamy SI w tym miejscu. (To jest druga połówka rozwijanej pętli) mov mov mov add mov add mov add shl add add mov shr dec mov jne dec jne
dx, di bl, DataIn[si-254] al, DataIn[si+2] dx, bx bl, DataIn[si+258] dx, ax al, DataIn[si+1] dx, bx ax, 3 si, 2 dx ,ax ah, 0 dx, 4 ch DataOut[si-1], dl jloop cl iloop
dec je
bp Done
;suma częściowa ;[i-1, j+1] ;[i, j+1] ;[i+1,j+1] ;[I,j] ;DataIn[I][j]* 8 ;zerowanie dla kolejnej iteracji ;dzielenie przez 16
; Specjalny przypadek, więc nie musimy przesuwać danej pomiędzy dwoma tablicami. Jest to wersja pętli ; rozwijanej hloop, która zamienia wejście i wyjście tablic , więc nie musimy przesuwać danej w pamięci mov mov mov mov assume
ax, OutSeg ds., ax ax, InSeg es, ax es:InSeg, ds.: OutSeg
mov mov mov mov mov mov mov mov
cl, 249 bh, cl bl, 1 ch, 254/2 si, bx dh, 0 bh, 0 ah, 0
mov mov mov add mov add mov add mov
dl, DataOut[si-256] al, dataOut[si-255] bl, DataOut[si+257] dx ,ax al, DataOut[si+256] dx, bx bl, DataOut[si+1] dx, ax al, DataOut[si+255]
mov
di, dx
add mov add
dx, bx bl, DataOut[si-1] dx, ax
hloop2: iloop2:
jloop2:
mov add mov shl add add shr shr mov mov mov add mov add mov add mov add shl add add mov shr dec mov jne dec jne dec je jmp
al, DataOut[si] dx, bx bl, DatOut[si-257] ax, 3 dx, bx dx ,ax ax, 3 dx, 4 DataIn[si], dl dx, di bl, DataOut[si-254] dx, ax al, DataOut[si+2] dx, bx bl, DataOut[si+258] dx, ax al, dataOut[si+1] dx, bx ax, 3 si, 2 dx, ax ah, 0 dx, 4 ch DataIn[si-1], dl jloop2 cl iloop2 bp Done2 hloop
; Łata gwarantująca, że dana zawsze znajduje się w segmencie wyjściowym Done2:
rep Done: ;
mov mov mov mov mov lea lea movsd print Byte
ax, InSeg ds, ax ax, OutSeg es, ax cx, (251*256) / 4 si, DataIn di, DataOut
“Zapisanie wyników”, cr, lf,0
Kod ten dostarcza dobrego przykładu tego rodzaju optymalizacji, którego boi się większość ludzi .jest wiele cykli obliczeń, szeregowanie instrukcji i innych szalonych rzeczy, które czynią program trudniejszym do odczytu i zrozumienia. Jest to rodzaj optymalizacji, z którego są znani programiści asemblerowi; rzecz , która daje początek zdaniu „nigdy nie optymalizuj wcześnie” . Nie powinniśmy nigdy próbować tego typu optymalizacji dopóki nie wyczerpiemy wszystkich innych możliwości. Pisząc nasz kod w taki sposób, uczynimy go bardzo trudnym dla dokonywania dalszych zmian w nim. Nawiasem mówiąc, powyższy kod zabiera około 15 godzin dla rozwinięcia i zdebuggowania (debuggowanie zabiera najwięcej czasu) Przyczyni się to do poprawy o 0.1 sekundy (dla 100 iteracji) na każdą godzinę pracy. Chociaż kod ten z pewnością nie jest w pełni optymalny, trudno jest uzasadnić próby zajmowania więcej czasu na poprawianie tego kodu przez mechaniczne działania (np. przesuwanie instrukcji itp.) ponieważ wydajność może poprawi się odrobinę.
W powyższych czterech krokach zredukowaliśmy czas działania kodu asemblerowego z 36 sekund do 2.5 sekundy. Całkiem imponujący wyczyn. Jednakże, nie powinniśmy uwierzyć ,że był to łatwy sposób lub nawet ,że były to tylko cztery zawiłe kroki Podczas faktycznego rozwoju tego przykładu, było wiele prób, które nie poprawiły wydajności (faktycznie, niektóre modyfikacje redukowały wydajność) a inne nie poprawiały wydajności wystarczająco uzasadniając ich wdrożenie. Aby zademonstrować ten ostatni punkt, poniższy kod zawiera główne zmiany w sposobie organizacji danych. Pętla główna działa na obiektach 16 bitowych w pamięci zamiast obiektach 8 bitowych,. W niektórych maszynach z dużym zewnętrznym cachem (256K lub lepszym) algorytm ten dostarcza drobnej poprawy wydajności (2.4 sekundy z 2.5 sekundy). Jednakże, na innej maszynie działa wolniej. Dlatego też, kod ten nie może być traktowany jako końcowa implementacja: ; IMGPRCS.ASM ; ; Program przetwarzający obrazek ; Program ten rozmywa obrazek w ośmiobitowej skali szarości poprzez piksela w tym obrazku z ośmioma ; pikselami wokół. Średnia jest obliczana z (CurCell*8 + pozostałe 8 komórek)/ 16 , obciążając aktualną ; komórkę o 50%. ; ; Ponieważ rozmiar obrazka to prawie 64 K, matryce wejściowa i wyjściowa są w różnych segmentach. ; ; Wersja #1: Proste tłumaczenie z Pascala na Asembler ; Wersja #2: Trzy główne optymalizacje. (1) używa instrukcji movsd zamiast pętli do kopiowania danych z ; DataOut z powrotem do DataIn. (2) Używa formy repeat...until dla wszystkich pętli (3) rozwija najskrytsze ; dwie pętle (które są odpowiedzialne za większość poprawiania wydajności) ; Wersja #3: Użycie rejestrów dla wszystkich zmiennych. Ustawia rejestry segmentu raz dla wszystkich ; wykonań głównej pętli wiec kod nie musi ponownie ładować ds. za każdym razem. Oblicza indeks do każdego ; wiersza tylko raz (na zewnątrz pętli j) ; Wersja #4: Eliminuje kopiowanie danych z DataOut do DataIn w każdym kroku. Usuwa hazardy. Utrzymuje ; podwyrażenia ; Wersja #5; Konwertuje tablicę danych do słów zamiast bajtów i działają na wartościach 16 bitowych. Dają ; minimalne przyspieszenie ; ; Porównanie wydajności (system 66 MHz 80486 DX/2) ; ; Ten kod 2,4 sekundy ; 3 optymalizacja 2.5 sekundy ; 2 optymalizacja 4 sekundy ; 1 optymalizacja 6 sekund ; Oryginalny kod ASM 36 sekund
dseg
.xlist include stdlib.a includelib stdlib.lib .list .386 option segment :use16 segment para public ‘data’
ImgData InName OutName Iterations
byte byte byte word
dseg
ends
251 dup (256 dup (?)) „roller1.raw”, 0 „roller2.raw”, 0 0
; Kod ten czyni niestosowne założenie, że poniższe segmenty są ładowane sąsiednio w pamięci! Również, ; ponieważ te segmenty są wyrównane paragrafem, kod ten zakłada , że segmenty te będą zawierały pełne ; 65, 536 bajtów. Nie możemy zadeklarować segmentu z dokładnie 65,536 bajtami w MASM. Jednakże, ; opcja wyrównania paragrafem zakłada ,że te ekstra bajty są dodawane na końcu każdego segmentu.
DataSeg1 Data1a DataSeg
segment para public ‘ds1’ byte 65535 dup (?) ends
DataSeg Data1b DataSeg2
segment para public ‘ds2’ byte 65536 dup (?) ends
DataSeg3 Data2a DataSeg3
segment para piblic ‘ds3’ byte 65535 dup (?) ends
DataSeg4 Data2b DataSeg4
segment para public ‘ds4’ byte 65535 dup (?) ends
cseg
segment para public ‘code’ assume cs: cseg, ds: dseg
Main
proc mov ax, dseg mov ds, ax meminit
GoodOpen:
GoodRead:
mov lea int jnc print byte jmp
ax, 3d00h dx, InName 21h GoodOpen
mov lea mov mov int cmp je print byte jmp
bx, ax dx, ImgData cx, 256 * 251 ah, 3Fh 21h ax, 256*251 GoodRead
print byte getsm atoi free mov cmp jle
;otwarcie pliku do odczytu
„Nie można otworzyć pliku wejściowego”, cr, lf, 0 Quit ;uchwyt pliku ; rozmiar pliku danych do odczytu ;zobacz czy możemy czytać dane
„Nie odczytano pliku poprawnie”, cr, lf, 0 Quit „Wprowadź liczbę iteracji: „,0
Iterations, ax ax, 0 Quit
printf byte „Wynik obliczony dla %d iteracji”, cr, lf, 0 dword Iterations ; Kopiujemy daną i rozszerzamy ją z ośmiu do szesnastu bitów. Pierwsza pętla obsługuje 32,785 bajtów, ; druga pętla obsługuje pozostałe bajty
CopyLoop:
CopyLoop1:
mov mov mov mov
ax, DataSeg1 es, ax ax, DatSeg3 fs, ax
mov mov lea xor lodsb mov stosw dec jne
ah, 0 cx, 32768 si, ImgData di ,di
mov mov mov mov mov xor lodsb mov stosw dec jne
di, DataSeg2 es, di di, DataSeg4 fs, di cx, (251* 256) – 32768 di ,di
;dana wyjściowa jest pod offsetem zero ;odczyt bajtu ;przechowanie słowa w DataSeg3 ;przechowanie słowa w DataSeg1
fs:[di], ax cx CopyLoop
;odczyt bajtu ;przechowanie słowa w DataSeg4 ;przechowanie słowa w DataSeg2
fs:[di], ax cx CopyLoop1
; hloop kończy jedną iterację przesunięciem danych z Data1a/Data1b do Data2a/Data2b hloop:
mov mov mov mov
ax, DataSeg1 ds, ax ax, DataSeg3 es, ax
; przetwarzanie pierwszych 127 wierszy (65,024 bajtów) tablicy:
iloop0: jloop0:
mov lea mov mov mov mov shl add mov add add add add mov add add add add shl add add shr add
cl ,127 si, Data1a+202h ch, 254/2 dx, [si] bx, [si – 200h] ax, dx dx, 3 bx, [si-1feh] bp, [si+2] bx, [si+200h] dx, bp bx, [si+202h] dx, [si-202h] di, [si-1fch] dx, [si-2] di, [si+4] dx, [si+1feh] di, [si+204h] bp, 3 dx, bx bp, ax dx, 4 bp, bx
;start pod [1,1] ;[i,j] ;[i-1, j] ;[i,j]* 8 ;[i-1, j+1] ;[i,j+1] ;[I+1,j] ;[i+1, j+1] ;[i-1, j-1] ;[i-1, j+2] ;[I, j-1] ;[i, j+2] ;[i+1, j-1] ;[i+1, j+2] ;[i, j+1]*8 ;dzielenie przez 16
mov add add shr dec mov jne
es:[si], dx bp, di si, 4 bp, 4 ch es:[si-2], bp jloop0
add dec jne
si, 4 cl iloop0
;przechowanie wejścia [i, j] ;wpływ kolejnej operacji przechowania ;dzielenie przez 16 ;przechowanie wejścia [i,j+1] ;przeskok do początku kolejnego wiersza
; Przetwarzanie ostatnich 124 wierszy tablicy. Wymaga to tego ,ze przełączamy z jednego segmentu do ; kolejnego .Zauważmy ,że segmenty nakładają się
iloop1: jloop1:
mov sub mov mov sub mov
ax, DataSeg2 ax, 40h ds., ax ax, DataSeg4 ax, 40h es, ax
mov mov mov mov mov mov shl
cl, 251 – 127-1 si, 202h ch, 254/2 dx, [si] bx, [si-200h] ax, dx dx, 3
;pozostałe wiersze do przetworzenia ; kontynuacja z kolejnymi wierszami
add mov add add add add mov add add add add shl add add shr add mov add add shr dec mov jne
bx, [si-1feh] bp, [si+2] bx, [si+200h] dx, bp bx, [si+202h] dx, [si-202h] di, [si-1fch] dx, [si-2] di, [si+4] dx, [si+1feh] di, [si+204h] bp,3 dx, bx bp, ax dx, 4 bp, bx es:[si], dx bp, di si ,4 bp, 4 ch es:[si-2], bp jloop1
;[i-1,j+1 ;[I,j+1] ;[I+1,j]
add dec jne
si, 4 cl iloop1
mov
ax, dseg
;robienie kopii ostatnich dwóch wierszy w DS2 ;robienie kopii ostatnich dwóch wierszy w DS4
;[i,j] ;[i-1,j] ;[i,j]*8
;[i+1, j+1] ;[i-1,j-1] ;[i-1, j+2] ;[i, j-1] ;[i,j+2] ;[i+1,j-1] ;[i+1, j+2] ;[i,j+1]*8 ;dzielenie przez 16 ; przechowanie wejścia [i,j] ;wpływ na kolejną operację przechowania ; przechowanie wejścia [i, j+1] ;skok do początku kolejnego wiersza
mov assume dec je
ds., ax ds.:dseg Iterations Done0
; Rozwijamy pętle iteracji więc możemy przesunąć dane z DataSeg 2/4 z powrotem do datSeg1/3 bez ; marnowania dodatkowego czasu. Inny niż kierunek przesuwania danych, kod ten jest praktycznie identyczny ; do powyższego
iloop2: jloop2:
iloop3: jloop3:
mov mov mov mov
ax, DataSeg3 ds., ax ax, DataSeg1 es, ax
mov lea mov mov mov mov shl add mov add add add add mov add add add add shl add add shr add mov add add shr dec mov
cl, 127 si, Data1a+202h ch, 254/2 dx, [si] bx, [si-200h] ax, dx dx, 3 bx, [si-1feh] bp, [si+2] bx, [si+200h] dx, bp bx, [si+202h] dx, [si-202h] di, [si-1fch] dx, [si-2] di, [si+4] dx, [si+1feh] di, [si+204h] bp, 3 dx, bx bp, ax dx, 4 bp, bx es:[si], dx bp, di si, 4 bp, 4 ch es:[si-2], bp
jne add dec jne mov sub mov mov sub mov mov mov mov mov mov
jloop2 si, 4 cl iloop2 ax, DataSeg4 ax, 40h ds, ax ax, DataSeg2 ax, 40h es, ax cl, 251-127-1 si, 202h ch, 254/2 dx, [si] bx. [si-200h]
Done2: Done0: Finish:
mov shl add mov add add add add mov add add add add shl add add shr add mov add add shr dec mov jne add dec jne mov mov assume dec je jmp mov mov jmp mov mov mov print byte
ax, dx dx, 3 bx, [si-1feh] bp, [si+2] bx, [si+200h] dx, bp bx, [si+202h] dx, [si-202h] di, [si-1fch] dx, [si-2] di, [si+4] dx, [si+1feh] di, [si+204h] bp, 3 dx, bx bp, ax dx, 4 bp, bx es:[si], dx bp, di si, 4 bp, 4 ch es:[si-2], bp jloop3 si, 4 cl iloop3 ax, dseg ds, ax ds:dseg Iterations Done2 hloop ax, DataSeg1 bx, DataSeg2 Finish ax, DataSeg3 bx, DataSeg4 ds, ax “zapisywanie wyniku”, cr,lf, 0
; Konwersja danych powrotnie do bajtu i za[psia do pliku wyjściowego:
CopyLoop3:
CopyLoop4:
mov mov mov lea xor lodsw stosb dec jne
ax, dseg es, ax cx, 32768 di, ImgData si, si
mov mov xor lodsw
ds., bx cx, (251*256) – 32768 si, si
;Dana wyjściowa jest pod offsetem zero ;odczyt słowa z tablicy końcowej ;zapis bajtu do tablicy wyjściowej
cx CopyLoop3
;odczyt końcowej danej słowa
stosb dec jne
;zapis bajtu danej do tablicy wyjściowej cx CopyLoop4
; Okay, zapis danej do pliku wyjściowego
GoodCreate:
GoodWrite: Quit: Main cseg sseg stk sseg zzzzzzseg LastBytes zzzzzzseg
mov mov mov mov lea int jnc print byte jmp
ah, 3ch cx, 0 dx, dseg ds., dx dx, OutName 21h GoodCreate
mov push mov mov lea mov mov int pop cmp je print byte jmp
bx ,ax bx dx, dseg ds, dx dx, ImgData cx, 256*251 ah, 40h 21h bx ax, 256*251 GoodWrite
;tworzenie pliku wyjściowego ;normalny atrybut pliku
„Nie można stworzyć pliku wyjściowego”, cr, lf,0 Quit ;uchwyt pliku ;gdzie dana może zostać znaleziona ;rozmiar danej do zapisu ;operacja zapisu ;przywrócenie uchwyty do zamknięcia ;zobacz czy zapisano dane
„Nie zapisano poprawnie pliku”, cr, lf,0 Quit
mov ah, 3eh int 21h ExitPgm endp ends
;operacja zamknięcia
segment para stack ‘stack’ byte 1024 dup (“stack”) ends segment para public ‚zzzzzz’ byte 16 dup (?) ends end Main
Oczywiście, absolutnie najlepszym sposobem poprawy wydajności każdego kawałka kodu jest lepszy algorytm. Wszystkie powyższe wersje asemblerowe były ograniczone przez pojedyncze wymaganie – musiały tworzyć taki sam plik jak oryginalny program pascalowski. Powyższy przykład optymalizacji jest doskonałym przykładem .Kod asemblerowy wiernie zachowuje semantykę oryginalnego programu Pascala; oblicza średnią ważoną wszystkich wewnętrznych pikseli jako sumę ośmiu sąsiadujących pikseli plus osiem razy bieżącą wartość piksela z całą sumą dzieloną przez16. Teraz jest to dobra funkcja rozmywająca, ale nie jest to jedynie funkcja rozmywająca .Użytkownik Photoshop (lub innego programu przetwarzania obrazu) nie musi martwić się o algorytm jako taki. Kiedy użytkownik wybiera „rozmycie obrazu” chce go jako wyjście z ostrości. Dokładnie jak dużo ostrości jest generalnie nie istotne. Faktycznie, mniejsza jest lepsza ponieważ użytkownik może zawsze uruchomić algorytm rozmycia ponownie (lub określić jakąś liczbę iteracji) poniższy program asemblerowy pokazuje jak uzyskać lepszą wydajność poprzez zmodyfikowanie algorytmu rozmycia redukując liczbę instrukcji potrzebnych do wykonania najskrytszych pętli Oblicza rozmycie poprzez uśrednienie piksela z
czterema sąsiednimi, powyżej poniżej, na lewo i na prawo od bieżącego piksela. Ta modyfikacja dostarcza programu, który uruchamia 100 iteracji w 2,2 sekundy z 12% poprawą w stosunku do poprzednich wersji: ; IMGPRCS.ASM ; ; Program przetwarzający obrazek ; Program ten rozmywa obrazek w ośmiobitowej skali szarości poprzez piksela w tym obrazku z ośmioma ; pikselami wokół. Średnia jest obliczana z (CurCell*8 + pozostałe 8 komórek)/ 16 , obciążając aktualną ; komórkę o 50%. ; ; Ponieważ rozmiar obrazka to prawie 64 K, matryce wejściowa i wyjściowa są w różnych segmentach. ; ; Wersja #1: Proste tłumaczenie z Pascala na Asembler ; Wersja #2: Trzy główne optymalizacje. (1) używa instrukcji movsd zamiast pętli do kopiowania danych z ; DataOut z powrotem do DataIn. (2) Używa formy repeat...until dla wszystkich pętli (3) rozwija najskrytsze ; dwie pętle (które są odpowiedzialne za większość poprawiania wydajności) ; Wersja #3: Użycie rejestrów dla wszystkich zmiennych. Ustawia rejestry segmentu raz dla wszystkich ; wykonań głównej pętli wiec kod nie musi ponownie ładować ds. za każdym razem. Oblicza indeks do każdego ; wiersza tylko raz (na zewnątrz pętli j) ; Wersja #4: Eliminuje kopiowanie danych z DataOut do DataIn w każdym kroku. Usuwa hazardy. Utrzymuje ; podwyrażenia ; Wersja #6: Zmiana algorytmu rozmycia używając kilku obliczeń. Wersja ta NIE tworzy takich samych danych ;jak inne programy ; ; Porównanie wydajności (system 66 MHz 80486 DX/2) ; ; Ten kod 2,2sekundy ; 3 optymalizacja 2.5 sekundy ; 2 optymalizacja 4 sekundy ; 1 optymalizacja 6 sekund ; Oryginalny kod ASM 36 sekund ;
print byte
„Obliczenie wyniku”, cr,lf,0
assume ds.: InSeg, es:OutSeg mov mov mov mov
ax, InSeg ds, ax ax, OutSeg es, ax
;Kopiowanie danej raz więc uzyskujemy brzegi w obu tablicach
rep
mov cx, (251*256)/4 lea si, DataIn lea di, DataOut movsd
; “hloop” powtarzana raz dla każdej iteracji hloop: mov mov mov mov
ax, InSeg ds., ax ax, OutSeg es, ax
; „iloop” przetwarza wiersze w matrycach illop:
mov mov mov mov mov mov mov mov
cl, 249 bh, cl bl, 1 ch, 254 /2 si, bx dh, 0 bh, 0 ah, 0
;i*256 ;star przy j = 1 ;tu obliczam sumę
; „jloop” przetwarza pojedyncze elementy tablicy. Pętla ta rozwijana jest raz pozwalając podzielić obleczenie na ; dwie części. jloop: ; Suma DataIn[i-1][j]+ DataIn[i-1][j+1]+DataIn[i+1][j]+DataIn[i+1][j+1] będzie użyta w drugiej połówce tego ; obliczenia. Więc zapisujemy tą wartość w rejestrze (di) póki jej nie będziemy potrzebowali mov mov shl mov add mov add mov add shl add mov shr add mov mov mov add mov add add shr dec mov jne dec jne dec je
dl, dataIn[si] al, DataIn[si-256] dx, 2 bl, DataIn[si-1] dx ,ax al, dataIn[si+1] dx, bx bl, DataIn[si+256] dx, ax ax,2 dx, bx bl, dataIn[si-255] dx, 3 ax, bx DataOut[si], dl bl, dataIn[si+2] dl, DataIn[si+257] ax, bx bl, dataIn[si] ax, dx ax, bx ax, 3 ch DataOut[si+1], al jloop cl iloop bp Done
;[I,j] ;[i-1, j] ;[i,j]*4 ;[I,j-1] ; [I,j+1] ;[I+1,j] ;[I,j+1]*4 ;[I-1,j+1] ;dzielenie przez 8 ;[i,j+2] ;[I+1,j+1] ;[i,j]
; Specjalny przypadek, wiec nie musimy przesuwać danej pomiędzy dwoma tablicami. Jest to rozwijana wersja hloop, która zmienia tablicę wejściową i wyjściową więc nie musimy danej w pamięci mov mov mov mov assume hloop2:
ax, OutSeg ds., ax ax, InSeg es, ax es:InSeg, ds.:OutSeg
iloop2:
mov mov mov mov mov mov mov mov
cl, 249 bh, cl bl, 1 ch, 254/2 si, bx dh, 0 bh, 0 ah, 0
mov mov mov add mov add mov add mov
dl, dataOut[si-256[ al, DataOut[si-255] bl, DataOut[si+257] dx, ax al, DataOut[si+256] dx, bx bl, DatOut[si+1] dx, ax al, DataOut[si+255]
mov add mov add mov add mov shl add add shr shr mov mov mov add mov add mov add mov add shl add add mov shr dec mov jne dec jne dec je jmp
di, dx dx, bx bl, DataOut[si-1] dx, ax al, DataOut[si] dx, bx bl, dataOut[si-257] ax, 3 dx, bx dx, ax ax, 3 dx, 4 DaatIn[si], dl dx, di bl, DataOut[si-254] dx, ax al, dataOut[si+2] dx, bx bl, datOut[si+258] dx, ax al, DataOut[si+1] dx, bx ax, 3 si, 2 dx, ax ah, 0 dx, 4 ch DataIn[si-1], dl jloop2 cl iloop2 bp Done2 hloop
jloop2:
; Łatka gwarantuje ,że dana zawsze pozostaje w segmencie danych Done2:
rep Done: ;
mov mov mov mov mov lea lea movsd print byte
ax, InSeg ds, ax ax, OutSeg es, ax cx, (251*256) /4 si, DataIn di, DataOut “Zapisanie wyniku”, cr, lf, 0
< Pozostały usunięty kod tutaj, zobacz program oryginalny >
Jedną ważną rzeczą do zapamiętania o kodzie w tej sekcji jest to ,że optymalizujemy go dla 100 iteracji. Jednak okazuje się ,że te optymalizacje stosuje się równie dobrze do większej ilości iteracji. Co niekoniecznie może być prawdą dla kilu iteracji. W szczególności, jeśli uruchamiamy tylko jedną iterację, każde skopiowanie danej na koniec operacji ułatwi skonsumowanie dużej części czasu zachowanego przez optymalizację. Ponieważ rzadko użytkownik będzie rozmywał obrazek 100 razy , nasza optymalizacja może nie być tak dobra jak można ją uczynić. Jednakże, sekcja ta dostarcza dobrego przykładu kroków jakie musimy wykonać aby zoptymalizować dany program. Sto iteracji było dobrym wyborem dla tego przykładu ponieważ było łatwiej zmierzyć czas działania wszystkich wersji programu. Jednak, musimy pamiętać ,że powinniśmy optymalizować nasze programy w przypadkach szczególnych a nie w każdym 25.6 PODSUMOWANIE Program komputerowy działa często znacznie wolniej niż wymaga tego zadanie. Proces zwiększania szybkości programu jest znany jako optymalizacja Niestety optymalizacja jest trudnym i czasochłonnym zadaniem, czasami nie da się wykonać lekko. Wielu programistów często optymalizuje swoje programy zanim określa czy muszą to robić czy nie lub (co gorsza) optymalizują tylko część programu uznając ,ze muszą przepisać ten kod po zoptymalizowaniu. Inni, ignoranci, często zabierają się do optymalizacji złej sekcji programu .Ponieważ optymalizacja jest powolnym i trudnym procesem, chcemy spróbować i uczynić ,że optymalizujemy nasz kod tylko raz. Sugeruje to ,że optymalizacja powinna być ostatnim zadaniem, kiedy piszemy program. Jedna szkoła mówi o filozofii grupy Późnej Optymalizacji. Ich argumentacją jest to, że zoptymalizowany program często niszczy czytelność i pielęgnowalność programu. Dlatego też powinno się tylko wybrać ten krok kiedy jest to absolutnie konieczne i tylko na końcowym etapie projektowanie programu. Grupa Wczesnej Optymalizacji, z doświadczenia wie, że programy, które nie są napisane jako szybkie, często muszą być przepisane kompletnie aby uczynić je szybszymi. Dlatego też, oni często przyjmują postawę, że optymalizacja powinna mieć miejsce razem ze zwykłym projektowaniem programu. Generalnie, punkt widzenia grupy wczesnej optymalizacji jest daleko różniący się od grupy późnej optymalizacji. Grupa wczesnej optymalizacji zakłada ,że dodatkowy czas spędzony nad optymalizacją programu podczas projektowania wymaga mniej czasu niż zaprojektowanie programu a potem jego optymalizacja. *:kiedy optymalizować, kiedy nie optymalizować” Po napisaniu programu i określeniu, że działa zbyt wolno, kolejnym krokiem jest zlokalizowanie kodu który działa zbyt wolno. Po zidentyfikowaniu wolnej sekcji naszego programu, możemy popracować nad przyspieszeniem naszego programu. Zlokalizowanie 10% kodu, który zabiera 90% czasu wykonania nie zawsze jest łatwym zadaniem. Czterema powszechnymi technikami używanymi przez ludzi są prób i błędów, optymalizacji wszystkiego, analiza programu i analiza eksperymentalna (tj. użycie profilu). Znalezienie „gorących miejsc” pierwszym krokiem optymalizacji. *”Jak znaleźć powolny kod w naszych programach” Argumentem używanym przez ludzi przekonującym do późniejszej optymalizacji jest to ,że maszyny są szybkie więc optymalizacja jest rzadko konieczna. Chociaż ten argument jest często wyolbrzymiany, często jest prawdą, że wiele nie zoptymalizowanych programów działających dość szybko i nie wymagają żadnej optymalizacji dla zadawalającej wydajności. Z drugiej strony, programy, które działają lepiej mogą być zbyt wolne kiedy są uruchamiane z innym oprogramowaniem.
*”Czy optymalizacja jest konieczna?” Są trzy formy optymalizacji jakie możemy zastosować dla poprawy wydajności programu: wybranie lepszego algorytmu, wybór lepszej implementacji algorytmu, lub „zliczanie cykli”. Wielu ludzi (zwłaszcza z grupy późnej optymalizacji) tylko rozważa przypadek późniejszej „optymalizacji”. To wstyd, ponieważ ostatni przypadek często tworzy najmniejszy przyrost poprawy wydajności. *”Trzy typy optymalizacji” Optymalizacja nie jest czymś czego możemy się nauczyć z książki. Potrzeba dużo doświadczenia i praktyki. Niestety, ci z niewielkim doświadczeniem praktycznym odkrywają, że ich wysiłki rzadko przynoszą efekty i generalnie zakładają, że optymalizacja nie jest warta wysiłku. Prawda jest taka, ze oni nie mają wystarczającego doświadczenia w pisaniu naprawdę optymalnego kodu a ich frustracja uniemożliwia wykorzystanie takiego doświadczenia Ostatnie części tego rozdziału poświęcono zademonstrowaniu co można osiągnąć kiedy optymalizujemy program. Pamiętaj zawsze o tym przykładzie, kiedy odczuwasz frustrację i jako początkujący wierzysz ,że nie można poprawić wydajności swojego programu *”Poprawianie implementacji algorytmu”