Почему вызов функции виртуальной функции с использованием адреса, хранящегося в таблице виртуальных методов, возвращает мусор?

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

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

#include <cstdio>

class Car
{
private:
int x;

virtual int first()
{
printf("IT WORKS!!\n");
int num = 5;
return num;
}
virtual int second()
{
printf("IT WORKS 2!!\n");
//int num  = 5;
return x;
}public:

Car(){
x = 2;
}
};

int main()
{
Car car;
void* carPtr = &car;
long **mVtable =(long **)(carPtr);

printf("VTable: %p\n", *mVtable);
printf("First Entry of VTable: %p\n", (void*) mVtable[0][0]);
printf("Second Entry of VTable: %p\n", (void*) mVtable[0][1]);

if(sizeof(void*) == 8){
printf("64 bit\n");
}

int (*firstfunc)() = (int (*)()) mVtable[0][0];
int x = firstfunc();

int (*secondfunc)() = (int (*)()) mVtable[0][1];
int x2 = secondfunc();

printf("first: %d\nsecond: %d", x, x2);
return 0;
}

Если кто-то может указать мне на то, что я делаю неправильно, это будет оценено. Кроме того, так как это работает по-разному в разных компиляторах, я тестирую его на http://cpp.sh/ используя c ++ 14.

Этот код выводит, где второй «мусорный» вывод может быть изменен:

VTable: 0x400890
First Entry of VTable: 0x400740
Second Entry of VTable: 0x400720
64 bit
IT WORKS!!
IT WORKS 2!!
first: 5
second: -888586240

1

Решение

Методы являются функциями, но указатели на методы обычно не являются указателями на функции.

Соглашение о вызове методов вызова не всегда согласуется с соглашением о вызове функций.

Мы можем обойти это. С еще более неопределенным поведением, но это работает по крайней мере иногда.

MSVC лязг г ++

Код:

template<class Sig>
struct fake_it;

template<class R, class...Args>
struct fake_it<R(Args...)>{
R method(Args...);

using mptr = decltype(&fake_it::method);
};
template<class R, class...Args>
struct fake_it<R(Args...) const> {
R method(Args...) const;

using mptr = decltype(&fake_it::method);
};

template<class Sig>
using method_ptr = typename fake_it<Sig>::mptr;

template<class Sig>
struct this_helper {
using type=fake_it<Sig>*;
};
template<class Sig>
struct this_helper<Sig const>{
using type=fake_it<Sig> const*;
};

template<class Sig>
using this_ptr = typename this_helper<Sig>::type;

теперь этот тестовый код:

Car car;
void* carPtr = &car;
auto **mVtable = (uintptr_t **)(carPtr);
printf("VTable: %p\n", *mVtable);
printf("First Entry of VTable: %p\n", (void*)mVtable[0][0]);
printf("Second Entry of VTable: %p\n", (void*)mVtable[0][1]);

if(sizeof(void*) == 8){
printf("64 bit\n");
}

auto firstfunc = to_method_ptr<int()>(mVtable[0][0]);
int x = (this_ptr<int()>(carPtr)->*firstfunc)();

auto secondfunc = to_method_ptr<int()>(mVtable[0][1]);
int x2 = (this_ptr<int()>(carPtr)->*secondfunc)();

printf("first: %d\nsecond: %d", x, x2);

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

Таким образом, мы можем восстановить указатель метода из данных в vtable, заполнив буфер 0, а затем интерпретировать память как указатель метода.

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

Мы надеемся, что это имитирует соглашение о вызовах, которое компилятор использует для других вызовов методов.


В clang / g ++ указатели не виртуальных методов — это два указателя, второй игнорируется. Я полагаю, что указатели виртуальных методов используют данные второго размера.

В MSVC указатели не виртуальных методов имеют размер одного указателя. Указатели виртуальных методов с виртуальным деревом наследования не имеют размер одного указателя. Я считаю, что это нарушает стандарт (который требует, чтобы указатели на элементы были взаимозаменяемыми).

В обоих случаях vtable хранит первую половину каждого указателя не виртуального метода.

2

Другие решения

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

В вашем коде вы не передаете его, поэтому метод просто возвращает мусор — он, вероятно, использует то, что происходит в регистре или в стеке, как если бы это был указатель экземпляра; вам повезло, что он явно не разбился.

Вы можете попробовать изменить свои прототипы, чтобы принять Car* параметр и проход &car к нему, но это может или не может работать, в зависимости от соглашения о вызовах, используемого вашим компилятором / платформой:

  • на Win32 / x86 / VC ++, например, методы используют stdcall соглашение о вызове (или cdecl для разнообразия), но получите this указатель в ecxчто-то, что вы не можете эмулировать с помощью обычного вызова функции;
  • с другой стороны, x86 gcc просто обрабатывает их как cdecl функции, проходящие this неявно, как если бы это был последний параметр.
6

Конструктор, который устанавливает x = 2, не запускается, когда вы вызываете указатель на функцию непосредственно в vtable. Вы возвращаете неинициализированную память из second, который может быть чем угодно.

0
По вопросам рекламы [email protected]