开发者

Delphi: any way to link to a variable in a BPL that has not been PACKAGEd?

I'm working on a project in RAD Studio 2007, using VCL classes in c++.

TDBLookupControl is part of VCL & has some undesirable behaviour, which is caused by use of an internal variable SearchTickCount

var
   SearchTickCount: Integer = 0; //file scope in DBCtrls.pas

procedure TDBLookupControl.ProcessSearchKey(Key: Char);
var
  TickCount: Integer;
  S: string;
begin
//some code remo开发者_运维技巧ved for brevity
      TickCount := GetTickCount;
      if TickCount - SearchTickCount > 2000 then SearchText := '';
      SearchTickCount := TickCount;
//some code removed for brevity
end;

However, SearchTickCount has never been PACKAGEd inside the VCL, as in example below.

extern PACKAGE int SearchTickCount;

I'd like to set SearchTickCount to zero (on demand) in my c++ code. Externing it in my code makes the c++ compile. However, the linker (obviously) cannot find the variable.

namespace Dbctrls
{
  extern int SearchTickCount;
}
// later on, inside a function
Dbctrls::SearchTickCount = 0;

Is there any way/workaround to link to this variable?

EDIT: Unfortunately, we're also using some custom controls that derive from TDBLookupControl so I was tring to avoid creating more custom controls.


The problem

SearchTickCount is a global (unit level) variable declared in the implementation section of the unit, it's not supposed to be accessed outside that unit. You'd have the same problem if you were working with Delphi, not C++ Builder.

The sane solutions

  • Subclass the TDBLookupControl, override ProcessSearchKey() and make sure it uses your own SearchTickCount, one that's easily accessible. Happily ProcessSearchKey() is virtual, in theory this should work, but in practice the code depends on FListField, that's a private field so we'll be back to square 1.
  • Copy the whole TDBLookupControl to your own TMyDBLookupControl and make sure you can access the SearchTickCount. This would definitively work.

The HACKY solution

Ofcourse, hacks are a lot more fun. The CPU has no problem finding the SearchTickCount because the address is coded into the ASM instructions that make up ProcessSearchKey's code. What the CPU can read, we can read.

Evaluating the code for the ProcessSearchKey method, it only uses one global variable (SearchTickCount) and it uses it in two places. First in this test:

if TickCount - SearchTickCount > 2000 then

then in this instruction:

SearchTickCount := TickCount;

If you look into the disassembly listing of that routine, global variable access is easily spotted, because it gives the address of the variable in square brackets, with no other qualifier. For the if to work the compiler does something like this:

SUB EAX, [$000000]

For the assignment, the compiler does something like this:

MOV [$000000], EAX // or ESI on Delphi 7 with debug enabled

If you look at the left of the assembler instruction, you can easily see the actual opcode, in HEX notation. For example the SUB EAX, [$000000] looks like this:

2B0500000000

My hacky solution exploits this. I get the address of the actual procedure (TDBLookupControl.ProcessSearchKey), scan the code looking for the opcode (2B 05) and grab the address. That's it, and it works.

Of course, this has potential problems. It depends on the code being compiled with those exact registers (EAX in my example). The compiler is free to choose different registers. I tested with both Delphi7 and Delphi 2010, with code compiled for Debug and compiled without Debug. In all 4 cases the compiler chose to use EAX for the SUB instruction, and in 3/4 cases chose to use ESI as the register for the MOV instruction. Because of that my code only looks for the SUB instruction.

On the other hand if the code works once, the code works every time. Code doesn't change once released, so if you can properly test on the development machine, you will not get nasty AV's at client's machine. But use at your own risk, this is, after all, a hack!

Here's the code:

unit Unit2;

interface

uses DbCtrls;

function GetSearchTickCountPointer: PInteger;

implementation

type
  THackDbLookupControl = class(TDBLookupControl); // Hack to get address of protected member
  TInstructionHack = packed record
    OpCodePrefix: Word;
    OpCodeAddress: PInteger;
  end;
  PInstructionHack = ^TInstructionHack;

function GetSearchTickCountPointer: PInteger;
var P: PInstructionHack;
    N: Integer;
begin
  P := @THackDbLookupControl.ProcessSearchKey;
  N := 0; // Sentinel counter, so we don't look for the opcode for ever
  while N < 2000 do
  begin
    if P.OpCodePrefix = $052B then // Looking for SUB EAX, [SearchTickCount]
    begin
      Result := P.OpCodeAddress;
      Exit;
    end;
    Inc(N);
    P := PInstructionHack(Cardinal(P)+1); // Move pointer 1 byte
  end;
  Result := nil;
end;

end.

You use the hacky version like this:

var P: PInteger;
begin
  P := GetSearchTickCountPointer;
  if Assigned(P) then
    P^ := 1; // change SearchTickCount value!
end;


Another 2 options:

Hacky

  • cut my own VCL

Fix the problematic implementation and make sure all packages work with my own version of VCL

Sane

  • avoid fkData and fkInternalCalc fields in TDBLookupControls

I missed this implementation detail before. SearchTickCount is checked only if the field kind is fkData or fkInternalCalc. Having calculated fields (fkCalculated) should totally avoid the problem.


static int* s_TimerMemoryAddress;

union VTableHelper
{
    char* pointer;
    char** deref;
    unsigned int adjustment;
};

#pragma pack(1)
struct TInstructionHack
{
    WORD OpCodePrefix;
    int* OpCodeAddresss;
};

union FuncPtr
{
    TInstructionHack* Checker;
    char* Increment;
};
#pragma pack()

Since TDBLookupControl::ProcessSearchKey is virtual, a pointer to this function doesn't return an actual non static member function pointer address. Instead, it returns a vtable address, which points to a thunk (a small bit of code that redirects the virtual function to correct derived object non static member function). Below code figures out the final (non-virtual) address of the member function TDBLookupControl::* ProcessSearchKey based on thunk

try
{
    std::auto_ptr<TDBLookupControlHelper> hack(new TDBLookupControlHelper);
    TDBLookupControlHelper* ptrptr = hack.get();

    VTableHelper thunk;
    thunk.pointer = reinterpret_cast<char*>(ptrptr);
    thunk.pointer = *thunk.deref;       //get virtual table pointer
    //adjust for specific function pointer (TDBLookupControl::* ProcessSearchKey)
      as specified by thunk
    thunk.adjustment += 0xF4;       
    thunk.pointer = *thunk.deref;
    thunk.adjustment += 0x02;       //adjust for long jump instruction
    thunk.pointer = *thunk.deref;
    //get actual location of TDBLookupControl::ProcessSearchKey
    thunk.pointer = *thunk.deref;

    FuncPtr ptr;
    ptr.Increment = thunk.pointer;

    //2000 is completely arbitrary, only to prevent an infinite loop
    for(int counter = 0; counter < 2000 && s_TimerMemoryAddress == NULL; ++counter)
    {
         // Looking for SUB EAX, [SearchTickCount]
        if(ptr.Checker->OpCodePrefix == 0x052B)
            s_TimerMemoryAddress = ptr.Checker->OpCodeAddresss;
        else
            ptr.Increment++;
        counter++;
    }
}
catch(...) // catch any illegal dereferences of VTableHelper
{
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜