10.10 Unsicherer (unsafe) Programmcode – Zeigertechnik in C#
10.10.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 solche 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 CLR 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.
10.10.2 Das Schlüsselwort »unsafe«
Der Kontext, in dem unsicherer Code gewünscht wird, muss mit Hilfe 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) {
[...]
}
Listing 10.53 Definition einer »unsafe«-Methode
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 2012 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.
10.10.3 Die 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 value = 4711;
int* pointer;
erzeugen wir eine int-Variable namens value 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 value zeigen soll, müssen wir pointer die Adresse von value übergeben:
pointer = &value;
Der &-Adressoperator liefert eine physikalische Speicheradresse. In der Anweisung wird die Adresse der Variablen value 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 value = 4711;
int* pointer;
pointer = &value;
Console.WriteLine(*pointer);
}
}
Listing 10.54 Zeigertechnik mit C#
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.
10.10.4 Die »fixed«-Anweisung
Während der Ausführung eines Programms werden dem Heap viele Objekte hinzugefügt oder 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 dieser Problematik bietet die fixed-Anweisung. fixed weist den Garbage Collector an, das Objekt zu »fixieren« – es wird danach nicht mehr verlagert. Da sich dies negativ 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 dabei 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 Listing an:
class Program {
int value;
static void Main() {
Program obj = new Program();
// unsicherer Code
unsafe {
// fixierter Code
fixed(int* pointer = &obj.value) {
*pointer = 9;
System.Console.WriteLine(*pointer);
}
}
}
}
Listing 10.55 Fixierter Programmcode
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 value 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.
10.10.5 Zeigerarithmetik
Sie können in C# Zeiger addieren und subtrahieren, so 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));
}
}
}
Listing 10.56 Zeigerarithmetik mit C#
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 Integer-Kapazitä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 an der Konsole angezeigt.
10.10.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. Mittels Punktnotation wird dann das Feld angegeben, dem der Wert 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;
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.