--->also available in English.

Win32 Assembler Tutorial Chapter 4k

So zutreffend wie dieses mal war der Titel dieses Tutorials noch nie: 4k alias 4096 Bytes sind eine Datenmenge, die in dem aktuellen Kapitel überall auftritt: Zuerst einmal ist sie die Größe der Speicherseitentabellen der x86er CPUs, welche in allen aktuellen 32-Bit-Betriebssystemen für Speicher- und Zugriffsschutz sowie das Auslagern des Speichers auf die Festplatte verwendet werden - natürlich auch in Win32. Zweitens sind 4k die maximale Programmgröße in den gleichnamigen Wettbewerben, was zu den Größenoptimierungstricks im zweiten Abschnitt paßt. Zuguterletzt organisiert auch das Dateisystem, welches im letzten Abschnitt angeschnitten wird, die Dateien oft in Blöcken von 4096 Bytes.
Weil es diesmal um mehrere kleinere Themen geht statt um ein großes gibt es diesmal kein Beispielprogramm. Macht aber nix, im Text gibt es genügend Beispielcode als auch wie gewohnt in der ZIP-Datei.

Dreigängemenü:

Das PE Dateiformat

Das PE (Portable Executable)-Format wird für Dateien wie .exe, .dll, .scr , .cpl usw. verwendet. Es besteht aus den Dateiköpfen (Header) und den einzelnen Sektionen

Folgende Header werden (in genau dieser Reihenfolge) verwendet:

Puh, bis jetzt sind schon (64 +?) + (4 + 20) + (96 + 16*8) = 312+ Bytes nur vom Dateikopf belegt, und das meiste davon ist mehr oder weniger nutzlos oder wird überhaupt nicht verwendet.

Die Sections

Die Sektionen sind das interessanteste an einer PE-Datei. Der gesamte Code, die Daten, Imports/Exports und/oder Ressourcen stecken in ihnen. Jede Sektion besteht aus 2 Teilen: Dem Section Header und den Daten der Sektion selbst.

Jeder Section Header ist 40 Bytes groß und aufeinanderfolgend angeordnet. Sie befinden sich direkt hinter dem optionalen Header. Sie enthalten den Name der Sektion, der jeder gewünschte Name sein kann, der maximal 8 Zeichen lang ist (unbenützte Zeichen werden mit Nullen gefüllt). So gibt es nichts, was einen daran hindert, seine Codesection .badcode oder die Ressourcesection blah zu nennen.
Der Section Header enthält desweiteren die Größe und den Offset der Sektion in der Datei und im Speicher.
Der letzte interressante Teil des Section Headers besteht aus den Flags (32 Bits), welche die Zugriffsrechte auf die Sektion angeben:

Es fällt auf, daß es kein Flag gibt welches angibt welchen Zweck eine Sektion erfüllt. Jedes Segment im Quellcode erzeugt üblicherweise eine Sektion, weitere Sektionen werden für den Import und Export externer Funktionen und Variablen sowie für dem Programm hinzugefügte Ressourcen verwendet. Eine PE-Datei besteht aus mindestens einer Sektion (sonst wäre sie leer) und kann soviele Sektionen haben wie es der Speicherplatz erlaubt.
Der Speicherplpatz, den eine Sektion in der Datei belegt entspricht der Größe der Sektion aufgerundet auf das nächste Vielfache des File Alignment-Feldes, das im Header definiert ist. Der Wert dieses Feldes ist eine Zweierpotenz zwischen 512 und 65536. Folglich belegt jede Sektion mindestens 512 Bytes in der Datei, selbst wenn sie nur ein einziges Byte enthält. Die einzigen Ausnahmen sind die letzte Sektion der Datei, bei welcher der ungenützte Teil weggelassen werden darf sowie komplett uninitialisierte Sektionen, für die keine Daten in der Datei gespeichert werden müssen.
Es gibt ein weiteres Feld, welches die Ausrichtung der Sektionen angibt: Object Alignment. Dieser Wert muß ein Vielfaches von 4096 sein, der Größe der Speicherseiten der x86-Prozessoren, und darf nicht kleiner als das File Alignment sein. Die Ausrichtung an der Größe der Speicherseiten liegt darin, daß das Betriebsystem die Zugriffsrechte praktisch vollständig über die Speicherseiten abwickelt.
Um eine PE-Datei klein zu halten setze man das FileAlignment auf 512 Bytes; um sie etwas schneller zu laden setzt man FileAlignment und ObjectAlignment auf 4096 Bytes. Im letzteren Falle muß der PE-Loader die einzelnen Sektionen nicht auf die nächste ObjectAlignment-Grenze aufblasen sondern kann sie direkt in den Speicher laden, da sie schon die richtige Größe haben.

Die meisten PE-Dateien nützen vordefinierte Namen für die Sektionen. Diese verwenden für bestimmte Zwecke jeweils bestimmte Namen, obwohl ihr Verhalten natürlich weiterhin von den Einträgen im PE-Header und/oder dem Data Directory bestimmt wird.
Folgende Bezeichnungen sind üblich:

Weitere Anmerkungen zu PE-Dateien, wie sie in den Speicher abgebildet werden und über die Speicherseiten zur Speicherverwaltung.

Die Sektionen können auf beliebige Art und Weise angeordnet sein. Das PE-Format erfordert nicht einmal, daß die Reihenfolge der Section Header mit der Reihenfolge der Sections in der Datei übereinstimmt.

Die oben genannten Sektionen werden von den meisten Compilern/Linkern verwendet. Dies ist allerdings nicht zwingend nötig. Laut der Definition des PE-Formats enthalten die Sektionen einfach nur irgendwelche Daten, die entsprechend der Beschreibung im Section Header in den Speicher geladen werden. Wofür sie benützt werden wird dagegen an verschiedenen Stellen im PE-Header beschrieben.
Aus diesem Grund kann man auch mehrere Sektionen in eine einzige packen.

Es gibt auch eine API-Funktion namens VirtualProtect, mit der man die Zugriffsrechte zu einem bestimmten Speicherbereich ändern kann. Per GlobalAlloc oder LocalAlloc angeforderter Speicher hat standardmäßig die Zugriffsrechte Readable + Writable + Executable, so daß man in diesen auch ohne weiteres Code hineinkopieren und ausführen kann

Weil der lineare Addressraum eines Prozesses auf beliebige Weise auf den physikalischen Speicher abgebildet werden kann schlagen Speicheranforderungen nie wegen Fragmentierung des physikalischen Speichers fehl. Allerdings kann der lineare Addressraum ebenfalls fragmentiert werden. Deshalb sollte man zuerst Speicher anfordern, der die gesamte Zeit belegt wird und dann erst Bereiche, die bald wieder freigegeben werden. Falls ein schon belegter Speicherblock in der Größe verändert werden soll, sollte man dem Speichermanager die Möglichkeit geben, diesen verschieben zu können, entweder in dem man ihn zuerst per Unlock entsperrt und dann per Lock wieder fixiert (Lock bei Speicher ist praktisch dasselbe wie ein Lock auf Surfaces oder Buffer in DirectX und gibt in allen Fällen einen Zeiger auf den gesperrten Speicherbereich zurück) oder indem man die GlobalReAlloc/LocalReAlloc Funktion folgendermaßen verwendet:

;für MASM und TASM: dword mit dword ptr oder large ersetzen

push dword GMEM_MOVEABLE        ;dieses Flag erlaubt der Speicherverwaltung, den Speicherblock an eine andere Stelle kopieren zu können
push dword NewSizeOfMemoryBlock ;die neue Größe des Speicherblocks
push dword MemoryHandle         ;das Handle wurde geliefert von GlobalAlloc oder GlobalReAlloc
call [GlobalReAlloc]            ;für MASM und TASM sind die Klammern zu entfernen
Ebenso kann mit per LocalAlloc/LocalReAlloc erhaltenem Speicher verfahren werden. Die Funktion gibt ein neues Handle für den Block zurück, welches zugleich die Adresse des Blocks ist. Von nun an ist das neue Handle zu benützen, da die Position des Blocks sich verändert haben kann. Übrigens gibt es keinen Unterschied zwischen Globalen und Lokalem Speicher mehr in Win32, da sich alles in einem einzigen 4GB großen Addressraum abspielt.

Die Adresse im linearen Addreßraum eines Prozesses, an die eine PE-Datei abgebildet wird ist durch das ImageBase-Feld im PE Header gekennzeichnet. Jedoch kann es vorkommen, daß diese Addresse schon belegt ist, so daß die PE-Datei anderswohin geladen werden muß. Die Relokationsinformationen werden dann benötigt, um die betreffenden Addressen neu zu berechnen. Bei Anwendungen die keine Funktionen oder Daten exportieren wird die Relokation nicht benötigt, da diese immer als erstes in ihren Adressraum geladen werden (mit einer Ausnahme: Bei einer Basisaddresse unterhalb 4MB wird die Datei immer an die Standardadresse 4MB reloziert). Deshalb sind die Relozierungsinformationen in einer Anwendung nicht nötig. DLLs dagegen werden oft reloziert, da vor ihnen schon einige andere Daten den Adressraum belegen und die vorgegebene Basisadresse oft schon belegt ist.
Einige Linker produzieren anscheinend Probleme bei der Relozierung. Indem man mit der Option -base ImageBase eine neue Basisaddresse setzt kann man das Problem aber meist lösen.

Größenoptimierung für Win32

Die einfachste Methode eine ausführbare Datei zu verkleinern ist ein EXE-Packer. Allerdings funtionieren diese bei PE-Dateien mit weniger als 10k nicht allzu gut. Eine andere Methode ist es, einen Dropper zu verwenden, ein Programm, das nichts anderes macht als eine Datei auf die Platte zu schreiben und es zu starten. Wenn der Dropper unter DOS läuft klappt dies besser, da der Header mitkomprimiert werden kann. Beide Varianten ändern an der ursprünglichen Dateigröße jedoch nichts und werden hier auch nicht weiter behandelt.

Der erste Angriffspunkt ist die Struktur einer PE-Datei.
Der gesamte PE Header (DOS MZ Header + DOS Code + PE Headers + Section Headers) wird mindestens zur nächsten 512-Byte-Grenze aufgeblasen. Aus diesem Grunde ist eine PE-Datei, noch bevor die Sektionen enthalten sind, mindestens 512 oder 1024 Byte groß. Der kleinste Wert für Section Alignment in der Datei ist 512 Bytes, so daß jede Sektion die Datei um mindestens 512 Bytes vergrößert. Die einzige Ausnahme ist die letzte Sektion, die nicht auf eine 512-Byte-Grenze erweitert werden muß, deshalb sollte die kleinste Sektion die letzte sein.

Solange man nicht den ganzen Header umorganisieren will, ist die einzige Möglichkeit zur Verkleinerung einer PE-Datei so wenige Sektionen wie möglich zu verwenden. Es ist sogar möglich, die Imports und Ressourcen mit dem Code und den Daten in ein- und dieselbe Sektion zu stecken. Code und Daten sind in derselben Section wenn der Quellcode für Daten und Code dasselbe Segment verwendet (genau wie bei Codesegment-Variablen). Und da DS,ES und SS den gleichen Speicherbereich wie CS verwenden wird nicht mal ein CS-Präfix benötigt. Diese Methode funktioniert mit jedem Assembler und Linker. Ob auch die Imports in derselben Section landen hängt entweder von der verwendeten Importbibliothek (*.lib) oder dem Assembler ab, je nachdem, wie die Imports eingebunden wurden. Bis jetzt ist mir kein Linker bekannt, der Ressourcen nicht in ein eigenes Segment steckt, allerdings kann man auf Ressourcen auch verzichten. Anwendungen werden üblicherweise nie reloziert, deshalb kann man die Relokationsinformationen problemlos aus der .EXE-Datei rauswerfen (einige Linker machen dies schon selbst).

Tip für Hardcore-Coder: Der aufgefüllte Bereich des PE Headers, der Code des DOS Stubs sowie alle anderen aufgefüllten Bereiche werden vom PE-Loader überhaupt nicht verwendet, so daß man dort wunderbar Daten und Code hineinstopfen kann. Die Code/Datasection vollzustopfen ist trivial, man braucht nur das entsprechende Segment im Quellcode zu füllen bis die Ausrichtungsgrenze erreicht ist. Um die restlichen leeren Felder (einige Hundert Byte) zu füllen sollten die Sektionen groß genug gesetzt sein und die Zugriffsrechte passen. Am einfachsten ist es, die Datei erneut in den Speicher zu laden, so daß man sich nicht mit Zugriffsrechten beschäftigen braucht, dies erfordert aber relativ viel Code. Oder man greift einfach auf das Speicherabbild zu (Der Header liegt direkt an der Basisaddresse), man muß aber beachten, daß die Datei nun dem ObjectAlignment entsprechend im Speicher liegt, so daß die Position im Speicher nicht diesselbe wie in der Datei ist.

Die zweite Methode zur Größenoptimierung ist dieselbe wie unter DOS: Den Code so kompakt wie möglich zu halten, indem man jedes eliminierbare Byte entfernt.
In diesem Tutorial werden nur speziell unter Win32 mögliche Tricks besprochen, die anderen sind dieselben wie unter DOS, es empfiehlt sich folglich, auch andere Tutorials zur Größenoptimierung zu lesen und, besonders wichtig, den kompletten x86-Befehlssatz gut kennenlernen und ein Gefühl dafür bekommen, welche und wieviele Opcode-Bytes eine Instruktion in Assembler erzeugt.

Einer der Bytefresser in Win32 sind Funktionsaufrufe. Push-Befehle auf den Stack sollten so kompakt wie möglich sein, am besten indem man Register verwendet. Viele Funktionsparameter sind auf 0 gesetzt. Solange ein Register auf 0 gesetzt ist lassen sich diese Parameter mit jeweils einem Byte erledigen. Die Instruktion zum Setzen eines Registers auf 0 braucht weniger Bytes als später durch die Verwendung des Registers als Push-Operand eingespart werden (z.B. benötigt xor eax,eax nur 1 Byte). Manchmal lassen sich auch die Aufrufe selbst optimieren. Liegt eine Konstruktion in folgender Form vor

	call [function_address]
	ret
kann man diese (wie vielleicht schon bekannt ist) durch folgenden Code ersetzen:
	jmp [function_address]

Als nächstes werfen wir mal einen Blick auf den Einsprungspunkt des Programms. Seine Definition lautet:

 WinMain( hInstance, hPrevInstance, lpszCmdLine, nCmdShow);

Jedes Programm ist also nichts anderes als eine normale Funktion mit folgenden Parametern auf dem Stack:

	[esp+16]: nCmdShow    ;dieser Wert gibt an, ob das Programmfenster normal, minimiert oder  maximiert angezeigt werden soll
	[esp+12]: lpszCmdLine ;Speicheraddresse der letzten Kommandozeileneingabe
	[esp+8]:  0           ;in Win32 immer auf 0 gesetzt
	[esp+4]:  hInstance   ;Instance Handle des aktuellen Prozesses
	[esp+0]:  stacked EIP ;Rücksprungaddresse
Ein Funktionsaufruf läßt sich mit diesem Wissen ersetzen: ExitProcess am Programmende. Eine einfacher ret - Befehl beendet das Programm ebenfalls (ist logisch: eine Funktion endet immer mit ret ;-). Darauf aufbauend der Anwärter für das Guinness-Buch der Rekorde, der kürzeste Code eines Win32-Programmes:
	Programmeinsprungspunkt: ret

	end Programmeinsprungspunkt
Der Code ist genau ein Byte groß (den Stack braucht man nicht abzuräumen, wenn sich das Programm beendet wirft ihn das Betriebsystem sowieso fort)!
Eventuell störend ist, daß DLLs nicht informiert werden, bevor sich das Programm beendet, was aber für 4k-Code meist kein Problem darstellt.

Ein anderer Vorteil von Win32 besteht darin, daß beim Beenden eines Programmes alle noch offenen Handles (Speicher, Dateien,...) automatisch geschlossen werden. Man muß sie also nicht unbedingt selber freigeben. Solange Größenoptimierung nicht das Hauptziel ist sollte man aus Gründen der Performance jedoch lieber die Handles sobald möglich von Hand freigeben.

Falls DirectX (oder andere Funktionen, die per COM Syntax aufgerufen werden) benützt wird erzeugt die Verwendung eines Makros wie das in den letzten beiden Tutorials verwendete jedesmal 20 Bytes an Code. Wenn man das Makro aber durch

DXcall:
	push edx
	mov edx,[edx]
	add edx,ecx
	mov edx,[edx]
	jmp edx
ersetzt und die Verwendung des Makros folgendermaßen vornimmt
	mov ecx, MethodToUse
	mov edx, InterfaceToUse
	call DXcall
verringert sich der Platzbedarf enorm.

Bei 32 Bit Code sind die Register standardmäßig 8 oder 32 Bit groß. Speicheraddressen sind immer 4 Byte groß. Einige arithmetische Instruktionen erlauben es, eine 4 Byte Immediate-Zahl (d.h. eine Zahl welche Teil einer Instruction wie in add eax,8 ist) als ein einzelnes vorzeichenbehaftetes Byte zu speichern solange sie in ein einzelnes vorzeichenbehaftetes Byte paßt. MASM und TASM benützen diese Kurzform standardmäßig überall wo sie möglich ist während NASM immer die 4-Byte-Form generiert. In diesem Fall wird in NASM ein Byte-Präfix benötigt, um die kürzere Form zu erhalten:

	add eax, byte 8

Wenn man nur einen Teil eines Dword-Operanden ändert verringert sich die Größe der Immediates ebenfalls. Allerdings spart man nur ein Byte ein, wenn man statt auf ein Dword ein Word verändert, da das 16 Bit-Operandengrößenpräfix ein eigenes Byte benötigt.

Das Belegen von Speicher kann ebenfalls verkleinert werden. Statt dazu GlobalAlloc oder LocalAlloc zu verwenden läßt sich Code ebenfalls einsparen wenn man den Speicher auf dem Stack anlegt. Dies ist mit dem Anlegen lokaler Variablen auf dem Stack identisch:

	sub esp,Speicherblockgröße ;man denke daran, daß der Stapel nach unten wächst
	                           ;esp zeigt nun auf den belegten Speicherblock
Wenn man den so belegten Speicher wieder freigeben muß (eigentlich nur nötig wenn eine ret-Instruction folgt) nimmt man:
	add esp,Speicherblockgröße
Von besonderer Bedeutung im Zusammenhang mit dem Stack sind zwei Felder des PE-Headers: StackReserveSize und StackCommitSize. Das erste gibt an, wie groß der Stapel maximal werden kann ohne andere Speicherbereiche im Addressraum des Prozesses zu überschreiben. Dazu wird die entsprechende Speichermenge in den Seitentabellen als vorbelegt markiert. Das zweite gibt die Menge an physikalischem Speicher an, der schon für den Stack in den Addreßraum gemappt wurde.
Wenn man den Stack zur Reservierung größerer Speicherblöcke verwendet sollte zumindest der StackReserve-Parameter groß genug sein, um den gesamten reservierten Speicher, die maximale Menge auf dem Stack zwischengespeicherter Parameter sowie eine Sicherheitsreserve zu enthalten. Das Programm läuft etwas schneller, wenn StackCommit so groß wie die maximal erwartete Stackgröße (mit darauf angelegtem Speicher) ist. Aber man sollte auch daran denken, daß man den Stack nicht zu groß machen sollte, da sonst der Speicher woanders fehlt (der freie Addreßraum eines Prozesses beträgt 2 oder 3 GB minus der Größe der gemappten Datei und der Stackgröße).
Das Anfordern von Speicher vom Stack hat den Nachteil, daß sich der Speicherblock weder vergrößern noch freigeben läßt solange noch Daten auf dem Stack liegen, die nach dem Belegen des Speicherblocks angelegt wurden.

Ein weiteres häufiges Problem ist Registermangel, so daß man auf Speichervariablen ausweichen muß. Eine im Code oder Datensegment vordefinierte Variable braucht für den Zugriff alleine schon 5 Bytes, eine relative Addresse ist immerhin immer noch mindestens ein Byte größer als wenn man stattdessen auf ein Register zugreift. Es wäre oft schon besser, wenn man alle 8 Universalregister verwenden könnte. 8 Register? Genau, esp ist auch eines, oder etwa nicht? Es läßt sich genauso wie esi,edi oder ebp einsetzen.
Und es gibt einige praktische Anweisungen wenn man den Inhalt von esp als Speicheraddresse auffaßt: push entspricht in etwa stosd und pop ist mit lodsd vergleichbar, mit dem Unterschied daß esp immer unabhängig vom DirectionFlag bei einem push herunter- und bei einem pop heraufgezählt wird. Dafür sind sie aber wesentlich vielseitiger weil sie fast jeden Operand als Quelle oder Ziel akzeptieren und nicht nur eax wie es bei lodsd oder stosd der Fall ist (dafür sind lodsd/stosd manchmal kleiner).

Es ist ein verbreiteter Irrglaube, daß das Verändern von esp oder des Speichers, auf den esp zeigt, das Programm zum abstürzen bringt solange man sich nicht auf push, pop, call, ret oder int beschränkt. Tatsache ist, daß nur solange esp als Stackzeiger verwendet wird esp auf die richtige Addresse zeigen muß. Es ist sogar möglich, esp auf einen anderen Speicherbereich zeigen zu lassen und zusätzlich esp immer noch als Stackzeiger zu benützen. Vorraussetzung dafür ist daß der Speicher, auf den esp zeigt vom Programm auch angefordert oder belegt wurde und unterhalb von esp unbenützter Speicher zur Verwendung als Stack zur Verfügung steht. Man kann auch esp als Zeiger auf einen Speicherbereich verwenden, der in einer Schleife mit absteigenden Addressen gefüllt wird und dabei immer noch esp als Stackzeiger verwenden da dabei stets der von der Schleife gefüllte Speicher oberhalb von esp liegt und der Stapel unterhalb von esp. Zusammenfassend sind also die folgenden Punkte zu beachten:

Da push, pop, call und ret nur an ihrer Position im Code auftreten läßt sich der aktuelle Zustand von esp sowie die Addresse auf die esp zeigt leicht verfolgen. Hardwareinterrupts können dagegen immer auftreten und lassen sich zudem nicht per Interrupt-Flag ausmaskieren (das Interrupt-Flag kann nur im Kernel verändert werden). Das gibt in Win32 aber keine Probleme: Interruptprozeduren sind ein Teil des Kernels und/oder andere VirtuellerMaschinen, aus diesem Grund wird bei einem Interrupt esp auf einen eigenen Speicherbereich gesetzt und nach dem Interrupt wiederhergestellt.

Console Apps and File I/O

Bis jetzt haben alle Beispiele des Tutorials die grafische Oberfläche verwendet. Aber es gibt auch noch Kommandozeilenprogramme für die Konsole. Die Konsole ist ein einfaches Fenster welches dazu benützt wird um Eingaben von der Tastatur zu lesen und um Text darin auszugeben. Es ist dasselbe Fenster, welches auch für die DOS-Box verwendet wird. Jedes Programm kann eine Konsole haben (allerdings pro Programm maximal eine), der einzige Unterschied zwischen Konsolenanwendungen und GUI(=GraphicalUserInterface)-Anwendungen besteht darin, daß Konsolenanwendungen sie nicht extra (per AllocConsole) erzeugen müssen sondern schon mit einer dazugehörigen Konsole gestartet werden. Es handelt sich nur um einen einzigen Eintrag im PE-Header, der dem PE-Loader mitteilt ob er eine Konsole anfordern soll oder nicht. Falls man eine Konsolenanwendung von einer Konsole aus startet halten alle anderen Aktivitäten in dieser solange an bis das Programm sich beendet.

Auf zweierlei Arten lassen sich Zeichen von der Konsole lesen bzw. ausgeben. Die sogenannten low-level-Funktionen arbeiten direkt mit dem Ein- und Ausgabepuffer der Konsole, eine zweidimensionale Cursorposition verwendend. Und es gibt die Funktionen welche mit den Standarddateiehandles STDIN, STDOUT und STDERR arbeiten. Standardhandles lassen sich wie Dateihandles in den Funktionen zum Dateizugriff verwenden.

Ein Standardhandle erhält man entweder, indem man GetStdHandle oder CreateFile mit dem vorgegebenen Dateinamen des Standardhandles aufruft. Obwohl Konsoleanwendungen nicht sonderlich schön aussehen sind sie für Debugging- und Testzwecke äußerst praktisch. Nicht nur daß man ein unabhängiges Fenster hat, welches sich leicht auslesen und beschreiben läßt und sich für die Besitzer mehrere Bildschirme auf den zweiten verschieben läßt, es bietet zudem die Möglichkeit, die Ein- und Ausgaben entweder in eine Datei auf der Platte oder in eine andere Standarddatei wie COM oder LPT umzuleiten. Man kann mit Hilfe der Konsole auch Daten mit einem anderen Prozess austauschen, wenn die Konsolenanwendung von einer anderen Anwendung gestartet wurde. Zu guter Letzt läßt sich eine Konsolenanwendung sehr leicht in eine GUI-Anwendung ändern, welche die Ausgaben statt auf die Konsole in eine Datei auf der Festplatte schreibt. Man muß nur ein Flag im Linker anpassen und ein paar Parameter von CreateFile anpassen.

Der folgende Code liefert ein Handle für STDOUT:

	;für MASM und TASM ist dword mit dword ptr oder large ersetzen

	push dword STD_OUTPUT_HANDLE ;um STDERR zu benützen geht alternativ STD_ERROR_HANDLE
	call [GetStdHandle]          ;für MASM und TASM sind die Klammern zu entfernen
	                             ;eax enthält das gewünschte Handle oder -1 wenn etwas
	                             ;nicht funktioniert hat
Ein nicht-umleitbares Handle bekommt man per:
	push dword 0
	push dword FILE_ATTRIBUTE_NORMAL
	push dword CreationFlag        ;siehe unten
	push dword 0
	push dword FILE_SHARE_READ + FILE_SHARE_WRITE ;oft reicht auch 0 als Parameter aus
	push dword GENERIC_WRITE
	push dword address_of_filename ;identisch mit offset filename
	call [CreateFileA]
	                               ;eax enthält das gewünschte Handle oder -1 im Fehlerfall
Die folgenden Dateinamen sind bei CreateFile möglich: Die folgenden Werte für creation flags können bei CreateFile verwendet werden, um auf eine normale Datei auf der Platte zuzugreifen: Die Konsole, Schnittstelle oder Datei wird beschrieben mit:
	push dword 0
	push dword AddresseEinerVariableWelcheMitDerAnzahlGeschriebenerBytesGefülltWerdenSoll
	push dword AnzahlZuSchreibenderBytes
	push dword AddresseDerZuSchreibendenBytes
	push dword handle ;erhalten durch GetStdHandle oder CreateFile
	call [WriteFile]
Bei Zeichenketten braucht keine abschließende 0 zu folgen, da die Länge der Zeichenkette explizit angegeben wird. Um eine neue Zeile zu starten muß man einfach die Bytes für den Zeilenrücklauf einfügen.

Sonstige Anmerkungen

Nun sollten die meisten Knackpunkte bekannt sein die man braucht um Programme in Assembler für Win32 zu schreiben oder auf Win32 zu portieren. Die meisten weitergehenden Themen sind in den SDKs enthalten, sind dort recht gut dokumentiert und oft mit Beispielcode versehen (überwiegend in C, aber dennoch auch für nicht-C-Programmierer verständlich). Damit ist dieses Win32Asm-Tutorial beendet, da die meisten Algorithmen nicht sprachspezifisch sind und somit nicht in dieses Tutorial passen. Es sollte auch kein Problem darstellen dürfen sich im SDK zurechtzufinden (und nur Sourcecode zu verbreiten bringt auch nichts, man muß es schon selber implementieren können).

Im Gedächtnis behalten sollte man sich aber immer:

Viel Spaß beim Coden und haltet nach anderen Texten von mir Ausschau

T$

Mail an den Autor: webmeister@deinmeister.de

Hauptseite Programmieren Win32Asm Downloads Software Hardware Cartoons+Co Texte Sitemap