Таблицы и функциональные указатели, указывающие на разные адреса - PullRequest
4 голосов
/ 03 апреля 2012

Недавно я читал в блоге на bitquid статью о том, как управлять памятью, и автор начал рассказывать о 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().

РЕДАКТИРОВАТЬ: Вот определение класса:

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

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

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

Ответы [ 2 ]

8 голосов
/ 03 апреля 2012

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, который представляет этот указатель):

mov eax, [ecx] ; Read vtable pointer
lea edx, [eax + 4 * function_index ] ; function_index being the index of function in the vtable
call edx  
1 голос
/ 03 апреля 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
...