Wenn eine Zahl zu groß ist, springt sie dann zum nächsten Speicherplatz?

30

Ich habe die C-Programmierung überprüft und es gibt nur ein paar Dinge, die mich stören.

Nehmen wir diesen Code zum Beispiel:

int myArray[5] = {1, 2, 2147483648, 4, 5};
int* ptr = myArray;
int i;
for(i=0; i<5; i++, ptr++)
    printf("\n Element %d holds %d at address %p", i, myArray[i], ptr);

Ich weiß, dass ein int einen Maximalwert von positiv 2.147.483.647 halten kann. Geht man darüber hinweg, "geht es über" zur nächsten Speicheradresse, wodurch Element 2 an dieser Adresse als "-2147483648" angezeigt wird? Aber dann ist das nicht wirklich sinnvoll, da in der Ausgabe immer noch steht, dass die nächste Adresse den Wert 4, dann 5 enthält. Wenn die Zahl auf die nächste Adresse übergegangen wäre, würde das den an dieser Adresse gespeicherten Wert nicht ändern ?

Ich erinnere mich vage daran, wie ich in MIPS Assembly programmiert habe und beobachtet habe, wie sich die Werte der Adressen während des Programms schrittweise geändert haben, dass sich die diesen Adressen zugewiesenen Werte ändern würden.

Wenn ich mich nicht falsch erinnere, ist hier eine andere Frage: Wenn die einer bestimmten Adresse zugewiesene Nummer größer ist als der Typ (wie in myArray [2]), hat dies dann keinen Einfluss auf die unter der nachfolgenden Adresse gespeicherten Werte?

Beispiel: Wir haben int myNum = 4 Milliarden an der Adresse 0x10010000. Natürlich kann myNum keine 4 Milliarden speichern, daher erscheint es als negative Zahl an dieser Adresse. Obwohl diese große Zahl nicht gespeichert werden kann, hat dies keine Auswirkungen auf den unter der nachfolgenden Adresse 0x10010004 gespeicherten Wert. Richtig?

Die Speicheradressen haben gerade genug Platz, um bestimmte Zahlen- / Zeichengrößen aufzunehmen, und wenn die Größe den Grenzwert überschreitet, wird sie anders dargestellt (wie der Versuch, 4 Milliarden in das int zu speichern, aber es wird als negative Zahl angezeigt) und Daher hat dies keine Auswirkung auf die unter der nächsten Adresse gespeicherten Zahlen / Zeichen.

Entschuldigung, wenn ich über Bord gegangen bin. Ich habe den ganzen Tag einen großen Hirnfurz davon.

gedrungen
quelle
10
Möglicherweise werden Sie mit Überläufen von Zeichenfolgen verwechselt .
Robbie Dee
19
Hausaufgaben: Ändern eine einfache CPU , so dass es funktioniert verschütten. Sie werden sehen, dass die Logik weitaus komplexer wird, und zwar für ein "Feature", das überall Sicherheitslücken garantiert, ohne überhaupt nützlich zu sein.
Phihag
4
Wenn Sie sehr große Zahlen benötigen, können Sie eine Zahlendarstellung verwenden, die den für große Zahlen verwendeten Speicher erhöht. Der Prozessor selbst kann dies nicht, und es ist kein Merkmal der C-Sprache, aber eine Bibliothek kann es implementieren - eine übliche C-Bibliothek ist die GNU Multiple Precision Arithmetic Library . Die Bibliothek muss den Speicher verwalten, um die Zahlen zu speichern, für die zusätzlich zur Arithmetik Performancekosten anfallen. In vielen Sprachen ist so etwas eingebaut (was die Kosten nicht vermeidet).
Steve314
1
Schreiben Sie einen einfachen Test, ich bin kein C-Programmierer, sondern etwas in der Art von int c = INT.MAXINT; c+=1;und sehen, was mit c passiert ist.
JonH
2
@JonH: Das Problem ist der Überlauf in Undefined Behavior. Der AC-Compiler erkennt diesen Code möglicherweise und leitet daraus ab, dass er nicht erreichbar ist, da er bedingungslos überläuft. Da nicht erreichbarer Code keine Rolle spielt, kann er beseitigt werden. Endergebnis: kein Code mehr.
MSalters

Antworten:

48

Nein, tut es nicht. In C haben Variablen einen festen Satz von Speicheradressen, mit denen gearbeitet werden kann. Wenn Sie auf einem System mit 4 Byte arbeiten intsund eine intVariable auf setzen 2,147,483,647und dann hinzufügen 1, enthält die Variable normalerweise -2147483648. (Auf den meisten Systemen. Das Verhalten ist tatsächlich undefiniert.) Andere Speicherorte werden nicht geändert.

Im Wesentlichen lässt der Compiler nicht zu, dass Sie einen Wert zuweisen, der für den Typ zu groß ist. Dies erzeugt einen Compilerfehler. Wenn Sie es mit einer Groß- / Kleinschreibung erzwingen, wird der Wert abgeschnitten.

Bitweise betrachtet, wenn der Typ nur 8 Bits speichern kann und Sie versuchen, den Wert 1010101010101mit einer Groß- / Kleinschreibung in ihn zu zwingen , erhalten Sie die unteren 8 Bits, oder 01010101.

Unabhängig davon, was Sie in Ihrem Beispiel tun myArray[2], myArray[3]wird "4" enthalten. Es gibt kein "Überlaufen". Sie versuchen, etwas zu schreiben, das mehr als 4 Bytes umfasst. Dadurch wird alles auf der oberen Ebene abgeschwächt, und die unteren 4 Bytes bleiben erhalten. Auf den meisten Systemen führt dies zu -2147483648.

Aus praktischer Sicht möchten Sie nur sicherstellen, dass dies niemals und niemals geschieht. Diese Art von Überläufen führt häufig zu schwer zu lösenden Fehlern. Mit anderen Worten, wenn Sie der Meinung sind, dass es eine Chance gibt, dass alle Ihre Werte in Milliardenhöhe liegen, verwenden Sie sie nicht int.

Gort den Roboter
quelle
52
Wenn Sie auf einem System mit 4-Byte-Ints arbeiten und eine int-Variable auf 2.147.483.647 setzen und dann 1 hinzufügen, enthält die Variable -2147483648. => Nein , es handelt sich um ein undefiniertes Verhalten. Es kann sich also um eine Schleife handeln oder etwas ganz anderes tun. Ich habe gesehen, wie Compiler Prüfungen auf der Grundlage des Fehlens eines Überlaufs optimierten und zum Beispiel Endlosschleifen erhielten ...
Matthieu M.
Entschuldigung, ja, Sie haben Recht. Ich hätte dort ein "normalerweise" hinzufügen sollen.
Gort the Robot
@MatthieuM aus sprachlicher Sicht, das stimmt. In Bezug auf die Ausführung auf einem bestimmten System, worüber wir hier sprechen, ist es absoluter Unsinn.
Hobbs
@hobbs: Das Problem ist, dass das Ausführen des Programms tatsächlich ein unerwartetes Verhalten erzeugt, das mit dem Überschreiben des Speichers vergleichbar ist, wenn die Compiler das Programm aufgrund eines undefinierten Verhaltens beschädigen.
Matthieu M.
24

Überlauf von Ganzzahlen mit Vorzeichen ist undefiniertes Verhalten. In diesem Fall ist Ihr Programm ungültig. Der Compiler muss dies nicht für Sie überprüfen, sodass möglicherweise eine ausführbare Datei erstellt wird, die anscheinend etwas Vernünftiges bewirkt. Es gibt jedoch keine Garantie dafür, dass dies der Fall ist.

Ein vorzeichenloser Ganzzahlüberlauf ist jedoch genau definiert. Es wird modulo UINT_MAX + 1 umbrechen. Der von Ihrer Variablen nicht belegte Speicher wird nicht beeinflusst.

Siehe auch https://stackoverflow.com/q/18195715/951890

Vaughn Cato
quelle
Überlauf von Ganzzahlen mit Vorzeichen ist genauso gut definiert wie Überlauf von Ganzzahlen ohne Vorzeichen. Wenn das Wort $ N $ Bits hat, liegt die Obergrenze des vorzeichenbehafteten Integer-Überlaufs bei $$ 2 ^ {N-1} -1 $$ (wobei ein Umlauf auf $ -2 ^ {N-1} $ erfolgt) Die obere Grenze für den vorzeichenlosen Integer-Überlauf liegt bei $$ 2 ^ N - 1 $$ (wobei der Umlauf auf $ 0 $ erfolgt). Gleiche Mechanismen für Addition und Subtraktion, gleiche Größe des darstellbaren Zahlenbereichs ($ 2 ^ N $). nur eine andere Grenze des Überlaufs.
Robert Bristow-Johnson
1
@ Robertbristow-Johnson: Nicht nach dem C-Standard.
Vaughn Cato
Nun, Standards sind manchmal anachronistisch. Wenn man sich die SO-Referenz ansieht, gibt es einen Kommentar, der sie direkt trifft: "Der wichtige Hinweis hier ist jedoch, dass es in der modernen Welt keine Architektur mehr gibt, die etwas anderes als die von 2 mit Komplement-Vorzeichen versehene Arithmetik verwendet. Dass die Sprachstandards immer noch eine Implementierung ermöglichen beispielsweise auf einem PDP-1 ist ein rein historisches Artefakt -. Andy Ross 12. August '13 um 20:12" Uhr
Robert Bristow-Johnson
Ich nehme an, es ist nicht im C-Standard, aber ich nehme an, es könnte eine Implementierung geben, bei der keine reguläre binäre Arithmetik verwendet wird int. Ich nehme an, sie könnten Gray-Code oder BCD oder EBCDIC verwenden . Keine Ahnung, warum irgendjemand Hardware für das Rechnen mit Gray-Code oder EBCDIC entwerfen würde, aber ich weiß auch nicht, warum irgendjemand unsignedmit Binärdaten arbeiten und intmit etwas anderem als dem 2er-Komplement signieren würde .
Robert Bristow-Johnson
14

Hier gibt es also zwei Dinge:

  • das Sprachniveau: Was sind die Semantiken von C
  • die Maschinenebene: Was ist die Semantik der Baugruppe / CPU, die Sie verwenden

Auf der Sprachebene:

In C:

  • Überlauf und Unterlauf sind als Modulo-Arithmetik für vorzeichenlose ganze Zahlen definiert, daher ihr Wert "Schleifen".
  • Überlauf und Unterlauf sind undefiniertes Verhalten für vorzeichenbehaftete Ganzzahlen, daher kann alles passieren

Für diejenigen, die ein "was auch immer" -Beispiel wollen, habe ich gesehen:

for (int i = 0; i >= 0; i++) {
    ...
}

einbiegen in:

for (int i = 0; true; i++) {
    ...
}

und ja, das ist eine legitime Transformation.

Dies bedeutet, dass aufgrund einer seltsamen Compilertransformation tatsächlich das Risiko besteht, dass beim Überlauf Speicher überschrieben wird.

Hinweis: Verwenden Sie auf Clang oder gcc -fsanitize=undefinedin Debug, um den Undefined Behavior Sanitizer zu aktivieren, der bei Unterlauf / Überlauf von Ganzzahlen mit Vorzeichen abgebrochen wird.

Oder Sie können den Speicher überschreiben, indem Sie das Ergebnis der Operation verwenden, um ein Array zu indizieren (nicht markiert). Dies ist bei fehlender Unterlauf- / Überlauferkennung leider weitaus wahrscheinlicher.

Hinweis: Verwenden Sie auf Clang oder gcc -fsanitize=addressin Debug, um den Address Sanitizer zu aktivieren, der bei einem Zugriff außerhalb der Grenzen abgebrochen wird.


Auf Maschinenebene :

Es hängt wirklich von der Montageanleitung und der verwendeten CPU ab:

  • Bei x86 verwendet ADD bei Überlauf / Unterlauf das 2-Komplement und setzt das OF (Overflow Flag).
  • Auf der zukünftigen Mill-CPU gibt es 4 verschiedene Überlaufmodi für Add:
    • Modulo: 2-Komplement-Modulo
    • Trap: Es wird ein Trap generiert, der die Berechnung anhält
    • Saturate: Wert bleibt bei Unterlauf auf min oder bei Überlauf auf max stehen
    • Doppelte Breite: Das Ergebnis wird in einem Register mit doppelter Breite generiert

Beachten Sie, dass die CPU bei einem Überlauf keinen Speicher überschreibt, egal ob in Registern oder im Speicher.

Matthieu M.
quelle
Sind die letzten drei Modi signiert? (Ist für den ersten nicht von Bedeutung, da es sich um ein 2-Komplement handelt.)
Deduplizierer,
1
@Deduplicator: Gemäß Einführung in das Mill-CPU-Programmiermodell gibt es verschiedene Opcodes für vorzeichenbehaftete und vorzeichenlose Additionen. Ich gehe davon aus, dass beide Opcodes die 4 Modi unterstützen (und mit verschiedenen Bitbreiten und Skalaren / Vektoren arbeiten können). Andererseits ist es vorerst Vapor Hardware;)
Matthieu M.
4

Der Grund für die weitere Antwort von @ StevenBurnap liegt in der Funktionsweise von Computern auf Maschinenebene.

Ihr Array ist im Speicher abgelegt (zB im RAM). Wenn eine arithmetische Operation ausgeführt wird, wird der Wert im Speicher in die Eingangsregister der Schaltung kopiert, die die Arithmetik ausführt (die ALU: Arithmetic Logic Unit ), und die Operation wird dann an den Daten in den Eingangsregistern ausgeführt, wobei ein Ergebnis erzeugt wird im Ausgangsregister. Dieses Ergebnis wird dann an der richtigen Speicheradresse in den Speicher zurückkopiert, wobei andere Speicherbereiche unberührt bleiben.

Pharap
quelle
4

Als Erstes (unter der Annahme des C99-Standards) möchten Sie möglicherweise den <stdint.h>Standardheader einbeziehen und einige der dort definierten Typen verwenden, insbesondere int32_teine 32-Bit-Ganzzahl mit Vorzeichen oder uint64_teine 64-Bit-Ganzzahl ohne Vorzeichen. Möglicherweise möchten Sie Typen wie int_fast16_taus Leistungsgründen verwenden.

Lesen Sie die Antworten anderer, in denen erklärt wird, dass vorzeichenlose Arithmetik niemals an benachbarte Speicherstellen gelangt (oder überläuft). Vorsicht vor undefiniertem Verhalten bei signiertem Überlauf.

Wenn Sie dann genau riesige Ganzzahlen berechnen müssen (z. B. eine Fakultät von 1000 mit allen 2568 Dezimalstellen), möchten Sie Bigints, also willkürliche Präzisionszahlen (oder Bignums). Algorithmen für eine effiziente Bigint-Arithmetik sind sehr clever und erfordern normalerweise die Verwendung spezieller Maschinenbefehle (z. B. einige Add-Words mit Übertrag, falls Ihr Prozessor über diese verfügt). Daher empfehle ich in diesem Fall dringend, eine vorhandene Bigint-Bibliothek wie GMPlib zu verwenden

Basile Starynkevitch
quelle