Sollte dieser unsichere Code auch in .NET Core 3 funktionieren?

42

Ich überarbeite meine Bibliotheken, Span<T>um Heap-Zuweisungen nach Möglichkeit zu vermeiden, aber da ich auch ältere Frameworks anvisiere, implementiere ich auch einige allgemeine Fallback-Lösungen. Aber jetzt habe ich ein seltsames Problem gefunden und bin mir nicht ganz sicher, ob ich einen Fehler in .NET Core 3 gefunden habe oder ob ich etwas Illegales mache.

Die Angelegenheit:

// This returns 1 as expected but cannot be used in older frameworks:
private static uint ReinterpretNew()
{
    Span<byte> bytes = stackalloc byte[4];
    bytes[0] = 1; // FillBytes(bytes);

    // returning bytes as uint:
    return Unsafe.As<byte, uint>(ref bytes.GetPinnableReference());
}

// This returns garbage in .NET Core 3.0 with release build:
private static unsafe uint ReinterpretOld()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1; // FillBytes(bytes);

    // returning bytes as uint:
    return *(uint*)bytes;
}

Interessanterweise ReinterpretOldfunktioniert es gut in .NET Framework und in .NET Core 2.0 (also könnte ich schließlich damit zufrieden sein), aber es stört mich ein bisschen.

Übrigens. ReinterpretOldkann auch in .NET Core 3.0 durch eine kleine Änderung behoben werden:

//return *(uint*)bytes;
uint* asUint = (uint*)bytes;
return *asUint;

Meine Frage:

Ist dies ein Fehler oder funktioniert es ReinterpretOldin älteren Frameworks nur aus Versehen und sollte ich das Update auch für sie anwenden?

Bemerkungen:

  • Der Debug-Build funktioniert auch in .NET Core 3.0
  • Ich habe versucht, mich zu bewerben [MethodImpl(MethodImplOptions.NoInlining)], ReinterpretOldaber es hatte keine Wirkung.
György Kőszeg
quelle
2
Zu Ihrer Information: return Unsafe.As<byte, uint>(ref bytes[0]);oder return MemoryMarshal.Cast<byte, uint>(bytes)[0];- keine Notwendigkeit zu verwenden GetPinnableReference(); Blick in das andere Stück
Marc Gravell
SharpLab für den Fall, dass es jemand anderem hilft. Die beiden Versionen, die dies vermeiden Span<T>, werden zu unterschiedlichen IL kompiliert. Ich glaube nicht, dass Sie etwas Ungültiges tun: Ich vermute einen JIT-Fehler.
Kanton7
Was ist der Müll, den Sie sehen? Verwenden Sie den Hack, um Locals-Init zu deaktivieren? Dieser Hack hat erhebliche Auswirkungen stackalloc(dh er löscht nicht den zugewiesenen Speicherplatz)
Marc Gravell
@ canton7 Wenn sie mit derselben IL kompiliert werden, können wir nicht schließen, dass es sich um einen JIT-Fehler handelt. Wenn die IL identisch ist, usw., klingt dies eher nach einem Compiler-Fehler, wenn überhaupt, vielleicht mit einem älteren Compiler. György: Können Sie genau angeben, wie Sie dies kompilieren? Welches SDK zum Beispiel? Ich kann den Müll nicht
tadeln
1
Es sieht so aus, als ob Stackalloc nicht immer Null ist: link
canton7

Antworten:

35

Oh, das ist ein lustiger Fund. Was hier passiert, ist, dass Ihr Einheimischer optimiert wird - es gibt keine Einheimischen mehr, was bedeutet, dass es keine gibt .locals init, was bedeutet, dass stackallocsich das anders verhält und den Raum nicht abwischt;

private static unsafe uint Reinterpret1()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1;

    return *(uint*)bytes;
}

private static unsafe uint Reinterpret2()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1;

    uint* asUint = (uint*)bytes;
    return *asUint;
}

wird:

.method private hidebysig static uint32 Reinterpret1() cil managed
{
    .maxstack 8
    L_0000: ldc.i4.4 
    L_0001: conv.u 
    L_0002: localloc 
    L_0004: dup 
    L_0005: ldc.i4.1 
    L_0006: stind.i1 
    L_0007: ldind.u4 
    L_0008: ret 
}

.method private hidebysig static uint32 Reinterpret2() cil managed
{
    .maxstack 3
    .locals init (
        [0] uint32* numPtr)
    L_0000: ldc.i4.4 
    L_0001: conv.u 
    L_0002: localloc 
    L_0004: dup 
    L_0005: ldc.i4.1 
    L_0006: stind.i1 
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldind.u4 
    L_000a: ret 
}

Ich denke, ich würde gerne sagen, dass dies ein Compiler-Fehler ist, oder zumindest: ein unerwünschter Nebeneffekt und Verhalten, da frühere Entscheidungen getroffen wurden, um "emit the .locals init" zu sagen , speziell um zu versuchen und Bleiben Sie stackallocgesund - aber ob die Compiler-Leute zustimmen, liegt bei ihnen.

Die Problemumgehung lautet: Behandeln Sie den stackallocRaum als undefiniert (was, um fair zu sein, das ist, was Sie tun sollen); Wenn Sie erwarten, dass es Nullen sind: Stellen Sie es manuell auf Null.

Marc Gravell
quelle
2
Es scheint, dass es dafür ein offenes Ticket gibt. Ich werde dem einen neuen Kommentar hinzufügen.
György Kőszeg
Huh, meine ganze Arbeit und ich haben nicht bemerkt, dass die erste fehlte locals init. Schön.
Kanton7
1
@ canton7 Wenn Sie so etwas wie ich sind, überspringen Sie automatisch .maxstackund .localsmachen es besonders einfach, nicht zu bemerken, dass es da ist / nicht da ist :)
Marc Gravell
1
The content of the newly allocated memory is undefined.laut MSDN. Die Spezifikation sagt nicht, dass der Speicher auch auf Null gesetzt werden sollte. Es sieht also so aus, als ob es nur versehentlich oder aufgrund eines nicht vertraglichen Verhaltens auf alten Frameworks funktioniert.
Luaan