1

我正在开发一个复杂的程序,该程序将具有调用函数的插件,但是这些函数的方法将在启动时选择,并使用函数指针进行分配。

而不是传递函数指针,我希望在主可执行文件中有一些有效的包装函数来调用适当的函数。

由于这是针对插件接口的,调用约定将根据构建目标定义__cdecl或定义__stdcall(使用宏),并且函数将声明为extern "C".

基本上我希望能够在我的可执行文件中声明一个符号,插件可以根据需要加载。对于需要解决复杂科学问题的不同任务,但是有多种解决方案或方法可以获取这些任务的结果,这些将自行存储在插件中,因此很容易添加新方法(无需重新编译整个应用程序)这也使得共享新方法变得更容易,因为任何拥有基本代码的人都可以添加任何不需要自己经验的插件。

我想出的任何方法都可以使用这个概念,或者我必须在加载插件时将函数映射传递给插件,但是该函数映射的细节取决于加载的配置和插件,因此我实际上并不在我完成加载插件之前知道它是什么,这将是一个问题。因此,我的解决方案是将地图作为一组全局变量存储在主可执行文件中,可通过包装函数访问。

然而,这不是直截了当的,因为函数具有调用约定,其中涉及在调用之后和返回之前操作堆栈,这应该在包装器上被忽略,它还应该jmp对 intel x386 ASM 执行无条件跳转,而不是call对 intel x386进行函数调用ASM 和控件应该从跳转到函数返回到调用代码而不是包装器。但是,我需要 C/C++ 代码独立于编译器/平台/处理器来执行此操作。

下面是我收集的一个基本概念示例,用于测试我的想法并展示我想要做什么:

C++ 代码(Microsoft Visual C++ 2010(特定))

#include <iostream>
void * pFunc;
int doit(int,int);
int wrapper(int, int);
int main() {
    pFunc = (void*)doit;
    std::cout << "Wrapper(2,3): " << wrapper(2,3) << std::endl;
    std::cout << "doit(2,3):    " << doit(2,3) << std::endl;
    return 0; }
int doit(int a,int b) { return a*b; }
__declspec(naked) int wrapper(int, int) { __asm jmp pFunc }

代码已经过测试可以正常工作,两个调用都输出 6

wrapper 和 doit 的 ASM 输出

PUBLIC  ?wrapper@@YAHHH@Z               ; wrapper
; Function compile flags: /Odtp
;   COMDAT ?wrapper@@YAHHH@Z
_TEXT   SEGMENT
___formal$ = 8                      ; size = 4
___formal$ = 12                     ; size = 4
?wrapper@@YAHHH@Z PROC                  ; wrapper, COMDAT
; File c:\users\glen fletcher\documents\visual studio 2010\projects\test_wrapper\test_wrapper.cpp
; Line 15
    jmp DWORD PTR ?pFunc@@3PAXA         ; pFunc
?wrapper@@YAHHH@Z ENDP                  ; wrapper
_TEXT   ENDS
PUBLIC  ?doit@@YAHHH@Z                  ; doit
; Function compile flags: /Ogtp
;   COMDAT ?doit@@YAHHH@Z
_TEXT   SEGMENT
_a$ = 8                         ; size = 4
_b$ = 12                        ; size = 4
?doit@@YAHHH@Z PROC                 ; doit, COMDAT
; Line 14
    push    ebp
    mov ebp, esp
    mov eax, DWORD PTR _a$[ebp]
    imul    eax, DWORD PTR _b$[ebp]
    pop ebp
    ret 0
?doit@@YAHHH@Z ENDP                 ; doit
; Function compile flags: /Ogtp
_TEXT   ENDS

用于包装器的非包装器 ASM

PUBLIC wrapper
_1$ = 8
_2$ = 12
_TEXT SEGMENT
wrapper PROC
   push ebp
   mov ebp, esp
   mov eax, DWORD PTR _2$[ebp]
   push eax
   mov ecx, DWORD PTR _1$[ebp]
   push ecx
   call DWORD PTR pFunc
   add esp, 8
   pop ebp
   ret 0
wrapper ENDP
_TEXT ENDS

如何获得以跨平台和跨编译器的方式生成的原始代码?与具有编译器生成的 Epilog 和 prolog 代码的 C/C++ 函数的标准相反,注意也不想对处理器做出假设,因此不能做一个单独的 ASM 文件,希望编译器生成代码仅使用无条件跳转语句。

goto不起作用,因为 pFunc 是变量而不是标签,甚至不确定它goto是否会在函数之间起作用。

4

2 回答 2

2

至于你的问题,

如何获取以跨平台和跨编译器的方式生成的原始代码?

去,答案是“根本没有”。

函数调用约定深入到平台和编译器/语言细节中。您正在接触所谓的ABI(应用程序二进制接口);像这样的问题:

  • 对于所有数字/类型/参数顺序,参数如何/在哪里从调用者传递到被调用函数?
  • 语言的“隐藏”特性(如 C++ this)是如何实现的?
  • 寄存器使用的规则是什么(通过对“目标上下文”进行函数调用来破坏哪些寄存器)?
  • 对于所有类型的“返回值”,返回值如何/在哪里?
  • 源(调用者)和目标(被调用者)上下文是否使用相同的数据结构布局规则?
  • 您如何处理处理器操作状态的变化(如果您尝试在 64 位模式下执行时调用 32 位代码,反之亦然)?

在这个 SO 线程中给出了类似的答案,然后特别针对有关执行“downcalls” 64bit Windows --> 32bit Windows 的问题stdcall。唉,除了“它很复杂,通常不可能并且总是非常强烈的代码/编译器和操作系统相关”之外,没有太多要添加的内容。

这可以在特定情况下完成(技术术语是“thunk”。每个“thunk”都非常具体:比如说,如果你知道被调用的函数使用 32bit Windows/x86 样式fastcall并且只有一个参数,你可以写一个“ thunk”执行接口(可能还有处理器状态切换),允许您从 64 位 Linux 代码中调用它。不过,该 thunk 与第一个参数是传入的浮点值的情况不同XMM0。 .. 等等。

对于一般情况...再次参考无限堆的编程知识,对不起,没有通用函数指针:(

编辑: 如果关注的是代码生成,请尝试以下操作:

/* sourcefile 1 */
extern void (*p)(char *, ...);

static __inline__ void wrapper(char *arg, char *s) {
    return p(arg, s);
}

int main(int argc, char **argv)
{
    wrapper("Hello, I am %s\n", argv[0]);
    return 0;
}

/* sourcefile 2 */
extern void printf(char*, ...);
void (*p)(char *, ...) = printf;

如果我编译这两个,使用gcc with optimization,编译器会创建以下代码main

0000000000400500 <主>:
  400500: 48 83 ec 08 sub $0x8,%rsp
  400504: 48 8b 36 移动 (%rsi),%rsi
  400507: bf 0c 06 40 00 移动 $0x40060c,%edi
  40050c: ff 15 d6 03 10 00 callq *1049558(%rip) # 5008e8 <p>
  400512: 31 c0 xor %eax,%eax
  400514: 48 83 c4 08 添加 $0x8,%rsp
  400518:c3 retq

这几乎是您想要的 - 除了它消除了 wrapper(),而是通过函数指针直接内联调用。

于 2013-11-14T11:47:07.073 回答
0

我为我的问题制定了一个解决方案,而不是使用裸函数或传递函数指针列表。

我可以将指针传递给函数指针结构,即

struct Functions {
   bool (AppAPI *logInfo(std::string,...)),
   bool (AppAPI *logWarn(std::string,...)),
   bool (AppAPI *logError(std::string,...)),
   bool (AppAPI *registerFunction(std::string,void *))
   ...
} PluginFunctions;

for (int i = 0;i<plugins;i++) {
   plugin[i].initialize(&PluginFunctions)
}

PluginFunctions.logInfo = LogInfo;
...

作为插件的init函数,传递一个指向struct的指针,它可以存储这个然后从内存中加载函数指针的当前值,struct只是内存中的一个指针表,函数指针可以在struct之后设置已传递给插件,它仍然更新插件。

于 2013-11-15T03:03:21.417 回答