So verschieben Sie Parameter in Methoden ohne Parameter im sicheren Code

Hallo Diesmal lachen wir weiter über den normalen Methodenaufruf. Ich schlage vor, sich mit dem Methodenaufruf mit Parametern vertraut zu machen, ohne Parameter zu übergeben. Wir werden auch versuchen, den Referenztyp in eine Zahl umzuwandeln - seine Adresse, ohne Zeiger und unsicheren Code zu verwenden .


Haftungsausschluss


Bevor Sie mit der Geschichte fortfahren, empfehle ich Ihnen dringend, den vorherigen Beitrag über StructLayout zu lesen . Hier werde ich einige Funktionen verwenden, die dort beschrieben wurden.

Außerdem möchte ich warnen, dass dieser Artikel kein Material enthält, das in realen Projekten verwendet werden sollte.

Einige erste Informationen


Bevor wir mit dem Üben beginnen, erinnern wir uns daran, wie der C # -Code in Assembler-Code konvertiert wird.
Lassen Sie uns ein einfaches Beispiel untersuchen.

public class Helper { public virtual void Foo(int param) { } } public class Program { public void Main() { Helper helper = new Helper(); var param = 5; helper.Foo(param); } } 

Dieser Code enthält nichts Schwieriges, aber die von JiT generierten Anweisungen enthalten mehrere wichtige Punkte. Ich schlage vor, nur ein kleines Fragment des generierten Codes zu betrachten. In meinen Beispielen werde ich Assembler-Code für 32-Bit-Maschinen verwenden.

 1: mov dword [ebp-0x8], 0x5 2: mov ecx, [ebp-0xc] 3: mov edx, [ebp-0x8] 4: mov eax, [ecx] 5: mov eax, [eax+0x28] 6: call dword [eax+0x10] 

In diesem kleinen Beispiel können Sie die Fastcall-Aufrufkonvention beobachten, bei der Register zum Übergeben von Parametern verwendet werden (die ersten beiden Parameter von links nach rechts in den Registern ecx und edx) und die verbleibenden Parameter von rechts nach links durch den Stapel geleitet werden. Der erste (implizite) Parameter ist die Adresse der Instanz der Klasse, für die die Methode aufgerufen wird (für nicht statische Methoden).

In unserem Fall ist der erste Parameter die Adresse der Instanz, der zweite unser int- Wert.

In der ersten Zeile sehen wir also die lokale Variable 5, hier gibt es nichts Interessantes.
In der zweiten Zeile kopieren wir die Adresse der Helper-Instanz in das ecx-Register. Dies ist die Adresse des Zeigers auf die Methodentabelle.
In der dritten Zeile wird die lokale Variable 5 in das edx-Register kopiert
In der vierten Zeile sehen wir das Kopieren der Methodentabellenadresse in das eax-Register
Die fünfte Zeile enthält das Laden des Werts aus dem Speicher an die Adresse, die 40 Byte größer ist als die Adresse der Methodentabelle: der Beginn der Methodenadressen in der Methodentabelle. (Die Methodentabelle enthält verschiedene Informationen, die zuvor gespeichert wurden. Zum Beispiel die Adresse der Basisklassen-Methodentabelle, die EEClass-Adresse, verschiedene Flags, einschließlich des Garbage Collector-Flags usw.) Somit wird die Adresse der ersten Methode aus der Methodentabelle nun im eax-Register gespeichert.
Hinweis: In .NET Core wurde das Layout der Methodentabelle geändert. Jetzt gibt es ein Feld (mit 32/64 Bit Offset für 32- bzw. 64-Bit-Systeme), das die Adresse des Beginns der Methodenliste enthält.
In der sechsten Zeile wird die Methode von Anfang an mit Offset 16 aufgerufen, dh mit der fünften in der Methodentabelle. Warum ist unsere einzige Methode die fünfte? Ich erinnere Sie daran, dass das Objekt 4 virtuelle Methoden hat ( ToString (), Equals (), GetHashCode () und Finalize () ), die alle Klassen haben werden.

Gehe zu Übung;


Praktisch:
Es ist Zeit, eine kleine Demonstration zu starten. Ich schlage einen so kleinen Rohling vor (sehr ähnlich dem Rohling aus dem vorherigen Artikel).

  [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public Test1 Test1; [FieldOffset(0)] public Test2 Test2; } public class Test1 { public virtual int Useless(int param) { Console.WriteLine(param); return param; } } public class Test2 { public virtual int Useless() { return 888; } } public class Stub { public void Foo(int stub) { } } 

Und lassen Sie uns das Zeug so verwenden:

  class Program { static void Main(string[] args) { Test2 fake = new CustomStructWithLayout { Test2 = new Test2(), Test1 = new Test1() }.Test2; Stub bar = new Stub(); int param = 55555; bar.Foo(param); fake.Useless(); Console.Read(); } } 

Wie Sie vielleicht erraten haben, wird aus den Erfahrungen des vorherigen Artikels die nutzlose (int j) Methode vom Typ Test1 aufgerufen.

Aber was wird angezeigt? Ich glaube, der aufmerksame Leser hat diese Frage bereits beantwortet. "55555" wird auf der Konsole angezeigt.

Aber schauen wir uns noch die generierten Codefragmente an.

  mov ecx, [ebp-0x20] mov edx, [ebp-0x10] cmp [ecx], ecx call Stub.Foo(Int32) mov ecx, [ebp-0x1c] mov eax, [ecx] mov eax, [eax+0x28] call dword [eax+0x10] 

Ich denke, Sie erkennen das Aufrufmuster der virtuellen Methode, es beginnt nach dem Aufruf von Stub.Foo (Int32) . Wie wir sehen können, wird ecx erwartungsgemäß mit der Adresse der Instanz gefüllt, auf der die Methode aufgerufen wird. Da der Compiler jedoch der Meinung ist, dass wir eine Methode vom Typ Test2 aufrufen, die keine Parameter enthält, wird nichts in edx geschrieben. Wir haben jedoch vorher einen anderen Methodenaufruf. Und dort haben wir edx verwendet, um Parameter zu übergeben. Und natürlich haben wir keine Anweisungen, diese klare Edx. Wie Sie in der Konsolenausgabe sehen können, wurde der vorherige edx-Wert verwendet.

Es gibt noch eine andere interessante Nuance. Ich habe speziell den aussagekräftigen Typ verwendet. Ich schlage vor, den Parametertyp der Foo-Methode des Stub-Typs durch einen beliebigen Referenztyp zu ersetzen, z. B. eine Zeichenfolge. Der Parametertyp der Methode Useless () ändert sich jedoch nicht. Unten sehen Sie das Ergebnis auf meinem Computer mit einigen klarstellenden Informationen: WinDBG und Rechner :)


Klickbares Bild

Das Ausgabefenster zeigt die Adresse des Referenztyps in Dezimalschreibweise an.

Insgesamt


Wir haben das Wissen über das Aufrufen von Methoden mithilfe der Fastcall-Konvention aktualisiert und sofort das wunderbare edx-Register verwendet, um einen Parameter in zwei Methoden gleichzeitig zu übergeben. Wir haben auch auf alle Typen gespuckt und mit dem Wissen, dass alles nur Bytes sind, leicht die Adresse des Objekts erhalten, ohne Zeiger und unsicheren Code zu verwenden. Außerdem habe ich vor, die empfangene Adresse für noch weniger anwendbare Zwecke zu verwenden!

Danke für die Aufmerksamkeit!

PS C # -Code finden Sie hier

Source: https://habr.com/ru/post/de447254/


All Articles