重新实现第三方非虚拟功能



我想知道是否有一种方法可以在第三方类Base中重新实现非虚拟函数foo

其动机是我只需要在foo中添加一个回调。然而,该函数是从类Base的许多位置调用的,并且由于它不是虚拟的,因此它需要在适当的位置更改Base,或者大幅重写将从Base派生的类的实现。我想避免这两种情况。

我根本不需要多态性,派生类将只有一个实例,并且类型在编译时是已知的(例如,CRTP而不是虚拟化也足够了)。

我尝试使用一个类,它继承了一个声明foo为虚拟的辅助类,但没有成功。这里有一个例子,其中bar模拟Base实现的任何地方,可以从中调用foo

/// Ideally, do not modify the `Base` at all
struct Base {
void foo()
{
cout << "Base::foo" << endl;
}
void bar()
{
cout << "bar" << endl;
foo();  //< foo is not virtual !
}
};
struct Virtual {
virtual void foo() = 0;
};
struct Virtual_base : Virtual, Base {
void foo() override = 0;  //< it still does not affect Base !
};
struct Virtual_derived : Virtual_base {
void foo() override
{
cout << "Virtual_derived::foo" << endl;
Base::foo();
}
};

好吧,Virtual_derived::foo确实覆盖了,但不出所料,Base::bar仍然没有变化。我也尝试了CRTP方法,但显然没有成功,因为它仍然与Base保持封装的问题相冲突。

我有点担心答案是这是不可能的。。是吗?

在Windows上,您可以使用热修补:https://jpassing.com/2011/05/03/windows-hotpatching-a-walkthrough/。

使用/hhotpatch编译。这将在每个函数的开头添加一个两字节的NOP,在此之前添加一个6字节的NOP(32位上为5),允许您修补重定向。你想做的是修改开头的两个字节的nop,跳回6字节的nop块,然后它可以跳到你的回调包装器,然后它调用你的回调,然后跳回函数本身。要实现它,请将其添加到C++源文件中:

void pages_allow_all_access(void* range_begin, size_t range_size) {
DWORD new_settings = PAGE_EXECUTE_READWRITE;
DWORD old_settings;
VirtualProtect(
range_begin,
range_size,
new_settings,
&old_settings
);
}
void patch(void* patch_func, void* callback_wrapper) {
char* patch_func_bytes = (char*)patch_func;
char* callback_wrapper_bytes = (char*)callback_wrapper;

pages_allow_all_access(patch_func_bytes - 6, 8);
// jmp short -5 (to the jmp rel32 instruction)
patch_func_bytes[0] = 0xEB;
patch_func_bytes[1] = 0x100 - 0x7;
// nop (probably won't be executed)
patch_func_bytes[-6] = 0x90;
// jmp rel32 to callback_wrapper
patch_func_bytes[-5] = 0xE9;
*(int32_t*)&patch_func_bytes[-4]
= (int32_t)(callback_wrapper_bytes - patch_func_bytes);
}

回调包装器可能需要在程序集文件中定义:

callback_wrapper:
; save registers
pushad
pushfd
call QWORD PTR [callback]
popfd
popad
jmp QWORD PTR [after_trampoline]

符号callback和after_rampoline应该在C++文件中公开(因此在全局范围内)。

void* callback = &callback_func;
void* after_trampoline = (char*)&patch_func + 2;

然后在main的顶部调用patch或其他合适的初始化时间,您就设置好了。

此外,您可能必须使用VirtualProtect调用允许对正在修改的内存页(patch_func所在的内存页)授予写权限:https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect。编辑:我已经将此代码添加到上面的示例中。

稍后我可能会在Linux或其他Unixy系统上添加这样做的方法。

当函数中没有一组方便的NOP时,挂钩会变得更加困难,尤其是在x86体系结构上,因为指令的长度变化很大,因此很难通过编程找到指令的结束位置,以便跳回下一条指令@ajm建议这个库:https://github.com/kubo/funchook对于Linux&OSX。然而,就我个人而言,当我没有热补丁时,我通常会使用调试器在补丁目标中找到一系列长度至少为9字节的指令,我可以替换这些指令。然后,在程序中,我使用类似于上面的技术,用跳转到绝对立即64位来替换这些指令,但我也添加了那些替换后的指令,以便在回调包装器的末尾执行。避免替换call或jmp指令,因为它们通常是相对于指令指针的,后者在回调包装器中的值与原始函数中的值不同。

总之,这取决于一个人对其项目的需求。

在标准C++中,这是不可能的。时期

这个问题缺乏更详细的要求,所以@Anonymous1847和@ajm提出的技术——补丁/挂钩——实际上正确地回答了我的问题。然而,我正在寻找一个标准的、可移植的、稳定的解决方案。我当然不知道这些技术的细节,但hotpatch存在可移植性问题,而对于hook,我认为由于它们依赖于反汇编,因此不能认为它通常是可靠的,相当脆弱的,主要是在平台和编译器特定的方面,如优化。我还想知道这些技术将如何应对在从Base的导数派生的类中进一步覆盖foo的需要。此外,我想说的是,这些会显著降低代码的可读性。尽管如此,这些都是可能的解决方案,可以满足人们的需求。

当我谈到";准备";作为Base将从中继承的类,我意识到,即使它无论如何都是可能的,析构函数~Base也可能是非虚拟的(就像在本例中一样),这不会受到其他任何东西的影响(修复),只能受到Base本身的影响。(离题:虽然这在C++的情况下可能是一个问题,但我想在其他语言的情况下不会,因为所有函数都是虚拟的。我仍然认为这种"继承的逆"在某些情况下可能有用,但我不知道任何语言,也不知道处理它的基本概念。有人知道为什么吗?)

我发现最有益的解决方案是@Ulrich建议的解决方案,尽管这违反了我最初的问题。原因是,根据我上面提到的论点,它的优点IMO超过了缺点——将Base::foo更改为虚拟就足够了。不必担心运行时开销,因为优化器可能会使其失去机会。然而,当我们谈论第三方库时,一个人必须保留他/她自己的副本(例如,在Gitlab/Github的情况下分叉它),并进行一些小的修改(比如虚拟的东西),并以某种方式自行维护副本。例如,如果一个人仍然想跟踪原始库,那么继续执行这个小";补丁";根据图书馆的更新(我对这些东西没有太多经验)。(当然,只有在Base的原始接口不会完全改变的情况下。)

请随意纠正我的判断。

最新更新