反汇编研究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 ");} virtual void func2() { printf("A func2 ");} }; class B :public A { public: int i; virtual void func1() { printf("%d ",this->i); } virtual void func2() { printf("B func2 ");} }; 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 ");} virtual void func2() { printf("A func2 ");} }; class B :public A { public: int i; virtual void func1() { printf("%d ",this->i); } virtual void func2() { printf("B func2 ");} }; 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