MENÜ English Ukrainian Russisch Startseite

Kostenlose technische Bibliothek für Bastler und Profis Kostenlose technische Bibliothek


Informatik und Informationstechnologien. Vorlesungsskript: kurz das Wichtigste

Vorlesungsunterlagen, Spickzettel

Verzeichnis / Vorlesungsunterlagen, Spickzettel

Kommentare zum Artikel Kommentare zum Artikel

Inhaltsverzeichnis

  1. Einführung in die Informatik (Informatik. Information. Darstellung und Verarbeitung von Informationen. Zahlensysteme. Darstellung von Zahlen in einem Computer. Formalisiertes Konzept eines Algorithmus)
  2. Pascal-Sprache (Einführung in Pascal. Standardprozeduren und -funktionen. Pascal-Operatoren)
  3. Verfahren und Funktionen (Das Konzept eines Hilfsalgorithmus. Prozeduren in Pascal. Funktionen in Pascal. Vorausschauende Beschreibungen und Verknüpfung von Unterprogrammen. Direktive)
  4. Unterprogramme (Routinenparameter. Subroutinenparametertypen. String-Typ in Pascal. Prozeduren und Funktionen für String-Typ-Variablen. Datensätze. Sets)
  5. Dateien (Dateien. Dateioperationen. Module. Modultypen)
  6. Dynamisches Gedächtnis (Referenzdatentyp. Dynamischer Speicher. Dynamische Variablen. Arbeiten mit dynamischem Speicher. Untypisierte Zeiger)
  7. Abstrakte Datenstrukturen (Abstrakte Datenstrukturen. Stapel. Warteschlangen)
  8. Baumdatenstrukturen (Baumdatenstrukturen. Operationen an Bäumen. Beispiele für die Implementierung von Operationen)
  9. Zählt (Das Konzept eines Graphen. Methoden zur Darstellung eines Graphen. Darstellung eines Graphen durch eine Liste von Inzidenzen. Tiefen-First-Traversal-Algorithmus für einen Graphen. Darstellung eines Graphen als Liste von Listen. Breiten-First-Traversal-Algorithmus für einen Graphen )
  10. Objektdatentyp (Objekttyp in Pascal. Das Konzept eines Objekts, seine Beschreibung und Verwendung. Vererbung. Instanzen von Objekten erstellen. Komponenten und Umfang)
  11. Methoden (Methoden. Konstruktoren und Destruktoren. Destruktoren. Virtuelle Methoden. Objektdatenfelder und formale Methodenparameter)
  12. Objekttyp-Kompatibilität (Kapselung. Erweiterbare Objekte. Objekttypkompatibilität)
  13. Monteur (Über Assembler. Mikroprozessor-Softwaremodell. Benutzerregister. Allzweckregister. Segmentregister. Status- und Steuerregister)
  14. Registrieren (Mikroprozessor-Systemregister. Steuerregister. Systemadressregister. Debug-Register)
  15. Versammlungsprogramme (Assembler-Programmstruktur. Assembler-Syntax. Vergleichsoperatoren. Operatoren und ihre Priorität. Vereinfachte Segmentdefinitionsanweisungen. Von der MODEL-Anweisung erstellte Bezeichner. Speichermodelle. Speichermodellmodifikatoren)
  16. Montageanleitungsstrukturen (Aufbau einer Maschinenanweisung. Methoden zur Spezifizierung von Befehlsoperanden. Adressierungsmethoden)
  17. Befehle (Datenübertragungsbefehle. Arithmetische Befehle)
  18. Steuerübertragungsbefehle (Logische Befehle. Wahrheitstabelle für logische Negation. Wahrheitstabelle für logisches inklusives ODER. Wahrheitstabelle für logisches UND. Wahrheitstabelle für logisches exklusives ODER. Bedeutung der Abkürzungen im jcc-Befehlsnamen. Liste der bedingten Sprungbefehle für den Befehl. Bedingter Sprung Befehle und Flags)

VORTRAG Nr. 1. Einführung in die Informatik

1. Informatik. Information. Darstellung und Verarbeitung von Informationen

Die Informatik beschäftigt sich mit einer formalisierten Darstellung von Objekten und Strukturen ihrer Beziehungen in verschiedenen Bereichen der Wissenschaft, Technik und Produktion. Zur Modellierung von Objekten und Phänomenen werden verschiedene formale Werkzeuge verwendet, wie logische Formeln, Datenstrukturen, Programmiersprachen usw.

In der Informatik hat ein so grundlegender Begriff wie Information verschiedene Bedeutungen:

1) formelle Darstellung externer Informationsformen;

2) abstrakte Bedeutung von Informationen, ihr interner Inhalt, Semantik;

3) Beziehung von Informationen zur realen Welt.

Aber in der Regel wird Information als ihre abstrakte Bedeutung verstanden - Semantik. Indem wir die Darstellung von Informationen interpretieren, erhalten wir ihre Bedeutung, Semantik. Wenn wir uns also austauschen wollen, brauchen wir übereinstimmende Ansichten, damit die Richtigkeit der Interpretation nicht verletzt wird. Dazu wird die Interpretation der Darstellung von Informationen mit einigen mathematischen Strukturen identifiziert. In diesem Fall kann die Informationsverarbeitung durch rigorose mathematische Methoden durchgeführt werden.

Eine der mathematischen Beschreibungen von Informationen ist ihre Darstellung in Form einer Funktion y =f(x, t), wobei t die Zeit und x ein Punkt in einem bestimmten Feld ist, an dem der Wert von y gemessen wird. Abhängig von den Parametern der Chi-Funktion können Informationen klassifiziert werden.

Wenn es sich bei den Parametern um skalare Größen handelt, die eine kontinuierliche Reihe von Werten annehmen, werden die auf diese Weise erhaltenen Informationen als kontinuierlich (oder analog) bezeichnet. Wird den Parametern ein bestimmter Änderungsschritt gegeben, so spricht man von diskreten Informationen. Diskrete Informationen gelten als universell, da es möglich ist, für jeden spezifischen Parameter einen Funktionswert mit einem bestimmten Genauigkeitsgrad zu erhalten.

Diskrete Informationen werden üblicherweise mit digitalen Informationen identifiziert, die ein Sonderfall symbolischer Informationen alphabetischer Repräsentation sind. Ein Alphabet ist eine endliche Menge von Symbolen jeglicher Art. Sehr oft tritt in der Informatik eine Situation auf, in der die Zeichen eines Alphabets durch die Zeichen eines anderen repräsentiert werden müssen, d. h. um eine Codierungsoperation durchzuführen. Wenn die Anzahl der Zeichen des Codieralphabets kleiner als die Anzahl der Zeichen des Codieralphabets ist, dann ist der Codiervorgang selbst nicht kompliziert, andernfalls ist es notwendig, einen festen Satz von Zeichen des Codieralphabets für eine eindeutig korrekte Codierung zu verwenden.

Wie die Praxis gezeigt hat, ist das einfachste Alphabet, mit dem Sie andere Alphabete kodieren können, binär und besteht aus zwei Zeichen, die normalerweise mit 0 und 1 bezeichnet werden. Mit n Zeichen des Binäralphabets können Sie 2n Zeichen kodieren, und das reicht aus um ein beliebiges Alphabet zu kodieren.

Der Wert, der durch ein Symbol des binären Alphabets dargestellt werden kann, wird als minimale Informationseinheit oder Bit bezeichnet. Folge von 8 Bits - Bytes. Ein Alphabet, das 256 verschiedene 8-Bit-Folgen enthält, wird als Byte-Alphabet bezeichnet.

Als Standard wird heute in der Informatik ein Code angenommen, bei dem jedes Zeichen durch 1 Byte codiert wird. Es gibt auch andere Alphabete.

2. Zahlensysteme

Ein Zahlensystem ist eine Reihe von Regeln für die Benennung und Schreibweise von Zahlen. Es gibt positionelle und nicht-positionale Zahlensysteme.

Das Zahlensystem wird Positional genannt, wenn der Wert der Ziffer der Zahl von der Position der Ziffer in der Zahl abhängt. Andernfalls wird es als nichtpositional bezeichnet. Der Wert einer Zahl wird durch die Position dieser Ziffern in der Zahl bestimmt.

3. Darstellung von Zahlen in einem Computer

32-Bit-Prozessoren können mit bis zu 232-1 RAM arbeiten, und Adressen können im Bereich 00000000 - FFFFFFFF geschrieben werden. Im Real-Modus arbeitet der Prozessor jedoch mit einem Speicher von bis zu 220-1, und Adressen fallen in den Bereich von 00000 bis FFFFF. Speicherbytes können zu Feldern sowohl fester als auch variabler Länge kombiniert werden. Ein Wort ist ein Feld fester Länge bestehend aus 2 Bytes, ein Doppelwort ist ein Feld von 4 Bytes. Feldadressen können gerade oder ungerade sein, wobei gerade Adressen Operationen schneller ausführen.

Festkommazahlen werden in Computern als ganzzahlige Binärzahlen dargestellt und können 1, 2 oder 4 Bytes groß sein.

Binäre ganze Zahlen werden im Zweierkomplement dargestellt, Festkommazahlen im Zweierkomplement. Wenn eine Zahl außerdem 2 Bytes einnimmt, wird die Struktur der Zahl nach der folgenden Regel geschrieben: Die höchstwertige Ziffer wird dem Vorzeichen der Zahl zugeordnet, der Rest den Binärziffern der Zahl. Der Komplementärcode einer positiven Zahl ist gleich der Zahl selbst, und der Komplementärcode einer negativen Zahl kann mit der folgenden Formel erhalten werden: x = 10i - \x\, wobei n die Ziffernkapazität der Zahl ist.

Im binären Zahlensystem erhält man einen zusätzlichen Code durch Invertieren von Bits, d. h. Ersetzen von Einheiten durch Nullen und umgekehrt, und Addieren von Eins zum niedrigstwertigen Bit.

Die Anzahl der Bits der Mantisse bestimmt die Genauigkeit der Darstellung von Zahlen, die Anzahl der Maschinenordnungsbits bestimmt den Darstellungsbereich von Gleitkommazahlen.

4. Formalisiertes Konzept eines Algorithmus

Ein Algorithmus kann nur existieren, wenn gleichzeitig ein mathematisches Objekt existiert. Das formalisierte Konzept eines Algorithmus ist mit dem Konzept rekursiver Funktionen, normaler Markov-Algorithmen, Turing-Maschinen verbunden.

In der Mathematik wird eine Funktion als einwertig bezeichnet, wenn es für einen beliebigen Satz von Argumenten ein Gesetz gibt, durch das der eindeutige Wert der Funktion bestimmt wird. Ein Algorithmus kann als solches Gesetz wirken; in diesem Fall heißt die Funktion berechenbar.

Rekursive Funktionen sind eine Unterklasse von berechenbaren Funktionen, und die Algorithmen, die die Berechnungen definieren, werden als begleitende rekursive Funktionsalgorithmen bezeichnet. Erstens werden die grundlegenden rekursiven Funktionen festgelegt, für die der begleitende Algorithmus trivial und eindeutig ist; dann werden drei Regeln eingeführt - Substitutions-, Rekursions- und Minimierungsoperatoren, mit deren Hilfe komplexere rekursive Funktionen auf der Grundlage von Grundfunktionen erhalten werden.

Die Grundfunktionen und ihre begleitenden Algorithmen können sein:

1) eine Funktion von n unabhängigen Variablen, identisch gleich Null. Wenn das Vorzeichen der Funktion dann φn ist, dann sollte der Wert der Funktion unabhängig von der Anzahl der Argumente gleich Null gesetzt werden;

2) die Identitätsfunktion von n unabhängigen Variablen der Form ψni. Wenn das Vorzeichen der Funktion dann ψni ist, dann sollte der Wert der Funktion als Wert des i-ten Arguments genommen werden, von links nach rechts gezählt;

3) Λ ist eine Funktion eines unabhängigen Arguments. Wenn das Vorzeichen der Funktion dann λ ist, dann sollte der Wert der Funktion als der Wert genommen werden, der dem Wert des Arguments folgt. Verschiedene Gelehrte haben ihre eigenen Herangehensweisen an das Formalisierte vorgeschlagen

Darstellung des Algorithmus. Zum Beispiel schlug der amerikanische Wissenschaftler Church vor, dass die Klasse der berechenbaren Funktionen auf rekursive Funktionen beschränkt ist und dass es daher unabhängig von dem Algorithmus, der einen Satz nicht negativer Ganzzahlen in einen anderen umwandelt, einen Algorithmus gibt, der die rekursive Funktion begleitet entspricht dem angegebenen. Wenn es also unmöglich ist, eine rekursive Funktion zur Lösung eines gegebenen Problems zu konstruieren, dann gibt es keinen Algorithmus, um es zu lösen. Ein anderer Wissenschaftler, Turing, entwickelte einen virtuellen Computer, der eine Eingabefolge von Zeichen zu einer Ausgabe verarbeitete. In diesem Zusammenhang stellte er die These auf, dass jede berechenbare Funktion Turing-berechenbar ist.

VORTRAG Nr. 2. Pascal-Sprache

1. Einführung in die Sprache Pascal

Die Grundsymbole der Sprache – Buchstaben, Zahlen und Sonderzeichen – bilden ihr Alphabet. Die Pascal-Sprache enthält den folgenden Satz grundlegender Symbole:

1) 26 lateinische Kleinbuchstaben und 26 lateinische Großbuchstaben:

ABCDEFGHIJKLMNOPQRSTUVWXYZ

abcdefghijklmnopqrstuvwxyz;

2) _ (Unterstrich);

3) 10 Ziffern: 0123456789;

4) Betriebszeichen:

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

5) Begrenzer:

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

6) Spezifizierer: ^ # $;

7) Service (reserviert) Wörter:

ABSOLUTE, ASSEMBLER, AND, ARRAY, ASM, BEGIN, CASE, CONST, CONSTRUCTOR, DESTRUCTOR, DIV, DO, DOWNTO, ELSE, END, EXPORT, EXTERNAL, WART, FILE, FOR, FORWARD, FUNCTION, GOTO, IF, IMPLEMENTATION, IN, INDEX, VERERBT, INLINE, SCHNITTSTELLE, UNTERBRECHEN, LABEL, BIBLIOTHEK, MOD, NAME, NIL, NAHE, NICHT, OBJEKT, VON ODER, VERPACKT, PRIVAT, PROZEDUR, PROGRAMM, ÖFFENTLICH, AUFZEICHNUNG, WIEDERHOLUNG, RESIDENTEN, SET, SHL, SHR, STRING, THEN, TO, TYPE, UNIT, BIS, USES, VAR, VIRTUAL, WHILE, WITH, XOR.

Zusätzlich zu den aufgeführten enthält der Satz von Grundzeichen ein Leerzeichen. Innerhalb von Doppelzeichen und reservierten Wörtern dürfen keine Leerzeichen verwendet werden.

Typenkonzept für Daten

In der Mathematik ist es üblich, Variablen nach einigen wichtigen Merkmalen zu klassifizieren. Es wird streng zwischen reellen, komplexen und logischen Variablen unterschieden, zwischen Variablen, die einzelne Werte darstellen, und einer Menge von Werten usw. Bei der Verarbeitung von Daten auf einem Computer ist eine solche Klassifizierung noch wichtiger. In jeder algorithmischen Sprache ist jede Konstante, Variable, jeder Ausdruck oder jede Funktion von einem bestimmten Typ.

In Pascal gibt es eine Regel: Der Typ wird explizit in der Deklaration einer Variablen oder Funktion angegeben, die ihrer Verwendung vorausgeht. Das Pascal-Typkonzept hat die folgenden Haupteigenschaften:

1) Jeder Datentyp definiert eine Menge von Werten, zu denen eine Konstante gehört, die eine Variable oder ein Ausdruck annehmen oder eine Operation oder Funktion erzeugen kann;

2) die Art des Werts, der durch eine Konstante, Variable oder einen Ausdruck gegeben wird, kann durch ihre Form oder Beschreibung bestimmt werden;

3) Jede Operation oder Funktion erfordert Argumente mit festem Typ und erzeugt ein Ergebnis mit festem Typ.

Daraus folgt, dass der Compiler Typinformationen verwenden kann, um die Berechenbarkeit und Korrektheit verschiedener Konstrukte zu überprüfen.

Der Typ definiert:

1) mögliche Werte von Variablen, Konstanten, Funktionen, Ausdrücken, die zu einem bestimmten Typ gehören;

2) die interne Form der Datenpräsentation in einem Computer;

3) Operationen und Funktionen, die für Werte eines bestimmten Typs ausgeführt werden können.

Es sei darauf hingewiesen, dass die obligatorische Beschreibung des Typs zu Redundanz im Text von Programmen führt, diese Redundanz jedoch ein wichtiges Hilfsmittel für die Entwicklung von Programmen ist und als notwendige Eigenschaft moderner algorithmischer Hochsprachen angesehen wird.

In Pascal gibt es skalare und strukturierte Datentypen. Skalare Typen umfassen Standardtypen und benutzerdefinierte Typen. Zu den Standardtypen gehören Integer-, Real-, Character-, Boolean- und Adresstypen.

Integer-Typen definieren Konstanten, Variablen und Funktionen, deren Werte durch den Satz von Integern realisiert werden, die in einem bestimmten Computer zulässig sind.

Reelle Typen definieren jene Daten, die durch eine Teilmenge reeller Zahlen implementiert werden, die in einem bestimmten Computer zulässig sind.

Die benutzerdefinierten Typen sind enum und range. Strukturierte Typen gibt es in vier Varianten: Arrays, Sets, Records und Files.

Zusätzlich zu den aufgeführten enthält Pascal zwei weitere Typen – prozedural und objekt.

Ein Sprachausdruck besteht aus Konstanten, Variablen, Funktionszeigern, Operatorzeichen und Klammern. Ein Ausdruck definiert eine Regel zur Berechnung eines Wertes. Die Reihenfolge der Berechnung wird durch den Vorrang (Priorität) der darin enthaltenen Operationen bestimmt. Pascal hat die folgende Operatorpriorität:

1) Berechnungen in Klammern;

2) Berechnung von Funktionswerten;

3) unäre Operationen;

4) Operationen *, /, div, mod und;

5) Operationen +, -, oder, xor;

6) Vergleichsoperationen =, <>, <, >, <=, >=.

Ausdrücke sind Teil vieler Pascal-Sprachoperatoren und können auch Argumente für eingebaute Funktionen sein.

2. Standardverfahren und -funktionen

Arithmetische Funktionen

1. Funktion Abs (X);

Gibt den absoluten Wert des Parameters zurück.

X ist ein Ausdruck vom Typ Real oder Integer.

2. Funktion ArcTan(X: Extended): Erweitert;

Gibt den Arkustangens des Arguments zurück.

X ist ein Ausdruck vom Typ Real oder Integer.

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

Gibt den Exponenten zurück.

X ist ein Ausdruck vom Typ Real oder Integer.

4.Frac(X: Real): Real;

Gibt den Bruchteil des Arguments zurück.

X ist ein reeller Typausdruck. Das Ergebnis ist der Bruchteil von X, d.h.

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

5. Funktion Int(X: Real): Real;

Gibt den ganzzahligen Teil des Arguments zurück.

X ist ein reeller Typausdruck. Das Ergebnis ist der ganzzahlige Teil von X, also X auf Null gerundet.

6. Funktion Ln(X: Real): Real;

Gibt den natürlichen Logarithmus (Ln e = 1) eines reellen Ausdrucks X zurück.

7.Funktion Pi: Erweitert;

Gibt den Pi-Wert zurück, der als 3.1415926535 definiert ist.

8.Funktion Sin (X: Erweitert): Erweitert;

Gibt den Sinus des Arguments zurück.

X ist ein reeller Typausdruck. Sin gibt den Sinus des Winkels X im Bogenmaß zurück.

9.Funktion Sqr (X: Erweitert): Erweitert;

Gibt das Quadrat des Arguments zurück.

X ist ein Fließkommaausdruck. Das Ergebnis ist vom gleichen Typ wie X.

10.Funktion Sqrt (X: Erweitert): Erweitert;

Gibt die Quadratwurzel des Arguments zurück.

X ist ein Fließkommaausdruck. Das Ergebnis ist die Quadratwurzel von X.

Verfahren und Funktionen zur Wertumwandlung

1. Prozedur Str(X [: Breite [: Dezimalstellen]]; var S);

Konvertiert die Zahl X in eine Zeichenfolgendarstellung gemäß

Formatierungsoptionen für Breite und Dezimalstellen. X ist ein Ausdruck eines reellen oder ganzzahligen Typs. Breite und Dezimalstellen sind ganzzahlige Ausdrücke. S ist eine Variable vom Typ String oder ein nullterminiertes Zeichenarray, wenn die erweiterte Syntax erlaubt ist.

2. Funktion Chr(X: Byte): Char;

Gibt das Zeichen mit der Ordnungszahl X in der ASCII-Tabelle zurück.

3.Funktion hoch (X);

Gibt den größten Wert im Bereich des Parameters zurück.

4.FunktionLow(X);

Gibt den kleinsten Wert im Parameterbereich zurück.

5 FunctionOrd(X): Lange Ganzzahl;

Gibt den Ordinalwert eines Aufzählungstypausdrucks zurück. X ist ein Aufzählungstypausdruck.

6. Funktion Round(X: Extended): Lange Ganzzahl;

Rundet einen reellen Wert auf eine ganze Zahl. X ist ein reeller Typausdruck. Round gibt einen Longint-Wert zurück, bei dem es sich um den auf die nächste ganze Zahl gerundeten Wert von X handelt. Wenn X genau in der Mitte zwischen zwei ganzen Zahlen liegt, wird die Zahl mit dem größten absoluten Wert zurückgegeben. Wenn der gerundete Wert von X außerhalb des Longint-Bereichs liegt, wird ein Laufzeitfehler generiert, den Sie mit der EInvalidOp-Ausnahme behandeln können.

7. Funktion Trunc(X: Extended): Lange Ganzzahl;

Kürzt einen reellen Typwert auf eine Ganzzahl. Wenn der gerundete Wert von X außerhalb des Longint-Bereichs liegt, wird ein Laufzeitfehler generiert, den Sie mit der EInvalidOp-Ausnahme behandeln können.

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

Konvertiert eine Zahl aus einem Zeichenfolgenwert S in eine Zahl

Darstellung V. S - String-Typ-Ausdruck - eine Folge von Zeichen, die eine ganze oder reelle Zahl bildet. Wenn der S-Ausdruck ungültig ist, wird der Index des ungültigen Zeichens in der Code-Variablen gespeichert. Andernfalls wird Code auf Null gesetzt.

Ordinalwertprozeduren und -funktionen

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

Subtrahiert eins oder N von der Variablen X. Dec(X) entspricht X:= X - 1 und Dec(X, N) entspricht X:= X - N. X ist eine Variable vom Typ Aufzählung oder PChar, falls erweitert Syntax zulässig ist und N ein ganzzahliger Ausdruck ist. Die Dec-Prozedur generiert optimalen Code und ist besonders nützlich in langen Schleifen.

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

Fügt eins oder N zur Variablen X hinzu. X ist eine Variable vom Aufzählungstyp oder PChar-Typ, wenn die erweiterte Syntax zulässig ist, und N ist ein Ausdruck vom ganzzahligen Typ. Inc (X) entspricht der Anweisung X:= X + 1 und Inc (X, N) entspricht der Anweisung X:= X + N. Die Inc-Prozedur erzeugt optimalen Code und ist besonders nützlich in langen Schleifen.

3. FunctionOdd(X: LongInt): Boolean;

Gibt True zurück, wenn X eine ungerade Zahl ist, andernfalls False.

4.Funktion Pred(X);

Gibt den vorherigen Wert des Parameters zurück. X ist ein Aufzählungstypausdruck. Das Ergebnis ist vom gleichen Typ.

5 Funktion Succ(X);

Gibt den nächsten Parameterwert zurück. X ist ein Aufzählungstypausdruck. Das Ergebnis ist vom gleichen Typ.

3. Pascal-Sprachoperatoren

Bedingter Operator

Das Format der vollständigen Bedingungsanweisung ist wie folgt definiert: If B then SI else S2; wobei B eine Verzweigungsbedingung (Entscheidungsfindung), ein logischer Ausdruck oder eine Beziehung ist; SI, S2 - eine ausführbare Anweisung, einfach oder zusammengesetzt.

Bei der Ausführung einer bedingten Anweisung wird zuerst der Ausdruck B ausgewertet, dann wird sein Ergebnis analysiert: Wenn B wahr ist, wird die Anweisung S1 ausgeführt - die Verzweigung von then und die Anweisung S2 wird übersprungen; wenn B falsch ist, dann wird die Anweisung S2 - der Else-Zweig ausgeführt und die Anweisung S1 übersprungen.

Es gibt auch eine abgekürzte Form des Bedingungsoperators. Es wird geschrieben als: Wenn B, dann S.

Auswahloperator

Die Betreiberstruktur ist wie folgt:

Fälle von

c1: Anweisung1;

c2: Anweisung2;

...

cn: AnweisungN;

sonst Anleitung

end;

wobei S ein ordinaler Ausdruck ist, dessen Wert berechnet wird;

с1, с2..., сп – Konstanten vom Ordinaltyp, mit denen Ausdrücke verglichen werden

S; Anweisung1,..., AnweisungN – Operatoren, von denen derjenige ausgeführt wird, dessen Konstante mit dem Wert des Ausdrucks S übereinstimmt;

Anweisung – eine Anweisung, die ausgeführt wird, wenn der Wert des Sylq-Ausdrucks mit keiner der Konstanten c1, c2...cn übereinstimmt.

Dieser Operator ist eine Verallgemeinerung des bedingten If-Operators für eine beliebige Anzahl von Alternativen. Es gibt eine abgekürzte Form der Anweisung, wo es keine andere Verzweigung gibt.

Schleifenanweisung mit Parameter

Parameterschleifenanweisungen, die mit dem Wort for beginnen, bewirken, dass die Anweisung, die eine zusammengesetzte Anweisung sein kann, wiederholt ausgeführt wird, während der Steuervariablen eine aufsteigende Folge von Werten zugewiesen wird.

Gesamtansicht des for-Operators:

for <loop counter> := <start value> to <end value> do <statement>;

Wenn die for-Anweisung ausgeführt wird, werden die Start- und Endwerte einmal bestimmt, und diese Werte werden während der gesamten Ausführung der for-Anweisung beibehalten. Die im Hauptteil der for-Anweisung enthaltene Anweisung wird einmal für jeden Wert im Bereich zwischen Start- und Endwert ausgeführt. Der Schleifenzähler wird immer auf einen Anfangswert initialisiert. Wenn die for-Anweisung ausgeführt wird, wird der Wert des Schleifenzählers mit jeder Iteration erhöht. Wenn der Startwert größer als der Endwert ist, wird die im Hauptteil der for-Anweisung enthaltene Anweisung nicht ausgeführt. Wenn das Schlüsselwort downto in einer Schleifenanweisung verwendet wird, wird der Wert der Steuervariablen bei jeder Iteration um eins verringert. Wenn der Startwert in einer solchen Anweisung kleiner als der Endwert ist, wird die im Hauptteil der Schleifenanweisung enthaltene Anweisung nicht ausgeführt.

Wenn die im Hauptteil der for-Anweisung enthaltene Anweisung den Wert des Schleifenzählers ändert, ist dies ein Fehler. Nach der Ausführung der for-Anweisung wird der Wert der Steuervariablen undefiniert, es sei denn, die Ausführung der for-Anweisung wurde durch eine Sprunganweisung unterbrochen.

Schleifenanweisung mit Vorbedingung

Eine Vorbedingungsschleifenanweisung (beginnend mit dem Schlüsselwort while) enthält einen Ausdruck, der die wiederholte Ausführung der Anweisung (die eine zusammengesetzte Anweisung sein kann) steuert. Zyklusform:

Während B S tut;

wobei B eine logische Bedingung ist, deren Wahrheit überprüft wird (es ist eine Bedingung zum Beenden der Schleife);

S - Schleifenkörper - eine Anweisung.

Der Ausdruck, der die Wiederholung einer Anweisung steuert, muss vom Typ Boolean sein. Es wird ausgewertet, bevor die innere Anweisung ausgeführt wird. Die innere Anweisung wird wiederholt ausgeführt, solange der Ausdruck True ergibt. Wenn der Ausdruck von Anfang an mit False ausgewertet wird, wird die in der Vorbedingungsschleifenanweisung enthaltene Anweisung nicht ausgeführt.

Schleifenanweisung mit Nachbedingung

Bei einer Schleifenanweisung mit Nachbedingung (beginnend mit dem Wort repeat) ist der Ausdruck, der die wiederholte Ausführung einer Folge von Anweisungen steuert, in der Repeat-Anweisung enthalten. Zyklusform:

S bis B wiederholen;

wobei B eine logische Bedingung ist, deren Wahrheit überprüft wird (es ist eine Bedingung zum Beenden der Schleife);

S - eine oder mehrere Schleifenkörperanweisungen.

Das Ergebnis des Ausdrucks muss vom Typ Boolean sein. Die zwischen den Schlüsselwörtern repeat und until eingeschlossenen Anweisungen werden nacheinander ausgeführt, bis das Ergebnis des Ausdrucks wahr ist. Die Anweisungsfolge wird mindestens einmal ausgeführt, da der Ausdruck nach jeder Ausführung der Anweisungsfolge ausgewertet wird.

VORTRAG № 3. Verfahren und Funktionen

1. Das Konzept eines Hilfsalgorithmus

Der Problemlösungsalgorithmus wird entworfen, indem das gesamte Problem in separate Teilaufgaben zerlegt wird. Typischerweise werden Subtasks als Subroutinen implementiert.

Ein Unterprogramm ist ein Hilfsalgorithmus, der wiederholt im Hauptalgorithmus mit unterschiedlichen Werten einiger eingehender Größen, Parameter genannt, verwendet wird.

Ein Unterprogramm in Programmiersprachen ist eine Folge von Anweisungen, die nur an einer Stelle im Programm definiert und geschrieben sind, aber von einer oder mehreren Stellen im Programm zur Ausführung aufgerufen werden können. Jede Subroutine wird durch einen eindeutigen Namen identifiziert.

Es gibt zwei Arten von Subroutinen in Pascal, Prozeduren und Funktionen. Eine Prozedur und eine Funktion sind eine benannte Folge von Deklarationen und Anweisungen. Bei der Verwendung von Prozeduren oder Funktionen muss das Programm den Text der Prozedur oder Funktion und einen Aufruf der Prozedur oder Funktion enthalten. Die in der Beschreibung angegebenen Parameter heißen formal, die im Aufruf des Unterprogramms angegebenen heißen aktuell. Alle formalen Parameter lassen sich in folgende Kategorien einteilen:

1) Parameter-Variablen;

2) konstante Parameter;

3) Parameterwerte;

4) Prozedurparameter und Funktionsparameter, d. h. prozedurale Parameter;

5) untypisierte variable Parameter.

Die Texte der Prozeduren und Funktionen befinden sich im Abschnitt Beschreibungen der Prozeduren und Funktionen.

Prozedur- und Funktionsnamen als Parameter übergeben

Bei vielen Problemen, insbesondere in der Computermathematik, ist es notwendig, die Namen von Prozeduren und Funktionen als Parameter zu übergeben. Dazu hat TURBO PASCAL einen neuen Datentyp eingeführt – prozedural oder funktional, je nachdem, was beschrieben wird. (Prozedur- und Funktionstypen werden im Abschnitt Typdeklaration beschrieben.)

Ein Funktions- und Prozedurtyp ist definiert als die Überschrift einer Prozedur und eine Funktion mit einer Liste formaler Parameter, aber ohne Namen. Es ist möglich, eine Funktion oder einen Prozedurtyp ohne Parameter zu definieren, zum Beispiel:

tippe

Proc = Prozedur;

Nachdem ein prozeduraler oder funktionaler Typ deklariert wurde, kann er verwendet werden, um formale Parameter zu beschreiben – die Namen von Prozeduren und Funktionen. Außerdem ist es notwendig, solche realen Prozeduren oder Funktionen zu schreiben, deren Namen als Aktualparameter übergeben werden.

2. Prozeduren in Pascal

Jede Prozedurbeschreibung enthält eine Überschrift, gefolgt von einem Programmblock. Die allgemeine Form des Prozedurkopfs ist wie folgt:

Prozedur <Name> [(<Liste formaler Parameter>)];

Eine Prozedur wird mit einer Prozeduranweisung aktiviert, die den Namen der Prozedur und die erforderlichen Parameter enthält. Die Anweisungen, die beim Ausführen der Prozedur ausgeführt werden sollen, sind im Anweisungsteil des Prozedurmoduls enthalten. Wenn eine in einer Prozedur enthaltene Anweisung einen Prozedurbezeichner innerhalb eines Prozedurmoduls verwendet, wird die Prozedur rekursiv ausgeführt, dh sie verweist bei der Ausführung auf sich selbst.

3. Funktionen in Pascal

Eine Funktionsdeklaration definiert den Teil des Programms, in dem der Wert berechnet und zurückgegeben wird. Die allgemeine Form des Funktionskopfes ist wie folgt:

Funktion <Name> [(<Liste formaler Parameter>)]: <Rückgabetyp>;

Die Funktion wird aktiviert, wenn sie aufgerufen wird. Beim Aufruf einer Funktion werden der Funktionsbezeichner und ggf. zu ihrer Auswertung notwendige Parameter angegeben. Ein Funktionsaufruf kann als Operand in Ausdrücke eingefügt werden. Wenn der Ausdruck ausgewertet wird, wird die Funktion ausgeführt und der Wert des Operanden wird zum Rückgabewert der Funktion.

Der Operatorteil des Funktionsblocks gibt die Anweisungen an, die ausgeführt werden müssen, wenn die Funktion aktiviert wird. Ein Modul muss mindestens eine Zuweisungsanweisung enthalten, die einem Funktionsbezeichner einen Wert zuweist. Das Ergebnis der Funktion ist der zuletzt zugewiesene Wert. Existiert keine solche Zuweisungsanweisung oder wurde sie nicht ausgeführt, so ist der Rückgabewert der Funktion undefiniert.

Wird beim Aufruf einer Funktion innerhalb eines Moduls ein Funktionsbezeichner verwendet, so wird die Funktion rekursiv ausgeführt.

4. Weiterleitungsbeschreibungen und Verbindung von Unterprogrammen. Richtlinie

Ein Programm kann mehrere Unterprogramme enthalten, d. h. die Struktur des Programms kann kompliziert sein. Diese Subroutinen können sich jedoch auf derselben Verschachtelungsebene befinden, daher muss die Deklaration der Subroutine zuerst kommen und dann der Aufruf, es sei denn, es wird eine spezielle Vorwärtsdeklaration verwendet.

Eine Prozedurdeklaration, die anstelle eines Anweisungsblocks eine Vorwärtsdirektive enthält, wird als Vorwärtsdeklaration bezeichnet. Irgendwo nach dieser Deklaration muss eine Prozedur durch eine definierende Deklaration definiert werden. Eine definierende Deklaration ist eine Deklaration, die denselben Prozedurbezeichner verwendet, aber die Liste der formalen Parameter weglässt und einen Anweisungsblock enthält. Die Vorwärtsdeklaration und die definierende Deklaration müssen im selben Teil der Prozedur- und Funktionsdeklaration erscheinen. Dazwischen können weitere Prozeduren und Funktionen deklariert werden, die auf die Forward-Declaration-Prozedur verweisen können. Somit ist eine gegenseitige Rekursion möglich.

Die Vorwärtsbeschreibung und die Definitionsbeschreibung sind die vollständige Beschreibung des Verfahrens. Das Verfahren gilt als unter Verwendung der Vorwärtsbeschreibung beschrieben.

Wenn das Programm ziemlich viele Unterroutinen enthält, ist das Programm nicht mehr visuell und es ist schwierig, darin zu navigieren. Um dies zu vermeiden, werden einige Unterprogramme als Quelldateien auf der Festplatte gespeichert und bei Bedarf über eine Kompilierungsdirektive mit dem Hauptprogramm beim Kompilieren verbunden.

Eine Direktive ist ein spezieller Kommentar, der überall in einem Programm platziert werden kann, wo ein normaler Kommentar stehen kann. Sie unterscheiden sich jedoch dadurch, dass die Direktive eine spezielle Notation hat: Direkt nach der schließenden Klammer ohne Leerzeichen wird das S-Zeichen geschrieben, und dann, wiederum ohne Leerzeichen, wird die Direktive angegeben.

Beispiel

1) {SE+} - mathematischen Koprozessor emulieren;

2) {SF+} - bilden einen entfernten Typ von Prozedur und Funktionsaufruf;

3) {SN+} - mathematischen Koprozessor verwenden;

4) {SR+} - Prüfen Sie, ob die Bereiche außerhalb der Grenzen liegen.

Einige Kompilierungsschalter können einen Parameter enthalten, zum Beispiel:

{$1 Dateiname} - fügt die benannte Datei in den Text des kompilierten Programms ein.

VORTRAG Nr. 4. Unterprogramme

1. Unterprogrammparameter

Die Beschreibung einer Prozedur oder Funktion spezifiziert eine Liste formaler Parameter. Jeder in einer formalen Parameterliste deklarierte Parameter ist lokal für die zu beschreibende Prozedur oder Funktion und kann durch seinen Bezeichner in dem Modul referenziert werden, das dieser Prozedur oder Funktion zugeordnet ist.

Es gibt drei Arten von Parametern: Wert, Variable und nicht typisierte Variable. Sie sind wie folgt gekennzeichnet.

1. Eine Gruppe von Parametern ohne vorangestelltes Schlüsselwort ist eine Liste von Wertparametern.

2. Eine Gruppe von Parametern, denen das Schlüsselwort const vorangestellt ist und auf die ein Typ folgt, ist eine Liste konstanter Parameter.

3. Eine Gruppe von Parametern, denen das Schlüsselwort var vorangestellt ist und auf die ein Typ folgt, ist eine Liste nicht typisierter Variablenparameter.

4. Eine Gruppe von Parametern, denen das Schlüsselwort var oder const vorangestellt ist und nicht von einem Typ gefolgt wird, ist eine Liste nicht typisierter Variablenparameter.

2. Arten von Unterprogrammparametern

Wertparameter

Ein formaler Wertparameter wird als lokale Variable der Prozedur oder Funktion behandelt, mit der Ausnahme, dass er seinen Anfangswert vom entsprechenden tatsächlichen Parameter ableitet, wenn die Prozedur oder Funktion aufgerufen wird. Änderungen, die ein formaler Wertparameter erfährt, wirken sich nicht auf den Wert des tatsächlichen Parameters aus. Der entsprechende Istwert-Parameterwert muss ein Ausdruck sein, und sein Wert darf kein Dateityp oder ein Strukturtyp sein, der einen Dateityp enthält.

Der Aktualparameter muss von einem Typ sein, der zuweisungskompatibel zum Typ des formalen Wertparameters ist. Wenn der Parameter vom Typ String ist, dann hat der formale Parameter ein Größenattribut von 255.

Konstante Parameter

Formale konstante Parameter funktionieren genauso wie eine schreibgeschützte lokale Variable, die ihren Wert erhält, wenn eine Prozedur oder Funktion vom entsprechenden aktuellen Parameter aufgerufen wird. Zuweisungen an einen formal konstanten Parameter sind nicht erlaubt. Ein formaler konstanter Parameter kann auch nicht als aktueller Parameter an eine andere Prozedur oder Funktion übergeben werden. Ein konstanter Parameter, der einem tatsächlichen Parameter in einer Prozedur- oder Funktionsanweisung entspricht, muss denselben Regeln folgen wie der tatsächliche Parameterwert.

In Fällen, in denen ein formaler Parameter seinen Wert nicht ändert, wenn eine Prozedur oder Funktion ausgeführt wird, sollte anstelle eines Wertparameters ein konstanter Parameter verwendet werden. Konstante Parameter ermöglichen die Implementierung einer Prozedur oder Funktion zum Schutz vor versehentlichen Zuweisungen an einen formalen Parameter. Außerdem kann der Compiler für Struktur- und Zeichenfolgentypparameter effizienteren Code generieren, wenn er anstelle von Wertparametern für Konstantenparameter verwendet wird.

Variable Parameter

Ein variabler Parameter wird verwendet, wenn ein Wert von einer Prozedur oder Funktion an das aufrufende Programm übergeben werden muss. Der entsprechende Aktualparameter in einer Prozedur- oder Funktionsaufrufanweisung muss eine Variablenreferenz sein. Wenn eine Prozedur oder Funktion aufgerufen wird, wird die formale Parametervariable durch die tatsächliche Variable ersetzt, alle Änderungen im Wert der formalen Parametervariablen werden im tatsächlichen Parameter widergespiegelt.

Innerhalb einer Prozedur oder Funktion führt jeder Verweis auf einen formalen Variablenparameter zum Zugriff auf den tatsächlichen Parameter selbst. Der Typ des Aktualparameters muss mit dem Typ des formalen Variablenparameters übereinstimmen, aber diese Einschränkung kann umgangen werden, indem ein untypisierter Variablenparameter verwendet wird).

Nicht typisierte Parameter

Wenn der formale Parameter ein untypisierter Variablenparameter ist, dann kann der entsprechende aktuelle Parameter eine beliebige Referenz auf eine Variable oder Konstante sein, unabhängig von ihrem Typ. Ein mit dem Schlüsselwort var deklarierter nicht typisierter Parameter kann geändert werden, während ein mit dem Schlüsselwort const deklarierter nicht typisierter Parameter schreibgeschützt ist.

In einer Prozedur oder Funktion hat ein untypisierter Variablenparameter keinen Typ, d. h. er ist mit Variablen aller Typen inkompatibel, bis ihm durch Variablentypzuweisung ein bestimmter Typ zugewiesen wird.

Obwohl nicht typisierte Parameter mehr Flexibilität bieten, sind mit ihrer Verwendung einige Risiken verbunden. Der Compiler kann die Gültigkeit von Operationen für nicht typisierte Variablen nicht überprüfen.

Prozedurale Variablen

Nach der Definition eines prozeduralen Typs wird es möglich, Variablen dieses Typs zu beschreiben. Solche Variablen werden als prozedurale Variablen bezeichnet. Wie einer ganzzahligen Variablen, der ein Wert eines ganzzahligen Typs zugewiesen werden kann, kann einer prozeduralen Variablen ein Wert eines prozeduralen Typs zugewiesen werden. Ein solcher Wert könnte natürlich eine andere Prozedurvariable sein, aber es könnte auch eine Prozedur- oder Funktionskennung sein. Die Deklaration einer Prozedur oder Funktion kann in diesem Zusammenhang als Beschreibung einer speziellen Art von Konstanten angesehen werden, deren Wert die Prozedur oder Funktion ist.

Wie bei jeder anderen Zuweisung müssen die Werte der Variablen auf der linken Seite und auf der rechten Seite zuweisungskompatibel sein. Prozedurale Typen müssen, um zuweisungskompatibel zu sein, die gleiche Anzahl von Parametern haben, und die Parameter an den entsprechenden Positionen müssen vom gleichen Typ sein. Parameternamen in einer prozeduralen Typdeklaration haben keine Auswirkung.

Um die Zuweisungskompatibilität zu gewährleisten, muss eine Prozedur oder Funktion, wenn sie einer Prozedurvariablen zugewiesen werden soll, außerdem die folgenden Anforderungen erfüllen:

1) es sollte keine Standardprozedur oder -funktion sein;

2) eine solche Prozedur oder Funktion kann nicht verschachtelt werden;

3) ein solches Verfahren darf kein Inline-Verfahren sein;

4) es darf keine Unterbrechungsprozedur sein.

Standardprozeduren und -funktionen sind die im Systemmodul beschriebenen Prozeduren und Funktionen wie Writeln, Readln, Chr, Ord. Verschachtelte Prozeduren und Funktionen mit prozeduralen Variablen können nicht verwendet werden. Eine Prozedur oder Funktion wird als verschachtelt betrachtet, wenn sie innerhalb einer anderen Prozedur oder Funktion deklariert wird.

Die Verwendung von prozeduralen Typen ist nicht nur auf prozedurale Variablen beschränkt. Wie jeder andere Typ kann ein prozeduraler Typ an der Deklaration eines strukturellen Typs teilnehmen.

Wenn einer Prozedurvariablen der Wert einer Prozedur zugewiesen wird, passiert auf der physikalischen Ebene, dass die Adresse der Prozedur in der Variablen gespeichert wird. Tatsächlich ist eine Prozedurvariable einer Zeigervariablen sehr ähnlich, nur dass sie nicht auf Daten verweist, sondern auf eine Prozedur oder Funktion. Wie ein Zeiger belegt eine prozedurale Variable 4 Bytes (zwei Wörter), die eine Speicheradresse enthalten. Das erste Wort speichert den Offset, das zweite Wort speichert das Segment.

Prozedurale Parameter

Da prozedurale Typen in jedem Kontext verwendet werden können, ist es möglich, Prozeduren oder Funktionen zu beschreiben, die Prozeduren und Funktionen als Parameter annehmen. Prozedurale Parameter sind besonders nützlich, wenn Sie eine gemeinsame Aktion für mehrere Prozeduren oder Funktionen ausführen müssen.

Wenn eine Prozedur oder Funktion als Parameter übergeben werden soll, muss sie denselben Typkompatibilitätsregeln folgen wie die Zuweisung. Das heißt, solche Prozeduren oder Funktionen müssen mit der far-Direktive kompiliert werden, sie können keine eingebauten Funktionen sein, sie können nicht verschachtelt werden und sie können nicht mit den Inline- oder Interrupt-Attributen beschrieben werden.

VORTRAG Nr. 5. String-Datentyp

1. Zeichenfolge in Pascal eingeben

Eine Folge von Zeichen einer bestimmten Länge wird als String bezeichnet. Variablen vom Typ String werden definiert, indem der Name der Variablen, das reservierte Wort String und optional, aber nicht notwendigerweise, die maximale Größe, d. h. die Länge des Strings, in eckigen Klammern angegeben werden. Wenn Sie die maximale Zeichenfolgengröße nicht festlegen, beträgt sie standardmäßig 255, d. h. die Zeichenfolge besteht aus 255 Zeichen.

Auf jedes Element einer Zeichenfolge kann über seine Nummer verwiesen werden. Allerdings werden Strings als Ganzes ein- und ausgegeben, nicht Element für Element, wie es bei Arrays der Fall ist. Die Anzahl der eingegebenen Zeichen darf die in der maximalen Zeichenfolgengröße festgelegte nicht überschreiten. Wenn also eine solche Überschreitung auftritt, werden die „zusätzlichen“ Zeichen ignoriert.

2. Prozeduren und Funktionen für Variablen vom Typ String

1. Function Copy(S: String; Index, Count: Integer): String;

Gibt einen Teilstring eines Strings zurück. S ist ein Ausdruck vom Typ String.

Index und Count sind ganzzahlige Ausdrücke. Die Funktion gibt eine Zeichenfolge zurück, die Count Zeichen enthält, beginnend an der Indexposition. Wenn Index größer als die Länge von S ist, gibt die Funktion einen leeren String zurück.

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

Entfernt einen Teilstring von Zeichen der Länge Count aus dem String S, beginnend an Position Index. S ist eine Variable vom Typ String. Index und Count sind ganzzahlige Ausdrücke. Wenn Index größer als die Länge von S ist, werden keine Zeichen entfernt.

3. Prozedur Insert(Source: String; var S: String; Index: Integer);

Verkettet eine Teilzeichenfolge zu einer Zeichenfolge, beginnend an einer angegebenen Position. Quelle ist ein Ausdruck vom Typ String. S ist eine Variable vom Typ String beliebiger Länge. Index ist ein Ausdruck vom Typ Integer. Insert fügt Source in S ein, beginnend an Position S[Index].

4. Funktionslänge (S: String): Ganzzahl;

Gibt die Anzahl der tatsächlich in String S verwendeten Zeichen zurück. Beachten Sie, dass bei der Verwendung von nullterminierten Strings die Anzahl der Zeichen nicht unbedingt gleich der Anzahl der Bytes ist.

5. Funktion Pos(Substr: String; S: String): Integer;

Sucht nach einem Teilstring in einem String. Pos sucht in S nach Substr und gibt einen ganzzahligen Wert zurück, der der Index des ersten Zeichens von Substr in S ist. Wenn Substr nicht gefunden wird, gibt Pos null zurück.

3. Aufzeichnungen

Ein Datensatz ist eine Sammlung einer begrenzten Anzahl logisch zusammenhängender Komponenten, die zu verschiedenen Typen gehören. Die Bestandteile eines Datensatzes werden als Felder bezeichnet, die jeweils durch einen Namen identifiziert werden. Ein Datensatzfeld enthält den Namen des Felds, gefolgt von einem Doppelpunkt, um den Typ des Felds anzugeben. Datensatzfelder können von jedem in Pascal erlaubten Typ sein, mit Ausnahme des Dateityps.

Die Beschreibung eines Datensatzes in der Sprache Pascal erfolgt über das Dienstwort RECORD, gefolgt von der Beschreibung der Bestandteile des Datensatzes. Die Beschreibung des Eintrags endet mit dem Dienstwort END.

Beispielsweise enthält ein Notizbuch Nachnamen, Initialen und Telefonnummern, daher ist es praktisch, eine separate Zeile in einem Notizbuch als den folgenden Eintrag darzustellen:

Geben Sie Zeile = Datensatz ein

FIO: Zeichenkette[20];

TEL: Zeichenkette[7];

end;

var str: Zeile;

Datensatzbeschreibungen sind auch ohne Verwendung des Typnamens möglich, zum Beispiel:

var str : Aufzeichnung

FIO : Zeichenkette[20];

TEL : Zeichenkette[7];

end;

Der Verweis auf einen Datensatz als Ganzes ist nur in Zuweisungsanweisungen erlaubt, in denen Datensatznamen des gleichen Typs links und rechts vom Zuweisungszeichen verwendet werden. In allen anderen Fällen werden getrennte Datensatzfelder betrieben. Um auf eine einzelne Datensatzkomponente zu verweisen, müssen Sie den Namen des Datensatzes angeben und, durch einen Punkt getrennt, den Namen des gewünschten Felds angeben. Ein solcher Name wird zusammengesetzter Name genannt. Eine Datensatzkomponente kann auch ein Datensatz sein, in diesem Fall enthält der Distinguished Name nicht zwei, sondern mehr Namen.

Das Referenzieren von Datensatzkomponenten kann vereinfacht werden, indem der with append-Operator verwendet wird. Es ermöglicht Ihnen, die zusammengesetzten Namen, die jedes Feld charakterisieren, durch reine Feldnamen zu ersetzen und den Datensatznamen in der Join-Anweisung zu definieren.

Manchmal hängt der Inhalt eines einzelnen Datensatzes vom Wert eines seiner Felder ab. In der Pascal-Sprache ist eine Datensatzbeschreibung erlaubt, die aus gemeinsamen und abweichenden Teilen besteht. Der Variantenteil wird mit dem Fall P des Konstrukts angegeben, wobei P der Name des Felds aus dem gemeinsamen Teil des Datensatzes ist. Die möglichen Werte, die von diesem Feld akzeptiert werden, werden auf die gleiche Weise wie in der Variantenanweisung aufgelistet. Anstatt jedoch die auszuführende Aktion anzugeben, wie dies in einer Variant-Anweisung der Fall ist, werden die Variant-Felder in Klammern angegeben. Die Beschreibung des Variantenteils endet mit dem Servicewort end. In der Überschrift des Variantenteils kann der Feldtyp P angegeben werden. Datensätze werden mit typisierten Konstanten initialisiert.

4. Sätze

Das Konzept einer Menge in der Pascal-Sprache basiert auf dem mathematischen Konzept von Mengen: Es ist eine begrenzte Sammlung verschiedener Elemente. Ein Aufzählungs- oder Intervalldatentyp wird verwendet, um einen konkreten Mengentyp zu konstruieren. Der Typ der Elemente, aus denen eine Menge besteht, wird als Basistyp bezeichnet.

Ein multipler Typ wird mit dem Satz von Funktionswörtern beschrieben, zum Beispiel:

Typ M = Satz von B;

Hier ist M der Pluraltyp, B ist der Basistyp.

Die Zugehörigkeit von Variablen zu einem Pluraltyp kann direkt im Variablendeklarationsabschnitt bestimmt werden.

Konstanten vom Typ Menge werden als eine eingeklammerte Folge von Elementen oder Bereichen des Basistyps geschrieben, die durch Kommas getrennt sind. Eine Konstante der Form [] bedeutet eine leere Teilmenge.

Eine Menge umfasst eine Menge von Elementen des Basistyps, alle Teilmengen der gegebenen Menge und die leere Teilmenge. Wenn der Basistyp, auf dem die Menge aufgebaut ist, K Elemente hat, dann ist die Anzahl der Teilmengen, die in dieser Menge enthalten sind, gleich 2 hoch K. Die Reihenfolge, in der die Elemente des Basistyps in den Konstanten aufgelistet sind, ist gleichgültig . Der Wert einer Variablen eines multiplen Typs kann durch eine Konstruktion der Form [T] angegeben werden, wobei T eine Variable des Basistyps ist.

Zuweisungs- (:=), Vereinigungs- (+), Schnittpunkt- (*) und Subtraktions- (-) Operationen sind auf Variablen und Konstanten eines Mengentyps anwendbar. Das Ergebnis dieser Operationen ist ein Wert vom Typ Plural:

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

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

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

Die folgenden Operationen sind auf mehrere Werte anwendbar: Identität (=), Nichtidentität (<>), enthalten in (<=), enthält (>=). Das Ergebnis dieser Operationen hat einen booleschen Typ:

1) ['A','B'] = ['A','C'] ergibt FALSE ;

2) ['A','B'] <> ['A','C'] ergibt TRUE;

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

4) ['C','D'] >= ['A'] ergibt FALSE.

Um mit Werten eines Mengentyps zu arbeiten, wird zusätzlich zu diesen Operationen die In-Operation verwendet, die prüft, ob das Element des Basistyps links vom Operationszeichen zu der Menge rechts vom Operationszeichen gehört . Das Ergebnis dieser Operation ist ein boolescher Wert. Die Operation, zu prüfen, ob ein Element zu einer Menge gehört, wird oft anstelle von Vergleichsoperationen verwendet.

Wenn mehrere Datentypen in Programmen verwendet werden, werden Operationen an Bitketten von Daten durchgeführt. Jeder Wert des multiplen Typs im Computerspeicher entspricht einer Binärziffer.

Werte eines multiplen Typs können keine Elemente einer E/A-Liste sein. In jeder konkreten Implementierung des Compilers aus der Pascal-Sprache ist die Anzahl der Elemente des Basistyps, auf dem die Menge aufgebaut ist, begrenzt.

Die Initialisierung mehrerer Typwerte erfolgt über typisierte Konstanten.

Hier sind einige Verfahren zum Arbeiten mit Mengen.

1. Prozedur ausschließen (var S: Menge von T; I:T);

Entfernt Element I aus Menge S. S ist eine Variable vom Typ "set" und I ist ein Ausdruck eines Typs, der mit dem ursprünglichen Typ S kompatibel ist. Exclude(S, I) ist dasselbe wie S : = S - [I], generiert aber effizienteren Code.

2. Prozedur Include(var S: Menge von T; I:T);

Fügt der Menge S ein Element I hinzu. S ist eine Variable vom Typ "set" und I ist ein Ausdruck eines Typs, der mit dem Typ S kompatibel ist. Das Konstrukt Include(S, I) ist dasselbe wie S : = S + [ I], generiert aber effizienteren Code.

VORTRAG Nr. 6. Dateien

1. Dateien. Dateioperationen

Die Einführung des Dateityps in die Pascal-Sprache wird durch die Notwendigkeit verursacht, die Fähigkeit bereitzustellen, mit peripheren (externen) Computergeräten zu arbeiten, die für die Eingabe, Ausgabe und Datenspeicherung ausgelegt sind.

Der Dateidatentyp (oder Datei) definiert eine geordnete Sammlung einer beliebigen Anzahl von Komponenten des gleichen Typs. Die gemeinsame Eigenschaft von Array, Set und Record ist, dass die Anzahl ihrer Komponenten beim Schreiben des Programms festgelegt wird, während die Anzahl der Dateikomponenten im Programmtext nicht festgelegt wird und beliebig sein kann.

Beim Arbeiten mit Dateien werden I/O-Operationen durchgeführt. Eine Eingabeoperation bedeutet das Übertragen von Daten von einem externen Gerät (von einer Eingabedatei) in den Hauptspeicher eines Computers, eine Ausgabeoperation ist eine Übertragung von Daten vom Hauptspeicher zu einem externen Gerät (in eine Ausgabedatei). Dateien auf externen Geräten werden oft als physische Dateien bezeichnet. Ihre Namen werden vom Betriebssystem bestimmt.

In Pascal-Programmen werden Dateinamen mit Strings angegeben. Um mit Dateien im Programm zu arbeiten, müssen Sie eine Dateivariable definieren. Pascal unterstützt drei Dateitypen: Textdateien, Komponentendateien, nicht typisierte Dateien.

Dateivariablen, die in einem Programm deklariert werden, werden als logische Dateien bezeichnet. Alle grundlegenden Prozeduren und Funktionen, die I/O-Daten liefern, funktionieren nur mit logischen Dateien. Die physische Datei muss der logischen Datei zugeordnet werden, bevor die Dateiöffnungsprozeduren durchgeführt werden können.

Textdateien

Einen besonderen Platz in der Pascal-Sprache nehmen Textdateien ein, deren Bestandteile zeichenartig sind. Zur Beschreibung von Textdateien definiert die Sprache den Standardtyp Text:

var TF1, TF2: Text;

Textdateien sind eine Folge von Zeilen, und Zeilen sind eine Folge von Zeichen. Zeilen sind von variabler Länge, jede Zeile endet mit einem Zeilenabschlusszeichen.

Komponentendateien

Eine Komponente oder typisierte Datei ist eine Datei mit dem deklarierten Typ ihrer Komponenten. Komponentendateien bestehen aus maschinellen Darstellungen variabler Werte; sie speichern Daten in der gleichen Form wie Computerspeicher.

Die Beschreibung der Dateitypwerte lautet:

Typ M = Datei von T;

wobei M der Name des Dateityps ist;

T - Komponententyp.

Dateikomponenten können alle skalaren Typen sein und von strukturierten Typen - Arrays, Sets, Records. In fast allen spezifischen Implementierungen der Pascal-Sprache ist das Konstrukt „Datei von Dateien“ nicht erlaubt.

Alle Operationen an Komponentendateien werden unter Verwendung von Standardprozeduren durchgeführt.

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

Untypisierte Dateien

Nicht typisierte Dateien ermöglichen es Ihnen, beliebige Abschnitte des Computerspeichers auf die Festplatte zu schreiben und sie von der Festplatte in den Speicher zu lesen. Untypisierte Dateien werden wie folgt beschrieben:

var f: Datei;

Jetzt listen wir die Verfahren und Funktionen für die Arbeit mit verschiedenen Dateitypen auf.

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

Die AssignFile-Prozedur ordnet einen externen Dateinamen einer Dateivariablen zu.

F ist eine Dateivariable eines beliebigen Dateityps, FileName ist ein String-Ausdruck oder ein PChar-Ausdruck, wenn erweiterte Syntax zulässig ist. Alle weiteren Operationen mit F werden mit einer externen Datei durchgeführt.

Sie können eine Prozedur nicht mit einer bereits geöffneten Dateivariablen verwenden.

2. Prozedur Schließen (varF);

Die Prozedur unterbricht die Verknüpfung zwischen der Dateivariablen und der Datei auf der externen Festplatte und schließt die Datei.

F ist eine Dateivariable eines beliebigen Dateityps, die durch die Prozeduren Reset, Rewrite oder Append geöffnet wird. Die mit F verknüpfte externe Datei wird vollständig modifiziert und dann geschlossen, wodurch der Dateideskriptor zur Wiederverwendung freigegeben wird.

Mit der Direktive {SI+} können Sie Fehler während der Programmausführung mithilfe der Ausnahmebehandlung behandeln. Wenn die Direktive {$1-} deaktiviert ist, müssen Sie IOResult verwenden, um nach E/A-Fehlern zu suchen.

3.Funktion Eof(var F): Boolean;

{Typisierte oder nicht typisierte Dateien}

Funktion Eof[(var F: Text)]: Boolean;

{Textdateien}

Überprüft, ob die aktuelle Dateiposition das Ende der Datei ist oder nicht.

Eof(F) gibt True zurück, wenn die aktuelle Dateiposition nach dem letzten Zeichen der Datei liegt oder wenn die Datei leer ist; andernfalls gibt Eof(F) False zurück.

Mit der Direktive {SI+} können Sie Fehler während der Programmausführung mithilfe der Ausnahmebehandlung behandeln. Wenn die Direktive {SI-} deaktiviert ist, müssen Sie IOResult verwenden, um nach E/A-Fehlern zu suchen.

4. Prozedur löschen (var F);

Löscht die mit F verknüpfte externe Datei.

F ist eine Dateivariable eines beliebigen Dateityps.

Vor Aufruf der Erase-Prozedur muss die Datei geschlossen werden.

Mit der Direktive {SI+} können Sie Fehler während der Programmausführung mithilfe der Ausnahmebehandlung behandeln. Wenn die Direktive {SI-} deaktiviert ist, müssen Sie IOResult verwenden, um nach E/A-Fehlern zu suchen.

5. Funktion FileSize(var F): Integer;

Gibt die Größe der Datei F in Bytes zurück. Wenn F jedoch eine typisierte Datei ist, gibt FileSize die Anzahl der Datensätze in der Datei zurück. Die Datei muss geöffnet sein, bevor Sie die FileSize-Funktion verwenden. Wenn die Datei leer ist, gibt FileSize(F) Null zurück. F ist eine Variable eines beliebigen Dateityps.

6.Funktion FilePos(var F): LongInt;

Gibt die aktuelle Position einer Datei innerhalb einer Datei zurück.

Vor Verwendung der FilePos-Funktion muss die Datei geöffnet sein. Die FilePos-Funktion wird bei Textdateien nicht verwendet. F ist eine Variable eines beliebigen Dateityps, mit Ausnahme des Texttyps.

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

Öffnet eine vorhandene Datei.

F ist eine Variable eines beliebigen Dateityps, die mit AssignFile einer externen Datei zugeordnet ist. RecSize ist ein optionaler Ausdruck, der verwendet wird, wenn F eine nicht typisierte Datei ist. Wenn F eine nicht typisierte Datei ist, bestimmt RecSize die Datensatzgröße, die beim Übertragen von Daten verwendet wird. Wenn RecSize weggelassen wird, beträgt die Standarddatensatzgröße 128 Byte.

Die Reset-Prozedur öffnet eine vorhandene externe Datei, die der Dateivariablen F zugeordnet ist. Wenn keine externe Datei mit diesem Namen vorhanden ist, tritt ein Laufzeitfehler auf. Wenn die mit F verknüpfte Datei bereits geöffnet ist, wird sie zuerst geschlossen und dann erneut geöffnet. Die aktuelle Dateiposition wird auf den Anfang der Datei gesetzt.

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

Erstellt und öffnet eine neue Datei.

F ist eine Variable eines beliebigen Dateityps, die mit AssignFile einer externen Datei zugeordnet ist. RecSize ist ein optionaler Ausdruck, der verwendet wird, wenn F eine nicht typisierte Datei ist. Wenn F eine nicht typisierte Datei ist, bestimmt RecSize die Datensatzgröße, die beim Übertragen von Daten verwendet wird. Wenn RecSize weggelassen wird, beträgt die Standarddatensatzgröße 128 Bytes.

Die Rewrite-Prozedur erstellt eine neue externe Datei mit dem Namen, der F zugeordnet ist. Wenn bereits eine externe Datei mit demselben Namen existiert, wird sie gelöscht und eine neue leere Datei erstellt.

9. Prozedursuche (var F; N: LongInt);

Verschiebt die aktuelle Dateiposition zur angegebenen Komponente. Sie können das Verfahren nur mit offenen typisierten oder nicht typisierten Dateien verwenden.

Die aktuelle Position der Datei F wird auf die Nummer N verschoben. Die Nummer der ersten Komponente der Datei ist 0.

Die Seek(F, FileSize(F))-Anweisung verschiebt die aktuelle Dateiposition an das Ende der Datei.

10. Prozedur Append(var F: Text);

Öffnet eine vorhandene Textdatei, um Informationen an das Ende der Datei anzuhängen (append).

Wenn eine externe Datei mit dem angegebenen Namen nicht vorhanden ist, tritt ein Laufzeitfehler auf. Wenn Datei F bereits geöffnet ist, wird sie geschlossen und erneut geöffnet. Die aktuelle Dateiposition wird auf das Dateiende gesetzt.

11.Funktion Eoln[(var F: Text)]: Boolean;

Überprüft, ob die aktuelle Dateiposition das Ende einer Zeile in einer Textdatei ist.

Eoln(F) gibt True zurück, wenn sich die aktuelle Dateiposition am Ende einer Zeile oder Datei befindet; andernfalls gibt Eoln(F) False zurück.

12. Prozedur Read(F, V1 [, V2,..., Vn]);

{Typisierte und nicht typisierte Dateien}

Prozedur Read([var F: Text;] V1 [, V2,..., Vn]);

{Textdateien}

Bei typisierten Dateien liest die Prozedur die Dateikomponente in eine Variable. Bei jedem Lesevorgang rückt die aktuelle Position in der Datei zum nächsten Element vor.

Bei Textdateien werden ein oder mehrere Werte in eine oder mehrere Variablen eingelesen.

Bei Variablen vom Typ String liest Read alle Zeichen bis (aber nicht einschließlich) der nächsten Zeilenende-Markierung oder bis Eof(F) True ergibt. Die resultierende Zeichenfolge wird der Variablen zugewiesen.

Bei einer Variablen vom Typ Integer oder Real wartet die Prozedur auf eine Folge von Zeichen, die nach den Regeln der Pascal-Syntax eine Zahl bilden. Das Lesen stoppt, wenn das erste Leerzeichen, der erste Tabulator oder das erste Zeilenende gefunden wird oder wenn Eof(F) zu True ausgewertet wird. Wenn die numerische Zeichenfolge nicht dem erwarteten Format entspricht, tritt ein E/A-Fehler auf.

13. Prozedur Readln([var F: Text;] V1 [, V2..., Vn]);

Es ist eine Erweiterung der Read-Prozedur und für Textdateien definiert. Liest eine Zeichenkette in der Datei, einschließlich der Zeilenende-Markierung, und springt zum Anfang der nächsten Zeile. Der Aufruf der Funktion Readln(F) ohne Parameter verschiebt die aktuelle Dateiposition an den Anfang der nächsten Zeile, falls vorhanden, andernfalls springt sie an das Ende der Datei.

14. Funktion SeekEof[(var F: Text)]: Boolean;

Gibt das Dateiende zurück und kann nur für offene Textdateien verwendet werden. Wird normalerweise verwendet, um numerische Werte aus Textdateien zu lesen.

15. Funktion SeekEoln[(var F: Text)]: Boolean;

Gibt den Zeilenabschluss in einer Datei zurück und kann nur für offene Textdateien verwendet werden. Wird normalerweise verwendet, um numerische Werte aus Textdateien zu lesen.

16. Prozedur Write([var F: Text;] P1 [, P2,..., Pn]);

{Textdateien}

Schreibt einen oder mehrere Werte in eine Textdatei.

Jeder Eingabeparameter muss vom Typ Char, einem der Integer-Typen (Byte, ShortInt, Word, Longint, Cardinal), einem der Fließkommatypen (Single, Real, Double, Extended, Currency), einem der String-Typen ( PChar, AisiString , ShortString) oder einen der booleschen Typen (Boolean, Bool).

Prozedur Write(F, V1,..., Vn);

{Typisierte Dateien}

Schreibt eine Variable in eine Dateikomponente. Die Variablen VI...., Vn müssen vom gleichen Typ sein wie die Dateielemente. Jedes Mal, wenn eine Variable geschrieben wird, wird die aktuelle Position in der Datei zum nächsten Element verschoben.

17. Prozedur Writeln([var F: Text;] [P1, P2,..., Pn]);

{Textdateien}

Führt einen Schreibvorgang durch und platziert dann eine Zeilenende-Markierung in der Datei.

Der Aufruf von Writeln(F) ohne Parameter schreibt eine Zeilenende-Markierung in die Datei. Die Datei muss für die Ausgabe geöffnet sein.

2. Module. Arten von Modulen

Ein Modul (1Ж1Т) in Pascal ist eine speziell entworfene Bibliothek von Subroutinen. Ein Modul kann im Gegensatz zu einem Programm nicht alleine gestartet werden, es kann nur am Aufbau von Programmen und anderen Modulen teilnehmen. Mit Modulen können Sie persönliche Bibliotheken mit Prozeduren und Funktionen erstellen und Programme fast jeder Größe erstellen.

Ein Modul in Pascal ist eine separat gespeicherte und unabhängig kompilierte Programmeinheit. Im Allgemeinen ist ein Modul eine Sammlung von Softwareressourcen, die von anderen Programmen verwendet werden sollen. Unter Programmressourcen werden beliebige Elemente der Pascal-Sprache verstanden: Konstanten, Typen, Variablen, Unterprogramme. Das Modul selbst ist kein ausführbares Programm, seine Elemente werden von anderen Programmeinheiten verwendet.

Alle Programmelemente des Moduls lassen sich in zwei Teile gliedern:

1) Programmelemente, die für die Verwendung durch andere Programme oder Module bestimmt sind, solche Elemente werden außerhalb des Moduls sichtbar genannt;

2) Softwareelemente, die nur für den Betrieb des Moduls selbst erforderlich sind, werden als unsichtbar (oder versteckt) bezeichnet.

Dementsprechend enthält das Modul zusätzlich zum Header drei Hauptteile, genannt Schnittstelle, ausführbar und initialisiert.

Im Allgemeinen hat ein Modul folgenden Aufbau:

unit <Modulname>; {Modultitel}

Schnittstelle

{Beschreibung der sichtbaren Programmelemente des Moduls}

Implementierung

{Beschreibung der versteckten Programmierelemente des Moduls}

beginnen

{Anweisungen zur Initialisierung von Modulelementen}

Ende.

In einem bestimmten Fall darf das Modul keinen Implementierungsteil und keinen Initialisierungsteil enthalten, dann sieht die Modulstruktur wie folgt aus:

unit <Modulname>; {Modultitel}

Schnittstelle

{Beschreibung der sichtbaren Programmelemente des Moduls}

Implementierung

Ende.

Die Verwendung von Prozeduren und Funktionen in Modulen hat ihre eigenen Besonderheiten. Der Kopf des Unterprogramms enthält alle Informationen, die für den Aufruf erforderlich sind: Name, Liste und Typ der Parameter, Ergebnistyp für Funktionen. Diese Informationen müssen anderen Programmen und Modulen zur Verfügung stehen. Andererseits kann der Text einer Subroutine, die ihren Algorithmus implementiert, nicht von anderen Programmen und Modulen verwendet werden. Daher werden die Überschriften von Prozeduren und Funktionen in den Schnittstellenteil des Moduls und der Text in den Implementierungsteil gestellt.

Der Schnittstellenteil des Moduls enthält nur sichtbare (für andere Programme und Module zugängliche) Kopfzeilen von Prozeduren und Funktionen (ohne das Dienstwort vorwärts). Der vollständige Text der Prozedur oder Funktion wird in den Implementierungsteil gestellt, und der Header darf keine Liste formaler Parameter enthalten.

Der Quellcode des Moduls muss mit der Make-Direktive des Compile-Untermenüs kompiliert und auf die Festplatte geschrieben werden. Das Ergebnis der Modulkompilierung ist eine Datei mit der Erweiterung . TPU (Turbo-Pascal-Einheit). Der Basisname des Moduls wird aus dem Header des Moduls entnommen.

Um ein Modul mit dem Programm zu verbinden, müssen Sie seinen Namen in der Modulbeschreibung angeben, zum Beispiel:

verwendet Crt, Graph;

Falls die Namen von Variablen im Schnittstellenteil des Moduls und in dem Programm, das dieses Modul verwendet, gleich sind, wird auf die im Programm beschriebene Variable verwiesen. Um auf eine in einem Modul deklarierte Variable zu verweisen, müssen Sie einen zusammengesetzten Namen verwenden, der aus dem Modulnamen und dem Variablennamen, getrennt durch einen Punkt, besteht. Die Verwendung zusammengesetzter Namen gilt nicht nur für Variablennamen, sondern für alle im Schnittstellenteil des Moduls deklarierten Namen.

Die rekursive Verwendung von Modulen ist verboten.

Wenn ein Modul einen Initialisierungsabschnitt hat, werden die Anweisungen in diesem Abschnitt ausgeführt, bevor das Programm, das dieses Modul verwendet, mit der Ausführung beginnt.

Lassen Sie uns die Arten von Modulen auflisten.

1. SYSTEM-Modul.

Das SYSTEM-Modul implementiert untergeordnete Unterstützungsroutinen für alle eingebauten Einrichtungen wie E/A, String-Manipulation, Gleitkommaoperationen und dynamische Speicherzuordnung.

Das SYSTEM-Modul enthält alle standardmäßigen und eingebauten Pascal-Routinen und -Funktionen. Jede Pascal-Subroutine, die nicht Teil von Standard-Pascal ist und in keinem anderen Modul gefunden wird, ist im System-Modul enthalten. Dieses Modul wird automatisch in allen Programmen verwendet und muss nicht in der uses-Anweisung angegeben werden.

2. DOS-Modul.

Das DOS-Modul implementiert zahlreiche Pascal-Routinen und -Funktionen, die den am häufigsten verwendeten DOS-Aufrufen wie GetTime, SetTime, DiskSize usw. entsprechen.

3. CRT-Modul.

Das CRT-Modul implementiert eine Reihe leistungsstarker Programme, die eine vollständige Kontrolle über die Funktionen des PCs bieten, wie z. B. Bildschirmmodussteuerung, erweiterte Tastaturcodes, Farben, Fenster und Sounds. Das CRT-Modul kann nur in Programmen verwendet werden, die auf Personal Computern IBM PC, PC AT, PS/2 von IBM laufen und mit diesen voll kompatibel sind.

Einer der Hauptvorteile der Verwendung des CRT-Moduls ist die größere Geschwindigkeit und Flexibilität bei der Durchführung von Bildschirmoperationen. Programme, die nicht mit dem CRT-Modul arbeiten, zeigen Informationen auf dem Bildschirm unter Verwendung des Betriebssystems DOS an, was mit zusätzlichem Overhead verbunden ist. Bei Verwendung des CRT-Moduls werden Ausgabeinformationen direkt an das BIOS (Basic Input/Output System) oder für noch schnellere Operationen direkt an den Videospeicher gesendet.

4. GRAPH-Modul.

Mit den in diesem Modul enthaltenen Prozeduren und Funktionen können Sie verschiedene Grafiken auf dem Bildschirm erstellen.

5. OVERLAY-Modul.

Mit dem OVERLAY-Modul können Sie den Speicherbedarf eines Real-Modus-DOS-Programms reduzieren. Tatsächlich ist es möglich, Programme zu schreiben, die die Gesamtmenge des verfügbaren Speichers überschreiten, da immer nur ein Teil des Programms im Speicher ist.

VORTRAG Nr. 7. Dynamisches Gedächtnis

1. Referenzdatentyp. dynamisches Gedächtnis. Dynamische Variablen

Eine statische Variable (statisch zugewiesen) ist eine explizit im Programm deklarierte Variable, auf die mit Namen verwiesen wird. Der Platz im Speicher zum Platzieren statischer Variablen wird bestimmt, wenn das Programm kompiliert wird. Im Gegensatz zu solchen statischen Variablen können Pascal-Programme dynamische Variablen erstellen. Die Haupteigenschaft dynamischer Variablen besteht darin, dass sie während der Programmausführung erstellt und ihnen Speicher zugewiesen wird.

Dynamische Variablen werden in einem dynamischen Speicherbereich (Heap-Bereich) abgelegt. Eine dynamische Variable wird in Variablendeklarationen nicht explizit angegeben und kann nicht namentlich angesprochen werden. Auf solche Variablen wird unter Verwendung von Zeigern und Referenzen zugegriffen.

Ein Referenztyp (Zeiger) definiert eine Reihe von Werten, die auf dynamische Variablen eines bestimmten Typs zeigen, der als Basistyp bezeichnet wird. Eine Referenztypvariable enthält die Adresse einer dynamischen Variablen im Speicher. Wenn der Basistyp ein nicht deklarierter Bezeichner ist, muss er im selben Teil der Typdeklaration deklariert werden wie der Zeigertyp.

Das reservierte Wort nil bezeichnet eine Konstante mit einem Zeigerwert, der auf nichts zeigt.

Lassen Sie uns ein Beispiel für die Beschreibung dynamischer Variablen geben.

var p1, p2 : ^real;

p3, p4 : ^ ganze Zahl;

2. Arbeiten mit dynamischem Speicher. Untypisierte Zeiger

Dynamische Speicherprozeduren und -funktionen

1. Prozedur Neu (var p: Pointer).

Weist Speicherplatz im dynamischen Speicherbereich zu, um die dynamische Variable p aufzunehmenЛ, und weist dem Zeiger p seine Adresse zu.

2. Prozedur Dispose(varp: Pointer).

Gibt den von der New-Prozedur für die dynamische Variablenzuordnung zugewiesenen Speicher frei, und der Wert des Zeigers p wird undefiniert.

3. Prozedur GetMem(varp: Zeiger; Größe: Wort).

Weist einen Speicherabschnitt im Heap-Bereich zu, weist dem Zeiger p die Adresse seines Anfangs zu, die Größe des Abschnitts in Bytes wird durch den Größenparameter angegeben.

4. Prozedur FreeMem (var p: Zeiger; Größe: Wort).

Gibt den Speicherbereich frei, dessen Anfangsadresse durch den p-Zeiger angegeben wird und dessen Größe durch den Größenparameter angegeben wird. Der Wert des Zeigers p wird undefiniert.

5. Prozedurmarke (var p: Pointer)

Schreibt in den Zeiger p die Adresse des Beginns eines Abschnitts des freien dynamischen Speichers zum Zeitpunkt seines Aufrufs.

6. Prozedurfreigabe (var p: Pointer)

Gibt einen Abschnitt des dynamischen Speichers ab der Adresse frei, die von der Mark-Prozedur in den Zeiger p geschrieben wurde, d. h. löscht den dynamischen Speicher, der nach dem Aufruf der Mark-Prozedur belegt war.

7. MaxAvaikLongint-Funktion

Gibt die Länge des längsten freien Heaps in Bytes zurück.

8. MemAvaikLongint-Funktion

Gibt die Gesamtmenge des freien dynamischen Speichers in Bytes zurück.

9. Hilfsfunktion SizeOf(X):Word

Gibt die Anzahl der von X belegten Bytes zurück, wobei X entweder ein Variablenname eines beliebigen Typs oder ein Typname sein kann.

Der eingebaute Typ Pointer bezeichnet einen untypisierten Zeiger, also einen Zeiger, der nicht auf einen bestimmten Typ zeigt. Variablen vom Typ Pointer können dereferenziert werden: Die Angabe des ^-Zeichens nach einer solchen Variable führt zu einem Fehler.

Wie der durch nil bezeichnete Wert sind Pointer-Werte mit allen anderen Pointer-Typen kompatibel.

VORTRAG № 8. Abstrakte Datenstrukturen

1. Abstrakte Datenstrukturen

Strukturierte Datentypen wie Arrays, Sets und Records sind statische Strukturen, da sich ihre Größe während der gesamten Ausführung des Programms nicht ändert.

Häufig ist es erforderlich, dass Datenstrukturen im Zuge der Lösung eines Problems ihre Größe ändern. Solche Datenstrukturen werden dynamisch genannt. Dazu gehören Stapel, Warteschlangen, Listen, Bäume usw.

Die Beschreibung dynamischer Strukturen unter Verwendung von Arrays, Datensätzen und Dateien führt zu einer Verschwendung von Computerspeicher und erhöht die Zeit zum Lösen von Problemen.

Jede Komponente einer dynamischen Struktur ist ein Datensatz, der mindestens zwei Felder enthält: ein Feld vom Typ "Zeiger" und das zweite - für die Datenplatzierung. Im Allgemeinen kann ein Datensatz nicht einen, sondern mehrere Zeiger und mehrere Datenfelder enthalten. Ein Datenfeld kann eine Variable, ein Array, eine Menge oder ein Datensatz sein.

Wenn der zeigende Teil die Adresse eines Elements der Liste enthält, wird die Liste als unidirektional (oder einfach verknüpft) bezeichnet. Wenn es zwei Komponenten enthält, ist es doppelt verbunden. Sie können verschiedene Operationen auf Listen ausführen, zum Beispiel:

1) Hinzufügen eines Elements zur Liste;

2) Entfernen eines Elements aus der Liste mit einem gegebenen Schlüssel;

3) Suche nach einem Element mit einem gegebenen Wert des Schlüsselfeldes;

4) Sortieren der Elemente der Liste;

5) Teilung der Liste in zwei oder mehr Listen;

6) Kombinieren von zwei oder mehr Listen zu einer;

7) andere Operationen.

In der Regel besteht jedoch nicht die Notwendigkeit aller Operationen zur Lösung verschiedener Probleme. Daher gibt es je nach anzuwendenden Grundoperationen unterschiedliche Arten von Listen. Die beliebtesten davon sind Stack und Queue.

2. Stapel

Ein Stack ist eine dynamische Datenstruktur, bei der das Hinzufügen einer Komponente und das Entfernen einer Komponente an einem Ende, dem oberen Ende des Stacks, erfolgt. Der Stapel arbeitet nach dem LIFO-Prinzip (Last-In, First-Out) – „Last in, first out“.

Es gibt normalerweise drei Operationen, die auf Stacks ausgeführt werden:

1) anfängliche Bildung des Stapels (Aufzeichnung der ersten Komponente);

2) Hinzufügen einer Komponente zum Stack;

3) Auswahl der Komponente (Löschung).

Um einen Stapel zu bilden und damit zu arbeiten, müssen Sie zwei Variablen vom Typ "Zeiger" haben, von denen die erste die Spitze des Stapels bestimmt und die zweite eine Hilfsvariable ist.

Beispiel. Schreiben Sie ein Programm, das einen Stapel bildet, ihm eine beliebige Anzahl von Komponenten hinzufügt und dann alle Komponenten liest und sie auf dem Bildschirm anzeigt. Nehmen Sie eine Zeichenkette als Daten. Dateneingabe - von der Tastatur, ein Zeichen für das Ende der Eingabe - eine Zeichenfolge END.

STAPEL programmieren;

verwendet Crt;

tippe

Alpha = Zeichenkette[10];

PKomp = ^Komp;

Comp = Rekord

SD: Alfa

pWeiter : PComp

end;

jung

pTop:PComp;

sc: Alfa;

ProzedurStack erstellen (var pTop : PComp; var sC : Alfa);

beginnen

Neu (pOben);

pTop^.pNext := NIL;

pTop^.sD := sc;

end;

ProzedurComp hinzufügen (var pTop : PComp; var sC : Alfa);

var pAux : PComp;

beginnen

NEU(pAux);

pAux^.pNext := pTop;

pTop := pAux;

pTop^.sD := sc;

end;

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

beginnen

sc := pTop^.sD;

pTop := pTop^.pNext;

end;

beginnen

Clrscr;

writeln(' STRING EINGEBEN ');

readln(sC);

CreateStack(pTop, sc);

wiederholen

writeln(' STRING EINGEBEN ');

readln(sC);

AddComp(pTop, sc);

bis sC = 'ENDE';

writeln('****** AUSGABE *****');

wiederholen

DelComp(pTop, sc);

schreiben(sC);

bis pTop = NULL;

Ende.

3. Warteschlangen

Eine Warteschlange ist eine dynamische Datenstruktur, bei der eine Komponente an einem Ende hinzugefügt und am anderen Ende abgerufen wird. Die Warteschlange funktioniert nach dem FIFO-Prinzip (First-In, First-Out) – „First in, first served“.

Um eine Warteschlange zu bilden und damit zu arbeiten, sind drei Variablen vom Typ Zeiger erforderlich, von denen die erste den Beginn der Warteschlange bestimmt, die zweite - das Ende der Warteschlange, die dritte - Hilfsvariablen.

Beispiel. Schreiben Sie ein Programm, das eine Warteschlange bildet, ihr eine beliebige Anzahl von Komponenten hinzufügt und dann alle Komponenten liest und sie auf dem Bildschirm anzeigt. Nehmen Sie eine Zeichenfolge als Daten. Dateneingabe - von der Tastatur, ein Zeichen für das Ende der Eingabe - eine Zeichenfolge END.

ProgrammQUEUE;

verwendet Crt;

tippe

Alpha = Zeichenkette[10];

PKomp = ^Komp;

Comp = Rekord

SD: Alfa

pWeiter : PComp;

end;

jung

pBegin, pEnd : PComp;

sc: Alfa;

Prozedurwarteschlange erstellen (var pBegin, pEnd:PComp; var sC:Alfa);

beginnen

Neu(pBeginn);

pBegin^.pNext := NIL;

pBegin^.sD := sc;

pEnd := pBegin;

end;

Prozedur Add ProcedureQueue (var pEnd : PComp; var sc : Alfa);

var pAux : PComp;

beginnen

Neu(pAux);

pAux^.pNext := NIL;

pEnd^.pNext := pAux;

pEnd := pAux;

pEnd^.sD := sc;

end;

Prozedur DelQueue (var pBegin : PComp; var sC : Alfa);

beginnen

sc := pBegin^.sD;

pBegin := pBegin^.pNext;

end;

beginnen

Clrscr;

writeln(' STRING EINGEBEN ');

readln(sC);

CreateQueue(pBegin, pEnd, sc);

wiederholen

writeln(' STRING EINGEBEN ');

readln(sC);

AddQueue(pEnd, sc);

bis sC = 'ENDE';

writeln(' ***** ERGEBNISSE ANZEIGEN *****');

wiederholen

DelQueue(pBegin, sc);

schreiben(sC);

bis pBegin = NIL;

Ende.

VORTRAG Nr. 9. Baumartige Datenstrukturen

1. Baumdatenstrukturen

Eine baumartige Datenstruktur ist eine endliche Menge von Elementen – Knoten, zwischen denen Beziehungen bestehen – die Verbindung zwischen der Quelle und dem Generierten.

Wenn wir die von N. Wirth vorgeschlagene rekursive Definition verwenden, dann ist eine Baumdatenstruktur vom Basistyp t entweder eine leere Struktur oder ein Knoten vom Typ t, bei dem sich eine endliche Menge von Baumstrukturen vom Basistyp t, sogenannte Teilbäume, befindet damit verbundenen.

Als nächstes geben wir die Definitionen an, die beim Arbeiten mit Baumstrukturen verwendet werden.

Wenn Knoten y direkt unter Knoten x liegt, dann wird Knoten y als unmittelbarer Nachkomme von Knoten x bezeichnet, und x ist der unmittelbare Vorfahre von Knoten y, d. h. wenn sich Knoten x auf der i-ten Ebene befindet, dann ist Knoten y entsprechend befindet sich auf (i + 1) - der Ebene.

Die maximale Ebene eines Baumknotens wird als Höhe oder Tiefe des Baums bezeichnet. Ein Vorfahre hat nicht nur einen Knoten des Baums - seine Wurzel.

Baumknoten, die keine Kinder haben, werden Blattknoten (oder Blätter des Baums) genannt. Alle anderen Knoten werden interne Knoten genannt. Die Anzahl der unmittelbaren Kinder eines Knotens bestimmt den Grad dieses Knotens, und der maximal mögliche Grad eines Knotens in einem gegebenen Baum bestimmt den Grad des Baums.

Vorfahren und Nachkommen können nicht vertauscht werden, d.h. die Verbindung zwischen dem Original und dem Generierten wirkt nur in eine Richtung.

Wenn Sie von der Wurzel des Baums zu einem bestimmten Knoten gehen, wird die Anzahl der Zweige des Baums, die in diesem Fall durchlaufen werden, als Länge des Pfads für diesen Knoten bezeichnet. Wenn alle Zweige (Knoten) eines Baums geordnet sind, dann wird der Baum als geordnet bezeichnet.

Binäre Bäume sind ein Spezialfall von Baumstrukturen. Dies sind Bäume, in denen jedes Kind höchstens zwei Kinder hat, die linken und rechten Teilbäume genannt werden. Somit ist ein binärer Baum eine Baumstruktur, deren Grad zwei ist.

Die Reihenfolge eines Binärbaums wird durch die folgende Regel bestimmt: Jeder Knoten hat sein eigenes Schlüsselfeld, und für jeden Knoten ist der Schlüsselwert größer als alle Schlüssel in seinem linken Teilbaum und kleiner als alle Schlüssel in seinem rechten Teilbaum.

Ein Baum, dessen Grad größer als zwei ist, heißt stark verzweigt.

2. Operationen an Bäumen

Außerdem betrachten wir alle Operationen in Bezug auf binäre Bäume.

I. Baumkonstruktion

Wir stellen einen Algorithmus zur Konstruktion eines geordneten Baums vor.

1. Wenn der Baum leer ist, werden die Daten an die Wurzel des Baums übertragen. Wenn der Baum nicht leer ist, wird einer seiner Äste so absteigend, dass die Ordnung des Baums nicht verletzt wird. Als Ergebnis wird der neue Knoten zum nächsten Blatt des Baums.

2. Um einem bereits bestehenden Baum einen Knoten hinzuzufügen, können Sie den obigen Algorithmus verwenden.

3. Beim Löschen eines Knotens aus dem Baum sollten Sie vorsichtig sein. Wenn der zu entfernende Knoten ein Blatt ist oder nur ein Kind hat, dann ist die Operation einfach. Wenn der zu löschende Knoten zwei Nachkommen hat, muss unter seinen Nachkommen ein Knoten gefunden werden, der an seine Stelle gesetzt werden kann. Dies ist notwendig, da der Baum bestellt werden muss.

Sie können dies tun: Tauschen Sie den zu entfernenden Knoten mit dem Knoten mit dem größten Schlüsselwert im linken Teilbaum oder mit dem Knoten mit dem kleinsten Schlüsselwert im rechten Teilbaum aus und löschen Sie dann den gewünschten Knoten als Blatt.

II. Suchen eines Knotens mit einem bestimmten Schlüsselfeldwert

Beim Durchführen dieser Operation ist es notwendig, den Baum zu durchqueren. Es ist notwendig, die verschiedenen Schreibweisen eines Baums zu berücksichtigen: Präfix, Infix und Postfix.

Es stellt sich die Frage: Wie können die Knoten des Baums so dargestellt werden, dass es am bequemsten ist, mit ihnen zu arbeiten? Es ist möglich, einen Baum mithilfe eines Arrays darzustellen, wobei jeder Knoten durch einen kombinierten Typwert beschrieben wird, der ein zeichenartiges Informationsfeld und zwei referenzartige Felder aufweist. Dies ist jedoch nicht sehr praktisch, da Bäume eine große Anzahl von Knoten haben, die nicht vorbestimmt sind. Daher ist es am besten, bei der Beschreibung eines Baums dynamische Variablen zu verwenden. Dann wird jeder Knoten durch einen gleichartigen Wert repräsentiert, der eine Beschreibung einer bestimmten Anzahl von Informationsfeldern enthält, und die Anzahl der entsprechenden Felder muss gleich dem Grad des Baums sein. Es ist logisch, das Fehlen von Nachkommen mit null zu definieren. Dann könnte die Beschreibung eines Binärbaums in Pascal so aussehen:

TYPE TreeLink = ^Baum;

Baum = Aufzeichnung;

Inf : <Datentyp>;

Links, rechts: TreeLink;

Ende.

3. Beispiele für die Durchführung von Operationen

1. Konstruieren Sie einen Baum mit n Knoten minimaler Höhe oder einen perfekt balancierten Baum (die Anzahl der Knoten des linken und des rechten Teilbaums eines solchen Baums darf sich um nicht mehr als eins unterscheiden).

Rekursiver Konstruktionsalgorithmus:

1) Der erste Knoten wird als Wurzel des Baums genommen.

2) Der linke Teilbaum aus nl Knoten wird auf die gleiche Weise aufgebaut.

3) der rechte Teilbaum von nr Knoten wird auf die gleiche Weise aufgebaut;

nr = n - nl - 1. Als Informationsfeld nehmen wir die über die Tastatur eingegebenen Knotennummern. Die rekursive Funktion, die diese Konstruktion implementiert, sieht folgendermaßen aus:

Funktion Baum(n : Byte) : TreeLink;

Var t: TreeLink; nl,nr,x : Byte;

Beginnen

Wenn n = 0, dann Tree := nil

sonst

Beginnen

nl := n div 2;

nr = n - nl - 1;

writeln('Vertexnummer eingeben ');

readln(x);

Newt);

t^.inf := x;

t^.links := Baum(nl);

t^.right := Tree(nr);

Baum := t;

End;

{Baum}

Ende.

2. Suchen Sie im binär geordneten Baum den Knoten mit dem gegebenen Wert des Schlüsselfelds. Wenn kein solches Element im Baum vorhanden ist, fügen Sie es dem Baum hinzu.

Search Procedure(x : Byte; var t : TreeLink);

Beginnen

Wenn t = Null dann

Beginnen

Newt);

t^inf := x;

t^.links := nil;

t^.right := nil;

Ende

Sonst wenn x < t^.inf dann

Suchen(x, t^.links)

Sonst wenn x > t^.inf dann

Suchen(x, t^.rechts)

sonst

Beginnen

{Gefundenes Element verarbeiten}

...

End;

Ende.

3. Schreiben Sie Prozeduren zum Traversieren von Bäumen in Vorwärts-, Symmetrie- bzw. Rückwärtsreihenfolge.

3.1. Prozedur Preorder(t : TreeLink);

Beginnen

Wenn t <> nil dann

Beginnen

WriteIn(t^.inf);

Vorbestellung(t^.links);

Vorbestellung(t^.right);

End;

End;

3.2. Prozedur Inorder(t : TreeLink);

Beginnen

Wenn t <> nil dann

Beginnen

Inorder(t^.links);

WriteIn(t^.inf);

Inorder(t^.right);

End;

Ende.

3.3. Prozedur Postorder(t : TreeLink);

Beginnen

Wenn t <> nil dann

Beginnen

postorder(t^.links);

postorder(t^.right);

WriteIn(t^.inf);

End;

Ende.

4. Löschen Sie im binär geordneten Baum den Knoten mit dem angegebenen Wert des Schlüsselfelds.

Lassen Sie uns eine rekursive Prozedur beschreiben, die das Vorhandensein des erforderlichen Elements im Baum und die Anzahl der Nachkommen dieses Knotens berücksichtigt. Wenn der zu löschende Knoten zwei Kinder hat, wird er durch den größten Schlüsselwert in seinem linken Teilbaum ersetzt und erst dann endgültig gelöscht.

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

Var p: TreeLink;

Prozedur Delete2(var q : TreeLink);

Beginnen

Wenn q^.right <> nil dann Delete2(q^.right)

sonst

Beginnen

p^.inf := q^.inf;

p := q;

q := q^.links;

End;

End;

Beginnen

Wenn t = Null dann

Writeln('kein Element gefunden')

Sonst wenn x < t^.inf dann

Löschen1(x, t^.links)

Sonst wenn x > t^.inf dann

Löschen1(x, t^.rechts)

sonst

Beginnen

P := t;

Wenn p^.left = nil dann

t := p^.rechts

sonst

Wenn p^.right = nil dann

t := p^.links

sonst

Delete2(p^.links);

End;

Ende.

VORTRAG Nr. 10. Zählt

1. Das Konzept eines Graphen. Möglichkeiten, einen Graphen darzustellen

Ein Graph ist ein Paar G = (V,E), wobei V eine Menge von Objekten beliebiger Natur ist, Knoten genannt, und E eine Familie von Paaren ei = (vil, vi2), vijOV, Kanten genannt. Im allgemeinen Fall kann die Menge V und/oder die Familie E unendlich viele Elemente enthalten, aber wir werden nur endliche Graphen betrachten, d. h. Graphen, für die sowohl V als auch E endlich sind. Wenn die Reihenfolge der in ei enthaltenen Elemente von Bedeutung ist, wird der Graph als gerichtet bezeichnet, abgekürzt - digraph, ansonsten - ungerichtet. Die Kanten eines Digraphen heißen Bögen. Im Folgenden gehen wir davon aus, dass der Begriff „Graph“, der ohne Angabe verwendet wird (gerichtet oder ungerichtet), einen ungerichteten Graphen bezeichnet.

Wenn e = , dann heißen die Ecken v und u Enden der Kante. Hier sagen wir, dass die Kante e zu jedem der Eckpunkte v und u benachbart (einfallend) ist. Die Ecken v und und werden auch benachbart (incident) genannt. Im allgemeinen Fall sind Kanten der Form e = ; solche Kanten werden Schleifen genannt.

Der Grad eines Scheitelpunkts in einem Diagramm ist die Anzahl der Kanten, die auf diesen Scheitelpunkt fallen, wobei Schleifen doppelt gezählt werden. Da jede Kante mit zwei Eckpunkten inzident ist, ist die Summe der Grade aller Eckpunkte im Diagramm gleich der doppelten Anzahl der Kanten: Sum(deg(vi), i=1...|V|) = 2 * | E|.

Die Gewichtung eines Knotens ist eine Zahl (reell, ganzzahlig oder rational), die einem gegebenen Knoten zugeordnet ist (interpretiert als Kosten, Durchsatz usw.). Gewicht, Kantenlänge - eine Zahl oder mehrere Zahlen, die als Länge, Bandbreite usw. interpretiert werden.

Ein Pfad in einem Diagramm (oder eine Route in einem Digraphen) ist eine abwechselnde Folge von Eckpunkten und Kanten (oder Bögen in einem Digraphen) der Form v0, (v0,v1), v1..., (vn - 1,vn ), vn. Die Zahl n nennt man Weglänge. Ein Pfad ohne sich wiederholende Kanten wird als Kette bezeichnet; ein Pfad ohne sich wiederholende Eckpunkte wird als einfache Kette bezeichnet. Der Pfad kann geschlossen werden (v0 = vn). Ein geschlossener Pfad ohne sich wiederholende Kanten wird als Zyklus (oder Kontur in einem Digraphen) bezeichnet; ohne sich wiederholende Scheitelpunkte (außer dem ersten und letzten) - eine einfache Schleife.

Ein Graph heißt zusammenhängend, wenn es zwischen zwei beliebigen seiner Knoten einen Pfad gibt, andernfalls nicht zusammenhängend. Ein nicht zusammenhängender Graph besteht aus mehreren zusammenhängenden Komponenten (zusammenhängende Teilgraphen).

Es gibt verschiedene Möglichkeiten, Diagramme darzustellen. Betrachten wir jeden von ihnen separat.

1. Inzidenzmatrix.

Dies ist eine rechteckige Matrix der Dimension nx n, wobei n die Anzahl der Eckpunkte und am die Anzahl der Kanten ist. Die Werte der Matrixelemente werden wie folgt bestimmt: Wenn die Kante xi und der Scheitelpunkt vj inzident sind, dann ist der Wert des entsprechenden Matrixelements gleich eins, andernfalls ist der Wert Null. Für gerichtete Graphen wird die Inzidenzmatrix nach dem folgenden Prinzip erstellt: Der Wert des Elements ist gleich - 1, wenn die Kante xi vom Scheitelpunkt vj stammt, gleich 1, wenn die Kante xi in den Scheitelpunkt vj eintritt, und andernfalls gleich XNUMX .

2. Adjazenzmatrix.

Dies ist eine quadratische Matrix der Dimension nxn, wobei n die Anzahl der Eckpunkte ist. Wenn die Eckpunkte vi und vj benachbart sind, das heißt, wenn es eine Kante gibt, die sie verbindet, dann ist das entsprechende Matrixelement gleich eins, andernfalls ist es gleich null. Die Regeln zum Aufbau dieser Matrix für gerichtete und ungerichtete Graphen unterscheiden sich nicht. Die Adjazenzmatrix ist kompakter als die Inzidenzmatrix. Es ist zu beachten, dass diese Matrix ebenfalls sehr dünn besetzt ist, aber im Fall eines ungerichteten Graphen symmetrisch zur Hauptdiagonale ist, sodass Sie nicht die gesamte Matrix, sondern nur die Hälfte davon (eine Dreiecksmatrix) speichern können ).

3. Liste der Nachbarschaften (Vorfälle).

Es ist eine Datenstruktur, die eine Liste benachbarter Scheitelpunkte für jeden Graphscheitelpunkt speichert. Die Liste ist ein Array von Zeigern, deren i-tes Element einen Zeiger auf die Liste von Scheitelpunkten enthält, die an den i-ten Scheitelpunkt angrenzen.

Eine Adjazenzliste ist effizienter als eine Adjazenzmatrix, da sie die Speicherung von Nullelementen eliminiert.

4. Liste der Listen.

Es handelt sich um eine baumähnliche Datenstruktur, bei der ein Zweig Listen von Scheitelpunkten neben jedem Scheitelpunkt des Graphen enthält und der zweite Zweig auf den nächsten Scheitelpunkt des Graphen zeigt. Diese Art der Darstellung des Graphen ist die optimalste.

2. Darstellung eines Graphen durch eine Inzidenzliste. Graph-Tiefendurchquerungsalgorithmus

Um ein Diagramm als Inzidenzliste zu implementieren, können Sie den folgenden Typ verwenden:

Typenliste = ^S;

S=Aufzeichnung;

inf: Byte;

weiter: Liste;

end;

Dann ist der Graph wie folgt definiert:

Var Gr : array[1..n] der Liste;

Wenden wir uns nun der Traversierungsprozedur des Graphen zu. Dies ist ein Hilfsalgorithmus, mit dem Sie alle Scheitelpunkte des Diagramms anzeigen und alle Informationsfelder analysieren können. Wenn wir eine Graphtraversierung genauer betrachten, gibt es zwei Arten von Algorithmen: rekursive und nicht rekursive.

Mit dem rekursiven Tiefendurchlaufalgorithmus nehmen wir einen beliebigen Scheitelpunkt und finden einen beliebigen unsichtbaren (neuen) Scheitelpunkt v daneben. Dann nehmen wir den Knoten v als nicht neu und finden einen beliebigen neuen Knoten neben ihm. Wenn ein Scheitelpunkt keine neueren unsichtbaren Scheitelpunkte hat, nehmen wir an, dass dieser Scheitelpunkt verwendet wird, und kehren eine Ebene höher zu dem Scheitelpunkt zurück, von dem wir zu unserem verwendeten Scheitelpunkt gelangt sind. Die Traversierung wird auf diese Weise fortgesetzt, bis es keine neuen ungescannten Scheitelpunkte im Graphen gibt.

In Pascal würde das Tiefen-Zuerst-Traversal-Verfahren wie folgt aussehen:

Prozedur Obhod(gr : Graph; k : Byte);

Var g : Diagramm; l: Liste;

Beginnen

nov[k] := falsch;

g := gr;

Während g^.inf <> k tun

g := g^.nächste;

l := g^.smeg;

Während l <> nil beginne

Wenn nov[l^.inf] dann Obhod(gr, l^.inf);

l := l^.nächste;

End;

End;

Beachten

In diesem Verfahren meinten wir mit der Beschreibung des Typs Graph die Beschreibung eines Graphen durch eine Liste von Listen. Array nov[i] ist ein spezielles Array, dessen i-tes Element True ist, wenn der i-te Knoten nicht besucht wird, und andernfalls False.

Häufig wird auch ein nicht-rekursiver Durchlaufalgorithmus verwendet. In diesem Fall wird die Rekursion durch einen Stack ersetzt. Sobald ein Scheitelpunkt betrachtet wurde, wird er auf den Stapel geschoben und wird verwendet, wenn keine neuen Scheitelpunkte mehr benachbart sind.

3. Darstellung eines Graphen durch eine Liste von Listen. Breitendiagramm-Durchlaufalgorithmus

Ein Diagramm kann mithilfe einer Liste von Listen wie folgt definiert werden:

TypeList = ^Tlist;

tlist=record

inf: Byte;

weiter: Liste;

end;

Grafik = ^TGpaph;

TGpaph = Datensatz

inf: Byte;

smeg : Liste;

weiter: Grafik;

end;

Wenn wir den Graphen in der Breite durchlaufen, wählen wir einen beliebigen Knoten aus und sehen alle angrenzenden Knoten gleichzeitig durch. Anstelle eines Stapels wird eine Warteschlange verwendet. Der Breitensuchalgorithmus ist sehr praktisch, um den kürzesten Pfad in einem Diagramm zu finden.

Hier ist ein Verfahren zum Durchlaufen eines Diagramms in der Breite in Pseudocode:

Verfahren Obhod2(v);

{Werte Spisok, Nov - global}

Beginnen

Warteschlange = O;

Warteschlange <= v;

nov[v] = Falsch;

Während Warteschlange <> O tun

Beginnen

p <= Warteschlange;

Für u in spisok(p) tun

Wenn neu[u] dann

Beginnen

nov[u] := Falsch;

Warteschlange <= u;

End;

End;

End;

VORTRAG Nr. 11. Objektdatentyp

1. Objekttyp in Pascal. Das Konzept eines Objekts, seine Beschreibung und Verwendung

Historisch gesehen war der erste Programmieransatz die prozedurale Programmierung, auch bekannt als Bottom-up-Programmierung. Zunächst wurden gemeinsame Bibliotheken von Standardprogrammen erstellt, die in verschiedenen Bereichen der Computeranwendung verwendet werden. Auf der Grundlage dieser Programme wurden dann komplexere Programme erstellt, um spezifische Probleme zu lösen.

Die Computertechnologie entwickelte sich jedoch ständig weiter, sie wurde eingesetzt, um verschiedene Probleme der Produktion und der Wirtschaft zu lösen, und daher wurde es notwendig, Daten in verschiedenen Formaten zu verarbeiten und nicht standardmäßige Probleme (z. B. nicht numerische) zu lösen. Daher begannen sie bei der Entwicklung von Programmiersprachen, auf die Erstellung verschiedener Arten von Daten zu achten. Dies trug zur Entstehung so komplexer Datentypen wie kombiniert, mehrfach, Zeichenfolge, Datei usw. bei. Vor der Lösung des Problems führte der Programmierer eine Dekomposition durch, d. h. die Aufteilung der Aufgabe in mehrere Teilaufgaben, für die jeweils ein separates Modul geschrieben wurde . Die Hauptprogrammierungstechnologie umfasste drei Phasen:

1) Top-Down-Design;

2) modulare Programmierung;

3) strukturelle Codierung.

Aber ab Mitte der 60er Jahre des XNUMX. Jahrhunderts begannen sich neue Konzepte und Ansätze zu bilden, die die Grundlage der Technologie der objektorientierten Programmierung bildeten. Bei diesem Ansatz erfolgt die Modellierung und Beschreibung der realen Welt auf der Ebene der Konzepte eines bestimmten Fachgebiets, zu dem das zu lösende Problem gehört.

Die objektorientierte Programmierung ist eine Programmiertechnik, die unserem Verhalten sehr ähnlich ist. Es ist eine natürliche Weiterentwicklung früherer Innovationen im Programmiersprachendesign. Die objektorientierte Programmierung ist struktureller als alle bisherigen Entwicklungen zur strukturierten Programmierung. Es ist auch modularer und abstrakter als frühere Versuche der Datenabstraktion und der internen Programmierung von Details. Eine objektorientierte Programmiersprache zeichnet sich durch drei Haupteigenschaften aus:

1) Kapselung. Das Kombinieren von Datensätzen mit Prozeduren und Funktionen, die die Felder dieser Datensätze manipulieren, bildet einen neuen Datentyp – ein Objekt;

2) Erbschaft. Definition eines Objekts und seine weitere Verwendung zum Aufbau einer Hierarchie von untergeordneten Objekten mit der Fähigkeit für jedes untergeordnete Objekt, das sich auf die Hierarchie bezieht, auf den Code und die Daten aller übergeordneten Objekte zuzugreifen;

3) Polymorphismus. Geben Sie einer Aktion einen einzigen Namen, der dann in der Objekthierarchie nach oben und unten geteilt wird, wobei jedes Objekt in der Hierarchie diese Aktion auf eine für sie geeignete Weise ausführt.

Apropos Objekt, wir führen einen neuen Datentyp ein – Objekt. Ein Objekttyp ist eine Struktur, die aus einer festen Anzahl von Komponenten besteht. Jede Komponente ist entweder ein Feld, das Daten eines streng definierten Typs enthält, oder eine Methode, die Operationen an einem Objekt ausführt. Analog zur Deklaration von Variablen gibt die Deklaration eines Feldes den Datentyp dieses Feldes und den das Feld benennenden Bezeichner an: Analog zur Deklaration einer Prozedur oder Funktion gibt die Beschreibung einer Methode den Titel der Prozedur an, Funktion, Konstruktor oder Destruktor.

Ein Objekttyp kann Komponenten eines anderen Objekttyps erben. Wenn Typ T2 von Typ T1 erbt, dann ist Typ T2 ein untergeordnetes Element von Typ T1, und Typ T1 selbst ist ein übergeordnetes Element von Typ T2. Die Vererbung ist transitiv, d.h. wenn TK von T2 erbt und T2 von T1 erbt, dann erbt TK von T1. Der Geltungsbereich (Domäne) eines Objekttyps besteht aus sich selbst und allen seinen Nachkommen.

Der folgende Quellcode ist ein Beispiel für eine Objekttypdeklaration, type

tippe

Punkt = Objekt

X, Y: ganze Zahl;

end;

Rect = Objekt

A, B: TPunkt;

Prozedur Init(XA, YA, XB, YB: Integer);

Prozedur Copy(var R: TRectangle);

Prozedur Move(DX, DY: Integer);

Prozedur Grow(DX, DY: Integer);

Prozedur Intersect(var R: TRectangle);

Prozedur Union(var R: TRectangle);

Funktion enthält (P: Punkt): Boolean;

end;

StringPtr = ^String;

FieldPtr = ^TField;

TField = Objekt

X, Y, Länge: Ganzzahl;

Name: StringPtr;

Konstruktor Copy(var F: TField);

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

Destruktor Fertig; virtuell;

Verfahren Anzeige; virtuell;

Verfahren Bearbeiten; virtuell;

Funktion GetStr: String; virtuell;

Funktion PutStr(S:String): Boolean; virtuell;

end;

StrFieldPtr = ^TStrField;

StrField = Objekt (TField)

Wert: PString;

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

Destruktor Fertig; virtuell;

Funktion GetStr: String; virtuell;

Funktion PutStr(S:String): Boolean;

virtuell;

Funktion Holen: Zeichenfolge;

Prozedur Put(S: String);

end;

NumFieldPtr = ^TNumField;

TNumField = Objekt (TField)

privat

Wert, Min, Max: Lange Ganzzahl;

Öffentlichkeit

Konstruktor Init(FX, FY, FLen: Integer; FName: String;

FMin, FMax: Lange Ganzzahl);

Funktion GetStr: String; virtuell;

Funktion PutStr(S:String): Boolean; virtuell;

Funktion Holen: Lange Ganzzahl;

Funktion Put(N: Lange Ganzzahl);

end;

ZipFieldPtr = ^TZipField;

ZipField = Objekt (TNumField)

Funktion GetStr: String; virtuell;

Funktion PutStr(S:String): Boolean;

virtuell;

Ende.

Im Gegensatz zu anderen Typen können Objekttypen nur im Typdeklarationsabschnitt auf der äußersten Ebene des Gültigkeitsbereichs eines Programms oder Moduls deklariert werden. Daher können Objekttypen nicht in einem Variablendeklarationsabschnitt oder innerhalb eines Prozedur-, Funktions- oder Methodenblocks deklariert werden.

Ein Dateitypkomponententyp kann keinen Objekttyp oder Strukturtyp haben, der Objekttypkomponenten enthält.

2. Erbschaft

Der Vorgang, bei dem ein Typ die Eigenschaften eines anderen Typs erbt, wird als Vererbung bezeichnet. Der Nachkomme wird als abgeleiteter (untergeordneter) Typ bezeichnet, und der Typ, von dem der untergeordnete Typ erbt, wird als übergeordneter (übergeordneter) Typ bezeichnet.

Bisher bekannte Pascal-Record-Typen können nicht erben. Borland Pascal erweitert jedoch die Pascal-Sprache, um die Vererbung zu unterstützen. Eine dieser Erweiterungen ist eine neue Datenstrukturkategorie, die sich auf Datensätze bezieht, aber viel leistungsfähiger ist. Die Datentypen in dieser neuen Kategorie werden mit dem neuen reservierten Wort "Objekt" definiert. Ein Objekttyp kann als vollständiger, unabhängiger Typ in der Art der Beschreibung von Pascal-Einträgen definiert werden, aber er kann auch als Nachkomme eines vorhandenen Objekttyps definiert werden, indem der übergeordnete Typ in Klammern nach dem reservierten Wort "Objekt" gesetzt wird.

3. Objekte instanziieren

Eine Objektinstanz wird erzeugt, indem eine Variable oder Konstante eines Objekttyps deklariert wird oder indem die Standardprozedur New auf eine Variable des Typs "Zeiger auf Objekttyp" angewendet wird. Das resultierende Objekt wird als Instanz des Objekttyps bezeichnet;

jung

F: TFeld;

Z: TZipField;

FP:PField;

ZP: PZipField;

Angesichts dieser Variablendeklarationen ist F eine Instanz von TField und Z eine Instanz von TZipField. In ähnlicher Weise zeigt FP nach dem Anwenden von New auf FP und ZP auf eine TField-Instanz und ZP auf eine TZipField-Instanz.

Wenn ein Objekttyp virtuelle Methoden enthält, müssen Instanzen dieses Objekttyps initialisiert werden, indem ein Konstruktor aufgerufen wird, bevor eine virtuelle Methode aufgerufen wird.

Unten ist ein Beispiel:

jung

S: StrFeld;

tun

S.Init(1, 1, 25, 'Vorname');

S.Put('Vladimir');

S.Display;

...

S Fertig;

Ende.

Wenn S.Init nicht aufgerufen wurde, führt der Aufruf von S.Display dazu, dass dieses Beispiel fehlschlägt.

Das Zuweisen einer Instanz eines Objekttyps impliziert keine Initialisierung der Instanz. Ein Objekt wird durch vom Compiler generierten Code initialisiert, der zwischen dem Aufruf des Konstruktors und dem Punkt ausgeführt wird, an dem die Ausführung tatsächlich die erste Anweisung im Codeblock des Konstruktors erreicht.

Wenn die Objektinstanz nicht initialisiert und die Bereichsprüfung aktiviert ist (durch die {SR+}-Direktive), gibt der erste Aufruf der virtuellen Methode der Objektinstanz einen Laufzeitfehler aus. Wenn die Bereichsprüfung deaktiviert ist (durch die Direktive {SR-}), kann der erste Aufruf einer virtuellen Methode eines nicht initialisierten Objekts zu unvorhersehbarem Verhalten führen.

Die obligatorische Initialisierungsregel gilt auch für Instanzen, die Komponenten von Strukturtypen sind. Zum Beispiel:

jung

Kommentar: Array [1..5] von TStrField;

I: Ganzzahl

beginnen

für I := 1 bis 5 tun

Comment [I].Init (1, I + 10, 40, 'Vorname');

.

.

.

for I := 1 bis 5 do Comment [I].Done;

end;

Bei dynamischen Instanzen ist die Initialisierung normalerweise die Platzierung und die Bereinigung die Entsorgung, was durch die erweiterte Syntax der Standardprozeduren New und Dispose erreicht wird. Zum Beispiel:

jung

SP: StrFieldPtr;

beginnen

New(SP, Init(1, 1, 25, 'Vorname');

SP^.Put('Vladimir');

SP^.Anzeige;

.

.

.

Entsorgen (SP, Fertig);

Ende.

Ein Zeiger auf einen Objekttyp ist zuweisungskompatibel mit einem Zeiger auf einen beliebigen übergeordneten Objekttyp, sodass zur Laufzeit ein Zeiger auf einen Objekttyp auf eine Instanz dieses Typs oder auf eine Instanz eines beliebigen untergeordneten Typs zeigen kann.

Beispielsweise kann ein Zeiger des Typs ZipFieldPtr Zeigern des Typs PZipField, PNumField und PField zugewiesen werden, und zur Laufzeit kann ein Zeiger des Typs PField entweder nil sein oder auf eine Instanz von TField, TNumField oder TZipField oder irgendeine zeigen Instanz eines untergeordneten Typs von TField.

Diese Zuweisungszeiger-Kompatibilitätsregeln gelten auch für Objekttypparameter. Beispielsweise können der TField.Cop-Methode Instanzen von TField, TStrField, TNumField, TZipField oder jedem anderen untergeordneten Typ von TField übergeben werden.

4. Bestandteile und Geltungsbereich

Der Geltungsbereich eines Bean-Identifizierers geht über den Objekttyp hinaus. Darüber hinaus erstreckt sich der Geltungsbereich eines Bean-Identifikators über die Blöcke von Prozeduren, Funktionen, Konstruktoren und Destruktoren, die die Methoden des Objekttyps und seiner Nachkommen implementieren. Basierend auf diesen Überlegungen muss die Schreibweise des Komponentenbezeichners innerhalb des Objekttyps und innerhalb aller seiner Nachkommen sowie innerhalb aller seiner Methoden eindeutig sein.

Der Geltungsbereich des im privaten Teil der Typdeklaration beschriebenen Komponentenbezeichners ist auf das Modul (Programm) beschränkt, das die Objekttypdeklaration enthält. Mit anderen Worten, private Identifier-Beans verhalten sich wie gewöhnliche öffentliche Identifier innerhalb des Moduls, das die Objekttypdeklaration enthält, und außerhalb des Moduls sind alle privaten Beans und Identifier unbekannt und unzugänglich. Indem Sie verwandte Objekttypen in dasselbe Modul einfügen, können Sie diese Objekte auf die privaten Komponenten der anderen zugreifen lassen, und diese privaten Komponenten sind anderen Modulen unbekannt.

In einer Objekttypdeklaration kann ein Methodenkopf die Parameter des zu beschreibenden Objekttyps spezifizieren, auch wenn die Deklaration noch nicht vollständig ist.

VORTRAG Nr. 12. Methoden

1. Methoden

Eine Methodendeklaration innerhalb eines Objekttyps entspricht einer Forward-Methodendeklaration (forward). Daher muss eine Methode irgendwo nach einer Objekttypdeklaration, aber innerhalb desselben Geltungsbereichs wie der Geltungsbereich der Objekttypdeklaration implementiert werden, indem ihre Deklaration definiert wird.

Bei prozeduralen und funktionalen Methoden nimmt die definierende Deklaration die Form einer normalen Prozedur- oder Funktionsdeklaration an, mit der Ausnahme, dass in diesem Fall der Prozedur- oder Funktionsbezeichner als Methodenbezeichner behandelt wird.

Für Konstruktor- und Destruktormethoden nimmt die definierende Deklaration die Form einer Prozedurmethodendeklaration an, mit der Ausnahme, dass das reservierte Wort Prozedur durch das reservierte Wort Konstruktor oder Destruktor ersetzt wird.

Die definierende Methodendeklaration kann, muss aber nicht, die Liste der Formalparameter des Methodenkopfes im Objekttyp wiederholen. In diesem Fall muss der Methodenheader in Reihenfolge, Typen und Parameternamen genau mit dem Header im Objekttyp übereinstimmen, und im Rückgabetyp des Funktionsergebnisses, wenn die Methode eine Funktion ist.

Die definierende Beschreibung einer Methode enthält immer einen impliziten Parameter mit dem Bezeichner Self, der einem formalen Variablenparameter eines Objekttyps entspricht. Innerhalb eines Methodenblocks stellt Self die Instanz dar, deren Methodenkomponente zum Aufrufen der Methode angegeben wurde. Somit werden alle Änderungen an den Werten der Self-Felder in der Instanz widergespiegelt.

Der Geltungsbereich einer Objekttyp-Bean-Kennung erstreckt sich auf Blöcke von Prozeduren, Funktionen, Konstruktoren und Destruktoren, die Methoden dieses Objekttyps implementieren. Die Wirkung ist die gleiche, als ob am Anfang des Methodenblocks eine with-Anweisung der folgenden Form eingefügt würde:

mit selbst machen

beginnen

...

end;

Basierend auf diesen Überlegungen muss die Schreibweise von Komponentenbezeichnern, formalen Methodenparametern, Self und allen Bezeichnern, die in den ausführbaren Teil der Methode eingeführt werden, eindeutig sein.

Wenn eine eindeutige Methodenkennung erforderlich ist, wird die qualifizierte Methodenkennung verwendet. Es besteht aus einer Objekttypkennung gefolgt von einem Punkt und einer Methodenkennung. Wie jeder anderen Kennung kann einer qualifizierten Methodenkennung optional eine Paketkennung und ein Punkt vorangestellt werden.

Virtuelle Methoden

Methoden sind standardmäßig statisch, aber mit Ausnahme von Konstruktoren können sie virtuell sein (indem die virtual-Direktive in die Methodendeklaration aufgenommen wird). Der Compiler löst Verweise auf statische Methodenaufrufe während des Kompilierungsprozesses auf, während virtuelle Methodenaufrufe zur Laufzeit aufgelöst werden. Dies wird manchmal als späte Bindung bezeichnet.

Wenn ein Objekttyp eine virtuelle Methode deklariert oder erbt, müssen die Variablen dieses Typs initialisiert werden, indem ein Konstruktor aufgerufen wird, bevor eine virtuelle Methode aufgerufen wird. Somit muss ein Objekttyp, der eine virtuelle Methode beschreibt oder erbt, auch mindestens eine Konstruktormethode beschreiben oder erben.

Ein Objekttyp kann alle Methoden überschreiben, die er von seinen Eltern erbt. Wenn eine Methodendeklaration in einem untergeordneten Element denselben Methodenbezeichner angibt wie eine Methodendeklaration im übergeordneten Element, überschreibt die Deklaration im untergeordneten Element die Deklaration im übergeordneten Element. Der Gültigkeitsbereich einer überschreibenden Methode wird auf den Gültigkeitsbereich des untergeordneten Elements erweitert, in dem die Methode eingeführt wurde, und bleibt so, bis der Methodenbezeichner erneut überschrieben wird.

Das Überschreiben einer statischen Methode ist unabhängig vom Ändern des Methodenheaders. Im Gegensatz dazu muss eine virtuelle Methodenüberschreibung die Reihenfolge, Parametertypen und -namen sowie Funktionsergebnistypen, falls vorhanden, beibehalten. Außerdem muss die Neudefinition wieder die virtuelle Direktive beinhalten.

Dynamische Methoden

Borland Pascal unterstützt zusätzliche spät gebundene Methoden, die als dynamische Methoden bezeichnet werden. Dynamische Methoden unterscheiden sich von virtuellen Methoden nur in der Art und Weise, wie sie zur Laufzeit versendet werden. Im Übrigen sind dynamische Verfahren virtuellen Verfahren gleichgestellt.

Eine dynamische Methodendeklaration entspricht einer virtuellen Methodendeklaration, aber die dynamische Methodendeklaration muss den dynamischen Methodenindex enthalten, der unmittelbar nach dem virtuellen Schlüsselwort angegeben wird. Der Index einer dynamischen Methode muss eine ganzzahlige Konstante zwischen 1 und 656535 sein und muss unter den Indizes anderer dynamischer Methoden, die im Objekttyp oder seinen Vorgängern enthalten sind, eindeutig sein. Zum Beispiel:

Prozedur FileOpen (var Msg: TMessage); virtuelle 100;

Eine Überschreibung einer dynamischen Methode muss mit der Reihenfolge, den Typen und Namen der Parameter übereinstimmen und genau mit dem Ergebnistyp der Funktion der übergeordneten Methode übereinstimmen. Die Überschreibung muss auch eine virtuelle Direktive enthalten, gefolgt von demselben dynamischen Methodenindex, der im Vorgängerobjekttyp angegeben wurde.

2. Konstruktoren und Destruktoren

Konstruktoren und Destruktoren sind spezialisierte Formen von Methoden. In Verbindung mit der erweiterten Syntax der Standardprozeduren New und Dispose haben Konstruktoren und Destruktoren die Fähigkeit, dynamische Objekte zu platzieren und zu entfernen. Darüber hinaus haben Konstruktoren die Möglichkeit, die erforderliche Initialisierung von Objekten durchzuführen, die virtuelle Methoden enthalten. Wie alle Methoden können Konstruktoren und Destruktoren vererbt werden, und Objekte können beliebig viele Konstruktoren und Destruktoren enthalten.

Konstruktoren werden verwendet, um neu erstellte Objekte zu initialisieren. Typischerweise basiert die Initialisierung auf den Werten, die dem Konstruktor als Parameter übergeben werden. Ein Konstruktor kann nicht virtuell sein, da der Dispatch-Mechanismus einer virtuellen Methode von dem Konstruktor abhängt, der das Objekt zuerst initialisiert hat.

Hier sind einige Beispiele für Konstruktoren:

Konstruktor Field.Copy(var F: Field);

beginnen

Selbst := F;

end;

Konstruktor Field.Init(FX, FY, FLen: integer; FName: string);

beginnen

X := FX;

J := FJ;

GetMem(Name, Länge(FName) + 1);

Name^ := FName;

end;

Konstruktor TStrField.Init(FX, FY, FLen: integer; FName: string);

beginnen

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

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

GetMem(Wert, Len);

Wert^ := '';

end;

Die Hauptaktion eines Konstruktors eines abgeleiteten (untergeordneten) Typs, wie z. B. das obige TStr-Feld. Init ist fast immer ein Aufruf an den entsprechenden Konstruktor seines unmittelbaren Elternobjekts, um die geerbten Felder des Objekts zu initialisieren. Nach Ausführung dieser Prozedur initialisiert der Konstruktor die Felder des Objekts, die nur zum abgeleiteten Typ gehören.

Destruktoren sind das Gegenteil von Konstruktoren und werden verwendet, um Objekte zu bereinigen, nachdem sie verwendet wurden. Normalerweise besteht die Bereinigung darin, alle Zeigerfelder im Objekt zu entfernen.

Beachten

Ein Destruktor kann virtuell sein und ist es oft. Ein Destruktor hat selten Parameter.

Hier sind einige Beispiele für Destruktoren:

Destruktor Feld Fertig;

beginnen

FreeMem(Name, Länge(Name^) + 1);

end;

Destruktor StrField.Done;

beginnen

FreeMem(Wert, Len);

Feld erledigt;

end;

Der Destruktor eines untergeordneten Typs, wie z. B. TStrField oben. Done entfernt normalerweise zuerst die im abgeleiteten Typ eingeführten Zeigerfelder und ruft dann als letzten Schritt den entsprechenden Kollektor-Destruktor des unmittelbar übergeordneten Elements auf, um die geerbten Zeigerfelder des Objekts zu entfernen.

3. Destruktoren

Borland Pascal bietet eine spezielle Methode namens Garbage Collector (oder Destruktor) zum Aufräumen und Löschen eines dynamisch zugewiesenen Objekts. Der Destruktor kombiniert den Schritt des Löschens eines Objekts mit allen anderen Aktionen oder Aufgaben, die für diesen Objekttyp erforderlich sind. Sie können mehrere Destruktoren für einen einzelnen Objekttyp definieren.

Der Destruktor wird zusammen mit allen anderen Objektmethoden in der Typdefinition des Objekts definiert:

Typ

Temployee = Objekt

Name: Zeichenkette[25];

Titel: string[25];

Rate: Real;

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

Destruktor Fertig; virtuell;

Funktion GetName: String;

Funktion GetTitle: String;

Funktion GetRate: Rate; virtuell;

Funktion GetPayAmount: Real; virtuell;

end;

Destruktoren können vererbt werden und sie können entweder statisch oder virtuell sein. Da unterschiedliche Finalizer tendenziell unterschiedliche Objekttypen erfordern, wird allgemein empfohlen, dass Destruktoren immer virtuell sind, damit für jeden Objekttyp der richtige Destruktor ausgeführt wird.

Der reservierte Wortdestruktor muss nicht für jede Bereinigungsmethode angegeben werden, auch wenn die Typdefinition des Objekts virtuelle Methoden enthält. Destruktoren funktionieren wirklich nur mit dynamisch zugewiesenen Objekten.

Beim Aufräumen eines dynamisch allokierten Objekts erfüllt der Destruktor eine besondere Funktion: Er sorgt dafür, dass im dynamisch allokierten Speicherbereich immer die richtige Anzahl Bytes freigegeben wird. Es besteht kein Grund zur Sorge, einen Destruktor mit statisch zugeordneten Objekten zu verwenden; Indem der Programmierer den Typ des Objekts nicht an den Destruktor übergibt, entzieht der Programmierer einem Objekt dieses Typs die vollen Vorteile der dynamischen Speicherverwaltung in Borland Pascal.

Destruktoren werden tatsächlich zu sich selbst, wenn polymorphe Objekte gelöscht und der von ihnen belegte Speicher freigegeben werden muss.

Polymorphe Objekte sind solche Objekte, die aufgrund der erweiterten Typkompatibilitätsregeln von Borland Pascal einem übergeordneten Typ zugewiesen wurden. Eine Instanz eines Objekts vom Typ Thourly, die einer Variablen vom Typ TEmployee zugewiesen ist, ist ein Beispiel für ein polymorphes Objekt. Diese Regeln können auch auf Objekte angewendet werden; ein Zeiger auf THourly kann frei einem Zeiger auf TEmployee zugewiesen werden, und das Objekt, auf das dieser Zeiger zeigt, ist wieder ein polymorphes Objekt. Der Begriff „polymorph“ ist angemessen, da Code, der ein Objekt verarbeitet, zur Kompilierzeit „nicht genau weiß“, welche Art von Objekt er schließlich verarbeiten muss. Das einzige, was es weiß, ist, dass dieses Objekt zu einer Hierarchie von Objekten gehört, die Nachkommen des angegebenen Objekttyps sind.

Offensichtlich sind die Größen der Objekttypen unterschiedlich. Wenn es also an der Zeit ist, ein Heap-zugewiesenes polymorphes Objekt zu bereinigen, woher weiß Dispose, wie viele Bytes Heap-Speicherplatz freizugeben ist? Zur Kompilierzeit können aus einem polymorphen Objekt keine Informationen über die Größe des Objekts extrahiert werden.

Der Destruktor löst dieses Rätsel, indem er auf die Stelle verweist, an der diese Informationen geschrieben sind – in den TCM-Implementierungsvariablen. Jede TBM eines Objekttyps enthält die Größe dieses Objekttyps in Bytes. Die Tabelle der virtuellen Methoden jedes Objekts ist über den versteckten Parameter Self verfügbar, der an die Methode gesendet wird, wenn die Methode aufgerufen wird. Ein Destruktor ist nur eine Art Methode, und wenn ein Objekt ihn aufruft, erhält der Destruktor eine Kopie von Self auf dem Stapel. Wenn also ein Objekt zur Kompilierzeit polymorph ist, wird es aufgrund der späten Bindung zur Laufzeit nie polymorph sein.

Um diese spät gebundene Freigabe durchzuführen, muss der Destruktor als Teil der erweiterten Syntax der Dispose-Prozedur aufgerufen werden:

Entsorgen (P, Fertig);

(Ein Aufruf des Destruktors außerhalb der Dispose-Prozedur gibt überhaupt keinen Speicher frei.) Was hier wirklich passiert, ist, dass der Garbage Collector des Objekts, auf das P zeigt, wie eine normale Methode ausgeführt wird. Sobald die letzte Aktion jedoch abgeschlossen ist, sucht der Destruktor die Größe der Implementierung seines Typs im TCM und übergibt die Größe an die Dispose-Prozedur. Die Dispose-Prozedur beendet den Prozess, indem sie die korrekte Anzahl von Bytes des Heap-Speicherplatzes löscht, der (der Speicherplatz) zuvor zu P^ gehörte. Die Anzahl der freizugebenden Bytes ist korrekt, unabhängig davon, ob P auf eine Instanz des Typs TSalried zeigt oder ob es auf einen der untergeordneten Typen des Typs TSalried zeigt, wie z. B. TCommissioned.

Beachten Sie, dass die Destruktormethode selbst leer sein und nur diese Funktion ausführen kann:

destructorAnObject.Done;

beginnen

end;

Was in diesem Destruktor nützlich ist, ist nicht die Eigenschaft seines Hauptteils, jedoch generiert der Compiler Epilogcode als Antwort auf das reservierte Wort des Destruktors. Es ist wie ein Modul, das nichts exportiert, aber unsichtbar arbeitet, indem es seinen Initialisierungsabschnitt ausführt, bevor es das Programm startet. Alle Aktionen finden hinter den Kulissen statt.

4. Virtuelle Methoden

Eine Methode wird virtuell, wenn auf ihre Objekttypdeklaration das neue reservierte Wort virtual folgt. Wenn eine Methode in einem übergeordneten Typ als virtuell deklariert wird, müssen alle gleichnamigen Methoden in untergeordneten Typen ebenfalls als virtuell deklariert werden, um einen Compilerfehler zu vermeiden.

Das Folgende sind die Objekte aus der Beispiel-Gehaltsabrechnung, richtig virtualisiert:

Typ

Mitarbeiter = ^Mitarbeiter;

Temployee = Objekt

Name, Titel: string[25];

Rate: Real;

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

Funktion GetPayAmount : Real; virtuell;

Funktion GetName : String;

Funktion GetTitle : String;

Funktion GetRate : Real;

Verfahren Zeigen; virtuell;

end;

PStündlich = ^TStündlich;

Thourly = object(TEmployee);

Zeit: Ganzzahl;

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

Funktion GetPayAmount : Real; virtuell;

Funktion GetTime : Integer;

end;

PSangestellt = ^TSangestellt;

TSalried = object(TEmployee);

Funktion GetPayAmount : Real; virtuell;

end;

PCommissioned = ^TCommissioned;

TCommissioned = Objekt (Angestellt);

Provision: Real;

Verkaufsbetrag: Real;

Konstruktor Init(AName, ATitel: String; ARate,

AProvision, ASalesAmount: Real);

Funktion GetPayAmount : Real; virtuell;

end;

Ein Konstruktor ist eine spezielle Art von Prozedur, die einige Einrichtungsarbeiten für den virtuellen Methodenmechanismus durchführt. Darüber hinaus muss der Konstruktor aufgerufen werden, bevor eine virtuelle Methode aufgerufen wird. Das Aufrufen einer virtuellen Methode, ohne zuerst den Konstruktor aufzurufen, kann das System blockieren, und der Compiler hat keine Möglichkeit, die Reihenfolge zu überprüfen, in der die Methoden aufgerufen werden.

Jeder Objekttyp, der virtuelle Methoden hat, muss einen Konstruktor haben.

Warnung

Der Konstruktor muss aufgerufen werden, bevor eine andere virtuelle Methode aufgerufen wird. Der Aufruf einer virtuellen Methode ohne vorherigen Aufruf des Konstruktors kann eine Systemsperre verursachen und der Compiler kann die Reihenfolge, in der die Methoden aufgerufen werden, nicht überprüfen.

Beachten

Für Objektkonstruktoren wird empfohlen, den Bezeichner Init zu verwenden.

Jede einzelne Objektinstanz muss mit einem separaten Konstruktoraufruf initialisiert werden. Es reicht nicht aus, eine Instanz eines Objekts zu initialisieren und diese Instanz dann anderen zuzuweisen. Andere Instanzen, auch wenn sie gültige Daten enthalten, werden nicht mit einem Zuweisungsoperator initialisiert und blockieren das System bei allen Aufrufen ihrer virtuellen Methoden. Zum Beispiel:

jung

FBee, GBee: Biene; { zwei Bee-Instanzen erstellen }

beginnen

FBee.Init(5, 9) { Konstruktoraufruf für FBee }

GBee := FBee; {Gbee ist ungültig! }

end;

Was genau erstellt ein Konstrukteur? Jeder Objekttyp enthält im Datensegment etwas, das als virtuelle Methodentabelle (VMT) bezeichnet wird. Das TVM enthält die Größe des Objekttyps und für jede virtuelle Methode einen Zeiger auf den Code, der diese Methode ausführt. Ein Konstruktor stellt eine Beziehung zwischen der aufrufenden Implementierung des Objekts und dem Typ TCM des Objekts her.

Es ist wichtig zu bedenken, dass es für jeden Objekttyp nur eine TBM gibt. Separate Instanzen eines Objekttyps (d. h. Variablen dieses Typs) enthalten nur die Verbindung zur TBM, nicht aber die TBM selbst. Der Konstrukteur setzt den Wert dieser Verbindung auf TBM. Aus diesem Grund können Sie die Ausführung nirgendwo starten, bevor Sie den Konstruktor aufgerufen haben.

5. Objektdatenfelder und formale Methodenparameter

Die Tatsache, dass Methoden und ihre Objekte einen gemeinsamen Gültigkeitsbereich haben, impliziert, dass die formalen Parameter einer Methode nicht mit den Datenfeldern des Objekts identisch sein können. Dies ist keine neue Einschränkung, die durch die objektorientierte Programmierung auferlegt wird, sondern eher die gleichen alten Geltungsbereichsregeln, die Pascal schon immer hatte. Dies ist dasselbe wie zu verhindern, dass die formalen Parameter einer Prozedur mit den lokalen Variablen der Prozedur identisch sind:

Prozedur CrunchIt(Crunchee: MyDataRec, Crunchby,

Fehlercode: Ganzzahl);

jung

A, B: Zeichen;

Fehlercode: Ganzzahl;

beginnen

.

.

.

Die lokalen Variablen einer Prozedur und ihre formalen Parameter haben einen gemeinsamen Geltungsbereich und können daher nicht identisch sein. Wenn Sie versuchen, so etwas zu kompilieren, erhalten Sie die Meldung "Fehler 4: Doppelte Kennung". Derselbe Fehler tritt auf, wenn Sie versuchen, einen formalen Methodenparameter auf den Namen des Felds des Objekts zu setzen, zu dem diese Methode gehört.

Die Umstände sind etwas anders, da das Platzieren des Prozedur-Headers in einer Datenstruktur eine Anspielung auf eine Innovation in Turbo Pascal ist, aber die Grundprinzipien des Pascal-Bereichs haben sich nicht geändert.

VORTRAG Nr. 13. Kompatibilität von Objekttypen

1. Einkapselung

Die Kombination von Code und Daten in einem Objekt wird Kapselung genannt. Grundsätzlich ist es möglich, genügend Methoden bereitzustellen, damit der Benutzer eines Objekts niemals direkt auf die Felder des Objekts zugreifen wird. Einige andere objektorientierte Sprachen wie Smalltalk erfordern eine obligatorische Kapselung, aber Borland Pascal hat die Wahl.

Beispielsweise sind die Objekte TEmployee und Thourly so geschrieben, dass es absolut nicht nötig ist, direkt auf ihre internen Datenfelder zuzugreifen:

tippe

Temployee = Objekt

Name, Titel: string[25];

Rate: Real;

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

Funktion GetName : String;

Funktion GetTitle : String;

Funktion GetRate : Real;

Funktion GetPayAmount : Real;

end;

Thourly = object(TEmployee)

Zeit: Ganzzahl;

Prozedur Init(AName, ATitel: Zeichenkette; ARate:

Real, Zeit: Ganzzahl);

Funktion GetPayAmount : Real;

end;

Hier gibt es nur vier Datenfelder: Name, Title, Rate und Time. Die Methoden GetName und GetTitle zeigen den Nachnamen bzw. die Position des Mitarbeiters an. Die GetPayAmount-Methode verwendet Rate und im Fall einer Arbeitszeit Thourly und Time, um die Höhe der Zahlungen an die Arbeitszeit zu berechnen. Auf diese Datenfelder muss nicht mehr direkt verwiesen werden.

Unter der Annahme, dass eine AnHourly-Instanz vom Typ THourly existiert, könnten wir eine Reihe von Methoden verwenden, um AnHourly-Datenfelder wie folgt zu manipulieren:

mit einer stündlichen tun

beginnen

Init (Aleksandr Petrov, Gabelstaplerfahrer' 12.95, 62);

{Zeigt den Nachnamen, die Position und die Höhe der Zahlungen an}

Show;

end;

Zu beachten ist, dass der Zugriff auf die Felder eines Objekts nur mit Hilfe von Methoden dieses Objekts erfolgt.

2. Expandierende Objekte

Leider bietet Standard-Pascal keine Möglichkeit, flexible Prozeduren zu erstellen, die es Ihnen ermöglichen, mit völlig unterschiedlichen Datentypen zu arbeiten. Die objektorientierte Programmierung löst dieses Problem mit Vererbung: Wird ein abgeleiteter Typ definiert, dann werden die Methoden des übergeordneten Typs geerbt, können aber auf Wunsch überschrieben werden. Um eine geerbte Methode zu überschreiben, deklarieren Sie einfach eine neue Methode mit demselben Namen wie die geerbte Methode, aber mit einem anderen Hauptteil und (falls erforderlich) einem anderen Parametersatz.

Lassen Sie uns im folgenden Beispiel einen untergeordneten Typ von TEmployee definieren, der einen Mitarbeiter darstellt, der einen Stundensatz erhält:

const

Zahlungsperioden = 26; { Zahlungsfristen }

Überstundenschwelle = 80; { für die Zahlungsfrist }

Überstundenfaktor = 1.5; {Stundensatz}

tippe

Thourly = object(TEmployee)

Zeit: Ganzzahl;

Prozedur Init(AName, ATitel: Zeichenkette; ARate:

Real, Zeit: Ganzzahl);

Funktion GetPayAmount : Real;

end;

Prozedur THourly.Init(AName, ATitle: string;

ARate: Real, Atime: Integer);

beginnen

TEmployee.Init(AName, ATitle, ARate);

Zeit := AZeit;

end;

Funktion Thourly.GetPayAmount: Real;

jung

Überstunden: Ganzzahl;

beginnen

Überstunden := Zeit - OvertimeThreshold;

wenn Überstunden > 0 dann

GetPayAmount := RoundPay(OvertimeThreshold * Rate +

Tarif Überstunden * Überstundenfaktor * Tarif)

sonst

GetPayAmount := RoundPay(Zeit * Rate)

end;

Eine Person, die einen Stundenlohn erhält, ist ein Arbeiter: Er hat alles, was zur Definition des TEmployee-Objekts verwendet wird (Name, Position, Gehalt), und nur die Höhe des Geldes, das der Stundenlohn erhält, hängt davon ab, wie viele Stunden er während des gearbeitet hat Zeitraum zu zahlen. Daher erfordert Thourly auch ein Zeitfeld.

Da Thourly ein neues Zeitfeld definiert, erfordert seine Initialisierung eine neue Init-Methode, die sowohl die Zeit als auch die geerbten Felder initialisiert. Anstatt geerbten Feldern wie Name, Title und Rate direkt Werte zuzuweisen, warum nicht die Initialisierungsmethode des TEmployee-Objekts wiederverwenden (dargestellt durch die erste THourly Init-Anweisung).

Das Aufrufen einer Methode, die überschrieben wird, ist nicht der beste Stil. Im Allgemeinen ist es möglich, dass TEmployee.Init eine wichtige, aber versteckte Initialisierung durchführt.

Beim Aufrufen einer überschriebenen Methode müssen Sie sicher sein, dass der abgeleitete Objekttyp die Funktionalität des übergeordneten Objekts enthält. Darüber hinaus wirkt sich jede Änderung an der übergeordneten Methode automatisch auf alle untergeordneten Methoden aus.

Nach dem Aufruf von TEmployee.Init kann Thourly.Init dann eine eigene Initialisierung durchführen, die in diesem Fall nur aus der Zuweisung des in ATime übergebenen Werts besteht.

Ein weiteres Beispiel für eine überschriebene Methode ist die Funktion Thourly.GetPayAmount, die den Auszahlungsbetrag für einen stündlichen Mitarbeiter berechnet. Tatsächlich hat jeder Typ von TEmployee-Objekt seine eigene GetPayAmount-Methode, da der Typ des Arbeiters davon abhängt, wie die Berechnung durchgeführt wird. Die Thourly.GetPayAmount-Methode sollte berücksichtigen, wie viele Stunden der Mitarbeiter gearbeitet hat, ob Überstunden angefallen sind, wie hoch der Erhöhungsfaktor für Überstunden war usw.

TAngestellte Methode. GetPayAmount sollte nur die Rate des Mitarbeiters durch die Anzahl der Zahlungen in jedem Jahr dividieren (in unserem Beispiel).

Einheitsarbeiter;

Schnittstelle

const

Zahlungsperioden = 26; {Im Jahr}

Überstundenschwelle = 80; {für jeden Zahlungszeitraum}

Überstundenfaktor=1.5; {Erhöhung gegen normale Zahlung}

tippe

Temployee = Objekt

Name, Titel: string[25];

Rate: Real;

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

Funktion GetName : String;

Funktion GetTitle : String;

Funktion GetRate : Real;

Funktion GetPayAmount : Real;

end;

Thourly = object(TEmployee)

Zeit: Ganzzahl;

Prozedur Init(AName, ATitel: Zeichenkette; ARate:

Real, Zeit: Ganzzahl);

Funktion GetPayAmount : Real;

Funktion GetTime : Real;

end;

TGehalt = Objekt(TMitarbeiter)

Funktion GetPayAmount : Real;

end;

TCommissioned = Objekt (TSalried)

Provision: Real;

Verkaufsbetrag: Real;

Konstruktor Init(AName, ATitel: String; ARate,

AProvision, ASalesAmount: Real);

Funktion GetPayAmount : Real;

end;

Implementierung

Funktion RoundPay(Löhne: Real) : Real;

{Runden Sie Auszahlungen auf, um Beträge von weniger als zu ignorieren

Geldeinheit}

beginnen

RoundPay := Trunc(Löhne * 100) / 100;

.

.

.

TEmployee ist die Spitze unserer Objekthierarchie und enthält die erste GetPayAmount-Methode.

Funktion TEmployee.GetPayAmount : Real;

beginnen

RunError (211); {Laufzeitfehler geben}

end;

Es mag überraschen, dass die Methode einen Laufzeitfehler ausgibt. Wenn Employee.GetPayAmount aufgerufen wird, tritt ein Fehler im Programm auf. Wieso den? Weil TEmployee die Spitze unserer Objekthierarchie ist und keinen echten Arbeiter definiert; Daher wird keine der TEmployee-Methoden auf eine bestimmte Weise aufgerufen, obwohl sie geerbt werden können. Alle unsere Mitarbeiter arbeiten entweder auf Stunden-, Gehalts- oder Akkordbasis. Ein Laufzeitfehler beendet die Programmausführung und gibt 211 aus, was einer Fehlermeldung entspricht, die einem abstrakten Methodenaufruf zugeordnet ist (wenn das Programm TEmployee.GetPayAmount versehentlich aufruft).

Unten ist die Thourly.GetPayAmount-Methode, die Dinge wie Überstundenvergütung, geleistete Arbeitsstunden usw. berücksichtigt.

Funktion Thourly.GetPayAMount : Real;

jung

OverTime: Ganzzahl;

beginnen

Überstunden := Zeit - OvertimeThreshold;

wenn Überstunden > 0 dann

GetPayAmount := RoundPay(OvertimeThreshold * Rate +

Tarif Überstunden * Überstundenfaktor * Tarif)

sonst

GetPayAmount := RoundPay(Zeit * Rate)

end;

Die Methode TSalried.GetPayAmount ist viel einfacher; Wette darauf

dividiert durch die Anzahl der Zahlungen:

Funktion TSalried.GetPayAmount : Real;

beginnen

GetPayAmount := RoundPay(Rate / PayPeriods);

end;

Wenn Sie sich die Methode TCommissioned.GetPayAmount ansehen, sehen Sie, dass sie TSalaried.GetPayAmount aufruft, die Provision berechnet und sie zu dem von der Methode TSalried zurückgegebenen Wert addiert. GetPayAmount.

Funktion TCommissioned.GetPayAmount : Real;

beginnen

GetPayAmount := RoundPay(TSalried.GetPayAmount +

Provision * Verkaufsbetrag);

end;

Wichtiger Hinweis: Während Methoden überschrieben werden können, können Datenfelder nicht überschrieben werden. Sobald ein Datenfeld in einer Objekthierarchie definiert wurde, kann kein untergeordneter Typ ein Datenfeld mit genau demselben Namen definieren.

3. Kompatibilität von Objekttypen

Die Vererbung modifiziert die Typkompatibilitätsregeln von Borland Pascal bis zu einem gewissen Grad. Unter anderem erbt ein abgeleiteter Typ die Typkompatibilität aller seiner übergeordneten Typen.

Diese erweiterte Typkompatibilität nimmt drei Formen an:

1) zwischen Implementierungen von Objekten;

2) zwischen Zeigern auf Objektimplementierungen;

3) zwischen formalen und tatsächlichen Parametern.

Es ist jedoch sehr wichtig, sich daran zu erinnern, dass sich die Typkompatibilität in allen drei Formen nur vom untergeordneten zum übergeordneten Element erstreckt. Mit anderen Worten, untergeordnete Typen können anstelle von übergeordneten Typen frei verwendet werden, aber nicht umgekehrt.

Beispielsweise ist „TSalried“ ein untergeordnetes Element von „TEmployee“ und „TSosh-missioned“ ein untergeordnetes Element von „TSalried“. Betrachten Sie in diesem Sinne die folgenden Beschreibungen:

Typ

Mitarbeiter = ^Mitarbeiter;

PSangestellt = ^TSangestellt;

PCommissioned = ^TCommissioned;

jung

EinMitarbeiter: TEmployee;

AAngestellt: TAngestellt;

PIn Betrieb genommen: TIn Betrieb genommen;

TEmployeePtr: TEmployee;

TSangestelltPtr: PSangestellt;

TCommissionedPtr: PCommissioned;

Unter diesen Beschreibungen sind die folgenden Operatoren gültig

Zuordnungen:

EinMitarbeiter :=ASangestellt;

ASalied := ACommissioned;

TCommissionedPtr := ACommissioned;

Beachten

Einem übergeordneten Objekt kann eine Instanz eines beliebigen seiner abgeleiteten Typen zugewiesen werden. Rückabtretungen sind nicht zulässig.

Dieses Konzept ist neu für Pascal, und zunächst kann es schwierig sein, sich daran zu erinnern, welche Auftragstypkompatibilität besteht. Sie müssen so denken: Die Quelle muss in der Lage sein, den Empfänger vollständig zu füllen. Abgeleitete Typen enthalten aufgrund der Vererbungseigenschaft alles, was ihre übergeordneten Typen enthalten. Daher ist der abgeleitete Typ entweder genau gleich groß oder (was meistens der Fall ist) größer als sein Elterntyp, aber niemals kleiner. Das Zuweisen eines übergeordneten (übergeordneten) Objekts zu einem untergeordneten (untergeordneten) Objekt könnte einige Felder des untergeordneten Objekts undefiniert lassen, was gefährlich und daher illegal ist.

In Zuweisungsanweisungen werden nur Felder, die beiden Typen gemeinsam sind, von der Quelle zum Ziel kopiert. Im Zuweisungsoperator:

AnEmployee:= ACommissioned;

Nur die Felder „Name“, „Titel“ und „Rate“ von ACommissioned werden in AnEmployee kopiert, da dies die einzigen Felder sind, die TCommissioned und TEmployee gemeinsam haben. Typkompatibilität funktioniert auch zwischen Zeigern auf Objekttypen und folgt den gleichen allgemeinen Regeln wie für Objektimplementierungen. Ein Zeiger auf ein Kind kann einem Zeiger auf das Elternteil zugewiesen werden. Angesichts der vorherigen Definitionen gelten die folgenden Zeigerzuweisungen:

TSalriedPtr:= TCommissionedPtr;

TEmployeePtr:= TSalriedPtr;

TEmployeePtr:= PCommissionedPtr;

Denken Sie daran, dass umgekehrte Zuweisungen nicht erlaubt sind!

Ein formaler Parameter (entweder ein Wert oder ein variabler Parameter) eines bestimmten Objekttyps kann als eigentlichen Parameter ein Objekt seines eigenen Typs oder Objekte aller untergeordneten Typen annehmen. Wenn Sie einen Prozedurkopf wie folgt definieren:

Prozedur CalcFedTax(Victim: TSalried);

dann können die eigentlichen Parametertypen TSalried oder TCommissioned sein, aber nicht TEmployee. Victim kann auch ein variabler Parameter sein. In diesem Fall gelten die gleichen Kompatibilitätsregeln.

Hinweis:

Es gibt einen grundlegenden Unterschied zwischen Wertparametern und variablen Parametern. Ein Wertparameter ist ein Zeiger auf das tatsächliche Objekt, das als Parameter übergeben wird, während ein variabler Parameter nur eine Kopie des tatsächlichen Parameters ist. Darüber hinaus enthält diese Kopie nur die Felder, die im Typ des formalen Wertparameters enthalten sind. Das heißt, der Aktualparameter wird buchstäblich in den Typ des Formalparameters konvertiert. Ein variabler Parameter ist eher wie das Umwandeln in ein Muster, in dem Sinne, dass der tatsächliche Parameter unverändert bleibt.

Wenn der formale Parameter ein Zeiger auf einen Objekttyp ist, kann der tatsächliche Parameter ein Zeiger auf diesen Objekttyp oder auf einen beliebigen untergeordneten Typ sein. Der Titel des Verfahrens sei gegeben:

Prozedur Worker.Add(AWorker: PSalared);

Gültige Aktualparametertypen wären dann PSalried oder PCommissioned, aber nicht PEmployee.

VORTRAG Nr. 14. Monteur

1. Über Assembler

Assembler war einmal eine Sprache, ohne deren Wissen es unmöglich war, einen Computer dazu zu bringen, irgendetwas Nützliches zu tun. Allmählich änderte sich die Situation. Bequemere Kommunikationsmittel mit einem Computer erschienen. Aber im Gegensatz zu anderen Sprachen ist Assembler nicht gestorben und konnte dies im Prinzip auch nicht. Wieso den? Auf der Suche nach einer Antwort werden wir versuchen zu verstehen, was Assemblersprache im Allgemeinen ist.

Kurz gesagt, Assemblersprache ist eine symbolische Darstellung der Maschinensprache. Alle Prozesse in der Maschine auf der untersten Hardwareebene werden nur durch Befehle (Anweisungen) der Maschinensprache gesteuert. Daraus wird deutlich, dass trotz des gebräuchlichen Namens die Assemblersprache für jeden Computertyp unterschiedlich ist. Dies gilt auch für das Erscheinungsbild von in Assembler geschriebenen Programmen und die Ideen, die diese Sprache widerspiegelt.

Hardwarebezogene Probleme wirklich zu lösen (oder noch mehr hardwarebezogene Probleme, wie etwa die Geschwindigkeit eines Programms zu verbessern), ist ohne Assembler-Kenntnisse unmöglich.

Ein Programmierer oder jeder andere Benutzer kann alle High-Level-Tools bis hin zu Programmen zum Erstellen virtueller Welten verwenden und vielleicht nicht einmal ahnen, dass der Computer tatsächlich nicht die Befehle der Sprache ausführt, in der sein Programm geschrieben ist, sondern deren transformierte Darstellung in Form einer langweiligen und langweiligen Folge von Befehlen einer völlig anderen Sprache - der Maschinensprache. Stellen Sie sich nun vor, dass ein solcher Benutzer ein nicht standardmäßiges Problem hat. Beispielsweise muss sein Programm mit einem ungewöhnlichen Gerät funktionieren oder andere Aktionen ausführen, die Kenntnisse über die Prinzipien der Computerhardware erfordern. Egal wie gut die Sprache ist, in der der Programmierer sein Programm geschrieben hat, er kommt nicht ohne Assemblerkenntnisse aus. Und es ist kein Zufall, dass fast alle Compiler von Hochsprachen Mittel enthalten, um ihre Module mit Modulen in Assembler zu verbinden oder den Zugriff auf die Assembler-Programmierebene zu unterstützen.

Ein Computer besteht aus mehreren physischen Geräten, die jeweils mit einer Einheit verbunden sind, die als Systemeinheit bezeichnet wird. Um ihren funktionellen Zweck zu verstehen, betrachten wir das Blockdiagramm eines typischen Computers (Abb. 1). Sie erhebt keinen Anspruch auf absolute Genauigkeit und soll nur den Zweck, die Verbindung und die typische Zusammensetzung der Elemente eines modernen Personal Computers zeigen.

Reis. 1. Strukturdiagramm eines Personal Computers

2. Softwaremodell des Mikroprozessors

Auf dem heutigen Computermarkt gibt es eine große Vielfalt unterschiedlicher Arten von Computern. Daher kann davon ausgegangen werden, dass der Verbraucher eine Frage dazu haben wird, wie er die Fähigkeiten eines bestimmten Typs (oder Modells) eines Computers und seine Unterscheidungsmerkmale von Computern anderer Typen (Modelle) bewerten kann. Um alle Konzepte zusammenzufassen, die einen Computer hinsichtlich seiner funktionalen programmgesteuerten Eigenschaften charakterisieren, gibt es einen speziellen Begriff - Computerarchitektur. Zum ersten Mal wurde das Konzept der Computerarchitektur mit dem Aufkommen von Maschinen der 3. Generation für ihre vergleichende Bewertung erwähnt.

Es ist sinnvoll, mit dem Erlernen der Assemblersprache eines beliebigen Computers erst dann zu beginnen, wenn Sie herausgefunden haben, welcher Teil des Computers noch sichtbar und für die Programmierung in dieser Sprache verfügbar ist. Dies ist das sogenannte Computerprogrammmodell, von dem ein Teil das Mikroprozessorprogrammmodell ist, das zweiunddreißig Register enthält, die dem Programmierer mehr oder weniger zur Verfügung stehen.

Diese Register lassen sich in zwei große Gruppen einteilen:

1) 6 Benutzerregister;

2) 16 Systemregister.

3. Benutzerregistrierungen

Wie der Name schon sagt, werden Benutzerregister aufgerufen, weil der Programmierer sie beim Schreiben seiner Programme verwenden kann. Zu diesen Registern gehören (Abb. 2):

1) acht 32-Bit-Register, die von Programmierern zum Speichern von Daten und Adressen verwendet werden können (sie werden auch als Universalregister (RON) bezeichnet):

eax/ax/ah/al;

ebx/bx/bh/bl;

edx/dx/dh/dl;

ecx/cx/ch/cl;

ebp/bp;

esi/si;

bearbeiten/di;

esp/sp.

2) sechs Segmentregister: cs, ds, ss, es, fs, gs;

3) Status- und Steuerregister:

Flags eflags/flags registrieren;

eip/ip-Befehlszeigerregister.

Reis. 2. Benutzerregistrierung

Viele dieser Register werden mit einem Schrägstrich angegeben. Dies sind keine unterschiedlichen Register – sie sind Teile eines großen 32-Bit-Registers. Sie können im Programm als separate Objekte verwendet werden.

4. Allgemeine Register

Alle Register dieser Gruppe erlauben den Zugriff auf ihre "unteren" Teile. Nur die unteren 16- und 8-Bit-Teile dieser Register können für die Selbstadressierung verwendet werden. Die oberen 16 Bit dieser Register stehen nicht als eigenständige Objekte zur Verfügung.

Lassen Sie uns die Register auflisten, die zur Gruppe der Mehrzweckregister gehören. Da sich diese Register physikalisch im Mikroprozessor innerhalb des Rechenwerks (AL>) befinden, werden sie auch als ALU-Register bezeichnet:

1) eax/ax/ah/al (Akkuregister) - Batterie. Wird zum Speichern von Zwischendaten verwendet. Bei einigen Befehlen ist die Verwendung dieses Registers erforderlich;

2) ebx/bx/bh/bl (Basisregister) - Basisregister. Wird verwendet, um die Basisadresse eines Objekts im Speicher zu speichern;

3) ecx/cx/ch/cl (Zählregister) - Zählerregister. Es wird in Befehlen verwendet, die einige sich wiederholende Aktionen ausführen. Seine Verwendung ist oft implizit und versteckt im Algorithmus des entsprechenden Befehls.

Beispielsweise analysiert der Schleifenorganisationsbefehl zusätzlich zum Übertragen der Steuerung an einen Befehl, der sich an einer bestimmten Adresse befindet, den Wert des esx/cx-Registers und dekrementiert ihn um eins;

4) edx/dx/dh/dl (Datenregister) - Datenregister.

Genau wie das eax/ax/ah/al-Register speichert es Zwischendaten. Einige Befehle erfordern seine Verwendung; bei einigen Befehlen geschieht dies implizit.

Die folgenden zwei Register dienen zur Unterstützung der sogenannten Chain-Operationen, also Operationen, die nacheinander Ketten von Elementen verarbeiten, die jeweils 32, 16 oder 8 Bit lang sein können:

1) esi/si (Quellindexregister) – Quellindex.

Dieses Register enthält bei Kettenoperationen die aktuelle Adresse des Elements in der Quellkette;

2) edi/di (Destination Index register) - Index des Empfängers (Empfänger). Dieses Register enthält bei Kettenoperationen die aktuelle Adresse in der Zielkette.

In der Architektur des Mikroprozessors auf Hardware- und Softwareebene wird eine solche Datenstruktur wie ein Stack unterstützt. Um mit dem Stack zu arbeiten, gibt es im Mikroprozessor-Befehlssystem spezielle Befehle, und im Mikroprozessor-Softwaremodell gibt es dafür spezielle Register:

1) esp/sp (Stapelzeigerregister) - Stapelzeigerregister. Enthält einen Zeiger auf den Stapelanfang im aktuellen Stapelsegment.

2) ebp/bp (Basiszeigerregister) – Stapelrahmen-Basiszeigerregister. Entwickelt, um den wahlfreien Zugriff auf Daten innerhalb des Stacks zu organisieren.

Die Verwendung von Hard-Pinning von Registern für einige Befehle macht es möglich, ihre Maschinendarstellung kompakter zu codieren. Das Wissen um diese Merkmale spart bei Bedarf zumindest einige Bytes an Speicher, der durch den Programmcode belegt wird.

5. Segmentregister

Es gibt sechs Segmentregister im Mikroprozessor-Softwaremodell: cs, ss, ds, es, gs, fs.

Ihre Existenz ist auf die Besonderheiten der Organisation und Verwendung von RAM durch Intel-Mikroprozessoren zurückzuführen. Sie liegt darin begründet, dass die Mikroprozessorhardware die strukturelle Organisation des Programms in Form von drei Teilen, Segmenten genannt, unterstützt. Dementsprechend wird eine solche Speicherorganisation als segmentiert bezeichnet.

Um anzuzeigen, auf welche Segmente das Programm zu einem bestimmten Zeitpunkt Zugriff hat, sind Segmentregister vorgesehen. Tatsächlich (mit einer leichten Korrektur) enthalten diese Register die Speicheradressen, von denen die entsprechenden Segmente beginnen. Die Logik zur Verarbeitung eines Maschinenbefehls ist so aufgebaut, dass Adressen in wohldefinierten Segmentregistern implizit verwendet werden, wenn ein Befehl abgerufen wird, auf Programmdaten zugegriffen wird oder auf den Stapel zugegriffen wird.

Der Mikroprozessor unterstützt die folgenden Arten von Segmenten.

1. Codesegment. Enthält Programmbefehle. Um auf dieses Segment zuzugreifen, wird das cs-Register (Code-Segment-Register) verwendet - das Segment-Code-Register. Es enthält die Adresse des Maschinenbefehlssegments, auf das der Mikroprozessor Zugriff hat (d. h. diese Befehle werden in die Mikroprozessor-Pipeline geladen).

2. Datensegment. Enthält die vom Programm verarbeiteten Daten. Um auf dieses Segment zuzugreifen, wird das ds-Register (Data Segment Register) verwendet - ein Segmentdatenregister, das die Adresse des Datensegments des aktuellen Programms speichert.

3. Segment stapeln. Dieses Segment ist ein Speicherbereich, der Stapel genannt wird. Der Mikroprozessor organisiert die Arbeit mit dem Stack nach folgendem Prinzip: Das zuletzt in diesen Bereich geschriebene Element wird zuerst ausgewählt. Um auf dieses Segment zuzugreifen, wird das ss-Register (Stack-Segment-Register) verwendet – das Stack-Segment-Register, das die Adresse des Stack-Segments enthält.

4. Zusätzliches Datensegment. Die Algorithmen zur Ausführung der meisten Maschinenbefehle gehen implizit davon aus, dass sich die von ihnen verarbeiteten Daten im Datensegment befinden, dessen Adresse im ds-Segmentregister steht. Wenn dem Programm ein Datensegment nicht ausreicht, hat es die Möglichkeit, drei weitere zusätzliche Datensegmente zu verwenden. Aber im Gegensatz zum Hauptdatensegment, dessen Adresse im ds-Segmentregister enthalten ist, müssen bei Verwendung zusätzlicher Datensegmente deren Adressen explizit unter Verwendung spezieller Segment-Redefinitionspräfixe im Befehl angegeben werden. Adressen zusätzlicher Datensegmente müssen in den Registern es, gs, fs (Extension Data Segment Registers) enthalten sein.

6. Status- und Steuerregister

Der Mikroprozessor enthält mehrere Register, die ständig Informationen über den Zustand sowohl des Mikroprozessors selbst als auch des Programms enthalten, dessen Anweisungen gerade in die Pipeline geladen werden. Zu diesen Registern gehören:

1) Flagregister Flags/Flags;

2) eip/ip-Befehlszeigerregister.

Mithilfe dieser Register können Sie Informationen über die Ergebnisse der Befehlsausführung erhalten und den Zustand des Mikroprozessors selbst beeinflussen. Betrachten wir den Zweck und den Inhalt dieser Register genauer.

1. flags/flags (Flag-Register) - Flag-Register. Die Bittiefe von eflags/flags beträgt 32/16 Bit. Einzelne Bits dieses Registers haben einen bestimmten funktionalen Zweck und werden Flags genannt. Der untere Teil dieses Registers ist genau derselbe wie das Flags-Register für 18086. Abbildung 3 zeigt den Inhalt des Flags-Registers.

Reis. 3. Der Inhalt des flags-Registers

Je nach Verwendung lassen sich die Flags des eflags/Flags-Registers in drei Gruppen einteilen:

1) acht Statusflags.

Diese Flags können sich ändern, nachdem Maschinenbefehle ausgeführt wurden. Die Statusflags des eflags-Registers spiegeln die Einzelheiten des Ergebnisses der Ausführung von arithmetischen oder logischen Operationen wider. Dadurch ist es möglich, den Zustand des Rechenprozesses zu analysieren und mit bedingten Sprungbefehlen und Unterprogrammaufrufen darauf zu reagieren. Tabelle 1 listet die Status-Flags und ihren Zweck auf.

2) eine Steuerflagge.

Bezeichnet als df (Verzeichnis-Flag). Es befindet sich in Bit 10 des eflags-Registers und wird von verketteten Befehlen verwendet. Der Wert des df-Flags bestimmt die Richtung der elementweisen Verarbeitung bei diesen Operationen: vom Anfang der Zeichenfolge zum Ende (df = 0) oder umgekehrt, vom Ende der Zeichenfolge zum Anfang (df = 1). Es gibt spezielle Befehle für die Arbeit mit dem df-Flag: eld (entferne das df-Flag) und std (setze das df-Flag). Durch die Verwendung dieser Befehle können Sie das df-Flag gemäß dem Algorithmus anpassen und sicherstellen, dass Zähler automatisch erhöht oder verringert werden, wenn Sie Operationen an Zeichenfolgen ausführen.

3) fünf Systemflags.

Sie steuern E/A, maskierbare Interrupts, Debugging, Taskwechsel und den virtuellen Modus 8086. Es wird Anwendungsprogrammen nicht empfohlen, diese Flags unnötig zu ändern, da dies in den meisten Fällen zum Abbruch des Programms führt. Tabelle 2 listet die System-Flags und ihren Zweck auf.

Tabelle 1. StatusflagsTabelle 2. Systemflags

2. eip/ip (Befehlszeigerregister) - Befehlszeigerregister. Das eip/ip-Register ist 32/16 Bit breit und enthält den Offset des nächsten auszuführenden Befehls relativ zu den Inhalten des cs-Segmentregisters im aktuellen Befehlssegment. Dieses Register ist für den Programmierer nicht direkt zugänglich, aber sein Wert wird durch verschiedene Steuerbefehle geladen und geändert, die Befehle für bedingte und unbedingte Sprünge, das Aufrufen von Prozeduren und das Zurückkehren von Prozeduren umfassen. Das Auftreten von Interrupts modifiziert auch das eip/ip-Register.

VORTRAG Nr. 15. Register

1. Mikroprozessorsystemregister

Schon der Name dieser Register deutet darauf hin, dass sie bestimmte Funktionen im System ausführen. Die Nutzung von Systemregistern ist streng reglementiert. Sie stellen den geschützten Modus bereit. Sie können auch als Teil der Mikroprozessorarchitektur betrachtet werden, die absichtlich sichtbar gelassen wird, damit ein qualifizierter Systemprogrammierer die einfachsten Operationen ausführen kann.

Systemregister können in drei Gruppen eingeteilt werden:

1) vier Steuerregister;

2) vier Register von Systemadressen;

3) acht Debug-Register.

2. Steuerregister

Die Gruppe der Steuerregister umfasst vier Register: cr0, cr1, cr2, cr3. Diese Register dienen der allgemeinen Systemsteuerung. Steuerregister stehen nur Programmen mit Privilegstufe 0 zur Verfügung.

Obwohl der Mikroprozessor vier Steuerregister hat, sind nur drei davon verfügbar - cr1 ist ausgenommen, dessen Funktionen noch nicht definiert wurden (es ist für zukünftige Verwendung reserviert).

Das cr0-Register enthält Systemflags, die die Betriebsmodi des Mikroprozessors steuern und seinen Zustand global widerspiegeln, unabhängig von den spezifischen Aufgaben, die ausgeführt werden.

Zweck der Systemmerker:

1) pe (Protect Enable), Bit 0 – aktiviert den geschützten Betriebsmodus. Der Zustand dieses Flags zeigt an, in welchem ​​der beiden Modi – real (pe = 0) oder geschützt (pe = 1) – der Mikroprozessor zu einem bestimmten Zeitpunkt arbeitet;

2) mp (Math Present), Bit 1 – das Vorhandensein eines Coprozessors. Immer 1;

3) ts (Task Switched), Bit 3 – Aufgabenumschaltung. Der Prozessor setzt dieses Bit automatisch, wenn er zu einer anderen Task wechselt;

4) bin (Ausrichtungsmaske), Bit 18 – Ausrichtungsmaske. Dieses Bit aktiviert (am = 1) oder deaktiviert (am = 0) die Ausrichtungssteuerung;

5) cd (Cache Disable), Bit 30 – Cache-Speicher deaktivieren.

Mit diesem Bit können Sie die Verwendung des internen Caches (des First-Level-Cache) deaktivieren (cd = 1) oder aktivieren (cd = 0);

6) pg (Paging), Bit 31 – Paging aktivieren (pg = 1) oder deaktivieren (pg = 0).

Das Flag wird im Paging-Modell der Speicherorganisation verwendet.

Das cr2-Register wird beim RAM-Paging verwendet, um die Situation zu registrieren, wenn der aktuelle Befehl auf die Adresse zugegriffen hat, die in einer Speicherseite enthalten ist, die sich derzeit nicht im Speicher befindet.

In einer solchen Situation tritt eine Ausnahmenummer 14 im Mikroprozessor auf, und die lineare 32-Bit-Adresse des Befehls, der diese Ausnahme verursacht hat, wird in das Register cr2 geschrieben. Mit dieser Information bestimmt der Ausnahmebehandler 14 die gewünschte Seite, lagert sie in den Speicher aus und nimmt den normalen Betrieb des Programms wieder auf;

Das cr3-Register wird auch für den Paging-Speicher verwendet. Dies ist das sogenannte Seitenverzeichnisregister der ersten Ebene. Es enthält die physikalische 20-Bit-Basisadresse des Seitenverzeichnisses der aktuellen Aufgabe. Dieses Verzeichnis enthält 1024 32-Bit-Deskriptoren, von denen jeder die Adresse der Seitentabelle der zweiten Ebene enthält. Jede der Seitentabellen der zweiten Ebene wiederum enthält 1024 32-Bit-Deskriptoren, die Seitenrahmen im Speicher adressieren. Die Seitenrahmengröße beträgt 4 KB.

3. Register von Systemadressen

Diese Register werden auch Speicherverwaltungsregister genannt.

Sie dienen dem Schutz von Programmen und Daten im Multitasking-Modus des Mikroprozessors. Beim Betrieb im mikroprozessorgeschützten Modus ist der Adressraum unterteilt in:

1) global – allen Tasks gemeinsam;

2) lokal - getrennt für jede Aufgabe.

Diese Trennung erklärt das Vorhandensein der folgenden Systemregister in der Mikroprozessorarchitektur:

1) das Register der globalen Deskriptortabelle gdtr (Global Descriptor Table Register), das eine Größe von 48 Bit hat und eine 32-Bit (Bits 16-47) Basisadresse der globalen Deskriptortabelle GDT und eine 16-Bit (Bits 0-15) Grenzwert, der die Größe in Bytes der GDT-Tabelle ist;

2) das lokale Deskriptortabellenregister ldtr (Local Descriptor Table Register), das eine Größe von 16 Bit hat und den sogenannten Selektor des Deskriptors der lokalen Deskriptortabelle LDT enthält. Dieser Selektor ist ein Zeiger in der GDT-Tabelle, die die beschreibt Segment, das die lokale Deskriptortabelle LDT enthält;

3) das Register der Interrupt-Deskriptortabelle idtr (Interrupt Descriptor Table Register), das eine Größe von 48 Bit hat und eine 32-Bit (Bits 16-47) Basisadresse der IDT-Interrupt-Deskriptortabelle und eine 16-Bit (Bits 0–15) Grenzwert, der die Größe in Bytes der IDT-Tabelle ist;

4) 16-Bit-Task-Register tr (Task Register), das wie das ldtr-Register einen Selektor enthält, also einen Zeiger auf einen Deskriptor in der GDT-Tabelle, der den aktuellen Task Segment Status (TSS) beschreibt. Dieses Segment wird für jede Aufgabe im System angelegt, hat eine streng geregelte Struktur und enthält den Kontext (aktueller Zustand) der Aufgabe. Der Hauptzweck von TSS-Segmenten besteht darin, den aktuellen Status einer Task im Moment des Wechsels zu einer anderen Task zu speichern.

4. ​​​​Debug-Register

Dies ist eine sehr interessante Gruppe von Registern, die für das Hardware-Debugging vorgesehen sind. Hardware-Debugging-Tools tauchten erstmals im i486-Mikroprozessor auf. In der Hardware enthält der Mikroprozessor acht Debug-Register, aber nur sechs davon werden tatsächlich verwendet.

Die Register dr0, dr1, dr2, dr3 haben eine Breite von 32 Bit und dienen zum Setzen der linearen Adressen von vier Haltepunkten. Der in diesem Fall verwendete Mechanismus ist der folgende: Jede vom aktuellen Programm generierte Adresse wird mit den Adressen in den Registern dr0 ... dr3 verglichen, und wenn eine Übereinstimmung vorliegt, wird eine Debugging-Ausnahme mit der Nummer 1 generiert.

Das Register dr6 wird als Debug-Statusregister bezeichnet. Die Bits in diesem Register werden gemäß den Gründen gesetzt, die das Auftreten der letzten Ausnahme Nummer 1 verursacht haben.

Wir listen diese Bits und ihren Zweck auf:

1) b0 – wenn dieses Bit auf 1 gesetzt ist, ist die letzte Ausnahme (Interrupt) als Ergebnis des Erreichens des im Register dr0 definierten Prüfpunkts aufgetreten;

2) b1 - ähnlich wie b0, aber für einen Kontrollpunkt im Register dr1;

3) b2 - ähnlich wie b0, aber für einen Kontrollpunkt im Register dr2;

4) bЗ - ähnlich wie b0, aber für einen Kontrollpunkt im Register dr3;

5) bd (Bit 13) – dient zum Schutz der Debug-Register;

6) bs (Bit 14) – auf 1 gesetzt, wenn Ausnahme 1 durch den Zustand des Flags tf = 1 im Flaggs-Register verursacht wurde;

7) bt (Bit 15) wird auf 1 gesetzt, wenn Ausnahme 1 durch einen Wechsel zu einer Task mit gesetztem Trap-Bit in TSS t = 1 verursacht wurde.

Alle anderen Bits in diesem Register werden mit Nullen aufgefüllt. Der Ausnahmebehandler 1 muss basierend auf dem Inhalt von dr6 den Grund für die Ausnahme bestimmen und die erforderlichen Maßnahmen ergreifen.

Das Register dr7 wird als Debug-Steuerregister bezeichnet. Es enthält Felder für jedes der vier Debug-Breakpoint-Register, mit denen Sie die folgenden Bedingungen angeben können, unter denen ein Interrupt generiert werden soll:

1) Checkpoint-Registrierungsstandort - nur in der aktuellen Aufgabe oder in jeder Aufgabe. Diese Bits belegen die unteren 8 Bits des Registers dr7 (2 Bits für jeden Haltepunkt (eigentlich ein Haltepunkt), der durch die Register dr0, dr1, dr2 bzw. dr3 gesetzt wird).

Das erste Bit jedes Paares ist die sogenannte Ortsauflösung; Wenn Sie es setzen, wird der Haltepunkt wirksam, wenn er sich im Adressraum der aktuellen Aufgabe befindet.

Das zweite Bit in jedem Paar definiert die globale Berechtigung, die anzeigt, dass der gegebene Haltepunkt innerhalb der Adressräume aller im System befindlichen Tasks gültig ist;

2) die Art des Zugriffs, durch den der Interrupt ausgelöst wird: nur beim Holen eines Befehls, beim Schreiben oder beim Schreiben/Lesen von Daten. Die Bits, die diese Art des Auftretens eines Interrupts bestimmen, befinden sich im oberen Teil dieses Registers. Auf die meisten Systemregister kann programmgesteuert zugegriffen werden.

VORTRAG Nr. 16. Assembler-Programme

1. Die Struktur des Programms in Assembler

Ein Assemblersprachenprogramm ist eine Sammlung von Speicherblöcken, die als Speichersegmente bezeichnet werden. Ein Programm kann aus einem oder mehreren dieser Blocksegmente bestehen. Jedes Segment enthält eine Sammlung von Sprachsätzen, von denen jeder eine separate Programmcodezeile belegt.

Es gibt vier Arten von Assembly-Anweisungen:

1) Befehle oder Anweisungen, die symbolische Analoga von Maschinenbefehlen sind. Während des Übersetzungsprozesses werden Assembleranweisungen in die entsprechenden Befehle des Mikroprozessor-Befehlssatzes umgewandelt;

2) Makros. Das sind Sätze des Programmtextes, die in bestimmter Weise formalisiert sind und während der Sendung durch andere Sätze ersetzt werden;

3) Anweisungen, die dem Assembler-Übersetzer anzeigen, bestimmte Aktionen auszuführen. Direktiven haben keine Entsprechungen in der Maschinendarstellung;

4) Kommentarzeilen mit beliebigen Zeichen, einschließlich Buchstaben des russischen Alphabets. Kommentare werden vom Übersetzer ignoriert.

2. Assembly-Syntax

Die Sätze, aus denen ein Programm besteht, können ein syntaktisches Konstrukt sein, das einem Befehl, Makro, einer Anweisung oder einem Kommentar entspricht. Damit der Assembler-Übersetzer sie erkennt, müssen sie nach bestimmten syntaktischen Regeln gebildet werden. Verwenden Sie dazu am besten eine formale Beschreibung der Syntax der Sprache, wie die Grammatikregeln. Die gebräuchlichsten Arten, eine Programmiersprache auf diese Weise zu beschreiben, sind Syntaxdiagramme und erweiterte Backus-Naur-Formen. Für den praktischen Gebrauch sind Syntaxdiagramme bequemer. Beispielsweise kann die Syntax von Anweisungen in Assemblersprache unter Verwendung der in den folgenden Abbildungen gezeigten Syntaxdiagramme beschrieben werden.

Reis. 4. Assembler-Satzformat

Reis. 5. Richtlinienformat

Reis. 6. Format von Befehlen und Makros

Auf diesen Zeichnungen:

1) Markenname – ein Identifizierer, dessen Wert die Adresse des ersten Bytes des Satzes des Quellcodes des Programms ist, das er bezeichnet;

2) Name – ein Identifikator, der diese Direktive von anderen Direktiven mit demselben Namen unterscheidet. Durch die Verarbeitung einer bestimmten Direktive durch den Assembler können diesem Namen bestimmte Eigenschaften zugeordnet werden;

3) ein Operationscode (COP) und eine Anweisung sind mnemonische Bezeichnungen der entsprechenden Maschinenanweisung, Makroanweisung oder Übersetzeranweisung;

4) Operanden - Teile der Befehls-, Makro- oder Assembler-Direktiven, die Objekte bezeichnen, an denen Operationen durchgeführt werden. Assembler-Operanden werden durch Ausdrücke mit numerischen und Textkonstanten, Variablenlabels und Bezeichnern mit Operationszeichen und einigen reservierten Wörtern beschrieben.

Wie verwendet man Syntaxdiagramme? Es ist sehr einfach: Alles, was Sie tun müssen, ist, den Pfad von der Eingabe des Diagramms (links) zu seiner Ausgabe (rechts) zu finden und ihm dann zu folgen. Wenn ein solcher Pfad existiert, dann ist der Satz oder die Konstruktion syntaktisch korrekt. Wenn es keinen solchen Pfad gibt, akzeptiert der Compiler diese Konstruktion nicht. Achten Sie bei der Arbeit mit Syntaxdiagrammen auf die durch die Pfeile angegebene Traversierungsrichtung, da es unter den Pfaden auch solche geben kann, die von rechts nach links verfolgt werden können. Tatsächlich spiegeln syntaktische Diagramme die Logik des Übersetzers wider, wenn er die Eingabesätze des Programms parst.

Erlaubte Zeichen beim Schreiben des Textes von Programmen sind:

1) alle lateinischen Buchstaben: A - Z, a - z. In diesem Fall werden Groß- und Kleinbuchstaben als gleichwertig betrachtet;

2) Zahlen von 0 bis 9;

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

4) Trennzeichen.

Assembler-Sätze werden aus Lexemen gebildet, die syntaktisch untrennbare Folgen gültiger Sprachsymbole sind, die für den Übersetzer sinnvoll sind.

Die Token sind wie folgt.

1. Identifikatoren – Sequenzen gültiger Zeichen, die verwendet werden, um Programmobjekte zu bezeichnen, wie beispielsweise Operationscodes, Variablennamen und Bezeichnungsnamen. Die Regel für das Schreiben von Bezeichnern lautet wie folgt: Ein Bezeichner kann aus einem oder mehreren Zeichen bestehen. Als Zeichen können Sie Buchstaben des lateinischen Alphabets, Zahlen und einige Sonderzeichen verwenden - _, ?, $, @. Ein Bezeichner darf nicht mit einer Ziffer beginnen. Die Länge des Bezeichners kann bis zu 255 Zeichen betragen, wobei der Übersetzer nur die ersten 32 Zeichen akzeptiert und den Rest ignoriert. Sie können die Länge möglicher Bezeichner mit der Befehlszeilenoption mv anpassen. Außerdem ist es möglich, den Übersetzer anzuweisen, zwischen Groß- und Kleinbuchstaben zu unterscheiden oder deren Unterschied zu ignorieren (was standardmäßig der Fall ist). Dazu werden die Kommandozeilenoptionen /mu, /ml, /mx verwendet.

2. Zeichenketten - Zeichenfolgen, die in einfache oder doppelte Anführungszeichen eingeschlossen sind.

3. Ganzzahlen in einem der folgenden Zahlensysteme: binär, dezimal, hexadezimal. Die Identifizierung von Zahlen beim Schreiben in Assemblerprogrammen erfolgt nach bestimmten Regeln:

1) Dezimalzahlen erfordern keine zusätzlichen Zeichen, um identifiziert zu werden, z. B. 25 oder 139;

2) Um Binärzahlen im Quelltext des Programms zu identifizieren, muss das lateinische "b" eingefügt werden, nachdem die Nullen und Einsen geschrieben wurden, aus denen sie bestehen, z. B. 10010101 b.

3) Hexadezimalzahlen haben mehr Konventionen beim Schreiben:

a) Erstens bestehen sie aus den Zahlen 0...9, Klein- und Großbuchstaben des lateinischen Alphabets a, b, c, d, e, Gili D B, C, D, E, E

b) Zweitens kann es für den Übersetzer schwierig sein, Hexadezimalzahlen zu erkennen, da diese nur aus den Ziffern 0...9 bestehen können (z. B. 190845) oder mit einem Buchstaben des lateinischen Alphabets beginnen können (z. B. efl5 ). Um dem Übersetzer zu „erklären“, dass es sich bei einem bestimmten Token nicht um eine Dezimalzahl oder einen Bezeichner handelt, muss der Programmierer die Hexadezimalzahl auf besondere Weise hervorheben. Schreiben Sie dazu den lateinischen Buchstaben „h“ an das Ende der Folge von Hexadezimalziffern, aus denen eine Hexadezimalzahl besteht. Das ist ein Muss. Wenn eine Hexadezimalzahl mit einem Buchstaben beginnt, wird davor eine führende Null geschrieben: 0 efl5 h.

So haben wir herausgefunden, wie die Sätze eines Assembler-Programms aufgebaut sind. Aber das ist nur die oberflächlichste Betrachtung.

Fast jeder Satz enthält eine Beschreibung des Objekts, an dem oder mit dessen Hilfe eine Aktion ausgeführt wird. Diese Objekte werden Operanden genannt. Sie können wie folgt definiert werden: Operanden sind Objekte (einige Werte, Register oder Speicherzellen), die von Anweisungen oder Anweisungen beeinflusst werden, oder sie sind Objekte, die die Aktion von Anweisungen oder Anweisungen definieren oder verfeinern.

Operanden können mit arithmetischen, logischen, bitweisen und Attributoperatoren kombiniert werden, um einen Wert zu berechnen oder einen Speicherort zu bestimmen, der von einem bestimmten Befehl oder einer bestimmten Anweisung beeinflusst wird.

Betrachten wir die Eigenschaften der Operanden in der folgenden Klassifizierung genauer:

1) Konstante oder unmittelbare Operanden – eine Zahl, ein String, ein Name oder ein Ausdruck, der einen festen Wert hat. Der Name darf nicht verschiebbar sein, dh er darf nicht von der Adresse des in den Speicher zu ladenden Programms abhängen. Beispielsweise kann es mit den Operatoren equal oder = definiert werden;

2) Adressoperanden, spezifizieren den physikalischen Ort des Operanden im Speicher durch Spezifizieren von zwei Komponenten der Adresse: Segment und Offset (Fig. 7);

Reis. 7. Syntax der Beschreibung von Adressoperanden

3) verschiebbare Operanden – beliebige symbolische Namen, die einige Speicheradressen darstellen. Diese Adressen können den Speicherort einiger Befehle (wenn der Operand ein Label ist) oder Daten (wenn der Operand der Name eines Speicherorts im Datensegment ist) anzeigen.

Verschiebbare Operanden unterscheiden sich von Adressoperanden darin, dass sie nicht an eine bestimmte physikalische Speicheradresse gebunden sind. Die Segmentkomponente der Adresse des verschobenen Operanden ist unbekannt und wird bestimmt, nachdem das Programm zur Ausführung in den Speicher geladen wurde.

Der Adresszähler ist eine spezielle Art von Operanden. Es wird durch das Zeichen S bezeichnet. Die Besonderheit dieses Operanden besteht darin, dass der Assembler-Übersetzer, wenn er auf dieses Symbol im Quellprogramm trifft, ihn stattdessen durch den aktuellen Wert des Adresszählers ersetzt. Der Wert des Adresszählers oder Platzierungszählers, wie er manchmal genannt wird, ist der Offset des aktuellen Maschinenbefehls vom Beginn des Codesegments. Im Listenformat entspricht die zweite oder dritte Spalte dem Adresszähler (je nachdem, ob die Spalte mit der Verschachtelungsebene in der Liste vorhanden ist oder nicht). Wenn wir eine Auflistung als Beispiel nehmen, ist ersichtlich, dass, wenn der Übersetzer den nächsten Assemblerbefehl verarbeitet, der Adresszähler um die Länge des generierten Maschinenbefehls erhöht wird. Es ist wichtig, diesen Punkt richtig zu verstehen. Beispielsweise ändert die Verarbeitung von Assembler-Direktiven den Zähler nicht. Direktiven sind im Gegensatz zu Assemblerbefehlen nur Anweisungen an den Compiler, bestimmte Aktionen auszuführen, um die Maschinendarstellung des Programms zu bilden, und für sie erzeugt der Compiler keine Konstrukte im Speicher.

Beachten Sie bei der Verwendung eines solchen Ausdrucks zum Springen die Länge der Anweisung selbst, in der dieser Ausdruck verwendet wird, da der Wert des Adresszählers dem Offset im Anweisungssegment dieser Anweisung entspricht und nicht der darauf folgenden Anweisung . In unserem Beispiel benötigt der jmp-Befehl 2 Bytes. Aber seien Sie vorsichtig, die Länge einer Anweisung hängt davon ab, welche Operanden sie verwendet. Eine Anweisung mit Registeroperanden ist kürzer als eine Anweisung, bei der sich einer ihrer Operanden im Speicher befindet. In den meisten Fällen kann diese Information erhalten werden, indem das Format des Maschinenbefehls bekannt ist und die Auflistungsspalte mit dem Objektcode des Befehls analysiert wird;

4) Registeroperand ist nur ein Registername. In einem Assembler-Programm können Sie die Namen aller Mehrzweckregister und der meisten Systemregister verwenden;

5) Basis- und Indexoperanden. Dieser Operandentyp wird verwendet, um indirekte Basisadressierung, indirekte Indexadressierung oder Kombinationen und Erweiterungen davon zu implementieren;

6) Strukturoperanden werden verwendet, um auf ein bestimmtes Element eines komplexen Datentyps namens Struktur zuzugreifen.

Datensätze (ähnlich einem Strukturtyp) werden verwendet, um auf ein Bitfeld eines Datensatzes zuzugreifen.

Operanden sind elementare Komponenten, die einen Teil des Maschinenbefehls bilden und die Objekte bezeichnen, an denen die Operation ausgeführt wird. In einem allgemeineren Fall können Operanden als Komponenten in komplexeren Formationen enthalten sein, die als Ausdrücke bezeichnet werden. Ausdrücke sind Kombinationen von Operanden und Operatoren, betrachtet als Ganzes. Das Ergebnis der Ausdrucksauswertung kann die Adresse einer Speicherzelle oder ein konstanter (absoluter) Wert sein.

Wir haben bereits die möglichen Arten von Operanden betrachtet. Wir listen nun die möglichen Arten von Assembler-Operatoren und die syntaktischen Regeln zur Bildung von Assembler-Ausdrücken auf und geben eine kurze Beschreibung der Operatoren.

1. Arithmetische Operatoren. Diese beinhalten:

1) unäres „+“ und „-“;

2) binär "+" und "-";

3) Multiplikation "*";

4) ganzzahlige Division "/";

5) den Rest aus der Division "mod" bekommen.

Diese Operatoren befinden sich auf den Prioritätsebenen 6,7,8, 4, XNUMX in Tabelle XNUMX.

Reis. 8. Syntax arithmetischer Operationen

2. Schiebeoperatoren verschieben den Ausdruck um die angegebene Anzahl von Bits (Abb. 9).

Reis. 9. Syntax von Shift-Operatoren

3. Vergleichsoperatoren (liefern den Wert „wahr“ oder „falsch“) sind zur Bildung logischer Ausdrücke gedacht (Abb. 10 und Tabelle 3). Der logische Wert "wahr" entspricht einer digitalen Einheit und "falsch" - Null.

Reis. 10. Syntax von Vergleichsoperatoren

Tabelle 3. Vergleichsoperatoren

4. Logische Operatoren führen bitweise Operationen an Ausdrücken durch (Abb. 11). Ausdrücke müssen absolut sein, d.h. solche, deren Zahlenwert vom Übersetzer errechnet werden kann.

Reis. 11. Syntax logischer Operatoren

5. Indexoperator []. Klammern sind ebenfalls ein Operator, und der Übersetzer nimmt ihre Anwesenheit als Anweisung wahr, den Wert von Ausdruck_1 hinter diesen Klammern mit dem in Klammern eingeschlossenen Ausdruck_2 zu addieren (Abb. 12).

Reis. 12. Indexoperator-Syntax

Beachten Sie, dass die folgende Bezeichnung in der Literatur über Assembler verwendet wird: Wenn sich der Text auf den Inhalt eines Registers bezieht, wird sein Name in Klammern gesetzt. Auch wir werden uns an diese Notation halten.

6. Der Typ-Redefinitionsoperator ptr wird verwendet, um den Typ eines Labels oder einer Variable, die durch einen Ausdruck definiert wird, neu zu definieren oder zu qualifizieren (Fig. 13).

Der Typ kann einen der folgenden Werte annehmen: byte, word, dword, qword, tbyte, near, far.

Reis. 13. Syntax des Typumdefinitionsoperators

7. Der Segment-Redefinitionsoperator ":" (Doppelpunkt) erzwingt die Berechnung einer physikalischen Adresse relativ zu einer bestimmten Segmentkomponente: "Segmentregistername", "Segmentname" aus der entsprechenden SEGMENT-Direktive oder "Gruppenname" (Abb. 14). Bei der Erörterung der Segmentierung haben wir darüber gesprochen, dass der Mikroprozessor auf Hardwareebene drei Arten von Segmenten unterstützt - Code, Stack und Daten. Was ist diese Hardwareunterstützung? Um beispielsweise die Ausführung des nächsten Befehls auszuwählen, muss der Mikroprozessor notwendigerweise den Inhalt des Segmentregisters cs und nur ihn betrachten. Und dieses Register enthält bekanntlich die (noch nicht verschobene) physikalische Adresse des Beginns des Befehlssegments. Um die Adresse eines bestimmten Befehls zu erhalten, muss der Mikroprozessor den Inhalt von cs mit 16 multiplizieren (was eine Verschiebung um vier Bits bedeutet) und den resultierenden 20-Bit-Wert zum 16-Bit-Inhalt des ip-Registers addieren. Ungefähr dasselbe passiert, wenn der Mikroprozessor die Operanden im Maschinenbefehl verarbeitet. Wenn es sieht, dass der Operand eine Adresse ist (eine effektive Adresse, die nur ein Teil der physikalischen Adresse ist), dann weiß es, in welchem ​​Segment es suchen muss - standardmäßig ist es das Segment, dessen Startadresse im Segmentregister ds gespeichert ist .

Aber was ist mit dem Stack-Segment? Im Rahmen unserer Betrachtung interessieren uns die Register sp und bp. Wenn der Mikroprozessor eines dieser Register als Operanden sieht (oder einen Teil davon, wenn der Operand ein Ausdruck ist), dann bildet er standardmäßig die physikalische Adresse des Operanden, indem er den Inhalt des ss-Registers als seine Segmentkomponente verwendet. Dies ist ein Satz von Mikroprogrammen in der Mikroprogramm-Steuereinheit, von denen jedes einen der Befehle im Mikroprozessor-Maschinenbefehlssystem ausführt. Jedes Mikroprogramm arbeitet nach seinem eigenen Algorithmus. Natürlich können Sie es nicht ändern, aber Sie können es leicht korrigieren. Dies erfolgt über das optionale Präfixfeld für Maschinenbefehle. Wenn wir uns darauf einigen, wie der Befehl funktioniert, dann fehlt dieses Feld. Wenn wir den Algorithmus des Befehls ändern möchten (wenn dies natürlich für einen bestimmten Befehl zulässig ist), muss das entsprechende Präfix gebildet werden.

Ein Präfix ist ein Ein-Byte-Wert, dessen numerischer Wert seinen Zweck bestimmt. Der Mikroprozessor erkennt durch den angegebenen Wert, dass dieses Byte ein Präfix ist, und die weitere Arbeit des Mikroprogramms wird unter Berücksichtigung der empfangenen Anweisung zur Korrektur seiner Arbeit durchgeführt. Jetzt interessiert uns einer von ihnen - das Präfix für die Segmentersetzung (Neudefinition). Sein Zweck besteht darin, dem Mikroprozessor (und tatsächlich der Firmware) anzuzeigen, dass wir das Standardsegment nicht verwenden möchten. Die Möglichkeiten für eine solche Neudefinition sind natürlich begrenzt. Das Kommandosegment kann nicht umdefiniert werden, die Adresse des nächsten ausführbaren Kommandos wird eindeutig durch das cs:ip-Paar bestimmt. Und hier Segmente eines Stapels und Daten - es ist möglich. Dafür ist der ":"-Operator da. Der Assembler-Übersetzer, der diese Anweisung verarbeitet, erzeugt das entsprechende Ein-Byte-Segment-Ersetzungspräfix.

Reis. 14. Syntax des Segment-Neudefinitionsoperators

8. Der Strukturtyp-Namensoperator "."(Punkt) zwingt den Compiler auch dazu, bestimmte Berechnungen durchzuführen, wenn er in einem Ausdruck vorkommt.

9. Der Operator zum Erhalten der Segmentkomponente der Adresse des Ausdrucks seg gibt die physische Adresse des Segments für den Ausdruck (Fig. 15) zurück, die ein Label, eine Variable, ein Segmentname, ein Gruppenname oder irgendein symbolischer Name sein kann .

Reis. 15. Syntax des Segmentkomponenten-Empfangsoperators

10. Der Operator zum Erhalten des Offsets des Ausdrucks offset ermöglicht es Ihnen, den Wert des Offsets des Ausdrucks (Fig. 16) in Bytes relativ zum Anfang des Segments zu erhalten, in dem der Ausdruck definiert ist.

Reis. 16. Syntax des Offset-Get-Operators

Wie in Hochsprachen erfolgt die Ausführung von Assembler-Operatoren bei der Auswertung von Ausdrücken entsprechend ihrer Prioritäten (Tabelle 4). Operationen mit gleicher Priorität werden sequentiell von links nach rechts ausgeführt. Das Ändern der Ausführungsreihenfolge ist möglich, indem Klammern gesetzt werden, die den höchsten Vorrang haben.

Tabelle 4. Operatoren und ihre Priorität

3. Segmentierungsrichtlinien

Im Laufe der vorherigen Diskussion haben wir alle Grundregeln für das Schreiben von Anweisungen und Operanden in einem Programm in Assemblersprache herausgefunden. Offen bleibt die Frage, wie die Befehlsfolge richtig formatiert werden muss, damit der Übersetzer sie verarbeiten und der Mikroprozessor sie ausführen kann.

Bei der Betrachtung der Architektur des Mikroprozessors haben wir gelernt, dass er sechs Segmentregister hat, durch die er gleichzeitig arbeiten kann:

1) mit einem Codesegment;

2) mit einem Stapelsegment;

3) mit einem Datensegment;

4) mit drei zusätzlichen Datensegmenten.

Erinnern Sie sich noch einmal daran, dass ein Segment physikalisch ein Speicherbereich ist, der von Befehlen und (oder) Daten belegt ist, deren Adressen relativ zum Wert im entsprechenden Segmentregister berechnet werden.

Die syntaktische Beschreibung eines Segments in Assembler ist die in Abbildung 17 gezeigte Konstruktion:

Reis. 17. Segmentbeschreibungssyntax

Es ist wichtig zu beachten, dass die Funktionalität eines Segments etwas breiter ist als das einfache Aufteilen des Programms in Code-, Daten- und Stack-Blöcke. Die Segmentierung ist Teil eines allgemeineren Mechanismus, der sich auf das Konzept der modularen Programmierung bezieht. Es beinhaltet die Vereinheitlichung des Entwurfs von Objektmodulen, die vom Compiler erstellt wurden, einschließlich derjenigen aus verschiedenen Programmiersprachen. Dadurch können Sie Programme kombinieren, die in verschiedenen Sprachen geschrieben sind. Für die Implementierung verschiedener Optionen für eine solche Union sind die Operanden in der SEGMENT-Direktive vorgesehen.

Lassen Sie uns sie genauer betrachten.

1. Das Segmentausrichtungsattribut (Ausrichtungstyp) weist den Linker an, sicherzustellen, dass der Anfang des Segments an der angegebenen Grenze platziert wird. Dies ist wichtig, da die richtige Ausrichtung den Datenzugriff auf i80x86-Prozessoren beschleunigt. Gültige Werte für dieses Attribut sind wie folgt:

1) BYTE - Ausrichtung wird nicht durchgeführt. Ein Segment kann an jeder Speicheradresse beginnen;

2) WORT – das Segment beginnt an einer Adresse, die ein Vielfaches von zwei ist, d. h. das letzte (niedrigstwertige) Bit der physikalischen Adresse ist 0 (ausgerichtet auf die Wortgrenze);

3) DWORD – das Segment beginnt an einer Adresse, die ein Vielfaches von vier ist, d. h. die letzten zwei (niedrigstwertigen) Bits sind 0 (Doppelwort-Grenzausrichtung);

4) PARA – das Segment beginnt an einer Adresse, die ein Vielfaches von 16 ist, d. h. die letzte Hexadezimalziffer der Adresse muss Oh sein (Ausrichtung auf die Absatzgrenze);

5) PAGE – das Segment beginnt an einer Adresse, die ein Vielfaches von 256 ist, d. h. die letzten beiden Hexadezimalziffern müssen 00h sein (an der Grenze einer 256-Byte-Seite ausgerichtet);

6) MEMPAGE – das Segment beginnt an einer Adresse, die ein Vielfaches von 4 KB ist, d. h. die letzten drei Hexadezimalziffern müssen OOOh sein (Adresse der nächsten 4-KB-Speicherseite). Der Standardausrichtungstyp ist PARA.

2. Das Combine-Segment-Attribut (kombinatorischer Typ) teilt dem Linker mit, wie er Segmente verschiedener Module kombinieren soll, die denselben Namen haben. Die Attributwerte der Segmentkombination können sein:

1) PRIVAT – das Segment wird nicht mit anderen Segmenten mit demselben Namen außerhalb dieses Moduls zusammengeführt;

2) ÖFFENTLICH – bewirkt, dass der Linker alle Segmente mit demselben Namen verbindet. Das neue zusammengeführte Segment wird vollständig und kontinuierlich sein. Alle Adressen (Offsets) von Objekten, dies kann von der Art des Befehls und des Datensegments abhängen, werden relativ zum Beginn dieses neuen Segments berechnet;

3) COMMON – platziert alle Segmente mit dem gleichen Namen an der gleichen Adresse. Alle Segmente mit dem angegebenen Namen werden sich überlappen und den Speicher teilen. Die Größe des resultierenden Segments entspricht der Größe des größten Segments;

4) AT xxxx – lokalisiert das Segment an der absoluten Adresse des Absatzes (Absatz ist die Speichermenge, ein Vielfaches von 16; daher ist die letzte hexadezimale Ziffer der Absatzadresse 0). Die absolute Adresse eines Absatzes wird durch xxx angegeben. Der Linker platziert das Segment unter Berücksichtigung des Kombinationsattributs an einer gegebenen Speicheradresse (diese kann beispielsweise verwendet werden, um auf einen Videospeicher oder einen ROM>-Bereich zuzugreifen). Physikalisch bedeutet dies, dass das Segment, wenn es in den Speicher geladen wird, ausgehend von dieser absoluten Adresse des Absatzes lokalisiert wird, aber um darauf zuzugreifen, muss der im Attribut angegebene Wert in das entsprechende Segmentregister geladen werden. Alle Labels und Adressen in einem so definierten Segment sind relativ zu der gegebenen absoluten Adresse;

5) STACK – Definition eines Stapelsegments. Veranlasst den Linker, alle Segmente mit demselben Namen zu verbinden und die Adressen in diesen Segmenten relativ zum ss-Register zu berechnen. Der kombinierte Typ STACK (stack) ähnelt dem kombinierten Typ PUBLIC, außer dass das ss-Register das Standardsegmentregister für Stapelsegmente ist. Das sp-Register wird auf das Ende des verketteten Stapelsegments gesetzt. Wenn kein Stapelsegment angegeben ist, gibt der Linker eine Warnung aus, dass kein Stapelsegment gefunden wurde. Wenn ein Stapelsegment erstellt wird und der kombinierte STACK-Typ nicht verwendet wird, muss der Programmierer die Segmentadresse explizit in das ss-Register laden (ähnlich dem ds-Register).

Das Kombinationsattribut ist standardmäßig PRIVATE.

3. Ein Segmentklassenattribut (Klassentyp) ist eine Zeichenfolge in Anführungszeichen, die dem Linker hilft, die richtige Segmentreihenfolge zu bestimmen, wenn er ein Programm aus mehreren Modulsegmenten zusammensetzt. Der Linker kombiniert im Speicher alle Segmente mit demselben Klassennamen (der Klassenname kann im Allgemeinen alles sein, aber es ist besser, wenn er die Funktionalität des Segments widerspiegelt). Eine typische Verwendung eines Klassennamens besteht darin, alle Codesegmente eines Programms zusammenzufassen (normalerweise wird dafür die Klasse "Code" verwendet). Mithilfe des Klassentypisierungsmechanismus können Sie auch initialisierte und nicht initialisierte Datensegmente gruppieren.

4. Segmentgrößenattribut. Für i80386 und höhere Prozessoren können Segmente 16-Bit oder 32-Bit sein. Dies betrifft in erster Linie die Größe des Segments und die Reihenfolge, in der die physikalische Adresse darin gebildet wird. Das Attribut kann folgende Werte annehmen:

1) USE16 – das bedeutet, dass das Segment eine 16-Bit-Adressierung erlaubt. Bei der Bildung einer physikalischen Adresse kann nur ein 16-Bit-Offset verwendet werden. Dementsprechend kann ein solches Segment bis zu 64 KB Code oder Daten enthalten;

2)USE32 – das Segment ist 32-Bit. Bei der Bildung einer physikalischen Adresse kann ein 32-Bit-Offset verwendet werden. Daher kann ein solches Segment bis zu 4 GB Code oder Daten enthalten.

Alle Segmente sind in sich gleich, da die Direktiven SEGMENT und ENDS keine Informationen über den funktionalen Zweck der Segmente enthalten. Um sie als Code-, Daten- oder Stapelsegmente zu verwenden, müssen Sie dies zunächst dem Übersetzer mitteilen, wofür eine spezielle ASSUME-Direktive verwendet wird, die das in Abb. 18. Diese Direktive teilt dem Übersetzer mit, welches Segment an welches Segmentregister gebunden ist. Dies ermöglicht wiederum dem Übersetzer, die in den Segmenten definierten symbolischen Namen korrekt zu binden. Das Binden von Segmenten an Segmentregister wird unter Verwendung der Operanden dieser Direktive ausgeführt, wobei segment_name der Name des Segments sein muss, das im Quellcode des Programms durch die SEGMENT-Direktive oder das Schlüsselwort nothing definiert ist. Wenn als Operand nur das Schlüsselwort nothing verwendet wird, werden die bisherigen Segmentregisterzuweisungen aufgehoben, und zwar für alle sechs Segmentregister auf einmal. Aber das Schlüsselwort nothing kann anstelle des Arguments für den Segmentnamen verwendet werden; in diesem Fall wird die Verbindung zwischen dem Segment mit dem Namen Segmentname und dem entsprechenden Segmentregister selektiv getrennt (siehe Abb. 18).

Reis. 18. ASUME-Richtlinie

Für einfache Programme, die ein Segment für Code, Daten und Stack enthalten, möchten wir die Beschreibung vereinfachen. Zu diesem Zweck haben die Übersetzer MASM und TASM die Möglichkeit eingeführt, vereinfachte Segmentierungsanweisungen zu verwenden. Aber hier entstand ein Problem in Bezug auf die Tatsache, dass es notwendig war, die Unfähigkeit, die Platzierung und Kombination von Segmenten direkt zu steuern, irgendwie zu kompensieren. Zu diesem Zweck begannen sie, zusammen mit vereinfachten Segmentierungsdirektiven, die Direktive zum Spezifizieren des MODEL-Speichermodells zu verwenden, das teilweise begann, die Platzierung von Segmenten zu steuern und die Funktionen der ASSUME-Direktive auszuführen (daher, wenn vereinfachte Segmentierungsdirektiven verwendet werden, die ASSUME-Direktive kann weggelassen werden). Diese Direktive bindet Segmente, die im Falle der Verwendung vereinfachter Segmentierungsdirektiven vordefinierte Namen haben, mit Segmentregistern (obwohl Sie ds immer noch explizit initialisieren müssen).

Die Syntax der MODEL-Direktive ist in Abbildung 19 dargestellt.

Reis. 19. Syntax der MODEL-Direktive

Der obligatorische Parameter der MODEL-Direktive ist das Speichermodell. Dieser Parameter definiert das Speichersegmentierungsmodell für die POE. Es wird davon ausgegangen, dass ein Programmmodul nur bestimmte Typen von Segmenten haben kann, die durch die bereits erwähnten vereinfachten Segmentbeschreibungsdirektiven definiert sind. Diese Anweisungen sind in Tabelle 5 aufgeführt.

Tabelle 5. Vereinfachte Segmentdefinitionsanweisungen

Das Vorhandensein des Parameters [name] in einigen Direktiven weist darauf hin, dass es möglich ist, mehrere Segmente dieses Typs zu definieren. Andererseits ist das Vorhandensein mehrerer Arten von Datensegmenten auf die Anforderung zurückzuführen, die Kompatibilität mit einigen Compilern von Hochsprachen sicherzustellen, die unterschiedliche Datensegmente für initialisierte und nicht initialisierte Daten sowie Konstanten erstellen.

Bei Verwendung der MODEL-Direktive stellt der Compiler mehrere Identifikatoren zur Verfügung, auf die während des Programmbetriebs zugegriffen werden kann, um Informationen über bestimmte Eigenschaften eines bestimmten Speichermodells zu erhalten (Tabelle 7). Lassen Sie uns diese Bezeichner und ihre Werte auflisten (Tabelle 6).

Tabelle 6. Von der MODEL-Direktive erstellte Bezeichner

Wir können jetzt unsere Diskussion der MODEL-Direktive abschließen. Die Operanden der MODEL-Direktive werden verwendet, um ein Speichermodell zu spezifizieren, das den Satz von Programmsegmenten, die Größen von Daten- und Codesegmenten und das Verfahren zum Verbinden von Segmenten und Segmentregistern definiert. Tabelle 7 zeigt einige Werte des „Memory Model“-Parameters der MODEL-Direktive.

Tabelle 7. Speichermodelle

Der „modifier“-Parameter der MODEL-Direktive ermöglicht es Ihnen, einige Merkmale der Verwendung des ausgewählten Speichermodells anzugeben (Tabelle 8).

Tabelle 8. Modifikatoren des Speichermodells

Die optionalen Parameter „Sprache“ und „Sprachmodifikator“ definieren einige Eigenschaften von Prozeduraufrufen. Die Notwendigkeit, diese Parameter zu verwenden, ergibt sich beim Schreiben und Binden von Programmen in verschiedenen Programmiersprachen.

Die von uns beschriebenen standardmäßigen und vereinfachten Segmentierungsanweisungen schließen sich nicht gegenseitig aus. Standardanweisungen werden verwendet, wenn der Programmierer die vollständige Kontrolle über die Platzierung von Segmenten im Speicher und ihre Kombination mit Segmenten aus anderen Modulen haben möchte.

Vereinfachte Direktiven sind nützlich für einfache Programme und Programme, die mit Programmmodulen verknüpft werden sollen, die in Hochsprachen geschrieben sind. Dadurch kann der Linker Module aus verschiedenen Sprachen effizient verknüpfen, indem er die Verknüpfung und Verwaltung standardisiert.

VORTRAG Nr. 17. Befehlsstrukturen in Assembler

1. Maschinenbefehlsstruktur

Ein Maschinenbefehl ist eine Anweisung an den Mikroprozessor, die nach bestimmten Regeln codiert ist, um eine Operation oder Aktion auszuführen. Jeder Befehl enthält Elemente, die Folgendes definieren:

1) was tun? (Die Antwort auf diese Frage wird durch das als Operationscode (COP) bezeichnete Befehlselement gegeben.);

2) Objekte, an denen etwas getan werden muss (diese Elemente werden Operanden genannt);

3) wie zu tun? (Diese Elemente werden als Operandentypen bezeichnet und normalerweise implizit angegeben.)

Das in Fig. 20 gezeigte Maschinenbefehlsformat ist das allgemeinste. Die maximale Länge eines Maschinenbefehls beträgt 15 Bytes. Ein echter Befehl kann eine viel kleinere Anzahl von Feldern enthalten, bis zu einem - nur KOP.

Reis. 20. Maschinenbefehlsformat

Lassen Sie uns den Zweck der Maschinenbefehlsfelder beschreiben.

1. Präfixe.

Optionale Maschinenbefehlselemente, die jeweils 1 Byte groß sind oder weggelassen werden können. Im Arbeitsspeicher stehen Präfixe vor dem Befehl. Der Zweck von Präfixen besteht darin, die vom Befehl ausgeführte Operation zu modifizieren. Eine Anwendung kann die folgenden Arten von Präfixen verwenden:

1) Segmentersatzpräfix. Gibt explizit an, welches Segmentregister in diesem Befehl verwendet wird, um den Stack oder die Daten zu adressieren. Das Präfix überschreibt die standardmäßige Segmentregisterauswahl. Segmentersetzungspräfixe haben folgende Bedeutung:

a) 2eh - Ersatz des Segments cs;

b) 36h - Austausch des Segments ss;

c) 3eh - Ersatz von Segment ds;

d) 26h - Austausch von Segmenten es;

e) 64h - Austausch des Segments fs;

e) 65h - Austausch des Segments gs;

2) Das Präfix der Adressbitzahl gibt die Bitzahl der Adresse an (32- oder 16-Bit). Jeder Anweisung, die einen Adressoperanden verwendet, wird die Bitbreite der Adresse dieses Operanden zugewiesen. Diese Adresse kann 16 oder 32 Bit lang sein. Wenn die Adressbreite für diesen Befehl 16 Bit beträgt, bedeutet dies, dass der Befehl einen 16-Bit-Offset enthält (Fig. 20), er entspricht einem 16-Bit-Offset des Adressoperanden relativ zum Beginn eines Segments. Im Zusammenhang mit Fig. 21 wird dieser Offset als effektive Adresse bezeichnet. Wenn die Adresse 32 Bit beträgt, bedeutet dies, dass der Befehl einen 32-Bit-Offset enthält (Abb. 20), er entspricht einem 32-Bit-Offset des Adressoperanden relativ zum Anfang des Segments und sein Wert bildet eine 32 -Bit-Offset im Segment. Das Adress-Bitness-Präfix kann verwendet werden, um die Standard-Adress-Bitness zu ändern. Diese Änderung wirkt sich nur auf den Befehl aus, dem das Präfix vorangestellt ist;

Reis. 21. Der Mechanismus der Bildung einer physikalischen Adresse im Realmodus

3) Operanden-Bitbreiten-Präfix ist ähnlich wie Adress-Bitbreiten-Präfix, gibt aber die Operanden-Bitlänge (32 Bit oder 16 Bit) an, mit der die Anweisung arbeitet. Welche Regeln gelten standardmäßig für das Setzen von Adress- und Operanden-Bitbreitenattributen?

Im realen Modus und im virtuellen 18086-Modus betragen die Werte dieser Attribute 16 Bit. Im geschützten Modus hängen die Attributwerte vom Zustand des D-Bits in den ausführbaren Segmentdeskriptoren ab. Wenn D = 0, dann sind die Standardattributwerte 16 Bit; wenn D = 1, dann 32 Bit.

Präfixwerte für Operandenbreite 66h und Adressbreite 67h. Mit dem Adressbitpräfix im Realmodus können Sie die 32-Bit-Adressierung verwenden, beachten Sie jedoch die Beschränkung der Segmentgröße von 64 KB. Ähnlich wie beim Präfix für die Adressbreite können Sie das Präfix für die Operandenbreite im Realmodus verwenden, um mit 32-Bit-Operanden zu arbeiten (z. B. in arithmetischen Anweisungen).

4) Das Wiederholungspräfix wird bei Kettenbefehlen (Zeilenverarbeitungsbefehlen) verwendet. Dieses Präfix "schleift" den Befehl, um alle Elemente der Kette zu verarbeiten. Das Befehlssystem unterstützt zwei Arten von Präfixen:

a) bedingungslos (rep - OOh), wodurch der verkettete Befehl gezwungen wird, eine bestimmte Anzahl von Malen wiederholt zu werden;

b) Bedingung (repe/repz - OOh, repne/repnz - 0f2h), die beim Schleifen einige Flags überprüft und als Ergebnis der Überprüfung ein vorzeitiges Verlassen der Schleife möglich ist.

2. Betriebscode.

Erforderliches Element, das die vom Befehl ausgeführte Operation beschreibt. Viele Befehle entsprechen mehreren Operationscodes, von denen jeder die Nuancen der Operation bestimmt. Die nachfolgenden Felder des Maschinenbefehls bestimmen die Position der an der Operation beteiligten Operanden und die Besonderheiten ihrer Verwendung. Die Berücksichtigung dieser Felder ist mit den Arten der Angabe von Operanden in einem Maschinenbefehl verbunden und wird daher später durchgeführt.

3. Adressierungsmodusbyte modr/m.

Der Wert dieses Bytes bestimmt die verwendete Adressform des Operanden. Operanden können sich im Speicher in einem oder zwei Registern befinden. Wenn sich der Operand im Speicher befindet, dann spezifiziert das modr/m-Byte die Komponenten (Offset-, Basis- und Indexregister), die verwendet werden, um seine effektive Adresse zu berechnen (Fig. 21). Im geschützten Modus kann das Sib-Byte (Scale-Index-Base) zusätzlich verwendet werden, um die Position des Operanden im Speicher zu bestimmen. Das modr/m-Byte besteht aus drei Feldern (Abb. 20):

1) Das Mod-Feld bestimmt die Anzahl der Bytes, die von der Operandenadresse im Befehl belegt werden (Fig. 20, das Offset-Feld im Befehl). Das mod-Feld wird in Verbindung mit dem r/m-Feld verwendet, das angibt, wie die Adresse des Operanden "Befehls-Offset" zu modifizieren ist. Wenn beispielsweise mod = 00 ist, bedeutet dies, dass der Befehl kein Offset-Feld enthält und die Adresse des Operanden durch den Inhalt des Basis- und (oder) Indexregisters bestimmt wird. Welche Register zur Berechnung der effektiven Adresse verwendet werden, wird durch den Wert dieses Bytes bestimmt. Wenn mod = 01, bedeutet dies, dass das Offset-Feld im Befehl vorhanden ist, 1 Byte belegt und durch den Inhalt des Basis- und (oder) Indexregisters modifiziert wird. Wenn mod = 10, bedeutet dies, dass das Offset-Feld im Befehl vorhanden ist, 2 oder 4 Bytes belegt (abhängig von der standardmäßigen oder vorangestellten Adressgröße) und durch den Inhalt des Basis- und/oder Indexregisters modifiziert wird. Wenn mod = 11, bedeutet dies, dass es keine Operanden im Speicher gibt: Sie befinden sich in Registern. Derselbe Wert des Mod-Bytes wird verwendet, wenn ein Direktoperand in der Anweisung verwendet wird;

2) das reg/cop-Feld bestimmt entweder das Register, das sich im Befehl anstelle des ersten Operanden befindet, oder eine mögliche Erweiterung des Opcodes;

3) das r/m-Feld wird in Verbindung mit dem mod-Feld verwendet und bestimmt entweder das im Befehl an der Stelle des ersten Operanden befindliche Register (falls mod = 11) oder die zur Berechnung der effektiven Adresse verwendeten Basis- und Indexregister (zusammen mit dem Offset-Feld im Befehl).

4. Byte-Maßstab – Index – Basis (Byte-Sib).

Dient zur Erweiterung der Adressierungsmöglichkeiten von Operanden. Das Vorhandensein des Sib-Bytes in einem Maschinenbefehl wird durch eine Kombination aus einem der Werte 01 oder 10 des Felds mod und dem Wert des Felds r/m = 100 angezeigt. Das Sib-Byte besteht aus drei Feldern:

1) Skalenfelder ss. Dieses Feld enthält den Skalierungsfaktor für den Indexkomponentenindex, der die nächsten 3 Bits des Sib-Bytes belegt. Das Feld ss kann einen der folgenden Werte enthalten: 1, 2, 4, 8.

Bei der Berechnung der effektiven Adresse wird der Inhalt des Indexregisters mit diesem Wert multipliziert;

2) Indexfelder. Wird zum Speichern der Indexregisternummer verwendet, die zum Berechnen der effektiven Adresse des Operanden verwendet wird;

3) Grundfelder. Wird zum Speichern der Basisregisternummer verwendet, die auch zum Berechnen der effektiven Adresse des Operanden verwendet wird. Als Basis- und Indexregister können fast alle Mehrzweckregister verwendet werden.

5. Offset-Feld im Befehl.

Eine 8-, 16- oder 32-Bit-Ganzzahl mit Vorzeichen, die ganz oder teilweise (vorbehaltlich der obigen Überlegungen) den Wert der effektiven Adresse des Operanden darstellt.

6. Das Feld des unmittelbaren Operanden.

Ein optionales Feld, das ein direkter 8-Bit-, 16-Bit- oder 32-Bit-Operand ist. Das Vorhandensein dieses Feldes spiegelt sich natürlich im Wert des modr/m-Bytes wider.

2. Methoden zum Spezifizieren von Befehlsoperanden

Der Operand wird implizit auf Firmware-Ebene gesetzt

In diesem Fall enthält die Anweisung explizit keine Operanden. Der Befehlsausführungsalgorithmus verwendet einige Standardobjekte (Register, Flags in Eflags usw.).

Beispielsweise arbeiten die Befehle cli und sti implizit mit dem if interrupt-Flag im eflags-Register, und der xlat-Befehl greift implizit auf das al-Register und eine Zeile im Speicher an der Adresse zu, die durch das ds:bx-Registerpaar angegeben ist.

Der Operand wird in der Anweisung selbst angegeben (Direktoperand)

Der Operand steht im Befehlscode, ist also Teil davon. Um einen solchen Operanden in einem Befehl zu speichern, wird ein bis zu 32 Bit langes Feld zugewiesen (Abbildung 20). Der Direktoperand kann nur der zweite (Quell-)Operand sein. Der Zieloperand kann sich entweder im Speicher oder in einem Register befinden.

Beispiel: mov ax,0ffffti verschiebt die hexadezimale Konstante ffff in das Register ax. Der Befehl add sum, 2 addiert den Inhalt des Feldes an der Adresse sum mit der Ganzzahl 2 und schreibt das Ergebnis an die Stelle des ersten Operanden, also in den Speicher.

Der Operand befindet sich in einem der Register

Registeroperanden werden durch Registernamen angegeben. Register können verwendet werden:

1) 32-Bit-Register EAX, EBX, ECX, EDX, ESI, EDI, ESP, EUR;

2) 16-Bit-Register AX, BX, CX, DX, SI, DI, SP, BP;

3) 8-Bit-Register AH, AL, BH, BL, CH, CL, DH, DL;

4) Segmentregister CS, DS, SS, ES, FS, GS.

Beispielsweise addiert der Befehl ax,bx addieren die Inhalte der Register ax und bx und schreibt das Ergebnis in bx. Der Befehl dec si dekrementiert den Inhalt von si um 1.

Der Operand ist im Speicher

Dies ist die aufwendigste und zugleich flexibelste Art, Operanden anzugeben. Damit können Sie die folgenden zwei Hauptarten der Adressierung implementieren: direkt und indirekt.

Die indirekte Adressierung hat wiederum folgende Varianten:

1) indirekte Basisadressierung; sein anderer Name ist die indirekte Adressierung des Registers;

2) indirekte Basisadressierung mit Offset;

3) indirekte Indexadressierung mit Offset;

4) indirekte Basisindexadressierung;

5) indirekte Basisindexadressierung mit Offset.

Der Operand ist ein I/O-Port

Zusätzlich zum RAM-Adressraum unterhält der Mikroprozessor einen I/O-Adressraum, der verwendet wird, um auf I/O-Geräte zuzugreifen. Der E/A-Adressraum beträgt 64 KB. Adressen werden für jedes Computergerät in diesem Raum zugewiesen. Ein bestimmter Adresswert innerhalb dieses Raums wird als E/A-Port bezeichnet. Physikalisch entspricht der I/O-Port einem Hardware-Register (nicht zu verwechseln mit einem Mikroprozessor-Register), auf das mit speziellen Assembler-Befehlen ein- und ausgehend zugegriffen wird.

Zum Beispiel:

in al,60h; Geben Sie ein Byte von Port 60h ein

Register, die von einem E/A-Port adressiert werden, können 8,16, 32 oder XNUMX Bit breit sein, aber die Bitbreite des Registers ist für einen bestimmten Port festgelegt. Die In- und Out-Befehle wirken auf einen festen Bereich von Objekten. Als Informationsquelle bzw. Empfänger dienen die sogenannten Akkumulatorregister EAX, AX, AL. Die Wahl des Registers wird durch die Bitanzahl des Ports bestimmt. Die Portnummer kann als direkter Operand in den In- und Out-Befehlen oder als Wert im DX-Register angegeben werden. Mit der letzten Methode können Sie die Portnummer dynamisch im Programm ermitteln.

Der Operand liegt auf dem Stack

Anweisungen können überhaupt keine Operanden haben, können einen oder zwei Operanden haben. Die meisten Befehle erfordern zwei Operanden, von denen einer der Quelloperand und der andere der Zieloperand ist. Wichtig ist, dass sich ein Operand in einem Register oder Speicher befinden kann und der zweite Operand in einem Register oder direkt in der Anweisung stehen muss. Ein Direktoperand kann nur ein Quelloperand sein. In einem Maschinenbefehl mit zwei Operanden sind die folgenden Kombinationen von Operanden möglich:

1) registrieren - registrieren;

2) Register - Speicher;

3) Speicher - Register;

4) Direktoperand - Register;

5) unmittelbarer Operand - Speicher.

Ausnahmen von dieser Regel gibt es bei:

1) Kettenbefehle, die Daten von Speicher zu Speicher verschieben können;

2) Stack-Befehle, die Daten vom Speicher auf einen Stack übertragen können, der sich ebenfalls im Speicher befindet;

3) Befehle vom Typ Multiplikation, die zusätzlich zu dem im Befehl angegebenen Operanden noch einen zweiten, impliziten Operanden verwenden.

Von den aufgeführten Kombinationen von Operanden werden am häufigsten Register - Speicher und Speicher - Register verwendet. Angesichts ihrer Bedeutung werden wir sie genauer betrachten. Wir werden die Diskussion mit Beispielen von Assembler-Befehlen begleiten, die zeigen, wie sich das Format eines Assembler-Befehls ändert, wenn die eine oder andere Art der Adressierung angewendet wird. Sehen Sie sich in diesem Zusammenhang noch einmal Fig. 21 an, die das Prinzip der Bildung einer physikalischen Adresse auf dem Adressbus des Mikroprozessors zeigt. Es ist ersichtlich, dass die Adresse des Operanden als Summe zweier Komponenten gebildet wird - dem um 4 Bit verschobenen Inhalt des Segmentregisters und der 16-Bit-Effektivadresse, die im Allgemeinen als Summe aus drei Komponenten berechnet wird: Basis, Offset und Index.

3. Adressierungsmethoden

Wir listen die Merkmale der Haupttypen der Adressierung von Operanden im Speicher auf und betrachten sie dann:

1) direkte Adressierung;

2) indirekte grundlegende (Register-)Adressierung;

3) indirekte Grundadressierung (Register) mit Offset;

4) indirekte Indexadressierung mit Offset;

5) indirekte Basisindexadressierung;

6) indirekte Basisindexadressierung mit Offset.

Direkte Adressierung

Dies ist die einfachste Form der Adressierung eines Operanden im Speicher, da die effektive Adresse in der Anweisung selbst enthalten ist und keine zusätzlichen Quellen oder Register zu ihrer Bildung verwendet werden. Die effektive Adresse wird direkt aus dem Maschinenbefehls-Offsetfeld (siehe Fig. 20) genommen, das 8, 16, 32 Bit groß sein kann. Dieser Wert identifiziert eindeutig das im Datensegment befindliche Byte, Wort oder Doppelwort.

Es gibt zwei Arten der direkten Adressierung.

Relative direkte Adressierung

Wird für bedingte Sprungbefehle verwendet, um die relative Sprungadresse anzugeben. Die Relativität eines solchen Übergangs liegt in der Tatsache, dass das Offset-Feld des Maschinenbefehls einen 8-, 16- oder 32-Bit-Wert enthält, der als Ergebnis der Operation des Befehls zum Inhalt von hinzugefügt wird das ip/eip-Befehlszeigerregister. Als Ergebnis dieser Addition erhält man die Adresse, zu der übergegangen wird.

Absolute Direktansprache

In diesem Fall ist die effektive Adresse Teil des Maschinenbefehls, aber diese Adresse wird nur aus dem Wert des Offset-Felds im Befehl gebildet. Zur Bildung der physikalischen Adresse des Operanden im Speicher addiert der Mikroprozessor dieses Feld mit dem um 4 Bit verschobenen Wert des Segmentregisters. In einem Assembler-Befehl können mehrere Formen dieser Adressierung verwendet werden.

Eine solche Adressierung wird jedoch selten verwendet - häufig verwendeten Zellen im Programm werden symbolische Namen zugewiesen. Während der Übersetzung berechnet der Assembler die Offset-Werte dieser Namen und ersetzt sie in der Maschinenanweisung, die er im Feld "Instruction Offset" erzeugt. Als Ergebnis stellt sich heraus, dass der Maschinenbefehl seinen Operanden direkt adressiert, wobei er tatsächlich in einem seiner Felder den Wert der effektiven Adresse hat.

Andere Arten der Adressierung sind indirekt. Das Wort „indirekt“ im Namen dieser Adressierungsarten bedeutet, dass nur ein Teil der effektiven Adresse in der Anweisung selbst stehen kann und ihre restlichen Bestandteile in Registern sind, die durch ihren Inhalt durch das modr/m-Byte und angezeigt werden. möglicherweise durch das Sib-Byte.

Indirekte Grundadressierung (Register).

Bei dieser Adressierung kann sich die effektive Adresse des Operanden in jedem der Mehrzweckregister befinden, mit Ausnahme von sp / esp und bp / ebp (dies sind spezielle Register für die Arbeit mit einem Stapelsegment). Syntaktisch in einem Befehl wird dieser Adressierungsmodus dadurch ausgedrückt, dass der Registername in eckige Klammern [] eingeschlossen wird. Zum Beispiel platziert die Anweisung mov ax, [ecx] in den Registern ax den Inhalt des Wortes an der Adresse aus dem Datensegment mit dem im Register esx gespeicherten Offset. Da der Inhalt des Registers im Laufe des Programms leicht geändert werden kann, ermöglicht diese Adressierungsmethode die dynamische Zuweisung der Adresse eines Operanden für einige Maschinenbefehle. Diese Eigenschaft ist beispielsweise sehr nützlich, um zyklische Berechnungen zu organisieren und mit verschiedenen Datenstrukturen wie Tabellen oder Arrays zu arbeiten.

Indirekte Basis-(Register-)Adressierung mit Offset

Diese Art der Adressierung ist eine Ergänzung zur vorherigen und dient dem Zugriff auf Daten mit einem bekannten Offset relativ zu einer Basisadresse. Diese Art der Adressierung ist bequem für den Zugriff auf die Elemente von Datenstrukturen zu verwenden, wenn der Offset der Elemente in der Phase der Programmentwicklung im Voraus bekannt ist und die Basis-(Start-)Adresse der Struktur dynamisch berechnet werden muss die Phase der Programmausführung. Die Änderung des Inhalts des Basisregisters ermöglicht Ihnen den Zugriff auf die Elemente mit demselben Namen in verschiedenen Instanzen derselben Art von Datenstrukturen.

Beispielsweise überträgt die Anweisung mov ax,[edx+3h] die Wörter aus dem Speicherbereich in die Register ax an der Adresse: Inhalt von edx + 3h.

Die Anweisung mov ax,mas[dx] verschiebt ein Wort in die ax-Register an der Adresse: den Inhalt von dx plus den Wert des Bezeichners mas (denken Sie daran, dass der Compiler jedem Bezeichner einen Wert gleich dem Offset dieses Bezeichners von zuweist Anfang des Datensegments).

Indirekte Indexadressierung mit Offset

Diese Art der Adressierung ist der indirekten Basisadressierung mit Offset sehr ähnlich. Auch hier wird eines der Mehrzweckregister zur Bildung der effektiven Adresse verwendet. Aber die Indexadressierung hat ein interessantes Feature, das für die Arbeit mit Arrays sehr praktisch ist. Damit verbunden ist die Möglichkeit der sogenannten Skalierung des Inhalts des Indexregisters. Was ist das?

Schauen Sie sich Abbildung 20 an. Uns interessiert das Sib-Byte. Bei der Erörterung der Struktur dieses Bytes haben wir festgestellt, dass es aus drei Feldern besteht. Eines dieser Felder ist das ss-Skalenfeld, mit dem die Inhalte des Indexregisters multipliziert werden.

Beispielsweise wird in der Anweisung mov ax,mas[si*2] der Wert der effektiven Adresse des zweiten Operanden durch den Ausdruck mas+(si)*2 berechnet. Da der Assembler nicht über die Mittel verfügt, um die Array-Indizierung zu organisieren, muss der Programmierer sie selbst organisieren.

Die Fähigkeit zur Skalierung hilft erheblich bei der Lösung dieses Problems, vorausgesetzt jedoch, dass die Größe der Array-Elemente 1, 2, 4 oder 8 Bytes beträgt.

Indirekte Basisindexadressierung

Bei dieser Art der Adressierung wird die effektive Adresse als Summe der Inhalte zweier Mehrzweckregister gebildet: Basis und Index. Diese Register können beliebige Mehrzweckregister sein, und häufig wird eine Skalierung des Inhalts eines Indexregisters verwendet.

Indirekte Basisindexadressierung mit Offset

Diese Art der Adressierung ist die Ergänzung zur indirekten indizierten Adressierung. Die effektive Adresse wird als Summe aus drei Komponenten gebildet: dem Inhalt des Basisregisters, dem Inhalt des Indexregisters und dem Wert des Offset-Felds in der Anweisung.

Beispielsweise verschiebt die Anweisung mov eax,[esi+5] [edx] ein Doppelwort in das eax-Register an der Adresse: (esi) + 5 + (edx).

Der Befehl add ax,array[esi] [ebx] fügt den Inhalt des Registers ax zum Inhalt des Wortes an der Adresse hinzu: den Wert des Bezeichner-Arrays + (esi) + (ebx).

VORTRAG Nr. 18. Mannschaften

1. Datenübertragungsbefehle

Um die praktische Anwendung zu erleichtern und ihre Spezifität widerzuspiegeln, ist es bequemer, die Befehle dieser Gruppe in Übereinstimmung mit ihrem funktionalen Zweck zu betrachten, nach dem sie in die folgenden Befehlsgruppen unterteilt werden können:

1) allgemeine Datenübertragungen;

2) Eingang-Ausgang zum Port;

3) mit Adressen und Zeigern arbeiten;

4) Datentransformationen;

5) Arbeiten Sie mit dem Stapel.

Allgemeine Datenübertragungsbefehle

Diese Gruppe umfasst die folgenden Befehle:

1) mov ist der grundlegende Datenübertragungsbefehl. Es implementiert eine Vielzahl von Versandoptionen. Beachten Sie die Besonderheiten dieses Befehls:

a) Der Befehl mov kann nicht von einem Speicherbereich in einen anderen übertragen werden. Wenn ein solcher Bedarf besteht, sollte jedes derzeit verfügbare Allzweckregister als Zwischenpuffer verwendet werden;

b) es ist unmöglich, einen Wert direkt aus dem Speicher in ein Segmentregister zu laden. Um einen solchen Ladevorgang durchzuführen, müssen Sie daher ein Zwischenobjekt verwenden. Dies kann ein Allzweckregister oder ein Stack sein;

c) Sie können den Inhalt eines Segmentregisters nicht in ein anderes Segmentregister übertragen. Dies liegt daran, dass es im Befehlssystem keinen entsprechenden Opcode gibt. Aber die Notwendigkeit für solche Maßnahmen entsteht oft. Sie können eine solche Übertragung mit den gleichen Mehrzweckregistern wie Zwischenregister durchführen;

d) Sie können das Segmentregister CS nicht als Zieloperand verwenden. Der Grund ist einfach. Tatsache ist, dass in der Architektur des Mikroprozessors das cs:ip-Paar immer die Adresse des Befehls enthält, der als nächstes ausgeführt werden soll. Das Ändern des Inhalts des CS-Registers mit dem Befehl mov würde tatsächlich eine Sprungoperation bedeuten, keine Übertragung, was nicht akzeptabel ist. 2) xchg - wird für die bidirektionale Datenübertragung verwendet. Für diese Operation können Sie natürlich eine Folge von mehreren mov-Anweisungen verwenden, aber aufgrund der Tatsache, dass die Austauschoperation ziemlich häufig verwendet wird, hielten es die Entwickler des Mikroprozessor-Befehlssystems für notwendig, eine separate xchg-Austauschanweisung einzuführen. Die Operanden müssen natürlich vom gleichen Typ sein. Es ist (wie bei allen Assemblerbefehlen) nicht erlaubt, den Inhalt zweier Speicherzellen miteinander auszutauschen.

Port-E/A-Befehle

Schauen Sie sich Abbildung 22 an. Sie zeigt ein stark vereinfachtes Konzeptdiagramm der Computerhardwaresteuerung.

Reis. 22. Konzeptdiagramm der Computerhardwaresteuerung

Wie Sie in Abbildung 22 sehen können, ist die unterste Ebene die BIOS-Ebene, auf der die Hardware direkt über die Ports verwaltet wird. Dies implementiert das Konzept der Geräteunabhängigkeit. Beim Austausch von Hardware müssen lediglich die entsprechenden BIOS-Funktionen korrigiert und auf neue Adressen und die Logik der Ports umgestellt werden.

Grundsätzlich ist die Verwaltung von Geräten direkt über Ports einfach. Informationen zu Portnummern, deren Bittiefe und Steuerinformationsformat finden Sie in der technischen Beschreibung des Geräts. Sie müssen nur das Endziel Ihrer Aktionen, den Algorithmus, nach dem ein bestimmtes Gerät arbeitet, und die Reihenfolge der Programmierung seiner Ports kennen, d. H. Tatsächlich müssen Sie wissen, an was und in welcher Reihenfolge Sie senden müssen den Port (beim Schreiben) oder lesen (beim Lesen) und wie diese Informationen interpretiert werden sollen. Dazu genügen nur zwei Befehle, die im Befehlssystem des Mikroprozessors vorhanden sind:

1) im Akkumulator, port_number - Eingang zum Akkumulator vom Port mit der Nummer port_number;

2) out port, accumulator - gibt den Inhalt des Akkumulators an den Port mit der Nummer port_number aus.

Befehle zum Arbeiten mit Adressen und Speicherzeigern

Beim Schreiben von Programmen in Assembler wird intensiv mit den Adressen von Operanden gearbeitet, die sich im Speicher befinden. Um diese Art von Operationen zu unterstützen, gibt es eine spezielle Gruppe von Befehlen, die die folgenden Befehle enthält:

1) lea Ziel, Quelle – Laden der effektiven Adresse;

2) IDs Ziel, Quelle – Laden des Zeigers in das Datensegmentregister ds;

3) les destination, source – Laden des Zeigers in das Register der zusätzlichen Datensegmente es;

4) lgs Ziel, Quelle – Laden des Zeigers in das Register des zusätzlichen Datensegments gs;

5) lfs Ziel, Quelle – Laden des Zeigers in das Register des zusätzlichen Datensegments fs;

6) lss Ziel, Quelle – Ladezeiger in Stapelsegmentregister ss.

Der lea-Befehl ähnelt dem mov-Befehl insofern, als er ebenfalls eine Bewegung ausführt. Der lea-Befehl überträgt jedoch keine Daten, sondern die effektive Adresse der Daten (d. h. den Offset der Daten vom Beginn des Datensegments) an das durch den Zieloperanden angegebene Register.

Um eine Aktion in einem Programm auszuführen, reicht es oft nicht aus, nur den Wert der effektiven Datenadresse zu kennen, sondern es ist notwendig, einen vollständigen Zeiger auf die Daten zu haben. Ein vollständiger Datenzeiger besteht aus einer Segmentkomponente und einem Offset. Alle anderen Befehle dieser Gruppe ermöglichen es Ihnen, einen solchen vollständigen Zeiger auf einen Operanden im Speicher in einem Registerpaar zu erhalten. Der Name des Segmentregisters, in dem die Segmentkomponente der Adresse abgelegt wird, wird dabei durch den Operationscode bestimmt. Dementsprechend wird der Offset in das durch den Zieloperanden angegebene allgemeine Register gestellt.

Aber nicht alles ist so einfach mit dem Quellenoperanden. Tatsächlich können Sie im Befehl als Quelle nicht direkt den Namen des Operanden im Speicher angeben, auf den wir einen Zeiger erhalten möchten. Zuerst müssen Sie den Wert des vollständigen Zeigers in einem Speicherbereich abrufen und die vollständige Adresse des Namens dieses Bereichs im get-Befehl angeben. Um diese Aktion auszuführen, müssen Sie sich die Anweisungen zum Reservieren und Initialisieren von Speicher merken.

Bei der Anwendung dieser Direktiven ist ein Sonderfall möglich, wenn der Name einer anderen Datendefinitionsdirektive (eigentlich der Name einer Variablen) im Operandenfeld angegeben wird. Die Adresse dieser Variablen wird dabei im Speicher gebildet. Welche Adresse generiert wird (effektiv oder vollständig), hängt von der angewandten Richtlinie ab. Bei dw wird nur der 16-Bit-Wert der effektiven Adresse im Speicher gebildet, bei dd wird die volle Adresse in den Speicher geschrieben. Die Position dieser Adresse im Speicher ist wie folgt: Das niedrige Wort enthält den Offset, das hohe Wort enthält die 16-Bit-Segmentkomponente der Adresse.

Wenn Sie beispielsweise die Arbeit mit einer Zeichenkette organisieren, ist es bequem, ihre Startadresse in einem bestimmten Register zu platzieren und diesen Wert dann in einer Schleife für den sequentiellen Zugriff auf die Elemente der Kette zu ändern.

Die Notwendigkeit, Befehle zu verwenden, um einen vollständigen Datenzeiger im Speicher zu erhalten, d. h. die Adresse des Segments und den Offset-Wert innerhalb des Segments, entsteht insbesondere, wenn mit Ketten gearbeitet wird.

Befehle zur Datenkonvertierung

Viele Mikroprozessorbefehle können dieser Gruppe zugeordnet werden, aber die meisten von ihnen haben bestimmte Merkmale, die erfordern, dass sie anderen funktionellen Gruppen zugeordnet werden. Daher kann aus dem gesamten Satz von Mikroprozessorbefehlen nur ein Befehl direkt Datenkonvertierungsbefehlen zugeordnet werden: xlat [Adresse_der_Umcodierungstabelle]

Dies ist ein sehr interessantes und nützliches Team. Seine Wirkung besteht darin, dass es den Wert im al-Register durch ein anderes Byte aus der Speichertabelle ersetzt, die sich an der durch den Operanden remap_table_address angegebenen Adresse befindet.

Das Wort "Tabelle" ist sehr bedingt, tatsächlich ist es nur eine Folge von Bytes. Die Adresse des Bytes im String, das den Inhalt des al-Registers ersetzt, wird durch die Summe (bx) + (al) bestimmt, d. h. der Inhalt von al fungiert als Index im Byte-Array.

Beachten Sie bei der Arbeit mit dem xlat-Befehl den folgenden subtilen Punkt. Obwohl der Befehl die Adresse der Bytekette angibt, aus der der neue Wert abgerufen werden soll, muss diese Adresse vorab in das bx-Register geladen werden (beispielsweise unter Verwendung des lea-Befehls). Daher wird der Operand lookup_table_address nicht wirklich benötigt (die Optionalität des Operanden wird durch Einschließen in eckige Klammern angezeigt). Der Byte-String (Umcodierungstabelle) ist ein Speicherbereich mit einer Größe von 1 bis 255 Byte (der Bereich einer vorzeichenlosen Zahl in einem 8-Bit-Register).

Stack-Befehle

Diese Gruppe besteht aus einer Reihe spezialisierter Befehle, die sich darauf konzentrieren, flexible und effiziente Arbeit mit dem Stack zu organisieren.

Der Stack ist ein Speicherbereich, der speziell für die temporäre Speicherung von Programmdaten reserviert ist. Die Bedeutung des Stacks wird dadurch bestimmt, dass ihm in der Struktur des Programms ein eigenes Segment zur Verfügung gestellt wird. Falls der Programmierer vergessen hat, ein Stack-Segment in seinem Programm zu deklarieren, gibt der Tlink-Linker eine Warnmeldung aus.

Der Stack hat drei Register:

1) ss - Stapelsegmentregister;

2) sp/esp - Stapelzeigerregister;

3) bp/ebp – Stack-Frame-Basiszeigerregister.

Die Stapelgröße hängt vom Betriebsmodus des Mikroprozessors ab und ist auf 64 KB (bzw. 4 GB im geschützten Modus) begrenzt.

Es steht immer nur ein Stack zur Verfügung, dessen Segmentadresse im SS-Register enthalten ist. Dieser Stack wird als aktueller Stack bezeichnet. Um auf einen anderen Stack zu verweisen („switch the stack“), muss eine andere Adresse in das ss-Register geladen werden. Das SS-Register wird vom Prozessor automatisch verwendet, um alle Befehle auszuführen, die auf dem Stapel arbeiten.

Wir listen einige weitere Features für die Arbeit mit dem Stack auf:

1) das Schreiben und Lesen von Daten auf dem Stack erfolgt nach dem LIFO-Prinzip,

2) Wenn Daten auf den Stack geschrieben werden, wächst letzterer zu niedrigeren Adressen hin. Diese Funktion ist in den Befehlsalgorithmus für die Arbeit mit dem Stack eingebettet;

3) bei Verwendung der esp/sp- und ebp/bp-Register zur Speicheradressierung berücksichtigt der Assembler automatisch, dass die darin enthaltenen Werte Offsets relativ zum ss-Segmentregister sind.

Im Allgemeinen ist der Stapel wie in Abbildung 23 gezeigt organisiert.

Reis. 23. Konzeptdiagramm der Stack-Organisation

Die SS-, ESP/SP- und EUR/BP-Register sind so ausgelegt, dass sie mit dem Stack arbeiten. Diese Register werden auf komplexe Weise verwendet, und jedes von ihnen hat seinen eigenen funktionalen Zweck.

Das ESP/SP-Register zeigt immer auf den Anfang des Stapels, dh es enthält den Offset, an dem das letzte Element auf den Stapel geschoben wurde. Die Stapelbefehle ändern dieses Register implizit so, dass es immer auf das letzte Element zeigt, das auf den Stapel geschoben wurde. Wenn der Stapel leer ist, dann ist der Wert von esp gleich der Adresse des letzten Bytes des dem Stapel zugewiesenen Segments. Wenn ein Element auf den Stapel geschoben wird, dekrementiert der Prozessor den Wert des esp-Registers und schreibt dann das Element an die Adresse des neuen Scheitelpunkts. Beim Entnehmen von Daten aus dem Stapel kopiert der Prozessor das Element, das sich an der obersten Adresse befindet, und inkrementiert dann den Wert des Stapelzeigerregisters, esp. Es stellt sich also heraus, dass der Stack in Richtung abnehmender Adressen nach unten wächst.

Was ist, wenn wir auf Elemente nicht ganz oben, sondern innerhalb des Stapels zugreifen müssen? Verwenden Sie dazu das EBP-Register.Das EBP-Register ist das Stack-Frame-Basiszeigerregister.

Ein typischer Trick beim Eingeben einer Subroutine besteht beispielsweise darin, die gewünschten Parameter zu übergeben, indem man sie auf den Stack schiebt. Wenn das Unterprogramm auch aktiv mit dem Stack arbeitet, wird der Zugriff auf diese Parameter problematisch. Der Ausweg besteht darin, die Adresse der obersten Stelle des Stapels im Frame-Zeiger (Basiszeiger) des Stapels zu speichern, nachdem die erforderlichen Daten in den Stapel geschrieben wurden - das EUR-Register. Über den Wert in EUR kann später auf die übergebenen Parameter zugegriffen werden.

Der Anfang des Stacks liegt an höheren Speicheradressen. In Abbildung 23 wird diese Adresse durch das Paar ss:fffF bezeichnet. Die Verschiebung von wT ist hier bedingt gegeben. In Wirklichkeit wird dieser Wert durch den Wert bestimmt, den der Programmierer bei der Beschreibung des Stapelsegments in seinem Programm angibt.

Um die Arbeit mit dem Stapel zu organisieren, gibt es spezielle Befehle zum Schreiben und Lesen.

1. Push-Quelle - Schreiben des Wertes der Quelle an die Spitze des Stapels.

Interessant ist der Algorithmus dieses Befehls, der folgende Aktionen beinhaltet (Abb. 24):

1) (sp) = (sp) - 2; der Wert von sp wird um 2 verringert;

2) Der Wert aus der Quelle wird an die Adresse geschrieben, die durch das ss:sp-Paar angegeben ist.

Reis. 24. Funktionsweise des Push-Befehls

2. Pop-Zuweisung – Schreiben des Werts von der Spitze des Stapels an die Stelle, die durch den Zieloperanden angegeben ist. Der Wert wird somit von der Spitze des Stapels "entfernt". Der Algorithmus des Pop-Befehls ist die Umkehrung des Algorithmus des Push-Befehls (Abb. 25):

1) Schreiben des Inhalts der Spitze des Stapels an der durch den Zieloperanden angegebenen Stelle;

2) (sp) = (sp) + 2; Erhöhung des Wertes von sp.

Reis. 25. Wie der Pop-Befehl funktioniert

3. pusha - ein Gruppenschreibbefehl auf den Stack. Durch diesen Befehl werden die Register ax, cx, dx, bx, sp, bp, si, di sequentiell auf den Stack geschrieben. Beachten Sie, dass der ursprüngliche Inhalt von sp geschrieben wird, d. h. der Inhalt, der vor der Ausgabe des Pusha-Befehls vorhanden war (Abb. 26).

Reis. 26. Wie der Pusha-Befehl funktioniert

4. pushaw ist fast gleichbedeutend mit dem Befehl pusha, was ist der Unterschied? Das Bitness-Attribut kann entweder use16 oder use32 sein. Schauen wir uns an, wie die Befehle pusha und pushaw mit jedem dieser Attribute funktionieren:

1) use16 – der Pushaw-Algorithmus ähnelt dem Pusha-Algorithmus;

2) use32 - pushaw ändert sich nicht (d.h. es ist unempfindlich gegenüber der Segmentbreite und arbeitet immer mit wortgroßen Registern - ax, cx, dx, bx, sp, bp, si, di). Der Befehl pusha reagiert empfindlich auf die eingestellte Segmentbreite und arbeitet bei Angabe eines 32-Bit-Segments mit den entsprechenden 32-Bit-Registern, also eax, esx, edx, ebx, esp, ebp, esi, edi.

5. pushad - wird ähnlich ausgeführt wie der pusha-Befehl, aber es gibt einige Besonderheiten.

Die folgenden drei Befehle führen die Umkehrung der obigen Befehle aus:

1) Rora;

2) Papagei;

3) Pop.

Die nachfolgend beschriebene Befehlsgruppe ermöglicht es Ihnen, das Flag-Register auf dem Stack zu speichern und ein Wort oder Doppelwort in den Stack zu schreiben. Beachten Sie, dass die unten aufgeführten Befehle die einzigen im Befehlssatz des Mikroprozessors sind, die den Zugriff auf den gesamten Inhalt des Flag-Registers ermöglichen (und erfordern).

1. pushf - speichert das Register der Flags auf dem Stack.

Die Funktionsweise dieses Befehls hängt vom Segmentgrößenattribut ab:

1) use 16 - das Flags-Register mit einer Größe von 2 Bytes wird auf den Stack geschrieben;

2) use32 - das eflags-Register von 4 Bytes wird auf den Stack geschrieben.

2. pushfw - Speichern eines wortgroßen Flag-Registers auf dem Stack. Funktioniert immer wie pushf mit dem Attribut use16.

3. pushfd – Speichern der Flags oder eflags-Flags-Register auf dem Stack, abhängig von dem Bitbreitenattribut des Segments (dh dasselbe wie pushf).

In ähnlicher Weise führen die folgenden drei Befehle die Umkehrung der oben beschriebenen Operationen aus:

1) Puff;

2) popftv;

3) popfd.

Abschließend stellen wir die wichtigsten Arten von Operationen fest, wenn die Verwendung des Stacks fast unvermeidlich ist:

1) Unterprogramme aufrufen;

2) Zwischenspeicherung von Registerwerten;

3) Definition lokaler Variablen.

2. Arithmetische Anweisungen

Der Mikroprozessor kann Ganzzahl- und Gleitkommaoperationen ausführen. Zu diesem Zweck besteht seine Architektur aus zwei separaten Blöcken:

1) eine Vorrichtung zum Durchführen ganzzahliger Operationen;

2) eine Vorrichtung zum Ausführen von Gleitkommaoperationen.

Jedes dieser Geräte hat sein eigenes Befehlssystem. Im Prinzip kann ein Integer-Gerät viele der Funktionen eines Fließkomma-Geräts übernehmen, aber dies wird rechenintensiv sein. Für die meisten Probleme mit der Assemblersprache ist Ganzzahlarithmetik ausreichend.

Überblick über eine Gruppe arithmetischer Anweisungen und Daten

Ein Integer-Rechengerät unterstützt etwas mehr als ein Dutzend arithmetische Anweisungen. Abbildung 27 zeigt die Klassifizierung der Befehle in dieser Gruppe.

Reis. 27. Klassifizierung von Rechenbefehlen

Die Gruppe der Integer-Arithmetikbefehle arbeitet mit zwei Arten von Zahlen:

1) ganzzahlige Binärzahlen. Nummern können eine vorzeichenbehaftete Ziffer haben oder nicht, d. h. vorzeichenbehaftete oder vorzeichenlose Nummern sein;

2) ganzzahlige Dezimalzahlen.

Berücksichtigen Sie die Maschinenformate, in denen diese Datentypen gespeichert werden.

Ganzzahlige Binärzahlen

Eine binäre Festkomma-Ganzzahl ist eine Zahl, die im binären Zahlensystem codiert ist.

Die Dimension einer binären Ganzzahl kann 8, 16 oder 32 Bit betragen. Das Vorzeichen einer Binärzahl wird dadurch bestimmt, wie das höchstwertige Bit in der Darstellung der Zahl interpretiert wird. Dies sind 7,15 oder 31. Bits für Zahlen der entsprechenden Dimension. Interessant ist dabei, dass es unter den Rechenbefehlen nur zwei Befehle gibt, die dieses höchstwertige Bit wirklich als Vorzeichen berücksichtigen, nämlich die ganzzahligen Multiplikations- und Divisionsbefehle imul und idiv. In anderen Fällen liegt die Verantwortung für Aktionen mit vorzeichenbehafteten Zahlen und dementsprechend mit einem Vorzeichenbit beim Programmierer. Der Wertebereich einer Binärzahl hängt von ihrer Größe und Interpretation des höchstwertigen Bits entweder als höchstwertiges Bit der Zahl oder als Vorzeichenbit der Zahl ab (Tabelle 9).

Tabelle 9. Bereich der Binärzahlen Dezimalzahlen

Dezimalzahlen sind eine spezielle Art der Darstellung numerischer Informationen, die auf dem Prinzip beruht, jede Dezimalstelle einer Zahl durch eine Gruppe von vier Bits zu codieren. Dabei enthält jedes Byte der Zahl eine oder zwei Dezimalziffern im sogenannten binär codierten Dezimalcode (BCD – Binary-Coded Decimal). Der Mikroprozessor speichert BCD-Zahlen in zwei Formaten (Abb. 28):

1) gepacktes Format. In diesem Format enthält jedes Byte zwei Dezimalziffern. Eine Dezimalziffer ist ein 0-Bit-Binärwert zwischen 9 und 4. In diesem Fall belegt der Code der höchsten Stelle der Nummer die höchsten 4 Bits. Daher reicht der Darstellungsbereich einer dezimal gepackten Zahl in 1 Byte von 00 bis 99;

2) unverpacktes Format. In diesem Format enthält jedes Byte eine Dezimalziffer in den vier niedrigstwertigen Bits. Die oberen 4 Bits werden auf Null gesetzt. Dies ist die sogenannte Zone. Daher reicht der Bereich für die Darstellung einer dezimalen ungepackten Zahl in 1 Byte von 0 bis 9.

Reis. 28. Darstellung von BCD-Zahlen

Wie beschreibe ich binäre Dezimalzahlen in einem Programm? Dazu können Sie nur zwei Datenbeschreibungs- und Initialisierungsdirektiven verwenden - db und dt. Die Möglichkeit, nur diese Direktiven zur Beschreibung von BCD-Zahlen zu verwenden, ergibt sich aus der Tatsache, dass auch für solche Zahlen das Prinzip „low byte at low address“ gilt, was für ihre Verarbeitung sehr praktisch ist. Und im Allgemeinen ist bei der Verwendung eines solchen Datentyps wie BCD-Zahlen die Reihenfolge, in der diese Zahlen im Programm beschrieben werden, und der Algorithmus zu ihrer Verarbeitung eine Frage des Geschmacks und der persönlichen Vorlieben des Programmierers. Dies wird deutlich, wenn wir uns die Grundlagen der Arbeit mit BCD-Zahlen unten ansehen.

Arithmetische Operationen mit binären ganzen Zahlen

Addition von vorzeichenlosen Binärzahlen

Der Mikroprozessor führt die Addition von Operanden gemäß den Regeln zum Addieren von Binärzahlen durch. Solange der Wert des Ergebnisses die Dimensionen des Operandenfeldes nicht überschreitet, gibt es keine Probleme. Beispielsweise darf beim Addieren bytegroßer Operanden das Ergebnis die Zahl 255 nicht überschreiten. In diesem Fall ist das Ergebnis falsch. Betrachten wir, warum dies geschieht.

Machen wir zum Beispiel die Addition: 254 + 5 = 259 in Binärform. 11111110 + 0000101 = 1 00000011. Das Ergebnis ging über 8 Bit hinaus und sein richtiger Wert passt in 9 Bit, und der Wert 8 blieb im 3-Bit-Feld des Operanden, was natürlich nicht wahr ist. Im Mikroprozessor wird dieses Ergebnis der Addition vorhergesagt, und spezielle Mittel sind vorgesehen, um solche Situationen zu beheben und sie zu verarbeiten. Um also die Situation des Überschreitens des Bitrasters des Ergebnisses zu beheben, wie in diesem Fall, ist das Carry-Flag cf beabsichtigt. Es befindet sich in Bit 0 des Flag-Registers EFLAGS/FLAGS. Es ist das Setzen dieses Flags, das die Tatsache der Übertragung von Eins von der höheren Ordnung des Operanden festlegt. Natürlich muss der Programmierer die Möglichkeit eines solchen Ergebnisses der Additionsoperation berücksichtigen und Mittel zur Korrektur bereitstellen. Dies beinhaltet das Einschließen von Codeabschnitten nach der Additionsoperation, in der das cf-Flag geparst wird. Dieses Flag kann auf verschiedene Weise geparst werden.

Am einfachsten und zugänglichsten ist die Verwendung des bedingten Verzweigungsbefehls jcc. Dieser Befehl hat als Operanden den Namen des Labels im aktuellen Codesegment. Der Übergang zu diesem Label wird ausgeführt, wenn als Ergebnis der Operation des vorherigen Befehls das cf-Flag auf 1 gesetzt wird. Es gibt drei binäre Additionsbefehle im Mikroprozessor-Befehlssystem:

1) inc operand - Inkrementoperation, d. h. den Wert des Operanden um 1 erhöhen;

2) operand_1, operand_2 hinzufügen - Additionsanweisung mit dem Funktionsprinzip: operand_1 = operand_1 + operand_2;

3) adc operand_1, operand_2 - Additionsanweisung unter Berücksichtigung des Carry-Flags vgl. Prinzip der Befehlsoperation: operand_1 = operand_1 + operand_2 + value_sG.

Achten Sie auf den letzten Befehl - dies ist der Additionsbefehl, der die Übertragung von einem von der höheren Ordnung berücksichtigt. Wir haben bereits den Mechanismus für das Erscheinen einer solchen Einheit betrachtet. Somit ist der adc-Befehl ein Mikroprozessorwerkzeug zum Addieren langer Binärzahlen, deren Dimensionen die Längen von Standardfeldern überschreiten, die von dem Mikroprozessor unterstützt werden.

Vorzeichenbehaftete binäre Addition

Tatsächlich ist sich der Mikroprozessor des Unterschieds zwischen vorzeichenbehafteten und vorzeichenlosen Zahlen "nicht bewusst". Stattdessen hat er die Möglichkeit, das Auftreten charakteristischer Situationen zu fixieren, die sich im Rechenprozess entwickeln. Wir haben einige davon behandelt, als wir über vorzeichenlose Additionen diskutiert haben:

1) das cf Carry-Flag, das es auf 1 setzt, zeigt an, dass die Operanden außerhalb des Bereichs waren;

2) der adc-Befehl, der die Möglichkeit eines solchen Austritts berücksichtigt (Übertrag vom niedrigstwertigen Bit).

Eine andere Möglichkeit besteht darin, den höherwertigen (Vorzeichen-) Bitzustand des Operanden zu registrieren, was unter Verwendung des Überlauf-Flags im EFLAGS-Register (Bit 11) erfolgt.

Sie erinnern sich natürlich, wie Zahlen in einem Computer dargestellt werden: positiv - im Binärformat, negativ - im Zweierkomplement. Betrachten Sie verschiedene Optionen zum Hinzufügen von Zahlen. Die Beispiele sollen das Verhalten der beiden höchstwertigen Bits der Operanden und die Korrektheit des Ergebnisses der Additionsoperation zeigen.

Beispiel

30566 = 0111011101100110

+

00687 = 00000010 10101111 XNUMX

=

31253 = 01111010 00010101 XNUMX

Wir überwachen die Überweisungen ab der 14. und 15. Stelle und die Richtigkeit des Ergebnisses: Es gibt keine Überweisungen, das Ergebnis ist korrekt.

Beispiel

30566 = 0111011101100110

+

30566 = 0111011101100110

=

1132 = 11101110 11001100 XNUMX

Es gab einen Transfer aus der 14. Kategorie; Ab der 15. Kategorie findet kein Transfer statt. Das Ergebnis ist falsch, weil es einen Überlauf gibt – der Wert der Zahl stellte sich als größer heraus, als eine vorzeichenbehaftete 16-Bit-Zahl (+32 767) haben kann.

Beispiel

-30566 = 10001000 10011010

+

-04875 = 11101100 11110101

=

-35441 = 01110101 10001111

Es gab eine Übertragung von der 15. Stelle, es gibt keine Übertragung von der 14. Stelle. Das Ergebnis ist falsch, da es sich statt einer negativen Zahl als positiv herausstellte (das höchstwertige Bit ist 0).

Beispiel

-4875 = 11101100 11110101

+

-4875 = 11101100 11110101

=

09750 = 11011001 11101010 XNUMX

Es gibt Übertragungen vom 14. und 15. Bit. Das Ergebnis stimmt.

Wir haben also alle Fälle untersucht und festgestellt, dass die Überlaufsituation (Setzen des OF-Flags auf 1) während der Übertragung auftritt:

1) ab der 14. Stelle (für vorzeichenbehaftete positive Zahlen);

2) ab der 15. Stelle (für negative Zahlen).

Umgekehrt tritt kein Überlauf auf (dh das OF-Flag wird auf 0 zurückgesetzt), wenn es einen Übertrag von beiden Bits gibt oder wenn es keinen Übertrag in beiden Bits gibt.

Der Überlauf wird also mit dem Überlauf-Flag von registriert. Zusätzlich zum Flag of wird beim Übertragen vom höherwertigen Bit das Transfer-Flag CF auf 1 gesetzt. Da der Mikroprozessor nichts über die Existenz von vorzeichenbehafteten und vorzeichenlosen Zahlen weiß, ist allein der Programmierer für die korrekten Aktionen mit verantwortlich die resultierenden Zahlen. Sie können die CF- und OF-Flags mit den bedingten Sprungbefehlen JC\JNC bzw. JO\JNO parsen.

Die Befehle zum Addieren von Zahlen mit Vorzeichen sind die gleichen wie für Zahlen ohne Vorzeichen.

Subtraktion von vorzeichenlosen Binärzahlen

Wie bei der Analyse der Additionsoperation werden wir das Wesentliche der Prozesse diskutieren, die bei der Durchführung der Subtraktionsoperation auftreten. Ist der Minuend größer als der Subtrahend, dann gibt es kein Problem – die Differenz ist positiv, das Ergebnis ist richtig. Wenn der Minuend kleiner als der Subtrahend ist, gibt es ein Problem: Das Ergebnis ist kleiner als 0, und dies ist bereits eine vorzeichenbehaftete Zahl. In diesem Fall muss das Ergebnis umgebrochen werden. Was bedeutet das? Mit der üblichen Subtraktion (in einer Spalte) machen sie ein Darlehen von 1 von der höchsten Ordnung. Der Mikroprozessor tut dasselbe, d. h. er nimmt 1 von der Ziffer, die der höchsten Eins im Bitraster des Operanden folgt. Lassen Sie es uns anhand eines Beispiels erklären.

Beispiel

05 = 00000000 00000101 XNUMX

-10 = 00000000 00001010

Um die Subtraktion durchzuführen, machen wir es

erstrangiges imaginäres Darlehen:

100000000 00000101

-

00000000 00001010

=

11111111 11111011

Also im Wesentlichen die Aktion

(65 + 536) - 5 = 10

0 ist hier sozusagen gleichbedeutend mit der Zahl 65536. Das Ergebnis ist natürlich falsch, aber der Mikroprozessor geht davon aus, dass alles in Ordnung ist, obwohl er die Tatsache des Ausleihens einer Einheit festlegt, indem er das Carry-Flag setzt, vgl. Aber schauen Sie sich das Ergebnis der Subtraktionsoperation noch einmal genau an. Es ist -5 im Zweierkomplement! Führen wir ein Experiment durch: Stellen Sie die Differenz als Summe von 5 + (-10) dar.

Beispiel

5 = 00000000 00000101 XNUMX

+

(-10)= 11111111 11110110

=

11111111 11111011

d.h. wir haben das gleiche Ergebnis wie im vorherigen Beispiel erhalten.

Daher muss nach dem Befehl zum Subtrahieren von vorzeichenlosen Zahlen der Zustand des CE-Flags analysiert werden: Wenn es auf 1 gesetzt ist, zeigt dies an, dass von der höheren Ordnung geliehen wurde und das Ergebnis in einem zusätzlichen Code erhalten wurde .

Wie die Additionsbefehle besteht auch die Gruppe der Subtraktionsbefehle aus der kleinstmöglichen Menge. Diese Befehle führen die Subtraktion gemäß den Algorithmen durch, die wir jetzt betrachten, und die Ausnahmen müssen vom Programmierer selbst berücksichtigt werden. Zu den Subtraktionsbefehlen gehören:

1) dec operand - Dekrementoperation, d. h. den Wert des Operanden um 1 verringern;

2) Unteroperand_1, Operand_2 – Subtraktionsbefehl; sein Funktionsprinzip: operand_1 = operand_1 - operand_2;

3) sbb operand_1, operand_2 - Subtraktionsbefehl unter Berücksichtigung des Darlehens (ci-Flag): operand_1 = operand_1 - operand_2 - value_sG.

Wie Sie sehen, gibt es unter den Subtraktionsbefehlen einen sbb-Befehl, der das Carry-Flag berücksichtigt, vgl. Dieser Befehl ähnelt adc, aber jetzt dient das cf-Flag als Indikator dafür, dass beim Subtrahieren von Zahlen 1 von der höchstwertigen Ziffer ausgeliehen wird.

Vorzeichenbehaftete binäre Subtraktion

Hier ist alles etwas komplizierter. Der Mikroprozessor braucht nicht zwei Geräte zu haben - Addition und Subtraktion. Es reicht aus, nur eines zu haben - das Zusatzgerät. Aber für die Subtraktion durch Addieren von Zahlen mit einem Vorzeichen in einem zusätzlichen Code müssen beide Operanden dargestellt werden - sowohl der reduzierte als auch der subtrahierte. Das Ergebnis sollte auch als Zweierkomplementwert behandelt werden. Aber hier treten Schwierigkeiten auf. Erstens hängen sie damit zusammen, dass das höchstwertige Bit des Operanden als Vorzeichenbit betrachtet wird. Betrachten Sie das Beispiel der Subtraktion von 45 - (-127).

Beispiel

Subtraktion vorzeichenbehafteter Zahlen 1

45 = 0010 1101 XNUMX

-

-127 = 1000 0001

=

-44 = 1010 1100

Nach dem Vorzeichenbit zu urteilen, erwies sich das Ergebnis als negativ, was wiederum darauf hinweist, dass die Zahl als Komplement gleich -44 betrachtet werden sollte. Das korrekte Ergebnis sollte 172 sein. Hier, wie bei der vorzeichenbehafteten Addition, trafen wir auf einen Mantissenüberlauf, wenn das signifikante Bit der Zahl das Vorzeichenbit des Operanden änderte. Sie können diese Situation anhand des Inhalts des Überlauf-Flags verfolgen. Das Setzen auf 1 zeigt an, dass das Ergebnis für einen Operanden dieser Größe außerhalb des Bereichs der vorzeichenbehafteten Zahlen liegt (d. h. das höchstwertige Bit hat sich geändert), und der Programmierer muss Maßnahmen ergreifen, um das Ergebnis zu korrigieren.

Beispiel

Subtraktion vorzeichenbehafteter Zahlen 2

-45-45 = -45 + (-45) = -90.

-45 = 11010011

+

-45 = 11010011

=

-90 = 1010 0110

Hier ist alles in Ordnung, das Overflow-Flag von wird auf 0 zurückgesetzt und eine 1 im Vorzeichenbit zeigt an, dass der Ergebniswert eine Zweierkomplementzahl ist.

Subtraktion und Addition großer Operanden

Wie Sie bemerken, arbeiten die Additions- und Subtraktionsanweisungen mit Operanden einer festen Dimension: 8, 16, 32 Bit. Was aber, wenn Sie Zahlen mit einer größeren Dimension, beispielsweise 48 Bit, mit 16-Bit-Operanden addieren müssen? Lassen Sie uns zum Beispiel zwei 48-Bit-Zahlen hinzufügen:

Reis. 29. Hinzufügen großer Operanden

Abbildung 29 zeigt Schritt für Schritt die Technologie zum Hinzufügen langer Zahlen. Es ist ersichtlich, dass das Addieren von Multibyte-Zahlen auf die gleiche Weise erfolgt wie das Addieren von zwei Zahlen "in einer Spalte" - gegebenenfalls mit der Implementierung, 1 auf das höchste Bit zu übertragen. Wenn wir es schaffen, diesen Prozess zu programmieren, werden wir den Bereich der Binärzahlen, auf denen wir Additions- und Subtraktionsoperationen durchführen können, erheblich erweitern.

Das Prinzip der Subtraktion von Zahlen mit einem das Standard-Bitraster von Operanden überschreitenden Darstellungsbereich ist das gleiche wie bei der Addition, d. h. es wird das Carry-Flag cf verwendet. Sie müssen sich nur den Vorgang des Subtrahierens in einer Spalte vorstellen und die Mikroprozessoranweisungen korrekt mit der sbb-Anweisung kombinieren.

Um unsere Erörterung der Additions- und Subtraktionsbefehle abzuschließen, gibt es zusätzlich zu cf und den Flags noch einige andere Flags im eflags-Register, die mit binären arithmetischen Befehlen verwendet werden können. Dies sind die folgenden Flaggen:

1) zf - Null-Flag, das auf 1 gesetzt wird, wenn das Ergebnis der Operation 0 ist, und auf 1, wenn das Ergebnis ungleich 0 ist;

2) sf - Vorzeichen-Flag, dessen Wert nach arithmetischen Operationen (und nicht nur) mit dem Wert des höchstwertigen Bits des Ergebnisses übereinstimmt, d.h. mit Bit 7, 15 oder 31. Somit kann dieses Flag für Operationen verwendet werden auf vorzeichenbehafteten Nummern.

Multiplikation von vorzeichenlosen Zahlen

Der Befehl zum Multiplizieren von vorzeichenlosen Zahlen lautet

mul faktor_1

Wie Sie sehen können, enthält der Befehl nur einen Multiplikatoroperanden. Der zweite Operand factor_2 wird implizit angegeben. Seine Position ist festgelegt und hängt von der Größe der Faktoren ab. Da das Ergebnis einer Multiplikation im Allgemeinen größer ist als jeder ihrer Faktoren, muss auch ihre Größe und Lage eindeutig bestimmt werden. Optionen für die Größen der Faktoren und die Platzierung des zweiten Operanden und das Ergebnis sind in Tabelle 10 gezeigt.

Tabelle 10. Anordnung der Operanden und Ergebnis der Multiplikation

Aus der Tabelle ist ersichtlich, dass das Produkt aus zwei Teilen besteht und je nach Größe der Operanden an zwei Stellen platziert wird – anstelle von Faktor_2 (unterer Teil) und im zusätzlichen Register ah, dx, edx (oberer Teil). Teil). Wie kann man dann dynamisch (d. h. während der Programmausführung) wissen, dass das Ergebnis klein genug ist, um in ein Register zu passen, oder dass es die Dimension des Registers überschritten hat und der höchste Teil in einem anderen Register landete? Dazu verwenden wir die uns bereits aus der vorherigen Diskussion bekannten cf- und overflow-Flags:

1) wenn der führende Teil des Ergebnisses Null ist, dann sind nach der Produktoperation die Flags cf = 0 und of = 0;

2) Wenn diese Flags ungleich Null sind, bedeutet dies, dass das Ergebnis über den kleinsten Teil des Produkts hinausgegangen ist und aus zwei Teilen besteht, die bei der weiteren Arbeit berücksichtigt werden sollten.

Vorzeichenbehaftete Zahlen multiplizieren

Der Befehl zum Multiplizieren von Zahlen mit einem Vorzeichen lautet

[imul operand_1, operand_2, operand_3]

Dieser Befehl wird auf die gleiche Weise ausgeführt wie der Befehl mul. Eine Besonderheit des imul-Befehls ist nur die Bildung des Zeichens.

Wenn das Ergebnis klein ist und in ein Register passt (d. h. wenn cf = of = 0), dann ist der Inhalt des anderen Registers (der hohe Teil) eine Vorzeichenerweiterung – alle seine Bits sind gleich dem hohen Bit (Vorzeichenbit ) des unteren Teils des Ergebnisses. Andernfalls (wenn cf = of = 1) ist das Vorzeichen des Ergebnisses das Vorzeichenbit des hohen Teils des Ergebnisses, und das Vorzeichenbit des niedrigen Teils ist das signifikante Bit des binären Ergebniscodes.

Division von vorzeichenlosen Zahlen

Der Befehl zum Dividieren vorzeichenloser Zahlen lautet

div-Teiler

Der Divisor kann sich im Speicher oder in einem Register befinden und 8, 16 oder 32 Bit groß sein. Die Stelle des Dividenden ist fest und hängt wie bei der Multiplikationsanweisung von der Größe der Operanden ab. Das Ergebnis des Divisionsbefehls sind die Quotienten- und Restwerte.

Optionen für die Position und Größe der Operanden der Divisionsoperation sind in Tabelle 11 dargestellt.

Tabelle 11. Anordnung der Operanden und Ergebnis der Division

Nachdem der Divisionsbefehl ausgeführt wurde, sind die Inhalte der Flags undefiniert, aber es kann eine Unterbrechungsnummer 0 auftreten, die als "Division durch Null" bezeichnet wird. Diese Art der Unterbrechung gehört zu den sogenannten Ausnahmen. Diese Art von Interrupt tritt innerhalb des Mikroprozessors aufgrund einiger Anomalien während des Rechenvorgangs auf. Interrupt O, "dividiere durch Null", während der Ausführung des div-Befehls kann aus einem der folgenden Gründe auftreten:

1) der Teiler ist Null;

2) der Quotient ist nicht in dem ihm zugeordneten Bitraster enthalten, was in folgenden Fällen passieren kann:

a) wenn ein Dividenden mit einem Wert von einem Wort durch einen Divisor mit einem Wert von Bytes dividiert wird und der Wert des Dividenden mehr als 256-mal größer ist als der Wert des Divisors;

b) wenn ein Dividenden mit einem Wert von einem Doppelwort durch einen Divisor mit einem Wert von einem Wort dividiert wird und der Wert des Dividenden mehr als 65-mal größer ist als der Wert des Divisors;

c) bei Division des Dividenden mit Vierfachwortwert durch einen Divisor mit Doppelwortwert, und der Wert des Dividenden mehr als 4 mal größer ist als der Wert des Divisors.

Teilung mit Vorzeichen

Der Befehl zum Teilen von Zahlen mit Vorzeichen lautet

idiv-Teiler

Für diesen Befehl gelten alle betrachteten Bestimmungen bezüglich Befehlen und vorzeichenbehafteten Nummern. Wir beachten lediglich die Besonderheiten des Auftretens der Ausnahme 0, „Division durch Null“, bei Zahlen mit Vorzeichen. Es tritt auf, wenn der Idiv-Befehl aus einem der folgenden Gründe ausgeführt wird:

1) der Teiler ist Null;

2) der Quotient ist nicht in dem ihm zugeordneten Bitraster enthalten.

Letzteres wiederum kann passieren:

1) wenn ein Dividenden mit einem vorzeichenbehafteten Wortwert durch einen Divisor mit einem vorzeichenbehafteten Bytewert dividiert wird und der Wert des Dividenden mehr als das 128-fache des Werts des Divisors beträgt (daher sollte der Quotient nicht außerhalb des Bereichs von -128 liegen bis + 127);

2) bei Division des Dividenden durch einen vorzeichenbehafteten Doppelwortwert durch den Divisor durch einen vorzeichenbehafteten Wortwert, und der Wert des Dividenden mehr als das 32-fache des Werts des Divisors beträgt (daher darf der Quotient nicht außerhalb des Bereichs von - 768 bis +32) ;

3) wenn der Dividende durch einen vorzeichenbehafteten Quadword-Wert durch einen vorzeichenbehafteten Doppelwort-Divisor dividiert wird und der Wert des Dividenden mehr als das 2-fache des Werts des Divisors beträgt (daher darf der Quotient nicht außerhalb des Bereichs von -147 bis + liegen 483 648 2 147).

Hilfsbefehle für ganzzahlige Operationen

Es gibt mehrere Befehle im Befehlssatz des Mikroprozessors, die es einfacher machen können, Algorithmen zu programmieren, die arithmetische Berechnungen durchführen. Dabei können verschiedene Probleme auftreten, für deren Lösung die Mikroprozessor-Entwickler mehrere Befehle bereitgestellt haben.

Geben Sie Konvertierungsbefehle ein

Was ist, wenn die Größen der an arithmetischen Operationen beteiligten Operanden unterschiedlich sind? Angenommen, bei einer Additionsoperation ist ein Operand ein Wort und der andere ein Doppelwort. Es wurde oben gesagt, dass Operanden des gleichen Formats an der Additionsoperation teilnehmen müssen. Wenn die Zahlen vorzeichenlos sind, ist die Ausgabe leicht zu finden. In diesem Fall kann aus dem ursprünglichen Operanden eine neue (Doppelwortformat) gebildet werden, deren High-Bits einfach mit Nullen aufgefüllt werden können. Bei Zahlen mit Vorzeichen ist die Situation komplizierter: Wie kann das Vorzeichen des Operanden während der Programmausführung dynamisch berücksichtigt werden? Um solche Probleme zu lösen, hat der Befehlssatz des Mikroprozessors sogenannte Typumwandlungsbefehle. Diese Befehle erweitern Bytes zu Wörtern, Wörter zu Doppelwörtern und Doppelwörter zu Vierfachwörtern (64-Bit-Werte). Typkonvertierungsanweisungen sind besonders nützlich, wenn vorzeichenbehaftete Ganzzahlen konvertiert werden, da sie automatisch die höherwertigen Bits des neu konstruierten Operanden mit den Werten des Vorzeichenbits des alten Objekts füllen. Diese Operation führt zu ganzzahligen Werten mit demselben Vorzeichen und derselben Größe wie das Original, jedoch in einem längeren Format. Eine solche Transformation wird als Vorzeichenausbreitungsoperation bezeichnet.

Es gibt zwei Arten von Typumwandlungsbefehlen.

1. Anweisungen ohne Operanden. Diese Befehle arbeiten mit festen Registern:

1) cbw (Convert Byte to Word) – ein Befehl, um ein Byte (im al-Register) in ein Wort (im ah-Register) umzuwandeln, indem der Wert des hohen Bits al auf alle Bits des ah-Registers verteilt wird;

2) cwd (Convert Word to Double) – ein Befehl, um ein Wort (im Register ax) in ein Doppelwort (in den Registern dx: ax) umzuwandeln, indem der Wert des High-Bits ax auf alle Bits des Registers dx verteilt wird;

3) cwde (Convert Word to Double) – ein Befehl, um ein Wort (im Register ax) in ein Doppelwort (im Register eax) umzuwandeln, indem der Wert des High-Bits ax auf alle Bits der oberen Hälfte des eax-Registers verteilt wird ;

4) cdq (Convert Double Word to Quarter Word) – ein Befehl, um ein Doppelwort (im eax-Register) in ein Vierfachwort (in den edx: eax-Registern) umzuwandeln, indem der Wert des höchstwertigen Bits von eax auf alle verteilt wird Bits des edx-Registers.

2. Befehle movsx und movzx im Zusammenhang mit Befehlen zur Verarbeitung von Zeichenfolgen. Diese Befehle haben eine nützliche Eigenschaft im Kontext unseres Problems:

1) movsx operand_1, operand_2 - Senden mit Vorzeichenweitergabe. Erweitert einen 8- oder 16-Bit-Wert von operand_2, der ein Register oder ein Speicheroperand sein kann, auf einen 16- oder 32-Bit-Wert in einem der Register, wobei der Wert des Vorzeichenbits verwendet wird, um die höheren Positionen von operand_1 zu füllen. Diese Anweisung ist nützlich, um vorzeichenbehaftete Operanden für arithmetische Operationen vorzubereiten;

2) movzx operand_1, operand_2 - mit Null-Erweiterung senden. Erweitert den 8-Bit- oder 16-Bit-Wert von operand_2 auf 16-Bit oder 32-Bit und löscht (füllt) die hohen Positionen von operand_2 mit Nullen. Diese Anweisung ist nützlich, um vorzeichenlose Operanden für die Arithmetik vorzubereiten.

Andere nützliche Befehle

1. xadd Ziel, Quelle - Austausch und Ergänzung.

Mit dem Befehl können Sie zwei Aktionen nacheinander ausführen:

1) Ziel- und Quellwerte austauschen;

2) Platziere den Zieloperanden anstelle der Summe: Ziel = Ziel + Quelle.

2. neg Operand - Negation mit Zweierkomplement.

Die Anweisung invertiert den Wert des Operanden. Physikalisch führt der Befehl eine Aktion aus:

operand = 0 - Operand, d.h. subtrahiert den Operanden von Null.

Der Befehl neg operand kann verwendet werden:

1) um das Vorzeichen zu ändern;

2) um eine Subtraktion von einer Konstanten durchzuführen.

Arithmetische Operationen mit Binär-Dezimal-Zahlen

In diesem Abschnitt werden wir uns die Besonderheiten jeder der vier grundlegenden arithmetischen Operationen für gepackte und ungepackte BCD-Zahlen ansehen.

Es stellt sich zu Recht die Frage: Wozu brauchen wir BCD-Zahlen? Die Antwort könnte lauten: BCD-Zahlen werden in Geschäftsanwendungen benötigt, also dort, wo die Zahlen groß und präzise sein müssen. Wie wir bereits am Beispiel von Binärzahlen gesehen haben, sind Operationen mit solchen Zahlen für die Assemblersprache recht problematisch. Zu den Nachteilen der Verwendung von Binärzahlen gehören:

1) Werte im Wort- und Doppelwortformat haben eine begrenzte Reichweite. Wenn das Programm für den Finanzbereich konzipiert ist, wird die Begrenzung des Betrags in Rubel auf 65 (für ein Wort) oder sogar 536 (für ein Doppelwort) den Anwendungsbereich erheblich einschränken.

2) das Vorhandensein von Rundungsfehlern. Können Sie sich ein Programm vorstellen, das irgendwo in einer Bank läuft, das den Wert des Guthabens nicht berücksichtigt, wenn es mit binären ganzen Zahlen arbeitet und mit Milliarden arbeitet? Ich möchte nicht der Autor eines solchen Programms sein. Die Verwendung von Fließkommazahlen wird nicht sparen - dort gibt es das gleiche Rundungsproblem;

3) Darstellung einer großen Menge von Ergebnissen in symbolischer Form (ASCII-Code). Geschäftsprogramme führen nicht nur Berechnungen durch; Einer der Zwecke ihrer Verwendung ist die zeitnahe Bereitstellung von Informationen für den Benutzer. Dazu müssen die Informationen natürlich in symbolischer Form dargestellt werden. Das Konvertieren von Zahlen von binär nach ASCII erfordert einen gewissen Rechenaufwand. Eine Fließkommazahl ist noch schwieriger in eine symbolische Form zu übersetzen. Aber wenn Sie sich die hexadezimale Darstellung einer entpackten Dezimalziffer und ihres entsprechenden Zeichens in der ASCII-Tabelle ansehen, sehen Sie, dass sie sich um 30h unterscheiden. Somit ist die Umwandlung in symbolische Form und umgekehrt viel einfacher und schneller.

Sie haben wahrscheinlich bereits gesehen, wie wichtig es ist, zumindest die Grundlagen von Aktionen mit Dezimalzahlen zu beherrschen. Betrachten Sie als Nächstes die Merkmale der Durchführung grundlegender arithmetischer Operationen mit Dezimalzahlen. Wir bemerken sofort, dass es keine separaten Befehle für Addition, Subtraktion, Multiplikation und Division von BCD-Zahlen gibt. Dies geschah aus durchaus verständlichen Gründen: Die Dimension solcher Zahlen kann beliebig groß sein. BCD-Zahlen können addiert und subtrahiert werden, sowohl gepackt als auch ungepackt, aber nur ungepackte BCD-Zahlen können dividieren und multiplizieren. Warum das so ist, wird die weitere Diskussion zeigen.

Arithmetik auf entpackten BCD-Zahlen

Fügen Sie ungepackte BCD-Zahlen hinzu

Betrachten wir zwei Fälle von Addition.

Beispiel

Das Ergebnis der Addition ist nicht mehr als 9

6 = 0000 0110 XNUMX

+

3 = 0000 0011 XNUMX

=

9 = 0000 1001 XNUMX

Es findet kein Übergang von der Junior- zur Senior-Tetrade statt. Das Ergebnis stimmt.

Beispiel

Das Ergebnis der Addition ist größer als 9:

06 = 0000 0110 XNUMX

+

07 = 0000 0111 XNUMX

=

13 = 0000 1101 XNUMX

Wir haben keine BCD-Nummer mehr erhalten. Das Ergebnis ist falsch. Das korrekte Ergebnis im entpackten BCD-Format sollte 0000 0001 0000 0011 im Binärformat (oder 13 im Dezimalformat) lauten.

Nach der Analyse dieses Problems beim Hinzufügen von BCD-Zahlen (und ähnlicher Probleme bei der Ausführung anderer arithmetischer Operationen) und möglicher Lösungsmöglichkeiten entschieden sich die Entwickler des Mikroprozessor-Befehlssystems, keine speziellen Befehle für die Arbeit mit BCD-Zahlen einzuführen, sondern mehrere Korrekturbefehle einzuführen .

Der Zweck dieser Befehle besteht darin, das Ergebnis der Operation gewöhnlicher arithmetischer Befehle für Fälle zu korrigieren, in denen die Operanden in ihnen BCD-Zahlen sind.

Bei der Subtraktion in Beispiel 10 ist ersichtlich, dass das erhaltene Ergebnis korrigiert werden muss. Um den Vorgang des Addierens von zwei einstelligen ungepackten BCD-Zahlen im Mikroprozessor-Befehlssystem zu korrigieren, gibt es einen speziellen Befehl - aaa (ASCII Adjust for Addition) - Korrektur des Additionsergebnisses für die Darstellung in symbolischer Form.

Diese Anweisung hat keine Operanden. Es arbeitet implizit nur mit dem al-Register und analysiert den Wert seiner unteren Tetrade:

1) wenn dieser Wert kleiner als 9 ist, dann wird das cf-Flag auf XNUMX zurückgesetzt und der Übergang zur nächsten Anweisung wird ausgeführt;

2) Wenn dieser Wert größer als 9 ist, werden die folgenden Aktionen ausgeführt:

a) 6 wird zum Inhalt der unteren Tetrade al addiert (aber nicht zum Inhalt des gesamten Registers!) Dadurch wird der Wert des Dezimalergebnisses in die richtige Richtung korrigiert;

b) das Flag cf wird auf 1 gesetzt, wodurch die Übertragung auf das höchstwertige Bit festgelegt wird, damit es bei nachfolgenden Aktionen berücksichtigt werden kann.

Nehmen wir also in Beispiel 10 an, dass der Summenwert 0000 1101 in al ist, hat das Register nach der aaa-Anweisung 1101 + 0110 = 0011, d. h. binär 0000 0011 oder dezimal 3, und das cf-Flag wird auf 1 gesetzt. d.h. die Übertragung wurde im Mikroprozessor gespeichert. Als nächstes muss der Programmierer den adc-Additionsbefehl verwenden, der den Übertrag aus dem vorherigen Bit berücksichtigt.

Subtraktion von entpackten BCD-Zahlen

Die Situation hier ist ganz ähnlich wie bei der Addition. Betrachten wir die gleichen Fälle.

Beispiel

Das Ergebnis der Subtraktion ist nicht größer als 9:

6 = 0000 0110 XNUMX

-

3 = 0000 0011 XNUMX

=

3 = 0000 0011 XNUMX

Wie Sie sehen können, gibt es keine Ausleihe aus dem Seniorenheft. Das Ergebnis ist korrekt und bedarf keiner Korrektur.

Beispiel

Das Ergebnis der Subtraktion ist größer als 9:

6 = 0000 0110 XNUMX

-

7 = 0000 0111 XNUMX

=

-1 = 1111 1111

Die Subtraktion erfolgt nach den Regeln der binären Arithmetik. Daher ist das Ergebnis keine BCD-Zahl.

Das korrekte Ergebnis im entpackten BCD-Format sollte 9 (0000 1001 im Binärformat) sein. In diesem Fall wird wie bei einem normalen Subtraktionsbefehl, d.h. bei BCD-Zahlen, eigentlich die Subtraktion von 16 - 7 durchgeführt werden soll, von einer Anleihe von der höchstwertigen Stelle ausgegangen Bei einer Addition muss das Subtraktionsergebnis korrigiert werden. Dafür gibt es einen speziellen Befehl - aas (ASCII Adjust for Subtraction) - Korrektur des Subtraktionsergebnisses zur Darstellung in symbolischer Form.

Der aas-Befehl hat ebenfalls keine Operanden und arbeitet mit dem al-Register, indem er seine Tetrade kleinster Ordnung wie folgt analysiert:

1) wenn sein Wert kleiner als 9 ist, dann wird das cf-Flag auf 0 zurückgesetzt und die Steuerung wird an den nächsten Befehl übertragen;

2) Wenn der Tetradenwert in al größer als 9 ist, führt der aas-Befehl die folgenden Aktionen aus:

a) subtrahiert 6 vom Inhalt der unteren Tetrade des Registers al (Anmerkung - nicht vom Inhalt des gesamten Registers);

b) setzt die obere Tetrade von Register al zurück;

c) setzt das cf-Flag auf 1, wodurch das imaginäre Borgen höherer Ordnung festgelegt wird.

Es ist klar, dass der aas-Befehl in Verbindung mit den grundlegenden sub- und sbb-Subtraktionsbefehlen verwendet wird. In diesem Fall ist es sinnvoll, den sub-Befehl nur einmal zu verwenden, beim Subtrahieren der niedrigsten Ziffern der Operanden sollte dann der sbb-Befehl verwendet werden, der eine mögliche Ausleihe der höchsten Ordnung berücksichtigt.

Multiplikation von entpackten BCD-Zahlen

Am Beispiel des Addierens und Subtrahierens von ungepackten Zahlen wurde deutlich, dass es keine Standardalgorithmen gibt, um diese Operationen auf BCD-Zahlen durchzuführen, und der Programmierer diese Operationen entsprechend den Anforderungen an sein Programm selbst implementieren muss.

Die Umsetzung der beiden verbleibenden Operationen - Multiplikation und Division - ist noch komplizierter. Im Mikroprozessor-Befehlssatz gibt es nur Mittel zur Erzeugung von Multiplikation und Division von einstelligen ungepackten BCD-Zahlen.

Um Zahlen beliebiger Dimension zu multiplizieren, müssen Sie den Multiplikationsprozess selbst implementieren, basierend auf einem Multiplikationsalgorithmus, zum Beispiel "in einer Spalte".

Um zwei einstellige BCD-Zahlen zu multiplizieren, müssen Sie:

1) platziere einen der Faktoren im AL-Register (wie von der mul-Anweisung gefordert);

2) platziere den zweiten Operanden in einem Register oder Speicher, wobei ein Byte zugewiesen wird;

3) multiplizieren Sie die Faktoren mit dem Befehl mul (das Ergebnis wird wie erwartet in ah sein);

4) Das Ergebnis wird natürlich im Binärcode vorliegen, also muss es korrigiert werden.

Um das Ergebnis nach der Multiplikation zu korrigieren, wird ein spezieller Befehl verwendet - aam (ASCII Adjust for Multiplication) - Korrektur des Ergebnisses der Multiplikation für die Darstellung in symbolischer Form.

Es hat keine Operanden und arbeitet mit dem AX-Register wie folgt:

1) teilt al durch 10;

2) Das Ergebnis der Division wird wie folgt geschrieben: Quotient in al, Rest in ah. Als Ergebnis enthalten nach Ausführung des aam-Befehls die AL- und ah-Register die korrekten BCD-Ziffern des Produkts zweier Ziffern.

Bevor wir unsere Diskussion über den Befehl aam beenden, müssen wir noch eine weitere Verwendung dafür erwähnen. Mit diesem Befehl kann eine Binärzahl im AL-Register in eine entpackte BCD-Zahl umgewandelt werden, die im ah-Register abgelegt wird: Die höchstwertige Ziffer des Ergebnisses ist in ah, die niedrigstwertige Ziffer ist in al. Es ist klar, dass die Binärzahl im Bereich 0...99 liegen muss.

Division von ungepackten BCD-Zahlen

Der Prozess der Durchführung der Divisionsoperation zweier entpackter BCD-Zahlen unterscheidet sich etwas von den anderen Operationen, die zuvor mit ihnen betrachtet wurden. Auch hier sind Korrekturmaßnahmen erforderlich, die jedoch vor der Hauptoperation durchgeführt werden müssen, die eine BCD-Zahl direkt durch eine andere BCD-Zahl dividiert. Zuerst müssen Sie im Register ah zwei ungepackte BCD-Ziffern des Dividenden erhalten. Das macht es dem Programmierer in gewisser Weise bequem für ihn. Als nächstes müssen Sie den Befehl aad - aad (ASCII Adjust for Division) - Divisionskorrektur für die symbolische Darstellung ausführen.

Der Befehl hat keine Operanden und wandelt die zweistellige entpackte BCD-Zahl im Ax-Register in eine Binärzahl um. Diese Binärzahl wird anschließend bei der Division die Rolle des Dividenden spielen. Zusätzlich zur Konvertierung platziert der Befehl aad die resultierende Binärzahl im AL-Register. Die Dividende wird natürlich eine Binärzahl aus dem Bereich 0...99 sein.

Der Algorithmus, mit dem der Befehl aad diese Konvertierung durchführt, lautet wie folgt:

1) Multipliziere die höchste Ziffer der ursprünglichen BCD-Zahl in ah (der Inhalt von AH) mit 10;

2) Führen Sie die Addition AH + AL durch, deren Ergebnis (Binärzahl) in AL eingetragen wird;

3) Setzen Sie den Inhalt von AH zurück.

Als nächstes muss der Programmierer einen normalen div-Divisionsbefehl ausgeben, um die Division des Inhalts von ax durch eine einzelne BCD-Ziffer durchzuführen, die sich in einem Byte-Register oder einer Byte-Speicherstelle befindet.

Ähnlich wie aash können mit dem Befehl aad auch entpackte BCD-Zahlen aus dem Bereich 0...99 in ihr binäres Äquivalent konvertiert werden.

Um Zahlen mit größerer Kapazität zu dividieren, sowie im Fall der Multiplikation, müssen Sie Ihren eigenen Algorithmus implementieren, beispielsweise "in einer Spalte", oder einen optimaleren Weg finden.

Arithmetik mit gepackten BCD-Zahlen

Wie oben erwähnt, können gepackte BCD-Zahlen nur addiert und subtrahiert werden. Um andere Aktionen mit ihnen auszuführen, müssen sie zusätzlich entweder in ein entpacktes Format oder in eine binäre Darstellung konvertiert werden. Da gepackte BCD-Zahlen nicht von großem Interesse sind, werden wir sie kurz betrachten.

Hinzufügen von gepackten BCD-Zahlen

Kommen wir zunächst zum Kern des Problems und versuchen, zwei zweistellige gepackte BCD-Zahlen zu addieren. Beispiel zum Hinzufügen von gepackten BCD-Zahlen:

67 = 01100111

+

75 = 01110101

=

142 = 1101 1100 = 220

Wie Sie sehen können, ist das Ergebnis im Binärformat 1101 1100 (oder 220 dezimal), was falsch ist. Dies liegt daran, dass der Mikroprozessor das Vorhandensein von BCD-Zahlen nicht kennt und sie gemäß den Regeln zum Addieren von Binärzahlen addiert. Eigentlich sollte das Ergebnis in BCD 0001 0100 0010 (oder 142 dezimal) sein.

Es ist ersichtlich, dass, wie für ungepackte BCD-Zahlen, für gepackte BCD-Zahlen eine Notwendigkeit besteht, die Ergebnisse arithmetischer Operationen irgendwie zu korrigieren.

Der Mikroprozessor sieht für diesen Befehl daa - daa (Dezimalanpassung für Addition) - Korrektur des Additionsergebnisses zur Darstellung in Dezimalform vor.

Der daa-Befehl wandelt den Inhalt des al-Registers nach dem in der Beschreibung des daa-Befehls angegebenen Algorithmus in zwei gepackte Dezimalziffern um und speichert die resultierende Einheit (falls das Ergebnis der Addition größer als 99 ist) im cf-Flag, dabei wird der Übergang zum höchstwertigen Bit berücksichtigt.

Subtraktion von gepackten BCD-Zahlen

Ähnlich wie bei der Addition behandelt der Mikroprozessor die gepackten BCD-Zahlen als binär und subtrahiert die BCD-Zahlen entsprechend als binär.

Beispiel

Subtraktion von gepackten BCD-Zahlen.

Lassen Sie uns 67-75 subtrahieren. Da der Mikroprozessor die Subtraktion im Wege der Addition durchführt, gehen wir wie folgt vor:

67 = 01100111

+

-75 = 10110101

=

-8 = 0001 1100 = 28

Wie Sie sehen können, ist das Ergebnis 28 in Dezimalzahl, was absurd ist. In BCD sollte das Ergebnis 0000 1000 (oder 8 dezimal) sein.

Bei der Programmierung der Subtraktion von gepackten BCD-Zahlen muss der Programmierer, ebenso wie bei der Subtraktion von ungepackten BCD-Zahlen, das Vorzeichen selbst kontrollieren. Dies erfolgt unter Verwendung des CF-Flags, das die Ausleihe hoher Ordnung festlegt.

Die Subtraktion von BCD-Zahlen selbst wird durch einen einfachen sub- oder sbb-Subtraktionsbefehl durchgeführt. Die Korrektur des Ergebnisses erfolgt durch den Befehl das - das (Dezimalanpassung für Subtraktion) - Korrektur des Ergebnisses der Subtraktion für die Darstellung in Dezimalform.

Der das-Befehl konvertiert den Inhalt des AL-Registers in zwei gepackte Dezimalziffern gemäß dem Algorithmus, der in der Beschreibung des das-Befehls angegeben ist.

VORTRAG Nr. 19. Steuerübertragungsbefehle

1. Logikbefehle

Neben den Mitteln arithmetischer Berechnungen verfügt das Mikroprozessor-Befehlssystem auch über Mittel zur logischen Datenumwandlung. Mit logischen Mitteln solche Datentransformationen, die auf den Regeln der formalen Logik beruhen.

Formale Logik arbeitet auf der Ebene wahrer und falscher Aussagen. Für einen Mikroprozessor bedeutet dies normalerweise 1 bzw. 0. Für einen Computer ist die Sprache der Nullen und Einsen nativ, aber die minimale Dateneinheit, mit der Maschinenanweisungen arbeiten, ist ein Byte. Auf der Systemebene ist es jedoch oft erforderlich, auf der niedrigstmöglichen Ebene, der Bitebene, arbeiten zu können.

Reis. 29. Mittel zur logischen Datenverarbeitung

Die Mittel der logischen Datentransformation umfassen logische Befehle und logische Operationen. Der Operand einer Assembler-Anweisung kann allgemein ein Ausdruck sein, der wiederum eine Kombination aus Operatoren und Operanden ist. Unter diesen Operatoren kann es Operatoren geben, die logische Operationen auf Ausdrucksobjekten implementieren.

Bevor wir uns diese Tools im Detail ansehen, wollen wir uns überlegen, was die logischen Daten selbst sind und welche Operationen mit ihnen ausgeführt werden.

Boolesche Daten

Die theoretische Grundlage für die logische Datenverarbeitung ist die formale Logik. Es gibt mehrere Logiksysteme. Eine der bekanntesten ist die Aussagenlogik. Eine Aussage ist jede Aussage, die entweder als wahr oder falsch bezeichnet werden kann.

Der Aussagenkalkül ist eine Reihe von Regeln, die verwendet werden, um die Wahrheit oder Falschheit einer Kombination von Aussagen zu bestimmen.

Der Aussagenkalkül wird sehr harmonisch mit den Prinzipien des Computers und den grundlegenden Methoden seiner Programmierung kombiniert. Alle Hardwarekomponenten eines Computers sind auf Logikchips aufgebaut. Das System zur Darstellung von Informationen in einem Computer auf der untersten Ebene basiert auf dem Konzept eines Bits. Ein Bit, das nur zwei Zustände hat (0 (falsch) und 1 (wahr)), passt natürlich in den Aussagenkalkül.

Gemäß der Theorie können die folgenden logischen Operationen auf Anweisungen (auf Bits) ausgeführt werden.

1. Negation (logisches NICHT) - eine logische Operation an einem Operanden, deren Ergebnis der Kehrwert des Werts des ursprünglichen Operanden ist.

Diese Operation ist durch die folgende Wahrheitstabelle (Tabelle 12) eindeutig gekennzeichnet.

Tabelle 12. Wahrheitstabelle für logische Negation

2. Logische Addition (logisches inklusives ODER) – eine logische Operation an zwei Operanden, deren Ergebnis „wahr“ (1) ist, wenn einer oder beide Operanden wahr (1) sind, und „falsch“ (0), wenn beide Operanden wahr sind falsch (0).

Diese Operation wird unter Verwendung der folgenden Wahrheitstabelle (Tabelle 13) beschrieben.

Tabelle 13. Wahrheitstabelle für logisches inklusives ODER

3. Logische Multiplikation (logisches UND) – eine logische Operation an zwei Operanden, deren Ergebnis nur dann wahr (1) ist, wenn beide Operanden wahr (1) sind. In allen anderen Fällen ist der Wert der Operation "false" (0).

Diese Operation wird unter Verwendung der folgenden Wahrheitstabelle (Tabelle 14) beschrieben.

Tabelle 14. Logik-UND-Wahrheitstabelle

4. Logische exklusive Addition (logisches exklusives ODER) - eine logische Operation an zwei Operanden, deren Ergebnis "wahr" (1) ist, wenn nur einer der beiden Operanden wahr (1) ist, und falsch (0), wenn Beide Operanden sind entweder falsch (0) oder wahr (1). Diese Operation wird unter Verwendung der folgenden Wahrheitstabelle (Tabelle 15) beschrieben.

Tabelle 15. Wahrheitstabelle für logisches XOR

Der Befehlssatz des Mikroprozessors enthält fünf Befehle, die diese Operationen unterstützen. Diese Befehle führen logische Operationen an den Bits der Operanden durch. Die Dimensionen der Operanden müssen natürlich gleich sein. Wenn beispielsweise die Dimension der Operanden gleich dem Wort (16 Bits) ist, dann wird die logische Operation zuerst an den Null-Bits der Operanden durchgeführt und ihr Ergebnis wird anstelle von Bit 0 des Ergebnisses geschrieben. Als nächstes wiederholt der Befehl diese Aktionen sequentiell für alle Bits vom ersten bis zum fünfzehnten.

Logikbefehle

Das Mikroprozessor-Befehlssystem verfügt über den folgenden Befehlssatz, der die Arbeit mit logischen Daten unterstützt:

1) und operand_1, operand_2 - logische Multiplikationsoperation. Der Befehl führt eine bitweise logische UND-Operation (Konjunktion) an den Bits der Operanden operand_1 und operand_2 durch. Das Ergebnis wird anstelle von operand_1 geschrieben;

2) og operand_1, operand_2 - logische Additionsoperation. Der Befehl führt eine bitweise logische ODER-Operation (Disjunktion) an den Bits der Operanden operand_1 und operand_2 durch. Das Ergebnis wird anstelle von operand_1 geschrieben;

3) xor operand_1, operand_2 - Operation der logischen exklusiven Addition. Der Befehl führt eine bitweise logische XOR-Operation an den Bits der Operanden operand_1 und operand_2 durch. Das Ergebnis wird anstelle des Operanden geschrieben;

4) test operand_1, operand_2 - "Test"-Operation (unter Verwendung des logischen Multiplikationsverfahrens). Der Befehl führt eine bitweise logische UND-Operation an den Bits der Operanden operand_1 und operand_2 durch. Der Zustand der Operanden bleibt gleich, nur die Flags zf, sf und pf werden geändert, was es ermöglicht, den Zustand einzelner Bits des Operanden zu analysieren, ohne ihren Zustand zu ändern;

5) Nicht-Operand - Operation der logischen Negation. Der Befehl führt eine bitweise Umkehrung (Ersetzen des Wertes durch das Gegenteil) jedes Bits des Operanden durch. Das Ergebnis wird anstelle des Operanden geschrieben.

Um die Rolle logischer Befehle im Befehlssatz von Mikroprozessoren zu verstehen, ist es sehr wichtig, die Bereiche ihrer Anwendung und typische Methoden ihrer Verwendung bei der Programmierung zu verstehen.

Mit Hilfe von logischen Befehlen ist es möglich, einzelne Bits im Operanden auszuwählen, um sie zu setzen, zurückzusetzen, zu invertieren oder einfach auf einen bestimmten Wert zu prüfen.

Um solche Arbeiten mit Bits zu organisieren, spielt operand_2 normalerweise die Rolle einer Maske. Mit Hilfe der in Bit 1 gesetzten Bits dieser Maske werden die für eine bestimmte Operation notwendigen operand_1-Bits ermittelt. Lassen Sie uns zeigen, welche logischen Befehle für diesen Zweck verwendet werden können:

1) Um bestimmte Ziffern (Bits) auf 1 zu setzen, wird der Befehl og operand_1, operand_2 verwendet.

In dieser Anweisung muss operand_2, der als Maske fungiert, 1-Bits anstelle der Bits enthalten, die in operand_1 auf XNUMX gesetzt werden sollten;

2) Um bestimmte Ziffern (Bits) auf 0 zurückzusetzen, wird der Befehl und operand_1, operand_2 verwendet.

In dieser Anweisung muss operand_2, der als Maske fungiert, Null-Bits anstelle der Bits enthalten, die in operand_0 auf 1 gesetzt werden müssen;

3) Befehl xor operand_1, operand_2 wird angewendet:

a) herauszufinden, welche Bits in operand_1 und operand unterschiedlich sind;

b) den Zustand der spezifizierten Bits in operand_1 zu invertieren.

Die für uns interessanten Maskenbits (operand_2) bei der Ausführung des xor-Befehls müssen einfach sein, der Rest muss Null sein;

Der Befehl test operand_1, operand_2 (check operand_1) wird verwendet, um den Status der angegebenen Bits zu überprüfen.

Die geprüften Bits von operand_1 in der Maske (operand_2) müssen auf eins gesetzt werden. Der Algorithmus des test-Befehls ähnelt dem Algorithmus des and-Befehls, ändert jedoch nicht den Wert von operand_1. Das Ergebnis des Befehls besteht darin, den Wert des Null-Flags zf zu setzen:

1) wenn zf = 0, dann wird als Ergebnis der logischen Multiplikation ein Nullergebnis erhalten, d. h. ein Einheitsbit der Maske, das nicht mit dem entsprechenden Einheitsbit des Operanden übereinstimmt;

2) wenn zf = 1, dann wird als Ergebnis der logischen Multiplikation ein Ergebnis ungleich Null erhalten, d. h. mindestens ein Einheitsbit der Maske stimmt mit dem entsprechenden Einheitsbit von operand_1 überein.

Um auf das Ergebnis des Testbefehls zu reagieren, ist es ratsam, den Sprungbefehl jnz label (Jump if Not Zero) - jump if the zero flag zf non-zero oder den Reverse-Action-Befehl - jz label (Jump if Zero ) - springen, wenn das Null-Flag zf = 0 ist.

Die folgenden beiden Befehle suchen nach dem ersten auf 1 gesetzten Operandenbit. Die Suche kann sowohl am Anfang als auch am Ende des Operanden erfolgen:

1) bsf operand_1, operand_2 (Bit Scanning Forward) – Bits vorwärts scannen. Der Befehl durchsucht (scannt) die Bits von operand_2 vom niedrigstwertigen zum höchstwertigen (von Bit 0 zum höchstwertigen Bit) auf der Suche nach dem ersten auf 1 gesetzten Bit. Wenn eines gefunden wird, wird operand_1 mit der Zahl von gefüllt dieses Bit als ganzzahliger Wert. Wenn alle Bits von operand_2 0 sind, dann wird das Null-Flag zf auf 1 gesetzt, andernfalls wird das zf-Flag auf 0 zurückgesetzt;

2) bsr operand_1, operand_2 (Bit-Scanning-Reset) – Scan-Bits in umgekehrter Reihenfolge. Der Befehl durchsucht (scannt) die Bits von operand_2 vom höchstwertigen bis zum niederwertigsten (vom höchstwertigen Bit bis Bit 0) auf der Suche nach dem ersten Bit, das auf 1 gesetzt ist. Wenn eines gefunden wird, wird operand_1 mit der Zahl von gefüllt dieses Bit als ganzzahliger Wert. Wichtig ist, dass die Position des ersten Einheitsbits links noch relativ zu Bit 0 gezählt wird. Wenn alle Bits von operand_2 0 sind, dann wird das Null-Flag zf auf 1 gesetzt, andernfalls wird das zf-Flag auf 0 zurückgesetzt.

In den neuesten Modellen von Intel-Mikroprozessoren sind einige weitere Anweisungen in der Gruppe der logischen Anweisungen erschienen, mit denen Sie auf ein bestimmtes Bit des Operanden zugreifen können. Der Operand kann sich entweder im Speicher oder in einem allgemeinen Register befinden. Die Bitposition wird durch den Bit-Offset relativ zum niederwertigsten Bit des Operanden angegeben. Der Offset-Wert kann als direkter Wert angegeben oder in einem Mehrzweckregister enthalten sein. Sie können die Ergebnisse der Befehle bsr und bsf als Offset-Wert verwenden. Alle Anweisungen weisen dem CE-Flag den Wert des ausgewählten Bits zu.

1) bt-Operand, bit_offset (Bittest) - Bittest. Der Befehl überträgt den Bitwert an das cf-Flag;

2) bts-Operand, offset_bit (Bit Test and Set) - Prüfen und Setzen eines Bits. Die Anweisung überträgt den Bitwert auf das CF-Flag und setzt dann das zu prüfende Bit auf 1;

3) btr operand, bit_offset (Bit Test and Reset) - Prüfen und Zurücksetzen eines Bits. Die Anweisung überträgt den Bitwert an das CF-Flag und setzt dieses Bit dann auf 0;

4) BTC-Operand, offset_bit (Bit Test and Convert) - Prüfen und Invertieren eines Bits. Der Befehl packt den Wert eines Bits in das cf-Flag und invertiert dann den Wert dieses Bits.

Schaltbefehle

Die Befehle in dieser Gruppe stellen auch eine Manipulation einzelner Bits der Operanden bereit, jedoch auf eine andere Weise als die oben diskutierten logischen Befehle.

Alle Schiebebefehle verschieben Bits im Operandenfeld je nach Opcode nach links oder rechts. Alle Verschiebungsbefehle haben die gleiche Struktur - Operand kopieren, shift_count.

Die Anzahl der zu verschiebenden Bits - counter_shifts - steht an der Stelle des zweiten Operanden und kann auf zwei Arten eingestellt werden:

1) statisch, wobei ein fester Wert über einen direkten Operanden gesetzt wird;

2) dynamisch, was bedeutet, dass der Wert des Schiebezählers in das cl-Register eingegeben wird, bevor der Schiebebefehl ausgeführt wird.

Anhand der Dimension des cl-Registers ist klar, dass der Wert des Schiebezählers zwischen 0 und 255 liegen kann. Tatsächlich ist dies jedoch nicht ganz richtig. Zur Optimierung akzeptiert der Mikroprozessor nur den Wert der fünf niederwertigsten Bits des Zählers, d.h. der Wert liegt im Bereich von 0 bis 31.

Alle Schiebebefehle setzen das Carry-Flag, vgl.

Wenn Bits aus dem Operanden verschoben werden, treffen sie zuerst auf das Carry-Flag und setzen es gleich dem Wert des nächsten Bits außerhalb des Operanden. Wohin dieses Bit als nächstes geht, hängt von der Art des Schiebebefehls und dem Programmalgorithmus ab.

Schaltbefehle lassen sich nach dem Funktionsprinzip in zwei Arten unterteilen:

1) lineare Schaltbefehle;

2) zyklische Schaltbefehle.

Lineare Schaltbefehle

Zu Befehlen dieses Typs gehören Befehle, die nach folgendem Algorithmus wechseln:

1) das nächste Bit, das geschoben wird, setzt das CF-Flag;

2) das von der anderen Seite in den Operanden eingetragene Bit hat den Wert 0;

3) Wenn das nächste Bit verschoben wird, geht es in das CF-Flag, während der Wert des vorherigen verschobenen Bits verloren geht! Lineare Schaltbefehle werden in zwei Untertypen unterteilt:

1) logische lineare Schaltbefehle;

2) arithmetische lineare Verschiebungsbefehle.

Die logischen linearen Verschiebungsbefehle umfassen Folgendes:

1) shl operand, counter_shifts (Shift Logical Left) - logische Verschiebung nach links. Der Inhalt des Operanden wird um die durch shift_count angegebene Anzahl von Bits nach links verschoben. Rechts (an der Stelle des niederwertigsten Bits) werden Nullen eingetragen;

2) shr-Operand, shift_count (Logische Verschiebung nach rechts) – logische Verschiebung nach rechts. Der Inhalt des Operanden wird um die durch shift_count angegebene Anzahl von Bits nach rechts verschoben. Links (an der Stelle des höchstwertigen Vorzeichenbits) werden Nullen eingetragen.

Abbildung 30 zeigt, wie diese Befehle funktionieren.

Reis. 30. Schema der Arbeit der Befehle der linearen logischen Verschiebung

Arithmetische lineare Schiebebefehle unterscheiden sich von logischen Schiebebefehlen dadurch, dass sie auf besondere Weise mit dem Vorzeichenbit des Operanden arbeiten.

1) sal operand, shift_counter (Shift Arithmetic Left) - arithmetische Verschiebung nach links. Der Inhalt des Operanden wird um die durch shift_count angegebene Anzahl von Bits nach links verschoben. Rechts (an der Stelle des niederwertigsten Bits) werden Nullen eingetragen. Der sal-Befehl behält das Vorzeichen nicht bei, setzt aber das Flag mit / im Falle eines Vorzeichenwechsels um das nächste vorgerückte Bit. Ansonsten ist der sal-Befehl genau derselbe wie der shl-Befehl;

2) sar-Operand, shift_count (Shift Arithmetic Right) - arithmetische Verschiebung nach rechts. Der Inhalt des Operanden wird um die durch shift_count angegebene Anzahl von Bits nach rechts verschoben. In den linken Operanden werden Nullen eingefügt. Der Befehl sar bewahrt das Vorzeichen und stellt es nach jeder Bitverschiebung wieder her.

Abbildung 31 zeigt, wie lineare arithmetische Verschiebungsbefehle funktionieren.

Reis. 31. Operationsschema von linearen arithmetischen Verschiebungsbefehlen

Drehbefehle

Die zyklischen Schiebebefehle umfassen Befehle, die die Werte der verschobenen Bits speichern. Es gibt zwei Arten von zyklischen Schaltbefehlen:

1) einfache zyklische Schaltbefehle;

2) zyklische Schiebebefehle über das Carry-Flag vgl.

Zu den einfachen zyklischen Schaltbefehlen gehören:

1) rol operand, shift_counter (Rotate Left) - zyklische Verschiebung nach links. Der Inhalt des Operanden wird um die durch den Operanden shift_count angegebene Anzahl von Bits nach links verschoben. Linksverschobene Bits werden von rechts auf denselben Operanden geschrieben;

2) gog operand, counter_shifts (Rotate Right) - zyklische Verschiebung nach rechts. Der Inhalt des Operanden wird um die durch den Operanden shift_count angegebene Anzahl von Bits nach rechts verschoben. Nach rechts verschobene Bits werden in denselben Operanden auf der linken Seite geschrieben.

Reis. 32. Funktionsschema der Befehle einer einfachen zyklischen Verschiebung

Wie aus Abbildung 32 ersichtlich ist, führen die Befehle einer einfachen zyklischen Verschiebung im Laufe ihrer Arbeit eine nützliche Aktion aus, nämlich: Das zyklisch verschobene Bit wird nicht nur vom anderen Ende in den Operanden geschoben, sondern gleichzeitig dessen value wird zum Wert des CE-Flags.

Die zyklischen Schiebebefehle durch das Carry-Flag CF unterscheiden sich von den einfachen zyklischen Schiebebefehlen dadurch, dass das verschobene Bit nicht sofort von dessen anderer Seite in den Operanden gelangt, sondern zuerst in das Carry-Flag CE geschrieben wird. Erst die nächste Ausführung dieses Schiebebefehls ( vorausgesetzt, dass es zyklisch ausgeführt wird) führt zur Plazierung des vorher vorgerückten Bits vom anderen Ende des Operanden (Fig. 33).

Zu den zyklischen Schaltbefehlen über das Carry-Flag gehören:

1) rcl-Operand, shift_count (Durch Übertrag nach links rotieren) – zyklische Verschiebung nach links durch Übertrag.

Der Inhalt des Operanden wird um die durch den Operanden shift_count angegebene Anzahl von Bits nach links verschoben. Die verschobenen Bits werden wiederum zum Wert des Carry-Flags, vgl.

2) rsg operand, shift_count (Rotate through Carry Right) – zyklische Verschiebung nach rechts durch einen Übertrag.

Der Inhalt des Operanden wird um die durch den Operanden shift_count angegebene Anzahl von Bits nach rechts verschoben. Die verschobenen Bits werden wiederum zum Wert des Carry-Flags CF.

Reis. 33. Rotationsbefehle über Carry Flag CF

Bild 33 zeigt, dass beim Durchschieben des Carry-Flags ein Zwischenelement erscheint, mit dessen Hilfe insbesondere zyklisch verschobene Bits, insbesondere die Fehlanpassung von Bitfolgen, ersetzt werden können.

Im Folgenden bedeutet Nichtübereinstimmung einer Bitsequenz eine Aktion, die es auf irgendeine Weise ermöglicht, die erforderlichen Abschnitte dieser Sequenz zu lokalisieren und zu extrahieren und sie an eine andere Stelle zu schreiben.

Zusätzliche Schaltbefehle

Das Befehlssystem der neuesten Intel-Mikroprozessormodelle, beginnend mit dem i80386, enthält zusätzliche Umschaltbefehle, die die zuvor besprochenen Funktionen erweitern. Dies sind die Schaltbefehle mit doppelter Präzision:

1) shld operand_1, operand_2, shift_counter – Linksverschiebung mit doppelter Genauigkeit. Der Befehl shld führt eine Ersetzung durch, indem er die Bits von operand_1 nach links verschiebt und seine Bits auf der rechten Seite mit den Werten der von operand_2 verschobenen Bits gemäß dem Diagramm in Abb. füllt. 34. Die Anzahl der zu verschiebenden Bits wird durch den Wert von „shift_counter“ bestimmt, der im Bereich von 0 bis 31 liegen kann. Dieser Wert kann als unmittelbarer Operand angegeben oder im CL-Register enthalten sein. Der Wert von operand_2 wird nicht geändert.

Reis. 34. Das Schema des shld-Befehls

2) shrd operand_1, operand_2, shift_counter – Rechtsverschiebung mit doppelter Genauigkeit. Der Befehl führt die Ersetzung durch, indem er die Bits des Operanden operand_1 nach rechts verschiebt und seine Bits auf der linken Seite mit den Werten der von operand_2 verschobenen Bits gemäß dem Diagramm in Abbildung 35 füllt. Die Anzahl der zu verschiebenden Bits beträgt wird durch den Wert des Shift_Counters bestimmt, der im Bereich 0...31 liegen kann. Dieser Wert kann durch den unmittelbaren Operanden angegeben oder im CL-Register enthalten sein. Der Wert von operand_2 wird nicht geändert.

Reis. 35. Das Schema des Shrd-Befehls

Wie wir angemerkt haben, verschieben die Befehle shld und shrd bis zu 32 Bit, aber aufgrund der Besonderheiten bei der Angabe von Operanden und des Operationsalgorithmus können diese Befehle verwendet werden, um mit Feldern mit einer Länge von bis zu 64 Bit zu arbeiten.

2. Steuerübertragungsbefehle

Wir haben einige Befehle kennengelernt, aus denen die linearen Abschnitte des Programms gebildet werden. Jeder von ihnen führt im Allgemeinen eine gewisse Datenumwandlung oder -übertragung durch, wonach der Mikroprozessor die Steuerung an die nächste Anweisung übergibt. Aber nur sehr wenige Programme arbeiten so konsistent. Es gibt normalerweise Stellen in einem Programm, an denen entschieden werden muss, welche Anweisung als nächstes ausgeführt wird. Diese Lösung könnte sein:

1) bedingungslos - An diesem Punkt ist es notwendig, die Kontrolle nicht auf den nächsten Befehl zu übertragen, sondern auf einen anderen, der sich in einiger Entfernung vom aktuellen Befehl befindet;

2) bedingt – die Entscheidung, welcher Befehl als nächstes ausgeführt wird, wird auf der Grundlage der Analyse einiger Bedingungen oder Daten getroffen.

Ein Programm ist eine Folge von Befehlen und Daten, die eine bestimmte Menge an RAM-Speicherplatz belegen. Dieser Speicherplatz kann entweder zusammenhängend sein oder aus mehreren Fragmenten bestehen.

Welche Programmanweisung als nächstes ausgeführt werden soll, lernt der Mikroprozessor aus dem Inhalt des Registerpaares cs:(e)ip:

1) cs - Codesegmentregister, das die physikalische (Basis-)Adresse des aktuellen Codesegments enthält;

2) eip/ip – Befehlszeigerregister, das einen Wert enthält, der den Versatz im Speicher des nächsten auszuführenden Befehls relativ zum Beginn des aktuellen Codesegments darstellt.

Welche Register verwendet werden, hängt vom eingestellten Adressierungsmodus use16 oder use32 ab. Wenn use 16 angegeben ist, wird ip verwendet, wenn use32 angegeben ist, wird eip verwendet.

Steuerübertragungsbefehle ändern somit den Inhalt der cs- und eip/ip-Register, wodurch der Mikroprozessor nicht den nächsten Programmbefehl in der Reihenfolge zur Ausführung auswählt, sondern den Befehl in einem anderen Abschnitt des Programms. Die Pipeline innerhalb des Mikroprozessors wird zurückgesetzt.

Nach dem Funktionsprinzip können die Mikroprozessorbefehle, die die Organisation von Übergängen im Programm bereitstellen, in 3 Gruppen eingeteilt werden:

1. Unbedingte Weitergabe von Steuerbefehlen:

1) ein unbedingter Verzweigungsbefehl;

2) ein Befehl zum Aufrufen einer Prozedur und zum Zurückkehren von einer Prozedur;

3) einen Befehl zum Aufrufen von Software-Interrupts und zum Zurückkehren von Software-Interrupts.

2. Befehle zur bedingten Kontrollübergabe:

1) Sprungbefehle durch das Ergebnis des Vergleichsbefehls p;

2) Übergangsbefehle gemäß dem Zustand eines bestimmten Flags;

3) Anweisungen zum Springen durch den Inhalt des esx/cx-Registers.

3. Befehle zur Zyklussteuerung:

1) ein Befehl zum Organisieren eines Zyklus mit einem Zähler ехх/сх;

2) ein Befehl zum Organisieren eines Zyklus mit einem Zähler ех/сх mit der Möglichkeit des vorzeitigen Verlassens des Zyklus durch eine zusätzliche Bedingung.

Bedingungslose Sprünge

Die vorangegangene Diskussion hat einige Einzelheiten des Übergangsmechanismus enthüllt. Sprungbefehle modifizieren das eip/ip-Befehlszeigerregister und möglicherweise das cs-Codesegmentregister. Was genau geändert werden muss, hängt ab von:

1) vom Typ des Operanden in der unbedingten Verzweigungsanweisung (nah oder fern);

2) vom Spezifizieren eines Modifikators vor der Sprungadresse (in der Sprunganweisung); in diesem Fall kann die Sprungadresse selbst entweder direkt in der Anweisung (direkter Sprung) oder in einem Register oder einer Speicherzelle (indirekter Sprung) liegen.

Der Modifikator kann die folgenden Werte annehmen:

1) near ptr - direkter Übergang zu einem Label innerhalb des aktuellen Codesegments. Nur das eip/ip-Register wird modifiziert (abhängig vom angegebenen use16- oder use32-Codesegmenttyp) basierend auf der im Befehl angegebenen Adresse (Label) oder einem Ausdruck mit dem Wertextraktionssymbol - $;

2) far ptr - direkter Übergang zu einem Label in einem anderen Codesegment. Die Sprungadresse wird als unmittelbarer Operand oder Adresse (Label) angegeben und besteht aus einem 16-Bit-Selektor und einem 16/32-Bit-Offset, die in die cs- bzw. ip/eip-Register geladen werden;

3) Wort ptr – indirekter Übergang zu einem Label innerhalb des aktuellen Codesegments. Nur eip/ip wird geändert (durch den Offset-Wert aus dem Speicher an der im Befehl angegebenen Adresse oder aus einem Register). Offsetgröße 16 oder 32 Bit;

4) dword ptr - indirekter Übergang zu einem Label in einem anderen Codesegment. Beide Register - cs und eip / ip - werden modifiziert (durch einen Wert aus dem Speicher - und nur aus dem Speicher, aus einem Register). Das erste Wort/Doppelwort dieser Adresse stellt den Offset dar und wird in ip/eip geladen; das zweite/dritte Wort wird in cs geladen. jmp unbedingte Sprunganweisung

Die Befehlssyntax für einen unbedingten Sprung ist jmp [modifier] jump_address - ein unbedingter Sprung ohne Speicherung von Informationen über den Rückkehrpunkt.

Jump_address ist die Adresse in Form eines Labels oder die Adresse des Speicherbereichs, in dem sich der Sprungzeiger befindet.

Insgesamt gibt es im Mikroprozessor-Befehlssystem mehrere Codes von Maschinenbefehlen für den unbedingten Sprung jmp.

Ihre Unterschiede werden durch die Übergangsdistanz und die Art und Weise, wie die Zieladresse angegeben wird, bestimmt. Die Sprungweite wird durch die Position des Operanden jump_address bestimmt. Diese Adresse kann sich im aktuellen Codesegment oder in einem anderen Segment befinden. Im ersten Fall wird der Übergang als Intra-Segment oder Close bezeichnet, im zweiten als Inter-Segment oder Distant. Ein Intra-Segment-Sprung setzt voraus, dass nur die Inhalte des eip/ip-Registers geändert werden.

Es gibt drei Optionen für die segmentinterne Verwendung des jmp-Befehls:

1) gerade kurz;

2) gerade;

3) indirekt.

Процедуры

Die Assemblersprache verfügt über mehrere Tools, die das Problem des Duplizierens von Codeabschnitten lösen. Diese beinhalten:

1) Verfahrensmechanismus;

2) Makro-Assembler;

3) Unterbrechungsmechanismus.

Eine Prozedur, oft auch Subroutine genannt, ist die grundlegende Funktionseinheit zum Zerlegen (Aufteilen in mehrere Teile) einer Aufgabe. Eine Prozedur ist eine Gruppe von Befehlen zum Lösen einer bestimmten Teilaufgabe und hat die Möglichkeit, die Kontrolle von dem Punkt zu erhalten, an dem die Aufgabe auf einer höheren Ebene aufgerufen wird, und die Kontrolle an diesen Punkt zurückzugeben.

Im einfachsten Fall kann das Programm aus einer einzigen Prozedur bestehen. Mit anderen Worten, eine Prozedur kann als ein wohlgeformter Satz von Befehlen definiert werden, die nach einmaliger Beschreibung bei Bedarf überall im Programm aufgerufen werden können.

Um eine Befehlsfolge als Prozedur in Assembler zu beschreiben, werden zwei Direktiven verwendet: PROC und ENDP.

Die Prozedurbeschreibungssyntax ist wie folgt (Fig. 36).

Reis. 36. Syntax der Beschreibung der Prozedur im Programm

Abbildung 36 zeigt, dass im Prozedurkopf (PROC-Direktive) nur der Prozedurname obligatorisch ist. Unter der großen Anzahl von Operanden der PROC-Direktive sollte [Abstand] hervorgehoben werden. Dieses Attribut kann die Werte near oder far annehmen und charakterisiert die Möglichkeit, die Prozedur aus einem anderen Codesegment aufzurufen. Standardmäßig ist das Attribut [Distanz] auf „Nähe“ gesetzt.

Die Prozedur kann überall im Programm platziert werden, aber so, dass sie nicht zufällig die Kontrolle bekommt. Wenn die Prozedur einfach in den allgemeinen Befehlsstrom eingefügt wird, nimmt der Mikroprozessor die Anweisungen der Prozedur als Teil dieses Stroms wahr und führt dementsprechend die Anweisungen der Prozedur aus.

Bedingte Sprünge

Der Mikroprozessor hat 18 bedingte Sprungbefehle. Mit diesen Befehlen können Sie Folgendes überprüfen:

1) die Beziehung zwischen Operanden mit einem Vorzeichen ("größer - kleiner");

2) die Beziehung zwischen Operanden ohne Vorzeichen ("höher - niedriger");

3) Zustände der arithmetischen Flags ZF, SF, CF, OF, PF (aber nicht AF).

Bedingte Sprungbefehle haben die gleiche Syntax:

jcc jump_label

Wie Sie sehen können, beginnt der mnemonische Code aller Befehle mit "j" - vom Wort Sprung (springen), es - bestimmt die spezifische Bedingung, die vom Befehl analysiert wird.

Wie beim Operanden jump_label kann sich dieses Label nur innerhalb des aktuellen Codesegments befinden, eine Übertragung der Steuerung zwischen den Segmenten bei bedingten Sprüngen ist nicht erlaubt. Diesbezüglich steht der Modifikator außer Frage, der in der Syntax der unbedingten Sprungbefehle vorhanden war. In frühen Modellen des Mikroprozessors (i8086, i80186 und i80286) konnten bedingte Verzweigungsbefehle nur kurze Sprünge ausführen – von –128 bis +127 Bytes von dem Befehl, der auf den bedingten Verzweigungsbefehl folgte. Beginnend mit dem Mikroprozessormodell 80386 wird diese Einschränkung aufgehoben, aber, wie Sie sehen können, nur innerhalb des aktuellen Codesegments.

Um eine Entscheidung darüber zu treffen, wo die Kontrolle an den bedingten Sprungbefehl übergeben wird, muss zunächst eine Bedingung gebildet werden, auf deren Grundlage die Entscheidung zur Übergabe der Kontrolle getroffen wird.

Quellen für einen solchen Zustand können sein:

1) jeder Befehl, der den Zustand von arithmetischen Flags ändert;

2) die Vergleichsanweisung p, die die Werte zweier Operanden vergleicht;

3) der Zustand des esx/cx-Registers.

cmp-Vergleichsbefehl

Der Seitenvergleichsbefehl hat eine interessante Arbeitsweise. Es ist genau dasselbe wie der Subtraktionsbefehl – ​​Unteroperand, operand_2.

Der p-Befehl subtrahiert wie der sub-Befehl Operanden und setzt Flags. Das einzige, was es nicht tut, ist das Ergebnis der Subtraktion anstelle des ersten Operanden zu schreiben.

Die Befehlssyntax str - str operand_1, operand_2 (compare) - vergleicht zwei Operanden und setzt Flags basierend auf den Ergebnissen des Vergleichs.

Die durch den p-Befehl gesetzten Flags können durch spezielle bedingte Verzweigungsbefehle analysiert werden. Bevor wir sie uns ansehen, wollen wir uns ein wenig mit der Mnemonik dieser bedingten Sprunganweisungen befassen (Tabelle 16). Das Verständnis der Notation bei der Namensbildung der bedingten Sprungbefehle (das Element im Namen des jcc-Befehls haben wir ihm genannt) wird ihr Einprägen und ihre weitere praktische Verwendung erleichtern.

Tabelle 16. Bedeutung der Abkürzungen im jcc-Befehlsnamen. Tabelle 17. Liste der bedingten Sprungbefehle für den Befehl p operand_1, operand_2

Wundern Sie sich nicht über die Tatsache, dass mehrere verschiedene mnemonische Codes von bedingten Verzweigungsbefehlen denselben Flag-Werten entsprechen (sie sind in Tabelle 17 durch einen Schrägstrich voneinander getrennt). Der Unterschied im Namen ist auf den Wunsch der Mikroprozessorentwickler zurückzuführen, die Verwendung von bedingten Sprungbefehlen in Kombination mit bestimmten Befehlsgruppen zu vereinfachen. Daher spiegeln unterschiedliche Namen eher eine unterschiedliche funktionale Ausrichtung wider. Die Tatsache, dass diese Befehle auf dieselben Flags reagieren, macht sie jedoch im Programm absolut gleichwertig und gleich. Daher sind sie in Tabelle 17 nicht nach Namen gruppiert, sondern nach den Werten der Flags (Bedingungen), auf die sie reagieren.

Bedingte Verzweigungsbefehle und Flags

Die mnemonische Bezeichnung einiger bedingter Sprungbefehle spiegelt den Namen des Flags wider, mit dem sie arbeiten, und hat die folgende Struktur: Das erste Zeichen ist "j" (Jump, jump), das zweite ist entweder die Flag-Bezeichnung oder das Negationszeichen " n", gefolgt vom Namen des Flags . Diese Teamstruktur spiegelt seinen Zweck wider. Wenn kein Zeichen "n" vorhanden ist, wird der Zustand des Flags überprüft, wenn es gleich 1 ist, wird zum Übergangsetikett übergegangen. Ist das Zeichen "n" vorhanden, wird der Merkerzustand auf Gleichheit 0 geprüft und bei Erfolg auf die Sprungmarke gesprungen.

Befehlsmnemonik, Flagnamen und Sprungbedingungen sind in Tabelle 18 gezeigt. Diese Befehle können nach allen Befehlen verwendet werden, die die angegebenen Flags modifizieren.

Tabelle 18. Bedingte Sprungbefehle und Flags

Wenn Sie sich die Tabellen 17 und 18 genau ansehen, können Sie sehen, dass viele der darin enthaltenen bedingten Sprungbefehle äquivalent sind, da beide auf der Analyse derselben Flags basieren.

Bedingte Sprungbefehle und das esx/cx-Register

Die Architektur des Mikroprozessors beinhaltet die spezifische Verwendung vieler Register. Beispielsweise wird das EAX / AX / AL-Register als Akkumulator verwendet, und die BP-, SP-Register werden verwendet, um mit dem Stapel zu arbeiten. Das ECX / CX-Register hat auch einen bestimmten funktionalen Zweck: Es fungiert als Zähler bei Schleifensteuerbefehlen und beim Arbeiten mit Zeichenketten. Es ist möglich, dass die bedingte Verzweigungsanweisung, die dem esx/cx-Register zugeordnet ist, funktionell korrekter dieser Gruppe von Anweisungen zugeordnet wird.

Die Syntax für diese bedingte Verzweigung lautet:

1) jcxz jump_label (Sprung, wenn ex Null ist) - springe, wenn cx Null ist;

2) jecxz jump_label (Jump Equal å Zero) – springe, wenn å null ist.

Diese Befehle sind sehr nützlich beim Schleifen und beim Arbeiten mit Zeichenketten.

Es sollte beachtet werden, dass es eine Beschränkung gibt, die dem jcxz/jecxz-Befehl innewohnt. Im Gegensatz zu anderen bedingten Übertragungsanweisungen kann die jcxz/jecxz-Anweisung nur kurze Sprünge von -128 Bytes oder +127 Bytes von der darauf folgenden Anweisung adressieren.

Organisation von Zyklen

Wie Sie wissen, ist der Zyklus eine wichtige algorithmische Struktur, ohne deren Verwendung wahrscheinlich kein Programm auskommt. Die zyklische Ausführung eines bestimmten Programmabschnitts können Sie beispielsweise mit der bedingten Übergabe von Steuerbefehlen oder dem unbedingten Sprungbefehl jmp organisieren. Bei einer solchen Zyklusorganisation werden alle Operationen für ihre Organisation manuell durchgeführt. Angesichts der Bedeutung eines solchen algorithmischen Elements wie eines Zyklus haben die Entwickler des Mikroprozessors jedoch eine Gruppe von drei Befehlen in das Befehlssystem eingeführt, die die Programmierung von Zyklen erleichtern. Diese Anweisungen verwenden auch das esx/cx-Register als Schleifenzähler.

Lassen Sie uns eine kurze Beschreibung dieser Befehle geben:

1) Schleife Transition_Label (Schleife) – Wiederholen Sie den Zyklus. Mit dem Befehl können Sie Schleifen ähnlich wie for-Schleifen in Hochsprachen mit automatischer Dekrementierung des Schleifenzählers organisieren. Die Aufgabe des Teams besteht darin, Folgendes zu tun:

a) Dekrement des ECX/CX-Registers;

b) Vergleichen des ECX/CX-Registers mit Null: wenn (ECX/CX) = 0, dann wird die Steuerung an den nächsten Befehl nach der Schleife übertragen;

2) loope/loopz jump_label

Die Befehle loope und loopz sind absolute Synonyme. Die Arbeit von Befehlen besteht darin, die folgenden Aktionen auszuführen:

a) Dekrement des ECX/CX-Registers;

b) Vergleichen des ECX/CX-Registers mit Null;

c) Analyse des Status des Null-Flags ZF Wenn (ECX/CX) = 0 oder XF = 0, wird die Steuerung nach der Schleife an den nächsten Befehl übergeben.

3) loopne/loopnz jump_label

Auch die Befehle loopne und loopnz sind absolute Synonyme. Die Arbeit von Befehlen besteht darin, die folgenden Aktionen auszuführen:

a) Dekrement des ECX/CX-Registers;

b) Vergleichen des ECX/CX-Registers mit Null;

c) Analyse des Zustands des Null-Flags ZF: Wenn (ECX/CX) = 0 oder ZF = 1, wird die Steuerung nach der Schleife an den nächsten Befehl übergeben.

Die Befehle loope/loopz und loopne/loopnz funktionieren reziprok. Sie erweitern die Wirkung des Schleifenbefehls, indem sie zusätzlich das zf-Flag parsen, was es ermöglicht, ein vorzeitiges Verlassen der Schleife zu organisieren, indem dieses Flag als Indikator verwendet wird.

Die Schleifenbefehle loop, loope/loopz und loopne/loopnz haben den Nachteil, dass sie nur kurze Sprünge (von -128 auf +127 Byte) realisieren. Um mit langen Schleifen zu arbeiten, müssen Sie bedingte Sprünge und die jmp-Anweisung verwenden, versuchen Sie also, beide Arten der Organisation von Schleifen zu beherrschen.

Autor: Tsvetkova A.V.

Wir empfehlen interessante Artikel Abschnitt Vorlesungsunterlagen, Spickzettel:

Krisenmanagement. Krippe

Grundlagen des Managements. Krippe

Ökologie. Vorlesungsnotizen

Siehe andere Artikel Abschnitt Vorlesungsunterlagen, Spickzettel.

Lesen und Schreiben nützlich Kommentare zu diesem Artikel.

<< Zurück

Neueste Nachrichten aus Wissenschaft und Technik, neue Elektronik:

Alkoholgehalt von warmem Bier 07.05.2024

Bier, eines der häufigsten alkoholischen Getränke, hat einen ganz eigenen Geschmack, der sich je nach Temperatur des Konsums verändern kann. Eine neue Studie eines internationalen Wissenschaftlerteams hat herausgefunden, dass die Biertemperatur einen erheblichen Einfluss auf die Wahrnehmung des alkoholischen Geschmacks hat. Die vom Materialwissenschaftler Lei Jiang geleitete Studie ergab, dass Ethanol- und Wassermoleküle bei unterschiedlichen Temperaturen unterschiedliche Arten von Clustern bilden, was sich auf die Wahrnehmung des alkoholischen Geschmacks auswirkt. Bei niedrigen Temperaturen bilden sich eher pyramidenartige Cluster, wodurch die Schärfe des „Ethanol“-Geschmacks abnimmt und das Getränk weniger alkoholisch schmeckt. Im Gegenteil, mit steigender Temperatur werden die Cluster kettenförmiger, was zu einem ausgeprägteren alkoholischen Geschmack führt. Dies erklärt, warum sich der Geschmack einiger alkoholischer Getränke, wie z. B. Baijiu, je nach Temperatur ändern kann. Die Erkenntnisse eröffnen Getränkeherstellern neue Perspektiven, ... >>

Hauptrisikofaktor für Spielsucht 07.05.2024

Computerspiele werden bei Teenagern zu einer immer beliebteren Unterhaltungsform, die damit verbundene Gefahr einer Spielsucht bleibt jedoch ein erhebliches Problem. Amerikanische Wissenschaftler führten eine Studie durch, um die Hauptfaktoren zu ermitteln, die zu dieser Sucht beitragen, und um Empfehlungen für ihre Vorbeugung abzugeben. Über einen Zeitraum von sechs Jahren wurden 385 Teenager beobachtet, um herauszufinden, welche Faktoren sie für eine Spielsucht prädisponieren könnten. Die Ergebnisse zeigten, dass 90 % der Studienteilnehmer nicht von einer Sucht bedroht waren, während 10 % spielsüchtig wurden. Es stellte sich heraus, dass der Schlüsselfaktor für die Entstehung einer Spielsucht ein geringes Maß an prosozialem Verhalten ist. Jugendliche mit einem geringen Maß an prosozialem Verhalten zeigen kein Interesse an der Hilfe und Unterstützung anderer, was zu einem Verlust des Kontakts zur realen Welt und einer zunehmenden Abhängigkeit von der virtuellen Realität durch Computerspiele führen kann. Basierend auf diesen Ergebnissen, Wissenschaftler ... >>

Verkehrslärm verzögert das Wachstum der Küken 06.05.2024

Die Geräusche, die uns in modernen Städten umgeben, werden immer durchdringender. Allerdings denken nur wenige Menschen darüber nach, welche Auswirkungen dieser Lärm auf die Tierwelt hat, insbesondere auf so empfindliche Tiere wie Küken, die noch nicht aus ihren Eiern geschlüpft sind. Aktuelle Forschungsergebnisse bringen Licht in diese Frage und weisen auf schwerwiegende Folgen für ihre Entwicklung und ihr Überleben hin. Wissenschaftler haben herausgefunden, dass der Verkehrslärm bei Zebraküken zu ernsthaften Entwicklungsstörungen führen kann. Experimente haben gezeigt, dass Lärmbelästigung das Schlüpfen der Küken erheblich verzögern kann und die schlüpfenden Küken mit einer Reihe gesundheitsfördernder Probleme konfrontiert sind. Die Forscher fanden außerdem heraus, dass die negativen Auswirkungen der Lärmbelästigung auch auf die erwachsenen Vögel übergreifen. Reduzierte Fortpflanzungschancen und verringerte Fruchtbarkeit weisen auf die langfristigen Auswirkungen von Verkehrslärm auf die Tierwelt hin. Die Studienergebnisse unterstreichen den Bedarf ... >>

Zufällige Neuigkeiten aus dem Archiv

Trockenfrachtschiff für Spezialeinheiten 02.08.2016

Massengutfrachter sind Schiffe, die für die Beförderung von Fest-, Massen- und Stückgütern bestimmt sind. Ein moderner Spezialkämpfer kann durchaus als "verpackte" Fracht eingestuft werden, angesichts seiner Ausrüstung aus Körperschutz, genauer gesagt jetzt einem gepanzerten Anzug, einem Helm, einem Nachtsichtgerät, einem Exoskelett usw. - Behälter, mit einem Wort. Daher ist es angebracht, ein Trockenfrachtschiff als neues Spezialeinheiten-Marinefahrzeug mit dem Namen Dry Combat Submersible - "Dry Combat Submarine" zu bezeichnen.

"Trockene" Mini-U-Boote unterscheiden sich von "nassen" darin, dass sich bei letzteren die Jäger außerhalb des Bootes befinden, d.h. im Wasser und verwenden zusätzlich zu den eigenen Lufttanks Druckluft aus dem U-Boot. Trockene Mini-U-Boote mit Jägern im Inneren sollten 2003 bei den US Navy SEALs in Dienst gestellt werden, aber nur eines der geplanten sechs wurde hergestellt, und dann wurde der Vertrag mit Northrop Grumman wegen Mängeln gekündigt. Jetzt hat ein weiterer Gigant des militärisch-industriellen Komplexes, Lockheed Martin, vom Kommando der US Special Forces einen Regierungsauftrag in Höhe von 166 Millionen Dollar erhalten, um bestehende „nasse“ Mini-U-Boote zu ersetzen.

Der Vertrag sieht den Bau von drei "trockenen" U-Booten vor. In Übereinstimmung mit dem Ziel des Pentagon, Kosteneinsparungen durch die Nutzung bestehender Lösungen zu erzielen, nahmen die Konstrukteure von Lockheed Martin das kommerzielle Modell S302 mit einer Länge von 9,36 m, einer Breite von 2,34 m und einer Höhe von 2,34 m. Das neue U-Boot wird Platz für zwei Piloten und sechs Jäger bieten. Die maximale Tauchtiefe beträgt 100 m, die Austrittstiefe der Jäger aus dem Boot 30 m, die Höchstgeschwindigkeit 5 Knoten. Laut Lockheed Martin wird das neue Fahrzeug über verbesserte Hydrodynamik und Fahrwerk verfügen.

Weitere interessante Neuigkeiten:

▪ Smarte Nike-Sneaker werden von Ihrem Smartphone aus gesteuert

▪ Nerven stoppen die Blutung

▪ Beschleuniger GeForce GTX 970 EXOC Sniper Edition

▪ Neuer Miniatur-PWM-Controller

▪ UCC28780 Flyback-Controller mit Nullspannungsschaltung

News-Feed von Wissenschaft und Technologie, neue Elektronik

 

Interessante Materialien der Freien Technischen Bibliothek:

▪ Website-Abschnitt Niederfrequenzverstärker. Artikelauswahl

▪ Artikel Irren ist menschlich. Populärer Ausdruck

▪ Artikel Wie lernten die Ägypter, Mumien aufzubewahren? Ausführliche Antwort

▪ Artikel Biomasse-Separator. Jobbeschreibung

▪ Artikel BALUN oder nicht BALUN? Enzyklopädie der Funkelektronik und Elektrotechnik

▪ Artikel Heftklammern, die in einem Umschlag verbunden werden. Fokusgeheimnis

Hinterlasse deinen Kommentar zu diesem Artikel:

Name:


E-Mail optional):


Kommentar:





Alle Sprachen dieser Seite

Startseite | Bibliothek | Artikel | Sitemap | Site-Überprüfungen

www.diagramm.com.ua

www.diagramm.com.ua
2000-2024