【问题标题】:How to call an assembly function from C++ dynamically?如何从 C++ 动态调用汇编函数?
【发布时间】:2015-10-19 14:06:34
【问题描述】:

要求:对于某个项目,我们有独特的要求。该应用程序支持一种表达式语言,允许用户定义他们自己的复杂表达式,这些表达式可以在运行时进行评估(每秒数百次),并且它们需要在机器级别执行以提高性能。

WORKING:我们的表达式解析器完美地将脚本翻译成相应的汇编语言程序。我们通过静态链接我们的 C 测试程序生成的目标文件来检查它,它们产生了正确的结果。 由于客户端可以随时更改脚本,我们的程序(在运行时)检测到更改,调用生成相应汇编例程的解析器。然后我们从后端调用汇编器来创建目标代码。

问题

我们如何从 C++ 程序中动态调用这个汇编例程 (加载器)?

我们不应该调用 C++ 编译器来将它与加载程序链接,因为加载程序已经运行了其他子例程,我们不能关闭加载程序,重新编译然后执行新的加载程序程序。

我尝试在线搜索解决方案,但每次结果都充斥着 .NET 程序集动态调用。我们的应用与 .NET 无关。

【问题讨论】:

  • 所以基本上,您想获取一些输入,生成程序集(您已经完成并且正在工作),然后从生成它的应用程序中开始执行程序集?对吗?
  • 我从来没有想过要尝试这样做......所以可能有更好的方法,但我想你可以将程序集写入一个简单的 DLL 文件并在运行期间加载 DLL -time,找到包含你刚才写的程序集的函数对应的正确函数地址,然后开始调用吧……
  • 对...就像我说的那样,这不是最优雅的方式,但是您可以在带有任何参数/返回的函数内部编写一个包含您创建的可执行程序集的 DLL你需要,然后让程序 C 在运行时加载它(通过 LoadLibrary()),这将在 Windows 上完成。我相信你可以在 linux 上对共享对象做类似的事情,但我不是 linux 人,所以我不知道。
  • Linux 可以使用dlopen 函数加载共享库,您可以使用dlsym 查找符号。如果您的汇编代码是独立的,您可以要求汇编程序生成一个普通的二进制文件,然后您可以简单地将其加载到内存中并执行(注意设置权限)。对于 windows 和 linux,这应该是最小的不同。
  • 你应该看看GModule。

标签: c++ assembly dynamic linker


【解决方案1】:

首先,“生成的plugin”方法(在 Linux 上;我的回答侧重于 Linux,但可以通过一些努力适应 Windows;您可以使用多平台框架,如 QtPOCO 或来自GTK 的Glib;然后使用可以在Windows、Linux、MacOSX 和Android 上使用的通用API 来包装插件加载能力,如dlopen):

  • 在某个文件/tmp/generated01.c 中生成C(或汇编)代码(您甚至可以使用标准C++ 容器生成C++ 代码,但它的编译速度会明显变慢;注意name mangling 所以发出并使用extern "C" 函数;阅读C++ dlopen mini HowTo)。请参阅 this answer 解释为什么生成 C 是值得的(并且可能比生成汇编代码更好、更便携)。
  • 运行(使用fork+execve+waitpid,或简单地system)通过运行gcc -fPIC -Wall -O /tmp/generated01.c -shared -o /tmp/generated01.so命令将生成的文件编译成共享对象/tmp/genenerated01.so;您实际上需要获得position-independent code,因此需要-fPIC 标志。如果在生成的汇编代码上使用dlopen,则需要改进汇编生成器以发出 PIC 代码。
  • dlopen 那个新的/tmp/generated01.so(所以使用dynamic linker),见dlopen(3);你甚至可以 remove 现在生成的无用 C 文件 /tmp/generated01.c
  • dlsym相关符号获取生成代码的函数指针,见dlsym(3);您的应用程序只需使用这些函数指针调用生成的代码。
  • 当你确定你不需要它的任何函数并且没有调用框架使用它时,你可以dlclose那个共享对象库(但你可以接受通过不调用dlclose来泄漏一些地址空间完全)

上述方法是值得的,可以多次使用(我的manydl.c 证明你可以dlopen 一百万个不同的共享对象),并且实际上甚至兼容(即使在发出 C 代码时!) 交互式 Read-Eval-Print-Loop - 在大多数当前的台式机、笔记本电脑和服务器上 - 因为大多数时候生成的 /tmp/generated01.c 会非常小(例如,最多几百行)非常快生成和编译(由gcc 等...)。我什至在MELT 中使用它的REPL 模式。在 Linux 上,这种插件方法通常需要将主应用程序与 -rdynamic 链接(以便 dlopen-ed 插件可以引用和调用主应用程序中的函数)。


然后,其他方法可能是使用一些 Just-In-Time compilation,例如

  • GNU lightning(它发出机器代码非常快 - 非常短的 JIT 发出时间,但生成的代码运行缓慢,因为它非常未优化)
  • asmjit;它是特定于 x86-64 的,可让您生成单独的 x86-64 机器指令
  • GNU libjit 可用于多个平台,并为其他平台提供“解释器”模式
  • LLVMClang/LLVM 编译器的一部分,可用作 JIT 库)
  • GCCJITGCC 的新 JIT 库前端)

粗略地说,该列表的第一个元素能够相当快地发出 JIT 机器代码,但该代码的运行速度不如使用 gcc -fPIC -O1-O2 编译的等效生成的 C 代码(但会运行通常慢 2 到 5 倍!);最后两个元素(LLVM 和 GCCJIT)是基于编译器的:因此它们能够优化并发出高效代码,但代价是 JIT 代码发出较慢。所有 JIT 库都能够(就像 dlsym 对插件所做的那样)为新 JIT 构建的函数提供函数指针。

请注意,需要进行权衡:某些技术能够快速生成某些机器代码,前提是您接受生成的代码稍后运行速度稍慢;其他技术(特别是 GCCJIT 或 LLVM)正在花费时间来优化生成的机器代码,因此需要更多时间来发出机器代码,但该代码稍后会快速运行。您不应该期望两者(生成时间短,执行时间快),因为没有免费的午餐。


我认为手动生成一些汇编代码实际上是不值得的。您将无法生成非常优化的代码(因为 optimization 是非常 difficult 的艺术,并且 GCC 和 Clang 都有数百万用于优化传递的源代码行),除非您为此花费多年的工作。使用一些 JIT 库更容易,“编译”为 C 或 C++ 也很容易(您将优化的负担留给您正在调用的 C 编译器)。


您还可以考虑将您的应用程序重写为具有homoiconicitymetaprogramming 功能的某种语言(例如multi-stage programming),例如Common Lisp(以及许多其他语言,例如提供@ 987654349@)。它的SBCL 实现总是发出机器代码...

您还可以在您的应用程序中嵌入解释器,例如 Lua - 甚至可能是 LuaJit- 或 Guile。嵌入现有语言的主要优点是有资源(书籍、模块等)和了解它们的社区(设计一种好的语言很困难!)。此外,嵌入式解释器库设计得很好并且可能调试得很好(因为使用了很多),其中一些速度足够快(因为使用了bytecode 技术)。

【讨论】:

  • 了解 C 的 JIT 编译非常有趣。我假设它会创建可以动态调用的机器代码?如果真的动态创建非常快的代码,那么可以考虑。我以前从未听说过。
  • 您不能期望生成时间短,生成代码的执行时间快。您需要做出一些权衡并对各种方法进行基准测试。
【解决方案2】:

正如 cmets 已经建议的那样,LoadLibrary (Windows) 和 dlopen (Linux/POSIX) 是迄今为止最简单的解决方案。这些专门用于动态加载代码。同样重要的是,它们都允许 卸载,并且有一些函数可以通过名称获取函数入口点。

【讨论】:

    【解决方案3】:

    您可以动态执行此操作。我将以linux案例为例。由于您的解析器工作正常并生成机器代码,您应该能够为 Windows 生成 .so(对于 linux)或 .dll。

    接下来,将库加载为

    handle = dlopen(so_file_name, RTLD_LAZY);
    

    下一步获取函数指针

    func = dlsym(handle, "function_name");
    

    那么你应该可以将它作为 func() 执行

    你需要试验的一件事(以防你没有得到想要的结果)是关闭并打开so文件或dll文件(你只需要在需要时才这样做,否则可能会降低性能)

    【讨论】:

      【解决方案4】:

      听起来您可以生成正确的字节码。因此,您可以确保生成与位置无关的代码,将其写入可执行内存,然后在代码上调用或创建线程。最简单的方法就是将指向您编写代码的内存基址的指针转换为函数指针,然后调用它。

      如果您编写字节码以避免引用不同的部分,而是引用其加载基数的偏移量,那么“加载”代码就像将其写入可执行内存一样容易。一旦开始执行,您可以调用/pop/jmp 来查找代码的基础。

      相反,可能是最简单的解决方案,就是将代码编写为期望参数的函数,这样您就可以将代码的基数和任何其他参数传递给它,就像使用任何其他函数一样,只要您为函数指针使用正确的 typedef,并且生成的程序集正确处理参数。只要避免创建绝对跳转或对绝对地址的数据引用,就不会有任何问题。

      【讨论】:

      • 但是使用一些现有的 JIT 库,或者发出一些C 代码,要容易得多。
      • 我猜,听起来他们已经生成了代码。我想这取决于这个程序将来如何使用,以及它是一个长期项目还是短期项目。
      • 看起来他们正在生成 assembler 代码(在某些文本文件中),而不是 machine 代码(在内存中)。这实际上完全不同。
      • 哦,出于某种原因,我认为他们正在使用解析器生成字节码,我想我错了。
      • @superultranova:您提出的想法与我的团队正在努力的工作非常接近。实际上,我们离机器代码并不远。并且生成的代码进行了一定程度的优化。 Basile Starynkevitch 也询问了细节。除了该项目将在 24x7 运行的专用机器上使用之外,我无法透露太多信息。
      【解决方案5】:

      为时已晚,但我认为这会对其他人有所帮助。 如果您想动态执行一段代码,您可以为此创建一个解释器。 将你的表达式编译成一些字节码,然后编写解释器来执行它。

      这里有一个关于编写解释器的教程,但是是在 python 中。

      https://ruslanspivak.com/lsbasi-part1/

      您可以使用 c/c++ 编写它

      【讨论】:

      • 没错,但一般来说解释器(甚至字节码解释器)比编译代码或机器代码或生成的 C 或 C++ 代码慢。我知道罕见的例外情况。
      • 是的,确实如此,但通过优化,您可以获得几乎接近预期的性能。这又是一个想法。
      • 我不这么认为,除非您考虑将partial evaluationwhole program optimization 结合使用。也许使用SBCL 在实践中可能更简单
      猜你喜欢
      • 2012-12-03
      • 1970-01-01
      • 2016-09-07
      • 2017-02-18
      • 1970-01-01
      • 2012-04-21
      • 2018-10-13
      • 1970-01-01
      相关资源
      最近更新 更多