[C++] Positie v.e. method in de Vtable?

Pagina: 1
Acties:

  • MrBucket
  • Registratie: Juli 2003
  • Laatst online: 29-10-2022
Weet iemand toevallig of het mogelijk is om van een gegeven klasse te bepalen op welke index een methode in de vtable staat? Dit mag compile-time of runtime bepaald worden.
Ik gebruik MS Visual C++ 7.0 als compiler.

m.a.w., stel dat ik een class heb:
C++:
1
2
3
4
5
6
7
class MyClass{
public:
  MyClass();
  virtual ~MyClass();
  virtual int methodA();
  virtual int methodB();
};

Dan wil ik met een macro of functie uit kunnen vogelen dat MyClass::methodB() de derde entry is in de vtable. Nu is het in dit voorbeeld duidelijk te zien, maar wanneer je klasse is afgeleid van een aantal superklassen (evt. met multiple inheritance) dan wordt het al snel onoverzichtelijk.

Achtergrond:
Ik wil een COM-object maken die een dispinterface implementeert. Nu biedt Automation 2 methoden om dit op een makkelijke manier te doen, nl. CreateDispTypeInfo() en CreateStdDispatch(). De eerste genereert een ITypeInfo object op basis van een array van methode-omschrijvingen, en de tweede genereert een IDispatch-implementatie op basis van dit ITypeInfo object.

Het punt is dat CreateDispTypeInfo() van elke methode die je via IDispatch wil exposen wilt weten waar die in de vtable te vinden is, zodat de gegenereerde IDispatch::Invoke() de juiste methode kan aanroepen.
Nu kan je dit wel met trial-and-error uitvinden, maar da's niet echt handig en bovendien nogal error-prone.

Heeft iemand enig idee?

Verwijderd

Gebruik anders de ATL lib die meegeleverd wordt door MS, die lost al dit soort nasty plumbing problemen voor je op.

  • MrBucket
  • Registratie: Juli 2003
  • Laatst online: 29-10-2022
Goed, ik ben zelf maar aan 't hacken geslagen, en ik ben eruit gekomen. Mocht iemand ooit nog zoeken via GoT of googlen naar dit probleem en hier terecht komen, dan hoop ik dat dit diegene verder kan helpen ;)

nb.Ik heb de code iets aangepast, zodat het nu ook goed overweg kan met virtual method pointers van classes die meerdere base classes hebben, en het opvragen van een vtable index van een method die op de nul-de entry in een vtable staat, gaat nu ook goed.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/* VTableInfo.h */

#ifndef __VTableInfo_h
#define __VTableInfo_h

//
//  The function GetVTableInfo returns, for a given virtual method of a class,
//  in which vtable it can be found, and at which index. The OLE function
//  CreateDispTypeInfo requires this index in order to expose a dispatch 
//  method. 
//
//  Note that this is a serious *hack*, which is intended to function only 
//  with Microsoft Visual C++ version 13.00.9466 (shipped with MS Visual 
//  Studio.NET, use cl /? to get this version number). It relies heavily on 
//  the assembly code generated for pointers to virtual functions, and may 
//  therefore not work with other versions of this compiler.
//
//  In short, whenever a pointer to a virtual method is taken, it will not 
//  refer to that method's address, but rather to a piece of generated code
//  that is known as 'vcall'. For each virtual method pointer, such a 'vcall'
//  is generated. 
//  Hence, when this method pointer is used to execute that method, it will 
//  call the 'vcall' code. This 'vcall' takes an object's instance pointer, 
//  and uses this object's vtable to call the method's implementation. A 
//  'vcall' consists of 3 assembly instructions (two MOV's and one JMP) to 
//  accomplish this.
//  
//  GetVTableInfo works by using such a virtual method pointer to locate its
//  related 'vcall' code, and extracting the vtable nr from this pointer and 
//  the vtable index used directly from the assembly code.
//
//  Leon Bouquiet.
//

namespace VTableTools{

/** 
 *  Defines in which vtable, and at which index in this vtable a virtual 
 *  method can be found. 
 */
struct VTableInfoType{
    /** The vtable nr. used (usually 0, can be higher when multiple 
     *  inheritance is used. */
    int m_iSelector;
    /** The index within this vtable which holds the virtual method's address 
     */
    int m_iIndex;
};

/**
 *  Possible error codes for GetVTableInfo(), see docs for background info.
 */
enum VTableErrorType{
    /** No errors, the vtable info was successfully extracted. */
    VTE_NO_ERROR,
    /** The supplied function pointer is NULL.*/
    VTE_INVALID_FUNCPTR,
    /** Couldn't locate the JMP instruction to the 'vcall' code (returned in 
     *  debug builds only).*/
    VTE_NO_LONGJMP_INSTR,
    /** The 1st instruction in the 'vcall' code didn't match. */
    VTE_NO_GET_INSTANCE_INSTR,
    /** The 2nd instruction in the 'vcall' code didn't match. */
    VTE_NO_GET_VTABLE_INSTR,
    /** The 3rd instruction in the 'vcall' code didn't match. */
    VTE_NO_VTABLE_CALL_INSTR
};

/**
 *  Convenience macro that requires only the class and method name to extract
 *  the vtable info. For MyClass::MyVirtMethod, use it like this:
 *  VTableErrorType err = GET_VTABLE_INFO(MyClass, MyVirtMethod, &vtableInfo)
 */
#define GET_VTABLE_INFO(ClassName, MethodName, pInfo)                                    \
    GetVTableInfo<ClassName>(   (void (__stdcall ClassName::*)())ClassName::MethodName,  \
                                pInfo)

/**
 *  Templated function that fills a VTableInfoType struct with the given 
 *  virtual method's vtable position.
 *  
 *  @param    pfn        Pointer to a virtual method (must be __stdcall)
 *  @param    pInfo    On return, holds the method's vtable number and index.
 *  @return            VTE_NO_ERROR, or an VTableErrorType code on failure.
 */
template<class C>
VTableErrorType GetVTableInfo(void (__stdcall C::*pfn)(), VTableInfoType *pInfo){
    //For a given C *pInstance, the passed method would be called as: 
    //((*pInstance).*(pfn))();

    //Clear the returned VTablePosType struct and verify the passed function pointer
    pInfo->m_iSelector = 0;
    pInfo->m_iIndex = -1;
    if(pfn == NULL)
        return VTE_INVALID_FUNCPTR;

    //Step 1. Get the address of pfn (i.e. &pfn) as an int *, so we can work 
    //with it. Since the compiler doesn't allow us to cast these types, pass 
    //pfn through the eax register, so the compiler can't type-check it.
    unsigned int  *piFn = NULL;
    __asm{
        push       eax;
        lea        eax, pfn;
        mov        dword ptr[piFn], eax;
        pop        eax;
    };

    //If the passed method pointer is 4 bytes in size, it only stores a 
    //pointer. However, when a method pointer of a class with multiple base
    //classes is passed, it will be 8 bytes; the last 4 bytes then hold the
    //vtable selector.
    if(sizeof(pfn) == 8){
        //Extract the vtable selector (stored as an offset, so divide by 4)
        pInfo->m_iSelector = piFn[1] >> 2;
    }

    //Find the generated 'vcall' code. When compiled in debug mode, piFn[0]
    //points to a long jump that jumps to the vcall code, while in release 
    //mode, piFn[0] points _directly_ to the 'vcall' code.
    unsigned char *pVCall;
    #ifndef NDEBUG        //Debug mode
        //Step 2a: piFn[0] points to a 5-byte opcode, representing a long jump. 
        //Verify that this is indeed a long jmp (0xE9), and get its offset 
        //(last 4 bytes)
        unsigned char *pbJmp = (unsigned char *)piFn[0];
        if(pbJmp[0] != 0xE9)
            return VTE_NO_LONGJMP_INSTR;
        int iJmpOffset = *((int *)(pbJmp + 1));

        //Step 2b: locate the generated 'vcall' code, by adding the iJmpOffset to
        //the address of the JMP instruction, plus the instruction length (5 bytes)
        pVCall = &((pbJmp)[5 + iJmpOffset]);

    #else                //Release mode
        pVCall = *((unsigned char **)piFn);
    #endif

    //Step 3: verify the 'vcall' opcodes at *pVCall

    //First instruction should get object instance pointer from stack, coded as
    //"mov eax, dword ptr[esp, 4]" (opcode: 8B 44 24 04)
    if(!((pVCall[0] == 0x8B) && (pVCall[1] == 0x44) && (pVCall[2] == 0x24) && (pVCall[3] == 0x04)))
        return VTE_NO_GET_INSTANCE_INSTR;

    //Second instruction dereferences this instance pointer with an added 
    //selector value to select the proper vtable. This is coded as:
    //"mov eax, dword ptr[eax + selector]" (opcode: 8B selector)
    if(pVCall[4] != 0x8B)
        return VTE_NO_GET_VTABLE_INSTR;

    //Third instruction simulates the virtual function call through a jmp. It
    //can be coded either with or without offset, i.e.  
    //  "jmp dword ptr[eax + vtbl_index]"    (opcodes: FF 60 vtbl_index) or
    //  "jmp dword ptr[eax]"                (opcodes: FF 20, implies offset=0)
    if(!((pVCall[6] == 0xFF) && ((pVCall[7] == 0x60) || (pVCall[7] == 0x20))))
        return VTE_NO_VTABLE_CALL_INSTR;

    //Step 4: extract the index, if present. Because this value is extracted 
    //as a direct offset rather than an index, divide by 4
    if(pVCall[7] == 0x60)
        pInfo->m_iIndex    = (pVCall[8] >> 2);
    else
        pInfo->m_iIndex = 0;

    return VTE_NO_ERROR;
}

}; //End namespace VTableTools

#endif //__VTableInfo_h

[ Voor 17% gewijzigd door MrBucket op 15-11-2004 10:29 ]


  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Die cast kan je ook zo doen, zonder assembler:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T, typename U>
T cast(U src) {
   union {
      T x;
      U y;
   } tmp;
   tmp.y = src;
   return tmp.x;
};

typedef void (__stdcall C::*CFuncPtr)();
CFuncPtr fp = &C::foo;
unsigned char* raw = cast<unsigned char*>(fp);


Scheelt weer wat assembler dan :P

  • MrBucket
  • Registratie: Juli 2003
  • Laatst online: 29-10-2022
Hmm, ik bemerk toch nog 2 kleine probleempjes:

- Als je een virtuele methode meegeeft die entry 0 in de vtable heeft, dan wordt de error VTE_NO_VTABLE_CALL_INSTR gegenereerd. In dit geval wordt de 3e instructie nl. niet als "jmp dword ptr[eax+vtbl_index]" gecodeerd (3 bytes), maar simpelweg als "jmp dword ptr[eax]" (zonder offset, nu maar 2 bytes).

- Hij kan toch niet goed overweg met multiple inherited classes. Het blijkt nl. dat een virtual method pointer van zo'n class geen 4 bytes, maar 8 bytes groot is. En idd, de laatste 4 bytes bevat het vtable-nr... :X

Toch nog maar ff bijschaven...

//edit: gedaan. Ik heb de code in mijn eerdere code hiervoor aangepast.

[ Voor 9% gewijzigd door MrBucket op 15-11-2004 10:30 ]


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 09-04 22:08
Volgens mij had MSVC++ in het geval van multiple virtual inheritance toch meerdere vtables, of vergis ik me nu? Dus dan kun je het niet eens hebben over "de" vtable. (virtual inheritance kan leiden tot kleine stubs waarbij de 'this' pointer wordt geadjust voordat de eigenlijke call gebeurt.)

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • MrBucket
  • Registratie: Juli 2003
  • Laatst online: 29-10-2022
MSalters schreef op zondag 14 november 2004 @ 22:51:
Volgens mij had MSVC++ in het geval van multiple virtual inheritance toch meerdere vtables, of vergis ik me nu? Dus dan kun je het niet eens hebben over "de" vtable. (virtual inheritance kan leiden tot kleine stubs waarbij de 'this' pointer wordt geadjust voordat de eigenlijke call gebeurt.)
Yup, klopt. En de m_iSelector member van VTableInfoType moet dus bevatten welke van deze vtables je gebruikt ;) Als er geen multiple inheritance aan te pas komt zal dit altijd vtable nr 0 zijn, maar in geval van meerdere base-classes heb je dus ook meerdere vtables

Om even met ASCII-art aan de gang te gaan, het ziet er als volgt uit:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Bij de volgende class-definities:
class Base1{
  virtual void MethA();
  virtual void MethB();
};
class Base2{
  virtual void MethC();
};
class Object: public Base1, Base2:
};

...hoort deze geheugen-layout:
Obj pointer     Obj Data                vtables          Method calls

+------+      +--------------+        +-----------+
| pObj | ---> | VTable ptr 0 -------> | MethA ptr -----> Base1::MethA()
+------+      | VTable ptr 1 ---+     | MethB ptr -----> Base1::MethB()
              +--------------+  |     +-----------+
              |Instance data |  |
              +--------------+  |
                                +---> +-----------+
                                      | MethC ptr -----> Base2::MethC()
                                      +-----------+

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 15:32

.oisyn

Moderator Devschuur®

Demotivational Speaker

Vaag, van wat ik ervan gezien heb maakt VC++ van functionpointers naar virtual functions een call stub die gewoon als een gewone method wordt aangeroepen. Die call stub doet vervolgens de vtable lookup. Ik snap dus niet hoe je dan op jouw manier aan adressen van de functies zelf kunt komen...

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


  • MrBucket
  • Registratie: Juli 2003
  • Laatst online: 29-10-2022
.oisyn schreef op maandag 15 november 2004 @ 11:31:
Vaag, van wat ik ervan gezien heb maakt VC++ van functionpointers naar virtual functions een call stub die gewoon als een gewone method wordt aangeroepen. Die call stub doet vervolgens de vtable lookup. Ik snap dus niet hoe je dan op jouw manier aan adressen van de functies zelf kunt komen...
Het klopt wat je zegt. Wat ik doe is het adres van die virtual method pointer volgen om de stub-code (wat ik de 'vcall' noem) te vinden (nb, dit gebeurt dus runtime, niet compile-time). En vervolgens ploeter ik door de assembly-code van die stub om uit te vinden welke entry uit de vtable daar wordt gebruikt.

Yup, 't is een hack :X

[ Voor 5% gewijzigd door MrBucket op 15-11-2004 13:03 ]

Pagina: 1