Ich habe dann mal Thunks implementiert.
Erst einmal den vollständigen, kompilierbaren Code, welcher bei der Fenstererstellung noch das Hello-World-Beispiel aus der MSDN benutzt, um das es hier aber ja schließlich nicht geht:
Code: Alles auswählen
// Suppress warnings from Windows and STL headers
#pragma warning(disable : 4668 4710 4711 4820 4986)
#include <Windows.h>
#include <cstdint>
#include <string>
#include <iostream>
#include <type_traits>
// Helpers
namespace memory
{
template <typename Type>
void * AllocateReadWriteVirtualMemory()
{
return ::VirtualAlloc(0, sizeof(Type), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
}
template <typename Type>
Type * MakeMemoryExecutable(
Type * thePointer
) {
unsigned long oldProtection;
VirtualProtect(thePointer, sizeof(Type), PAGE_EXECUTE, &oldProtection);
return thePointer;
}
void DeallocateVirtualMemory(
void * thePointer
) {
::VirtualFree(thePointer, 0, MEM_RELEASE);
}
}
namespace meta
{
namespace addressOf
{
template<class Type>
struct DisableConversionOperators
{
Type & myValue;
inline DisableConversionOperators(
Type & theValue
) : myValue(theValue)
{
return;
}
inline operator Type&() const
{
return myValue;
}
private:
DisableConversionOperators & operator=(
DisableConversionOperators const &
);
};
template<class Type>
struct ReturnFunctionAddress
{
static inline Type * AddressOf(
Type & theValue,
long
) {
return reinterpret_cast<Type*>(&const_cast<char &>(reinterpret_cast<const volatile char &>(theValue)));
}
static inline Type * AddressOf(
Type * theValue,
int
) {
return theValue;
}
};
template<class Type>
Type * AddressOf(
Type & theValue
) {
return ReturnFunctionAddress<Type>::AddressOf(DisableConversionOperators<Type>(theValue), 0);
}
}
using namespace addressOf;
namespace unionCast
{
template<typename DestinationType, typename SourceType>
inline DestinationType UnionCast(SourceType theSource)
{
static_assert(sizeof(DestinationType) == sizeof(SourceType), "UnionCast size mismatch.");
union
{
SourceType mySource;
DestinationType myDestination;
} unionForCast;
unionForCast.mySource = theSource;
return unionForCast.myDestination;
}
}
using namespace unionCast;
}
// Windows
namespace windows
{
typedef ::LRESULT (__stdcall * WindowProcedureType)(::HWND, unsigned int, ::WPARAM, ::LPARAM);
// Thunks
namespace thunks
{
namespace
{
template <typename SourceType>
void FillArray(
char * theArray,
size_t & theOffset,
SourceType theSource
) {
SourceType * destination = reinterpret_cast<SourceType *>(theArray + theOffset);
*destination = theSource;
theOffset += sizeof(SourceType);
}
}
#ifdef _M_IX86
template <typename WindowClassType>
class ThunkImplementation
{
public:
explicit ThunkImplementation(
WindowClassType & theWindowClass,
WindowProcedureType theWindowProcedure
) {
size_t offset = 0u;
// mov dword ptr [esp+4], <pointer to theWindowClass>
FillArray<uint32_t>(myCode, offset, 0x042444c7);
FillArray(myCode, offset, meta::AddressOf(theWindowClass));
// jmp <relative address to theWindowProcedure>
FillArray<uint8_t>(myCode, offset, 0xe9);
FillArray(myCode, offset, meta::UnionCast<char *>(theWindowProcedure) - reinterpret_cast<char *>(this) - sizeof(*this));
::FlushInstructionCache(GetCurrentProcess(), this, sizeof(*this));
}
operator WindowProcedureType() const
{
return meta::UnionCast<WindowProcedureType>(myCode);
}
void * operator new(
size_t
) {
return memory::AllocateReadWriteVirtualMemory<ThunkImplementation>();
}
void operator delete(
void * thePointer
) {
memory::DeallocateVirtualMemory(thePointer);
}
private:
typedef char ThunkCode[sizeof(uint32_t) + sizeof(WindowClassType *) + sizeof(uint8_t) + sizeof(ptrdiff_t)];
ThunkCode myCode;
};
#elif defined _M_AMD64
template <typename WindowClassType>
class ThunkImplementation
{
public:
explicit ThunkImplementation(
WindowClassType & theWindowClass,
WindowProcedureType theWindowProcedure
) {
size_t offset = 0u;
// mov rcx, <pointer to theWindowClass>
FillArray<uint16_t>(myCode, offset, 0xb948);
FillArray(myCode, offset, meta::AddressOf(theWindowClass));
// mov rax, <pointer to theWindowProcedure>
FillArray<uint16_t>(myCode, offset, 0xb848);
FillArray(myCode, offset, meta::UnionCast<char *>(theWindowProcedure));
// jmp rax
FillArray<uint16_t>(myCode, offset, 0xe0ff);
::FlushInstructionCache(GetCurrentProcess(), this, sizeof(*this));
}
operator WindowProcedureType() const
{
return meta::UnionCast<WindowProcedureType>(myCode);
}
void * operator new(
size_t
) {
return memory::AllocateReadWriteVirtualMemory<ThunkImplementation>();
}
void operator delete(
void * thePointer
) {
memory::DeallocateVirtualMemory(thePointer);
}
private:
typedef char ThunkCode[sizeof(uint16_t) + sizeof(WindowClassType *) + sizeof(uint16_t) + sizeof(ptrdiff_t) + sizeof(uint16_t)];
ThunkCode myCode;
};
#else
#error Unsupported architecture.
#endif
template <typename WindowClassType>
class Thunk
{
public:
Thunk(
WindowClassType & theWindowClass,
WindowProcedureType theWindowProcedure
) : myThunk(memory::MakeMemoryExecutable(new ThunkImplementation<WindowClassType>(theWindowClass, theWindowProcedure)))
{
return;
}
~Thunk()
{
delete myThunk;
}
operator WindowProcedureType() const
{
return *myThunk;
}
private:
thunks::ThunkImplementation<WindowClassType> * myThunk;
};
}
namespace
{
void SetWindowProcedure(
::HWND theWindowhandle,
WindowProcedureType theWindowProcedure
) {
::SetWindowLongPtrW(theWindowhandle, GWLP_WNDPROC, reinterpret_cast<std::make_signed<size_t>::type>(theWindowProcedure));
}
}
template <typename DerivedWindowProcedure>
class ThunkedWindowProcedure
{
public:
explicit ThunkedWindowProcedure(
::HWND theWindowHandle,
DerivedWindowProcedure * theThisPointer
) : myThunk(*theThisPointer, DefaultWindowProcedure),
myWindowHandle(theWindowHandle)
{
SetWindowProcedure(theWindowHandle, static_cast<WindowProcedureType>(myThunk));
}
void SetWindowHandle(
::HWND theWindowHandle
) {
myWindowHandle = theWindowHandle;
}
void GetWindowHandle()
{
return myWindowHandle;
}
protected:
::HWND myWindowHandle;
thunks::Thunk<DerivedWindowProcedure> myThunk;
private:
static ::LRESULT __stdcall DefaultWindowProcedure(
::HWND theWindowHandle,
unsigned int theMessage,
::WPARAM theAdditionalMessageInformation1,
::LPARAM theAdditionalMessageInformation2
) {
DerivedWindowProcedure * thisPointer = reinterpret_cast<DerivedWindowProcedure * >(theWindowHandle);
return thisPointer->WindowProcedure
(
theMessage,
theAdditionalMessageInformation1,
theAdditionalMessageInformation2
);
}
};
class ExampleWindowProcedure : public ThunkedWindowProcedure<ExampleWindowProcedure>
{
public:
typedef std::basic_string<wchar_t> WideString;
// This is legal code, see N3337 12.6.2 Clause 12
#pragma warning(push)
#pragma warning(disable : 4355)
explicit ExampleWindowProcedure(
::HWND theWindowHandle,
WideString theString
) : ThunkedWindowProcedure(theWindowHandle, this),
myString(theString)
{
return;
}
#pragma warning(pop)
public:
::LRESULT WindowProcedure(
unsigned int theMessage,
::WPARAM theAdditionalMessageInformation1,
::LPARAM theAdditionalMessageInformation2
) {
if(theMessage == WM_DESTROY)
{
::PostQuitMessage(0);
}
else if(theMessage == WM_PAINT)
{
PAINTSTRUCT paintInformation;
auto displayDeviceContext = ::BeginPaint(myWindowHandle, &paintInformation);
TextOut(displayDeviceContext, 5, 5, myString.c_str(), static_cast<int>(myString.length()));
::EndPaint(myWindowHandle, &paintInformation);
}
else
{
return DefWindowProc
(
myWindowHandle,
theMessage,
theAdditionalMessageInformation1,
theAdditionalMessageInformation2
);
}
return 0;
}
private:
WideString myString;
};
}
// The following code is taken from the "Hello, World!" sample
// http://msdn.microsoft.com/en-us/library/vstudio/bb384843
#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <tchar.h>
// Global variables
// The main window class name.
static TCHAR szWindowClass[] = _T("win32app");
// The string that appears in the application's title bar.
static TCHAR szTitle[] = _T("Win32 Guided Tour Application");
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = ::DefWindowProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
if (!RegisterClassEx(&wcex))
{
MessageBox(NULL,
_T("Call to RegisterClassEx failed!"),
_T("Win32 Guided Tour"),
NULL);
return 1;
}
// The parameters to CreateWindow explained:
// szWindowClass: the name of the application
// szTitle: the text that appears in the title bar
// WS_OVERLAPPEDWINDOW: the type of window to create
// CW_USEDEFAULT, CW_USEDEFAULT: initial position (x, y)
// 500, 100: initial size (width, length)
// NULL: the parent of this window
// NULL: this application does not have a menu bar
// hInstance: the first parameter from WinMain
// NULL: not used in this application
HWND hWnd = CreateWindow(
szWindowClass,
szTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
500, 100,
NULL,
NULL,
hInstance,
NULL
);
if (!hWnd)
{
MessageBox(NULL,
_T("Call to CreateWindow failed!"),
_T("Win32 Guided Tour"),
NULL);
return 1;
}
windows::ExampleWindowProcedure window(hWnd, L"Hello, World!");
// The parameters to ShowWindow explained:
// hWnd: the value returned from CreateWindow
// nCmdShow: the fourth parameter from WinMain
ShowWindow(hWnd,
nCmdShow);
UpdateWindow(hWnd);
// Main message loop:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int) msg.wParam;
}
Funktionsweise:
- Die Fensterklasse wcex kriegt zunächst einmal die ::DefWindowProc als Fensterprozedur:
- Wir werden die später umsetzen
- Das bedeutet aber auch, dass wir hier ein paar Nachrichten, wie etwa WM_NCCREATE, gar nicht mitkriegen, weil die an ::DefWindowProc gehen
- In der jetztigen Implementierung gibt es nichts, was diese Nachrichten bräuchte; wenn ihr die jedoch unbedingt selbst behandeln müsst, gibt es keinen Grund, bereits bei Erstellung von wcex schon eine eigene Fensterprozedur anzugeben
- Nach einem gewöhnlichen Aufruf an ::CreateWindow erstellen wir eine Instanz von Typ ExampleWindowProcedure, welche unsere Beispiels-Fensterprozedur halten wird:
- Die Klasse kriegt im Konstruktor das Fenster-Handle vom zuvor erstellten Fenster (HWND) und einen String, hier „Hello, World!“, übergeben
- Diese Fensterprozedur macht jetzt erstmal nichts anderes, als diesen Member-String ins Fenster zu malen – zumindest ein kleines Beispiel für die Verwendung den this-Zeigers
- Unsere ExampleWindowProcedure ist von ThunkedWindowProcedure<ExampleWindowProcedure> abgeleitet, einer generischen Basisklasse für gethunkte Fensterprozeduren (hier wird ein CRTP benutzt)
- ThunkedWindowProcedure<DerivedWindowProcedure> enthält ein Thunk-Objekt vom Typ thunks::Thunk<DerivedWindowProcedure> (Template-Parameter hat den Typ der abgeleiteten Klasse) namens myThunk
- Dieses Thunk-Objekt wird eine Thunk-Implementierung im Speicher anlegen
- Dieses Thunk-Objekt myThunk wird erstellt und nimmt im Konstruktor zwei Argumente entgegen:
- Einen this-Pointer auf die abgeleitete Klasse
- Einen Pointer auf die Fensterprozedur als Member-Funktion der abgeleiteten Klasse
- Der Thunk-Konstruktor für den Typ Thunk erstellt mit diesen zwei Parametern eine Thunk-Implementierung, welche abhängig von der Architektur ist und die ganze Low-Level-Arbeit macht:
- Die Thunk-Implementierung myThunk (ja, ich weiß, gleicher Name wie oben, das ist jetzt doof) ist ein Pointer vom Typ ThunkImplementation<WindowClassType>, welcher dynamisch allokiert und freigegeben wird
- Bei dieser Allokation und Freigabe werden operator new/operator delete der ThunkImplementation aufgerufen
- Diese setzen mittels AllocateReadWriteVirtualMemory die Thunk-Implementierung an den Anfang einer Page, welche lesbar und beschreibbar, aber nicht ausführbar ist.
- Die Thunk-Implementierung hat ein char-Array, und dieses wird mit ein wenig Daten als Code gefüllt:
- Für x86: sizeof(uint32_t) + sizeof(WindowClassType *) + sizeof(uint8_t) + sizeof(ptrdiff_t)
mov dword ptr [esp+4], <pointer to theWindowClass>
jmp <relative address to theWindowProcedure>
- Für x64: sizeof(uint16_t) + sizeof(WindowClassType *) + sizeof(uint16_t) + sizeof(ptrdiff_t) + sizeof(uint16_t)
mov rcx, <pointer to theWindowClass>
mov rax, <pointer to theWindowProcedure>
jmp rax
- Was der Code genau macht, dazu komme ich später
- Anschließend wird die Page auf nicht mehr lesbar oder schreibbar gesetzt, sondern nur noch auf ausführbar (MakeMemoryExecutable)
- Nun setzen wir endlich die Fensterprozedur unseres Fensters mittels ::SetWindowLongPtrW mit Parameter GWLP_WNDPROC um, und zwar auf die Speicheradresse, an der unser char-Array der ThunkImplementation-Instanz liegt
- Damit wird der obige Code, den wir als Daten in unser char-Array gesteckt haben, ausgeführt
- Das einzige, was noch fehlt, ist zu erklären, was der Code nun macht:
- Für x86:
- Metavariablen:
- <pointer to theWindowClass> ist ein this-Pointer auf unsere ExampleWindowProcedure-Instanz
- <relative address to theWindowProcedure> ist eine relative Adresse auf eine statische Methode mit __stdcall-Konvention namens DefaultWindowProcedure in der Elternklasse von ExampleWindowProcedure
- Da wir __stdcall-Konvention haben, und da alles über den Stack läuft, überschreiben wir den ersten Parameter an DefaultWindowProcedure (das ist ::HWND theWindowHandle) mit dem this-Pointer
- Dann machen wir einen jmp auf DefaultWindowProcedure
- Dort frickeln wir uns mittels DerivedWindowProcedure * thisPointer = reinterpret_cast<DerivedWindowProcedure * >(theWindowHandle); wieder unseren this-Pointer zurück
- Und rufen dann mittels thisPointer->WindowProcedure(…) die Fensterprozedur endlich auf
- Für x64:
- Eigentlich so wie x86, nur dass __stdcall vom Kompiler ignoriert wird, und x64-Konvention benutzt wird.
- Dadurch geht erstmal nichts über den Stack, sondern über die Register; hierbei steckt in rcx der erste Parameter an DefaultWindowProcedure
- Wir überschreiben dieses Register mit dem this-Pointer, und der Rest funktioniert wie unter x86
Das ganze ist unter x86 und x64 unter Windows 8 getestet und funktioniert.
Abschließend noch zur Performance unter x64 im Release-Modus:
- Windows ruft unsere benutzerdefiniert gesetzte Fensterprozedur in user32.dll!UserCallWinProcCheckWow auf:
- In unserem Beispiel hat die CPU 00000000024A0000 in r14 geladen
- user32.dll!UserCallWinProcCheckWow() + 0x13b bytes:
000007FEC980171B 41 FF D6 call r14
- Dies ruft den Code in unserer Thunk-Implementierung auf:
00000000024A0000 48 B9 68 F9 A7 00 00 00 00 00 mov rcx,0A7F968h
00000000024A000A 48 B8 20 15 DC 13 F6 07 00 00 mov rax,7F613DC1520h
00000000024A0014 FF E0 jmp rax
- An der in rax geladenen Speicheradresse 000007F613DC1520 steht unsere DefaultWindowProcedure-Funktion, welche der Optimizer auf einen einzigen unkonditionalen jmp optimiert hat:
static ::LRESULT __stdcall DefaultWindowProcedure(
::HWND theWindowHandle,
unsigned int theMessage,
::WPARAM theAdditionalMessageInformation1,
::LPARAM theAdditionalMessageInformation2
) {
DerivedWindowProcedure * thisPointer = reinterpret_cast<DerivedWindowProcedure * >(theWindowHandle);
return thisPointer->WindowProcedure
(
theMessage,
theAdditionalMessageInformation1,
theAdditionalMessageInformation2
);
}
000007F613DC1520 E9 FB FB FF FF jmp ExampleWindowProcedure::WindowProcedure (7F613DC1120h)
- Und der jmp geht dann an unsere Fensterprozedur in unserer Instanz:
::LRESULT WindowProcedure(
unsigned int theMessage,
::WPARAM theAdditionalMessageInformation1,
::LPARAM theAdditionalMessageInformation2
) {
000007F613DC1120 40 53 push rbx
000007F613DC1122 48 81 EC 90 00 00 00 sub rsp,90h
000007F613DC1129 48 8B 05 F0 3E 00 00 mov rax,qword ptr [__security_cookie (7F613DC5020h)]
000007F613DC1130 48 33 C4 xor rax,rsp
000007F613DC1133 48 89 84 24 80 00 00 00 mov qword ptr [rsp+80h],rax
000007F613DC113B 48 8B D9 mov rbx,rcx
if(theMessage == WM_DESTROY)
000007F613DC113E 83 FA 02 cmp edx,2
000007F613DC1141 75 0C jne ExampleWindowProcedure::WindowProcedure+2Fh (7F613DC114Fh)
{
…
Mein Code ist stark durch die Links aus meinem vorherigen Beitrag inspiriert, insbesondere dem
hier; jedoch kann man die dortigen Methoden nicht unter x64 direkt so anwenden. Die Microsoft ATL implementiert es genau so wie ich es nun getan habe. Damit dürfte man diese Methode als mit Microsofts Segen behaftet ansehen.
Im Übrigen funktioniert sämtlicher sonstiger Thunking-Code aus dem Internet wegen DEP nicht mehr. Dieser Post ist das einzige unter x64 funktionierende Thunking-Beispiel, welches mir bekannt ist.
Ich habe mal getestet, was passiert, wenn man direkt die Instanzmethode anspringen würde (was nur unter x64 aufgrund der Aufrufkonvention geht): Der Compiler erstellt korrekterweise einen Eintrag in die Relocation-Table, und ist damit genauso effizient wie unsere obige Implementierung; nur dass die obige auch unter x86 funktioniert, da der Compiler dort mehr Zwischencode als Kleber zwischen den Aufrufkonventionen generiert.
Der einzige Overhead in obiger Fassung ist ein einziger
jmp, wie bei einer Relokationstabelle. Womit der Code wohl ziemlich optimal ist.