Neu in .NET 6 [15]: Direkte Speicherzugriffe

Der Dotnet-Doktor Holger Schwichtenberg  –  4 Kommentare

Nach den "rohen" Dateisystemzugriffe geht es nun um direkte Speicherzugriffe in .NET 6.

Für direkte Speicherzugriffe gibt es in .NET 6 die neue Klasse System.Runtime.InteropServices.NativeMemory und Verbindung mit der in C# seit Version 1.0 vorhandenen Zeigerarithmetik. Diese Klasse kann man nur in einem unsafe-Block verwenden.

Das folgende Listing zeigt aussagekräftige Beispiele für direkte Speicherzugriffe:

  • Zunächst wird eine Menge von Speicher mit AllocZeroed() bezogen.
  • Die Speicheradresse und der aktuelle Wert (sollte 0 sein) wird ausgegeben.
  • Der Wert an der Adresse wird auf 42 geändert.
  • Der Zeiger wird um 10 verschoben.
  • Der Wert sollte 0 sein und wird auf 42 geändert.
  • Der Zeiger wird auf eine Adresse außerhalb des allokierten Speichers verschoben.

Beim Zugriff darauf stürzt das Programm ab und zwar mit folgender Meldung: "Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt."

Wenn man nicht so einen Unsinn baut, muss man am Ende mit NativeMemory.Free() den allokierten Speicher wieder freigeben.

static public void DirectMemoryAllocation()
{

unsafe
{
nuint MemSize = 1024 * 100;
for (int i = 0; i < 1; i++)
{
// Hole Speicher "ausgenullt" (alternativ: Alloc())
Console.WriteLine($"Allokiere nun {MemSize} Bytes...");
byte* m1 = (byte*)System.Runtime.InteropServices.
NativeMemory.AllocZeroed(MemSize);

CUI.H2("Wert Lesen und ändern");
IntPtr p1 = (IntPtr)m1;
Console.WriteLine("Speicheradresse: " + p1);
Console.WriteLine("Inhalt vorher: " + *m1);
Debug.Assert(*m1 == 0);
*m1 = 42;
Console.WriteLine("Inhalt nachher: " + *m1);
Debug.Assert(*m1 == 42);

CUI.H2("Schaue etwas weiter in den " +
"allokierten Speicher");
IntPtr p2 = new IntPtr(p1.ToInt64() + 10);
byte w2 = *(byte*)p2.ToPointer();
Console.WriteLine("Speicheradresse: " + p2 +
" Wert: " + w2);
Debug.Assert(w2 == 0);

byte* m2 = (byte*)p2.ToPointer();
Console.WriteLine("Inhalt vorher: " + *m2);
*m2 = 42;
Console.WriteLine("Inhalt nachher: " + *m2);
Debug.Assert(*m2 == 42);

try
{
// bei dem folgenden Unsinn des Entwicklers
// hilft in .NET Core CLR kein try-catch!!!
CUI.H2("Versuche,außerhalb des allokierten " +
"Speichers zu lesen");
IntPtr p3 = new IntPtr(p1.ToInt64() +
(long)MemSize + 2000000);
byte w3 = *(byte*)p3.ToPointer();
Console.WriteLine("Speicheradresse: " + p3 +
" Wert: " + w3);
Console.WriteLine("Besser nicht den Wert ändern!!!");
}
catch (Exception ex)
{
CUI.Error(ex.Message);
}

// Speicher wieder freigeben
NativeMemory.Free(m1);
}
}

Noch zwei Hinweise dazu:

  1. Bei dem Versuch, den allokierten Speicher zu überschreiten, beendet die .NET Core Runtime, auf der auch .NET 6 basiert, das Programm, selbst wenn es einen Try-Catch-Block gibt. Das hat Microsoft auch in der Dokumentation beschrieben: ".NET Core only: … recovery from corrupted process state exceptions is not supported".
  2. Ob man solche direkten Speicherzugriffe in .NET wirklich braucht, um die Performance zu verbessern, muss jeder selber wissen. Microsoft sagt: "intended for low-level code and algorithms. Application developers would rarely if ever use these".
Der Try-Catch-Blog hilft nicht gegen den Absturz.
Der Try-Catch-Blog hilft nicht gegen den Absturz.