Przede wszystkim dla tych co nie wiedzą czym jest funkcja wirtualna źródła: [1],[2] i [3].
O tym jak działa wywołanie funkcji i jak istotną rolę w tym procederze pełni stos napisałem tutaj.
Przykładowy kawałek kodu w C++:
class A {
public:
virtual void fun();
__declspec(noinline) void fun2();
};
class B : public A {
public:
virtual void fun();
};
int main(int argc, char * argv[])
{
A * a = new A();
A * b = new B();
a->fun();
b->fun();
a->fun2();
b->fun2();
}
Wywołania klasyczne:
; 36 : a->fun2();
mov eax, edi
call ?fun2@A@@QAEXXZ ; A::fun2
; 37 : b->fun2();
mov eax, esi
call ?fun2@A@@QAEXXZ ; A::fun2
Wywołania wirtualne:
Po kompilacji:
; 26 : a->fun();
mov eax, DWORD PTR [edi]
mov edx, DWORD PTR [eax+4]
mov ecx, edi
call edx
; 27 : b->fun();
mov eax, DWORD PTR [esi]
mov edx, DWORD PTR [eax+4]
mov ecx, esi
call edx
Przy rozwiązaniu statycznym:
1. do rejestru eax ładowany jest adres obiektu, czyli wskaźnik this,
2. wykonywana jest instrukcja call z adresem funkcji fun2.
W przypadku wirtualnym:
1. do rejestru eax ładowane są pierwsze 4 bajty obiektu,
2. do rejestru edx ładowana jest pamięć spod adresu eax + 4,
3. do rejestru ecx ładowany jest adres obiektu, czyli wskaźnik this,
4. wykonywana jest instrukcja call z zawartością rejestru edx.
Na co tu zwrócić uwagę?
1. przy wywołaniu statycznym adres funkcji jest na stałe zapisany w kodzie,
2. przy wywołaniu wirtualnym adres funkcji jest ładowany z pamięci,
3. adres funkcji wirtualnej znajduje się w tablicy metod wirtualnych
4. adres tej tabeli jest zapisany jako pierwsze pole w obiekcie.
Tak to wygląda na stosie:
EBP -0xXX -0x4 0x0 +0x4 +0x8 | Co lokalne lokalne oldBP retAddr Parametry funkcji | Rejestr <-ESP <-EBP |
Dokładnie tak samo jak w przypadku statycznym.
Pod adresem powrotnym znajduje się opcode ff To jest dwubajtowa instrukcja. Jeden na ff i jeden na parametr. Dokładny opis wszystkich instrukcji i parametrów można znaleźć w [4] i [5].
Jak te informacje zamienić na adres funkcji ?
Jeszcze nie wiem, ale mam dwa pomysły:
1. Wszystko wskazuje na to, że poszukiwana wartość zapisana jest jedynie w tablicy metod wirtualnych. Można pokusić się o analizę kodu wywołania pod kątem indeksu metody w tablicy. Mając ten indeks wystarczy odczytać [[this]+indeks].
2. Można też olać dokładny adres funkcji i zapisać sobie jedynie zawartość retAddr. Jeżeli mamy adres 0x01234568 oraz wiemy, że funkcja a - 0x01234000 i b - 0x01235000, to jasne jest, ze była to funkcja a.
Pierwsze rozwiązanie jest fajne, bo daje nam na talerzu to czego chcemy.
Drugie rozwiązanie jest fajne, bo:
a. działa nie zależnie od tego czy wywołanie jest statyczne czy dynamiczne,
b. powinno działać nie zależnie od użytego kompilatora, czy wybranych ustawień budowania
Wniosek ?
Rozwiązanie drugie jest wyborem lepszym. Jest prostsze w implementacji i bardziej uniwersalne. Będzie wymagało jednak stworzenia dodatkowego narzędzia, które zamieni zapisane dane na adresy funkcji.
Źródła:
1) Wikibooks - Funkcje wirtualne
2) Wikipedia - Metoda wirtualna
3) What is a "virtual member function"?
4) Intel Instruction Set A-M
5) Intel Instruction Set N-Z
1 komentarz:
Mysle, ze wiekszosc kompilatorow stosuje wlasnie taka implementacje, jest ona prosta i wydajna. Pierwsze pole obiektu to wskaznik na vtbales, ktora zawiera kolejno wskazniki na kolejne funkcje wirtualne, jakie powinny zostac wykonane, przy probie wywolania danej metody wirtualnej przez wskaznik lub referencje do obiektu bazowego lub dowolnego pochodnego.
struct vtbl {
void (**vtable)(A*);
};
vtbl* avt = reinterpret_cast<vtbl*>(a)
avt->vtable[0](a); // a->fun()
ABI VC i GCC w tej kwestii jest kompatybilne, kiedys poprzez wspolny interfejs, wywolywalem metody z kodu skompilowanego w jednym, w kodzie drugim.
Na tej zasadzie (w skrocie) oparty jest tez COM i jego ABI, gdzie interfejsy COM, przy uzywaniu jezyka C++, sa czysto abstrakcyjnymi klasami, a przy uzyciu w kodzie C wskaznikami na kolejne pola vtables.
Prześlij komentarz