函数指针是什么,可能会答指向函数的指针。

成员函数指针是什么,答指向成员函数的指针。

成员函数指针和函数指针有什么不同?

虚函数指针和非虚成员函数指针有什么不同?

你真正了解成员函数指针了吗?

本篇带你看一看反汇编中,成员函数指针的实体,以及运作机理,与函数指针到底有什么不同。

函数指针是函数执行功能的第一条机器指令的地址,这样描述也不让人满意,至少比指向函数的指针具体一些。也就是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平台)的更多相关文章

  1. 从汇编看c++成员函数指针(三)

    前面的从汇编看c++中成员函数指针(一)和从汇编看c++成员函数指针(二)讨论的要么是单一类,要么是普通的多重继承,没有讨论虚拟继承,下面就来看一看,当引入虚拟继承之后,成员函数指针会有什么变化. 下 ...

  2. 从汇编看c++成员函数指针(二)

    下面先看一段c++源码: #include <cstdio> using namespace std; class X { public: virtual int get1() { ; } ...

  3. C/C++ 不带参数的回调函数 与 带参数的回调函数 函数指针数组 例子

    先来不带参数的回调函数例子 #include <iostream> #include <windows.h> void printFunc() { std::cout<& ...

  4. C\C++语言重点——指针篇 | 为什么指针被誉为 C 语言灵魂?(一文让你完全搞懂指针)

    本篇文章来自小北学长的公众号,仅做学习使用,部分内容做了适当理解性修改和添加了博主的个人经历. 注:这篇文章好好看完一定会让你掌握好指针的本质! 看到标题有没有想到什么? 是的,这一篇的文章主题是「指 ...

  5. C/C++中带可变参数的函数

    1.带可变参数的函数由来 当函数中的参数个数不确定时,这时候就需要带可变参数的函数! 如我们经常使用的C库函数printf()实际就是一个可变参数的函数, 其原型为: int printf( cons ...

  6. 带参数的main函数

    带参数的main函数 int main(int argc,char **argv)  或int main(int argc,char *argv[])  /*解析 依据<C程序设计语言(第二版. ...

  7. 利用反汇编手段解析C语言函数

    1.问题的提出函数是 C语言中的重要概念.利用好函数能够充分利用系统库的功能写出模块独立.易于维护和修改的程序.函数并不是 C 语言独有的概念,其他语言中的方法.过程等本质上都是函数.可见函数在教学中 ...

  8. [从jQuery看JavaScript]-匿名函数与闭包(Anonymous Function and Closure)【转】

    (function(){ //这里忽略jQuery所有实现 })(); 半年前初次接触jQuery的时候,我也像其他人一样很兴奋地想看看源码是什么样的.然而,在看到源码的第一眼,我就迷糊了.为什么只有 ...

  9. C语言带参数的main函数

    C语言带参数的main函数 #include<stdio.h> int main(int argc,char*argv[]) { int i; ;i<argc;i++) printf ...

随机推荐

  1. 全面认识nslookup命令及子命令

  2. google hack 语法

    google hack a b c 自动对词进行拆分匹配 拆分标准 空格 "a b c " 把a b c 当成一个整体去查 " a*b" *通配符 里面是一个或 ...

  3. Markdown的基本使用指南

    目录 1.标题 2.列表 2.1无序列表 2.1有序列表 3.引用 4.图片和链接 5.粗体和斜体 6.分割线 7.代码框和代码块 8.列表 9.可选框 10.添加目录 Markdown 是一种用来写 ...

  4. ubuntu 虚拟机设置静态ip

    $ sudo vim /etc/network/interfaces auto ens33   # 使用的网络接口,之前查询接口是为了这里     iface ens33 inet static    ...

  5. IntelliJ IDEA 2019.2最新版本免费激活码

    IntelliJ IDEA 2019.2最新版本免费激活码 支持IDEA所有版本 正版授权激活码 今天更新了一下,支持java13等新功能.下面是激活码 812LFWMRSH-eyJsaWNlbnNl ...

  6. 使用 git bisect 定位你的 BUG

    Git 是开发者的好帮手,今天跟大家分享的是用 git bisect 来找到你代码中的 bad commit . 背景 你可能遇到过这种情况, 昨天下班前把模块开发完了, 单元测试验证通过, git ...

  7. CVE-2019-16097:Harbor任意管理员注册漏洞复现

    0x00 Harbor简介 Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器,通过添加一些企业必需的功能特性,例如安全.标识和管理等,扩展了开源Docker Distri ...

  8. MySQL字符集与排序规则总结

      字符集与排序规则概念 在数据库当中都有字符集和排序规则的概念, 很多开发人员甚至包括有些DBA都会将这个混淆,当然这个情况也有一些情有可原的原因.一来两者本来就是相辅相成,相互依赖关联: 另外一方 ...

  9. [py2neo]Ubuntu14 安装py2neo失败问题解决

    环境 1.操作系统Ubuntu14 2.py2neo版本4.1 3.python版本python3.4 问题 pip install py2neo==4.1  安装失败,提示: Cannot unin ...

  10. PyQt图形化布局

    安装PyQt第三方库 pip install PyQt5 安装Qt Designer(Qt的布局工具) pip install PyQt5-tools PyChram设置Qt工具 配置Qt Desig ...