Skip to content
← Tous les articles
· Delta1 Labs Glass.NETDécompilation.NETAnalyse approfondie

Décompiler les fonctions locales dans Glass.NET : mangling des noms et clôtures par struct ref

Une fonction locale n'est pas une lambda, et le compilateur le prouve dans l'IL. Il n'y a ni allocation de délégué ni classe de clôture : une fonction locale sans capture devient une simple méthode statique au nom manglé comme <Run>g__Add|0_0, et une fonction qui capture devient une méthode qui reçoit ses variables capturées par ref dans un struct d'affichage de type valeur, de sorte que rien n'atterrit sur le tas. Voici exactement ce que le compilateur C# émet pour les deux types, comment un décompileur qui ne reconnaît pas le motif vous rend la méthode manglée et le struct ref, et comment Glass.NET le replie en la fonction locale que vous avez écrite.

Une fonction locale ressemble à la sœur plus discrète d”une lambda — une petite méthode nichée dans une autre méthode — et on suppose souvent qu”elles compilent vers la même chose. Ce n”est pas le cas, et la différence est toute la raison d”être des fonctions locales : pas de délégué, pas de clôture allouée sur le tas. Le compilateur C# abaisse une fonction locale en une vraie méthode, et quand cette fonction capture une variable, il passe l”état capturé par ref dans un struct de type valeur, de sorte que rien n”est alloué sur le tas juste pour appeler un auxiliaire que vous avez écrit en ligne.

Cet abaissement est assez distinctif pour que le lire soit une bonne façon de comprendre ce que le compilateur fait réellement pour vous. Ceci est une plongée pratique dans exactement cela : les noms de méthode manglés, le struct d”affichage ref, l”IL du site d”appel pour les cas avec et sans capture, et comment Glass.NET reconnaît toute la forme et la replie en la fonction locale avec laquelle vous avez commencé — au lieu de vous rendre une méthode statique nommée <Sum>g__Add|0_0 et un struct que vous devez rétroconcevoir.

La source

Une méthode, deux fonctions locales. Add capture le total englobant ; Twice ne capture rien et est marquée static pour le dire.

public static class Demo
{
    public static int Sum(int[] xs)
    {
        int total = 0;

        void Add(int v) => total += v;       // captures `total`
        static int Twice(int v) => v * 2;    // captures nothing

        foreach (var x in xs)
            Add(Twice(x));

        return total;
    }
}

Deux fonctions locales qui se lisent presque à l”identique dans la source compilent vers deux formes très différentes, car l”une touche une variable englobante et l”autre non.

Ce que le compilateur émet

Twice est la facile. Elle ne capture rien, donc elle devient une simple méthode statique privée — la seule chose inhabituelle à son sujet est le nom, que le compilateur mangle pour que vous ne puissiez jamais l”écrire vous-même :

.method assembly hidebysig static int32 '<Sum>g__Twice|0_1'(int32 v) cil managed
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    ldarg.0
    ldc.i4.2
    mul
    ret
}

Le nom est <Sum>g__Twice|0_1 : la méthode englobante entre chevrons, g__, le nom d”origine de la fonction locale, une barre verticale, et une paire ordinal/compteur qui le garde unique. Le nom entre g__ et | est votre nom, Twice, conservé tel quel — ce qui est précisément ce qui permet à un décompileur de le récupérer.

Add est l”intéressante. Elle capture total, donc le compilateur hisse total dans un struct d”affichage de type valeur et donne à Add un paramètre ref vers lui :

.class nested private sealed sequential ansi '<>c__DisplayClass0_0'
    extends [mscorlib]System.ValueType       // a struct, not a class
{
    .field public int32 total
}

.method assembly hidebysig static void '<Sum>g__Add|0_0'(
    int32 v,
    valuetype Demo/'<>c__DisplayClass0_0'& 'display')   // passed by ref
{
    ldarg.1              // ref display
    ldarg.1
    ldfld int32 Demo/'<>c__DisplayClass0_0'::total
    ldarg.0              // v
    add
    stfld int32 Demo/'<>c__DisplayClass0_0'::total
    ret
}

C”est le cœur. Une lambda qui capturerait total aurait besoin d”un objet de clôture de type référence, alloué sur le tas, avec total comme champ, invoqué via un délégué Func/Action. La fonction locale n”a besoin de rien de tout cela : le type d”affichage est un struct (extends System.ValueType), il vit sur la pile de l”appelant, et Add le reçoit par ref pour que ses écritures sur total soient visibles de retour dans Sum. Zéro allocation, pas de délégué.

Le site d”appel dans Sum rend le passage par ref explicite. Le struct d”affichage est une variable locale ; les appels prennent son adresse avec ldloca et invoquent les méthodes manglées directement avec call — pas de newobj, pas de callvirt via un délégué :

.locals init (valuetype Demo/'<>c__DisplayClass0_0' display, ...)
ldloca.s   display
ldflda     int32 Demo/'<>c__DisplayClass0_0'::total   // &display.total = 0
...
// inside the loop, for Add(Twice(x)):
ldloca.s   x_current
call       int32 Demo::'<Sum>g__Twice|0_1'(int32)     // Twice(x) — plain static call
ldloca.s   display
call       void  Demo::'<Sum>g__Add|0_0'(int32, valuetype Demo/'<>c__DisplayClass0_0'&)
sourcevoid Add(int v) => total += v;capture totalIL abaisséstruct <>c__DisplayClass0_0 { int total; }<Sum>g__Add|0_0( int v, ref display)ldloca.s displaycall <Sum>g__Add|0_0pas de newobj · pas de délégué · sur la pilereconstruction Glassvoid Add(int v) => total += v;fonction locale imbriquée

Ce qu”un décompileur naïf montre

Un décompileur qui ne modélise pas le motif est fidèle aux métadonnées et donc illisible. Il liste <Sum>g__Twice|0_1 et <Sum>g__Add|0_0 comme des méthodes statiques sœurs de la classe — pas du tout imbriquées dans Sum — et fait remonter le struct d”affichage comme un vrai type avec un paramètre ref qu”il vous laisse interpréter :

// Naive output: structure lost, the capture mechanism exposed.
[StructLayout(LayoutKind.Auto)]
private struct <>c__DisplayClass0_0 { public int total; }

[CompilerGenerated]
private static void <Sum>g__Add|0_0(int v, ref <>c__DisplayClass0_0 display)
    => display.total += v;

[CompilerGenerated]
private static int <Sum>g__Twice|0_1(int v) => v * 2;

public static int Sum(int[] xs)
{
    <>c__DisplayClass0_0 display = default;
    display.total = 0;
    foreach (int x in xs)
        <Sum>g__Add|0_0(<Sum>g__Twice|0_1(x), ref display);
    return display.total;
}

Tout cela est correct et rien n”est ce que vous avez écrit. La localité est perdue — Add et Twice ne sont plus à l”intérieur de Sum — et la plomberie ref display que le compilateur a ajoutée pour vous est désormais quelque chose qu”un lecteur doit décoder.

Ce que Glass reconstruit

Glass est bâti sur ICSharpCode.Decompiler, le moteur derrière ILSpy, qui reconnaît toute la forme : le motif de nom <Englobante>g__Nom|n_m, la marque [CompilerGenerated] et le struct d”affichage de type valeur passé par ref. Il inverse les trois. Les méthodes manglées sont ré-imbriquées dans Sum en fonctions locales nommées par le texte entre g__ et | ; le struct d”affichage et son cheminement ref sont dissous en une simple variable capturée ; et le static de Twice est restauré car sa signature ne montre aucun état capturé :

// Glass output: the local functions you actually wrote.
public static int Sum(int[] xs)
{
    int total = 0;

    void Add(int v) => total += v;
    static int Twice(int v) => v * 2;

    foreach (int x in xs)
        Add(Twice(x));

    return total;
}

Il vaut la peine de nommer la différence avec le cas de la lambda, car Glass la montre correctement aussi. Si Add avait été écrite comme Action<int> add = v => total += v;, la reconstruction montrerait un délégué adossé à une clôture de type référence — un newobj d”une classe d”affichage, les variables capturées comme ses champs, et l”invocation via Action. Ici il n”y a de délégué nulle part, et Glass n”en invente pas : il montre une méthode imbriquée et une variable locale capturée ordinaire, ce qui est la vérité de l”IL. Lire les deux côte à côte dans Glass est la façon la plus claire de voir, dans votre propre code, pourquoi le conseil « préférez une fonction locale à une lambda quand vous ne stockez pas de délégué » concerne les allocations, pas le style.

Quand descendre à la vue IL

La reconstruction est ce que vous voulez la plupart du temps, mais quelques points sur les fonctions locales ne sont visibles qu”en dessous, et Glass vous laisse descendre directement à l”IL pour le même membre :

  • La capture s”est-elle faite par ref ou par valeur ? Le struct d”affichage est passé par ref pour que les écritures se propagent ; le confirmer dans l”IL (valuetype ...DisplayClass...&) est la façon de raisonner sur le fait qu”une mutation à l”intérieur de la fonction locale est vue par l”appelant.
  • Le compilateur a-t-il entièrement éliminé le struct d”affichage ? Pour une fonction véritablement sans capture, vous verrez une méthode statique nue avec seulement ses paramètres déclarés et aucun struct — la preuve que la marquer static n”a rien coûté ni rien alloué.
  • Collisions d”ordinaux et surcharges. Le suffixe |n_m désambiguïse plusieurs fonctions locales et plusieurs portées lexicales ; la vue IL est là où vous associez une méthode manglée précise à une position source précise quand une méthode en a plusieurs.

Lisez le C# pour récupérer l”intention et la structure ; descendez à l”IL quand vous devez vous prouver exactement comment la capture a été câblée. Que les deux vues concordent — une fonction locale imbriquée en haut, une méthode statique manglée et un struct ref en dessous — est tout l”intérêt : Glass vous montre le code que vous avez écrit sans cacher la machinerie qui le rend peu coûteux.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.