1

A bit of context... I need to write a fix for a bug in the VCL's TField.CopyData routine.

Not being a huge fan of recompiling the VCL, I usually opt for method hooks.

Usually, it's pretty straightforward, write the replacement method and hook it in place of the original one:

HookProc(@TMenu.ProcessMenuChar, @TPatchMenu.ProcessMenuChar, Backup);

But this time, the method is virtual and overloaded. I can't seem to find a reliable way to get the right method address.

If it wasn't overloaded, I could simply use @TField.CopyData and it would work, but that isn't guaranteed to give the right address for an overloaded function.

If it wasn't virtual, I could extend the method described here and do the following

type
  TCopyDataMethod = procedure(Source, Dest : TValueBuffer) of object;

procedure DoHook;
var vOldMethod : TCopyDataMethod;
begin
  vOldMethod := TField(nil).CopyData;
  HookProc(TMethod(vOldMethod).Code, @TSomeClass.NewMethod, Backup);
end;

But that gives an access violation (It is using the nil reference to lookup in the VMT for the right CopyData address).

I tried various syntax that all gave me "incompatible types" at compile time.

I did come up with a solution (posted as answer), but it is less than ideal.

Community
  • 1
  • 1
Ken Bourassa
  • 6,363
  • 1
  • 19
  • 28

3 Answers3

2

Inspired from David Heffernan's answer, here's the solution I was looking/hoping for.

type
  TCopyDataMethod = procedure(Source, Dest : TValueBuffer) of object;

procedure Proc;
var VMT : NativeInt;
    vMethod : TCopyDataMethod;
begin
  VMT := NativeInt(TField);
  vMethod := TField(@Vmt).CopyData;
  uOriginalAddress := @vMethod;
    [...]
end;

I believe this is as straightforward as it can get.

Ken Bourassa
  • 6,363
  • 1
  • 19
  • 28
0

The only solution I've found to this problem is to create an instance of the class

procedure Proc;
var vMethod : TCopyDataMethod;
    vFieldOrig : TField;
    [...]
begin
  vFieldNew := nil;
  vFieldOrig := TField.Create(nil);
  try

    vMethod := vFieldOrig.CopyData;
    uOriginalAddress := @vMethod;
    [...]
  finally
    vFieldOrig.Free;
  end;
end;

Hopefully, there is a more elegant solution.

Ken Bourassa
  • 6,363
  • 1
  • 19
  • 28
0

If you know the index in the VMT, you can get the address directly from the class. If you look at a call to CopyData under the debugger you see the following:

Project1.dpr.19: THackField(Field).CopyData(TValueBuffer(nil), TValueBuffer(nil));
0053886B 33C9             xor ecx,ecx
0053886D 33D2             xor edx,edx
0053886F A104345400       mov eax,[$00543404]
00538874 8B18             mov ebx,[eax]
00538876 FF93A4000000     call dword ptr [ebx+$000000a4]

Here we can see that in my XE7, the method point is at an offset of $000000a4 bytes from the VMT. And the class is a pointer to the VMT.

So, you can get your method address like this:

Pointer(NativeUInt(TField) + $00a4)

I expect that you could come up with a more elegant solution using enhanced RTTI. But you typically do this when looking to patch bugs in specific library versions. So you hard code values for specific versions, and throw a compiler error otherwise. At least, that's what I do!

Having said all of this, there's nothing to be ashamed of with instantiating an instance at startup as you demonstrate in your answer. So long as that has no unwanted side effects I can't see any real downside.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Not that I was ashamed or anything, I just felt silly of adding the overhead of creating an object for something that should (and is) readily available. Not that in this particular context the overhead is big. But it bugs me when I can't manage to do something in a way that I believe should be achievable. – Ken Bourassa Jan 05 '16 at 19:33
  • If you know assembler, that has a vmtoffset, but it may fail for overloaded virtual methods. – Rudy Velthuis Jan 10 '16 at 19:57
  • @Rudy No, the offset is fixed. It's different for each overload, but it's fixed all the same. – David Heffernan Jan 10 '16 at 20:00
  • I know the offset for a virtual method is fixed, but VMTOFFSET may not recognize the **overload** you may want to address and simply pick the first one, IOW the same as the original problem, i.e. addressing the right overload. There should be a mechanism for this.. – Rudy Velthuis Jan 11 '16 at 11:19