Why is the function call to the virtual function using the address stored in the virtual method table returning garbage?
Why is the function call to the virtual function using the address stored in the virtual method table returning garbage?
I'm calling virtual functions from the address in the virtual table as an exercise to test my understanding of the concept. However, as soon as I thought I made a breakthrough in my understanding of the virtual method table, I run into another issue that I just don't understand.
In the code below, I've created a class called Car
which contains a member variable x and two virtual functions, first and second. Now, I call these two virtual methods by hacking through the virtual table. The first function returns the correct answer, but the second returns some random value or garbage instead of what it was initialized to be.
Car
#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: %pn", *mVtable);
printf("First Entry of VTable: %pn", (void*) mVtable[0][0]);
printf("Second Entry of VTable: %pn", (void*) mVtable[0][1]);
if(sizeof(void*) == 8)
printf("64 bitn");
int (*firstfunc)() = (int (*)()) mVtable[0][0];
int x = firstfunc();
int (*secondfunc)() = (int (*)()) mVtable[0][1];
int x2 = secondfunc();
printf("first: %dnsecond: %d", x, x2);
return 0;
If someone can point me to what I'm doing wrong that would be appreciated. Also, since this works differently across compilers, I'm testing it on http://cpp.sh/ using c++14.
That code out outputs, where the "garbage" second output is subject to change:
VTable: 0x400890
First Entry of VTable: 0x400740
Second Entry of VTable: 0x400720
64 bit
IT WORKS!!
IT WORKS 2!!
first: 5
second: -888586240
Car
second
secondfunc
Could you cast to member function pointers instead of function pointers?
– François Andrieux
Sep 6 '18 at 18:43
You did not provide the address of the object before calling its methods (by x86/AMD ABI first six arguments are placed in registers).
– Nikita Kniazev
Sep 6 '18 at 22:13
Ok. I understand why it's not working now. However, now I need to figure out how I can actually do it. I don't know if it's even possible but the wiki suggests it is. en.wikipedia.org/wiki/Virtual_method_table#Invocation
– legendaryz
Sep 7 '18 at 14:44
3 Answers
3
Methods are functions, but method pointers are generally not function pointers.
The calling convention of calling methods does not always agree with the calling convention of calling functions.
We can get around this. With yet more undefined behavior, but that works at least sometimes.
MSVC clang g++
Code:
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;
now this test code:
Car car;
void* carPtr = &car;
auto **mVtable = (uintptr_t **)(carPtr);
printf("VTable: %pn", *mVtable);
printf("First Entry of VTable: %pn", (void*)mVtable[0][0]);
printf("Second Entry of VTable: %pn", (void*)mVtable[0][1]);
if(sizeof(void*) == 8)
printf("64 bitn");
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: %dnsecond: %d", x, x2);
The code above relies on method pointers being a pair of function pointer and a second section that if all 0s is non-virtual dispatch, and the vtable to contain just the function pointer component.
So we can reconstruct a method pointer from the data in the vtable by padding a buffer with 0s, then interpreting the memory as a method pointer.
To get the call to work, we create a fake type with a method that matches our signature, then cast our pointer to that type and invoke it with a member function pointer reconstructed from our original type's vtable.
This, we hope, mimics the calling convention of that the compiler uses for other method calls.
In clang/g++ non-virtual method pointers are two pointers with the second one ignored. Virtual method pointers, I believe, use the second pointer-sized data.
In MSVC, non-virtual method pointers are the size of one pointer. Virtual method pointers with a virtual inheritance tree are not the size of one pointer. I believe this violates the standard (that requires that member pointers be inter-castable between).
In both cases, the vtable appears to store the first half of each non-virtual method pointer.
Looks extremely hacky but I like it. Will try it out later. However, honestly, I'm particularly interested in the approach they take on the wiki. Not sure if it's just cynically incomplete bs or not. en.wikipedia.org/wiki/Virtual_method_table#Invocation
– legendaryz
Sep 7 '18 at 14:42
@legendaryz You cannot replicate the MSVC calling convention through a function call; you can replicate it using this hack. And it also replicates the clang/g++ calling conventions with the same hack.
– Yakk - Adam Nevraumont
Sep 7 '18 at 14:46
Oh ok. I see. Thanks.
– legendaryz
Sep 7 '18 at 15:11
Methods are indeed generally implemented as regular functions, but they need to receive the this
pointer to access the data of a specific instance - in facts, when you invoke a method over an instance a pointer to the instance gets passed as a hidden parameter.
this
In your code you aren't passing it in, so the method just returns garbage - it's probably using whatever happens to be in a register or on the stack as if it was the instance pointer; you are lucky enough that it doesn't plainly crash.
You may try changing your prototypes to accept a Car*
parameter and pass &car
to it, but it may or may not work, depending on the calling convention used by your compiler/platform:
Car*
&car
stdcall
cdecl
this
ecx
cdecl
this
One of the few cases where it's polite to say "this is garbage"
– Tim Randall
Sep 6 '18 at 19:24
The wiki I was referencing suggests it can be done in a strange way. Not sure if it's even syntactically correct. en.wikipedia.org/wiki/Virtual_method_table#Invocation
– legendaryz
Sep 7 '18 at 14:46
The constructor, which sets x = 2
, doesn't run when you call a function pointer directly into the vtable. You're returning uninitialized memory from second
, which can be anything.
x = 2
second
No, the ctor runs directly in main.
– o11c
Sep 6 '18 at 18:17
@o11c The function is called directly without any reference to which instance of the class it is being called from (i.e.
this
), so x
is uninitialized since the constructor is not run in the context of the call to mVTable[0][1]
– Govind Parmar
Sep 6 '18 at 18:18
this
x
mVTable[0][1]
Car car
is constructed, but the function that's called isn't given a this
pointer that points to car
. We're in undefined behavior territory– Tim Randall
Sep 6 '18 at 18:20
Car car
this
car
@TimRandall the undefined behavior here started way before not passing
this
... 🙄– Matteo Italia
Sep 6 '18 at 18:21
this
Pretty sure it's all unconventional, but there has to be a way to do it. I can retrieve the value of the x in a similar manner from the object. So I think, @TimRandall explanation makes sense. It also lines up with the other answers. P.S. the wiki I'm referencing en.wikipedia.org/wiki/Virtual_method_table#Invocation
– legendaryz
Sep 7 '18 at 14:52
Thanks for contributing an answer to Stack Overflow!
But avoid …
To learn more, see our tips on writing great answers.
Required, but never shown
Required, but never shown
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
For which instance of
Car
are you calling functionsecond
through pointersecondfunc
?– Ivan
Sep 6 '18 at 18:10