Einführung in die Informatik 2 Benjamin Gufler Erstellt mit LATEX II Inhaltsverzeichnis II Rechnerstruktur, Hardware, Maschinennahe Programmierung 1 1 Codierung / Informationstheorie 1.1 Codes / Codierung . . . . . . . . . . . 1.1.1 Binärcodes einheitlicher Länge 1.1.2 Codes variabler Länge . . . . . 1.1.3 Serien-/ Parallelwortcodierung 1.1.4 Codebäume . . . . . . . . . . . 1.2 Codes und Entscheidungsinformation . 1.3 Sicherung von Nachrichtenübertragung 1.3.1 Codesicherung . . . . . . . . . 1.3.2 Übertragungssicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 6 7 7 8 9 9 10 2 Binäre Schaltnetze und Schaltwerke 2.1 Boolesche Algebra / Boolesche Funktionen . . . . . . . . 2.1.1 Boolesche Funktionen . . . . . . . . . . . . . . . 2.1.2 Partielle Ordnung auf BF . . . . . . . . . . . . . 2.2 Normalformen boolescher Funktionen . . . . . . . . . . . 2.2.1 Das boolesche Normalformtheorem . . . . . . . . 2.2.2 Vereinfachte Normalformen (DNF) . . . . . . . . 2.3 Schaltnetze . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.1 Schaltfunktionen und Schaltnetze . . . . . . . . . 2.3.2 Darstellung von Schaltnetzen . . . . . . . . . . . 2.3.3 Halbaddierer . . . . . . . . . . . . . . . . . . . . 2.3.4 Arithmetische Schaltnetze . . . . . . . . . . . . . 2.3.5 Zahldarstellung . . . . . . . . . . . . . . . . . . . 2.3.6 Weitere arithmetische Operationen . . . . . . . . 2.3.7 Schaltnetze zur Übertragung von Information . . 2.4 Schaltwerke . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Schaltwerksfunktionen . . . . . . . . . . . . . . . 2.4.2 Schaltfunktionen als Schaltwerksfunktionen . . . 2.4.3 Schaltwerke . . . . . . . . . . . . . . . . . . . . . 2.4.4 Schaltwerksfunktionen und endliche Automaten . 2.4.5 Schaltwerke zum Speichern: Verzögerungsnetze . 2.4.6 Klassen von Schaltwerken . . . . . . . . . . . . . 2.4.7 Komposition von Schaltwerken und Schaltnetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 11 13 13 13 14 15 15 16 18 20 21 24 24 25 25 26 26 26 27 28 29 3 Aufbau von Rechenanlagen 3.1 Strukturierter Aufbau von 3.1.1 Der Rechnerkern . 3.1.2 Speichereinheit . . 3.1.3 E/A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 33 33 33 . . . . . . . . . . . . . . . . . . Rechenanlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . III . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IV INHALTSVERZEICHNIS . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 35 37 38 4 Maschinennahe Programmierung 4.1 Maschinennahe Programmiersprachen . . . . . . . . . . . 4.1.1 Binärwörter als Befehle . . . . . . . . . . . . . . . 4.1.2 Der Befehlsvorrat der MI . . . . . . . . . . . . . . 4.1.3 Einfache Maschinenprogramme . . . . . . . . . . . 4.1.4 Assemblersprachen . . . . . . . . . . . . . . . . . . 4.1.5 Ein- und Mehradressform . . . . . . . . . . . . . . 4.1.6 Unterprogrammtechniken . . . . . . . . . . . . . . 4.2 Adressiertechniken und Speicherverwaltung . . . . . . . . 4.2.1 Konstante . . . . . . . . . . . . . . . . . . . . . . . 4.2.2 Operandenversorgung über Register . . . . . . . . 4.2.3 Absolute Adressierung . . . . . . . . . . . . . . . . 4.2.4 Relative Adressierung . . . . . . . . . . . . . . . . 4.2.5 Indizierung, Zugriff auf Felder . . . . . . . . . . . . 4.2.6 Symbolische Adressierung . . . . . . . . . . . . . . 4.2.7 Geflechtsstrukturen und indirekte Adressierung . . 4.2.8 Speicherverwaltung . . . . . . . . . . . . . . . . . . 4.2.9 Stackverwaltung von blockstrukturierten Sprachen 4.3 Techniken maschinennaher Programmierung . . . . . . . . 4.3.1 Auswertung von Ausdrücken / Termen . . . . . . . 4.3.2 Maschinennahe Realisierung von Ablaufstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 39 40 40 41 43 44 45 47 47 48 48 48 48 49 50 52 52 54 55 56 3.2 3.1.4 Befehle und Daten auf Maschinenebene . . . 3.1.5 Operandenspezifikation und Adressrechnung . 3.1.6 Der Befehlszyklus . . . . . . . . . . . . . . . Hardwarekomponenten . . . . . . . . . . . . . . . . . . . . . . . . . Teil II Rechnerstruktur, Hardware, Maschinennahe Programmierung 1 3 Stichworte: • Zeichenweise Darstellung von Information → Codierung • Schaltungen / Schaltwerke - Schaltlogik • Rechnerstrukturen: Aufbau von Rechneranlagen • Maschinennahe Programmierung 4 Kapitel 1 Codierung / Informationstheorie In der Codierung studieren wir die Darstellung von Information durch Zeichenfolgen (vgl. formale Sprache). 1.1 Codes / Codierung Wir konzentrieren uns stark auf eine Darstellung durch zwei Zeichen (Bits - Binary Digits). B = {L, 0} Bn Menge der Binärwörter der Länge n (n - Bit - Wörter) Sei A ein Alphabet (linear geordneter endlicher Zeichensatz), B ein Alphabet. c : A → B Code / Codierung c : A∗ → B ∗ Wortcodierung Wichtig auf einer Codierung ist die Umkehrbarkeit: d : {c(a) : c ∈ A} → A mit d(c(a)) = a. Voraussetzung: c injektiv. Binärcodierung: c : A → B∗ Achtung: B∗ ist lexikographisch linear geordnet (0 < L). 1.1.1 Binärcodes einheitlicher Länge Wir betrachten nun für gegebenes n ∈ N die Abbildung c : A → Bn . Achtung: Wir setzen voraus, dass c injektiv ist, aber nicht notwendigerweise surjektiv. Für zwei Codewörter aus Bn können wir nach dem Abstand fragen, wir sprechen von Hamming-Abstand: hd : Bn timesBn → N0 n X hd(ha1 , . . . , an i , hb1 , . . . , bn i) = d(ai , bi ) i=1 5 6 KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE wobei für x, y ∈ B gilt ( 1 d(x, y) = 0 falls x 6= y falls x = y Auf dieser Basis definieren wir einen Hammingabstand für einen Binärcode c: hd(c) = min {hd(c(a), c(b)) : a, b ∈ A ∧ a 6= b} hd(c) = 0 c nicht injektiv. Um den Hammingabstand hoch zu halten, benutzt man Paritätsbits. Beispiel Gegeben sei eine Codierung c : A → Bn mit hd(c) ≥ 1. Gesucht ist eine Codierung c0 : A → Bn+1 mit hd(c) ≥ 2. Wir definieren: ( L falls die Quersumme von b gerade ist pb : Bn → B, pb(b) = 0 sonst und für a ∈ A c0 (a) = c(a) ◦ hpb(c(a))i Behauptung: hd(c0 ) ≥ 2 Beweis: Seien a, a0 ∈ A, a 6= a0 gegeben. 1. Fall: hdc (a, a0 ) ≥ 2 ⇒ hdc0 (a, a0 ) ≥ 2 2. Fall: hdc (a, a0 ) = 1. Dann gilt pb(a) 6= pb(a0 ), da die Quersumme von a gerade ist, wenn die Quersumme von a0 ungerade ist und umgekehrt. Also ist hdc0 (a, a0 ) = 2. Wichtiges Thema der Codierung: Kryptographie: Verschlüsselung von Nachrichten. Weiteres Thema: Codierung der Ziffern. Direkter Code: c : {0, . . . , 9} → B4 c(z) 1-aus-10-Code z 0 0000 000000000L 1 000L 00000000L0 2 00L0 0000000L00 3 00LL 000000L000 4 0L00 00000L0000 0000L00000 5 0L0L 6 0LL0 000L000000 7 0LLL 00L0000000 8 L000 0L00000000 9 L00L L000000000 1.1.2 Codes variabler Länge Wir betrachten nun Codes, bei denen die Codewörter unterschiedliche Längen besitzen. Beispiele • altes Fernsprechwählsystem: 1 7→ L0 2 7→ LL0 .. . 0 7→ LLLLLLLLLL0 • Morsecode 1.1. CODES / CODIERUNG 1.1.3 7 Serien-/ Parallelwortcodierung Bei der Übertragung von binär codierter Information unterscheiden wir zwei Situationen: • ein Draht / sequentielle Übertragung • n Drähte (n > 1) / parallele Übertragung Bei Übertragung eines Wortes w ∈ A∗ mit Binärcodierung c : A → B∗ einfachstes Vorgehen: c∗ : A∗ → B∗ c∗ (ha1 , . . . , an i) = c(a1 ) ◦ c(a2 ) ◦ · · · ◦ c(an ) Kritische Frage: Ist c∗ injektiv, wenn c injektiv ist? Nein! Gegenbeispiel: Seien a1 , a2 ∈ A mit a1 6= a2 , c(a1 ) =< L > und c(a2 ) =< LL >. Dann ist c∗ (< a1 a1 a1 >) =< LLL >= c∗ (< a1 a2 >). Frage: Unter welchen Voraussetzungen ist die Antwort ja? Antwort: 1. Codes gleicher Länge 2. die Fano-Bedingung gilt Fano-Bedingung von Coole: Kein Codewort c(a1 ) für a1 ∈ A ist Präfix eines Codeworts c(a2 ) für a2 ∈ A mit a1 6= a2 . Behauptung: Gilt für c die Fano-Bedingung, so ist c∗ injektiv. Beweis (durch Widerspruch): Seien a = ha1 , . . . , an i , b = hb1 , . . . , bm i ∈ A∗ , a 6= b und c∗ (a) = c∗ (b). O.B.d.A. sei n < m. Sei i so gewählt, dass ak = bk für 1 ≤ k ≤ i und n = i oder ai+1 6= bi+1 . 1. Fall: ai+1 6= bi+1 ; dann gilt c(ai+1 ) Präfix von c(bi+1 ) oder c(bi+1 ) Präfix von c(ai+1 ). Widerspruch zur Fano-Bedingung. 2. Fall: n = i; dann gilt (wegen m > n) c∗ (hbi+1 , . . . , bm i) = ε. Widerspruch zur Fano-Bedingung. Parallelwortcodierung: c : A → Bn ck∗ : A∗ → (Bn )∗ ck∗ (ha1 , . . . , ak i) = hc(a1 )i ◦ · · · ◦ hc(ak )i Beispiel L 0 L 0 0 0 L L 0 0 L 0 L 0 L 0 0 0 L L 0 0 L 0 Trivial: ck∗ injektiv, falls c injektiv. 1.1.4 Codebäume Codes lassen sich durch Tabellen oder durch Codebäume (s. Abb. (1.1)) darstellen. B C D A L 0LL 0L0 00 Trivialerweise ist für Codes, dargestellt durch Codebäume, stets die Fano-Bedingung gegeben. 8 KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE L 0 A L 0 D L 0 B C Abbildung 1.1: Codebaum 1.2 Codes und Entscheidungsinformation Kernfrage: Wie viel Information trägt eine Nachricht? Prinzip: Um so ungewöhnlicher (seltener) eine Information ist, desto mehr Informationsgehalt hat sie. Dafür setzen wir gegebenenfalls Wahrscheinlichkeiten für Nachrichten voraus. Eine stochastische Nachrichtenquelle für ein endliches Alphabet A erzeugt eine Folge von Zeichen aus A, bei der zu jedem Zeitpunkt die Wahrscheinlichkeiten für jedes Zeichen einer vorgegebenen zeitunabhängigen Wahrscheinlichkeitsverteilung entsprechen. Beobachtung: Seltene Zeichen tragen viel Information, häufige Zeichen wenig. P Gegeben: A Alphabet, p : A → [0, 1] Wahrscheinlichkeiten (d.h. a∈A p(a) = 1 und p(a) > 0∀a ∈ A). Mittlerer Entscheidungsgehalt (Entropie): X 1 H= p(a) ld p(a) a∈A Entropie: Maß für Gleichverteilung der Wahrscheinlichkeiten: H groß ⇔ Wahrscheinlichkeiten etwa gleichverteilt, H klein ⇔ starke Unterschiede in den Wahrscheinlichkeiten. H Zeichen i p(i) 1 a 2 1 1 b Beispiel 2 3 a 4 ≈ 0, 8 1 b 4 Seien für die Zeichen aus A Wahrscheinlichkeiten p : A → [0, 1] sowie eine Binärcodierung c : A → B gegeben. Wir können die mittlere Wortlänge L berechnen als X L= p(a)|c(a)| a∈A wobei |c(a)| die Länge von c(a) bezeichnet. Um L klein zu halten, codieren wir Zeichen mit großen Wahrscheinlichkeiten mit kurzen Codewörtern. Größere Spielräume erhalten wir, wenn wir nicht Einzelzeichen, sondern Wörter aus A∗ codieren. → Huffman-Algorithmus P Jede Menge M ⊆ A von Zeichen hat auch eine Wahrscheinlichkeit p(M ) = a∈M p(a). Es ist p(A) = 1. Ziel: A so in zwei disjunkte Teilmengen A0 und AL aufteilen, dass p(A0 ) ≈ 12 ≈ p(AL ) ist. 1.3. SICHERUNG VON NACHRICHTENÜBERTRAGUNG Beispiel i a b p(i) 3 4 1 4 i aa ab ba bb 9 p(i) L= 9 16 3 16 3 16 1 16 1 X p(w)|c(w)| m m w∈A Shannonsches Codierungstheorem: 1. Für beliebige Wortcodes gilt H ≤ L. 2. Der Wert L − H kann durch geschickte Wahl der Wortcodes beliebig klein gemacht werden. Redundanz eines Codes: L − H Relative Redundanz eines Codes: 1 − H L Gesetz von Merkel: Die Reaktionszeit t einer Versuchsperson, um aus n Gegenständen einen bestimmten auszuwählen, lässt sich berechen durch t = 200 + 180 ld(n)[msec] 1.3 Sicherung von Nachrichtenübertragung Beim Übertragen von Nachrichten sind zwei Aspekte von besonderer Bedeutung: • Kapazität: Wie viele Informationseinheiten pro Zeiteinheit können übertragen werden? • Störung: Wie hoch ist die Wahrscheinlichkeit, dass Daten fehlerhaft übertragen werden? 1.3.1 Codesicherung Diskreter Kanal ohne Speicher: Überträgt Binärwerte, wobei Störungen auftreten können, deren Wahrscheinlichkeiten zu jedem Zeitpunkt gleich sind (vgl. stochastische Nachrichtenquelle). Irrtumswahrscheinlichkeiten: p0 Wahrscheinlichkeit, dass das Zeichen 0 als L übertragen wird pL Wahrscheinlichkeit, dass das Zeichen L als 0 übertragen wird einseitige Störung: (p0 = 0 ∧ pL > 0) ∨ (p0 > 0 ∧ pL = 0) symmetrische Störung: p0 = pL In manchen Fällen führt eine Störung nicht zu einer Veränderung L zu 0 oder 0 zu L, sondern führt auf ein Zeichen, das als fehlerhaft erkannt werden kann (Verlustzeichen). Wir erhalten eine Übertragung von Zeichen aus {0, L} in Zeichen aus {0, L, ⊥} (⊥: Fehlerzeichen). Ziel bei Kanälen mit Übertragungsfehlern: Die Wahrscheinlichkeit für Fehler soll möglichst klein gehalten werden, d.h. Fehler sollen erkannt und korrigiert werden (durch Redundanz). Lemma: Hat ein Code Hammingabstand h, so können Störungen, die weniger als h Zeichen betreffen, sicher erkannt werden. Idee: Prüf- / Paritätsbits einsetzen 10 1.3.2 KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE Übertragungssicherheit Kanäle haben beschränkte Übertrangugskapazität: s1 : Anzahl der Zeichen, die ein Kanal pro Zeiteinheit überträgt s0 : Anzahl der Zeichen, die pro Zeiteinheit übertragen werden sollen R = ss01 : Senderate R < 1: Überkapazität, d.h. Codesicherung durch Redundanz möglich R > 1: Unterkapazität: Nachricht kann nur in Teilen übertragen werden, kommt gestört an. Beispiel R = 31 → wir übertragen statt der Einzelzeichen jedes Zeichen drei Mal hintereinander. Bei Störungen mit pE < 0, 5 decodieren wir wie folgt: 000, 00L, 0L0, L00 → 0 Die Fehlerrate sei p für Einzelzeichenübertragung; dann ist 3p2 − 2p3 die Wahrscheinlichkeit einer Störung bei Dreifachübertragung 14 Liegt die Übertragungsrate bei 16 , dann können wir 14-Bit-Wörter als 16-BitWörter mit 2 Paritätsbits übertragen. Falls R ≥ 1 ist, reicht die Kapazität nicht aus. Trotzdem können wir Nachrichten (gestört) übertragen. Beispiel R = 3, d.h. wir sollen drei Mal so viel Nachrichten übertragen, als möglich ist. Idee: Drei Bits werden auf ein Bit komprimiert (Mehrheitstechnik. s.o.) und beim Empfang wieder in drei gleiche Bits umgewandelt. Irrtumswahrscheinlichkeit: (R > 1) pE = p R−1 + R 2R Kapitel 2 Binäre Schaltnetze und Schaltwerke Zur Darstellung diskreter (symbolischer) Information reichen zwei Werte (Bits) aus. Zur Darstellung technischer Art bits es viele Möglichkeiten: • elektrische: – Spannung / keine Spannung – Strom / kein Strom – Licht / kein Licht • mechanische: – Wasser / kein Wasser – Dampf / kein Dampf Wir betrachten im Weiteren nicht die technische Darstellung von Bits, sondern studieren, wie wir bestimmte Information (Zahlen, Datenstrukturen) durch Bitsequenzen darstellen können und wir wir Verarbeitungsvorgänge durch boolesche Funktionen realisieren können. 2.1 Boolesche Algebra / Boolesche Funktionen Die Wahrheitswerte bilden mit den Operationen ¬, ∨ und ∧ eine boolesche Algebra. Prädikate bilden ebenfalls eine boolesche Algebra: Statt einer Prädikatsdarstellung p : M → B können wir stets eine Mengendarstellung S ⊆ M verwenden. Jedes Prädikat über M definiert genau eine Teilmenge von M und umgekehrt. Sp = {x ∈ M : p(x)}: p(x) = (x ⊂ Sp) Allgemein gilt: ∧ ∼ ∩, ∨ ∼ ∪, ¬ ∼ Komplement und ⇒∼⊆. 2.1.1 Boolesche Funktionen n-stellige boolesche Funktion: f : Bn → B Beispiel: 1. 0-stellige boolesche Funktionen: Bn = {ε} Es gibt genau zwei 0-stellige boolesche Funktionen: true, false. 11 12 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE 2. 1-stellige boolesche Funktionen: f : B → B true false x ¬x konstant L konstant 0 Identität Negation 3. 2-stellige boolesche Funktionen: f : B2 → B true false x y ¬x ¬y x∨y x∧y x ∨ ¬y ¬x ∨ y Konstanten (x ∧ y) ∨ (¬x ∧ ¬y) (x ∧ ¬y) ∨ (¬x ∧ y) ¬(x ∨ y) ¬(x ∧ y) x ∧ ¬y ¬x ∧ y Äquivalenz Antivalenz nor nand Bisubtraktion Projektionen negative Projektionen Disjunktion Konjunktion Inverse Implikation, Subjunktion Implikation 4. 3-stellige boolesche Funktionen: f : B3 → B ... n Anzahl der n-stelligen booleschen Funktionen 2(2 ) Für jedes n ∈ N bilden die n-stelligen booleschen Funktionen eine boolesche Algebra. BFn : Menge der booleschen Funktionen der Stelligkeit n. Seien f, g ∈ BFn . Dann sind (¬f ), (f ∧ g) ∈ BFn und es gilt: • (¬f )(x1 , . . . , xn ) = ¬f (x1 , . . . , xn ) • (f ∧ g)(x1 , . . . , xn ) = f (x1 , . . . , xn ) ∧ g(x1 , . . . , xn ) • (f ∨ g)(x1 , . . . , xn ) = f (x1 , . . . , xn ) ∨ g(x1 , . . . , xn ) Außerdem ist (f ∧ ¬f ), (f ∨ ¬f ) ∈ BFn . Wie lassen sich boolesche Funktionen darstellen? 1. Durch Formeln oder Aussagenlogik: Sei f ∈ BFn . Dann können wir f (x1 , . . . , xn ) durch die aussagenlogischen Terme über den Identifikatoren x1 , . . . , xn (z.B.: f (x1 , x2 , x3 ) = x1 ∨(¬x1 ∧x2 )) darstellen. 2.2. NORMALFORMEN BOOLESCHER FUNKTIONEN x1 x2 2. Durch Tabellen: x3 f (x1 , x2 , x3 ) 0 0 0 0 0 0 L 0 0 L 0 L 0 L L L L 0 0 L 13 L 0 L L L L 0 L L L L L 3. Entscheidungsdiagramme (Bäume) 2.1.2 Partielle Ordnung auf BF Für eine beliebige boolesche Algebra definieren wir eine partielle Ordnung f ≥ g durch f ≥ g ⇔def f = f ∧ g. Aus den Gesetzen der booleschen Algebra lässt sich nachweisen, dass ≥ eine partielle Ordnung ist. f ≥ g ist gleichbedeutend mit f ⇒ g. f ∨ ¬f ist kleinstes Element: true (schwächste Aussage). f ∧ ¬f ist größtes Element: false (stärkste Aussage). 2.2 Normalformen boolescher Funktionen Eine boolesche Funktion kann immer durch boolesche Terme dargestellt werden. Es existieren viele syntaktisch verschiedene Terme, die die gleiche Funktion darstellen: (¬x1 ∧ ¬x2 ) ∨ (x1 ∧ x2 ) ≡ (x1 ⇒ x2 ) ∧ (x2 ⇒ x1 ) ≡ (¬x1 ∨ x2 ) ∧ (¬x2 ∨ x1 ) Wir behandeln im Weiteren folgende Fragen: 1. Gibt es einheitliche Termdarstellungen für BF (Normalformen)? 2. Wie können wir aus einer Tabellen- oder Entscheidungsbaumdarstellung eine Termdarstellung bekommen? 3. Wir können wir besonders einfache und kurze Termdarstellungen bekommen? Diese Fragen sind auch für die technische Realisierung von booleschen Funktionen durch Schaltungen entscheidend, da Termdarstellungen direkt in Schaltungen umgesetzt werden können. 2.2.1 Das boolesche Normalformtheorem Ziel: Wir wollen für beliebige boolesche Funktionen eine Termnormalform definieren und zeigen, wie diese aus einer Tabelle erzeugt werden kann. Wir definieren eine disjunktive Normalform (DNF): hdefi ::= hkfi {∨ hkfi}∗ |0|L hkfi ::= hliterali {∧ hliterali}∗ hliterali ::= {¬} hidi Beispiel (x1 ∧ ¬x2 ∧ x3 ∧ ¬x4 ) ∨ (¬x1 ∧ x2 ∧ x3 ∧ x4 ) ∨ (∧x1 ∧ ¬x2 ∧ x3 ∧ ¬x4 ) Als Identifikatoren wählen wir x1 , . . . , xn . In der vollständigen DNF kommt in jedem Literal jeder Identifikator genau ein Mal vor. Hilfskonstruktion: Seien b1 , . . . , bn ∈ B. Wir definieren einen Term minterm(b1 , . . . , bn ) = (v1 ∧ · · · ∧ vn ) wobei ( xi vi = ¬xi falls bi = L falls bi = 0 14 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE Beispiel minterm(0, L, L, 0, L) = (¬x1 ∧ x2 ∧ x3 ∧ ¬x4 ∧ x5 ) Theorem: Für f ∈ BFn gilt: _ f (x1 , . . . , xn ) = (f (b1 , . . . , bn ) ∧ minterm (b1 , . . . , bn )) b1 ,...,bn ∈B Beweis: durch Induktion über n. Beispiel x1 x2 x3 f (x1 , x2 , x3 ) L L L L L L 0 L L 0 L 0 L 0 0 0 0 L L 0 0 L 0 L 0 0 L 0 0 0 0 0 f (x1 , x2 , x3 ) =(L ∧ x1 ∧ x2 ∧ x3 ) ∨ (L ∧ x1 ∧ x2 ∧ ¬x3 ) ∨ (0 ∧ x1 ∧ ¬x2 ∧ x3 ) ∨ (0 ∧ x1 ∧ ¬x2 ∧ ¬x3 ) ∨ (0 ∧ ¬x1 ∧ x3 ∧ x3 ) ∨ (L ∧ ¬x1 ∧ x2 ∧ ¬x3 ) ∨ (0 ∧ ¬x1 ∧ ¬x2 ∧ x3 ) ∨ (0 ∧ ¬x1 ∧ ¬x2 ∧ ¬x3 ) =(x1 ∧ x2 ∧ x3 ) ∨ (x1 ∧ x2 ∧ ¬x3 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 ) Analog zur disjunktiven gibt es die konjunktive Normalform (KNF); die Rollen von und und oder sind dort vertauscht. Die DNF beantwortet die am Eingang gestellten Fragen. Jede boolesche Funktion hat eine eindeutige vollständige DNF. 2.2.2 Vereinfachte Normalformen (DNF) Die vollständige DNF lässt sich oft weiter vereinfachen. Regeln zur Vereinfachung . . . ∨ (t1 ∧ · · · ∧ ti−1 ∧ xi ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . . ∨(t1 ∧ · · · ∧ ti−1 ∧ ¬xi ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . . ⇒ · · · ∨ (t1 ∧ · · · ∧ ti−1 ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . . Beispiel (x1 ∧ x2 ∧ x3 ) ∨ (x1 ∧ x2 ∧ ¬x3 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 ) =(x1 ∧ x2 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 ) Nun können Terme folgender Gestalt auftreten: . . . ∨ (t1 ∧ t2 ) ∨ · · · ∨ (t1 ) ∨ . . . . Diese Terme können ersetzt werden durch . . . ∨ (t1 ) ∨ . . . . Durch diese Regeln kann von der vollständigen DNF zur vereinfachten DNF übergegangen werden. Binäre Entscheidungsdiagramme Da die Identifikatoren geordnet sind (x1 , . . . , xn ) können wir Entscheidungsbäume für boolesche Funktionen konstruieren. • n = 0, d.h. f ∈ BF0 ; der Entscheidungsbaum ist ein Blatt b ∈ B, und zwar L falls f (ε) = L 0 falls f (ε) = 0 2.3. SCHALTNETZE 15 Abbildung 2.1: Beispiel eines Schaltplans • n > 0: Wir definieren den Entscheidungsbaum wie folgt: 0 L EBL EB0 wobei EBL der Entscheidungsbaum der booleschen Funktion fL ∈ BFn−1 mit fL (x1 , . . . , xn−1 = f (L, x1 , . . . , xn−1 ) und EB0 der Entscheidungsbaum zur booleschen Funktion f0 ∈ BFn−1 mit f0 (x1 , . . . , xn−1 ) = f (0, x1 , . . . , xn−1 ) seien. 2.3 Schaltnetze Zur Realisierung von boolschen Funktionen (wir sprechen ab jetzt von Schaltfunktionen) können Schaltnetze verwendet werden. Schaltnetze entsprechen gerichteten, azyklischen Graphen mit Eingängen und Ausgängen. Definition: Ein Schaltnetz mit n Eingängen und m Ausgängen ist ein gerichteter, azyklischer Graph mit n Eingangskanten und m Ausgangskanten, dessen Knoten mit den Namen boolscher Funktionen markiert sind. Schaltnetze sind mit boolschen Termen eng verwandt. Jeder boolsche Term lässt sich als ein Schaltnetz darstellen. Die so entstehenden Schaltnetze haben jedoch die Eigentümlichkeit, dass jede Kante genau ein Ziel hat. Deshalb führen wir im Folgenden neben boolschen Termen eine Funktionaltermdarstellung für boolsche Funktionen ein, durch die beliebige Schaltnetze mit Kanten mit Mehrfachziel (Verzweigung) dargestellt werden können. Im Folgenden behandeln wir eine Reihe boolscher Funktionen, ihre Darstellung durch boolsche Terme und durch Funktionsterme (funktionale Terme). Funktionsterme sind durch Verknüpfungen aus Grundfunktionen aufgebaut. Die Verknüpfungen entsprechen der graphischen Darstellung durch Schaltnetze. Der Schaltplan, gegeben durch das Schaltnetz, kann direkt in eine technische Realisierung umgesetzt werden. 2.3.1 Schaltfunktionen und Schaltnetze Schaltfunktion: f : Bn → Nm , m, n ∈ N. Schaltglied: (s. Abb. 2.2) Ein Schaltnetz besteht aus einer Menge von Schaltgliedern, die über Kanten verbunden sind. Es entsteht ein gerichteter Graph. In Schaltnetzen sind keine Zyklen zugelasen. Jede Kante hat genau eine Quelle, unter Umständen aber mehrere Ziele, d.h. Kanten können sich verzweigen. 16 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE ...n... f ...m... Abbildung 2.2: Schaltglied ...n... & Abbildung 2.3: Konjunktions-Schaltglieder • Konjunktion: logisches • Disjunktion: logisches und (s. Abb. 2.3) oder (s. Abb. 2.4) • Negation: (s. Abb. 2.5) Sei ein Schaltnetz mit n Eingängen und m Ausgängen gegeben. Wird für jedes Schaltglied des Netzes eine Schaltfunktion angegeben, so definiert das Schaltnetz selbst wieder eine Schaltfunktion aus SFm n . In einem Schaltnetz entspricht • jedes Schaltglied einer Schaltfunktion • jede Kante einem Wahrheitswert 2.3.2 Darstellung von Schaltnetzen Es gibt zwei grundlegende Möglichkeiten, Schaltnetze durch Formeln darzustellen: 1. benannte Kanten 2. Kombination (Komposition) von Schaltgliedern Benannte Kanten: Jeder Knoten in einem Netz (vgl. Abb. 2.6) entspricht einer Gleichung (y1 , . . . , ym ) = f (x1 , . . . , xn ). Dieses Darstellungsverfahren eignet sich gut für kleine Schaltnetze ohne Regelmäßigkeiten. Besser für große Schaltnetze mit starken Regelmäßigkeiten eignen sich kombinatorische Schreibweisen. ...n... >= 1 Abbildung 2.4: Disjunktions-Schaltglieder 2.3. SCHALTNETZE 17 1 Abbildung 2.5: Negations-Schaltglieder x1 ... xn f y 1 ... y m Abbildung 2.6: Benannte Kanten m2 1 1. Parallele Komposition (s. Abb. 2.7): Seien f1 ∈ SFm n1 und f2 ∈ SFn2 . Dann ist m1 +m2 f = (f1 k f2 ) ∈ SFn1 +n2 , und es gilt f (x1 , . . . , xn1 +n2 ) = (y1 , . . . , ym1 +m2 ), wobei f1 (x1 , . . . , xn1 ) = (y1 , . . . , ym1 ) f2 (xn1 +1 , . . . , xn1 +n2 ) = (ym1 +1 , . . . , ym1 +m2 ) 0 m 2. Sequentielle Komposition (s. Abb. 2.8): Seien f1 ∈ SFm n , f2 ∈ SFm . Es ist m0 f = (f1 ◦ f2 ) ∈ SFn mit f (x1 , . . . , xn ) = f2 (f1 (x1 , . . . , xn )). Eigenschaften: k, ◦ sind assoziativ, aber nicht kommutativ. Zusatzfunktionen • Projektion πin ∈ SF1n 1≤i≤n n πi (x1 , . . . , xn ) = xi • Identität I ∈ SF11 In ∈ SFnn I(x) = x In (x1 , . . . , xn ) = (x1 , . . . , xn ) f ... ... f1 f2 ... ... Abbildung 2.7: Parallele Komposition 18 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE n ... f f1 m ... f2 ... m’ Abbildung 2.8: Sequentielle Komposition • Verzweigung V (x) = (x, x) V ∈ SF21 Vn ∈ SF2n n Vn (x1 , . . . , xn ) = (x1 , . . . , xn , x1 , . . . , xn ) • Permutation P ∈ SF22 Pn ∈ SFnn P (x1 , x2 ) = (x2 , x1 ) Pn (x1 , . . . , xn ) = (x2 , . . . , xn , x1 ) • Tupelung m2 1 f1 ∈ SFm n , f2 ∈ SFn 2 [f1 , f2 ] ∈ SFm−1+m n [f1 , f2 ] = (Vn ◦ (f1 k f2 )) • Senken U ∈ SF01 Un ∈ SF0n • Konstanten K(0), K(L) ∈ SF10 2.3.3 Der a b s u Kn (0), Kn (L) ∈ SFn0 Halbaddierer Halbaddierer ist ein Schaltglied HA ∈ SF22 ; HA(a, b) = (u, s). 0 L 0 L 0 0 L L 0 L L 0 0 0 0 L s = (¬a ∧ b) ∨ (a ∧ ¬b) u=a∧b Antivalenz Diese Gleichungen führen auf den Aufbau gemäß Abbildung 2.9. Umformung ergibt: s = ¬(¬(a∨b)∨(a∧b)). Damit kann der Halbaddierer effizienter aufgebaut werden (s. Abb. 2.10). 2.3. SCHALTNETZE 19 a b u s Abbildung 2.9: Ineffiziente Schaltung für Halbaddierer a b u s Abbildung 2.10: Effizientere Schaltung für Halbaddierer 20 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE a b c VA u s Abbildung 2.11: Volladdierer a b c VA HA HA u s Abbildung 2.12: Volladdierer aus Halbaddierern 2.3.4 Arithmetische Schaltnetze Beim Addieren von Binärworten, die Zahlen binär darstellen, verwenden wir Volladdierer (s. Abb. 2.11). Tabelle: a 0 L 0 L 0 L 0 L b 0 0 L L 0 0 L L c 0 0 0 0 L L L L s 0 L L 0 L 0 0 L u 0 0 0 L 0 L L L n-stellige Binäraddition in Binärzahldarstellung: +2 ∈ SFn2n : han . . . a1 i +2 hbn . . . b1 i = hsn . . . s1 i Zuzüglich wird ein Übertrag u erzeugt: u = hun+1 . . . u1 i mit u1 = 0. Es gilt (uk+1 , sk ) = VA(ak , bk , uk ) Dies definiert s1 , . . . , sn , d.h. das Ergebnis hsn . . . s1 i (s. Abb. 2.13). un+1 zeigt an, ob die Arithmetik übergelaufen ist. Induktive Definition eines Addiernetzes: AN1 = VA ANn+1 = (I2 k An )(VA k In ) 2.3. SCHALTNETZE a n bn 21 un VA a 2 b2 a 1 b1 0 VA VA ... sn s2 u3 u n+1 s1 u2 Abbildung 2.13: Addiernetz für zwei n-Bit-Worte (vgl. Abb. 2.14) Analog definieren wir die Binärsubtraktion −2 : (Bn )2 → Bn , han . . . a1 i −2 hbn . . . b1 i = hsn . . . s1 i. si = (ai ∧ ¬bi ∧ ¬ui ) ∨ (¬ai ∧ bi ∧ ¬ui ) ∨ (¬ai ∧ ¬bi ∧ ui ) ∨ (ai ∧ bi ∧ ui ) u1 = 0 ui+1 = (¬ai ∧ bi ) ∨ (¬ai ∧ ui ) ∨ (bi ∧ ui ) Achtung: Der Übertrag repräsentiert nun einen negativen Wert. Bemerkung: Falls un+1 = L ist, dann gilt b >2 a (>2 : (Bn )2 → B). Die Subtraktion liefert als Abfallprodukt den Größenvergleich von a und b. 2.3.5 Zahldarstellung Positive Zahlen (natürliche Zahlen im Intervall [0, 2n − 1]) stellen wir durch die übliche Gewichtdarstellung dar: w (han . . . a1 i) = n X w(ai )2i−1 i=1 wobei w(L) = 1 und w(0) = 0 seien. Zur Darstellung negativer Zahlen: Statt Darstellung der Zahl durch den Absolutwert und ein Vorzeichen wählen wir eine Darstellung mit einem negativen Gewicht. Einerkomplementdarstellung c1 : [−2n + 1, 2n − 1] → Bn+1 d1 : Bn+1 → [−2n + 1, 2n − 1] d1 (c1 (z)) = z ∀z ∈ [−2n + 1, 2n − 1] d1 (hbn+1 . . . b1 i) = (−2n + 1)w(bn+1 + n X w(bi )2i−1 i=1 Achtung: Wir erhalten zwei Darstellungen der Null! d1 ( h0 . . . 0i ) = 0 = d1 ( hL . . . Li ) = −2n + 1 + | {z } | {z } positive Null negative Null n X 2i−1 i=1 Die Vorteile der Einerkomplementdarstellung sehen wir, wenn wir arithmetische Operationen ausführen. 22 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE an a 1 b1 u bn ... AN n ... sn u’ a n+1 b n+1 an bn s1 a 1 b1 u ... AN n u’ VA u’’ sn ... s1 AN n+1 s n+1 Abbildung 2.14: zur induktiven Definition des Addiernetzes 2.3. SCHALTNETZE 23 Beispiel: Komplementbildung d1 (hbn+1 . . . b1 i) = z ⇒ c1 (−z) = h¬bn+1 . . . ¬b1 i Addition in der Einerkomplementdarstellung: Wir definieren hsn+2 . . . s1 i = h0an+1 . . . a1 i +2 h0bn+1 . . . b1 i hrn+2 . . . r1 i = han+1 an+1 . . . a1 i +2 hbn+1 bn+1 . . . b1 i +2 h0 . . . 0sn+2 i Behauptung: Bei der Addition der durch a und b in Einerkomplementdarstellung gegebenen Zahlen tritt genau dann ein Überlauf auf (d.h. die Summe der Zahlen liegt außerhalb von [−2n + 1, 2n − 1]), falls rn+1 6= rn+2 . an+1 an+1 an . . . a1 bn+1 bn+1 bn . . . b1 sn+1 sn . . . s1 • 1. Fall: an+1 = bn+1 = 0, d.h. beide Zahlen sind positiv, d.h. Überlauf genau dann, wenn un+1 = L, rn+2 = 0 ⇒ rn+2 6= rn+1 . • 2. Fall: an+1 6= bn+1 , d.h. eine negative und eine positive Zahl werden addiert. Gilt un+1 = 0, dann ist rn+1 = rn+2 = L. Gilt un+1 = L, dann ist rn+1 = r+2 = 0. • 3. Fall: an+1 = bn+1 = L. Beide Zahlen sind negativ. Nur wenn un+1 = L ist, findet kein Überlauf statt. Dann gilt rn+1 = rn+2 = L; sonst gilt rn+1 6= rn+2 ⇒ Überlauf Ergebnis: In Einerkomplementdarstellung können wir praktisch ohne Aufwand das Komplement bilden (negieren des Zahlenwerts) und bei Addition der Zahlen unabhängig von der Frage, ob eine der Zahlen (oder beide) negativ ist, ein einfaches Addiernetz verwenden. Zusätzlich können wir noch durch Verwendung eines zusählichen Bits einen arithmetischen Überlauf feststellen. Zweierkomplementdarstellung c2 : [−2n , 2n − 1] → Bn+1 d2 : Bn+1 → [−2n , 2n − 1] d2 (hbn+1 . . . b1 i) = −2n w(bn+1 ) + n X w(bi )2i−1 i=1 Bemerkungen: Nur eine Darstellung der Null! d2 (hL . . . Li) = −1. Komplementbildung: d2 (hbn+1 . . . b1 i) = z ⇒ c2 (−z) = h¬bn+1 . . . ¬b1 i +2 h0 . . . 0Li (Einer rückt auf bei der Komplementbildung.) Dafür wird die Addition einfacher: hrn+1 . . . r1 i = han+1 an+1 . . . a1 i +2 hbn+1 bn+1 . . . b1 i Wieder gilt: rn+2 6= rn+1 ⇔ Überlauf bei Addition. Achtung: Kein Einerrücklauf bei Addition! 24 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE 2.3.6 Weitere arithmetische Operationen Auch Multiplikation und Division von Binärzahlen können wir durch Schaltnetze darstellen. Wir beschränken uns auf positive Zahlen. Multiplikation ∗2 : Bm × Bn → Bm+n , ham . . . an i ∗2 hbn . . . b1 i = hpm+n . . . p1 i. i Zur Definition von p verwenden wir eine Hilfsdefinition für ri = rm+n+ . . . r1i mit ( ak−i+1 ∧ bi falls i ≤ k ≤ m + i − 1 i rk = 0 sonst 1 n . . . r11 . Damit gilt hpm+n . . . p1 i = rm+n . . . r1n +2 · · · +2 rm+n Division /2 : Bm × Bn → Bm−n+1 für m ≥ n; ham . . . a1 i /2 hbn . . . b1 i = hqm−n+1 . . . q1 i. Wir setzen ham . . . a1 i <2 hbn . . . b1 0i voraus. Dies kann durch anfügen führenden Nullen (falls hbn . . . b1 i = 6 h0 werden. Wir definieren . . . 0i) immer erreicht eine Folge von Binärworten rni . . . r0i durch rn0 . . . r00 = ham . . . am−n+1 i. Gilt rni . . . r0i ≥2 h0bm . . . b1 i, so gilt qm−n−i+1 = L . . . r0i+1 = rest( rni . . . r0i −2 h0bm . . . b1 i) ◦ ham−n−i+2 i rni+1 Sonst ist qm−n−i+1 = 0 i rni−1 . . . r0i = rn−1 . . . r0i am−n−i+2 Fazit: Alle arithmetischen Operationen auf natürlichen, ganzen Zahlen und Binärbrüchen (Fixpunktschreibweise) können durch Schaltnetze realisiert werden. Achtung: Genauigkeit beschränkt - wir rechnen in einem endlichen Teilbereich der Zahlen. Gleiches gilt für Operationen der Aussagenlogik. 2.3.7 Schaltnetze zur Übertragung von Information Wir können über Leitungen Nachrichten übertragen. Sind unterschiedliche Partner an einer Übertragung beteiligt, so besteht oft der Wunsch, dass Nachrichten gezielt von einem Partner zum anderen übertrangen werden (Bsp. Telefon, E-Mail). Dies Erfordert eine Adressierung der Nachrichten (im Bsp. Telefonnummern, E-MailAdressen). Wir betrachten als elementares Beispiel Schaltungen mit Steuerleitungen. Sei {1, . . . , n} die Menge der Teilnehmer und c : {1, . . . , n} → Bm eine Codierfunktion, die jedem Teilnehmer einen Binärcode zuordnet. Weiter sei d : Bm → {1, . . . , m}. Forderung: d(c(i)) = i ∀i ∈ {1, . . . , n}. Einfaches - aus - n - Code * Beispiel: 1 + c(i) = . . 0} 0 . . 0}10 | .{z | .{z i−1 n−i Sei ferner eine weniger redundante Binärcodierung c0 für {1, . . . , n} gegeben: c0 : {1, . . . , n} → Bj DF : Bj → Bn DF(c0 (i))0c(i) ∀1 ≤ i ≤ n 2.4. SCHALTWERKE 25 Teilnehmer 1 j ... Teilnehmer n k ... . . . k ... DF n ... . k. . Abbildung 2.15: Multiplexer Eine Schaltung wie in Abbildung 2.15 bledent aus n Eingangsbündeln der Breite k alle bis auf eines aus; welches weitergegeben wird, bestimmt die Adresse für DF. Diese Schaltung nennen wir Multiplexer: MX : Bj × Bk·n → Bk Demultiplexer: DMX : Bj × Bk → Bk·m 2.4 Schaltwerke Bisher haben wir Schaltnetze betrachtet. Diese hatten keine Zyklen. Nun betrachten wir Schaltwerke. Diese sind wie Schaltwerke, allerdings sind nun Zyklen zugelassen. In der Praxis wird eine Schaltung nicht nur ein Mal benutzt (d.h. es wird nicht nur ein Binärwort eingegeben), sondern nacheinander wird eine Folge von Binärwörtern eingegeben und an den Ausgängen entsteht eine Folge von Binärworten. 2.4.1 Schaltwerksfunktionen Schaltfunktion: f : Bn → Bm Schaltwerksfunktion: g : (Bn )∗ → (Bm )∗ , wobei |g(x)| = |x| für x ∈ (Bn )∗ , d.h. jedes Eingabewort wird auf ein Ausgabewort abgebildet. Beispiel: Schaltwerksfunktion x1 . . . xn y1 . . . ym Zeitpunkt (Takt) (x1 ) L 0 L 0 1 (y 1 ) 2 (x ) 0 L 0 L 2 (y 2 ) 3 (x ) L 0 L 0 3 (y 3 ) 4 0 L 4 (y 4 ) (x ) 0 L 5 (x ) 0 L 0 L 5 (y 5 ) 6 (x ) L L L L 6 (y 6 ) .. .. .. .. .. .. .. . . . . . . . Achtung: Zwischen xi und y j dürfen auch für i 6= j Abhängigkeiten bestehen. Wichtig: Zeitflusseigenschaft (Kausalität): Ausgaben zum Zeitpunkt j dürfen höchstens von Eingaben zum Zeitpunkt i ≤ j abhängen. 26 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE Präfixordnung auf (Bn )∗ : Für x, y ∈ (Bn )∗ gelte x v y ⇔def ∃z ∈ (Bn )∗ : x ◦ z = y Die Kausalität entspricht der Präfix-Monotonie: x v x0 ⇒ g(x) v g(x0 ). Schaltwerksfunktion: g : (Bn )∗ → (Bm )∗ mit |g(x)| = |x| ∀x ∈ (Bn )∗ und g präfixmonoton (d.h. Kausalität ist gegeben). 2.4.2 Schaltfunktionen als Schaltwerksfunktionen Gegeben sei eine Schaltfunktion f : Bn → Bm . Wir definieren eine Schaltwerksfunktion f ∗ : (Bn )∗ → (Bm )∗ durch f ∗ ( x1 , x2 , . . . , xk ) := f (x1 ), f (x2 ), . . . , f (xk ) Die Schaltwerksfunktion f ∗ hat die Eigenschaft, dass die Ausgabe f (xk ) zum Zeitpunkt k lediglich von der Eingabe xk zum Zeitpunkt k abhängt. Die Funktion speichert nichts von den früheren Eingaben. Solche Schaltwerksfunktionen heißen auch kombinatorisch, die anderen sequentiell. 2.4.3 Schaltwerke Ein Schaltwerk ist ein gerichteter Graph mit Knoten, die Schaltwerksfunktionen entsprechen, und Kanten, die Leitungen entsprechen. Jetzt sind Zyklen zugelassen. In der Praxis werden wir jedoch nur eingeschränkt Zyklen betrachten. 2.4.4 Schaltwerksfunktionen und endliche Automaten Ein endlicher Automat hat einen Zustand aus einer endlichen Zustandsmenge state. Er bekommt in einem gegebenen Zustand eine Eingabe aus einer Eingabemenge in und erzeugt eine Ausgabe y aus einer Ausgabemenge out und einen neuen Zustand aus state. Wir sprechen von einem Zustand. Wir stellen die Zustandsübergänge durch eine Zustandsübergangsfunktion δ : state × in → state × out dar. Bei Schaltwerken betrachten wir Automaten, bei denen alle drei Mengen durch Binärwörter dargestellt werden (z, m, n ∈ N): δ : Bz × Bn → Bz × Bm Solche Automaten lassen sich kompakt durch Zustandsübergangsdiagramme darstellen. Beispiel: B2 × B2 → B2 × B2 . Zustandsübergangsdiagramm: s. Abb. 2.16. Achtung: Bestimmte Binärkombinationen treten als Zustände und Eingänge nicht auf. Die Kreise stellen die Zustände dar. Ein Pfeil vom Kreis σ1 zum Kreis σ2 mit Beschriftung x/y steht für δ(σ1 , x) = (σ2 , y). Ein Zustandsübergang besteht in einem Wechsel von einem Zustand σ1 zu einem Zustand σ2 . Ausgelöst wird der Übergang von einer Eingabe x und er erzeugt eine Ausgabe y. Schaltwerke können in ihrem Verhalten durch 2.4. SCHALTWERKE 27 L0/L0 00/0L 0L/0L L0 0L 00/L0 L0/L0 0L/0L Abbildung 2.16: Zustandsübergangsdiagramm • Automaten mit Ein-/Ausgabe • Schaltwerksfunktion beschrieben werden. Dabei gilt: jede Schaltewerksfunktion definiert einen Automaten. Automaten definieren Schaltwerksfunktionen Gegeben sei δ : state × in → state × out. Jeder Zustand σ ∈ state definiert eine Schaltwerksfunktion fσ : in∗ → out∗ wie folgt: Seien a ∈ in, x ∈ in∗ und b ∈ out. fσ (hai ◦ x) = hbi ◦ fσ0 (x) ⇔ delta(σ, a) = (σ 0 , b) fσ beschreibt das Ein-/Ausgabeverhalten des Automaten, wenn er im Zustand σ gestartet wird. Beispiel: obiger Automat Es gilt fL0 (h00i ◦ x) = hL0i ◦ fL0 (x) und f0L (h00i ◦ x) = hoLi ◦ f0L (x), d.h. fσ (h00i ◦ x) = hσi ◦ fσ (x) (Lesegleichung: Durch Eingabe von h00i wird der Zustand ausgegeben (gelesen) und beibehalten). Für σ, σ 0 ∈ {0L, L0} gilt fσ (hσ 0 i ◦ x) = hσ 0 i ◦ fσ0 (x) (Schreibgleichung: Der Zustand σ 0 wird geschrieben: Die Eingabe von σ 0 ∈ {0L, L0} führt dazu, dass der Automat in den Zustand σ 0 übergeht). Fazit: Der Automat speichert durch seinen Zustand genau ein Bit, das beliebig oft gelesen oder überschrieben werden kann. Schaltwerksfunktionen definieren Automaten Wir ordnen einer Schaltwerksfunktion f : in∗ → out∗ einen Automaten δf : SWF × in → SWF × out zu. Dabei steht SWF für die Menge der Schaltwerksfunktioonen in∗ → out∗ . Dabei gelte für g, g 0 ∈ SWF, a ∈ in, b ∈ out: δf (g, a) = (g 0 , b), wobei b und g 0 definiert sind durch g(hai ◦ x) = hbi ◦ g 0 (x) für alle x ∈ in∗ . Da g eine Schaltwerksfunktion ist, ist b durch a eindeutig bestimmt. Es gilt g 0 (x) = rest(g(hai ◦ x)). Jede Funktion f ∈ SWF definiert einen Zustand des Automaten. Jedes Schaltwerk kann in seinem Verhalten entweder als Automat mit Ein-/Ausgabe oder als Schaltwerksfunktion beschrieben werden - die Darstellungen sind ineinander überführbar. 2.4.5 Schaltwerke zum Speichern: Verzögerungsnetze Rückkopplung: das Flip-Flop (s. Abb. 2.17) Es gilt Qneu = ¬(r ∨ Q), Qneu = ¬(s ∨ Q). Beispiel für das Schalten des Flip-Flops: • Schreiben: 28 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE s Q Q r Abbildung 2.17: Flip-Flop: Nor-Latch L L L L L ... s r 0 0 0 0 0 ... Q 0 0 L L L ... Q L 0 0 0 0 ... Qneu 0 L L L L . . . Qneu 0 0 0 0 0 . . . Idee: Wir wiederholen die gleiche Eingabe aus s und r, bis die Q, Q stabilisieren. • Lesen: s r Q Q Qneu Qneu 0 0 0 L 0 L 0 0 0 L 0 L 0 0 0 L 0 L 0 0 0 L 0 L ... ... ... ... ... ... Das Flip-Flop ähnelt in seinem Schaltverhalten stark dem Automaten, den wir im vorhergehenden Beispiel beschrieben haben. Allerdings treten beim Schreiben Zwischenzustände auf. Das Flip-Flop stabilisiert sich nach einem Zwischenschritt. Allgemeine Feststellung: 1. Bei komplizierten Schaltungen können vorher Zwischenschritte erforderlich sein, um einen stabilen Zustand und eine stabile Ausgabe zu erreichen. 2. Unter gewissen Umständen tritt keine Stabilisierung ein. Bei Schaltungen, die sich stabilisieren, ergeben sich zwei Sichten: • Mikrosicht: Automat, Schaltwerksfunktion, in der der Stabilisationsvorgang sichtbar ist. • Makrosicht: Abstraktion der Mikrosicht. Wir wiederholen in der Mikrosicht die Eingabe, bis Stabilisierung eintritt, und nehmen die vorliegende Ausgabe als Ausgabe und den dann vorliegenden Zustand als neuen Zustand. Die Zahl, wie oft die Eingabe wiederholt wird, bis eine Stabilisierung erreicht wird, bestimmt die Frequenz und den Takt der Schaltung. 2.4.6 Klassen von Schaltwerken Typischerweise baut man nicht Schaltwerke mit beliebig komplizierten, verzögerungsfreien Rückkopplungsleitungen, sondern verwendet Flip-Flops zur Realisierung von Speichern und verwendet sonst nur verzögerte Rückkopplungen. 2.4. SCHALTWERKE 29 Verzögerung (delay): Dn : (Bn )∗ → (Bn )∗ a ∈ Bn Dna (hx1 . . . xn i) = hax1 . . . xn−1 i d.h. die Ausgabe ist um einen Takt verzögert. 2.4.7 Komposition von Schaltwerken und Schaltnetzen Aus Speichergliedern (Flip-Flops) und Verzögerungsgliedern können wir, unter Verwendung der Konzepte aus den Schaltnetzen, beliebig komplizierte und mächtige Schaltungen aufbauen: • Speicher • Verarbeitungswerke • Übertragungsnetze Damit lassen sich durch Schaltungen Bausteine (Chips) realisieren, aus denen dann Rechner aufgebaut werden können. 30 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE Kapitel 3 Aufbau von Rechenanlagen Eine Rechenanlage kann zunächst als riesiges Schaltwerk begriffen werden. Diese Sichtweise zeigt uns, wie wir uns die Realisierung einer Rechenanlage vorstellen können, ist aber für die Nutzung und Programmierung nicht sehr hilfreich. Programme entsprechen auf Schaltwerksebene bestimmten gespeicherten Binkombinationen (Bitwörtern). Für das Entwickeln größerer Programme brauchen wir eine strukturierte Sicht auf die Rechenanlage, durch die Programme greifbarer werden. 3.1 Strukturierter Aufbau von Rechenanlagen Das Grobkonzept eines Rechners (von-Neumann-Architektur) ist sein 50 Jahren praktisch unverändert. Eine Rechenanlage besteht demnach aus: • Rechnerkern / Prozessor / CPU • Speicher • Verbindungshardware (Bussysteme) • Peripherie (Ein-/Ausgabevorrichtungen) Wir sind im Weiteren primär an der Programmierung von Rechenanlagen interessiert. Somit interessiert uns die physikalische Struktur nur insoweit, als sie die Programme auf Maschinenebene beeinflusst. Dabei stellt sich die Frage, mit welchen Mitteln die Struktur einer Rechenanlage modelliert wird. Wir verwenden die Idee der Zustandsmaschine um eine Rechenanlage zu beschreiben. Dabei konzentrieren wir uns auf die Zustandsübergänge, die durch die Ausführung von Befehlen ausgelöst werden. Der Zustand einer Rechenanlage ist gegeben durch die Belegung ihrer Speicherzellen und Register (spezielle zusätzliche Speicherzellen für besondere Aufgaben). Zur Erläuterung der Struktur einer Rechenanlage und ihrer Programmierung verwenden wir eine Maschine MI (angelehnt an die VAX von Digital). Die Bestandteile des Rechners, insbesondere seine Speicherzellen und Register, stellen wir durch Programmvariable geeigneter Sorten dar und die Wirkungsweise der Programme / Befehle durch zuweisungsorientierte Programme, die auf diesen Programmvariablen arbeiten. Wichtig: Im Speicher stehen Programme und Daten! Der Modellmaschine MI besteht im Prinzip aus vier Rechnerkernen, zwei E/AProzessoren, dem Arbeitsspeicher und dem Bussystem. 31 32 KAPITEL 3. AUFBAU VON RECHENANLAGEN Rechnerkern E/A−Prozessoren Steuerwerk Peripherie Rechenwerk Bus (Verbindung) Speichereinheit Abbildung 3.1: Schematischer Aufbau eines von-Neumann-Rechners Plattenspeicher Arbeitsspeicher E/A 1 E/A 2 Adressbus Datenbus Steuerbus Abbildung 3.2: Spezifische Rechnerarchitektur der MI . . . . . . Rechnerkern Terminals 3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN 3.1.1 33 Der Rechnerkern Rechnerkern: Hardware-Betriebsmittel, welches autonom den Kontrollfluss steuert (entscheidet, welcher Befehl ausgeführt wird) und die Datentransformationen ausführt. Der Rechnerkern wird in Steuerewerk und Rechenwerk unterteilt. Das Steuerwerk enthält den Taktgeber und die Register, die die Information enthalten, die für die Ablaufsteuerung erforderlich ist (Befehlszähler, . . . ). Das Steuerwerk erzeugt auch die Steuersignale für die Ansteuerung des Rechenwerks. Das Rechenwerk enthält die Operandenregister und die Schaltnetze/-werke zur Ausführung der Operationen. Damit bestimmt das Rechenwerk den Befehlsvorrat. Die Register im Rechnerkern sind Speicherzellen mit extrem kurzen Zugriffszeiten. Die Bestandteile der MI werden durch folgende Deklarationen von Programmvariablen charakterisiert: 16 Register für 32-Bit-Wörter: var [0:31] array bit R0, ..., R15 Aufgaben: R0 : R13 frei verwendbar für Programmierung R14 Zeiger für Stapel (stack pointer, SP) R15 Befehlszähler (program counter, PC) Zusäzlich im Steuerewerk: var [0:7] array bit IR Instruktionsregister var [0:31] array bit PSL Prozessorstatusregister Weitere Hilfsregister für Steuer-/Rechenwerk: var [0:31] array bit tmp0, ..., tmp3 Hilfsregister für die ALU var [0:7] array bit am legt Adressiermodus fest var [0:31] array bit adr für die Adressrechnung var [0:31] array bit index für die Indexrechnung Die Anzahl der Register in einem Rechnerkern kann sehr stark schwanken (früher oft nur ein Register: Akkumulator, heute ganze Registerbänke). 3.1.2 Speichereinheit Die Speichereinheit besteht aus einer Folge von Speicherzellen, auf die über Adressen zugegriffen wird. MI: var [0:232 − 1] array [0:7] array bit M Speicher var [0:31] array bit MAR Speicheradressregister var [0:31] array bit MBR speicherregister 3.1.3 E/A Die Ein-/Ausgabe erfolgt über Peripheriegeräte durch die E/A-Prozessoren. (genaueres später) 3.1.4 Befehle und Daten auf Maschinenebene Auf Maschinenebene werden alle Daten und Befehle durch Bitwörter dargestellt (Binärdarstellung). Ein Befehl ist ein Binärwort, das im Hauptspeicher gespeichert werden kann und bei Ausführung (laden in den Rechnerkern und Interpretation) eine Zustandsänderung der Maschine bewirkt. Bestandteile eines Befehls: • Charakterisierung der Operation • Charakterisierung der Operanden 34 KAPITEL 3. AUFBAU VON RECHENANLAGEN Wir unterscheiden eine ganze Reihe unterschiedlicher Befehlsarten. Die Operanden kennzeichnen die Werte, mit denen wir arbeiten: Sie werden immer durch Bitwörter dargestellt, können aber ganz unterschiedliche Werte bezeichnen: • Adressen • Zahlen • Charakter Darstellung von Zahlen durch Bitwörter: natürliche Zahlen Binärschreibweise ganze Zahlen Zweierkomplementdarstellung Fixpunktdarstellung Darstellung von Zahlen aus [−1, 1[ Gleitpuntkdarstellung Darstellung von Zahlen durch Wert und Exponent Festpunktdarstellung: Zweierkomplementdarstellung (32 Bit), Multiplikation mit 2−31 . Gleitpunktdarstellung: b = hb0 b1 . . . bn bn+1 . . . bn+m i mit b0 : Vorzeichen, b1 . . . bn Exponent, bn+1 . . . bn+m Mantisse. Wir berechnen den dargestellten Zahlenwert mit Hilfe folgender Hilfswerte: e= n X 2n−i w(bi ) i=1 v= m X 2−i w(bn+1 ) i=1 Es gilt 0 ≤ e < 2n , 0 ≤ v < 1. exp = e − 2n−1 + 1 ⇒ −2n−1 + 1 ≤ exp ≤ 2n−1 man = 1 + v ⇒ 1 ≤ man < 2 Fälle der Interpretation: • Normalfall: −2n−1 + 1 < exp < 2n−1 . Wir erhalten als dargestellt Zahl (−2)w(b0 ) · 2exp · man • Darstellung der Null: e = 0 ∧ v = 0 • Unterlauf: e = 0 ∧ v 6= 0 • Überlauf: e = emax = 2n − 1 ∧ v = 0 • Fehlerfall: e = emax = 2n − 1 ∧ v 6= 0 Unterlauf, Überlauf und Fehlerfall sind keine gültigen Zahldarstellungen. Auf Maschinenebene werden alle Daten durch Bitsequenzen dargestellt. Sorte Kennung Wortlänge sort byte = [0:7] array bit B 8 sort half-word = [0:15] array bit H 16 sort word = [0:31] array bit W 32 sort floating-point = [0:31] array bit F 32 (8 + 23) sort double-float = [0:63] array bit D 64 (11 + 52) Problem: Einer Ziffernfolge 37216 ist nicht mehr anzusehen, ob sie in Oktal-, Dezimaloder Hexadezimaldarstellung ist. Ausweg: (37216)8 oktal, (37216)10 dezimal, (37216)16 hexadezimal Konvention: Wir verwenden Dezimaldarstellung und geben Oktal- und Hexadezimaldarstellung explizit an. 3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN 35 Auch Befehle werden auf Maschinenebene durch Bitsequenzen darstestellt. Bequemer und lesbarer sind Darstellungen, bei denen die Teile des Befehls durch Namen udn Dezimalzahlen dargestellt werden. Bestandteile eines Befehls: • Operation: Angabe, welche Umformung bzw. welche Umspeicherung ausgeführt wird • Operandenspezifikation: Angabe, mit welchen Werten / Speicherzellen / Registern dabei gearbeitet wird Syntax: hcommandi ::= hinstructionParti {hopParti {, hopParti}∗ } Es gibt Maschinen mit starrem Befehlsformat (festgelegte Länge der Bitsequenz). Nachteil: Jeder Befehl hat die gleiche Anzahl von Bits für Operandenspezifikationen zur Verfügung. Vorteil: Jeder Befehl passt genau in eine Speicherstelle, das Ansteuern von Befehlen wird einfacher. Die MI hat flexible Befehlsformate. Der Instruktionsteil (Operationsangabe) des Befehls wird im Rechnerkern (im Steuerwerk) in Steuerbits umgesetzt, die die entsprechenden Umformungen und Umspeicherungen auslösen. 3.1.5 Operandenspezifikation und Adressrechnung Jeder Befehl arbeitet mit bestimmten Werten (Bitsequenzen) und bestimmten Speicherplätzen / Registern. Wir sprechen von Operanden. Beispiel: Einfachste Art der Operandenspezifikation: wir geben das Register W R9 (W: Kennung, R9: Register) oder die Speicherzelle W 1743 direkt an. Damit werden die Speicherzellen 1743-1746 angesprochen. Die einfachste Form der Angabe von Operanden ist die direkte (absolute) Adressierung: Es werden Register / Adresse explizit angegeben. Weil wir an mehr Flexibilität interessiert sind und uns in einem Programm nicht festlegen wollen, an welcher Stelle im Speicher es (absolut) steht, verwenden wir kompliziertere Adressierverfahren. Dazu verwenden wir eine Operandenspezifikation. Dies ist eine Angabe, aus der wir die Operanden berechnen können (Adressrechnung). Jeder Operand entspricht einer Lokation (einem Speicherplatz oder Register; genauer: einem Ausschnitt aus einem Speicher / Register oder einer Folge). Ein Operand bestimmt einen bestimmten Platz (Lokation) in der Maschine, an der eine Bitsequenz gespeicher ist. sort operand = register (type k, nat n) | memo (type k, nat n) sort type = {B, H, W, F, D} Eine Operandenspezifikation ist ein syntaktischer Ausdruck, der angibt, wie der Operand zu berechnen ist. Jede Operandenspezifikation wird bei Ausführung eines Befehls im Befehlszyklusin einen Operanden umgerechnet. Dazu verwenden wir folgende Hilfsprozeduren: 1 2 4 6 8 fct val = (operand x) seq bit: if x in register then R[n(x)][32-wl(k(x)):31] else get (wl(k(x))/8, n(x)) fi fct get = (nat i, nat n) seq bit: if i = 0 then empty else conc (M[n], get(i-1, n+1)) fi 36 KAPITEL 3. AUFBAU VON RECHENANLAGEN Adressierarten: • direkte Adressierung: Die absolute Adresse (oder das Register) wird im Adressteil angegeben. • direkte Angabe des Operandenwerts im Befehl als Binär-, Hexadezimal- oder Dezimalzahl. • relative Adressierung: Zum Adressteil im Befehl wird der Inhalt eines Registers addiert. • indirekte Adressierung / Adresssubstitution: Die Speicherzelle, die über den Adressteil angesteuert wird, enthält selbst eine Adresse der Speicherzelle, die den Operanden bildet. • Indizierung: Zur ermittelten Adresse wird ein weiterer Wert addiert (typische Anwendung: hochzählen eines Zählers in einer Wiederholungsanweisung zum Zugriff auf Feldelemente) BNF-Syntax der MI-Operandenspezifikation: hopParti ::= habsoluteAddressi | himmediateOpi | hregisteri | hrelativeAddressi | hindexedRelativeAddressi | hindirectAddressi | hindexedIndirectAddressi | hstackAddressi Ein Teil der Adressierart wird durch folgende Werte bestimmt: 1 2 4 sort operandSpec = opspec (nat reg, bool isr, bool idr, incr icr, nat rel, indexSpec int) sort indexSpec = {-} | reg (nat) sort incr = {-1 | 0 | 1 } • absolute Adressierung: habsoluteAddressi ::= hintexpi (integer expression) • Operand als Wert: himmediateOpi ::= I hintexpi • Register als Operand: hregisteri ::= R0| . . . |R15|P C|SP • relative Adresse: hrelativeAddressi ::= {hintexpi +}! hregisteri • indizierte relative Adressierung: hindexedRelativeAddressi ::= hrelativeAddressi hindexi hindexi ::= / hregisteri / 3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN 37 • indirekte Adressierung: hindirectAddressi ::=!(hrelativeAddressi)|!! hregisteri • indizierte indirekte Adressierung: hindexedIndirectAddressi ::= hindirectAddressi hindexi • Stack-Adressierung: hstackAddressi ::= −! hregisteri |! hregisteri + 3.1.6 Der Befehlszyklus Der Befehlszyklus bezeichnet die Folge der Schritte, die bei der Ausführung eines Befehls ausgeführt werden. Die Maschinen durchlaufen immer wieder die gleiche Sequenz von Schritten und führen dabei jeweils einen Befehl aus. Wir beschreiben den Befehlszyklus wiederum durch ein Programm. In der MI besteht ein Befehl aus einem Operationsteil und bis zu vier Operanden. In der Maschine ist der Operationsteil durch ein Byte repräsentiert. In diesem Byte sind die folgenden Informationen verschlüsselt: fct args = (byte) nat Anzahl der Operanden fct kenn = (byte) type Kennung fct result = (byte) bool Wird ein Resultat erzeugt? Der Befehlszyklus der MI entspricht dem folgenden Programm. 1 2 4 6 8 10 12 14 16 18 20 22 24 26 28 while not(stop) do fetch_operator; if 1 <= args(IR) then fetch_operand (kenn(IR), tmp1[32-wl(kenn(IR)):31]) fi; if 2 <= args(IR) then fetch_operand (kenn(IR), tmp2[32-wl(kenn(IR)):31]) fi; if 2 <= args(IR) then fetch_operand (kenn(IR), tmp3[32-wl(kenn(IR)):31]) fi; execute (IR); if result (IR) then put_result fi od proc fetch_operator =: fetch (B, R15, IR); R15 := R15 + 1 proc fetch = (type k, [0:31] array bit adr, var array[0:wl(k)-1] array bit r): MAR := adr; for i := 1 to wl(k)/8 do MBR[(i-1)*8:i*8-1] := M[MAR]; MAR := MAR + 1 od; r := MBR[0:wl(k)-1] 38 KAPITEL 3. AUFBAU VON RECHENANLAGEN 30 32 34 36 38 proc put = (type k, [0:31] array bit adr, var array[0:wl(k)-1] array bit r): MAR := adr; MBR[0:wl(k)-1] := r; for i := 1 to wl(k)/8 do M[MAR] := MBR[(i-1)*8:i*8-1]; MAR := MAR + 1; od Für die Adressrechnung zur Ermittlung der Operanden benötigen wir eine weitere Information, die aus dem Byte ermittelt wird, das den Adressiermodus bestimmt: fct reg = (byte) nat Registerzahl fct isr = (byte) bool steht Operand im Register? fct isrel = (byte) bool relative Adressierung? fct idr = (byte) bool indirekte Adressierung? fct isidx = (byte) bool Indexadressierung? fct icr = (byte) incr Registerinkrement / -dekrement Bei der Ausführung eines Befehls (execute) wird nicht nur ein Ergebnis berechnet und zurückgeschrieben. Es werden auch bestimmte Bits in einem zusätzlichen Register (dem Prozessorstatusregister) gesetzt. Dabei sind folgende vier Bits für uns von besonderer Bedeutung: C carry Übertrag V overflow Überlauf Z zero Resultat ist Null N negative Resultat ist negativ Die Schritte zum Bereitstellen der Operanden und der Abspeicherung des Resultats sind für alle Befehle, abhängig von der Zahl der Argumente und der Frage, ob ein Resultat anfällt, gleich. Die Wirkung eines Befehls wird durch die Rechenvorschrift proc execute = (byte): ... beschrieben. 3.2 Hardwarekomponenten Wir unterscheiden folgende Bestandteile einer Rechenanlage: • Prozessor • Speicher – Hauptspeicher – Hintergrundspeicher – Speichermittel für das Archivieren • Eingabegeräte • Ausgabegeräte • Datenübertragungsgeräte / Netze Kapitel 4 Maschinennahe Programmierung Die im letzten Kapitel eingeführten Programmstrukturen beschreiben am Beispiel der MI die Wirkungsweise eines Rechners. Nun führen wir die einzelnen Befehle ein, beschreiben ihre Wirkung und formulieren erste kurze Maschinenprogramme. Grundsätzliche Feststellungen: • Ein Maschinenprogramm ist eine endliche Folge von Befehlen, die zur Ausführungszeit im Hauptspeicher stehen. Damit hat jeder Befehl eine absolute und eine relative Adresse. • Diese Befehle werden, gesteuert durch den IC (PC) nacheinander ausgeführt. • Jedes Programm, das nicht in Maschinensprache geschrieben ist, muss – von Hand oder durch ein Übersetzungsprogramm (Compiler) in Maschinensprache übersetzt werden, bevor es ausgeführt werden kann, oder – ein Interpretationsprogramm (Interpreter) angegeben werden; dies ist ein in Maschinensprache gegebenes Programm, das das Programm in Programmiersprache durchläuft und dabei die entsprechenden Befehle ausführt. 4.1 Maschinennahe Programmiersprachen Struktur, Form und Umfang eines Befehlssatzes und der einzelnen Befehle wird durch den Rechnerkern geprägt. Maschinenprogramme sind Folgen von Befehlen und weisen somit keine besondere Struktur auf. Es ist Aufgabe des Programmierers, Maschinenprogramme strukturiert zu schreiben. Deshalb: ! Trickreiche Programmierung vermeiden! ! Transparente Programmstrukturierung anstreben! ! Ausreichend dokumentieren! ! Umfangreiche Programme in Programmteile strukturieren! 39 40 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG 4.1.1 Binärwörter als Befehle In der MI entspricht ein Programm zur Ausführungszeit einer Folgt von Bytes im Speicher. In dieser Form ist ein Programm für Menschen praktisch unlesbar. ∗ hbinaryMachineCommandi ::= hbytei hbinaryAddressi ::={0|L}8L hbinaryMachineProgrammi ::={hbinaryAddressi hbytei}∗ |{hbinaryAddressi hbinaryMachineCommandi} Wir vermeiden diese Form der Befehlsangabe und wählen eine etwas lesbarere. Dabei werden Befehle durch Angabe der Befehlsbezeichnung, des Typs der Operanden und der Adressen in Dezimalschreibweise beschrieben. 4.1.2 Der Befehlsvorrat der MI Die Wirkung eines Befehls wird durch die Rechenvorschrift execute im Befehlszyklus festgelegt. Dabei werden gewisse Hilfsregister / Spezialregister und insbesondere das PSL (Prozessorstatusregister) geändert. Beispiele für Befehle der MI: • Transportbefehle - Umspeichern von Informationen MOVE W 1000, 2000 Es wird der Inhalt der Speicherzellen 1000 - 1003 in tmp1 geladen, in tmp0 umgesetzt und in die Speicherzellen 2000 - 2003 zurückgespeichert. MOVE B 1000, 2000 Der Inhalt der Speicherzelle 1000 wird mit Hilfe der temporären Register in die Zelle 2000 geschrieben. MOVE B 1000, R4 R4[24:31] wird durch das Byte in Zelle 1000 überschrieben. MOVEA 1000, 2000 (move address) ist wirkungsgleich zu MOVE W I1000, 2000: der Wert (die Adresse) 1000 wird den Speicherzellen 2000 - 2003 zugewiesen. CLEAR B R7 Dieser Befehl bewirkt das Überschreiben von R7[24:31] durch das Byte 0016 . • Logische Operationen OR W R8, 3001, R1 bewirkt bitweise Disjunktion der 32 Bit in R8 mit den 32 Bit in 3001 - 3004; das Resultat wird in Register R1 geschrieben. Typische Anwendung: Maskieren von Werten. • Arithmetische Operationen ADD, SUB, MULT, DIV Wirkung entsprechend der Zahldarstellung (gesteuert über die Kennung), s. später • Vergleichsoperationen CMP W 3007, R6 Dieser Befehl dient dazu, gewisse Bits im PSL zu setzen. Im Beispiel werden die 32 Bit in den Zellen 3007 - 3010 in tmp1 und die 32 Bit aus R6 in tmp2 geschrieben. Dann wird tmp1 mit tmp2 verglichen: 1 2 Z := (tmp1 = tmp2) N := (tmp1 < tmp2) 4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN 41 • In Sprungbefehlen können wir nun auf die so gesetzten Bits Bezug nehmen. JUMP 5000 Einfacher (unbedingter) Sprung: der Befehlszähler wird mit Adresse 5000 belegt. JEQ 5000 (jump equal): Der Befehlszähler wird mit Adresse 5000 belegt, falls Z=L gilt; anderenfalls hat der Befehl keine Wirkung. • Shiftbefehle erlauben es, die Bits in einem Operanden zu verschieben. (im Bsp: Rechtsshift um 2 Bit) LL000L0L 0LLL000L Zwei Bits werden frei und können entweder durch Nachziehen von 0 oder L belegt werden oder durch kreisförmiges Durchschieben. Dieser Befehlssatz ist nur ein Ausschnitt der wichtigsten Befehle der MI. Reale Maschinen haben oft erheblich umfangreichere Befehlssätze, wobei oft nur Abkürzungen für Befehle oder das Zusammenfassen mehrerer Befehle zu einem dadurch möglich wird. Wir haben als wichtige Befehle nur die ausgeklammert, die für die Systemprogrammierung von Bedeutung sind. 4.1.3 Einfache Maschinenprogramme Zur Ausführungszeit der Programme stehen Daten und Programme gleichermaßen binär codiert im Speicher. Dabei ist es ratsam, Daten und Programme in getrennten Speicherbereichen abzulegen. Beispiel: Speicherorganisation Adresse (hex) Verwendung 0000 0000 reserviert 0000 0400 Programmspeicher 3FFF FFFF 4000 0000 Kontrollbereich (Daten, Stack) 7FFF FFFF 8000 0000 Systembereich BFFF FFFF C000 0000 reserviert für Systemprogramme FFFF FFFF Die im Beispiel verwendeten Adressen sind virtuell, d.h. die tatsächlich im Hauptspeicher vorhandene Menge von Adressen ist erheblich kleiner. Zur Ausführungszeit werden den virtuellen Adressen richtige Adressen zugeordnet. Ein Teil der Daten steht im Hintergrundspeicher und wird bei Bedarf in den Hauptspeicher geladen. Zur Erhöhung der Lesbarkeit unserer Programme arbeiten wir mit Adressen, ohne uns um die Frage zu kümmern, ob diese virtuell oder real sind, und verwenden in Programmen symbolische Adressen für Sprungbefehle. Beispiele für Maschinenprogramme 42 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG 1. Berechnen des Maximums zweier Zahlen: Seien zwei Zahlen (32 Bit) im Speicher durch die Adressen in den Registern R1 und R2 gegeben. Das Maximum der Zahlen soll in Register R0 geschrieben werden. 1 2 4 6 CMP W !R1, !R2 JLE max2 MOVE W !R1, R0 JUMP ende max2: MOVE W !R2, R0 ende: 2. Durchsuchen eines Speicherabschnitts. Gegeben seien Adressen in R0 und R1, wobei R0 ¡ R1 gelte. Der Speicher von R0 bis R1 soll nach einem 32-Bit-Wert, der in R2 steht, durchsucht werden. Falls der Wert gefunden wird, ist die Adresse dazu in das Register R3 zu schreiben. Anderenfalls wird 0 nach R3 geschrieben. Bei etwas komplizierteren Aufgaben ist es hilfreich, die Lösung zuerst als problemorientiertes Programm zu formulieren und dann erst das MI-Programm zu schreiben. 1 2 4 6 8 R3 := R0; suche: if M[R3] = else R3 := if R3 fi; R3 := fi ende: R2 then goto ende R3 + 4; <= R1 then goto suche 0 Umsetzung in ein MI-Programm: 1 2 4 6 8 MOVE W R0, R3 suche: CMP W !R3, R2 JEQ ende ADD W I4, R3 CMP W R3, R1 JLE suche CLEAR W R3 ende: 3. Sortieren der Inhalte eines Speicherbereichs. Aufgabe: Man sortiere die Bytes im Speicher zwischen der Adresse in R0 und der Adresse in R1 aufsteigend. R2, . . . , R11 dürfen als Hilfsregister verwendet werden. Wir verwenden einen einfachen Algorithmus: 1 2 4 6 8 R2 := R0; while R2 < R1 do if M[R2] > M[R2+1] then M[R2], M[R2+1] := M[R2+1], M[R2]; if R0 < R2 then R2 := R2 - 1 fi else R2 := R2 + 1 fi od 4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN 43 Wir brechen das Programm in Sprungbefehle auf: 1 2 4 6 8 10 12 R2 := R0; if R2 >= R1 then goto ende fi; R3 := R2 + 1; if M[R2] > M[R3] then goto vertauschen fi; R2 := R3; goto m_while; vertauschen: R4[24:31] := M[R2]; M[R2] := M[R3]; M[R3] := R4[24:31]; if R0 = R2 then goto m_while; R2 := R2 - 1; goto m_while; ende: m_while: Nun ist das Schreiben des MI-Programms einfach: 1 2 4 6 8 10 12 14 16 4.1.4 MOVE W R0, R2 CMP W R1, R2 JLE ende ADD W I1, R2, R3 CMP B !R3, !R3 JLT vertauschen MOVE W R3, R2 JUMP loop vertauschen: MOVE B !R2, R4 MOVE B !R3, !R2 MOVE B R4, !R3 CMP W R0, R2 JEQ loop SUB W I1, R2 JUMP loop ende: loop: Assemblersprachen Assemblersprachen sind maschinennahe Programmiersprachen, die durch geringfügige Erweiterung reiner Maschinensprachen entstehen. Die Umsetzung von Assemblersprachen in reine Maschinensprachen erfolgt durch Programme, sogenannte Assemblierer (engl. assembler). Typische Erweiterungen in Assembler sind: • mehrfache Marken • eingeschränkte arithmetische Ausdrücke • direkte Operanden (in der MI ohnehin vorgesehen) • symbolische Adressierung (analog zu Programmvariablen) • Segmentierung (Bindungs- und Gültigkeitsbereiche für Namen) • Definition und Einsetzung von Substitutionstexten (Makros) • Substitution von Makros = Makroexpansion Über die geschickte Einführung von Makros können wir auf bestimmte Anwendungssituationen zugeschnittene Assemblerstile definieren. 44 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG 4.1.5 Ein- und Mehradressform Eine wesentliche Charakteristik einer Maschinensprache ist die Anzahl der Operanden pro Befehl. • starres Befehlsformat: einheitliche Zahl von 0, 1, 2, 3 Operanden pro Befehl • flexibles Befehlsformat (siehe MI): Befehle haben zwischen 0 und n ∈ N Operanden Bei starrem Befehlsformat sprechen wir von Einadressform, wenn jeder Befehl genau einen Operanden enthält (entsprechend Zwei- und Dreiadressform). Wir erhalten bei Dreiadressform folgende Syntax: hbefehli ::= hregisteri ::=[hoperandi | − hoperandi |AC | hoperandi hoperatori hoperandi] if hbedingungi then goto hmarkei f i |if |skip goto hmarkei |goto hbefehlsfolgei ::={{hmarkei}∗ hbefehli}∗ hspeedi ::= hkonstantei |AC |h[1]|h[2]| . . . |a|b| . . . hbedingungi ::= hregisteri = 0 | hregisteri = 6 0 | hregisteri < 0 | hregisteri ≤ 0 hoperatori ::={+, −, ∗, /, . . .} hregisteri ::=AC |a|b| . . . |h[1]|h[2]| . . . Wir sprechen von einem Programm in Einadressform, wenn in jedem Befehl außer AC höchstens eine weitere Registerangabe auftritt. Der AC ist das zentrale Rechenregister, auf dem alle Berechnungen ausgeführt werden. Achtung: Dreiadressformen können immer in Einadressformen übersetzt werden. Beispiel a := b + c Dreiadressform wird umgeschrieben in AC := b AC := AC + c a := AC 4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN 4.1.6 45 Unterprogrammtechniken Wird eine Folge von Befehlen an mehreren Stellen in einem Maschinenprogramm benötigt, so bietet es sich an, das Programm nicht mehrfach zu schreiben, sondern • ein Makro zu definieren • ein Unterprogramm zu definieren, d.h. ein Programmstück, auf das über eine Marke gesprungen werden kann, und das über einen Rücksprung verlassen wird. Beim Springen in ein Unterprogramm sind folgende Daten zu übergeben (d.h. abzuspeichern, so dass das Unterprogramm darauf Zugriff hat): • Rücksprungadresse • Parameter Beim Rücksprung sind unter Umständen gewisse Resultate aus dem Unterprogramm an das aufrufende Programm zu übergeben. Techniken zur Übergabe der Daten: • Abspeichern in bestimmten Zellen im Unter- oder Hauptprogramm • Abspeichern in Registern • Abspeichern im Stack Wichtig: Einheitliche, für das ganze Programm gültige Regeln für die Übergabe der Parameter und Rücksprungadresse festlegen. Geschlossenes Unterprogramm: Ansprung erfolgt über Anfangsadresse, Rücksprung über die vor Ansprung angegebene Adresse. Rekursives Unterprogramm: Aus dem Unterprogramm wird die Anfangsadresse angesprungen. Dann muss die Rückkehradrese im Stack verwaltet werden. Beim offenen Einbau eines Unterprogramms (insbesondere beim Einsatz von Makroexpansion) kann Rekursion nicht behandelt werden. Die MI sieht spezielle Befehle für Unterprogrammaufrufe vor, die jedoch auch durch Befehle ersetzt werden können, die den Stack manipulieren und durch Sprungbefehle: • CALL speichert den aktuellen Stand des Befehlszählers (Register PC) im Stack ab und setzt den Befehlszähler auf die angegebene Adresse. • RET bewirkt die Rückkehr an die Adresse hinter der Aufrufstelle. Diese wird aus dem Stack in den PC geladen und der Stack wird zurückgesetzt. Beispiel: Aufruf eines Unterprogramms zur Berechnung der Fakultät MOVE W ..., R0 (Argument in R0 ablegen) CALL fac (Unterprogramm aufrufen) MOVE W R2, ... (Resultat abspeicher) Wichtig: Durch Unterprogrammaufrufe werden Teile des Speichers und der Register verändert. Für die Nutzung eines Unterprogramms ist es wichtig zu wissen: • wie Parameter / Resultat übergeben werden • welche Teile des Speichers / der Register durch das Unterprogramm verändert werden Bei einer strukturierten Programmierung auf Maschinenebene kommt der systematischen Nutzung von Unterprogrammen eine entscheidende Bedeutung zu. Unterprogramme sind sorgfältig zu dokumentieren. 46 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG Im allgemeinen Fall arbeitet das Hauptprogramm mit allen Registern. Das gleiche gilt für das Unterprogramm. Deshalb ist es naheliegend, vor dem Aufruf des Unterprogramms alle Register zu retten (im Speicher zwischenzuspeichern). Dann kann das Unterprogramm alle Register frei nutzen. Nach Rücksprung werden die alten Registerwerte restauriert. Bei der MI gibt es einen speziellen Befehl zum Retten der Register im Stack: PUSHR bewirkt ein Abspeichern der Register R14 bis R0 auf dem Stack entsprechend der folgenden Befehlssequenz: 1 2 4 SUB W I4, R14 MOVE W R14, !R14 SUB W I4, R14 MOVE W R13, !R14 ... Der Stackpointer wird insgesamt um 60 dekrementiert und die Registerinhalte werden im Stack gespeichert. Der Befehl POPR bewirkt das Restaurieren der Register aus dem Stack. Der Stackpointer wird um 60 erhöht. Ratsam ist es, bei der Verwendung von Unterprogrammen eine einheitliche Standardschnittstelle zu nutzen. Prinzipien: • Im aufrufenden Programm werden die Parameter des Unterprogramms in der Reihenfolge pn , . . . , p1 auf dem Stack abgelegt. Für ein Ergebnis wird ebenfalls Speicherplatz im Stack reserviert. Dann erfolgt der Aufruf des Unterprogramms. Die Rückkehradresse steht im Stack. • Im Unterprogramm werden die Registerwerte gesichert. Der nun erreichte Stackpointer-Stand wird in R13 abgelegt. R13 dient also als Basisadresse für den Stackpointer-Stand zur Aufrufzeit. In R12 tragen wir die Basisadresse der Parameter ein. Bei der Rückkehr aus dem Unterprogramm können wir diese Basisadresse verwenden, um den alten Stack-Pegel wiederherzustellen. • Nach der Ausführung des Unterprogramms wird über POPR der Registerzustand wiederhergestellt und über RET erfolgt der Rücksprung. • Nach Rückkehr stehen die Parameter und das Resultat im Stack. Nach Zurücksetzen des Stacks um die Parameter steht das Resultat zu Beginn des Stacks und kann abgespeichert werden. Also: 1 2 4 6 ... MOVE kn pn, -!SP ... MOVE k1 p1, -!SP CALL up ADD W I..., SP ... 8 10 12 14 up: PUSHR MOVE W SP, R13 MOVEA 64+!R13, R12 ... MOVE W R13, SP POPR RET 4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG 47 Beispiel: ggt 1 2 MOVE W I0, -!SP MOVE W ..., -!SP MOVE W ..., -!SP CALL ggt ADD W I8, SP MOVE W !SP+, ... 4 6 8 10 12 14 16 18 20 22 24 26 ggt: PUSHR MOVE W SP, R13 MOVEA 64+!R13, R12 CMP W 4+!R12, !R12 JNE else then: MOVE W !R12, 8+!R12 JUMP rueck else: JLT op1lop2 SUB W 4+!R12, !R12 JUMP reccall op1lop2: SUB W !R12, 4+!R12 reccall: MOVE W I0, -!SP MOVE W !R12, -!SP MOVE W 4+!R12, -!SP CALL ggt ADD W I8, SP MOVE W !SP+, 8+!R12 rueck: MOVE W R13, SP POPR RET 4.2 Adressiertechniken und Speicherverwaltung In höheren Programmiersprachen werden Datenstrukturen zur strukturierten Darstellung von Informationen verwendet: • Grunddatentypen • Enumerationssorten • Records • Union • Array • rekursive Datensorte • Referenzen Im Folgenden interessiert uns die Frage, wie wir diese Datenstrukturen auf Maschinenebene darstellen und darauf zugreifen können. 4.2.1 Konstante Eine Konstante entspricht einer Operandenspezifikation, die einen Wert repräsentiert, ohne dabei auf Register oder den Speicher Bezug zu nemen. In der MI schreiben wir z.B. I 2735. Zur Ausführungszeit ist der Wert 2735 in Binärdarstellung im 48 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG Hauptspeicher abgelegt. Zur Ausführungszeit des entsprechenden Befehls zeigt der Befehlszähler in der Adressrechnung auf die Adresse im Hauptspeicher, unter der der Wert abgelegt ist. 4.2.2 Operandenversorgung über Register Bei der Operandenversorgung über Register sind die Zugriffszeiten in der Regel geringer als beim Zugriff auf den Hauptspeicher. In der MI: Rn, 0 ≤ n ≤ 15. 4.2.3 Absolute Adressierung Im Befehl wird die Adresse als Wert angegeben. Dies bedeutet, dass das Programm bzw. seine Daten im Hauptspeicher fixiert sind, d.h. die Lage im Hauptspeicher festgelegt ist. Dies bedeutet eine Inflexibilität, die zu vermeiden ist. Absolute Adressierung macht höchstens für eine Reihe von Systemprogrammen Sinn. 4.2.4 Relative Adressierung Idee: Wir adressieren relativ zu einer frei festlegbaren Anfangsadresse für • den Programmcode • die Daten Ein Maschinenprogramm heißt relativ adressiert, wenn es keine Adressteile bzw. zur Verwendung als Adressteile vorgesehene Operandenteile enthält, die auf eine absolute Lage des Programms oder seiner Daten im Speicher Bezug nehmen. Nur dann können wir das Programm und seine Daten an beliebigen Stellen im Speicher ansiedeln. Für die Methodik wichtige Frage: Ist die Tatsache, dass ein Maschinenprogramm relativ adressiert ist, einfach zu erkennen und zu überprüfen (Codierstandards)? In der MI ist natürlich syntaktisch erkennbar, ob ausschließlich mit relativer Adressierung in der Operandenspezifikation gearbeitet wird. Dies garantiert allerdings noch nicht, dass das Programm relativ adressiert ist. (Achtung: Unterschied Syntax / Semantik!) 4.2.5 Indizierung, Zugriff auf Felder In höheren Programmiersprachen verwenden wir Felder ([n:m] array m a). Wir behandeln nun die Frage, wie solche Felder in einer Maschine (im Hauptspeicher) abgelegt werden und wie systematisch darauf zugegriffen wird. Wir legen die Elemente eines Felds konsekutiv im Speicher ab. Um den Zugriff zu organisieren, verwenden wir folgende Darstellung: Adresse Inhalt 4711 4711 + 1 − n fiktive Anfangsadresse a0 für a[0] 4712 a[n] 4713 a[n+1] .. .. . . 4711+m a[m] Für den Index k, n ≤ k ≤ m, erhalten wir durch k + a0 die Adresse des Feldelements. Sei α die Anfangsadresse des Felds. Dann schreiben wir in die Speicherzelle die fiktive Adresse α + 1 − n. Vorteil: Wenig Speicher für Verwaltungsinformationen zum Feld, uniformer Zugriff. Nachteil: keine vollständigen Informationen zur Überprüfung, ob der Index außerhalb der Indexgrenzen liegt. 4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG 49 In der MI: Enthält R0 die fiktive Anfangsadresse des Felds und R1 den Index k, so wird auf a[k] durch !R0/R1/ zugegriffen. Enthält R0 hingegen die effektive Anfangsadresse des Felds, so erfolgt der Zugriff auf das k-te Feldelement durch !!R0/R1/. Diese Zugriffstechnik ist problemlos mit der relativen Adressierung kombinierbar. Die Zugriffstechnik setzt voraus, dass zur Ausführungszeit beim Anlegen des Felds (auf dem Stack) die Feldgrenzen festliegen und sich nicht mehr ändern. Mehrstufige Felder können nach dem selben Prinzip behandelt werden: Sei [n1:m1, ..., ni:mi] array m a mit nq ≤ mq für 1 ≤ q ≤ i gegeben. Wir verwenden folgendes Schema: Adresse Inhalt 4711 a0 fiktive Adresse von a[0, ..., 0] 4712 s1 Spanne m1 − n1 .. .. .. . . . 4711+i-1 si−1 4711+i a[n1, ..., ni] Fiktive Anfangsadresse: Spanne mi−1 − ni−1 erstes Feldelement a0 = α + i − (n1 + s1 (n2 + s2 (. . . ))) Adresse des Elements a[k1, ..., ki] (mit nq ≤ kq ≤ mk für 1 ≤ q ≤ i): a0 + k1 + s1 (k2 + s2 (. . . )) Beispiel: 1 2 4 Einfacher Algorithmus auf Feldern (alle Feldelemente addieren) v := 0; for i := 1 to n do v := v + a[i] od Werte in der MI: R0 Anfangsadresse der Feldelemente (zeigt auf das erste Feldelement a[1]) R1 Zähler R2 Abbruchswert R3 Aufnahme des Resultats 1 2 4 6 8 MOVE W I n, R2 CLEAR W R3 CLEAR W R1 for: CMP W R2, R1 JLT ende ADD W !R0/R1/, R3 ADD W I 1, R1 JUMP for ende: HALT 4.2.6 Symbolische Adressierung Auch die relative Adressierung ist noch sehr fehleranfällig, da der Programmierer in Registern und Zahlen sein Programm verstehen muss. Bequemer und weniger fehleranfällig ist das Arbeiten in Programmen mit frei gewählten Bezeichnungen. Bei Assemblerprogrammen sprechen wir von symbolischen Adressen. Bei frei gewählen Bezeichnungen sind folgende Prinzipien zu beachten: • sprechende Bezeichnungen 50 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG • data dictionary anlegen: ein Verzeichnis aller Begriffe und deren Bedeutung • Codierungsstandards für die Wahl von Bezeichnungen Vor der Ausführung des Programms werden die Bezeichnungen vom Assemblierer durch Adressen ersetzt. Dazu benutzt man eine Datenstruktur Adressbuch, in der die Beziehung Bezeichnung – relative Adresse festgelegt ist. Dabei ist darauf zu achten, dass diese Zuordnung so gewählt ist, dass das Programm effizient ist: • wenig Speicher verbraucht • kurze Ausführungszeiten hat Dazu werden Programme gezielt optimiert (unter Einsatz von Pipelining und Caching). 4.2.7 Geflechtsstrukturen und indirekte Adressierung Bei problemorientierten Programmiersprachen findet sich das Konzept Referenz / Verweis / Zeiger / Pointer. Auf der MI-Ebene bilden wir Referenzen auf Adressen ab. Die Referenz auf einen Wert oder eine Variable wird zu der Adresse der Speicherzelle, die diesen Wert enthält oder diese Variable repräsentiet. Bei umfangreichen Datenpaketen / -strukturen (beispielsweise Feldern) entsprechen die Referenzen den Anfangsadressen des Datenpakets. Referenzen entsprechen den Konzept der indirekten Adressierung. Referenzen / Verweise bieten große Vorteile für die Effizienz von Programmen: • Statt großer Datenpakete können wir in Prozedur- / Unterprogrammaufrufen die Verweise / Anfangsadresse übergeben. • In Geflechtsstrukturen können wir gewisse Daten, die für eine Reihe von Programmelementen bedeutsam sind, einheitlich verwalten (data sharing). Beispiel: Eine Typvereinbarung für Pointer / Records, aus der wir verkettete Listen (aber auch Bäume) aufbauen können, lautet wie folgt: 1 2 4 6 type rlist = ^slist; list = record inhalt: s; naechster, letzter: rlist end; Die Abbildung auf den Speicher (MI) könnte - unter der Annahme, dass der Typ s der Kennung W entspricht - so aussehen, dass ein Knoten aus 3 * 4 Bytes besteht, von denen je ein 4-Byte-Block die Adresse naechster, letzter und den abgelegten Wert enthält. Wir nehmen an, dass durch die Listenelemente eine zweifach verkettete Liste aufgebaut wird, wobei im ersten Element der Verweis letzter nil ist und im letzten Listenelement der Verweis naechster nil ist. Durchlaufen der Liste: 1 2 4 zeiger := listenanfang; while zeiger <> nil do if zeiger^.inhalt = suchmuster then goto gefunden fi; 4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG 6 8 zeiger := zeiger^.naechster od goto nicht_gefunden R0 Darstellung in der MI: R1 R2 1 2 4 6 Zeiger Listenanfang Suchmusteradresse MOVE W R1, R0 loop: CMP W I 0, R0 JEQ nicht_gefunden CMP W !R2, 8+!R0 JEQ gefunden MOVE W !R0, R0 JUMP loop Programm für das Anhängen eines Listenelements am Ende der Liste: 1 2 4 6 8 10 12 14 if listenanfang <> nil then zeiger := listenanfang; while zeiger^.naechster <> nil do zeiger := zeiger^.naechster od; zeiger^.naechster := neu; neu^.letzter := zeiger; neu^.inhalt := ...; neu^.naechster := nil else listenanfang := neu; neu^.letzter := zeiger; neu^.inhalt := ...; neu^.naechster := nil fi R0 R2 R3 R4 1 2 4 6 8 10 12 14 16 Zeiger Listenanfang neu inhalt MOVE W R2, R0 CMP W I 0, R0 JNE loop CLEAR W !R3 CLEAR W 4+!R3 MOVE W R4, 8+!R3 MOVE W R3, R2 JUMP ende loop: CMP W I 0, !R0 JEQ listenende MOVE W !R0, R0 JUMP loop listenende: MOVE W R3, !R0 CLEAR W !!R0 MOVE W R0, 4+!!R0 MOVE W R4, 8+!!R0 51 52 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG 4.2.8 Speicherverwaltung Für die Ausführung eines Maschinenprogramms mit komplexen Daten ist eine Speicherorganisation erforderlich. Darunter verstehen wir die Systematik der Ablage von Programm und Daten im Speicher. Ziele dabei sind: • effiziente Nutzung des Speichers • einfache, gut handhabbare Strukturen Beobachtungen: • Praktisch alle Programme benötigen während ihrer Ausführung zusätzlichen Speicherplatz, der stückweise belegt und wieder freigegeben wird. • Oft werden Speicherplätze in der umgekehrten Reihenfolge, in der sie belegt werden, wieder freigegeben (Stack-Prinzip) - der zuletztbelegte Platz wird zuerst wieder freigegeben. • Für gewisse Datenstrukturen eignet sich das Stack-Prinzip nicht (Zeiger- / Referenzstrukturen). Damit nutzen wir drei Arten der Verwaltung von Speicher: • statischer Speicher: Hier werden alle Daten abgelegt, die über die gesamte Laufzeit des Programms benötigt werden. • Stack: Hier werden alle Daten abgelegt, die nach dem Stack-Prinzip benötigt und freigegeben werden. Bei der Blockstruktur wird beim Betreten eines Blocks der Speicher für die lokalen Variablen (bei Prozeduraufrufen für Parameter) bereitgestellt und beim Verlassen des Blocks wieder freigegeben. • Listenspeicher (Heap): Hier werden alle Daten abgelegt, deren Speicherplätze nicht im Stack verwaltet werden können. Dabei wird im Quellprogramm in der Regel Speicherplatz explizit angefordert (Kreieren von Objekten, new/allocate zum Bereitstellen von Speicher für Zeiger). Die Freigabe von Speicher erfolgt: – entweder explizit (deallocate) Vorteil: Der Programmierer kann die Speicherverwaltung kontrollieren. Nachteil: Erhöhte Komplexität der Programme, Gefahr ins Leere zeigender Referenzen (dangling references). – oder implizit durch Speicherbereinigung (garbage collection). Dabei wird durch Erreichbarkeitsanalyse festgestellt, auf welche Zeigerelemente / Objekte noch zugegriffen werden kann; die nicht mehr erreichbaren werden entfernt. Vorteil: erheblich einfacher, keine Fehlergefahr durch Zeiger ins Leere. Nachteil: weniger effizient, Echtzeitverhalten der Programme kaum vorhersagbar. 4.2.9 Stackverwaltung von blockstrukturierten Sprachen Eine Programmiersprache heißt blockstrukturiert, wenn lokale Variablen / Konstanten verwendet werden, die in abgegrenzten Bindungsbereichen deklariert werden, soweit keine Referenzen / Zeiger auftreten. Für lokale Bezeichner / Variablen unterscheiden wir: • Deklaration / Bindung (genau ein Mal) • benutzendes Auftreten 4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG 53 Da die gleiche Bezeichnung in unterschiedlichen Bindungsbereichen gebunden werden kann, ist die Zuordnung zwischen Bindung und Nutzung wichtig (vgl. statische gegen dynamische Bindung). Blockstrukturen entsprechen Klammerstrukturen. Durch Durchnummerieren der Blockklammern kann jeder Block eindeutig gekennzeichnet werden. Durch dieses Verfahren können wir Blöcke eindeutig kennzeichnen. Blockschachtelungstiefe (BST): Anzahl der einen Block umfassenden Blöcke + 1. Die Blöcke definieren einen Baum. Beispiel: 1 2 4 6 8 10 Die Blockstruktur begin begin begin end begin end end begin end end entspricht dem Baum 1 1.1 1.2 1.1.1 1.1.2 In diesem Fall bestimmt die statische Struktur der Blöcke die Struktur des Stacks. Beispiel: 1 2 4 Blockorientiertes Programm mit Prozeduraufruf | // 1 proc p =: | // 1.1 ... | // 1.1 6 8 | // 1.2 ... | // 1.2 10 12 14 16 | // 1.3 ... p; ... | // 1.3 | // 1 Hier unterscheiden wir für jeden Abschnitt im Stack (der den lokalen Variablen eines Blocks entspricht) den statischen Vorgänger (in der Aufschreibung der nächste umfassende Block) und den dynamischen Vorgänger (in der Ausführung der nächste umfassende Block) (s. Abb 4.1). In der Speicherorganisation für blockorientierte Sprachen verwenden wir einen Stack, in dem jeweils auch der Verweis auf den statischen und dynamischen Vorgänger sowie die Blockschachtelungstiefe abgelegt wird. Ein Prozeduraufruf entspricht 54 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG 1.1 1 1 ... 1.3 1.3 1.3 1 1 1 1 Abbildung 4.1: Blöcke auf dem Stack imer dem Betreten eines Blocks. Jeder Speicherabschnitt auf dem Stack enthält schematisch folgende Informationen: • statischer Vorgänger • dynamischer Vorgänger • Blockschachtelungstiefe • Rückkehradresse (bei Prozeduraufrufen) • Parameter (bei Prozeduraufrufen) • Resultat (bei Prozeduraufrufen) • lokale Variablen 4.3 Techniken maschinennaher Programmierung Ein komplexes maschinennahes Programm kann kaum in einem Anlauf gleich als Maschinenprogramm geschrieben werden. Geschickter ist es, zunächst ein problemorientiertes Programm zu schreiben, dies zu analysieren und zu verifizieren und dann in ein Maschinenprogramm umzusetzen. Die Umsetzung erfolgt • entweder per Hand • oder durch ein Übersetzungsprogramm (Compiler) Typische Bestandteile höherer Programmiersprachen: • Datenstrukturen • Blockstrukturen • Ablaufstrukturen – Ausdrücke – Zuweisungen – if - then - else – Wiederholungsanweisungen – Funktionen, Prozeduren Maschinennahe Umsetzung: • Speicherung • Umsetzung in Konzepte maschinennaher Sprachen (s.u.) 4.3. TECHNIKEN MASCHINENNAHER PROGRAMMIERUNG 4.3.1 55 Auswertung von Ausdrücken / Termen Ein Ausdruck besteht aus einem Gebirge von Funktionsaufrufen. abs(ab − cd) + ((e − f )(g − j)) + abs - * - - * efgj * a bcd Zugehöriges Single-Assignment-Programm: 1 2 4 6 8 h1 h2 h3 h4 h5 h6 h7 h8 := := := := := := := := a * b; c * d; h1 - h2; abs (h3); e - f; g - j; h5 * h6; h4 + h7; := := := := := := := := a * b; c * d; h1 - h2; abs (h1); e - f; g - j; h2 * h3; h1 + h2; oder: 1 2 4 6 8 h1 h2 h1 h1 h2 h3 h2 h1 Zur Optimierung des Speicherbedarfs beim Auswerten von Ausdrücken verwenden wir statt Hilfsvariablen einen Stack. Operationen finden immer auf dem ersten Element des Stacks statt. Deshalb kann das erste Element auch in einer speziellen Hilfsvariable AC gehalten werden. Operationen: • lege Wert auf den Stack • führe ein- oder zweistellige Operation auf dem Stack aus Das Arbeiten auf dem Stack zur Auswertung eines Ausdrucks kann einfach durch die Postfix-Darstellung des Operatorbaums erreicht werden. im Beispiel: 1 2 4 a b * c d * - abs e f - g j - * + AC := a push AC; AC := b AC := top * AC; pop push AC; AC := c 56 6 8 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG push AC; AD := d AC := top * AC; pop AC := top - AC; pop AC := abs (AC) ... In der MI können wir ohne Verwendung eines Registers direkt auf dem Stack arbeiten: 1 2 MOVE W a, -!SP MULT W !SP+, !SP ... Wir schreiben nun ein Programm, das einem kleinen Ausschnitt aus einem Compiler entspricht. Es nimmt die Umsetzung von Ausdrücken in eine Sequenz von Operationen auf dem Stack vor. (Das erzeugte Programm legt schließlich das Ergebnis in AC ab, der Stack ist nach Ausführung des Programms wieder im Anfangszustand.) Wir benötigen eine Datenstruktur zur Darstellung von Operatorbäumen. 1 2 sort expr = nullary (symbol sym) | mono (op op, expr e0) | duo (expr e1, op op, expr e2) Dabei nehmen wir an, dass die Sorten symbol (Menge der Identifikatoren für Werte) und op (Operationssymbole) gegeben sind. Im Folgenden wird ein Programm angegeben, das einen Operatorbaum als Eingang nimmt und ein Stackprogramm zur Auswertung des durch den Operatorbaum gegebenen Ausdrucks ausdruckt: 1 2 4 6 8 10 12 proc eaf = (expr e): if e in nullary then print ("AC := ", sym(e)) elif e in mono then eaf (e0(e)); print ("AC := ", op(e), "AC") else eaf (e1(e)); print ("push AC"); eaf (e2(e)); print ("AC := top ", op(e), "AC"); print ("pop") fi 4.3.2 Maschinennahe Realisierung von Ablaufstrukturen Eine Zuweisung 1 x := E wird umgesetzt in (sei e der Operatorbaum zu E): 1 2 eaf (e); x := AC Die bedingte Anweisung 1 if E = 0 then S1 else S2 fi wird wie folgt umgesetzt (sei e der Operatorbaum zu E): 4.3. TECHNIKEN MASCHINENNAHER PROGRAMMIERUNG 1 2 4 6 8 t: 10 12 57 eaf (E); if AC = 0 then goto t fi ..... (Maschinencode fuer S2) ..... goto ende ..... (Maschinencode fuer S1) ..... ende: Die Wiederholungsanweisung 1 while E <> 0 do S od wird zu: 1 2 4 6 8 while: eaf (e); if AC = 0 then goto ende fi .... (Maschinencode fuer S) .... goto while ende: Im Folgenden schreiben wir ein Programm, das die Umsetzung (Übersetzung) eines while / if - Programms in ein maschinennahes Programm vornimmt. Wir benötigen eine Sorte zur Repräsentation von Programmen: 1 2 4 sort statement = assign (symbol x, expr e) | if (expr cond, statement st, statement se) | while (expr e, statement s) | seq (statement s1, statement s2) Im Übersetzungsvorgang werden zusätzlich Marken für Sprünge benötigt. Wir nehmen an, dass wir zu einer gegebenen Marke m in der Sorte label Über eine Funktion 1 fct next = (label) label verfügen, so dass m, next(m), next(next(m)), . . . eine Folge unterschiedlicher Marken liefert (Generator für Marken). Programm für das Erzeugen eines maschinennahen Programms: 1 2 4 6 8 10 proc gen = (statement z, var label m): if z in assign then eaf (e(z)); print (x(z), " := AC") elif z in seq then gen (s1(z), m); gen (s2(z), m) elif z in if then label me := m; label mt := next (m); 58 12 14 16 18 20 22 24 26 28 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG m := next (mt); eaf (cont(z)); print ("if AC = 0 then goto ", mt, " fi"); gen (se(z), m); print ("goto ", me); print (mt, ":"); gen (st(z), m); print (me, ":") else label mw := m; label ma := next (m); m := next (ma); print (mw, ":"); eaf (c(z)); print ("if AC = 0 then goto ", ma, "fi"); gen (s(z), m); print ("goto ", mw); print (ma, ":") fi