Les premiers pas
Le code source étant écrit et exécuté sous le Common Language Runtime (CLR) de .NET est appelé «code géré» ou «code managé». Il s'agit d'un type de code spécialement conçu pour être pris en charge, surveillé et optimisé par l'environnement d'exécution de .NET. Le compilateur managé traduit les fichiers de code sources correspondants, comme ceux avec les extensions *.cs (C#) ou *.vb (Visual Basic .NET), en un langage intermédiaire de bas niveau appelé CIL (Common Intermediate Language), étant ensuite accompagné d'un manifeste d'assembly et de métadonnées de type. Ce processus permet d'assurer une compatibilité et une interopérabilité entre différents langages de programmation au sein de l'écosystème .NET.
Ainsi, le MSIL (Microsoft Intermediate Language), étant une autre appellation du CIL, représente un langage intermédiaire central pris en charge par le cadre d'application .NET. Grâce à ce langage, il est possible de créer, construire et compiler des applications .NET, même en utilisant uniquement du code CIL autonome, sans dépendre de langages de haut niveau. Le code MSIL constitue en quelque sorte la structure de base, le squelette, de tout assembly .NET. Plus vous explorez en profondeur les instructions fournies par le MSIL, plus vous développez une compréhension fine et avancée du développement d'applications dans l'univers .NET.
Dans cette page, vous découvrirez une présentation complète et détaillée des ensembles d'instructions MSIL ainsi que leur signification. Cette compréhension sera approfondie à travers l'écriture d'un programme simple utilisant des opcodes MSIL. Vous verrez également comment le compilateur MSIL, nommé ilasm.exe, joue un rôle essentiel en permettant la construction et l'exécution de ce code d'assembly .NET sans passer par le processus classique de compilation offert par l'environnement de développement Visual Studio. Cette approche vous permettra de mieux saisir le fonctionnement interne du CLR et des assemblies .NET.
Les outils de base
Programmer avec des ensembles d'instructions MSIL est assez complexe et considéré comme une tâche ardue, car le développeur utilise directement la grammaire intégrée du CLR, appelée «opcodes», au lieu de la syntaxe anglaise conviviale de C#, F# ou VB.Net. Il est donc conseillé d'installer les outils suivants sur la machine du chercheur lors de ce voyage :
- .NET Framework 4.0 ou version ultérieure
- IDE Visual Studio 2010 ou version ultérieure
- Utilitaires ILDASM.exe, ILASM.exe
- Notepad++
- SharpDevelop (facultatif)
- Xamarin Studio (facultatif)
Bien que le code MSIL puisse être créé via l'éditeur simple Notepad, il est recommandé d'écrire du code MSIL à l'aide d'éditeurs à part entière comme SharpDevelop.
Les composantes internes MSIL
Un assembly .NET contient du code MSIL, conceptuellement similaire au bytecode Java, car il n'est compilé en instructions spécifiques à la plateforme que lorsque cela est absolument nécessaire. Le CLR .NET utilise un compilateur JIT pour chaque processeur ciblant l'environnement d'exécution, chacun optimisé pour la plateforme sous-jacente. Les binaires .NET contiennent des métadonnées décrivant les caractéristiques de chaque type du binaire. Ces métadonnées sont officiellement appelées un manifeste, contenant des informations sur la version actuelle de l'assembly, ainsi que la liste de tous les assemblys référencés en externe et des informations sur la culture.
La figure suivante démontre apparemment que chaque code source de programmation autorisé .NET est finalement compilé en MSIL plutôt que directement dans un ensemble d'instructions spécifique. Ce potentiel permet à tous les langages pris en charge par .NET d'interagir entre eux. De plus, le code MSIL offre les mêmes avantages auxquels les professionnels Java sont habitués :
Chaque langage de programmation pris en charge par .NET associe ses mots clefs respectifs aux mnémoniques MSIL. Le code en langage intermédiaire (IL) a tendance à être complexe et totalement incompréhensible. Par exemple, lors du chargement d'une variable chaîne en mémoire, nous n'utilisons pas l'opcode convivial StringLoading, mais plutôt ldstr. Supposons que nous ayons construit l'exemple de programme suivant en langage C# pour comprendre le code généré correspondant derrière les grammaires MSIL.
Le code C# suivant effectue une simple addition de deux valeurs numériques via la méthode testCalcul. Les binaires .NET ne contiennent pas d'instructions spécifiques à la plateforme, mais utilisent un code IL indépendant, généré à l'aide du compilateur C# correspondant (csc.exe) pendant le processus de compilation :
- class Program
- {
- static void Main(string[] args)
- {
- // Appel de la méthode
- testCalcul(20, 40);
- Console.ReadKey();
- }
-
- // Méthode statique de démonstration
- static void testCalcul(int iPartie1, int iPartie2)
- {
- int Resultat;
- Resultat = iPartie1 + iPartie2;
- Console.WriteLine("Sortie de calcul :: {0}", Resultat);
- }
- }
Une fois ce code compilé, le CLR localise et charge ce binaire .NET en mémoire. Vous obtenez ainsi un seul assembly *.exe contenant un manifeste, des métadonnées et des instructions MSIL. Heureusement, le cadre d'application .NET est fourni avec un excellent utilitaire permettant de désassembler tout binaire .NET en son code IL correspondant, appelé «ILDASM.EXE».
Nous pourrions utiliser l'utilitaire ildasm.exe pour désassembler le code IL, soit en mode invite de commande, soit via une représentation graphique classique. Si vous ouvriez cet assembly avec ILDASM.EXE en mode graphique, vous obtiendriez la représentation back-end réelle de chaque instruction de code C# dans l'ensemble d'instructions des opcodes CIL correspondant, comme illustré ci-dessous :
Le fichier ILDASM.EXE charge tout assembly .NET et analyse son contenu, notamment le code MSIL, le manifeste et les métadonnées. Il est généralement capable de récupérer toutes les métadonnées des binaires .NET dans une représentation d'opcode CIL. Double-cliquez sur la méthode de calcul du test (testCalcul) pour examiner le code MSIL généré sous-jacent, comme illustré ci-dessous :
De plus, si vous souhaitez explorer les métadonnées de type pour l'assemblage actuellement chargé, appuyez sur Ctrl+M (View > MetaInfo > Show!) affichant les métadonnées sur la méthode de calcul du test comme dans l'exemple suivant :
Grammaire des opcodes
Le MSIL est un langage de programmation orienté objet complet, comme C#. Il inclut toutes les fonctionnalités classiques de la programmation orientée objet, telles que l'héritage, les classes, les instructions de contrôle, les interfaces, et bien plus encore. Comme mentionné précédemment, il est possible de créer des applications .NET directement en MSIL sans même utiliser l'IDE de Visual Studio. Mais pourquoi la programmation MSIL est-elle si importante à comprendre ? Parce qu'elle aide les développeurs à mieux écrire, déboguer et maintenir leur code. Le tableau 1 présente une brève liste, avec descriptions, de l'ensemble d'instructions MSIL :
| Opcode | Description |
|---|---|
| nop | Aucune opération n'est effectuée non plus |
| sub, div, add, mul, rem | Effectuer des opérations mathématiques de base |
| add.ovf | Spécifier une valeur entière signée avec contrôle de dépassement |
| add.ovf.un | Spécifier une valeur entière non signée avec vérification de dépassement. |
| box, unbox | Conversion du type de référence en type de valeur et vice-versa |
| br.s, br | Passer à une autre étiquette à un emplacement spécifique |
| castclass | Conversion de type d'une instance vers un type différent |
| bgt | Instruction de branchement de contrôle pour une condition supérieure à. |
| beg | Instruction de branchement de contrôle pour une condition égale à |
| ble | Instruction de branchement de contrôle pour une condition inférieure ou égale à |
| break | Point d'arrêt typique du débogueur |
| brnull | Branche vers la cible si la valeur est nulle |
| dup | Dupliquer la valeur sur la pile |
| call | Appelle la méthode spécifiée |
| ceq | Si valeur1 est égale à valeur2 alors empile sur 1 sinon 0 |
| cgt | Si la valeur 1 est supérieure à la valeur 2, empiler sur 1, sinon 0 |
| clt | Si la valeur 1 est inférieure à la valeur 2, appuyez sur 1, sinon 0 |
| ldc | Charge la valeur des constantes dans la pile mémoire |
| ldobj | Charge la valeur de l'objet dans la pile mémoire |
| ldstr | Charge la valeur de la chaîne dans la pile mémoire |
| ldarg | Charge l'adresse d'un paramètre d'une fonction dans la pile mémoire |
| arglist | Renvoyer la liste des arguments pour la méthode actuelle |
| readonly | Spécifier l'adresse du tableau n'effectue aucune vérification de type lors de l'exécution |
| starg | Entrepose la valeur de la pile dans les listes de paramètres de méthode |
| stloc | Obtenir la valeur actuelle d'une variable à partir de la pile et la copier dans la variable locale |
| stobj | Entreposer une valeur de type dans une adresse mémoire |
| callvirt | Appel d'une fonction virtuelle (VC++, C++/CLI) |
| brtrue.s | Exécution de la branche dans le cas où la condition est différente de zéro ou vraie |
| brfalse.s | L'exécution de la branche continue si la condition est fausse |
| pop | Supprime une valeur du haut de la pile |
| ret | Terminer le flux d'exécution du corps de la méthode |
| throw | Lève une exception |
| rethrow | Renvoie l'exception actuelle |
| tail | Termine l'appel de méthode en cours |
| volatile | Spécifie qu'une référence de pointeur est volatile |
De la même manière, le tableau suivant illustre les correspondances de types de données C# correspondent aux types MSIL correspondants :
| Type de données MSIL | Équivalent C# |
|---|---|
| int32 | int |
| unsigned int32 | uint |
| int64 | long |
| float32 | float |
| float64 | double |
| bool | bool |
| string | string |
| object | object |
| char | char |
| unsigned int8 | byte |