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

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

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

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

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

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

函数指针是函数执行功能的第一条机器指令的地址,这样描述也不让人满意,至少比指向函数的指针具体一些。也就是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. libevent::bufferevent

    #include <cstdio> #include <netinet/in.h> #include <sys/socket.h> #include <fcn ...

  2. perftools::tcmalloc

    安装libunwind wget http://ftp.yzu.edu.tw/nongnu/libunwind/libunwind-1.1.tar.gz ./configure make make i ...

  3. 21.Nginx代理缓存

    1.环境准备 操作系统 应用服务 外网地址 内网地址 CentOS7.6 LB01 10.0.0.5 172.16.1.5 CentOS7.6 Web01 10.0.0.7 172.16.1.7 2. ...

  4. 虚拟机--python环境配置

    1.安装VMware 2.安装Ubantu (1)打开VMware,打开编辑-->首选项,更改虚拟机存储的位置. (2)创建新的虚拟机(自定义): (3)编辑虚拟机--点击CD/VCD--更改成 ...

  5. Java基础系列1:Java面向对象

    该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 概述: Java是面 ...

  6. Vue入坑第一篇

    写在前面的话:文章是个人学习过程中的总结,为方便以后回头在学习.文章中会参考官方文档和其他的一些文章,示例均为亲自编写和实践,若有写的不对的地方欢迎大家和我一起交流. 一.前言 本篇作为vue入门的一 ...

  7. 适用于Windows桌面应用程序的.NET Core 3

    介绍 9月,微软发布了新版.NET Core,用于构建Windows桌面应用程序,包括WPF和Windows Forms.从那时起开发人员可以将传统的nfx桌面应用程序(和控件库)迁移到.NET Co ...

  8. 使用pyquery

    简单举例 from pyquery import PyQuery as pq html = ''' <div> <ul> <li class="item-O&q ...

  9. 在虚拟机上的关于Apache(阿帕奇)(2)开启个人用户主页功能

    首先下载httpd服务 在这里我们主要谈一谈个人主页功能分为不加密和加密两种 不加密 我们先来建立几个用户,使用命令:useradd  longshisan 使用命令:   Passwd  longs ...

  10. lua程序设计(一)

    摘要:lua程序设计第二版学习笔记 脚本语言的基础语法大都比较简单,这里只列举一些lua独有,或者需要特别注意的语法点. 书中前三章的内容是一些惯常的引言,基础数据类型,运算符等内容,相对简单,这里就 ...