Hallo liebes Forum,
ich habe wieder eine Frage bezüglich der C-Programmierung. Zurzeit sind
wir in der Techniker Schule beim Thema Arrays und bearbeiten diverse
Aufgaben.
Zuletzt war die Aufgabe eine Passworteingabe in der Konsole zu
realisieren, wo man die Zeichen mittels * verstecken sollte. Eig. eine
einfache Aufgabe wenn man das \b kennt.
Bei der Aufgaben Besprechung fiel mir auf, dass keinerlei Abfrage drinne
war, in der man nach rechtzeitig ein beschreiben eines Arrays außerhalb
seiner Größe verhinderte.
So habe ich dies angesprochen und gesagt wenn man nun mehr Zeichen
eingibt als das Array groß ist, dass dies zu einer Speicherverletzung
führt und das Programm abstürzen würde. Ich war dann ziemlich erstaunt
als eben letzteres nicht eingetreten ist, als der Lehrer dies dann in
seinem Musterprogramm tat. Was mir dann ziemlich peinlich war ;)
Plattform: Windows und IDE Dev CPP mit GCC.
Meine Lösung sah so aus:
1
// Nun lese das Passwort ein
2
printf("\nBitte geben Sie nun das Passwort ein:\n");
3
4
while((key=getch())!=13)
5
{
6
// Backspace verarbeiten
7
if(key==8&&x>0)
8
{
9
passwort[x]=0;
10
x--;
11
printf("\b\b");
12
}
13
// Zeichen verarbeiten
14
elseif(key!=8&&x<=size-2)
15
{
16
passwort[x]=key;
17
x++;
18
printf("*");
19
}
20
// Wurden mehr als 18 Zeichen eingegeben? Dann breche ab
21
elseif(x>size-2)
22
{
23
break;
24
}
25
// Wenn noch nix eingegeben wurde und Backspace gedrückt wurde.
26
else
27
{
28
continue;
29
}
30
}
31
32
daten.passwort[x]='\0';
Nun danach hab ich mich damit nochmal beschäftigt und so Späßchen
probiert:
1
chararray[10];
2
array[14]='A';
3
printf("%c",array[14]);
Sowas wurde anstandslos kompiliert und ohne Fehler / Warnungen. Und
seltsamerweise hat das auch noch funktioniert?!
Habe ich irgendwas verpasst oder optimiert der Compiler sowas
automatisch????
Bisher dachte ich immer das wäre ein Buffer Overflow und muss
aufjedenfall verhindert und abgefangen werden.
Hat jemand eine Idee?
Liebe Grüße
Müller
Müller schrieb:>> Nun danach hab ich mich damit nochmal beschäftigt und so Späßchen> probiert:>
1
chararray[10];
2
array[14]='A';
3
printf("%c",array[14]);
>> Sowas wurde anstandslos kompiliert und ohne Fehler / Warnungen. Und> seltsamerweise hat das auch noch funktioniert?!
Dieses Array liegt auf dem Stack, auf dem Stack wird blockweise drauf
gepackt, wenn eine Funktion aufgerufen wird, oder gelöscht, wenn diese
Verlassen wird.
Auf einem 64bit System wird ziemlich sicher auf 8 byte gerundet, dein
10er block wird somit warscheinlich schon mal auf 16byte aufgerundet.
Und mit 14 bis du in dieser Limite drin.
Google mal nach "stack canary", mit denen kann ein Buffer Overflow
bedingt verhindert werden.
Und führe doch dein Experiment mal noch etwas weiter:
1
chararray0[8];
2
chararray[8];
3
chararray1[8];
4
array[14]='A';
5
printf("%c",array[14]);
Was passiert wohl mit array0 und array1, wenn du eine Überschreitung in
array machst?
[edit: Zitat Einrückung im Code entfernt]
mfg Andreas
Müller schrieb:> So habe ich dies angesprochen und gesagt wenn man nun mehr Zeichen> eingibt als das Array groß ist, dass dies zu einer Speicherverletzung> führt und das Programm abstürzen würde
Wenn das so wäre, wäre das ja alles einfach. Ist zB in Java oder .NET
Sprachen so, da stürzt das Programm "sicher" ab in so einem Fall. Bei
C(++) passiert bei einem solchen Buffer-Overflow halt einfach irgend
etwas, wie zB das Überschreiben von anderen Variablen, der
Rücksprung-Adresse, das Einschleusen von fremdem Code (der Grund für
sehr viele Sicherheitslücken) usw. Die C(++) -Standards sagen einfach,
das Verhalten ist "undefiniert". Deswegen ist es ja so wichtig,
korrekten Code zu schreiben, weil man bei solchen Fehlern keinen
"sicheren" Absturz garantieren kann. Unter Linux hilft zB das Programm
"valgrind" beim Finden solcher Fehler.
Müller schrieb:> Bisher dachte ich immer das wäre ein Buffer Overflow und muss> aufjedenfall verhindert und abgefangen werden.
Genau das passiert nicht.
In C werden keine Arraygrenzen überprüft.
Und das ist die große Gefahr bei C.
Es kann immer sein, dass Code der nicht richtig ist, zufällig
funktioniert.
Du darfst davon keine Rückschlüsse auf andere Compiler/Systeme ziehen.
Dabei sollte man bedenken, das C von Profis für Profis (die Autoren)
gemacht wurde.
Also für Leute, die wissen was sie tun.
C ist eine relativ einfache Sprache. Das heißt aber nicht, dass C
einfach zu programmieren ist.
Das Beschreiben eines Feldes außerhalb der definierten Grenzen führt zu
einem undefinierten Verhalten und undefiniertes Verhalten heißt, dass
alles möglich ist, auch dass das Programm (zumindest zunächst) so
funktioniert, wie erwartet. Wenn Dein Lehrer Deinen Einwand in der Tat
damit abgewiesen hat, dass es doch funktioniert, so sollte ihm das
peinlich sein und nicht Dir, denn er sollte wissen, dass man durch Tests
nur die Anwesenheit von Fehlern beweisen kann, aber niemals aber deren
Abwesenheit. Dein Einwand war also (erst einmal) völlig richtig.
(Vielleicht sollte das ja aber auch pädagogischer Trick um gezielt Dein
Vertrauen in Tests zu erschüttern. Ob das zielführend ist, sei einmal
dahin gestellt.)
Dein Irrtum liegt allerdings darin, dass es sehr wohl Abfragen zur
Bereichsüberschreitung gibt. C prüft Bereichsüberschreitungen in der Tat
nicht selbst, sondern überlässt das dem Programmierer. Das ist so
gewollt. Die Ausdrücke
1
x>0
und
1
x<=size-2
sind aber genau das, Abfragen zur Bereichsprüfung.
Wer Trojaner und Viren schreibt, nutzt solche Fehler aus.
Auf dem Stack sind Daten und Adressen für das return gemischt. So kann
ein zu langes Passwort die Return-Adresse überschreiben. Das return
springt dann an eine Stelle, die den Trojaner installiert.
Beim nächsten Mal nichts sagen - besser dem Dozenten einen Virus
unterschieben, der ihn auf seinen Fehler hinweist.
Hallo zusammen,
vielen Dank für das Feedback und die Aufklärung. Das da Bereiche mit
definierter Größe auf dem Stack reserviert werden, wusste ich nicht. Ist
das nicht unnütze "Speicherverschwendung"? Wenn ich 6 Bytes haben will
und der reserviert 8 Bytes? Okay 2 Bytes sind jetzt nicht die Welt, aber
wenn man 10000 Variablen hat? Oder denkt man sich: "Och joah das
Speichermanagment des OS regelt dat schoon..."
Wenn man Speicher dynamisch reserviert, wird ja auch nur soviel
reserviert, wie ich gerne hätte.
Der Code Snipsel stammte von mir, die Musterlösung müsste ich nochmal in
meinen Unterlagen nachschauen.
Dazu noch eine Frage, warum "programmiert" man nicht einen Compiler der
sowas standardmäßig prüft in C? Zumindestens bei so offentsichtlichen
Sachen, wie in meinen zweiten Snipsel?
Lieben Gruß
Müller
Müller schrieb:> Das da Bereiche mit> definierter Größe auf dem Stack reserviert werden, wusste ich nicht. Ist> das nicht unnütze "Speicherverschwendung"?
nein, denn wenn es mehr variabel werden kommen sie in den gleichen
Block.
Je kleiner die Blöcke werden, desto langsamer ist die Speicherverwaltung
weil viel mehr Blöcke verwaltet werden müssen.
>warum "programmiert" man nicht einen Compiler der>sowas standardmäßig prüft in C?
Weil das Programm grösser und langsamer wird. Gibt unterschiedliche
Programmiersprachen mit unterschiedlichen Prioritäten. Bei C ist halt
absolut oberste Priorität - das Programm soll klein und schnell sein.
Wer die Überprüfungen auf Programmiererfehler in seinem Programm haben
will, benutzt andere Programmiersprachen.
Gibt Tools für statische Prüfungen, wie das alte lint. Die finden aber
nur wenige Fehler.
Gibt auch Tools, die zur Laufzeit prüfen. Wie der oben bereits genannte
"stack canary". Wird aber nur zum Test benutzt, im fertigen Programm
wieder ausgebaut.
Noch einer schrieb:> Weil das Programm grösser und langsamer wird.
der Compiler soll den Fehler melden, das ändert also nichts am Programm.
Mich wundert auch das der GCC nicht mal eine Warnung ausgibt.
Müller schrieb:> Dazu noch eine Frage, warum "programmiert" man nicht einen Compiler der> sowas standardmäßig prüft in C? Zumindestens bei so offentsichtlichen> Sachen, wie in meinen zweiten Snipsel?
Wenn der Index aus einer Variablen kommt oder der Zugriff in einer
Funktion stattfindet, geht das alles nicht mehr.
Aber es gibt ja "C"-Compiler die das machen. Nennen sich dann
C++-Compiler und der Datentyp ist Vector.
Müller schrieb:> Hallo zusammen,>> [...]>> Wenn man Speicher dynamisch reserviert, wird ja auch nur soviel> reserviert, wie ich gerne hätte.
Das trifft so nicht grundsätzlich zu. Tatsächlich gibt es
Implementierungen die auch Speicher, der mit malloc allokiert wird, in
Blöcken reservieren.
> Der Code Snipsel stammte von mir, die Musterlösung müsste ich nochmal in> meinen Unterlagen nachschauen.
Hinweis: Das heisst: "Schnippsel" und kommt von "schneiden".
> Dazu noch eine Frage, warum "programmiert" man nicht einen Compiler der> sowas standardmäßig prüft in C? Zumindestens bei so offentsichtlichen> Sachen, wie in meinen zweiten Snipsel?
Es kommt halt darauf an, was man unter einem Compiler versteht. Der
primäre Zweck ist die "Übersetzung", nicht die Fehlerprüfung. Die Grenze
ist allerdings oft nicht ganz scharf. Sie verläuft aber ungefähr dort,
wo der Compiler eine Information über den Code "ohnehin" hat, d.h. wenn
er zu ihrer Ermittlung keine weiteren Analysen machen muss. Ein Beispiel
dafür sind Typinformationen. An sich bräuchte der Compiler dafür nur
Code erzeugen, der eben zu dem angegebenen Typ passt. Da er aber bei
Parametern den Typ des übergebenen Wertes weiß und den Typ, den die
Funktion erwartet, vergleicht er die beiden. Er muss das im Falle von
impliziten casts ohnehin tun.
Es sei zugegeben, dass Dein Beispiel relativ einfach zu implementieren
wäre. Das geht nur eben über den Zweck eines Compilers hinaus.
Wenn Du tatsächlich statische Codeanalysen durchführen willst, gibt es
dafür Werkzeuge wie "Lint".
Hallo zusammen,
Vielen Dank für die Antworten. Aber ich meine der GCC z.B. Hat doch für
jeden "Scheiß" eine Compiler Option, warum dann nicht bei so
offensichtlichen Sachen, sowas auch implementieren.
Mir ist schon bewusst, dass man das nicht komplett prüfen kann, Vorallem
wenn etwas dynamisches im Spiel ist.
Aber wenn ich mir ansehe, dass (ok dass war auf einem MacBook von einem
Kollegen) dass er sogar eine Warnung bei gets rausgibt, denke ich mir
nur, warum so flegmatisch sein ;-)
Das mit dem Snipsel hab ich mir wohl aus SNIppet und schniPSEL gebildet,
es lebe die Denglisierung ;-)
Müller schrieb:> Sowas wurde anstandslos kompiliert und ohne Fehler / Warnungen. Und> seltsamerweise hat das auch noch funktioniert?!>> Habe ich irgendwas verpasst oder optimiert der Compiler sowas> automatisch????
Wie bereits gesagt, es ist Undefined Behavior.
C99 §3.4.3 Terms, Definitions and Symbols: Undefined Behavior
1
Behavior, upon use of a nonportable or erroneous program construct or
2
of erroneous data, for which this International Standard imposes no
3
requirements.
4
5
Note: Possible undefined behavior ranges from ignoring the situation
6
completely with unpredictable results, to behaving during translation
7
or program execution in a documented manner characteristic of the
8
environment (with or without the issuance of a diagnostic message),
9
to terminating a translation or execution (with the issuance of a
10
diagnostic message).
11
12
Example: An example of undefined behavior is the behavior on integer
13
overflow.
> Bisher dachte ich immer das wäre ein Buffer Overflow und muss> aufjedenfall verhindert und abgefangen werden.
Falsch gedacht :-)
> Hat jemand eine Idee?
Das Konzept ist "Garbage in, garbage out": Müll rein, Müll raus. In
C(++) ist der Anwender für die Integrität und Konsistenz der Daten
verantwortlich.
Das ermöglicht schnelle Programme für die Leute, die korrekte Programme
schreiben auf Kosten der wenigen, die inkorrekte Programme schreiben.
Alternative wäre, ALLE mit einem Overhead zu bestrafen, der nötig wäre
wenn versucht würde, die Konsistenz zur Laufzeit überprüfen zu wollen.
Beispiel:
1
intget_int(int*p)
2
{
3
return*p;
4
}
Wie willst du überprüfen, ob p in einen gültigen Speicherbereich zeigt,
ob das Alignment stimmt (was vom Speicherbereich abhängen kann), die
Zugriffsrechte, ob da wirklich ein int (oder was kompatibles)
gespeichert ist, usw.?
Es gibt zwar Tools wie ubsan, asan, valgrind etc. die dabei helfen, zur
Entwicklungszeit Fehler aufzudecken, aber das ist weder ein Nachweis der
Korrektheit, noch würde man den Overhead im Endprodukt tolerieren.
> Nun danach hab ich mich damit nochmal beschäftigt und so Späßchen> probiert:
1
#include<stdio.h>
2
3
voidfunc(void)
4
{
5
chararray[10];
6
array[14]='A';
7
printf("%c",array[14]);
8
}
Ein avr-gcc v5 macht daraus
1
func:
2
ldi r24,lo8(65)
3
ldi r25,0
4
rjmp putchar
IIRC werden Warnungen für -Warray-bounds relativ spät in der
Compilierung ausgegeben. Grund ist, dass Indices fast nie zur
Compilezeit bekannt sind und durch Optimierung mehr potentielle Stellen
gefunden werden können.
Warum im Beispiel keine Warnung ausgegeben wird, da musst du nen
GCC-Entwickler fragen... Ohne Optimierung wird jedenfalls auch keine
Warnung ausgegeben obwohl außerhalb von array[] zugegriffen wird.
Müller schrieb:> Hallo zusammen,>> Vielen Dank für die Antworten. Aber ich meine der GCC z.B. Hat doch für> jeden "Scheiß" eine Compiler Option, warum dann nicht bei so> offensichtlichen Sachen, sowas auch implementieren.
Das habe ich Dir beantwortet. Was genau hast Du daran nicht verstanden?
Du verlangst ja auch keinen Dosenöffner im PKW nur weil Du Dir mal aufm
Rastplatz Ravioli warm machen möchtest. Fehlersuche ist eben keine
Aufgabe eines Compilers.
> Mir ist schon bewusst, dass man das nicht komplett prüfen kann, Vorallem> wenn etwas dynamisches im Spiel ist.
Darüber habe ich keine Aussage gemacht. Ich habe vielmehr verdeutlichen
wollen, dass der Compiler gewisse Fehler meldet, weil er die dazu
relevante Information ohnehin zur Codeerzeugung benötigt. Bei einem
Arrayzugriff braucht er zur Codeerzeugung nur die Startadresse und einen
Offset. Wie groß der Offset ist, d.h. ob effektiv diesseits der Größe
zugreift, ist für den erzeugten Code irrelevant. Das Prinzip ist:
"Soviel wie nötig" und nicht: "Alles was denkbar ist". In der Technik
ohnehin die generelle Maxime. Das letztere ist nur eine
Marketing-Masche.
> Aber wenn ich mir ansehe, dass (ok dass war auf einem MacBook von einem> Kollegen) dass er sogar eine Warnung bei gets rausgibt, denke ich mir> nur, warum so flegmatisch sein ;-)
Was ist das für eine Warnung und bei welchem Code genau? So kann ich
dazu nichts sagen und weiß auch nicht, ob der Fall vergleichbar ist.
> Das mit dem Snipsel hab ich mir wohl aus SNIppet und schniPSEL gebildet,> es lebe die Denglisierung ;-)
Tot wäre sie mir lieber. :-)
Müller schrieb:> Aber wenn ich mir ansehe, dass (ok dass war auf einem MacBook von einem> Kollegen) dass er sogar eine Warnung bei gets rausgibt, denke ich mir> nur, warum so flegmatisch sein ;-)
Das ist doch etwas anderes.
Das eine ist eine Warnung vor einer (abgekündigten) Funktion,
beim Anderen muss der Code analysiert werden.
>warum dann nicht bei so>offensichtlichen Sachen, sowas auch implementieren
Da hilft ein Blick in die Geschichtsbücher. Kernighan & Ritchie wollten
einen Compiler der alle Assemblertricks akzeptiert. Warnungen fanden die
überflüssig.
Später schrieben andere Leute zusätzliche Tools, die Warnungen für
gefährliche Konstruktionen ausgaben.
Heute sind die meisten C-Programmierer der Meinung, der Compiler soll
vor allen Dingen schnell sein. Und schnellen, kompakten Code erzeugen.
Nett, wenn er nebenbei noch ein paar Warnungen ausgeben kann. Das darf
aber den Compiler nicht langsamer machen.
Wer Warnungen haben will, benutzt zusätzlich zum Compiler noch weitere
Tools. Als wir Makefiles noch selbst schreiben konnten, hatten wir da 2
Targets eingetragen. Eines, was schnell compiliert. Und eines, was alle
Warnungen ausgab.
Johann L. schrieb:> Warum im Beispiel keine Warnung ausgegeben wird, da musst du nen> GCC-Entwickler fragen... Ohne Optimierung wird jedenfalls auch keine> Warnung ausgegeben obwohl außerhalb von array[] zugegriffen wird.
Interessanterweise wird gewarnt, wenn man das Array global macht und die
Optimierungen mindestens auf Level 2 stellt.
Müller schrieb:> Bei der Aufgaben Besprechung fiel mir auf, dass keinerlei Abfrage drinne> war, in der man nach rechtzeitig ein beschreiben eines Arrays außerhalb> seiner Größe verhinderte.> Meine Lösung sah so aus:
....
> // Zeichen verarbeiten> else if(key != 8 && x <= size-2)
....
Hier hast du doch deine Abfrage drinne?
Hans-Georg L. schrieb:> Was immer wieder gerne vergessen wird .. der Index von Arrays in C ist> signed und nicht unsigned.
Ein möglicherweise negativer Index? Welchen Sinn mag dies denn nun
haben?
Ich habe das so gelernt, dass das erste Element eines Arrays in C den
Index 0 hat. Und vor dem ersten Element gibt es kein anderes.
Hans-Georg L. schrieb:> Was immer wieder gerne vergessen wird .. der Index von Arrays in C ist> signed und nicht unsigned.
Stimmt, der AVR-GCC läßt kein Array oder Struct >32767Byte zu, obwohl
man 64kB RAM adressieren kann. Man muß mehre Arrays anlegen.
Schon recht merkwürdig, diese unnötige Einschränkung.
Le X. schrieb im Beitrag #4614496:
> Was möchte uns wohl der Peter mit diesem wortlos hingerotzten Codefetzen> sagen?> Dass der Compiler das schluckt? Geschenkt.> Mark Brandis ging es eher um den Sinn hinter dieser Regel
das es sinnvoll nutzbar ist ein negativer index.
man darf auch von einen Pointer etwas subtrahieren.
1
char*p;
2
char*p2=p-5;
nur weil es bei einem Array keinen offensichtlichen Grund gibt, ist es
für Pointer immer noch notwendig.
Peter D. schrieb:> Hans-Georg L. schrieb:>> Was immer wieder gerne vergessen wird .. der Index von Arrays in C ist>> signed und nicht unsigned.>> Stimmt, der AVR-GCC läßt kein Array oder Struct >32767Byte zu, obwohl> man 64kB RAM adressieren kann. Man muß mehre Arrays anlegen.> Schon recht merkwürdig, diese unnötige Einschränkung.
Ansonsten wäre es nicht mehr möglich, Diferenzen aus Zeigern zu bilden,
denn diese sollen in einen ptrdiff_t passen, der eben nur 16 Bits groß
ist und somit nur Differenzen von -32768...32767 aufnehmen kann.
Du kannst dir natürlich auch ein eigenes ABI definieren und
implementieren wenn das momentane ABI zu restriktive oder zu "klein"
ist.
Mark B. schrieb:> Hans-Georg L. schrieb:>> Was immer wieder gerne vergessen wird .. der Index von Arrays in C ist>> signed und nicht unsigned.>> Ein möglicherweise negativer Index? Welchen Sinn mag dies denn nun> haben?>
Das ist durchaus so gewollt und wurde immer wieder gerne für
Speicherverwaltung benutzt indem man zu dem angeforderten Speicherplatz
noch einen Infoblock zusätzlich belegt und einen Pointer auf den
Speicherbereich hinter dem Infoblock zurückgegeben hat.
Dann kann die Speicherverwaltung mit einem negativen index auf den
Infoblock zugreifen und der Benutzer mit positiven Index auf die Daten.