Entwickler-Ecke
Delphi Language (Object-Pascal) / CLX - Objekte, VMT, Interfaces,... Wie funktionierts?
reimo - Do 02.10.08 14:27
Titel: Objekte, VMT, Interfaces,... Wie funktionierts?
Ich schaue mir gerade in Delphi Interfaces an und möchte diese auch nutzen, bin aber leider von einer normalen Funktionsweise abgekommen und versuche die funktionsweise dahinter zu verstehen.
Kann mir vielleicht jemand erklären oder einen Link zu einem Tutorial schicken, wie folgendes funktioniert:
Wie wird ein Objekt einer Klasse im Speicher angelegt und was wird alles noch versteckt angelegt wird (VMT,..)?
Was sind Metaklassen?
Wie werden die Interfaces intern gehandel? Wie ist der Aufbau der Interfacetabellen.
Funktioniert es ähnlich wie in C++:
Hier enthält jedes Objekt einer Klasse einen Pointer (den vtab-Pointer), der auf die Virtuelle Tabelle dieser Klasse zeigt, in welcher die Adressen aller virtuellen Funktionen abgelegt sind.
Jede abgeleitete Klasse erbt die VMT – Tabelle von der Basisklasse und ersetzt die Adressen der Funktionen, die sie überschrieben hat, mit ihren eigenen.
Der vtabPointer liegt je nach Compiler am Anfang bzw. am Ende des Objektes.
Kann mir jetzt jemand ungefähr erklären, wie dies bei Delphi funktioniert? Inkl. Interfaces, virtueller Methoden, usw.
Was beinhaltet zum Beispiel die Interfacetable? Sind da nur die verwendeten Interface aufgeführt, oder auch Adressen auf die Implementierungen der Funktionen dieses Interfaces?
Oder werden sowohl die Adressen der virtuellen alsauch die der Interfaces – Methoden in der VMT gespeichert?
Habe die folgende Tabelle in System.Pas gefunden, was beschreibt sie?
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
| vmtSelfPtr = -76; vmtIntfTable = -72; vmtAutoTable = -68; vmtInitTable = -64; vmtTypeInfo = -60; vmtFieldTable = -56; vmtMethodTable = -52; vmtDynamicTable = -48; vmtClassName = -44; vmtInstanceSize = -40; vmtParent = -36; vmtSafeCallException = -32; vmtAfterConstruction = -28; vmtBeforeDestruction = -24; vmtDispatch = -20; vmtDefaultHandler = -16; vmtNewInstance = -12; vmtFreeInstance = -8; vmtDestroy = -4;
vmtQueryInterface = 0; vmtAddRef = 4; vmtRelease = 8; vmtCreateObject = 12; |
Sind das die Offsets, an welcher Stelle zB der Interface-Table – Pointer steht? Also wenn meine Klasse die Adresse $1234567 hat, so ist der interfaceTablePointer an der Stelle $1234567 + vmtIntfTable = $1234567 – 76! Stimmt das?
Gibt es zu dem ganzen eine erklärung?
Moderiert von
AXMD: Beiträge zusammengefügt
So, habe mich etwas rumgespielt und folgendes festgestellt:
Wenn ich ein Objekt erstelle, dann zeigt die Klassenreferenz auf einen anderen Speicherbereich, als eine Interfacereferenz auf das gleiche Objekt. Also folgendes:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
| TClasse1 = class(TInterfacedObject, IMy1)
Var Objekt1: TClass1; pMyIF: IMy1;
begin Objekt1 := TClasse1.Create; pMyIF:= Objekt1; end; |
In meinem Versuch hatte Objekt1 den Wert $96548C
Wenn ich mir jetzt die Variable im Watch-Fenster anschaue, so wird dort folgendes angeiezgt:
pMyIF: TClasse1($96548C) as IMy1
Erzwinge ich aber den Wert durch eine Typenumwandlung, dann wird mir für pIF folgendes angezeigt:
Pointer(pMyIF): 0x9654A0
Das sind 20 Byte über der Adresse des Objektes, oder wenn ich auf das Objekt über eine Klassenvariable drauf zugreife.
WORAUF greift der Zeiger zu? Was ist an dieser Speicherstelle? Ist das die Adresse des Pointers zur Interfacetabelle?
Hier mal mein Probecode inklusive Speicherangaben:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
| IMy1 = interface ['{3ABEA040-F5B4-4913-909C-FECCB930F056}'] function Test1: Integer; procedure SetV( v: Integer ); function GetV: Integer; end;
TClass1 = class( TInterfacedObject, IMy1 ) public Var1: Byte; Var2: Integer;
function Test1: Integer; procedure SetV( v: Integer ); constructor Create; destructor Destroy; override; function GetV: Integer; end;
var Objkt: TClass1; VMyI: IMy1; VUnknown: IUnknown;
begin Objkt := TClass1.Create; VMyI := Objkt; VUnknown := Objkt; .... end; |
Und hier der Speicher in aufsteigender Reihenfolge:
0x96548C -> Pointer(Objkt)
0x965494 -> Pointer(VUnknown) ( +8 Byte )
0x965498 -> @Objkt.Var1 ( +4 Byte (nächste 32Bit Adresse) )
0x96549C -> @Objkt.Var2 ( +4 Byte (nächste 32Bit Adresse) )
0x9654A0 -> Pointer(VMyI) ( +4 Byte (nächste 32Bit Adresse) )
Daraus entnehme ich, dass eine Interfacevariable(VMyI) nicht auf die Objektadresse zeigt sondern auf die Adresse einer bestimmten Klassenvariable. Nehme an, es handelt sich dabei um die Adresse des Interface-Table Pointers! STIMMT DAS?
BenBE - Fr 03.10.08 13:28
Die von dir gefundene Tabelle beschreibt die Offsets in einer Tabelle, auf die die Objekt-Instanz zeigt. Wenn Du also von dem dort eingetragenen Pointer um die angegebene Anzahl Bytes vorwärts gehst, gelangst Du zu einem Eintrag, der die in der Tabelle beschriebene Funktion\Angabe enthält. Das kann je nach Information wieder eine Tabelle sein, oder ein Methoden-Zeiger oder ein Zeiger auf eine anders geartete Struktur.
Das gesamt Speicherlayout hab ich auch nicht im Kopf. Bei Interfaces hab ich noch nicht in den Memory-Dump geschaut, da ich das bisher nicht brauchte; da Interfaces aber ähnlich wie Objekte funktionieren, ist deren Speicheraufbau auch ähnlich. Wie gesagt, müsste ich da aber auch erst reinschauen ...
P.S.: Wozu brauch man sowas eigentlich?
reimo - Sa 04.10.08 00:01
BenBE hat folgendes geschrieben : |
Die von dir gefundene Tabelle beschreibt die Offsets in einer Tabelle, auf die die Objekt-Instanz zeigt. Wenn Du also von dem dort eingetragenen Pointer um die angegebene Anzahl Bytes vorwärts gehst, gelangst Du zu einem Eintrag, der die in der Tabelle beschriebene Funktion\Angabe enthält. Das kann je nach Information wieder eine Tabelle sein, oder ein Methoden-Zeiger oder ein Zeiger auf eine anders geartete Struktur.
|
Das ist mir schon klar, dass an dieser Stelle meine Variablen, Pointer, usw drinnen stehen. Das ist aber nicht alles, denn der compiler fügt eben auch bestimmten Code in das Objekt ein, und genau um diesen geht es mir. Was alles wird wann und wohin hinzugefügt, damit Interfaces, virtuelle Methoden usw. funktionieren.
Bin auch schon dahinter gekommen, aber dazu gleich.
Zitat: |
Das gesamt Speicherlayout hab ich auch nicht im Kopf. Bei Interfaces hab ich noch nicht in den Memory-Dump geschaut, da ich das bisher nicht brauchte; da Interfaces aber ähnlich wie Objekte funktionieren, ist deren Speicheraufbau auch ähnlich. Wie gesagt, müsste ich da aber auch erst reinschauen ...
|
Naja, nicht ganz! Du vergleichst hier Interfaces mit Objekten und das sind zwei paar Schuhe! Interfaces haben darstellungsmäßig nichts mit Klassen oder Objekten gemeinsam (Bitte um Korrektur, falls ich falsch liegen sollte). Sie schreiben einer Klasse einfach nur vor, welche Funktionen implementiert sein müssen. Der Zugriff auf diese Implementierungen (wenn er über Interfacevariable erfolgt) unterscheidet sich erheblich von einem direkten Methodenaufruf.
Zitat: |
P.S.: Wozu brauch man sowas eigentlich?
|
hmmm, ich weiss nicht! Es Interessiert mich einfach! Außerdem, wenn ich weiss, wie es funktioniert, kann ich sicher irgendwie draus einen nutzen für meine eigenen Programme ziehen.
So, habe etwas Assemblercode gelesen, und dies ist mein derzeitiger Wissensstand (beschränkt sich derzeit auf Klassen mit Interfaces und virtuellen Methoden. Schon möglich, dass bei Verwendung anderer Konstrukte, sich daran etwas ändert).
Ein erzeugtes Objekt schaut im Speicher (Datensegment) folgendemassen aus:
Quelltext
1: 2: 3: 4: 5: 6: 7:
| // Adresse(bzw Offset) Beispieldaten Funktion
0x000000 0x0043DCA4 Das erste DWord enthält die Anfangsadresse seiner Klasse im Codesegment 0x000004 0x00760000 Der Inhalt der ersten Variable des Objekts 0x000008 0x00760000 Der Inhalt der zweiten Variable des Objekts ..... 0x00000n 0x0043DC1E Ein Pointer auf die VTable des implementierten Interfaces |
Dadurch, dass an erster Stelle eines Objekts die Klassenreferenz steht, lässt sich ganz einfach ein Pointer auf die eigene Klasse abfragen:
Delphi-Quelltext
1:
| tClassVar := TClass( Pointer(MeinObjekt)^ ); |
nichts anderes macht auch die Klassenmethode "TObject.ClassType"
Jetzt mal zum Codesegment. Hier muss ich meinen vorigen Post berichtigen bezüglich der Pointeroffsets (vmtIntfTable,...)! Diese beziehen sich nämlich nicht auf das Objekt, sondern auf die KLASSE!!!
Also liegt eine Klasse wie im oberen beispiel auf der Adresse 0x0043DCA4, so liegt der dazugehörige InterfaceTable-Pointer auf der Adresse
Quelltext
1:
| 0x0043DCA4 - vmtIntfTable = 0x0043DCA4 - 72 = 0x0043DC5C |
Wird jetzt die funktion "TObject.GetInterfaceTable" aufgerufen, so macht sie nichts anderes, als diesen Pointer zurück zu liefern.
Wäre das gleiche wie:
Delphi-Quelltext
1:
| ((PInteger(MeinObjekt)^) - 72)^ |
Ein Eintrag der in der InterfaceTable sieht folgendermassen aus:
Delphi-Quelltext
1: 2: 3: 4: 5: 6:
| Entry = record ID: TGUID; VTable: Pointer IOffset: Integer; ImplGetter: Integer; end; |
Greift man jetzt über eine InterfaceVariable auf ein Objekt zu:
so zeigt der Pointer nicht auf die anfangsadresse des Objektes, sondern auf die Adresse des Pointers auf die VTable. Im oberen beispiel würde myIFVar auf die Adresse "0x00000n" zeigen!
Dereferenziert man diesen Pointer, bekommt man die Adresse: "0x0043DC1E"!
Diese Adresse ist die gleiche, auf welche auch der Pointer "VTable" in der InterfaceTable zeigt (für das gleiche Interface natürlich):
Delphi-Quelltext
1:
| (MyObjekt.GetInterfaceEntry)^.VTable = PInteger(myIFVar)^ |
Ruft man jetzt eine Funktion über die Interfacevariable auf:
so dereferenziert der Compiler einfach myIFVar (bekommt dadurch einen Pointer auf eine Funktionstabelle) und addiert einfach den Offset der funktion dazu:
myIFVar^ + FunktionsOffset = Adresse in der Tabelle!
auf dieser Adresse sollte dann die Adresse der Funktion "Funktion1" stehen.
Es ist nicht ganz die direkte Einsprungsadresse zur Funktion, sondern zu einer kurzen Zwischenberechnung, die ich mir noch nicht genauer angeschaut habe.
Die
virtuellen Methoden dagegen funktionieren wieder etwas anders:
Der Pointer auf die Virtual-Method Table liegt am Offset 0 der Klasse im Codesegment, das heisst wenn wir von einem Objekt ausgehen sieht es folgendermassen aus:
Über
bekommen wir die Adresse der Klasse von Mein Objekt. An dieser Stelle, also an der ersten Stelle einer Klasse im Codesegment steht die Adresse der VMT. Wird diese dereferenziert, so bekommen wir den ersten Eintrag in dieser, also die Adresse der ersten funktion.
(IPointer(IPointer(MeinObjekt)^) + FunktionsOffset)^ ... Adressen der virtuellen Funktionen einer Klasse
So, das reicht für heute! Es ist halt das, was ich durch Debugging und Assemblercode Lesen herausgefunden hab. Ich hab keine Ahnung, wie viel davon Blödsinn ist, also falls ich irgendwo Schwachsinn geschrieben hab, dann schreibts mir bitte bescheid!
Allesquarks - So 05.10.08 19:00
Hm du stellst irgendwie keine Fragen ist schwierig darauf zu antworten.
Ich habe vor einiger Zeit ähnlich wie du mal mit interfaces und assembler rumgespielt und hätte jetzt gedacht, dass Interfacevariable und Objekt zeiger auf den gleichen Beriech verweisen. Die einzige Erklärung, die mir auf die schnelle zufliegt ist, dass eine Klasse ja mehrere Interfaces unterstützen kann und deshalb da so ein Versatz existiert. In diesem Zusammenhang dürfte eben genau der Assemblercode von Queryinterface glaube ich für dich interessant sein.
Ansonsten ist es gar nicht so schlecht immer bei den abstrakten Klassen abzuschauen (ich denke das meinte BenBE mit den Objekten) also bei den VMT, weil es ist zwar nicht ganz das gleiche doch extrem ähnlich. Der große unterschied ist glaube ich dass man bei interfaces DMTOffset verwenden muss und eine Klasse halt sozusagen mehrere "VMT's=interfaces" haben kann. Über die Suchbegriffe dynamic und virtual findest du da vlt auch mal was zum lesen und musst das nicht alles reverse engeneeren.
BenBE - So 05.10.08 19:38
Allesquarks hat folgendes geschrieben : |
Hm du stellst irgendwie keine Fragen ist schwierig darauf zu antworten. |
Wieso? Die Frage ist doch, eindeutig: "Wie funktionieren Klassen und Interfaces?" ... Ich weiß nicht, was da dran unklar ist :mrgreen:
Allesquarks hat folgendes geschrieben : |
Ich habe vor einiger Zeit ähnlich wie du mal mit interfaces und assembler rumgespielt und hätte jetzt gedacht, dass Interfacevariable und Objekt zeiger auf den gleichen Beriech verweisen. |
Das kann rein logisch schon nicht passen, da an Offset 0 ja ein Zeiger auf die RTTI steht. Wenn also beide Zeiger identisch wären, woran sollte ein Laufzeit-System dann unterscheiden können, welche konkreten Implementierungen zu nutzen sind.
Außerdem kommt hier noch ein ganz anderer Punkt hinzu: Man kann ja wie gesagt mehrere Interfaces in einer Klasse deklarieren. Was wenige wissen: Man muss dabei aber nicht mal die Namen beibehalten. Man kann also eine Methode, die eigentlich Foo heißt als Bar im Interface implementieren. Zudem muss die Ordnung der Methoden in einer Klasse nicht der Ordnung im Interface entsprechen, ... Damit also das Aufrufen über das Interface unabhängig von der Klasse immer mit den gleichen Offsets (in welchen Strukturen auch immer) möglich ist, muss die Anordnung der Interfacemethoden unabhängig von der Anordnung in einer beliebigen Klasse sein. Das geht nicht, wenn Interfaceinstanzzeiger und Klasseninstanz-Zeiger identisch wären.
Allesquarks hat folgendes geschrieben : |
Die einzige Erklärung, die mir auf die schnelle zufliegt ist, dass eine Klasse ja mehrere Interfaces unterstützen kann und deshalb da so ein Versatz existiert. In diesem Zusammenhang dürfte eben genau der Assemblercode von Queryinterface glaube ich für dich interessant sein.
Ansonsten ist es gar nicht so schlecht immer bei den abstrakten Klassen abzuschauen (ich denke das meinte BenBE mit den Objekten) also bei den VMT, weil es ist zwar nicht ganz das gleiche doch extrem ähnlich. |
Das mit dem Versatz bekommt man schon beim Implementieren eines einzigen Interfaces rein, da unterschiedliche Klassen jedes Interface unterschiedlich implementieren können.
Und jain ... Abstrakte Klassen sind genauso wie normale Klassen auch. der einzige Unterschied liegt darin begründet, dass Abstrakte Methoden in der VMT auf eine Pseudo-Prozedur mit einem Raise-Exceoption zeigen, während diese Aufgabe bei dynamischen Methoden System.@CallDynInst übernimmt ...
Allesquarks hat folgendes geschrieben : |
Der große unterschied ist glaube ich dass man bei interfaces DMTOffset verwenden muss und eine Klasse halt sozusagen mehrere "VMT's=interfaces" haben kann. |
1. Heißt das Teil Dynamic Method Index
2. Ist das kein Offset, sondern ein Index ;-)
3. Gibt's das Teil nicht für Interfaces, sondern für (wie's der Name sagt) dynamische Methoden.
Das ist auch der Unterschied, warum man für schnellen Code virtuelle Methoden und für geringen RAM-Verbrauch dynamische Methoden verwendet.
Bei virtuellen Methoden wird einfach ein fester Offset (VMT-Offset) zur Objekt-Instanz ergänzt. Bei dynamischen Methoden wird in jeder Klasse eine Tabelle geführt, die nur die überschriebenen Methoden enthält. Von der Elternklasse wird nun solange in die Vererbungsgeschichte zur spezielleren Klasse gesucht, bis man den Eintrag mit dem gewünschten Index findet.
Allesquarks hat folgendes geschrieben : |
Über die Suchbegriffe dynamic und virtual findest du da vlt auch mal was zum lesen und musst das nicht alles reverse engeneeren. |
Wirklich viel steht nicht mal in der Delphi-Hilfe ... Die Object Pascal Reference kannst Du hier ausnahmsweise auch mal Knicken. Hier helfen einzig und allein CPU-Fenster und System.pas ;-)
Allesquarks - Mo 06.10.08 20:04
Ja ja ist halt schon ein Jahr oder länger her, dass ich damit rumgespielt habe, deshalb waren meine Vorschläge auch mehr als Anregungen gedacht. Schade, dass ich damit offenbar nicht helfen konnte.
Entwickler-Ecke.de based on phpBB
Copyright 2002 - 2011 by Tino Teuber, Copyright 2011 - 2025 by Christian Stelzmann Alle Rechte vorbehalten.
Alle Beiträge stammen von dritten Personen und dürfen geltendes Recht nicht verletzen.
Entwickler-Ecke und die zugehörigen Webseiten distanzieren sich ausdrücklich von Fremdinhalten jeglicher Art!