在Visual Studio中使用自定义prolog和epilog代码编写裸函数

时间:2009-05-20 08:35:05

标签: visual-studio-2008 plugins calling-convention stdcall

我正在一个由我无法控制的主机调用的dll中编写一些插件代码。

主机假定插件作为__stdcall函数导出。主机被告知函数的名称和它所期望的参数的详细信息,并通过LoadLibrary,GetProcAddress动态地调用它,并手动将参数压入堆栈。

通常插件dll暴露一个恒定的接口。我的插件公开了一个在dll加载时配置的接口。为实现此目的,我的插件公开了一组标准入口点,这些入口点在编译dll时定义,并根据需要将它们分配给正在公开的内部功能。

每个内部函数可能采用不同的参数,但这会与物理入口点名称一起传递给主机。我的所有物理dll入口点都被定义为采用单个void *指针,并且我通过处理来自第一个参数的偏移量和已经传送给主机的已知参数列表来自己编组堆栈中的后续参数。

主机可以使用正确的参数成功调用插件中的函数,并且一切正常...但是,我知道a)我的函数没有清理堆栈,因为它们应该是'被定义为带有4字节指针的__stdcall函数,因此即使调用者将更多参数压入堆栈,它们总是在末尾执行'ret 4'。和b)我无法处理不带参数的函数,因为ret 4将在返回时从堆栈中弹出4个字节太多。

从我的插件中追溯到主机的调用代码后,我可以看到实际上a)并不是那么大的事;主机丢失一些堆栈空间,直到它从调度调用返回,此时它清理其堆栈帧,清理我的垃圾;但是......

我可以通过切换到__cdecl并且根本不清理来解决b)。我假设我可以通过切换到裸函数并编写我自己的通用参数清理代码来解决a。

因为我知道刚刚调用的函数所使用的参数空间量,所以我希望它会像下面那样简单:

extern "C" __declspec(naked) __declspec(dllexport) void  * __stdcall EntryPoint(void *pArg1)
{                                                                                                        
   size_t argumentSpaceUsed;
   {
      void *pX = RealEntryPoint(
         reinterpret_cast<ULONG_PTR>(&pArg1), 
         argumentSpaceUsed);

      __asm
      {
         mov eax, dword ptr pX
      }
   }
   __asm
   {
      ret argumentSpaceUsed
   }
}

但是这不起作用,因为ret需要编译时间常数......有什么建议吗?

更新:

感谢Rob Kennedy的建议我已经做到了,这似乎有用......

extern "C" __declspec(naked) __declspec(dllexport) void  * __stdcall EntryPoint(void *pArg1)
{      
   __asm {                                                                                                        
      push ebp          // Set up our stack frame            
      mov ebp, esp  
      mov eax, 0x0      // Space for called func to return arg space used, init to 0            
      push eax          // Set up stack for call to real Entry point
      push esp
      lea eax, pArg1                
      push eax                      
      call RealEntryPoint   // result is left in eax, we leave it there for our caller....         
      pop ecx 
      mov esp,ebp       // remove our stack frame
      pop ebp  
      pop edx           // return address off
      add esp, ecx      // remove 'x' bytes of caller args
      push edx          // return address back on                   
      ret                        
   }
}

这看起来不错吗?

1 个答案:

答案 0 :(得分:4)

由于ret需要一个常量参数,你需要安排你的函数有一个恒定数量的参数,但只有在你准备从函数返回时才需要这种情况。因此,在函数结束之前,执行以下操作:

  1. 将返回地址从堆栈顶部弹出并暂存; ECX是个好地方。
  2. 从堆栈中删除可变数量的参数,方法是单独弹出每个参数,或直接调整ESP
  3. 将返回地址推回堆栈。
  4. ret与常量参数一起使用。
  5. 顺便提一下,在一般情况下,您称为(a)的问题确实是一个问题。你很幸运,调用者似乎总是使用帧指针而不是堆栈指针来引用它自己的局部变量。但是,不需要函数来执行此操作,并且无法保证宿主程序的未来版本将继续以这种方式工作。编译器也可能只在调用期间在堆栈上保存一些寄存器值,然后期望能够在之后再次将它们弹出。你的代码会破坏它。