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

Auf wunsch von einigen hab ich es nun doch geschaft mal ein Assembler Tutorial zu schreiben. Es soll hier mal um die elementarsten Dinge gehen. Dabei hab ich, um möglichst ein Praxis nahes Programm vorzustellen, die Win32 API verwendet. Wer die API schon etwas besser kennt hat an dieser Stelle sicher Vorteile das ganze zu verstehen aber ich hoffe mal das auch alle anderen das nachvollziehen können.

Was ist Assembler für eine Programmiersprache?: Es handelt sich um eine sehr schnelle und harwarenahe Sprache die im Idealfall die schnellsten Programme erzeugen kann. Wichtig zu wissen: In Assembler gibt es nur eine sehr geringe Prüfung des Quellcodes von Seiten des Compilers. Es ist deshalb wichtig immer gut aufzupassen - auf dauer zugegeben etwas stressig weil schon einfachste Befehle einen Bluescreen fabrizieren wenn man nicht aufpasst.
Als erstes benötigt ihr den MASM32 den ihr hier [5,2MB] downloaden könnt.
Außerdem hab ich mal schnell einen einfachen Texteditor geschrieben der euch das Compilen und Linken erleichtern soll. (Sonst wäre das immer etwas Consolenarbeit) Den Editor gibts hier [0,2MB]

Wichtig: Der Editor und meine Ausführungen beruhen darauf, daß ihr MASM32 im Stantartordner habt (C:\MASM32\) und das eure .asm Dateien die wir schreiben wollen irgendwo auf der Festplatte C:\ liegen. (geht leider sonst nich so einfach)
Am besten ihr legt euch ein Verzeichniss an z.B.: C:\ASMFiles\ und darin dann immer einen Ordner für jedes Beispiel.

Als erstes öffnen wir den Editor oder das Notepad oder ähnliches und schreiben folgendes rein:
;==============================================================
 .386
 .model flat,stdcall
 option casemap:none
;==============================================================
Jetzt habt ihr bestimmt schon mal das erste Wissenswerte mitbekommen: Kommentare werden mit einem vorrangestelltem ; begonnen. Aber es gibt keinen Befehlsterminator wie ; am Ende der Zeile. Das heißt wiederum Zeilen wie .386 . model ... auf einer Zeile fuktionieren nicht. Einfach: Immer nur ein Befehl pro Zeile!
Was heißt nun der Rest:
.386 gibt dem Compiler hinweise auf die CPU (soll uns nich weiter kümmern an dieser Stelle)
.model flat,sdtcall legt das Verhalten beim Aufruf von fuktionen fest - das hier ist standart und wir wollen es einfach mal so stehen lassen.
option casemap:none legt fest, daß wir auf Groß- und Kleinschreibung wert legen wollen.

Damit hätten wir eigentlich auch schon alle Compilereinstellungen fertig und können mit unserem ersten Programm anfangen.
Angefangen wird mit ein paar Librarys die wir für unser Programm brauchen:
 include \masm32\include\windows.inc
 include \masm32\include\masm32.inc
 include \masm32\include\kernel32.inc

 includelib \masm32\lib\kernel32.lib
 includelib \masm32\lib\masm32.lib
include heißt das die dahinterstehenende Datei zum Project gebunden wird. Die windows.inc ist die Standart Win32 Datei - vergleichbar mit der windows.h in C++
includelib bindet eine lib ein - da stehen die Daten zu den .inc Dateien an. In der Regel gibt es zu den meisten .inc auch eine passende .lib

So jetzt können wir unseren eigentlichen Programmteil schreiben:
.data
 szText db "Hallo Welt!",10,0
.code
start:
 push offset szText
 call StdOut
 push 0
 push 0
 call StdIn
 push 0
 call ExitProcess
end start
Ok ok, etwas viel auf einmal deshalb eine schrittweise Erklärung dazu:
.data
.code
Ein Programm ist immer in zwei Teile gegliedert: Datensegment und Codesegment Diese beiden Teile werden im MASM32 durch die Zeile .data bzw. .code umgeschalten.
Es ist also auch erlaubt folgendes zu schreiben:
.code
 ;---
.data
 ;---
.code
Der Compiler sortiert sich das dann so das die Teile zusammenhängen.
Nun kommen wir zu Variablen in obigem Stück: szText db "Hallo Welt!",10,0
Was heißt das nun im Klartext:
Variabelname  Variabeltyp  Daten
szText        db           "Hallo Welt!",10,0
Hmm, Typen kennt man wohl eher im Bereich: int, char oder ähnliches aber in Assembler gibt es sowas eigentlich nicht. Hier gibt es an sich nur folgende Typen:
Variabeltyp  Größe    Name
db           1 Byte   Byte
dw           2 Byte   Word
dd           4 Byte   DoubleWord (DWORD)
dq           8 Byte   QuadWord (QWORD)
dt          10 Byte   TenByte
Das kommt dann vielleicht doch schon wieder etwas vertrauter vor. Als wichtigeste Typen merken wir uns db (Byte) und dd (DWORD).
Nun zurück auf unsere Variable szText. Da als Typ db angegeben ist, haben wir also Byte. Der Unterschied zu anderen Programmiersprachen ist an dieser Stelle, daß wir den Typ eines Elements angeben und bei der Wertzuweisung nun mehrere Elemente dieses Typs benutzen.
Das heißt:
szText db 0
Wäre also eine Variable vom Typ Byte mit dem Anfangswert 0.
szText db 32,32,32,0
Das wäre nun ebenfalls eine Variable vom Typ Byte aber gleich mehrere davon hintereinander. szText steht dabei praktisch nur für den Anfang der Kette von Werten die folgt.
Dabei ist 32 (Dezimal in der ASCII Tabelle) das gleiche wie " " und somit könnte man auch:
szText db " "," "," ",0
für obige Zeile schreiben aber eben auch:
szText db "   ",0
Wobei sich der Compiler da die Kommas dazwischen denkt.

Vergleich zu C++:
char szText [50] = "Hallo Welt!";
szText db "Hallo Welt!",0

Dabei gibt es keine Beschränkung der länge - nur sollte man wissen das eine Variable im Datensegment in ihrer länge später nicht mehr geändert werden kann. Das heißt wenn unsere Kette am anfang 12 Buchstaben umfasst werden wir nie mehr als diese 12 hineinpacken können. Um uns von Anfang an gleich mehrere leere Zeichen zu sichern schreiben wir folgendes:
szText db 100 DUP(0),0
Das heißt, daß die in Klammern stehende 0 einhundertmal wiederholt wird. Man kann auch so etwas schreiben: szText db 10 DUP ("Hallo "),0 Das würde dann 10 mal das Wort "Hallo" mit jeweils einem Freizeichen dahinter ergeben.

Wozu nun jedesmal die 0 am ende der Variable?:
Die "Text",0 beendet die Zeichenkette. Das ist unter C++ auch so - man nennt dies Nullterminierte-Strings.
Wenn man die 0 am Ende weg lässt führt das unweigerlich zu einem Fehler!

Ok das soll erstmal dazu reichen - wenn ihr das an der Stelle nicht so recht verstanden habt wird es vielleicht bei anderen Beispielen dann deutlicher. Auf jedenfall müßt ihr versuchen von der Idee der Datentypen wie ihr sie aus C++ kennt zu vergessen - eigentlich besteht alles nur aus Bytes. Ein dd ist praktisch auch nur ein db,db,db,db und kann theoretisch auch als diese lange Folge aufgeschrieben werden - nur eben nicht besonders leserlich.

Damit wissen wir jetzt das im Datensegment Variablen stehen die zu beginn des Programms da sein sollen und mit denen wir dann auch arbeiten können.

Das Codesegment beinhaltet nun den Teil, in dem zur Laufzeit was passieren soll. Dieser wird nacheinander abgearbeitet.
Die procedure main() wie man sie von C++ kennt gibt es hier nicht - aber etwas vergleichbares:
 start:
  ;--code--
 end start
Das heißt: Statt main() gibt es nun Start: und statt den {} Klammern nur am Ende das "end start"

Unser eigentlicher Programmteil:
start:
 push offset szText
 call StdOut
 push 0
 push 0
 call StdIn
 push 0
 call ExitProcess
end start
Hier haben wir nun schon die ersten Assemblerbefehle. Als erster wäre da "call" zu nennen. Dabmit wird eine Funktion aufgerufen. In C++ würde der letzte call wie folgt aussehen:

ExitProcess(0);

In Assembler sind das nun schon zwei Zeilen:

push 0
call ExitProcess

Call ruft also einfach nur eine Funktion auf. Die Parameter dafür werden aber nicht wie bei C++ dahinter angegeben sondern davor. Wieso ist das denn so?
Dazu müssen wir die Funktionsweise des Windowssystems (oder besser des gesamten PCs) zu verstehen. Eine Funktion wie man sie kennt ist im Speicher nur ein Punkt - ein sogenannter Einsprungpunkt. An dieser Stelle steht der Anfang eines Programmteils und nicht mehr. Da steht in Wirklichkeit nix von Parametern oder ähnlichem. Eine Funktion ist im Speicher einfach nur eine hexadezimale Adresse.
Also müssen wir Parameter vorher in den Zwischenspeicher(Stack) schieben, dann die Funktion aufrufen (unser Programm springt an die Speicheradresse X) und diese Funktion kann nun den Zwischenspeicher wieder auslesen um zu schauen was wir als Parameter haben wollen.

Das Klingt verdammt aufwendig aber so funktioniert mal dein System - C++ macht das gleiche nur schreibt der C++ Compiler das für uns so um.

Mit push legen wir einen Wert in den Stack. Wieviel Byte wir nach oben Pushen wollen hängt dabei von der Variable ab die wir dahinter angeben.
Wenn wir also push 0 schreiben weis der Compiler an sich nicht ob wir ein einzelnes Byte damit meinen oder ein DWORD. Deshalb ist die schreibweise push 0 etwas gewagt. In unserem 32 Bit Betriebssystem heißt keine Angabe = 32 Bit (oder eben 4 Byte was gleich einem dw ist).

Wieviele Parameter eine Funktion erwartet muß man dabei selbst wissen. (es gibt im MASM32 ein kleines Helferchen was ich im nächsten Tutorial herleiten werde)

Als letzte Unbekannte bleibt das Wort offset in unserem Beispiel:
start:
 push offset szText
 call StdOut
 push 0
 push 0
 call StdIn
 push 0
 call ExitProcess
end start
Offset gibt die Speicheradresse von der Variable die dahinter steht zurück. In C++ schreibt man dafür &szText in ASM offset szText.
Um genau zu sein setzt der Compiler an dieser Stelle dann die Adresse aus dem Datensegment ein.

Wenn ihr nun mal (wenn nicht schon längst getan) als Consolenanwendung Compiliert und anschließend startet (Run) habt ihr ein erstes Ergebnis.

Ok, ziemlich viel für diese eine Zeile. Dabei haben wir aber 3 Funktionen gecalled: StdOut (gibt einen Text auf die Console aus - ähnlich wie cout in C++), StdIn (würde einen Wert vom Keyboard einlesen. Da wir aber eigentlich keinen haben wollen geben wir als Parameter 0 an. StdIn ist also vergleichbar mit cin), ExitProcess (beendet das aktuelle Windwosprogramm)

So, mal hier noch schnell ein längeres Beispiel zum anschauen und testen. Im nächsten Tutorial gehts dann richtig los - Rechnen, Register, Flags, Sprünge, Windowsfunktionen aus der Win32 API... ;o)

;==============================================================
  .386
  .model flat,stdcall
  option casemap:none
;==============================================================

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

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

.data
 szLine db 80 DUP("="),0
 szCaption db "Mein erstes Consolen Programm:",10,0
 szEingabe db "Bitte eine Zahl eingeben:",0
 szAusgabe db "Deine Zahl:",0
 Zahl      db 20 DUP(0)
.code

start:
 push offset szLine
 call StdOut
 push offset szCaption
 call StdOut
 push offset szLine
 call StdOut
 push offset szEingabe
 call StdOut
 push 20
 push offset Zahl
 call StdIn
 push offset szAusgabe
 call StdOut
 push offset Zahl
 call StdOut

 push 0
 push 0
 call StdIn
 push 0
 call ExitProcess
end start