写在前面
之前一直以为通过函数指针能够直接获取到函数地址(这里的函数地址指的是汇编层面call指令跳转的地址),最近实验才发现并不是这样的,并且获取函数地址过程中包含着一些坑。话不多说,先看测试代码。
测试代码
#include <iostream>
#include <stdio.h>
int func_0(int a, int b)
{
return a + b;
}
class A
{
public:
A() {}
~A() {}
void func_1() { std::cout << "func_1 in A!" << std::endl; }
static void func_2() { std::cout << "func_2 in A!" << std::endl; }
virtual void func_3() { std::cout << "func_3 in A!" << std::endl; }
virtual void func_4() { std::cout << "func_4 in A!" << std::endl; }
};
template<typename addressType, typename funcType>
addressType union_cast(funcType func_ptr) // 获取类内成员函数的函数地址
{
union
{
funcType f;
addressType d;
}u;
u.f = func_ptr;
return u.d;
}
#define asm_cast(var, addr) \
{ \
__asm \
{ \
mov var, offset addr \
} \
}
func_0
是一个普通函数,用于测试普通函数的函数地址。func_1
是类中的普通成员函数,func_2
是类中的静态成员函数,func_3
和func_4
是类中的虚函数,分别用于测试类中函数的函数地址。union_cast
是一种获取类中成员函数地址的方法,asm_cast
是通过汇编获取类中成员函数地址的方法(win32)。
int main()
{
A *pA = new A;
pA->func_3();
typedef int(*func_0_pointer)(int a, int b);
func_0_pointer pFunc_0 = func_0;
std::cout << std::endl;
std::cout << "print normal function" << std::endl;
std::cout << "func_0: " << pFunc_0 << std::endl;
std::cout << std::endl;
typedef void(A::*func_1_pointer)();
typedef void(*func_2_pointer)();
typedef void(A::*func_3_pointer)();
typedef void(A::*func_4_pointer)();
func_1_pointer pFunc_1 = &A::func_1;
func_2_pointer pFunc_2 = &A::func_2;
func_3_pointer pFunc_3 = &A::func_3;
func_4_pointer pFunc_4 = &A::func_4;
std::cout << "print func_pointer" << std::endl;
std::cout << "A::func_1: " << pFunc_1 << std::endl;
std::cout << "A::func_2: " << pFunc_2 << std::endl;
std::cout << "A::func_3: " << pFunc_3 << std::endl;
std::cout << "A::func_4: " << pFunc_3 << std::endl;
std::cout << std::endl;
void *ptr_1 = union_cast<void *>(&A::func_1);
void *ptr_2 = union_cast<void *>(&A::func_2);
void *ptr_3 = union_cast<void *>(&A::func_3);
void *ptr_4 = union_cast<void *>(&A::func_4);
std::cout << "print union_cast" << std::endl;
std::cout << "A::func_1: " << ptr_1 << std::endl;
std::cout << "A::func_2: " << ptr_2 << std::endl;
std::cout << "A::func_3: " << ptr_3 << std::endl;
std::cout << "A::func_4: " << ptr_4 << std::endl;
std::cout << std::endl;
void *ptr_5 = nullptr;
void *ptr_6 = nullptr;
void *ptr_7 = nullptr;
void *ptr_8 = nullptr;
asm_cast(ptr_5, A::func_1);
asm_cast(ptr_6, A::func_2);
asm_cast(ptr_7, A::func_3);
asm_cast(ptr_8, A::func_4);
std::cout << "print asm_cast" << std::endl;
std::cout << "A::func_1: " << ptr_5 << std::endl;
std::cout << "A::func_2: " << ptr_6 << std::endl;
std::cout << "A::func_3: " << ptr_7 << std::endl;
std::cout << "A::func_4: " << ptr_8 << std::endl;
std::cout << std::endl;
std::cout << "print printf" << std::endl;
printf("A::func_1: %p\n", &A::func_1);
printf("A::func_2: %p\n", &A::func_2);
printf("A::func_3: %p\n", &A::func_3);
printf("A::func_4: %p\n", &A::func_4);
std::cout << std::endl;
return 0;
}
main
函数中,首先获取普通函数func_0
的函数指针,通过std::cout
打印;然后获取类中成员函数的函数指针,通过std::cout
打印;再分别使用union_cast
和asm_cast
获取类成员函数地址,通过std::cout
打印;最后直接通过printf
打印类中函数指针。
实验结果
图中可以看到在win32平台上,普通成员函数和类静态成员函数通过函数指针能够直接打印出函数地址。类中的成员函数通过std::cout
打印出来都是1。是由于<<
操作符没有对void(__thiscall A::*)()
类型进行重载,编译器将其转换为了bool类型,所以输出为1,对于类的静态函数,调用方式不是__thiscall
,有对应的重载,所以能正常打印出地址。再看最后printf
的输出能够打印出所有类中函数,是因为它能接收任意参数。
接着分析union_cast
和asm_cast
,对于类中的普通函数和静态函数两者打印的地址是一样的,对于虚函数两者打印的却不一样,不确定哪一个是正确的,或者两者都是错误的。于是通过汇编来看看。
这里的call eax
表示调用虚函数func_3
,这里eax
寄存器的地址值为0055154B
,和asm_cast
打印的值是一致的,看起来union_cast在VS2017环境下对虚函数地址转换是有问题的。由于查阅了一些资料显示通过union_cast转换虚函数得到的应该是偏移量,与这里的实验结果不符合,于是在MacOS下Clang编译器环境下重复实验,得到如下结果。
图中显示所有函数通过std::cout
打印出来都是1,猜测是由于Clang编译器中<<
操作符没有重载函数指针类型。通过union_cast
和printf
能够打印出地址,对于类中的普通函数和静态函数打印出的是绝对地址,对于虚函数打印出的是在虚表中的偏移量。与win32平台测试结果差异较大。
总结
- 如果项目中需要使用函数地址,最好在对应平台上实际测试后采用合适的方式。
- 从测试结果来看,
union_cast
在win32下VS2017和MacOS下Clang环境下获取类的普通函数和静态函数地址都没问题;win32上获取虚函数地址结果存疑,不确定获取的地址指向何处;MacOS上获取虚函数地址是偏移量,需要结合虚表指针计算出实际地址。 asm_cast
在win32下VS2017环境获取类的普通函数和静态函数没问题,获取虚函数也是汇编实际跳转地址。MacOS上架构不同没有测试asm_cast
。
更多内容关注公众号:C++学习与探索,欢迎留言讨论,创作不易,感谢点赞、关注和收藏~