Iczelion - 02 - MessageBox

Tutorial 2: MessageBox



In diesem Tutorial werden wir ein voll funktionsfähiges Windows-Programm erstellen, dass eine Message Box anzeigt, die den Text "Win32 Assembler ist Grossartig!" enthält.

Das Beispiel können Sie hier Download herunterladen.

Theorie:

Windows stellt Unmengen von Ressourcen für Windows-Programme zu Verfügung. Im Mittelpunkt steht dabei das Windows API (Application Programming Interface). Windows API ist eine riesige Ansammlung von sehr nützlichen Funktionen, die in Windows selbst ansässig sind und von jedem anderen Windows-Programm genutzt werden kann. Diese Funktionen befinden sich in verschiedenen Dynamic-Linked Libraries (DLLs), wie kernel32.dll, user32.dll und gdi32.dll. Kernel32.dll beinhaltet API-Funktionen, die mit dem Speicher- und Prozessmanagement zu tun haben. User32.dll kontrolliert die Benutzer-Interface-Aspekte ihres Programms. Gdi32.dll ist für graphische Operationen verantwortlich. Neben den "drei Wichtigsten", gibt es andere DLLs, die ihr Programm nutzen kann, vorausgesetzt, Sie haben genügend Informationen über die gewünschten API-Funktionen.

Windows-Programme linken diese DLLs dynamisch, d.h. der Code der API-Funktionen wird nicht in die ausführbare Datei des Windows-Programms geschrieben. Damit ihr Programm zu Laufzeit die gewünschten API-Funktionen zu Laufzeit finden kann, müssen Sie diese Informationen in der ausführbaren Datei unterbringen. Die Informationen sind in Import Libraries untergebracht. Sie müssen ihr Programm mit den korrekten Import Libraries linken oder es wird nicht in der Lage sein, die API-Funktionen zu finden.

Wenn ein Windows-Programm in den Speicher geladen wird, liest Windows die Informationen, die in dem Programm gespeichert sind, erst einmal aus. Die Informationen beinhalten die Namen der Funktionen, die das Programm benutzt und die DLLs, in welchen die Funktionen gespeichert sind. Wenn Windows solche Informationen im Programm findet, lädt es die DLLs und korrigiert die Funktionsadressen im Programm, so dass die Aufrufe die Kontrolle an die richtige Funktion übergeben.

Es gibt zwei Kategorien von API-Funktionen: Eine für ANSI und eine andere für Unicode. Die Name der ANSI-API-Funktionen enden mit einem "A", z.B. MessageBoxA. Die für Unicode enden mit einem "W" (für Wide (=umfangreich), glaube ich). Windows 95 unterstützt ANSI und Windows NT Unicode.

Wir benutzen in der Regel ANSI String, welche ein Array aus Buchstaben sind, das mit NULL terminiert wird. ANSI-Buchstaben sind 1 Byte groß. Während ANSI-Code für europäische Sprachen ausreichend ist, kann es verschiedene orientalische Sprache nicht händeln, die mehrere Tausende verschiedene Buchstaben haben. Darum wurde UNICODE erdacht. Ein UNICODE-Buchstabe ist 2 Bytes groß, was es möglich macht, 65536 verschieden Buchstaben in einem String zu haben.

Die meiste Zeit aber, werden Sie eine Include-Datei benutzen, wo Sie sich für die passenden API-Funktionen entscheiden und diese dann auswählen können. Sich einfach auf den API-Funktionsnamen beziehen, ohne die Endung.

Beispiel:

Ich werde Ihnen das nackte Programm-Skelett unten präsentieren. Wir werden dem Ganzen später Substanz verleihen.

.386 .model flat, stdcall .data .code start: end start
Die Ausführung beginnt mit dem ersten Befehl unmittelbar unter dem Label, das nach der end Direktive spezifiziert wird. In dem oberen Skelett beginnt die Ausführung mit dem ersten Befehl unmittelbar nach dem start Label. Die Ausführung wird Befehl für Befehl fortgefahren, bis ein Sprung-Befehl wie jmp, jne, je, ret, etc. vorliegt. Diese Befehle setzen die Ausführung an einer anderen Stelle im Code fort. Wenn das Programm beendet werden und zu Windows zurückkehren soll, sollte es eine API-Funktion aufrufen, ExitProcess.

ExitProcess proto uExitCode:DWORD
Die obere Linie nennt man einen Funktions-Prototypen. Ein Funktions-Prototyp definiert die Attribute einer Funktion für den Assembler/Linker, damit er ein Datentyp-Check für Sie machen kann. Das Format eines Funktions-Prototypen sieht folgendermaßen aus:

FunktionsName PROTO [ParameterName]:DatenTyp,[ParameterName]:DatenTyp,...


Kurzgefasst, dem Name der Funktion folgt das Schlüsselwort PROTO und dann eine Liste von Datentypen der Parameter, getrennt durch Kommata. Im obigen ExitProcess-Beispiel, wird ExitProcess als eine Funktion definiert, die nur einen Parameter vom Datentyp DWORD übergeben wird. Funktions-Prototypen sind sehr nützlich, wenn Sie den Hochsprachen-Aufruf-Syntax, invoke, benutzen. Sie können sich invoke als einen einfachen Aufruf mit Datentyp-Check vorstellen. Wenn Sie zum Beispiel folgendes tun:

call ExitProcess
ohne eine DWord auf den Stack zu pushen, wird der Assembler/Linker nicht in der Lage sein, den Fehler für Sie abzufangen. Sie werden es später bemerken, wenn ihr Programm abstürzt. Aber wenn Sie folgendes benutzen:

invoke ExitProcess
wird der Linker Sie informieren, dass Sie vergessen haben ein Doubleword auf den Stack zu pushen, was den Fehler vermeiden würde. Ich empfehle Ihnen invoke statt eines einfach Call zu benutzen. Die Syntax von invoke sieht wie folgend aus:

INVOKE  Ausdruck [,argumente]
Ausdruck kann der Name einer Funktion oder ein Funktionszeiger sein. Die Funktionsparameter werden mit Kommata getrennt.

Die meisten Funktions-Prototypen für API-Funktionen, werden in Include-Dateien gehalten. Wenn Sie Hutch's MASM32 benutzen, werden diese im MASM32\include Verzeichnis sein. Die Include-Dateien haben die Extension .INC und die Funktions-Prototypen für Funktionen aus einer DLL werden in einer .INC-Datei gespeichert, die den selben Namen trägt wie die DLL-Datei. Zum Beispiel wird ExitProcess aus Kernel32.lib exportiert, so dass der Funktions-Prototyp für ExitProcess in Kernel32.inc gespeichert wird.

Sie können auch eigene Funktions-Prototypen für ihre eigenen Funktionen erstellen.

Durch all meine Beispiele hindurch werde ich Hutch's Windows.inc benutzen, welche Sie von http://win32asm.cjb.net herunterladen können.

Nun zurück zu ExitProcess, der uExitCode Parameter ist der Wert, den ihr Programm zurückgeben soll, nachdem das Programm beendet wurde und zu Windows zurückkehrt. Sie können ExitProcess wie folgt aufrufen:

invoke ExitProcess, 0
Fügen Sie diese Zeile direkt nach dem start Label ein, so werden Sie ein Win32-Programm erhalten, welches unverzüglich zu Windows zurückkehrt, aber nichtsdestotrotz ist es ein richtiges Programm.

.386 .model flat, stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .data .code start: invoke ExitProcess,0 end start
option casemap:none teilt MASM mit, dass bei Labels Groß-/Kleinschreibung unterschieden wird, so dass ExitProcess und exitprocess unterschiedlich sind. Beachten Sie die neue Direktive include. Dieser Direktive folgt der Name der Datei, die anstelle der Direktive eingefügt werden soll. In dem obigen Beispiel, wenn MASM die Linie include \masm32\include\windows.inc vorfindet, wird es windows.inc öffnen, welche sich im \MASM32\include Ordner befindet und so behandeln, als ob Sie den Inhalt von windows.inc hier einfügen. Hutch's windows.inc enthält Definitionen von Konstanten und Strukturen, die Sie während der Win32-Programmierung benötigen. Sie enthält keine Funktions-Prototypen. windows.inc ist in keinster Weise vollständig. Hutch und ich versuchen soviele Konstanten und Strukturen wie möglich einzufügen, aber es gibt noch jede Menge die eingefügt werden müssen. Sie wird ständig aktualisiert. Prüfen Sie Hutch's oder meine Homepage für Updates.

Aus windows.inc enthält ihr Programm Konstanten- und Struktur-Definitionen. Für Funktions-Prototypen müssen Sie andere Include-Dateien einbinden. Sie sind alle im Verzeichnis \masm32\include gespeichert.

In unserem Beispiel oben, rufen wir eine Funktion auf, die aus der kernel32.dll exportiert wird, weshalb wir die Funktions-Prototypen der kernel32.dll einbinden mussen. Diese Datei ist die kernel32.inc. Wenn Sie sie mit einem Texteditor öffnen, werden Sie sehen, dass sie voll von Funktions-Prototypen der kernel32.dll ist. Wenn Sie kernel32.inc nicht einfügen, können Sie dennoch ExitProcess aufrufen, allerdings nur mit dem call Syntax. Sie können die Funktion nicht mit invoke aufrufen. Der springende Punkt hier ist: Um eine Funktion mit invoke aufzurufen, müssen Sie irgendwo den Funktions-Prototypen in ihrem Source Code einfügen. Im obigen Beispiel, können Sie, wenn Sie kernel32.inc nicht einbinden, irgendwo im Source Code den Funktions-Prototypen für ExitProcess definieren, bevor Sie sie mit invoke aufrufen und es wird funktionieren. Die Include-Dateien sind dazu da, ihnen die Arbeit zu ersparen die Prototypen einzugeben, also nutzen Sie sie, wo sie nur können.

Nun kommen wir zu einer neuen Direktive, includelib. Includelib arbeitet nicht wir include. Es ist nur ein Weg dem Assembler mitzuteilen, welche Import Library ihr Programm benutzt. Wenn der Assembler auf eine includelib Direktive trifft, fügt es einen Linker-Befehl in die Objekt-Datei ein, so dass der Linker weiß, welche Import Libraries er zu ihrem Programm hinzulinken muss. Sie werden aber nicht gezwungen, includelib zu benutzen. Sie können die Namen der Import Libraries auch an der Kommandozeile dem Linker übergeben, aber glauben Sie mir, es ist ermüdend und die Kommandozeile kann nur 128 Zeichen entgegen nehmen.

Speichern Sie nun das Beispiel unter dem Namen msgbox.asm. Davon ausgehend, dass ml.exe in ihrem Verzeichnis liegt, assemblieren Sie msgbox.asm mit:

ml  /c  /coff  /Cp msgbox.asm

  • /c teilt MASM mit, dass nur assembliert werden soll. Link.exe soll nicht aufgerufen werden. In der Regel wollen Sie link.exe nicht automatisch ausführen lassen, da Sie vorher noch einige andere Aufgaben erledigen müssen, bevor Sie link.exe ausführen.

  • /coff teilt MASM mit, eine .obj-Datei im COFF Format zu erstellen. MASM benutzt eine Variante von COFF (Common Object File Format) welche unter Unix, als eigenes Objekt- und ausführbare Dateiformat genutzt wird.
  • /Cp teilt MASM mit, Groß-/Kleinschreibung zu beachten. Wenn Sie Hutch's MASM32 Package benutzen, können Sie "option casemap:none" am Anfang ihres Source Code benutzen, direkt hinter der .model Direktive um den selben Effekt zu erreichen.
Nachdem Sie msgbox.asm erfolgreich assembliert haben, werden Sie die Datei msgbox.obj erhalten. msgbox.obj ist eine Objektdatei. Eine Objektdatei ist nur ein Schritt von einer ausführbaren Datei entfernt. Sie enthält die Befehle und Daten und binärer Form. Was fehlt sind einige Änderungen der Adressen seitens des Linkers.

Dann machen Sie mit link weiter:

link /SUBSYSTEM:WINDOWS  /LIBPATH:c:\masm32\lib  msgbox.obj

/SUBSYSTEM:WINDOWS
teilt Link mit, welche Art von ausführbarer Datei das Programm wird
/LIBPATH:<Pfad der import-library> teilt Link mit, wo die zu importierenden Libraries zu finden sind. Wenn Sie MASM32 benutzen, werden Sie im MASM32\lib-Verzeichnis sein.
Link liest die Objekt-Datei ein und ändert die Adressen aus den Import Libraries. Wenn der Vorgang beendet wurde, erhalten Sie msgbox.exe.

Nun haben Sie msgbox.exe. Fahren Sie fort, führen Sie sie aus. Sie werden feststellen, dass es nichts macht. Naja, wir haben ja auch noch nichts interessantes eingefügt. Aber nichtsdestotrotz ist es ein Windows-Programm. Und schauen Sie sich die Größe an. Auf meinem PC ist sie 1536 Bytes.

Nun werden wir eine MessageBox einfügen. Der Funktions-Prototyp ist:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
hwnd ist das Handle des Eltern-Fensters. Sie können bei einem Handle an eine Nummer denken, die das Fenster repräsentiert, das Sie ansprechen. Der Wert ist für Sie nicht wichtig. Sie müssen nur wissen, dass es das Fenster repräsentiert. Wenn Sie irgendwas mit dem Fenster machen wollen, müssen Sie es über sein Handle ansprechen.
lpText ist ein Zeiger auf den Text, den Sie in der Client Area der MessageBox anzeigen lassen wollen. Ein Zeiger ist die wirkliche Adresse von etwas. Ein Zeiger auf einen Text-String ist gleich der Adresse des Strings.
lpCaption ist ein Zeiger auf den Titelleistentext der MessageBox
uType spezifiziert das Icon und die Anzahl und Arten der Buttons der MessageBox
Lassen Sie uns msgbox.asm modifizieren und eine MessageBox einfügen.
 

.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib include \masm32\include\user32.inc includelib \masm32\lib\user32.lib .data MsgBoxCaption db "Iczelion Tutorial No.2",0 MsgBoxText db "Win32 Assembler ist grossartig!",0 .code start: invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK invoke ExitProcess, NULL end start
Assemblieren und führen sie es aus. Sie werden eine MessageBox sehen, die den Text "Win32 Assembler ist grossartig!" enthält.

Schauen wir noch mal auf den Sourcecode.

Wir definieren zwei Nullterminierte Strings in der .data Sektion. Erinnern Sie sich, dass jeder ANSI-String unter Windows mit NULL (0 hexadezimal) terminiert werden muss.

Wir benutzen zwei Konstanten, NULL und MB_OK. Diese Konstanten werden in der windows.inc dokumentiert. So können Sie die Namen benutzen, anstatt deren Werte. Das steigert die Leserlichkeit Ihres Sourcecodes.

Der addr Operator wird benutzt, um die Adresse eines Labels an die Funktion zu übergeben. Er ist nur gültig im Zusammenhang mit der invoke Direktive. Sie können ihn nicht benutzen um zum Beispiel die Adresse eines Labels in einem Register/Variable zu speichern. Sie können offset statt addr in dem obigen Beispiel benutzen. Aber dennoch gibt es einige Unterschiede zwischen den beiden:

  1. addr kann keine Vorwärtsreferenzen händeln, während offset das kann. Wenn ein Label zum Beispiel irgendwo weiter hinten im Sourcecode definiert ist, als die invoke Zeile, dann wird addr nicht funktionieren.
  2. invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK ...... MsgBoxCaption db "Iczelion Tutorial No.2",0 MsgBoxText db "Win32 Assembler ist grossartig!",0
    MASM wird einen Fehler melden. Wenn Sie offset statt addr benutzen in dem obigen Codeschnippsel verwenden, wird MASM es einfach assemblieren.

  3. addr kann lokale Variablen händeln, während offset das nicht kann. Eine lokale Variable ist nur etwas reservierter Speicherplatz auf dem Stack. Sie werden dessen Adresse nur zur Laufzeit kennen. offset wird während des assemblierens vom Assembler interpretiert. So ist es ganz natürlich, dass offset nicht für lokale Variablen funktionieren kann. addr kann deshalb lokale Variablen handhaben, auf Grund der Tatsache, dass der Assembler erst überprüft, ob die Variable, auf die addr Bezug nimmt eine globale oder eine lokale ist. Wenn es eine globale Variable ist, schreibt er die Adresse der Variable in die Object-Datei. In dieser Hinsicht arbeitet es wir offset. Wenn es eine lokale Variable ist, wird eine Befehlsfolge wie diese generiert, bevor die Funktion aufgerufen wird:
lea eax, LocalVar push eax
Da lea die Adresse eines Labels zur Laufzeit bestimmen kann, funktioniert das einwandfrei.


Deutsche Übersetzung: Joachim Rohde
Die original Win32Asm-Tutorials stammen von Iczelion's Win32 Assembly HomePage