我可以用错误的签名调用用dlsym()导入的函数,为什么

I can call a function imported with dlsym() with a wrong signature, why?

本文关键字:导入 函数 为什么 dlsym 错误 名调用 我可以      更新时间:2023-10-16

host.cpp具有:

int main (void)
{
void * th = dlopen("./p1.so", RTLD_LAZY);
void * fu = dlsym(th, "fu");
((void(*)(int, const char*)) fu)(2, "rofl");
return 0;
}

p1.cpp有:

#include <iostream>
extern "C" bool fu (float * lol)
{
std::cout << "fuuuuuuuu!!!n";
return true;
}

(我故意留下错误检查)

当执行host时,"fuuuuuuuu!!"会正确打印,即使我用完全不同的函数签名将void指针键入到符号。

为什么会发生这种情况?不同编译器之间的这种行为是否一致?

发生这种情况是因为UB,而且这种行为与任何事情都不一致,无论出于何种原因。

因为在void指针中没有关于函数签名的信息。或者除了地址之外的任何信息。如果你开始使用参数,你可能会遇到麻烦。

这实际上不是一个创建失败案例的好例子,因为:

  1. 您从不使用函数fu中的参数
  2. 函数fu的参数(或者激活帧本身的内存占用空间较小)比您要转换的函数指针类型少,因此您永远不会出现fu试图访问调用方设置的激活记录之外的内存的情况

最终,你所做的仍然是未定义的行为,但你没有做任何事情来创建可能导致问题的违规行为,因此它最终会成为一个无声的错误。

不同编译器之间的这种行为是否一致?

否。如果您的平台/编译器使用了一个调用约定,该约定要求被调用者清理堆栈,那么oops,如果被调用者和调用者期望的激活记录大小不匹配,您很可能会感到沮丧。。。当被调用者返回时,堆栈指针将被移动到错误的位置,这可能会损坏堆栈,并完全扰乱任何堆栈指针的相对寻址。

刚刚发生了,

  • C使用cdecl调用转换(因此调用者清除堆栈)
  • 函数不使用给定的实参实参

因此您的呼叫似乎工作正常。

但实际上行为是不明确的。更改签名或使用参数将导致程序崩溃:

添加:

例如,考虑stdcall调用转换,其中被调用者桅杆清除堆栈。在这种情况下,即使你为调用者和被调用者声明了正确的调用转换,你的程序仍然会崩溃,因为你的堆栈会被破坏,因为被调用者会根据它的签名清除它,但调用者会根据另一个签名填充:

#include <iostream>
#include <string>
extern "C" __attribute__((stdcall)) __attribute__((noinline)) bool fu (float * lol) 
{
std::cout << "fuuuuuuuu!!!n";
return true;
}
void x()
{
(( __attribute__((stdcall)) void(*)(int, const char*)) fu)(2, "rofl");
}
int main (void)
{
void * th = reinterpret_cast<void*>(&fu);
std::string s = "hello";
x();
std::cout << s;
return 0;
}