Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

CoqBlog

.NET is good :-)
{ Blog de coq }

Actualités

x86 / x64 / AnyCPU / Itanium : déterminer la plateforme cible d'un assembly .NET (sans connaitre la configuration de build)

Il est parfois nécessaire de déterminer la plateforme cible d'un assembly quand on ne dispose que de celui-ci et pas de la configuration de build utilisée pour le générer, par exemple pour s'assurer que les dépendances qui vont être chargées par un processus hôte sont bien compatibles au lieu de le découvrir à l'exécution via une exception BadImageFormatException.

Il existe plusieurs moyens d'y parvenir, le choix se faisant suivant le besoin de fond et le contexte (et les préférences personnelles), dont ceux-ci que nous allons détailler dans cet article :

  • par un outil simple dédié à .NET
  • par un outil moins simple
  • par code avec une librairie existante
  • par interprétation directe du fichier PE (avancé / par curiosité)

 

Contexte de rédaction :

Environnement : .NET 4.0 sous Windows 7 x86 et x64, Visual Studio 2010, Windows SDK v7.1

Version des documents cités :

Version des librairies utilisées pour les exemples de code :

 

 

Dans cet article nous allons prendre pour support 4 assemblies .NET 4.0 générés depuis du code C# avec la configuration de solution suivante :

Capture d'écran de la configuration de build pour la solution de test

Le cas des assemblies mixtes, contenant à la fois du code managé et natif, ne sera pas abordé.
Cet article part aussi du principe que l'assembly cible n'a pas été obfusqué d'une manière rendant impossible la lecture des en-têtes spécifiques à .NET.

Bien que n'ayant jamais eu affaire à la plateforme Itanium (IA-64), je l'ai incluse dans cet article car il peut y avoir confusion avec la plateforme x64 dans certains outils.

Les outils fournis par Microsoft seront lancés depuis la ligne de commande du SDK Windows version 7.1.

 

 

Par un outil simple dédié à .NET

Un outil comme ILSpy fourni la réponse assez facilement dans les propriétés de l'assembly chargé, par une des valeurs suivantes : AnyCPU, x64, x86 ou Itanium-64.

Capture d'écran des propriétés d'un assembly dans ILSpy 1.0.0.943

 

 

Par un outil moins simple

Si pour une raison ou une autre vous ne pouvez pas utiliser un outil comme ILSpy, il est possible de se débrouiller, un peu moins simplement, avec ceux fournis par le SDK Windows (gratuitement) ou Visual Studio.

Suivant le contexte et le niveau de certitude requis, 2 outils sont utilisables :

A noter que dans le cas présent CFF Explorer (GUI) pourrait remplacer DUMPBIN, par exemple si vous disposez déjà des outils .NET mais n'avez pas envie d'installer la partie C++ de Visual Studio ou du SDK.
Pour éviter tout risque de corruption accidentelle, utilisez cet outil sur une copie du fichier à inspecter : CFF Explorer permet de modifier les valeurs qu'il affiche.

 

Corflags

Commençons par CorFlags : les valeurs qui nous intéressent sont "PE" et "32BIT", et on remarque tout de suite une ambigüité entre les deux plateformes 64bit (x64 et Itanium) pour lesquelles l'outil affiche les mêmes valeurs :

corflags AnyCPULibrary.dll
[...]
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 1
ILONLY    : 1
32BIT     : 0
Signed    : 0

corflags x86Library.dll
[...]
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 3
ILONLY    : 1
32BIT     : 1
Signed    : 0

corflags x64Library.dll
[...]
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32+
CorFlags  : 1
ILONLY    : 1
32BIT     : 0
Signed    : 0

corflags ItaniumLibrary.dll
[...]
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32+
CorFlags  : 1
ILONLY    : 1
32BIT     : 0
Signed    : 0

La valeur "PE32+" pour les deux types d'architectures n'est pas forcément étonnante vu qu'elle est probablement issue du magic number présent dans les en-têtes du fichier Portable Executable et l'absence du flag "32BIT" dans une image 64bit est logique.

Si vous êtes certain de ne pas avoir à la fois des assemblies x64 et Itanium vous pouvez vous contenter d'utiliser CorFlags avec le tableau de correspondance suivant :

Tableau permettant de déterminer la plateforme cible de l'assembly à partir des valeur affichées par CorFlags

Dans le cas contraire, il est plus prudent d'utiliser les options /CLRHEADER et /HEADERS de DUMPBIN.

 

DUMPBIN

Si CorFlags laisse trop d'incertitudes, il est possible de se débrouiller avec les données fournies par les options /CLRHEADER et /HEADERS de DUMPBIN.

Voici un exemple de sortie avec notre assembly x86 :

dumpbin x86Library.dll /CLRHEADER /HEADERS
[...]
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
[...]

 

Les valeurs qui nous intéressent sont :

  • le type de machine, défini par un des champs de l'en-tête de fichier du format COFF/PE : "machine (x86)" (14C) dans l'exemple ci-dessus.
  • la présence du flag "32BITREQUIRED" dans la valeur "Flags" de l'en-tête CLI : "32-Bit Required" dans l'exemple.

Pour la liste des valeurs possibles pour le type de machine, voir la section COFF File Header (Object and Image) du document Microsoft PE and COFF Specification, ou les définitions IMAGE_FILE_MACHINE_* dans le fichier WinNT.h

Pour la liste des valeurs combinables pour la valeur "Flags" de l'en-tête CLI, voir la section File format extensions to PE / Section headers / CLI header / Runtime flags du document ECMA-335 : Common Language Infrastructure (CLI).

Quand le flag "32BITREQUIRED" est défini, la caractéristique "IMAGE_FILE_32BIT_MACHINE" (indiquée par "32 bit word machine" dans l'exemple) est censée être définie dans l'en-tête de fichier COFF, mais nous n'en tiendront pas directement compte.

La lecture des résultats est cette fois-ci sans ambigüité :

Tableau permettant de déterminer la plateforme cible de l'assembly à partir des valeur affichées par DUMPBIN

 

Dans CFF Explorer les libellés associés aux types de machines sont différents, mais les valeurs restent bien sûr les mêmes et le tableau ci-dessus reste utilisable.

Type de machine :
Capture d'écran de CFF Explorer montrant le type de machine

Flags .NET :
Capture d'écran de CFF Explorer montrant les flags .NET

 

 

Par code avec une librairie existante

Il existe au moins 2 librairies permettant de déterminer la plateforme cible d'un assembly .NET :

Pour le sujet qui nous intéresse, Cecil est probablement la solution la plus facile à prendre en main quand on n'a pas eu de contact avec ces librairies auparavant.
Cecil est d'ailleurs actuellement utilisé par ILSpy, l'outil montré en début d'article.

Voici des exemples de code basique à adapter à vos besoins, notamment pour ce qui est de la gestion des erreurs.
Je n'ai jamais utilisé ces librairies en production, ces exemples de code sont uniquement donnés à titre d'illustration.

L'énumération TargetPlatform utilisée par les 2 exemples est définie comme ceci :

public enum TargetPlatform
{
    Unknown = 0,
    AnyCPU = 1,
    x86 = 2,
    x64 = 3,
    Itanium = 4
}

 

Cecil

Nécessite une simple référence à l'assembly Mono.Cecil.

using Mono.Cecil;
// ...

public class TargetPlatformUtilities
{
    public static TargetPlatform GetTargetPlatform(String assemblyFilePath)
    {
        // !!! CODE D'ILLUSTRATION NON PRET POUR PRODUCTION !!!
        // !!! SAMPLE CODE NOT READY FOR PRODUCTION !!!

        // TODO : error handling, ...

        TargetPlatform detectedPlatform = TargetPlatform.Unknown;

        ModuleDefinition module = ModuleDefinition.ReadModule(assemblyFilePath);

        switch (module.Architecture)
        {
            case TargetArchitecture.AMD64:
                detectedPlatform = TargetPlatform.x64;
                break;
            case TargetArchitecture.I386:
                if ((module.Attributes & ModuleAttributes.Required32Bit) == ModuleAttributes.Required32Bit)
                {
                    detectedPlatform = TargetPlatform.x86;
                }
                else
                {
                    detectedPlatform = TargetPlatform.AnyCPU;
                }
                break;
            case TargetArchitecture.IA64:
                detectedPlatform = TargetPlatform.Itanium;
                break;
            default:
                detectedPlatform = TargetPlatform.Unknown;
                break;
        }

        return detectedPlatform;
    }
}

 

CCI Metadata

Nécessite des références aux assemblies :

  • Microsoft.Cci.MetadataHelper
  • Microsoft.Cci.MetadataModel
  • Microsoft.Cci.PeReader
using Microsoft.Cci;
// ...

public class TargetPlatformUtilities
{
    public static TargetPlatform GetTargetPlatform(String assemblyFilePath)
    {
        // !!! CODE D'ILLUSTRATION NON PRET POUR PRODUCTION !!!
        // !!! SAMPLE CODE NOT READY FOR PRODUCTION !!!

        // TODO : error handling, ...

        TargetPlatform detectedPlatform = TargetPlatform.Unknown;

        using (MetadataReaderHost host = new PeReader.DefaultHost())
        {
            IModule module = host.LoadUnitFrom(assemblyFilePath) as IModule;
            if (module != null)
            {
                switch (module.Machine)
                {
                    case Machine.AMD64:
                        detectedPlatform = TargetPlatform.x64;
                        break;
                    case Machine.I386:
                        if (module.Requires32bits)
                        {
                            detectedPlatform = TargetPlatform.x86;
                        }
                        else
                        {
                            detectedPlatform = TargetPlatform.AnyCPU;
                        }
                        break;
                    case Machine.IA64:
                        detectedPlatform = TargetPlatform.Itanium;
                        break;
                    default:
                        detectedPlatform = TargetPlatform.Unknown;
                        break;
                }
            }
        }

        return detectedPlatform;
    }
}

 

 

Par interprétation directe du fichier PE (avancé / par curiosité)

Les librairies comme CCI Metadata et Cecil contiennent le code nécessaire pour interpréter le fichier Portable Executable représentant l'assembly .NET, mais fournissent beaucoup plus de services que la simple détermination de plateforme cible.
Si vous désirez écrire un code dédié à cette seule tâche, il va falloir entrer dans le détail du format PE/COFF et ses extensions pour la CLI.

Voici quelques explications basées sur ma compréhension générale du sujet, c'est-à-dire du point de vue d'un développeur .NET sur plateforme Windows et donc actuellement confronté uniquement aux architectures x86 et x64 (ou x86-64/AMD64/Intel 64/...).

Dans la mesure du possible et suivant leur licence, il est sans doute plus prudent de se baser sur le code d'interprétation PE des librairies comme Cecil et CCI Metadata vu qu'elles ont déjà été éprouvées.

L'éditeur hexa utilisé ci-dessous est HxD. Si vous ne pouvez vous servir que de Visual Studio, il vous suffit d'ouvrir l'assembly dans l'éditeur binaire au lieu de l'éditeur de ressources (Ctrl+O => dropdown du bouton Open => Open With... => Binary Editor).

 

Type de machine

D'après le document Microsoft PE and COFF Specification le type de machine est stocké sur 2 octets en première valeur de l'en-tête COFF, soit à l'offset de fichier défini par l'offset de la signature PE + 0x4.
L'offset de la signature PE est définie par la valeur sur 4 octets située à l'offset de fichier 0x3C.

Exemple avec notre assembly x86 :

Position de l'en-tête PE, ici 0x80
Type de machine : position de l'en-tête PE

L'en-tête PE en lui-même ("PE\0\0")
Type de machine : l'en-tête PE

Et juste après, en première entrée de la partie COFF File Header (offset 0x84), la définition du type de machine
Type de machine : valeur du type de machine

0x14C, c'est-à-dire x86 (donc AnyCPU ou x86 du point de vue de la plateforme cible de compilation).

 

32-bit required ?

Pour déterminer cette valeur nous avons besoin d'accéder aux données de l'en-tête CLI ("CLR Runtime Header"), et plus particulièrement à la valeur Flags.
Nous devons récupérer l'adresse et la taille de cet en-tête dans la 15ème entrée de la liste des Optional Header Data Directories.
Nous allons nous reposer sur les documents Microsoft PE and COFF Specification et ECMA-335 : Common Language Infrastructure (CLI) (section File format extensions to PE / Section headers / CLI header / Runtime flags).

Format PE
Certains offsets et tailles de données vont maintenant varier suivant le fait que le fichier PE est au format PE32 ou PE32+ ("PE64").
Pour déterminer le format, il faut lire le magic number qui est la première valeur de l'en-tête optionnel Optional Header (obligatoire dans le cas d'une image, donc notre cas) : cette valeur sur 2 octets se trouve à l'offset de fichier [offset COFF File Header]+0x14 (=[offset Optional Header]).
Soit dans le cas de notre assembly x86, à l'offset 0x98
32-bit required ? : format PE
Magic number = 0x10B, soit le format PE32.
L'assembly AnyCPU déclarera la même valeur, alors que les assembly x64 et Itanium déclareront une valeur 0x20B (PE32+).

Nombre d'entrées Data Directories
Même s'il doit normalement être de 16 (0x10), nous allons vérifier que le nombre de Data Directories est bien supérieur ou égal à 15 (0xF).
Cette valeur sur 4 octets, NumberOfRvaAndSizes, se lit à l'offset [offset Optional Header]+0x5C dans le cas de PE32 (ou [offset Optional Header]+0x6C dans celui de PE32+).
Dans le cas de notre assembly x86, ce sera donc à l'offset 0xF4 :
32-bit required ? : nombre d'entrées Data Directories
Nous avons bien 16 entrées.

Data directory concernant l'en-tête CLI
Nous savons que l'entrée donnant l'adresse et la taille de l'en-tête CLI est la 15ème de la liste et se situe à l'offset [offset Optional Header]+0xD0 dans le cas de PE32 (ou [offset Optional Header]+0xE0 dans celui de PE32+).
Dans le cas de notre assembly x86, ce sera donc à l'offset 0x168 :
32-bit required ? : Data Directory contenant l'en-tête CLI
VirtualAddressCLIHeader = 0x00002008
SizeCLIHeader = 0x48

Détermination de l'offset de fichier de l'en-tête CLI : localisation de la Section Table
Comme nous travaillons sur le fichier et pas en mémoire, la valeur de VirtualAddress (Relative Virtual Address, RVA) ne nous sera pas très utile : nous avons besoin de la convertir en offset de fichier au moyen des en-têtes de section.
La table des sections (Section Table) se trouve directement après l'en-tête optionnel.
Comme la taille l'en-tête optionnel n'est pas fixe, nous aurons besoin de la valeur SizeOfOptionalHeader (2 octets) de l'en-tête COFF, lisible à l'offset de fichier [offset COFF File Header]+0x10
Soit dans notre exemple l'offset 0x94 :
32-bit required ? : Détermination de l'offset de fichier de l'en-tête CLI : localisation de la Section Table
La taille de l'en-tête optionnel est 0xE0 (224 octets) donc notre Section Table se trouve à l'offset [offset Optional Header]+0xE0 (=[offset Section Table])
Soit dans notre cas l'offset 0x178.

Détermination de l'offset de fichier de l'en-tête CLI : nombre de sections
Le nombre de sections est défini par la valeur NumberOfSections (2 octets) de l'en-tête COFF, lisible à l'offset de fichier [offset COFF File Header]+0x2
Soit dans notre exemple l'offset 0x86 :
32-bit required ? : Détermination de l'offset de fichier de l'en-tête CLI : nombre de sections
Nous avons donc 3 sections.

Détermination de l'offset de fichier de l'en-tête CLI : recherche de la section intéressante
La taille de chaque en-tête de section est de 40 octets (0x28), nos 3 en-têtes sont donc situés aux offsets 0x178, 0x1A0 et 0x1C8.
Ce qui nous intéresse en premier lieu est de trouver quelle section contient notre en-tête CLI, donc celle pour laquelle ce test est vérifié : VirtualAddress <= VirtualAddressCLIHeader < VirtualAddress+VirtualSize
Pour rappel, VirtualAddressCLIHeader = 0x00002008
La valeur VirtualSize (4 octets) est située à l'offset [offset Section Header]+0x8
La valeur VirtualAddress (4 octets) est située à l'offset [offset Section Header]+0xC
Dans notre exemple, c'est la section .text qui correspond ([offset Section Header]=0x178) :
32-bit required ? : Détermination de l'offset de fichier de l'en-tête CLI : recherche de la section intéressante
VirtualAddressSection = 0x00002000
VirtualSizeSection = 0x744

Détermination de l'offset de fichier de l'en-tête CLI : calcul de l'offset
Nous avons besoin de la valeur PointerToRawData (4 octets) de la section correspondant à VirtualAddressCLIHeader, située à l'offset [offset Section Header]+0x14
32-bit required ? : Détermination de l'offset de fichier de l'en-tête CLI : calcul de l'offset : extraction PointerToRawData
PointerToRawData = 0x200

Nous pouvons maintenant déterminer l'offset de fichier de notre en-tête CLI : il s'agit de la valeur définie par PointerToRawDataSection + (VirtualAddressCLIHeader - VirtualAddressSection).
[offset CLI Header] = 0x200 + (0x00002008 - 0x00002000)
[offset CLI Header] = 0x208

32-bit required ? : Détermination de l'offset de fichier de l'en-tête CLI : calcul de l'offset : en-tête CLI

Extraction de la valeur Flags de l'en-tête CLI
Le document ECMA-335 : Common Language Infrastructure (CLI) décrit l'en-tête CLI dans sa section File format extensions to PE / Section headers / CLI header / Runtime flags
La valeur (4 octets) est située à l'offset [offset CLI Header]+0x10, soit 0x218 pour notre assembly x86 :
32-bit required ? : Extraction de la valeur Flags de l'en-tête CLI
0x3 = COMIMAGE_FLAGS_ILONLY | COMIMAGE_FLAGS_32BITREQUIRED

32-bit required = true

 

En reprenant tout cela avec l'assembly x64, ça donne :

Position de l'en-tête PE : 0x80
[offset COFF File Header] = 0x84
Type de machine (offset 0x84) : 0x8664 (x64)
[offset Optional Header] = 0x98
Format PE (offset 0x98) : 0x20B (PE32+)
NumberOfRvaAndSizes (offset 0x104) : 0x10
VirtualAddressCLIHeader (offset 0x178) = 0x00002000
SizeCLIHeader (offset 0x17C) : 0x48
SizeOfOptionalHeader (offset 0x94): 0xF0
[offset Section Table] = 0x188
NumberOfSections (offset 0x86) : 2
Section contenant le header CLI : .text (0x00002000, 0x6EB)
[offset Section Header] = 0x188
PointerToRawData (offset 0x19C) : 0x200
[offset CLI Header] = 0x200
Flags (offset 0x210) : 0x1 (COMIMAGE_FLAGS_ILONLY, 32-bit required = false)

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: jeudi 30 juin 2011 02:13 par coq
Classé sous : ,

Commentaires

Rui a dit :

Moi je dit, t'es un grand malade toi d'écrire des trucs comme ça ;-)

Merci pour ce vrai beau post.

Pour ma part j'ai rarement eut directement le besoin mais en général une coup de réflector/ILspy quand c'est possible est ce qu'il y a de plus pratique. SInon par fois en prod le besoin le plus courant est juste de vérifier d'ou vient une dll (est-ce bien celle du build officiel?). Dans ce cas en général un drop de la dll dans un notepad en mode hexa permet de rapidement repérer les infos qui nous intéressent qui sont eux en texte presque clair (le chemin des sources par ex)

# juillet 1, 2011 02:39

coq a dit :

Je me suis dit que quitte à assouvir ma curiosité et prendre des notes, autant écrire un article pour les partager :-)

# juillet 1, 2011 13:50

Lutinore a dit :

Je voulais te dire que t'es un grand malade d'écrire des trucs pareils.. mais ça a déjà été dit en fait !

:p

# juillet 12, 2011 10:37

coq a dit :

:-)

# juillet 12, 2011 21:03
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- L’application des MiniDrones Parrot est aussi disponible pour Windows 8.1 par Blog de Jérémy Jeanson le 10-28-2014, 15:01

- L’application des MiniDrones Parrot est enfin disponible pour Windows Phone par Blog de Jérémy Jeanson le 10-27-2014, 09:49

- Mise à jour Samsung 840 EVO sur core server par Blog de Jérémy Jeanson le 10-27-2014, 05:59

- MVP Award 2014 ;) par Blog de Jérémy Jeanson le 10-27-2014, 05:42

- « Naviguer vers le haut » dans une librairie SharePoint par Blog de Jérémy Jeanson le 10-07-2014, 13:21

- PowerShell: Comment mixer NAGIOS et PowerShell pour le monitoring applicatif par Blog Technique de Romelard Fabrice le 10-07-2014, 11:43

- ReBUILD 2014 : les présentations par Le blog de Patrick [MVP Office 365] le 10-06-2014, 09:15

- II6 Management Compatibility présente dans Windows Server Technical Preview avec IIS8 par Blog de Jérémy Jeanson le 10-05-2014, 17:37

- Soft Restart sur Windows Server Technical Preview par Blog de Jérémy Jeanson le 10-03-2014, 19:43

- Non, le certificat public du CA n’est pas un certificat client !!! par Blog de Jérémy Jeanson le 10-03-2014, 00:08