Skip to content
← Alle Beiträge
· Delta1 Labs Glass.NETDekompilierung.NETTiefenanalyse

volatile dekompilieren: der modreq am Feld, der Prefix vor jedem Zugriff und der Volatile.Read-Aufruf, der gleich aussieht, es aber nicht ist

Das Schlüsselwort volatile hat keine eigene Laufzeit. Es kompiliert zu zwei separaten Metadaten-Stücken, die an verschiedenen Orten leben: einem Required Modifier, modreq(IsVolatile), der in die Signatur des Feldes eingeschweißt ist, und einem ein Byte langen volatile.-Prefix, der vor jedem ldfld und stfld steht, das das Feld anrührt. Ein Decompiler muss beide lesen, sich mit sich selbst darüber einig werden, welcher maßgeblich ist, und das Ergebnis dann nicht mit den beiden Dingen verwechseln, die in C# identisch aussehen, aber völlig unterschiedlich kompilieren – Volatile.Read auf einem gewöhnlichen Feld, was ein schlichter Aufruf ohne Prefix und ohne Modifier ist, und Interlocked.Increment auf einem volatile-Feld, das das Feld per Adresse nimmt und daher den Modifier trägt, aber nicht den Prefix. Dieser Beitrag geht das IL für alle drei durch, zeigt, was Glass.NET aus jedem rekonstruiert, erklärt, warum das Feldreferenz-Token selbst den modreq tragen muss, da der Zugriff sonst nicht einmal auflöst, und behandelt den einen Fall, den C# nicht buchstabieren kann – einen volatile.-Prefix ohne volatile-Feld dahinter – und den Aufruf, den Glass dafür emittiert.

volatile ist eines der kleinsten Schlüsselwörter in C# und eines der am weitesten verteilten im IL. Es kompiliert nicht zu einer Instruktion. Es kompiliert zu einem Modifier an der Signatur des Feldes und einem Prefix vor jedem Zugriff auf das Feld, an zwei verschiedenen Stellen in den Metadaten, und ein Decompiler muss beide lesen und entscheiden, welchem er glaubt. Dann muss er vermeiden, das Schlüsselwort mit seinen zwei Doppelgängern zu verwechseln: Volatile.Read auf einem Feld, das nicht volatile ist, und Interlocked.Increment auf einem Feld, das es ist. Alle drei lesen sich wie ein thread-sicherer Zugriff in C#; alle drei kompilieren zu unterschiedlichem IL. Dieser Beitrag geht die drei für Glass.NET durch.

Ein Schlüsselwort, zwei Metadaten-Stücke

Beginnen Sie mit der gewöhnlichen Form, einem Stop-Flag, das von einer Worker-Schleife abgefragt wird:

public sealed class Worker
{
    private volatile bool _stop;
    private int _iterations;

    public void RequestStop() => _stop = true;

    public void Run()
    {
        while (!_stop) { DoWork(); _iterations++; }
    }
}

Betrachten Sie zuerst das Feld. Die Deklaration ist nicht bool; sie ist bool, der einen Required Modifier trägt:

.field private bool modreq([System.Runtime]System.Runtime.CompilerServices.IsVolatile) _stop
.field private int32 _iterations

modreq – required Modifier, im Gegensatz zu modopt – bedeutet, dass der Modifier Teil der Signatur ist. Ein Konsument, der ihn nicht versteht, muss das Binden verweigern, weshalb IsVolatile ein modreq ist: Ein Compiler, der ihn ignorierte, würde stillschweigend nicht-volatile Zugriff auf ein volatile-Feld emittieren. Der praktische Effekt für einen Decompiler ist, dass jede Referenz auf das Feld den Modifier ebenfalls trägt, weil ein Feldreferenz-Token per Name und Signatur aufgelöst wird und bool _stop eine andere Signatur ist als bool modreq(IsVolatile) _stop.

Nun die Zugriffe. RequestStop ist ein Store, Run ist ein Load in einer Schleife:

.method public hidebysig instance void RequestStop() cil managed
{
  ldarg.0
  ldc.i4.1
  volatile.
  stfld      bool modreq([System.Runtime]System.Runtime.CompilerServices.IsVolatile) Worker::_stop
  ret
}

.method public hidebysig instance void Run() cil managed
{
  IL_0000:  br.s       IL_0016
  IL_0002:  ldarg.0
  IL_0003:  call       instance void Worker::DoWork()
  IL_0008:  ldarg.0
  IL_0009:  dup
  IL_000a:  ldfld      int32 Worker::_iterations
  IL_000f:  ldc.i4.1
  IL_0010:  add
  IL_0011:  stfld      int32 Worker::_iterations
  IL_0016:  ldarg.0
  IL_0017:  volatile.
  IL_0019:  ldfld      bool modreq([System.Runtime]System.Runtime.CompilerServices.IsVolatile) Worker::_stop
  IL_001e:  brtrue.s   IL_0002
  IL_0020:  ret
}

Zwei Dinge fallen auf. Der volatile.-Prefix (0xFE 0x13, weshalb der nächste Offset um zwei springt) sitzt unmittelbar vor dem ldfld/stfld und gilt nur für diese eine Instruktion – es ist die Anweisung des JIT, den Load nicht aus der Schleife herauszuheben oder den Wert in einem Register zu halten. Und die _iterations-Zugriffe direkt daneben haben keinen Prefix, weil dieses Feld keinen Modifier hat. Pro Zugriff, pro Feld: das ist das Paar.

Drei Zugriffe, die in C# gleich aussehen, und ihr IL1. volatile-Feld.field bool modreq(IsVolatile) _stopvolatile. ldfld _stopModifier: ja Prefix: ja→ volatile bool _stop; schlichter Read2. Volatile.Read(ref _flag).field bool _flagldflda _flag; call Volatile::ReadModifier: nein Prefix: nein→ expliziter Volatile.Read bleibt3. Interlocked auf volatileldflda int32 modreq(IsVolatile)_count; call Interlocked::IncrementModifier: ja Prefix: nein→ Interlocked.Increment(ref _count)Wie Glass.NET entscheidetDer Modifier am Feld ist die maßgebliche Wahrheit: C# hat eine Schreibweise für Volatilität, das Schlüsselwort an der Deklaration,und es impliziert den Prefix bei jedem direkten Load und Store – ein mit Prefix versehener Zugriff auf ein modreq-Feld wird daher schlicht dargestellt.Ein adressnehmender Zugriff (ldflda) trägt den Modifier, kann aber nie den Prefix tragen; der folgende call wird dargestellt wie geschrieben.Ein Prefix ohne modreq-Feld dahinter ist in C# nicht ausdrückbar; Glass stellt ihn als expliziten Volatile.Read-/Volatile.Write-Aufruf dar.

Was Glass liest, und welche Hälfte es glaubt

C# hat genau einen Weg, „volatile“ zu sagen: den Modifier an der Deklaration. Es gibt keine Syntax für ein nicht-volatiles Lesen eines volatile-Feldes und keine Syntax für ein volatiles Lesen eines gewöhnlichen Feldes per Prefix – dieser Fall muss über Volatile.Read gehen, was ein Methodenaufruf ist. Der Modifier auf Feldebene ist also das Einzige, was die Sprache ausdrücken kann, und es ist das, was Glass.NET als die maßgebliche Wahrheit nimmt.

Die Rekonstruktion lautet daher: modreq(IsVolatile) aus der Feldsignatur lesen → volatile an der Deklaration emittieren → einen volatile.-Prefix bei jedem direkten ldfld/stfld/ldsfld/stsfld dieses Feldes erwarten → einen schlichten Zugriff emittieren, weil das Schlüsselwort an der Deklaration die Bedeutung bereits trägt. Das Ergebnis ist der Quellcode, mit dem Sie begonnen haben:

private volatile bool _stop;     // from the modreq on the field
private int _iterations;

public void RequestStop() { _stop = true; }          // prefix consumed by the declaration

public void Run()
{
    while (!_stop) { DoWork(); _iterations++; }      // same
}

Der Prefix wird nicht weggeworfen; er wird geprüft. Wenn ein Feld den Modifier hat, aber einem direkten Zugriff der Prefix fehlt, stammt dieses IL nicht vom C#-Compiler – es stammt von handgeschriebenem IL, einem Rewriter oder einer anderen Sprache –, und Glass stellt den Zugriff dar, wie er geschrieben ist, und annotiert ihn, denn ihn stillschweigend zum Schlüsselwort zu befördern, würde einen Zaun behaupten, der nicht da ist. Die andere Richtung, ein Prefix ohne Modifier, ist der Fall am Ende dieses Beitrags.

Doppelgänger eins: Volatile.Read auf einem Feld, das nicht volatile ist

Nun die Form, zu der Sie in heißem Code greifen, wo nur ein Lesen synchronisieren muss und auf das Feld ansonsten frei zugegriffen wird:

private bool _ready;                                  // not volatile

public void Publish() { /* ... */ Volatile.Write(ref _ready, true); }
public bool IsReady  => Volatile.Read(ref _ready);

Das liest sich wie ein volatile-Feld mit einem zusätzlichen Wort. Das IL hat mit dem ersten Fall nichts gemein:

.field private bool _ready                          // no modreq

.method public hidebysig specialname instance bool get_IsReady() cil managed
{
  ldarg.0
  ldflda     bool Worker::_ready                    // take the ADDRESS — no prefix possible
  call       bool [System.Runtime]System.Threading.Volatile::Read(bool&)
  ret
}

Kein Modifier am Feld, kein Prefix an irgendeiner Instruktion. Die Volatilität lebt im Callee, und sie gilt nur für diesen einen Zugriff – jedes andere Lesen von _ready im Typ ist ein schlichter, ungezäunter Load, was genau der Sinn ist, es so zu schreiben. Glass.NET behält den Aufruf exakt wie geschrieben bei: ein nicht-volatiles Feld und ein expliziter Volatile.Read. Es zu private volatile bool _ready; mit einem schlichten Read zusammenzufalten, wäre eine semantische Änderung – es würde jeden anderen Zugriff auf das Feld zäunen, einschließlich derer, die der Autor bewusst schlicht gelassen hat. Die beiden Formen haben an dieser Stelle dieselbe Garantie und überall sonst unterschiedliche Garantien, und ein getreuer Decompiler bewahrt den Unterschied.

Doppelgänger zwei: Interlocked.Increment auf einem Feld, das volatile ist

Die dritte Form ist diejenige, die eine Compiler-Warnung erzeugt, und das IL erklärt die Warnung besser als die Dokumentation:

private volatile int _count;

public void Bump() => Interlocked.Increment(ref _count);   // warning CS0420

Interlocked.Increment nimmt ref int. Um ein Feld per Referenz zu übergeben, muss der Compiler seine Adresse nehmen, und eine Adresse ist kein Load und kein Store – es gibt nichts, worauf man einen volatile.-Prefix setzen könnte:

.field private int32 modreq([System.Runtime]System.Runtime.CompilerServices.IsVolatile) _count

.method public hidebysig instance void Bump() cil managed
{
  ldarg.0
  ldflda     int32 modreq([System.Runtime]System.Runtime.CompilerServices.IsVolatile) Worker::_count
  call       int32 [System.Runtime]System.Threading.Interlocked::Increment(int32&)
  pop
  ret
}

Das ldflda-Token trägt den Modifier – es muss, sonst würde die Feldreferenz nicht auflösen –, aber es gibt keinen Prefix, weil der Zugriff über die Referenz innerhalb von Increment geschieht, das ohnehin ein atomares Read-Modify-Write ausführt, das mindestens so stark ist wie ein volatile-Zugriff. Deshalb ist CS0420 („a reference to a volatile field will not be treated as volatile“) eine Warnung und kein Fehler: Der Code ist hier korrekt, der Compiler kann es nur im Allgemeinen nicht beweisen.

Glass.NET liest das als Interlocked.Increment(ref _count) zurück, mit _count weiterhin als volatile deklariert, weil der Modifier weiterhin am Feld ist. Die Warnung ist verschwunden, und sie ist nicht wiederherstellbar: Eine Warnung ist der Kommentar des Compilers, keine Metadaten, und nichts in der Assembly hält fest, dass sie je ausgelöst wurde. Dekompilierter Code ist der Code.

Der Fall, den C# nicht buchstabieren kann

Alles oben stammte vom C#-Compiler, der den Prefix immer nur bei Zugriffen auf modreq-Felder emittiert. Andere Erzeuger sind nicht so ordentlich. Handgeschriebenes IL, IL-Rewriter und andere Compiler können volatile. vor ein ldfld eines Feldes ohne Modifier setzen, oder vor ldind.i4/stind.i4/ldobj/stobj auf einem Pointer oder einer verwalteten Referenz, und auch cpblk/initblk akzeptieren es:

ldarg.1                 // a ref int parameter
volatile.
ldind.i4                // volatile read through a reference — no field, no modreq

C# hat dafür kein Schlüsselwort. Das einzige Konstrukt in der Sprache, das „ein volatiler Zugriff auf eine Stelle, die kein volatile-Feld ist“ bedeutet, ist die Volatile-API, also ist das, was Glass emittiert:

Volatile.Read(ref value)      // what Glass renders for volatile. ldind.i4 on a ref parameter

Es ist nicht das, was der ursprüngliche Autor geschrieben hat – er schrieb IL –, aber es ist das einzige C#, das bedeutet, was er geschrieben hat, und es kompiliert zu einem Aufruf mit derselben Acquire-Semantik. Die Alternative, den Prefix fallenzulassen und eine schlichte Dereferenzierung darzustellen, würde stillschweigend einen Zaun entfernen. Wenn die Form ungewöhnlich ist, bevorzugt Glass das Konstrukt, das die Garantie bewahrt, gegenüber dem, das wie typischer Quellcode aussieht.

Das Fazit

volatile ist ein Modifier auf Feldebene und ein Prefix pro Zugriff, die in zwei verschiedenen Teilen der Metadaten leben, und der Modifier ist die Hälfte, die ein Decompiler ausdrücken kann – Glass.NET liest modreq(IsVolatile) aus dem Feld, emittiert das Schlüsselwort an der Deklaration und behandelt den Prefix bei jedem direkten Load und Store als Bestätigung statt als Information. Die zwei Doppelgänger behalten aus gutem Grund ihre eigenen Formen: Volatile.Read auf einem gewöhnlichen Feld ist eine Adresse und ein Aufruf ohne Modifier, und es zum Schlüsselwort zu befördern, würde Zugriffe zäunen, die der Autor schlicht gelassen hat; Interlocked.Increment auf einem volatile-Feld ist eine Adresse mit dem Modifier und ohne Prefix und wird dargestellt wie geschrieben, während seine CS0420-Warnung schlicht nicht wiederherstellbar ist. Und die eine Form, die C# nicht buchstabieren kann – ein Prefix ohne volatile-Feld dahinter – kommt als Volatile-Aufruf heraus, weil das Bewahren des Zauns wichtiger ist, als wie typischer Quellcode auszusehen.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.