Menu English Ukrainian Rosyjski Strona główna

Bezpłatna biblioteka techniczna dla hobbystów i profesjonalistów Bezpłatna biblioteka techniczna


Notatki z wykładów, ściągawki
Darmowa biblioteka / Katalog / Notatki z wykładów, ściągawki

Informatyka i technologie informacyjne. Notatki z wykładu: krótko, najważniejsze

Notatki z wykładów, ściągawki

Katalog / Notatki z wykładów, ściągawki

Komentarze do artykułu Komentarze do artykułu

Spis treści

  1. Wprowadzenie do informatyki (Informatyka. Informacja. Prezentacja i przetwarzanie informacji. Systemy liczbowe. Reprezentacja liczb w komputerze. Sformalizowana koncepcja algorytmu)
  2. Język pascalowy (Wprowadzenie do Pascala. Standardowe procedury i funkcje. Operatory Pascala)
  3. Procedury i funkcje (Pojęcie algorytmu pomocniczego. Procedury w języku Pascal. Funkcje w języku Pascal. Opisy wyprzedzające i łączenie podprogramów. Dyrektywa)
  4. Podprogramy (Parametry rutynowe. Typy parametrów podprogramów. Typ stringu w Pascalu. Procedury i funkcje dla zmiennych typu string. Rekordy. Zbiory)
  5. Pliki (Pliki. Operacje na plikach. Moduły. Rodzaje modułów)
  6. Pamięć dynamiczna (Typ danych referencyjnych. Pamięć dynamiczna. Zmienne dynamiczne. Praca z pamięcią dynamiczną. Wskaźniki bez typu)
  7. Abstrakcyjne struktury danych (Abstrakcyjne struktury danych. Stosy. Kolejki)
  8. Struktury danych drzewa (Struktury danych drzewiastych. Operacje na drzewach. Przykłady realizacji operacji)
  9. Liczy (Pojęcie wykresu. Metody reprezentacji wykresu. Reprezentacja wykresu za pomocą listy przypadków. Algorytm przechodzenia w głąb wykresu. Reprezentacja wykresu w postaci listy list. Algorytm przechodzenia wszerz dla wykresu )
  10. Typ danych obiektu (Typ obiektu w Pascalu. Pojęcie obiektu, jego opis i zastosowanie. Dziedziczenie. Tworzenie instancji obiektów. Komponenty i zakres)
  11. Metody (Metody. Konstruktory i destruktory. Destruktory. Metody wirtualne. Pola danych obiektowych i formalne parametry metod)
  12. Zgodność typu obiektu (Enkapsulacja. Obiekty rozszerzalne. Zgodność typów obiektów)
  13. Monter (O asemblerze. Model oprogramowania mikroprocesora. Rejestry użytkownika. Rejestry ogólnego przeznaczenia. Rejestry segmentowe. Rejestry statusowe i kontrolne)
  14. Rejestry (Rejestry systemu mikroprocesorowego. Rejestry sterujące. Rejestry adresowe systemu. Rejestry debugujące)
  15. Programy montażowe (Struktura programu w asemblerze. Składnia asemblera. Operatory porównania. Operatory i ich pierwszeństwo. Uproszczone dyrektywy definicji segmentów. Identyfikatory tworzone przez dyrektywę MODEL. Modele pamięci. Modyfikatory modeli pamięci)
  16. Struktury instrukcji montażu (Struktura instrukcji maszynowej. Metody określania argumentów instrukcji. Metody adresowania)
  17. Polecenia (Polecenia przesyłania danych. Polecenia arytmetyczne)
  18. Polecenia transferu sterowania (Polecenia logiczne. Tablica prawdy dla logicznej negacji. Tabela prawdy dla logicznego LUB włącznie. Tablica prawdy dla logicznego AND. Tabela prawdy dla logicznego wyłącznego OR. Znaczenie skrótów w nazwie polecenia jcc. Lista poleceń skoku warunkowego dla polecenia. Skok warunkowy polecenia i flagi)

WYKŁAD nr 1. Wstęp do informatyki

1. Informatyka. Informacja. Reprezentacja i przetwarzanie informacji

Informatyka zajmuje się sformalizowanym przedstawieniem obiektów i struktur ich relacji w różnych dziedzinach nauki, technologii i produkcji. Do modelowania obiektów i zjawisk wykorzystywane są różne narzędzia formalne, takie jak formuły logiczne, struktury danych, języki programowania itp.

W informatyce tak podstawowe pojęcie jak informacja ma różne znaczenia:

1) formalne przedstawienie zewnętrznych form informacji;

2) abstrakcyjne znaczenie informacji, jej zawartość wewnętrzna, semantyka;

3) stosunek informacji do świata rzeczywistego.

Ale z reguły informacja jest rozumiana jako jej abstrakcyjne znaczenie - semantyka. Interpretując reprezentację informacji, uzyskujemy jej znaczenie, semantykę. Dlatego jeśli chcemy wymieniać informacje, potrzebujemy spójnych poglądów, aby nie naruszyć poprawności interpretacji. W tym celu interpretacja reprezentacji informacji jest utożsamiana z pewnymi strukturami matematycznymi. W takim przypadku przetwarzanie informacji może odbywać się za pomocą rygorystycznych metod matematycznych.

Jednym z matematycznych opisów informacji jest jej reprezentacja w postaci funkcji y =f(x, t), gdzie t to czas, x to punkt w pewnym polu, w którym mierzona jest wartość y. W zależności od parametrów funkcji chi (informacje można klasyfikować.

Jeżeli parametry są wielkościami skalarnymi, które przyjmują ciągłą serię wartości, to uzyskaną w ten sposób informację nazywamy ciągłą (lub analogową). Jeśli parametry mają określony krok zmiany, wówczas informacja nazywana jest dyskretną. Informacja dyskretna jest uważana za uniwersalną, ponieważ dla każdego określonego parametru można uzyskać wartość funkcji z określonym stopniem dokładności.

Informacja dyskretna jest zwykle utożsamiana z informacją cyfrową, co jest szczególnym przypadkiem informacji symbolicznej o reprezentacji alfabetycznej. Alfabet to skończony zbiór symboli dowolnej natury. Bardzo często w informatyce dochodzi do sytuacji, w której znaki jednego alfabetu muszą być reprezentowane przez znaki innego, czyli musi zostać wykonana operacja kodowania. Jeśli liczba znaków alfabetu kodującego jest mniejsza niż liczba znaków alfabetu kodującego, sama operacja kodowania nie jest skomplikowana, w przeciwnym razie konieczne jest użycie stałego zestawu znaków alfabetu kodującego w celu jednoznacznego poprawnego kodowania.

Jak pokazała praktyka, najprostszym alfabetem, który pozwala zakodować inne alfabety, jest alfabet binarny, składający się z dwóch znaków, które zwykle są oznaczone 0 i 1. Używając n znaków alfabetu binarnego, można zakodować 2n znaków i to wystarczy zakodować dowolny alfabet.

Wartość, która może być reprezentowana przez symbol alfabetu binarnego, nazywana jest minimalną jednostką informacji lub bitem. Sekwencja 8 bitów - bajty. Alfabet zawierający 256 różnych 8-bitowych sekwencji nazywany jest alfabetem bajtowym.

Jako standard w dzisiejszych czasach w informatyce przyjmuje się kod, w którym każdy znak jest zakodowany przez 1 bajt. Są też inne alfabety.

2. Systemy liczbowe

System liczbowy to zestaw zasad dotyczących nazywania i pisania liczb. Istnieją pozycyjne i niepozycyjne systemy liczbowe.

System liczbowy nazywa się pozycyjnym, jeśli wartość cyfry liczby zależy od położenia cyfry w liczbie. W przeciwnym razie nazywa się to niepozycyjnym. Wartość liczby jest określona przez położenie tych cyfr w liczbie.

3. Reprezentacja liczb w komputerze

Procesory 32-bitowe mogą pracować z maksymalnie 232-1 RAM, a adresy mogą być zapisywane w zakresie 00000000 - FFFFFFFF. Jednak w trybie rzeczywistym procesor pracuje z pamięcią do 220-1, a adresy mieszczą się w zakresie 00000 - FFFFF. Bajty pamięci można łączyć w pola o stałej i zmiennej długości. Słowo to pole o stałej długości składające się z 2 bajtów, podwójne słowo to pole 4 bajtów. Adresy pól mogą być parzyste lub nieparzyste, a adresy parzyste są szybsze.

Liczby stałoprzecinkowe są reprezentowane w komputerach jako liczby całkowite binarne, a ich rozmiar może wynosić 1, 2 lub 4 bajty.

Binarne liczby całkowite są reprezentowane w uzupełnieniu do dwóch, a liczby stałoprzecinkowe są przedstawiane w uzupełnieniu do dwóch. Ponadto, jeśli liczba zajmuje 2 bajty, wówczas strukturę liczby zapisuje się według następującej zasady: najbardziej znacząca cyfra jest przypisana do znaku liczby, a reszta do cyfr binarnych liczby. Kod dopełniający liczby dodatniej jest równy samej liczbie, a kod dopełniający liczby ujemnej można otrzymać za pomocą następującego wzoru: x = 10i - \x\, gdzie n to pojemność cyfrowa liczby.

W systemie liczb binarnych dodatkowy kod uzyskuje się poprzez odwracanie bitów, czyli zastąpienie jednostek zerami i odwrotnie oraz dodanie jedynki do najmniej znaczącego bitu.

Liczba bitów mantysy określa precyzję reprezentacji liczb, liczba bitów porządku maszynowego określa zakres reprezentacji liczb zmiennoprzecinkowych.

4. Sformalizowana koncepcja algorytmu

Algorytm może istnieć tylko wtedy, gdy w tym samym czasie istnieje jakiś obiekt matematyczny. Sformalizowane pojęcie algorytmu związane jest z pojęciem funkcji rekurencyjnych, normalnych algorytmów Markowa, maszyn Turinga.

W matematyce funkcję nazywa się jednowartościową, jeśli dla dowolnego zestawu argumentów istnieje prawo, zgodnie z którym określa się unikalną wartość funkcji. Algorytm może działać jako takie prawo; w tym przypadku mówi się, że funkcja jest obliczalna.

Funkcje rekurencyjne są podklasą funkcji obliczalnych, a algorytmy definiujące obliczenia nazywane są algorytmami towarzyszących funkcji rekurencyjnych. Po pierwsze, podstawowe funkcje rekurencyjne są ustalone, dla których towarzyszący algorytm jest trywialny, jednoznaczny; następnie wprowadza się trzy reguły - operatory podstawienia, rekurencji i minimalizacji, za pomocą których uzyskuje się bardziej złożone funkcje rekurencyjne na podstawie funkcji podstawowych.

Podstawowymi funkcjami i towarzyszącymi im algorytmami mogą być:

1) funkcja n zmiennych niezależnych, identycznie równych zero. Wtedy, jeśli znakiem funkcji jest φn, to niezależnie od liczby argumentów wartość funkcji powinna być równa zero;

2) funkcja tożsamości n zmiennych niezależnych postaci ψni. Następnie, jeśli znakiem funkcji jest ψni, to wartość funkcji należy przyjąć jako wartość i-tego argumentu, licząc od lewej do prawej;

3) Λ jest funkcją jednego niezależnego argumentu. Następnie, jeśli znakiem funkcji jest λ, to wartość funkcji należy przyjąć jako wartość następującą po wartości argumentu. Różni uczeni proponowali własne podejścia do sformalizowanego

reprezentacja algorytmu. Na przykład amerykański naukowiec Church zasugerował, że klasa funkcji obliczalnych jest wyczerpana przez funkcje rekurencyjne i w rezultacie niezależnie od algorytmu przetwarzającego jeden zbiór nieujemnych liczb całkowitych na inny, istnieje algorytm towarzyszący funkcji rekurencyjnej, który jest równoważny podanemu. Dlatego jeśli nie można skonstruować funkcji rekurencyjnej do rozwiązania danego problemu, to nie ma algorytmu do jego rozwiązania. Inny naukowiec, Turing, opracował wirtualny komputer, który przetwarzał wejściową sekwencję znaków na dane wyjściowe. W związku z tym postawił tezę, że każda funkcja obliczalna jest obliczalna według Turinga.

WYKŁAD № 2. Język Pascal

1. Wprowadzenie do języka Pascal

Podstawowe symbole języka - litery, cyfry i znaki specjalne - tworzą jego alfabet. Język Pascal zawiera następujący zestaw podstawowych symboli:

1) 26 małych liter łacińskich i 26 wielkich liter łacińskich:

ABCDEFGHIJKLMNOPQRSTUVWXYZ

ABCDEFGHIJKLMNOPQRSTU VWXYZ;

2) _ (podkreślenie);

3) 10 cyfr: 0123456789;

4) oznaki czynności:

+ - x / = <> < > <= >= := @;

5) ograniczniki:

., ' ( ) [ ] (..) { } (* *).. : ;

6) specyfikatory: ^ # $;

7) słowa służbowe (zastrzeżone):

ABSOLUTE, ASSEMBLER, AND, ARRAY, ASM, BEGIN, CASE, CONST, CONSTRUCTOR, DESTRUCTOR, DIV, DO, DOWNTO, ELSE, END, EXPORT, EXTERNAL, FAR, FILE, FOR, FORWARD, FUNCTION, GOTO, IF, IMPLEMENTATION, IN, INDEKS, DZIEDZICZONE, W LINII, INTERFEJS, PRZERWANIE, ETYKIETA, BIBLIOTEKA, MOD, NAZWA, BRAK, BLISKO, NIE, OBIEKT, Z, LUB, PAKOWANE, PRYWATNE, PROCEDURA, PROGRAM, PUBLICZNY, NAGRYWANIE, POWTARZANIE, MIESZKAŃCA, ZESTAW, SHL, SHR, STRING, POTEM, DO, TYP, JEDNOSTKA, DO, ZASTOSOWANIA, VAR, WIRTUALNY, PODCZAS, Z, XOR.

Oprócz wymienionych, zestaw podstawowych znaków zawiera spację. Spacji nie można używać wewnątrz podwójnych znaków i słów zastrzeżonych.

Koncepcja typu dla danych

W matematyce zwyczajowo klasyfikuje się zmienne według pewnych ważnych cech. Dokonuje się ścisłego rozróżnienia między zmiennymi rzeczywistymi, złożonymi i logicznymi, między zmiennymi reprezentującymi poszczególne wartości a zbiorem wartości itp. Przy przetwarzaniu danych na komputerze taka klasyfikacja jest jeszcze ważniejsza. W każdym języku algorytmicznym każda stała, zmienna, wyrażenie lub funkcja jest określonego typu.

W Pascalu obowiązuje zasada: typ jest wyraźnie określony w deklaracji zmiennej lub funkcji poprzedzającej jej użycie. Koncepcja typu Pascal ma następujące główne właściwości:

1) dowolny typ danych definiuje zestaw wartości, do których należy stała, które może przyjąć zmienna lub wyrażenie, lub może wytworzyć operacja lub funkcja;

2) rodzaj wartości podanej przez stałą, zmienną lub wyrażenie można określić poprzez ich formę lub opis;

3) każda operacja lub funkcja wymaga argumentów typu stałego i daje wynik typu stałego.

Wynika z tego, że kompilator może wykorzystać informacje o typie do sprawdzenia obliczalności i poprawności różnych konstrukcji.

Typ określa:

1) możliwe wartości zmiennych, stałych, funkcji, wyrażeń należących do danego typu;

2) wewnętrzną formę prezentacji danych w komputerze;

3) operacje i funkcje, które można wykonać na wartościach należących do danego typu.

Należy zauważyć, że obowiązkowy opis typu prowadzi do redundancji w tekście programów, ale taka redundancja jest ważnym narzędziem pomocniczym przy opracowywaniu programów i jest uważana za niezbędną właściwość nowoczesnych języków algorytmicznych wysokiego poziomu.

W Pascalu istnieją skalarne i strukturalne typy danych. Typy skalarne obejmują typy standardowe i typy zdefiniowane przez użytkownika. Typy standardowe obejmują typy całkowite, rzeczywiste, znakowe, logiczne i adresowe.

Typy całkowite definiują stałe, zmienne i funkcje, których wartości są realizowane przez zbiór liczb całkowitych dozwolony na danym komputerze.

Typy rzeczywiste definiują te dane, które są implementowane przez podzbiór liczb rzeczywistych, które są dozwolone w danym komputerze.

Typy zdefiniowane przez użytkownika to enum i range. Typy strukturalne występują w czterech odmianach: tablice, zestawy, rekordy i pliki.

Oprócz wymienionych, Pascal zawiera jeszcze dwa typy - proceduralny i obiektowy.

Wyrażenie językowe składa się ze stałych, zmiennych, wskaźników funkcji, znaków operatora i nawiasów. Wyrażenie definiuje regułę obliczania pewnej wartości. Kolejność obliczania jest określona przez pierwszeństwo (priorytet) operacji w nim zawartych. Pascal ma następujące pierwszeństwo operatorów:

1) obliczenia w nawiasach;

2) obliczanie wartości funkcji;

3) operacje jednorazowe;

4) operacje *, /, div, mod, i;

5) operacje +, -, lub, xor;

6) operacje relacyjne =, <>, <, >, <=, >=.

Wyrażenia są częścią wielu operatorów języka Pascal i mogą być również argumentami funkcji wbudowanych.

2. Standardowe procedury i funkcje

Funkcje arytmetyczne

1. Funkcja Abs (X);

Zwraca wartość bezwzględną parametru.

X jest wyrażeniem typu rzeczywistego lub całkowitego.

2. Funkcja ArcTan(X: Rozszerzona): Rozszerzona;

Zwraca arcus tangens argumentu.

X jest wyrażeniem typu rzeczywistego lub całkowitego.

3. Funkcja exp(X: Real): Real;

Zwraca wykładnik.

X jest wyrażeniem typu rzeczywistego lub całkowitego.

4.Frac (X: Rzeczywisty): Rzeczywisty;

Zwraca część ułamkową argumentu.

X to wyrażenie typu rzeczywistego. Wynikiem jest część ułamkowa X, tj.

Frac(X) = X-Int(X).

5. Funkcja Int(X: Rzeczywista): Rzeczywista;

Zwraca część całkowitą argumentu.

X to wyrażenie typu rzeczywistego. Wynikiem jest część całkowita X, tj. X zaokrąglone do zera.

6. Funkcja Ln(X: Rzeczywista): Rzeczywista;

Zwraca logarytm naturalny (Ln e = 1) wyrażenia X typu rzeczywistego.

7. Funkcja Pi: Rozszerzona;

Zwraca wartość Pi, która jest zdefiniowana jako 3.1415926535.

8. Funkcja Sin (X: Rozszerzony): Rozszerzony;

Zwraca sinus argumentu.

X to wyrażenie typu rzeczywistego. Sin zwraca sinus kąta X w radianach.

9. Funkcja Sqr (X: Rozszerzona): Rozszerzona;

Zwraca kwadrat argumentu.

X to wyrażenie zmiennoprzecinkowe. Wynik jest tego samego typu co X.

10. Function Sqrt (X: Rozszerzony): Rozszerzony;

Zwraca pierwiastek kwadratowy argumentu.

X to wyrażenie zmiennoprzecinkowe. Wynikiem jest pierwiastek kwadratowy z X.

Procedury i funkcje konwersji wartości

1. Procedura Str(X[:szerokość[:dziesiętne]];var S);

Konwertuje liczbę X na reprezentację ciągu zgodnie z

Opcje formatowania szerokości i miejsc dziesiętnych. X jest wyrażeniem typu rzeczywistego lub całkowitego. Width i Decimal są wyrażeniami typu całkowitego. S jest zmienną typu String lub tablicą znaków zakończonych znakiem NULL, jeśli dozwolona jest rozszerzona składnia.

2. Funkcja Chr(X: Bajt): Char;

Zwraca znak z liczbą porządkową X w tabeli ASCII.

3. Funkcja Wysoka (X);

Zwraca największą wartość z zakresu parametru.

4.Funkcja Niska(X);

Zwraca najmniejszą wartość z zakresu parametrów.

5 FunctionOrd(X): Longint;

Zwraca wartość porządkową wyrażenia typu wyliczeniowego. X jest wyrażeniem typu wyliczanego.

6. Funkcja Runda (X: Rozszerzona): Longint;

Zaokrągla wartość rzeczywistą do liczby całkowitej. X to wyrażenie typu rzeczywistego. Round zwraca wartość Longint, która jest wartością X zaokrągloną do najbliższej liczby całkowitej. Jeśli X jest dokładnie w połowie między dwiema liczbami całkowitymi, zwracana jest liczba o największej wartości bezwzględnej. Jeśli zaokrąglona wartość X znajduje się poza zakresem Longint, generowany jest błąd w czasie wykonywania, który można obsłużyć przy użyciu wyjątku EInvalidOp.

7. Funkcja Trunc (X: Extended): Longint;

Obcina wartość typu rzeczywistego do liczby całkowitej. Jeśli zaokrąglona wartość X znajduje się poza zakresem Longint, generowany jest błąd w czasie wykonywania, który można obsłużyć przy użyciu wyjątku EInvalidOp.

8. Procedura Val(S; var V; var Code: Integer);

Konwertuje liczbę z wartości ciągu S na liczbę

reprezentacja V. S - wyrażenie typu string - ciąg znaków tworzący liczbę całkowitą lub rzeczywistą. Jeśli wyrażenie S jest nieprawidłowe, indeks nieprawidłowego znaku jest przechowywany w zmiennej Code. W przeciwnym razie kod jest ustawiony na zero.

Procedury i funkcje pracy z wartościami porządkowymi

1. Procedura Dec(varX [; N: LongInt]);

Odejmuje jeden lub N od zmiennej X. Dec(X) odpowiada X:= X - 1, a Dec(X, N) odpowiada X:= X - N. X jest zmienną typu wyliczeniowego lub typu PChar, jeśli jest rozszerzona składnia jest dozwolona, ​​a N jest wyrażeniem typu integer. Procedura Dec generuje optymalny kod i jest szczególnie przydatna w długich pętlach.

2. Procedura Inc(varX [; N: LongInt]);

Dodaje jeden lub N do zmiennej X. X jest zmienną typu wyliczeniowego lub typu PChar, jeśli dozwolona jest rozszerzona składnia, a N jest wyrażeniem typu całkowitego. Inc (X) pasuje do instrukcji X:= X + 1, a Inc (X, N) pasuje do instrukcji X:= X + N. Procedura Inc generuje optymalny kod i jest szczególnie przydatna w długich pętlach.

3. FunctionOdd(X: LongInt): Boolean;

Zwraca True, jeśli X jest liczbą nieparzystą, w przeciwnym razie False.

4.FunkcjaPred(X);

Zwraca poprzednią wartość parametru. X jest wyrażeniem typu wyliczanego. Wynik jest tego samego typu.

5 Funkcja Succ(X);

Zwraca następną wartość parametru. X jest wyrażeniem typu wyliczanego. Wynik jest tego samego typu.

3. Operatory języka Pascal

Operator warunkowy

Format pełnej instrukcji warunkowej jest zdefiniowany w następujący sposób: Jeśli B to SI inaczej S2; gdzie B jest warunkiem rozgałęzienia (podejmowania decyzji), wyrażeniem logicznym lub relacją; SI, S2 - jedna instrukcja wykonywalna, prosta lub złożona.

Podczas wykonywania instrukcji warunkowej najpierw oceniane jest wyrażenie B, a następnie analizowany jest jego wynik: jeśli B jest prawdziwe, to wykonywana jest instrukcja S1 - gałąź to, a instrukcja S2 jest pomijana; jeśli B jest fałszywe, to instrukcja S2 - wykonywana jest gałąź else, a instrukcja S1 jest pomijana.

Istnieje również skrócona forma operatora warunkowego. Jest zapisany jako: Jeśli B to S.

Operator selekcji

Struktura operatora jest następująca:

przypadki

c1: instrukcja1;

c2: instrukcja2;

...

cn: instrukcjaN;

inna instrukcja

puszki;

gdzie S jest porządkowym wyrażeniem typu, którego wartość jest obliczana;

с1, с2..., сп - stałe typu porządkowego, z którymi porównywane są wyrażenia

S; instrukcja1,..., instrukcjaN - operatory, z których wykonywany jest ten, którego stała odpowiada wartości wyrażenia S;

instrukcja - instrukcja wykonywana jeśli wartość wyrażenia Sylq nie pasuje do żadnej ze stałych c1, c2.... cn.

Ten operator jest uogólnieniem warunkowego operatora If dla dowolnej liczby alternatyw. Istnieje skrócona forma wyrażenia, w której nie ma innej gałęzi.

Instrukcja pętli z parametrem

Instrukcje pętli parametrów rozpoczynające się słowem for powodują, że instrukcja, która może być instrukcją złożoną, jest powtarzana, podczas gdy zmiennej sterującej przypisywana jest rosnąca sekwencja wartości.

Widok ogólny wyciągu for:

for <licznik pętli> := <wartość początkowa> do <wartość końcowa> do <instrukcja>;

W momencie rozpoczęcia wykonywania instrukcji for wartości początkowe i końcowe są określane jednorazowo, a wartości te są zachowywane przez cały czas wykonywania instrukcji for. Instrukcja zawarta w treści instrukcji for jest wykonywana raz dla każdej wartości z zakresu między wartością początkową i końcową. Licznik pętli jest zawsze inicjowany do wartości początkowej. Gdy instrukcja for jest uruchomiona, wartość licznika pętli jest zwiększana z każdą iteracją. Jeśli wartość początkowa jest większa niż wartość końcowa, instrukcja zawarta w treści instrukcji for nie zostanie wykonana. Gdy słowo kluczowe downto jest używane w instrukcji pętli, wartość zmiennej sterującej jest zmniejszana o jeden przy każdej iteracji. Jeśli wartość początkowa w takiej instrukcji jest mniejsza niż wartość końcowa, instrukcja zawarta w treści instrukcji loop nie jest wykonywana.

Jeśli instrukcja zawarta w treści instrukcji for zmienia wartość licznika pętli, jest to błąd. Po wykonaniu instrukcji for wartość zmiennej sterującej staje się niezdefiniowana, chyba że wykonanie instrukcji for zostało przerwane przez instrukcję skoku.

Instrukcja pętli z warunkiem wstępnym

Instrukcja pętli warunku wstępnego (zaczynająca się od słowa kluczowego while) zawiera wyrażenie sterujące powtarzającym się wykonaniem instrukcji (która może być instrukcją złożoną). Kształt cyklu:

Podczas gdy B zrobić S;

gdzie B jest warunkiem logicznym, którego prawdziwość jest sprawdzana (jest to warunek zakończenia pętli);

S - treść pętli - jedna instrukcja.

Wyrażenie sterujące powtarzaniem instrukcji musi być typu Boolean. Jest oceniany przed wykonaniem instrukcji wewnętrznej. Instrukcja wewnętrzna jest wykonywana wielokrotnie, dopóki wyrażenie ma wartość True. Jeśli wyrażenie ma wartość False od początku, instrukcja zawarta w instrukcji pętli warunków wstępnych nie jest wykonywana.

Instrukcja pętli z warunkiem końcowym

W instrukcji pętli z warunkiem końcowym (zaczynającym się od słowa repeat) wyrażenie sterujące powtarzającym się wykonaniem sekwencji instrukcji jest zawarte w instrukcji repeat. Kształt cyklu:

powtarzaj S aż do B;

gdzie B jest warunkiem logicznym, którego prawdziwość jest sprawdzana (jest to warunek zakończenia pętli);

S - jedna lub więcej instrukcji treści pętli.

Wynik wyrażenia musi być typu logicznego. Instrukcje zawarte między słowami kluczowymi repeat i until są wykonywane sekwencyjnie, dopóki wynikiem wyrażenia nie będzie True. Sekwencja instrukcji zostanie wykonana co najmniej raz, ponieważ wyrażenie jest oceniane po każdym wykonaniu sekwencji instrukcji.

WYKŁAD nr 3. Procedury i funkcje

1. Pojęcie algorytmu pomocniczego

Algorytm rozwiązywania problemów został zaprojektowany przez rozłożenie całego problemu na oddzielne podzadania. Zazwyczaj podzadania są implementowane jako podprogramy.

Podprogram to pewien algorytm pomocniczy, który jest wielokrotnie używany w algorytmie głównym z różnymi wartościami niektórych przychodzących wielkości, zwanych parametrami.

Podprogram w językach programowania to ciąg instrukcji, które są zdefiniowane i zapisane tylko w jednym miejscu w programie, ale można je wywołać do wykonania z jednego lub kilku punktów programu. Każdy podprogram jest identyfikowany przez unikalną nazwę.

W Pascalu istnieją dwa rodzaje podprogramów: procedury i funkcje. Procedura i funkcja to nazwana sekwencja deklaracji i instrukcji. Podczas korzystania z procedur lub funkcji program musi zawierać tekst procedury lub funkcji oraz wywołanie procedury lub funkcji. Parametry określone w opisie nazywane są formalnymi, te określone w wywołaniu podprogramu nazywane są rzeczywistymi. Wszystkie parametry formalne można podzielić na następujące kategorie:

1) parametry – zmienne;

2) parametry stałe;

3) wartości parametrów;

4) parametry procedury i parametry funkcji, tj. parametry typu proceduralnego;

5) niewpisane parametry zmiennych.

Teksty procedur i funkcji znajdują się w dziale opisów procedur i funkcji.

Przekazywanie nazw procedur i funkcji jako parametrów

W wielu problemach, zwłaszcza w matematyce obliczeniowej, konieczne jest przekazywanie nazw procedur i funkcji jako parametrów. W tym celu TURBO PASCAL wprowadził nowy typ danych - proceduralny lub funkcjonalny, w zależności od tego, co jest opisane. (Typy proceduralne i funkcyjne są opisane w sekcji deklaracji typu).

Funkcja i typ proceduralny jest definiowany jako nagłówek procedury i funkcja z listą parametrów formalnych, ale bez nazwy. Możliwe jest zdefiniowanie funkcji lub typu proceduralnego bez parametrów, na przykład:

rodzaj

Proc = procedura;

Po zadeklarowaniu typu proceduralnego lub funkcjonalnego może służyć do opisu parametrów formalnych – nazw procedur i funkcji. Ponadto konieczne jest napisanie tych rzeczywistych procedur lub funkcji, których nazwy będą przekazywane jako rzeczywiste parametry.

2. Procedury w Pascalu

Każdy opis procedury zawiera nagłówek, po którym następuje blok programu. Ogólna postać nagłówka procedury wygląda następująco:

Procedura <nazwa> [(<lista parametrów formalnych>)];

Procedura jest aktywowana za pomocą instrukcji procedury, która zawiera nazwę procedury i wymagane parametry. Instrukcje, które mają zostać wykonane po uruchomieniu procedury, są zawarte w części instrukcji modułu procedury. Jeśli instrukcja zawarta w procedurze używa identyfikatora procedury w module procedury, to procedura będzie wykonywana rekurencyjnie, to znaczy, po wykonaniu będzie się odwoływać do siebie.

3. Funkcje w Pascalu

Deklaracja funkcji definiuje część programu, w której wartość jest obliczana i zwracana. Ogólny widok nagłówka funkcji wygląda następująco:

Function <nazwa> [(<lista parametrów formalnych>)]: <typ zwracany>;

Funkcja jest aktywowana po jej wywołaniu. Gdy funkcja jest wywoływana, określany jest identyfikator funkcji i wszelkie parametry niezbędne do jej oceny. Wywołanie funkcji może być zawarte w wyrażeniach jako operand. Gdy wyrażenie jest oceniane, funkcja jest wykonywana, a wartość operandu staje się wartością zwracaną przez funkcję.

Część operatora bloku funkcyjnego określa instrukcje, które muszą zostać wykonane, gdy funkcja jest aktywowana. Moduł musi zawierać co najmniej jedną instrukcję przypisania, która przypisuje wartość do identyfikatora funkcji. Wynikiem działania funkcji jest ostatnia przypisana wartość. Jeśli nie ma takiej instrukcji przypisania lub jeśli nie została wykonana, wartość zwracana przez funkcję jest niezdefiniowana.

Jeśli identyfikator funkcji jest używany podczas wywoływania funkcji w module, to funkcja jest wykonywana rekurencyjnie.

4. Przekazywanie opisów i łączenie podprogramów. Dyrektywa

Program może zawierać kilka podprogramów, tzn. struktura programu może być skomplikowana. Jednak te podprogramy mogą znajdować się na tym samym poziomie zagnieżdżenia, więc deklaracja podprogramu musi być pierwsza, a następnie wywołanie jej, chyba że jest używana specjalna deklaracja do przodu.

Deklaracja procedury, która zawiera dyrektywę forward zamiast bloku instrukcji, nazywana jest deklaracją forward. W pewnym momencie po tej deklaracji procedura musi zostać zdefiniowana za pomocą deklaracji definiującej. Deklaracja definiująca to taka, która używa tego samego identyfikatora procedury, ale pomija listę parametrów formalnych i zawiera blok instrukcji. Deklaracja przekazująca i deklaracja definiująca muszą pojawić się w tej samej części procedury i deklaracji funkcji. Pomiędzy nimi można zadeklarować inne procedury i funkcje, które mogą odwoływać się do procedury przekazywania deklaracji. W ten sposób możliwa jest wzajemna rekurencja.

Opis w przód i opis definiujący stanowią pełny opis procedury. Procedurę uważa się za opisaną za pomocą opisu wyprzedzającego.

Jeśli program zawiera dość dużo podprogramów, to program przestanie być wizualny, trudno będzie się w nim poruszać. Aby tego uniknąć, niektóre podprogramy są przechowywane jako pliki źródłowe na dysku iw razie potrzeby są połączone z głównym programem na etapie kompilacji za pomocą dyrektywy kompilacji.

Dyrektywa to specjalny komentarz, który można umieścić w dowolnym miejscu programu, gdzie może być normalny komentarz. Różnią się jednak tym, że dyrektywa ma specjalny zapis: zaraz po nawiasie zamykającym bez spacji pisany jest znak S, a następnie ponownie bez spacji wskazana jest dyrektywa.

Przykład

1) {SE+} - emuluj koprocesor matematyczny;

2) {SF+} - tworzą odległy typ procedury i wywołania funkcji;

3) {SN+} - użyj koprocesora matematycznego;

4) {SR+} - sprawdź, czy zakresy są poza granicami.

Niektóre przełączniki kompilacji mogą zawierać parametr, na przykład:

{$1 nazwa pliku} - dołącz nazwany plik do tekstu skompilowanego programu.

WYKŁAD nr 4. Podprogramy

1. Parametry podprogramu

Opis procedury lub funkcji określa listę parametrów formalnych. Każdy parametr zadeklarowany na formalnej liście parametrów jest lokalny dla deklarowanej procedury lub funkcji i może być przywoływany przez jego identyfikator w module skojarzonym z tą procedurą lub funkcją.

Istnieją trzy typy parametrów: wartość, zmienna i zmienna bez typu. Są one scharakteryzowane w następujący sposób.

1. Grupa parametrów bez poprzedzającego słowa kluczowego to lista parametrów wartości.

2. Grupa parametrów poprzedzona słowem kluczowym const, po której następuje typ, jest listą parametrów stałych.

3. Grupa parametrów poprzedzona słowem kluczowym var, po której następuje typ, jest listą parametrów zmiennych bez typu.

4. Grupa parametrów poprzedzonych słowem kluczowym var lub const, po których nie występuje typ, jest listą parametrów zmiennych bez typu.

2. Rodzaje parametrów podprogramów

Parametry wartości

Formalny parametr wartości jest traktowany jako zmienna lokalna dla procedury lub funkcji, z wyjątkiem tego, że wyprowadza swoją początkową wartość z odpowiedniego rzeczywistego parametru, gdy procedura lub funkcja jest wywoływana. Zmiany, którym przechodzi formalny parametr wartości, nie wpływają na wartość rzeczywistego parametru. Odpowiednia wartość parametru wartości rzeczywistej musi być wyrażeniem, a jego wartość nie może być typem pliku ani żadnym typem struktury zawierającym typ pliku.

Rzeczywisty parametr musi być typu, którego przypisanie jest zgodne z typem formalnego parametru wartości. Jeśli parametr jest typu string, to formalny parametr będzie miał atrybut rozmiaru 255.

Stałe parametry

Formalne parametry stałe działają podobnie do zmiennej lokalnej tylko do odczytu, która otrzymuje swoją wartość, gdy procedura lub funkcja jest wywoływana z odpowiedniego parametru rzeczywistego. Przypisania do stałego parametru formalnego są niedozwolone. Formalny parametr stały również nie może być przekazany jako rzeczywisty parametr do innej procedury lub funkcji. Stały parametr odpowiadający rzeczywistemu parametrowi w procedurze lub instrukcji funkcji musi podlegać tym samym regułom, co rzeczywista wartość parametru.

W przypadkach, gdy parametr formalny nie zmienia swojej wartości podczas wykonywania procedury lub funkcji, zamiast parametru wartości należy użyć parametru stałego. Parametry stałe pozwalają na zaimplementowanie procedury lub funkcji zabezpieczającej przed przypadkowym przypisaniem do parametru formalnego. Ponadto w przypadku parametrów typu struct i string kompilator może generować bardziej wydajny kod, gdy jest używany zamiast parametrów wartości dla parametrów stałych.

Zmienne parametry

Parametr zmiennej jest używany, gdy wartość musi zostać przekazana z procedury lub funkcji do programu wywołującego. Odpowiedni rzeczywisty parametr w procedurze lub instrukcji wywołania funkcji musi być odwołaniem do zmiennej. Kiedy procedura lub funkcja jest wywoływana, formalna zmienna parametryczna jest zastępowana przez rzeczywistą zmienną, wszelkie zmiany wartości formalnej zmiennej parametrycznej są odzwierciedlane w rzeczywistym parametrze.

W ramach procedury lub funkcji każde odwołanie do formalnego parametru zmiennej skutkuje dostępem do samego rzeczywistego parametru. Typ rzeczywistego parametru musi być zgodny z typem formalnego parametru zmiennej, ale to ograniczenie można obejść za pomocą parametru zmiennej bez typu).

Niewpisane parametry

Gdy parametr formalny jest parametrem zmiennej bez typu, odpowiedni parametr rzeczywisty może być dowolnym odwołaniem do zmiennej lub stałej, niezależnie od jej typu. Parametr bez typu zadeklarowany za pomocą słowa kluczowego var można modyfikować, natomiast parametr bez typu zadeklarowany za pomocą słowa kluczowego const jest tylko do odczytu.

W procedurze lub funkcji parametr zmiennej bez typu nie ma typu, tj. jest niekompatybilny ze zmiennymi wszystkich typów, dopóki nie otrzyma określonego typu przez przypisanie typu zmiennej.

Chociaż parametry nieopisane zapewniają większą elastyczność, istnieje pewne ryzyko związane z ich używaniem. Kompilator nie może sprawdzić poprawności operacji na zmiennych bez typu.

Zmienne proceduralne

Po zdefiniowaniu typu proceduralnego staje się możliwe opisywanie zmiennych tego typu. Takie zmienne nazywane są zmiennymi proceduralnymi. Podobnie jak zmienna typu integer, której można przypisać wartość typu integer, zmienna proceduralna może mieć przypisaną wartość typu proceduralnego. Taka wartość może oczywiście być inną zmienną proceduralną, ale może to być również identyfikator procedury lub funkcji. W tym kontekście deklarację procedury lub funkcji można postrzegać jako opis specjalnego rodzaju stałej, której wartością jest procedura lub funkcja.

Jak w przypadku każdego innego przypisania, wartości zmiennej po lewej i prawej stronie muszą być zgodne z przypisaniem. Typy proceduralne, aby były zgodne z przypisaniem, muszą mieć taką samą liczbę parametrów, a parametry w odpowiednich pozycjach muszą być tego samego typu. Nazwy parametrów w deklaracji typu proceduralnego nie mają wpływu.

Ponadto, aby zapewnić zgodność przypisania, procedura lub funkcja, jeśli ma być przypisana do zmiennej procedury, musi spełniać następujące wymagania:

1) nie powinna być standardową procedurą lub funkcją;

2) taka procedura lub funkcja nie mogą być zagnieżdżone;

3) taka procedura nie może być procedurą inline;

4) nie może być procedurą przerwania.

Standardowe procedury i funkcje to procedury i funkcje opisane w module System, takie jak Writeln, Readln, Chr, Ord. Nie można używać zagnieżdżonych procedur i funkcji ze zmiennymi proceduralnymi. Procedura lub funkcja jest uważana za zagnieżdżoną, gdy jest zadeklarowana w innej procedurze lub funkcji.

Użycie typów proceduralnych nie ogranicza się tylko do zmiennych proceduralnych. Jak każdy inny typ, typ proceduralny może uczestniczyć w deklaracji typu strukturalnego.

Kiedy zmiennej procedury przypisuje się wartość procedury, w warstwie fizycznej dzieje się to, że adres procedury jest przechowywany w zmiennej. W rzeczywistości zmienna proceduralna jest bardzo podobna do zmiennej wskaźnikowej, tylko zamiast odnosić się do danych, wskazuje na procedurę lub funkcję. Podobnie jak wskaźnik, zmienna proceduralna zajmuje 4 bajty (dwa słowa), które zawierają adres pamięci. Pierwsze słowo przechowuje przesunięcie, drugie słowo przechowuje segment.

Parametry typu proceduralnego

Ponieważ typy proceduralne mogą być używane w dowolnym kontekście, możliwe jest opisanie procedur lub funkcji, które przyjmują procedury i funkcje jako parametry. Parametry typu proceduralnego są szczególnie przydatne, gdy trzeba wykonać wspólne działania na wielu procedurach lub funkcjach.

Jeśli procedura lub funkcja ma zostać przekazana jako parametr, musi być zgodna z tymi samymi regułami zgodności typu, co przypisanie. Oznacza to, że takie procedury lub funkcje muszą być skompilowane z dyrektywą far, nie mogą być funkcjami wbudowanymi, nie mogą być zagnieżdżone i nie mogą być opisane za pomocą atrybutów inline lub przerwań.

WYKŁAD 5. Typ danych String

1. Typ ciągu w Pascalu

Sekwencja znaków o określonej długości nazywana jest ciągiem. Zmienne typu string są definiowane przez podanie w nawiasach kwadratowych nazwy zmiennej, słowa zastrzeżonego string oraz opcjonalnie, ale niekoniecznie, maksymalnego rozmiaru, tj. długości ciągu. Jeśli nie ustawisz maksymalnego rozmiaru ciągu, to domyślnie będzie to 255, czyli ciąg będzie składał się z 255 znaków.

Do każdego elementu ciągu można się odnieść poprzez jego numer. Jednak łańcuchy są danymi wejściowymi i wyjściowymi jako całość, a nie element po elemencie, jak ma to miejsce w przypadku tablic. Liczba wprowadzanych znaków nie może przekraczać określonej w maksymalnym rozmiarze ciągu, więc jeśli taka nadwyżka wystąpi, to „dodatkowe” znaki zostaną zignorowane.

2. Procedury i funkcje dla zmiennych typu string

1. Kopiowanie funkcji (S: ciąg; indeks, liczba: liczba całkowita): ciąg;

Zwraca podciąg ciągu. S jest wyrażeniem typu String.

Index i Count są wyrażeniami typu całkowitego. Funkcja zwraca ciąg zawierający znaki Count, zaczynając od pozycji Index. Jeśli indeks jest większy niż długość S, funkcja zwraca pusty ciąg.

2. Procedura Delete(var S: String; Index, Count: Integer);

Usuwa podciąg znaków o długości Count z ciągu S, zaczynając od pozycji Index. S jest zmienną typu String. Index i Count są wyrażeniami typu całkowitego. Jeśli indeks jest większy niż długość S, żadne znaki nie są usuwane.

3. Procedura Insert(Źródło: Ciąg; var S: Ciąg; Indeks: Liczba całkowita);

Łączy podciąg w ciąg, zaczynając od określonej pozycji. Źródło jest wyrażeniem typu String. S jest zmienną typu String o dowolnej długości. Indeks jest wyrażeniem typu integer. Wstaw wstawia Source do S, zaczynając od pozycji S[Indeks].

4. Długość funkcji (S: ciąg): liczba całkowita;

Zwraca liczbę znaków faktycznie użytych w ciągu S. Zauważ, że w przypadku używania ciągów zakończonych znakiem NULL liczba znaków niekoniecznie jest równa liczbie bajtów.

5. Function Pos(Substr: String; S: String): Liczba całkowita;

Wyszukuje podciąg w ciągu. Pos szuka Substr w S i zwraca wartość całkowitą, która jest indeksem pierwszego znaku Substr w S. Jeśli Substr nie zostanie znaleziony, Pos zwraca null.

3. Nagrania

Rekord to zbiór ograniczonej liczby logicznie powiązanych komponentów należących do różnych typów. Składniki rekordu nazywane są polami, z których każde jest identyfikowane za pomocą nazwy. Pole rekordu zawiera nazwę pola, po której następuje dwukropek wskazujący typ pola. Pola rekordów mogą być dowolnego typu dozwolonego w Pascalu, z wyjątkiem typu pliku.

Opis rekordu w języku Pascal odbywa się za pomocą słowa serwisowego RECORD, po którym następuje opis składników rekordu. Opis wpisu kończy się słowem serwisowym END.

Na przykład notatnik zawiera nazwiska, inicjały i numery telefonów, więc wygodnie jest przedstawić oddzielny wiersz w notatniku jako następujący wpis:

wpisz wiersz = rekord

FIO: Ciąg[20];

TEL: Ciąg[7];

puszki;

var str: Wiersz;

Opisy rekordów są również możliwe bez użycia nazwy typu, na przykład:

var str : Nagraj

FIO : Ciąg[20];

TEL : Ciąg[7];

puszki;

Odwoływanie się do rekordu jako całości jest dozwolone tylko w instrukcjach przypisania, w których nazwy rekordów tego samego typu są używane po lewej i prawej stronie znaku przypisania. We wszystkich pozostałych przypadkach obsługiwane są oddzielne pola rekordów. Aby odwołać się do pojedynczego składnika rekordu, należy podać nazwę rekordu i oddzielone kropką nazwę żądanego pola. Taka nazwa nazywana jest nazwą złożoną. Składnik rekordu może być również rekordem, w którym to przypadku nazwa wyróżniająca będzie zawierać nie dwie, ale więcej nazw.

Odwoływanie się do składników rekordu można uprościć za pomocą operatora with append. Umożliwia zastąpienie nazw złożonych, które charakteryzują każde pole, tylko nazwami pól i zdefiniowanie nazwy rekordu w operatorze dołączania.

Czasami zawartość pojedynczego rekordu zależy od wartości jednego z jego pól. W języku Pascal dozwolony jest opis rekordu składający się z części wspólnych i wariantów. Część wariantową określa się za pomocą przypadku P konstrukcji, gdzie P jest nazwą pola ze wspólnej części rekordu. Możliwe wartości akceptowane przez to pole są wymienione w taki sam sposób, jak w oświadczeniu wariantowym. Jednak zamiast określać czynność do wykonania, jak to ma miejsce w instrukcji wariantu, pola wariantów są podane w nawiasach. Opis części wariantowej kończy się słowem serwisowym. Typ pola P można określić w nagłówku części wariantowej. Rekordy są inicjowane za pomocą wpisanych stałych.

4. Zestawy

Pojęcie zbioru w języku Pascal opiera się na matematycznym pojęciu zbiorów: jest to ograniczony zbiór różnych elementów. Wyliczeniowy lub interwałowy typ danych służy do konstruowania konkretnego typu zestawu. Typ elementów tworzących zestaw nazywany jest typem bazowym.

Typ wielokrotny jest opisywany za pomocą zestawu słów funkcyjnych, na przykład:

typ M = zestaw B;

Tutaj M jest typem liczby mnogiej, B jest typem podstawowym.

Przynależność zmiennych do typu liczby mnogiej można określić bezpośrednio w sekcji deklaracji zmiennej.

Stałe typu zestawu są zapisywane jako ujęty w nawias ciąg elementów lub interwałów typu podstawowego, oddzielonych przecinkami. Stała postaci [] oznacza pusty podzbiór.

Zbiór zawiera zbiór elementów typu bazowego, wszystkie podzbiory danego zbioru oraz podzbiór pusty. Jeżeli typ bazowy, na którym budowany jest zbiór, ma K elementów, to liczba podzbiorów zawartych w tym zbiorze jest równa 2 do potęgi K. Kolejność wyliczania elementów typu bazowego w stałych jest obojętna. Wartość zmiennej typu wielokrotnego można podać za pomocą konstrukcji postaci [T], gdzie T jest zmienną typu bazowego.

Operacje przypisania (:=), sumy (+), przecięcia (*) i odejmowania (-) dotyczą zmiennych i stałych określonego typu. Wynikiem tych operacji jest wartość typu mnogiego:

1) ['A','B'] + ['A','D'] da ['A','B','D'];

2) ['A'] * ['A','B','C'] da ['A'];

3) ['A','B','C'] - ['A','B'] da ['C'].

Operacje dotyczą wielu wartości: tożsamość (=), brak tożsamości (<>), zawarty w (<=), zawiera (>=). Wynik tych operacji ma typ logiczny:

1) ['A','B'] = ['A','C'] da FAŁSZ ;

2) ['A','B'] <> ['A','C'] da PRAWDA;

3) ['B'] <= ['B','C'] da TRUE;

4) ['C','D'] >= ['A'] da FAŁSZ.

Oprócz tych operacji, do pracy z wartościami typu zbioru używany jest w operacji, który sprawdza, czy element typu bazowego po lewej stronie znaku operacji należy do zbioru po prawej stronie znaku operacji . Wynik tej operacji jest wartością logiczną. Często zamiast operacji relacyjnych stosuje się operację sprawdzania, czy element należy do zbioru.

Gdy w programach używanych jest wiele typów danych, operacje są wykonywane na ciągach bitów danych. Każda wartość typu multiple w pamięci komputera odpowiada jednej cyfrze binarnej.

Wartości typu wielokrotnego nie mogą być elementami listy I/O. W każdej konkretnej implementacji kompilatora z języka Pascal liczba elementów typu bazowego, na którym budowany jest zestaw, jest ograniczona.

Inicjalizacja wielu wartości typu odbywa się za pomocą wpisanych stałych.

Oto kilka procedur pracy z zestawami.

1. Procedura Wyklucz (var S: Zbiór T; I:T);

Usuwa element I ze zbioru S. S jest zmienną typu „zestaw”, a I jest wyrażeniem typu zgodnego z pierwotnym typem S. Exclude(S, I) jest tym samym co S : = S - [I] , ale generuje bardziej wydajny kod.

2. Procedura Include(var S: Zestaw T; I:T);

Dodaje element I do zbioru S. S jest zmienną typu „zestaw”, a I jest wyrażeniem typu zgodnego z typem S. Konstrukcja Include(S, I) jest taka sama jak S : = S + [I], ale generuje bardziej wydajny kod.

WYKŁAD nr 6. Pliki

1. Pliki. Operacje na plikach

Wprowadzenie typu pliku do języka Pascal spowodowane jest koniecznością zapewnienia możliwości współpracy z peryferyjnymi (zewnętrznymi) urządzeniami komputerowymi przeznaczonymi do wprowadzania, wyprowadzania i przechowywania danych.

Typ danych pliku (lub plik) definiuje uporządkowaną kolekcję dowolnej liczby komponentów tego samego typu. Wspólną właściwością tablicy, zbioru i rekordu jest to, że liczba ich składników jest określana na etapie pisania programu, podczas gdy liczba składników pliku w tekście programu nie jest określona i może być dowolna.

Podczas pracy z plikami wykonywane są operacje we / wy. Operacja wejściowa oznacza przeniesienie danych z urządzenia zewnętrznego (z pliku wejściowego) do pamięci głównej komputera, operacja wyjściowa to przeniesienie danych z pamięci głównej do urządzenia zewnętrznego (do pliku wyjściowego). Pliki na urządzeniach zewnętrznych są często nazywane plikami fizycznymi. Ich nazwy są określane przez system operacyjny.

W programach Pascala nazwy plików są określane za pomocą łańcuchów. Aby pracować z plikami w programie, musisz zdefiniować zmienną pliku. Pascal obsługuje trzy typy plików: pliki tekstowe, pliki składowe, pliki bez typu.

Zmienne plikowe zadeklarowane w programie nazywane są plikami logicznymi. Wszystkie podstawowe procedury i funkcje dostarczające dane I/O działają tylko z plikami logicznymi. Zbiór fizyczny musi być powiązany ze zbiorem logicznym przed wykonaniem procedur otwierania zbioru.

Pliki tekstowe

Szczególne miejsce w języku Pascal zajmują pliki tekstowe, których składniki są typu znakowego. Do opisu plików tekstowych język definiuje standardowy typ Tekst:

zmienna TF1, TF2: Tekst;

Pliki tekstowe to sekwencja wierszy, a wiersze to sekwencja znaków. Linie mają zmienną długość, każda linia kończy się terminatorem.

Pliki składowe

Komponent lub plik z typem to plik z zadeklarowanym typem jego komponentów. Pliki składowe składają się z maszynowych reprezentacji wartości zmiennych, przechowują dane w takiej samej formie jak pamięć komputera.

Opis wartości typu pliku to:

wpisz M = plik T;

gdzie M to nazwa typu pliku;

T - typ komponentu.

Komponenty plikowe mogą być wszystkich typów skalarnych, az typów strukturalnych - tablice, zbiory, rekordy. W prawie wszystkich konkretnych implementacjach języka Pascal konstrukcja "pliku plików" jest niedozwolona.

Wszystkie operacje na plikach składowych wykonywane są przy użyciu standardowych procedur.

Zapis(f,X1,X2,...XK)

Niewpisane pliki

Niewpisane pliki umożliwiają zapisywanie dowolnych sekcji pamięci komputera na dysk i odczytywanie ich z dysku na pamięć. Pliki bez typu są opisane w następujący sposób:

var f: plik;

Teraz wymienimy procedury i funkcje do pracy z różnymi typami plików.

1. Procedura Assign(var F; FileName: String);

Procedura AssignFile mapuje nazwę pliku zewnętrznego na zmienną pliku.

F jest zmienną plikową dowolnego typu, FileName jest wyrażeniem typu String lub wyrażeniem PChar, jeśli dozwolona jest rozszerzona składnia. Wszystkie dalsze operacje z F są wykonywane z zewnętrznym plikiem.

Nie można użyć procedury z już otwartą zmienną pliku.

2. Procedura Zamknij (var F);

Procedura przerywa połączenie między zmienną pliku a plikiem na dysku zewnętrznym i zamyka plik.

F jest zmienną plikową dowolnego typu pliku, otwieraną przez procedury Reset, Rewrite lub Append. Plik zewnętrzny skojarzony z F jest w pełni modyfikowany, a następnie zamykany, zwalniając deskryptor pliku do ponownego wykorzystania.

Dyrektywa {SI+} pozwala na obsługę błędów podczas wykonywania programu przy użyciu obsługi wyjątków. Po wyłączeniu dyrektywy {$1-} należy użyć IOResult do sprawdzenia błędów we/wy.

3.Funkcja Eof(var F): Boolean;

{Pliki wpisane lub niewpisane}

Funkcja Eof[(var F: tekst)]: Boolean;

{pliki tekstowe}

Sprawdza, czy bieżąca pozycja pliku jest końcem pliku.

Eof(F) zwraca True, jeśli bieżąca pozycja pliku jest po ostatnim znaku pliku lub jeśli plik jest pusty; w przeciwnym razie Eof(F) zwraca False.

Dyrektywa {SI+} pozwala na obsługę błędów podczas wykonywania programu przy użyciu obsługi wyjątków. Po wyłączeniu dyrektywy {SI-} należy użyć IOResult do sprawdzenia błędów we/wy.

4. Procedura Wymaż (varF);

Usuwa zewnętrzny plik skojarzony z F.

F to zmienna plikowa dowolnego typu.

Przed wywołaniem procedury Erase plik musi zostać zamknięty.

Dyrektywa {SI+} pozwala na obsługę błędów podczas wykonywania programu przy użyciu obsługi wyjątków. Po wyłączeniu dyrektywy {SI-} należy użyć IOResult do sprawdzenia błędów we/wy.

5. Function FileSize(var F): Liczba całkowita;

Zwraca rozmiar pliku F w bajtach. Jeśli jednak F jest plikiem typu, FileSize zwróci liczbę rekordów w pliku. Plik musi być otwarty przed użyciem funkcji FileSize. Jeśli plik jest pusty, FileSize(F) zwraca zero. F jest zmienną dowolnego typu pliku.

6.Funkcja FilePos(var F): LongInt;

Zwraca bieżącą pozycję pliku w pliku.

Przed użyciem funkcji FilePos plik musi być otwarty. Funkcja FilePos nie jest używana z plikami tekstowymi. F jest zmienną dowolnego typu pliku, z wyjątkiem typu Tekst.

7. Procedura Reset(var F [: File; RecSize: Word]);

Otwiera istniejący plik.

F jest zmienną dowolnego typu pliku skojarzoną z plikiem zewnętrznym przy użyciu AssignFile. RecSize to opcjonalne wyrażenie, które jest używane, jeśli F jest plikiem bez typu. Jeśli F jest plikiem bez typu, RecSize określa rozmiar rekordu, który jest używany podczas przesyłania danych. Jeśli RecSize zostanie pominięty, domyślny rozmiar rekordu to 128 bajtów.

Procedura Reset otwiera istniejący plik zewnętrzny skojarzony ze zmienną pliku F. Jeśli nie ma pliku zewnętrznego o tej nazwie, wystąpi błąd w czasie wykonywania. Jeśli plik skojarzony z F jest już otwarty, jest najpierw zamykany, a następnie otwierany ponownie. Bieżąca pozycja pliku jest ustawiona na początek pliku.

8. Procedura Rewrite(var F: File [; Recsize: Word]);

Tworzy i otwiera nowy plik.

F jest zmienną dowolnego typu pliku skojarzoną z plikiem zewnętrznym przy użyciu AssignFile. RecSize to opcjonalne wyrażenie, które jest używane, jeśli F jest plikiem bez typu. Jeśli F jest plikiem bez typu, RecSize określa rozmiar rekordu, który jest używany podczas przesyłania danych. Jeśli RecSize zostanie pominięty, domyślny rozmiar rekordu to 128 bajtów.

Procedura Rewrite tworzy nowy plik zewnętrzny o nazwie powiązanej z F. Jeśli plik zewnętrzny o tej samej nazwie już istnieje, jest on usuwany i tworzony jest nowy pusty plik.

9. Procedura Seek(var F; N: LongInt);

Przenosi bieżącą pozycję pliku do określonego komponentu. Tej procedury można używać tylko z otwartymi plikami z wpisanymi lub bez wpisanymi tekstami.

Bieżąca pozycja pliku F zostaje przeniesiona na numer N. Numer pierwszego komponentu pliku to 0.

Instrukcja Seek(F, FileSize(F)) przenosi bieżącą pozycję pliku na koniec pliku.

10. Procedura Append(var F: Text);

Otwiera istniejący plik tekstowy w celu dołączenia informacji na końcu pliku (dołącz).

Jeśli plik zewnętrzny o podanej nazwie nie istnieje, wystąpi błąd wykonania. Jeśli plik F jest już otwarty, zamyka się i otwiera ponownie. Bieżąca pozycja pliku jest ustawiona na końcu pliku.

11.Funkcja Eoln[(var F: tekst)]: Boolean;

Sprawdza, czy bieżąca pozycja pliku jest końcem wiersza w pliku tekstowym.

Eoln(F) zwraca True, jeśli bieżąca pozycja pliku znajduje się na końcu wiersza lub pliku; w przeciwnym razie Eoln(F) zwraca False.

12. Procedura Odczyt(F, V1 [, V2,..., Vn]);

{Pliki wpisane i niewpisane}

Procedura Odczyt([var F: Tekst;] V1 [, V2,..., Vn]);

{pliki tekstowe}

W przypadku plików wpisanych procedura wczytuje składnik pliku do zmiennej. Przy każdym odczycie bieżąca pozycja w pliku przesuwa się do następnego elementu.

W przypadku plików tekstowych jedna lub więcej wartości jest wczytywanych do jednej lub więcej zmiennych.

Ze zmiennymi typu String Read odczytuje wszystkie znaki do (ale nie włączając) następnego znacznika końca linii lub do momentu, gdy Eof(F) da wartość True. Wynikowy ciąg znaków jest przypisywany do zmiennej.

W przypadku zmiennej typu całkowitego lub rzeczywistego procedura oczekuje na ciąg znaków tworzących liczbę zgodnie z regułami składni Pascala. Czytanie zatrzymuje się, gdy napotkana zostanie pierwsza spacja, tabulator lub koniec wiersza, lub jeśli Eof(F) zwróci wartość True. Jeśli ciąg liczbowy nie jest zgodny z oczekiwanym formatem, wystąpi błąd we/wy.

13. Procedura Readln([var F: Tekst;] V1 [, V2..., Vn]);

Jest rozszerzeniem procedury Read i jest zdefiniowany dla plików tekstowych. Czyta ciąg znaków z pliku, w tym znacznik końca wiersza, i przechodzi na początek następnego wiersza. Wywołanie funkcji Readln(F) bez parametrów przenosi bieżącą pozycję pliku na początek następnej linii, jeśli taki istnieje, w przeciwnym razie przeskakuje na koniec pliku.

14. Funkcja SeekEof[(var F: tekst)]: Boolean;

Zwraca koniec pliku i może być używany tylko w przypadku otwartych plików tekstowych. Zwykle używany do odczytywania wartości liczbowych z plików tekstowych.

15. Funkcja SeekEoln[(var F: tekst)]: Boolean;

Zwraca terminator wiersza w pliku i może być używany tylko w przypadku otwartych plików tekstowych. Zwykle używany do odczytywania wartości liczbowych z plików tekstowych.

16. Procedura Write([var F: Tekst;] P1 [, P2,..., Pn]);

{pliki tekstowe}

Zapisuje jedną lub więcej wartości do pliku tekstowego.

Każdy parametr wpisu musi być typu Char, jeden z typów całkowitych (Byte, ShortInt, Word, Longint, Cardinal), jeden z typów zmiennoprzecinkowych (Single, Real, Double, Extended, Currency), jeden z typów łańcuchowych ( PChar, AisiString , ShortString) lub jeden z typów logicznych (Boolean, Bool).

Procedura Zapis(F, V1,..., Vn);

{Wpisane pliki}

Zapisuje zmienną w komponencie pliku. Zmienne VI...., Vn muszą być tego samego typu co elementy pliku. Za każdym razem, gdy zapisana jest zmienna, bieżąca pozycja w pliku jest przenoszona do następnego elementu.

17. Procedura Writeln([var F: Tekst;] [P1, P2,..., Pn]);

{pliki tekstowe}

Wykonuje operację zapisu, a następnie umieszcza w pliku znacznik końca wiersza.

Wywołanie Writeln(F) bez parametrów powoduje zapisanie do pliku znacznika końca wiersza. Plik musi być otwarty do wyjścia.

2. Moduły. Rodzaje modułów

Moduł (1Ж1Т) w Pascalu to specjalnie zaprojektowana biblioteka podprogramów. Moduł, w przeciwieństwie do programu, nie może być sam uruchamiany do wykonania, może jedynie uczestniczyć w budowaniu programów i innych modułów. Moduły umożliwiają tworzenie osobistych bibliotek procedur i funkcji oraz budowanie programów o niemal dowolnej wielkości.

Moduł w Pascalu to oddzielnie przechowywana i niezależnie skompilowana jednostka programu. Ogólnie rzecz biorąc, moduł to zbiór zasobów oprogramowania przeznaczonych do użytku przez inne programy. Zasoby programu rozumiane są jako dowolne elementy języka Pascal: stałe, typy, zmienne, podprogramy. Sam moduł nie jest programem wykonywalnym, jego elementy są wykorzystywane przez inne jednostki programowe.

Wszystkie elementy programowe modułu można podzielić na dwie części:

1) elementy programu przeznaczone do wykorzystania przez inne programy lub moduły, takie elementy nazywane są widocznymi na zewnątrz modułu;

2) elementy oprogramowania, które są niezbędne tylko do działania samego modułu, nazywane są niewidocznymi (lub ukrytymi).

Zgodnie z tym moduł, oprócz nagłówka, zawiera trzy główne części, zwane interfejsem, wykonywalne i inicjowane.

Ogólnie moduł ma następującą strukturę:

jednostka <nazwa modułu>; {tytuł modułu}

Interfejs

{opis widocznych elementów programowych modułu}

realizacja

{opis ukrytych elementów programistycznych modułu}

rozpocząć

{instrukcje inicjalizacji elementu modułu}

koniec.

W konkretnym przypadku moduł może nie zawierać części implementacyjnej i części inicjującej, wówczas struktura modułu będzie następująca:

jednostka <nazwa modułu>; {tytuł modułu}

Interfejs

{opis widocznych elementów programowych modułu}

realizacja

koniec.

Stosowanie procedur i funkcji w modułach ma swoje osobliwości. Nagłówek podprogramu zawiera wszystkie informacje niezbędne do jego wywołania: nazwę, listę i typ parametrów, typ wyniku dla funkcji. Informacje te muszą być dostępne dla innych programów i modułów. Z drugiej strony tekst podprogramu, który implementuje jego algorytm, nie może być używany przez inne programy i moduły. Dlatego nagłówki procedur i funkcji są umieszczone w części interfejsowej modułu, a tekst w części implementacyjnej.

Część interfejsowa modułu zawiera tylko widoczne (dostępne dla innych programów i modułów) nagłówki procedur i funkcji (bez słowa serwisowego forward). Pełny tekst procedury lub funkcji umieszczony jest w części implementacyjnej, a nagłówek nie może zawierać listy parametrów formalnych.

Kod źródłowy modułu musi zostać skompilowany za pomocą dyrektywy Make z podmenu Compile i zapisany na dysku. Wynikiem kompilacji modułu jest plik z rozszerzeniem . TPU (jednostka Turbo Pascala). Podstawowa nazwa modułu jest pobierana z nagłówka modułu.

Aby podłączyć moduł do programu, musisz podać jego nazwę w sekcji opisu modułu, na przykład:

używa Crt, wykresu;

W przypadku, gdy nazwy zmiennych w części interfejsowej modułu i w programie korzystającym z tego modułu są takie same, odniesienie będzie dotyczyło zmiennej opisanej w programie. Aby odwołać się do zmiennej zadeklarowanej w module, należy użyć nazwy złożonej składającej się z nazwy modułu i nazwy zmiennej oddzielonych kropką. Użycie nazw złożonych dotyczy nie tylko nazw zmiennych, ale wszystkich nazw zadeklarowanych w części interfejsowej modułu.

Rekurencyjne używanie modułów jest zabronione.

Jeśli moduł ma sekcję inicjującą, instrukcje w tej sekcji zostaną wykonane przed rozpoczęciem wykonywania programu, który używa tego modułu.

Wymieńmy typy modułów.

1. Moduł SYSTEM.

Moduł SYSTEM implementuje procedury obsługi niższego poziomu dla wszystkich wbudowanych funkcji, takich jak I/O, manipulacja łańcuchami, operacje zmiennoprzecinkowe i dynamiczna alokacja pamięci.

Moduł SYSTEM zawiera wszystkie standardowe i wbudowane procedury i funkcje Pascala. Każdy podprogram Pascala, który nie jest częścią standardowego Pascala i nie znajduje się w żadnym innym module, jest zawarty w module System. Ten moduł jest automatycznie używany we wszystkich programach i nie musi być określany w instrukcji uses.

2. Moduł DOS.

Moduł DOS implementuje liczne procedury i funkcje Pascala, które są odpowiednikami najczęściej używanych wywołań DOS, takich jak GetTime, SetTime, DiskSize i tak dalej.

3. Moduł CRT.

Moduł CRT implementuje szereg potężnych programów, które zapewniają pełną kontrolę nad funkcjami komputera, takimi jak sterowanie trybem ekranu, rozszerzone kody klawiatury, kolory, okna i dźwięki. Moduł CRT może być stosowany tylko w programach, które działają na komputerach osobistych IBM PC, PC AT, PS/2 firmy IBM i są z nimi w pełni kompatybilne.

Jedną z głównych zalet korzystania z modułu CRT jest większa szybkość i elastyczność w wykonywaniu operacji na ekranie. Programy nie współpracujące z modułem CRT wyświetlają informacje na ekranie za pomocą systemu operacyjnego DOS, co wiąże się z dodatkowym narzutem. Podczas korzystania z modułu CRT informacje wyjściowe są przesyłane bezpośrednio do podstawowego systemu wejścia/wyjścia (BIOS) lub, dla jeszcze szybszych operacji, bezpośrednio do pamięci wideo.

4. Moduł WYKRES.

Korzystając z procedur i funkcji zawartych w tym module, możesz tworzyć różne grafiki na ekranie.

5. Moduł NAKŁADKI.

Moduł OVERLAY pozwala zmniejszyć wymagania dotyczące pamięci programu DOS w trybie rzeczywistym. W rzeczywistości możliwe jest pisanie programów, które przekraczają całkowitą ilość dostępnej pamięci, ponieważ tylko część programu będzie znajdować się w pamięci w danym momencie.

WYKŁAD nr 7. Pamięć dynamiczna

1. Referencyjny typ danych. pamięć dynamiczna. Zmienne dynamiczne

Zmienna statyczna (statycznie alokowana) to zmienna jawnie zadeklarowana w programie, do której odwołuje się nazwa. Miejsce w pamięci na umieszczenie zmiennych statycznych jest określane podczas kompilacji programu. W przeciwieństwie do takich zmiennych statycznych, programy Pascala mogą tworzyć zmienne dynamiczne. Główną właściwością zmiennych dynamicznych jest to, że są one tworzone i przydzielana jest im pamięć podczas wykonywania programu.

Zmienne dynamiczne są umieszczane w dynamicznym obszarze pamięci (obszar sterty). Zmienna dynamiczna nie jest wyraźnie określona w deklaracjach zmiennych i nie można się do niej odwoływać za pomocą nazwy. Dostęp do takich zmiennych uzyskuje się za pomocą wskaźników i referencji.

Typ referencyjny (wskaźnik) definiuje zestaw wartości wskazujących na zmienne dynamiczne określonego typu, zwanego typem podstawowym. Zmienna typu referencyjnego zawiera adres zmiennej dynamicznej w pamięci. Jeśli typ podstawowy jest niezadeklarowanym identyfikatorem, musi być zadeklarowany w tej samej części deklaracji typu, co typ wskaźnika.

Zastrzeżone słowo nil oznacza stałą z wartością wskaźnika, która na nic nie wskazuje.

Podajmy przykład opisu zmiennych dynamicznych.

zmienna p1, p2 : ^real;

p3, p4 : ^ liczba całkowita;

2. Praca z pamięcią dynamiczną. Niewpisane wskaźniki

Procedury i funkcje pamięci dynamicznej

1. Procedura Nowa(var p: Wskaźnik).

Przydziela miejsce w obszarze pamięci dynamicznej, aby pomieścić zmienną dynamiczną pЛi przypisuje swój adres do wskaźnika p.

2. Procedura Dispose(varp: wskaźnik).

Zwalnia pamięć przydzieloną do dynamicznej alokacji zmiennych przez procedurę New, a wartość wskaźnika p staje się niezdefiniowana.

3. Procedura GetMem(varp: Wskaźnik; rozmiar: Word).

Alokuje sekcję pamięci w obszarze sterty, przypisuje adres jej początku do wskaźnika p, rozmiar sekcji w bajtach jest określony przez parametr size.

4. Procedura FreeMem(var p: wskaźnik; rozmiar: słowo).

Zwalnia obszar pamięci, którego początkowy adres jest określony przez wskaźnik p, a rozmiar jest określony przez parametr size. Wartość wskaźnika p staje się niezdefiniowana.

5. Znak procedury (var p: wskaźnik)

Zapisuje do wskaźnika p adres początku sekcji wolnej pamięci dynamicznej w momencie jej wywołania.

6. Zwolnienie procedury (var p: wskaźnik)

Zwalnia sekcję pamięci dynamicznej, zaczynając od adresu zapisanego do wskaźnika p przez procedurę Mark, tj. czyści pamięć dynamiczną, która była zajęta po wywołaniu procedury Mark.

7. Funkcja MaxAvaikLongint

Zwraca długość w bajtach najdłuższego wolnego sterty.

8. Funkcja MemAvaikLongint

Zwraca całkowitą ilość wolnej pamięci dynamicznej w bajtach.

9. Funkcja pomocnicza SizeOf(X):Word

Zwraca ilość bajtów zajmowanych przez X, gdzie X może być nazwą zmiennej dowolnego typu lub nazwą typu.

Typ wbudowany Pointer oznacza wskaźnik bez typu, to znaczy wskaźnik, który nie wskazuje na żaden konkretny typ. Zmienne typu Wskaźnik można wyłuskać: podanie znaku ^ po takiej zmiennej powoduje błąd.

Podobnie jak wartość oznaczona przez zero, wartości wskaźnika są kompatybilne ze wszystkimi innymi typami wskaźników.

WYKŁAD nr 8. Abstrakcyjne struktury danych

1. Abstrakcyjne struktury danych

Strukturalne typy danych, takie jak tablice, zestawy i rekordy, są strukturami statycznymi, ponieważ ich rozmiary nie zmieniają się podczas całego wykonywania programu.

Często wymagane jest, aby struktury danych zmieniały swoje rozmiary w trakcie rozwiązywania problemu. Takie struktury danych nazywane są dynamicznymi. Należą do nich stosy, kolejki, listy, drzewa itp.

Opis struktur dynamicznych za pomocą tablic, rekordów i plików prowadzi do marnotrawstwa pamięci komputera i wydłuża czas rozwiązywania problemów.

Każdy składnik dowolnej struktury dynamicznej jest rekordem zawierającym co najmniej dwa pola: jedno pole typu „wskaźnik”, a drugie – do rozmieszczenia danych. Ogólnie rekord może zawierać nie jeden, ale kilka wskaźników i kilka pól danych. Pole danych może być zmienną, tablicą, zestawem lub rekordem.

Jeżeli część wskazująca zawiera adres jednego elementu listy, to lista nazywana jest jednokierunkową (lub pojedynczo powiązaną). Jeśli zawiera dwa składniki, jest podwójnie połączony. Na listach możesz wykonywać różne operacje, na przykład:

1) dodanie elementu do listy;

2) usunięcie elementu z listy z danym kluczem;

3) wyszukaj element o podanej wartości pola kluczowego;

4) sortowanie elementów wykazu;

5) podział listy na dwie lub więcej list;

6) połączenie dwóch lub więcej list w jedną;

7) inne operacje.

Jednak z reguły nie ma potrzeby wykonywania wszystkich operacji w rozwiązywaniu różnych problemów. Dlatego w zależności od podstawowych operacji, które należy zastosować, istnieją różne rodzaje list. Najpopularniejsze z nich to stos i kolejka.

2. Stosy

Stos to dynamiczna struktura danych, do której dodawany jest składnik, a usuwany z jednego końca, zwanego wierzchołkiem stosu. Stos działa na zasadzie LIFO (Last-In, First-Out) - „ostatnie weszło, pierwsze wyszło”.

Zazwyczaj na stosach wykonywane są trzy operacje:

1) początkowa formacja stosu (zapis pierwszego składnika);

2) dodanie składnika do stosu;

3) wybór komponentu (skreślenie).

Aby utworzyć stos i z nim pracować, musisz mieć dwie zmienne typu „wskaźnik”, z których pierwsza określa wierzchołek stosu, a druga jest pomocnicza.

Przykład. Napisz program, który tworzy stos, dodaje do niego dowolną liczbę elementów, a następnie odczytuje wszystkie elementy i wyświetla je na ekranie wyświetlacza. Weź ciąg znaków jako dane. Wprowadzanie danych - z klawiatury, znak końca wprowadzania - ciąg znaków END.

Program STOS;

używa Crt;

rodzaj

Alfa = Ciąg[10];

PZm = ^Zm;

Comp = rekord

SD: Alfa;

pNastępny : PComp

puszki;

było

pTop : PCComp;

sc: Alfa;

Utwórz ProcedureStack(var pTop : PComp; var sC : Alfa);

rozpocząć

Nowy(pTop);

pGóra^.pDalej := NIL;

pGóra^.sD := sC;

puszki;

Dodaj ProcedureComp(var pTop : PComp; var sC : Alfa);

var pAux: PCp;

rozpocząć

NOWOŚĆ(pAux);

pAux^.pDalej := pGóra;

pTop := pAux;

pGóra^.sD := sC;

puszki;

Procedura DelComp(var pTop : PComp; var sC : ALFA);

rozpocząć

sc := pTop^.sD;

pGóra := pGóra^.pNastępny;

puszki;

rozpocząć

Clrscr;

writeln(' WPISZ ŁAŃCUCH ');

readln(sc);

CreateStack(pTop, sc);

powtarzać

writeln(' WPISZ ŁAŃCUCH ');

readln(sc);

AddComp(pTop, sc);

do sC = 'KONIEC';

writeln('****** WYJŚCIE ******');

powtarzać

DelComp(pTop, sc);

napisane(sC);

dopóki pTop = NIL;

koniec.

3. Kolejki

Kolejka to dynamiczna struktura danych, w której składnik jest dodawany na jednym końcu i pobierany na drugim końcu. Kolejka działa na zasadzie FIFO (First-In, First-Out) – „pierwszy weszło, ten lepszy”.

Do utworzenia kolejki i pracy z nią niezbędne są trzy zmienne typu wskaźnikowego, z których pierwsza określa początek kolejki, druga koniec kolejki, trzecia pomocnicza.

Przykład. Napisz program, który tworzy kolejkę, dodaje do niej dowolną liczbę komponentów, a następnie odczytuje wszystkie komponenty i wyświetla je na ekranie wyświetlacza. Weź ciąg znaków jako dane. Wprowadzanie danych - z klawiatury, znak końca wprowadzania - ciąg znaków END.

Program KOLEJKA;

używa Crt;

rodzaj

Alfa = Ciąg[10];

PZm = ^Zm;

Comp = rekord

SD: Alfa;

pNastępny : PCComp;

puszki;

było

pPoczątek, koniec : PComp;

sc: Alfa;

Utwórz ProcedureQueue(var pPoczątek,pEnd:PComp; var sC:Alfa);

rozpocząć

Nowy(pRozpocznij);

pPoczątek^.pNastępny := NIL;

pPoczątek^.sD := sC;

koniec := pPoczątek;

puszki;

Procedura Dodaj ProcedureQueue(var pEnd : PComp; var sc : Alfa);

var pAux: PCp;

rozpocząć

Nowy(pAux);

pAux^.pDalej := NIL;

pKoniec^.pNastępny := pAux;

pEnd := pAux;

koniec^.sD := sC;

puszki;

Procedura DelQueue(var pPoczątek : PComp; var sc : Alfa);

rozpocząć

sc := pPoczątek^.sD;

pPoczątek := pPoczątek^.pNastępny;

puszki;

rozpocząć

Clrscr;

writeln(' WPISZ ŁAŃCUCH ');

readln(sc);

CreateQueue(pPoczątek, koniec, sc);

powtarzać

writeln(' WPISZ ŁAŃCUCH ');

readln(sc);

AddQueue(pEnd, sc);

do sC = 'KONIEC';

writeln(' ***** WYŚWIETL WYNIKI *****');

powtarzać

DelQueue(pRozpocznij, sc);

napisane(sC);

do pPoczątek = NIL;

koniec.

WYKŁAD nr 9. Drzewiaste struktury danych

1. Struktury danych drzewa

Drzewopodobna struktura danych to skończony zbiór elementów-węzłów, pomiędzy którymi istnieją relacje – połączenie między źródłem a generowanym.

Jeśli użyjemy definicji rekurencyjnej zaproponowanej przez N. Wirtha, to drzewiasta struktura danych o typie bazowym t jest albo strukturą pustą, albo węzłem typu t, z którym jest skończony zbiór struktur drzewiastych o typie bazowym t, zwanych poddrzewami. powiązany.

Następnie podajemy definicje używane podczas pracy ze strukturami drzewiastymi.

Jeśli węzeł y znajduje się bezpośrednio pod węzłem x, to węzeł y nazywany jest bezpośrednim potomkiem węzła x, a x jest bezpośrednim przodkiem węzła y, czyli jeśli węzeł x znajduje się na i-tym poziomie, to węzeł y jest odpowiednio zlokalizowany na (i + 1) - tym poziomie.

Maksymalny poziom węzła drzewa nazywa się wysokością lub głębokością drzewa. Przodek nie posiada tylko jednego węzła drzewa – jego korzenia.

Węzły drzewa, które nie mają dzieci, nazywane są węzłami liści (lub liśćmi drzewa). Wszystkie inne węzły nazywane są węzłami wewnętrznymi. Liczba bezpośrednich dzieci węzła określa stopień tego węzła, a maksymalny możliwy stopień węzła w danym drzewie określa stopień drzewa.

Przodków i potomków nie można wymieniać, to znaczy, że połączenie między oryginałem a wygenerowanym działa tylko w jednym kierunku.

Jeśli przejdziesz od korzenia drzewa do jakiegoś konkretnego węzła, wtedy liczba gałęzi drzewa, przez które przejdziesz w tym przypadku, nazywana jest długością ścieżki dla tego węzła. Jeśli wszystkie gałęzie (węzły) drzewa są uporządkowane, mówi się, że drzewo jest uporządkowane.

Drzewa binarne to szczególny przypadek struktur drzewiastych. Są to drzewa, w których każde dziecko ma najwyżej dwoje dzieci, zwane poddrzewem lewym i prawym. Zatem drzewo binarne jest strukturą drzewa, której stopień wynosi dwa.

Kolejność drzewa binarnego jest określona przez następującą regułę: każdy węzeł ma swoje własne pole klucza, a dla każdego węzła wartość klucza jest większa niż wszystkich kluczy w jego lewym poddrzewie i mniejsza niż wszystkich kluczy w jego prawym poddrzewie.

Drzewo, którego stopień jest większy niż dwa, nazywamy silnie rozgałęzionym.

2. Operacje na drzewach

Dalej rozważymy wszystkie operacje w odniesieniu do drzew binarnych.

I. Budowa drzew

Przedstawiamy algorytm budowy uporządkowanego drzewa.

1. Jeśli drzewo jest puste, dane są przesyłane do korzenia drzewa. Jeśli drzewo nie jest puste, to jedna z jego gałęzi jest opada w taki sposób, że kolejność drzewa nie jest naruszona. W rezultacie nowy węzeł staje się kolejnym liściem drzewa.

2. Aby dodać węzeł do już istniejącego drzewa, możesz użyć powyższego algorytmu.

3. Podczas usuwania węzła z drzewa należy zachować ostrożność. Jeśli usuwany węzeł jest liściem lub ma tylko jedno dziecko, operacja jest prosta. Jeśli usuwany węzeł ma dwóch potomków, to wśród jego potomków konieczne będzie znalezienie węzła, który można umieścić na jego miejscu. Jest to konieczne ze względu na wymóg uporządkowania drzewka.

Możesz to zrobić: zamień węzeł do usunięcia na węzeł z największą wartością klucza w lewym poddrzewie lub na węzeł z najmniejszą wartością klucza w prawym poddrzewie, a następnie usuń żądany węzeł jako liść.

II. Znalezienie węzła z podaną wartością pola kluczowego

Podczas wykonywania tej operacji konieczne jest przejście drzewa. Należy wziąć pod uwagę różne formy notacji drzewa: przedrostek, wrostek i przyrostek.

Powstaje pytanie: jak reprezentować węzły drzewa, aby najwygodniej z nimi pracować? Możliwe jest przedstawienie drzewa za pomocą tablicy, gdzie każdy węzeł jest opisany wartością typu złożonego, która posiada pole informacyjne typu znakowego i dwa pola typu referencyjnego. Ale nie jest to zbyt wygodne, ponieważ drzewa mają dużą liczbę węzłów, które nie są z góry określone. Dlatego najlepiej jest używać zmiennych dynamicznych podczas opisywania drzewa. Wtedy każdy węzeł jest reprezentowany przez wartość tego samego typu, która zawiera opis danej liczby pól informacyjnych, a liczba odpowiadających im pól musi być równa stopniowi drzewa. Logiczne jest określenie braku potomków z zerem. Wtedy w Pascalu opis drzewa binarnego może wyglądać tak:

TYP TreeLink = ^Drzewo;

drzewo = rekord;

Inf : <typ danych>;

Lewo, Prawo : TreeLink;

Koniec

3. Przykłady realizacji operacji

1. Skonstruuj drzewo składające się z n węzłów o minimalnej wysokości lub drzewo idealnie zrównoważone (liczba węzłów lewego i prawego poddrzewa takiego drzewa nie może różnić się o więcej niż jeden).

Rekurencyjny algorytm konstrukcji:

1) pierwszy węzeł jest traktowany jako korzeń drzewa.

2) lewe poddrzewo nl węzłów jest zbudowane w ten sam sposób.

3) w ten sam sposób zbudowane jest prawe poddrzewo węzłów nr;

nr = n - nl - 1. Jako pole informacyjne przyjmiemy numery węzłów wprowadzone z klawiatury. Funkcja rekurencyjna implementująca tę konstrukcję będzie wyglądać tak:

Drzewo funkcji (n : Byte) : TreeLink;

Zmienna : TreeLink; nl,nr,x : Bajt;

Rozpocząć

Jeśli n = 0 to Drzewo := nil

Więcej

Rozpocząć

nl := n dział 2;

nr = n - nl - 1;

writeln('Podaj numer wierzchołka');

odczytln(x);

traszka);

t^.inf := x;

t^.lewo := Drzewo(nl);

t^.prawo := Drzewo(nr);

Drzewo := t;

End;

{Drzewo}

Koniec

2. W drzewie uporządkowanym binarnie znajdź węzeł z podaną wartością pola klucza. Jeśli nie ma takiego elementu w drzewie, dodaj go do drzewa.

Procedura wyszukiwania(x : Byte; var t : TreeLink);

Rozpocząć

Jeśli t = zero, to

Rozpocząć

Traszka);

t^inf := x;

t^.lewo := zero;

t^.prawo := zero;

Koniec

W przeciwnym razie, jeśli x < t^.inf to

Szukaj(x, t^.lewo)

W przeciwnym razie, jeśli x > t^.inf to

Szukaj(x, t^.prawo)

Więcej

Rozpocząć

{przetworzyć znaleziony element}

...

End;

Koniec

3. Napisz procedury przechodzenia przez drzewa w kolejności odpowiednio do przodu, symetrycznej i odwrotnej.

3.1. Procedura Preorder(t : TreeLink);

Rozpocząć

Jeśli t <> nil to

Rozpocząć

Zapis(t^.inf);

Zamów w przedsprzedaży(t^.po lewej);

Zamów w przedsprzedaży(t^.prawo);

End;

End;

3.2. Procedura Inorder(t : TreeLink);

Rozpocząć

Jeśli t <> nil to

Rozpocząć

Inorder(t^.lewo);

Zapis(t^.inf);

Kolejność(t^.prawo);

End;

Koniec

3.3. Procedura Postorder(t : TreeLink);

Rozpocząć

Jeśli t <> nil to

Rozpocząć

postorder(t^.po lewej);

postorder(t^.prawo);

Zapis(t^.inf);

End;

Koniec

4. W drzewie uporządkowanym binarnie usuń węzeł z podaną wartością pola klucza.

Opiszmy procedurę rekurencyjną, która uwzględni obecność wymaganego elementu w drzewie oraz liczbę potomków tego węzła. Jeśli usuwany węzeł ma dwoje dzieci, zostanie zastąpiony największą wartością klucza w lewym poddrzewie i dopiero wtedy zostanie trwale usunięty.

Procedura Delete1(x : Byte; var t : TreeLink);

War p : TreeLink;

Procedura Delete2(var q : TreeLink);

Rozpocząć

Jeśli q^.right <> nil, to Usuń2(q^.right)

Więcej

Rozpocząć

p^.inf := q^.inf;

p := q;

q := q^.lewo;

End;

End;

Rozpocząć

Jeśli t = zero, to

Writeln('nie znaleziono elementu')

W przeciwnym razie, jeśli x < t^.inf to

Usuń1(x, t^.lewo)

W przeciwnym razie, jeśli x > t^.inf to

Usuń1(x, t^.prawo)

Więcej

Rozpocząć

P. := t;

Jeśli p^.left = zero, to

t := p^.prawo

Więcej

Jeśli p^.right = zero, to

t := p^.lewo

Więcej

Usuń2(p^.lewo);

End;

Koniec

WYKŁAD nr 10. Liczy

1. Pojęcie grafu. Sposoby reprezentowania wykresu

Graf to para G = (V,E), gdzie V to zbiór obiektów o dowolnym charakterze, zwanych wierzchołkami, a E to rodzina par ei = (vil, vi2), vijOV, zwanych krawędziami. W ogólnym przypadku zbiór V i/lub rodzina E może zawierać nieskończoną liczbę elementów, ale rozważymy tylko grafy skończone, czyli takie, dla których zarówno V, jak i E są skończone. Jeżeli kolejność elementów zawartych w ei ma znaczenie, to graf nazywamy skierowanym, w skrócie - digrafem, inaczej - nieskierowanym. Krawędzie dwuznaku nazywane są łukami. W dalszej części przyjmiemy, że termin „graf” użyty bez specyfikacji (skierowany lub nieskierowany) oznacza graf nieskierowany.

Jeśli e = , to wierzchołki v i u nazywane są końcami krawędzi. Tutaj mówimy, że krawędź e przylega (incydent) do każdego z wierzchołków v i u. Wierzchołki v oraz i są również nazywane sąsiednimi (incydentami). W ogólnym przypadku krawędzie postaci e = ; takie krawędzie nazywane są pętlami.

Stopień wierzchołka na grafie to liczba krawędzi przypadających na ten wierzchołek, przy czym pętle są liczone dwukrotnie. Ponieważ każda krawędź jest incydentna z dwoma wierzchołkami, suma stopni wszystkich wierzchołków grafu jest równa dwukrotności liczby krawędzi: Sum(deg(vi), i=1...|V|) = 2 * | E|.

Waga węzła to liczba (rzeczywista, całkowita lub wymierna) przypisana do danego węzła (interpretowana jako koszt, przepustowość itp.). Waga, długość krawędzi - liczba lub kilka liczb, które są interpretowane jako długość, szerokość pasma itp.

Ścieżka w grafie (lub trasa w digrafie) to naprzemienna sekwencja wierzchołków i krawędzi (lub łuków w digrafie) postaci v0, (v0,v1), v1..., (vn - 1,vn ), w. Liczba n nazywana jest długością ścieżki. Ścieżkę bez powtarzających się krawędzi nazywamy łańcuchem, a ścieżkę bez powtarzających się wierzchołków nazywamy łańcuchem prostym. Ścieżkę można zamknąć (v0 = vn). Zamknięta ścieżka bez powtarzających się krawędzi nazywana jest cyklem (lub konturem w dwuznaku); bez powtarzania wierzchołków (z wyjątkiem pierwszego i ostatniego) - prosta pętla.

Graf jest połączony, jeśli istnieje ścieżka między dowolnymi dwoma jego wierzchołkami, aw przeciwnym razie jest rozłączony. Rozłączony graf składa się z kilku połączonych składowych (połączonych podgrafów).

Istnieje wiele sposobów reprezentowania wykresów. Rozważmy każdy z nich osobno.

1. Macierz incydentów.

Jest to macierz prostokątna o wymiarze nx n, gdzie n to liczba wierzchołków, a am to liczba krawędzi. Wartości elementów macierzy wyznacza się w następujący sposób: jeżeli krawędź xi i wierzchołek vj są incydentne, to wartość odpowiedniego elementu macierzy jest równa jedności, w przeciwnym wypadku wartość wynosi zero. Dla grafów skierowanych macierz częstości konstruowana jest według następującej zasady: wartość elementu jest równa - 1, jeśli krawędź xi pochodzi z wierzchołka vj, równa 1, jeśli krawędź xi wchodzi w wierzchołek vj, i równa XNUMX w przeciwnym razie .

2. Macierz sąsiedztwa.

Jest to macierz kwadratowa o wymiarze nxn, gdzie n jest liczbą wierzchołków. Jeżeli wierzchołki vi i vj sąsiadują ze sobą, czyli łączy je krawędź, to odpowiadający im element macierzy jest równy jeden, w przeciwnym razie jest równy zero. Zasady konstruowania tej macierzy dla grafów skierowanych i nieskierowanych nie różnią się. Macierz sąsiedztwa jest bardziej zwarta niż macierz częstości. Należy zauważyć, że ta macierz jest również bardzo rzadka, ale w przypadku grafu nieskierowanego jest symetryczna względem głównej przekątnej, więc można przechowywać nie całą macierz, ale tylko jej połowę (macierz trójkątna ).

3. Lista przyległości (incydentów).

Jest to struktura danych, która przechowuje listę sąsiednich wierzchołków dla każdego wierzchołka wykresu. Lista jest tablicą wskaźników, której i-ty element zawiera wskaźnik do listy wierzchołków sąsiadujących z i-tym wierzchołkiem.

Lista sąsiedztwa jest bardziej wydajna niż macierz sąsiedztwa, ponieważ eliminuje przechowywanie elementów zerowych.

4. Lista list.

Jest to struktura danych podobna do drzewa, w której jedna gałąź zawiera listy wierzchołków sąsiadujących z każdym z wierzchołków grafu, a druga gałąź wskazuje na następny wierzchołek grafu. Ten sposób przedstawiania wykresu jest najbardziej optymalny.

2. Reprezentacja wykresu przez listę zdarzeń. Algorytm przechodzenia przez głębokość wykresu

Aby zaimplementować wykres jako listę zdarzeń, możesz użyć następującego typu:

Lista typów = ^S;

S=rekord;

inf : bajt;

następny : Lista;

puszki;

Wtedy wykres definiujemy następująco:

Var Gr : tablica[1..n] listy;

Przejdźmy teraz do procedury przechodzenia przez wykres. Jest to algorytm pomocniczy, który pozwala przeglądać wszystkie wierzchołki wykresu, analizować wszystkie pola informacyjne. Jeśli rozważymy przechodzenie grafu dogłębnie, to istnieją dwa rodzaje algorytmów: rekurencyjne i nierekurencyjne.

Za pomocą rekurencyjnego algorytmu przechodzenia w głąb, bierzemy dowolny wierzchołek i znajdujemy dowolny niewidoczny (nowy) wierzchołek v sąsiadujący z nim. Następnie przyjmujemy wierzchołek v jako nie nowy i znajdujemy dowolny nowy wierzchołek sąsiadujący z nim. Jeśli jakiś wierzchołek nie ma nowszych niewidocznych wierzchołków, uważamy, że ten wierzchołek jest używany i zwracamy jeden poziom wyżej do wierzchołka, z którego dotarliśmy do naszego używanego wierzchołka. Przemierzanie jest kontynuowane w ten sposób, dopóki na wykresie nie pojawią się nowe nieprzeskanowane wierzchołki.

W Pascalu procedura przechodzenia w głąb wyglądałaby tak:

Procedura Obhod(gr : wykres; k : bajt);

Var g : Wykres; l : Lista;

Rozpocząć

lis[k] := fałsz;

sol := gr;

Podczas gdy g^.inf <> k do

g := g^.następny;

l := g^.smeg;

Podczas gdy l <> nic nie zaczynam

Jeśli nov[l^.inf] to Obhod(gr, l^.inf);

l := l^.następny;

End;

End;

Operacja

W tej procedurze, opisując typ Graph, mieliśmy na myśli opis wykresu za pomocą listy list. Array nov[i] jest specjalną tablicą, której i-ty element ma wartość True, jeśli i-ty wierzchołek nie jest odwiedzany, a False w przeciwnym razie.

Często używany jest również nierekurencyjny algorytm przechodzenia. W takim przypadku rekurencja jest zastępowana stosem. Po obejrzeniu wierzchołka jest on umieszczany na stosie i staje się używany, gdy nie ma już sąsiadujących z nim nowych wierzchołków.

3. Reprezentacja wykresu przez listę list. Algorytm przechodzenia grafu szerokości

Wykres można zdefiniować za pomocą listy list w następujący sposób:

TypeList = ^Tlista;

tlist=rekord

inf : bajt;

następny : Lista;

puszki;

Wykres = ^TGpaph;

TGpaph = rekord

inf : bajt;

smeg : Lista;

następny : Wykres;

puszki;

Przemierzając wykres wszerz, wybieramy dowolny wierzchołek i jednocześnie przeglądamy wszystkie sąsiadujące z nim wierzchołki. Zamiast stosu używana jest kolejka. Algorytm wyszukiwania wszerz jest bardzo przydatny do znajdowania najkrótszej ścieżki w grafie.

Oto procedura przechodzenia po grafie na szerokość w pseudokodzie:

Procedura Obhod2(v);

{wartości spisok, lis - globalne}

Rozpocząć

kolejka = O;

kolejka <= v;

nov[v] = Fałsz;

Podczas kolejki <> O do

Rozpocząć

p <= kolejka;

Dla u w spisok(p) do

Jeśli nowy[u] to

Rozpocząć

lis[u] := Fałsz;

kolejka <= u;

End;

End;

End;

WYKŁAD 11. Typ danych obiektowych

1. Typ obiektu w Pascalu. Pojęcie przedmiotu, jego opis i zastosowanie

Historycznie pierwszym podejściem do programowania było programowanie proceduralne, znane również jako programowanie oddolne. Początkowo tworzono wspólne biblioteki standardowych programów wykorzystywanych w różnych dziedzinach aplikacji komputerowych. Następnie w oparciu o te programy tworzono bardziej złożone programy do rozwiązywania konkretnych problemów.

Jednak technologia komputerowa stale się rozwijała, zaczęła być wykorzystywana do rozwiązywania różnych problemów produkcyjnych, ekonomicznych, dlatego konieczne stało się przetwarzanie danych o różnych formatach i rozwiązywanie niestandardowych problemów (na przykład nienumerycznych). Dlatego przy opracowywaniu języków programowania zaczęto zwracać uwagę na tworzenie różnego rodzaju danych. Przyczyniło się to do powstania tak złożonych typów danych jak łączone, wielokrotne, łańcuchowe, plikowe itp. Przed rozwiązaniem problemu programista przeprowadzał dekompozycję, czyli rozbicie zadania na kilka podzadań, dla których napisano osobny moduł . Główna technologia programowania obejmowała trzy etapy:

1) projekt odgórny;

2) programowanie modułowe;

3) kodowanie strukturalne.

Ale począwszy od połowy lat 60. XX wieku zaczęły powstawać nowe koncepcje i podejścia, które stanowiły podstawę technologii programowania obiektowego. W tym podejściu modelowanie i opis świata rzeczywistego odbywa się na poziomie pojęć z określonego obszaru tematycznego, do którego należy rozwiązywany problem.

Programowanie obiektowe to technika programowania, która bardzo przypomina nasze zachowanie. Jest to naturalna ewolucja wcześniejszych innowacji w projektowaniu języka programowania. Programowanie obiektowe jest bardziej strukturalne niż wszystkie poprzednie osiągnięcia dotyczące programowania strukturalnego. Jest również bardziej modułowa i bardziej abstrakcyjna niż poprzednie próby wewnętrznej abstrakcji danych i szczegółów programowania. Język programowania obiektowego charakteryzuje się trzema głównymi właściwościami:

1) Hermetyzacja. Połączenie rekordów z procedurami i funkcjami manipulującymi polami tych rekordów tworzy nowy typ danych - obiekt;

2) Dziedziczenie. Definicja obiektu i jego dalsze wykorzystanie do budowy hierarchii obiektów podrzędnych z możliwością dostępu do kodu i danych wszystkich obiektów nadrzędnych dla każdego obiektu podrzędnego powiązanego z hierarchią;

3) Polimorfizm. Nadanie akcji pojedynczej nazwy, która jest następnie udostępniana w górę iw dół hierarchii obiektów, przy czym każdy obiekt w hierarchii wykonuje tę akcję w sposób, który jej odpowiada.

Mówiąc o obiekcie, wprowadzamy nowy typ danych - obiekt. Typ obiektu to struktura składająca się ze stałej liczby komponentów. Każdy składnik to albo pole zawierające dane o ściśle określonym typie, albo metoda wykonująca operacje na obiekcie. Analogicznie do deklaracji zmiennych deklaracja pola określa typ danych tego pola oraz identyfikator nazywający pole: analogicznie do deklaracji procedury lub funkcji deklaracja metody określa tytuł procedury, funkcja, konstruktor lub destruktor.

Typ obiektu może dziedziczyć komponenty innego typu obiektu. Jeśli typ T2 dziedziczy po typie T1, to typ T2 jest dzieckiem typu T1, a sam typ T1 jest rodzicem typu T2. Dziedziczenie jest przechodnie, tj. jeśli TK dziedziczy po T2, a T2 dziedziczy po T1, to TK dziedziczy po T1. Zakres (domena) typu obiektu składa się z niego samego i wszystkich jego potomków.

Poniższy kod źródłowy jest przykładem deklaracji typu obiektu, type

rodzaj

punkt = obiekt

X, Y: liczba całkowita;

puszki;

Prost = obiekt

A, B: TPunkt;

procedura Init(XA, YA, XB, YB: liczba całkowita);

procedura Copy(var R: TRectangle);

procedura Przenieś(DX, DY: liczba całkowita);

procedura Grow(DX, DY: liczba całkowita);

procedura Intersect(var R: TRectangle);

procedura Union(var R: TRectangle);

funkcja Zawiera(P: Punkt): Boolean;

puszki;

StringPtr = ^Ciąg;

PolePtr = ^TFPole;

Pole = obiekt

X, Y, Len: liczba całkowita;

Nazwa: CiągPtr;

konstruktor Kopiuj(var F: TField);

konstruktor Init(FX, FY, FLen: Integer; FName: String);

destruktor Gotowe; wirtualny;

procedura Wyświetlacz; wirtualny;

procedura Edytuj; wirtualny;

funkcja GetStr: ciąg; wirtualny;

function PutStr(S: String): Boolean; wirtualny;

puszki;

StrFieldPtr = ^TStField;

StrField = obiekt(TField)

Wartość: PSString;

konstruktor Init(FX, FY, FLen: Integer; FName: String);

destruktor Gotowe; wirtualny;

funkcja GetStr: ciąg; wirtualny;

function PutStr(S: Ciąg): Boolean;

wirtualny;

funkcja Get:ciąg;

procedura Put(S: String);

puszki;

NumFieldPtr = ^TNumField;

TNumField = obiekt(TField)

prywatny

Wartość, Min., Maks.: Longint;

publiczny

konstruktor Init(FX, FY, FLen: Liczba całkowita; FName: String;

Fmin, FMaks: Longint);

funkcja GetStr: ciąg; wirtualny;

function PutStr(S: String): Boolean; wirtualny;

funkcja Pobierz: Longint;

funkcja Put(N: Longint);

puszki;

ZipFieldPtr = ^TZipField;

ZipField = obiekt(TNumField)

funkcja GetStr: ciąg; wirtualny;

function PutStr(S: Ciąg): Boolean;

wirtualny;

koniec.

W przeciwieństwie do innych typów, typy obiektów można deklarować tylko w sekcji deklaracji typu na zewnętrznym poziomie zakresu programu lub modułu. W związku z tym typy obiektów nie mogą być deklarowane w sekcji deklaracji zmiennej lub wewnątrz procedury, funkcji lub bloku metody.

Typ komponentu typu pliku nie może mieć typu obiektowego ani żadnego typu struktury zawierającego komponenty typu obiektowego.

2. Dziedziczenie

Proces, w którym jeden typ dziedziczy cechy innego typu, nazywa się dziedziczeniem. Potomek jest nazywany typem pochodnym (podrzędnym), a typ, z którego dziedziczy typ podrzędny, jest nazywany typem rodzica (rodzica).

Wcześniej znane typy rekordów Pascala nie mogą dziedziczyć. Jednak Borland Pascal rozszerza język Pascal o obsługę dziedziczenia. Jednym z tych rozszerzeń jest nowa kategoria struktury danych związana z rekordami, ale o wiele potężniejsza. Typy danych w tej nowej kategorii są definiowane przy użyciu nowego zastrzeżonego słowa „obiekt”. Typ obiektowy może być zdefiniowany jako kompletny, niezależny typ w sposób opisywania wpisów Pascala, ale może być również zdefiniowany jako potomek istniejącego typu obiektowego poprzez umieszczenie typu nadrzędnego w nawiasach po słowie zarezerwowanym "object".

3. Tworzenie instancji obiektów

Instancja obiektu jest tworzona przez zadeklarowanie zmiennej lub stałej typu obiektu lub zastosowanie standardowej procedury New do zmiennej typu „wskaźnik do typu obiektu”. Wynikowy obiekt jest nazywany instancją typu obiektu;

było

F: Pole;

Z: TZipField;

FP: PFpole;

ZP: PZipField;

Biorąc pod uwagę te deklaracje zmiennych, F jest instancją TField, a Z jest instancją TZipField. Podobnie, po zastosowaniu New do FP i ZP, FP wskaże instancję TField, a ZP wskaże instancję TZipField.

Jeśli typ obiektu zawiera metody wirtualne, wystąpienia tego typu obiektu muszą zostać zainicjowane przez wywołanie konstruktora przed wywołaniem dowolnej metody wirtualnej.

Poniżej przykład:

było

S: StrPole;

np

S.Init(1, 1, 25, 'Imię');

S.Put('Władimir');

S.Wyświetlacz;

...

S Gotowe;

koniec.

Jeśli S.Init nie został wywołany, wywołanie S.Display spowoduje niepowodzenie tego przykładu.

Przypisanie wystąpienia typu obiektu nie oznacza zainicjowania wystąpienia. Obiekt jest inicjowany przez kod wygenerowany przez kompilator, który jest uruchamiany między wywołaniem konstruktora a punktem, w którym wykonanie faktycznie osiąga pierwszą instrukcję w bloku kodu konstruktora.

Jeśli instancja obiektu nie jest zainicjowana i włączone jest sprawdzanie zakresu (przez dyrektywę {SR+}), to pierwsze wywołanie metody wirtualnej instancji obiektu daje błąd wykonania. Jeśli sprawdzanie zakresu jest wyłączone (przez dyrektywę {SR-}), to pierwsze wywołanie metody wirtualnej niezainicjowanego obiektu może prowadzić do nieprzewidywalnego zachowania.

Obowiązkowa reguła inicjowania dotyczy również wystąpień, które są składnikami typów struktur. Na przykład:

było

Komentarz: tablica [1..5] TStrField;

I: liczba całkowita

rozpocząć

dla I := 1 do 5 do

Komentarz [I].Init (1, I + 10, 40, 'imię');

.

.

.

for I := 1 do 5 wykonaj Komentarz [I].Gotowe;

puszki;

W przypadku wystąpień dynamicznych inicjowanie zwykle dotyczy umieszczania, a czyszczenie dotyczy usuwania, co jest osiągane za pomocą rozszerzonej składni standardowych procedur New i Dispose. Na przykład:

było

SP: strFieldPtr;

rozpocząć

New(SP, Init(1, 1, 25, 'imię');

SP^.Put('Władimir');

SP^.Wyświetlacz;

.

.

.

Utylizacja (SP, Gotowe);

koniec.

Wskaźnik do typu obiektu jest przypisaniem zgodnym ze wskaźnikiem do dowolnego typu obiektu nadrzędnego, więc w czasie wykonywania wskaźnik do typu obiektu może wskazywać na wystąpienie tego typu lub na wystąpienie dowolnego typu podrzędnego.

Na przykład wskaźnik typu ZipFieldPtr można przypisać do wskaźników typu PZipField, PNumField i PField, a w czasie wykonywania wskaźnik typu PField może mieć wartość zero lub wskazywać na wystąpienie TField, TNumField lub TZipField lub dowolne wystąpienie typu podrzędnego TField.

Te reguły zgodności wskaźnika przypisania dotyczą również parametrów typu obiektu. Na przykład do metody TField.Cop można przekazywać wystąpienia TField, TStrField, TNumField, TZipField lub dowolnego innego typu elementu podrzędnego TField.

4. Komponenty i zakres

Zakres identyfikatora ziarna wykracza poza typ obiektu. Co więcej, zakres identyfikatora bean rozciąga się na bloki procedur, funkcji, konstruktorów i destruktorów, które implementują metody typu obiektu i jego potomków. Na podstawie tych rozważań pisownia identyfikatora komponentu musi być unikalna w obrębie typu obiektu i we wszystkich jego potomkach, a także we wszystkich jego metodach.

Zakres identyfikatora komponentu opisanego w prywatnej części deklaracji typu jest ograniczony do modułu (programu), który zawiera deklarację typu obiektu. Innymi słowy, prywatne ziarna identyfikatorów działają jak zwykłe publiczne identyfikatory w module, który zawiera deklarację typu obiektu, a poza modułem wszelkie prywatne ziarna i identyfikatory są nieznane i niedostępne. Umieszczając powiązane typy obiektów w tym samym module, możesz sprawić, by te obiekty miały dostęp do swoich prywatnych komponentów, a te prywatne komponenty będą nieznane innym modułom.

W deklaracji typu obiektu nagłówek metody może określać parametry opisywanego typu obiektu, nawet jeśli deklaracja nie jest jeszcze kompletna.

WYKŁAD nr 12. Metody

1. Metody

Deklaracja metody wewnątrz typu obiektu odpowiada deklaracji metody do przodu (forward). Tak więc gdzieś po deklaracji typu obiektu, ale w tym samym zakresie co deklaracja typu obiektu, metoda musi zostać zaimplementowana poprzez zdefiniowanie jej deklaracji.

W przypadku metod proceduralnych i funkcjonalnych deklaracja definiująca przyjmuje postać normalnej procedury lub deklaracji funkcji, z tym wyjątkiem, że w tym przypadku identyfikator procedury lub funkcji jest traktowany jako identyfikator metody.

W przypadku metod konstruktora i destruktora deklaracja definiująca przyjmuje postać deklaracji metody procedury, z wyjątkiem tego, że procedura słowa zastrzeżonego jest zastępowana konstruktorem lub destruktorem słowa zastrzeżonego.

Deklaracja metody definiującej może, ale nie musi, powtarzać listę parametrów formalnych nagłówka metody w typie obiektu. W takim przypadku nagłówek metody musi dokładnie odpowiadać nagłówkowi w typie obiektu w kolejności, typach i nazwach parametrów oraz w typie zwracanym wyniku funkcji, jeśli metoda jest funkcją.

Definiujący opis metody zawsze zawiera niejawny parametr o identyfikatorze Self, odpowiadający parametrowi zmiennej formalnej, która ma typ obiektowy. W bloku metody Self reprezentuje wystąpienie, którego składnik metody został określony w celu wywołania metody. W ten sposób wszelkie zmiany wartości pól Self są odzwierciedlane w instancji.

Zakres identyfikatora bean typu obiektu rozciąga się na bloki procedur, funkcji, konstruktorów i destruktorów, które implementują metody tego typu obiektu. Efekt jest taki sam, jak gdyby na początku bloku metody wstawiono instrukcję with o następującej postaci:

z samym sobą

rozpocząć

...

puszki;

Na podstawie tych rozważań pisownia identyfikatorów komponentów, formalnych parametrów metody, Self i wszelkich identyfikatorów wprowadzonych do części wykonywalnej metody musi być unikalna.

Jeśli wymagany jest unikalny identyfikator metody, używany jest kwalifikowany identyfikator metody. Składa się z identyfikatora typu obiektu, po którym następuje kropka i identyfikator metody. Jak w przypadku każdego innego identyfikatora, kwalifikowany identyfikator metody może opcjonalnie być poprzedzony identyfikatorem pakietu i kropką.

Metody wirtualne

Metody są domyślnie statyczne, ale z wyjątkiem konstruktorów mogą być wirtualne (poprzez dołączenie dyrektywy virtual w deklaracji metody). Kompilator rozwiązuje odwołania do wywołań metod statycznych podczas procesu kompilacji, podczas gdy wywołania metod wirtualnych są rozwiązywane w czasie wykonywania. Nazywa się to czasem późnym wiązaniem.

Jeśli typ obiektu deklaruje lub dziedziczy dowolną metodę wirtualną, zmienne tego typu muszą zostać zainicjowane przez wywołanie konstruktora przed wywołaniem dowolnej metody wirtualnej. W związku z tym typ obiektu, który opisuje lub dziedziczy metodę wirtualną, musi również opisywać lub dziedziczyć co najmniej jedną metodę konstruktora.

Typ obiektu może przesłonić dowolną z metod, które dziedziczy po swoich rodzicach. Jeśli deklaracja metody w dziecku określa ten sam identyfikator metody, co deklaracja metody w elemencie nadrzędnym, wówczas deklaracja w elemencie podrzędnym zastępuje deklarację w elemencie nadrzędnym. Zakres zastępującej metody rozszerza się do zakresu elementu podrzędnego, w którym metoda została wprowadzona, i pozostanie taki, dopóki identyfikator metody nie zostanie ponownie zastąpiony.

Zastąpienie metody statycznej jest niezależne od zmiany nagłówka metody. W przeciwieństwie do tego, przesłonięcie metody wirtualnej musi zachować kolejność, typy i nazwy parametrów oraz typy wyników funkcji, jeśli takie istnieją. Ponadto redefinicja musi ponownie obejmować dyrektywę wirtualną.

Metody dynamiczne

Borland Pascal obsługuje dodatkowe metody późnego wiązania zwane metodami dynamicznymi. Metody dynamiczne różnią się od metod wirtualnych tylko sposobem, w jaki są wysyłane w czasie wykonywania. Pod wszystkimi innymi względami metody dynamiczne są uważane za równoważne metodom wirtualnym.

Deklaracja metody dynamicznej jest równoważna deklaracji metody wirtualnej, ale deklaracja metody dynamicznej musi zawierać indeks metody dynamicznej, który jest określony bezpośrednio po słowie kluczowym virtual. Indeks metody dynamicznej musi być stałą całkowitą z zakresu od 1 do 656535 i musi być unikalny wśród indeksów innych metod dynamicznych zawartych w typie obiektu lub jego przodkach. Na przykład:

procedura FileOpen(var Msg: TMessage); wirtualny 100;

Zastąpienie metody dynamicznej musi być zgodne z kolejnością, typami i nazwami parametrów oraz dokładnie odpowiadać typowi wyniku funkcji metody nadrzędnej. Zastąpienie musi również zawierać dyrektywę wirtualną, po której następuje ten sam indeks metody dynamicznej, który został określony w typie obiektu przodka.

2. Konstruktory i destruktory

Konstruktory i destruktory to wyspecjalizowane formy metod. Stosowane w połączeniu z rozszerzoną składnią standardowych procedur New i Dispose, konstruktory i destruktory mają możliwość umieszczania i usuwania obiektów dynamicznych. Dodatkowo konstruktorzy mają możliwość wykonania wymaganej inicjalizacji obiektów zawierających metody wirtualne. Podobnie jak wszystkie metody, konstruktory i destruktory mogą być dziedziczone, a obiekty mogą zawierać dowolną liczbę konstruktorów i destruktorów.

Konstruktory służą do inicjowania nowo tworzonych obiektów. Zazwyczaj inicjalizacja opiera się na wartościach przekazanych do konstruktora jako parametry. Konstruktor nie może być wirtualny, ponieważ mechanizm wysyłania metody wirtualnej zależy od konstruktora, który jako pierwszy zainicjował obiekt.

Oto kilka przykładów konstruktorów:

konstruktor Field.Copy(var F: Field);

rozpocząć

Ja := F;

puszki;

konstruktor Field.Init(FX, FY, FLen: liczba całkowita; FName: ciąg);

rozpocząć

X := FX;

T := FY;

GetMem (nazwa, długość (nazwa F) + 1);

Nazwa^ := FNazwa;

puszki;

konstruktor TStrField.Init(FX, FY, FLen: liczba całkowita; FName: ciąg);

rozpocząć

odziedziczony Init(FX, FY, FLen, FName);

Field.Init(FX, FY, FLen, FNazwa);

PobierzMem(Wartość, Dł);

Wartość^ := '';

puszki;

Główna akcja konstruktora typu pochodnego (podrzędnego), takiego jak powyższe pole TStr. Init jest prawie zawsze wywołaniem odpowiedniego konstruktora jego bezpośredniego rodzica w celu zainicjowania dziedziczonych pól obiektu. Po wykonaniu tej procedury konstruktor inicjuje pola obiektu, które należą tylko do typu pochodnego.

Destruktory są przeciwieństwem konstruktorów i służą do czyszczenia obiektów po ich użyciu. Normalnie czyszczenie polega na usunięciu wszystkich pól wskaźnika w obiekcie.

Operacja

Destruktor może być wirtualny i często jest. Destruktor rzadko ma parametry.

Oto kilka przykładów destruktorów:

destruktor Pole gotowe;

rozpocząć

FreeMem(Nazwa, Długość(Nazwa^) + 1);

puszki;

destruktor StrField.Done;

rozpocząć

FreeMem(Wartość, Len);

Pole gotowe;

puszki;

Destruktor typu podrzędnego, takiego jak powyższy TStrField. Gotowe, zazwyczaj najpierw usuwa pola wskaźnika wprowadzone w typie pochodnym, a następnie, jako ostatni krok, wywołuje odpowiedni destruktor kolektora bezpośredniego elementu nadrzędnego, aby usunąć dziedziczone pola wskaźnika obiektu.

3. Destruktory

Borland Pascal zapewnia specjalny rodzaj metody zwanej garbage collector (lub destruktor) do czyszczenia i usuwania dynamicznie przydzielonego obiektu. Destruktor łączy etap usuwania obiektu z innymi czynnościami lub zadaniami wymaganymi dla tego typu obiektu. Dla jednego typu obiektu można zdefiniować wiele destruktorów.

Destruktor jest zdefiniowany wraz ze wszystkimi innymi metodami obiektowymi w definicji typu obiektu:

rodzaj

Pracownik = obiekt

Nazwa: ciąg[25];

Tytuł: ciąg[25];

Oceń: Realne;

konstruktor Init(AName, ATitle: String; ARate: Real);

destruktor Gotowe; wirtualny;

funkcja GetName: ciąg;

funkcja GetTitle: String;

funkcja GetRate: Oceń; wirtualny;

funkcja GetPayAmount: Real; wirtualny;

puszki;

Destruktory mogą być dziedziczone i mogą być statyczne lub wirtualne. Ponieważ różne finalizatory zwykle wymagają różnych typów obiektów, ogólnie zaleca się, aby destruktory były zawsze wirtualne, aby dla każdego typu obiektu wykonywany był poprawny destruktor.

Destruktor słowa zastrzeżonego nie musi być określany dla każdej metody czyszczenia, nawet jeśli definicja typu obiektu zawiera metody wirtualne. Destruktory tak naprawdę działają tylko na dynamicznie alokowanych obiektach.

Kiedy dynamicznie alokowany obiekt jest czyszczony, destruktor wykonuje specjalną funkcję: zapewnia, że ​​w dynamicznie alokowanym obszarze pamięci zawsze zwalniana jest prawidłowa liczba bajtów. Nie ma obaw o używanie destruktora ze statycznie przydzielonymi obiektami; w rzeczywistości, nie przekazując typu obiektu do destruktora, programista pozbawia obiekt tego typu pełnych korzyści dynamicznego zarządzania pamięcią w Borland Pascal.

Destruktory w rzeczywistości stają się sobą, gdy obiekty polimorficzne muszą zostać wyczyszczone, a pamięć, którą zajmują, musi zostać zwolniona.

Obiekty polimorficzne to te obiekty, które zostały przypisane do typu nadrzędnego ze względu na rozszerzone zasady zgodności typów Borland Pascal. Przykładem obiektu polimorficznego jest instancja obiektu typu THourly przypisanego do zmiennej typu TEmployee. Reguły te można również zastosować do obiektów; wskaźnik do THourly można dowolnie przypisać do wskaźnika do TEmployee, a obiekt wskazywany przez ten wskaźnik ponownie będzie obiektem polimorficznym. Termin „polimorficzny” jest odpowiedni, ponieważ kod przetwarzający obiekt „nie wie” dokładnie w czasie kompilacji, jakiego typu obiekt będzie ostatecznie musiał przetworzyć. Wie tylko, że ten obiekt należy do hierarchii obiektów, które są potomkami określonego typu obiektu.

Oczywiście rozmiary typów obiektów są różne. Kiedy więc przychodzi czas na wyczyszczenie obiektu polimorficznego przydzielonego do sterty, skąd Dispose wie, ile bajtów przestrzeni sterty ma zwolnić? W czasie kompilacji nie można uzyskać informacji o rozmiarze obiektu z obiektu polimorficznego.

Destruktor rozwiązuje tę zagadkę, odwołując się do miejsca, w którym zapisana jest ta informacja - w zmiennych implementacji TCM. Każda TBM typu obiektu zawiera rozmiar w bajtach tego typu obiektu. Tablica metod wirtualnych dowolnego obiektu jest dostępna poprzez ukryty parametr Self, wysyłany do metody w momencie wywołania metody. Destruktor jest tylko rodzajem metody, więc gdy obiekt go wywołuje, destruktor pobiera kopię Self na stosie. W związku z tym, jeśli obiekt jest polimorficzny w czasie kompilacji, nigdy nie będzie polimorficzny w czasie wykonywania z powodu późnego wiązania.

Aby wykonać tę alokację z późnym wiązaniem, należy wywołać destruktor w ramach rozszerzonej składni procedury Dispose:

Usuń (P, Gotowe);

(Wywołanie destruktora poza procedurą Dispose nie zwalnia w ogóle żadnej pamięci.) To, co się tutaj naprawdę dzieje, to to, że garbage collector obiektu wskazywanego przez P jest wykonywany jak normalna metoda. Jednak po zakończeniu ostatniej akcji destruktor wyszukuje rozmiar implementacji swojego typu w TCM i przekazuje rozmiar do procedury usuwania. Procedura Dispose kończy proces, usuwając odpowiednią liczbę bajtów przestrzeni sterty, która (miejsce) wcześniej należała do P^. Liczba bajtów do zwolnienia będzie poprawna niezależnie od tego, czy P wskazywał na instancję typu TSalaried, czy też wskazywał na jeden z typów podrzędnych typu TSalaried, takich jak TCommissioned.

Zauważ, że sama metoda destruktora może być pusta i wykonywać tylko tę funkcję:

destruktorObiekt.Wykonane;

rozpocząć

puszki;

To, co jest przydatne w tym destruktorze, nie jest własnością jego ciała, jednak kompilator generuje kod epilogu w odpowiedzi na słowo zastrzeżone destruktora. Jest jak moduł, który niczego nie eksportuje, ale wykonuje pewną niewidoczną pracę, wykonując sekcję inicjalizacji przed uruchomieniem programu. Cała akcja rozgrywa się za kulisami.

4. Metody wirtualne

Metoda staje się wirtualna, jeśli po jej deklaracji typu obiektu następuje nowe zastrzeżone słowo virtual. Jeśli metoda w typie nadrzędnym jest zadeklarowana jako wirtualna, wszystkie metody o tej samej nazwie w typach podrzędnych muszą być również zadeklarowane jako wirtualne, aby uniknąć błędu kompilatora.

Poniżej znajdują się obiekty z przykładowej listy płac, odpowiednio zwirtualizowane:

rodzaj

PEpracownik = ^TEpracownik;

Pracownik = obiekt

Nazwa, Tytuł: ciąg[25];

Oceń: Realne;

konstruktor Init(AName, ATitle: String; ARate: Real);

funkcja GetPayAmount : Real; wirtualny;

funkcja GetName : String;

funkcja GetTitle : String;

funkcja GetRate : Real;

procedura Pokaż; wirtualny;

puszki;

PH co godzinę = ^T co godzinę;

Czwarty = obiekt(Pracownik);

Czas: liczba całkowita;

konstruktor Init(AName, ATitle: String; ARate: Real; Time: Integer);

funkcja GetPayAmount : Real; wirtualny;

funkcja GetTime : Liczba całkowita;

puszki;

PSopłacany = ^TSopłacany;

TSalaried = obiekt(Tpracownik);

funkcja GetPayAmount : Real; wirtualny;

puszki;

PCzadane = ^TCzadane;

TCommissioned = obiekt (opłacany);

Prowizja : Rzeczywista;

Kwota sprzedaży: rzeczywista;

konstruktor Init(AName, ATitle: String; ARate,

AProwizja, AKwota Sprzedaży: Rzeczywista);

funkcja GetPayAmount : Real; wirtualny;

puszki;

Konstruktor to specjalny rodzaj procedury, która wykonuje pewne czynności konfiguracyjne dla mechanizmu metody wirtualnej. Ponadto konstruktor musi zostać wywołany przed wywołaniem jakiejkolwiek metody wirtualnej. Wywołanie metody wirtualnej bez wcześniejszego wywołania konstruktora może zablokować system, a kompilator nie ma możliwości sprawdzenia kolejności wywoływania metod.

Każdy typ obiektu, który ma metody wirtualne, musi mieć konstruktora.

ostrzeżenie

Konstruktor musi zostać wywołany przed wywołaniem jakiejkolwiek innej metody wirtualnej. Wywołanie metody wirtualnej bez wcześniejszego wywołania konstruktora może spowodować blokadę systemu, a kompilator nie może sprawdzić kolejności wywoływania metod.

Operacja

W przypadku konstruktorów obiektów sugeruje się użycie identyfikatora Init.

Każde odrębne wystąpienie obiektu musi zostać zainicjowane przy użyciu oddzielnego wywołania konstruktora. Nie wystarczy zainicjować jedną instancję obiektu, a następnie przypisać tę instancję innym. Inne instancje, nawet jeśli mogą zawierać prawidłowe dane, nie będą inicjowane za pomocą operatora przypisania i będą blokować system przy wszelkich wywołaniach ich metod wirtualnych. Na przykład:

było

FBee, GBee: pszczoła; { utwórz dwie instancje Bee }

rozpocząć

FBee.Init(5, 9) { wywołanie konstruktora dla FBee }

GBee := FBee; { Gbee jest nieprawidłowy! }

puszki;

Co właściwie tworzy konstruktor? Każdy typ obiektu zawiera coś, co nazywa się wirtualną tabelą metod (VMT) w segmencie danych. TVM zawiera rozmiar typu obiektu i, dla każdej metody wirtualnej, wskaźnik do kodu, który wykonuje tę metodę. Konstruktor ustanawia relację między implementacją wywołującą obiekt a typem obiektu TCM.

Należy pamiętać, że dla każdego typu obiektu istnieje tylko jedna TBM. Oddzielne instancje typu obiektu (tj. zmienne tego typu) zawierają tylko połączenie z TBM, ale nie samą TBM. Konstruktor ustawia wartość tego połączenia na TBM. Z tego powodu nigdzie nie można rozpocząć wykonywania przed wywołaniem konstruktora.

5. Pola danych obiektu i parametry metody formalnej

Implikacją faktu, że metody i ich obiekty mają wspólny zakres, jest to, że parametry formalne metody nie mogą być identyczne z żadnym z pól danych obiektu. Nie jest to jakieś nowe ograniczenie narzucone przez programowanie obiektowe, ale raczej te same stare reguły zakresu, które zawsze miał Pascal. Jest to to samo, co zapobieganie identyczności formalnych parametrów procedury ze zmiennymi lokalnymi procedury:

procedura CrunchIt(Crunchee: MyDataRec, Crunchby,

Kod błędu: liczba całkowita);

było

A, B: znak;

Kod błędu: liczba całkowita;

rozpocząć

.

.

.

Zmienne lokalne procedury i jej parametry formalne mają wspólny zakres i dlatego nie mogą być identyczne. Otrzymasz "Błąd 4: Zduplikowany identyfikator" jeśli spróbujesz skompilować coś takiego, ten sam błąd wystąpi, gdy spróbujesz ustawić formalny parametr metody na nazwę pola obiektu, do którego należy metoda.

Okoliczności są nieco inne, ponieważ umieszczenie nagłówka procedury wewnątrz struktury danych jest ukłonem w stronę innowacji w Turbo Pascalu, ale podstawowe zasady zakresu Pascala nie uległy zmianie.

WYKŁAD nr 13. Kompatybilność typów obiektów

1. Hermetyzacja

Połączenie kodu i danych w obiekcie nazywa się enkapsulacją. W zasadzie możliwe jest zapewnienie wystarczającej liczby metod, aby użytkownik obiektu nigdy nie miał bezpośredniego dostępu do pól obiektu. Niektóre inne języki obiektowe, takie jak Smalltalk, wymagają obowiązkowej enkapsulacji, ale Borland Pascal ma wybór.

Na przykład obiekty TEmployee i THourly są napisane w taki sposób, że absolutnie nie ma potrzeby bezpośredniego dostępu do ich wewnętrznych pól danych:

rodzaj

Pracownik = obiekt

Nazwa, Tytuł: ciąg[25];

Oceń: Realne;

procedure Init(AName, ATitle: string; ARate: Real);

funkcja GetName : String;

funkcja GetTitle : String;

funkcja GetRate : Real;

funkcja GetPayAmount : Real;

puszki;

Czwarty = obiekt (Pracownik)

Czas: liczba całkowita;

procedura Init(AName, ATitle: string; ARate:

Rzeczywiste, czas: liczba całkowita);

funkcja GetPayAmount : Real;

puszki;

Znajdują się tu tylko cztery pola danych: Imię i Nazwisko, Tytuł, Oceń i Czas. Metody GetName i GetTitle wyświetlają odpowiednio nazwisko i stanowisko pracownika. Metoda GetPayAmount wykorzystuje Rate, aw przypadku działającego Tgodziny i Czas do obliczenia kwoty wpłat do działającego. Nie ma już potrzeby bezpośredniego odwoływania się do tych pól danych.

Zakładając istnienie instancji AnHourly typu THourly, możemy użyć zestawu metod do manipulowania polami danych AnHourly w następujący sposób:

z godzinowym do

rozpocząć

Init (Aleksandr Pietrow, operator wózka widłowego 12.95, 62);

{Wyświetla nazwisko, stanowisko i kwotę płatności}

Pokazać;

puszki;

Należy zauważyć, że dostęp do pól obiektu odbywa się tylko za pomocą metod tego obiektu.

2. Rozszerzanie obiektów

Niestety standardowy Pascal nie zapewnia żadnych udogodnień do tworzenia elastycznych procedur pozwalających na pracę z zupełnie różnymi typami danych. Programowanie obiektowe rozwiązuje ten problem dzięki dziedziczeniu: jeśli zdefiniowano typ pochodny, metody typu nadrzędnego są dziedziczone, ale w razie potrzeby można je przesłonić. Aby przesłonić metodę dziedziczoną, po prostu zadeklaruj nową metodę o tej samej nazwie co metoda dziedziczona, ale z inną treścią i (jeśli to konieczne) innym zestawem parametrów.

Zdefiniujmy typ podrzędny TEmployee, który reprezentuje pracownika, który otrzymuje stawkę godzinową w następującym przykładzie:

const

OkresyPłatności = 26; { okresy płatności }

Próg dogrywki = 80; { za okres płatności }

Współczynnik nadgodzin = 1.5; { stawka godzinowa }

rodzaj

Czwarty = obiekt (Pracownik)

Czas: liczba całkowita;

procedura Init(AName, ATitle: string; ARate:

Rzeczywiste, czas: liczba całkowita);

funkcja GetPayAmount : Real;

puszki;

procedura THourly.Init(Nazwa, ATitle: ciąg;

ARate: rzeczywista, czas: liczba całkowita);

rozpocząć

TEmployee.Init(Nazwisko, ATtytuł, ARate);

Czas := Czas;

puszki;

funkcja THourly.GetPayAmount: Real;

było

Nadgodziny: liczba całkowita;

rozpocząć

Nadgodziny := Czas - Próg Nadgodziny;

jeśli dogrywka > 0 to

GetPayAmount := RoundPay(Próg Nadgodzin * Stawka +

Stawka Nadgodziny * Nadgodziny Współczynnik * Stawka)

więcej

GetPayAmount := RoundPay (czas * stawka)

puszki;

Osoba, która otrzymuje stawkę godzinową jest pracownikiem: ma wszystko, co służy do zdefiniowania przedmiotu Pracownik (nazwisko, stanowisko, stawka), a od tego, ile godzin przepracował w ciągu godziny, zależy tylko kwota, jaką otrzyma osoba godzinowa termin płatności. Dlatego THourly wymaga również pola Czas.

Ponieważ THourly definiuje nowe pole Time, jego inicjalizacja wymaga nowej metody Init, która inicjuje zarówno pola czasu, jak i pola odziedziczone. Zamiast bezpośrednio przypisywać wartości do dziedziczonych pól, takich jak Name, Title i Rate, dlaczego nie użyć ponownie metody inicjalizacji obiektu TEmployee (zilustrowanej przez pierwszą instrukcję THourly Init).

Wywołanie metody, która jest nadpisywana, nie jest najlepszym stylem. Ogólnie rzecz biorąc, możliwe jest, że TEmployee.Init wykonuje ważną, ale ukrytą inicjalizację.

Podczas wywoływania przesłoniętej metody należy upewnić się, że pochodny typ obiektu zawiera funkcjonalność elementu nadrzędnego. Ponadto każda zmiana w metodzie rodzicielskiej automatycznie wpływa na wszystkich potomków.

Po wywołaniu TEmployee.Init, THourly.Init może następnie wykonać własną inicjalizację, która w tym przypadku polega jedynie na przypisaniu wartości przekazanej w ATime.

Innym przykładem zastąpionej metody jest funkcja THourly.GetPayAmount, która oblicza kwotę wypłaty dla pracownika godzinowego. W rzeczywistości każdy typ obiektu TEmployee ma swoją własną metodę GetPayAmount, ponieważ typ pracownika zależy od sposobu wykonania obliczenia. Metoda THourly.GetPayAmount powinna uwzględniać ile godzin przepracował pracownik, czy były nadgodziny, jaki był czynnik wzrostu nadgodzin itp.

Metoda płatna. GetPayAmount powinno jedynie podzielić stawkę pracownika przez liczbę płatności w każdym roku (w naszym przykładzie).

pracownicy jednostki;

Interfejs

const

OkresyPłatności = 26; {W roku}

Próg dogrywki = 80; {za każdy okres płatności}

Współczynnik nadgodzin = 1.5; {podwyżka w stosunku do normalnej płatności}

rodzaj

Pracownik = obiekt

Nazwa, Tytuł: ciąg[25];

Oceń: Realne;

procedure Init(AName, ATitle: string; ARate: Real);

funkcja GetName : String;

funkcja GetTitle : String;

funkcja GetRate : Real;

funkcja GetPayAmount : Real;

puszki;

Czwarty = obiekt (Pracownik)

Czas: liczba całkowita;

procedura Init(AName, ATitle: string; ARate:

Rzeczywiste, czas: liczba całkowita);

funkcja GetPayAmount : Real;

funkcja GetTime : Real;

puszki;

TSalaried = obiekt (pracownik)

funkcja GetPayAmount : Real;

puszki;

TCommissioned = obiekt (TSalaried)

Prowizja : Rzeczywista;

Kwota sprzedaży: rzeczywista;

konstruktor Init(AName, ATitle: String; ARate,

AProwizja, AKwota Sprzedaży: Rzeczywista);

funkcja GetPayAmount : Real;

puszki;

realizacja

funkcja RoundPay(Wynagrodzenie: Real) : Real;

{zaokrąglaj wypłaty, aby zignorować kwoty mniejsze niż

jednostka monetarna}

rozpocząć

RoundPay := Trunc(Płace * 100) / 100;

.

.

.

TEmployee jest szczytem naszej hierarchii obiektów i zawiera pierwszą metodę GetPayAmount.

funkcja TEmployee.GetPayAmount : Real;

rozpocząć

RunError(211); { podaj błąd w czasie wykonywania }

puszki;

Może dziwić, że metoda daje błąd w czasie wykonywania. W przypadku wywołania Employee.GetPayAmount w programie wystąpi błąd. Czemu? Ponieważ TEmployee jest szczytem naszej hierarchii obiektów i nie definiuje prawdziwego pracownika; dlatego żadna z metod TEmployee nie jest wywoływana w określony sposób, chociaż mogą być dziedziczone. Wszyscy nasi pracownicy pracują na godziny, wynagrodzenie lub akord. Błąd czasu wykonywania kończy wykonywanie programu i wyświetla 211, co odpowiada komunikatowi o błędzie powiązanemu z wywołaniem metody abstrakcyjnej (jeśli program przez pomyłkę wywoła TEmployee.GetPayAmount).

Poniżej znajduje się metoda THourly.GetPayAmount, która uwzględnia takie rzeczy jak wynagrodzenie za nadgodziny, przepracowane godziny itp.

funkcja THourly.GetPayAMMount : Real;

było

Nadgodziny: liczba całkowita;

rozpocząć

Nadgodziny := Czas - Próg Nadgodziny;

jeśli dogrywka > 0 to

GetPayAmount := RoundPay(Próg Nadgodzin * Stawka +

Stawka Nadgodziny * Nadgodziny Współczynnik * Stawka)

więcej

GetPayAmount := RoundPay (czas * stawka)

puszki;

Metoda TSalaried.GetPayAmount jest znacznie prostsza; postawić na to

podzielone przez liczbę wpłat:

funkcja TSalaried.GetPayAmount : Real;

rozpocząć

GetPayAmount := RoundPay(Stawka/OkresyPłatności);

puszki;

Jeśli spojrzysz na metodę TCommissioned.GetPayAmount, zobaczysz, że wywołuje ona TSalaried.GetPayAmount, oblicza prowizję i dodaje ją do wartości zwracanej przez metodę TSalaried. PobierzKwotęPłatności.

funkcja TСommissioned.GetPayAmount : Real;

rozpocząć

GetPayAmount := RoundPay(TSalaried.GetPayAmount +

Prowizja * Kwota sprzedaży);

puszki;

Ważna uwaga: Chociaż metody można zastąpić, pola danych nie mogą zostać zastąpione. Po zdefiniowaniu pola danych w hierarchii obiektów żaden typ podrzędny nie może zdefiniować pola danych o dokładnie takiej samej nazwie.

3. Kompatybilność typów obiektów

Dziedziczenie modyfikuje do pewnego stopnia reguły zgodności typów Borland Pascal. Między innymi typ pochodny dziedziczy zgodność typów wszystkich swoich typów nadrzędnych.

Ta rozszerzona zgodność typów przybiera trzy formy:

1) między realizacjami obiektów;

2) między wskaźnikami do realizacji obiektów;

3) pomiędzy parametrami formalnymi a rzeczywistymi.

Jednak bardzo ważne jest, aby pamiętać, że we wszystkich trzech formach zgodność typów rozciąga się tylko od dziecka do rodzica. Innymi słowy, typy potomne mogą być swobodnie używane zamiast typów nadrzędnych, ale nie odwrotnie.

Na przykład TSalaried jest dzieckiem TEmployee, a TSosh-missioned jest dzieckiem TSalaried. Mając to na uwadze, rozważ następujące opisy:

rodzaj

PEpracownik = ^TEpracownik;

PSopłacany = ^TSopłacany;

PCzadane = ^TCzadane;

było

Pracownik: Pracownik;

ASalary: TSalary;

PCzamówione: TZamówione;

TEpracownikPtr: PEpracownik;

TSalowanyPtr: PSalowany;

TZamówionePtr: PZamówione;

Pod tymi opisami obowiązują następujące operatory

zadania:

Pracownik :=Wynagrodzony;

AWynagrodzenie := Azamówione;

TRozpoczętyPtr := ARozpoczęty;

Operacja

Do obiektu nadrzędnego można przypisać wystąpienie dowolnego z jego typów pochodnych. Przypisania wsteczne są niedozwolone.

Ta koncepcja jest nowa w Pascalu i na początku może być trudno zapamiętać, jaka jest kompatybilność typu zamówienia. Musisz myśleć w ten sposób: źródło musi być w stanie całkowicie wypełnić odbiornik. Typy pochodne zawierają wszystko, co zawierają ich typy nadrzędne ze względu na właściwość dziedziczenia. Dlatego typ pochodny jest albo dokładnie tego samego rozmiaru, albo (co najczęściej zdarza się) jest większy niż jego rodzic, ale nigdy mniejszy. Przypisanie obiektu rodzica (rodzica) do dziecka (dziecka) może pozostawić niektóre pola obiektu rodzica niezdefiniowane, co jest niebezpieczne i dlatego nielegalne.

W instrukcjach przypisania tylko pola, które są wspólne dla obu typów, zostaną skopiowane ze źródła do miejsca docelowego. W operatorze przypisania:

Pracownik:= Azatrudniony;

Tylko pola Name, Title i Rate z ACommissioned zostaną skopiowane do AnEmployee, ponieważ są to jedyne pola wspólne dla TCommissioned i TEmployee. Zgodność typów działa również między wskaźnikami do typów obiektów i podlega tym samym ogólnym zasadom, co w przypadku implementacji obiektów. Wskaźnik do dziecka można przypisać do wskaźnika do rodzica. Biorąc pod uwagę poprzednie definicje, następujące przypisania wskaźników są prawidłowe:

TSalariedPtr:= TZamówionePtr;

TEpracownikPtr:= TSalariedPtr;

PtrPpracownika:=PtrPrzPrac;

Pamiętaj, że odwrotne przypisania nie są dozwolone!

Parametr formalny (wartość lub parametr zmienny) danego typu obiektu może przyjąć jako rzeczywisty parametr obiekt własnego typu lub obiekty wszystkich typów podrzędnych. Jeśli zdefiniujesz taki nagłówek procedury:

procedura CalcFedTax (ofiara: TSalaried);

wtedy rzeczywiste typy parametrów mogą być TSalaried lub TCommissioned, ale nie TEmployee. Ofiara może być również parametrem zmiennym. W takim przypadku obowiązują te same zasady zgodności.

Uwaga:

Istnieje zasadnicza różnica między parametrami wartości a parametrami zmiennymi. Parametr wartości jest wskaźnikiem do rzeczywistego obiektu przekazanego jako parametr, podczas gdy parametr zmienny jest po prostu kopią rzeczywistego parametru. Co więcej, ta kopia zawiera tylko te pola, które są zawarte w typie formalnego parametru wartości. Oznacza to, że rzeczywisty parametr jest dosłownie konwertowany na typ parametru formalnego. Zmienny parametr jest bardziej podobny do rzutowania na wzór, w tym sensie, że rzeczywisty parametr pozostaje niezmieniony.

Podobnie, jeśli parametr formalny jest wskaźnikiem do typu obiektu, rzeczywisty parametr może być wskaźnikiem do tego typu obiektu lub dowolnego typu podrzędnego. Niech tytuł procedury zostanie podany:

procedura Worker.Add(AWorker: PSalared);

Prawidłowe rzeczywiste typy parametrów byłyby wtedy PSalaried lub PCCommissioned, ale nie PEmployee.

WYKŁAD nr 14. Monter

1. O asemblerze

Dawno, dawno temu asembler był językiem bez wiedzy o tym, że nie można sprawić, by komputer robił cokolwiek pożytecznego. Stopniowo sytuacja się zmieniła. Pojawiły się wygodniejsze środki komunikacji z komputerem. Ale w przeciwieństwie do innych języków, asembler nie umarł, co więcej, nie mógł tego zrobić w zasadzie. Czemu? W poszukiwaniu odpowiedzi postaramy się zrozumieć, czym w ogóle jest język asemblera.

W skrócie, język asemblera jest symboliczną reprezentacją języka maszynowego. Wszystkie procesy w maszynie na najniższym poziomie sprzętowym sterowane są wyłącznie poleceniami (instrukcjami) języka maszynowego. Z tego jasno wynika, że ​​pomimo wspólnej nazwy, język asemblera dla każdego typu komputera jest inny. Dotyczy to również wyglądu programów napisanych w asemblerze oraz idei, których odzwierciedleniem jest ten język.

Rzeczywiste rozwiązywanie problemów sprzętowych (a nawet bardziej sprzętowych, takich jak np. przyspieszenie programu) jest niemożliwe bez znajomości asemblera.

Programista lub każdy inny użytkownik może używać dowolnych narzędzi wysokiego poziomu aż do programów do budowania wirtualnych światów i być może nawet nie podejrzewa, że ​​komputer faktycznie wykonuje polecenia języka, w którym napisany jest jego program, ale ich przekształconą reprezentację. w postaci nudnej i nudnej sekwencji poleceń zupełnie innego języka - języka maszynowego. Teraz wyobraź sobie, że taki użytkownik ma niestandardowy problem. Na przykład jego program musi współpracować z jakimś nietypowym urządzeniem lub wykonywać inne czynności, które wymagają znajomości zasad działania sprzętu komputerowego. Bez względu na to, jak dobry jest język, w którym programista napisał swój program, nie może obejść się bez znajomości asemblera. I to nie przypadek, że prawie wszystkie kompilatory języków wysokiego poziomu zawierają środki łączenia swoich modułów z modułami w asemblerze lub obsługują dostęp do poziomu programowania asemblera.

Komputer składa się z kilku fizycznych urządzeń, z których każde jest połączone z jedną jednostką, zwaną jednostką systemową. Aby zrozumieć ich przeznaczenie, spójrzmy na schemat blokowy typowego komputera (rys. 1). Nie pretenduje do absolutnej dokładności i ma na celu jedynie pokazanie przeznaczenia, połączenia i typowej kompozycji elementów współczesnego komputera osobistego.

Ryż. 1. Schemat strukturalny komputera osobistego

2. Model oprogramowania mikroprocesora

Na dzisiejszym rynku komputerowym istnieje wiele różnych typów komputerów. W związku z tym można założyć, że konsument będzie miał pytanie, jak ocenić możliwości określonego typu (lub modelu) komputera i jego cechy odróżniające od komputerów innych typów (modeli). Aby zebrać wszystkie koncepcje charakteryzujące komputer pod względem jego funkcjonalnych właściwości sterowanych programem, istnieje specjalny termin - architektura komputera. Po raz pierwszy pojęcie architektury komputerowej zaczęto wspominać wraz z pojawieniem się maszyn trzeciej generacji w celu ich oceny porównawczej.

Rozpoczęcie nauki języka asemblera dowolnego komputera ma sens dopiero po ustaleniu, jaka część komputera jest widoczna i dostępna do programowania w tym języku. Jest to tzw. model programu komputerowego, którego częścią jest model programu mikroprocesorowego, który zawiera trzydzieści dwa rejestry mniej lub bardziej dostępne dla programisty.

Rejestry te można podzielić na dwie duże grupy:

1) 6 rejestrów użytkowników;

2) 16 rejestrów systemowych.

3. Rejestry użytkowników

Jak sama nazwa wskazuje, rejestry użytkownika są wywoływane, ponieważ programista może ich używać podczas pisania swoich programów. Rejestry te obejmują (rys. 2):

1) osiem rejestrów 32-bitowych, które mogą być używane przez programistów do przechowywania danych i adresów (są one również nazywane rejestrami ogólnego przeznaczenia (RON)):

eax/ax/ah/al;

ebx/bx/bh/bl;

edx/dx/dh/dl;

ecx/cx/ch/cl;

ebp/bp;

esi/si;

edi/di;

zwł./sp.

2) sześć rejestrów segmentowych: cs, ds, ss, es, fs, gs;

3) rejestry stanu i kontroli:

flagi rejestru flagi/flagi;

Rejestr wskaźnika poleceń eip/ip.

Ryż. 2. Rejestry użytkowników

Wiele z tych rejestrów jest podanych z ukośnikiem. Nie są to różne rejestry - są to części jednego dużego rejestru 32-bitowego. Mogą być używane w programie jako osobne obiekty.

4. Rejestry ogólne

Wszystkie rejestry z tej grupy umożliwiają dostęp do ich „dolnych” części. Tylko dolne 16- i 8-bitowe części tych rejestrów mogą być używane do samodzielnego adresowania. Górne 16 bitów tych rejestrów nie jest dostępnych jako niezależne obiekty.

Wymieńmy rejestry należące do grupy rejestrów ogólnego przeznaczenia. Ponieważ rejestry te są fizycznie zlokalizowane w mikroprocesorze wewnątrz jednostki arytmetyczno-logicznej (AL>), nazywane są również rejestrami ALU:

1) eax/ax/ah/al (Rejestr akumulatorów) - bateria. Służy do przechowywania danych pośrednich. W niektórych poleceniach użycie tego rejestru jest obowiązkowe;

2) ebx/bx/bh/bl (rejestr bazowy) - rejestr bazowy. Używany do przechowywania adresu bazowego jakiegoś obiektu w pamięci;

3) ecx/cx/ch/cl (rejestr licznika) - rejestr licznika. Jest używany w poleceniach, które wykonują pewne powtarzalne czynności. Jego użycie jest często ukryte i ukryte w algorytmie odpowiedniego polecenia.

Na przykład polecenie organizacji pętli pętli, oprócz przekazania sterowania do polecenia znajdującego się pod określonym adresem, analizuje i zmniejsza wartość rejestru esx/cx o jeden;

4) edx/dx/dh/dl (Rejestr danych) - rejestr danych.

Podobnie jak rejestr eax/ax/ah/al przechowuje dane pośrednie. Niektóre polecenia wymagają jego użycia; w przypadku niektórych poleceń dzieje się to niejawnie.

Następujące dwa rejestry służą do obsługi tzw. operacji łańcuchowych, czyli operacji sekwencyjnie przetwarzających łańcuchy elementów, z których każdy może mieć długość 32, 16 lub 8 bitów:

1) esi/si (rejestr indeksu źródłowego) - indeks źródłowy.

Ten rejestr w operacjach łańcuchowych zawiera aktualny adres elementu w łańcuchu źródłowym;

2) edi/di (rejestr wskaźnika przeznaczenia) - indeks odbiorcy (odbiorcy). Ten rejestr w operacjach łańcuchowych zawiera aktualny adres w łańcuchu docelowym.

W architekturze mikroprocesora, na poziomie sprzętu i oprogramowania, obsługiwana jest taka struktura danych jak stos. Do pracy ze stosem w systemie instrukcji mikroprocesora są specjalne polecenia, aw modelu oprogramowania mikroprocesora są do tego specjalne rejestry:

1) esp/sp (rejestr wskaźnika stosu) - rejestr wskaźnika stosu. Zawiera wskaźnik do szczytu stosu w bieżącym segmencie stosu.

2) ebp/bp (rejestr wskaźnika bazowego) - rejestr wskaźnika bazowego ramki stosu. Zaprojektowany do organizowania losowego dostępu do danych wewnątrz stosu.

Użycie twardego przypinania rejestrów dla niektórych instrukcji umożliwia bardziej zwięzłe kodowanie ich reprezentacji maszynowej. Znajomość tych cech pozwoli w razie potrzeby zaoszczędzić co najmniej kilka bajtów pamięci zajmowanej przez kod programu.

5. Rejestry segmentowe

W modelu oprogramowania mikroprocesorowego istnieje sześć rejestrów segmentowych: cs, ss, ds, es, gs, fs.

Ich istnienie wynika ze specyfiki organizacji i wykorzystania pamięci RAM przez mikroprocesory Intela. Polega ona na tym, że sprzęt mikroprocesorowy wspiera strukturalną organizację programu w postaci trzech części, zwanych segmentami. W związku z tym taka organizacja pamięci nazywana jest segmentacją.

W celu wskazania segmentów, do których program ma dostęp w określonym momencie, przeznaczone są rejestry segmentowe. W rzeczywistości (z niewielką poprawką) rejestry te zawierają adresy pamięci, od których zaczynają się odpowiednie segmenty. Logika przetwarzania instrukcji maszynowej jest skonstruowana w taki sposób, że podczas pobierania instrukcji, dostępu do danych programu lub dostępu do stosu, adresy w dobrze zdefiniowanych rejestrach segmentowych są niejawnie używane.

Mikroprocesor obsługuje następujące typy segmentów.

1. Segment kodu. Zawiera polecenia programu. Aby uzyskać dostęp do tego segmentu, używany jest rejestr cs (rejestr segmentu kodu) - rejestr kodu segmentu. Zawiera adres segmentu instrukcji maszynowych, do którego mikroprocesor ma dostęp (tj. instrukcje te są ładowane do potoku mikroprocesora).

2. Segment danych. Zawiera dane przetwarzane przez program. Aby uzyskać dostęp do tego segmentu, używany jest rejestr ds (rejestr segmentu danych) - rejestr segmentu danych, który przechowuje adres segmentu danych bieżącego programu.

3. Segment stosu. Ten segment jest regionem pamięci zwanym stosem. Mikroprocesor organizuje pracę ze stosem według następującej zasady: jako pierwszy wybierany jest ostatni element zapisany w tym obszarze. Aby uzyskać dostęp do tego segmentu, używany jest rejestr ss (rejestr segmentu stosu) - rejestr segmentu stosu zawierający adres segmentu stosu.

4. Dodatkowy segment danych. W domyśle algorytmy wykonywania większości instrukcji maszynowych zakładają, że przetwarzane przez nie dane znajdują się w segmencie danych, którego adres znajduje się w rejestrze segmentowym ds. Jeśli program nie ma wystarczającej ilości jednego segmentu danych, ma możliwość wykorzystania trzech dodatkowych segmentów danych. Ale w przeciwieństwie do głównego segmentu danych, którego adres jest zawarty w rejestrze segmentu ds, podczas używania dodatkowych segmentów danych, ich adresy muszą być wyraźnie określone za pomocą specjalnych prefiksów redefiniujących segment w poleceniu. Adresy dodatkowych segmentów danych muszą być zawarte w rejestrach es, gs, fs (rejestry rozszerzonego segmentu danych).

6. Rejestry statusu i kontroli

Mikroprocesor zawiera kilka rejestrów, które stale zawierają informacje o stanie zarówno samego mikroprocesora, jak i programu, którego instrukcje są aktualnie ładowane do potoku. Rejestry te obejmują:

1) flagi rejestru flag/flagi;

2) rejestr wskaźnika poleceń eip/ip.

Korzystając z tych rejestrów można uzyskać informacje o wynikach wykonania polecenia oraz wpływać na stan samego mikroprocesora. Rozważmy bardziej szczegółowo cel i zawartość tych rejestrów.

1. flagi/flagi (rejestr flag) - rejestr flag. Głębokość bitowa flag/flag wynosi 32/16 bitów. Poszczególne bity tego rejestru mają określony cel funkcjonalny i nazywane są flagami. Dolna część tego rejestru jest dokładnie taka sama jak rejestr flag dla 18086. Rysunek 3 pokazuje zawartość rejestru flag.

Ryż. 3. Zawartość rejestru flag

W zależności od sposobu ich użycia flagi rejestru flag/flag można podzielić na trzy grupy:

1) osiem flag stanu.

Te flagi mogą ulec zmianie po wykonaniu instrukcji maszynowych. Flagi stanu rejestru flag odzwierciedlają specyfikę wyniku wykonania operacji arytmetycznych lub logicznych. Umożliwia to analizę stanu procesu obliczeniowego i reagowanie na niego za pomocą poleceń skoku warunkowego i wywołań podprogramów. W tabeli 1 wymieniono flagi stanu i ich przeznaczenie.

2) jedna flaga kontrolna.

Oznaczony jako df (flaga katalogu). Znajduje się w bicie 10 rejestru flag i jest używany przez połączone polecenia. Wartość flagi df określa kierunek przetwarzania element po elemencie w tych operacjach: od początku ciągu do końca (df = 0) lub odwrotnie, od końca ciągu do jego początku (df = 1). Istnieją specjalne polecenia do pracy z flagą df: eld (usuń flagę df) i std (ustaw flagę df). Użycie tych poleceń pozwala dostosować flagę df zgodnie z algorytmem i zapewnić, że liczniki są automatycznie zwiększane lub zmniejszane podczas wykonywania operacji na ciągach.

3) pięć flag systemowych.

Kontrolują one wejścia/wyjścia, przerwania maskowalne, debugowanie, przełączanie zadań i tryb wirtualny 8086. Nie zaleca się, aby programy użytkowe niepotrzebnie modyfikowały te flagi, ponieważ w większości przypadków spowoduje to zakończenie programu. W tabeli 2 wymieniono flagi systemowe i ich przeznaczenie.

Tabela 1. Flagi stanu. Tabela 2. Flagi systemowe

2. eip/ip (rejestr wskaźnika instrukcji) - rejestr wskaźnika instrukcji. Rejestr eip/ip ma szerokość 32/16 bitów i zawiera przesunięcie następnej instrukcji do wykonania względem zawartości rejestru segmentu cs w bieżącym segmencie instrukcji. Rejestr ten nie jest bezpośrednio dostępny dla programisty, ale jego wartość jest ładowana i zmieniana przez różne polecenia sterujące, które obejmują polecenia skoków warunkowych i bezwarunkowych, wywoływanie procedur i powrót z procedur. Wystąpienie przerwań modyfikuje również rejestr eip/ip.

WYKŁAD nr 15. Spisy

1. Rejestry systemu mikroprocesorowego

Już sama nazwa tych rejestrów sugeruje, że pełnią one w systemie określone funkcje. Korzystanie z rejestrów systemowych jest ściśle regulowane. To oni zapewniają tryb chroniony. Można je również traktować jako część architektury mikroprocesorowej, która jest celowo widoczna, aby wykwalifikowany programista systemu mógł wykonywać najbardziej niskopoziomowe operacje.

Rejestry systemowe można podzielić na trzy grupy:

1) cztery rejestry kontrolne;

2) cztery rejestry adresów systemowych;

3) osiem rejestrów debugowania.

2. Rejestry kontrolne

W skład grupy rejestrów kontrolnych wchodzą cztery rejestry: cr0, cr1, cr2, cr3. Rejestry te służą do ogólnej kontroli systemu. Rejestry kontrolne są dostępne tylko dla programów z poziomem uprawnień 0.

Chociaż mikroprocesor ma cztery rejestry kontrolne, dostępne są tylko trzy z nich - wykluczony jest cr1, którego funkcje nie są jeszcze zdefiniowane (jest zarezerwowany do przyszłego użytku).

Rejestr cr0 zawiera flagi systemowe, które kontrolują tryby pracy mikroprocesora i odzwierciedlają jego stan globalnie, niezależnie od wykonywanych zadań.

Cel flag systemowych:

1) pe (Włącz ochronę), bit 0 - włącz chroniony tryb pracy. Stan tej flagi pokazuje, w którym z dwóch trybów - rzeczywistym (pe = 0) lub chronionym (pe = 1) - mikroprocesor pracuje w danym czasie;

2) mp (Math Present), bit 1 - obecność koprocesora. Zawsze 1;

3) ts (Task Switched), bit 3 - przełączanie zadań. Procesor automatycznie ustawia ten bit, gdy przełącza się na inne zadanie;

4) am (Alignment Mask), bit 18 - maska ​​wyrównania. Ten bit włącza (am = 1) lub wyłącza (am = 0) kontrolę wyrównania;

5) cd (Cache Disable), bit 30 - wyłącza pamięć podręczną.

Używając tego bitu, możesz wyłączyć (cd = 1) lub włączyć (cd = 0) korzystanie z wewnętrznej pamięci podręcznej (pamięci podręcznej pierwszego poziomu);

6) pg (PaGing), bit 31 - włącz (pg=1) lub wyłącz (pg=0) stronicowanie.

Flaga jest używana w modelu stronicowania organizacji pamięci.

Rejestr cr2 jest używany w stronicowaniu pamięci RAM do zarejestrowania sytuacji, w której bieżąca instrukcja uzyskała dostęp do adresu zawartego na stronie pamięci, której aktualnie nie ma w pamięci.

W takiej sytuacji w mikroprocesorze występuje wyjątek numer 14, a liniowy 32-bitowy adres instrukcji, która spowodowała ten wyjątek, jest zapisywany w rejestrze cr2. Na podstawie tych informacji program obsługi wyjątków 14 określa żądaną stronę, zamienia ją w pamięci i wznawia normalne działanie programu;

Rejestr cr3 jest również używany do pamięci stronicowania. Jest to tak zwany rejestr katalogów stron pierwszego poziomu. Zawiera 20-bitowy fizyczny adres bazowy katalogu stron bieżącego zadania. Katalog ten zawiera 1024 32-bitowe deskryptory, z których każdy zawiera adres tablicy stron drugiego poziomu. Z kolei każda z tablic stron drugiego poziomu zawiera 1024 32-bitowych deskryptorów, które adresują ramki stron w pamięci. Rozmiar ramki strony to 4 KB.

3. Rejestry adresów systemowych

Rejestry te są również nazywane rejestrami zarządzania pamięcią.

Przeznaczone są do ochrony programów i danych w trybie wielozadaniowym mikroprocesora. Podczas pracy w trybie chronionym mikroprocesorem przestrzeń adresowa dzieli się na:

1) globalny - wspólny dla wszystkich zadań;

2) lokalna – odrębna dla każdego zadania.

Podział ten wyjaśnia obecność następujących rejestrów systemowych w architekturze mikroprocesorowej:

1) rejestr globalnej tablicy deskryptorów gdtr (Global Descriptor Table Register), mający rozmiar 48 bitów i zawierający 32-bitowy (bity 16-47) adres bazowy globalnej tablicy deskryptorów GDT oraz 16-bitowy (bity 0-15) wartość graniczna, czyli rozmiar w bajtach tablicy GDT;

2) rejestr tablicy lokalnych deskryptorów ldtr (Local Descriptor Table Register), o rozmiarze 16 bitów i zawierający tzw. selektor deskryptora lokalnej tablicy deskryptorów LDT Selektor ten jest wskaźnikiem w tablicy GDT, który opisuje segment zawierający lokalną tablicę deskryptorów LDT;

3) rejestr tablicy deskryptorów przerwań idtr (Interrupt Descriptor Table Register), mający rozmiar 48 bitów i zawierający 32-bitowy (bity 16-47) adres bazowy tablicy deskryptorów przerwań IDT oraz 16-bitowy (bity 0-15) wartość graniczna, czyli rozmiar w bajtach tablicy IDT;

4) 16-bitowy rejestr zadań tr (Task Register), który podobnie jak rejestr ldtr zawiera selektor, czyli wskaźnik do deskryptora w tablicy GDT, opisujący aktualny status segmentu zadań (TSS). Segment ten tworzony jest dla każdego zadania w systemie, ma ściśle uregulowaną strukturę i zawiera kontekst (stan aktualny) zadania. Głównym celem segmentów TSS jest zapisanie aktualnego stanu zadania w momencie przejścia do innego zadania.

4. ​​​​Rejestry debugowania

Jest to bardzo ciekawa grupa rejestrów przeznaczona do debugowania sprzętowego. Narzędzia do debugowania sprzętu po raz pierwszy pojawiły się w mikroprocesorze i486. Sprzętowo mikroprocesor zawiera osiem rejestrów debugowania, ale tylko sześć z nich jest faktycznie używanych.

Rejestry dr0, dr1, dr2, dr3 mają szerokość 32 bitów i służą do ustawiania adresów liniowych czterech punktów przerwania. Mechanizm zastosowany w tym przypadku jest następujący: dowolny adres wygenerowany przez bieżący program jest porównywany z adresami w rejestrach dr0... dr3 i w przypadku dopasowania generowany jest wyjątek debugowania o numerze 1.

Rejestr dr6 nazywany jest rejestrem statusu debugowania. Bity w tym rejestrze są ustawione zgodnie z przyczynami, które spowodowały wystąpienie ostatniego wyjątku numer 1.

Wymieniamy te bity i ich przeznaczenie:

1) b0 - jeżeli ten bit jest ustawiony na 1, to ostatni wyjątek (przerwanie) wystąpił w wyniku osiągnięcia punktu kontrolnego zdefiniowanego w rejestrze dr0;

2) b1 - podobnie jak b0, ale dla punktu kontrolnego w rejestrze dr1;

3) b2 - podobnie jak b0, ale dla punktu kontrolnego w rejestrze dr2;

4) bЗ - podobnie jak b0, ale dla punktu kontrolnego w rejestrze dr3;

5) bd (bit 13) - służy do ochrony rejestrów debugowania;

6) bs (bit 14) - ustawiany na 1 jeśli wyjątek 1 był spowodowany stanem flagi tf=1 w rejestrze flag;

7) bt (bit 15) jest ustawiony na 1, jeśli wyjątek 1 był spowodowany przełączeniem na zadanie z bitem pułapki ustawionym w TSS t = 1.

Wszystkie pozostałe bity w tym rejestrze są wypełnione zerami. Program obsługi wyjątków 1, na podstawie zawartości dr6, musi określić przyczynę wyjątku i podjąć niezbędne działania.

Rejestr dr7 nazywany jest rejestrem kontrolnym debugowania. Zawiera pola dla każdego z czterech rejestrów punktów przerwania debugowania, które pozwalają określić następujące warunki, w których powinno zostać wygenerowane przerwanie:

1) lokalizacja rejestracji punktu kontrolnego - tylko w bieżącym zadaniu lub w dowolnym zadaniu. Bity te zajmują dolne 8 bitów rejestru dr7 (2 bity na każdy punkt przerwania (właściwie punkt przerwania) ustawiony odpowiednio przez rejestry dr0, dr1, dr2, dr3).

Pierwszy bit każdej pary to tak zwana rozdzielczość lokalna; ustawienie go mówi punktowi przerwania, aby zadziałał, jeśli znajduje się w przestrzeni adresowej bieżącego zadania.

Drugi bit w każdej parze określa zezwolenie globalne, które wskazuje, że dany punkt przerwania jest ważny w przestrzeniach adresowych wszystkich zadań w systemie;

2) rodzaj dostępu, przez który zainicjowane jest przerwanie: tylko podczas pobierania polecenia, podczas zapisu lub podczas zapisu / odczytu danych. Bity określające ten charakter wystąpienia przerwania znajdują się w górnej części tego rejestru. Większość rejestrów systemowych jest dostępna programowo.

WYKŁAD nr 16. Programy asemblera

1. Struktura programu w asemblerze

Program w języku asemblerowym jest zbiorem bloków pamięci zwanych segmentami pamięci. Program może składać się z jednego lub więcej takich segmentów bloków. Każdy segment zawiera zbiór zdań językowych, z których każdy zajmuje oddzielny wiersz kodu programu.

Oświadczenia montażowe są czterech typów:

1) polecenia lub instrukcje, które są symbolicznym odpowiednikiem poleceń maszynowych. Podczas procesu translacji instrukcje asemblera są konwertowane na odpowiednie polecenia zestawu instrukcji mikroprocesora;

2) makra. Są to zdania tekstu programu, które są w pewien sposób sformalizowane i zastępowane przez inne zdania podczas emisji;

3) dyrektywy, które są wskazówką dla tłumacza asemblera do wykonania określonych czynności. Dyrektywy nie mają odpowiedników w reprezentacji maszynowej;

4) linie komentarza zawierające dowolne znaki, w tym litery alfabetu rosyjskiego. Komentarze są ignorowane przez tłumacza.

2. Składnia zespołu

Zdania tworzące program mogą być konstrukcją składniową odpowiadającą poleceniu, makro, dyrektywie lub komentarzowi. Aby tłumacz asemblera mógł je rozpoznać, muszą być uformowane zgodnie z pewnymi regułami składniowymi. W tym celu najlepiej jest użyć formalnego opisu składni języka, podobnie jak zasad gramatyki. Najczęstszymi sposobami opisu języka programowania w ten sposób są diagramy składni i rozszerzone formularze Backusa-Naura. W praktyce diagramy składni są wygodniejsze. Na przykład, składnia instrukcji języka asemblerowego może być opisana za pomocą diagramów składni pokazanych na poniższych rysunkach.

Ryż. 4. Format zdania w asemblerze

Ryż. 5. Format dyrektywy

Ryż. 6. Format poleceń i makr

Na tych rysunkach:

1) nazwa etykiety - identyfikator, którego wartością jest adres pierwszego bajtu zdania kodu źródłowego programu, który oznacza;

2) nazwa - identyfikator odróżniający tę dyrektywę od innych dyrektyw o tej samej nazwie. W wyniku przetworzenia przez asembler pewnej dyrektywy, pewne cechy mogą być przypisane do tej nazwy;

3) kod operacji (COP) i dyrektywa to mnemoniczne oznaczenia odpowiedniej instrukcji maszynowej, makroinstrukcji lub dyrektywy tłumacza;

4) operandy - części dyrektyw command, macro lub assembler, oznaczające obiekty, na których wykonywane są operacje. Operandy asemblera są opisane za pomocą wyrażeń ze stałymi liczbowymi i tekstowymi, etykietami zmiennych i identyfikatorów za pomocą znaków operatora i niektórych słów zastrzeżonych.

Jak korzystać z diagramów składniowych? To bardzo proste: wystarczy znaleźć, a następnie podążać ścieżką od wejścia diagramu (po lewej) do jego wyjścia (po prawej). Jeśli taka ścieżka istnieje, to zdanie lub konstrukcja są poprawne składniowo. Jeśli nie ma takiej ścieżki, to kompilator nie zaakceptuje tej konstrukcji. Podczas pracy z diagramami składni zwracaj uwagę na kierunek przechodzenia wskazywany przez strzałki, ponieważ wśród ścieżek mogą znajdować się takie, którymi można podążać od prawej do lewej. W rzeczywistości diagramy składni odzwierciedlają logikę tłumacza podczas analizowania zdań wejściowych programu.

Dozwolone znaki podczas pisania tekstu programów to:

1) wszystkie litery łacińskie: A - Z, a - z. W takim przypadku wielkie i małe litery są uważane za równoważne;

2) liczby od 0 do 9;

3) znaki ?, @, S, _, &;

4) separatory.

Zdania w asemblerze są tworzone z leksemów, które są syntaktycznie nierozłącznymi sekwencjami ważnych symboli językowych, które mają sens dla tłumacza.

Żetony są następujące.

1. Identyfikatory to sekwencje prawidłowych znaków używanych do oznaczania obiektów programu, takich jak kody operacji, nazwy zmiennych i nazwy etykiet. Zasada pisania identyfikatorów jest następująca: identyfikator może składać się z jednego lub więcej znaków. Jako znaki możesz używać liter alfabetu łacińskiego, cyfr i niektórych znaków specjalnych - _, ?, $, @. Identyfikator nie może zaczynać się od cyfry. Długość identyfikatora może wynosić do 255 znaków, chociaż tłumacz akceptuje tylko pierwsze 32 i ignoruje resztę. Możesz dostosować długość możliwych identyfikatorów za pomocą opcji wiersza poleceń mv. Ponadto można powiedzieć tłumaczowi, aby rozróżniał wielkie i małe litery lub ignorował ich różnicę (co jest wykonywane domyślnie). W tym celu używane są opcje wiersza polecenia /mu, /ml, /mx.

2. Łańcuchy znaków - ciągi znaków ujęte w pojedyncze lub podwójne cudzysłowy.

3. Liczby całkowite w jednym z następujących systemów liczbowych: binarny, dziesiętny, szesnastkowy. Identyfikacja numerów podczas zapisywania ich w programach asemblera odbywa się według pewnych zasad:

1) liczby dziesiętne nie wymagają identyfikacji dodatkowych znaków, np. 25 lub 139;

2) w celu identyfikacji liczb binarnych w tekście źródłowym programu konieczne jest wpisanie łacińskiego „b” po wpisaniu zer i jedynek, które je tworzą, na przykład 10010101 b;

3) Liczby szesnastkowe mają więcej konwencji podczas pisania:

a) po pierwsze składają się z cyfr 0...9, małych i wielkich liter alfabetu łacińskiego a, b, c, d, e, Gili D B, C, D, E, E

b) po drugie, tłumacz może mieć trudności z rozpoznaniem liczb szesnastkowych ze względu na to, że mogą one składać się wyłącznie z cyfr 0...9 (np. 190845) lub zaczynać się od litery alfabetu łacińskiego (np. efl5 ). Aby „wyjaśnić” tłumaczowi, że dany token nie jest liczbą dziesiętną ani identyfikatorem, programista musi w specjalny sposób podświetlić liczbę szesnastkową. Aby to zrobić, wpisz łacińską literę „h” na końcu ciągu cyfr szesnastkowych tworzących liczbę szesnastkową. To konieczność. Jeżeli liczba szesnastkowa zaczyna się od litery, to przed nią wpisywane jest zero wiodące: 0 efl5 h.

W ten sposób zorientowaliśmy się, jak zbudowane są zdania programu asemblera. Ale to tylko najbardziej powierzchowny pogląd.

Niemal każde zdanie zawiera opis przedmiotu, na którym lub za pomocą którego dokonuje się jakiejś czynności. Obiekty te nazywane są operandami. Można je zdefiniować w następujący sposób: operandy to obiekty (niektóre wartości, rejestry lub lokalizacje pamięci), na które mają wpływ instrukcje lub dyrektywy, lub są to obiekty, które definiują lub udoskonalają działanie instrukcji lub dyrektyw.

Operandy można łączyć z operatorami arytmetycznymi, logicznymi, bitowymi i atrybutami, aby obliczyć pewną wartość lub określić lokalizację pamięci, na którą będzie miało wpływ dane polecenie lub dyrektywa.

Rozważmy bardziej szczegółowo cechy operandów w następującej klasyfikacji:

1) stałe lub natychmiastowe operandy - liczba, łańcuch, nazwa lub wyrażenie, które ma określoną wartość. Nazwa nie może być relokowalna, to znaczy nie może zależeć od adresu programu, który ma być załadowany do pamięci. Na przykład można go zdefiniować za pomocą operatorów równości lub =;

2) operandy adresowe, ustaw fizyczną lokalizację operandu w pamięci, określając dwa składniki adresu: segment i offset (rys. 7);

Ryż. 7. Składnia opisu argumentów adresowych

3) operandy relokowalne - dowolne nazwy symboliczne reprezentujące niektóre adresy pamięci. Adresy te mogą wskazywać lokalizację pamięci niektórych instrukcji (jeśli operandem jest etykieta) lub danych (jeśli operand jest nazwą komórki pamięci w segmencie danych).

Operandy relokowalne różnią się od operandów adresu tym, że nie są powiązane z konkretnym adresem pamięci fizycznej. Komponent segmentowy adresu przenoszonego operandu jest nieznany i zostanie określony po załadowaniu programu do pamięci w celu wykonania.

Licznik adresów to specyficzny rodzaj operandu. Jest on oznaczony znakiem S. Specyfika tego argumentu polega na tym, że kiedy tłumacz asemblera napotka ten symbol w programie źródłowym, zastępuje go bieżącą wartością licznika adresu. Wartość licznika adresu lub licznika rozmieszczenia, jak to się czasem nazywa, jest przesunięciem bieżącej instrukcji maszynowej od początku segmentu kodu. W formacie zestawienia druga lub trzecia kolumna odpowiada licznikowi adresów (w zależności od tego, czy kolumna z poziomem zagnieżdżenia jest obecna w zestawieniu). Jeśli weźmiemy jakąkolwiek listę jako przykład, to jest jasne, że kiedy translator przetwarza następną instrukcję asemblera, licznik adresów zwiększa się o długość wygenerowanej instrukcji maszynowej. Ważne jest prawidłowe zrozumienie tego punktu. Na przykład przetwarzanie dyrektyw asemblera nie zmienia licznika. Dyrektywy, w przeciwieństwie do poleceń asemblera, są tylko instrukcjami dla kompilatora, aby wykonać pewne czynności, aby utworzyć maszynową reprezentację programu, i dla nich kompilator nie generuje żadnych konstrukcji w pamięci.

Używając takiego wyrażenia do przeskoku, należy pamiętać o długości samej instrukcji, w której to wyrażenie jest używane, ponieważ wartość licznika adresu odpowiada przesunięciu w segmencie instrukcji tej instrukcji, a nie instrukcji następującej po niej . W naszym przykładzie polecenie jmp zajmuje 2 bajty. Ale bądź ostrożny, długość instrukcji zależy od tego, jakich operandów używa. Instrukcja z operandami rejestru będzie krótsza niż instrukcja z jednym z jej operandów umieszczonym w pamięci. W większości przypadków informacje te można uzyskać znając format instrukcji maszynowej i analizując kolumnę listingu z kodem obiektowym instrukcji;

4) operand rejestru to tylko nazwa rejestru. W programie asemblera możesz używać nazw wszystkich rejestrów ogólnego przeznaczenia i większości rejestrów systemowych;

5) argumenty bazowe i indeksowe. Ten typ operandu jest używany do implementacji pośredniego adresowania bazowego, pośredniego adresowania indeksowego lub ich kombinacji i rozszerzeń;

6) Operandy strukturalne służą do uzyskiwania dostępu do określonego elementu złożonego typu danych zwanego strukturą.

Rekordy (podobne do typu struct) służą do uzyskiwania dostępu do pola bitowego jakiegoś rekordu.

Argumenty to elementarne komponenty, które tworzą część instrukcji maszynowej, oznaczające obiekty, na których wykonywana jest operacja. W bardziej ogólnym przypadku operandy mogą być zawarte jako składniki w bardziej złożonych formacjach zwanych wyrażeniami. Wyrażenia to kombinacje operandów i operatorów, traktowane jako całość. Wynikiem oceny wyrażenia może być adres jakiejś komórki pamięci lub pewna wartość stała (bezwzględna).

Rozważaliśmy już możliwe typy operandów. Wymienimy teraz możliwe typy operatorów asemblera i zasady składniowe dla tworzenia wyrażeń asemblera i podajemy krótki opis operatorów.

1. Operatory arytmetyczne. Obejmują one:

1) jednoargumentowe „+” i „-”;

2) binarne „+” i „-”;

3) mnożenie „*”;

4) dzielenie liczb całkowitych „/”;

5) uzyskanie reszty z dzielenia „mod”.

Operatory te znajdują się na poziomach pierwszeństwa 6,7,8 w tabeli 4.

Ryż. 8. Składnia działań arytmetycznych

2. Operatory przesunięcia przesuwają wyrażenie o określoną liczbę bitów (rys. 9).

Ryż. 9. Składnia operatorów zmianowych

3. Operatory porównania (zwracają wartość „prawda” lub „fałsz”) są przeznaczone do tworzenia wyrażeń logicznych (rys. 10 i tabela 3). Wartość logiczna „prawda” odpowiada jednostce cyfrowej, a „fałsz” - zero.

Ryż. 10. Składnia operatorów porównania

Tabela 3. Operatory porównania

4. Operatory logiczne wykonują operacje bitowe na wyrażeniach (rys. 11). Wyrażenia muszą być bezwzględne, tj. takie, których wartość liczbową może obliczyć tłumacz.

Ryż. 11. Składnia operatorów logicznych

5. Operator indeksu [1]. Nawiasy są również operatorem, a tłumacz postrzega ich obecność jako instrukcję dodania wartości wyrażenia_2 za tymi nawiasami z wyrażeniem_12 zawartym w nawiasach (rys. XNUMX).

Ryż. 12. Składnia operatora indeksu

Zwróć uwagę, że w literaturze dotyczącej asemblera przyjmuje się następujące oznaczenie: gdy tekst odnosi się do zawartości rejestru, jego nazwę podaje się w nawiasach. Będziemy również trzymać się tej notacji.

6. Operator redefinicji typu ptr służy do przedefiniowania lub zakwalifikowania typu etykiety lub zmiennej zdefiniowanej przez wyrażenie (rys. 13).

Typ może przyjmować jedną z następujących wartości: bajt, słowo, dword, qword, tbajt, blisko, daleko.

Ryż. 13. Składnia operatora redefinicji typu

7. Operator redefinicji segmentu ":" (dwukropek) wymusza obliczenie adresu fizycznego względem konkretnie określonego składnika segmentu: „nazwa rejestru segmentu”, „nazwa segmentu” z odpowiedniej dyrektywy SEGMENT lub „nazwa grupy” (rys. 14). Omawiając segmentację, mówiliśmy o tym, że mikroprocesor na poziomie sprzętowym obsługuje trzy rodzaje segmentów - kod, stos i dane. Co to za wsparcie sprzętowe? Na przykład, aby wybrać wykonanie następnego polecenia, mikroprocesor musi koniecznie spojrzeć na zawartość rejestru segmentowego cs i tylko na nią. A ten rejestr, jak wiemy, zawiera (jeszcze nie przesunięty) adres fizyczny początku segmentu instrukcji. Aby uzyskać adres konkretnej instrukcji, mikroprocesor musi pomnożyć zawartość cs przez 16 (co oznacza przesunięcie o cztery bity) i dodać wynikową wartość 20-bitową do 16-bitowej zawartości rejestru ip. W przybliżeniu to samo dzieje się, gdy mikroprocesor przetwarza operandy w instrukcji maszynowej. Jeśli widzi, że operand jest adresem (adres efektywny, który jest tylko częścią adresu fizycznego), to wie, w którym segmencie go szukać - domyślnie jest to segment, którego adres początkowy jest przechowywany w rejestrze segmentowym ds. .

Ale co z segmentem stosu? W kontekście naszych rozważań interesują nas rejestry sp i bp. Jeśli mikroprocesor widzi jeden z tych rejestrów jako operand (lub jego część, jeśli operand jest wyrażeniem), to domyślnie tworzy fizyczny adres operandu, używając zawartości rejestru ss jako komponentu segmentowego. Jest to zestaw mikroprogramów w jednostce sterującej mikroprogramami, z których każdy wykonuje jedną z instrukcji w mikroprocesorowym systemie instrukcji maszynowych. Każdy mikroprogram działa według własnego algorytmu. Oczywiście nie możesz tego zmienić, ale możesz to nieco poprawić. Odbywa się to za pomocą opcjonalnego pola prefiksu polecenia maszyny. Jeśli zgadzamy się, jak działa polecenie, to tego pola brakuje. Jeżeli chcemy wprowadzić poprawkę (o ile oczywiście jest to dopuszczalne dla danego polecenia) do algorytmu polecenia, to konieczne jest utworzenie odpowiedniego prefiksu.

Prefiks to jednobajtowa wartość, której wartość liczbowa określa jej przeznaczenie. Mikroprocesor rozpoznaje po określonej wartości, że ten bajt jest prefiksem i dalsza praca mikroprogramu jest wykonywana z uwzględnieniem otrzymanej instrukcji, aby poprawić jego działanie. Teraz interesuje nas jeden z nich - przedrostek zastępujący (redefiniujący) segment. Jego celem jest wskazanie mikroprocesorowi (a właściwie oprogramowaniu układowemu), że nie chcemy używać domyślnego segmentu. Możliwości takiej redefinicji są oczywiście ograniczone. Segment polecenia nie może być przedefiniowany, adres następnego polecenia wykonywalnego jest jednoznacznie określony przez parę cs: ip. A tu segmenty stosu i danych – to możliwe. Do tego służy operator „:”. Translator asemblera, przetwarzając tę ​​instrukcję, generuje odpowiedni jednobajtowy przedrostek zastępujący segment.

Ryż. 14. Składnia operatora redefinicji segmentu

8. Operator nazewnictwa typu struktury "."(kropka) również wymusza na kompilatorze wykonanie pewnych obliczeń, jeśli występuje w wyrażeniu.

9. Operator uzyskiwania składowej segmentowej adresu wyrażenia seg zwraca fizyczny adres segmentu dla wyrażenia (rys. 15), którym może być etykieta, zmienna, nazwa segmentu, nazwa grupy lub jakaś nazwa symboliczna .

Ryż. 15. Składnia operatora odbierającego komponent segmentu

10. Operator do uzyskania przesunięcia wyrażenia offset pozwala uzyskać wartość przesunięcia wyrażenia (rys. 16) w bajtach względem początku segmentu, w którym zdefiniowano wyrażenie.

Ryż. 16. Składnia operatora get offset

Podobnie jak w językach wysokiego poziomu, wykonanie operatorów asemblera podczas oceny wyrażeń odbywa się zgodnie z ich priorytetami (Tabela 4). Operacje o tym samym priorytecie są wykonywane sekwencyjnie od lewej do prawej. Zmiana kolejności realizacji jest możliwa poprzez umieszczenie nawiasów, które mają najwyższy priorytet.

Tabela 4. Operatory i ich pierwszeństwo

3. Dyrektywy segmentacyjne

W trakcie poprzedniej dyskusji odkryliśmy wszystkie podstawowe zasady pisania instrukcji i operandów w programie asemblerowym. Pytanie, jak prawidłowo sformatować sekwencję poleceń, aby tłumacz mógł je przetwarzać, a mikroprocesor mógł je wykonywać, pozostaje otwarte.

Rozważając architekturę mikroprocesora dowiedzieliśmy się, że posiada on sześć rejestrów segmentowych, dzięki którym może pracować jednocześnie:

1) z jednym segmentem kodu;

2) z jednym segmentem stosu;

3) z jednym segmentem danych;

4) z trzema dodatkowymi segmentami danych.

Przypomnijmy raz jeszcze, że segment jest fizycznie obszarem pamięci zajmowanym przez polecenia i (lub) dane, których adresy są obliczane względem wartości w odpowiednim rejestrze segmentowym.

Opisem składniowym segmentu w asemblerze jest konstrukcja pokazana na rysunku 17:

Ryż. 17. Składnia opisu segmentu

Należy zauważyć, że funkcjonalność segmentu jest nieco szersza niż po prostu dzielenie programu na bloki kodu, danych i stosu. Segmentacja jest częścią bardziej ogólnego mechanizmu związanego z koncepcją programowania modułowego. Polega ona na ujednoliceniu konstrukcji modułów obiektowych tworzonych przez kompilator, w tym pochodzących z różnych języków programowania. Pozwala to na łączenie programów napisanych w różnych językach. Operandy w dyrektywie SEGMENT przeznaczone są do implementacji różnych opcji takiej unii.

Rozważ je bardziej szczegółowo.

1. Atrybut wyrównania segmentu (typ wyrównania) informuje kompozytora o upewnieniu się, że początek segmentu znajduje się na określonej granicy. Jest to ważne, ponieważ odpowiednie wyrównanie przyspiesza dostęp do danych na procesorach i80x86. Prawidłowe wartości tego atrybutu są następujące:

1) BYTE - wyrównanie nie jest wykonywane. Segment może zaczynać się pod dowolnym adresem pamięci;

2) WORD - segment zaczyna się od adresu będącego wielokrotnością dwóch, tzn. ostatni (najmniej znaczący) bit adresu fizycznego to 0 (wyrównany do granicy słowa);

3) DWORD - segment zaczyna się od adresu będącego wielokrotnością czterech, tzn. ostatnie dwa (najmniej znaczące) bity to 0 (podwójne wyrównanie granicy słowa);

4) PARA - segment zaczyna się od adresu będącego wielokrotnością 16, tzn. ostatnią cyfrą szesnastkową adresu musi być Oh (wyrównanie do granicy akapitu);

5) PAGE - segment zaczyna się pod adresem będącym wielokrotnością 256, tzn. dwie ostatnie cyfry szesnastkowe muszą być 00h (wyrównane do granicy 256-bajtowej strony);

6) MEMPAGE - segment zaczyna się od adresu będącego wielokrotnością 4 kB, tzn. ostatnie trzy cyfry szesnastkowe muszą być OOOh (adres kolejnej strony pamięci o wielkości 4 kB). Domyślnym typem wyrównania jest PARA.

2. Atrybut łączenia segmentów (typ kombinatoryczny) mówi konsolidatorowi, jak połączyć segmenty różnych modułów, które mają tę samą nazwę. Wartości atrybutów kombinacji segmentów mogą być:

1) PRYWATNY - segment nie będzie łączony z innymi segmentami o tej samej nazwie poza tym modułem;

2) PUBLIC - powoduje, że linker łączy wszystkie segmenty o tej samej nazwie. Nowy połączony segment będzie cały i ciągły. Wszystkie adresy (przesunięcia) obiektów, a to może zależeć od typu polecenia i segmentu danych, zostaną obliczone względem początku tego nowego segmentu;

3) WSPÓLNY - umieszcza wszystkie segmenty o tej samej nazwie pod tym samym adresem. Wszystkie segmenty o podanej nazwie będą się na siebie nakładać i współdzielić pamięć. Rozmiar wynikowego segmentu będzie równy rozmiarowi największego segmentu;

4) AT xxxx - lokalizuje segment pod bezwzględnym adresem akapitu (akapit to ilość pamięci, wielokrotność 16; dlatego ostatnia cyfra szesnastkowa adresu akapitu to 0). Bezwzględny adres akapitu jest podany przez xxx. Łącznik umieszcza segment pod podanym adresem pamięci (może to być użyte na przykład w celu uzyskania dostępu do pamięci wideo lub obszaru ROM>), mając atrybut łączenia. Fizycznie oznacza to, że segment po załadowaniu do pamięci będzie zlokalizowany począwszy od tego bezwzględnego adresu akapitu, ale aby uzyskać do niego dostęp, wartość określona w atrybucie musi zostać załadowana do odpowiedniego rejestru segmentowego. Wszystkie etykiety i adresy w tak zdefiniowanym segmencie odnoszą się do podanego adresu bezwzględnego;

5) STACK - definicja segmentu stosu. Powoduje, że linker łączy wszystkie segmenty o tej samej nazwie i oblicza adresy w tych segmentach względem rejestru ss. Połączony typ STACK (stos) jest podobny do połączonego typu PUBLIC, z wyjątkiem tego, że rejestr ss jest standardowym rejestrem segmentowym dla segmentów stosu. Rejestr sp jest ustawiony na końcu połączonego segmentu stosu. Jeśli nie określono żadnego segmentu stosu, konsolidator wyśle ​​ostrzeżenie, że nie znaleziono żadnego segmentu stosu. Jeśli tworzony jest segment stosu i nie jest używany połączony typ STACK, programista musi jawnie załadować adres segmentu do rejestru ss (podobnie jak rejestr ds).

Atrybut kombinacji domyślnie to PRIVATE.

3. Atrybut klasy segmentu (typ klasy) to łańcuch w cudzysłowie, który pomaga konsolidatorowi określić odpowiednią kolejność segmentów podczas asemblacji programu z wielu segmentów modułów. Konsolidator łączy w pamięci wszystkie segmenty o tej samej nazwie klasy (na ogół nazwa klasy może być dowolna, ale lepiej, jeśli odzwierciedla funkcjonalność segmentu). Typowym zastosowaniem nazwy klasy jest grupowanie wszystkich segmentów kodu programu (zwykle używa się do tego klasy "kod"). Korzystając z mechanizmu wpisywania klas, można również grupować zainicjowane i niezainicjowane segmenty danych.

4. Atrybut rozmiaru segmentu. W przypadku procesorów i80386 i nowszych segmenty mogą być 16-bitowe lub 32-bitowe. Wpływa to przede wszystkim na rozmiar segmentu i kolejność tworzenia w nim adresu fizycznego. Atrybut może przyjmować następujące wartości:

1) USE16 - oznacza to, że segment umożliwia adresowanie 16-bitowe. Podczas tworzenia adresu fizycznego można użyć tylko 16-bitowego przesunięcia. W związku z tym taki segment może zawierać do 64 KB kodu lub danych;

2)USE32 — segment będzie 32-bitowy. Podczas tworzenia adresu fizycznego można użyć 32-bitowego przesunięcia. Dlatego taki segment może zawierać do 4 GB kodu lub danych.

Wszystkie segmenty są same w sobie równe, ponieważ dyrektywy SEGMENT i ENDS nie zawierają informacji o funkcjonalnym przeznaczeniu segmentów. Aby wykorzystać je jako segmenty kodu, danych lub stosu, należy najpierw poinformować tłumacza o tym, do czego używana jest specjalna dyrektywa ASSUME, która ma format pokazany na rys. 18. Ta dyrektywa mówi tłumaczowi, który segment jest powiązany z którym rejestrem segmentu. To z kolei pozwoli tłumaczowi na poprawne powiązanie nazw symbolicznych zdefiniowanych w segmentach. Wiązanie segmentów do rejestrów segmentowych odbywa się za pomocą operandów tej dyrektywy, w której nazwa_segmentu musi być nazwą segmentu, zdefiniowaną w tekście źródłowym programu przez dyrektywę SEGMENT lub słowo kluczowe nic. Jeśli tylko słowo kluczowe nic jest użyte jako operand, to poprzednie przypisania rejestrów segmentowych są anulowane i dla wszystkich sześciu rejestrów segmentowych na raz. Ale słowo kluczowe nic może być użyte zamiast argumentu nazwy segmentu; w takim przypadku połączenie między segmentem o nazwie nazwa segmentu a odpowiednim rejestrem segmentu zostanie selektywnie zerwane (patrz Rys. 18).

Ryż. 18. ZAŁOŻENIE Dyrektywy

W przypadku prostych programów zawierających jeden segment dla kodu, danych i stosu chcielibyśmy uprościć jego opis. W tym celu tłumacze MASM i TASM wprowadzili możliwość korzystania z uproszczonych dyrektyw segmentacji. Ale tutaj pojawił się problem związany z faktem, że trzeba było jakoś zrekompensować niemożność bezpośredniego kontrolowania rozmieszczenia i kombinacji segmentów. W tym celu wraz z uproszczonymi dyrektywami segmentacji zaczęto używać dyrektywy do określania modelu pamięci MODEL, która częściowo zaczęła kontrolować rozmieszczenie segmentów i realizować funkcje dyrektywy ASSUME (stąd przy stosowaniu uproszczonych dyrektyw segmentacji Dyrektywę ASSUME można pominąć). Ta dyrektywa wiąże segmenty, które w przypadku stosowania uproszczonych dyrektyw segmentacji mają predefiniowane nazwy, z rejestrami segmentowymi (chociaż nadal trzeba jawnie zainicjować ds.).

Składnia dyrektywy MODEL jest pokazana na rysunku 19.

Ryż. 19. Składnia dyrektywy MODEL

Obowiązkowym parametrem dyrektywy MODEL jest model pamięci. Ten parametr definiuje model segmentacji pamięci dla modułu. Zakłada się, że moduł programu może mieć tylko pewne typy segmentów, które są zdefiniowane przez dyrektywy uproszczonego opisu segmentów, o których wspominaliśmy wcześniej. Te dyrektywy są pokazane w Tabeli 5.

Tabela 5. Uproszczone dyrektywy definicji segmentów

Obecność parametru [name] w niektórych dyrektywach wskazuje, że możliwe jest zdefiniowanie kilku segmentów tego typu. Z drugiej strony istnienie kilku rodzajów segmentów danych wynika z wymogu zapewnienia kompatybilności z niektórymi kompilatorami języków wysokiego poziomu, które tworzą różne segmenty danych dla danych zainicjowanych i niezainicjowanych, a także stałych.

Korzystając z dyrektywy MODEL, kompilator udostępnia kilka identyfikatorów, do których można uzyskać dostęp podczas działania programu w celu uzyskania informacji o pewnych charakterystykach danego modelu pamięci (tabela 7). Wymieńmy te identyfikatory i ich wartości (tabela 6).

Tabela 6. Identyfikatory tworzone przez dyrektywę MODEL

Możemy teraz zakończyć naszą dyskusję na temat dyrektywy MODEL. Operandy dyrektywy MODEL są używane do określenia modelu pamięci, który definiuje zestaw segmentów programu, rozmiary segmentów danych i kodu oraz metodę łączenia segmentów i rejestrów segmentowych. W tabeli 7 przedstawiono niektóre wartości parametru „model pamięci” dyrektywy MODEL.

Tabela 7. Modele pamięci

Parametr „modyfikator” dyrektywy MODEL pozwala doprecyzować niektóre cechy wykorzystania wybranego modelu pamięci (tabela 8).

Tabela 8. Modyfikatory modelu pamięci

Opcjonalne parametry „język” i „modyfikator języka” definiują niektóre cechy wywołań procedur. Konieczność użycia tych parametrów pojawia się podczas pisania i łączenia programów w różnych językach programowania.

Opisane przez nas dyrektywy dotyczące segmentacji standardowej i uproszczonej nie wykluczają się wzajemnie. Standardowe dyrektywy są używane, gdy programista chce mieć pełną kontrolę nad rozmieszczeniem segmentów w pamięci i ich łączeniem z segmentami z innych modułów.

Uproszczone dyrektywy są przydatne w przypadku prostych programów i programów przeznaczonych do łączenia z modułami programu napisanymi w językach wysokiego poziomu. Pozwala to linkerowi na efektywne łączenie modułów z różnych języków poprzez standaryzację łączenia i zarządzania.

WYKŁAD nr 17. Struktury dowodzenia w asemblerze

1. Struktura instrukcji maszyny

Polecenie maszynowe jest zakodowanym zgodnie z pewnymi regułami sygnałem dla mikroprocesora, aby wykonać jakąś operację lub czynność. Każde polecenie zawiera elementy, które definiują:

1) co robić? (Odpowiedź na to pytanie daje element polecenia zwany kodem operacji (COP).);

2) obiekty, na których coś trzeba zrobić (te elementy nazywane są operandami);

3) jak to zrobić? (Te elementy są nazywane typami operandów, zwykle określanymi niejawnie).

Format instrukcji maszynowych pokazany na rysunku 20 jest najbardziej ogólny. Maksymalna długość instrukcji maszynowej to 15 bajtów. Prawdziwe polecenie może zawierać znacznie mniejszą liczbę pól, maksymalnie jedno - tylko KOP.

Ryż. 20. Format instrukcji maszyny

Opiszmy przeznaczenie pól instrukcji maszynowych.

1. Przedrostki.

Opcjonalne elementy instrukcji maszynowych, z których każdy ma 1 bajt lub może być pominięty. W pamięci komendy poprzedzają prefiksy. Celem prefiksów jest modyfikacja operacji wykonywanej przez polecenie. Aplikacja może używać następujących typów przedrostków:

1) przedrostek zastępujący segment. Jawnie określa, który rejestr segmentowy jest używany w tej instrukcji do adresowania stosu lub danych. Prefiks zastępuje domyślny wybór rejestru segmentowego. Prefiksy zastępujące segmenty mają następujące znaczenie:

a) 2eh - wymiana segmentu cs;

b) 36h - wymiana segmentu ss;

c) 3eh - wymiana segmentu ds;

d) 26h - wymiana segmentu es;

e) 64h – wymiana odcinka fs;

e) 65h - wymiana segmentu gs;

2) prefiks bitowości adresu określa bitowość adresu (32- lub 16-bitowy). Każdej instrukcji używającej operandu adresu przypisywana jest szerokość bitowa adresu tego operandu. Ten adres może mieć 16 lub 32 bity. Jeśli długość adresu dla tego polecenia wynosi 16 bitów, oznacza to, że polecenie zawiera 16-bitowe przesunięcie (rys. 20), odpowiada to 16-bitowemu przesunięciu argumentu adresu względem początku pewnego segmentu. W kontekście rysunku 21, przesunięcie to nazywane jest adresem efektywnym. Jeśli adres ma 32 bity, oznacza to, że polecenie zawiera 32-bitowy offset (rys. 20), odpowiada on 32-bitowemu offsetowi argumentu adresu względem początku segmentu, a jego wartość tworzy 32 bity. -bitowe przesunięcie w segmencie. Prefiks bitowości adresu może służyć do zmiany domyślnej bitowości adresu. Ta zmiana wpłynie tylko na polecenie poprzedzone prefiksem;

Ryż. 21. Mechanizm tworzenia adresu fizycznego w trybie rzeczywistym

3) Prefiks szerokości bitu operandu jest podobny do prefiksu szerokości bitu adresu, ale wskazuje długość bitu operandu (32-bitowego lub 16-bitowego), z którą działa instrukcja. Jakie są zasady domyślnego ustawiania atrybutów adresu i szerokości bitu operandu?

W trybie rzeczywistym i wirtualnym 18086 wartości tych atrybutów wynoszą 16 bitów. W trybie chronionym wartości atrybutów zależą od stanu bitu D w wykonywalnych deskryptorach segmentów. Jeśli D = 0, to domyślne wartości atrybutów to 16 bitów; jeśli D = 1, to 32 bity.

Wartości prefiksów dla operandu o szerokości 66h i adresu o szerokości 67h. W przypadku bitowego prefiksu adresu trybu rzeczywistego można używać adresowania 32-bitowego, ale należy pamiętać o limicie rozmiaru segmentu 64 KB. Podobnie do prefiksu szerokości adresu, można użyć prefiksu szerokości operandu trybu rzeczywistego do pracy z operandami 32-bitowymi (na przykład w instrukcjach arytmetycznych);

4) prefiks powtórzeń jest używany z poleceniami łańcuchowymi (poleceniami przetwarzania linii). Ten przedrostek „zapętla” polecenie, aby przetworzyć wszystkie elementy łańcucha. System poleceń obsługuje dwa rodzaje przedrostków:

a) bezwarunkowe (rep - OOh), wymuszające powtórzenie powiązanego polecenia określoną liczbę razy;

b) warunkowe (repe/repz - OOh, repne/repnz - 0f2h), które podczas zapętlania sprawdzają niektóre flagi iw wyniku sprawdzenia możliwe jest wczesne wyjście z pętli.

2. Kod operacji.

Wymagany element opisujący operację wykonywaną przez polecenie. Wiele poleceń odpowiada kilku kodom operacji, z których każdy określa niuanse operacji. Kolejne pola instrukcji maszynowej określają lokalizację operandów biorących udział w operacji i specyfikę ich użycia. Uwzględnienie tych pól wiąże się ze sposobami określania argumentów w instrukcji maszynowej i dlatego zostanie wykonane później.

3. Tryb adresowania bajt modr/m.

Wartość tego bajtu określa używaną formę adresu operandu. Operandy mogą znajdować się w pamięci w jednym lub dwóch rejestrach. Jeśli operand znajduje się w pamięci, to bajt modr/m określa składniki (rejestry offsetowe, bazowe i indeksowe) użyte do obliczenia jego adresu efektywnego (Rysunek 21). W trybie chronionym bajt sib (Scale-Index-Base) może być dodatkowo użyty do określenia lokalizacji operandu w pamięci. Bajt modr/m składa się z trzech pól (rys. 20):

1) pole mod określa liczbę bajtów zajmowanych przez adres operandu w poleceniu (rys. 20, pole przesunięcia w poleceniu). Pole mod jest używane w połączeniu z polem r/m, które określa sposób modyfikacji adresu operandu „przesunięcie instrukcji”. Na przykład, jeśli mod = 00, oznacza to, że w poleceniu nie ma pola przesunięcia, a adres operandu jest określony przez zawartość rejestru bazowego i (lub) indeksowego. To, które rejestry zostaną użyte do obliczenia adresu efektywnego, zależy od wartości tego bajtu. Jeśli mod = 01, oznacza to, że pole offsetu jest obecne w poleceniu, zajmuje 1 bajt i jest modyfikowane przez zawartość rejestru bazowego i (lub) indeksowego. Jeśli mod = 10, oznacza to, że pole przesunięcia polecenia jest obecne, zajmuje 2 lub 4 bajty (w zależności od domyślnego lub zdefiniowanego przez prefiks rozmiaru adresu) i jest modyfikowane przez zawartość rejestru bazowego i/lub indeksowego. Jeśli mod = 11, oznacza to, że w pamięci nie ma operandów: są one w rejestrach. Ta sama wartość bajtu mod jest używana, gdy w instrukcji używany jest operand bezpośredni;

2) pole reg/cop określa albo rejestr znajdujący się w poleceniu w miejsce pierwszego operandu, albo możliwe rozszerzenie opcode;

3) pole r/m jest używane w połączeniu z polem mod i określa albo rejestr znajdujący się w poleceniu w miejscu pierwszego argumentu (jeśli mod = 11), albo rejestry bazowy i indeksowy używane do obliczenia adresu efektywnego (wraz z polem przesunięcia w poleceniu).

4. Skala bajtów - indeks - podstawa (bajt sib).

Służy do rozszerzenia możliwości adresowania operandów. Obecność bajtu sib w instrukcji maszynowej jest wskazywana przez kombinację jednej z wartości 01 lub 10 pola mod i wartości pola r/m = 100. Bajt sib składa się z trzech pól:

1) skala pól ss. To pole zawiera współczynnik skali dla indeksu składnika indeksowego, który zajmuje kolejne 3 bity bajtu sib. Pole ss może zawierać jedną z następujących wartości: 1, 2, 4, 8.

Przy obliczaniu adresu efektywnego zawartość rejestru indeksowego zostanie pomnożona przez tę wartość;

2) pola indeksowe. Używany do przechowywania numeru rejestru indeksu używanego do obliczania efektywnego adresu operandu;

3) pola bazowe. Służy do przechowywania numeru rejestru podstawowego, który jest również używany do obliczania efektywnego adresu operandu. Prawie wszystkie rejestry ogólnego przeznaczenia mogą być używane jako rejestry bazowe i indeksowe.

5. Pole przesunięcia w poleceniu.

8-, 16- lub 32-bitowa liczba całkowita ze znakiem reprezentująca, w całości lub w części (z zastrzeżeniem powyższych rozważań), wartość efektywnego adresu operandu.

6. Pole operandu bezpośredniego.

Opcjonalne pole, które jest 8-bitowym, 16-bitowym lub 32-bitowym operandem bezpośrednim. Obecność tego pola ma oczywiście odzwierciedlenie w wartości bajtu modr/m.

2. Metody określania argumentów instrukcji

Operand jest ustawiany niejawnie na poziomie oprogramowania układowego

W takim przypadku instrukcja wyraźnie nie zawiera argumentów. Algorytm wykonania polecenia wykorzystuje niektóre domyślne obiekty (rejestry, flagi w flagach itp.).

Na przykład polecenia cli i sti niejawnie działają z flagą przerwania if w rejestrze eflags, a polecenie xlat niejawnie uzyskuje dostęp do rejestru al i wiersza w pamięci pod adresem określonym przez parę rejestrów ds:bx.

Operand jest określony w samej instrukcji (operand natychmiastowy)

Operand znajduje się w kodzie instrukcji, to znaczy jest jego częścią. Aby przechowywać taki operand w poleceniu, przydzielane jest pole o długości do 32 bitów (Rysunek 20). Operand bezpośredni może być tylko drugim (źródłowym) operandem. Operand przeznaczenia może znajdować się w pamięci lub w rejestrze.

Na przykład: mov ax,0ffffti przenosi stałą szesnastkową ffff do rejestru ax. Polecenie add sum, 2 dodaje zawartość pola pod adresem suma z liczbą całkowitą 2 i zapisuje wynik w miejscu pierwszego operandu, czyli do pamięci.

Operand znajduje się w jednym z rejestrów

Operandy rejestrów są określone przez nazwy rejestrów. Rejestry mogą być używane:

1) 32-bitowe rejestry EAX, EBX, ECX, EDX, ESI, EDI, ESP, EUR;

2) 16-bitowe rejestry AX, BX, CX, DX, SI, DI, SP, BP;

3) 8-bitowe rejestry AH, AL, BH, BL, CH, CL, DH, DL;

4) rejestry segmentowe CS, DS, SS, ES, FS, GS.

Na przykład instrukcja add ax,bx dodaje zawartość rejestrów ax i bx i zapisuje wynik do bx. Polecenie dec si zmniejsza zawartość si o 1.

Operand jest w pamięci

Jest to najbardziej złożony i jednocześnie najbardziej elastyczny sposób określania operandów. Pozwala na realizację dwóch głównych typów adresowania: bezpośredniego i pośredniego.

Z kolei adresowanie pośrednie ma następujące odmiany:

1) pośrednie adresowanie bazowe; inna jego nazwa to adresowanie pośrednie w rejestrze;

2) pośrednie adresowanie bazowe z offsetem;

3) pośrednie adresowanie indeksu z przesunięciem;

4) pośrednie adresowanie indeksu bazowego;

5) pośrednie adresowanie indeksu bazowego z przesunięciem.

Operand to port I/O

Oprócz przestrzeni adresowej RAM mikroprocesor utrzymuje przestrzeń adresową I/O, która jest używana do uzyskiwania dostępu do urządzeń I/O. Przestrzeń adresowa we/wy wynosi 64 KB. Adresy są przydzielane dla dowolnego urządzenia komputerowego w tej przestrzeni. Określona wartość adresu w tej przestrzeni nazywana jest portem we/wy. Fizycznie port I / O odpowiada rejestrowi sprzętowemu (nie mylić z rejestrem mikroprocesora), do którego dostęp uzyskuje się za pomocą specjalnych instrukcji asemblera.

Na przykład:

w al, 60h; wprowadź bajt z portu 60h

Rejestry adresowane przez port I/O mogą mieć szerokość 8,16, 32 lub XNUMX bitów, ale szerokość bitów rejestru jest stała dla konkretnego portu. Polecenia wejścia i wyjścia działają na ustalonym zakresie obiektów. Jako źródło informacji lub odbiorcę wykorzystywane są tzw. rejestry akumulacyjne EAX, AX, AL. Wybór rejestru zależy od bitowości portu. Numer portu może być określony jako bezpośredni operand w instrukcjach wejścia i wyjścia lub jako wartość w rejestrze DX. Ostatnia metoda pozwala na dynamiczne określenie numeru portu w programie.

Operand jest na stosie

Instrukcje mogą w ogóle nie mieć operandów, mogą mieć jeden lub dwa operandy. Większość instrukcji wymaga dwóch operandów, z których jeden jest operandem źródłowym, a drugim operandem docelowym. Ważne jest, aby jeden operand mógł znajdować się w rejestrze lub pamięci, a drugi operand musi znajdować się w rejestrze lub bezpośrednio w instrukcji. Operand bezpośredni może być tylko operandem źródłowym. W dwuargumentowej instrukcji maszynowej możliwe są następujące kombinacje argumentów:

1) rejestr - rejestr;

2) rejestr - pamięć;

3) pamięć - rejestr;

4) operand natychmiastowy - rejestr;

5) operand natychmiastowy - pamięć.

Istnieją wyjątki od tej zasady dotyczące:

1) polecenia łańcuchowe, które mogą przenosić dane z pamięci do pamięci;

2) komendy stosu, które mogą przesyłać dane z pamięci do stosu, który również znajduje się w pamięci;

3) komendy typu mnożenia, które oprócz operandu określonego w poleceniu wykorzystują również drugi, niejawny operand.

Spośród wymienionych kombinacji operandów najczęściej używane są rejestr - pamięć i pamięć - rejestr. Ze względu na ich znaczenie omówimy je bardziej szczegółowo. Będziemy towarzyszyć dyskusji z przykładami instrukcji asemblera, które pokażą, jak zmienia się format instrukcji asemblera, gdy stosuje się taki lub inny typ adresowania. W związku z tym spójrz ponownie na rysunek 21, który pokazuje zasadę tworzenia adresu fizycznego na szynie adresowej mikroprocesora. Widać, że adres operandu powstaje jako suma dwóch składowych - zawartości rejestru segmentowego przesuniętego o 4 bity oraz 16-bitowego adresu efektywnego, który generalnie obliczany jest jako suma trzech składowych: bazy, przesunięcie i indeks.

3. Metody adresowania

Wymieniamy, a następnie rozważamy cechy głównych typów operandów adresowania w pamięci:

1) adresowanie bezpośrednie;

2) pośrednie podstawowe (rejestrowe) adresowanie;

3) pośrednie podstawowe (rejestrowe) adresowanie z offsetem;

4) pośrednie adresowanie indeksu z przesunięciem;

5) pośrednie adresowanie indeksu bazowego;

6) pośrednie adresowanie indeksu bazowego z przesunięciem.

Adresowanie bezpośrednie

Jest to najprostsza forma adresowania operandu w pamięci, ponieważ adres efektywny jest zawarty w samej instrukcji i do jej utworzenia nie są używane żadne dodatkowe źródła ani rejestry. Adres efektywny jest pobierany bezpośrednio z pola przesunięcia instrukcji maszynowej (patrz Rysunek 20), które może mieć rozmiar 8, 16, 32 bity. Ta wartość jednoznacznie identyfikuje bajt, słowo lub podwójne słowo znajdujące się w segmencie danych.

Adresowanie bezpośrednie może być dwojakiego rodzaju.

Względne adresowanie bezpośrednie

Używany w instrukcjach skoku warunkowego w celu wskazania względnego adresu skoku. Względność takiego przejścia polega na tym, że pole przesunięcia instrukcji maszynowej zawiera wartość 8-, 16- lub 32-bitową, która w wyniku działania instrukcji zostanie dodana do treści rejestr wskaźnika instrukcji ip/eip. W wyniku tego dodania uzyskuje się adres, do którego dokonywane jest przejście.

Bezwzględne adresowanie bezpośrednie

W tym przypadku adres efektywny jest częścią instrukcji maszynowej, ale adres ten jest tworzony tylko z wartości pola przesunięcia w instrukcji. Aby utworzyć fizyczny adres operandu w pamięci, mikroprocesor dodaje to pole z wartością rejestru segmentowego przesuniętą o 4 bity. Kilka form tego adresowania może być użytych w instrukcji asemblera.

Jednak takie adresowanie jest rzadko stosowane - powszechnie używane komórki w programie mają przypisane nazwy symboliczne. Podczas tłumaczenia asembler oblicza i podmienia wartości offsetów tych nazw do instrukcji maszynowej, którą generuje w polu „przesunięcie instrukcji”. W rezultacie okazuje się, że instrukcja maszynowa bezpośrednio adresuje swój operand, mając w rzeczywistości w jednym ze swoich pól wartość adresu efektywnego.

Inne rodzaje adresowania są pośrednie. Słowo „pośrednie” w nazwie tego typu adresowania oznacza, że ​​tylko część adresu efektywnego może znajdować się w samej instrukcji, a jej pozostałe składowe znajdują się w rejestrach, co wskazuje ich zawartość bajt modr/m oraz, prawdopodobnie przez bajt sib.

Pośrednie adresowanie podstawowe (rejestrowe)

Przy takim adresowaniu efektywny adres operandu może znajdować się w dowolnym rejestrze ogólnego przeznaczenia, z wyjątkiem sp / esp i bp / ebp (są to rejestry specyficzne do pracy z segmentem stosu). Syntaktycznie w poleceniu ten tryb adresowania jest wyrażony przez umieszczenie nazwy rejestru w nawiasach kwadratowych []. Na przykład, instrukcja mov ax, [ecx] umieszcza w rejestrach ax zawartość słowa pod adresem z segmentu danych z offsetem zapisanym w rejestrze esx. Ponieważ zawartość rejestru można łatwo zmieniać w trakcie działania programu, ta metoda adresowania umożliwia dynamiczne przypisanie adresu operandu do niektórych instrukcji maszynowych. Ta właściwość jest bardzo przydatna na przykład do organizowania cyklicznych obliczeń i pracy z różnymi strukturami danych, takimi jak tabele lub tablice.

Pośrednie adresowanie bazowe (rejestrowe) z przesunięciem

Ten typ adresowania jest dodatkiem do poprzedniego i jest przeznaczony do dostępu do danych ze znanym przesunięciem względem jakiegoś adresu bazowego. Ten rodzaj adresowania jest wygodny w użyciu w celu uzyskania dostępu do elementów struktur danych, gdy przesunięcie elementów jest znane z góry na etapie tworzenia programu, a adres bazowy (początkowy) struktury musi być obliczany dynamicznie, co etap realizacji programu. Modyfikacja zawartości rejestru bazowego umożliwia dostęp do elementów o tej samej nazwie w różnych instancjach tego samego typu struktur danych.

Na przykład instrukcja mov ax,[edx+3h] przenosi słowa z obszaru pamięci do rejestrów ax pod adresem: zawartość edx + 3h.

Instrukcja mov ax,mas[dx] przenosi słowo do rejestru ax pod adresem: zawartość dx plus wartość identyfikatora mas (pamiętaj, że kompilator przypisuje każdemu identyfikatorowi wartość równą offsetowi tego identyfikatora z początek segmentu danych).

Pośrednie adresowanie indeksu z przesunięciem

Ten rodzaj adresowania jest bardzo podobny do pośredniego adresowania bazowego z przesunięciem. Tutaj również jeden z rejestrów ogólnego przeznaczenia jest używany do utworzenia adresu efektywnego. Ale adresowanie indeksów ma jedną interesującą cechę, która jest bardzo wygodna przy pracy z tablicami. Wiąże się to z możliwością tzw. skalowania zawartości rejestru indeksowego. Co to jest?

Spójrz na rysunek 20. Interesuje nas bajt sib. Omawiając strukturę tego bajtu zauważyliśmy, że składa się on z trzech pól. Jednym z tych pól jest pole skali ss, przez które mnożona jest zawartość rejestru indeksów.

Na przykład w instrukcji mov ax,mas[si*2] wartość efektywnego adresu drugiego argumentu jest obliczana przez wyrażenie mas+(si)*2. Z uwagi na to, że asembler nie ma środków na zorganizowanie indeksowania tablic, programista musi je zorganizować sam.

Możliwość skalowania znacząco pomaga w rozwiązaniu tego problemu, ale pod warunkiem, że rozmiar elementów tablicy wynosi 1, 2, 4 lub 8 bajtów.

Pośrednie adresowanie indeksu bazowego

W przypadku tego typu adresowania adres efektywny jest tworzony jako suma zawartości dwóch rejestrów ogólnego przeznaczenia: bazowego i indeksowego. Rejestry te mogą być dowolnymi rejestrami ogólnego przeznaczenia i często stosuje się skalowanie zawartości rejestru indeksowego.

Pośrednie adresowanie indeksu bazowego z przesunięciem

Ten rodzaj adresowania jest uzupełnieniem adresowania pośredniego indeksowanego. Adres efektywny jest tworzony jako suma trzech składowych: zawartości rejestru bazowego, zawartości rejestru indeksowego i wartości pola przesunięcia w instrukcji.

Na przykład, instrukcja mov eax,[esi+5] [edx] przenosi podwójne słowo do rejestru eax pod adresem: (esi) + 5 + (edx).

Polecenie add ax,array[esi][ebx] dodaje zawartość rejestru ax do zawartości słowa pod adresem: wartość tablicy identyfikatorów + (esi) + (ebx).

WYKŁAD nr 18. Drużyny

1. Polecenia przesyłania danych

Dla wygody praktycznego zastosowania i odzwierciedlenia ich specyfiki wygodniej jest rozważyć polecenia tej grupy zgodnie z ich przeznaczeniem funkcjonalnym, zgodnie z którym można je podzielić na następujące grupy poleceń:

1) przekazywanie danych ogólnego przeznaczenia;

2) wejście-wyjście do portu;

3) pracować z adresami i wskaźnikami;

4) przekształcenia danych;

5) pracuj ze stosem.

Ogólne polecenia przesyłania danych

Ta grupa obejmuje następujące polecenia:

1) mov to podstawowe polecenie przesyłania danych. Wdraża szeroką gamę opcji wysyłki. Zwróć uwagę na specyfikę tego polecenia:

a) polecenie mov nie może być użyte do przeniesienia z jednego obszaru pamięci do drugiego. Jeśli zajdzie taka potrzeba, jako bufor pośredniczący należy wykorzystać dowolny aktualnie dostępny rejestr ogólnego przeznaczenia;

b) niemożliwe jest załadowanie wartości bezpośrednio z pamięci do rejestru segmentowego. Dlatego, aby wykonać takie obciążenie, musisz użyć obiektu pośredniego. Może to być rejestr ogólnego przeznaczenia lub stos;

c) nie można przenieść zawartości jednego rejestru segmentowego do innego rejestru segmentowego. Dzieje się tak, ponieważ w systemie poleceń nie ma odpowiedniego kodu operacji. Ale często pojawia się potrzeba takiego działania. Taki transfer można wykonać przy użyciu tych samych rejestrów ogólnego przeznaczenia, co rejestry pośrednie;

d) rejestr segmentowy CS nie może być użyty jako operand przeznaczenia. Powód jest prosty. Faktem jest, że w architekturze mikroprocesora para cs:ip zawsze zawiera adres polecenia, które powinno zostać wykonane w następnej kolejności. Zmiana zawartości rejestru CS za pomocą polecenia mov oznaczałaby w rzeczywistości operację skoku, a nie transfer, co jest niedopuszczalne. 2) xchg - służy do dwukierunkowego przesyłania danych. Do tej operacji można oczywiście użyć sekwencji kilku instrukcji mov, ale ze względu na to, że operacja wymiany jest wykorzystywana dość często, twórcy mikroprocesorowego systemu instrukcji uznali za konieczne wprowadzenie osobnej instrukcji wymiany xchg. Oczywiście operandy muszą być tego samego typu. Nie wolno (jak w przypadku wszystkich instrukcji asemblera) wymieniać między sobą zawartości dwóch komórek pamięci.

Polecenia wejścia/wyjścia portu

Spójrz na rysunek 22. Pokazuje on bardzo uproszczony, pojęciowy schemat sterowania sprzętem komputerowym.

Ryż. 22. Schemat koncepcyjny sterowania sprzętem komputerowym

Jak widać na Rysunku 22, najniższym poziomem jest poziom BIOS, gdzie sprzęt jest obsługiwany bezpośrednio przez porty. Realizuje to koncepcję niezależności sprzętowej. Podczas wymiany sprzętu konieczne będzie jedynie poprawienie odpowiednich funkcji BIOS-u, przeorientowanie ich na nowe adresy i logikę portów.

Zasadniczo zarządzanie urządzeniami bezpośrednio przez porty jest łatwe. Informacje o numerach portów, ich głębi bitowej, formacie informacji sterujących podane są w opisie technicznym urządzenia. Musisz tylko znać ostateczny cel swoich działań, algorytm według którego działa dane urządzenie i kolejność programowania jego portów, czyli tak naprawdę musisz wiedzieć, do czego i w jakiej kolejności należy wysyłać port (podczas pisania do niego) lub odczyt z niego (podczas odczytu) i jak należy interpretować te informacje. Aby to zrobić, wystarczą tylko dwa polecenia, które są obecne w systemie poleceń mikroprocesora:

1) w akumulatorze numer_portu - wejście do akumulatora z portu o numerze numer_portu;

2) port wyjściowy, akumulator - wyprowadza zawartość akumulatora do portu o numerze numer_portu.

Polecenia do pracy z adresami i wskaźnikami pamięci

Podczas pisania programów w asemblerze intensywna praca jest wykonywana z adresami operandów znajdujących się w pamięci. Do obsługi tego rodzaju operacji istnieje specjalna grupa poleceń, w skład której wchodzą następujące polecenia:

1) miejsce przeznaczenia, źródło - efektywne ładowanie adresu;

2) Id cel, źródło - ładowanie wskaźnika do rejestru segmentu danych ds;

3) les destination, source - załadowanie wskaźnika do rejestru dodatkowego segmentu danych es;

4) lgs cel, źródło - załadowanie wskaźnika do rejestru dodatkowego segmentu danych gs;

5) lfs cel, źródło - załadowanie wskaźnika do rejestru dodatkowego segmentu danych fs;

6) lss cel, źródło - wskaźnik obciążenia do rejestru segmentowego stosu ss.

Polecenie lea jest podobne do polecenia mov, ponieważ wykonuje również ruch. Jednak instrukcja lea nie przesyła danych, ale raczej efektywny adres danych (to znaczy przesunięcie danych od początku segmentu danych) do rejestru wskazanego przez operand docelowy.

Często, aby wykonać jakąś akcję w programie, nie wystarczy znać samą wartość efektywnego adresu danych, ale konieczne jest posiadanie pełnego wskaźnika do danych. Kompletny wskaźnik danych składa się z komponentu segmentu i przesunięcia. Wszystkie inne polecenia z tej grupy pozwalają na uzyskanie takiego pełnego wskaźnika do operandu w pamięci w parze rejestrów. W tym przypadku nazwa rejestru segmentowego, w którym umieszczony jest składnik segmentowy adresu, jest określona przez kod operacji. Odpowiednio, przesunięcie jest umieszczane w rejestrze ogólnym wskazanym przez operand docelowy.

Ale nie wszystko jest takie proste z operandem źródłowym. W rzeczywistości w poleceniu jako źródło nie można bezpośrednio określić nazwy operandu w pamięci, do którego chcielibyśmy otrzymać wskaźnik. Najpierw musisz pobrać wartość pełnego wskaźnika w jakimś obszarze pamięci i podać pełny adres nazwy tego obszaru w poleceniu get. Aby wykonać tę akcję, musisz zapamiętać dyrektywy dotyczące rezerwowania i inicjowania pamięci.

Podczas stosowania tych dyrektyw możliwy jest szczególny przypadek, gdy w polu operandu jest podana nazwa innej dyrektywy definicji danych (w rzeczywistości nazwa zmiennej). W takim przypadku adres tej zmiennej jest tworzony w pamięci. Który adres zostanie wygenerowany (efektywny czy kompletny) zależy od zastosowanej dyrektywy. Jeśli jest to dw, to w pamięci tworzona jest tylko 16-bitowa wartość adresu efektywnego, jeśli jest to dd, do pamięci zapisywany jest pełny adres. Lokalizacja tego adresu w pamięci jest następująca: młodsze słowo zawiera przesunięcie, starsze słowo zawiera 16-bitowy składnik segmentu adresu.

Na przykład, organizując pracę z łańcuchem znaków, wygodnie jest umieścić jego adres początkowy w określonym rejestrze, a następnie zmodyfikować tę wartość w pętli dla sekwencyjnego dostępu do elementów łańcucha.

Konieczność użycia poleceń w celu uzyskania pełnego wskaźnika danych w pamięci, tj. adresu segmentu i wartości przesunięcia w segmencie, pojawia się w szczególności podczas pracy z łańcuchami.

Polecenia konwersji danych

Do tej grupy można przypisać wiele instrukcji mikroprocesorowych, ale większość z nich ma pewne cechy, które wymagają ich przypisania do innych grup funkcyjnych. Dlatego z całego zestawu poleceń mikroprocesorowych tylko jedno polecenie można bezpośrednio przypisać do poleceń konwersji danych: xlat [adres_tabeli_transkodowania]

To bardzo ciekawy i użyteczny zespół. Jego efektem jest zastąpienie wartości w rejestrze al innym bajtem z tablicy pamięci znajdującej się pod adresem określonym przez operand recoding_table_address.

Słowo „tabela” jest bardzo warunkowe, w rzeczywistości to tylko ciąg bajtów. Adres bajtu w ciągu, który zastąpi zawartość rejestru al, jest określony przez sumę (bx) + (al), czyli zawartość al pełni rolę indeksu w tablicy bajtów.

Podczas pracy z poleceniem xlat zwróć uwagę na następujący subtelny punkt. Chociaż instrukcja określa adres ciągu bajtów, z którego ma zostać pobrana nowa wartość, adres ten musi być wstępnie załadowany (na przykład za pomocą polecenia lea) do rejestru bx. Tak więc operand lookup_table_address nie jest tak naprawdę potrzebny (pokazano, że operand jest opcjonalny, umieszczając go w nawiasach kwadratowych). Jeśli chodzi o ciąg bajtów (tablica transkodowania), jest to obszar pamięci o rozmiarze od 1 do 255 bajtów (zakres liczby bez znaku w rejestrze 8-bitowym).

Polecenia stosu

Ta grupa to zestaw wyspecjalizowanych poleceń skoncentrowanych na organizowaniu elastycznej i wydajnej pracy ze stosem.

Stos to obszar pamięci specjalnie przeznaczony do tymczasowego przechowywania danych programu. O znaczeniu stosu decyduje fakt, że w strukturze programu przewidziano dla niego osobny segment. W przypadku, gdy programista zapomniał zadeklarować segment stosu w swoim programie, linker tlink wyśle ​​komunikat ostrzegawczy.

Stos ma trzy rejestry:

1) ss - rejestr segmentów stosu;

2) sp/esp - rejestr wskaźnika stosu;

3) bp/ebp - rejestr wskaźnika bazowego ramki stosu.

Rozmiar stosu zależy od trybu pracy mikroprocesora i jest ograniczony do 64 KB (lub 4 GB w trybie chronionym).

W danym momencie dostępny jest tylko jeden stos, którego adres segmentu jest zawarty w rejestrze SS. Ten stos nazywa się bieżącym stosem. Aby odwołać się do innego stosu ("przełącz stos"), konieczne jest załadowanie innego adresu do rejestru ss. Rejestr SS jest automatycznie używany przez procesor do wykonywania wszystkich instrukcji, które działają na stosie.

Podajemy kilka innych funkcji pracy ze stosem:

1) zapis i odczyt danych na stosie odbywa się zgodnie z zasadą LIFO,

2) gdy dane są zapisywane na stosie, ten ostatni rośnie w kierunku niższych adresów. Ta funkcja jest osadzona w algorytmie poleceń do pracy ze stosem;

3) przy wykorzystaniu rejestrów esp/sp i ebp/bp do adresowania pamięci, asembler automatycznie uważa, że ​​wartości w nim zawarte są przesunięciami względem rejestru segmentowego ss.

Ogólnie stos jest zorganizowany tak, jak pokazano na rysunku 23.

Ryż. 23. Schemat koncepcyjny organizacji stosu

Rejestry SS, ESP/SP i EUR/BP są zaprojektowane do pracy ze stosem. Rejestry te są wykorzystywane w sposób kompleksowy, a każdy z nich ma swoje przeznaczenie funkcjonalne.

Rejestr ESP/SP zawsze wskazuje na wierzchołek stosu, to znaczy zawiera przesunięcie, w którym ostatni element został odsunięty na stos. Instrukcje stosu domyślnie zmieniają ten rejestr tak, że zawsze wskazuje na ostatni element włożony na stos. Jeśli stos jest pusty, to wartość esp jest równa adresowi ostatniego bajtu segmentu przydzielonego dla stosu. Kiedy element jest odkładany na stos, procesor zmniejsza wartość rejestru esp, a następnie zapisuje element pod adresem nowego wierzchołka. Podczas zdejmowania danych ze stosu procesor kopiuje element znajdujący się pod adresem wierzchołka, a następnie zwiększa wartość rejestru wskaźnika stosu, zwł. Okazuje się więc, że stos rośnie w kierunku malejących adresów.

Co zrobić, jeśli potrzebujemy uzyskać dostęp do elementów nie na górze, ale wewnątrz stosu? Aby to zrobić, użyj rejestru EBP.Rejestr EBP jest rejestrem wskaźnika bazowego ramki stosu.

Na przykład typowa sztuczka podczas wprowadzania podprogramu polega na przekazaniu żądanych parametrów poprzez wepchnięcie ich na stos. Jeśli podprogram pracuje również aktywnie ze stosem, dostęp do tych parametrów staje się problematyczny. Wyjściem jest zapisanie adresu wierzchołka stosu w ramce (podstawie) wskaźnika stosu, rejestrze EBP, po zapisaniu niezbędnych danych do stosu. Wartość w EUR można później wykorzystać do uzyskania dostępu do przekazanych parametrów.

Początek stosu znajduje się pod wyższymi adresami pamięci. Na rysunku 23 adres ten jest oznaczony parą ss: fffF. Przesunięcie wT jest tutaj podane warunkowo. W rzeczywistości wartość ta jest określona przez wartość, którą programista określa opisując segment stosu w swoim programie.

Aby zorganizować pracę ze stosem, istnieją specjalne polecenia do pisania i czytania.

1. push source - zapis wartości źródła na szczycie stosu.

Interesujący jest algorytm tego polecenia, który obejmuje następujące działania (ryc. 24):

1) (sp) = (sp) - 2; wartość sp zmniejsza się o 2;

2) wartość ze źródła jest zapisywana na adres określony przez parę ss:sp.

Ryż. 24. Jak działa polecenie push?

2. przypisanie pop - zapisanie wartości ze szczytu stosu do lokalizacji określonej przez operand docelowy. W ten sposób wartość jest „usuwana” ze szczytu stosu. Algorytm polecenia pop jest odwrotnością algorytmu polecenia push (rys. 25):

1) zapisanie zawartości wierzchołka stosu w miejscu wskazanym przez operand przeznaczenia;

2) (sp) = (sp) + 2; zwiększenie wartości sp. z o.o.

Ryż. 25. Jak działa polecenie pop

3. pusha - grupowe polecenie zapisu na stos. Za pomocą tego polecenia rejestry ax, cx, dx, bx, sp, bp, si, di są kolejno zapisywane na stosie. Zwróć uwagę, że oryginalna zawartość sp jest zapisana, to znaczy zawartość, która była przed wydaniem polecenia pusha (ryc. 26).

Ryż. 26. Jak działa polecenie pusha?

4. pushaw jest prawie synonimem polecenia pusha.Jaka jest różnica? Atrybutem bitness może być use16 lub use32. Przyjrzyjmy się, jak polecenia pusha i pushaw działają z każdym z tych atrybutów:

1) use16 - algorytm pushaw jest podobny do algorytmu pusha;

2) use32 - pushaw się nie zmienia (tzn. jest niewrażliwy na szerokość segmentu i zawsze działa z rejestrami wielkości słowa - ax, cx, dx, bx, sp, bp, si, di). Komenda pusha jest wrażliwa na ustawioną szerokość segmentu i gdy określony jest segment 32-bitowy, działa z odpowiednimi rejestrami 32-bitowymi, tj. eax, esx, edx, ebx, esp, ebp, esi, edi.

5. pushad - wykonywany podobnie do polecenia pusha, ale są pewne osobliwości.

Następujące trzy polecenia wykonują odwrotność powyższych poleceń:

1) rora;

2) popawy;

3) pop.

Grupa instrukcji opisanych poniżej umożliwia zapisanie rejestru flag na stosie i zapisanie słowa lub podwójnego słowa na stos. Zauważ, że instrukcje wymienione poniżej są jedynymi w zestawie instrukcji mikroprocesora, które umożliwiają (i wymagają) dostępu do całej zawartości rejestru flag.

1. pushf - zapisuje rejestr flag na stosie.

Działanie tego polecenia zależy od atrybutu rozmiaru segmentu:

1) użyj 16 - rejestr flag o rozmiarze 2 bajtów jest zapisywany na stosie;

2) use32 - rejestr flag o długości 4 bajtów jest zapisywany na stosie.

2. pushfw - zapisuje na stosie rejestr flag wielkości słowa. Zawsze działa jak pushf z atrybutem use16.

3. pushfd - zapisanie rejestru flag lub flag flag na stosie w zależności od atrybutu szerokości bitowej segmentu (tj. tak samo jak pushf).

Podobnie, następujące trzy polecenia wykonują odwrotność operacji omówionych powyżej:

1) popf;

2) popftv;

3) popfd.

Podsumowując, zwracamy uwagę na główne typy operacji, gdy użycie stosu jest prawie nieuniknione:

1) wywoływanie podprogramów;

2) tymczasowe przechowywanie wartości rejestru;

3) definicja zmiennych lokalnych.

2. Polecenia arytmetyczne

Mikroprocesor może wykonywać operacje na liczbach całkowitych i zmiennoprzecinkowych. Aby to zrobić, jego architektura składa się z dwóch oddzielnych bloków:

1) urządzenie do wykonywania operacji na liczbach całkowitych;

2) urządzenie do wykonywania operacji zmiennoprzecinkowych.

Każde z tych urządzeń ma swój własny system dowodzenia. W zasadzie urządzenie liczb całkowitych może przejąć wiele funkcji urządzenia zmiennoprzecinkowego, ale będzie to kosztowne obliczeniowo. Dla większości problemów używających języka asemblera, arytmetyka liczb całkowitych jest wystarczająca.

Przegląd grupy instrukcji arytmetycznych i danych

Urządzenie do obliczeń na liczbach całkowitych obsługuje nieco ponad tuzin instrukcji arytmetycznych. Rysunek 27 przedstawia klasyfikację poleceń w tej grupie.

Ryż. 27. Klasyfikacja poleceń arytmetycznych

Grupa instrukcji arytmetycznych na liczbach całkowitych działa z dwoma typami liczb:

1) całkowite liczby binarne. Liczby mogą, ale nie muszą mieć cyfry ze znakiem, tj. mogą być cyframi ze znakiem lub bez;

2) całkowite liczby dziesiętne.

Rozważ formaty maszyn, w których przechowywane są te typy danych.

Liczby binarne całkowite

Stałoprzecinkowa binarna liczba całkowita to liczba zakodowana w systemie liczb binarnych.

Wymiar binarnej liczby całkowitej może wynosić 8, 16 lub 32 bity. Znak liczby binarnej zależy od tego, jak interpretowany jest najbardziej znaczący bit w reprezentacji liczby. Jest to 7,15 lub 31 bitów dla liczb o odpowiednim wymiarze. Jednocześnie interesujące jest to, że wśród poleceń arytmetycznych są tylko dwa polecenia, które naprawdę uwzględniają ten najważniejszy bit jako znak, są to polecenia mnożenia i dzielenia liczb całkowitych imul i idiv. W innych przypadkach odpowiedzialność za działania z podpisanymi numerami i odpowiednio z bitem znaku spoczywa na programiście. Zakres wartości liczby binarnej zależy od jej wielkości i interpretacji najbardziej znaczącego bitu jako najbardziej znaczącego bitu liczby lub jako bitu znaku liczby (tabela 9).

Tabela 9. Zakres liczb binarnych Liczby dziesiętne

Liczby dziesiętne to specjalny rodzaj reprezentacji informacji liczbowych, który opiera się na zasadzie kodowania każdej cyfry dziesiętnej liczby przez grupę czterech bitów. W takim przypadku każdy bajt liczby zawiera jedną lub dwie cyfry dziesiętne w tzw. kodzie dziesiętnym zakodowanym binarnie (BCD - Binary-Coded Decimal). Mikroprocesor przechowuje numery BCD w dwóch formatach (rys. 28):

1) format zapakowany. W tym formacie każdy bajt zawiera dwie cyfry dziesiętne. Cyfra dziesiętna to 0-bitowa wartość binarna z zakresu od 9 do 4. W tym przypadku kod najwyższej cyfry liczby zajmuje najwyższe 4 bity. Dlatego zakres reprezentacji liczby dziesiętnej w 1 bajcie wynosi od 00 do 99;

2) format bez opakowania. W tym formacie każdy bajt zawiera jedną cyfrę dziesiętną w czterech najmniej znaczących bitach. Górne 4 bity są ustawione na zero. To jest tak zwana strefa. Dlatego zakres przedstawiania liczby dziesiętnej w 1 bajcie wynosi od 0 do 9.

Ryż. 28. Reprezentacja numerów BCD

Jak opisać binarne liczby dziesiętne w programie? Aby to zrobić, możesz użyć tylko dwóch dyrektyw opisu danych i inicjalizacji - db i dt. Możliwość wykorzystania tylko tych dyrektyw do opisu liczb BCD wynika z faktu, że do takich liczb obowiązuje zasada „młodszego bajtu pod niskim adresem”, co jest bardzo wygodne przy ich przetwarzaniu. I generalnie przy korzystaniu z takiego typu danych, jak liczby BCD, kolejność, w jakiej te liczby są opisane w programie oraz algorytm ich przetwarzania, jest kwestią gustu i osobistych preferencji programisty. Stanie się to jasne po przyjrzeniu się podstawom pracy z liczbami BCD poniżej.

Operacje arytmetyczne na binarnych liczbach całkowitych

Dodawanie liczb binarnych bez znaku

Mikroprocesor wykonuje dodawanie operandów zgodnie z zasadami dodawania liczb binarnych. Nie ma problemów, o ile wartość wyniku nie przekracza wymiarów pola operandu. Na przykład podczas dodawania operandów o rozmiarze bajtowym wynik nie może przekraczać liczby 255. Jeśli tak się stanie, wynik jest niepoprawny. Zastanówmy się, dlaczego tak się dzieje.

Na przykład zróbmy dodawanie: 254 + 5 = 259 binarnie. 11111110 + 0000101 = 1 00000011. Wynik przekroczył 8 bitów i jego poprawna wartość mieści się w 9 bitach, a wartość 8 pozostała w 3-bitowym polu operandu, co oczywiście nie jest prawdą. W mikroprocesorze przewidywany jest ten wynik dodawania i przewidziano specjalne środki do naprawy takich sytuacji i ich przetwarzania. Tak więc, aby naprawić sytuację wyjścia poza siatkę bitów wyniku, jak w tym przypadku, zamierzona jest flaga przeniesienia cf. Znajduje się w bicie 0 rejestru flag EFLAGS/FLAGS. To ustawienie tej flagi naprawia fakt przeniesienia jedynki z wyższego rzędu operandu. Oczywiście programista musi liczyć się z możliwością takiego wyniku operacji dodawania i zapewnić środki do korekty. Wiąże się to z dołączaniem sekcji kodu po operacji dodawania, w której analizowana jest flaga cf. Ta flaga może być analizowana na różne sposoby.

Najłatwiejszym i najbardziej dostępnym jest użycie polecenia gałęzi warunkowej jcc. Operandem tej instrukcji jest nazwa etykiety w bieżącym segmencie kodu. Przejście do tej etykiety następuje, jeśli w wyniku działania poprzedniego polecenia flaga cf jest ustawiona na 1. W systemie poleceń mikroprocesora występują trzy polecenia dodawania binarnego:

1) inc operand - operacja inkrementacyjna, czyli zwiększenie wartości operandu o 1;

2) dodaj operand_1, operand_2 - instrukcja dodawania z zasadą działania: operand_1 = operand_1 + operand_2;

3) adc operand_1, operand_2 - instrukcja dodawania uwzględniająca flagę przeniesienia por. Zasada działania polecenia: operand_1 = operand_1 + operand_2 + wartość_sG.

Zwróć uwagę na ostatnie polecenie - jest to polecenie dodawania, które uwzględnia przeniesienie jednego z wyższego rzędu. Rozważaliśmy już mechanizm pojawienia się takiej jednostki. Tak więc instrukcja adc jest mikroprocesorowym narzędziem do dodawania długich liczb binarnych, których wymiary przekraczają długości standardowych pól obsługiwanych przez mikroprocesor.

Podpisany dodatek binarny

W rzeczywistości mikroprocesor „nie zdaje sobie sprawy” z różnicy między liczbami podpisanymi i niepodpisanymi. Zamiast tego ma środki do ustalenia występowania charakterystycznych sytuacji, które rozwijają się w procesie obliczeń. Omówiliśmy niektóre z nich podczas omawiania niepodpisanego dodawania:

1) flaga przeniesienia cf, ustawienie jej na 1 wskazuje, że argumenty były poza zasięgiem;

2) komenda adc, która uwzględnia możliwość takiego wyjścia (carry od najmniej znaczącego bitu).

Innym sposobem jest zarejestrowanie stanu bitu wyższego rzędu (znaku) operandu, co odbywa się za pomocą flagi przepełnienia w rejestrze EFLAGS (bit 11).

Oczywiście pamiętasz, jak liczby są reprezentowane w komputerze: dodatnie - w systemie binarnym, ujemne - w uzupełnieniu do dwójki. Rozważ różne opcje dodawania liczb. Przykłady mają na celu pokazanie zachowania dwóch najbardziej znaczących bitów operandów oraz poprawności wyniku operacji dodawania.

Przykład

30566 = 0111011101100110

+

00687 = 00000010

=

31253 = 01111010

Monitorujemy przelewy z 14 i 15 cyfry oraz poprawność wyniku: nie ma przelewów, wynik jest poprawny.

Przykład

30566 = 0111011101100110

+

30566 = 0111011101100110

=

1132 = 11101110

Nastąpił transfer z 14 kategorii; nie ma przeniesienia z 15 kategorii. Wynik jest błędny, ponieważ nastąpiło przepełnienie - wartość liczby okazała się większa niż to, co może mieć 16-bitowa liczba ze znakiem (+32 767).

Przykład

-30566 = 10001000 10011010

+

-04875 = 11101100 11110101

=

-35441 = 01110101 10001111

Był przelew z 15 cyfry, nie ma przelewu z 14 cyfry. Wynik jest błędny, ponieważ zamiast liczby ujemnej okazał się dodatni (najbardziej znaczący bit to 0).

Przykład

-4875 = 11101100 11110101

+

-4875 = 11101100 11110101

=

09750 = 11011001

Są transfery z 14 i 15 bitów. Wynik jest poprawny.

W ten sposób zbadaliśmy wszystkie przypadki i odkryliśmy, że sytuacja przepełnienia (ustawienie flagi OF na 1) występuje podczas transferu:

1) od 14 cyfry (dla liczb dodatnich ze znakiem);

2) od 15 cyfry (dla liczb ujemnych).

Odwrotnie, nie występuje przepełnienie (tj. flaga OF jest resetowana do 0), jeśli występuje przeniesienie z obu bitów lub jeśli nie ma przeniesienia w obu bitach.

Tak więc przepełnienie jest rejestrowane z flagą przepełnienia. Oprócz flagi, podczas przesyłania z bitu wyższego rzędu, flaga transferu CF jest ustawiona na 1. Ponieważ mikroprocesor nie wie o istnieniu liczb ze znakiem i bez znaku, programista jest wyłącznie odpowiedzialny za prawidłowe działania z wynikowe liczby. Możesz analizować flagi CF i OF za pomocą instrukcji skoku warunkowego, odpowiednio, JC\JNC i JO\JNO.

Polecenia dodawania liczb ze znakiem są takie same jak w przypadku liczb bez znaku.

Odejmowanie liczb binarnych bez znaku

Podobnie jak w analizie operacji dodawania omówimy istotę procesów zachodzących podczas wykonywania operacji odejmowania. Jeżeli odjemna jest większa niż odjemna, to nie ma problemu - różnica jest dodatnia, wynik jest poprawny. Jeśli odjemna jest mniejsza niż odjęta, pojawia się problem: wynik jest mniejszy niż 0, a to już jest liczba ze znakiem. W takim przypadku wynik musi być opakowany. Co to znaczy? Przy zwykłym odejmowaniu (w kolumnie) pożyczają 1 od najwyższego rzędu. Mikroprocesor robi to samo, tj. pobiera 1 z cyfry następującej po najwyższej w siatce bitów operandu. Wyjaśnijmy na przykładzie.

Przykład

05 = 00000000

-10 = 00000000 00001010

Aby wykonać odejmowanie, zróbmy

pożyczka urojona dla seniorów:

100000000 00000101

-

00000000 00001010

=

11111111 11111011

Tak więc w istocie działanie

(65 + 536) - 5 = 10

0 tutaj jest niejako odpowiednikiem liczby 65536. Wynik oczywiście jest niepoprawny, ale mikroprocesor uważa, że ​​wszystko jest w porządku, chociaż naprawia fakt wypożyczenia jednostki ustawiając flagę przeniesienia por. Ale spójrz ponownie uważnie na wynik operacji odejmowania. To -5 w uzupełnieniu do dwóch! Przeprowadźmy eksperyment: przedstaw różnicę jako sumę 5 + (-10).

Przykład

5 = 00000000

+

(-10)= 11111111 11110110

=

11111111 11111011

tj. otrzymaliśmy taki sam wynik jak w poprzednim przykładzie.

Tak więc po poleceniu odejmowania liczb bez znaku należy przeanalizować stan flagi CE.Jeżeli jest ustawiona na 1, oznacza to, że była pożyczka z wyższego rzędu i wynik uzyskano w dodatkowym kodzie .

Podobnie jak instrukcje dodawania, grupa instrukcji odejmowania składa się z najmniejszego możliwego zestawu. Polecenia te wykonują odejmowanie zgodnie z algorytmami, które teraz rozważamy, a wyjątki musi wziąć pod uwagę sam programista. Polecenia odejmowania obejmują:

1) dec operand - operacja dekrementacji, czyli zmniejszenie wartości operandu o 1;

2) sub operand_1, operand_2 - polecenie odejmowania; jego zasada działania: operand_1 = operand_1 - operand_2;

3) sbb operand_1, operand_2 - polecenie odejmowania z uwzględnieniem pożyczki (flaga ci): operand_1 = operand_1 - operand_2 - wartość_sG.

Jak widać, wśród poleceń odejmowania znajduje się polecenie sbb, które uwzględnia flagę przeniesienia cf. To polecenie jest podobne do adc, ale teraz flaga cf działa jako wskaźnik pożyczania 1 od najbardziej znaczącej cyfry podczas odejmowania liczb.

Odejmowanie binarne ze znakiem

Tutaj wszystko jest nieco bardziej skomplikowane. Mikroprocesor nie musi mieć dwóch urządzeń - dodawania i odejmowania. Wystarczy mieć tylko jedno - dodatkowe urządzenie. Ale do odejmowania przez dodawanie liczb ze znakiem w dodatkowym kodzie konieczne jest przedstawienie obu operandów - zarówno zredukowanego, jak i odejmowanego. Wynik należy również traktować jako wartość dopełnienia do dwóch. Ale tutaj pojawiają się trudności. Przede wszystkim są one związane z faktem, że najbardziej znaczący bit operandu jest uważany za bit znaku. Rozważ przykład odejmowania 45 - (-127).

Przykład

Odejmowanie cyfr ze znakiem 1

45 = 0010

-

-127 = 1000 0001

=

-44 = 1010 1100

Sądząc po bicie znaku, wynik okazał się ujemny, co z kolei wskazuje, że liczbę należy traktować jako uzupełnienie równe -44. Prawidłowy wynik powinien wynosić 172. Tutaj, podobnie jak w przypadku dodawania ze znakiem, spotkaliśmy się z przepełnieniem mantysy, gdy znaczący bit liczby zmienił bit znaku operandu. Możesz śledzić tę sytuację na podstawie zawartości flagi przepełnienia. Ustawienie go na 1 wskazuje, że wynik jest poza zakresem liczb ze znakiem (tj. najbardziej znaczący bit się zmienił) dla operandu o tym rozmiarze, a programista musi podjąć działania, aby poprawić wynik.

Przykład

Odejmowanie cyfr ze znakiem 2

-45-45 = -45 + (-45) = -90.

-45=11010011

+

-45=11010011

=

-90 = 1010 0110

Tutaj wszystko jest w porządku, flaga przepełnienia jest resetowana do 0, a 1 w bicie znaku wskazuje, że wartość wynikowa jest liczbą uzupełnienia do dwóch.

Odejmowanie i dodawanie dużych operandów

Jak zauważysz, instrukcje dodawania i odejmowania działają z operandami o stałym wymiarze: 8, 16, 32 bity. Ale co, jeśli musisz dodać liczby o większym wymiarze, na przykład 48 bitów, używając 16-bitowych operandów? Na przykład dodajmy dwie liczby 48-bitowe:

Ryż. 29. Dodawanie dużych operandów

Rysunek 29 przedstawia technologię dodawania długich liczb krok po kroku. Widać, że proces dodawania liczb wielobajtowych przebiega tak samo, jak przy dodawaniu dwóch liczb „w kolumnie” – z zaimplementowaniem w razie potrzeby przeniesienia 1 do najwyższego bitu. Jeśli uda nam się zaprogramować ten proces, to znacznie rozszerzymy zakres liczb binarnych, na których możemy wykonywać operacje dodawania i odejmowania.

Zasada odejmowania liczb z zakresem reprezentacji przekraczającym standardowe siatki bitowe argumentów jest taka sama, jak przy dodawaniu, tj. stosowana jest flaga przeniesienia cf. Wystarczy wyobrazić sobie proces odejmowania w kolumnie i poprawnie połączyć instrukcje mikroprocesora z instrukcją sbb.

Aby zakończyć naszą dyskusję na temat instrukcji dodawania i odejmowania, oprócz cf i flag, w rejestrze flag istnieje kilka innych flag, których można używać z binarnymi instrukcjami arytmetycznymi. Są to następujące flagi:

1) zf - flaga zero, która jest ustawiana na 1, jeśli wynik operacji wynosi 0, i na 1, jeśli wynik nie jest równy 0;

2) sf - flaga znaku, której wartość po operacjach arytmetycznych (i nie tylko) pokrywa się z wartością najbardziej znaczącego bitu wyniku, czyli z bitem 7, 15 lub 31. Tym samym flaga ta może być używana do operacji na podpisanych numerach.

Mnożenie liczb bez znaku

Polecenie mnożenia liczb bez znaku to

mnożnik_1

Jak widać, polecenie zawiera tylko jeden operand mnożnika. Drugi argument czynnik_2 jest określony niejawnie. Jego lokalizacja jest stała i zależy od wielkości czynników. Ponieważ, ogólnie rzecz biorąc, wynik mnożenia jest większy niż którykolwiek z jego czynników, jego rozmiar i położenie muszą być również jednoznacznie określone. Opcje dotyczące wielkości czynników i umiejscowienia drugiego argumentu oraz wyniku przedstawiono w tabeli 10.

Tabela 10. Układ operandów i wynik mnożenia

Z tabeli widać, że iloczyn składa się z dwóch części i w zależności od wielkości operandów jest umieszczony w dwóch miejscach - w miejscu factor_2 (część dolna) oraz w rejestrze dodatkowym ah, dx, edx (wyższy część). Skąd zatem dynamicznie (tj. podczas wykonywania programu) wiedzieć, że wynik jest na tyle mały, że mieści się w jednym rejestrze, albo że przekroczył wymiar rejestru i najwyższa część znalazła się w innym rejestrze? W tym celu wykorzystujemy znane nam już z poprzedniej dyskusji flagi cf i overflow:

1) jeżeli wiodąca część wyniku wynosi zero, to po operacji iloczynu flagi cf = 0 i of = 0;

2) jeśli te flagi są niezerowe, oznacza to, że wynik wyszedł poza najmniejszą część iloczynu i składa się z dwóch części, co należy uwzględnić w dalszej pracy.

Pomnóż podpisane liczby

Polecenie mnożenia liczb ze znakiem to

[imul operand_1, operand_2, operand_3]

To polecenie jest wykonywane w taki sam sposób, jak polecenie mul. Charakterystyczną cechą polecenia imul jest tylko formowanie znaku.

Jeśli wynik jest mały i mieści się w jednym rejestrze (czyli jeśli cf = z = 0), to zawartość drugiego rejestru (górna część) jest rozszerzeniem znaku - wszystkie jego bity są równe bitowi starszemu (bit znaku ) dolnej części wyniku. W przeciwnym razie (jeśli cf = z = 1), znakiem wyniku jest bit znaku wysokiej części wyniku, a bit znaku niskiej części jest znaczącym bitem binarnego kodu wyniku.

Podział liczb niepodpisanych

Poleceniem dzielenia liczb bez znaku jest

dzielnik div

Dzielnik może znajdować się w pamięci lub w rejestrze i mieć rozmiar 8, 16 lub 32 bitów. Lokalizacja dywidendy jest stała i podobnie jak w instrukcji mnożenia zależy od wielkości operandów. Wynikiem polecenia dzielenia jest wartość ilorazu i reszty.

Opcje lokalizacji i wielkości operandów operacji dzielenia pokazano w tabeli 11.

Tabela 11. Układ operandów i wynik dzielenia

Po wykonaniu instrukcji dzielenia zawartość flag jest niezdefiniowana, ale może wystąpić przerwanie numer 0, zwane "dziel przez zero". Ten rodzaj przerwy należy do tzw. wyjątków. Ten rodzaj przerwania występuje wewnątrz mikroprocesora z powodu pewnych anomalii podczas procesu obliczeniowego. Przerwanie O, "podziel przez zero", podczas wykonywania polecenia div może wystąpić z jednego z następujących powodów:

1) dzielnik wynosi zero;

2) iloraz nie jest uwzględniony w przydzielonej mu siatce bitowej, co może mieć miejsce w następujących przypadkach:

a) przy dzieleniu dzielnej o wartości słowa przez dzielnik o wartości bajtów, a wartość dzielnej jest ponad 256 razy większa od wartości dzielnika;

b) przy dzieleniu dywidendy o wartości słowa podwójnego przez dzielnik o wartości słowa, a wartość dywidendy jest ponad 65 536 razy większa od wartości dzielnika;

c) przy dzieleniu dywidendy o wartości poczwórnej słowa przez dzielnik o wartości podwójnego słowa, a wartość dywidendy jest ponad 4 294 967 296 razy większa niż wartość dzielnika.

Podział ze znakiem

Polecenie dzielenia liczb za pomocą znaku to

dzielnik idiv

W przypadku tego polecenia obowiązują wszystkie rozważane postanowienia dotyczące poleceń i podpisanych numerów. Odnotowujemy tylko cechy występowania wyjątku 0, „dzielenie przez zero”, w przypadku liczb ze znakiem. Występuje podczas wykonywania polecenia idiv z jednego z następujących powodów:

1) dzielnik wynosi zero;

2) iloraz nie jest uwzględniony w przeznaczonej dla niego siatce bitów.

To z kolei może się zdarzyć:

1) przy dzieleniu dzielnej o wartości słowa ze znakiem przez dzielnik o wartości bajtu ze znakiem, a wartość dzielnej jest większa niż 128-krotność wartości dzielnika (zatem iloraz nie powinien być poza zakresem od -128 do + 127);

2) przy dzieleniu dywidendy przez wartość podwójnego słowa ze znakiem przez dzielnik przez wartość słowa ze znakiem, a wartość dywidendy jest większa niż 32 768 razy wartość dzielnika (zatem iloraz nie może znajdować się poza przedziałem od - 32 do +768) ;

3) przy dzieleniu dywidendy przez wartość poczwórną ze znakiem przez dzielnik ze znakiem podwójnego słowa, a wartość dywidendy jest większa niż 2 147 483 648 razy wartość dzielnika (zatem iloraz nie może być poza zakresem od -2 147 483 648 do + 2 147 483 647).

Instrukcje pomocnicze dla operacji na liczbach całkowitych

W zestawie instrukcji mikroprocesora znajduje się kilka instrukcji, które mogą ułatwić programowanie algorytmów wykonujących obliczenia arytmetyczne. Mogą pojawić się w nich różne problemy, dla których rozwiązanie twórcy mikroprocesora dostarczyli kilka poleceń.

Polecenia konwersji typu

Co się stanie, jeśli rozmiary operandów biorących udział w operacjach arytmetycznych są różne? Załóżmy na przykład, że w operacji dodawania jeden operand jest słowem, a drugi podwójnym słowem. Powiedziano powyżej, że w operacji dodawania muszą uczestniczyć operandy tego samego formatu. Jeśli liczby są bez znaku, dane wyjściowe są łatwe do znalezienia. W takim przypadku na podstawie oryginalnego operandu można utworzyć nowy (format podwójnego słowa), którego wysokie bity można po prostu wypełnić zerami. Sytuacja jest bardziej skomplikowana dla liczb ze znakiem: jak uwzględnić znak operandu dynamicznie podczas wykonywania programu? Aby rozwiązać takie problemy, zestaw instrukcji mikroprocesora zawiera tak zwane instrukcje konwersji typu. Instrukcje te rozszerzają bajty na słowa, słowa na słowa podwójne, a słowa podwójne na słowa poczwórne (wartości 64-bitowe). Instrukcje konwersji typu są szczególnie przydatne podczas konwersji liczb całkowitych ze znakiem, ponieważ automatycznie wypełniają bity wyższego rzędu nowo skonstruowanego operandu wartościami bitu znaku starego obiektu. Ta operacja daje w wyniku wartości całkowite tego samego znaku i tej samej wielkości co oryginał, ale w dłuższym formacie. Taka transformacja nazywana jest operacją propagacji znaku.

Istnieją dwa rodzaje poleceń konwersji typów.

1. Instrukcje bez operandów. Te polecenia działają ze stałymi rejestrami:

1) cbw (Convert Byte to Word) - polecenie konwersji bajtu (w rejestrze al) na słowo (w rejestrze ah) poprzez rozciągnięcie wartości starszego bitu al na wszystkie bity rejestru ah;

2) cwd (Convert Word to Double) - polecenie konwersji słowa (w rejestrze ax) na słowo podwójne (w rejestrach dx:ax) poprzez rozciągnięcie wartości starszego bitu ax na wszystkie bity rejestru dx;

3) cwde (Convert Word to Double) - polecenie konwersji słowa (w rejestrze ax) na słowo podwójne (w rejestrze eax) poprzez rozłożenie wartości starszego bitu ax na wszystkie bity górnej połowy rejestru eax ;

4) cdq (Convert Double Word to Quarter Word) - polecenie zamiany słowa podwójnego (w rejestrze eax) na słowo poczwórne (w rejestrach edx:eax) poprzez rozłożenie wartości najbardziej znaczącego bitu eax na wszystkie bity rejestru edx.

2. Polecenia movsx i movzx związane z poleceniami przetwarzania ciągów. Te polecenia mają przydatną właściwość w kontekście naszego problemu:

1) movsx operand_1, operand_2 - wysyłaj z propagacją znaku. Rozszerza 8- lub 16-bitową wartość operandu_2, która może być rejestrem lub operandem pamięci, do 16- lub 32-bitowej wartości w jednym z rejestrów, używając wartości bitu znaku do wypełnienia wyższych pozycji operandu_1. Ta instrukcja jest przydatna do przygotowywania podpisanych operandów do operacji arytmetycznych;

2) movzx operand_1, operand_2 - wysyłaj z zerowym rozszerzeniem. Rozszerza 8-bitową lub 16-bitową wartość operand_2 na 16-bitową lub 32-bitową, usuwając (wypełniając) wysokie pozycje operand_2 zerami. Ta instrukcja jest przydatna przy przygotowywaniu operandów bez znaku do arytmetyki.

Inne przydatne polecenia

1. xadd miejsce docelowe, źródło - wymiana i dodawanie.

Polecenie pozwala wykonać kolejno dwie czynności:

1) wymiany przeznaczenia i wartości źródłowych;

2) umieść operand przeznaczenia w miejscu sumy: cel = cel + źródło.

2. argument neg - negacja z uzupełnieniem do dwóch.

Instrukcja odwraca wartość operandu. Fizycznie polecenie wykonuje jedną akcję:

operand = 0 - operand, tj. odejmuje operand od zera.

Można użyć polecenia neg operand:

1) zmienić znak;

2) wykonać odejmowanie od stałej.

Operacje arytmetyczne na liczbach binarno-dziesiętnych

W tej sekcji przyjrzymy się specyfice każdej z czterech podstawowych operacji arytmetycznych dla spakowanych i rozpakowanych liczb BCD.

Słusznie może pojawić się pytanie: po co nam numery BCD? Odpowiedź może brzmieć: numery BCD są potrzebne w aplikacjach biznesowych, czyli tam, gdzie liczby muszą być duże i precyzyjne. Jak już widzieliśmy na przykładzie liczb binarnych, operacje na takich liczbach są dość problematyczne dla języka asemblerowego. Wady korzystania z liczb binarnych obejmują:

1) Wartości w formacie słowa i podwójnego słowa mają ograniczony zakres. Jeśli program jest przeznaczony do pracy w dziedzinie finansów, wówczas ograniczenie kwoty w rublach do 65 536 (za słowo) lub nawet 4 294 967 296 (za podwójne słowo) znacznie zawęzi zakres jego zastosowania;

2) obecność błędów zaokrągleń. Czy możesz sobie wyobrazić program działający gdzieś w banku, który nie bierze pod uwagę wartości salda operując na binarnych liczbach całkowitych i operuje miliardami? Nie chciałbym być autorem takiego programu. Użycie liczb zmiennoprzecinkowych nie uratuje - istnieje tam ten sam problem z zaokrąglaniem;

3) prezentacja dużej ilości wyników w formie symbolicznej (kod ASCII). Programy biznesowe nie tylko wykonują obliczenia; jednym z celów ich wykorzystania jest niezwłoczne dostarczenie informacji użytkownikowi. W tym celu oczywiście informacje muszą być przedstawione w formie symbolicznej. Konwersja liczb z binarnych do ASCII wymaga trochę wysiłku obliczeniowego. Jeszcze trudniej przełożyć liczbę zmiennoprzecinkową na formę symboliczną. Ale jeśli spojrzysz na szesnastkową reprezentację niepakowanej cyfry dziesiętnej i odpowiadającego jej znaku w tabeli ASCII, zobaczysz, że różnią się one o 30 godzin. Dzięki temu konwersja do postaci symbolicznej i odwrotnie jest znacznie łatwiejsza i szybsza.

Prawdopodobnie już zauważyłeś, jak ważne jest opanowanie przynajmniej podstaw działań z liczbami dziesiętnymi. Następnie rozważ cechy wykonywania podstawowych operacji arytmetycznych na liczbach dziesiętnych. Od razu zauważamy, że nie ma oddzielnych poleceń dodawania, odejmowania, mnożenia i dzielenia liczb BCD. Dokonano tego z całkiem zrozumiałych powodów: wymiar takich liczb może być dowolnie duży. Liczby BCD można dodawać i odejmować, zarówno spakowane, jak i rozpakowane, ale tylko niepakowane liczby BCD mogą dzielić i mnożyć. Dlaczego tak jest, zostanie wyjaśnione w dalszej dyskusji.

Arytmetyka na rozpakowanych liczbach BCD

Dodaj rozpakowane numery BCD

Rozważmy dwa przypadki dodawania.

Przykład

Wynik dodawania to nie więcej niż 9

6 = 0000

+

3 = 0000

=

9 = 0000

Nie ma przeniesienia z juniora do starszej tetrady. Wynik jest poprawny.

Przykład

Wynik dodawania jest większy niż 9:

06 = 0000

+

07 = 0000

=

13 = 0000

Nie otrzymaliśmy już numeru BCD. Wynik jest błędny. Prawidłowy wynik w rozpakowanym formacie BCD powinien wynosić 0000 0001 0000 0011 binarnie (lub 13 dziesiętnie).

Po przeanalizowaniu tego problemu podczas dodawania numerów BCD (i podobnych problemów przy wykonywaniu innych operacji arytmetycznych) i możliwych sposobów jego rozwiązania, twórcy mikroprocesorowego systemu poleceń postanowili nie wprowadzać specjalnych poleceń do pracy z liczbami BCD, ale wprowadzić kilka poleceń korygujących .

Celem tych instrukcji jest poprawienie wyniku działania zwykłych instrukcji arytmetycznych dla przypadków, w których argumentami w nich są liczby BCD.

W przypadku odejmowania w przykładzie 10 widać, że uzyskany wynik wymaga korekty. Do poprawienia operacji dodawania dwóch jednocyfrowych numerów BCD bez opakowania w mikroprocesorowym systemie dowodzenia służy specjalne polecenie - aaa (ASCII Adjust for Addition) - poprawka wyniku dodawania dla reprezentacji w postaci symbolicznej.

Ta instrukcja nie ma operandów. Działa niejawnie tylko z rejestrem al i analizuje wartość jego niższej tetrady:

1) jeżeli ta wartość jest mniejsza niż 9, to flaga cf jest resetowana do XNUMX i następuje przejście do następnej instrukcji;

2) jeżeli wartość ta jest większa niż 9, to wykonywane są następujące czynności:

a) 6 dodaje się do zawartości dolnej czwórki (ale nie do zawartości całego rejestru!) Zatem wartość wyniku dziesiętnego jest korygowana we właściwym kierunku;

b) flaga cf jest ustawiona na 1, tym samym ustalając transfer do najbardziej znaczącego bitu, aby można go było uwzględnić w kolejnych działaniach.

Czyli w przykładzie 10 zakładając, że wartość sumy 0000 1101 jest w al, po instrukcji aaa rejestr będzie miał 1101 + 0110 = 0011, czyli binarne 0000 0011 lub dziesiętne 3, a flaga cf będzie ustawiona na 1, tzn. przelew został zapisany w mikroprocesorze. Następnie programista będzie musiał użyć instrukcji dodawania adc, która uwzględni przeniesienie z poprzedniego bitu.

Odejmowanie nieopakowanych numerów BCD

Sytuacja tutaj jest dość podobna do dodawania. Rozważmy te same przypadki.

Przykład

Wynik odejmowania nie jest większy niż 9:

6 = 0000

-

3 = 0000

=

3 = 0000

Jak widać, nie ma pożyczki od starszego notebooka. Wynik jest poprawny i nie wymaga korekty.

Przykład

Wynik odejmowania jest większy niż 9:

6 = 0000

-

7 = 0000

=

-1 = 1111 1111

Odejmowanie odbywa się zgodnie z zasadami arytmetyki binarnej. Dlatego wynik nie jest numerem BCD.

Prawidłowy wynik w rozpakowanym formacie BCD powinien wynosić 9 (0000 1001 w formacie binarnym). W tym przypadku zakłada się pożyczkę od najbardziej znaczącej cyfry, tak jak przy normalnym poleceniu odejmowania, czyli w przypadku liczb BCD należy faktycznie wykonać odejmowanie 16 - 7. Widać więc, że tak jak w przypadku w przypadku dodawania należy skorygować wynik odejmowania. Do tego służy specjalne polecenie - aas (ASCII Adjust for Substration) - korekta wyniku odejmowania dla reprezentacji w postaci symbolicznej.

Instrukcja aas również nie ma operandów i działa na rejestrze al, analizując swoją tetradę najmniejszego rzędu w następujący sposób:

1) jeżeli jego wartość jest mniejsza niż 9, to flaga cf jest resetowana do 0 i sterowanie jest przekazywane do następnego polecenia;

2) jeśli wartość tetrady w al jest większa niż 9, to polecenie aas wykonuje następujące czynności:

a) odejmuje 6 od zawartości dolnej czwórki rejestru al (uwaga - nie od zawartości całego rejestru);

b) resetuje górną tetradę rejestru al;

c) ustawia flagę cf na 1, tym samym ustalając wyimaginowaną pożyczkę wysokiego rzędu.

Jasne jest, że polecenie aas jest używane w połączeniu z podstawowymi poleceniami odejmowania sub i sbb. W takim przypadku sensowne jest użycie polecenia sub tylko raz, przy odejmowaniu najniższych cyfr operandów należy użyć polecenia sbb, które uwzględni ewentualną pożyczkę z najwyższego rzędu.

Mnożenie rozpakowanych numerów BCD

Na przykładzie dodawania i odejmowania nierozpakowanych liczb stało się jasne, że nie ma standardowych algorytmów wykonywania tych operacji na liczbach BCD, a programista musi sam, w oparciu o wymagania dla swojego programu, zaimplementować te operacje.

Realizacja dwóch pozostałych operacji - mnożenia i dzielenia - jest jeszcze bardziej skomplikowana. W zestawie instrukcji mikroprocesorowych są tylko środki do produkcji mnożenia i dzielenia jednocyfrowych niepakowanych numerów BCD.

Aby pomnożyć liczby o dowolnym wymiarze, należy samodzielnie zaimplementować proces mnożenia, oparty na jakimś algorytmie mnożenia, np. „w kolumnie”.

Aby pomnożyć dwa jednocyfrowe liczby BCD, musisz:

1) umieścić jeden z czynników w rejestrze AL (zgodnie z instrukcją mul);

2) umieścić drugi operand w rejestrze lub pamięci, przydzielając bajt;

3) pomnóż współczynniki poleceniem mul (wynik, zgodnie z oczekiwaniami, będzie w ah);

4) wynik oczywiście będzie w kodzie binarnym, więc należy go poprawić.

Do poprawienia wyniku po mnożeniu służy specjalne polecenie - aam (ASCII Adjust for Multiplication) - korekta wyniku mnożenia dla reprezentacji w postaci symbolicznej.

Nie ma operandów i działa na rejestrze AX w następujący sposób:

1) dzieli al przez 10;

2) wynik dzielenia zapisuje się następująco: iloraz w al, reszta w ah. W rezultacie po wykonaniu instrukcji aam rejestry AL i ah zawierają poprawne cyfry BCD iloczynu dwóch cyfr.

Zanim zakończymy dyskusję na temat polecenia aam, musimy zwrócić uwagę na jeszcze jedno jego zastosowanie. Za pomocą tego polecenia można zamienić liczbę binarną w rejestrze AL na rozpakowaną liczbę BCD, która zostanie umieszczona w rejestrze ah: najbardziej znacząca cyfra wyniku to ah, najmniej znacząca cyfra to al. Oczywiste jest, że liczba binarna musi mieścić się w zakresie 0... 99.

Podział nieopakowanych numerów BCD

Proces wykonywania operacji dzielenia dwóch rozpakowanych numerów BCD różni się nieco od innych operacji rozważanych wcześniej z nimi. Tutaj również wymagane są działania korygujące, ale muszą być one wykonane przed operacją główną, która bezpośrednio dzieli jeden numer BCD przez inny numer BCD. Najpierw w rejestrze ah, musisz uzyskać dwie rozpakowane cyfry BCD dywidendy. To sprawia, że ​​programista jest dla niego w pewien sposób wygodny. Następnie należy wydać polecenie aad - aad (ASCII Adjust for Division) - korekta podziału dla reprezentacji symbolicznej.

Instrukcja nie posiada operandów i konwertuje dwucyfrowy rozpakowany numer BCD w rejestrze ax na liczbę binarną. Ta liczba binarna będzie później pełnić rolę dywidendy w operacji podziału. Oprócz konwersji polecenie aad umieszcza wynikową liczbę binarną w rejestrze AL. Dywidenda będzie oczywiście liczbą binarną z zakresu 0... 99.

Algorytm, za pomocą którego polecenie aad wykonuje tę konwersję, jest następujący:

1) pomnożyć najwyższą cyfrę oryginalnego numeru BCD w ah (zawartość AH) przez 10;

2) wykonać dodawanie AH + AL, którego wynik (liczba binarna) wpisywany jest do AL;

3) zresetować zawartość AN.

Następnie programista musi wydać normalne polecenie dzielenia div, aby wykonać dzielenie zawartości ax przez pojedynczą cyfrę BCD umieszczoną w rejestrze bajtowym lub komórce pamięci bajtowej.

Podobnie jak aash, polecenie aad może być również użyte do konwersji rozpakowanych liczb BCD z zakresu 0... 99 na ich binarny odpowiednik.

Aby podzielić liczby o większej pojemności, a także w przypadku mnożenia, trzeba zaimplementować własny algorytm, na przykład „w kolumnie”, lub znaleźć bardziej optymalny sposób.

Arytmetyka na spakowanych numerach BCD

Jak wspomniano powyżej, spakowane numery BCD mogą być tylko dodawane i odejmowane. Aby wykonać na nich inne działania, należy je dodatkowo przekonwertować na format rozpakowany lub na reprezentację binarną. Ze względu na to, że spakowane numery BCD nie wzbudzają dużego zainteresowania, rozważymy je pokrótce.

Dodawanie spakowanych numerów BCD

Najpierw przejdźmy do sedna problemu i spróbujmy dodać dwa dwucyfrowe spakowane liczby BCD. Przykład dodawania spakowanych numerów BCD:

67 = 01100111

+

75 = 01110101

=

142 = 1101 1100 = 220

Jak widać, wynik binarny to 1101 1100 (lub 220 dziesiętnie), co jest niepoprawne. Dzieje się tak, ponieważ mikroprocesor nie jest świadomy istnienia liczb BCD i dodaje je zgodnie z zasadami dodawania liczb binarnych. W rzeczywistości wynik w BCD powinien wynosić 0001 0100 0010 (lub 142 w postaci dziesiętnej).

Widać, że tak jak dla niepakowanych liczb BCD, tak dla upakowanych liczb BCD istnieje potrzeba jakiegoś korekty wyników operacji arytmetycznych.

Mikroprocesor przewiduje to polecenie daa - daa (Decimal Adjust for Addition) - korekta wyniku dodawania do prezentacji w postaci dziesiętnej.

Rozkaz daa konwertuje zawartość rejestru al na dwie upakowane cyfry dziesiętne zgodnie z algorytmem podanym w opisie rozkazu daa.Wynikowa jednostka (jeśli wynik dodawania jest większy niż 99) jest zapisywana we fladze cf, uwzględniając tym samym przeniesienie do najbardziej znaczącego bitu.

Odejmowanie spakowanych liczb BCD

Podobnie do dodawania, mikroprocesor traktuje spakowane liczby BCD jako binarne i odpowiednio odejmuje liczby BCD jako binarne.

Przykład

Odejmowanie spakowanych liczb BCD.

Odejmijmy 67-75. Ponieważ mikroprocesor wykonuje odejmowanie na drodze dodawania, będziemy postępować tak:

67 = 01100111

+

-75=10110101

=

-8 = 0001 1100 = 28

Jak widać, wynik to 28 w postaci dziesiętnej, co jest absurdem. W BCD wynik powinien wynosić 0000 1000 (lub 8 dziesiętnie).

Programując odejmowanie spakowanych liczb BCD, programista, a także przy odejmowaniu nierozpakowanych liczb BCD, musi sam kontrolować znak. Odbywa się to za pomocą flagi CF, która naprawia pożyczkę wysokiego rzędu.

Samo odejmowanie liczb BCD jest wykonywane za pomocą prostego polecenia odejmowania sub lub sbb. Korekta wyniku wykonywana jest poleceniem das - das (Decimal Adjust for Subtraction) - korekta wyniku odejmowania dla reprezentacji w postaci dziesiętnej.

Polecenie das konwertuje zawartość rejestru AL na dwie upakowane cyfry dziesiętne zgodnie z algorytmem podanym w opisie polecenia das.

WYKŁAD nr 19. Polecenia przekazania kontroli

1. Polecenia logiczne

Wraz ze środkami obliczeń arytmetycznych mikroprocesorowy system dowodzenia posiada również środki logicznej konwersji danych. Za pomocą logicznych środków takie przekształcenia danych, które opierają się na zasadach logiki formalnej.

Logika formalna działa na poziomie twierdzeń prawdziwych i fałszywych. Dla mikroprocesora oznacza to zwykle odpowiednio 1 i 0. W przypadku komputera język zer i jedynek jest natywny, ale minimalną jednostką danych, z którą działają instrukcje maszynowe, jest bajt. Jednak na poziomie systemu często konieczne jest działanie na najniższym możliwym poziomie, na poziomie bitowym.

Ryż. 29. Sposoby logicznego przetwarzania danych

Środki logicznej transformacji danych obejmują polecenia logiczne i operacje logiczne. Operand instrukcji asemblera może być ogólnie wyrażeniem, które z kolei jest kombinacją operatorów i operandów. Wśród tych operatorów mogą znajdować się operatory, które implementują operacje logiczne na obiektach wyrażeń.

Zanim szczegółowo omówimy te narzędzia, zastanówmy się, jakie są same dane logiczne i jakie operacje są na nich wykonywane.

Dane logiczne

Teoretyczną podstawą logicznego przetwarzania danych jest logika formalna. Istnieje kilka systemów logiki. Jednym z najbardziej znanych jest rachunek zdań. Zdanie to każde stwierdzenie, które można uznać za prawdziwe lub fałszywe.

Rachunek zdań to zbiór reguł używanych do określenia prawdziwości lub fałszu pewnej kombinacji zdań.

Rachunek zdań jest bardzo harmonijnie połączony z zasadami działania komputera i podstawowymi metodami jego programowania. Wszystkie elementy sprzętowe komputera są zbudowane na układach logicznych. System reprezentacji informacji w komputerze na najniższym poziomie oparty jest na pojęciu bitu. Trochę, mając tylko dwa stany (0 (fałsz) i 1 (prawda)), naturalnie pasuje do rachunku zdań.

Zgodnie z teorią na instrukcjach (na bitach) można wykonać następujące operacje logiczne.

1. Negacja (logiczne NIE) - operacja logiczna na jednym operandzie, której wynikiem jest odwrotność wartości oryginalnego operandu.

Ta operacja jest jednoznacznie scharakteryzowana przez poniższą tabelę prawdy (Tabela 12).

Tabela 12. Tabela prawdy dla logicznej negacji

2. Dodawanie logiczne (logiczne LUB) - operacja logiczna na dwóch operandach, której wynikiem jest „prawda” (1), jeśli jeden lub oba operandy są prawdziwe (1) i „fałsz” (0), jeśli oba operandy są fałszywe (0).

Operację tę opisuje poniższa tabela prawdy (Tabela 13).

Tabela 13. Tabela prawdy dla logicznej inkluzywnej OR

3. Mnożenie logiczne (logiczne AND) - operacja logiczna na dwóch operandach, której wynik jest prawdziwy (1) tylko wtedy, gdy oba operandy są prawdziwe (1). We wszystkich innych przypadkach wartość operacji to „fałsz” (0).

Operację tę opisuje poniższa tabela prawdy (Tabela 14).

Tabela 14. Tabela logiki i prawdy

4. Logiczne wykluczające dodawanie (logiczne wykluczające OR) - operacja logiczna na dwóch operandach, której wynikiem jest „prawda” (1), jeśli tylko jeden z dwóch operandów jest prawdziwy (1), a fałszywy (0), jeśli oba operandy są albo fałszywe (0), albo prawdziwe (1). Operację tę opisuje poniższa tabela prawdy (Tabela 15).

Tabela 15. Tabela prawdy dla logicznego XOR

Zestaw instrukcji mikroprocesora zawiera pięć instrukcji obsługujących te operacje. Instrukcje te wykonują operacje logiczne na bitach operandów. Oczywiście wymiary operandów muszą być takie same. Na przykład, jeśli wymiar operandów jest równy słowu (16 bitów), to operacja logiczna jest wykonywana najpierw na bitach zerowych operandów, a jej wynik jest zapisywany w miejscu bitu 0 wyniku. Następnie polecenie powtarza te czynności sekwencyjnie na wszystkich bitach od pierwszego do piętnastego.

Polecenia logiczne

System poleceń mikroprocesora posiada następujący zestaw poleceń obsługujących pracę z danymi logicznymi:

1) i operand_1, operand_2 - operacja mnożenia logicznego. Polecenie wykonuje bitową operację logiczną AND (koniunkcję) na bitach operandów operand_1 i operand_2. Wynik jest zapisywany w miejscu operand_1;

2) og operand_1, operand_2 - operacja dodawania logicznego. Polecenie wykonuje bitową operację logiczną OR (rozłączenie) na bitach operandów operand_1 i operand_2. Wynik jest zapisywany w miejscu operand_1;

3) xor operand_1, operand_2 - operacja logicznego dodawania wykluczającego. Polecenie wykonuje bitową logiczną operację XOR na bitach operandów operand_1 i operand_2. Wynik jest zapisywany w miejscu operandu;

4) test operand_1, operand_2 - operacja "testowa" (przy użyciu metody mnożenia logicznego). Polecenie wykonuje bitową operację logiczną AND na bitach operandów operand_1 i operand_2. Stan operandów pozostaje taki sam, zmieniane są tylko flagi zf, sf i pf, co umożliwia analizę stanu poszczególnych bitów operandu bez zmiany ich stanu;

5) nieoperand - operacja logicznej negacji. Polecenie wykonuje bitową inwersję (zastępując wartość przeciwną) każdego bitu operandu. Wynik jest zapisywany w miejscu operandu.

Aby zrozumieć rolę poleceń logicznych w zestawie instrukcji mikroprocesora, bardzo ważne jest zrozumienie obszarów ich zastosowania i typowych metod ich wykorzystania w programowaniu.

Za pomocą poleceń logicznych możliwe jest wybranie poszczególnych bitów w operandzie w celu ich ustawienia, zresetowania, odwrócenia lub po prostu sprawdzenia określonej wartości.

Aby zorganizować taką pracę z bitami, operand_2 zwykle pełni rolę maski. Za pomocą bitów tej maski ustawionych w bicie 1, określane są bity argumentu_1 niezbędne dla określonej operacji. Pokażmy, jakie polecenia logiczne można w tym celu wykorzystać:

1) aby ustawić pewne cyfry (bity) na 1, używa się polecenia og operand_1, operand_2.

W tej instrukcji operand_2, który działa jak maska, musi zawierać 1 bit w miejsce tych bitów, które powinny być ustawione na 1 w operand_XNUMX;

2) aby zresetować pewne cyfry (bity) do 0, używa się polecenia i operandu_1, operandu_2.

W tej instrukcji operand_2, który działa jak maska, musi zawierać bity zerowe zamiast tych, które muszą być ustawione na 0 w operand_1;

3) komenda xor operand_1, operand_2 jest stosowany:

a) dowiedzieć się, które bity w operandzie_1 i operandzie różnią się;

b) odwrócić stan określonych bitów w operand_1.

Interesujące nas bity maski (operand_2) podczas wykonywania polecenia xor muszą być pojedyncze, reszta musi wynosić zero;

Polecenie testowe operand_1, operand_2 (sprawdzenie operandu_1) służy do sprawdzania statusu określonych bitów.

Sprawdzane bity operandu_1 w masce (operand_2) muszą być ustawione na jeden. Algorytm polecenia test jest podobny do algorytmu polecenia i, ale nie zmienia wartości operand_1. Wynikiem polecenia jest ustawienie wartości flagi zerowej zf:

1) jeśli zf = 0, to w wyniku mnożenia logicznego otrzymuje się wynik zerowy, tj. jeden bit jednostkowy maski, który nie pasuje do odpowiedniego bitu jednostkowego argumentu;

2) jeśli zf = 1, to w wyniku mnożenia logicznego uzyskuje się niezerowy wynik, tj. co najmniej jeden jednostkowy bit maski pokrywa się z odpowiadającym jednostkowym bitem argumentu_1.

Aby zareagować na wynik polecenia testowego, zaleca się użycie polecenia skoku jnz label (Skocz, jeśli nie zero) - skok, jeśli flaga zero zf jest niezerowa, lub polecenia odwrotnego działania - etykieta jz (Skocz, jeśli zero ) - skok jeśli flaga zero zf = 0.

Poniższe dwie komendy wyszukują pierwszy bit operandu ustawiony na 1. Wyszukiwanie można przeprowadzić zarówno od początku, jak i od końca operandu:

1) bsf operand_1, operand_2 (Bit Scanning Forward) - skanowanie bitów do przodu. Instrukcja przeszukuje (skanuje) bity operandu_2 od najmniej znaczącego do najbardziej znaczącego (od bitu 0 do najbardziej znaczącego bitu) w poszukiwaniu pierwszego bitu ustawionego na 1. Jeśli taki zostanie znaleziony, operand_1 jest wypełniany liczbą ten bit jako wartość całkowitą. Jeśli wszystkie bity operandu_2 wynoszą 0, wtedy flaga zero zf jest ustawiana na 1, w przeciwnym razie flaga zf jest resetowana do 0;

2) bsr operand_1, operand_2 (Bit Scanning Reset) - skanuj bity w odwrotnej kolejności. Instrukcja przeszukuje (skanuje) bity operandu_2 od najbardziej znaczącego do najmniej znaczącego (od najbardziej znaczącego bitu do bitu 0) w poszukiwaniu pierwszego bitu ustawionego na 1. Jeśli taki zostanie znaleziony, operand_1 jest wypełniany liczbą ten bit jako wartość całkowitą. Ważne jest, aby pozycja pierwszego bitu jednostki po lewej stronie była nadal liczona względem bitu 0. Jeśli wszystkie bity operandu_2 są równe 0, wtedy flaga zero zf jest ustawiona na 1, w przeciwnym razie flaga zf jest resetowana do 0.

W najnowszych modelach mikroprocesorów Intela w grupie instrukcji logicznych pojawiło się jeszcze kilka instrukcji, które pozwalają na dostęp do jednego konkretnego bitu operandu. Operand może znajdować się w pamięci lub w rejestrze ogólnym. Pozycja bitu jest określona przez przesunięcie bitu względem najmniej znaczącego bitu operandu. Wartość przesunięcia może być określona jako wartość bezpośrednia lub zawarta w rejestrze ogólnego przeznaczenia. Możesz użyć wyników poleceń bsr i bsf jako wartości przesunięcia. Wszystkie instrukcje przypisują wartość wybranego bitu do flagi CE.

1) operand bt, bit_offset (Bit Test) - test bitów. Instrukcja przekazuje wartość bitową do flagi cf;

2) operand bts, offset_bit (Bit Test and Set) - sprawdzanie i ustawianie bitu. Instrukcja przekazuje wartość bitu do flagi CF, a następnie ustawia bit do sprawdzenia na 1;

3) operand btr, bit_offset (Bit Test and Reset) - sprawdzanie i resetowanie bitu. Instrukcja przekazuje wartość bitu do flagi CF, a następnie ustawia ten bit na 0;

4) operand btc, offset_bit (Bit Test and Convert) - sprawdzanie i odwracanie bitu. Instrukcja zawija wartość bitu we flagę cf, a następnie odwraca wartość tego bitu.

Polecenia przesunięcia

Instrukcje z tej grupy również zapewniają manipulację poszczególnymi bitami operandów, ale w inny sposób niż instrukcje logiczne omówione powyżej.

Wszystkie instrukcje przesunięcia przesuwają bity w polu operandu w lewo lub w prawo, w zależności od kodu operacji. Wszystkie instrukcje zmiany mają tę samą strukturę - operand kopiowania, liczba_przesunięć.

Liczba bitów do przesunięcia - counter_shifts - znajduje się w miejscu drugiego argumentu i można ją ustawić na dwa sposoby:

1) statycznie, co polega na ustaleniu stałej wartości za pomocą operandu bezpośredniego;

2) dynamicznie, co oznacza wpisanie wartości licznika przesunięcia do rejestru cl przed wykonaniem instrukcji przesunięcia.

Na podstawie wymiaru rejestru cl jasne jest, że wartość licznika przesunięcia może wynosić od 0 do 255. Ale w rzeczywistości nie jest to do końca prawdą. Dla celów optymalizacji mikroprocesor akceptuje tylko wartość pięciu najmniej znaczących bitów licznika, czyli wartość mieści się w zakresie od 0 do 31.

Wszystkie instrukcje przesunięcia ustawiają flagę przeniesienia, por.

Gdy bity przesuwają się poza operand, najpierw trafiają w flagę przeniesienia, ustawiając ją na wartość następnego bitu poza operandem. To, gdzie ten bit idzie dalej, zależy od typu instrukcji przesunięcia i algorytmu programu.

Polecenia Shift można podzielić na dwa typy zgodnie z zasadą działania:

1) polecenia przesunięcia liniowego;

2) polecenia przesunięcia cyklicznego.

Polecenia przesunięcia liniowego

Polecenia tego typu obejmują polecenia, które przesuwają się zgodnie z następującym algorytmem:

1) następny wciśnięty bit ustawia flagę CF;

2) bit wpisany do operandu z drugiego końca ma wartość 0;

3) gdy następny bit jest przesunięty, przechodzi do flagi CF, podczas gdy wartość poprzedniego przesuniętego bitu jest tracona! Polecenia przesunięcia liniowego dzielą się na dwa podtypy:

1) logiczne polecenia przesunięcia liniowego;

2) instrukcje arytmetycznego przesunięcia liniowego.

Logiczne polecenia przesunięcia liniowego obejmują:

1) operand shl, counter_shifts (Shift Logical Left) - logiczne przesunięcie w lewo. Zawartość operandu jest przesuwana w lewo o liczbę bitów określoną przez shift_count. Po prawej stronie (na pozycji najmniej znaczącego bitu) wpisywane są zera;

2) operand shr, shift_count (Shift Logical Right) - logiczne przesunięcie w prawo. Zawartość operandu jest przesuwana w prawo o liczbę bitów określoną przez shift_count. Po lewej stronie (w pozycji najbardziej znaczącego bitu znaku) wpisywane są zera.

Rysunek 30 pokazuje, jak działają te polecenia.

Ryż. 30. Schemat działania poleceń liniowego przesunięcia logicznego

Instrukcje arytmetycznego przesunięcia liniowego różnią się od instrukcji przesunięcia logicznego tym, że operują na bicie znaku operandu w specjalny sposób.

1) operand sal, shift_counter (Shift Arithmetic Left) - przesunięcie arytmetyczne w lewo. Zawartość operandu jest przesuwana w lewo o liczbę bitów określoną przez shift_count. Po prawej stronie (w miejscu najmniej znaczącego bitu) wpisywane są zera. Instrukcja sal nie zachowuje znaku, ale ustawia flagę z / w przypadku zmiany znaku o kolejny bit wyprzedzenia. W przeciwnym razie polecenie sal jest dokładnie takie samo, jak polecenie shl;

2) operand sar, shift_count (Shift Arithmetic Right) - arytmetyczne przesunięcie w prawo. Zawartość operandu jest przesuwana w prawo o liczbę bitów określoną przez shift_count. Zera są wstawiane do operandu po lewej stronie. Polecenie sar zachowuje znak, przywracając go po każdym przesunięciu bitu.

Rysunek 31 pokazuje, jak działają instrukcje liniowego przesunięcia arytmetycznego.

Ryż. 31. Schemat działania poleceń liniowego przesunięcia arytmetycznego

Polecenia obracania

Instrukcje przesunięcia cyklicznego zawierają instrukcje, które przechowują wartości przesuniętych bitów. Istnieją dwa rodzaje instrukcji przesunięcia cyklicznego:

1) proste polecenia przesunięcia cyklicznego;

2) polecenia przesunięcia cyklicznego za pomocą flagi przeniesienia, zob.

Proste polecenia przesunięcia cyklicznego obejmują:

1) operand rol, licznik_przesunięcia (Rotate Left) - cykliczne przesunięcie w lewo. Zawartość operandu jest przesuwana w lewo o liczbę bitów określoną przez operand shift_count. Bity przesunięte w lewo są zapisywane do tego samego operandu od prawej;

2) operand gog, counter_shifts (Obróć w prawo) - cykliczne przesunięcie w prawo. Zawartość operandu jest przesuwana w prawo o liczbę bitów określoną przez operand shift_count. Bity przesunięte w prawo są zapisywane do tego samego operandu po lewej stronie.

Ryż. 32. Schemat działania poleceń prostego przesunięcia cyklicznego

Jak widać z rysunku 32, instrukcje prostego przesunięcia cyklicznego w trakcie swojej pracy wykonują jedną użyteczną akcję, a mianowicie: bit przesunięty cyklicznie jest nie tylko wpychany do operandu z drugiego końca, ale jednocześnie jego value staje się wartością flagi CE.

Polecenia przesunięcia cyklicznego przez flagę przeniesienia CF różnią się od prostych poleceń przesunięcia cyklicznego tym, że przesunięty bit nie wchodzi natychmiast do operandu z drugiego końca, ale jest najpierw zapisywany do flagi przeniesienia CE. Tylko następne wykonanie tego polecenia przesunięcia ( pod warunkiem, że jest wykonywany w pętli) powoduje umieszczenie wcześniej zaawansowanego bitu na drugim końcu operandu (rys. 33).

Poniższe są związane z poleceniami przesunięcia cyklicznego za pośrednictwem flagi przeniesienia:

1) operand rcl, shift_count (Rotate through Carry Left) - cykliczne przesunięcie w lewo przez carry.

Zawartość operandu jest przesuwana w lewo o liczbę bitów określoną przez operand shift_count. Przesunięte bity z kolei stają się wartością flagi przeniesienia cf.

2) rsg operand, shift_count (Obrót przez Carry Right) - cykliczne przesunięcie w prawo przez carry.

Zawartość operandu jest przesuwana w prawo o liczbę bitów określoną przez operand shift_count. Przesunięte bity z kolei stają się wartością flagi przeniesienia CF.

Ryż. 33. Obrót instrukcji za pomocą Carry Flag CF

Figura 33 pokazuje, że przy przechodzeniu przez flagę przeniesienia pojawia się element pośredni, za pomocą którego w szczególności można zastąpić cyklicznie przesuwane bity, w szczególności niezgodność sekwencji bitów.

Odtąd niezgodność sekwencji bitowej oznacza czynność, która pozwala w pewien sposób zlokalizować i wyodrębnić niezbędne sekcje tej sekwencji i zapisać je w innym miejscu.

Dodatkowe polecenia zmiany biegów

System poleceń najnowszych modeli mikroprocesorów Intel, począwszy od i80386, zawiera dodatkowe polecenia zmiany biegów, które rozszerzają możliwości, o których mówiliśmy wcześniej. Oto polecenia zmiany biegów o podwójnej precyzji:

1) shld operand_1, operand_2, shift_counter - przesunięcie w lewo z podwójną precyzją. Polecenie shld dokonuje zamiany poprzez przesunięcie bitów operand_1 w lewo, wypełniając jego bity po prawej stronie wartościami bitów przesuniętych z operand_2 zgodnie ze schematem na ryc. 34. Liczbę bitów do przesunięcia określa wartość licznika_przesunięcia, która może mieścić się w przedziale 0...31. Wartość ta może być podana jako bezpośredni operand lub zawarta w rejestrze cl. Wartość operand_2 nie ulega zmianie.

Ryż. 34. Schemat polecenia shld

2) shrd operand_1, operand_2, shift_counter - przesunięcie w prawo o podwójnej precyzji. Instrukcja dokonuje zamiany poprzez przesunięcie bitów operandu_1 w prawo, wypełniając jego bity po lewej stronie wartościami bitów przesuniętych z operandu_2 zgodnie ze schematem na rysunku 35. Liczba bitów do przesunięcia wynosi określana przez wartość licznika_przesunięcia, która może mieścić się w zakresie 0... 31. Wartość ta może być określona przez bezpośredni operand lub zawarta w rejestrze cl. Wartość operand_2 nie ulega zmianie.

Ryż. 35. Schemat polecenia shrd

Jak zauważyliśmy, polecenia shld i shrd przesuwają się do 32 bitów, ale ze względu na specyfikę określania operandów i algorytmu działania, polecenia te mogą być używane do pracy z polami o długości do 64 bitów.

2. Polecenia transferu sterowania

Zapoznaliśmy się z niektórymi poleceniami, z których powstają liniowe sekcje programu. Każdy z nich na ogół wykonuje jakąś konwersję lub transfer danych, po czym mikroprocesor przekazuje sterowanie do następnej instrukcji. Jednak bardzo niewiele programów działa w tak spójny sposób. Zazwyczaj w programie są miejsca, w których należy podjąć decyzję, która instrukcja zostanie wykonana jako następna. Takim rozwiązaniem może być:

1) bezwarunkowe - w tym momencie konieczne jest przeniesienie kontroli nie na następne polecenie, ale na inne, które znajduje się w pewnej odległości od bieżącego polecenia;

2) warunkowe - decyzja o tym, które polecenie zostanie wykonane w następnej kolejności, podejmowana jest na podstawie analizy niektórych warunków lub danych.

Program to sekwencja poleceń i danych, które zajmują określoną ilość miejsca w pamięci RAM. Ta przestrzeń pamięci może być ciągła lub składać się z wielu fragmentów.

Którą instrukcję programu należy wykonać następnie, mikroprocesor dowiaduje się z zawartości cs: (e) pary rejestrów ip:

1) cs - rejestr segmentu kodu, który zawiera adres fizyczny (bazowy) bieżącego segmentu kodu;

2) eip/ip - rejestr wskaźnika instrukcji, który zawiera wartość reprezentującą przesunięcie w pamięci następnej instrukcji do wykonania względem początku bieżącego segmentu kodu.

To, który konkretny rejestr zostanie użyty, zależy od ustawionego trybu adresowania use16 lub use32. Jeśli podano use 16, to używane jest ip, jeśli use32, to używane jest eip.

W ten sposób instrukcje transferu sterowania zmieniają zawartość rejestrów cs i eip / ip, w wyniku czego mikroprocesor wybiera do wykonania nie następną w kolejności instrukcję programu, ale instrukcję w jakiejś innej sekcji programu. Potok wewnątrz mikroprocesora jest resetowany.

Zgodnie z zasadą działania polecenia mikroprocesora zapewniające organizację przejść w programie można podzielić na 3 grupy:

1. Bezwarunkowe przekazanie poleceń sterujących:

1) bezwarunkowe polecenie oddziału;

2) polecenie wywołania procedury i powrotu z procedury;

3) polecenie wywołania przerwań programowych i powrotu z przerwań programowych.

2. Polecenia warunkowego przekazania kontroli:

1) komendy skoku przez wynik komendy porównania p;

2) polecenia przejścia zgodnie ze stanem określonej flagi;

3) instrukcje przeskakiwania przez zawartość rejestru esx/cx.

3. Polecenia sterowania cyklem:

1) polecenie zorganizowania cyklu z licznikiem ехх/сх;

2) polecenie zorganizowania cyklu z licznikiem ех/сх z możliwością wcześniejszego wyjścia z cyklu dodatkowym warunkiem.

Bezwarunkowe skoki

Poprzednia dyskusja ujawniła pewne szczegóły mechanizmu przejścia. Instrukcje skoku modyfikują rejestr wskaźnika instrukcji eip/ip i ewentualnie rejestr segmentu kodu cs. Co dokładnie należy zmodyfikować, zależy od:

1) o rodzaju argumentu w bezwarunkowej instrukcji oddziałowej (bliski lub daleki);

2) od określenia modyfikatora przed adresem skoku (w instrukcji skoku); w tym przypadku sam adres skoku może znajdować się albo bezpośrednio w instrukcji (skok bezpośredni), albo w rejestrze lub komórce pamięci (skok pośredni).

Modyfikator może przyjmować następujące wartości:

1) near ptr - bezpośrednie przejście do etykiety wewnątrz bieżącego segmentu kodu. Modyfikowany jest tylko rejestr eip/ip (w zależności od określonego typu segmentu kodu use16 lub use32) na podstawie adresu (etykiety) podanego w poleceniu lub wyrażeniu wykorzystującym symbol wyodrębniania wartości - $;

2) far ptr - bezpośrednie przejście do etykiety w innym segmencie kodu. Adres skoku jest określony jako bezpośredni operand lub adres (etykieta) i składa się z 16-bitowego selektora i 16/32-bitowego przesunięcia, które są ładowane odpowiednio do rejestrów cs i ip/eip;

3) słowo ptr - pośrednie przejście do etykiety wewnątrz bieżącego segmentu kodu. Modyfikowane jest tylko eip/ip (przez wartość przesunięcia z pamięci pod adresem podanym w poleceniu lub z rejestru). Rozmiar przesunięcia 16 lub 32 bity;

4) dword ptr - pośrednie przejście do etykiety w innym segmencie kodu. Oba rejestry - cs i eip / ip - są modyfikowane (o wartość z pamięci - i tylko z pamięci, z rejestru). Pierwsze słowo/dword tego adresu reprezentuje przesunięcie i jest ładowane do ip/eip; drugie/trzecie słowo jest ładowane do cs. jmp instrukcja skoku bezwarunkowego

Składnia polecenia skoku bezwarunkowego to jmp [modyfikator] adres_skoku - skok bezwarunkowy bez zapisywania informacji o punkcie powrotu.

Jump_address to adres w postaci etykiety lub adres obszaru pamięci, w którym znajduje się wskaźnik skoku.

W sumie w systemie instrukcji mikroprocesorowych istnieje kilka kodów instrukcji maszynowych dla skoku bezwarunkowego jmp.

Ich różnice są określane przez odległość przejścia i sposób określenia adresu docelowego. Odległość skoku jest określona przez położenie operandu jump_address. Ten adres może znajdować się w bieżącym segmencie kodu lub w jakimś innym segmencie. W pierwszym przypadku przejście nazywa się wewnątrzsegmentowym lub bliskim, w drugim - międzysegmentowym lub odległym. Skok wewnątrz segmentu zakłada, że ​​zmieniana jest tylko zawartość rejestru eip/ip.

Istnieją trzy opcje użycia komendy jmp wewnątrz segmentów:

1) prosty krótki;

2) prosto;

3) pośrednie.

Procedury

Język asemblerowy posiada kilka narzędzi, które rozwiązują problem duplikowania sekcji kodu. Obejmują one:

1) mechanizm postępowania;

2) asembler makr;

3) mechanizm przerwania.

Procedura, często nazywana również podprogramem, jest podstawową jednostką funkcjonalną rozłożenia (podziału na kilka części) zadania. Procedura jest grupą poleceń do rozwiązania konkretnego podzadania i ma możliwość przejęcia kontroli z punktu, w którym zadanie jest wywołane na wyższym poziomie i zwrócenia kontroli do tego punktu.

W najprostszym przypadku program może składać się z jednej procedury. Innymi słowy, procedurę można zdefiniować jako dobrze sformułowany zestaw poleceń, który po jednokrotnym opisaniu można w razie potrzeby wywołać w dowolnym miejscu programu.

Aby opisać sekwencję poleceń jako procedurę w języku asemblerowym, używane są dwie dyrektywy: PROC i ENDP.

Składnia opisu procedury jest następująca (rys. 36).

Ryż. 36. Składnia opisu procedury w programie

Rysunek 36 pokazuje, że w nagłówku procedury (dyrektywa PROC) obowiązkowa jest tylko nazwa procedury. Wśród dużej liczby operandów dyrektywy PROC należy podkreślić [odległość]. Atrybut ten może przyjmować wartości bliskie lub dalekie i charakteryzuje możliwość wywołania procedury z innego segmentu kodu. Domyślnie atrybut [odległość] jest ustawiony na blisko.

Procedurę można umieścić w dowolnym miejscu w programie, ale w taki sposób, aby losowo nie przejąć kontroli. Jeżeli procedura zostanie po prostu wstawiona do ogólnego strumienia instrukcji, wówczas mikroprocesor będzie postrzegał instrukcje procedury jako część tego strumienia i odpowiednio wykona instrukcje procedury.

Skoki warunkowe

Mikroprocesor posiada 18 instrukcji skoku warunkowego. Te polecenia pozwalają sprawdzić:

1) związek między operandami ze znakiem („większy - mniej”);

2) relacja między operandami bez znaku („wyższy – niższy”);

3) stany flag arytmetycznych ZF, SF, CF, OF, PF (ale nie AF).

Polecenia skoku warunkowego mają tę samą składnię:

jcc jump_label

Jak widać, kod mnemoniczny wszystkich poleceń zaczyna się od „j” - od słowa skok (skok), to - określa konkretny warunek analizowany przez polecenie.

Jeśli chodzi o operand jump_label, ta etykieta może znajdować się tylko w bieżącym segmencie kodu; międzysegmentowe przekazywanie sterowania w skokach warunkowych jest niedozwolone. W związku z tym nie ma mowy o modyfikatorze, który był obecny w składni poleceń skoku bezwarunkowego. We wczesnych modelach mikroprocesora (i8086, i80186 i i80286) instrukcje rozgałęzienia warunkowego mogły wykonywać tylko krótkie skoki - od -128 do +127 bajtów od instrukcji następującej po instrukcji rozgałęzienia warunkowego. Począwszy od modelu mikroprocesora 80386, to ograniczenie zostało usunięte, ale, jak widać, tylko w ramach bieżącego segmentu kodu.

Aby podjąć decyzję o tym, gdzie kontrola zostanie przekazana do komendy skoku warunkowego, należy najpierw sformować warunek, na podstawie którego zostanie podjęta decyzja o przekazaniu kontroli.

Źródłami takiego stanu mogą być:

1) każde polecenie, które zmienia stan flag arytmetycznych;

2) instrukcja porównania p, która porównuje wartości dwóch argumentów;

3) stan rejestru esx/cx.

polecenie porównania cmp

Polecenie porównania stron działa w ciekawy sposób. Jest to dokładnie to samo, co polecenie odejmowania - sub operand, operand_2.

Instrukcja p, podobnie jak instrukcja sub, odejmuje operandy i ustawia flagi. Jedyne, czego nie robi, to zapisanie wyniku odejmowania w miejscu pierwszego operandu.

Składnia polecenia str - str operand_1, operand_2 (porównaj) - porównuje dwa operandy i ustawia flagi na podstawie wyników porównania.

Flagi ustawione przez polecenie p mogą być analizowane przez specjalne instrukcje rozgałęzienia warunkowego. Zanim przyjrzymy się im, zwróćmy trochę uwagi na mnemoniki tych instrukcji skoku warunkowego (Tabela 16). Zrozumienie notacji przy tworzeniu nazwy poleceń skoku warunkowego (element w nazwie polecenia jcc, jak go oznaczyliśmy) ułatwi ich zapamiętywanie i dalsze praktyczne wykorzystanie.

Tabela 16. Znaczenie skrótów w nazwie komendy jcc Tabela 17. Lista komend skoku warunkowego dla komendy p operand_1, operand_2

Nie zdziw się faktem, że kilka różnych kodów mnemonicznych poleceń gałęzi warunkowych odpowiada tym samym wartościom flag (są one oddzielone od siebie ukośnikiem w Tabeli 17). Różnica w nazwie wynika z chęci twórców mikroprocesorów, aby ułatwić korzystanie z instrukcji skoku warunkowego w połączeniu z pewnymi grupami instrukcji. Dlatego różne nazwy odzwierciedlają raczej inną orientację funkcjonalną. Jednak fakt, że te polecenia reagują na te same flagi, czyni je absolutnie równoważnymi i równymi w programie. Dlatego w tabeli 17 są one pogrupowane nie według nazwy, ale według wartości flag (warunków), na które odpowiadają.

Warunkowe instrukcje rozgałęzienia i flagi

Oznaczenie mnemoniczne niektórych instrukcji skoku warunkowego odzwierciedla nazwę flagi, z którą działają, i ma następującą strukturę: pierwszy znak to „j” (skok, skok), drugi to oznaczenie flagi lub znak negacji „ n", po której następuje nazwa flagi . Ta struktura zespołu odzwierciedla jego cel. Jeśli nie ma znaku "n", to sprawdzany jest stan flagi, jeśli jest równy 1, następuje przejście do etykiety skoku. Jeśli występuje znak "n", to stan flagi jest sprawdzany pod kątem równości do 0 i jeśli się powiedzie, wykonywany jest skok do etykiety skoku.

Mnemoniki poleceń, nazwy flag i warunki skoku przedstawiono w tabeli 18. Polecenia te mogą być używane po dowolnych poleceniach, które modyfikują określone flagi.

Tabela 18. Instrukcje i flagi skoku warunkowego

Jeśli przyjrzysz się uważnie tabelom 17 i 18, zobaczysz, że wiele zawartych w nich instrukcji skoku warunkowego jest równoważnych, ponieważ obie opierają się na analizie tych samych flag.

Instrukcje skoku warunkowego i rejestr esx/cx

Architektura mikroprocesora wiąże się ze specyficznym wykorzystaniem wielu rejestrów. Na przykład rejestr EAX / AX / AL służy jako akumulator, a rejestry BP, SP służą do pracy ze stosem. Rejestr ECX / CX ma również pewien cel funkcjonalny: działa jako licznik w poleceniach sterujących pętlą oraz podczas pracy z ciągami znaków. Możliwe, że funkcjonalnie instrukcja rozgałęzienia warunkowego skojarzona z rejestrem esx/cx byłaby bardziej poprawnie przypisana do tej grupy instrukcji.

Składnia tej instrukcji rozgałęzienia warunkowego to:

1) jcxz jump_label (Skocz, jeśli ex wynosi zero) - przeskocz, jeśli cx wynosi zero;

2) jecxz jump_label (Jump Equal ех Zero) - skocz, jeśli ех wynosi zero.

Te polecenia są bardzo przydatne podczas wykonywania pętli i pracy z ciągami znaków.

Należy zauważyć, że istnieje ograniczenie związane z poleceniem jcxz/jecxz. W przeciwieństwie do innych instrukcji transferu warunkowego, instrukcja jcxz/jecxz może adresować tylko krótkie skoki -128 bajtów lub +127 bajtów od instrukcji następującej po niej.

Organizacja cykli

Cykl, jak wiadomo, jest ważną strukturą algorytmiczną, bez której prawdopodobnie żaden program nie może się obejść. Możesz zorganizować cykliczne wykonywanie określonej sekcji programu, na przykład za pomocą warunkowego przesyłania poleceń sterujących lub bezwarunkowego polecenia skoku jmp. Przy takiej organizacji cyklu wszystkie operacje związane z jego organizacją są wykonywane ręcznie. Biorąc jednak pod uwagę znaczenie takiego elementu algorytmicznego, jakim jest cykl, twórcy mikroprocesora wprowadzili do systemu instrukcji grupę trzech poleceń, co ułatwia programowanie cykli. Te instrukcje również używają rejestru esx/cx jako licznika pętli.

Podajmy krótki opis tych poleceń:

1) pętla przejście_label (Pętla) - powtórz cykl. Polecenie pozwala organizować pętle podobne do pętli for w językach wysokiego poziomu z automatycznym zmniejszaniem licznika pętli. Zadaniem zespołu jest wykonanie następujących czynności:

a) dekrementacja rejestru ECX/CX;

b) porównanie rejestru ECX/CX z zerem: jeśli (ECX/CX) = 0, to sterowanie jest przekazywane do następnego polecenia po pętli;

2) loope/loopz jump_label

Polecenia loope i loopz są bezwzględnymi synonimami. Praca komend polega na wykonaniu następujących czynności:

a) dekrementacja rejestru ECX/CX;

b) porównanie rejestru ECX/CX z zerem;

c) analiza stanu flagi zerowej ZF jeśli (ECX/CX) = 0 lub XF = 0, sterowanie jest przekazywane do następnego polecenia po pętli.

3) loopne/loopnz jump_label

Polecenia loopne i loopnz są również bezwzględnymi synonimami. Praca komend polega na wykonaniu następujących czynności:

a) dekrementacja rejestru ECX/CX;

b) porównanie rejestru ECX/CX z zerem;

c) analiza stanu flagi zerowej ZF: jeśli (ECX/CX) = 0 lub ZF = 1, sterowanie jest przekazywane do następnego polecenia po pętli.

Polecenia loope/loopz i loopne/loopnz działają odwrotnie. Rozszerzają one działanie polecenia loop o dodatkowo parsowanie flagi zf, co umożliwia zorganizowanie wczesnego wyjścia z pętli, używając tej flagi jako wskaźnika.

Wadą pętli poleceń loop, loope/loopz i loopne/loopnz jest to, że implementują one tylko krótkie skoki (od -128 do +127 bajtów). Aby pracować z długimi pętlami, będziesz musiał użyć skoków warunkowych i instrukcji jmp, więc spróbuj opanować oba sposoby organizowania pętli.

Autor: Tsvetkova A.V.

Polecamy ciekawe artykuły Sekcja Notatki z wykładów, ściągawki:

Kierownictwo. Kołyska

Język rosyjski i kultura mowy. Kołyska

Endokrynologia. Notatki do wykładów

Zobacz inne artykuły Sekcja Notatki z wykładów, ściągawki.

Czytaj i pisz przydatne komentarze do tego artykułu.

<< Wstecz

Najnowsze wiadomości o nauce i technologii, nowa elektronika:

Otwarto najwyższe obserwatorium astronomiczne na świecie 04.05.2024

Odkrywanie kosmosu i jego tajemnic to zadanie, które przyciąga uwagę astronomów z całego świata. Na świeżym powietrzu wysokich gór, z dala od miejskiego zanieczyszczenia światłem, gwiazdy i planety z większą wyrazistością odkrywają swoje tajemnice. Nowa karta w historii astronomii otwiera się wraz z otwarciem najwyższego na świecie obserwatorium astronomicznego - Obserwatorium Atacama na Uniwersytecie Tokijskim. Obserwatorium Atacama, położone na wysokości 5640 metrów nad poziomem morza, otwiera przed astronomami nowe możliwości w badaniu kosmosu. Miejsce to stało się najwyżej położonym miejscem dla teleskopu naziemnego, zapewniając badaczom unikalne narzędzie do badania fal podczerwonych we Wszechświecie. Chociaż lokalizacja na dużej wysokości zapewnia czystsze niebo i mniej zakłóceń ze strony atmosfery, budowa obserwatorium na wysokiej górze stwarza ogromne trudności i wyzwania. Jednak pomimo trudności nowe obserwatorium otwiera przed astronomami szerokie perspektywy badawcze. ... >>

Sterowanie obiektami za pomocą prądów powietrza 04.05.2024

Rozwój robotyki wciąż otwiera przed nami nowe perspektywy w zakresie automatyzacji i sterowania różnymi obiektami. Niedawno fińscy naukowcy zaprezentowali innowacyjne podejście do sterowania robotami humanoidalnymi za pomocą prądów powietrza. Metoda ta może zrewolucjonizować sposób manipulowania obiektami i otworzyć nowe horyzonty w dziedzinie robotyki. Pomysł sterowania obiektami za pomocą prądów powietrza nie jest nowy, jednak do niedawna realizacja takich koncepcji pozostawała wyzwaniem. Fińscy badacze opracowali innowacyjną metodę, która pozwala robotom manipulować obiektami za pomocą specjalnych strumieni powietrza, takich jak „palce powietrzne”. Algorytm kontroli przepływu powietrza, opracowany przez zespół specjalistów, opiera się na dokładnym badaniu ruchu obiektów w strumieniu powietrza. System sterowania strumieniem powietrza, realizowany za pomocą specjalnych silników, pozwala kierować obiektami bez uciekania się do siły fizycznej ... >>

Psy rasowe chorują nie częściej niż psy rasowe 03.05.2024

Dbanie o zdrowie naszych pupili to ważny aspekt życia każdego właściciela psa. Powszechnie uważa się jednak, że psy rasowe są bardziej podatne na choroby w porównaniu do psów mieszanych. Nowe badania prowadzone przez naukowców z Texas School of Veterinary Medicine and Biomedical Sciences rzucają nową perspektywę na to pytanie. Badanie przeprowadzone w ramach projektu Dog Aging Project (DAP) na ponad 27 000 psów do towarzystwa wykazało, że psy rasowe i mieszane były na ogół jednakowo narażone na różne choroby. Chociaż niektóre rasy mogą być bardziej podatne na pewne choroby, ogólny wskaźnik rozpoznań jest praktycznie taki sam w obu grupach. Główny lekarz weterynarii projektu Dog Aging Project, dr Keith Creevy, zauważa, że ​​istnieje kilka dobrze znanych chorób, które występują częściej u niektórych ras psów, co potwierdza pogląd, że psy rasowe są bardziej podatne na choroby. ... >>

Przypadkowe wiadomości z Archiwum

Polimer zmienia kolor pod wpływem naprężeń mechanicznych 27.08.2015

Naukowcy z University of Pennsylvania (USA) opracowali polimer, który może zmieniać kolor w zależności od siły uderzenia.

Polimer oparty jest na kryształach fotonicznych wytworzonych za pomocą litografii holograficznej. Ze względu na okresową zmianę współczynnika załamania kryształy reagują na odkształcenie zmianą koloru.

Co najlepsze, do działania nie wymagają nawet źródła zasilania. Technologia będzie przydatna do tworzenia kasków ochronnych dla wojska lub sportowców na wypadek kontuzji, pomagając obiektywnie ocenić siłę uderzenia.

Kolor powłoki nałożonej na hełm pozwoli zapewnić poszkodowanemu w porę pomoc medyczną w odpowiedniej ilości, co więcej okazuje się na tyle lekki, że nie obciąża hełmu.

Inne ciekawe wiadomości:

▪ Nieoślepiający system świateł drogowych Forda

▪ Infineon IMC100 — cyfrowa platforma sterowania silnikiem

▪ Amazon wysycha

▪ Głosowanie w świecie zwierząt

▪ Serce bije do poczęcia

Wiadomości o nauce i technologii, nowa elektronika

 

Ciekawe materiały z bezpłatnej biblioteki technicznej:

▪ sekcja witryny Iluzje wizualne. Wybór artykułów

▪ artykuł Silnik pralki do pompy głębinowej. Wskazówki dla mistrza domu

▪ artykuł Co to jest pierwotniak? Szczegółowa odpowiedź

▪ artykuł Chmury niższego poziomu. Wskazówki podróżnicze

▪ artykuł Identyfikacja przewodów za pomocą kolorów lub oznaczeń cyfrowych. Encyklopedia elektroniki radiowej i elektrotechniki

▪ artykuł Linie elektroenergetyczne napowietrzne o napięciu powyżej 1 kV. Lokalizacja przewodów i kabli oraz odległość między nimi. Encyklopedia elektroniki radiowej i elektrotechniki

Zostaw swój komentarz do tego artykułu:

Imię i nazwisko:


Email opcjonalny):


komentarz:





Wszystkie języki tej strony

Strona główna | biblioteka | Artykuły | Mapa stony | Recenzje witryn

www.diagram.com.ua

www.diagram.com.ua
2000-2024