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

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

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

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

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

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

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

    #include <cstdio> #include <stdio.h> #include <stdlib.h> #include <string.h> ...

  2. ASP.NET Core 3.0 一个 jwt 的轻量角色/用户、单个API控制的授权认证库

    目录 说明 一.定义角色.API.用户 二.添加自定义事件 三.注入授权服务和中间件 三.如何设置API的授权 四.添加登录颁发 Token 五.部分说明 六.验证 说明 ASP.NET Core 3 ...

  3. WSL捣鼓记——图形化(以emacs为例)

    前言 这学期开始学习linux,但笔记本装了双系统之后指纹识别会失效,开虚拟机又十分占据内存,于是乎基本需要使用linux的时候就用wsl,可奈何只有命令行界面,在需要使用图形软件(如emacs)的时 ...

  4. SVN命令行笔记

    SVN命令行笔记 近期玩了一下命令行,记录如下. svn info <path> #查看文件,路径信息 svn log <path> #查看文件,路径历史记录 svn st(s ...

  5. LeetCode刷题笔记(1)常用知识点

    1.Integer.parseInt(String s, int radix)方法的作用是:将radix进制的字符串s转化成10进制的int型数字并返回. Integer.valueof(String ...

  6. Rust入坑指南:有条不紊

    随着我们的坑越来越多,越来越大,我们必须要对各种坑进行管理了.Rust为我们提供了一套坑务管理系统,方便大家有条不紊的寻找.管理.填埋自己的各种坑. Rust提供给我们一些管理代码的特性: Packa ...

  7. pymssql默认关闭自动模式开启事务行为浅析

    使用Python采集SQL Server数据库服务器磁盘信息时,遇到了一个错误"CONFIG statement cannot be used inside a user transacti ...

  8. C++学习笔记8_零碎的知识

    1. int main(void) { int arr[] = {1,2,3,4}; //数组的长度 int len = sizeof(arr)/sizeof(int); //由此可以看出,sizeo ...

  9. 「考试」 Or

    不得不说是一道多项式神题了. 虽然说颓代码颓的很厉害不过最终A掉了. 好好讲一讲这道题. 涉及的知识点是:高阶导数,NTT,指数型母函数,泰勒公式,以及意志力和数学推导能力. 那就开始了. 一个测试点 ...

  10. P3097 [USACO13DEC]最优挤奶(线段树优化dp)

    盲猜dp系列... 题意:给定序列,选了i就不能选与i相邻的两个,求最大值,带修改 蒟蒻在考场上10min打完以为只有两种情况的错解...居然能骗一点分... 先讲下当时的思路吧. f[i][0/1] ...