Plateforme cible en .NET en 5 min

A partir des options du “Configuration Manager” dans Visual, on peut compiler ses projets suivant plusieurs plateformes: “AnyCPU”, “x86”, “x64” et “Itanium”. A chacune de ses valeurs correspond une plateforme cible sur laquelle les assemblies pourront être chargées. Le mauvais choix de plateforme cible peut mener à des exceptions de type BadImageFormatException qu’il n’est pas forcément facile à prévoir.

Ainsi une assembly générée avec pour plateforme cible:

  • “AnyCPU” sera exécutée par le CLR 32 ou 64 bits.
  • “x86” sera exécutée exclusivement par le CLR 32 bits.
  • “x64” sera exécutée exclusivement par le CLR 64 bits.
  • “Itanium” sera exécutée sur une architecture Itanium.

Tout se complique en sachant qu’une machine avec une architecture 64 bits peut exécuter du code compilé pour les plateformes “AnyCPU”, “x86” et “x64”.

Le choix de la plateforme entraîne d’autres implications qu’il est important d’avoir en tête pour éviter des erreurs ou exceptions à l’exécution.

1. Différences de fonctionnement des exécutables

Quel que soit la plateforme cible choisie, s’il n’y a que de code managé, le code IL généré est pratiquement le même. Dans une assembly “AnyCPU”, il n’y a qu’une seule version du code IL (et non une version 32 bits et une version 64 bits). D’ailleurs quelque soit la plateforme, la taille des assemblies est pratiquement la même.

L’indication de la plateforme permet de rajouter des indications dans l’entête de l’assembly. A l’exécution en fonction de ces indications, le compilateur JIT choisira comment transformer le code IL en code machine:

  • Un code 64 bits ne peut être exécuté que sur une architecture 64 bits,
  • Un code 32 bits peut être exécuté sur une architecture 32 bits mais aussi une architecture 64 bits avec WoW64 (Windows 32-bit on Windows 64-bit qui est sous-sytème de Windows capable d’exécuter du code 32 bits).
  • Un code “AnyCPU” peut être exécuté sur une architecture 32 bits et 64 bits, toutefois il sera exécuté par un CLR 32 bits ou un CLR 64 bits (voir plus bas pour plus de détails).

Ainsi suivant la plateforme cible choisie, les assemblies résultants peuvent avoir ou non un comportement différent:

  • Les types primitifs: les types primitifs (integer ou long) sont codés sur 32 bits quel que soit l’architecture, en revanche les pointeurs sont codés sur 32 ou 64 bits suivant les architectures.
  • IntPtr: pour stocker des adresses qui s’adaptent à l’architecture, on peut utiliser le type IntPtr. En 32 bits, IntPtr.Size = 4; en 64 bits, IntPtr.Size = 8.
  • Assemblies système: suivant l’architecture, des assemblies systèmes différentes sont chargées puis utilisées. Par exemple, en 32 bits, C:\Windows\Microsoft.NET\Framework peut être utilisé alors qu’en 64 bits, on utilise plutôt C:\Windows\Microsoft.NET\Framework64.
  • Lecture de la base de registres: si on exécute
    RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\CustomNode");

    Une application 32 bits exécutée sur une machine 32 bits lira la clé dans: HKEY_LOCAL_MACHINE\SOFTWARE\CustomNode. De même une application 64 bits exécutée sur une machine 64 bits lira la clé dans HKEY_LOCAL_MACHINE\SOFTWARE\CustomNode. En revanche, une application 32 bits exécutée sur une machine 64 bits lira la clé dans: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\CustomNode.

D’autres différences existent, on peut trouver une liste plus exhaustive de ces différences sur les pages suivantes:

Tests

Si on livre des assemblies séparées en 32 ou 64 bits, il est donc important de tester séparement dans toutes les architectures livrées.

Limite de mémoire occupée par le processus

Par défaut, un processus 32 bits ne peut occuper plus de 2 GB en mémoire. Toutefois en ajoutant
IMAGE_FILE_LARGE_ADDRESS_AWARE dans l’entête de l’assembly (voir plus bas), on peut augmenter cette limite à:

  • 3 GB sur un Windows 32 bits
  • 4 GB si le processus 32 bits est exécuté sur un Windows 64 bits.

Par défaut, pour un processus 64 bits, IMAGE_FILE_LARGE_ADDRESS_AWARE est ajouté dans l’entête de l’assembly. La limite est donc de:

  • 8 TB sur une architecture 64 bits classique,
  • 7 TB sur une architecture Itanium,
  • 128 TB si le processus s’exécute sur Windows 8.1 ou Windows Server 2012 R2,
  • 2 GB si IMAGE_FILE_LARGE_ADDRESS_AWARE n’est pas rajouté dans l’entête.

Plus de renseignements sont disponibles sur MSDN.

Comment ajouter IMAGE_FILE_LARGE_ADDRESS_AWARE dans l’entête d’une assembly ?

Pour lever la limite de 2 GB qu’un processus 32 bits peut occuper en mémoire, il faut ajouter IMAGE_FILE_LARGE_ADDRESS_AWARE dans l’entête de l’assembly.
Pour effectuer cette modification, on peut s’aider de l’outil editbin.exe qui est livré avec Visual Studio C++. Il est accessible avec la ligne de commandes Visual Studio. Toutefois on peut y accéder dans le répertoire:

C:\Program Files\Microsoft Visual Studio [version de VS par exemple 10.0]\VC\bin 

Pour ajouter IMAGE_FILE_LARGE_ADDRESS_AWARE dans l’entête d’une assembly, il suffit d’exécuter la commande:

editbin /LARGEADDRESSAWARE [chemin de l'assembly]

Pour supprimer IMAGE_FILE_LARGE_ADDRESS_AWARE de l’entête:

editbin /LARGEADDRESSAWARE:no [chemin de l'assembly]

On peut vérifier que l’entête a été modifié en utilisant dumpbin (voir plus bas pour plus de détails). Il faut exécuter la commande:

dumpbin /headers [chemin de l'assembly]

Dans le résultat, il faut ensuite vérifier la présence d’une ligne: Application can handle large (>2GB) addresses, par exemple:

FILE HEADER VALUES
             14C machine (x86)
               3 number of sections
               0 file pointer to symbol table
               0 number of symbols
              E0 size of optional header
              22 characteristics
                   Executable
                   Application can handle large (>2GB) addresses

Il est possible d’exécuter la commande editbin.exe dans un évènement “post-build” du projet Visual Studio pour que l’entête soit systématiquement modifié à chaque compilation de l’assembly.

Chargement des assemblies

Une incompatiblité dans le choix de la plateforme cible peut mener à des exceptions de type BadImageFormatException lorsque le CLR tente de charger des assemblies incompatibles.

On peut résumer les différents cas possibles dans le tableau suivant:

Architecture du système Windows Architecture du processus Plateformes cible compatibles
Windows 32 bits 32 bits x86, AnyCPU
Windows 64 bits 32 bits x86, AnyCPU
64 bits x64, AnyCPU

Ainsi, on peut remarquer qu’il est possible d’exécuter un processus 32 bits sur une architecture 64 bits. De plus la plateforme cible “AnyCPU” semble être compatible avec toutes les architectures. Ceci ne veut pas dire que le choix de cette plateforme cible ne ménera jamais à une exception lors du chargement. En effet, sur une architecture 64 bits, à la notion de plateforme cible “AnyCPU”, il faut ajouter la préférence de l’architecture du processus.

Préférence de l’architecture pour la plateforme “AnyCPU”

Avant Visual Studio 2010, “AnyCPU” était la plateforme par défaut pour un projet et l’architecture de préférence du processus était 64 bits. Ainsi si on lance un exécutable compilé suivant la plateforme “AnyCPU” sur une architecture 64 bits, il s’exécute dans un processus 64 bits et s’il doit charger une assembly “x86”, une BadImageFormatException se produit car le CLR 64 bits ne pourra pas charger cette assembly. Seules les assemblies compilées suivant les plateformes “AnyCPU” et “x64” peuvent être chargées.

Pour Visual Studio 2010, la plateforme cible par défaut est “x86”. Il n’y a pas de confusion possible puisqu’une assembly compilé suivant cette plateforme, sera forcément exécutée dans un processus 32 bits. Seules les assemblies compilées suivant les plateformes “AnyCPU” et “x86” peuvent être chargées.

Enfin à partir du framework 4.5 et de Visual Studio 2012, il est possible d’indiquer l’architecture de préférence quand on choisit la plateforme cible “AnyCPU”:

  • “AnyCPU” correspond à l’architecture de préférence 64 bits.
  • “Any CPU 32-bit prefered” correspond à l’architecture de préférence 32 bits.

La valeur par défaut est “Any CPU 32-bit prefered”, ce qui signifie que:

  • Sur une architecture 32 bits, l’exécutable s’exécute dans un processus 32 bits et que les assemblies compatibles ont pour plateforme cible “AnyCPU” et “x86”.
  • De même, sur une architecture 64 bits, l’exécutable s’exécute dans un processus 32 bits et que les assemblies compatibles ont pour plateforme cible “AnyCPU” et “x86”.

2. Outils pour vérifier l’entête d’une assembly

Certains outils permettent de connaître la plateforme cible d’une assembly.

Task Manager

Le gestionnaire de tâches permet de voir si un processus exécuté est 32 ou 64 bits. Il suffit de lancer le gestionnaire de tâches suivant l’une des méthodes suivantes:

  • Appuyer simultanément sur les touches Ctrl + Maj + Echap,
  • Clique droit sur la barre de tâches puis cliquer sur “Start Task Manager”,
  • Appuyer sur Windows + E puis taper “taskmgr”.

Dans l’onglet “Processes”, on peut voir la liste des processus:

  • Si le nom du processus est suffixé avec “*32” alors il s’agit d’un processus 32 bits (Exemple: “firefox.exe *32”)
  • Si le nom du processus n’est pas suffixé avec “*32” alors il s’agit d’un processus 64 bits.

Process Explorer

ProcessExplorer est un outils assez puissant qui permet d’indiquer des informations supplémentaires par rapport au Task Manager.

On peut télécharger ProcessExplorer sur: https://technet.microsoft.com/en-us/sysinternals/processexplorer.aspx.

Pour voir la liste des processus, il faut:

  1. Executer ProcessExplorer avec les droits administrateur,
  2. Faire un clique droit sur l’entête des colonnes pour afficher la colonne “Image Type”,
  3. On peut voir directement quels sont les processus 32 et 64 bits

Corflags.exe

Corflags.exe est avec le SDK Windows et est accessible à partir de la ligne de commande Visual Studio.
Le chemin dépend de la version du système, par exemple sur Windows 7 32 bits:

C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\CorFlags.exe

Sur une machine Windows 8.1 64 bits avec le framework 4.5.1:

C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\CorFlags.exe

En utilisant CorFlags.exe en tapant la ligne suivante, on peut avoir des détails sur l’entête de l’assembly:

CorFlags.exe [chemin de l'assembly]

Framework 4.0

A la version 4.0 du framework, les valeurs qui présentent un intéret sont “PE” et “32BIT”:

Plateforme cible PE 32BIT
AnyCPU PE32 0
x86 PE32 1
x64 PE32+ 0
Itanium PE32+ 0

On peut remarquer que les plateformes cible “x64” et “AnyCPU” donnent le même résultat.

Architecture de préférence par défaut des processus pour la plateforme “AnyCPU”

Pour la version 4.0 du framework, l’architecture de préférence des processus compilé avec la plateforme cible “AnyCPU” est par défaut 64 bits. Ainsi sur une machine 64 bits, l’assembly “AnyCPU” sera exécutée de préférence dans un processus 64 bits.

A partir du framework 4.5

A partir de la version 4.5 du framework et de Visual Studio 2012, dans les options de compilation:

  1. Clique droit sur le projet correspondant à l’assembly
  2. Dans l’onglet “Build”

Le paramètre “Prefer 32-bit” permet d’indiquer l’architecture de préférence des processus dans le cas de la plateforme cible “AnyCPU”. Ainsi sur une machine 64 bits, une assembly avec la plateforme cible “AnyCPU” sera exécutée dans un processus:

  • 32 bits si la case est cochée.
  • 64 bits si la case est décochée.

CorFlags.exe a aussi évolué avec les paramètres “32BITREQ” et “32BITPREF” permettant la préférence de l’architecture:

Plateforme cible Case “Prefer 32-bit” cochée PE 32BITREQ 32BITPREF
x86 N/A PE32 1 0
x64 N/A PE32+ 0 0
AnyCPU Non PE32 0 0
AnyCPU Oui PE32 0 1
Paramètre CLR Header

Attention à ce paramètre qui n’indique pas vraiment la version du framework:

  • Pour les frameworks 1.0 et 1.1, CLR Header = 2.0
  • Pour les frameworks ayant une version supérieure ou égale à 2.0, CLR Header = 2.5.

Plus d’informations à propos de CorFlags.exe sur MSDN.

Dumpbin.exe

Dumpbin est livré avec Visual Studio C++, le chemin de l’exécutable est:

C:\Program Files\Microsoft Visual Studio [version VS]\VC\bin

A partir de la ligne de commandes de Visual Studio, en tapant la ligne suivante, on peut avoir quelques détails sur l’entête de l’assembly:

dumpbin [chemin de l'assembly] /CLRHEADER /HEADERS

Parmi les informations affichées, quelques unes permettent de déterminer l’architecture pour laquelle l’assembly a été compilée.

Par exemple:

FILE HEADER VALUES
             14C machine (x86)
               3 number of sections
        4DFA7751 time date stamp Thu Jun 16 23:36:17 2011
               0 file pointer to symbol table
               0 number of symbols
              E0 size of optional header
            2102 characteristics
                   Executable
                   32 bit word machine
                   DLL

OPTIONAL HEADER VALUES
             10B magic # (PE32)
            8.00 linker version
             800 size of code
[...]

  clr Header:

              48 cb
            2.05 runtime version
            2058 [     5B4] RVA [size] of MetaData Directory
               3 flags
                   IL Only
                   32-Bit Required
               0 entry point token
[...]

La paramètre “machine” permet d’indiquer directement la plateforme cible.

Plus d’informations à propos de Dumpbin sur MSDN.

ILSpy

ILSpy permet d’indiquer directement l’architecture cible d’une assembly ainsi que la version du “runtime”. En cliquant sur le nom de l’assembly, ILSpy affiche des informations contenues dans l’entête de l’assembly.

Plus d’informations à propos d’ILSpy sur: ilspy.net.

3. Résoudre des problèmes de chargement d’assemblies

Dans le cas de BadImageFormatException, parmi toutes les dépendances d’un exécutable, il est parfois difficile d’identifier l’assembly dont l’architecture cible est incompatible avec celle de l’exécutable. Certains outils permettent d’avoir plus d’informations sur les dépendances d’une assembly et de visualiser le détail de chargement des assemblies pour un exécutable donné:
Quelques outils pour résoudre les problèmes de chargement d’assemblies.

Références

Détecter l’architecture d’un exécutable:

Leave a Reply