Last time we looked at a way of completely changing the class of a running object instance. As we discussed there, that hacking technique had a number of problems. But there are many ways to skin a cat (sorry, cat lovers!), and in the case of trying to fix the TProgressBar flickering issue on Windows Vista without changing the interface declaration of TProgressBar there are at least three other possible solutions.
One of these solution is to patch the existing TProgressBar VMT at runtime. We know from earlier articles that the VMT contains a load of information about a class - including an array of all virtual method implementation (the actual VMT - virtual method table) and a sparse array of all overridden and introduced dynamic and message methods (known as the DMT or dynamic method table). In this article we will look at how we can patch an existing class VMT and replace the DMT with one that fits our need.
Note that the compiler stores the class VMT tables in write-protected code pages (even though they do not strictly contain executable code). This means that if we naively try to overwrite parts of the VMT we will get access violations triggered by the hardware memory protection system. The clean way of writing self-modifying code is to use the VirtualProtect API to change the protection of the memory pages to allow writing before performing the patch, then restoring the original protection when we're done. Here is a routine that will safely overwrite a 4-byte DWORD in the code segment:
procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD);
// Self-modifying code - change one DWORD in the code segment
var
RestoreProtection, Ignore: DWORD;
begin
if VirtualProtect(Code, SizeOf(Code^), PAGE_EXECUTE_READWRITE,
RestoreProtection) then
begin
Code^ := Value;
VirtualProtect(Code, SizeOf(Code^), RestoreProtection, Ignore);
FlushInstructionCache(GetCurrentProcess, Code, SizeOf(Code^));
end;
end;
To be on the safe side we follow Microsoft's recommended practice of flushing the instruction cache after performing the patch by calling FlushInstructionCache. This is to avoid nasty things from happening if the patched code has already been pre-fetched by (one of) the CPU.
Now we just have to figure out where to patch and what to patch it with. We have covered the details of how dynamic and message methods work in earlier posts. Looking at the source of TProgressBar and confirmed by peeking with the debugger, there are no dynamic or message method overrides at all in this class. This is good news because it means that the DynamicTable pointer in the VMT is nil and this makes it much easier to perform the patch. So now we know where to patch - the DMT slot in the VMT of TProgressBar. To keep things simple in the initial version we use the vmtDynamicTable constant exported by the System unit.
{ Virtual method table entries }
const
// ...
vmtDynamicTable = -48;
Given a TProgressBar class reference we can calculate the address of the DMT slot in the VMT by using this code:
var
OriginalDmt: PDWORD;
begin
OriginalDmt := PDWORD(TProgressBar);
Inc(OriginalDmt, vmtDynamicTable div SizeOf(DWORD));
if OriginalDmt^ = 0 then // Assert that there is no existing Dmt
Now we have to find what to replace the nil DMT pointer with. We could try to build a DMT table by hand by using the tables we documented here, but it would be wasted effort. The compiler is much better than us building VMTs and DMTs, so we can just use the TProgressBarVistaFix from the previous post.
type
TProgressBarVistaFix = class(TProgressBar)
private
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message
WM_ERASEBKGND;
end;
procedure TProgressBarVistaFix.WMEraseBkgnd(var Message: TWmEraseBkgnd);
begin
DefaultHandler(Message);
end;
We can get to the DMT pointer for this new class in the same way and use it to overwrite the (previously nil) DMT pointer in the TProgressBar class. This is my initial patching routine:
procedure PatchTProgressBarDMT;
var
OriginalDmt, NewDmt: PDWORD;
begin
OriginalDmt := PDWORD(TProgressBar);
Inc(OriginalDmt, vmtDynamicTable div SizeOf(DWORD));
if OriginalDmt^ = 0 then // Assert that there is no existing Dmt
begin
NewDmt := PDWORD(TProgressBarVistaFix);
Inc(NewDmt, vmtDynamicTable div SizeOf(DWORD));
PatchCodeDWORD(OriginalDmt, NewDmt^);
end;
end;
Testing this code shows that it works and that the WM_ERASEBKFND handler is called and the flicker is gone on Vista. However, I'm not too satisfied with the code, so I refactor it into a separate general unit - this time using the HVVMT unit and the PVmt record pointer.
unit HVPatching;
interface
uses
Windows;
procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD);
procedure ReplaceClassDmt(TargetClass, SourceClass: TClass);
implementation
uses
HVVMT;
procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD);
// Self-modifying code - change one DWORD in the code segment
var
RestoreProtection, Ignore: DWORD;
begin
if VirtualProtect(Code, SizeOf(Code^), PAGE_EXECUTE_READWRITE,
RestoreProtection) then
begin
Code^ := Value;
VirtualProtect(Code, SizeOf(Code^), RestoreProtection, Ignore);
FlushInstructionCache(GetCurrentProcess, Code, SizeOf(Code^));
end;
end;
procedure ReplaceClassDmt(TargetClass, SourceClass: TClass);
begin
Assert(Assigned(TargetClass) and Assigned(SourceClass));
PatchCodeDWORD(@GetVmt(TargetClass).DynamicTable,
DWORD(GetVmt(SourceClass).DynamicTable));
end;
end.
Then we can use it like this:
initialization
ReplaceClassDmt(TProgressBar, TProgressBarVistaFix);
end.
In this case we were lucky. In other cases the patched class may already have one or more dynamic or message methods - so that there will already be a DMT pointer in the VMT. In this case we must play the compiler and manually allocate a new DMT with one extra entry, copy the existing DMT indicies and method code pointers and finally patch in the new dynamic method index or message-id and the address of the hook routine. This is quite a bit more complex, but if there is interest in digging further down we might revisit this later.
I think the PatchCodeDWord works for 32 bits windows. As Delphi XE2 supports 64 bits now, how to write the PatchCode that works for 64 bits runtime?
ReplyDeleteThat is left as an excersise for the reader... :)
ReplyDeletex64 apps has QWord sized pointers. Can't see anything else that can go wrong :D
ReplyDelete