MASM32 für Anfänger Teil 216-11-02 

Ok, hab mir gerade mal die Zeit genommen um das nächste ASM Tut zu schreiben. Wie im letzten schon angesprochen wird es diesmal etwas mehr zur Sache gehen. Nachdem ihr mal ausprobiert habt wie man ein Programm compiliert und etwas von der Grundstruktur gesehen habt, soll es jetzt um die wichtigsten Funktionen in der Sprache Assembler selbst gehen.

Was kennt nun der Assembler eigentlich für Sprachelemente? Im wesentlichen besteht die Sprache nur aus den sogenanten "Opcodes". Wir konzentrieren uns hier auf die Intel 8086/80186/80286/80386/80486 Opcodes. In der Schule werden es wohl 8088 oder ähnliches sein. Da diese aber überaus veraltet sind programmiert damit eigentlich keiner mehr. Aber es gibt viele Opcodes die es bei beiden CPUs gibt.

Eine englische Beschreibung der einzelnen Opcodes (ich werde hier nur ein paar wenige vorstellen - sind einfach zu viele) findet ihr in der Datei: C:\masm32\HELP\OPCODES.HLP
Da gibt es auch Hilfe zu den FPU Opcodes und der MASM32 Library.

Um Opcodes zu benutzen gibt es im allgemeinen folgende Möglichkeiten:
opcode reg
opcode mem
opcode reg,reg
opcode mem,reg
opcode reg,mem

opcode mem,mem gibt es nicht.
"reg" heißt Register und "mem" heißt Memory - diese Beziechnung findet man auch in vielen Hilfen. Was ist nun ein Register:

Register sind ein paar wenige Speicherplätze in deiner CPU. Die wichtigsten (für den Programmieren meistverwendetsten) sind EAX,EBX,ECX,EDX,EDI,ESI.
Ein solches Register kann man immer in mehrere Teile zerlegen. EAX zum Beispiel kann man in AX,AH,AL zerlegen. Genauso auch EDX in DX, DH, DL.
Was ist das nun schon wieder?
EAX ist in unser Intel CPU derzeit der größte Block und das sind genau 32 BIT.
AX wäre davon die Hälfte also 16 BIT.
AH wäre vom AX das obere Byte - also AH ist demnach 8 BIT.
AL wäre vom AX das untere Byte - also AH ist auch 8 BIT.

Nun sollte man sich gleich mal überlegen wozu man wohl was gebrauchen könnte. In Windows werden alle Speicheradressen so angegeben: 0xFFFFFFFF Hex. Das heißt wir brauchen für jede Speicheradresse immer 32Bit also ein EAX. Wenn wir hingegen einfach nur 1 plus 1 zusammen ziehen wollen wäre EAX große Verschwendung - da würde ein AH + AL reichen.
Da aber Windows nun mal zum größten Teil mit 32 Bit Variablen rechnet und unser System ja "sooo" schnell ist das es uns nicht weiter interessiert wie viel Speicher wir vollklatschen, werden wir uns auf die 32BIT Register konzentrieren.

Zu den Registern gibt es jetzt noch die sogenanten FLAGS. Das ist ebenfalls ein kleiner Abschnitt in der CPU in der nur durch eine Bitfolge (0 Flag aus 1 Flag an) eine Reihe von Infos festgehalten. Das sieht dann ungefähr so aus:
FLAGS = 2 Byte (16 BIT):

|F|E|D|C|B|A|9|8|7|6|5|4|3|2|1|0|
 | | | | | | | | | | | | | | | '---  CF Carry Flag
 | | | | | | | | | | | | | | '---  1
 | | | | | | | | | | | | | '---  PF Parity Flag
 | | | | | | | | | | | | '---  0
 | | | | | | | | | | | '---  AF Auxiliary Flag
 | | | | | | | | | | '---  0
 | | | | | | | | | '---  ZF Zero Flag
 | | | | | | | | '---  SF Sign Flag
 | | | | | | | '---  TF Trap Flag  (Single Step)
 | | | | | | '---  IF Interrupt Flag
 | | | | | '---  DF Direction Flag
 | | | | '---  OF Overflow flag
 | | '-----  IOPL I/O Privilege Level  (286+ only)
 | '-----  NT Nested Task Flag  (286+ only)
 '-----  0
Ok, ok das muß man jetzt nich verstehen - nur mal zum anschauen gedacht. (Man sieht hier wo welches Flag in diesem 16 BIT Block liegt - das ist uns aber eigentlich egal da sich darum bitteschön der Prozessor kümmern soll ;o)

Wie funktionieren nun die Opcodes im zusammenhang mit den Flags?
Dazu mal ein kleines Codebeispiel was wir anschließend diskutieren:
MOV CX,10

LoopIt:
 ; mache irgendwas
 DEC CX
JNZ LoopIt
Ja - genau das ist eine "for" schleife wenn man so will. Wie funktioniert das:
Am Anfang setzen wir unser CX Register auf 10 (so oft wird dann die Schleife abgearbeitet)
Als nächstes kommt ein Label - Label vereinfachen Sprünge ungemein und werden immer in der Form: "LabelName:" geschrieben (nix mehr dahinter).
Es gibt ein paar "instant" Labels die die Schreibarbeit vereinfachen aber dazu später mehr.
Nun kommt unser Schleifenkörper der hier leer ist - bis auf die DEC CX Anweisung. DEC, wie der Name schon sagt, decrementiert den nachstehenden Operanden - also CX.
Nun kommt noch der Sprung. Wir springen unter der Bedingung das unser Zero Flag nicht gesetzt ist. Als Befehl heißt das JumpNotZero - JNZ.

Jetzt ist natürlich die Frage - wieso weiß der denn ob das Zero Flag 0 ist - ich hab ja gar nicht danach gefragt alla IF CX = 0 THEN. Das ist das tolle an der CPU - bei jeder Rechnung wird praktisch gleichzeitig mit berechnet ob das aktuelle Ergebnis 0 ist, ob wir über das Ziel hinausgeschossen sind (250 + 100 in einem 1 Byte Register) oder ähnliches.
Wenn wir also DEC CX schreiben wird CX - 1 gerechnet und gleichzeitig das Zero Flag gesetzt - entweder das Ergebnis war 0 - dann Flag setzen (1) - sonst nicht setzen (0).

Gleich noch ein ein paar Rechenbeispiele:
MOV AH,5
MOV AL,5
ADD AH,AL   ; AH wird 10
SHL AL,1    ; Shift Left um eine Stelle - also *2
SUB AH,AL   ; AH wird 0
Na gut, da wir jetzt ein bisschen wissen was ein Register ist und was man so damit macht, kommen wir nun zu Memory Variablen.

.data
  Zahl1 dd 200
  Zahl2 dd  45
.code
  XOR eax,eax    ; wir könnten auch MOV eax,Zahl2 schreiben aber so
  ADD eax,Zahl2  ; mal was anderes ;o)
  ADD eax,Zahl1  ; Man kann nicht einfach ADD Zahl1,Zahl2 schrieben!
  AND eax,0Fh    ; Was kommt wohl hier raus ??? genau 5 da wir praktisch
                 ; nur die Bits übrig lassen die maximal 0F groß sind - also 16
                 ; ist nicht wichtig aber im Assembler kann man auch sehr
                 ; guten gebrauch von den ganzen Bit und Shift Operatoren machen ;o)
Erstmal noch was zur ersten Zeile: XOR ist das Exclusive OR. Das heißt das alle Bits zusammen geodert werden aber nur dann eine 1 entsteht wenn Bit1 ungleich Bit2 war. Wenn man also zwei identische Werte mit XOR zusammen wirft bekommt man immer 0. (mal drüber nachdenken)

Dieses XOR funktioniert schneller (in der CPU) als hätten wir MOV eax,0 geschrieben - was aber das gleiche bewirken würde.

Wichtig bleibt noch zu erwähnen das immer nur gleiche größen zusammengerechnet werden können. Das heißt, ein AL kann nicht mit einer DWORD Speichervariablen addiert werden. Sowas sagt uns aber in der Regel der Compiler ;o)

Nehmen wir uns wieder ein leeres Programm zur Hand und basteln uns einen, schon vom letzten mal, bekannten Kopf:
;==============================================================
  .386
  .model flat,stdcall
  option casemap:none
;==============================================================

 include \masm32\include\windows.inc
 include \masm32\include\masm32.inc
 include \masm32\include\kernel32.inc
 include \masm32\include\user32.inc


 includelib \masm32\lib\kernel32.lib
 includelib \masm32\lib\user32.lib
 includelib \masm32\lib\masm32.lib

.data

.code

start:

 push 0
 call ExitProcess

end start
Um uns etwas die Schreibarbeit zu erleichtern möchte ich ein MASM32 berühmtes Macro benutzen: INVOKE.
Ein Macro ist eigentlich nur was für Schreibfaule - der Compiler stellt dann an diese Stelle den eigentlich notwendigen Code - wir werden im nächsten Tutorial mal ein Macro schreiben damit ihr wisst wie man sowas selber bastelt. Erstmal reicht es wenn wir wissen wie Invoke funktioniert.
Invoke macht einen Funktionsaufruf etwas logischer.
INVOKE MessageBox,0,ADDR szText,ADDR szCaption,MB_OK

würde ausgeschrieben folgendes heißen:

push MB_OK   ; Konstante aus der Win32 API
push offset szCaption
push offset szText
push 0
call MessageBox
Wie man sieht ist die eine Invoke Zeile deutlich kürzer und vorallem sieht das mehr danach aus was man aus C++ oder ähnlichen Sprachen kennt. Wir geben bei Invoke die Parameter in der richtigen Reihenfolge an nicht wie sonst umgekehrt.
Ok, Invoke vereinfacht uns das nun - was ist aber eigentlich das PUSH für die Variablen einer Funktion?:
Als weitere wichtige Komponente in deinem System gibt es den sogenannten Stack. Das ist eine lange Kette von Werten. (Der Stack ist dabei ein Speicherabschnitt um den sich Windows kümmert - oder kümmern sollte *grins*) Mit Push hängen wir einen Wert ans Ende der Liste an.
Der gegensätzliche Befehl zu PUSH ist POP.
.data
  Zahl1 dd 200
  Zahl2 dd  45
.code
  PUSH Zahl1
  POP Zahl2
Das würde zum Beispiel bewirken, daß der Wert von Zahl1 in den Stack geschoben wird und anschließend wieder in Zahl2 abgelegt wird. Das Ergebnis ist, daß wir Zahl1 = Zahl2 bekommen. Das funktioniert auch noch recht schnell und ist eine Möglichkeit eine Variable in einen anderen zu kopieren (gleiche größe vorrausgesetzt)- da ja MOV Zahl1,Zahl2 nicht geht.

Wie funktioniert nun der Stack:
Zugrunde liegt das FILO Prinzip: FirstIn - LastOut das kommt dadurch zustande das wir mit Push einen Wert auf den Stack legen (anhängen) und mit POP immer den letzten der Liste zurückbekommen. Wenn wir nun vor einem call die Parameter pushen werfen wir diese nacheinander auf den Stack. Die Funktion ihrer Seits holt diese Werte dann wieder vom Stack (mit POP). Dabei bekommt sie aber als erstes den von uns als letztes gepushten Wert.

Dabei ist es auch möglich den Stack für Zerlegung von Werten zu benutzen. Beispiel:
MOV EAX,00FFFF00h
PUSH EAX
POP AX
POP DX
Dann liegt in AX nun die unteren 2 Byte und in DX die oberen 2 Byte. Das kann man durchaus an mancher Stelle gebrauchen.

Dabei gibt es folgendes zu beachten: Auf den Stack kann (zumindest bei MASM32) nur 16 BIT oder 32 BIT Werte geladen werden - also PUSH AH geht nicht. Außerdem ist es absolut wichtig zu beachten wie viele Byte man in den Stack geschoben hat - genauso viele muß man auch wieder herunternehmen. Das heißt, folgendes würde einen schönen Stacküberlauf produzieren:
LoopOfDeath:
 PUSH eax
 ; Alles mögliche
 POP ax
JMP LoopOfDeath
So, da wir nun geklärt haben was der Stack ist, wieso eigentlich die Parameter für einen Funktionsaufruf verkehrtherum übergeben werden und was man beim Stack beachten muß, kommen wir wieder zurück zu unserem eigentlichem Übungsprogramm.

Schreiben wir erstmal eine invoke Anweisung aus unserem alten Call:
start:

 invoke ExitProcess,0
end start
Sieht doch schon mal besser aus. Als kleiner Test lassen wir uns mal eine kleine Message ausgeben:
.data
  szText db "Hallo Welt!",0
  szCaption db "Nachricht:",0
.code
start:
 invoke MessageBox,0,ADDR szText,ADDR szCaption,MB_OK
 invoke ExitProcess,0
end start
Nun können wir bereits als Windows Variante compilieren da wir keine Ausgabe auf der Console mehr haben.
Nun das ganze mal in einer Schleife:
start:
 mov cx,3
 LoopIt:
  push cx
  invoke MessageBox,0,ADDR szText,ADDR szCaption,MB_OK
  pop cx
  dec cx
 JNZ LoopIt
 invoke ExitProcess,0
end start
Wir speichern unser CX dabei mit push und holen es nach der MessageBox zurück - weil MessageBox selbst mit höchster Wahrscheinlichkeit unser CX umschreiben wird - leider hat eine CPU nun mal nur die paar Register und nicht jede Funktion kann ihre eigenen haben - Register sind sozusagen absolut Global - theoretisch würde kein Multitasking funktionieren wenn man bedenkt das ein Programm ja ständig die Register umändern würde die ein anderes Programm gerade mühevoll gesetzt hat. Dafür ist aber Windows verantwortlich und das soll uns deshalb nicht weiter kümmern.

Na gut, das soll es erstmal gewesen sein. Das man mit diesem Wissen schon eine Menge machen kann soll hier noch mal ein kleineres Beispiel zeigen. Dazu ist das Wissen der Win32 API Vorraussetzung. Durchsucht mal eure Festplatte ob ihr die Datei win32.hlp findet. Bei den meisten Programmiersprachen ist diese ca. 20MB große Datei dabei und ist absofort eure Bibel *grins*. Also hier das Sample:
;==============================================================
  .386
  .model flat,stdcall
  option casemap:none
;==============================================================

 include \masm32\include\windows.inc
 include \masm32\include\masm32.inc
 include \masm32\include\kernel32.inc
 include \masm32\include\user32.inc


 includelib \masm32\lib\kernel32.lib
 includelib \masm32\lib\user32.lib
 includelib \masm32\lib\masm32.lib

.data
  szText db "Gut das du die Leertaste drückst!",0
  szCaption db "Nachricht:",0
.code

start:
 LoopIt:
  invoke GetKeyState,VK_SPACE
   cmp eax,0
   jl Message ; zu unserer Message springen
  invoke GetKeyState,VK_ESCAPE
   cmp eax,0
   jl Ende  ; ans Programmende springen
 jmp LoopIt

 Message:
  invoke MessageBox,0,ADDR szText,ADDR szCaption,MB_OK
  jmp LoopIt ; zurück in die Schleife springen

 Ende:
 invoke ExitProcess,0
end start
Das ganze als Windows Anwendung compiliert und laufen lassen. Egal wo und wann man nun die Leertaste drückt bekommt man ein kleines Fensterchen dafür. Beendet wird das Programm über ESC zu irgendeinem Zeitpunkt. Dies soll nur mal ein Beispiel sein, ist in der Realität wenig Sinnvoll und raubt uns auf Grund der Endlosschleife auch jede menge Systemlast. Wer Win 2K oder XP hat kann sich das im Taskmanager ja mal ansehen ;o)
Aber man kann eigentlich schön erkennen wie das mit den Sprüngen funktioniert und wie man eine Taste abfragt ;o)

Übrigens: CMP vergleicht zwei Werte miteinander und setzt die entsprechenden Flags. In diesem Falle wird getestet ob der Rückgabewert (liegt in eax) der GetKeyState Funktion kleiner 0 ist. Ist das der Fall ist die Taste gerade gedrückt.