Delphi:有什么方法可以链接到 BPL 中尚未打包的变量吗?

发布于 2024-11-17 12:07:12 字数 1021 浏览 3 评论 0原文

我正在 RAD Studio 2007 中开发一个项目,使用 C++ 中的 VCL 类。

TDBLookupControl 是 VCL 和 TDBLookupControl 的一部分。有一些不良行为,这是由于使用内部变量 SearchTickCount 引起的。

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

procedure TDBLookupControl.ProcessSearchKey(Key: Char);
var
  TickCount: Integer;
  S: string;
begin
//some code removed for brevity
      TickCount := GetTickCount;
      if TickCount - SearchTickCount > 2000 then SearchText := '';
      SearchTickCount := TickCount;
//some code removed for brevity
end;

但是,SearchTickCount 从未在 VCL 内打包,如下例所示。

extern PACKAGE int SearchTickCount;

我想在我的 C++ 代码中将 SearchTickCount 设置为零(按需)。 在我的代码中外部它使 c++ 编译。但是,链接器(显然)无法找到该变量。

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

有什么方法/解决方法可以链接到这个变量吗?

编辑: 不幸的是,我们还使用了一些从 TDBLookupControl 派生的自定义控件,因此我试图避免创建更多自定义控件。

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 removed 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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

偏爱你一生 2024-11-24 12:07:12

问题

SearchTickCount 是在单元的实现部分中声明的全局(单元级)变量,不应该在该单元之外访问它。如果您使用 Delphi 而不是 C++ Builder,您也会遇到同样的问题。

明智的解决方案

  • TDBLookupControl 子类化,覆盖 ProcessSearchKey() 并确保它使用您自己的 SearchTickCount,这是一个易于访问的解决方案。令人高兴的是 ProcessSearchKey() 是虚拟的,理论上这应该可行,但实际上代码依赖于 FListField,这是一个私有字段,所以我们将回到第 1 步。
  • 将整个 TDBLookupControl 复制到您自己的 TMyDBLookupControl 并确保您可以访问 SearchTickCount。这绝对有效。

HACKY 解决方案

当然,黑客更有趣。 CPU 可以毫无问题地找到 SearchTickCount,因为该地址被编码到构成 ProcessSearchKey 代码的 ASM 指令中。 CPU能读的,我们也能读。

评估 ProcessSearchKey 方法的代码,它只使用一个全局变量 (SearchTickCount),并在两个地方使用它。首先在这个测试中:

if TickCount - SearchTickCount > 2000 then

然后在这条指令中:

SearchTickCount := TickCount;

如果您查看该例程的反汇编列表,很容易发现全局变量访问,因为它在方括号中给出了变量的地址,没有其他限定符。为了使 if 工作,编译器会执行如下操作:

SUB EAX, [$000000]

对于赋值,编译器会执行以下操作:

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

如果您查看汇编器指令的左侧,您可以轻松地看到实际的操作码,在十六进制表示法。例如 SUB EAX,[$000000] 看起来像这样:

2B0500000000

我的黑客解决方案利用了这个。我获取实际过程的地址 (TDBLookupControl.ProcessSearchKey),扫描代码查找操作码 (2B 05) 并获取地址。就是这样,并且有效。

当然,这有潜在的问题。这取决于使用这些确切的寄存器编译的代码(在我的示例中为EAX)。编译器可以自由选择不同的寄存器。我使用Delphi7和Delphi 2010进行了测试,代码编译为调试,编译为不调试。在所有 4 种情况下,编译器都选择使用 EAX 作为 SUB 指令,并且在 3/4 种情况下选择使用 ESI 作为MOV 指令。因此,我的代码只查找 SUB 指令。

另一方面,如果代码运行一次,则代码每次都运行。代码一旦发布就不会改变,所以如果你可以在开发机器上正确测试,你就不会在客户端机器上遇到讨厌的反病毒软件。但使用风险自负,这毕竟是一种黑客行为!

这是代码:

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.

您可以像这样使用 hacky 版本:

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

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;
沧桑㈠ 2024-11-24 12:07:12

另外 2 个选项:

Hacky

  • 剪切我自己的 VCL

修复有问题的实现,并确保所有包都与我自己的 VCL 版本一起工作

Sane

  • 避免 TDBLookupControl 中的 fkData 和 fkInternalCalc 字段

我之前错过了这个实现细节。仅当字段类型为 fkData 或 fkInternalCalc 时才会检查 SearchTickCount。拥有计算字段(fkCalculated)应该完全避免这个问题。

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.

平安喜乐 2024-11-24 12:07:12
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()

由于 TDBLookupControl::ProcessSearchKey 是虚拟的,因此是指向该函数的指针
不返回实际的非静态成员函数指针地址。
相反,它返回一个 vtable 地址,该地址指向一个 thunk
(一小段代码,将虚拟函数重定向到正确的派生函数
对象非静态成员函数)。下面的代码计算出最终的(非虚拟)
基于thunk的成员函数TDBLookupControl::* ProcessSearchKey的地址

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
{
}
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
{
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文