|
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.
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.
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".
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.
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.
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 |
0101 | POP 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:
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! |
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.
Das Register DX wird mit dem Wert &H010D geladen.
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.
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:
Da BP direkt mit dem STACK-Segment zusammenarbeitet können wir den STACK über BP ansprechen.
Und zwar folgendermaßen:
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
Befehl | Syntax | Bedeutung |
INC | INC Register | Register = Register +1 |
DEC | DEC Register | Register = Register -1 |
ADD | ADD Ziel, Wert | Ziel = Ziel + Wert Ziel: Register oder Speicherstelle Quelle: Wert, Register, Speicherstelle |
SUB | SUB Ziel, Wert | Ziel = Ziel - Wert Ziel: Register oder Speicherstelle Quelle: Wert, Register, Speicherstelle |
IMUL | IMUL Register | AX = AX * Register Das I steht für Integer. AX liefert die ersten 16 BIT des Ergebnisses zurück |
MUL | MUL Register | AX = AX * Register AX liefert die ersten 16 BIT des Ergebnisses zurück DX liefert die letzten 16 BIT der Ergebnisses zurück |
IDIV | IDIV Register | AX = AX \ Register Das I steht für Integer. AX liefert die ersten 16 BIT des Ergebnisses zurück |
DIV | DIV Register | DX: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:
Befehl | Syntax | Bedeutung |
PUSH | PUSH Register | Register auf dem STACK ablegen |
POP | POP Register | Register vom STACK holen |
MOV | MOV Ziel, Quelle | Ziel = 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]
|
LODSB | LODSB | AL = [DS:SI] Direction-Flag = 0 -> SI = SI + 1 Direction-Flag = 1 -> SI = SI - 1 |
LODSW | LODSW | AX = [DS:SI] Direction-Flag = 0 -> SI = SI + 2 Direction-Flag = 1 -> SI = SI - 2 |
STOSB | STOSB | [ES:DI] = AL Direction-Flag = 0 -> DI = DI + 1 Direction-Flag = 1 -> DI = DI - 1 |
STOSW | STOSW | [ES:DI] = AX Direction-Flag = 0 -> DI = DI + 2 Direction-Flag = 1 -> DI = DI - 2 |
MOVSB | MOVSB | [ES:DI] = [DS:SI] Direction-Flag = 0 -> DI = DI + 1 : SI = SI + 1 Direction-Flag = 1 -> DI = DI - 1 : SI = SI - 1 |
MOVSW | MOVSW | [ES:DI] = [DS:SI] Direction-Flag = 0 -> DI = DI + 2 : SI = SI + 2 Direction-Flag = 1 -> DI = DI - 2 : SI = SI - 2 |
Flag-Operationen:
Befehl | Syntax | Bedeutung |
CLD | CLD | Direction-Flag = 0 |
CLC | CLC | Carry-Flag = 0 |
CLI | CLI | Interrupt-Flag = 0 |
CMC | CMC | Wechselt Wert des Carry-Flags |
STD | STD | Direction-Flag = 1 |
STC | STC | Carry-Flag = 1 |
STI | STI | Interrupt-Flag = 1 |
Vergleichsoperationen:
Befehl | Syntax | Bedeutung |
CMP | CMP Wert1, Wert2 | Vergleicht Wert1 und Wert2 miteinander und setzt Flags Wert: Register, Speicherstelle,Wert
Wert1 = Wert2 -> Zero-Flag = 0
Wert1 <> Wert2 -> Zero-Flag = 1
|
Sprungbefehle:
Befehl | Syntax | Bedeutung |
JMP | JMP marke | Springt zur angegebenen Marke Marke: Wert,Register, Speicherstelle |
JZ | JZ marke | Springt zur angegebenen Marke wenn das Zero-Flag 1 ist Marke: Wert,Register, Speicherstelle |
JNZ | JNZ marke | Springt 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!!! |
LOOP | LOOP marke | Springt solange zu marke bis CX = 0 ist CX wird bei jedem Durchlauf um 1 verringert |
LOOPZ | LOOPZ marke | Springt solange zu marke bis CX = 0 ODER das Zero-Flag = 1 ist CX wird bei jedem Durchlauf um 1 verringert |
LOOPNZ | LOOPNZ marke | Springt solange zu marke bis CX = 0 ODER das Zero-Flag = 0 ist CX wird bei jedem Durchlauf um 1 verringert |
BIT-Manipulation:
Befehl | Syntax | Bedeutung |
AND | AND Ziel, Wert | Ziel = Ziel AND Wert |
OR | OR Ziel, Wert | Ziel = Ziel OR Wert |
NOT | NOT Ziel, Wert | Ziel = Ziel NOT Wert |
XOR | XOR Ziel, Wert | Ziel = Ziel XOR Wert |
SHL | SHL Register, Wert | Schiebt alle Bits des Registers um Wert nach links Wert: 1 oder CL Beispiel: SHL AX,1 -> AX = AX * 2 |
SHR | SHR Register, Wert | Schiebt alle Bits des Registers um Wert nach rechts Wert: 1 oder CL Beispiel: SHR AX,1 -> AX = AX \ 2 |
ROL | ROL Register, Wert | Lässt alle Bits des Registers um Wert nach links rotieren Wert: 1 oder CL |
ROR | ROR Register, Wert | Lässt alle Bits des Registers um Wert nach rechts rotieren Wert: 1 oder CL |
Sonstiges:
Befehl | Syntax | Bedeutung |
NOP | NOP | Macht 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.
|
|
|