阿里笔试题-修改虚函数表指针

https://www.zhihu.com/question/29256578/answer/43725188

一道阿里实习生笔试题的疑惑?

问题:
#include <iostream> using namespace std; class animal { protected: int age; public: virtual void print_age(void) = 0; }; class dog : public animal { public: dog() {this -> age = 2;} ~dog() { } virtual void print_age(void) {cout<<"Wang, my age = "<<this -> age<<endl;} }; class cat: public animal { public: cat() {this -> age = 1;} ~cat() { } virtual void print_age(void) {cout<<"Miao, my age = "<<this -> age<<endl;} }; int main(void) { cat kitty; dog jd; animal * pa; int * p = (int *)(&kitty); int * q = (int *)(&jd); p[0] = q[0]; pa = &kitty; pa -> print_age(); return 0; }
输出是:
Wang, my age = 1

今天线上笔试遇到的一道题,很好奇,这几句:
int * p = (int *)(&kitty);
int * q = (int *)(&jd);
p[0] = q[0];

这是为什么呢?
--------------------------------------------------------------------------------
确实是:
p[0] = q[0];
我没有记错。

当时读题时,看到 基类,派生类,虚函数,,我就猜到肯定是要考 多态,虚函数表这些知识,
当时就是对这句挺疑惑:
p[0] = q[0];
因为这句可能会修改虚函数表,但也不一定阿,因为时间比较紧,就赌了一把,认为虚函数表被修改了。
所以笔试完了,就挺困惑,宿舍熄灯后,怎么也睡不着,就拿到知乎上,想问问各位。

说实话,真的很感谢 @蓝色 大大,这么晚还回答了

这个是昨天笔试完,晚上11点左右提的,不能算我笔试违规吧? T_T

好吧,我也不匿名了,感觉匿名不好,不懂就问,为啥要匿名。
我不是 陈浩大大,只是个普通院校 大三 计算机科班生。
 
解答:
作者:RednaxelaFX
链接:https://www.zhihu.com/question/29256578/answer/43725188
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

首先

大大的答案已经把思路说得很清楚了。这是个hack,不是C++语言规范所保证的行为,而是某些C++编译器采用的C++ ABI的行为。

 

放个传送门:为什么bs虚函数表的地址(int*)(&bs)与虚函数地址(int*)*(int*)(&bs) 不是同一个? - RednaxelaFX 的回答

其次,这代码不但依赖某些C++编译器的行为,还依赖平台的指针宽度是32位。
int * p = (int *)(&kitty);
int * q = (int *)(&jd);
p[0] = q[0];
这几句不应该用int*,而应该用intptr_t*才对。这样才能保证拷贝的是一个指针宽度的数据,而不是一个int宽度的数据。
  • 在32位平台上,int通常是32位,而指针是32位,所以正好匹配了,程序能正常运行;
  • 在64位平台上,如果是流行的LP64模型,int是32位而指针是64位,这里实际上只拷贝了指针的一半,程序能否正常运行就看运气了。

如果是在一个64位且小端(little endian)的平台上,那这代码拷贝的是指针的低32位。很可能会运气好能正常运行,因为dog类与cat类的vtable可能正好在内存里处于很近的位置,它们的地址的高32位可能正好相同,地址不同的地方都在低32位,这样这个程序就运气好能正常运行。
如果是在一个64位且大端(big endian)的平台上,那这段代码拷贝的是指针的高32位,那就完全达不到效果了。

不知道谁出的这种题⋯
或者题主把题目的细节记错了。
后面有回答说原本的笔试题不是p[0] = q[0],而是p[1] = q[1]。如果是这样的话那仍然只能在32位平台上能行,在64位平台上就纱布了。

再次,这种题还有很多玩法。例如说一种简单的玩法是像这样:
#include <iostream>
using namespace std;

class animal
{
protected:
  int age_;
  animal(int age): age_(age) { }

public:
  virtual void print_age(void) = 0;
  virtual void print_kind() = 0;
  virtual void print_status() = 0;
};

class dog : public animal
{
public:
  dog(): animal(2) { }
  ~dog() { }

  virtual void print_age(void) {
    cout << "Woof, my age = " << age_ << endl;
  }

  virtual void print_kind() {
    cout << "I'm a dog" << endl;
  }

  virtual void print_status() {
    cout << "I'm barking" << endl;
  }
};

class cat : public animal
{
public:
  cat(): animal(1) { }
  ~cat() { }

  virtual void print_age(void) {
    cout << "Meow, my age = " << age_ << endl;
  }

  virtual void print_kind() {
    cout << "I'm a cat" << endl;
  }

  virtual void print_status() {
    cout << "I'm sleeping" << endl;
  }
};

void print_random_message(void* something) {
  cout << "I'm crazy" << endl;
}

int main(void)
{
  cat kitty;
  dog puppy;
  animal* pa = &kitty;

  intptr_t* cat_vptr = *((intptr_t**)(&kitty));
  intptr_t* dog_vptr = *((intptr_t**)(&puppy));

  intptr_t fake_vtable[] = {
    dog_vptr[0],         // for dog::print_age
    cat_vptr[1],         // for cat::print_kind
    (intptr_t) print_random_message
  };
  *((intptr_t**) pa) = fake_vtable;

  pa->print_age();    // Woof, my age = 1
  pa->print_kind();   // I'm a cat
  pa->print_status(); // I'm crazy

  return 0;
}

直接整个vtable伪造出来然后想往里面填啥就填啥。

至于有没有实际应用使用了题主原本写的那种代码,还真有。(但这么用的都该拖出去打pp⋯

例如说Oracle/Sun JDK / OpenJDK里的HotSpot VM,在PermGen Removal之前,有一类叫klassOopDesc的对象是由GC管理的,但里面还嵌套包含一个Klass的子类对象,而Klass类有vptr。为了能正确管理klassOopDesc里嵌套的Klass的vptr,就有了这么个奇葩的东西:
class Klass_vtbl
好同学们请不要学这种例子⋯这个奇葩的结构在PermGen Removal后就移除了嗯。

posted on 2018-03-06 21:45  Shihu  阅读(266)  评论(0编辑  收藏  举报

导航