函数指针和成员函数指针有什么不同,反汇编带看清成员函数指针的本尊(gcc@x64平台)

时间:2023-12-11 23:30:56

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

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

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

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

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

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

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