Jeder, der schon einmal ein Fenster mit dem Win32-API erstellt hat wird das Problem kennen, dass das API in C geschrieben ist und sich in vielen Fällen nicht wirklich kooperativ zum OOP-Ansatz von C++ verhält. Ein naheliegendes Beispiel ist das WNDPROC-Callback das man mit der Windowklasse an das Betriebssystem übergeben muss, welches dann bei einkommenden Nachrichten aufgerufen wird. Auf Grund der Kompatibilität zu C kann da nicht eine gebundene Funktion einer Klasse verwendet werden, da in C die Klassen grundsätzlich so nicht vorhanden sind. Daher wird eine globale oder statische (die im Endeffekt wie eine globale zu betrachten ist) Funktion einer Klasse erwartet. Dies macht das Synchronisieren von Fenster zu einer heiklen Gratwanderung.
Die Methoden, die ich früher verwendet habe waren die weit herum bekannten Varianten mit einem assoziativen Container und einer Mutex (welche performancetechnisch suboptimal ist) oder über SetWindowLogPtr und GWL_USERDATA. Erstere ist wie angedeutet für die Performance nicht wirklich förderlich und ohne die Mutex überhaupt nicht threadsicher. Letztere ist instabil, da es grundsätzlich jeder anderen Funktion erlaubt ist selbst was über SetWindowLogPtr bei GWL_USERDATA reinzuschreiben, was dann zu einem Absturz führt.
Die in meinen Augen optimale Lösung verwendet die Eigenschaften (vor allem Ähnlichkeiten) der beiden Aufrufkonventionen __stdcall und __thiscall. Denn der einzige Unterschied - welcher aber entscheidend ist, dass es nicht einfach so ohne weiteres klappt - ist die Tatsache, dass bei __thiscall die Adresse des this-Zeigers im ECX-Register übergeben wird. Die Verantwortung für das Aufräumen des Stacks, die Reihenfolge der Argumente ist identisch. Wir können dies ausnutzen und folgende Schlüsse ziehen:
So lange wir nichts am Stack ändern können wir vor dem Aufruf eigentlich fast unbeschränkt aktiv sein. Da das erwartete Callback nicht __fastcall ist, sondern __stdcall können wir die Register unbeschränkt benutzen (bei __fastcall bestünde die Möglichkeit, dass in einem oder zwei Register Funktionsargumente stünden).
Um an unser Ziel zu gelangen nutzen wir eine der Kerneigenschaften der Von-Neumann-Architektur: Sowohl das Programm als auch sein Status stehen im gleichen Speicher. Wir generieren zu Laufzeit Maschinencode, welcher alle nötigen Vorbereitungen trifft damit unsere Memberfunktion aufgerufen werden kann. In Assembler gesprochen muss der Code folgendes tun:
Code asm:
1 2 3 | lea ecx, this
mov eax, addresseDerMemberFunktion
jmp eax |
In einem ersten Schritt erstellen wir die struct, welche den Maschinencode fassen wird. Ich habe die Member extra nach ihrer Funktion benannt, damit es klar wird, wofür sie sind:
Code cpp:
1 2 3 4 5 6 7 8 9 10 | #pragma pack(push, 1)
struct FuncStub
{
unsigned short leaEcx;
unsigned long thisPtr;
unsigned char movEax;
unsigned long funcAddr;
unsigned short jmpEax;
};
#pragma pack(pop) |
#pragma pack(push, 1) ist hier sehr wichtig. Die Grösse unserer struct ist 13 Byte. Jeder halbwegs vernünftige Compiler würde hier entweder auf 14 oder 16 Byte erweitern, damit die struct besser und schöner in den Speicher passt. Wo die zusätzlichen Bytes eingefügt würden können wir nicht beeinflussen, was mitunter verheerend wäre, würden diese Bytes irgendwo zwischen unsere Member geschoben. Mit der erwähnten Anweisung sagen wir dem Compiler, dass er die Grösse der struct an ein Vielfaches von 1 ausrichten soll. Da 13 ein Vielfaches von 1 ist wird sich der Compiler hüten an unserer struct etwas zu hüten. Aus Performancegründen und weil das Alignement ja eigentlich eine gute Sache ist wird diese Ausrichtung am Ende der struct wieder rückgängig gemacht.
In einem zweiten Schritt wird ein abstrakter Entwurf unserer Window-Klasse erstellt:
Code cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class Window
{
FuncStub* m_trampoline;
LRESULT WndProc(HWND w, UINT m, WPARAM wp, LPARAM lp)
{
switch(m)
{
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(w, m, wp, lp);
}
public:
Window()
{
}
~Window()
{
}
WNDPROC GetProc()
{
return reinterpret_cast<WNDPROC>(m_trampoline);
}
}; |
Wichtig ist hier anzumerken, dass WndProc nicht __stdcall sein darf. Bei __stdcall wird bei einer Memberfunktion der this-Zeiger als Funktionsargument übergeben, bei __thiscall im ECX-Register!
Der nächste Schritt scheint auf den ersten Blick klar zu sein: Speicher für unseren Maschinencode in m_trampoline allozieren, also m_trampoline = new FuncStub();
Falsch gedacht! Windows (und auch andere Betriebssysteme) trennen aus Sicherheitsgründen normalen Speicher und Speicher, der als Code ausgeführt werden kann klar voneinander. Dies soll verhindern, dass bösartige Software den Speicher von gutartiger Software so ändert und durch Überläufe u.Ä. dann auch zum Auführen zwingt, dass die bösartige Software als Teil der gutartigen ausgeführt wird und dadurch vom Virenscanner mehr Vertrauen bekommt, da man möglicherweise bereits früher einmal gesagt hat, dass bei genannter gutartiger Software keine Warnungen mehr angezeigt werden sollen. Wer Speicher allozieren will, welcher explizit zum Auführen gedacht ist, so gibt es dafür zwei Varianten:
1. VirtualAlloc/VirtualFree
2. HeapCreate/HeapAlloc/HeapFree/HeapDestroy
Ich werde mich für die zweite Möglichkeit entscheiden aus einem ganz pragmatischen Grund: VirtualAlloc erstellt eine neue Speicherpage. Diese ist mindestens 4kb gross. Für 13 Bytes Maschinencode würden also mindestens 4kb an Speicher alloziert. Das ist schlichtweg eine Verschwendung. Bei einem neuen Heap kann man die Grösse aufs Byte genau angeben, die man möchte (hat man bei VirtualAlloc auch das Gefühl, ist aber nicht so!).
Wir erweitern daher unsere Klasse folgendermassen:
Code cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | class Window
{
FuncStub* m_trampoline;
HANDLE m_hHeap;
LRESULT WndProc(HWND w, UINT m, WPARAM wp, LPARAM lp)
{
switch(m)
{
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(w, m, wp, lp);
}
public:
Window()
{
m_hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
m_trampoline = (FuncStub*)HeapAlloc(m_hHeap, 0, sizeof(FuncStub));
}
~Window()
{
HeapFree(m_hHeap, 0, m_trampoline);
HeapDestroy(m_hHeap);
}
WNDPROC GetProc()
{
return reinterpret_cast<WNDPROC>(m_trampoline);
}
}; |
Ich habe hier bewusst auf eine Prüfung der Rückgabewerte verzichtet, da ich das als Standardwissen erachte, welches hier nur vom eigentlich relevanten Punkt ablenkt. Wichtig hingegen ist, dass wir bei HeapCreate angeben, dass der Speicher immer auch als ausführbar alloziert werden soll.
Was jetzt als nächstes kommt ist eigentlich recht simpel: Es muss die Repräsentation dessen, was oben in Assembler geschrieben ist in Maschinensprache umgesetzt werden. Dies ist natürlich ziemlich plattformabhängig und muss auf jeden Fall auch in die Planung mit einbezogen werden! In meinem Fall (x86 Intel Prozessor) sind die Instruktionen folgendermassen:
Code :
1 2 3 | lea ecx => 0x0D8D
mov eax => 0xB8
jmp eax => 0xE0FF |
Bevor man jetzt aber gleich m_trampoline die entsprechenden Werte zuweist muss man sich noch etwas bewusst sein:
C++ verbietet grundsätzlich das direkte Abfragen von Adressen von gebundenen Funktionen. Der Grund dafür ist recht simpel: Man schiesst sich mit solchen Adressen sehr schnell und sehr unschön ins Knie. Aus diesem Grund sind auch Adressen von gebundenen Funktion der einzige Typ, welcher nicht nach void* konvertiert werden kann über reinterpret_cast. Es muss daher eine etwas unschöne, aber funktionierende Umwandlungsvariante hin (die man bitte sonst nicht verwenden soll, nur um Compilerfehler wegzumachen!!):
Code cpp:
1 2 3 4 5 6 7 | template<class Src, class Dst>
Dst Cast(Src s)
{
union { Src sr; Dst ds; } u;
u.sr = s;
return u.ds;
} |
Diese Funktion ermöglicht es dem Anwender wirklich alles in alles umzuwandeln, unabhängig von Typ, Grösse und Sinn. Mit dieser Funktion im Gepäck kann man jetzt den Konstruktor mit der Generierung des Maschinencodes erweitern. Am Schluss sieht die Klasse und die struct folgendermassen aus:
Code cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | #pragma pack(push, 1)
struct FuncStub
{
unsigned short leaEcx;
unsigned long thisPtr;
unsigned char movEax;
unsigned long funcAddr;
unsigned short jmpEax;
};
#pragma pack(pop)
class Window
{
FuncStub* m_trampoline;
HANDLE m_hHeap;
typedef LRESULT (Window::*tWndClbck)(HWND, UINT, WPARAM, LPARAM);
template<class Src, class Dst>
Dst Cast(Src s)
{
union { Src sr; Dst ds; } u;
u.sr = s;
return u.ds;
}
LRESULT WndProc(HWND w, UINT m, WPARAM wp, LPARAM lp)
{
switch(m)
{
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(w, m, wp, lp);
}
public:
Window()
{
m_hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
m_trampoline = (FuncStub*)HeapAlloc(m_hHeap, 0, sizeof(FuncStub));
m_trampoline->leaEcx = 0x0D8D;
m_trampoline->thisPtr = (unsigned long)this;
m_trampoline->movEax = 0xB8;
m_trampoline->funcAddr = Cast<tWndClbck, unsigned long>(&Window::WndProc);
m_trampoline->jmpEax = 0xE0FF;
}
~Window()
{
HeapFree(m_hHeap, 0, m_trampoline);
HeapDestroy(m_hHeap);
}
WNDPROC GetProc()
{
return reinterpret_cast<WNDPROC>(m_trampoline);
}
}; |
Damit ist jetzt das Codestück fertig, welches den Aufruf an WNDPROC des Betriebssystems mit der Instanz der Klasse erweitert und dann an die definierte Memberfunktion weiterleitet.
Da zu jeder Erklärung auch ein Beispiel gehört werde ich das auch nicht vorenthalten:
Code cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | #include <Windows.h>
#pragma pack(push, 1)
struct FuncStub
{
unsigned short leaEcx;
unsigned long thisPtr;
unsigned char movEax;
unsigned long funcAddr;
unsigned short jmpEax;
};
#pragma pack(pop)
class Window
{
FuncStub* m_trampoline;
HANDLE m_hHeap;
typedef LRESULT (Window::*tWndClbck)(HWND, UINT, WPARAM, LPARAM);
template<class Src, class Dst>
Dst Cast(Src s)
{
union { Src sr; Dst ds; } u;
u.sr = s;
return u.ds;
}
LRESULT WndProc(HWND w, UINT m, WPARAM wp, LPARAM lp)
{
switch(m)
{
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(w, m, wp, lp);
}
public:
Window()
{
m_hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
m_trampoline = (FuncStub*)HeapAlloc(m_hHeap, 0, sizeof(FuncStub));
m_trampoline->leaEcx = 0x0D8D;
m_trampoline->thisPtr = (unsigned long)this;
m_trampoline->movEax = 0xB8;
m_trampoline->funcAddr = Cast<tWndClbck, unsigned long>(&Window::WndProc);
m_trampoline->jmpEax = 0xE0FF;
}
~Window()
{
HeapFree(m_hHeap, 0, m_trampoline);
HeapDestroy(m_hHeap);
}
WNDPROC GetProc()
{
return reinterpret_cast<WNDPROC>(m_trampoline);
}
};
BOOL WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, INT)
{
Window wnd;
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.hCursor = LoadCursor(0, IDC_ARROW);
wc.hIcon = LoadIcon(0, IDI_APPLICATION);
wc.hInstance = GetModuleHandle(0);
wc.lpfnWndProc = wnd.GetProc(); // Das ist der entscheidende Punkt!
wc.lpszClassName = TEXT("_Trampoline");
wc.lpszMenuName = NULL;
wc.style = CS_VREDRAW | CS_HREDRAW;
if(RegisterClass(&wc) == 0)
{
OutputDebugString(TEXT("RegisterClass failed! See fs:[0x34] for last error!"));
return 0;
}
HWND hWindow = CreateWindow(TEXT("_Trampoline"), TEXT("Titel"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, GetModuleHandle(0), NULL);
if(hWindow == NULL)
{
OutputDebugString(TEXT("CreateWindow failed! See fs:[0x34] for last error!"));
return 0;
}
ShowWindow(hWindow, SW_SHOW);
MSG msg;
while(GetMessage(&msg, 0, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
UnregisterClass(TEXT("_Trampoline"), GetModuleHandle(0));
} |
Fragen und Anregungen sind natürlich immer erwünscht! Wie immer habe ich das jetzt ohne direkte Vorbereitung aus dem Kopf direkt hier aufs "Papier" gebracht. Es ist gut möglich, dass vielleicht an einer Stelle ein Fehler vorhanden ist. Falls dem so ist freue ich mich, wenn man mich darauf hinweist!
Mit freundlichen Grüssen
Cromon




Bereiche
Kategorien
Forum - Programming





tutorials.de-Systemmitteilung