Décompiler les filtres d'exception : le catch (e) when (...) qui devient une région filter et le endfilter qui l'exécute
Un bloc catch avec un type est l'une des choses les plus simples que lit un décompilateur : une clause de gestionnaire avec un token de type, terminé. Un catch avec une clause when est l'une des plus épineuses, car c'est un type de région d'exception entièrement différent. Le filtre n'a pas de token de type ; le test de type se déplace dans un bloc d'IL séparé qui s'exécute pendant que la pile est encore en cours de déroulement, évalue la condition when, et se termine par un endfilter qui renvoie 1 pour exécuter le gestionnaire ou 0 pour continuer la recherche. Reconstruire catch (IOException e) when (e.HResult == 5) signifie reconnaître cette structure à deux blocs, remonter l'objet d'exception que le runtime a poussé sur la pile dans la variable e, extraire le test de type du filtre comme type du catch, et lire le reste comme l'expression when — puis savoir que les exceptions levées à l'intérieur du filtre sont avalées, ce qui est une garantie réelle du langage visible dans l'IL. Cet article lit l'IL exact qu'émet un catch filtré, parcourt le protocole d'endfilter et montre comment Glass.NET reconstruit la clause.
L”essentiel de ce qu”un décompilateur fait avec la gestion des exceptions est d”apparier des motifs évidents. Un try/finally, ce sont deux régions et un leave ; un catch (IOException e) est une clause de gestionnaire étiquetée avec un token de type, et le lecteur ne fait qu”imprimer le type. La seule construction de gestion d”exceptions qui mérite vraiment un second regard est le catch filtré — catch (E e) when (condition) — car ce n”est pas du tout le même type de clause. Il n”a pas de token de type, il exécute du code avant même que le gestionnaire ne soit choisi, et il se termine par une instruction dont la plupart des développeurs C# n”ont jamais entendu parler : endfilter.
Cet article lit l”IL qu”un catch filtré émet réellement, parcourt le protocole d”endfilter étape par étape, et montre comment Glass.NET reconstruit la clause — y compris la seule garantie réelle du langage que le filtre cache : les exceptions levées à l”intérieur de lui sont avalées.
Un catch filtré est une clause à deux régions sans type
Voici la source que nous allons décompiler :
try
{
DoIo();
}
catch (IOException e) when (e.HResult == 5)
{
Log("access denied");
}
Un catch (IOException e) ordinaire compilerait vers une région de gestionnaire avec IOException comme type de clause. Le when change le type de la clause. Les métadonnées enregistrent maintenant une clause filter, dont le champ de type est vide, et la méthode porte deux régions au lieu d”une : une région filter et une région de gestionnaire. Décompilé vers le rendu d”ILDasm, la structure est :
.try IL_0000 to IL_0007 filter IL_0007 handler IL_0021 to IL_002e
IL_0000: call void DoIo()
IL_0005: leave.s IL_002e
// ---- filter region: the runtime pushes the exception object here ----
IL_0007: isinst [System.Runtime]System.IO.IOException
IL_000c: dup
IL_000d: brtrue.s IL_0013
IL_000f: pop
IL_0010: ldc.i4.0
IL_0011: br.s IL_001f
IL_0013: callvirt instance int32 [System.Runtime]System.Exception::get_HResult()
IL_0018: ldc.i4.5
IL_0019: ceq
IL_001b: br.s IL_001f
IL_001f: endfilter
// ---- handler region: exception object on the stack again ----
IL_0021: pop
IL_0022: ldstr "access denied"
IL_0027: call void Log(string)
IL_002c: leave.s IL_002e
IL_002e: ret
Lisez la région filter comme un petit programme que le runtime exécute avant de décider d”entrer dans le gestionnaire. L”objet d”exception est déjà sur la pile quand la région démarre — le runtime l”y a mis. isinst IOException fait le test de type qu”a demandé la partie catch (IOException ...) ; dup/brtrue conserve le résultat du cast s”il a correspondu ; s”il n”a pas correspondu, pop et ldc.i4.0 laissent 0 et sautent à endfilter, abandonnant la clause. S”il a correspondu, le code évalue e.HResult == 5, laissant le résultat de ceq — 0 ou 1 — sur la pile. L”un ou l”autre chemin atteint endfilter avec exactement un entier derrière lui.
endfilter est l”instruction qui choisit le gestionnaire
endfilter est tout le fond de l”affaire, et il a un protocole qui vaut la peine d”être énoncé avec précision. La région filter doit laisser exactement un int32 sur la pile ; endfilter le dépile et le remet au runtime. 1 (EXCEPTION_EXECUTE_HANDLER) signifie exécuter le gestionnaire ; 0 (EXCEPTION_CONTINUE_SEARCH) signifie cette clause ne s”applique pas, continue à dérouler pour en trouver une autre. C”est pourquoi chaque région filter au monde se termine de la même façon — une comparaison qui produit 0 ou 1, puis endfilter. Pour le décompilateur, endfilter est le marqueur de frontière : tout, du début de la région filter jusqu”à lui, est la condition when (plus le test de type implicite), et tout après l”étiquette du gestionnaire est le corps du catch.
Puis la région du gestionnaire commence — et l”objet d”exception est sur la pile de nouveau, poussé par le runtime une seconde fois à l”entrée du gestionnaire. Dans notre IL, IL_0021: pop le dépile, parce que le corps n”utilise que la chaîne littérale. Si le corps avait utilisé e, le compilateur aurait émis stloc vers l”emplacement de e au lieu de pop. Reconnaître ce pop-ou-stloc de tête est ce qui permet à Glass.NET de savoir où la condition se termine et où commence le corps du catch écrit par un humain, et si la variable d”exception est réellement lue.
Ce que Glass.NET reconstruit, et d”où
Mettez les pièces ensemble et la reconstruction est mécanique. Glass.NET voit une clause filter sans token de type et sait immédiatement que c”est un catch when. Il lit le prologue de la région filter — l”isinst contre IOException, la danse dup/brtrue/pop/ldc.i4.0 de vérification de null — et en remonte deux choses : la cible de l”isinst devient le type du catch, et l”objet d”exception que le runtime a poussé devient la variable e. Tout entre ce prologue et endfilter est l”expression when. Le stloc/pop de tête de la région du gestionnaire confirme si e est utilisée. Le résultat :
catch (IOException e) when (e.HResult == 5)
{
Log("access denied");
}
— récupéré fidèlement, sans PDB, parce que chaque partie est encodée dans l”IL et dans les métadonnées de région d”exception dont le runtime lui-même dépend. Un décompilateur qui ne modélisait pas les filtres vous montrerait un catch sans type avec un bloc d”IL de comparaison qu”il ne savait pas où placer, ce qui est exactement le genre de lacune « le code tourne mais la décompilation est fausse » qu”un lecteur naviguant dans un binaire inconnu ne peut pas se permettre.
La garantie cachée dans le filtre : les exceptions levées sont avalées
Une dernière chose que l”IL rend lisible, et c”est une vraie règle du langage, pas une bizarrerie. Si la région filter elle-même lève — disons que e.HResult était remplacé par une propriété pouvant lever — le CLR capture cette exception et traite le filtre comme ayant renvoyé 0, continuer la recherche. La condition when qui lève ne fait pas planter votre programme et ne s”échappe pas ; elle fait seulement que ce catch est ignoré. C”est un comportement spécifié, et c”est pourquoi une clause when devrait être peu coûteuse et sans effet de bord : une condition qui peut lever est un bug subtil, car elle désactive en silence le catch que vous pensiez avoir écrit.
Glass.NET rend la clause comme le catch ... when (...) qu”elle était et laisse le comportement d”avalement pour ce qu”il est — une propriété des filtres que le lecteur doit comprendre, pas quelque chose qu”un décompilateur devrait réécrire. Rendre cette structure visible est la valeur : le code décompilé vous dit que c”est un filtre, et un lecteur qui connaît les filtres sait qu”une condition qui lève ici échoue la correspondance en silence. C”est la différence entre une décompilation qui reproduit les octets et une qui reproduit le sens.
À retenir
Le catch filtré est la construction d”exception où IL et source cessent d”être un pour un, et le lire est un bon test pour un décompilateur. Il n”y a pas de token de type — le type s”est déplacé dans un isinst à l”intérieur d”une région filter séparée. La condition s”exécute avant que le gestionnaire ne soit choisi, contre un objet d”exception que le runtime pousse sur la pile. endfilter dépile le 0-ou-1 qui décide de tout. Et un throw à l”intérieur du filtre est avalé comme garantie du langage. Glass.NET modélise tout cela : la structure à deux régions, le type remonté du filtre, la variable depuis l”exception poussée, la condition jusqu”à endfilter, et le gestionnaire après. Ce que vous récupérez est le catch (E e) when (…) que vous avez écrit — et une image précise du seul endroit où la gestion des exceptions de C# fait quelque chose que la plupart des gens n”ont jamais su qu”elle faisait.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.