文章目录
提出疑问c++层面非虚函数虚函数ida动调分析非虚函数虚函数近一步解释c++代码ida调试注意提出疑问
首先问大家一句,什么是函数指针?
肯定有的人会这样回答,函数指针?不就是指向函数地址的一个指针吗?或者就是一个存放着一个函数首地址的变量?
当然,那些有点底层基础的肯定会这样说,函数就是一堆连续的机器码,而函数指针,就是存放了这堆连续机器码首地址的变量。
详细了解函数指针(因为这里主要讲成员函数指针原理):
c/c++函数指针(Hook前奏1)
c/c++ typedef定义函数指针(Hook前奏2)
那么大家是不是回答的时候,考虑的地方是不是仅仅局限于 一般的函数????那么成员函数呢???
为什么得强调成员函数呢?因为成员函数包括了虚函数和非虚函数(这里涉及虚表问题,可以先简单看看列出的虚函数系列,否则接下来问题会有点难以接受。)
虚函数系列:
详解虚函数的实现过程之初探虚表(1)
详解虚函数的实现过程之单继承(2)
详解虚函数的实现过程之多重继承(3)
详解虚函数的实现过程之虚基类(4)
详解虚函数的实现过程之菱形继承(5)
c++层面
首先,上两份成员函数指针代码
非虚函数
#include<iostream>using namespace std;class a {public:int add(int a, int b) {return a + b;}};typedef int (a::* pClassFun)(int, int);int main() {pClassFun pointer = &a::add;a aa;cout << (aa.*pointer)(10, 20);}
虚函数
#include<iostream>using namespace std;class a {public:virtual int add(int a, int b) {return a + b;}};typedef int (a::* pClassFun)(int, int);int main() {pClassFun pointer = &a::add;a aa;cout << (aa.*pointer)(10, 20);}
一般:
int (*pointer)(int, int); // 声明函数指针
这里,pointer指向的函数类型是int (int, int),即函数的参数是两个int型,返回值也是int型。
注:pointer两端的括号必不可少,如果不写这对括号,则pointer是一个返回值为int* 的函数。
成员函数指针:
int (A::*pf)(int, int); // 声明一个成员函数指针
,这里A::*pf两端的括号也是必不可少的,如果没有这对括号,则pf是一个返回A类数据成员(int型)指针的函数。
注意:和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则。
pf = &A::add;
//正确:必须显式地使用取址运算符(&)
pf = A::add;
// 错误
当我们初始化一个成员函数指针时,其指向了类的某个成员函数,但并没有指定该成员所属的对象——直到使用成员函数指针时,才提供成员所属的对象。
仔细观察后,除了一个有virtual,一个没有virtual,其它的都一样。是的,事实的确如此。
ida动调分析
我把所写的代码,然后生成可执行程序后,拖入ida
进行分析一番
首先分析
非虚函数
看了一下,它俩的汇编代码,主要是经过一个判断跳,如果是虚函数的话,那么它的寻址是比较复杂的(即跳到那个寻址比较多的代码),如果是非虚函数的话,那么它的寻址是超级简单
非虚函数执行完call _main 之后直接去取出 这个成员函数的地址
地址里面直接是函数的主体,毫无疑问,取出来的就是函数地址
虚函数
看了一下反编译代码差不多,找到关键点(同一个位置的判断跳转,只是命名不同),jz short loc_401427
,而这个判断跳转取决于上面取出来的eax
值,接下来动调一下,
虚函数执行完call _main 之后又多执行了几行代码
.text:004013FA mov[ebp+var_10], 1.text:00401401 mov[ebp+var_C], 0.text:00401408 leaeax, [ebp+var_28].text:0040140B mov[esp], eax.text:0040140E call __ZN1aC1Ev ; a::a(void)
然后取出地址的时候,并非直接取,而是又调用了一个函数,进行一系列的操作
call __ZN1aC1Ev
:00410EFC push ebp.text:00410EFD movebp, esp.text:00410EFF moveax, [ebp+arg_0].text:00410F02 movdword ptr [eax], offset off_4469B8.text:00410F08 popebp.text:00410F09 retn
然后取地址也就这一行
movdword ptr [eax], offset off_4469B8
但是我们跟随一下,它是不是函数主体呢?
毫无疑问,不是。。。。所以这里取出来的是虚表的地址,再进一层,所以这个4469B8
是虚表的地址。
才找到函数主体
而且我们要看的是返回值,因为只有返回值才会赋值给我们的函数指针,并非看[eax],而是看eax,
执行完.text:00410EFF mov eax, [ebp+arg_0]
发现eax
里面的值是0x64ff18
,看了一下也就是指针的地址。。
(就是从函数外面把函数指针的地址当做参数传进去,然后把虚表地址 放在指针里面,也就是把它当做虚表指针来使用。。记住,这个指针里面存放的不是函数的地址,而是虚表地址。)然后指针里面的值存放的是虚表的地址,虚表里面又放着虚函数的地址。
edx
里面存放的是虚表的地址,然后再进行取内容,即取到了函数的地址,作为参数传过去。至于这加减操作,也就是虚表里面不一定只存放着一个虚函数的地址。。因为eax存放着偏移+1值,这里eax刚开始是既可以用作判断跳转,又可以当虚表偏移,所以加上eax值后必须自减1,回到正确的偏移
近一步解释
c++代码
#include<iostream>using namespace std;class a {public:virtual int add(int a, int b) {return a + b;}virtual int decrease(int a, int b) {return a - b;}};typedef int (a::* pClassFun)(int, int);int main() {pClassFun pointer = &a::add;a aa;cout << (aa.*pointer)(10, 20);pClassFun pointer1 = &a::decrease;cout << (aa.*pointer1)(20, 10);}
ida调试
加了一个虚函数,加了一个函数指针指向它,接下来我们来看看是不是另外一个虚函数是不是紧接着放在虚表的中的第一个虚函数地址后面。如果是的话,那么另外一个函数指针就直接没用了,它直接通过第一个函数指针值加一个偏移再取内容就ok啦!
第一部分和上面一样。
第二部分是不是没有去再一次寻址了,也就是没有下面这个call
eax存放的是偏移加1值,然后一个指针是4个字节,第一个指针的偏移是0 ~ 3,第二个也就是4 ~ 7,eax存放起始偏移+1,所以也就是5
这里的意思不就是取出虚表地址,再取出函数地址相应的偏移,然后再取一次内容吗?(也就是取函数的地址)
注意
dec
是自减,也就是加上了5,但是偏移是4,所以减1,即第二个指针!!!因为eax存放着偏移+1值,这里eax刚开始是既可以用作判断跳转,又可以当虚表偏移,所以加上eax值后必须自减1,回到正确的偏移