Odczytywanie prywatnych pól w Delphi
Patryk27
Ten artykuł należy potraktować bardziej jako ciekawostkę, ponieważ jego zastosowanie jest w praktyce znikome.
Informacja: przedstawiona niżej wiedza opiera się na moich własnych obserwacjach. Zachowanie może być różne w różnych kompilatorach i/lub na różnych architekturach. Artykuł został napisany w oparciu o Delphi 7 na architekturze 32-bitowej.
Przypuśćmy, że mamy klasę "Foo", a w niej prywatne (private) pole o nazwie Value i typie String.
Type TFoo = Class
Private
Value: String;
End;
Dodajmy także konstruktor ustawiający to pole na konkretną wartość:
Type TFoo = Class
Private
Value: String;
Public
Constructor Create(fValue: String);
End;
Kod klasy należy oczywiście zapisać do osobnego modułu, niżeli sam program. Inaczej możliwy jest dostęp do prywatnych pól i metod.
I jakiś przykładowy program korzystający z tej klasy:
prog.pas:
Unit prog;
Interface
Type TFoo = Class
Private
Value: String;
Public
Constructor Create(fValue: String);
End;
Implementation
Constructor TFoo.Create(fValue: String);
Begin
Value := fValue;
End;
End.
{$APPTYPE CONSOLE}
Uses Prog;
Var Foo: TFoo;
Begin
Foo := TFoo.Create('Hello World!');
Foo.Free;
End.
Rodzi się pytanie - w jaki sposób (przypuszczając, że do konstruktora przekażemy nieznany nam ciąg znaków) odczytać i wyświetlić tę wartość?
Istnieją (przynajmniej) 2 sposoby zrobienia tego.
Uwaga: W obu sposobach kolejność pól ma znaczenie!
Sposób 1
Sposób pierwszy polega na stworzeniu klasy, w której pierwsze pole będzie typu String i będzie ono public (czyli widoczne dla nas).
Uwaga: Jeżeli przed naszym polem Value występują jakieś zmienne, musimy je zadeklarować także w TFooHack (typy także są ważne!).
Potem wystarczy rzutować TFoo na utworzoną w ten sposób klasę i voilà!
Utwórzmy sobie taką klasę (może być w pliku z klasą TFoo, może być bezpośrednio w kodzie programu, nie ma to znaczenia):
Type TFooHack = Class
Public
Value: String;
End;
I teraz, aby wyświetlić Value wystarczy rzutować TFoo na TFooHack i odczytać:
Writeln(TFooHack(Foo).Value);
Proste i logiczne.
Kod programu:
{$APPTYPE CONSOLE}
Uses Prog;
Type TFooHack = Class
Public
Value: String;
End;
Var Foo: TFoo;
Begin
Foo := TFoo.Create('Hello World!');
Writeln(TFooHack(Foo).Value);
Foo.Free;
Readln;
End.
Sposób 2
Sposób ten jest podobny do sposobu pierwszego, lecz wymaga "nieco" więcej wiedzy z zakresu tego, w jaki sposób działają klasy i sam kompilator.
Pokrótce to wytłumaczę:
Na przykłady wyjaśnienia, utwórzmy sobie klasę zawierającą jakieś 3 pola.
Niech będzie to String, Integer oraz Byte:
Type TClass = Class
Private
A: String;
B: Integer;
C: Byte;
End;
Po utworzeniu tej klasy, w pamięci będzie ona wyglądać tak:
addr+0 - 4 bajty - prawdopodobnie adres VMT
addr+4 - 4 bajty - adres zmiennej `A`
addr+8 - 4 bajty - wartość zmiennej `B`
addr+12 - 4 bajty* - wartość zmiennej `C`
addr to oczywiście adres utworzonego obiektu.
-
- pomimo, że
sizeof(Byte)jest równe1, kompilator automatycznie dopasuje rozmiar pól w pamięci do rozmiaru największego pola (pamiętaj:sizeof(String)jest w czasie kompilacji znane i równe4, ponieważ jest to niejawny wskaźnik!).
Zastanawiałeś się może kiedyś, czemu otrzymujeszAccess Violationprzy próbie odczytu pola z nieutworzonego obiektu?
Jeżeli obiekt nie jest utworzony, jest duża doza prawdopodobieństwa, że wskazuje nanil(nie musi, ale w większości tak jest).
Więc odczytując pierwsze pole, odczytujesz pamięć znajdującą się pod adresem0+4, w której są "śmieci", stądAccess Violation.
Ale wracając:
Jak (biorąc pod uwagę to, co przed chwilą powiedziałem) odczytać poleAnie tworząc osobnej klasy i nie korzystając z konstrukcjinazwa_klasy.pole?
Jest to dziecinnie proste.
Najpierw należy oczywiście utworzyć obiekt i zainicjować pola na jakieś wartości (akurat tutaj wystarczyłoby zainicjowaćA, ale przypiszemy jakieś wartości wszystkim polom od razu):
- pomimo, że
Var C: TClass;
Begin
C := TClass.Create;
C.A := 'test';
C.B := 25;
C.C := 200;
Readln;
End.
Teraz odczyt pola:
Writeln(PString(Integer(C)+4+)^);
Zastanówmy się nieco nad tym kodem i skąd te "rzeczy" się tutaj wzięły.
Jak już napisałem wcześniej - adres string'u będącego polem A znajduje się pod adresem addr+4.
Najpierw więc musimy pobrać adres klasy (tj.miejsce w pamięci; najprościej rzutując na Integer (w przypadku x64 należy rzutować na Int64) - Integer(C)) oraz dodać do niego adres pola A, czyli 4 (patrz: tabelka wyżej).
Mając już adres tego pola, należy pamiętać, że String to wskaźnik na pole w pamięci, w którym znajduje się nasz ciąg znaków.
Tak więc rzutujemy pobrany wcześniej adres na PString (PString = ^String), a z PString na String (rzutowanie z wskaźnika na typ prosty robi operator ^, w tym wypadku przed ostatnim nawiasem zamykającym).
Jeżeli wszystko poszło dobrze, powinien nam się w konsoli wyświetlić napis test.
Hurra! Odczytaliśmy pole A!
No więc teraz odczytajmy pole B; jest to równie łatwe, należy tylko pamiętać o zmianie typów (String -> Integer):
Writeln(PInteger(Integer(C)+8)^);
Odczytanie pola C nie powinno już stanowić dla Ciebie problemu.
W taki sam sposób można zapisywać także wartości do pól:
PInteger(Integer(C)+8)^ := 666;
Zmieni wartość pola B na 666.
Ale wracając do klasy TFoo - wiemy już, jak odczytać konkretne pole klasy, więc do dzieła!
Przypomnę jeszcze tylko deklarację:
Type TFoo = Class
Private
Value: String;
Public
Constructor Create(fValue: String);
End;
Pole Value jest pierwsze, tak więc jest ono osiągalne pod adresem addr+4:
Writeln(PString(Integer(Foo)+4)^);
Kod jest zrozumiały i chyba nie wymaga zbędnego tłumaczenia (czyt.wytłumaczyłem wcześniej i nie zamierzam się powtarzać).
Zakończenie
Jak już powiedziałem na początku: ten artykuł nie jest specjalnie pożyteczny w "normalnych" kodach, tylko jako ciekawostka (i poszerzenie wiedzy dotyczącej samego kompilatora).
Włącz sobie (o ile dobrze pamiętam) - {$J+}, a stałe będą się zachowywały jak zmienne :>
A to już próbowałem i działa, jednak jeśli potrzebuję zmieniać wartość to wolę wykorzystać zmienne :>
Tak btw, na siłę można w ten sposób także zmieniać stałe :>
No dość ciekawy artykuł i tak jak napisałeś - jeśli chcemy odczytać jakiekolwiek dane wystarczy znać ich adres w pamięci, reszta nie powinna przyspożyć problemów;
Mimo wszystko dobra ciekawostka :)
Jeszcze cd.
addr+0 - 4 bajty - prawdopodobnie adres VMT, prosiłbym o informację czy jest to rzeczywiście adres VMT czy jakiś inny (nie jestem pewien, a informacji w internecie o tym nie mogę znaleźć).