Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

CoqBlog

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

Actualités

VMMap en mode instrumentation sur système 64bit : attention à la plateforme cible du build .NET

Si vous tentez d'utiliser VMMap en mode instrumentation* vous avez peut-être rencontré une erreur relativement peu explicite au lancement du profiling :

VMMap : message d'erreur "Unable to launch application", "The handle is invalid"

Unable to launch application:
"E:\Temp\ApplicationToProfile\ApplicationToProfile\bin\Release\ApplicationToProfile.exe"

Error:
The handle is invalid.

 

Cause

Cela se produit car sur un système 64bit, quand on lui fournit un exécutable compilé en plateforme cible Any CPU (Any CPU à l'ancienne, pas le nouveau Any CPU + "Prefer 32-bit" de VS2012), VMMap utilise son lanceur 32bit par défaut alors que notre processus va démarrer avec le CLR 64bit.

Ce comportement se constate facilement en utilisant Process Monitor (procmon) pour examiner le lancement du profiling, en ajoutant des filtres "Process Name" pour "vmmap.exe", "vmmap64.exe" et "ApplicationToProfile.exe" (dans mon cas) :

Process Monitor - Filtres

Process Monitor - Trace lancement profiling VMMap

On voit bien que vmmap64.exe (auto-extrait et lancé par VMMap si utilisé sur OS 64bit) relance sa version 32bit (vmmap.exe) pour qu'elle prenne en charge notre exécutable alors qu'il aurait dû lancer notre exécutable directement.
Pour les curieux la valeur du paramètre "-p" est juste le chemin de l'exécutable, en représentation texte des bytes UTF16 Big Endian de la chaine de caractères.

 

Solution

Pour pouvoir utiliser le mode instrumentation de VMMap sur une application .NET lancée sur un système 64bit alors qu'elle était compilée en plateforme cible Any CPU, il faut que celle-ci soit recompilée explicitement pour la plateforme qu'elle va utiliser, c'est-à-dire x64 :

Configuration Manager de Visual Studio

VMMap - Profiling d'application .NET en cours

 

 

* : Pour rappel le mode instrumentation est, par opposition au mode snapshot simple où on s'attache à un processus déjà démarré, le mode où VMMap lance lui même l'exécutable et instrumente certaines méthodes pour pouvoir tracer les allocations de mémoire.

On y accède par le second onglet, "Launch and trace a new process", de la boite de dialogue "Select or Launch Process" :

VMMap : boite de dialogue "Select or Launch Process", onglet "Launch and trace a new process"

Posted: samedi 25 mai 2013 22:25 par coq | 0 commentaire(s)
Classé sous : ,
ProcDump 6.0 : support du filtrage sur messages d'exceptions .NET, des filtres multiples et du ciblage par nom de service

D'après le post sur le blog Sysinternals la dernière version majeure de ProcDump, très sympathique outil de prise de dump offrant un bon nombre de possibilités de paramétrage, apporte quelques évolutions plutôt pratiques :

  • affichage et filtrage sur le message des exceptions .NET (ou JScript)
  • support des filtres multiples
  • ciblage de processus par nom de service Windows

 

Affichage et filtrage sur le message des exceptions .NET (ou JScript)

Le switch -f agit maintenant sur le message de l'exception .NET en plus du nom du type de l'exception.
Cela peut s'avérer intéressant avec certains types d'exception dont le message contient des informations de contexte intéressantes : par exemple filtrer sur la description permet ainsi de limiter le nombre de dumps pris avant de tomber sur la bonne exception.

A titre d'exemple, voici une ligne de commande permettant d'obtenir un dump pour une exception de première chance du type InvalidOperationException dont le message contient "filtrez-moi" :

procdump -e 1 -f "System.InvalidOperationException*filtrez-moi*" -w ProcDumpTestBench

Ainsi, le dump n'est pris qu'à la 3ème exception levée par mon programme de test : la première avait le bon message mais pas le bon type, la seconde avait le bon type mais pas le bon message :

ProcDump v6.00 - Writes process dump files
Copyright (C) 2009-2013 Mark Russinovich
Sysinternals - www.sysinternals.com
With contributions from Andrew Richards

Waiting for process named ProcDumpTestBench...

Process:               ProcDumpTestBench.exe (6556)
CPU threshold:         n/a
Performance counter:   n/a
Commit threshold:      n/a
Threshold seconds:     n/a
Number of dumps:       1
Hung window check:     Disabled
Exception monitor:     First Chance+Unhandled
Exception filter:      *System.InvalidOperationException*filtrez-moi*
Terminate monitor:     Disabled
Dump file:             D:\Temp\ProcDumpTestBench_YYMMDD_HHMMSS.dmp


Press Ctrl-C to end monitoring without terminating the process.

CLR Version: v4.0.30319

[13:39:06] Exception: E0434F4D.System.NullReferenceException ("Filtrez-moi !")
[13:39:13] Exception: E0434F4D.System.InvalidOperationException ("Operation is not valid due to the current state of the object.")
[13:39:33] Exception: E0434F4D.System.InvalidOperationException ("Filtrez-moi !")

First-chance Exception.
Writing dump file D:\Temp\ProcDumpTestBench_130520_133933.dmp ...
Dump written.

Dump count reached.

Concernant le support JScript, je vous laisse tester.

 

Support des filtres multiples

Il est maintenant possible de spécifier plusieurs fois le switch -f, ce qui nous permet d'effectuer un filtrage en mode "OU".

Pour reprendre mon test précédent, si je veux obtenir un dump pour une exception de première chance du type InvalidOperationException dont le message contient "filtrez-moi" OU quand une exception de type NullReferenceException est levée, je peux utiliser une ligne de commande de ce type :

procdump -n 3 -e 1 -f "System.NullReferenceException" -f "System.InvalidOperationException*filtrez-moi*" -w ProcDumpTestBench

Au passage : "-n 3" permet de prendre au maximum 3 dumps avant que ProcDump s'arrête. Dans le cas présent je ne l'utilise pas pour limiter le nombre de dump pris mais plutôt pour pouvoir en prendre plusieurs avant que ProcDump se coupe (sinon par défaut il se coupe au premier dump pris) et me permet ainsi d'obtenir plusieurs dumps sans relancer la commande.

On voit qu'aucun dump n'est pris pour la première exception car elle ne correspond à aucun des 2 filtres, mais que les 3 suivantes ont déclenché une prise de dump car elles correspondaient à au moins un des 2 filtres :


Copyright (C) 2009-2013 Mark Russinovich
Sysinternals - www.sysinternals.com
With contributions from Andrew Richards

Waiting for process named ProcDumpTestBench...

Process:               ProcDumpTestBench.exe (6556)
CPU threshold:         n/a
Performance counter:   n/a
Commit threshold:      n/a
Threshold seconds:     n/a
Number of dumps:       3
Hung window check:     Disabled
Exception monitor:     First Chance+Unhandled
Exception filter:      *System.NullReferenceException*
                       *System.InvalidOperationException*filtrez-moi*
Terminate monitor:     Disabled
Dump file:             D:\Temp\ProcDumpTestBench_YYMMDD_HHMMSS.dmp


Press Ctrl-C to end monitoring without terminating the process.

CLR Version: v4.0.30319

[13:49:05] Exception: E0434F4D.System.InvalidOperationException ("Operation is not valid due to the current state of the
object.")
[13:49:44] Exception: E0434F4D.System.NullReferenceException ("Object reference not set to an instance of an object.")

First-chance Exception.
Writing dump file D:\Temp\ProcDumpTestBench_130520_134944.dmp ...
Dump written.

[13:49:48] Exception: E0434F4D.System.NullReferenceException ("Filtrez-moi !")

First-chance Exception.
Writing dump file D:\Temp\ProcDumpTestBench_130520_134948.dmp ...
Dump written.

[13:49:50] Exception: E0434F4D.System.InvalidOperationException ("Filtrez-moi !")

First-chance Exception.
Writing dump file D:\Temp\ProcDumpTestBench_130520_134950.dmp ...
Dump written.

Dump count reached.

 

Ciblage de processus par nom de service Windows

Auparavant on ne pouvait spécifier la cible que par son nom de processus ou son ID.
Maintenant on peut aussi utiliser le nom de service, ce qui peut être pratique lors des sessions de debug d'un même service dont plusieurs instances se trouvent sur la même machine (exemple connu : SQL Server et ses instances nommées) : on évite ainsi de changer manuellement l'ID processus dans la ligne de commande ProcDump à chaque fois que le service démarre, car dans le cas d'instance multiples le nom du processus n'est pas pratique : toutes les instances du service portent le même nom de processus, seul le nom de service diffère.

Ce qui est dommage dans le cas présent c'est que le switch -w ne semble pas être supporté quand on spécifie un nom de service.
Pour rappel le switch -w nous permet de demander à ProcDump de se mettre en attente du démarrage de la cible si elle n'est pas encore disponible au lancement de la commande.

Le switch -w couplé à la spécification du nom de service pourrait nous permettre de debugger plus facilement les scénarios où le service rencontre une erreur au démarrage (ou très peu de temps après).

Dommage, mais j'espère que ce support viendra plus tard :-)

Posted: lundi 20 mai 2013 14:50 par coq | 0 commentaire(s)
Classé sous : , ,
.NET / Debug : inspection de la mémoire d'applications .NET (dump ou processus live) : première livraison d'une librairie .NET par Microsoft

Excellente nouvelle pour ceux qui ont besoin d'effectuer des analyses poussées et/ou automatiques de dumps d'application .NET : Microsoft nous livre une première beta d'une librairie .NET appelée Microsoft.Diagnostics.Runtime (ClrMD).
A noter qu'elle est estampillée "beta" mais de ce que j'en ai vu le niveau de qualité est très bon.

 

A quoi ça sert ?

Cela veut tout simplement dire qu'il y a maintenant moyen d'écrire des applications, en C#, permettant d'effectuer tout ou partie des tâches réalisables via des extensions du debugger (comme SOS, SOSEX, PSSCOR, ...), mais aussi bien plus encore.
Pour ne prendre que quelques exemples de tâches pour lesquelles les extensions actuelles ne sont pas forcément prévues :

  • extraire la liste complète des chaines de caractères présentes dans le dump, avec possibilité de filtrage par génération, longueur, valeur etc
  • extraire des statistiques de doublons de chaines de caractères présentes dans le dump
  • extraire des statistiques sur la répartition des objets par génération, leur ventilation sur les différentes heaps dans le cas d'un GC server, le nombre d'objets boxed présents sur la heap et leur répartition par génération, etc etc
  • ...
Je n'ai pas d'exemple de code à vous présenter dans l'immédiat, mais Lee Culver en fourni un à la fin de son post de présentation de ClrMD sur le blog MSDN de l'équipe .NET : .NET Crash Dump and Live Process Inspection - .NET Blog
Attention : à l'heure où j'écris ces lignes le sample comporte un petit bug, lisez bien les 2 premiers commentaires sur le post (ceci dit si vous ne les lisez pas vous vous apercevrez très vite du bug ;-))

Alors ? C# c'est quand même plus sympa comme language de script que du WinDbg-script, non ? ;-)

 

La fin de SOS, SOSEX & co ?

Je ne pense pas.
De mon point de vue cette librairie ne fait en rien de l'ombre aux extensions existantes comme l'excellente SOSEX de Steve Johnson, qui couvre bien d'autres besoins qu'il n'est pas forcément utile (ou possible) de recouvrir avec un code managé (sauf pour les besoins d'analyse automatisée, bien sûr).

 

Mes impressions

J'ai eu l'occasion d'utiliser ClrMD ces derniers jours sur des dumps, plutôt massifs, d'application .NET 4.0 et je dois dire que je suis plutôt content des possibilités offertes et des performances, surtout que cette librairie est sortie au moment où certains besoins d'analyse faisaient que je commençais à me demander s'il n'allait pas falloir que je me (re)mette au C/C++ pour tenter de développer une extension pour WinDbg...
Mon C/C++ étant (très) rouillé et le développement de ce type d'extension ciblant .NET n'étant apparemment pas des plus aisés* (mais je n'ai pas forcément encore suffisamment creusé le sujet), je suis très content de voir cette librairie apparaitre !

Et de toute façon, si le besoin se fait sentir, il est toujours possible d'utiliser l'API managée pour produire un fichier intermédiaire qui pourra être traité par un code non-managé sans avoir besoin de d'utiliser directement le dump depuis celui-ci.

En tout cas la mise à disposition de ClrMD me semble encourageante !
Depuis longtemps j'avais l'impression que Microsoft ne prenait pas correctement en compte la nécessité de fournir aux développeurs utilisant sa technologie .NET des moyens avancés utilisables via .NET permettant d'effectuer des analyses post-mortem, pas forcément couvertes par les moyens mis à disposition par l'éditeur lui-même au travers de Visual Studio ou de l'extension SOS (voire de PSSCOR) : il semble que les choses évoluent dans le bon sens avec la mise à disposition de cette librairie.

Une des premières phrases du post de Lee Culver semble en tout cas montrer la prise en compte de ces besoins :

"We understand that this API won’t be for everyone -- hopefully debugging .NET crash dumps is a rare thing for you. However, our .NET Runtime team has had so much success automating complex diagnostics tasks with this API that we wanted to release it publicly."

Au passage, il est fait mention des crash dumps, j'aurais parlé de dumps tout court : les dumps ne sont pas seulement intéressants pour les cas de crashes, loin de là !
En effet dans les cas où l'environnement de déploiement des applications n'est pas parfaitement maitrisé par l'éditeur et où l'analyse porte sur un comportement non reproduit en environnement de test, il n'est en général pas possible (ni souhaitable/souhaité) d'obtenir un accès administrateur temporaire à une machine vouée à retourner en production pour y utiliser un débuggeur (même s'il ne laisse pas de traces de son passage) : obtenir une série de dumps peut s'avérer plus facile car les développeurs n'ont pas besoin d'accès au serveur, et des outils comme ProcDump pouvant être "facilement" utilisés par les personnes gérant la production (en collaboration avec les développeurs, bien sûr).

J'ai bien conscience du fait que les développeurs du runtime .NET avaient sans doute envie de partager ce genre d'outils depuis longtemps (les développeurs ont tendance à aimer fournir des jouets à d'autres développeurs), je pense que la mise à disposition publique devait coincer à d'autres niveaux. Je ne connais pas très bien l'organisation interne de Microsoft sur ce type de décision, mais je suppose qu'il y a toujours un manager pour poser un tampon Yes/No (voire un passage par une section juridique) et je ne parle même pas de l'aspect budget : bref, je pense que cette mise à disposition publique est un bon signe pour l'avenir.

En tout cas j'espère que l'équipe recevra suffisamment de retours positifs pour les motiver à continuer sur cette lancée !

Bon coding/debug/analyse !

 

* : En plus d'après ce que je sais, avant l'apparition de la version 4.5 de l'API de debug du CLR, on ne pouvait pas énumérer les objets présents en mémoire, ce qui est légèrement génant : la documentation de ICorDebugProcess::EnumerateObjects arbore un joli "This method has not been implemented."
Ce problème serait maintenant résolu par l'apparition de ICorDebugProcess5::EnumerateHeap mais comme dit plus haut il m'est actuellement assez difficile de tester.

Posted: samedi 11 mai 2013 22:21 par coq | 0 commentaire(s)
Classé sous : , ,
.NET / Debug post-mortem : obtenir le fichier mscordacwks.dll correspondant à un dump quand on n'a plus d'accès direct à ce fichier

En général, si sur le moment la machine où la session de debug post-mortem à lieu ne dispose pas de la même version du CLR que la machine source, on récupère le fichier mscordacwks.dll sur la machine d'origine du dump.

Cependant, si la session de debugging à lieu plus tard ou si le dump est réutilisé par la suite, par exemple pour vérifier une théorie lorsqu'un problème similaire se produit, il est plus compliqué (pour ne pas dire impossible) de mettre la main sur une machine disposant encore de la bonne version du runtime.

Dans ce genre de cas, la méthode décrite par Sasha Goldshtein dans le post Obtaining mscordacwks.dll for CLR Versions You Don’t Have est une excellente solution : on récupère le fichier dans les setups qui permettent de les installer, qu'on peut localiser facilement grâce aux listes que maintient Doug Stewart pour faire le lien entre une version du CLR et le setup d'où elle provient :

 

Petit complément à l'article de Sasha Goldshtein

Si en utilisant 7-Zip ou même Universal Extractor vous vous retrouvez avec des fichiers inutilisables en sortie du fichier cab, tels que "_manifest_.cix.xml", "0", "1", "2", "3", ... (ce qui est notamment le cas en sortie des fichiers cab trouvés dans les fichiers .msu) le mieux est de plutôt passer par l'outil expand.exe pour extraire les fichiers de l'archive CAB.

Exemple pour ce patch :
md "Windows6.0-KB2572075-x86" & expand.exe -f:* Windows6.0-KB2572075-x86.cab Windows6.0-KB2572075-x86

 

Vous voilà parés pour assouvir votre passion pour l'archéologie ! ;-)

L'installation de .NET 4.5 n'est pas anodine : le targeting .NET 4.0 n'isole pas de tous les changements brisants, au runtime comme à la compilation
.NET 4.5 est un remplacement de .NET 4.0, pas une version qui s'installe à côté. 

Ça semble évident une fois que c'est dit mais je pense qu'il est tout de même utile de faire un petit rappel, juste au cas où :-)

La prise en compte de ce fait dès maintenant pour des applications .NET 4.0 n'étant pas amenées à être migrées explicitement vers .NET 4.5, mais qui pourraient être amenées à cohabiter avec des applications .NET 4.5, pourrait permettre d'être proactif sur la correction d'éventuels bugs et d'éviter des effets de bord lors de la mise en production de l'autre application (ou pourrait aider à justifier une isolation de l'environnement des applications existantes, au besoin).
Ce rappel pourrait aussi être utile à ceux qui analysent des bugs sur des applications déployées dans des environnements qu'ils ne maitrisent pas forcément complètement : prendre en compte ce fait dans le processus d'analyse d'incident pourrait leur faire gagner un peu de temps.

Je parle ici de l'installation du framework seul et non pas de celle du SDK ou de Visual Studio 2012.

 

Changements ayant un impact à l'exécution d'une application déjà compilée pour .NET 4.0

Une liste de changements brisants est disponible ici : Application Compatibility in the .NET Framework 4.5

Ces changements peuvent avoir un impact direct sur une application .NET 4.0 existante sans qu'elle soit recompilée.
Prenons l'exemple du changement "Unobserved exceptions in System.Threading.Tasks.Task operations" de la catégorie "Core" :

Change : Because the System.Threading.Tasks.Task class represents an asynchronous operation, it catches all non-severe exceptions that occur during asynchronous processing. In the .NET Framework 4.5, if an exception is not observed and your code never waits on the task, the exception will no longer propagate on the finalizer thread and crash the process during garbage collection.

Impact : This change enhances the reliability of applications that use the Task class to perform unobserved asynchronous processing. The previous behavior can be restored by providing an appropriate handler for the TaskScheduler.UnobservedTaskException event.

 

Prenons cet exemple de code mettant en avant une Task, avec exception non gérée, dont on n'attend pas le résultat :

static void Main(string[] args)
{
Task.Factory.StartNew(() =>
{
throw new NotImplementedException();
});

// Attente de 5 secondes pour laisser de la marge
Thread.Sleep(5000);

// Forçage d'une collection complète du GC
// et attente de l'exécution des finaliseurs
GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine("Press a key to quit...");
Console.ReadKey(true);
}
 
 
Sous .NET 4.0, ce type de code entrainait forcément le crash de l'application au moment de la finalisation de l'instance de Task :

Unhandled Exception: System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NotImplementedException: The method or operation is not implemented.
   at Program.<Main>b__0()
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.TaskExceptionHolder.Finalize()

 

Une fois .NET 4.5 installé, et sans rien faire d'autre, elle ne crashera plus.

A première vue excellente nouvelle pour une vraie application sauf que, suivant ce qu'elle fait de ce qui ressort du traitement en échec, le changement de comportement peut avoir des conséquences plus ou moins graves qui étaient peut-être, jusqu'alors, évitées du fait que l'application s'arrêtait brutalement avant que l'état non validé ne soit utilisé (en partant du principe que l'instance de Task était finalisée suffisamment tôt).

 

Changements ayant un impact à la compilation (csc.exe, MSBuild et VS2010 sous certaines conditions)

En tant que remplacement de .NET 4.0, .NET 4.5 met aussi à jour le compilateur C# livré avec le Framework (csc.exe) : tout code compilé via csc.exe risque donc de bénéficier de certains changements qui lui ont été apportés dès qu'une nouvelle compilation aura lieu après l'installation de .NET 4.5.
Cela peut avoir un impact sur certaines applications où du code est compilé à la volée via csc.exe (comme des sites ASP.NET, par exemple).

Concernant la compilation via MSBuild (ligne de commande et agents de build) et Visual Studio 2010, je pense que le comportement va dépendre de la configuration MSBuild utilisée pour le projet.
Je n'ai pas encore vraiment creusé ce vaste sujet.

Concernant la compilation sous Visual Studio 2012 en targeting .NET 4.0, la question ne se pose pas : les améliorations du compilateur éligibles s'appliquent car selon toute logique le compilateur in-process doit avoir le même comportement que csc.exe.

En tout cas, là encore, si la compilation bascule sur les dernières évolutions il peut y avoir quelques effets de bord.

Prenons cet exemple de code connu pour ne pas donner le résultat auquel on aurait pu s'attendre en .NET 4.0 (exemple bateau, mais dans l'immédiat je n'ai pas trouvé d'idée de code concret qui n'ajoute pas trop de détails masquant le fond du problème) :

static void Main(string[] args)
{
Int32[] integers = new Int32[] { 1, 2, 3, 4, 5 };

// !!! NE PAS UTILISER CE CODE !!!
// !!! DON'T USE THAT CODE !!!
foreach (Int32 integer in integers)
{
Task.Factory.StartNew(() =>
{
Console.WriteLine(integer);
});
}
Thread.Sleep(2000);
Console.WriteLine("Press a key to quit.");
Console.ReadKey(true);
}

En .NET 4.0, au lieu d'afficher 5 chiffres différents dans un ordre indéterminé comme on aurait pu s'y attendre à la première lecture, ce code a toutes les chances d'afficher une série de cinq caractères '5'.
Sans rentrer dans les détails, ce comportement est lié à la façon dont le compilateur va émettre le code C# correspondant à cette utilisation d'une variable hors scope dans le délégué, et à l'endroit où est déclarée cette variable.
 
Pour mémoire, afin d'obtenir l'effet désiré sous .NET 4.0, il fallait corriger le code d'une de ces manières :
 
static void Main(string[] args)
{
Int32[] integers = new Int32[] { 1, 2, 3, 4, 5 };

// !!! NE PAS UTILISER CE CODE !!!
// !!! DON'T USE THAT CODE !!!
foreach (Int32 integer in integers)
{
Int32 i = integer;
Task.Factory.StartNew(() =>
{
Console.WriteLine(i);
});
}
Thread.Sleep(2000);
Console.WriteLine("Press a key to quit.");
Console.ReadKey(true);
}

static void Main(string[] args)
{
Int32[] integers = new Int32[] { 1, 2, 3, 4, 5 };

// !!! NE PAS UTILISER CE CODE !!!
// !!! DON'T USE THAT CODE !!!
foreach (Int32 integer in integers)
{
Task.Factory.StartNew((i) =>
{
Console.WriteLine(i);
},
integer);
}
Thread.Sleep(2000);
Console.WriteLine("Press a key to quit.");
Console.ReadKey(true);
}
 
Le compilateur C# livré avec .NET 4.5 n'émet plus le même code IL que celui de .NET 4.0 pour le premier exemple de code : ce qu'on pouvait considérer comme un bug du compilateur ou plutôt un comportement non-intuitif a été corrigé pour cette release et le code compilé affichera une liste de 5 chiffres différents dans un ordre indéfini, comme initialement attendu.
Ce changement est visible pour une compilation sous Visual Studio 2012 (y compris en cible .NET 4.0) ou en utilisant le CSC mis à jour par l'installation de .NET 4.5, mais pourrait aussi s'appliquer comme dit plus haut à des compilations depuis Visual Studio 2010 ou MSBuild en général.

Là encore cela peut sembler être une bonne nouvelle, sauf que si l'environnement de maintenance d'une application existante n'est pas isolé de celui servant à produire les nouvelles applications ciblant le nouveau framework .NET, on risque de se retrouver à livrer une régression à un endroit non modifié du code : si jamais l'application fonctionnait correctement avec ce bug non détecté, le fait que le code initialement défaillant "tombe en marche" peut avoir des conséquences indésirables.

Visual Studio 2012 / .NET 4.5 : le grand retour de la plateforme cible AnyCPU par défaut ?

Pas tout à fait : il y a une subtilité à connaitre pour les projets .NET 4.5.

 

Sous Visual Studio 2010, contrairement à ses prédécesseurs, certains types de projets .NET (dont Service Windows, Console, Windows Forms, WPF, ...) étaient créés en plateforme cible x86 et non plus AnyCPU, et ce quelle que soit la version du framework cible (de 2.0 à 4.0).

Pour rappel, le fonctionnement de la plateforme cible AnyCPU telle qu'elle existe depuis que le code .NET peut s'exécuter en x64 (et non pas en 32bit sous WOW64) pouvait être résumé de cette manière pour une application autonome (pour une application hébergée sous IIS les paramètres du pool entrent en jeu aussi) :

SI système x64 ET CLR x64 disponible
    ALORS démarrer l'application en x64
SINON SI système x64 ET CLR x64 non disponible ET CLR x86 disponible
    ALORS démarrer l'application en x86
SINON SI système x86 ET CLR x86 disponible
    ALORS démarrer l'application en x86
SINON
    ???

Sous Visual Studio 2010 une solution ne comportant que des projets ciblant par défaut une plateforme x86 possédait donc ce type de configuration de build :

Capture d'écran de la boite de dialogue de gestion de la configuration de build sous Visual Studio 2010 - x86

En cas d'ajout d'un projet en cible AnyCPU par défaut (un projet de type librairie par exemple), la solution se voyait ajouter une plateforme "Mixed Platforms" sous laquelle tous les projets étaient compilés :

Capture d'écran de la boite de dialogue de gestion de la configuration de build sous Visual Studio 2010 - Mixed Platforms

(Je laisse volontairement de côté ce qui se passe sur les autres plateformes de solution, ce n'est pas le sujet.)

La configuration de projet pour une cible AnyCPU est la suivante :

Capture d'écran des propriétés de build d'un projet C# - AnyCPU

Côté compilateur C#, ça donne une spécification de /platform:anycpu (la valeur par défaut).

 

 

Et sous Visual Studio 2012 ?

Avec Visual Studio 2012, la création d'un projet Console (par exemple) fait de nouveau apparaitre le terme "Any CPU" comme plateforme cible par défaut.

Prenons une solution de test à laquelle on ajoute un projet de chaque version du framework disponible (2.0, 3.0, 3.5, 4.0 et 4.5) :

Capture d'écran de la boite de dialogue de gestion de la configuration de build sous Visual Studio 2012 - AnyCPU

Vous vous attendiez à ce que ces applications démarrent en x64 sur un OS x64 disposant du CLR x64 ?
Pas tout à fait :

Capture d'écran du gestionnaire de tâches avec l'affichage de la plateforme

L'application .NET 4.5 a démarré sous le CLR 32bit, ce qui n'est pas vraiment ce que nous attendions.

L'explication de ce comportement se trouve dans les propriétés du projet .NET 4.5 :

Capture d'écran des propriétés de build du projet .NET 4.5
Et c'est coché par défaut.

Ce qui côté compilateur C# donne une spécification de /platform:anycpu32bitpreferred :

anycpu (default) compiles your assembly to run on any platform. Your application runs as a 64-bit process whenever possible and falls back to 32-bit when only that mode is available.

anycpu32bitpreferred compiles your assembly to run on any platform. Your application runs in 32-bit mode on systems that support both 64-bit and 32-bit applications. You can specify this option only for projects that target the .NET Framework 4.5.

Cette option est bien désactivée pour les projets .NET 4.0 et inférieurs :

Capture d'écran des propriétés de build du projet .NET 4.0

 

Côté détection sur les binaires finaux je ne reviendrais pas sur l'ensemble des possibilités, que nous avons vu il y a quelques temps pour les versions précédentes de .NET, nous nous contenterons d'ILSpy qui permet bien de faire la différence :

Application .NET 4.5 / AnyCPU+Prefer 32-bit :

Capture d'écran d'ILSpy, sur l'assembly .NET 4.5 / AnyCPU+Prefer 32-bit

Application .NET 4.0 / AnyCPU :

Capture d'écran d'ILSpy, sur l'assembly .NET 4.0 / AnyCPU

 

Bref, il faut le savoir.

Personnellement j'aurais préféré que sous Visual Studio la notion "32-bit preferred" soit directement dans le nom de la plateforme de projet, ce qui l'aurait rendu directement visible dans le gestionnaire de configurations de build de la solution.
J'aurais également préféré qu'une solution ne comprenant que des projets de ce type dispose d'une plateforme de solution elle aussi nommée "Any CPU 32-bit preferred", et qu'une solution comprennant les 2 types de "Any CPU" passe en plateforme de solution "Mixed Platforms".

Après c'est sûr que pour ceux qui n'auront connu que les projets .NET 4.5 sous Visual Studio 2012, c'est moins gênant.

SqlParameter.Value et référence nulle : ce qu'il ne faut pas oublier

Il s'agit d'un point qui peut être facilement oublié, surtout si on ne réalise pas souvent de code d'accès aux données "bas niveau" : affecter une référence nulle à la propriété Value d'une instance de la classe SqlParameter ne provoque PAS l'envoi d'un NULL au serveur SQL.
Dans les faits, affecter une référence nulle à un paramètre sur un appel de procédure stockée équivaut à demander à ce que la valeur par défaut du paramètre soit utilisée : quand on doit envoyer un NULL au serveur, on doit affecter DBNull.Value en tant que valeur du paramètre.

Si on ne veille pas à ce point on risque de rencontrer des comportements non désirés (liste probablement non-exhaustive) :

  • Levée d'une exception SqlException avec ce type de message : "Procedure or function 'sp_DoSomething' expects parameter '@Name', which was not supplied." ("La procédure ou fonction 'sp_DoSomething' attend le paramètre '@Name', qui n'a pas été fourni.").
  • Utilisation non désirée de la valeur par défaut du paramètre de procédure stockée.

Je considère le cas d'utilisation non désirée de la valeur par défaut comme le plus problématique car il peut mener à une corruption de données directe.
Par "corruption de données directe" j'entends le cas où l'action en elle-même corrompt les données, donc l'absence de risque de corruption directe ne veut pas dire qu'il n'y aura pas un de ces cas de corruption indirecte qui peut survenir quand un code étouffe un cas d'erreur non attendu.

A ma connaissance l'oubli d'utilisation de DBNull.Value dans le cas de requêtes INSERT/UPDATE paramétrées ne devrait pas provoquer de corruption de données directe : il devrait toujours entrainer la levée d'une exception avec un message du type "The parameterized query '...' expects the parameter '...', which was not supplied." ("La requête paramétrée '...' attend le paramètre '...', qui n'a pas été fourni.").

 

Je pense qu'il est nécessaire de s'assurer que les équipes de développement sont conscientes de ce genre de détail de fond même si elles utilisent habituellement des frameworks qui masquent la plomberie ADO.NET : ça évitera peut-être quelques problèmes le jour où elles devront travailler sans.
La connaissance de ce genre de détails peut s'avérer utile lors de phases de debug sur du code hérité ou tout simplement sur du code mettant en oeuvre des librairies tierces.

 

Illustrons un peu tout ça avec un exemple de ce qu'il se passe dans le cas d'une procédure stockée.

Prenons ce prototype de procédure stockée :

CREATE PROCEDURE dbo.sp_DoSomething
(
@Id int,
@Name nvarchar(1024)
)
AS
BEGIN

PRINT N'Something';

END

Cette méthode qui l'utilise (à noter que j'utilise les définitions de paramètres "à l'ancienne" mais le comportement de AddWithValue est le même) :

public void DoSomething(Int32 id, String name)
{
using (SqlConnection connection = new SqlConnection(this.ConnectionString))
{
using (SqlCommand command = new SqlCommand("sp_DoSomething", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@Id", SqlDbType.Int, 4).Value = id;
command.Parameters.Add("@Name", SqlDbType.NVarChar, 1024)
.Value = name;

connection.Open();
command.ExecuteNonQuery();
}
}
}


Et cet appel de la méthode :

instance.DoSomething(
1, // id
null // name
);

Le résultat de l'exécution sera similaire à celui que nous aurions eu en utilisant le code TSQL suivant dans SQL Server Management Studio :

EXEC sp_DoSomething @Id=1, @Name=DEFAULT; 

Alors que nous espérions probablement ceci :

EXEC sp_DoSomething @Id=1, @Name=NULL; 

Notre procédure stockée ne spécifiant pas de valeur par défaut pour le paramètre @Name, cet appel va échouer avec l'erreur citée plus haut.

 

Le risque de corruption de données peut intervenir quand une valeur par défaut est définie pour le paramètre @Name :

CREATE PROCEDURE [dbo].[sp_DoSomething]
(
@Id int,
@Name nvarchar(1024) = N'lorem ipsum'
)
AS
BEGIN

PRINT @Name;

END

Si on utilise le même code côté .NET, il n'y aura pas d'erreur mais un print de "lorem ipsum".
Si le paramètre est utilisé pour être persisté ou pour prendre des décisions, on a utilisé une valeur "lorem ipsum" à la place d'un NULL, avec toutes les conséquences que cela peut avoir.

 

En général le code que nous voulions produire aurait été quelque chose de ce genre :

public void DoSomething(Int32 id, String name)
{
using (SqlConnection connection = new SqlConnection(this.ConnectionString))
{
using (SqlCommand command = new SqlCommand("sp_DoSomething", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@Id", SqlDbType.Int, 4).Value = id;
command.Parameters.Add("@Name", SqlDbType.NVarChar, 1024)
.Value = name ?? (Object)DBNull.Value;

connection.Open();
command.ExecuteNonQuery();
}
}
}

 

On pourrait se dire qu'il suffit que les membres de l'équipe soient au courant du fait que l'affectation d'une référence nulle entraine l'utilisation de la valeur par défaut mais dans les faits je pense que ce serait déstabilisant pour beaucoup de personnes, donc dangereux pour les phases de maintenance et lors de l'intégration de nouvelles personnes à l'équipe.
On s'attend en général plus à un parallèle entre null (C#) et NULL (SQL) qu'à ce genre de comportement donc autant établir le fait que null ne doit jamais être affecté explicitement comme valeur de paramètre, et garder uniquement l'absence d'ajout du paramètre comme déclencheur de l'utilisation de la valeur par défaut.
Ce n'est que mon avis et il est largement influencé par le fait que je ne travaille jamais dans des environnements où la définition d'une valeur par défaut pour un paramètre sert à autre chose qu'à éviter de briser du code existant pendant une phase de transition (ou pour des paramètres de déclenchement d'un mode debug/trace verbeuse qui doit être désactivé par défaut) : il existe peut-être des cas où cet avis n'est pas pertinent.

Debug : "Set Next Statement" ("Définir l'instruction suivante") n'est PAS une machine à remonter le temps

Je dois bien admettre que je ne suis pas très fan de cette fonctionnalité.
Sous Visual Studio elle s'appelle "Set Next Statement" ("Définir l'instruction suivante"), sous SharpDevelop c'est "Set current statement" (ou "Définir l'instruction suivante" si l'IDE est en français) et à ma connaissance cette fonctionnalité n'est pas disponible sous MonoDevelop (du moins sous Windows).

Dans tous les cas, il est important que l'utilisateur du debugger comprenne bien ce que fait cette fonctionnalité et surtout ce qu'elle ne fait pas.
A noter que dans ce post je ne parle en aucun cas de la fonctionnalité IntelliTrace apparue avec Visual Studio 2010 (édition Ultimate minimum...), sur laquelle je n'ai de toute façon aucun recul.

 

Où se trouve cette fonctionnalité ?

Lors d'une session de debug elle est utilisable, par exemple lorsqu'un point d'arrêt est atteint, depuis les menus contextuels ou par simple déplacement du curseur jaune.

Sous Visual Studio :
Capture d'écran de l'élément "Set Next Statement" du menu contextuel de Visual Studio

Sous SharpDevelop :
Capture d'écran de l'élément "Set current statement" du menu contextuel de SharpDevelop

 

Ce que cette fonctionnalité ne fait pas

La fonctionnalité Set Next Statement n'est PAS une machine à remonter le temps : si nous l'utilisons pour déplacer le point d'exécution à un endroit que nous avons déjà passé, les effets des instructions qui ont eu lieu entre ce point dans le passé et le point présent ne seront PAS annulés.
Il n'y a aucun mécanisme de rollback sur les changements d'états, les changements en mémoire en général et encore moins sur les éléments externes (bases de données, fichiers, ...)

 

Ce que cette fonctionnalité fait

Elle ne fait rien d'autre que ce qu'elle annonce : elle nous permet de changer le point d'exécution, c'est-à-dire de définir l'instruction à partir de laquelle l'exécution reprendra quand l'exécution du thread sera relancée.
Cette manoeuvre peut être effectuée, au sein d'une méthode, vers l'avant (pour éviter d'exécuter certaines instructions) ou vers l'arrière (pour recommencer l'exécution de certaines instructions).

Un cas d'utilisation courant de cette fonctionnalité est lorsque l'utilisateur du debugger effectue un step over sur un appel de méthode au lieu du step into qu'il voulait faire.
Dans ce genre de cas l'utilisateur du debugger va vouloir rejouer l'exécution de la méthode sans reprendre sa session de debug de zéro.
Cette manoeuvre est rarement sans risque : il faudrait pour cela que l'exécution de la méthode ne modifie en rien l'état du programme, et qu'elle n'utilise aucune donnée qui soit modifiée par un autre thread ou un élément externe.

 

A quoi faut-il faire attention en utilisant cette fonctionnalité ?

A tout.

Il faut faire attention aux changements d'état qu'effectue le code qui va être rejoué :

  • pour éviter les corruptions de données
  • pour éviter, à l'issue de la phase de debug, de remonter un contexte de reproduction d'un bug qui n'est pas celui d'origine mais un effet de bord de la manoeuvre
  • ...

Il faut faire attention à l'état actuel du programme et comprendre les effets de la reprise du code en un point donné car :

  • nous n'avons pas envie de libérer un verrou que nous n'avons pas acquis
  • nous n'avons pas envie d'utiliser l'instance d'une classe dont nous avons déjà appelé la méthode Dispose
  • nous n'avons pas envie d'utiliser une référence qui a été affecté à null après utilisation
  • ...

Les cas du verrou et de l'appel de Dispose sont intéressants car ils impliquent que l'utilisateur du debugger soit capable de faire attention aux instructions de bloc qui ne sont pas forcément très remarquables (telles que lock et using) et de se représenter mentalement ce que le compilateur va générer comme code à la compilation.
Dans le cas contraire, il va devoir comprendre pourquoi il obtient des exceptions étonnantes lors de la reprise d'exécution, telle que la très claire "System.Threading.SynchronizationLockException : Object synchronization method was called from an unsynchronized block of code." si le point d'exécution est repositionné en plein milieu d'un bloc lock alors que ce dernier avait été complété.

Car après tout, le mot clé lock n'est pas grand chose d'autre qu'une syntaxe plus concise pour laquelle le compilateur va générer l'équivalent d'un bloc try/finally utilisant la classe Monitor.

Si le mot clé lock n'avait pas existé, nous n'aurions pas écrit ça

private void DoSomething()
{
lock (this.lockTarget)
{
// ...
}
}

mais ça

private void DoSomething()
{
System.Threading.Monitor.Enter(this.lockTarget);
try
{
// ...
}
finally
{
System.Threading.Monitor.Exit(this.lockTarget);
}
}

Et là on s'aperçoit tout de suite mieux des conséquences d'un repositionnement du point d'exécution à hauteur du commentaire, donc dans le bloc try.
Avant de se risquer à utiliser "Set Next Statement" dans un code de ce genre, l'utilisateur du debugger devra aussi être capabable, le cas échéant, de se rendre compte des conséquences de l'exécution de code hors verrou qu'il a provoquée alors qu'elle n'était pas censée arriver afin de prendre la décision d'arrêter la session de debug à temps.

Le cas du bloc using est sensiblement identique à celui du bloc lock hormis le fait que la levée, sur les appels de méthodes de la classe, d'une exception ObjectDisposedException à même d'informer l'utilisateur du debugger de son erreur avant qu'il n'aille trop loin dépendra de l'implémentation du pattern Dispose et du design général de la classe.

Décidément, je n'aime vraiment pas ce "Set Next Statement"...

Quelques trucs intéressants (02/10/2011)

 

Debug

 

Debug / outils

  • CLR Stack Explorer – Preview (Sasha Goldshtein)
    Une preview d'un outil permettant notamment d'afficher les callstacks des threads managés sans sortir un debugger, mais aussi des informations sur les verrous. Outil à suivre.
    Pour rappel, concernant les callstacks, Process Hacker commence aussi à fournir ce genre de visu.

 

Sécurité

  • How to Preview Shortened URLs (TinyURL, bit.ly, is.gd, and more) (Joshua Long, via Kate Gregory)
    Pour ceux qui détestent faire confiance aux services d'obfuscation de raccourcissement d'url dans certains contextes : ce post, faisant encore l'objet de mises à jour, donne des informations sur la manière d'obtenir l'URL réelle qui se cache sous la version raccourcie.
    L'existence d'extensions de navigateurs offrant cette fonctionnalité rend la vie plus facile mais ça peut être utile de connaitre la version manuelle.

 

Internet Explorer

  • PrivBar Update (Aaron Margosis)
    Pour ceux qui ont besoin d'obtenir rapidement les informations concernant les privilèges dont dispose une instance d'Internet Explorer, Aaron Margosis a livré une mise à jour de son extension PrivBar.

 

Divers

  • Greg's Cable Map - Cablemap.info (Greg Mahlknecht)
    Une carte des câbles de télécommunications sous-marins montrant les points d'atterrissement et donnant accès à quelques détails sur les différents câbles.
    Cette carte est le résultat d'un travail de recoupement de données par Greg Mahlknecht (avec visiblement des contributions d'autres personnes) et pas forcément une carte précise, mais c'est déjà très bien ! La précision du positionnement est donnée dans les détails de chaque câble.
    On ne sait jamais quand ça peut servir ce genre de chose (oui, les probabilités que ce soit justement "jamais" sont plutôt fortes, je sais !).
Mise à jour de l'extension de debugger SOSEX

Steve Johnson a livré une mise à jour de son extension SOSEX, en y ajoutant notamment quelques commandes.

Rien que mdso, remplissant le même rôle que la commande dso (DumpStackObjects) de SOS/PSSCOR, vaut le détour : elle sera sans doute appréciée par ceux qui ont un jour maudit la commande dso pour son affichage en "double" des objets présents plusieurs fois sur la stack ou pour l'impossibilité de filtrer la sortie sur certains types.

La commande msdo résoud ce problème :

  • par défaut la commande élimine les doublons (le switch /a permet d'afficher la liste complète au cas où la localisation exacte de chaque entrée serait nécessaire)
  • les switches /mt et /t permettent de ne lister que les objets des types spécifiés, soit par MethodTable, soit par un filtre sur le nom du/des type(s)
    ex :
    !mdso /mt:045096ac
    !mdso /t:*Button

Je vois laisse découvrir la liste des nouvelles commandes et les améliorations apportées aux commandes existantes sur le post de Steve : Major SOSEX Update

Outils : amélioration de l'affichage des call stacks des threads managés dans Process Hacker

La version 2.20 de Process Hacker, un outil similaire à Process Explorer dont je vous avais parlé il y a peu, intègre entres autres évolutions l'affichage amélioré des call stacks des threads managés.
Ca n'atteint bien sûr pas (encore?) le niveau de détails qu'on obtient sous Visual Studio ou avec SOS/PSSCOR sous WinDbg (!CLRStack / !DumpStack) mais ça permet déjà d'avoir une meilleure vision de ce qu'il se passe sur le thread.

Pour un thread exécutant du code .NET, les versions précédentes donnaient ce type d'affichage de call stack :

Capture d'écran de la boite de dialogue d'affichage de la call stack d'un thread managé dans Process Hacker 2.19

 

Alors qu'à partir de la version 2.20 nous disposons d'un affichage plus pratique, du point de vue du développeur .NET :

Capture d'écran de la boite de dialogue d'affichage de la call stack d'un thread managé dans Process Hacker 2.20

 

Voici donc un point sur lequel Process Hacker présente un avantage sur Process Explorer qui n'offre pas encore cette fonctionnalité (au moins jusqu'à la version 15.03) :

Capture d'écran de la boite de dialogue d'affichage de la call stack d'un thread managé dans Process Explorer 15.03

Pratique à éviter : utiliser des techniques "à la phishing" dans des communications normales

Ca fait plusieurs fois que je vois des choses de ce genre et ça à tendance à m'énerver.

On reçoit un mail (HTML, pas texte) contenant un lien qui semble brut, par exemple vers un profil sur un réseau social quelconque.
Prenons comme exemple quelque chose de ce genre (complètement fabriqué pour l'occasion, toute coïncidence avec un système existant serait involontaire) :

Capture d'écran d'un exemple de contenu html contenant un faux lien brut, sans affichage de l'URL réelle

En passant le curseur au dessus du lien on s'aperçoit que sa cible n'est pas du tout celle affichée par le texte mais qu'il pointe vers autre chose, probablement un système de tracking qui fera ensuite la redirection vers la page réelle (parce que des fois ce n'est même pas clair) :

Capture d'écran d'un exemple de contenu html contenant un faux lien brut, avec affichage de l'URL réelle

C'est désagréable car en fin de compte on à l'impression que l'expéditeur à utilisé une technique similaire à celles qu'on observe dans les tentatives de phishing (hameçonnage).
Autant dire que ça ne joue pas en faveur de l'émetteur qui, pour ma part, perd une partie de sa crédibilité, voire plus si je suis pris dans un moment de fatigue et que je me fais "avoir" en cliquant sur le lien trop vite.

 

La différence de perception entre cette pratique et l'exemple ci-dessous est importante :

Capture d'écran d'un exemple de contenu html présentant un peu mieux le lien de tracking, c'est-à-dire sans faux lien brut

Dans ce second cas j'ai plus de chances de penser, par réflexe, à vérifier le lien avant de cliquer : c'est justement pour celà que les liens faisant semblant d'être brut sont plutôt utilisés dans les emails malveillants.

 

Si vous êtes un expéditeur légitime vous n'avez rien à gagner à utiliser ce genre de technique douteuse, que ce soit volontairement ou non.

Si vous êtes utilisateur d'un système de mailing qui mettrait automatiquement à votre disposition un système de suivi en remplaçant vos liens par des liens de tracking, vérifiez ce qu'il se passe en vous envoyant une de vos communications au préalable.
Et dans le doute bâtissez vos communications HTML (pour le mode texte forcément ça ne s'applique pas) en évitant d'y placer des URLs brutes qui ne soient pas celles du système de tracking : ça vous évitera de vous retrouver involontairement dans ce cas.

Quelques trucs intéressants (11/07/2011)

 

Debug / Performance

 

Outils

 

Sécurité

  • De l’usage « non sécurisé » des flashcodes (blog Conix Security)
    Un exemple concret d'une des raisons pour lesquelles il faut, tout autant que pour n'importe quel lien, réfléchir avant de scanner un des codes-barres qu'on trouve maintenant un peu partout, notamment s'il est affiché dans un lieu public et si son utilisation amène à divulguer des informations bancaires ou personnelles.
  • “AlwaysInstallElevated” is Equivalent to Granting Administrative Rights (post d'Aaron Margosis)
    Un paramètre de configuration permettant de mettre à disposition des utilisateurs un moyen d'élever leurs privilèges, super... A garder désactivé donc.

 

SQL Server

  • SQL Server Version Database (SQLSecurity.com, via Greg Duncan)
    Une liste des différents builds de SQL Server existant au fil des livraisons de Service Packs et de Cumulative Updates, avec les liens vers les articles de KB ou les pages du Download Center.
    Utile pour déterminer ce qui peut manquer sur une installation existante afin d'évaluer la nécessité de passage d'un Cumulative Update, et le cas échéant son niveau d'urgence.
    Cette page liste actuellement les builds des versions 6.5 à 2008 R2.
    Une liste du même genre est disponible sur le blog de Pankaj Mittal : SQL Server Version History

 

Windows

 

Divers

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)

Quelques trucs intéressants (21/05/2011)

Développement

 

Debug / Performance

  • Psscor4 Managed-Code Debugging Extension for WinDbg (centre de téléchargement MS)
    Même si la version de SOS pour CLR 4.0 a apporté quelques commandes supplémentaires par rapport à la version CLR 2.0, on n'y retrouvait pas encore l'intégralité des ajouts de PSSCOR2. Ce problème est maintenant résolu grace à la mise à disposition de PSSCOR4.
    Un bémol de mon côté : sur la totalité de mes configurations je suis confronté à un problème avec cette extension dans sa version actuelle (26/04/2011). En effet, la sortie de la commande DumpHeap (exemple : !DumpHeap -type Exception) m'affiche deux lignes par objet :
        0:004> !dumpheap -type Exception
        Loading the heap objects into our cache.
         Address       MT     Size
        01a01024 5049fdc4       84    0 System.Exception
        01a01024 5049fdc4       84
        
    Comme ça se produit aussi quand la commande spécifie le paramètre -short, ça peut s'avérer problématique au delà de la gêne visuelle.
    Le problème a été remonté et j'attend des nouvelles, mais si vous n'y êtes pas confronté vous pouvez profiter pleinement de l'extension dès maintenant.
  • ProcExp and XPerf tracing (blog de Maarten Van De Bospoort)
    Il me semble avoir déjà eu un problème similaire en jouant avec ETW et il ne m'avait pas traversé l'esprit de vérifier si un outil d'analyse/monitoring du type Process Explorer/Process Monitor/... était en cours d'exécution.
  • Shared Source CLI 2.0 Internals (ebook gratuit, blog de Joel Pobar)
    Ce n'est plus tout jeune et ce n'est pas le genre de livre qu'on ouvre tous les jours, mais autant l'avoir sous la main quand on se retrouve dans les sources de Rotor au fil de ses recherches.
    Visiblement j'avais zappé l'annonce de la sortie du livre car j'étais encore sur le draft jusqu'à ce que je retombe sur le blog de Joel Pobar et un post sur Rotor à la fin duquel il est lié.
    Le lien est aussi disponible sur cette page, section "Book Contributions" > "Shared Source CLI 2.0 Internals (co-author)"

 

Divers

  • Unicode 6.0 — One character at a time (blog BabelStone)
    Un slideshow Javascript qui tente (suivant les polices disponibles sur la machine) d'afficher les 109384 caractères d'Unicode 6.0, en réponse (en quelque sorte) à une vidéo de 33 minutes en affichant 49571.
    De quoi martyriser un peu son navigateur avec du simple texte.
    La slideshow est paramétrable tant sur les plages de caractères à afficher que sur le tempo d'affichage et la taille de caractère : vous n'êtes pas obligé d'utiliser le mode violent.
    Au passage, l'auteur est aussi celui de 2 outils dont je vous avais parler il y a quelques temps : BabelPad et BabelMap.
IE9 sur intranet : désactiver la bascule systématique en mode de compatibilité

Internet Explorer 9 bascule par défaut sur le rendu en mode de compatibilité pour les sites situés dans la zone Intranet.
Ca peut s'avérer légèrement agaçant quand on possède une majorité de sites qui n'en ont pas besoin et qui dans certains cas ont un rendu assez désagréable dans ce mode.
Ce comportement existait aussi sous IE8 mais ne m'avait jamais vraiment posé problème, alors que sous IE9 si.

Pour changer cela il faut aller dans les paramètres du mode de compatibilité accessibles via le menu principal (Alt ou F10) :

Capture d'écran du menu Tools d'Internet Explorer 9

Et décocher l'option "Afficher les sites intranet dans Affichage de compatibilité" ("Display intranet sites in Compatibility View") :

Capture d'écran de la boite de dialogue "Compatibility View Settings" d'Internet Explorer 9

Les sites posant problème pourront toujours être basculés en mode de compatibilité au cas par cas.

Côté GPO, je n'ai pas vraiment trouvé l'équivalent dans la liste des options disponibles pour Internet Explorer.

Où voir la zone de sécurité de la page courante dans IE9 ?

Avant Internet Explorer 9, cette information était visible simplement dans la barre d'état.
Maintenant même les personnes qui voudraient sacrifier quelques pixels d'espace d'affichage pour obtenir quelques informations contextuelles ne "peuvent" plus le faire vu que la barre d'état ne contient plus vraiment d'information utile.

La dernière fois je m'étais demandé où cette information pouvait maintenant se trouver, sans prendre le temps de réellement chercher sur le moment, et je viens de le découvrir en lisant un post d'Eric Lawrence : la zone se voit tout simplement dans la boite de dialogue de propriétés de la page (je me demande si ce c'était pas déjà le cas sur les versions antérieures) :

Capture d'écran de la boite de dialogue de propriétés de la page courante dans Internet Explorer 9

Bon c'est vrai que je suis un peu idiot de ne pas avoir pensé à vérifier que cette information ne se trouvait pas à cet endroit, mais en même temps jusqu'à maintenant je l'avais toujours eue sous les yeux...

Je me demande si cette décision de mise à mort de la barre d'état a été prise pour qu'un maximum de personnes laisse cette barre d'état désactivée : pour moi la raison "affichage retiré pour ne pas déstabiliser l'utilisateur lambda avec des informations techniques" ne tient pas vraiment vu que la barre d'état est masquée par défaut.
Et je n'ai pas trouvé d'option qui permette de redonner une utilité à la barre d'état, mais je l'ai peut-être tout simplement ratée.

Quelques trucs intéressants (23/04/2011)

 

Debug / Performance

  • CLR 4 Does Not Use LoadLibrary to Load Assemblies (blog de Sasha Goldshtein)
    Il s'agit d'un changement apporté par .NET 4 qui n'est pas forcément celui qu'on remarque le plus dans la liste ".NET Framework 4 Migration Issues" (section Core > Reflection) : "To prevent redundant loading of assemblies and to save virtual address space, the CLR now loads assemblies by using only the Win32 MapViewOfFile function. It no longer also calls the LoadLibrary function."
    L'auteur met en avant une conséquence de ce changement : l'absence de module entraine l'absence de chargement des symboles et par conséquent l'absence des numéros de ligne dans les stacktraces quand on utilise des outils WinDbg et les extensions SOS/SOSEX.
    A noter que Visual Studio 2010 et MDbg sont capables d'utiliser les symboles quand un dump de processus .NET 4 leur est soumis, donc au pire des cas on peut utiliser un de ces outils pour récupérer le numéro de ligne et conserver les extensions habituelles pour le debug avancé.
  • Silverlight Memory Leaks and AutomationPeers (blog de Sergio Loscialo)
    Un cas intéressant qui montre notamment que si l'environnement cible d'une application risque d'inclure de l'UI Automation, il vaut mieux en tenir compte durant le développement et les tests.
    Dans le cas d'applications Silverlight grand public autant dire qu'il vaut mieux en tenir compte systématiquement, vu l'hétérogénéité de configurations à laquelle il faut s'attendre.
  • Top Customer Misconceptions about Software Problems (blog de Roberto Alexis Farah)
    Une exposition de 13 idées reçues sur l'analyse d'incidents et le debug.
    Je suis presque certain que chacun d'entre nous peut avoir une impression de déjà-vu dans au moins un des cas :-)

 

Outils / SQL Server

  • SQL Sentry Plan Explorer (outil gratuit, SQL Sentry)
    Un outil permettant de faciliter l'analyse de plan d'exécution SQL Server, comparé à ce qui est disponible dans SSMS.
    A priori il était disponible en version beta depuis octobre 2010 mais je le découvre seulement maintenant via un post de Jonathan Kehayias, qui propose un script PowerShell permettant de nettoyer un plan d'exécution des StatementText afin de ne pas les divulguer à l'extérieur.

 

Divers

  • Mapping Sockets to a Process In .NET Code (blog Fiddler)
    Je m'étais déjà demandé comment Fiddler et d'autres outils comme Network Monitor faisaient le lien entre une connexion et le processus lié, mais je n'avais jamais pris le temps de réellement creuser le sujet. En fin de compte ils utilisent une fonction, de l'API IP Helper, que j'ai déjà utilisée pour autre chose.
    Le post donne un exemple d'utilisation de GetExtendedTcpTable depuis du code .NET.
  • The double-Ctrl+Alt+Del feature is really a kludge (blog The Old New Thing)
    Si vous vous demandiez pourquoi la technique du double "Ctrl+Alt+Suppr" qui était disponible sur l'écran de bienvenue de Windows XP ne l'est plus dans les versions ultérieures, ce post contient la réponse.
    Ce post n'est plus tout jeune mais je l'avais raté à l'époque (ou ne m'en souvenais plus) et je viens de (re)tomber dessus.

 

PowerShell

  • PowerShell Language now licensed under the Community Promise (blog Windows PowerShell)
    Ou la possibilité d'implémenter librement PowerShell sur d'autres plateformes que Windows.
    Intéressant. Je ne travaille jamais moi-même sur d'autres plateformes, mais j'avoue que l'idée de pouvoir échanger en script PowerShell avec ceux qui le font est séduisante. On verra bien comment ça va évoluer.
    En dehors de l'aspect ouverture aux autres plateformes, ça fait aussi un peu de lecture pour les curieux !
Freeze de SSMS (SQL Server Management Studio) lors du clic droit sur le noeud du serveur dans l'Object Explorer

Je me suis dernièrement retrouvé devant un cas de blocage de SQL Server Management Studio 2008 R2 assez "intéressant" : un clic droit sur le noeud du serveur local dans l'explorateur d'objets entrainait un blocage de SSMS pendant un laps de temps assez long (que je n'ai pas réellement mesuré).

Capture d'écran de l'Object Explorer de SQL Server Management Studio

Une fois le blocage terminé les tentatives suivantes réussissaient sans problème particulier, jusqu'à l'ouverture de session suivante.

MAJ 28/03/2011 : j'ai un peu étoffée la partie expliquant le blocage du thread d'interface graphique.

 

Une petite analyse rapide avec WinDbg/PSSCOR2 a montré une stack intéressante :

La clé du problème

OS Thread Id: 0x1da4 (5)
ESP EIP
0766efc0 77d104f2 [ComPlusMethodFrameGeneric: 0766efc0] System.Management.IWbemServices.GetObject_(System.String, Int32, System.Management.IWbemContext, System.Management.IWbemClassObjectFreeThreaded ByRef, IntPtr)
0766efe0 6751983a System.Management.ManagementObject.Initialize(Boolean)
0766f048 67515b32 System.Management.ManagementBaseObject.get_Properties()
0766f054 67515f06 System.Management.ManagementBaseObject.GetPropertyValue(System.String)
0766f064 67515e55 System.Management.ManagementBaseObject.get_Item(System.String)
0766f068 6acff89e Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.ROOT.CIMV2.Win32.Service.get_State()
0766f070 6acfc5dc Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.Service.UpdateState(Boolean)
0766f0b0 6acfcb7c Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.Service.BeginServiceLookupInt()
0766f0e4 6acfb97d Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.Service+AsyncWmiBinding.RequestHandler(System.Object)
0766f0f0 6f769f4f System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(System.Object)
0766f0f8 6f795731 System.Threading.ExecutionContext.runTryCode(System.Object)
0766f528 70061b5c [HelperMethodFrame_PROTECTOBJ: 0766f528] System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode, CleanupCode, System.Object)
0766f590 6f795627 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0766f5ac 6f780255 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0766f5c4 6f76a4b3 System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(System.Threading._ThreadPoolWaitCallback)
0766f5d8 6f76a349 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(System.Object)
0766f768 70061b5c [GCFrame: 0766f768]

est ici l'appel "Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.ROOT.CIMV2.Win32.Service.get_State()" : SSMS semble être bloqué pendant la tentative de récupération du statut du service Windows de SQL Server.

Il s'agit d'un thread effectuant un traitement probablement lancé en asynchrone lors de la connexion au serveur, et qui possède peut-être un verrou intéressant :

0:000> !threads
[...]
PreEmptive GC Alloc Lock
ID OSID ThreadOBJ State GC Context Domain Count APT Exception
0 1 cc8 0093e220 2004220 Enabled 03a4731c:03a47f70 00936680 0 STA
4 2 16b4 033f8918 b220 Enabled 00000000:00000000 00936680 0 MTA (Finalizer)
5 3 1da4 034afa70 180b220 Enabled 00000000:00000000 00936680 3 MTA (Threadpool Worker)
7 4 10e0 07a9d370 80a220 Enabled 00000000:00000000 00936680 0 MTA (Threadpool Completion Port)
8 5 804 07ab3ff0 1220 Enabled 00000000:00000000 00936680 0 Ukn
[...]

0:000> !SyncBlk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
149 033ff1ac 3 1 034afa70 1da4 5 0387db14 System.Object
Waiting threads: 0
-----------------------------
Total 152
CCW 39
RCW 36
ComClassFactory 0
Free 9

De son côté le thread de l'interface graphique est en attente de l'obtention d'un verrou, lors d'un appel à une autre méthode WMI :

0:000> !CLRStack
OS Thread Id: 0xcc8 (0)
ESP EIP
003de2ac 77d100ed [GCFrame: 003de2ac]
003de37c 77d100ed [HelperMethodFrame_1OBJ: 003de37c] System.Threading.Monitor.Enter(System.Object)
003de3d4 6acfd475 Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.Service.get_CanStart()
003de404 6ad10c0c Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.ConfigureServicesHandler.UpdateMenuCommandStatus(System.ComponentModel.Design.MenuCommand)
003de418 6ace01c9 Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.DefaultMenuHandler.UpdateMenuCommandsStatus(System.ComponentModel.Design.MenuCommand)
003de444 6acdf3b1 Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.DefaultMenuHandler.GetMenuItems(System.Collections.ArrayList)
003de500 6acdffcc Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.DefaultMenuHandler.GetMenuItems()
003de504 6acdc40b Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.ExplorerHierarchyNode.ShowContextMenu(System.Drawing.Point)
003de528 6acdd77b Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.LazyTreeView.WmContextMenu(System.Windows.Forms.Message ByRef)
003de554 6acdd847 Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.LazyTreeView.WndProc(System.Windows.Forms.Message ByRef)
003de568 6d0b84a0 System.Windows.Forms.Control+ControlNativeWindow.OnMessage(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:14059
003de570 6d0b8421 System.Windows.Forms.Control+ControlNativeWindow.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:14114
003de584 6d0b82fa System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\NativeWindow.cs:647
003de72c 02daa074 [NDirectMethodFrameStandalone: 003de72c] System.Windows.Forms.UnsafeNativeMethods.SendMessage(System.Runtime.InteropServices.HandleRef, Int32, IntPtr, IntPtr)
003de748 6d0c3654 System.Windows.Forms.Control.SendMessage(Int32, IntPtr, Int32) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:11345
003de764 6d929d1e System.Windows.Forms.TreeView.WmNotify(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\TreeView.cs:2891
003de7e4 6d0ebdb7 System.Windows.Forms.TreeView.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\TreeView.cs:3339
003deaf4 6acdd89a Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.LazyTreeView.WndProc(System.Windows.Forms.Message ByRef)
003deb08 6d0b84a0 System.Windows.Forms.Control+ControlNativeWindow.OnMessage(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:14059
003deb10 6d0b8421 System.Windows.Forms.Control+ControlNativeWindow.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:14114
003deb24 6d0b82fa System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\NativeWindow.cs:647
003deccc 02daa074 [NDirectMethodFrameStandalone: 003deccc] System.Windows.Forms.UnsafeNativeMethods.SendMessage(System.Runtime.InteropServices.HandleRef, Int32, IntPtr, IntPtr)
003dece8 6d0c36d4 System.Windows.Forms.Control.SendMessage(Int32, IntPtr, IntPtr) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:11340
003ded04 6d0c2fdb System.Windows.Forms.Control.ReflectMessageInternal(IntPtr, System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:10742
003ded14 6d0cd417 System.Windows.Forms.Control.WmNotify(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:13094
003ded2c 6d0b8962 System.Windows.Forms.Control.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:13811
003ded30 6d0c1baa [InlinedCallFrame: 003ded30] f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\ScrollableControl.cs:1491
003ded90 6d0c1b60 System.Windows.Forms.ContainerControl.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\ContainerControl.cs:1898
003ded98 6d0a02d0 System.Windows.Forms.UserControl.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\UserControl.cs:378
003deda0 6d0b84a0 System.Windows.Forms.Control+ControlNativeWindow.OnMessage(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:14059
003deda8 6d0b8421 System.Windows.Forms.Control+ControlNativeWindow.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:14114
003dedbc 6d0b82fa System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\NativeWindow.cs:647
003df180 02daa074 [InlinedCallFrame: 003df180] System.Windows.Forms.UnsafeNativeMethods.CallWindowProc(IntPtr, IntPtr, Int32, IntPtr, IntPtr)
003df17c 6d0b8ad3 System.Windows.Forms.NativeWindow.DefWndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\NativeWindow.cs:810
003df1c0 6d0b8a2c System.Windows.Forms.Control.DefWndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:5737
003df1c4 6d618ac3 System.Windows.Forms.TreeView.WmMouseDown(System.Windows.Forms.Message ByRef, System.Windows.Forms.MouseButtons, Int32) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\TreeView.cs:2601
003df1e4 6d929610 System.Windows.Forms.TreeView.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\TreeView.cs:3077
003df4f4 6acdd8a6 Microsoft.SqlServer.Management.UI.VSIntegration.ObjectExplorer.LazyTreeView.WndProc(System.Windows.Forms.Message ByRef)
003df508 6d0b84a0 System.Windows.Forms.Control+ControlNativeWindow.OnMessage(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:14059
003df510 6d0b8421 System.Windows.Forms.Control+ControlNativeWindow.WndProc(System.Windows.Forms.Message ByRef) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs:14114
003df524 6d0b82fa System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr) f:\dd\ndp\fx\src\WinForms\Managed\System\WinForms\NativeWindow.cs:647

Et c'est justement celui tenu par le thread 5 :

0:000> kv
ChildEBP RetAddr Args to Child
[...]
003de270 7007c5b6 ffffffff 00000001 00000000 mscorwks!CLREvent::WaitEx+0xf7 (FPO: [Non-Fpo])
003de284 7017c002 ffffffff 00000001 00000000 mscorwks!CLREvent::Wait+0x17 (FPO: [Non-Fpo])
003de310 701c146c 0093e220 ffffffff 0093e220 mscorwks!AwareLock::EnterEpilog+0x8c (FPO: [Non-Fpo])
003de32c 701c13f0 a1dfa9ce 003de3ec 0387dae4 mscorwks!AwareLock::Enter+0x61 (FPO: [Non-Fpo])
003de3cc 6acfd475 0387db14 00000000 00000000 mscorwks!JIT_MonEnterWorker_Portable+0xb3 (FPO: [Non-Fpo])
003de3fc 6ad10c0c 0351d148 03898ce0 038980d8 ObjectExplorer_ni+0x37d475
003de410 6ace01c9 00000000 00000000 00000000 ObjectExplorer_ni+0x390c0c
003de43c 6acdf3b1 00000000 00000000 00000000 ObjectExplorer_ni+0x3601c9
003de4f8 6acdffcc 6acdc40b 0391179c 038980d8 ObjectExplorer_ni+0x35f3b1
003de518 6acdd77b 000000bf 0000000a 00000000 ObjectExplorer_ni+0x35ffcc
003de54c 6acdd847 00000000 003de588 0351d4e8 ObjectExplorer_ni+0x35d77b
003de560 6d0b84a0 003de57c 6d0b8421 00000000 ObjectExplorer_ni+0x35d847
[...]

La présence du thread d'arrière plan ne fait qu'allonger le temps d'attente, le thread de l'interface graphique aurait probablement connu le même blocage s'il n'avait pas été là.

 

Un rapide coup d'oeil aux processus WmiPrvSE montre que l'un d'entres eux est (une fois de plus) dans un sale état à cause de quelques problèmes comme celui-ci : The "Win32_Service" WMI class leaks memory in Windows Server 2008 R2 and in Windows 7

Capture d'écran de l'état du processus WmiPrvSE dans le Task Manager de Windows

Les connaisseurs auront remarqué que le commit size du processus sélectionné a atteint le plafond autorisé par le job, dont on peut voir les paramètres via la boite de dialogue de propriétés de processus fournie par Process Explorer :

Capture d'écran de la boite de dialogue de propriétés du processus WmiPrvSE dans Process Explorer

Ceux qui utilisent System Center Operations Manager (SCOM) pour surveiller leurs serveurs ont sans doute déjà reçu quelques avertissements et constaté quelques crashes de ces processus WmiPrvSE, vu que l'agent de SCOM fait un usage assez intensif de WMI.
Pour ma part je n'avais encore jamais été confronté à ce type de conséquence avec le blocage de l'application cliente.

Un arrêt (brutal, pas le choix dans mon cas) du processus fait disparaitre le problème constaté sur SSMS.

Informer de ce problème les DBAs, développeurs et équipes techniques en général pour qu'ils ne soient pas trop étonnés me semble une bonne idée.
Notamment pour qu'ils attendent un peu avant de prendre la décision de tuer leur processus SSMS, afin de ne pas perdre quelques scripts non sauvegardés ou quelques requêtes en cours d'exécution. Ca a tendance à les rendre un poil grincheux ;-).

Quelques trucs intéressants (13/03/2011)

 

Debug

  • Debug Analyzer.NET (outil créé par Sukesh Ashok Kumar, Support Escalation Engineer Microsoft)
    Un outil qui mérite sans doute qu'on s'intéresse à son évolution : il se propose tout simplement de permettre l'analyse de dumps mémoire d'applications .NET en utilisant des plugins écris en .NET.
    Je n'ai pas encore testé l'outil en profondeur mais en tout cas il me parait au minimum être une bonne chose pour permettre aux personnes qui sont rebutées par WinDbg+SOS/PSSCOR d'effectuer une analyse en amont avant de faire intervenir une personne ayant acquise l'expérience permettant de creuser plus en profondeur. Ca permettra peut-être de réduire la charge de travail de cette personne sur les incidents ne nécessitant pas réellement une analyse en profondeur.
  • Fiddler and the IE9 Release Candidate (blog MSDN Fiddler)
    Entres autres, une nouvelle du côté de la gestion du proxy sous Internet Explorer 9 : examiner les requêtes sur localhost/127.0.0.1/... devrait maintenant être un peu moins "compliqué".
    Ce changement n'affecte que IE9, pas forcément les autres applications qui ignoraient aussi les proxys pour les requêtes sur les interfaces de loopback.
  • Prise de dumps avec WER (blog de Sébastien Bovo)
    Une description des clés de registre disponibles (depuis Windows Server 2008 et Windows Vista SP1) pour paramétrer la génération d'un dump local par Windows Error Reporting. Ca peut toujours servir.

 

Divers

Plus de Messages Page suivante »


Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01