Die Community zu .NET und Classic VB.
Menü

Visual Basic und Assembler - Seite 2

 von 

Grundzüge der Assemblerprogrammierung
Nächste Seite >>
Übersicht
<< Vorherige Seite

Begriffsbestimmungen  

Was ist Assembler

Für gewöhnlich wird heutzutage mit Basic, Fortran, Pascal, Delphi u.a. programmiert. Diese Sprachen werden Hochsprachen genannt, weil sie es ermöglichen, mit einem einzigen Befehl Fenster zu öffnen, Eingaben anzufordern und vieles mehr. Wer nur Hochsprachen kennt, hat keine Vorstellung davon, wie viel Programmieraufwand nötig ist, um tatsächlich ein Fenster auf dem Bildschirm zu erzeugen, denn der Prozessor, das Herzstück eines jeden PCs, kennt derlei Befehle nicht. Er verfügt zwar über Verbindungen (Ports) zu den Ein-/Ausgabegeräten, über die z.B. einzelne Punkte (Pixel) des Bildschirms gesetzt werden - Fenster oder auch bloß Text sind ihm aber fremd. Vielmehr hat jeder Prozessor seine eigene eher spartanisch ausgestattete Sprache, die Maschinensprache, und es ist Aufgabe der Hochsprachen-Compiler, ihren Befehlssatz in ausführbare Maschinensprache umzusetzen. Das Ergebnis ist eine unleserliche Bytefolge.

Selbstverständlich ist es möglich, diese Bytefolge, den Maschinensprache-Code selbst, d.h. ohne VB zu erzeugen - es dauert nur einfach länger. Hierfür stehen Hilfsprogramme zur Verfügung, die im Grunde genauso arbeiten wie der VB-Compiler: Sie ersetzen die Befehle, sog. Mnemoniks, durch die Bytefolgen, die sie repräsentieren. Der Unterschied zu Hochsprachen-Compilern besteht eigentlich nur darin, dass Mnemoniks 1:1 umgesetzt werden, d.h. jede Programmzeile genau einen ausführbaren Maschinenbefehl (OpCode) erzeugt, während einzelne Befehle einer Hochsprache derer etliche erzeugen können.

Programme, die Mnemoniks verarbeiten, werden seit jeher Assembler genannt, was dasselbe heißt wie Compiler (to assemble = to compile = zusammenbauen/-fügen). Der Programmcode, den sie erzeugen, heißt oftmals Assemblercode (ASM-Code), was es begrifflich erschwert, ihn vom ebenso genannten Quellcode zu unterscheiden.

Assembler und Prozessorfamilien

Wie gesagt, hat jeder Prozessor seine eigene Sprache, für deren Entwicklung der Prozessorhersteller (nicht die Softwareindustrie!) zuständig ist. Im Nachhinein nennt man Weiterentwicklungen, wie z.B. die Entwicklung des 8086 über den 80186 hin zum 80586 (sinnigerweise "Pentium") auch Prozessorfamilien. Inzwischen gibt es von Intel mehrere ähnlich strukturierte Prozessoren, die der Hersteller selbst als IA-Familie (Intel Architecture) bezeichnet. Computerhersteller legen sich mit der Auswahl eines Prozessors i.d.R. auf Gedeih und Verderb fest, da die Übertragung (Portation) vorhandener Programme auf andere Prozessoren ein schwieriges Unterfangen ist. So sind IBM-PCs seit Anbeginn, d.h. seit 1981, mit Weiterentwicklungen des 8086 von Intel bzw. baugleichen (kompatiblen) von AMD bestückt. Damit alte Programme weiterhin korrekt laufen, sind Neuentwicklungen stets abwärtskompatibel, enthalten also den kompletten Befehlssatz des Vorgängers, bieten darüber hinaus aber Erweiterungen. Für jeden Prozessor gibt es eigene Assembler. Für VB-Programmierer auf Windows-Plattformen ist zumeist nur der 80x86 von Belang - und auf ihn werden wir uns hier konzentrieren.

Wozu Assembler nutzen?

Hochsprachen haben Vor- und Nachteile. Der größte Vorteil ist sicher, Programmieren zu vereinfachen. Der ausgesprochene Name der Abkürzung BASIC spricht hier Bände: B EGINNERS A LL PURPOSE S YMBOLIC I NSTRUCTION C ODE, d.h. eine Befehlssprache, die es auch Anfängern ermöglicht, (beinahe) jede Aufgabe programmtechnisch umzusetzen. Von Nachteil kann dagegen die notwendige Beschränkung ihres Befehlssatzes sowie die stupide Umsetzung unterschiedlicher Aufgaben nach stets demselben Muster sein.

Als Beispiele für die notwenige Beschränkung mögen das Fehlen jeglicher Bitoperationen, direkten Speicherzugriffe oder Portmanipulationen in VB dienen. Auf den Punkt gebracht: Compiler erzeugen Maschinencode. Die Möglichkeiten des Maschinencodes sind grundsätzlich unbegrenzt. Hochsprachen bieten aber nur einen fixen Befehlssatz und grenzen damit die Programmiermöglichkeiten ein.

Als Beispiel für die stupide Umsetzung unterschiedlicher Aufgaben mag das Durchsuchen von Arrays nach bestimmten Werten herhalten: Wird ein Stringarray durchsucht, so kann dies schon aufgrund der Arraystruktur nur Wert für Wert in einer Programmschleife geschehen; wird ein Long-Array durchsucht, muss in VB genauso vorgegangen werden, obgleich die Maschinensprache des 80x86 einen entsprechenden Befehl bereitstellt, sodass keine zeitraubende For-Next-Konstruktion notwendig wäre. Es heißt, C-Code käme der Maschinensprache am nächsten. Doch auch C-Compiler wissen nicht, was sie tun; sie verstehen den kompilierten Programmcode nicht, und so passiert es, dass - in Text übersetzt - vor allem bei Schleifen Befehlsstrukturen wie diese auftauchen:

Lade X aus Speicherstelle 4711 (Zähler)
Inkrementiere X
Speichere X in 4711
Lade X aus Speicherstelle 4711
Vergleiche X mit dem Maximum
Springe ggf. hinter das Schleifenende
Sonst: Lade X aus Speicherstelle 4711 und tu irgendwas damit

X wird hier ein ums andere mal mit demselben Wert geladen. Das ist normalerweise auch nicht schlimm. Wenn die Schleife aber millionenfach durchlaufen werden soll, ist die Zeitersparnis eines kürzeren Codes durchaus auch ohne besondere Messtechnik spürbar.

Vor allem bei aufwendigen Operationen wie Graphikbearbeitung, Daten-Verschlüsselung o.ä. kann es daher sinnvoll sein, Zeit in die Assembler-Programmierung zu investieren. Die Betonung liegt auf kann, denn der VB-Compiler erzeugt gut optimierten Code, so dass umständlich geschriebene ASM-Programme mitunter auch langsamer sind ...

Welche Voraussetzungen muss der Assembler-Programmierer mitbringen?

Keine besonderen! Wie gesagt, ist Maschinensprache recht spartanisch ausgestattet, d.h. hinsichtlich Befehlsumfang und Syntax stellt sie die einfachste, in Anlehnung an den Begriff der Hochsprache - die primitivste Programmiersprache dar. Gerade hier ist aber der Clou: Da Prozessoren eigentlich nichts können außer addieren, muss der Programmierer jede - auch die einfachste - Aufgabe in kleinste Teilaufgaben sezieren. Es gibt nur Ja oder Nein, 1 oder 0, 5 Volt oder 0 Volt - keine "Grautöne" und kein Pardon, d.h. keine Fehlermeldung, sondern Computerabstürze, wenn etwas inkorrekt programmiert ist. Einzige Voraussetzungen für den Assembler-Programmierer sind damit ausgeprägtes analytisches Denken, Geduld und die Bereitschaft, sich auf das Binärsystem einzulassen.

Welchen Assembler nutzen?

Auf dem Markt sind viele Assembler erhältlich: Macro-Assembler (MASM) von Microsoft, Turbo-Assembler (TASM) von Borland, um nur die beiden bekanntesten und ältesten zu nennen. Wer einen C-Compiler besitzt, kann dessen Inline-Assembler nutzen. Außerdem wird Windows mit dem Zusatzprogramm DEBUG ausgeliefert, das es ermöglicht, Maschinencode zu erzeugen und abzuspeichern. Was letztlich zum Einsatz kommen sollte, wird vor allem durch die Anwendung bestimmt. Geht es darum, einige wenige Zeilen in Maschinencode umzusetzen, reicht der Debugger aus. Werden größere Routinen benötigt, ist die Arbeit damit etwas mühselig und sollte ein gängiges Produkt mit Beispielcodes, Programmbibliotheken usw. zum Einsatz kommen. Da MASM inzwischen kostenfrei zu haben ist (interessanterweise überall, nur nicht im Microsoft-Netzwerk MASM-Programm , MASM-Handbuch) und naturgemäß den Microsoft-Produkten am nächsten steht, wurden nachfolgende Beispiele hiermit verfasst. Grundsätzlich ist es aber einerlei, welcher Assembler verwendet wird; allerdings kann es sein, dass in anderen Umgebungen die eine oder andere Zeile angepasst werden muss.

Assemblercode in VB einbinden

Es gibt zwei Möglichkeiten, ASM-Code in VB einzubinden:

  • Zum einen bietet es sich an, DLLs ) zu erzeugen und dem VB-Programm per Deklaration bekannt zu machen. Der Code wird dabei voll in die DLL integriert und es besteht Zugriff auf alle im Umfeld deklarierten Variablen sowie auf das komplette Windows-API ). Sicher ist das die "sauberste" Lösung.
  • Zum anderen ist es möglich, Programmcode in einem Byte- oder Long-Array sowie in einem String (wegen der Unicode-Umwandlungen nicht empfehlenswert) innerhalb VBs zu speichern und direkt aufzurufen, denn, was für Hochsprachen undenkbar ist, weil ihr Code interpretiert/kompiliert werden muss, ist für Maschinensprache eine Selbstverständlichkeit: Maschinencode kann zur Laufzeit eines VB-Programms erzeugt, verändert und ausgeführt werden. In diesem Fall arbeitet das Assemblerprogramm sozusagen "stand-alone", d.h. es besteht nicht die Möglichkeit, auf Variablen zuzugreifen; allerdings können sie von VB beim Aufruf übergeben werden. MSDN Executing Assemble Code (Der originale Artikel in der MSDN existiert leider nicht mehr, daher verweist dieser Link auf eine archivierte Version des Textes.)

Verständlicherweise beziehen sich alle bisher im ActiveVB-Forum veröffentlichten Beiträge auf die letzte Variante, die direkte Einbindung in den VB-Code, und so soll es auch im nachfolgenden Text sein. Ist der Code erst einmal, sagen wir, in einem Array untergebracht, gibt es leider in VB keine Möglichkeit, ihn direkt aufzurufen; vielmehr muss ein Trick verwendet werden, um ihn ausführen zu können:

Einbindung durch Überschreiben VB-eigener Modul-Funktionen

Beim Kompilieren eines VB-Programms legt der Compiler die Adressen für alle enthaltenen Funktionen fest und erzeugt Call-Befehle an den Stellen, wo sie benutzt werden. Kopiert man den selbst erzeugten Maschinencode mit RtlMoveMemory an die mit AddressOf ermittelte Startadresse der Funktion, so wird anstelle des ursprünglich vom Compiler erzeugten Codes der eigene ausgeführt. Die Parameterdefinition der überschriebenen Funktion bleibt selbstverständlich erhalten, so dass die Assemblerroutine mit beliebig vielen Parametern versehen werden kann. Allerdings muss gewährleistet sein, dass die überschriebene Funktion auch genügend Speicherlatz belegt, um den ASM-Code aufzunehmen, sonst würden undefiniert darunter liegende Speicherbereiche überschrieben. Dieses Problem lässt sich umgehen, indem der ASM-Code in einen fixen/unbeweglichen (non-movable) Speicherbereich geladen und an den Anfang der Basicfunktion bloß ein Sprungbefehl gesetzt wird ). Der Sprungbefehl ist nur 5 Bytes lang; um sicherzustellen, dass die Basicfunktion genügend Speicher belegt, reicht es daher aus, eine For-Next-Konstruktion o.ä. einzufügen.

M.E. ist das Überschreiben vorhandener Basicfunktionen die unsauberste aller denkbaren Lösungen. Nichtsdestotrotz soll sie hier natürlich demonstriert werden (Beispiel 1).

Einbindung durch Überschreiben von VTable-Einträgen

Es ist auch möglich, die Funktion in einer Klasse zu kapseln. Der Vorteil ist, dass anstelle der eigentlichen Funktion nur ihr VTable-Eintrag, d.h. der Verweis (Pointer) auf den Funktionsanfang umgelenkt werden muss. Dabei ist zu beachten, dass

  • der ASM-Code in einem fixen Speicherbereich untergebracht ist, damit der VTable-Eintrag nicht nach einer Speicherreorganisation durch das Windows-Management ins Nirgendwo weist
  • VTables schreibgeschützt sind, d.h. mit Protect/Unprotect VirtualMemory gearbeitet werden muss
  • der VTable-Eintrag vor dem Schließen der Klasseninstanz wieder auf den Originalwert zurückzusetzen ist, da sonst ein Speicher-Ausnahmefehler erzeugt wird
  • nur eine Assemblerroutine pro Klasse untergebracht werden kann, da zur Entwicklungszeit nicht bekannt ist, in welcher Reihenfolge der Compiler die VTable-Einträge anlegen wird - für die erste Funktion ist das stets (ObjPtr + 28) -, bzw. erst aufwendig eben diese Informationen eingeholt werden müssen.

Diese Methode erscheint mir wesentlich sauberer, da kein Programmcode überschrieben wird, sondern ein Sprungvektor - und dafür sind Sprungvektoren da. Über das Terminate-Ereignis ist zudem sichergestellt, dass der reservierte Speicher auch wieder freigegeben wird. Allerdings ist der Programmieraufwand größer und die vom Klassen-Interface benötigte Zeit sollte auch nicht unterschätzt werden (Klassen sind langsam!). Siehe Beispiel 2.

Aufruf durch CallWindowProc

CallWindowProc ist eine im Windows-API definierte Funktion, eigentlich dazu gedacht, eine Windows-Prozedur mit den dafür üblichen vier Parametern aufzurufen. Man kann sich aber den Umstand, dass Windows grundsätzlich jede übergebene Adresse aufruft, zu nutze machen, indem man die Adresse des ASM-Codes übergibt. Als nachteilig mag man die Beschränkung auf vier Parameter empfinden, die aber nur eine vermeintliche ist, denn es ließe sich ja ein Pointer auf ein Array mit beliebig vielen Werten übergeben. Ad hoc erscheint außerdem nachteilig, dass der ASM-Code vom VB-Programm nicht direkt, sondern über Zwischenschritte aufgerufen wird. Allerdings benötigt der Aufruf über CallWindowProc gerade einmal 40 bis 50 CPU-Takte (clock cycles). Bei einem 1 GHz PC sind das 0.0000009 Sekunden. Ich denke, wenn diese Zeit nicht durch die Nutzung der Assemblerroutine wettgemacht wird, lohnt der Aufwand ohnehin nicht.

Unverkennbar: Ich bevorzuge diese Lösung der Einbindung von ASM-Code in VB, weil sie einfach und unproblematisch ist. Die ASM-Routine ist sauber in das Programm eingebunden, und die Speicherverwaltung wird VB überlassen, was die Sache vereinfacht. (Beispiel 3)

Nächste Seite >>
Grundzüge der Assemblerprogrammierung
<< Vorherige Seite
Übersicht