gfx gfx
gfx
homegfxgaestebuchgfxforumgfxkontaktgfximpressum
gfx
QBasic
  1. QB-Forum
  2. QB-Forum
  QB-MonsterFAQ
  Tutorials
AK-LIB
  Download
  Archiv
  Hilfe
  TroubleFAQ
  Tools
Projekte
  DosFont
Extras
  Links
Statistik
Tutorials
Assembler in Qbasic
Autor: Andre Klein

         Vorwort   letzte Aktualisierung: 24.07.2003
In diesem Tutorial geht es darum Assembler-Grundkenntnisse zu erwerben und wie man Assembler-Programme in Qbasic einbaut. Desweiteren gibt es ganz am Ende eine kleine Liste mit den wichtigsten Befehlen.

       Abschnitt 1   Was ist Assembler?
Assembler ist die "Programmier"-Sprache die der Prozessor als einzige versteht.
Das heißt das Assemblerbefehle nur aus 0en und 1en bestehen.
Da das aber zu kompliziert zum programmieren ist hat man alle Befehle in 8-Bit-Blöcke unterteilt. Das heißt das 1 Befehl = 1 BYTE ist.
Wenn der Prozessor mehr Informationen braucht dann nimmt er einfach das nächste BYTE dazu usw.
Diese BYTE-Kombinationen werden in HEX dargestellt.
ein Beispiel:
B83412 würde für den Prozessor bedeuten das er das REGISTER AX mit dem Wert &H1234 abspeichert.
Aber irgendwie kann man sich das auch nicht wirklich gut merken, also hat man sich noch was einfallen lassen: Die sogenannten MNEMONIC's.
Mnemonics sind einfach nur Alternativ-Befehle die man sich viel besser merken kann und die dann in die jeweilige HEX-Kombination umgewandelt werden.
Damit man aber mit Mnemonic's arbeiten kann, braucht man ein entsprechendes Programm dafür. Ein Beispiel wäre DEBUG.EXE das standartmäßig mit DOS und WIN mitgeliefert wird.
Da dieses Programm am meisten verbreitet ist werde ich mich darauf in allen Erklärungen beziehen.
Aber bevor wir uns mit den Mnemonics beschäftigen schauen wir uns erstmal einige andere Dinge an.

       Abschnitt 2   Was ist Register?
Die sogenannten REGISTER sind quasi VARIABLEN mit denen der Prozessor seine Operationen ausführt.
Du kannst sie mit Variablen aus QB vergleichen. Nur das es eine bestimmte Anzahl Register gibt und sie immer den gleichen Namen haben. Ausserdem sind es 16-BIT Register, also INTEGER, und können Werte von 0 - 65535 annehmen.
Alle Register auf einen Blick:
AX - Akkumulator
BX - Adressregister
CX - Zählregister
DX - Adressregister für I/O
CS - CodeSegment
DS - DatenSegment
SS - StapelSegment
ES - ExtraSegment
SP - Stapelzeiger
BP - Basiszeiger
SI - Quell-Register
DI - Ziel-Register
IP - ProgrammZähler
F - FLAGS

Jetzt ist es so, das bestimmte REGISTER zusammengehören und einige "frei" sind.

CS und IP
im Segment CS und dem Offset IP befindet sich IMMER der Befehl der gerade vom Prozessor ausgeführt werden soll. Wenn der Prozessor fertig ist dann erhöht er automatisch diesen Zähler um die aktuelle Befehlslänge und führt den nächsten Befehl aus. Für CS und IP gibt es KEINE Rechenoperationen da das viel zu gefährlich wäre.

SS, SP und BP
Im Segment SS und dem Offset SP liegt der sogenannte STACK. Dort werden Werte abgelegt, Adressen und noch andere Dinge gespeichert.
Das Register BP arbeitet auch direkt mit dem Register SS zusammen und dient als "Extra-Stack-Pointer".

DS,ES, SI und DI
Mit diesen Registern können Datenbereiche im Speicher adressiert werden wobei DS und ES für Segmente stehen und SI und DI für Offset's.

AX, BX, CX und DX
Diese Register werden meistens für Rechenoperation genutzt und sind voneinander unabhängig.
Desweiteren werden diese Register in zwei 8-Bit-Register unterteilt. AX besteht aus AL und AH. BX besteht aus BL und BH. CX besteht aus CL und CH und DX besteht aus DL und DH.
Da jedes ganze Register ein 16-Bit-Register ist, sind die Teilregister nur 8-Bit breit. können also jeweils einen Wert von 0-255 annehmen.
ein kleinen Beispiel:
Alle Wertzuweisungen werden in HEX angegeben.
AH = 12
AL = 34
daraus ergibt sich das AX automatisch 1234 ist.
Man kann mit 8-Bit Registern oder mit 16-Bit-Registern rechnen. Wichtig ist nur zu wissen das wenn man z.B. AL verändert sich auch AX verändert oder umgekehrt: wenn man AX verändert verändert sich auch AL.

FLAGS (F)
Das Register F ist ein 16-Bit-Register das den Status der letzen Operation des Prozessors zurückgibt. Hier die wichtigsten:
- Carry-Flag (CF)
- Parity-Flag (PF)
- Auxiliary-Carry-Flag (AF)
- Zero-Flag (ZF)
- Sign-Flag (SF)
- Overflow-Flag (OF)
- Interrupt-Flag (IF)
- Direction-Flag (DF)

Je nachdem welcher Befehl ausgeführt wurde, werden die entsprechenden Flags gesetzt oder gelöscht. Zu sagen ist noch das Flags auch durch Programme verändert werden können!

       Abschnitt 3   Was sind Segment und Offset?
Mit Segment und Offset kannst du eine ganz bestimmte Zelle im Speicher adressieren.
Der Speicher ist organisatorisch gesehen wie eine Tabelle aufgebaut. Segment ist quasi wie eine Zeile und das Offset wie eine Spalte.
Jedes Segment hat 16 Byte.
Alle Segment- und Offsetangaben werden in HEX dargestellt.
ein Beispiel wäre: 0040:0002
Das würde heißen: SEGMENT = &H40 und OFFSET = &H2.

Der komplette Speicher beginnt am Segment 0000 und dem Offset 0000.
also 0000:0000
Das erste Segment geht von 0000:0000 bis 0000:000F
Das zweite Segment geht von 0001:0000 bis 0001:000F
Das dritte Segment geht von 0002:0000 bis 0002:000F

Jetzt ist es aber so das man für das Offset auch einen größeren Wert als &HF (15) eintragen kann. Was passiert dann?
Beispiel: 0040:0200
Das haut ja dann nicht mehr mit unseren 16 Byte pro Segment hin. Oder doch?
Doch es haut hin, denn alles was über &HF ist wird automatisch als Segment dazugerechnet.
In diesem Beispiel wäre die wirkliche Adresse: 0060:0000
Das heißt im endeffekt das man Adressen durch mehrere Kombinationen darstellen kann.
Adresse 0040:0200 = 0041:01F0 = 0042:01E0 = 0043:01D0 = 0050:0100 = 0060:0000.

Insgesamt können wir durch diese Addressierung 1048576 Byte Speicher adressieren. In diesem Speicher befindet sich das Betriebssystem, der Grafikbereich, der konventionelle Arbeitspeicher und und und.
Der XMS oder auch RAM genannt befindet sich außerhalb dieses Bereiches und ist mit der normalen Adressierung nicht zu erreichen. Aber das ist ein anderes Thema.

Falls die Beschreibung des Speicheraufbaus zu unverständlich oder zu kompliziert war dann mach dir nichts draus. Denn so nötig braucht man das nicht für die ersten Schritte in Assembler.
Das wirkliche Verstehen kommt später erst wenn man mit Assembler gearbeitet hat.

       Abschnitt 4   DEBUG.EXE - Wie schreibe ich eine COM-Datei?
So jetzt geht es ans eingemachte.
Warum soll ich denn eine COM-Datei schreiben?
Der Grund dafür ist der, das eine COM-Datei PUREN ASSEMBLER enthält. Er ist zwar als Maschinencode gespeichert, aber diesen CODE kann man später sehr einfach in seine QB-Programme übernehmen.

Das erste was wir machen ist die Datei DEBUG.EXE zu starten. Das sollte unbedingt aus einem VOLLBILD-DOS-FENSTER geschehen oder direkt unter DOS.
Wenn du nicht weißt wo sie ist dann suche sie mal im DOS oder WINDOWS-Verzeichnis.

Wenn du sie gestartet hast dann müßtest du jetzt folgendes sehen:

-_

Das ist jetzt unser Eingabefeld.
Wenn du jetzt mal ein ? eingibst und Enter drückst dann kommt eine Liste mit allen Befehlen die du eingeben kannst.
wie du siehst steht "q" für QUIT.
Und darunter ist wieder unser Eingabefeld.

So jetzt schreiben wir unser allererstes Programm in Assembler mit sogenannten Mnemonics.
Dazu gibt du jetzt ein "a" wie Assembler ein und drückst Enter.
Du müßtest jetzt das hier sehen:

xxxx:0100 _

Die Xe stehen für das Segment des aktuellen Code's. Diese Segmente können immer andere Werte haben, sind aber für die COM-Datei uninteressant.
Die 0100 sind das Offset und die Startadresse deines Assembler-Programms im Speicher. Dieses START-OFFSET ist immer gleich.

Jetzt siehst du wieder einen blinkenden Coursor. An dieser Stelle kannst du jetzt die Mnemonic eintragen.
Wir schreiben folgendes Programm.
Die Offset's dienen nur zur Orientierung und werden nicht mitgeschrieben. Nach jeder Eingabe Enter-Drücken nicht vergessen!
ich erkläre später was wir gemacht haben.

0100 PUSH CS
0101POP DS
0102 MOV AH, 09
0104 MOV DX, 010D
0107 INT 21
0109 MOV AH, 4C
010B INT 21
010D DB 48
010E DB 45
010F DB 4C
0110 DB 4C
0111 DB 4F
0112 DB 20
0113 DB 57
0114 DB 4F
0115 DB 52
0116 DB 4C
0117 DB 44
0118 DB 24

Wenn du das fertig eingegeben hast und er nicht gemeckert hat (wegen Schreibfehler) dann drückst du ohne irgendwas einzugeben nochmal Enter. Dann gelangst du zur Haupt-Eingabe zurück.
Jetzt müßen wir das ganze noch speichern.
Dazu müßen wir ihm zuerst den Namen der Datei sagen. Das machen wir mit "n":
- n hello.com
Dann drücken wir Enter.

Dann müßen wir ihm noch sagen wieviele BYTE's er denn speichern soll. Dazu geben wir "rcx" ein und drücken Enter.
Dann steht da CX und irgendeine Zahl. Darunter blinkt ein Courser. Dort geben wir jetzt 19 ein und drücken Enter.

Dann drücken wir jetzt nur noch "w" für schreiben und schon speichert er das ganze.

Wenn er das alles ohne Fehler gemacht hat, beenden wir Debug mit "q" und Enter.

Dann starten wir jetzt einfach mal unser Programm "hello.com".

Wenn wir alles richtig gemacht haben, müßte er jetzt "Hello World" auf den Bildschirm schreiben. Wenn nicht dann haben wir etwas falsch geschrieben. Wenn er sogar abstürzt dann haben wir ganz gewaltig was falsch geschrieben. Also, wenn es nicht geklappt hat dann nochmal das Programm überprüfen und evtl neuschreiben.

Wenn es jetzt geklappt hat, dann nehmen wir das Programm mal Schritt für Schritt auseinander.
Das heißt wir starten jetzt wieder DEBUG aber mit der Datei dran. Also debug hello.com. Wenn man es so eingibt lädt er das Programm was in hello.com ist.

Zur Sicherheit wollen wir uns das aktuelle Programm anschauen. Dazu geben wir "u 100" ein. Das bewirkt das wir das im Speicher befindliche Programm ab dem Offset &H100 jetzt sehen.
Wenn du das Programm bis zum Offset 010B wiedererkennst dann hat er es richtig geladen.
Wenn du dir jetzt mal diese Auflistung näher anschaust dann endeckst du einige Werte die vorher noch nicht dort waren. Das sind nämlich die HEX-Kombinationen für die jeweiligen Befehle.

Jetzt zu den einzelnen Befehlen:

PUSH CS
POP DS

Mit dem PUSH-Befehl speichert man den INHALT eines Registers im sogenannten STACK ab. Das ganze nennt man "retten".
Dabei behält das Register den aktuellen Wert. Dieser Wert wird quasi nur auf den STACK kopiert. Und der STACK-POINTER (SP) geht mit.
Bei unserem Beispiel PUSH CS wird das sogenannte CODE-SEGMENT (CS) auf den STACK gelegt. Das ist immer das Segment in dem unser Programm abläuft.

Das Gegenteil von PUSH ist POP. Mit dem Befehl POP DS sagen wir ihm das der zuletzt gespeicherte Wert vom STACK in DS geschrieben werden soll. Dort geht der STACK-POINTER (SP) wieder zurück.

Warum das ganze?
Im endeffekt geht es darum das wir DS mit dem WERT von CS laden wollen. Da es aber für CS keine Befehle gibt müßen wir uns mit PUSH und POP behelfen.
Wenn wir jetzt beide Befehle zusammennehmen haben wir nichts anderes als ein "LET DS = CS in QB oder LET DS = ProgrammSegment"

Einzelheiten zum STACK:
Den Aufbau des Stack's kann man wie einen STAPEL ansehen (das ist auch die deutsche Übersetzung). Das heißt man legt oben einen Wert rauf und kann dann auch nur von oben wieder einen Wert runternehmen.
Das ganze nennt sich dann LIFO-Prinzip (Last-In First-Out, oder deutsch: was zuletzt raufgetan wurde muß auch zuerst wieder runtergenommen werden)
hier noch ein Beispiel:
PUSH AX
PUSH BX
PUSH CX
PUSH DX
wir haben jetzt diese Register nacheinander auf den Stack getan. Wenn wir sie jetzt wieder haben wollen müßen wir dies in der umgekehrten Reihenfolge machen.
also so:
POP DX
POP CX
POP BX
POP AX

Wichtig ist noch zu sagen das es verschiedene STACK's gibt. Für COM- und EXE-Dateien wird der Stack von DOS/WIN bereitgestellt. Und bei QB von QB. Dem Programm selber ist es aber eigentlich egal wo der Stack ist.

GANZ WICHTIG: Zu jedem PUSH gehört ein POP!!!! Wenn man es nicht macht dann hat man irgendwann einen STACK-OVERFLOW und der Rechner stürzt ab! Das wird dir später noch klarer werden!


MOV AH,9
Mit dem MOV-Befehl sagen wir ihm das das Teilregister AH den Wert &H9 annehmen soll. Quasi "LET AH = &H9".
Der MOV-Befehl kann auch auf andere Register angewandt werden.
Dabei gilt: MOV Ziel, Quelle.

MOV DX, 010D
Das Register DX wird mit dem Wert &H010D geladen.

INT 21
Mit dem INT-Befehl kann man sogenannte Interrupte aufrufen. Als Kurzerklärung ist zu sagen das es vom Betriebssystem bereitgestellte Unterprogramme sind.
Eine detailierte Erklärung findest du hier: Interrupte in QB

Der Interrupt &H21 ist ein von DOS/WIN bereitgestellter Interrupt. Bevor man ihn aufruft müßen wir ihm sagen das wir mit der Unterfunktion &H9 arbeiten wollen.
Diese Nummer wird immer in AH angegeben. Und wenn du dich errinnerst haben wir in unserem Programm AH mit &H9 geladen.
Mit dieser Unterfunktion kann man dann einen Text auf den Bildschirm schreiben.
Dazu braucht dieser Interrupt aber noch weitere Informationen. Und zwar in welchem Segment und an welchem Offset sich dieser Text befindet.
Das Segment erwartet er in dem Register DS. Und wenn du dich dran errinnern kannst dann haben wir DS mit dem Programmsegment geladen.
Das Offset erwartet er im Register DX. Wenn du dir jetzt unser Programm nochmal anschaust fällt dir vielleicht was auf!
Genau! DX haben wir mit &H010D geladen. Und das ist das Offset was hinter dem LETZTEN INT 21 steht.
Und jetzt das unglaubliche: wo wir vorhin das Programm geschrieben haben, haben wir bei den letzten Befehlen immer DB und eine Zahl dazu eingegeben. Das war der Text. Denn mit DB kann man die ASCII-Werte in HEX-Form direkt ins Programm einbauen!

Das ganze heißt jetzt das wir unser Programm haben und gleich dahinter steht unser Text.
Wichtig ist noch das dieser Text mit einem "$" enden muß. Damit weiß der Interrupt wo der Text zu Ende ist. Das $ hat den HEX-Wert 24.

MOV AH, 4C
INT 21
Wir rufen den Interrupt mit der Unterfunktion 4C auf. Das bedeutet das wir hier das laufende Programm beenden wollen. Diese Befehls-Kombination ist einem END oder SYSTEM aus QB gleichzusetzen.
Und es muß immer darauf geachtet werden das dies ausgeführt wird. Wenn wir es nicht machen würden, würde der Prozessor als nächstes versuchen unseren "Text" auszuführen. Aber das wäre ja unlogisch und er würde Abstürzen.

Aber woher weiß dieser Interrupt wo er beim Beenden des Programmes weitermachen soll?
Dafür ist der STACK da, denn wenn ein Programm aufgerufen wird, werden die Ursprungskoordinaten gespeichert. Dann wird das Programm ausgeführt. Danach springt das Programm wieder an die Ursprungskoordinaten zurück. Und diese Koordinaten werden an einer bestimmten Stelle des STACK's erwartet. Nämlich genau dort, wo sie abgespeichert wurden. Und jetzt erinnere dich mal an die Sache mit PUSH und POP. Stell dir mal vor das ein PUSH "zuviel" ist auf dem STACK. Dann würde der Prozessor eine ganz andere Rücksprungsadresse erhalten und würde irgendwo landen, wo nicht wirklich ein lauffähiges Programm ist. Das hat dann meistens den Effekt das der Prozessor abstürzt!

       Abschnitt 5   Jetzt machen wir unser Hello World Programm für QB fertig.
Um in QB ein Assemblerprogramm aufzurufen gibt es verschiedene Möglichkeiten. Die erste und am meisten verwendete ist die mit CALL ABSOLUTE.
Und mit der werden wir auch arbeiten.

Dazu schauen wir uns erstmal diesen Befehl an.
CALL ABSOLUTE ist in Qbasic V1.0 fest integriert. Ab QB V4.0 ist dieser Befehl in die QB.QLB ausgelagert worden. Um unter V4.0 damit zu arbeiten muß man vorher die QB.QLB laden. Das macht man mit "QB.EXE /L". Man muß aber darauf achten das sich die QB.QLB im aktuellen Verzeichnis befindet!
Für weitere Informationen zu Bibliotheken siehe: Bibliotheken in QB

Da wir unser Assembler-Programm später in der STRING-Variable prg$ speichern lautet der Aufruf wie folgt:
DEF SEG = VARSEG(prg$)
CALL ABSOLUTE ( [Parameterliste], SADD(prg$))
DEF SEG

Mit dem Befehl DEF SEF = VARSEG(prg$) teilen wir Qbasic das SEGMENT unseres Assembler-Programmes mit.
Mit dem Befehl SADD(prg$) teilen wir Qbasic das OFFSET unseres Assembler-Programmes mit.
Und dieses Offset ist das mindeste was in den Klammern bei CALL ABSOLUTE stehen muß. Wenn man dazu noch Parameter übergibt, dann ist das Offset der allerletzte Wert in den Klammern. Also ist das Offset immer ganz rechts!

Parameterübergabe:
Es besteht, wie du siehst, die Möglichkeit Variablen an das Assemblerprogramm zu übergeben.
Ein Beispiel wäre CALL ABSOLUTE ( BYVAL a%, BYVAL b%, BYVAL c%, SADD(prg$))
Es können beliebig viele Variablen übergeben werden. Sie werden alle durch Kommas getrennt und der letzte Wert ist wieder das Offset des Assembler-Programmes.
Der Befehl BYVAL bewirkt das der Inhalt der Variablen übergeben wird. Wenn es nicht dort steht, dann wird nur das Variablen-Offset übergeben.

Wie kann man jetzt diese übergebenen Variablen im Assemblerprogramm benutzen?
Alle übergebenen Variablen werden auf dem STACK gespeichert. Und von dort kann man sie ins Programm einlesen.
Dazu müssen wir uns jetzt mal anschauen wie die Daten auf dem Stack abgelegt werden.
Wir übergeben die Variablen a%, b%, c%, SADD(prg$)
Diese Variablen werden jetzt nach und nach auf den STACK gePUSHt. Angefangen wird dabei von links. Das ganze würde dann Assemblertechnisch angedeutet so aussehen:
PUSH a%
PUSH b%
PUSH c%
PUSH QB-Ursprungs-OFFSET
PUSH QB-Ursprungs-SEGMENT

Der Stackpointer(SP) zeigt dann auf den letzten gePUSHten Wert, also dem QB-Segment.
Das ganze sieht dann so aus:
[SP + 08] - a%
[SP + 06] - b%
[SP + 04] - c%
[SP + 02] - QB-Offset
[SP + 00] - QB-Segment

Da wir während der Ausführung des Assemblerprogrammes das Register DS nicht verändern dürfen(will QB so!), müssen wir es am Anfang auch auf den STACK legen und am Ende des Programmes wieder von dort runterholen. Desweiteren benötigen wir auch noch das Register BP, das eigentlich nicht erhalten werden braucht, Aber da man Segment- und Offset-Variablen eleganterweise nicht verändert legen wir BP auch noch auf den STACK.
Im Assemblerprogramm machen wir das mit PUSH DS und PUSH BP. Danach sieht der STACK so aus:
[SP + 0C] - a%
[SP + 0A] - b%
[SP + 08] - c%
[SP + 06] - QB-Offset
[SP + 04] - QB-Segment
[SP + 02] - DS
[SP + 00] - BP

Wie du siehst werden die Adressen der Variabeln bei jedem PUSH oder POP geändert, und das ist für unser Assemblerprogramm irgendwie doof. Deshalb brauchen wir einen festen Pointer. Diesen legen wir auf BP.
Das sieht dann folgendermaßen aus:
MOV BP, SP

Da BP direkt mit dem STACK-Segment zusammenarbeitet können wir den STACK über BP ansprechen. Und zwar folgendermaßen:
MOV AX, [BP+08]
Mit diesem Befehl sagen wir ihm das AX den Wert annehmen soll der in der Speicherzelle [BP+08] abgelegt ist. In unserem Fall ist es die Variable c%.
Eckige Klammern?
Beispiel:
MOV AX, 1234 - würde heißen: AX = 1234
MOV AX, [1234]- würde heißen: AX = Inhalt der Speicherstelle 1234

Wie beendet man jetzt das Assembler-Programm?
Bei COM-Dateien haben wir gelernt das wir INT 21 mit der Unterfunktion AH=4C aufrufen müßen. Dies ist bei CALL ABSOLUTE nicht der Fall, hier reicht ein einfaches RETF. Das darf man aber erst ausführen lassen wenn alle PUSH's wieder gePOPt wurden.

Zur Verständlichkeit hier nochmal das komplette Assemblergerüst für jedes Assemblerprogramm das man mit CALL ABSOLUTE aufruft.
PUSH DS
PUSH BP
MOV BP, SP
...
hier der eigentliche Programmcode
...
POP BP
POP DS
RETF


So, damit wissen wir erstmal genug um unser "Hello World" Programm für QB zu schreiben.
Damit das ganze leichter zu handhaben ist, wollen wir den Text jederzeit in QB verändern können.
Das heißt das wir in QB einen STRING haben in der unser Text steht. Dem Assembler-Programm müßen wir dann mitteilen in welchem SEGMENT und an welchem OFFSET der Text steht.
Dazu übergeben wir die Variablen segment% und offset%
Das heißt: CALL ABSOLUTE (BYVAL offset%, BYVAL segment%, SADD(prg$))

Damit sieht das komplette Assemblerprogramm so aus:
PUSH DS
PUSH BP
MOV BP, SP
-----------------------
MOV AH, 9 Wir wollen Unterfunktion 9 benutzen
MOV DS, [BP+08] DS mit dem Text-Segment laden
MOV DX, [BP+0A] DX mit dem Text-Offset laden
INT 21 INT 21 aufrufen und den Text schreiben lassen
-----------------------
POP BP
POP DS
RETF

Jetzt starten wir wieder DEBUG.EXE, drücken "a 100" und Enter. "a 100" bedeutet das wir ab dem Offset 100 unser Programm schreiben wollen. Dann geben wir das komplette Programm ein.
Dann den Namen eingeben: -n hello2.asm
Wir nennen die Datei jetzt absichtlich nicht .COM, da sonst die Gefahr besteht das man aus Versehen das Programm ausführt. Da es aber auf QB zugeschnitten ist, ist es als COM-Datei nicht lauffähig.
jetzt mit "rcx" noch die Anzahl der zu speichernden Bytes eingeben. In unserem Beispiel sind es &H11, also 11 eingeben.
Dann noch "w" drücken und Enter.
Damit ist unser Assemblerprogramm fertig und wir beenden DEBUG mit "q".

Jetzt haben wir das komplette Assemblerprogramm in der Datei "hello2.asm" stehen und müssen den Inhalt irgendwie in unsere STRING-Variable prg$ bekommen.
Als erste Möglichkeit nehmen wir eine sehr einfache Methode.
Dazu starten wir Qbasic und schreiben das folgende Programm:
OPEN "hello2.asm" FOR BINARY AS #1 Unsere Assemblerdatei öffnen
prg$ = SPACE$(LOF(1)) Länge von prg$ bestimmen
GET #1, 1, prg$ Programm aus der Datei holen und in prg$ speichern
CLOSE #1 fertig
jetzt noch den Rest laden
text$ = "Hello World" Text bestimmen
text$ = text$ + "$" Der Text muß mit einem $ enden!
segment% = VARSEG(text$) Segment des Textes bestimmen
offset% = SADD(text$) Offset des Textes bestimmen
CLS
und jetzt das Programm mit CALL ABSOLUTE aufrufen
DEF SEG = VARSEG(prg$)
CALL ABSOLUTE (BYVAL offset%, BYVAL segment%, SADD(prg$))
DEF SEG

Und JETZT das QB-Programm starten. Wenn jetzt alles funktioniert müßtest du "Hello World" lesen können. Wenn er abgestürzt ist dann nochmal das Assemblerprogramm überprüfen.

Jetzt wollen wir es so machen, das man keine externe Datei benötigt sondern das der Code direkt in unserem QB-Programm steht.
Da die beste Möglichkeit, ein Assembler-Programm in Qbasic unterzubringen, die ist das man den Code im HEX-Format speichert, schreiben wir uns jetzt ein Programm das die Datei "hello2.asm" in eine Hex-Datei umwandelt.
Das folgende Programm tut dies. Und speichert den HEX-String in der Datei "hello2.hex".
OPEN "hello2.asm" FOR BINARY AS #1
OPEN "hello2.hex" FOR OUTPUT AS #2
a$ = SPACE$(1)
FOR i% = 1 TO LOF(1)
GET #1, ,a$
b$= LTRIM$(RTRIM$(HEX$(ASC(a$))))
IF LEN(b$) = 1 THEN b$ = "0" + b$
hexcode$ = hexcode$ + b$
NEXT i%
hexcode$ = CHR$(39) + hexcode$
PRINT #2, hexcode$
CLOSE #1: CLOSE #2

Wenn man dann die Datei "hello2.asm" in "hello2.hex" umgewandelt hat, dann steht das folgende in der Datei:

'1E5589E5B4098E5E088B560ACD215D1FCB

dieser String kann dann mittels BEARBEITEN->KOPIEREN und EINFÜGEN direkt ins Bas-Programm eingebaut werden.
in unserem Fall wäre das dann:
hexprg$ = "1E5589E5B4098E5E088B560ACD215D1FCB"
Das REM-Zeichen ' dient nur dazu das man den Code einfach in den QB-Editor übernehmen kann, ohne das QB überprüft ob es sich um einen Befehl handelt. Soll heißen, das das REM-Zeichen unbedingt noch weggenommen werden muß!!!

Da CALL ABSOLUTE jetzt aber nichts mit einem HEX-String anfangen kann, müssen wir es jetzt wieder in einen ASCII-String umwandeln. Das macht das folgende Programm:
FOR i% = 1 TO LEN(hexprg$) STEP 2
a$ = MID$(hexprg$, i%, 2)
b$ = CHR$(VAL("&H"+a$))
prg$ = prg$ + b$
NEXT i%
Damit haben wir jetzt das Assembler-Hex-Programm wieder in einen ASCII-STRING umgewandelt. Mit diesem können wir jetzt CALL ABSOLUTE aufrufen.
Vollständigkeits halber hier noch mal das komplette Programm:
hexprg$ = "1E5589E5B4098E5E088B560ACD215D1FCB"
--------------------------------------------------------------
FOR i% = 1 TO LEN(hexprg$) STEP 2
a$ = MID$(hexprg$, i%, 2)
b$ = CHR$(VAL("&H"+a$))
prg$ = prg$ + b$
NEXT i%
--------------------------------------------------------------
text$ = "Hello World"
text$ = text$ + "$"
segment% = VARSEG(text$)
offset% = SADD(text$)
CLS
--------------------------------------------------------------
DEF SEG = VARSEG(prg$)
CALL ABSOLUTE (BYVAL offset%, BYVAL segment%, SADD(prg$))
DEF SEG

So, damit haben wir alles gelernt um ein Assemblerprogramm selber zu schreiben und dauerhaft in einem QB-Programm einzufügen. Damit wir jetzt noch mehr mit Assembler machen können, zeige ich dir jetzt mal die wichtigsten Befehle.

       Abschnitt 6   Assembler-Befehle

Rechenoperationen:
Wichtig: Bei jeder Rechenoperation werden entsprechende FLAGS gesetzt.
Beispiele sind:
Ergebnis = 0 -> Zero-Flag = 1
Ergebnis <>0 -> Zero-Flag = 0
Ergebnis hatte ein Übertrag -> Overflow-Flag = 1
Ergebnis hatte kein Übertrag -> Overflow-Flag = 0
BefehlSyntaxBedeutung
INCINC RegisterRegister = Register +1
DECDEC RegisterRegister = Register -1
ADDADD Ziel, WertZiel = Ziel + Wert
Ziel: Register oder Speicherstelle
Quelle: Wert, Register, Speicherstelle
SUBSUB Ziel, WertZiel = Ziel - Wert
Ziel: Register oder Speicherstelle
Quelle: Wert, Register, Speicherstelle
IMULIMUL RegisterAX = AX * Register
Das I steht für Integer.
AX liefert die ersten 16 BIT des Ergebnisses zurück
MULMUL RegisterAX = AX * Register
AX liefert die ersten 16 BIT des Ergebnisses zurück
DX liefert die letzten 16 BIT der Ergebnisses zurück
IDIVIDIV RegisterAX = AX \ Register
Das I steht für Integer.
AX liefert die ersten 16 BIT des Ergebnisses zurück
DIVDIV RegisterDX:AX = DX:AX / Register
AX liefert die ersten 16 BIT des Ergebnisses zurück
DX liefert die letzten 16 BIT der Ergebnisses zurück

Kopieroperationen:
BefehlSyntaxBedeutung
PUSHPUSH RegisterRegister auf dem STACK ablegen
POPPOP RegisterRegister vom STACK holen
MOVMOV Ziel, QuelleZiel = Quelle
Ziel: Register oder Speicherstelle
Quelle: Wert, Register, Speicherstelle
Beispiele für das Zusammenarbeiten von Segment und Offset-Variabeln:
MOV AX, [BP+00] -> AX = Inhalt von SS:[BP+00]
MOV AX, [SI+00] -> AX = Inhalt von DS:[SI+00]
MOV AX, [DI+00] -> AX = Inhalt von DS:[DI+00]
MOV AX, [BX+00] -> AX = Inhalt von DS:[BX+00]
LODSBLODSBAL = [DS:SI]
Direction-Flag = 0 -> SI = SI + 1
Direction-Flag = 1 -> SI = SI - 1
LODSWLODSWAX = [DS:SI]
Direction-Flag = 0 -> SI = SI + 2
Direction-Flag = 1 -> SI = SI - 2
STOSBSTOSB[ES:DI] = AL
Direction-Flag = 0 -> DI = DI + 1
Direction-Flag = 1 -> DI = DI - 1
STOSWSTOSW[ES:DI] = AX
Direction-Flag = 0 -> DI = DI + 2
Direction-Flag = 1 -> DI = DI - 2
MOVSBMOVSB[ES:DI] = [DS:SI]
Direction-Flag = 0 -> DI = DI + 1 : SI = SI + 1
Direction-Flag = 1 -> DI = DI - 1 : SI = SI - 1
MOVSWMOVSW[ES:DI] = [DS:SI]
Direction-Flag = 0 -> DI = DI + 2 : SI = SI + 2
Direction-Flag = 1 -> DI = DI - 2 : SI = SI - 2

Flag-Operationen:
BefehlSyntaxBedeutung
CLDCLDDirection-Flag = 0
CLCCLCCarry-Flag = 0
CLICLIInterrupt-Flag = 0
CMCCMCWechselt Wert des Carry-Flags
STDSTDDirection-Flag = 1
STCSTCCarry-Flag = 1
STISTIInterrupt-Flag = 1

Vergleichsoperationen:
BefehlSyntaxBedeutung
CMPCMP Wert1, Wert2Vergleicht Wert1 und Wert2 miteinander und setzt Flags
Wert: Register, Speicherstelle,Wert
Wert1 = Wert2 -> Zero-Flag = 0
Wert1 <> Wert2 -> Zero-Flag = 1

Sprungbefehle:
BefehlSyntaxBedeutung
JMPJMP markeSpringt zur angegebenen Marke
Marke: Wert,Register, Speicherstelle
JZJZ markeSpringt zur angegebenen Marke wenn das Zero-Flag 1 ist
Marke: Wert,Register, Speicherstelle
JNZJNZ markeSpringt zur angegebenen Marke wenn das Zero-Flag 0 ist
Marke: Wert,Register, Speicherstelle
CALL
RET
CALL marke
RET
Springt zur angegebenen Marke
Ursprungskoordianten werden auf dem STACK abgelegt
RET holt sich diese Koordinaten wieder und springt zurück
Marke: Wert,Register, Speicherstelle
Wichtig: Innerhalb der geCALLten Routine alle PUSH's wieder POPen!!!
INT
IRET
INT nummer
IRET
Unterbricht das Programm und springt zu der Interruptnummer
Ursprungskoordianten werden auf dem STACK abgelegt
IRET holt sich diese Koordinaten wieder und springt zurück
nummer: Interruptnummer in HEX
Wichtig: Innerhalb der geINTten Routine alle PUSH's wieder POPen!!!
LOOPLOOP markeSpringt solange zu marke bis CX = 0 ist
CX wird bei jedem Durchlauf um 1 verringert
LOOPZLOOPZ markeSpringt solange zu marke bis CX = 0 ODER das Zero-Flag = 1 ist
CX wird bei jedem Durchlauf um 1 verringert
LOOPNZLOOPNZ markeSpringt solange zu marke bis CX = 0 ODER das Zero-Flag = 0 ist
CX wird bei jedem Durchlauf um 1 verringert

BIT-Manipulation:
BefehlSyntaxBedeutung
ANDAND Ziel, WertZiel = Ziel AND Wert
OROR Ziel, WertZiel = Ziel OR Wert
NOTNOT Ziel, WertZiel = Ziel NOT Wert
XORXOR Ziel, WertZiel = Ziel XOR Wert
SHLSHL Register, WertSchiebt alle Bits des Registers um Wert nach links
Wert: 1 oder CL
Beispiel: SHL AX,1 -> AX = AX * 2
SHRSHR Register, WertSchiebt alle Bits des Registers um Wert nach rechts
Wert: 1 oder CL
Beispiel: SHR AX,1 -> AX = AX \ 2
ROLROL Register, WertLässt alle Bits des Registers um Wert nach links rotieren
Wert: 1 oder CL
RORROR Register, WertLässt alle Bits des Registers um Wert nach rechts rotieren
Wert: 1 oder CL

Sonstiges:
BefehlSyntaxBedeutung
NOPNOPMacht nichts.


       Abschnitt 7   nützliche Befehlsfolgen
Hier ein paar Beispiele wie man bestimmte QB-Befehlsfolgen in Assembler erstellen kann.

IF AX = &H30 THEN ...
----------------------------------
CMP AX,30
JNZ marke
hier das Programm, das ausgeführt werden soll wenn AX = &H30 ist.
marke:

IF AX > &H30 THEN ...
----------------------------------
CMP AX,30
JNG marke
hier das Programm, das ausgeführt werden soll wenn AX > &H30 ist.
marke:

IF AX < &H30 THEN ...
----------------------------------
CMP AX,30
JNL marke
hier das Programm, das ausgeführt werden soll wenn AX < &H30 ist.
marke:


         Abschluss   letzte Aktualisierung: 24.07.2003
So, das wars erstmal. Wenn du noch weitere Fragen hast, noch etwas unklar ist oder du Fehler bemerkt hast, dann kannst du mir gerne eine E-Mail schicken.

Mail an Webmaster.




Werbung
Partner