c++类成员函数指针_类函数指针-程序员宅基地

技术标签: windows核心编程  

提出疑问

首先问大家一句,什么是函数指针?
肯定有的人会这样回答,函数指针?不就是指向函数地址的一个指针吗?或者就是一个存放着一个函数首地址的变量?
当然,那些有点底层基础的肯定会这样说,函数就是一堆连续的机器码,而函数指针,就是存放了这堆连续机器码首地址的变量。
详细了解函数指针(因为这里主要讲成员函数指针原理):
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 lea     eax, [ebp+var_28]
.text:0040140B mov     [esp], eax
.text:0040140E call    __ZN1aC1Ev                      ; a::a(void)

然后取出地址的时候,并非直接取,而是又调用了一个函数,进行一系列的操作

call __ZN1aC1Ev

:00410EFC push    ebp
.text:00410EFD mov     ebp, esp
.text:00410EFF mov     eax, [ebp+arg_0]
.text:00410F02 mov     dword ptr [eax], offset off_4469B8
.text:00410F08 pop     ebp
.text:00410F09 retn

然后取地址也就这一行

 mov     dword 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,回到正确的偏移

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/CSNN2019/article/details/112837696

智能推荐

Media Player Classic无法解析媒体中的某些流_mpc-be无法渲染筛选图表中的某些pin-程序员宅基地

文章浏览阅读5.5k次。 推荐链接:轻松注册,推荐一个网站获得20元网络账本 —免费的网络记帐本,今天你记账了吗?倍新咨询—投资咨询专家在线炒外汇—免费订阅外汇月刊学不好英语,不是你的错。因为学习内容太枯燥乏味! 为什么不试试《看电影学英语》?问题现象::媒体流 0RealAudio 解码器音频切换器媒体类型 0:--------------------------AM_MEDIA_T_mpc-be无法渲染筛选图表中的某些pin

c语言-背包问题贪心算法_背包问题贪心算法c语言代码-程序员宅基地

文章浏览阅读1.2w次,点赞15次,收藏125次。#include<stdio.h>#define MAX 200typedef struct Solution{ float x[MAX]; //表示该号物品放在多少背包里 int order[MAX];//表示物品的序号,相当其名字}Solution;Solution X;int m=15;//背包容量int n=7;//物品数量int p[]={10,5,15,7,6,18,3};int w[]={2,3,5,7,1,4,1};void Gre._背包问题贪心算法c语言代码

基于ssm高校成绩预警系统 可做毕业设计参考源码免费获取_基于ssm框架的学业预警系统-程序员宅基地

文章浏览阅读1.1k次,点赞32次,收藏22次。作为后期维护运营成本较低的设计模式,MVC也被名正言顺的被我选择进行系统的设计。选用MYSQL数据库存储数据,可以使多个客户端同时访问软件读取数据库数据,针对多线程的访问可以使用Archive,首先将存储的数据完成动态划分,之后存放到数据库相应的表中,这种划分方式直接提高了数据库数据管理的速度,读取数据库的数据库非常灵活高效,同时Mysql数据库是开源的,可以完成二次开发,很大程度的节省了使用者的成本,和开发语言结合实现一个完美的网站或者系统,综合MYSQL数据库的各个方面,目前是许多开发者的首选。_基于ssm框架的学业预警系统

Js 给JSON对象排序_json有没有顺序-程序员宅基地

文章浏览阅读3.7k次。众所周知,json对象是没有顺序的。只有数组才有排序功能。但我们遇到的业务场景里面,不仅仅需要对数组排序,也有需要对对象排序的情况。例如下面这种数据:let data = {zhangsan: {age: 18, height: 189}, lisi: {age: 18, height: 175}}此时如果对这种数据排序。可以这样写:let data = {zhangsan: {age: 18, height: 189}, lisi: {age: 18, height: 175_json有没有顺序

深入了解Linux: dbus-daemon系统总线的作用与管理_system_bus_socket dbus-deamon-程序员宅基地

文章浏览阅读479次,点赞3次,收藏4次。是Linux和其他类Unix系统中的消息总线系统,它允许不同的程序(通常是进程)进行相互通信。提供了两种类型的消息总线,一种是系统总线(system bus),另一种是会话总线(session bus)。系统总线用于系统级别的消息传递,而会话总线用于用户级别的消息传递。当你运行命令时,你是在启动系统总线。系统总线用于所有用户和系统服务之间的通信,并且它是在系统启动时由系统初始化脚本启动的。这个总线用于那些需要广播或监听整个系统事件的服务,比如硬件添加、网络状态变化等。这里是一些关于。_system_bus_socket dbus-deamon

SOCKET_ec20 多路socket连接-程序员宅基地

文章浏览阅读347次。1) IPv4套接字地址结构IPv4套接字地址结构通常也称为“网际套接字地址结构”,以sockaddr_in命名,定义在头文件中。 struct in_addr{ in_addr_t s_addr; //32bits IPv4 address,network byte ordered };_ec20 多路socket连接

随便推点

idea 如何定位类或方法_idea定位类方法-程序员宅基地

文章浏览阅读1w次。使用maven 打包报错如下:法一:Ctrl+ N,打开搜索输入类名 UserInfoServiceImpl点击 UserInfoServiceImpl 类,根据报警提示找到第32行约第44个字符处找到方法 getUserInfo4OpenApi,按住Ctrl点进去点击去之后,会发现getUserInfo4OpenApi 这个方法继承自UserService 类。如..._idea定位类方法

内容领先地位无法撼动,腾讯音乐与环球续约将共建新厂牌-程序员宅基地

文章浏览阅读243次。近日,腾讯音乐娱乐集团(NYSE:TME)发布了2020年Q2财报,延续了Q1的增长势头。数据显示,2020年Q2季度,腾讯音乐总营收同比增长17.5%达69.3亿元,调整后净利润11.5亿元,在线音乐付费用户更是强劲增长达4710万,同比创纪录增长超51.9%。亮眼成绩单的背后,腾讯音乐的内容领先优势持续强化。在财报发布当天,腾讯音乐与全球领先的音乐娱乐公司环球音乐集团(UMG)共同重磅宣布:基于长期不断深化的紧密合作伙伴关系,双方续签数年期版权授权战略合作协议,同时在该合作协议下,双方还宣.

axure和蓝湖上查看页面的说明和上传文件_axure pr 上传蓝湖-程序员宅基地

文章浏览阅读299次。在原型上添加说明。_axure pr 上传蓝湖

一道简单的 Java 笔试题,但值得很多人反思!-程序员宅基地

文章浏览阅读388次。2019独角兽企业重金招聘Python工程师标准>>> ..._依次插入元素进入list并改变这个list内容要求任何时候这个list都是有序的

生成模型与辨别模型(Generative vs discriminative model)_鉴别模型-程序员宅基地

文章浏览阅读1k次。生成模型与辨别模型(Generative vs discriminative model)这两个模型一般是讲的分类问题生成模型1、使用贝叶斯理论推断后验分布p(Ck|x)p(Ck|x)p(C_k|x),需要考虑先验分布p(Ck)p(Ck)p(C_k)和p(X|Ck)p(X|Ck)p(X|C_k) 也可以对联合分布p(X,Ck)p(X,Ck)p(X,C_k)建模 2、使用决策论对x分..._鉴别模型

单片机学习:lwip-udp_lwip udp_bind-程序员宅基地

文章浏览阅读2.5k次。UDP编程虽然不难,但是有很多不懂得地方。 第一个就是UDP的块,udp_pcb主要是记录udp的信息,如本地IP,端口号,远程IP,远程端口号,recv函数,和自定义参数args。 一个程序中一般有多个控制块,这些控制块通过next指针连接在一起,但接收到一个数据块时就会遍历这些udp_pcb控制块,找到符合的控制块,接着调用recv中的回调函数进行处理。第二个就是一些操作函数。第一个就是_lwip udp_bind