通过虚函数表调用虚函数与通过虚函数表(绕过访问权限控制)

一、背景知识

          在讲解虚函数的时候,我们知道,如果类中有虚函数,则该类中存在一个虚函数表(V-Table),每个该类的对象都会有一个指针指向该虚函数表,存储这类中虚函数的函数指针,而虚函数表的地址就存在该类对象内存的开始处,目的是为了方便查找虚函数。

        在陈浩的技术专栏中写过一篇对C++虚函数表解析很透彻的的文章: C++ 虚函数表解析   

        这篇文章里对虚函数表的结构做了很透彻的解析,并且配有直观的图片讲解,大家看了以后会比较明白的。这里我们着重讲下通过虚函数表调用虚函数与通过虚函数表(绕过访问权限控制)的代码实现。

二、通过虚函数表调用虚函数

在陈浩大哥的那篇文章里面提到如何通过虚函数表调用函数,并有示例代码如下:

#include<iostream>
#include<string>
using namespace std;

typedef void(*Fun)(void);

class Base {
 public:
  virtual void f() {
    cout << "Base::f()" << endl;
  }
  virtual void g() {
    cout << "Base::g()" << endl;
  }
  virtual void h() {
    cout << "Base::h()" << endl;
  }
};
上面是基类代码,Base类中依次有三个虚函数: f(), g(), h(),它们的指针也被依次存储在Base类的V-Table中。下面代码尝试通过V-Table中的虚函数指针来调用虚函数:

int main() {
  Base b;
  Fun fp = NULL;
  cout << "虚函数表地址:" << (int*)(&b) << endl;
  cout << "虚函数表第一个虚函数指针地址:" <<(int*)*(int*)(&b) << endl;
  for (int i = 0; i != 3; ++i) {
    fp = (Fun)*((int*)*(int*)(&b) + i);
    fp();
  }
  return 0;
}

我在自己电脑(64位系统)上运行上述测试代码时,运行结果如下:

虚函数表地址:0x7fff13ec2c80
虚函数表第一个虚函数指针的地址:0x400c70
Base::f()
Segmentation fault

通过虚函数表获取到虚函数的指针,然后调用虚函数执行,由结果看出,第一个虚函数被正确调用了,二在获取第二个虚函数指针时,发生了段错误。

为什么会出现这种情况呢? 经过认真分析发现,是由于忽略了运行环境导致的,原因如下:

陈浩大哥给出的示例代码是在32位系统上运行的,我现在使用的是64位的系统,那么32位系统和64为系统有什么区别呢? 指针就是地址号,所以指针size的大小和编址地址位数一致。32位系统中虚拟地址有32位,所以地址也是32位的,因此在32位系统下,指针也是32位的,即4个字节,因此可以把指针转换为(int*);同理,在64位系统下,指针就是64位的,即8个字节,这时再将指针数组转换为(int*)就会出现问题,int*在做+1偏移的时候是偏移一个int的大小的长度,在64位系统中,int仍然是32位的,这个32位系统中一致。因此,在64位系统下,把指针转数组化为long*,如:

long lo[] = {1.0, 2.0, 3.0};
long* p_L = &lo;
cout << *p_L;  // 输出lo[0] == 1.0  
cout << *++p_L;  //输出lo[0] == 2.0
这个时候,每次偏移的地址大小是8个字节,刚好和指针大小相同;如果把指针数组转换为int*, 则每次+1偏移4个字节,就会出现段错误,因为便宜8个字节才是下一个指针的地址。 另外,虚函数表的地址和虚函数表中第一个虚函数指针的地址应该是相同的,因为这就是数组地址和数组首元素地址一样道理。因为(int*)(&b)得到的仅是虚函数表地址的地址。

int main() {
  Base b;
  Fun fp = NULL;
  cout << "虚函数表地址:" << (long*)*(long*)(&b) << endl;
  cout << "虚函数表第一个虚函数指针地址:" <<(long*)*(long*)(&b) << endl;
  for (int i = 0; i != 3; ++i) {
    fp = (Fun)*((long*)*(long*)(&b) + i);
    fp();
  }
  return 0;
}
运行结果如下:
虚函数表地址:0x400c70
虚函数表第一个虚函数指针地址:0x400c70
Base::f()
Base::g()
Base::h()

上述代码可能有点混乱,不那么容易理解,现在我们来逐步分解下:

int main() {
  Base b;
  Fun fp = NULL;
  long* v_table_addr_addr = (long*)&b;
  long* v_table_addr =(long*)*v_table_addr_addr;
  cout << "虚函数表的地址:" << v_table_addr << endl;
  long* first_v_func_ptr_addr = v_table_addr;
  cout << "虚函数表第一个虚函数指针的地址:" << first_v_func_ptr_addr << endl;
  fp = (Fun)*first_v_func_ptr_addr;
  fp();
  for (int i = 0; i != 3; ++i) {
    fp = (Fun)*(v_table_addr + i); 
    fp();
  }
  return 0;
}

之前我们说过,如果一个类中有虚函数,那么这个类就会有一个虚函数表,这个类的每个对象都有一个指向该表的指针,并且保存在对象内存地址的最开始处。

  long* v_table_addr_addr = (long*)&b;
&b取得对象的地址,(long*)转换为指针的地址,这个地址中存储的就是虚函数表的地址。

   

 long* v_table_addr =(long*)*v_table_addr_addr;

对上述地址解引用*v_table_addr_addr;就可以得到虚函数表的地址。

也就是虚函数表首元素的地址:  long* first_v_func_ptr_addr = v_table_addr;

然后对它解引用,并将其转换为函数指针: fp = (Fun)*first_v_func_ptr_addr;

得到函数指针后,就可以想普通的函数一样调用它了: fp();

  for (int i = 0; i != 3; ++i) {
    fp = (Fun)*(v_table_addr + i); 
    fp();
  }

上述代码是通过for循环 ,依次调用虚函数表里的虚函数,它和访问数组元素一样:

v_table_addr为指针数组(虚函数表)首地址,通过+i每次加1指向下一个函数指针。这里因为我们把v_table_addr 定义为了long*类型,它和指针类型一样都是占8个字节,因此,每次刚好便宜8个字节的内存空间,指向下一个函数指针。

运行结果如下:

虚函数表的地址:0x400c90
虚函数表第一个虚函数指针的地址:0x400c90
Base::f()
Base::f()
Base::g()
Base::h()

更加简练的调用写法:

int main() {
Base b;
long** p = (long**)&b;  //(long*)&b指向v_table, (long**)&b则指向v_table的第一个元素
for(int i = 0; i !=3; ++i){
((Fun)*(*p+i))();  // *p取到v_table的地址, *p+i 指到v_table第i个元素,*(*p+i)取出第i个函数指针,((Fun)*(*p+i))(); 转换并调用。
}
return 0;  
}
运行结果:

Base::f()
Base::g()
Base::h()



三、通过虚函数表绕过访问权限控制

我们可以将虚函数定义为private类型,这样在类外面就不能通过对象调用private类型的函数了。

但是在类的虚函数表中,不论其访问类型是public还是private的,其所有虚函数都会存放其中。

如此以来,我们便可以绕过对象的访问权限控制,不通过对象,而是通过类的虚函数表获取相应的函数指针,调用该函数。

class Base {
 public:
  virtual void f() {
    cout << "Base::f()" << endl;
  }
  virtual void g() {
    cout << "Base::g()" << endl;
  }
private:
  virtual void h() {
    cout << "Base::h()" << endl;
  }
};
对于类Base的对象b:

Base b;
b.f();  
b.g();
b.h();  // error! private!
我们可以通过b对象,直接调用f()和g(),但是不能调用h(),因为h()是private,除了类内部和其友元外,别的地方不能直接对它调用。

但是我们通过其类的虚函数表,可以绕过其访问控制权限,调用private类型的虚函数。非虚函数是不行的,因为虚函数表中只有虚函数的指针。

示例代码:

int main() {
  Base b;
  b.f();
  b.g();
  Fun fp = NULL;
  fp = (Fun)*((long*)*(long*)(&b) + 2);
  fp();
  //下面是简练一点的写法
  long** p = (long**)&b;  //(long*)&b指向v_table, (long**)&b则指向v_table的第一个元素
  ((Fun)*(*p+2))();  // *p取到v_table的地址, *p+2 指到v_table第三个元素,*(*p+2)取出第三个函数指针,(Fun)*(*p+2)(); 转换并调用。
  return 0;
}
运行结果:

Base::f()
Base::g()
Base::h()
Base::h()



                原创文章,转载请注明: 转载自  

IIcyZhao's Road

          本文链接地址:  http://blog.csdn.net/iicy266/article/details/11906807

你可能感兴趣的:(C++,虚函数,指针)