Aufgabe 1: Ein-/Ausgabe
Die erste Grafikkarte, die IBM verkaufte, war dieser Color Graphics Adapter aus dem Jahr 1981. Für anfangs 1000 DM bekam man bis zu 16 Farben und hatte ganze 16 Kilobyte Videospeicher. Die Grafikauflösung betrug bis zu 640×200 Pixel – aber dann nur mit zwei Farben. Alle 16 Farben konnte man allerdings nur im Textmodus gleichzeitig nutzen. Und dieser Textmodus ist auch bei den Nachfolgern Enhanced Graphics Adapter (EGA) und Video Graphics Array (VGA) bestehen geblieben und wird von heutigen Systemen im Allgemeinen auch noch unterstützt.
Wir haben in diesem Textmodus 25 Zeilen mit je 80 Zeichen zur Verfügung, können also 2000 Zeichen gleichzeitig am Bildschirm anzeigen. Für jedes Zeichen brauchen wir zwei Bytes, das erste davon beinhaltet das darzustellende Zeichen selbst.
Der dazu verwendete Zeichensatz Codepage 437 trifft die druckbaren ASCII-Zeichen. In ASCII sind die ersten 32 Codes allerdings nicht druckbare Steuerzeichen, wie beispielsweise 10 für den Zeilenumbruch. Wenn wir hier diesen Wert als erstes Byte setzen, sehen wir jedoch einen Block mit einem Kreis – denn alle Werte haben im Zeichensatz eine grafische Repräsentation.
Das zweite Byte dient der Konfiguration der Darstellung. Neben 'Spezialeffekte" wie dem Blinken kann man damit die Farbe steuern.
Von den 16 Farben stehen als Hintergrundfarbe nur die ersten 8 zur Verfügung. Da sind allerdings alle relevanten Farben mit dabei, die anderen sind eigentlich nur aufgehellte Farbvarianten, welche nur bei der Vordergrundfarbe verwendet werden können.
Der Videospeicher für diese Bytes ist in unseren Arbeitsspeicher eingeblendet, ab Adresse 0xb8000
. Diese Adresse kommt etwas willkürlich vor, ist jedoch historisch bedingt, davor lag ab 0xb0000
der monochrome Textmodus. Ein an die erste Adresse gespeichertes Byte – hier das B
– wird an der ersten Stelle mit dem nachfolgenden spezifizierten Darstellungsattribut eingeblendet, das Byte an der Adresse 0xb8002
entspricht dann dem zweiten Zeichen am Bildschirm.
Der hier dargestellte Speicher würde am Bildschirm in Rot das Wort "Bar" auf hellgrauem Hintergrund ausgeben, wobei das erste Zeichen blinken wird. Bitte beachten: Das Blinken wird nicht zwangsläufig im Emulator dargestellt, da das höchstwertige Bit auch anders interpretiert werden kann – aber die Farben sollten übereinstimmen!
Um jetzt das Darstellungsattribut nach unseren Wünschen zu setzen, kann man die im Grundstudium gelernten Bitoperationen verwenden: Bitweises Und, Oder, exklusives Oder, Negation oder das Shiften – was je stelle einer Multiplikation bzw. Division mit 2 entspricht. Um eine bessere Lesbarkeit zu ermöglichen, sollte man jedoch nicht wie hier die dezimale Repräsentation verwenden, sondern besser Hex-Werte, oder konstante Variablen mit entsprechenden Namen.
Um jetzt beispielsweise in einer Variable x
das n-te Bit zu setzen oder zu löschen, verwendet man in der Regel diese gut lesbaren Konstrukte.
Noch lesbar sind allerdings Bitfelder. Felder in Strukturen können mit einer Länge definiert werden, hier fg
für foreground mit 4 bit und background mit 3 bit sowie blink mit einem Bit. Der Zugriff ist dann wie bei Strukturen üblich. Eine intuitive Art die Attribute zu definieren.
Die gesetzten Werte entsprechen dann einem Byte mit diesem Speicherlayout, das erste Feld, foreground, beginnt beim niedrig wertigsten Bit. Nach dem C Standard ist die Reihenfolge übrigens nicht spezifiziert, aber auf dem x86 bilden alle relevanten Übersetzer das wie im Beispiel ab. Allerdings gibt es bei Strukturen Fallstricke, die man wissen sollte, wenn man eine gewisse Darstellung im Speicher erwartet.
Diese Struktur hat ein char
Array, drei Elemente zu je ein Byte, einen int
, welcher auf der x86 Architektur 4 Byte belegt, sowie ein bool
, welcher wieder ein Byte braucht.
Somit haben wir 8 Byte an Nutzdaten, allerdings belegt es 12 Bytes. Grund sind Füllbytes, die der Übersetzer einbaut – dadurch ausgerichtete Werte führen zu einem schnelleren Zugriff. Aber wenn wir ein bestimmtes Layout im Speicher brauchen, können wir den Übersetzer instruieren solche Optimierungen zu unterlassen, und zwar durch die Attribuierung packed
. Nun hat die Struktur die erwartete Größe.
Da solche Fehler – eine Struktur hat nicht die geforderte Größe – gar nicht so einfach zu erkennen sind, empfiehlt es sich in solchen Fällen eine statische Zusicherungen anzugeben: Hiermit wird beim Übersetzen die Größe geprüft – und bei einem Fehler abgebrochen. Es wird mit diesen Zusicherungen kein Code erzeugt, es hat somit keine Auswirkung auf die eigentliche Laufzeit und darf entsprechend reichlich verwendet werden.
Zurück zum Bildschirm – um die aktuelle Position bei der Texteingabe zu sehen ist eine Schreibmarke – auch Cursor genannt – hilfreich. Die Position können wir natürlich einfach in einer Variable speichern... Oder wir nehmen die entsprechende Funktionalität in unserer Hardware. Die CGA Karte hatte 18 Steuerregister, wovon viele noch für die analogen Kathodenstrahlröhrenbildschirme bestimmt sind – welche heutzutage zum Glück jedoch keine Relevanz mehr haben.
Uns interessieren deshalb nur die Register 14 und 15. Wir brauchen beide, da wir bei den 25 Zeilen mit 80 Zeichen 2000 mögliche Positionen für den Cursor adressieren können, jedoch ein Register nur 8 Bit fasst. Nun, der Cursor kann in der Tat sogar Position bis 8000 annehmen, der CGA Textmodus bietet nämlich noch drei weitere Bildschirmseiten an, welche im Speicher direkt hintereinanderliegen. Register 12 & 13 erlauben dies theoretisch zu konfigurieren, aber da diese Funktionalität keinen echten Mehrwert für uns bietet ignorieren wir sie, und konzentrieren uns nur auf den Cursor für die ersten Bildschirmseite.
Hier werden uns dadurch Stolpersteine in den Weg gelegt, dass ein direkter Zugriff auf die Werte der Steuerregister nicht möglich ist: Wir müssen den Umweg über ein Index- und Datenregister nehmen, auf welche wir über I/O Ports zugreifen können.
Das Lesen der Cursorposition läuft dann wie folgt ab: Wir schreiben den Index des gewünschten Steuerregisters – zum Beispiel 15
für Cursor low – mittels der out
-Instruktion in das Indexregister. Anschließend können wir mittels in
-Instruktion vom Datenregister den niederwertigen Teil des derzeitigen Cursors in eine lokale Variable lesen. Für den höherwertigen Teil schreiben wir 14
für Cursor high in das Indexregister, und dann erscheint im Datenregister der höherwertige Teil der Cursorposition, welchen wir wieder mittels in
lesen können. Und haben nun unsere Cursorposition. Natürlich können wir auch die Cursorposition setzen, in dem wir nun mittels out
den neuen Wert in das Datenregister schreiben. Und sofern der Cursor vom Bildschirm verschwindet, haben wir hier wohl einen zu hohen Wert eingegeben.
Der CGA Textmodus demonstriert dabei gut die beiden hauptsächlichen Methoden wie ein Geräte angesteuert wird: Entweder ist es im Arbeitsspeicher eingeblendet, wie der Video RAM, in dem wir die anzuzeigenden Zeichen schreiben. Oder über I/O Ports wie beim Cursor mittels in
- und out
-Instruktion.
Unsere CPU ist dabei über den Front-Side-Bus mit der Northbridge verbunden, auch Memory Controller Hub genannt, welcher Zugriff auf den Arbeitsspeicher bietet, aber auch moderne Grafikkarten bedient. Von dort geht es weiter zur Southbridge, dem I/O Controller Hub, ursprünglich für die Peripherie verantwortlich
Legen wir nun eine Adresse auf den Bus, z. B. 0002
, so kann dies sowohl Arbeitsspeicher als auch I/O sein.
Diese Mehrdeutigkeit wird durch den Einsatz eines Steuerbusses gelöst: Die out
-Instruktion setzt beispielsweise die Leitung entsprechend, dass nur der I/O Controller sich angesprochen fühlt und den Inhalt über den Datenbus liest. Aus diesem Grund sind für I/O Ports die in/out Instruktionen auf der x86 Architektur notwendig.