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 Violation
przy 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ć poleA
nie 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źć).