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ówne 1, 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ówne 4, ponieważ jest to niejawny wskaźnik!).
      Zastanawiałeś się może kiedyś, czemu otrzymujesz Access Violation przy próbie odczytu pola z nieutworzonego obiektu?
      Jeżeli obiekt nie jest utworzony, jest duża doza prawdopodobieństwa, że wskazuje na nil (nie musi, ale w większości tak jest).
      Więc odczytując pierwsze pole, odczytujesz pamięć znajdującą się pod adresem 0+4, w której są "śmieci", stąd Access Violation.
      Ale wracając:
      Jak (biorąc pod uwagę to, co przed chwilą powiedziałem) odczytać pole A nie tworząc osobnej klasy i nie korzystając z konstrukcji nazwa_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):
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).

5 komentarzy

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źć).