函数指针和成员函数指针有什么不同,反汇编带看清成员函数指针的本尊(gcc@x64平台)
函数指针是什么,可能会答指向函数的指针。
成员函数指针是什么,答指向成员函数的指针。
成员函数指针和函数指针有什么不同?
虚函数指针和非虚成员函数指针有什么不同?
你真正了解成员函数指针了吗?
本篇带你看一看反汇编中,成员函数指针的实体,以及运作机理,与函数指针到底有什么不同。
函数指针是函数执行功能的第一条机器指令的地址,这样描述也不让人满意,至少比指向函数的指针具体一些。也就是call指令的地址操作数。那么成员函数指针也应该指向一条执行指令的地址。但是其实成员函数指针它是一个trunk。
下面我们通过两个类来进行分析,父类obj以及子类objobj。我们定义成员函数指针和通过成员函数指针来调用成员函数观察真相。
struct point {float x,y;};
struct obj
{
virtual ~obj {}
void foo(int) {}
void foo(point) {}
virtual void vfoo() {}
};
struct objobj : public obj
{
virtual ~objobj {}
virtual void vfoo() {}
};
void main()
{
obj o;
objobj oo;
void* pofp = (void*) (void(obj::*)(point))&obj::foo;
void(obj::*pi)(int) = &obj::foo;
void(obj::*pp)(point) = &obj::foo;
void(objobj::*vp)() = &objobj::vfoo;
NOOP
((&oo)->*vp)();
NOOP
((&oo)->*pi)();
NOOP
((&o)->*pp)(pt);
}
相信大家很轻松就知道这里有4个指针变量 pofp, pi, pp, vp, 分别指向obj::foo或objobj::vfoo函数的执行地址。通过‘*’号引用地址就可以调用函数的代码。其中有一个不是函数指针类型,但只要进行类型转换就可以调用到函数的代码了。
究竟事实是这样吗?
gcc对本例的void* pofp = (void*) (void(obj::*)(point))&obj::foo;只作出警告并且可以编译,然而在vc中是错误不能通过编译的。因为将成员函数指针转换成其它类型的指针是被禁止的。
为什么vc要禁止这种转换呢,原因是成员函数指针不是一个单纯指向函数执行代码的地址的指针。先来看上面成员函数指针定义所对应的反汇编代码:
0x0000000000400975 <+>: movq $0x400c60,-0x18(%rbp) # void* pofp = (void*) (void(obj::*)(point))&obj::foo;
0x000000000040097d <+>: movq $0x400bde,-0x80(%rbp)
0x0000000000400985 <+>: movq $0x0,-0x78(%rbp) # void(obj::*pi)(int) = &obj::foo;
0x000000000040098d <+>: movq $0x400c60,-0x90(%rbp)
0x0000000000400998 <+>: movq $0x0,-0x88(%rbp) # void(obj::*pp)(point) = &obj::foo;
0x00000000004009a3 <+>: movq $0x11,-0xa0(%rbp)
0x00000000004009ae <+>: movq $0x0,-0x98(%rbp) # void(objobj::*vp)() = &objobj::vfoo;
可以看到除了void* pofp是一个8字节长的指针外, pi, pp, vp都是一个有两个8字节长成员变量的结构体。而且vp中并没有存放代码地址。这是怎么一回事呢?
这个成员函数指针到底是怎么样运作的,请看下面对通过成员函数指针调用函数的代码的反汇编:
((&oo)->*vp)();
0x00000000004009ba <main+>: mov -0xa0(%rbp),%rax
0x00000000004009c1 <main+>: and $0x1,%eax
0x00000000004009c4 <main+>: test %al,%al
0x00000000004009c6 <main+>: je 0x4009f6 <main+>
0x00000000004009c8 <main+>: mov -0x98(%rbp),%rax
0x00000000004009cf <main+>: mov %rax,%rdx
0x00000000004009d2 <main+>: lea -0x160(%rbp),%rax
0x00000000004009d9 <main+>: add %rdx,%rax
0x00000000004009dc <main+>: mov (%rax),%rdx
0x00000000004009df <main+>: mov -0xa0(%rbp),%rax
0x00000000004009e6 <main+>: sub $0x1,%rax
0x00000000004009ea <main+>: lea (%rdx,%rax,),%rax
=> 0x00000000004009ee <main+>: mov (%rax),%rax
0x00000000004009f1 <main+>: mov %rax,%rdx
0x00000000004009f4 <main+>: jmp 0x4009fd <main+>
0x00000000004009f6 <main+>: mov -0xa0(%rbp),%rdx
0x00000000004009fd <main+>: mov -0x98(%rbp),%rax
0x0000000000400a04 <main+>: mov %rax,%rcx
0x0000000000400a07 <main+>: lea -0x160(%rbp),%rax
0x0000000000400a0e <main+>: add %rcx,%rax
0x0000000000400a11 <main+>: mov %rax,%rdi
0x0000000000400a14 <main+>: callq *%rdx
((&oo)->*pi)();
0x0000000000400a17 <main+>: mov -0x80(%rbp),%rax ;-0x80(%rbp)和-0x78(%rbp)一起存放成员函数指针信息
0x0000000000400a1b <main+>: and $0x1,%eax
0x0000000000400a1e <main+>: test %al,%al
0x0000000000400a20 <main+>: je 0x400a4a <main+>
0x0000000000400a22 <main+>: mov -0x78(%rbp),%rax
0x0000000000400a26 <main+>: mov %rax,%rdx
0x0000000000400a29 <main+>: lea -0x160(%rbp),%rax ;-0x160(%rbp)是类对象在局部范围的位置
0x0000000000400a30 <main+>: add %rdx,%rax ;找出(多)继承类中正确的位置
0x0000000000400a33 <main+>: mov (%rax),%rdx ;取出虚表
0x0000000000400a36 <main+>: mov -0x80(%rbp),%rax
0x0000000000400a3a <main+>: sub $0x1,%rax
0x0000000000400a3e <main+>: lea (%rdx,%rax,),%rax
0x0000000000400a42 <main+>: mov (%rax),%rax
0x0000000000400a45 <main+>: mov %rax,%rdx
0x0000000000400a48 <main+>: jmp 0x400a4e <main+>
0x0000000000400a4a <main+>: mov -0x80(%rbp),%rdx
0x0000000000400a4e <main+>: mov -0x78(%rbp),%rax
0x0000000000400a52 <main+>: mov %rax,%rcx
0x0000000000400a55 <main+>: lea -0x160(%rbp),%rax
0x0000000000400a5c <main+>: add %rcx,%rax
0x0000000000400a5f <main+>: mov $0x1,%esi
0x0000000000400a64 <main+>: mov %rax,%rdi
0x0000000000400a67 <main+>: callq *%rdx
可以看到在call真正的执行地址之前都有一段相同的处理,这段相同的代码就是成员函数指针在幕后做的处理。
大至逻辑为
1.区分是不是虚函数;
2.找出正确的this位置,初始化this参数;
3.虚函数的话,找出正确的this位置,取出正确的虚函数表。
4.最后才能正确地调用正确的成员函数。
我逆向这段处理的伪代码如下:
struct trunk{
int64 off_poly; // 两个成员的位置应该互换一下 Z.@20170214修正
int64 off_func;
};
obj obj;
trunk trunk;
void* f;
if(trunk.off_func & )
{
// a virtual function trunk;
void** poly = (char*)&obj + trunk.off_poly;
void** vtable = (void**)*poly;
vtable = (char*)vtable + (trunk.off_func - );
f = *vtable;
}
else
{
// a non-virt function trunk;
f = (void*)trunk.off_func;
}
poly* poly = (char*)&obj + trunk.off_poly;
poly->f();
对于多重继承必须要找出正确的this位置。所以成员函数指针并不能像函数指针那样只是一个指向函数的指针,而需要一个trunk。
下面是objobj和obj的虚函数表的分布:
vtable of objobj
0x400f30 <_ZTV6objobj+>: 0x0000000000400cce 0x0000000000400d08
0x400f40 <_ZTV6objobj+>: 0x0000000000400d2e (gdb) i sy 0x0000000000400cce
objobj::~objobj() in section .text of a.out
(gdb) i sy 0x0000000000400d08
objobj::~objobj() in section .text of a.out
(gdb) i sy 0x0000000000400d2e
objobj::vfoo() in section .text of a.out vtable of obj
0x400f70 <_ZTV3obj+>: 0x0000000000400b8a 0x0000000000400bb8 # obj::~obj(), obj::~obj()
0x400f80 <_ZTV3obj+>: 0x0000000000400ca6 # obj::vfoo()
如果你还没有离开,并且足够细心的话,你就会发现obj和objobj只有两个虚函数dtor和vfoo,怎么会虚函数表中会有三个函数?
请留意下一篇,在析构函数中调用虚函数会发生什么?
Z.@20170214 补充:
当定义一个空的成员指针时,汇编代码并非直接定义一个指针放进0,而是实例一个成员指针结构,并将结构内的两个成员变量赋值为0。
函数指针和成员函数指针有什么不同,反汇编带看清成员函数指针的本尊(gcc@x64平台)的更多相关文章
- 从汇编看c++成员函数指针(三)
前面的从汇编看c++中成员函数指针(一)和从汇编看c++成员函数指针(二)讨论的要么是单一类,要么是普通的多重继承,没有讨论虚拟继承,下面就来看一看,当引入虚拟继承之后,成员函数指针会有什么变化. 下 ...
- 从汇编看c++成员函数指针(二)
下面先看一段c++源码: #include <cstdio> using namespace std; class X { public: virtual int get1() { ; } ...
- C/C++ 不带参数的回调函数 与 带参数的回调函数 函数指针数组 例子
先来不带参数的回调函数例子 #include <iostream> #include <windows.h> void printFunc() { std::cout<& ...
- C\C++语言重点——指针篇 | 为什么指针被誉为 C 语言灵魂?(一文让你完全搞懂指针)
本篇文章来自小北学长的公众号,仅做学习使用,部分内容做了适当理解性修改和添加了博主的个人经历. 注:这篇文章好好看完一定会让你掌握好指针的本质! 看到标题有没有想到什么? 是的,这一篇的文章主题是「指 ...
- C/C++中带可变参数的函数
1.带可变参数的函数由来 当函数中的参数个数不确定时,这时候就需要带可变参数的函数! 如我们经常使用的C库函数printf()实际就是一个可变参数的函数, 其原型为: int printf( cons ...
- 带参数的main函数
带参数的main函数 int main(int argc,char **argv) 或int main(int argc,char *argv[]) /*解析 依据<C程序设计语言(第二版. ...
- 利用反汇编手段解析C语言函数
1.问题的提出函数是 C语言中的重要概念.利用好函数能够充分利用系统库的功能写出模块独立.易于维护和修改的程序.函数并不是 C 语言独有的概念,其他语言中的方法.过程等本质上都是函数.可见函数在教学中 ...
- [从jQuery看JavaScript]-匿名函数与闭包(Anonymous Function and Closure)【转】
(function(){ //这里忽略jQuery所有实现 })(); 半年前初次接触jQuery的时候,我也像其他人一样很兴奋地想看看源码是什么样的.然而,在看到源码的第一眼,我就迷糊了.为什么只有 ...
- C语言带参数的main函数
C语言带参数的main函数 #include<stdio.h> int main(int argc,char*argv[]) { int i; ;i<argc;i++) printf ...
随机推荐
- boost::multi_index 多索引容器
#include "stdafx.h" #include <string> #include <boost/multi_index_container.hpp&g ...
- vue内使用echarts
18年下班年用的vue + echarts,现在才想起来总结,着实不敬业 线上的项目叫股往(http://rich.xchol.com/#/) 好了,进入正题: 首先,需要新建一个vue的项目,在vu ...
- vmware14安装centos7的步骤(图文详解)
一.centos的安装 centos分为桌面版和黑屏版(命令行版):在这里我使用的是命令行版. 这里选择安装程序光盘映像文件,文件就是centos7的iso文件. 虚拟机的名称和位置自行设置; 虚拟机 ...
- Bugku SQL注入2的思考
网络安全初学者,欢迎评论交流学习,若内容中有错误欢迎各位指正. 题目地址:http://123.206.87.240:8007/web2/ 题目提示:都过滤了绝望吗?,提示 !,!=,=,+,-,^, ...
- FileZilla Server超详细配置
FileZilla Server下载安装完成后,必须启动软件进行设置,由于此软件是英文,本来就是一款陌生的软件,再加上英文(注:本站提供中文版本,请点击下载),配置难度可想而知,站长从网上找到一篇非常 ...
- e.target与事件委托简例(原生和jQuery的区别)
target定义(英译:目标,目的): target 事件属性可返回事件的目标节点(触发该事件的节点),如生成事件的元素.文档或窗口. 语法: event.target event.target.no ...
- Hystrix dashboard - Unable to connect to Command Metric Stream.
在使用boot 2.0.*以上版本 + cloud Finchley.RELEASE 查看仪表盘的时候会报错 Unable to connect to Command Metric Stream &l ...
- 6G仅仅是比5G多1G吗??
第六代移动通信系统(6th generation mobile networks,或6th generation wireless systems),简称6G,是指第六代移动通信技术,是5G系统后的延 ...
- ubuntu18.04 flink-1.9.0 Standalone集群搭建
集群规划 Master JobManager Standby JobManager Task Manager Zookeeper flink01 √ √ flink02 √ √ flink03 √ √ ...
- 【Java必修课】通过Value获取Map中的键值Key的四种方法
1 简介 我们都知道Map是存放键值对<Key,Value>的容器,知道了Key值,使用方法Map.get(key)能快速获取Value值.然而,有的时候我们需要反过来获取,知道Value ...