2015-12-04 5 views
2

J'ai ce programme en C#:différent IL généré lors de l'ajout d'un plus int variables

using System; 

class Program 
{ 
    public static void Main() 
    { 
    int i = 4; 
    double d = 12.34; 
    double PI = Math.PI; 
    string name = "Ehsan"; 


    } 
} 

et quand je le compiler, qui suit est le IL généré par le compilateur pour principal:

.method public hidebysig static void Main() cil managed 
{ 
    .entrypoint 
    // Code size  30 (0x1e) 
    .maxstack 1 
    .locals init (int32 V_0, 
      float64 V_1, 
      float64 V_2, 
      string V_3) 
    IL_0000: nop 
    IL_0001: ldc.i4.4 
    IL_0002: stloc.0 
    IL_0003: ldc.r8  12.34 
    IL_000c: stloc.1 
    IL_000d: ldc.r8  3.1415926535897931 
    IL_0016: stloc.2 
    IL_0017: ldstr  "Ehsan" 
    IL_001c: stloc.3 
    IL_001d: ret 
} // end of method Program::Main 

qui est bien et je le comprends, maintenant si j'ajoute une autre variable entière alors quelque chose de différent est généré, voici le code C# modifié:

using System; 

class Program 
{ 
    public static void Main() 
    { 
    int unassigned; 
    int i = 4; 
    unassigned = i; 
    double d = 12.34; 
     double PI = Math.PI; 
    string name = "Ehsan"; 


    } 
} 

et voici l'IL généré contre le code ci-dessus C#:

.method public hidebysig static void Main() cil managed 
{ 
    .entrypoint 
    // Code size  33 (0x21) 
    .maxstack 1 
    .locals init (int32 V_0, 
      int32 V_1, 
      float64 V_2, 
      float64 V_3, 
      string V_4) 
    IL_0000: nop 
    IL_0001: ldc.i4.4 
    IL_0002: stloc.1 
    IL_0003: ldloc.1 
    IL_0004: stloc.0 
    IL_0005: ldc.r8  12.34 
    IL_000e: stloc.2 
    IL_000f: ldc.r8  3.1415926535897931 
    IL_0018: stloc.3 
    IL_0019: ldstr  "Ehsan" 
    IL_001e: stloc.s V_4 // what is happening here in this case 
    IL_0020: ret 
} // end of method Program::Main 

Si vous constatez maintenant la déclaration stloc.s est généré avec V_4 qui est locale, mais je ne suis pas clair à ce sujet et je suis aussi ne pas obtenir c'est le but de ces locaux ici, je veux dire ceux-ci:

.locals init (int32 V_0, 
       float64 V_1, 
       float64 V_2, 
       string V_3) 
+1

Il n'y a pas de 'stloc.4', donc vous devez utiliser' stloc.s local_variable_reference' à la place.* Je ne comprends pas non plus quel est le but de ces locaux ici * Ce sont vos variables locales 'int i; double d; double PI; nom de la chaîne; – PetSerAl

+0

ce que vous voulez dire par là, il n'y a pas de '' stloc.4''? –

+1

Il existe quatre raccourcis 'stloc'' stloc.0', 'stloc.1',' stloc.2' et 'stloc.3'. Pour adresser une variable locale avec l'index 4 ou supérieur, vous devez utiliser 'stloc.s' ou' stloc'. – PetSerAl

Répondre

5

Certaines choses à noter. Tout d'abord, il s'agit vraisemblablement d'une construction de débogage, ou du moins de certaines optimisations désactivées dans la compilation. Ce que je vous attendre à voir ici est:

.method public hidebysig static void Main() cil managed 
{ 
    .entrypoint 

    IL_0000: ret 
} 

Ce qui veut dire, étant donné que les habitants ne sont pas utilisés, je pense que le compilateur de simplement les ignorer complètement. Ce ne sera pas le cas lors d'une construction de débogage, mais ceci est un bon exemple de la différence considérable entre ce que dit le C# et ce que dit la VA.

La prochaine chose à noter est comment une méthode IL est structurée. Vous avez un tableau de valeurs locales, qui est défini avec le bloc .locals, de différents types. Ceux-ci correspondent généralement assez étroitement à ce que le C# avait, bien qu'il y ait souvent des raccourcis et des réarrangements faits. Enfin, nous avons l'ensemble des instructions qui agissent sur ces locals, les arguments, et une pile sur laquelle il peut pousser, à partir de laquelle il peut sauter, et sur lequel diverses instructions vont interagir. La chose suivante à noter est que l'IL que vous voyez ici est une sorte d'assemblage pour le byte-code: Chaque instruction a ici un mappage un-à-un à un ou deux octets, et chaque valeur consomme aussi un certain nombre d'octets. Par exemple, stloc V_4 (pas réellement présent dans vos exemples, mais nous y viendrons) serait mappé à 0xFE 0x0E 0x04 0x000xFE 0x0E est le codage de stloc et 0x04 0x00 celui de 4 qui est l'indice du local en question. Cela signifie "pop la valeur du haut de la pile, et le stocker dans le 5ème (index 4) local".

Maintenant, il y a quelques abréviations ici. L'une d'entre elles est la forme "courte" .s de plusieurs instructions (_S au nom de la valeur équivalente System.Reflection.Emit.OpCode). Ce sont des variantes d'autres instructions qui prennent une valeur d'un octet (signée ou non signée selon l'instruction) où l'autre forme prend une valeur de deux ou quatre octets, généralement des indices ou des distances relatives à sauter. Donc, au lieu de stloc V_4, nous pouvons avoir stloc.s V_4 qui est seulement 0x13 0x4, et est donc plus petit.

Ensuite, il existe certaines variantes qui incluent une valeur particulière dans l'instruction.Donc, au lieu de stloc V_0 ou stloc.s V_0, nous pouvons simplement utiliser stloc.0 qui est juste le seul octet 0x0A.

Cela fait beaucoup de sens quand on considère qu'il est commun d'avoir seulement une poignée de gens du pays en cours d'utilisation à la fois, en utilisant soit stloc.s ou (mieux encore) les goûts de stloc.0, stloc.1, etc.) donne petit des économies qui s'ajoutent à beaucoup.

Mais seulement tellement. Si nous avions par exemple stloc.252, stloc.253 etc. alors il y aurait beaucoup de telles instructions, et le nombre d'octets nécessaires pour chaque instruction devrait être plus, et ce serait globalement une perte. Les formes super-courtes de la locale (stloc, ldloc) et liées à l'argument (ldarg) vont seulement jusqu'à 3. (Il y a un starg et starg.s mais pas de starg.0 etc. car le stockage des arguments est relativement rare). ldc.i4/ldc.i4.s (pousser une valeur signée constante de 32 bits sur la pile) a des versions super-courtes allant de ldc.i4.0 à ldc.i4.8 et aussi lcd.i4.m1 pour -1.

Il est également à noter que le V_4 n'existe pas dans votre code du tout. Tout ce que vous avez examiné l'IL avec vous ne saviez pas que vous utilisiez le nom de variable name alors il a juste utilisé V_4. (Qu'est-ce que vous utilisez, BTW? J'utilise ILSpy pour la plupart, et si vous déboguer les informations associées au fichier, il aurait appelé name en conséquence).

Ainsi, pour produire une version non court-circuité commentée de votre méthode avec des noms plus comparables que nous pourrions écrire le CIL suivant:

.method public hidebysig static void Main() cil managed 
{ 
    .entrypoint 
    .maxstack 1 
    .locals init (int32 unassigned, 
      int32 i, 
      float64 d, 
      float64 PI, 
      string name) 
    nop       // Do Nothing (helps debugger to have some of these around). 
    ldc.i4 4     // Push number 4 on stack 
    stloc i     // Pop value from stack, put in i (i = 4) 
    ldloc i     // Push value in i on stack 
    stloc unassigned   // Pop value from stack, put in unassigned (unassigned = i) 
    ldc.r8 12.34    // Push the 64-bit floating value 12.34 onto the stack 
    stloc d     // Push the value on stack in d (d = 12.34) 
    ldc.r8 3.1415926535897931 // Push the 64-bit floating value 3.1415926535897931 onto the stack. 
    stloc PI      // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI) 
    ldstr "Ehsan"    // Push the string "Ehsan" on stack 
    stloc name     // Pop the value from stack, put in name 
    ret       // return. 
} 

Ce se comporte à peu près comme votre code fait, mais être un peu plus . Donc, nous remplaçons le stloc avec stloc.0 ... stloc.3 où l'on peut, stloc.s où nous ne pouvons pas utiliser les mais peut encore utiliser stloc.s et ldc.i4 4 avec ldc.i4.4, et nous aurons bytecode plus court qui fait la même chose:

.method public hidebysig static void Main() cil managed 
{ 
    .entrypoint 
    .maxstack 1 
    .locals init (int32 unassigned, 
      int32 i, 
      float64 d, 
      float64 PI, 
      string name) 
    nop       // Do Nothing (helps debugger to have some of these around). 
    ldc.i4.4      // Push number 4 on stack 
    stloc.1      // Pop value from stack, put in i (i = 4) 
    ldloc.1      // Push value in i on stack 
    stloc.0      // Pop value from stack, put in unassigned (unassigned = i) 
    ldc.r8 12.34    // Push the 64-bit floating value 12.34 onto the stack 
    stloc.2      // Push the value on stack in d (d = 12.34) 
    ldc.r8 3.1415926535897931 // Push the 64-bit floating value 3.1415926535897931 onto the stack. 
    stloc.3      // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI) 
    ldstr "Ehsan"    // Push the string "Ehsan" on stack 
    stloc.s name     // Pop the value from stack, put in name 
    ret       // return. 
} 

Et maintenant nous avons exactement le même code que votre démontage, sauf que nous avons de meilleurs noms. Rappelez-vous, les noms n'apparaissent pas dans le code de l'octet, donc le désassembleur ne pourrait pas faire un aussi bon travail que nous pouvons.


Votre question dans un commentaire devrait vraiment être une autre question, mais il offre une chance d'ajouter quelque chose d'important que je brièvement ci-dessus. Considérons:

public static void Maybe(int a, int b) 
{ 
    if (a > b) 
    Console.WriteLine("Greater"); 
    Console.WriteLine("Done"); 
} 

Compile dans le débogage et vous vous retrouvez avec quelque chose comme:

.method public hidebysig static 
    void Maybe (
    int32 a, 
    int32 b 
) cil managed 
{ 
    .maxstack 2 
    .locals init (
    [0] bool CS$4$0000 
) 

    IL_0000: nop 
    IL_0001: ldarg.0 
    IL_0002: ldarg.1 
    IL_0003: cgt 
    IL_0005: ldc.i4.0 
    IL_0006: ceq 
    IL_0008: stloc.0 
    IL_0009: ldloc.0 
    IL_000a: brtrue.s IL_0017 

    IL_000c: ldstr "Greater" 
    IL_0011: call void [mscorlib]System.Console::WriteLine(string) 
    IL_0016: nop 

    IL_0017: ldstr "Done" 
    IL_001c: call void [mscorlib]System.Console::WriteLine(string) 
    IL_0021: nop 
    IL_0022: ret 
} 

Maintenant, une chose à noter est que toutes les étiquettes comme IL_0017 etc. sont ajoutés à chaque ligne en fonction de l'indice de l'instruction. Cela rend la vie plus facile pour le désassembleur, mais n'est pas vraiment nécessaire à moins qu'une étiquette ne soit sautée.Nous allons dépouiller toutes les étiquettes qui ne sont pas sautaient à:

.method public hidebysig static 
    void Maybe (
    int32 a, 
    int32 b 
) cil managed 
{ 
    .maxstack 2 
    .locals init (
    [0] bool CS$4$0000 
) 

    nop 
    ldarg.0 
    ldarg.1 
    cgt 
    ldc.i4.0 
    ceq 
    stloc.0 
    ldloc.0 
    brtrue.s IL_0017 

    ldstr "Greater" 
    call void [mscorlib]System.Console::WriteLine(string) 
    nop 

    IL_0017: ldstr "Done" 
    call void [mscorlib]System.Console::WriteLine(string) 
    nop 
    ret 
} 

Maintenant, nous allons examiner ce que chaque ligne fait:

.method public hidebysig static 
    void Maybe (
    int32 a, 
    int32 b 
) cil managed 
{ 
    .maxstack 2 
    .locals init (
    [0] bool CS$4$0000 
) 

    nop     // Do nothing 
    ldarg.0    // Load first argument (index 0) onto stack. 
    ldarg.1    // Load second argument (index 1) onto stack. 
    cgt     // Pop two values from stack, push 1 (true) if the first is greater 
         // than the second, 0 (false) otherwise. 
    ldc.i4.0    // Push 0 onto stack. 
    ceq     // Pop two values from stack, push 1 (true) if the two are equal, 
         // 0 (false) otherwise. 
    stloc.0    // Pop value from stack, store in first local (index 0) 
    ldloc.0    // Load first local onto stack. 
    brtrue.s IL_0017  // Pop value from stack. If it's non-zero (true) jump to IL_0017 

    ldstr "Greater"  // Load string "Greater" onto stack. 

         // Call Console.WriteLine(string) 
    call void [mscorlib]System.Console::WriteLine(string) 
    nop     // Do nothing 

    IL_0017: ldstr "Done" // Load string "Done" onto stack. 
         // Call Console.WriteLine(string) 
    call void [mscorlib]System.Console::WriteLine(string) 
    nop     // Do nothing 
    ret     // return 
} 

Écrivons ce retour en C# dans un très littéral étape par étape :

public static void Maybe(int a, int b) 
{ 
    bool shouldJump = (a > b) == false; 
    if (shouldJump) goto IL_0017; 
    Console.WriteLine("Greater"); 
IL_0017: 
    Console.WriteLine("Done"); 
} 

Essayez cela et vous verrez qu'il fait la même chose. L'utilisation de goto est parce que CIL n'a pas vraiment quelque chose comme for ou while ou même des blocs que nous pouvons mettre après un if ou else, il a juste des sauts et des sauts conditionnels.

Mais pourquoi est-ce que cela dérange de stocker la valeur (ce que j'ai appelé shouldJump dans ma réécriture C#) plutôt que d'agir sur elle? C'est juste pour faciliter l'examen de ce qui se passe à chaque point si vous déboguez. En particulier, pour qu'un débogueur puisse s'arrêter au point où a > b est élaboré mais n'a pas encore été activé, alors a > b ou son contraire (a <= b) doit être stocké.

Les versions de débogage ont tendance à écrire CIL qui passe beaucoup de temps à écrire un enregistrement de ce qu'il vient de faire, pour cette raison. Avec une version de version, nous obtiendrions quelque chose comme:

.method public hidebysig static 
    void Maybe (
    int32 a, 
    int32 b 
) cil managed 
{ 
    ldarg.0   // Load first argument onto stack 
    ldarg.1   // Load second argument onto stack 
    ble.s IL_000e  // Pop two values from stack. If the first is 
        // less than or equal to the second, goto IL_000e: 
    ldstr "Greater" // Load string "Greater" onto stack. 
        // Call Console.WriteLine(string) 
    call void [mscorlib]System.Console::WriteLine(string) 
        // Load string "Done" onto stack. 
    IL_000e: ldstr "Done" 
        // Call Console.WriteLine(string) 
    call void [mscorlib]System.Console::WriteLine(string) 
    ret 
} 

Ou faire une même ligne par ligne d'écriture différée en C#:

public static void Maybe(int a, int b) 
{ 
    if (a <= b) goto IL_000e; 
    Console.WriteLine("Greater"); 
IL_000e: 
    Console.WriteLine("Done"); 
} 

vous pouvez donc voir comment la version release est plus concisément faire la même chose.

+0

pouvez-vous effacer mon plus de confusion? –

+0

Si je peux concision, je le ferai. Qu'Est-ce que c'est? –

+0

c'est en fait une autre question mais il est également lié à IL –

5

MSIL est fortement micro-optimisé pour rendre le stockage aussi petit que possible. Rendez-vous au Opcodes class et notez les instructions Stloc. Il y en a 6 versions, elles font toutes exactement la même chose.

Stloc_0, Stloc_1, Stloc_2 et Stloc_3 sont les minimales, ils ne prennent qu'un seul octet. Le nombre variable qu'ils utilisent est implicite, de 0 à 3. Très couramment utilisé bien sûr.

Ensuite, il y a Stloc_S, c'est un opcode de deux octets, le second octet pour encoder le numéro de variable. Celui-ci doit être utilisé quand une méthode a plus de 4 variables.

Enfin, il y a Stloc, il s'agit d'un code opération de trois octets, utilisant deux octets pour coder le numéro de variable. Doit être utilisé lorsqu'une méthode comporte plus de 256 variables. J'espère que vous ne ferez jamais cela. Vous n'avez pas de chance lorsque vous écrivez un monstre qui a plus de 65536 variables, ce qui n'est pas supporté. A été fait btw, le code généré automatiquement peut dépasser cette limite.

donc facile de voir ce qui est arrivé dans le deuxième extrait, vous avez ajouté la variable unassigned et augmenté le nombre de variables locales de 4 à 5. Comme il n'y a pas Stloc_4, le compilateur doit utiliser Stloc_S pour affecter la 5ème variable de .

+0

des réflexions sur celui-ci: http://stackoverflow.com/questions/34026958/purpose-and-sens-of-specialname-and-rtspecialname-in-il –

+0

Oui, cela n'a rien à voir avec votre question. Il suffit de suivre le lien le premier commentaire pour voir mes pensées. –