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.


vorheriges
Index
nächstes