Laden...

Speicherung von Enum Werten im IL | Dekompilierung mit Tools

Erstellt von lutzeslife vor 6 Jahren Letzter Beitrag vor 6 Jahren 1.987 Views
L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 6 Jahren
Speicherung von Enum Werten im IL | Dekompilierung mit Tools

Hallo Community,

ich habe mal zwei Fragen:

1.) Stimmt es dass Enum Werte im IL Code als Zahl gespeichert werden und dann der entsprechende verknüpfte Wert zur Laufzeit geholt wird?

Wenn ja: 2) Wie schaffen es Dekompiler beim wiederherstellen des Codes aus dieser Zahl den korrekten Wert zu ermitteln, also so das wieder der textuelle Wert darsteht?

Hintergrund ist der: Angenommen es gibt zwei Bibliotheken A und B. B referenziert A und hat in seinem Code

Bibliothek A



public enum Test
{
Alpha = 0
Beta = 1,
Gamma =2,
}


Bibliothek B



public Test TestEnum => Test.Gamma;


Wenn mann sich den IL Code anschaut steht da nun die 1. Und daher stellt sich die Frage wie das Decompiler machen, wenn die referenzierte Bibliothek A nicht auf dem Ziel System ist.

Hintergrund ist der, dass es darum geht, aus dem Enum Test -> ein FlagsEnum zu machen, dann ändern sich aber die Zahlen ja, weil es dann 2-Potenzen sein müssen und dass müsste ja dann zu Seiteneffekten kommen, weil dann aus der 2 die in der Bibliothek B kompiliert wird zur Laufzeit vielleicht dann zu Beta wird.

  1. Gibt es da eventuell ein Möglichkeit das zu umgehen?

Beste Grüße

Mit freundlichen Grüßen
lutzeslife

16.842 Beiträge seit 2008
vor 6 Jahren

H

1.) Stimmt es dass Enum Werte im IL Code als Zahl gespeichert werden und dann der entsprechende verknüpfte Wert zur Laufzeit geholt wird?

Schau Dir den einfach den ILCode an, zB mit LinqPad oder ILSpy.

Ein Enum hat eine Ganzzahl im Hintergrund. Steht auch in der Doku.
Steht auch in der Doku - muss man nur angucken.

Braucht man nicht mal ILCode dazu. 😉

Wie schaffen es Dekompiler beim wiederherstellen des Codes aus dieser Zahl den korrekten Wert zu ermitteln, also so das wieder der textuelle Wert darsteht?

Ein Enum hat kein Text. Genau wie eine Variable nachher auch nicht über den Text sondern die Adresse identifiziert wird.

L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 6 Jahren

Das mit der ersten Frage hatte ich mir schon indirekt gedacht, aber die Frage mit dem Decompiler, wie er das macht. Wenn im IL Code nur eine Ganzzahl steht, dass beim dekombilierten Code wieder der ursprüngliche Bezeichner (z.B. Alpha) wieder steht.

Mit freundlichen Grüßen
lutzeslife

16.842 Beiträge seit 2008
vor 6 Jahren

Mh? Im ILCode stehen doch die Typen inkl. Values.
Im Fall von ENums werden daraus Konstanten.

public enum Test
{
Alpha = 0
Beta = 1,
Gamma =2,
}

Daraus wird


public const Test Alpha = 0;
public const Test Beta = 1;
public const Test Gamma = 2;

Schau Dir einfach mal den ILCode an.

L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 6 Jahren

Also ich habe noch mal geschaut aber irgendwie stehe ich auf dem Schlauch: Soweit ich das mit (ildasm) sehe wird doch nur die ganze Zahlwert gespeichert:

Das Testprojekt ist im Anhang.

Mit freundlichen Grüßen
lutzeslife

L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 6 Jahren

Hier noch das Bild

Mit freundlichen Grüßen
lutzeslife

D
261 Beiträge seit 2015
vor 6 Jahren

Du hast dein Enum aber in deiner Library definiert. Also musst du auch diese mit ildasm öffnen.

L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 6 Jahren

Das war mir schon klar, die Frage war, wie der Decompiler den Code wieder mit dem Bezeichner herstellt statt mit der ganzen Zahl. Macht er aber gar nicht


public class EnumTestClass
  {
    public EnumTest EnumTestProperty
    {
      get
      {
        return (EnumTest) 1;
      }
    }
  }

Er macht dass nur wenn auch die referenziert DLL woher der Enum stammt gefunden wird. Andernfalls wird korrekterweise die Zahl wiedergegeben. In meinem ersten Test hat er die referenzierte Lib noch gefunden und dann korrekterweise daraus


public class EnumTestClass
  {
    public EnumTest EnumTestProperty => EnumTest.Alpha;
  }

Für meine Ausgangsfrage bedeutet dass aber, dass bei einer Umstellung von einem Enum -> auf ein FlagEnum alle Bibliotheken die dieses Enum verwenden neu kompiliert werden müssen, weil sonst nicht gewährleistet werden kann, dass der richtige EnumWert zugewiesen wird.

Okay dass hat wieder Licht ins Dunkeln gebracht

Mit freundlichen Grüßen
lutzeslife

2.080 Beiträge seit 2012
vor 6 Jahren

Definiere mal ein Enum mit den Werten 1, 2 und 3. Dann erstelle eine Variable mit dem Typ dieses Enums und caste den Wert 5 in das Enum. Du wirst sehen, das geht 😉
Wenn Du diesen Wert nun in der Konsole ausgibst, dann wird er 5 ausgeben. Wenn Du statt der 5 eine 2 wählst, wird er den Bezeichner aus dem Enum verwenden.

Die Enums sind eigentlich nur eine Vereinfachung, technisch sind sie nach wie vor nur eine Zahl.
Wenn der Decompiler die ursprüngliche Typ-Definition (Der Original-Typ steht ja am Member) des Enums findet, dann kann er anhand des Zahlen-Wertes den Enum-Bezeichner finden. Kennt der Decompiler die Typ-Definition nicht, dann muss er zwangsweise bei dem Zahlenwert bleiben.

Wenn Du nun in Assembly 1 die Zahlen hinter den Bezeichnern änderst, wird das wahrscheinlich dazu führen, dass die Verwendung in Assembly 2 noch auf die alten Zahlen verweist und damit andere Bezeichner angezeigt werden, wenn Du decompilest.
Wenn Du nun in Assembly 1 den Bezeichner vor der Zahl änderst, die Zahl aber nicht, dann wird die Verwendung in Assembly 2 wahrscheinlich auch die neuen Bezeichner verwenden, da die Suche anhand des Zahlenwertes geschieht und nicht anhand des Namens.

Die Magie liegt also darin, dass am Member weiterhin das Enum als Typ hinterlegt ist und die Runtime so schlau ist, den Wert jeweils zu casten bzw. die Aufrufe (z.B. ToString()) an die Überschreibung vom Enum weiter zu geben.
Enum.ToString() zum Beispiel ruft folgendes auf:

return Enum.InternalFormat((RuntimeType)GetType(), GetValue());

Und das wiederum enthält:

private static String InternalFormat(RuntimeType eT, Object value)
{
    Contract.Requires(eT != null);
    Contract.Requires(value != null);
    if (!eT.IsDefined(typeof(System.FlagsAttribute), false)) // Not marked with Flags attribute
    {
        // Try to see if its one of the enum values, then we return a String back else the value
        String retval = GetName(eT, value);
        if (retval == null)
            return value.ToString();
        else
            return retval;
    }
    else // These are flags OR'ed together (We treat everything as unsigned types)
    {
        return InternalFlagsFormat(eT, value);
    }
}

Das FlagsAttribute lassen wir mal außer acht, denn das wird vermutlich auf das Gleiche hinaus laufen, nur eben mit mehreren Werten. Spannend ist das GetName() und der anschließende Check auf null.

PS:
Hab's mir gerade über ILSpy bzw. dnSpy angeschaut und scheinbar tut die Runtime gar nichts, die ganze Magie verrichtet der Compiler, der die Casts bzw. die Aufrufe richtig weiter reicht.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.