Systemnahes Programmieren 4/10
Hintergrundwissen
Vorwort zu diesem Kapitel:
Ich als C++-Programmierer halte recht wenig vn der Systemnahen Programmierung. Ich versuche für gewöhnlich so portabel, wie möglich zu sein. Da QB aber nur Programe für DOS erstellt muss man keine Rücksicht auf Portierbarkeit nehmen. Also kann man zur Steigerung der Geschwindigkeit oder der Möglichkeiten, die in QB beide sehr beschränkt sind, guten Gewissens Systemnah programmieren.
Falls Sie sich bereits mit der Systemnahen Programmierung in QB auskennen können Sie dieses Kapitel getrost überspringen, da Sie hier nichts neues lernen. Unter Systemnah wird hier speziel das Verwenden von 'PEEK' und 'POKE', sowie der Umgang mit 'CALL Interrupt' und 'CALL Absolute' verstanden.
Adressen:
Jeder Ort im Arbeitsspeicher hat eine Adresse. Da wir uns im Real-Mode befinden bsteht eine solche Adresse aus zwei Teilen: Dem Segment und einem Offset. (Was der Rel-Mode ist hat uns im Moment nicht zu interessieren. Nur soviel: weil wir uns im Real-Mode befinden können wir nur die 640KB konventionellen speicher verwenden, statt den gesammten Arbeitsspeicher.)
Ein Segment ist jeweils ein Abschnitt aus 64Byte im Speicher. Mit dem Offset kann man die genaue Position innerhalb des Segments bestimmen. Was interessant ist, ist dass das Offset größer sein kann, als alle Positionen innerhalb eines Segments. So kann man mittels des Offsets über das segment hinweg in die darauf folgenden Segmente zeigen.
+-----------------+-----------------+-----------------+--- | erstes Segment | zweites Segment | ... | +-----------------+-----------------+-----------------+--- |-----> mögliches Offset |-----------> anderes Offset |-----------------------------> segmentübergreifendes Offset
Die Angabe, an welcher Stelle im Speicher Sie gerade lesen oder schreiben wollen, erfolgt immer durch eine Adresse. Sowohl das Segment, als auch das Offset werden durch eine 16-Bit Zahl dargestellt. Eie übliche Art der Notation ist: Segment:Offset, wobei meistens hexadezimalzahlen verwendet werden, was aber durch ein nachgestelltes 'h' gekennzeichnet wird: '55E3:002Bh'.
Man könnte denken, urch zwei Mal 16Bit, also 32Bit ließen sich 232Byte, also 4GB adressieren. Dummerweise konnte man sich früher nicht vorstelen, das jemand jemals mehr als einen 1MB an Speicher braucht. Deshalb hat man festglegt, dass sich Segment und Offset überlagern und so effektiv 20Bit entstehen, mit denen 1MB adressiert werden kann:
SSSS SSSS SSSS SSSS < Segment-Bits OOOO OOOO OOOO OOOO < Offset-Bits
Durch diese Überlappung ergibt sich zudem der manchmal nervige Effekt, das ein und die selbe Adresse durch verschiedene Kombinationen von Segmenten und Offsets dargestellt werden kann. Das konnten Sie bereits an der Tatsache sehen, dass es segmentübergreifende Offsets gab, die genau durch diesen Effekt zustande kommen.
Umgang mit Adressen:
Wie gesagt hat jede Stelle im Speicher eine Adresse. Die Adresse einer Variable erhalten Sie durch 'VARSEG' und 'VARPTR'. Ersteres liefert das Segment, letzteres das Offset. Eine ausnahme bilden hier Strings mit variabler Länge. Variable länge bedetet, dass Sie kein 'STRING * konstante' verwendet haben.
Sie teilen QB mit 'DEF SEG' mit, in welchem Segment Sie arbeiten wollen. Innerhalb dieses Segments können Sie dann mit 'PEEK' und 'POKE' lesen bzw. schreiben. Beide erwartetn dabei das Offset als Parameter.
Damit Sie das folgende Beispiel verstehen errinere ich nchmal kurz daran, dass 'INTEGER' 2Byte groß ist:
DIM quelle AS INTEGER quelle = 5 DIM byte1 AS INTEGER, byte2 AS INTEGER DEF SEG = VARSEG(quelle) byte1 = PEEK(VARPTR(quelle)) byte2 = PEEK(VARPTR(quelle) + 1) DIM ziel AS INTEGER DEF SEG = VARSEG(ziel) POKE VARPTR(ziel), byte1 POKE VARPTR(ziel) + 1, byte2
Dieses Beispiel macht auf umständliche Weise das, was Sie auch per 'ziel = quelle' machen könnten.
Allerdings macht es nur äußerst selten Sinn Variablen auf diese weise zu manipulieren. Viel interessanter Sind Adressen, die DOS für sich selbst verwendet. Bei 'A000:0000h' Beginnt beispielsweise der Speicher dar Grafikkarte, falls sie sich in einem Grafischen Modus befinden und bei 'B800:0000h' im Text-Modus.
SCREEN 13 DEF SEG = &HA000 POKE 0, 15 SLEEP
Das letzte Beispiel ist ein komplettes Programm. Es setzt den Pixel ganz oben Links auf 15, was bekanntlich weiß entspricht. Bit diesm wissen können Sie ihr eigenes 'PSET' schreiben:
DEF SEG = &HA000 POKE y * 320& + x, farbe
Diese Variante ist sogar schneller als das normale 'PSET', wenn Sie nicht vor jedem Pixel das Segment neu angeben und das ganze nicht durch einen 'SUB'-Aufruf verlangsamen. Wenn Sie jedoch mehr als einen Pixel setzen wollen ist das Beispiel eine attraktive Variante, die wenn es um Geschwindigkeit geht 'PSET' vorzuziehen ist.
Es gibt noch einige weitere interessante Adressen, von denen Sie die wichtigsten im Laufe des Textes kennen lernen werden.
'CALL Interrupt' und 'CALL Absolute'
Damit diese 'SUB's aufgerufen werden können, müssen Sie QB mit 'QB /l' bzw. 'QBX /l' starten. Wozu brauche ich diese Prozeduren?
Zunächst zu 'CALL Absolute': Mit 'CALL Abslute' kann dynamisch geladener Assemblercode Ausgeführt werden. Assembler ist eine Sprache, die quasi direkt auf Maschienenebene arbeitet. Auf diese weise können beispielsweise Maus-Routinen geschreiben werden und einige weitere interessante Dinge. Dieser Text ist jedch kein Assmbler-Tutorium. Deshalb werde ich es hierbei belasssen.
Nun zu 'CALL Interrupt', dass auch noch in einer weiteren Version 'CALL InteruptX' existiert. Es gibt sogenannte Software-Interrupts, nicht zu verwenchseln mit Hardware-Interrupts, mit denen Sie möglicherweise schonmal beim einrichten des Systems in berührung gekommen sind. Software-Interrupts sind kleine Funktionen, die vom Betriebssystem oder direkt vom BIOS bereitgestellt werden. Interrupt '33h' Beispielsweise bietet Zugriff auf die Maus.
Befor Sie Interrupts verwenden können, müssen Sie erstmal näheres über Ihre CPU erfahren. Intern besitzt Sie einige kleine Speicher, die Sie verwendet, um zu rechnen, usw. Diese heißen Register. Die CPU besitzt nur recht wenige davon. Sie heißen 'AX, 'BX', 'CX' und 'DX'. Des weiteren existieren noch einige Register zum speichern von Segmenten, Offsets und dann noch eins für Flags. Folgender Typ wird benötigt, damit Sie mit 'CALL Interrupt' arbeiten können:
TYPE RegType ax AS INTEGER bx AS INTEGER cx AS INTEGER dx AS INTEGER bp AS INTEGER si AS INTEGER di AS INTEGER flags AS INTEGER END TYPE
Für 'CALL InterruptX' benötigen Sie folgende:
TYPE RegTypeX ax AS INTEGER bx AS INTEGER cx AS INTEGER dx AS INTEGER bp AS INTEGER si AS INTEGER di AS INTEGER flags AS INTEGER ds AS INTEGER es AS INTEGER END TYPE
Um einen Software-Interrupt aufzurufen brauchen Sie seine Nummer (um eine vollständige Liste aller Interrupts zu erhalten sollten eine Suchmaschiene ihrer Wahl verwenden). Jeder dieser Unterrupts besteht aus vielen kleinen Funktionen. Diese wird für gewöhnlich durch den Wert in 'ax' ausgewählt. Alle weiteren Parameter, die die Interrupt Funktion benötigt erhält Sie durch die übrigen Register.
Damits nicht zu theoretisch wird demonstriere ich das jetzt mal an einem Beispiel:
DIM register AS RegType register.ax = 1 CALL Interrupt(&H33, register, register)
Dieses Beispiel schaltet die Maus ein. Mit '2' in 'ax' können Sie sie wieder ausschalten, mit '3' können Sie die Position und den Status der Maustasten abfragen, usw.
In und Outputs:
Um mit den Karten im System zu komunizieren können Sie 'IN', 'OUT' und 'WAIT' verwenden. Für alle Drei benötigen Sie einen sogenannten Port. Für einen Schnellen Umgang mit der Palette verwenden Sie beispielsweise die Ports '3C7h', '3C8h' und '3C9h'. Näheres dazu erfahren Sie im apitel 'Tipps'.
Warum eigentlich 'interrupt' und was andere Tutorien s falsch machen
Warum heißen Interrupts eigentlich Interrupts? Dieses Thema wir oft in Tutorien, die sich mit diesem Thema beschäftigen versucht zu erklären. Einige beantworten es jedoch föllig falsch. Sie sagen Sowas, wie der Begriff Interrupt sei eigentlich falsch gewählt, da es sich um BIOS oder System-API-aufrufe handele.
Das ist natürlich quatsch. Warum sollten sie Intterupt heißen, wenn es ein unpassender Name ist? Interrupts werden mittels des Assemblerbefehls 'int' aufgerufen. Er unterbricht (english: to interrupt) das atuelle Programm und führt stattdessen eine leine Routine des Betriebsystems oder des BIOS aus.
Und nun noch ein paar komentare zu Auszügen aus anderen Tutorien zum Thema Interrupts, genauer zum Thema, wieso ich Interrupts benutzen sollte:
Kompatibilität - der Einsatz von Interrupten garantiert gleiche Funktionen bei unterschiedlichen Betriebssystemen.
Toll QB gibts nur für DOS.
Sicherheit des Quellcodes - das Programm stürzt nicht ab weil in irgend einer Library ein Bug sitzt.
Ich kann genauso gut Bugs einbauen, wie andere.
UND - wir wollen ja programmieren - nicht zusammenkleistern.
Das ist das kontraproduktivste und falscheste, was ich je in disem Zusammenhang gehört habe. Wir wollen das Rad nicht neu erfinden. Lieber auf bewährtes zurückgreifen, das funktioniert, als unnötig selbst was zusammenschustern, was wieder von neuem gedebut werden muss und zudem dann auchnoch unkompatibel ist.