Я вызываю виртуальные функции с адреса в виртуальной таблице в качестве упражнения для проверки моего понимания концепции. Однако, как только я подумал, что совершил прорыв в своем понимании таблицы виртуальных методов, я столкнулся с другой проблемой, которую просто не понимаю.
В приведенном ниже коде я создал класс 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
Методы являются функциями, но указатели на методы обычно не являются указателями на функции.
Соглашение о вызове методов вызова не всегда согласуется с соглашением о вызове функций.
Мы можем обойти это. С еще более неопределенным поведением, но это работает по крайней мере иногда.
Код:
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 хранит первую половину каждого указателя не виртуального метода.
Методы действительно обычно реализуются как обычные функции, но они должны получить this
указатель для доступа к данным конкретного экземпляра — на самом деле, когда вы вызываете метод над экземпляром, указатель на экземпляр передается как скрытый параметр.
В вашем коде вы не передаете его, поэтому метод просто возвращает мусор — он, вероятно, использует то, что происходит в регистре или в стеке, как если бы это был указатель экземпляра; вам повезло, что он явно не разбился.
Вы можете попробовать изменить свои прототипы, чтобы принять Car*
параметр и проход &car
к нему, но это может или не может работать, в зависимости от соглашения о вызовах, используемого вашим компилятором / платформой:
stdcall
соглашение о вызове (или cdecl
для разнообразия), но получите this
указатель в ecx
что-то, что вы не можете эмулировать с помощью обычного вызова функции;cdecl
функции, проходящие this
неявно, как если бы это был последний параметр. Конструктор, который устанавливает x = 2
, не запускается, когда вы вызываете указатель на функцию непосредственно в vtable. Вы возвращаете неинициализированную память из second
, который может быть чем угодно.