vTables и указатели функций, указывающие на разные адреса

Недавно я читал статью в блоге bitsquid о том, как управлять памятью, и автор начал рассказывать о vtable и о том, как компилятор добавляет в класс указатель. Вот ссылка на статью. Так как я почти ничего не знал о vtalbe, я начал искать объяснение в сети. Я наткнулся на эту ссылку. Основываясь на том, что я прочитал, я сделал следующий код:

char cache[24];

printf("Size of int = %d\n", sizeof(int));
printf("Size of A = %d\n", sizeof(A));

A* a = new(cache)A(0,0);
printf("%s\n",cache);
printf("vTable    : %d\n",*((int*)cache));
printf("cache addr: %d\n",&cache);

int* funcPointer = (int*)(*((int*)cache));
printf("A::sayHello: %d\n",&A::sayHello);
printf("funcPointer: %d\n",*funcPointer);

A — это класс с двумя целыми членами и виртуальной функцией sayHello().

EDIT: Вот определение класса:

class A {
public:
    int _x;
    int _y;
public:
    A(int x, int y) : _x(x), _y(y){ }
    virtual void sayHello() { printf("Hello You!"); }
};

В основном то, что я пытался сделать, это посмотреть, будет ли указатель внутри vtable указывать на то же место, где находится адрес, который я получаю от &A::sayHello, но дело в том, что когда я запускаю программу, адрес внутри указателя в vtable и адрес sayHello() всегда отличается от 295. Кто-нибудь знает, почему это может происходить? Есть ли какой-то заголовок, который я пропустил? Я запускаю Visual Studio Express 2008 на 64-битной машине.

Из того, что я отладил, адрес, возвращаемый *funcPointer, является истинным адресом функции sayHello(). Но почему &A::sayHello() возвращает другой адрес?


person Kunashu    schedule 02.04.2012    source источник
comment
Можете ли вы показать определение класса A?   -  person Mysticial    schedule 03.04.2012
comment
Это определяется реализацией, а также в большей степени зависит от определения класса. Без определения A (и компилятора/архитектуры) мало что можно сказать.   -  person David Rodríguez - dribeas    schedule 03.04.2012


Ответы (2)


C++ имеет интересную особенность:

Если вы возьмете указатель на виртуальную функцию и используете его, виртуальная функция будет разрешена и вызвана.

Возьмем простой пример

struct A
{
    virtual DoSomething(){ printf("A"); }
};

struct B: public A
{
    virtual DoSomething() { printf("B"); }
};

void main()
{
    A * a, b;
    void (A::*pointer_to_function)();

    pointer_to_function = &A::DoSomething;
    a = new A;
    b = new B;

    (a.*pointer_to_function)() //print "A"
    (b.*pointer_to_function)() //print "B"
}

Таким образом, адрес, который вы видите с помощью &A::DoSomething, является адресом батута, а не реальным адресом функции.

Если вы перейдете к сборке, вы увидите, что функция делает что-то подобное (регистр может измениться, но ecx, который представляет указатель this):

mov eax, [ecx] ; Read vtable pointer
lea edx, [eax + 4 * function_index ] ; function_index being the index of function in the vtable
call edx  
person crazyjul    schedule 02.04.2012
comment
Спасибо большое, это многое объясняет! Так этот трамплин всегда присутствует для каждой функции в классе или только для виртуальных функций? - person Kunashu; 03.04.2012
comment
Нет, только для виртуальных функций, поскольку невиртуальные функции определяются во время компиляции. - person crazyjul; 03.04.2012
comment
Ха-ха, использование батута вместо удара, так формально! В качестве примечания: переходники (батуты) генерируются только, когда вы явно берете адрес функции-члена, это форма оптимизации. Если вы не возьмете адрес, вы даже не сгенерируете эту функцию. - person std''OrgnlDave; 03.04.2012
comment
Если кому-то интересно, вот ссылка, которая объясняет, как работает преобразователь (тамполины). C Tutorial PointertoMember Function Еще раз спасибо всем за помощь. - person Kunashu; 13.04.2012

Обратите внимание, что все это определяется реализацией!

Хотя в некоторых реализациях может использоваться функция батута, это не единственный способ сделать это, и не то, как это реализует gcc.

С gcc, если вы запустите опубликованный вами код, вы получите следующее:

A::sayHello: 1

Потому что вместо хранения адреса функции-батута указатель функции-члена для виртуальной функции хранится как { vtable offset + 1, this-ptr offset }, и то, что вы печатаете, является первым словом этого. (см. http://sourcery.mentor.com/public/cxx-abi/abi.html#member-pointers) для более подробной информации).

В этом случае sayHello является единственной записью vtable, поэтому смещение vtable равно 0. 1 добавляется, чтобы пометить этот указатель функции-члена как виртуальную функцию-член.

Если вы проверите сборку для выполнения вызовов указателя функции-члена при компиляции с g++, вы получите некоторые инструкции на сайте вызова, которые вычисляют адрес вызываемой функции, если это виртуальный указатель функции-члена:

        (a->*pointer_to_function)(); //print "A"
Load the first word of the member function pointer into rax:
      4006df:       48 8b 45 c0             mov    -0x40(%rbp),%rax
Check the lower bit:
      4006e3:       83 e0 01                and    $0x1,%eax
      4006e6:       84 c0                   test   %al,%al
If non-virtual skip the next bit:
      4006e8:       74 1b                   je     400705 <main+0x81>
virtual case, load the this pointer offset and add the this pointer (&a):
      4006ea:       48 8b 45 c8             mov    -0x38(%rbp),%rax
      4006ee:       48 03 45 e0             add    -0x20(%rbp),%rax
rax is now the real 'this' ptr. dereference to get the vtable ptr:
      4006f2:       48 8b 10                mov    (%rax),%rdx
Load the vtable offset and subtract the flag:
      4006f5:       48 8b 45 c0             mov    -0x40(%rbp),%rax
      4006f9:       48 83 e8 01             sub    $0x1,%rax
Add the vtable offset to the addr of the first vtable entry (rdx):
      4006fd:       48 01 d0                add    %rdx,%rax
Dereference that vtable entry to get a real function pointer:
      400700:       48 8b 00                mov    (%rax),%rax
Skip the next line:
      400703:       eb 04                   jmp    400709 <main+0x85>

non-virt case, load the function address from the member function pointer:
      400705:       48 8b 45 c0             mov    -0x40(%rbp),%rax

Load the 'this' pointer offset:
      400709:       48 8b 55 c8             mov    -0x38(%rbp),%rdx
Add the actual 'this' pointer:
      40070d:       48 03 55 e0             add    -0x20(%rbp),%rdx
And finally call the function:
      400711:       48 89 d7                mov    %rdx,%rdi
      400714:       ff d0                   callq  *%rax
person je4d    schedule 02.04.2012