在进入问题的细节之前,我想透露一下,这是作为一项研究任务交给我的,所以它可能会也可能不会。简而言之,最终目标将是拥有一个类,该类能够根据运行时条件调用两个函数之一,并且开销最小。我们的类将有两个主要方法:
(1) set_direction(bool) :在这里,我们想以编程方式更改下一个方法中的调用指令。我们可以在这里使用任何方法来做到这一点,我探索过的一些方法是二进制编辑、挂钩,甚至更改已定义的可执行缓冲区的内容。
(2) branch() :这里我们将调用通过上述方法选择的两个函数之一。关键部分是我们希望以最小的开销来做到这一点。这意味着,没有指针取消引用和条件,只是对绝对地址的简单调用(或 JMP)。
我们的代码看起来像这样:
class BranchChanger
{
private:
func if_branch_func;
func else_branch_func;
/* possibly other stuff here as well */
public:
BranchChanger(func if_branch, func else_branch);
void set_direction(bool); // change method called by 'branch' programatically
auto branch(); // call either if_branch or else_branch directly with minimal overhead
}
我们的主程序应该是这样的:
#include <branch.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main() {
BranchChanger branch = BranchChanger(&add, &sub);
bool condition = rand() % 2; // our runtime condition
branch.set_direction(condition);
branch.branch(a, b); // call either add or sub with minimal overhead
}
问题是,当分支被调用时我们想要发生的是:
CALL <some_address>
但在现实中,总是这样:
CALL QWORD PTR [<some_address>]
由于函数指针的工作方式,我们无法通过这种方式获取函数的地址,因为取消引用函数指针只会衰减回指向函数的指针。如果我们用我们可以编辑的字节码定义一个可执行缓冲区,我们可以用一些简洁的 linux 系统调用分配它,即使这样也需要通过我们不想要的指针来访问。我尝试过函数挂钩(包括 trampolines、detours 等)、内联 asm、间接函数编译器属性,甚至尝试用符号填充。但是我没有成功,因为 branch() 方法总是会由于指针取消引用而涉及某种间接。
我有一个非常蛮力的想法是修改当前的 ELF,但是我不确定这有多可行。我想做的是在调用 set_direction 时手动复制 ELF 中任一函数的地址,将其粘贴到分支主体中。如您所见,我的想法已经用完了,我希望能有一些新鲜的眼光来看待这个问题,也许我遗漏了一些东西(或者这根本不可能)。
注意,将类的所需语法视为占位符,使用它可能无法实际执行我们想要的操作,但这是我们的目标。
所有回复将不胜感激。
编辑:这是我的主管想要的(旧对话的片段)
""""""""""""""""""""""""""""""""""
不想在运行时这样做:
if(方向)code1();否则代码 2();
另外,不想取消引用函数指针。 目标:最小化运行时开销。
(1) 运行时没有条件检查; (2) 没有指针/函数指针解除引用
理想情况下,单个汇编 JMP 指令(无条件),我们可以通过编程方式编辑其地址。
如何在 C++ 中以编程方式编辑汇编代码?例如。如何更改无条件 JMP 的地址? (您可以直接将机器代码写入内存。)因为汇编指令只是内存中的数字。函数指针应该可以给你编辑的地址。
这在运行时需要时间。相反:
JMP
我们可以通过编程将其设置为两个值之一。 这消除了开销。
function1() { ... }
function2() { ... }
<address> = &function1
<address> = &function2
""""""""""""""""""""""""""""""""" 编辑:感谢您的评论,我只是想指出,解决方案的任何内容都摆在桌面上,无论它多么骇人听闻。我们正在寻找的是概念证明和原型实现。我们想要在 linux 机器上的 gcc 编译器(最新版本 pref)上工作的原型。
在大多数平台上改变机器代码(作为避免间接调用的唯一方法)确保巨大的开销,如果可能的话。
通过比较,只有通过间接调用实现指针解引用的开销是从内存中读取地址。现有的 ISA 旨在有效地支持调用表。