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 0x00
où 0xFE 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.
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
ce que vous voulez dire par là, il n'y a pas de '' stloc.4''? –
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