Как получить указатель из C ++ vtable? - PullRequest
15 голосов
/ 24 февраля 2011

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

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

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

Foo* foo;

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

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

Мои вопросы:

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

  2. Существует ли какой-либо удаленно независимый от компилятора способ сделать это, например, кто-то реализовал библиотеку, которая предоставляет 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, я мог бы сгенерировать фрагмент, подобный приведенному выше, для каждой отдельной платформы, как способ получения указателя из виртуальные таблицы.

Ответы [ 5 ]

3 голосов
/ 24 февраля 2011

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

Первое (и очевидное): Общее программирование (иначе шаблоны)

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

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

Вместо введения стратегии в алгоритм, введите алгоритм в стратегии. Таким образом, вначале у вас будет один виртуальный вызов, а затем он будет проходить «нормально». Шаблоны могут помочь еще раз здесь.

2 голосов
/ 24 февраля 2011

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

Нет, в C ++ нет стандартного способа сделать это.Выше указано, но не то, что вы просите.

2 голосов
/ 24 февраля 2011

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

1 голос
/ 24 февраля 2011

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

Не существует стандартного способа получения указателя vtable из экземпляра, поскольку он зависит от реализации компилятора. Смотрите этот пост для более подробной информации. Тем не менее, G ++ и MSVC ++, похоже, размечают объекты класса, как описано в wikipedia . Классы могут иметь указатели на несколько таблиц. Ради простоты я расскажу о классы, которые имеют только один указатель 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-компилятора возможно кэширование поиск в виртуальной таблице путем регенерации кода.

Сначала 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 это чтение / запись. Таким образом, попытка изменить содержимое виртуальной таблицы в Windows приведет к нарушению прав доступа, если вы не измените доступ к странице с помощью VirtualProtect .

0 голосов
/ 24 февраля 2011

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

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

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

Шаблоныдругой стандартный C ++ способ повторного использования кода без поиска vtable.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...