Die Community zu .NET und Classic VB.
Menü

Subclassing per ASM innerhalb einer Klasse und ohne Modul

 von 

Motivation 

Die Technik des Subclassings dürfte wohl den meisten ein Begriff sein. Leider kann die dazu benötigte Callback- Funktion in VB nur in einem Modul abgelegt werden. Es soll nun gezeigt werden, wie man mittels eines in Maschinensprache erstellten Wrappers diese Einschränkung umgehen und damit auf ein Modul verzichten kann. Wer eine grobe Vorstellung davon hat, was Subclassing ist und wie Software auf Maschinenebene funktioniert, sollte diesem Text relativ problemlos folgen können. Die anderen sowie diejenigen, die vorher ihr Wissen noch mal auffrischen wollen, seien an dieser Stelle auf Tutorial 0005 bzw.Tutorial 4011 verwiesen.

Das Problem  

Subclassings werden mittels der API-Funktion SetWindowLong() und dem Parameter GWL_WNDPROC gesetzt. Als dritter Parameter wird die Adresse der Callback-Funktion erwartet, welche folgende Signatur besitzen muss:

Public Function WindowProc(ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

End Function

Listing 1

Mittels des AddressOf-Operators wird der Zeiger, also die Adresse, an der sich der Maschinencode der Funktion im Speicher befindet, ermittelt.

OldWndProc = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf WindowProc)

Listing 2

Dies funktioniert jedoch nur so lange, wie WindowProc in einem Modul definiert ist. Versucht man nun, die Funktion in eine Klasse zu verschieben, beschwert sich VB darüber mit der Meldung Ungültige Verwendung des AddressOf-Operators. Wo liegt aber nun der Unterschied zwischen in Modulen definierten Funktionen und deren Äquivalenten in Klassen?

Von Modulen zu Klassen  

Variablen, die ausserhalb einer Klasse definiert werden, stehen zur Laufzeit des Programms genau einmal pro Programm an einer festen, dafür reservierten Adresse im Speicher, die zur Kompilierzeit bestimmt wird. Über diese Adresse kann immer noch zur Kompilierzeit für das ganze Programm festgelegt werden, wo genau die Daten vorzufinden sind.

Member-Variablen von Klassen hingegen liegen zur Laufzeit genau einmal pro geladener Instanz der Klasse vor. Da die Anzahl der geladenen Instanzen im Allgemeinen letztendlich von Benutzereingaben abhängt, ist es zur Kompilierzeit nicht möglich, Speicher vorab zu belegen und damit Adressen zu bestimmen. Beides muss also zur Laufzeit dynamisch geschehen. Da man zur Laufzeit aber keinen Compiler zur Verfügung hat, kann nicht einfach der Quellcode kurzerhand mit einem neuen Satz an Adressen verknüpft werden. Auch will man nicht unnötig mehrfach den gleichen Code im Speicher halten. Aus diesem Grund gehört zu jeder Instanz einer Klasse eine eindeutige Zahl, anhand derer nun der zur Klasse zugehörige Satz an Variablen erreicht werden kann: der Instanzzeiger.

Damit Code, der in einer Klasse definiert ist, nun auch tatsächlich auf die zu einer bestimmten Instanz gehörenden Variablen zugreifen kann, muss der Instanzzeiger bekannt gemacht werden. Dies geschieht in VB bei jedem Aufruf einer in einer Klasse liegenden Funktion mittels eines für den Programmierer unsichtbaren Parameters. Aus der engen Verwandtschaft von VB zu COM (Component Object Modell, eine unter Windows verwendete Technik, die Interprozesskommunikation und dynamischen Objekterzeugung in vielen Programmiersprachen bereitstellt) alle Klassen in VB sind COM-Objekte heraus, folgt eine weitere, nicht offensichtliche Konvention. COM definiert als Rückgabewert einer Funktion den Win32-Typ HRESULT, eine 32-Bit breite Variable, die über Fehler oder (partiellen) Erfolg berichtet. Der vom VB-Programmierer gesetzte Rückgabewert muss dem offensichtlich weichen, woraus sich ein weiterer, für den Programmierer unsichtbarer Parameter ergibt, der nichts anderes als ein Zeiger auf eine 32-Bit breite Puffervariable ist.

Definiert man obige WindowProc in einer beliebigen Klasse und betrachtet das Kompilat, so findet man folgende nach VB zurück übersetzte Signatur:

Public Function WindowProc(ByVal Instanzzeiger As Long, ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long,
ByVal lParam As Long, ByRef Rückgabe As Long) As Long

End Function

Listing 3

Diese stimmt aber zum einen nicht mehr mit der von SetWindowLong() geforderten Signatur überein. Zum anderen kann die API offensichtlich weder mit dem Instanzzeiger einer Klasse, noch mit dem zusätzlichen Parameter umgehen.

Der erste Ansatz  

Um die Nachrichten aus einem Subclassing trotzdem in einer Klasse verarbeiten zu können, kann man sich einer Hilfsfunktion in einem Modul bedienen, welche die Parameter unverändert in eine bestimmte Instanz der Klasse leitet.

Public Function WindowProc(ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
    WindowProc = Klasse.WindowProc(hWnd, Msg, wParam, lParam)
End Function

Listing 4

Wie man sich leicht überlegen kann, muss dabei zu jeder Instanz der Klasse eine Referenz bereitgehalten werden. Außerdem muss entweder

  • für jedes zu erstellende Subclassing eine eigene Hilfsfunktion erstellt werden
  • innerhalb einer einzigen Hilfsfunktion ermittelt werden, zu welcher Klasseninstanz die Nachricht gehört

Beide Varianten sind nicht zufrieden stellend. Für eine Kapselung der Funktionalität scheint jedoch die erste Variante am besten geeignet, weswegen diese im Weiteren als Ansatz für einen Wrapper gewählt wird. Das nächste Etappenziel liegt nun darin, den Instanzzeiger sowie die Adresse der Funktion WindowProc zu ermitteln.

Vom Instanzzeiger zu Mitgliedern der Klasse  

An den Instanzzeiger einer Klasseninstanz, der als erster Parameter der WindowProc erwartet wird, gelangt man über die undokumentierte Funktion

Instanzzeiger = ObjPtr(Klasseninstanz)

Listing 5

respektive innerhalb der fraglichen Klasse selbst über

Instanzzeiger = ObjPtr(Me)

Listing 6

Nun muss noch die Adresse der sich nun in einer Klasse befindlichen WindowProc ermittelt werden, damit diese aus dem Wrapper heraus aufgerufen werden kann. Da es sich wie weiter oben erwähnt bei einer Klasse in VB letztendlich um ein COM-Objekt handelt, auf dessen Methoden möglicherweise auch andere Sprachen zugreifen können sollen, müssen die Adressen zur Laufzeit ermittelbar sein.

Der von COM dazu genutzte Mechanismus nennt sich vTable-Binding. Dabei ist obiger Instanzzeiger nichts anderes als ein Zeiger auf einen Speicherbereich. Folgt man der vom Zeiger beschriebenen Adresse, so erhält man den vPtr, einen Zeiger, der wiederum auf das erste Element einer Tabelle zeigt, in der alle öffentlichen Methoden des Objekts aufgeführt sind die vTable (virtual method table) für das standardmäßige Interface, in diesem Fall das Interface der implementierten Klasse.

Der Vollständigkeit halber sei an dieser Stelle noch erwähnt, dass ein Objekt durchaus mehrere vTables besitzen kann, abhängig davon, wie viele Interfaces implementiert werden. Aus diesem Grund muss jede vTable als erste Einträge die drei Methoden des wiederum von COM beschriebenen IUnknown-Interfaces auflisten:

Public Function QueryInterface(ByVal riid As IID) As Long
Public Function AddRef() As Long
Public Function Release() As Long

Listing 7

AddRef() erhöht den internen Referenzzähler, der verhindert, dass eine noch benötigte Instanz der Klasse gelöscht wird; Release() erniedrigt diesen wieder und löscht bei Bedarf die Instanz. Von erweitertem Interesse ist die Funktion QueryInterface(). Dieser wird die GUID (global unique identifier, ein 128 Bit breiter Wert, der mit einer Wahrscheinlichkeit von 1:2^128 weltweit eindeutig ist) des gesuchten Interfaces übergeben. Wird dieses Interface durch das Objekt implementiert, so liefert die Funktion einen Zeiger auf das Interface zurück, von dem ausgehend wieder die zugehörige vTable erreicht werden kann.

Damit bleibt nur noch die Frage, an welcher Stelle der Tabelle die gesuchte Funktion zu finden ist. Leider lässt sich dieses Problem nicht auf triviale Weise lösen, da Bezeichner, also auch Funktionsnamen, durch den Kompilationsvorgang verloren gehen. COM löst dieses Problem dadurch, dass jedes implementierte Interface eine eigene vTable besitzt, deren Aufbau sich daraus ergibt, dass seitens der zugehörigen Interfaces eine bestimmte Reihenfolge der Methoden definiert ist.

Diesen Umstand kann man sich nun zunutze machen, da der VB-Compiler öffentliche Funktionen gemäß ihrer Reihenfolge in der vTable einträgt. Damit gilt im Weiteren der allgemeine Grundsatz, dass die Callback-Funktion öffentlich ist und in der Quellcodedatei unmittelbar auf den Deklarationsteil folgt. Dies führt für Klassen, sowie deren spezialisierten Geschwistern, Usercontrol und Form, zu festen Positionen in der Tabelle, an denen die gesuchten Adressen zu finden sind. Im Code folgt man dazu dem Instanzzeiger

CopyMemory vPtr, ByVal Instanzzeiger, 4

Listing 8

und liest im Falle einer Klasse das achte Element

CopyMemory CallbackAddress, ByVal (vPtr + 7 * 4), 4

Listing 9

im Falle einer Form das vierhundertsiebenundvierzigste Element

CopyMemory CallbackAddress, ByVal (vPtr + 446 * 4), 4

Listing 10

im Falle eines Usercontrols das vierhundertneunzigste Element

CopyMemory CallbackAddress, ByVal (vPtr + 489 * 4), 4

Listing 11

An dieser Stelle sei nochmals eindringlich daran erinnert, auf obige Konvention zu achten. Liest man vom falschen vTable-Eintrag, so wird man mindestens Fehlfunktionen des Programms beobachten, in den meisten Fällen unmittelbar in Form eines Absturzes. Auch sollte man von öffentlich deklarierten Variablen Abstand nehmen, da diese auch Einfluss auf den Aufbau der vTable nehmen. Lässt sich dies gar nicht vermeiden, so sind für jede Variable acht Bytes zum Offset zu addieren.

Wechsel auf die Maschinenebene  

Nun besteht die Aufgabe also darin, die vier Parameter der Fensterprozedur entgegen zu nehmen, diese um den Instanzzeiger und den Rückgabepuffer zu erweitern und damit letztendlich die Funktion innerhalb der Klasse aufzurufen.

Dazu stellt sich zuerst die Frage der verwendeten Aufrufkonvention, also, in welcher Art und Weise die Parameter an eine Funktion übergeben und der Rückgabewert erhalten wird. Hier definiert COM die stdcall (standard call) genannte Konvention. Diese beschreibt, dass die Parameter vor dem Funktionsaufruf in umgekehrter Reihenfolge, also von rechts nach links, auf den Stack (Stapelspeicher des Programms, verwendet first-in, last-out Logik) geschoben werden. Verantwortlich für die Bereinigung des Stacks ist die aufgerufene Unterfunktion. Des Weiteren legt die Unterfunktion den Rückgabewert im Prozessorregister EAX ab. Auf Maschinenebene sieht der Aufruf der Funktion WindowProc im einfachsten Fall wie folgt aus:

push lParam
push wParam
push Msg
push hWnd
call WindowProc

Listing 12

Die push-Instruktion legt den jeweiligen Operanden als unterstes Element auf den Stack, der damit nach unten(!) wächst. Die call-Instruktion sorgt schliesslich dafür, dass die Funktion WindowProc (die in Form einer codierten Adresse vorliegt) aufgerufen wird. Dabei legt der Prozessor noch automatisch einen weiteren Wert auf den Stack, den Rücksprungzeiger. Dieser zeigt auf die Instruktion, die unmittelbar nach der call-Instruktion folgt. Durch die first-in, last-out Logik des Stacks lassen sich damit auch komplexe Unterfunktionsaufrufe und deren Spezialfall, die Rekursion, effizient verarbeiten.

Für eine in Maschinencode implementierte WindowProc bedeutet das also, dass der Stack mit folgendem Aufbau vorzufinden sein wird:

(unbekannte nicht relevante Daten)
lParam
wParam
Msg
hWnd
Rücksprungzeiger <- ESP

ESP (extended stack pointer) ist das Prozessorregister, das den Stapelzeiger enthält. Dieser zeigt immer auf das unterste Element des Stacks. Der Rücksprungzeiger stellt die Adresse dar, an der nach der Ausführung der Funktion WindowProc fortgesetzt werden soll.

Die erste Variante  

Eine erste Variante des ASM-Wrappers könnte damit so aussehen:

push 0
push ESP
push [ESP+18h]
push [ESP+18h]
push [ESP+18h]
push [ESP+18h]
push Instanzzeiger
mov EAX, Funktionszeiger
call EAX
pop EAX
ret 10h

Listing 13

push 0 reserviert auf dem Stack eine 32-Bit breite Variable, die im Folgenden als Zwischenspeicher für den Rückgabewert der Methode dient. Da ESP wieder auf das unterste Element des Stacks zeigt, legt das folgende push ESP den Zeiger auf die unmittelbar vorher gepushte Variable, also den Zwischenspeicher für den Rückgabewert, auf den Stack. Dies ist der letzte Parameter der in einer Klasse definierten WindowProc- Funktion. Betrachtet man an dieser Stelle den Stack:

(unbekannte nicht relevante Daten)
lParam
wParam
Msg
hWnd
Rücksprungzeiger
0
Zeiger auf Rückgabepuffer <- ESP

so sieht man, dass der nächste Parameter, lParam, sechs Einträge weiter oben steht. Entsprechend wird lParam als das Element gepusht, dass sich an ESP+6 (Elemente) *4 (Bytes) = ESP+24 Bytes oder in hexadezimaler Schreibweise an ESP+18h befindet. Nach der Ausführung einer weiteren push-Instruktion befindet sich nun lParam an unterster Stelle des Stacks.

(unbekannte nicht relevante Daten)
lParam
wParam
Msg
hWnd
Rücksprungzeiger
0
Zeiger auf Rückgabepuffer
lParam <- ESP

Der nächste Parameter, wParam, befindet sich nun auch sechs Einträge weiter oben, wodurch sich hier wie, auch für die dann folgenden Msg und hWnd, wieder die gleiche Instruktion ergibt. Übrig bleibt noch der Instanzzeiger, der übrigens erst zur Laufzeit des VB-Programms feststeht und deswegen vorerst nur in Form einer beliebigen Zahl codiert wird. Vor dem Aufruf der Funktion durch call EAX wird noch der Funktionszeiger in das Register EAX geschrieben. Der Umweg über das Register wird einem technisch möglichen call Funktionszeiger vorgezogen, um ein müßiges Berechnen von relativen Sprungoffsets zu vermeiden, da die call-Instruktion in letzter Variante nicht mit absoluten Adressen umgehen kann. Mittels pop EAX wird nach Abarbeitung der Unterfunktion noch der Wert, der im Rückgabepuffer abgelegt wurde, gemäß stdcall-Konvention in das Register EAX verschoben. Das Ende des Wrappers wird mit ret 10h signalisiert, was die durch den Wrapper implementierte Funktion beendet und gleichzeitig noch 10h Bytes = 10h / 4 (Bytes) = 4 Einträge vom Stack löscht.

Kompatibilität mit aktuellen Systemen  

Der obige Maschinencode muss, um mit Techniken wie DEP (Data Executen Prevention, Technik zur Verhinderung der Abarbeitung von als Daten markierten Segmenten als Maßnahme gegen Buffer-Overflow Lücken) kompatibel zu bleiben, in einem Speicherbereich abgelegt werden, der explizit für die Ausführung von Code markiert ist. Entsprechenden Speicher fordert man ausschließlich über die API-Funktion VirtualAlloc() an. Wird der Speicher nicht mehr benötigt, muss er mit VirtualFree() wieder freigegeben werden. Genau das wird in obigem Ansatz jedoch zum Problem. In dem Moment, in dem der Speicher freigegeben wird, muss sichergestellt sein, dass das Programm nicht mehr in die WindowProc(), oder genauer, in den Wrapper springt. Genau das kann man aber nicht garantieren, da beispielsweise das dafür prädestinierte Unload-Event innerhalb einer Rekursion der WindowProc() - und damit auch des Wrappers - entstanden sein könnte. Jeglicher Versuch, hier den Speicher freizugeben, resultiert in einer Zugriffsverletzung und damit im sofortigen Absturz des Programms.

Die überarbeitete Variante  

Um auch dieses Problem zu lösen, muss nun die Rekursionstiefe protokolliert werden. Ferner muss der Wrapper, sofern ein Entladen während einer Rekursionstiefe ungleich null angefordert wird, in der Lage sein, sich selbst zu löschen, sobald er nicht mehr benötigt wird. Eine Implementation dessen führt zu:

inc [Zähler]

push 0
push ESP
push [ESP +18h]
push [ESP +18h]
push [ESP +18h]
push [ESP +18h]
push Instanzzeiger
mov eax, Funktionszeiger
call eax

dec [Zähler]

mov EAX, [Signal]
test EAX, EAX
jne Marke1

pop EAX
ret 16

Marke1:
mov EAX, [Zähler]
test EAX, EAX

je Marke2

pop EAX
ret 16

Marke2:
pop EAX
pop ECX
pop EAX
pop EAX
pop EAX
pop EAX
push MEM_RELEASE
push 0
push Wrapper Adresse
push ECX
mov EAX, VirtualFree Adresse
jmp EAX

Listing 14

[Zähler] und [Signal] sind hier Zeiger auf Variablen, die erst zur Laufzeit generiert werden; die eckigen Klammern bedeuten, dass einem Zeiger gefolgt werden soll. Im ersten Schritt wird nun die Zählervariable, auf die Zähler zeigt, per inc erhöht. Darauf folgt der aus der ersten Variante übernommene Aufruf der Methode. Direkt im Anschluss wird die Zählervariable wieder per dec erniedrigt.

Nun wird das Signal, dass die Selbstlöschung anzeigt, eingelesen (mov). Mittels der test-Instruktion wird überprüft, ob mindestens ein Bit in beiden Operanden gesetzt, als in diesem Fall, ob das Signal gesetzt ist. Wenn nein, wird die Funktion mit dem aus der aufgerufenen Methode übernommenen Rückgabewert beendet. Wenn ja, springt der Code zu Marke1 und prüft hier, ob der Zähler gleich Null ist. Wenn nein, wird die Funktion auch hier wie eben beendet. Wenn ja, wird zu Marke2 gesprungen.

In diesem Fall folgt nun der Code, durch den sich der Wrapper selbst entlädt. Offensichtlich kann dies aber aus dem gleichen Grund wie innerhalb von VB nicht direkt per call VirtualFree() geschehen - der Stack enthält dann ja in Form eines Rücksprungzeigers noch eine Referenz auf den ASM-Wrapper, die durch die obligatorische ret-Instruktion einer Unterfunktion verarbeitet werden würde. Deswegen muss dafür gesorgt werden, dass nach Aufruf der VirtualFree()-Funktion der Code in keinem Fall mehr in den Wrapper zurückkehrt. Zuerst wird dazu der Stack

(unbekannte nicht relevante Daten)
lParam
wParam
Msg
hWnd
Rücksprungzeiger
0 <- ESP

von den für den Rückgabewert reservierten 4 Bytes befreit. Der Rücksprungzeiger wird als nächstes im Register ECX temporär gesichert. Sodann werden auch die nun überflüssigen Parameter hWnd bis lParam entfernt. Der Stack hat nun also die Form, die er schon vor dem Aufruf des Wrappers hatte:

(unbekannte nicht relevante Daten) <- ESP

Nun wird der Stack um die Parameter für die VirtualFree()-Funktion ergänzt:

(unbekannte nicht relevante Daten)
MEM_RELEASE
0
[Wrapper] <- ESP

wobei [Wrapper] der Zeiger auf den Datenblock ist, der für den Wrapper reserviert wurde. Um nun wie oben beschrieben ein call VirtualFree() zu vermeiden, baut man die call-Instruktion mit alternativen Befehlen nach und schiebt dabei VirtualFree() genau den Rücksprungzeiger unter, den der Wrapper selbst bei einer ret-Instruktion verwenden würde. push ECX führt nun zu:

(unbekannte nicht relevante Daten)
MEM_RELEASE
0
[Wrapper]
Rücksprungzeiger in die Funktion, die den Wrapper aufgerufen hat <- ESP

Um die VirtualFree()-Funktion letztendlich auszuführen, wird deren Adresse in das Register EAX geschrieben (mov EAX) und per jmp (unbedingter Sprung) an deren Funktionsadresse gesprungen. Der Speicher wird nun gelöscht, ohne dass versucht wird, in den Wrapper zurück zu springen.

Implementation in VB  

In der fraglichen Klasse, in der das Subclassing verwendet werden soll, wird die Funktion WindowProc(), wie weiter oben beschrieben, öffentlich und direkt auf den Deklarationsteil folgend definiert. Im Class_Initialize()-Event wird der Wrapper zuerst in einem Bytearray bearbeitet. Insbesondere werden hier, wie wie weiter oben beschrieben, die Zeiger der Variablen, der Zeiger auf den Wrappers selbst, die Adresse der WindowProc()-Funktion sowie der Instanzzeiger der Klasse an die entsprechende Stelle kopiert. Das Sprungziel für die jmp-Instruktion zur VirtualFree()-Funktion muss dabei noch extrahiert werden. Dazu wird zuerst ein Handle auf die kernel32.dll angefordert und sodann die Adresse ausgelesen.

pVirtualFree = GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualFree")

Listing 15

Der fertige Wrapper wird zuletzt noch in DEP-kompatiblen Speicher, der explizite Lese-, Schreib- und Ausführrechte besitzt und mit

pASMWrapper = VirtualAlloc(ByVal 0&, 112, MEM_COMMIT, PAGE_EXECUTE_READWRITE)

Listing 16

angefordert wird, verschoben. Im Class_Terminate()-Event wird in jedem Fall das Subclassing beendet und im gleichen Zuge geprüft, ob sich der Wrapper momentan in einer Rekursion befindet, indem der Zähler, der sich, wie auch das Signal zur Selbstlöschung, am Ende des Wrappers befindet, ausgelesen und auf Null getestet wird.

Call CopyMemory(Counter, ByVal (pASMWrapper + 104), 4)

Listing 17

Ist dies nicht der Fall, wird der Speicher freigegeben.

Call VirtualFree(ByVal pASMWrapper, 0, MEM_RELEASE)

Listing 18

Ist der Wrapper noch in Verwendung, wird das Signal zur Selbstlöschung gesetzt.

Flag = 1
Call CopyMemory(ByVal (pASMWrapper + 108), Flag, 4)

Listing 19

Der Code  

Die vollständige Implementation des Codes beinhaltet zudem noch Hilfsfunktionen zum Setzen und Zurücksetzen des Subclassings und befindet sich in der Klassenrubrik unter http://www.activevb.de/rubriken/klassen/windows/subclasser.html.

Anmerkungen  

  • Anmerkung 1: Der Wrapper liefert im Falle einer Selbstlöschung nicht den Rückgabewert, der in VB gesetzt wird, an den Aufrufer zurück. Dies liegt daran, dass die Abarbeitung des Programms derart modifiziert wird, als wenn der Aufrufer direkt VirtualFree() aufgerufen hätte. Entsprechend liegt auch dessen Rückgabewert im EAX-Register. Dieses Problem lässt sich mit der hier vorgestellten Technik nicht vermeiden. Ein möglicher Lösungsvorschlag wäre eine programmweit einzige Funktion zu definieren, die das Freigeben des von einer Instanz des Wrappers belegten Speichers derart übernimmt, dass der Rückgabewert der WindowProc() zwischengespeichert wird. Dabei muss natürlich sichergestellt werden, dass diese Funktion verfügbar sein wird, bis alle Instanzen des Wrappers entladen sind. Da Fensternachrichten aber zumeist keinen Rückgabewert auswerten, ist dieses Problem eher als unkritisch zu bewerten und kann vernachlässigt werden.
  • Anmerkung 2: Man hätte sich, um VirtualFree() aufzurufen, auch die ret-Instruktion zunutze machen können. In der Praxis wird die Verwendung der ret-Instruktion für diesen Zweck auf einigen Prozessorserien die Ausführungsgeschwindigkeit des Programms stark beeinträchtigen. Dies liegt daran, dass halbwegs moderne Prozessoren neben Sprungvorhersagen für effektives Pipelining auch Vorhersagen für Funktionsaufrufe anstellen. Dies geschieht in Form eines separaten, für den Programmierer versteckten, Stacks, der bei einer call-Instruktion, äquivalent zum Programmstack, um den Rücksprungzeiger ergänzt und bei einer ret- Instruktion wieder bereinigt wird. Werden nun besagte Instruktionen ohne das jeweilige Gegenstück eingesetzt, enthält der Stack Werte, die nicht mehr zum tatsächlichen Programmverlauf passen. Dadurch wird die Pipeline des Prozessors vorab mit falschen Befehlen gefüllt, die, sobald auf eine ret-Instruktion gestossen wird, zu einer kompletten Entleerung selbiger führt und dadurch die Programmabarbeitung ausbremst. Prozessoren, die über eine weiterentwickelte Technik verfügen, bemerken den Missstand und versuchen den Fehler automatisch zu korrigieren; auf allen anderen bleibt das Problem für den weiteren Programmverlauf bestehen.
  • Anmerkung 3: Die hier vorgestellte Technik kann mit leichten Anpassungen des Wrappers natürlich auch für Techniken verwendet werden, die eine Callback-Funktion mit anderer Signatur voraussetzen. Besonderes Augenmerk ist dabei auf den Puffer für den Rückgabewert zu legen, falls dieser nicht wie in obigem Fall 32 Bit breit ist.
  • Anmerkung 4: Die genaue Position der Adresse einer Callback-Funktionen in der vTable lässt sich nicht auf einfache Weise ermitteln. Mit einem Disassembler kann im Falle einer Klasse beispielsweise die Zeile .text:00401D32 call dword ptr [ecx+1Ch] gefunden werden. ECX enthält dabei die Adresse der vTable, während 1Ch in dezimaler Schreibweise der Zahl 28 entspricht. Insgesamt ergibt sich damit wie oben der Ausdruck vPtr + 7 * 4.

Ihre Meinung  

Falls Sie Fragen zu diesem Tutorial haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.