C++和全动态函数

6
我遇到了有关绕路的问题。众所周知,绕路只能在5个字节的空间内移动(即'jmp'调用和4个字节的地址)。因此,在类(方法)中是不可能有“钩子”函数的,因为您无法提供'this'指针,因为没有足够的空间(更彻底地解释问题请参见这里)。因此,我一整天都在构思解决方案,现在我想听听您对这个问题的看法,以便在不确定是否可能的情况下开始一个3-5天的项目。
最初,我有三个目标:我希望'hook'函数是类方法,我希望整个方法是面向对象的(没有静态函数或全局对象),并且最糟糕/最难的部分是完全动态的。这是我的(理论上的)解决方案;通过汇编,人们可以在运行时修改函数(任何绕路方法都是一个完美的例子)。因此,既然我可以动态地修改函数,那么我不应该也能够动态地创建它们吗?例如;我为要使用malloc/new分配的存储器分配了约30个字节。不会只需用与不同汇编运算符对应的二进制数字替换所有字节,然后直接调用地址(因为它将包含一个函数)吗?
注:我预先知道我要绕路的所有函数的返回值和所有参数,并且由于我正在使用GCC,thiscall约定与_cdecl基本相同。
这就是我的思路/即将实现的方法;我创建了一个'Function'类。该构造函数采用可变数量的参数(除第一个参数外,该参数描述目标函数的返回值)。
每个参数都是钩子将接收的参数的描述(大小和是否为指针)。因此,假设我想为int * RandomClass::IntCheckNum(short arg1)创建一个Function类。那么我只需像这样操作:Function func(Type(4, true), Type(4, true), Type(2, false))。其中'Type'定义为Type(uint size, bool pointer)。然后通过汇编,我可以动态地创建函数(注意:这全部使用_cdecl调用约定),因为我可以计算参数数量和总大小。
编辑:在示例中,Type(4, true)是返回值(int*),第二个Type(4, true)是RandomClass 'this'指针,Type(2, false)描述第一个参数(short arg1)。
通过这种实现方式,我可以轻松地将类方法作为回调函数,但这需要大量的汇编代码(而且我甚至没有特别丰富的经验)。 最终,唯一非动态的东西将是我的回调类中的方法(这也需要前后回调)。
所以我想知道:这是可能的吗?需要多少工作量?我是否在自找麻烦?
编辑:如果我表述不够清晰,请见谅,但如果您想要更详细的解释,请随时提问!
编辑2:我还想知道是否可以在某个地方找到所有汇编操作符的十六进制值?列表会很有帮助!或者是否可能以某种方式“保存”asm(“”)代码到内存地址(我非常怀疑)。

为什么要使用Detours呢?难道不能使用纯C++解决方案,例如std::function吗?还是我漏掉了什么? - Konrad Rudolph
不是我能帮你澄清事情。 你想在类中实现可重写的函数吗?(我的意思是你可以在运行时更改它们) 如果是这样,我认为(一旦完成)它可能会为C++中的AI编程开辟巨大的机遇。 +1 - akaltar
@akaltar 这被称为遗传编程,实际上根本不需要可重写函数。 - Konrad Rudolph
@KonradRudolph 或许我“可以”,但是我必须通过汇编语言来使用接口(因为那将是控制动态函数的唯一方式),而且我甚至不想考虑需要多少汇编代码。 - Elliott Darfink
你可以查看libffi中闭包的实现。 - rodrigo
显示剩余2条评论
1个回答

4
您所描述的通常称为“thunking”,并且是相当常见的实现方式。历史上,最常见的目的是在16位和32位代码之间进行映射(通过自动生成一个新的32位函数来调用现有的16位函数或反之亦然)。我相信一些C++编译器会生成类似的函数,以在多重继承中调整基类指针到子类指针。
它肯定看起来像是解决您问题的可行方案,我不预见到任何巨大的问题。只需确保使用操作系统中需要的任何标志来分配内存,以确保内存是可执行的(大多数现代操作系统默认提供不可执行的内存)。
如果在Win32中工作,您可能会发现此链接有用:http://www.codeproject.com/Articles/16785/Thunking-in-Win32-Simplifying-Callbacks-to-Non-sta 关于查找汇编操作的十六进制值,我知道的最好参考资料是NASM汇编器手册的附录(我不仅仅是因为我帮助编写了它而说这个)。这里有一个拷贝: http://www.posix.nl/linuxassembly/nasmdochtml/nasmdoca.html

哇,太棒了!这些链接真的很有趣,特别是关于 thunking 过程的阅读(只可惜它是 Win32 的)。现在请原谅我如果听起来有点傻,但正如之前提到的,我对汇编并不是特别熟练(我只知道一点 AT&T 语法),所以我必须询问你提到的 NASM 汇编器。我有两个问题:所有 ASM 运算符都只使用一个字节吗?其次,由于每个运算符都指定了许多不同的值,我应该关注哪一个?我想这取决于我的变量大小;但是对于 'push',有 13 种不同的值,我怎么知道我想要哪一个? - Elliott Darfink
1
这些是不同类型的推送指令变体(寄存器类型、立即值、间接内存引用)。指南顶部有所有不同模式的描述,因此请使用它来确定您需要哪个,然后只需查找列表以找到所需的指令格式。比如说,您想要推送EBX:那是一个reg32,所以您需要第二个变体,即“o32 50+r”。o32是操作数大小前缀,在运行32位代码时会被忽略;50+r是50十六进制加上寄存器的代码(3,它们在顶部列出),因此53h是您的代码。 - Jules
1
回答你的第一个问题,不,有一些指令长度超过一个字节,而且有些指令的大小取决于上下文(请参见上面的PUSH示例:在32位模式下,“o32”前缀不会生成任何代码,但如果您正在生成16位代码,则会在指令开头出现一个额外的66h字节)。然而,所有最常见的指令都是单字节的。 - Jules

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接