Wskaźnik na obiekty.

0

.

0

Możesz skorzystać z narzędzia nm, służy do znajdywania adresu i nazw poszczególnych klas, funkcji itd. (Tak opisane w skrócie)
http://www.thegeekstuff.com/2012/03/linux-nm-command/
Dodatkowo polecam jeszcze użycie narzędzia objdump i gdb.

5

W teorii to nie da się. W praktyce jednak są pewne metody, które pozwalają w jakimś stopniu odtworzyć klasę, ale jest to bardzo trudne. Jak tego dokonać?
Po pierwsze, musisz uzbroić się w disassemblera, bo inaczej nie da rady. Tutaj wybór jest dosyć spory, ale ja polecam z całego serca IDA.

  1. Konstruktory.
    Konstruktory to bardzo przydatna sprawa, trzeba je tylko znaleźć. Konstruktory oczywiście będą wywoływane podczas wywoływania new, lub w konstruktorach innych klas. Jeśli podpinasz się do jakiejś gry, no to prawie na pewno w niej wszystko jest zrobione obiektowo, i z dziedziczeniem, i to często z metodami wirtualnymi. I to własnie metody wirtualne są pewnym odnośnikiem. Przede wszystkim trzeba mieć na uwadze, że różne kompilatory różny kod wypluwają, i różnie może to wyglądać, bo sposób implementacji standardu C++ nie jest określony. Ważne żeby działało tak jak ma działać.
    Jak działają metody wirtualne? Jest to bardzo prosty mechanizm, kompilator tworzy dla każdej klasy tak zwaną vtabelę (https://en.wikipedia.org/wiki/Virtual_method_table), a więc wszystkie metody wirtualne, które są w danej klasie odziedziczone lub dodane, są w takiej liście:
 
.rdata:0000000140DC9270 B0 42 0F+    off_140DC9270   dq offset sub_1400F42B0 ; DATA XREF: sub_1400F38F0+3Ao
.rdata:0000000140DC9270 40 01 00+                                            ; sub_1400F42E0+6o
.rdata:0000000140DC9278 70 32 0F+                    dq offset sub_1400F3270
.rdata:0000000140DC9280 80 32 0F+                    dq offset sub_1400F3280
.rdata:0000000140DC9288 90 32 0F+                    dq offset sub_1400F3290
.rdata:0000000140DC9290 00 86 0F+                    dq offset sub_1400F8600
.rdata:0000000140DC9298 50 43 0F+                    dq offset sub_1400F4350
.rdata:0000000140DC92A0 10 86 0F+                    dq offset sub_1400F8610
.rdata:0000000140DC92A8 20 86 0F+                    dq offset sub_1400F8620

To jest taki tylko przykład (platforma x64 / Visual C++), i tutaj adres 000140DC9270 jest kopiowany do pierwszego pola klasy, jeśli klasa posiada jakąkolwiek metodę wirtualną. No i później wywołanie polega na dobraniu się do pierwszego pola obiektu klasy, i wyciągnięciu adresu metody którą chcemy wywołać.
I właśnie te zapisanie wskaźnika do pierwszego pola ma spore znaczenie, bo możemy sobie już jakoś oznaczyć te metody w vtabeli, że należą one do danej klasy.
Jeśli zachodzi dziedziczenie, no to wywoływany jest konstruktor bazowy, i tam też jest nadawana vtabela, ale konstruktor bazowy jest wywoływany przed wpisaniem wskaźnika do vtabeli w pierwsze pole. Więc powstający obiekt zawsze ma vtabelę pochodnej klasy.

Poza tym, w konstruktorach muszą być konstruowane obiekty które są bezpośrednio w klasie. To znaczy:

class foo1 
{ 
	int a; 
public: 
	foo1() : a(5) { } 
};
class foo2 
{
	foo1 obiekt;
public:
	foo2() {}
	virtual void method();
}; 

W konstruktorze klasy foo2, zostanie wywołany konstruktor foo1 na pewnym offsecie. W tym przypadku mamy vtabelę, więc na x86 będzie to +4, a na x64 +8.
Możemy z niemal całą pewnością stwierdzić, że programiści piszący grę korzystali z arrayów, list, smart pointerów itd., a więc w np:

class foo
{
	std::vector<int> member;
};

Jeśli std::vector ma metodę wirtualną, no to std::vector<int>::vector (konstruktor), nada vtabelę na pewnym offsecie w obiekcie klasy foo. + kilka pól zostanie pewnie na wskaźnik na dane, ilość elementów, itd. itd.
W przypadku smart pointerów, implementacja std::unique_ptr zeżre prawdopodobnie miejsce tylko na wskaźnik.

class foo
{
	std::unique_ptr<int> member;	
};

I tutaj w konstruktorze klasy foo, w przypadku zinlineowania konstruktora klasy std::unique_ptr<int> zostanie na pewnym offsecie nadane 0. I tyle.
Ale w przypadku np. std::shared_ptr, zależnie od implementacji może być pointer, i wskaźnik na referencje. A więc 2 pola x sizeof(void *).

Należy jednak pamiętać, że implementacje są przeróżne, i to zależy. Jest wysoko prawdopodobne, że programiści gry sami napisali własnego STLa, i wtedy trzeba się domyślać samemu co mogą oznaczać poszczególne pola w klasie smart pointera / vectora / listy / whatever. Systemy czasami mogą być bardzo skomplikowane patrząc od zewnątrz i zgadnięcie jak to może wyglądać w prawdziwym kodzie może potrwać bardzo dużo czasu.

  1. Destruktory.
    Destruktory niszczą obiekt, ale również niszczą obiekty znajdujące się w klasie.
    Jeśli mamy doczynienia ze smart pointerem, no to w destruktorze klasy:
class foo
{
	std::unique_ptr<int> member;	
};

musi się znajdować destruktor std::unique_ptr<int>. I tutaj sprawa jest całkiem trywialna, bo niemal od razu widać, że na pewnym offsecie znajduje się smart pointer, bo jest wołany delete przed destruktor std::unique_ptr<int> i możliwe, że jest nadawane nullptr. W przypadku std::shared_ptr, zostanie zmniejszona referencja, i jeśli wynosi 0, no to wołany jest delete lub delete[]. I tutaj również destruktor może być zinlineowany.

Jak znaleźć destruktor? W przypadku kiedy destruktor jest wirtualny, no to znajduje się on na offsecie 0 w vtabeli klasy. Jeśli nie jest virtualny, to jest to zwykła metoda, której znalezienie może potrwać. W VC++ zwykle destruktor przyjmuje boola, który określa, czy destruktor ma usunąć swój obiekt, czy nie.

  1. Metody.
    Metody to tak na prawdę funkcje, które za pierwszy parametr przyjmują wskaźnik do obiektu (this). W przypadku metod wirtualnych no to jak pisałem wyżej, znalezienie ich trudne nie jest, ale jeśli metody są niewirtualne, no to sprawa robi się już bardzo skomplikowana. Tutaj trzeba będzie już analizować kod, aby dojść do tego, czy metoda należy do klasy lub też nie.

Jeśli mamy coś takiego:

class foo1
{
public:
	void method1() { /* ... */ }
	virtual void method2() { /* ... */ }
};
class foo2
{
public:
	void method1() { /* ... */ }
	virtual void method2() { /* ... */ }
};
void some_func()
{
	foo1 obiekt1;
	foo2 obiekt2;
	obiekt2.method1();
}

No to w funkcji zostaną skonstruowane dwa obiekty (nadane zostaną inne vtabele), a tylko na jednym zostanie wykonana metoda. I po offsecie, który idzie do metody w pierwszym argumencie można dojść do tego do kogo należy metoda niewirtualna. Ale jak się nazywa? Nie wiadomo. Jeśli w tej metodzie są wykonywane pewne rzeczy które jednoznacznie wskazują na jej nazwę, no to możemy jej się domyślić. Jednak jeśli jest tam wykonywane pierdyliard metod wirtualnych, no to będzie trudno.

  1. Stringi.
    Jakiekolwiek stringi są naszym sprzymierzeńcem. Jak programiści używają assertów, które wyświetlają w której metodzie doszło do asserta no to lepiej być nie może. Jedynie problem może polegać na tym, że metoda jest zinlineowana, i wtedy string odnosi się do tej zinlineowanej metody co nam nic nie da.
    Ale jednak, w większości sporo nam to da, i sporo może nam powiedzieć o budowie kodu. Poza tym, programiści mogą printować do logów przeróżne rzeczy, które pomogą nam ustalić nazwę metody. Jeśli przykładowo funkcja do logu printuje: "[dx9] NVAPI initialized", no to mamy inicjalizację directx.

  2. RTTI - dynamic_cast<>
    RTTI to jest prawdziwa skarbnica wiedzy. W wielu miejscach może zajść potrzeba castowania do klas pochodnych, i jakoś trzeba sprawdzić, czy dobrze castujemy. Tutaj twórcy mogą wykorzystać własny mechanizm, lub RTTI i dynamic_cast<>. Twórcy częściej wykorzystują RTTI, i mając RTTI masz część nazw klas, ich dziedziczenie, vtabele. Jeśli twórcy wykorzystują swój własny mechanizm, no to sprawa jest skomplikowana, bo byś musiał odgadnąć jak ich ten mechanizm działa. Odgadnąć może być złym słowem, bo w praktyce po prostu analizujesz kod. Lub ewentualnie twórcy w ogóle nie korzystają z takiego mechanizmu, no to wtedy nie masz zupełnie nic.

  3. SDK
    Czasami twórcy udostępniają SDK, gdzie klasy mogą być niemal identyczne jak w samej grze. Mam tu na myśli Valve, i ich SDK do Source. SDK udostępnione przez innych twórców, mogą być zupełnie inaczej napisane, i pamięć może być zupełnie wydzielona.

Jakiekolwiek narzędzia z postu powyżej raczej nic nie pomogą, bo sprawa jest nietrywialna.

Jeśli chodzi o nazwy pól w klasie - w żaden sposób nie możesz wprost się dowiedzieć jakie one na prawdę są. Możesz jedynie się domyślić na podstawie typu, wykorzystania, itd.

Rozmiar klasy można określić na podstawie kilku rzeczy:

  • konstruktora (największy offset użyty w konstruktorze, może być pewnym miernikiem rozmiaru, ale liczba będzie prawdopodobnie nieco zaniżona)
  • operatora new użytego podczas tworzenia obiektu - jest to pewnik, liczba która jest argumentem operatora new, musi być rozmiarem klasy. Ale trzeba brać pod ewentualność, że tworzonych jest kilka obiektów na raz, ale trzeba się wtedy spodziewać wielokrotnego wywołania konstruktora.
  • jeśli obiekt klasy znajduje się w innym obiekcie bezpośrednio, no to prawdopodobnie następny offset, który jest nadpisywany po konstruktorze obiektu, jest już polem następny, i wtedy trzeba tylko odjąć ten offset, od offsetu, na którym konstruowany jest obiekt.
0
J0ras napisał(a):

Przykładowo, nie mam headera classy jakiejś, ale mam jej wskaźnik i chciałbym go podmapować jakoś tzn. podłożyć co bym potrzebował, przesunięcia na jej metody? i jak to połączyć? i czy dobrze to wytłumaczyłem?

Przecież finalne - te wykonywalne programy, nie są w żaden sposób obiektowe,
no bo nie ma procesorów obiektowych. :)

0

Odnośnie metod w klasach: one nie mają żadnych przesunięć, lokalizacji w programie wynikowym!
To są zwyczajne procedury, czyli skoki do podprogramów, które mogą leżeć w dowolnym miejscu całego kodu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1