Zeiger in C und C++ - Universität Konstanz

Transcrição

Zeiger in C und C++ - Universität Konstanz
Zeiger in C und C++
Zeiger in C und C++
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
1
Zeiger in C und C++
Zeiger in Java und C/C++
• Zeigervariable (kurz: Zeiger, engl.: pointer): eine Variable,
die als Wert eine Speicheradresse enthält
• Java:
– Zeiger werden implizit für Referenztypen (Klassen und
Arrays) verwendet, es gibt keine besondere Syntax für
Zeigeroperationen
• C/C++:
– Zeiger sind eine eigene Gruppe von Datentypen und
werden explizit durch den Programmierer als Zeiger
deklariert
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
2
Zeiger in C und C++
Motivation
• Warum benötigt man überhaupt Zeiger?
– Hauptgrund: dynamische Speicherverwaltung
• Adressen von Variablen werden vom Compiler
festgelegt, d.h. der benötigte Speicherplatz muß zur
Compilierzeit bekannt sein
• Problem: die genauen Daten, die das Programm
verarbeiten soll, sind zur Compilierzeit i.a. nicht bekannt,
der Compiler kann nicht wissen, wieviel Speicherplatz
benötigt wird
• Lösung: Speicherplatz kann vom Programm zur Laufzeit
angefordert (allokiert) werden, Zugriff auf diesen
Speicherplatz erfordert Zeiger
Datenstrukturen und Algorithmen WS2004/05
3
Universität Konstanz
Zeiger in C und C++
Zeigerdeklaration
• Zeiger sind in C/C++ typisiert, d.h. eine Zeigervariable
enthält Informationen darüber, auf welchen Typ sie zeigt.
• Zeigertypen werden bei der Deklaration durch das
Zeichen "*" hinter dem Typ gekennzeichnet.
– Beispiel:
• Deklaration einer Variable vom Typ int (kein
Zeiger):
int i;
• Deklaration eines Zeigers auf einen Wert vom
Typ int: int* j;
oft auch: int *j;
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
4
Zeiger in C und C++
Adressermittlung
• Wie kann die Adresse, die ein Zeiger speichern soll,
ermittelt werden?
– erste Möglichkeit: Zeiger als Ergebnis einer
Speicherallokation
• die Speicherverwaltung liefert eine Adresse auf
einen Speicherbereich, den das Programm
benutzen darf
• in C:
– Funktion void* malloc(size_t size)
(deklariert in stdlib.h) versucht, size Bytes
Speicher zu allokieren
Datenstrukturen und Algorithmen WS2004/05
5
Universität Konstanz
Zeiger in C und C++
Adressermittlung
– Ergebnis von malloc ist ein Zeiger vom Typ
void* (untypisierter Zeiger), der mit allen
Zeigertypen zuweisungskompatibel ist.
– Achtung: Der Aufruf von malloc kann fehlschlagen, Ergebnis ist dann ein Zeiger mit dem Wert 0
(Nullpointer, zur Unterscheidung von der Zahl 0
oft durch die Konstante NULL bezeichnet).
– Beispiel:
Speicher für eine int-Variable allokieren:
int *j=malloc(sizeof(int));
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
6
Zeiger in C und C++
Adressermittlung
• in C++:
– Verwendung von malloc ist auch möglich, aber
Zeiger vom Typ void* erfordern explizite Typumwandlung (type cast, Syntax wie in Java)
– Beispiel:
int *j=(int *)malloc(sizeof(int));
– Besser in C++: Verwendung des new-Operators,
da bei Klassen automatisch Konstruktoren aufgerufen werden
– Beispiel:
int *j=new int;
Datenstrukturen und Algorithmen WS2004/05
7
Universität Konstanz
Zeiger in C und C++
Adressermittlung
– Zweite Möglichkeit: Zeiger auf Adresse einer bereits
existierenden Variablen
• in C und C++:
– der unäre Operator & liefert die Adresse der
hinter ihm stehenden Variablen (nicht zu
verwechseln mit dem binären Operator &, der
das bitweise logische Und bildet)
– Beispiel:
int i;
int *j=&i;
j enthält die Adresse von i
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
8
Zeiger in C und C++
Speicherfreigabe
• C/C++: Die Funktion free(void *ptr) (ebenfalls in
stdlib.h deklariert) gibt einen mit malloc allokierten
Speicherbereich frei.
– Beispiel:
free(j);
• C++: Der Operator delete ruft bei Klassen den Destruktor
auf und gibt den mit new allokierten Speicherbereich frei.
– Beispiel:
delete j;
• Niemals mit malloc allokierten Speicher mit delete oder
mit new allokierten Speicher mit free freigeben!
Datenstrukturen und Algorithmen WS2004/05
9
Universität Konstanz
Zeiger in C und C++
Dereferenzierung von Zeigern
• Dereferenzierung: Zugriff auf den Wert, der an der in der
Zeigervariablen gespeicherten Adresse steht ("der Wert,
auf den der Zeiger zeigt")
• Zur Dereferenzierung eines Zeigers dient der unäre
Operator * (nicht zu verwechseln mit dem binären
Multiplikationsoperator *).
• Beispiel:
int *i=new int;
int j;
*i=3;
j=*i;//j hat nach dieser Zeile den Wert 3
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
10
Zeiger in C und C++
Pointer Aliasing
• Pointer Aliasing: Zwei Zeiger enthalten dieselbe Adresse
oder ein Zeiger enthält die Adresse einer anderen sichtbaren Variablen. Der eine Zeiger ist dann ein Alias des
anderen Zeigers bzw. der Variablen.
• Unnötiges Pointer Aliasing sollte vermieden werden, da es
dem Compiler die Optimierung erschwert.
• Beispiel:
int i=1;
int *j=&i;
*j=3;//i wird über Alias j geändert
• Optimierungsproblem: i kann nicht in einem CPU-Register
gehalten werden, da der Wert im Speicher geändert wird.
Datenstrukturen und Algorithmen WS2004/05
11
Universität Konstanz
Zeiger in C und C++
Arrays
• Array in C und C++: Zusammenhängender Speicherbereich einer festen (zur Compilierzeit bekannten) Größe
• Die Arrayvariable wird wie ein Zeiger auf den Anfang
dieses Speicherbereichs behandelt.
• Deklaration eines Arrays:
Typ Name[Anzahl der Elemente]
• Beispiel: Array mit 100 Elementen vom Typ int:
int i[100];
• Zugriff auf Arrayelemente: wie in Java
i[12]++; //Element mit Index 12 erhöhen
• Es gibt kein Feld length bei Arrays in C/C++.
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
12
Zeiger in C und C++
Arrays
• Vorsicht beim Zugriff auf Arrayelemente:
– Es findet keine Bereichsüberprüfung statt.
– Ändern eines Arrayelements mit einem Index außerhalb der Arraygröße führt zum Überschreiben von
fremdem Speicher (kann eigenes Programm oder unter
manchen Betriebssystemen auch andere Programme
ungewollt verändern).
– Beispiel: (das ist gültiger C-Code!)
int data[10];
int index=-10000;
data[index]=0; //BOOM!
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
13
Zeiger in C und C++
Pufferüberlauf - Buffer Overflow
• Häufiger Fehler in C/C++-Programmen: die fehlende
Bereichsüberprüfung wird nicht berücksichtigt, wenn externe
Daten eingelesen werden, der Lesepuffer kann überlaufen
(Buffer Overflow)
– besonders konstruierte Eingabedaten können das
Programm gezielt verändern
– besonders gefährdet sind Programme, die Eingaben aus
dem Internet verarbeiten (z.B. HTTP-Server)
• Angreifer können den Fehler ausnutzen, um beliebigen
Programmcode auf dem Zielrechner auszuführen (sog.
Internet-Würmer führen solche Attacken automatisiert
durch (Bsp: Code Red))
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
14
Zeiger in C und C++
Pufferüberlauf - Buffer Overflow
• Beispiel für ein Programm mit einem Buffer OverflowFehler (kopiert zeilenweise Eingabe nach Ausgabe):
#include <stdio.h>
void main()
{
char buf[1000]; //no line is longer
while(gets(buf)!=NULL) //read line
{
puts(buf); //write line
}
}
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
15
Zeiger in C und C++
Pufferüberlauf - Buffer Overflow
• Wie kann ein Angreifer beliebigen Programmcode
ausführen?
– Stack nach dem Aufrufen der main-Funktion:
buf[0]
buf[1]
buf[999]
Rücksprungadresse
niedrige Adressen
(Der Stack wächst auf x86Systemen in Richtung niedrige
Adressen.)
hohe Adressen
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
16
Zeiger in C und C++
Pufferüberlauf - Buffer Overflow
– Der Angreifer erzeugt eine Textzeile, die länger als 1000
Zeichen ist.
– Dadurch wird die Rücksprungadresse überschrieben
und das Programm stürzt beim Verlassen der mainFunktion ab (im günstigsten Fall).
– Die Kunst des Angreifers besteht darin, die Rücksprungadresse so zu überschreiben, das der neue Wert in den
Puffer zeigt. Dort kann beliebiger Programmcode stehen
(der vorher als Textzeile eingelesen wurde).
• Deshalb: Niemals irgendwelche Annahmen über fremde
Eingabedaten machen, ohne sie nachzuprüfen!
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
17
Zeiger in C und C++
Pufferüberlauf - Buffer Overflow
• Beispiel für einen speziell konstruierten HTTP-Request-String
(aus dem Logfile eines Webservers, erzeugt vom Wurm Code
Red II, infizierte weltweit ca. 400000 MS Internet Information
Server innerhalb weniger Stunden):
GET /default.ida?XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX%u9090%
u6858%ucbd3%u7801%u9090%u6858%ucbd3%u7801%u9090%
u6858%ucbd3%u7801%u9090%u9090%u8190%u00c3%u0003%
u8b00%u531b%u53ff%u0078%u0000%u00=a
nur zum Füllen des Puffers
Datenstrukturen und Algorithmen WS2004/05
eingeschleuster Code
Universität Konstanz
18
Zeiger in C und C++
Strings
• Strings werden in C durch char-Arrays dargestellt, das
Ende des Strings wird durch ein Element mit dem Wert 0
markiert (Strings in C sind nullterminiert).
• Es können String-Konstanten verwendet werden (Notation
wie in Java), der Compiler erzeugt dann selbst ein
entsprechendes char-Array.
• Beispiel:
char *s1="Text";
char s2[5];
s2[0]='T'; s2[1]='e'; s2[2]='x';
s2[3]='t'; s2[4]=0;
s1 und s2 zeigen beide auf
einen String "Text"
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
19
Zeiger in C und C++
Zeigerarithmetik
• In Java: Referenzen (Zeiger) sind fest und können nur
durch Zuweisen einer anderen Referenz verändert werden.
• In C/C++: Zu Zeigern können Werte addiert und subtrahiert werden (Zeigerarithmetik), dadurch ist z.B. das
schrittweise Schieben eines Zeigers über ein Array
möglich.
• Addieren eines Wertes zu einem Zeiger erhöht den Zeiger
um diesen Wert multipliziert mit der Größe des Typs, auf
den der Zeiger zeigt. (Subtraktion funktioniert analog)
• Mit Zeigern vom Typ void* ist keine Zeigerarithmetik
möglich (void hat keine Größe).
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
20
Zeiger in C und C++
Zeigerarithmetik
• Beispiel: Füllen eines Arrays mit 0
Ohne Zeigerarithmetik:
#define size 100;
int data[size];
int i;
for(i=0;i!=size;i++)
data[i]=0;
Mit Zeigerarithmetik:
#define size 100;
int data[size];
int *i;
for(i=data;i!=(data+size);i++)
*i=0;
Datenstrukturen und Algorithmen WS2004/05
21
Universität Konstanz
Zeiger in C und C++
Referenzen in C++
• Nur in C++: Referenzen sind ein weiterer Zeigertyp, der
nur für Funktionsparameter verwendet werden kann.
• Referenzen sind Zeiger, werden aber automatisch
dereferenziert (ähnlich wie in Java) und die Adresse kann
nicht verändert werden.
• Deklaration einer Referenz: wie Zeiger, aber & statt *
• Beispiel:
void setIntToOne(int &i)
{
i=1;
}
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
22
Zeiger in C und C++
Referenzen in C++
• Dasselbe Beispiel mit normalen Zeigern statt Referenzen:
void setIntToOne(int *i)
{
*i=1;
}
• Nach Möglichkeit sollte man Referenzen den Zeigern
vorziehen, da der Compiler sie besser optimieren kann
(kein Aliasing möglich).
Datenstrukturen und Algorithmen WS2004/05
23
Universität Konstanz
Zeiger in C und C++
Zusammenfassung
• Zeiger sind ein mächtiges Werkzeug in C und C++ und
werden oft benötigt, aber:
– Fehlerhafter Umgang mit Zeigern ist die wahrscheinlich
häufigste Ursache für Fehler in C/C++-Programmen.
– Oft vorkommende Fehler sind:
• Dereferenzierung von Zeigern, nachdem ihr Speicherbereich freigegeben (oder noch schlimmer:
schon wieder anderweitig vergeben) wurde ("wilde
Zeiger")
• Überschreitung von Arraygrenzen (Buffer Overflow)
– Sorgfältiges Arbeiten mit Zeigern ist notwendig.
Datenstrukturen und Algorithmen WS2004/05
Universität Konstanz
24