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
, overrideProcessSearchKey()
and make sure it uses your ownSearchTickCount
, one that's easily accessible. HappilyProcessSearchKey()
is virtual, in theory this should work, but in practice the code depends onFListField
, that's a private field so we'll be back to square 1. - Copy the whole
TDBLookupControl
to your ownTMyDBLookupControl
and make sure you can access theSearchTickCount
. 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
TDBLookupControl
s
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
{
}
精彩评论