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;
}
(我故意将错误检查出来)
当执行主机时,“fuuuuuuuu !!!”被正确打印,即使我将带有完全不同功能签名的符号的void指针类型化。
为什么会发生这种情况,并且不同编译器之间的这种行为是否一致?
答案 0 :(得分:5)
因为void指针中没有关于函数签名的信息。或者除了地址之外的任何信息。如果你开始使用参数,那么你可能会遇到麻烦。
答案 1 :(得分:5)
发生这种情况是因为UB,这种行为与任何事情都不一致,无论出于何种原因。
答案 2 :(得分:2)
这实际上并不是一个非常好的例子,可以创建一个失败的案例,因为:
fu
中的参数。fu
具有较少的参数(或激活框架本身的内存占用空间较小),而不是您要投射的函数指针类型,因此您永远不会最终fu
尝试在调用者设置的激活记录之外访问内存的情况。最后,您所做的仍然是未定义的行为,但您没有做任何事情来创建可能导致问题的违规,因此它最终会成为无声错误。
不同编译器之间的这种行为是否一致?
没有。如果您的平台/编译器使用了一个需要被调用者来清理堆栈的调用约定,那么如果调用者和调用者期望的激活记录大小不匹配,那么很可能会使用。 ..在返回被调用者时,堆栈指针将被移动到错误的位置,可能会破坏堆栈,并完全弄乱任何堆栈指针相对寻址。
答案 3 :(得分:2)
刚刚发生了,
C
使用cdecl
调用转换(因此调用者清除堆栈)所以你的电话似乎才能正常工作。
但实际上行为是未定义的。更改签名或使用参数将导致程序崩溃:
例如,考虑stdcall
调用转换,其中 callee 桅杆清除堆栈。在这种情况下,即使您为调用者和被调用者声明了正确的调用转换,您的程序仍然会崩溃,因为您的堆栈将被破坏,因为被调用者将根据其签名清除它,但调用者根据另一个签名填充:
#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;
}