反汇编研究C++虚函数表,调用类内函数,调用虚函数
环境:gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)
测试代码1:
#include <iostream>
#include <stdio.h>
using namespace std;
class A {
public:
virtual void func1() {printf("A func1\n");}
virtual void func2() {printf("A func2\n");}
};
class B :public A {
public:
int i;
virtual void func1() {printf("%d\n",this->i); }
virtual void func2() {printf("B func2\n");}
};
using namespace std;
typedef void(*FUNC)();
int main() {
B b;
b.i = 1;
A* a=&b;
/*FUNC* table = *(FUNC**)(&b);
(*table)();*/
a->func1();
return 0;
}
}
注:std::cout反汇编后不太看得懂,所以使用printf输出
%rbp-0x28处是a的地址所在,可以看到
之后的两个mov (%rax),rax分别将虚函数表的地址推进rax,将虚函数表中的第一个函数也就是B::func1函数的地址推进rax,最后callq *%rax来调用B::func1
记录下当前b的地址
继续运行直到call *%rax前
对%rax所指进行反汇编,可以发现此时rax的确指向B::func1,这也即虚函数的动态查找过程,实现了c++子类父类指针转换后仍然可以调用原本自己的虚函数
推测dynamic_cast进行转换时会更改类内虚函数表指针,而不是一成不变
另外,可以观察到B::fun1中将rdi压入了栈中,并且把rdi偏移地址为8处的4字节压入eax,rdi是64位操作系统&&程序传参时的第一个寄存器,从此可以得出,在调用类内函数时,c++会默认将this指针作为第一个参数传入,从前图的rdi的值也可以直到,rdi的确存着b的地址,也即a
测试代码2:
#include <iostream>
#include <stdio.h>
using namespace std;
class A {
public:
virtual void func1() {printf("A func1\n");}
virtual void func2() {printf("A func2\n");}
};
class B :public A {
public:
int i;
virtual void func1() {printf("%d\n",this->i); }
virtual void func2() {printf("B func2\n");}
};
using namespace std;
typedef void(*FUNC)();
int main() {
B b;
b.i = 1;
A* a=&b;
B b2;
FUNC* table = *(FUNC**)(&b);
(*table)();
/*a->func1();*/
return 0;
}
指向虚函数表的指针被存放在虚类的最开头,这里通过强转直接取到B::func1的地址进行调用
注意:B b2的原因是,在我当前环境下,创建完b后,rdi直到调用(*table)()都未被更改,导致rdi仍然存储着b的地址来调用(*table)也即B::func1,可以输出正确结果1
这里B b2后,实际rdi在调用(*table)()时,实际存着的地址是b2的
可以看到c++先将b2即this指针保存进rdi,然后调用B:::B()默认构造函数,之后rdi都没有改变过
步进到callq *%rax
rdi保存着b2的地址,所以程序继续执行,会输出一个垃圾值b2.i
前8子节为指向虚函数表的指针,预计输出1431652576,实际也是
over