Как получить указатель из С++ vtable?

Скажем, у вас есть класс С++:

class Foo {
 public:
  virtual ~Foo() {}
  virtual DoSomething() = 0;
};

Компилятор С++ переводит вызов в поиск vtable:

Foo* foo;

// Translated by C++ to:
//   foo->vtable->DoSomething(foo);
foo->DoSomething();

Предположим, что я писал компилятор JIT, и я хотел получить адрес функции DoSomething() для конкретного экземпляра класса Foo, поэтому я могу сгенерировать код, который перескакивает непосредственно, вместо того, чтобы выполнять поиск в таблице и косвенную ветвь.

Мои вопросы:

  • Есть ли какой-либо стандартный способ С++ для этого (я почти уверен, что ответ отрицательный, но хотелось попросить о полноте).

  • Есть ли какой-либо отдаленный независимый от компилятора способ сделать это, как библиотека, которую кто-то реализовал, которая предоставляет API для доступа к vtable?

Я полностью открыт для взлома, если они будут работать. Например, если я создал свой собственный производный класс и смог определить адрес его метода DoSomething, я мог бы предположить, что vtable является первым (скрытым) членом Foo и выполняет поиск по его таблице vtable, пока не найду значение моего указателя. Тем не менее, я не знаю способ получить этот адрес: если я пишу &DerivedFoo::DoSomething, я получаю указатель на элемент, что совершенно другое.

Возможно, я мог бы превратить указатель в элемент в смещение vtable. Когда я скомпилирую следующее:

class Foo {
 public:
  virtual ~Foo() {}
  virtual void DoSomething() = 0;
};

void foo(Foo *f, void (Foo::*member)()) {
  (f->*member)();
}

В GCC/x86-64 я получаю этот вывод сборки:

Disassembly of section .text:

0000000000000000 <_Z3fooP3FooMS_FvvE>:
   0:   40 f6 c6 01             test   sil,0x1
   4:   48 89 74 24 e8          mov    QWORD PTR [rsp-0x18],rsi
   9:   48 89 54 24 f0          mov    QWORD PTR [rsp-0x10],rdx
   e:   74 10                   je     20 <_Z3fooP3FooMS_FvvE+0x20>
  10:   48 01 d7                add    rdi,rdx
  13:   48 8b 07                mov    rax,QWORD PTR [rdi]
  16:   48 8b 74 30 ff          mov    rsi,QWORD PTR [rax+rsi*1-0x1]
  1b:   ff e6                   jmp    rsi
  1d:   0f 1f 00                nop    DWORD PTR [rax]
  20:   48 01 d7                add    rdi,rdx
  23:   ff e6                   jmp    rsi

Я не совсем понимаю, что происходит здесь, но если бы я мог перепроектировать это или использовать спецификацию ABI, я мог бы создать фрагмент, подобный выше для каждой отдельной платформы, в качестве способа получения указателя из виртуальные таблицы.

Ответ 1

Почему, по-вашему, &DerivedFoo::DoSomething отличается? Разве это не то, о чем вы просите? Как я думаю об этом, любой вызов DerivedFoo::DoSomething() вызовет ту же функцию, передав другой этот указатель. В таблице v различаются только разные типы, полученные из Foo, а не экземпляры.

Ответ 2

Я могу думать о двух других решениях, а не копаться в объектной модели С++.

Первый (и очевидный): Generic Programming (aka templates)

Не используйте базовый класс, рефакторируйте методы, которые зависят от базового класса, чтобы они принимали "Стратегию" в качестве аргумента шаблона. Это полностью устранит виртуальные вызовы.

Второй, менее очевидный, - это изменить зависимости.

Вместо того, чтобы вводить стратегию в алгоритм, введите алгоритм в стратегию. Таким образом, у вас будет один виртуальный вызов в начале, а затем он будет действовать "нормально". Шаблоны могут снова помочь здесь.

Ответ 3

Это не прямой ответ, и он не обязательно обновляется, но он содержит множество подробностей и предостережений, о которых вам нужно знать при попытке сделать что-то вроде этого: http://www.codeproject.com/KB/cpp/FastDelegate.aspx

Нет, не существует стандартного способа С++ для этого. Вышеупомянутое похоже, но не то же самое, что вы просите.

Ответ 4

Во-первых, типы классов имеют vtable. Экземпляры этого типа имеют указатель на таблицу vtable. Это означает, что если содержимое vtable изменяется для типа, все экземпляры этого типа пострадавшие. Но конкретный экземпляр может изменить свой указатель на vtable.

Нет стандартного способа извлечения указателя vtable из экземпляра, поскольку он зависит от реализации компилятора. Подробнее см. . Однако g++ и MSVС++ кажутся объектами класса макета, как описано в wikipedia. Классы могут иметь указатели на несколько vtables. Ради простоты я расскажу о классы, которые имеют только один указатель vtable.

Чтобы получить указатель на функцию из таблицы vtable, это можно сделать так просто:

int* cVtablePtr = (int*)((int*)c)[0];
void* doSomethingPtr = (void*)cVtablePtr[1];

Где c - экземпляр класса C для определения этого класса:

class A
{
public:
    virtual void A1() { cout << "A->A1" << endl; }
    virtual void DoSomething() { cout << "DoSomething" << endl; };
};

class C : public A
{
public:  
    virtual void A1() { cout << "C->A1" << endl; }
    virtual void C1() { cout << "C->C1" << endl; }
};

Класс C - это просто структура, первый член которой является указателем на vtable в этом случае.

В случае JIT-компилятора возможно кэширование поиск в vtable путем изменения кода.

Сначала компилятор JIT может произвести это:

void* func_ptr = obj_instance[vtable_offest][function_offset];
func_ptr(this, param1, param2)

Теперь, когда func_ptr известен, JIT может убить этот старый код и просто жесткий код, который указывает адрес функции в скомпилированный код:

hardcoded_func_ptr(this, param1, param2)

Одна вещь, которую я должен отметить, это то, что вы можете перезаписать экземпляр указателя vtable, не всегда можно перезаписать содержимое vtable. Например, в Windows vtable помечается как постоянное запоминающее устройство, но в OS X оно считывается/записывается. Поэтому в окнах, пытающихся изменить содержимое vtable, будет возникать нарушение доступа, если вы не измените доступ к странице с помощью VirtualProtect,

Ответ 5

Если вы вызываете derived->DoSomething(), а DoSomething() не является виртуальным в производном классе, компилятор должен генерировать прямой вызов уже.

Если вы вызываете base->DoSomething(), компилятор должен проверить, так или иначе, какую версию DoSomething() вызывать, а vtable - как эффективный метод, как любой. Если бы вы могли гарантировать, что это всегда будет экземпляр базового класса, вам не нужно будет делать метод виртуальным в первую очередь.

При определенных обстоятельствах может возникнуть смысл сделать static_cast, прежде чем вызывать кучу не виртуальных производных методов, которые являются виртуальными в базовом классе, но поскольку запросы vtable являются общими, учитываются и относительно недороги, это определенно подпадает под категорию преждевременной оптимизации.

Шаблоны - это еще одно стандартное средство С++ для повторного использования кода, не вызывая поиска vtable.