7.13 Unsicherer (unsafe) Programmcode – Zeigertechnik in C# 

7.13.1 Einführung 

Manchmal ist es erforderlich, auf die Funktionen einer in C geschriebenen herkömmlichen DLL zuzugreifen. Viele C-Funktionen erwarten jedoch Zeiger auf bestimmte Speicheradressen oder geben Zeiger als Aufrufergebnis zurück. Es kann auch vorkommen, dass in einer Anwendung der Zugriff auf Daten erforderlich ist, die sich nicht im Hauptspeicher, sondern beispielsweise im Grafikspeicher befinden. Das Problem ist im ersten Moment, dass C#-Code, der unter der Obhut der Common Language Runtime läuft und als sicherer bzw. verwalteter (managed) Code eingestuft wird, keine Zeiger auf Speicheradressen gestattet.
Ein Entwickler, der mit dieser Einschränkung in seiner Anwendung nicht leben kann, muss unsicheren Code schreiben. Trotz dieser seltsamen Bezeichnung ist unsicherer Code selbstverständlich nicht wirklich »unsicher« oder wenig vertrauenswürdig. Es handelt sich hierbei lediglich um C#-Code, der die Typüberprüfung durch den Compiler einschränkt und den Einsatz von Zeigern und Zeigeroperationen ermöglicht.
7.13.2 Das Schlüsselwort »unsafe« 

Der Kontext, in dem unsicherer Code gewünscht wird, muss mithilfe des Schlüsselworts unsafe deklariert werden. Es kann eine komplette Klasse oder eine Struktur ebenso als unsicher markiert werden wie eine einzelne Methode. Es ist sogar möglich, innerhalb des Anweisungsblocks einer Methode einen Teilbereich als unsicher zu kennzeichnen.
Ganz allgemein besteht ein nicht sicherer Bereich aus Code, der in geschweiften Klammern eingeschlossen ist und dem das Schlüsselwort unsafe vorangestellt wird. Im folgenden Codefragment wird die Methode Main als unsicher deklariert:
static unsafe void Main(string[] args) { // Anweisungen }
Die Angabe von unsafe ist aber allein noch nicht ausreichend, um unsicheren Code kompilieren zu können. Zusätzlich muss auch noch der Compilerschalter /unsafe gesetzt werden. In Visual Studio 2010 legen Sie diesen Schalter im Projekteigenschaftsfenster unter Erstellen Unsicheren Code zulassen fest. Wenn Sie vergessen, den Compilerschalter einzustellen, wird bei der Kompilierung ein Fehler generiert.
7.13.3 Deklaration von Zeigern 

In C/C++ sind Zeiger ein klassisches Hilfsmittel der Programmierung, in .NET hingegen nehmen Zeiger eine untergeordnete Rolle ein und werden meist nur in Ausnahmesituationen benutzt. Wir werden daher nicht allzu tief in die Thematik einsteigen und uns auf das Wesentlichste konzentrieren. Wenn Sie keine Erfahrungen mit der Zeigertechnik in C oder in anderen zeigerbehafteten Sprachen gesammelt haben und sich dennoch weiter informieren wollen, sollten Sie C-Literatur zur Hand nehmen.
Zeiger sind Verweise auf Speicherbereiche und werden allgemein wie folgt deklariert:
Datentyp* Variable
Dazu ein Beispiel. Mit der Deklaration
int intVar = 4711; int* pointer;
erzeugen wir eine int-Variable namens intVar und eine Zeigervariable pointer. pointer ist noch kein Wert zugewiesen und zeigt auf eine Speicheradresse, deren Inhalt als Integer interpretiert wird. Der *-Operator ermöglicht die Deklaration eines typisierten Zeigers und bezieht sich auf den vorangestellten Typ – hier Integer.
Wollen wir dem Zeiger pointer mitteilen, dass er auf die Adresse der Variablen intVar zeigen soll, müssen wir pointer die Adresse von intVar übergeben:
pointer = &intVar;
Der &-Adressoperator liefert eine physikalische Speicheradresse. In der Anweisung wird die Adresse der Variablen intVar ermittelt und dem Zeiger pointer zugewiesen.
Wollen wir den Inhalt der Speicheradresse erfahren, auf die der Zeiger verweist, muss dieser dereferenziert werden:
Console.WriteLine(*pointer);
Das Ergebnis wird 4711 lauten.
Fassen wir den gesamten (unsicheren) Code zusammen. Wenn Sie die Zeigertechnik unter C kennen, werden Sie feststellen, dass es syntaktisch keinen Unterschied gibt:
class Program { static unsafe void Main(string[] args) { int intVar = 4711; int* pointer; pointer = &intVar; Console.WriteLine(*pointer); } }
C# gibt einen Zeiger nur von einem Wertetyp und niemals von einem Referenztyp zurück. Das gilt jedoch nicht für Arrays und Zeichenfolgen, da Variablen dieses Typs einen Zeiger auf das erste Element bzw. den ersten Buchstaben liefern.
7.13.4 Die »fixed«-Anweisung 

Während der Ausführung eines Programms werden dem Heap viele Objekte hinzugefügt oder dort aufgegeben. Um eine unnötige Speicherbelegung oder Speicherfragmentierung zu vermeiden, schiebt der Garbage Collector die Objekte hin und her. Auf ein Objekt zu zeigen ist natürlich wertlos, wenn sich seine Adresse unvorhersehbar ändern könnte. Die Lösung dieses Problems bietet die fixed-Anweisung. fixed weist den Garbage Collector an, das Objekt zu »fixieren« – es wird danach nicht mehr verlagert. Da sich dies auf das Verhalten der Laufzeitumgebung auswirken kann, sollten als fixed deklarierte Blöcke nur kurzzeitig benutzt werden.
Hinter der fixed-Anweisung wird in runden Klammern ein Zeiger auf eine verwaltete Variable festgelegt. Diese Variable ist diejenige, die während der Ausführung fixiert wird.
fixed (<Typ>* <pointer> = <Ausdruck>) { ... }
Ausdruck muss implizit in Typ* konvertierbar sein.
Am besten sind die Wirkungsweise und der Einsatz von fixed anhand eines Beispiels zu verstehen. Sehen Sie sich daher zuerst das folgende Codefragment an:
class Program { int intVar; static void Main() { Program obj = new Program(); // unsicherer Code unsafe { // fixierter Code fixed(int* pointer = &obj.intVar) { *pointer = 9; System.Console.WriteLine(*pointer); } } } }
Im Code wird ein Objekt vom Typ Program in Main erzeugt. Es kann grundsätzlich nicht garantiert werden, dass das Program-Objekt obj vom Garbage Collector nicht im Speicher verschoben wird. Da der Zeiger pointer auf das objekteigene Feld intVar verweist, muss sichergestellt sein, dass sich das Objekt bei der Auswertung des Zeigers immer noch an derselben physikalischen Adresse befindet. Die fixed-Anweisung mit der Angabe, worauf pointer zeigt, garantiert, dass die Dereferenzierung an der Konsole das richtige Ergebnis ausgibt.
Beachten Sie, dass in diesem Beispiel nicht die gesamte Methode als unsicher markiert ist, sondern nur der Kontext, in dem der Zeiger eine Rolle spielt.
7.13.5 Zeigerarithmetik 

Sie können in C# Zeiger so addieren und subtrahieren wie in C oder in anderen Sprachen. Dazu bedient sich der C#-Compiler intern des sizeof-Operators, der die Anzahl der Bytes zurückgibt, die von einer Variablen des angegebenen Typs belegt werden. Addieren Sie beispielsweise zu einem Zeiger vom Typ int* den Wert 1, verweist der Zeiger auf eine Adresse, die um 4 Byte höher liegt, da ein Integer eine Breite von 4 Byte hat.
Im folgenden Beispiel wird ein int-Array initialisiert. Anschließend werden die Inhalte der Array-Elemente nicht wie üblich über ihren Index, sondern mittels Zeigerarithmetik an der Konsole ausgegeben.
class Program { unsafe static void Main(string[] args) { int[] arr = {10, 72, 333, 4550}; fixed(int* pointer = arr) { Console.WriteLine(*pointer); Console.WriteLine(*(pointer + 1)); Console.WriteLine(*(pointer + 2)); Console.WriteLine(*(pointer + 3)); } } }
Ein Array ist den Referenztypen und damit den verwalteten Typen zuzurechnen. Der C#-Compiler erlaubt es aber nicht, außerhalb einer fixed-Anweisung mit einem Zeiger auf einen verwalteten Typ zu zeigen. Mit
fixed(int* pointer = arr)
kommen wir dieser Forderung nach. Das Array arr wird implizit in den Typ int* konvertiert und ist gleichwertig mit folgender Anweisung:
int* pointer = &arr[0]
In der ersten Ausgabeanweisung wird pointer dereferenziert und der Inhalt 10 angezeigt, weil ein Zeiger auf ein Array immer auf das erste Element zeigt. In den folgenden Ausgaben wird die Ausgabeadresse des Zeigers um jeweils eine Integerkapazität erhöht, also um jeweils 4 Byte. Da die Elemente eines Arrays direkt hintereinander im Speicher abgelegt sind, werden der Reihe nach die Zahlen 72, 333 und 4550 auf der Konsole angezeigt.
7.13.6 Der Operator »->« 

Strukturen sind Wertetypen aus mehreren verschiedenen Elementen auf dem Stack und können ebenfalls über Zeiger angesprochen werden. Nehmen wir an, die Struktur Point sei wie folgt definiert:
public struct Point {
public int X;
public int Y;
}
Innerhalb eines unsicheren Kontexts können wir uns mit
Point point = new Point(); Point* ptr = &point;
einen Zeiger auf ein Objekt vom Typ Point besorgen. Beabsichtigen wir, das Feld X zu manipulieren und ihm den Wert 150 zuzuweisen, muss der Zeiger ptr zuerst dereferenziert werden. Auf das Ergebnis kann mittels Punktnotation zugegriffen werden, dem die Zahl zugewiesen werden soll. Der gesamte Ausdruck sieht dann wie folgt aus:
(*ptr).X = 150;
C# bietet uns mit dem Operator -> eine einfache Kombination aus Dereferenzierung und Feldzugriff an. Der Ausdruck kann daher gleichwertig auch so formuliert werden:
ptr->X = 150;