Vorzeichenüberlauf in C / C ++ erkennen


82

Auf den ersten Blick scheint diese Frage ein Duplikat von Wie erkennt man einen Ganzzahlüberlauf? es ist jedoch tatsächlich deutlich anders.

Ich habe festgestellt, dass das Erkennen eines vorzeichenlosen Ganzzahlüberlaufs zwar ziemlich trivial ist, das Erkennen eines vorzeichenbehafteten Überlaufs in C / C ++ jedoch schwieriger ist, als die meisten Leute denken.

Der naheliegendste und doch naivste Weg, dies zu tun, wäre etwa:

int add(int lhs, int rhs)
{
 int sum = lhs + rhs;
 if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
  /* an overflow has occurred */
  abort();
 }
 return sum; 
}

Das Problem dabei ist, dass gemäß dem C-Standard ein vorzeichenbehafteter Ganzzahlüberlauf ein undefiniertes Verhalten ist. Mit anderen Worten, gemäß dem Standard ist Ihr Programm genauso ungültig, als ob Sie einen Nullzeiger dereferenziert hätten, sobald Sie überhaupt einen signierten Überlauf verursachen. Sie können also kein undefiniertes Verhalten verursachen und dann versuchen, den Überlauf nachträglich zu erkennen, wie im obigen Beispiel für die Überprüfung nach der Bedingung.

Obwohl die obige Prüfung wahrscheinlich bei vielen Compilern funktioniert, können Sie sich nicht darauf verlassen. Da der C-Standard besagt, dass ein vorzeichenbehafteter Ganzzahlüberlauf undefiniert ist, optimieren einige Compiler (wie GCC) die obige Prüfung, wenn Optimierungsflags gesetzt sind, da der Compiler davon ausgeht, dass ein vorzeichenbehafteter Überlauf unmöglich ist. Dies unterbricht den Versuch, auf Überlauf zu prüfen, vollständig.

Eine andere Möglichkeit, den Überlauf zu überprüfen, wäre:

int add(int lhs, int rhs)
{
 if (lhs >= 0 && rhs >= 0) {
  if (INT_MAX - lhs <= rhs) {
   /* overflow has occurred */
   abort();
  }
 }
 else if (lhs < 0 && rhs < 0) {
  if (lhs <= INT_MIN - rhs) {
   /* overflow has occurred */
   abort();
  }
 }

 return lhs + rhs;
}

Dies scheint vielversprechender zu sein, da wir die beiden Ganzzahlen erst dann addieren, wenn wir im Voraus sicherstellen, dass das Durchführen einer solchen Addition nicht zu einem Überlauf führt. Daher verursachen wir kein undefiniertes Verhalten.

Diese Lösung ist jedoch leider viel weniger effizient als die ursprüngliche Lösung, da Sie eine Subtraktionsoperation ausführen müssen, um zu testen, ob Ihre Additionsoperation funktioniert. Und selbst wenn Sie sich nicht für diesen (kleinen) Leistungseinbruch interessieren, bin ich immer noch nicht ganz davon überzeugt, dass diese Lösung angemessen ist. Der Ausdruck lhs <= INT_MIN - rhsscheint genau der Art von Ausdruck zu sein, die der Compiler möglicherweise optimiert, da ein signierter Überlauf unmöglich ist.

Gibt es hier also eine bessere Lösung? Etwas, das garantiert 1) kein undefiniertes Verhalten verursacht und 2) dem Compiler keine Möglichkeit bietet, Überlaufprüfungen zu optimieren? Ich dachte, es könnte eine Möglichkeit geben, dies zu tun, indem beide Operanden in vorzeichenlose Zeichen umgewandelt werden und Überprüfungen durchgeführt werden, indem Ihre eigene Zwei-Komplement-Arithmetik gewürfelt wird, aber ich bin mir nicht sicher, wie ich das tun soll.


1
Ist es nicht besser, Code zu schreiben, bei dem keine Möglichkeit eines Überlaufs besteht, als zu versuchen, ihn zu erkennen?
Arun

9
@ArunSaha: Es ist wirklich schwierig, Berechnungen durchzuführen und sicherzustellen, dass sie nicht überlaufen, und es ist im allgemeinen Fall unmöglich zu beweisen. Die übliche Praxis besteht darin, einen möglichst breiten Ganzzahltyp zu verwenden und zu hoffen.
David Thornley

6
@Amardeep: Das Dereferenzieren eines Nullzeigers ist ebenso undefiniert wie der signierte Überlauf. Undefiniertes Verhalten bedeutet, dass nach dem Standard alles passieren kann. Man kann nicht davon ausgehen, dass sich das System nach dem signierten Überlauf nicht in einem ungültigen und instabilen Zustand befindet. Das OP wies auf eine Konsequenz hin: Es ist für den Optimierer völlig legal, Code zu entfernen, der einen signierten Überlauf erkennt, sobald er auftritt.
David Thornley

16
@Amardeep: Ich habe eine solche Implementierung erwähnt. GCC entfernt den Überlaufprüfcode, wenn Optimierungsflags gesetzt sind. Es wird also im Grunde Ihr Programm brechen. Dies ist wohl schlimmer als eine Nullzeiger-Dereferenzierung, da dies zu subtilen Sicherheitslücken führen kann, während die Dereferenzierung einer Null Ihr Programm wahrscheinlich nur stumpf mit einem Segfault überfrachtet.
Channel72

2
@Amardeep: Ich habe sicherlich Implementierungen, bei denen ein Überlauf abhängig von den Compilereinstellungen eine Falle verursachen würde. Es wäre schön, wenn die Sprachen es erlauben würden, anzugeben, ob bestimmte vorzeichenlose Variablen oder Mengen (1) sauber verpackt werden sollen, (2) fehlerhaft sind oder (3) alles tun sollen, was zweckmäßig ist. Beachten Sie, dass, wenn eine Variable kleiner als die Registergröße einer Maschine ist, die Erzeugung eines optimalen Codes verhindert werden kann, wenn vorzeichenlose Mengen sauber verpackt werden müssen.
Supercat

Antworten:


26

Ihr Ansatz mit Subtraktion ist korrekt und klar definiert. Ein Compiler kann es nicht weg optimieren.

Ein anderer korrekter Ansatz, wenn Sie einen größeren Ganzzahltyp zur Verfügung haben, besteht darin, die Arithmetik im größeren Typ auszuführen und dann zu überprüfen, ob das Ergebnis in den kleineren Typ passt, wenn Sie es zurückkonvertieren

int sum(int a, int b)
{
    long long c;
    assert(LLONG_MAX>INT_MAX);
    c = (long long)a + b;
    if (c < INT_MIN || c > INT_MAX) abort();
    return c;
}

Ein guter Compiler sollte die gesamte Addition und ifAnweisung in eine intAddition in Größe und einen einzelnen bedingten Jump-on-Overflow konvertieren und die größere Addition niemals tatsächlich ausführen.

Bearbeiten: Wie Stephen betonte, habe ich Probleme, einen (nicht so guten) Compiler, gcc, zu bekommen, um den vernünftigen Asm zu generieren. Der Code, den es generiert, ist nicht besonders langsam, aber sicherlich nicht optimal. Wenn jemand Varianten dieses Codes kennt, die gcc dazu bringen, das Richtige zu tun, würde ich sie gerne sehen.


1
Wenn Sie dies verwenden möchten, stellen Sie sicher, dass Sie sich meine bearbeitete Version ansehen. Im Original habe ich die Besetzung long longvor dem Hinzufügen dumm weggelassen .
R .. GitHub STOP HELPING ICE

3
Ist es Ihnen aus Neugier gelungen, einen Compiler für diese Optimierung zu gewinnen? Ein schneller Test gegen einige Compiler ergab keinen, der dies konnte.
Stephen Canon

2
Unter x86_64 ist die Verwendung von 32-Bit-Ganzzahlen nicht ineffizient. Die Leistung ist identisch mit der von 64-Bit. Eine Motivation für die Verwendung von Typen mit kleinerer als nativer Wortgröße besteht darin, dass es äußerst effizient ist, Überlaufbedingungen oder Übertragungen (für Arithmetik mit beliebiger Genauigkeit) zu handhaben, da der Überlauf / Übertrag an einem direkt zugänglichen Ort stattfindet.
R .. GitHub STOP HELPING ICE

2
@R., @Steven: Nein, der vom OP angegebene Subtraktionscode ist nicht korrekt. Bitte lesen Sie meine Antwort. Ich gebe dort auch einen Code, der es mit höchstens zwei Vergleichen macht. Vielleicht machen die Compiler das besser.
Jens Gustedt

3
Dieser Ansatz funktioniert nicht auf der ungewöhnlichen Plattform, auf der sizeof(long long) == sizeof(int). C gibt nur das an sizeof(long long) >= sizeof(int).
chux

36

Nein, Ihr 2. Code ist nicht korrekt, aber Sie sind nah dran: wenn Sie einstellen

int half = INT_MAX/2;
int half1 = half + 1;

das Ergebnis einer Addition ist INT_MAX. ( INT_MAXist immer eine ungerade Zahl). Das ist also eine gültige Eingabe. Aber in Ihrer Routine werden Sie haben INT_MAX - half == half1und Sie würden abbrechen. Ein falsches Positiv.

Dieser Fehler kann , indem sie repariert werden , <anstatt <=in beiden Kontrollen.

Aber dann ist auch Ihr Code nicht optimal. Folgendes würde reichen:

int add(int lhs, int rhs)
{
 if (lhs >= 0) {
  if (INT_MAX - lhs < rhs) {
   /* would overflow */
   abort();
  }
 }
 else {
  if (rhs < INT_MIN - lhs) {
   /* would overflow */
   abort();
  }
 }
 return lhs + rhs;
}

Um zu sehen, dass dies gültig ist, müssen Sie lhsauf beiden Seiten der Ungleichungen symbolisch addieren , und dies gibt Ihnen genau die arithmetischen Bedingungen, unter denen Ihr Ergebnis außerhalb der Grenzen liegt.


+1 für die beste Antwort. Minor: Schlagen Sie vor /* overflow will occurred */zu betonen, dass der springende Punkt darin besteht, zu erkennen, dass ein Überlauf aufgetreten wäre, wenn der Code dies getan hätte, lhs + rhsohne die Summe tatsächlich zu tun.
chux

16

Meiner Meinung nach ist die Verwendung der östlichsten Methode, um mit überlaufempfindlichem C ++ - Code umzugehen SafeInt<T>. Dies ist eine plattformübergreifende C ++ - Vorlage, die auf Code Plex gehostet wird und die Sicherheitsgarantien bietet, die Sie hier wünschen.

Ich finde es sehr intuitiv zu bedienen, da es viele der gleichen Verwendungsmuster wie normale numerische Operationen bietet und über und unter Flüssen über Ausnahmen ausdrückt.


14

Für den Fall gcc können wir ab den Versionshinweisen zu gcc 5.0 sehen, dass es jetzt zusätzlich __builtin_add_overflowzur Überprüfung des Überlaufs Folgendes bietet :

Ein neuer Satz integrierter Funktionen für Arithmetik mit Überlaufprüfung wurde hinzugefügt: __builtin_add_overflow, __builtin_sub_overflow und __builtin_mul_overflow und zur Kompatibilität mit clang auch andere Varianten. Diese Buildins haben zwei integrale Argumente (die nicht denselben Typ haben müssen), die Argumente werden auf den vorzeichenbehafteten Typ mit unendlicher Genauigkeit erweitert, +, - oder * werden für diese ausgeführt, und das Ergebnis wird in einer Ganzzahlvariablen gespeichert, auf die verwiesen wird durch das letzte Argument. Wenn der gespeicherte Wert dem Ergebnis mit unendlicher Genauigkeit entspricht, geben die integrierten Funktionen false zurück, andernfalls true. Der Typ der Ganzzahlvariablen, die das Ergebnis enthält, kann sich von den Typen der ersten beiden Argumente unterscheiden.

Zum Beispiel:

__builtin_add_overflow( rhs, lhs, &result )

Aus dem gcc-Dokument können wir die integrierten Funktionen zur Durchführung von Arithmetik mit Überlauf ersehen. Überprüfen Sie Folgendes :

[...] Diese integrierten Funktionen haben ein vollständig definiertes Verhalten für alle Argumentwerte.

clang bietet auch eine Reihe von eingebauten arithmetischen Einbauten :

Clang bietet eine Reihe von integrierten Funktionen, die eine überprüfte Arithmetik für sicherheitskritische Anwendungen auf eine Weise implementieren, die in C schnell und einfach ausgedrückt werden kann.

In diesem Fall wäre das eingebaute:

__builtin_sadd_overflow( rhs, lhs, &result )

Diese Funktion scheint zu sein , sehr außer einer Sache nützlich: int result; __builtin_add_overflow(INT_MAX, 1, &result);nicht explizit sagen , was in gespeichert wird resultbei Überlauf und leider ist ruhig auf die Angabe nicht definiertes Verhalten ist nicht auftreten. Sicher war das die Absicht - keine UB. Besser, wenn es das spezifiziert.
chux

1
@chux guter Punkt, hier steht, dass das Ergebnis immer definiert ist, ich habe meine Antwort aktualisiert. Es wäre ziemlich ironisch, wenn das nicht der Fall wäre.
Shafik Yaghmour

Interessanterweise hat Ihre neue Referenz kein (unsigned) long long *resultfür __builtin_(s/u)addll_overflow. Sicher sind diese fehlerhaft. Man wundert sich über die Richtigkeit anderer Aspekte. IAC, schön diese zu sehen __builtin_add/sub/mull_overflow(). Ich hoffe, sie schaffen es eines Tages zur C-Spezifikation.
chux

1
+1 Dies erzeugt eine viel bessere Assemblierung als alles, was Sie in Standard C erhalten könnten, zumindest nicht, ohne sich auf den Optimierer Ihres Compilers zu verlassen, um herauszufinden, was Sie tun. Man sollte erkennen, wann solche Buildins verfügbar sind, und nur dann eine Standardlösung verwenden, wenn der Compiler keine bereitstellt.
Alex Reinking

11

Wenn Sie einen Inline-Assembler verwenden, können Sie das Überlauf-Flag überprüfen . Eine andere Möglichkeit besteht darin, dass Sie einen sicheren Datentyp verwenden können . Ich empfehle, dieses Dokument über Integer Security zu lesen .


6
+1 Dies ist eine andere Art zu sagen: "Wenn C es nicht definiert, werden Sie zu plattformspezifischem Verhalten gezwungen." So viele Dinge, die bei der Montage leicht erledigt werden können, sind in C undefiniert und erzeugen im Namen der Portabilität Berge aus Maulwurfshügeln.
Mike DeSimone

5
Ich habe eine Asm-Antwort auf eine C-Frage abgelehnt. Wie ich bereits sagte, gibt es korrekte, tragbare Möglichkeiten, den Scheck in C zu schreiben, wodurch genau das gleiche Ergebnis generiert wird, das Sie von Hand schreiben würden. Wenn Sie diese verwenden, sind die Auswirkungen auf die Leistung natürlich gleich und weitaus geringer als bei den von Ihnen empfohlenen C ++ - Sicherheitsvorkehrungen.
R .. GitHub STOP HELPING ICE

1
@Matthieu: Wenn Sie Code schreiben, der nur für eine Implementierung verwendet wird, und diese Implementierung garantiert, dass etwas funktioniert, und Sie eine gute Ganzzahlleistung benötigen, können Sie sicherlich implementierungsspezifische Tricks verwenden. Das hat das OP jedoch nicht verlangt.
David Thornley

3
C unterscheidet aus guten Gründen zwischen implementierungsdefiniertem und undefiniertem Verhalten. Selbst wenn etwas mit UB in der aktuellen Version Ihrer Implementierung "funktioniert" , bedeutet dies nicht, dass es in zukünftigen Versionen weiterhin funktioniert. Betrachten Sie gcc und signiertes Überlaufverhalten ...
R .. GitHub STOP HELPING ICE

2
Da ich meine -1 auf die Behauptung gestützt habe, dass wir C-Code erhalten könnten, um den identischen Asm zu generieren, ist es wohl nur fair, ihn zurückzuziehen, wenn sich herausstellt, dass alle großen Compiler diesbezüglich Junk sind.
R .. GitHub STOP HILFE EIS

6

Der schnellstmögliche Weg ist die Verwendung des integrierten GCC:

int add(int lhs, int rhs) {
    int sum;
    if (__builtin_add_overflow(lhs, rhs, &sum))
        abort();
    return sum;
}

Auf x86 kompiliert GCC dies in:

    mov %edi, %eax
    add %esi, %eax
    jo call_abort 
    ret
call_abort:
    call abort

Hier wird die integrierte Überlauferkennung des Prozessors verwendet.

Wenn Sie mit der Verwendung von GCC-integrierten Funktionen nicht einverstanden sind, können Sie am schnellsten Bitoperationen für die Vorzeichenbits verwenden. Ein signierter Überlauf tritt zusätzlich auf, wenn:

  • Die beiden Operanden haben das gleiche Vorzeichen und
  • Das Ergebnis hat ein anderes Vorzeichen als die Operanden.

Das Vorzeichenbit von ~(lhs ^ rhs)ist aktiviert, wenn die Operanden das gleiche Vorzeichen haben, und das Vorzeichenbit von lhs ^ sumist aktiviert, wenn das Ergebnis ein anderes Vorzeichen als die Operanden hat. Sie können die Addition also in vorzeichenloser Form durchführen, um undefiniertes Verhalten zu vermeiden, und dann das Vorzeichenbit von ~(lhs ^ rhs) & (lhs ^ sum):

int add(int lhs, int rhs) {
    unsigned sum = (unsigned) lhs + (unsigned) rhs;
    if ((~(lhs ^ rhs) & (lhs ^ sum)) & 0x80000000)
        abort();
    return (int) sum;
}

Dies setzt sich zusammen in:

    lea (%rsi,%rdi), %eax
    xor %edi, %esi
    not %esi
    xor %eax, %edi
    test %edi, %esi
    js call_abort
    ret
call_abort:
    call abort

Das ist viel schneller als das Umwandeln in einen 64-Bit-Typ auf einem 32-Bit-Computer (mit gcc):

    push %ebx
    mov 12(%esp), %ecx
    mov 8(%esp), %eax
    mov %ecx, %ebx
    sar $31, %ebx
    clt
    add %ecx, %eax
    adc %ebx, %edx
    mov %eax, %ecx
    add $-2147483648, %ecx
    mov %edx, %ebx
    adc $0, %ebx
    cmp $0, %ebx
    ja call_abort
    pop %ebx
    ret
call_abort:
    call abort

1

Möglicherweise haben Sie besseres Glück, wenn Sie in 64-Bit-Ganzzahlen konvertieren und ähnliche Bedingungen wie diese testen. Zum Beispiel:

#include <stdint.h>

...

int64_t sum = (int64_t)lhs + (int64_t)rhs;
if (sum < INT_MIN || sum > INT_MAX) {
    // Overflow occurred!
}
else {
    return sum;
}

Vielleicht möchten Sie sich genauer ansehen, wie die Zeichenerweiterung hier funktioniert, aber ich denke, das ist richtig.


Entfernen Sie das bitweise-und-Cast aus der return-Anweisung. Sie sind falsch wie geschrieben. Die Konvertierung von größeren Ganzzahlen mit Vorzeichen in kleinere ist genau definiert, solange der Wert in den kleineren Typ passt und keine explizite Umwandlung erforderlich ist. Jeder Compiler, der eine Warnung ausgibt und vorschlägt, dass Sie eine Umwandlung hinzufügen, wenn Sie nur überprüft haben, dass der Wert nicht überläuft, ist ein fehlerhafter Compiler.
R .. GitHub STOP HELPING ICE

@R Du hast Recht, ich mag es einfach, explizit über meine Casts zu sprechen. Ich werde es jedoch aus Gründen der Richtigkeit ändern. Für zukünftige Leser lautete die Rückleitung return (int32_t)(sum & 0xffffffff);.
Jonathan

2
Beachten Sie, dass , wenn Sie schreiben sum & 0xffffffff, sumwird implizit Typ konvertiert unsigned int(unter der Annahme , 32-Bit int) , da 0xffffffffArt hat unsigned int. Dann ist das Ergebnis von bitweise und ein unsigned int, und wenn sumes negativ war, liegt es außerhalb des von unterstützten Wertenbereichs int32_t. Die Konvertierung in hat int32_tdann ein implementierungsdefiniertes Verhalten.
R .. GitHub STOP HELPING ICE

Beachten Sie, dass dies in ILP64- Umgebungen mit int64-Bit- Umgebungen nicht funktioniert .
RTX13

1

Wie wäre es mit:

int sum(int n1, int n2)
{
  int result;
  if (n1 >= 0)
  {
    result = (n1 - INT_MAX)+n2; /* Can't overflow */
    if (result > 0) return INT_MAX; else return (result + INT_MAX);
  }
  else
  {
    result = (n1 - INT_MIN)+n2; /* Can't overflow */
    if (0 > result) return INT_MIN; else return (result + INT_MIN);
  }
}

Ich denke, das sollte für jeden legitimen INT_MINund INT_MAX(symmetrischen oder nicht) funktionieren ; die Funktion wie gezeigt Clips, aber es sollte offensichtlich sein, wie man andere Verhaltensweisen bekommt).


+1 für einen netten alternativen Ansatz, der vielleicht intuitiver ist.
R .. GitHub STOP HELPING ICE

1
Ich denke, dies result = (n1 - INT_MAX)+n2;könnte überlaufen, wenn n1 klein wäre (sagen wir 0) und n2 negativ wäre.
Davmac

@davmac: Hmm ... vielleicht ist es notwendig, drei Fälle (n1 ^ n2) < 0aufzuteilen : Beginnen Sie mit einem für , was auf einer Zwei-Komplement-Maschine bedeuten würde, dass die Werte ein entgegengesetztes Vorzeichen haben und direkt hinzugefügt werden können. Wenn die Werte das gleiche Vorzeichen haben, ist der oben angegebene Ansatz sicher. Auf der anderen Seite bin ich neugierig, ob die Autoren des Standards erwartet haben, dass Implementierungen für Hardware mit zwei Komplementen für den stillen Überlauf im Falle eines Überlaufs auf eine Weise über die Schienen springen würden, die keine sofortige Beendigung eines abnormalen Programms erzwingt, sondern verursacht unvorhersehbare Störung anderer Berechnungen.
Supercat

0

Die naheliegende Lösung besteht darin, in vorzeichenloses Überlaufverhalten zu konvertieren, um das genau definierte vorzeichenlose Überlaufverhalten zu erhalten:

int add(int lhs, int rhs) 
{ 
   int sum = (unsigned)lhs + (unsigned)rhs; 
   if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) { 
      /* an overflow has occurred */ 
      abort(); 
   } 
   return sum;  
} 

Dies ersetzt das undefinierte signierte Überlaufverhalten durch die implementierungsdefinierte Konvertierung von Werten außerhalb des Bereichs zwischen signiert und nicht signiert. Sie müssen daher die Dokumentation Ihres Compilers überprüfen, um genau zu wissen, was passieren wird, aber es sollte zumindest genau definiert sein sollte auf jeder Zwei-Komplement-Maschine, die bei Konvertierungen keine Signale auslöst, das Richtige tun. Dies ist so ziemlich jede Maschine und jeder C-Compiler, die in den letzten 20 Jahren gebaut wurden.


Sie speichert noch das Ergebnis in sum, was eine ist int. Dies führt dazu, dass entweder ein implementierungsdefiniertes Ergebnis oder ein implementierungsdefiniertes Signal ausgelöst wird, wenn der Wert von (unsigned)lhs + (unsigned)rhsgrößer als ist INT_MAX.
R .. GitHub STOP HELPING ICE

2
@R: Das ist der springende Punkt - das Verhalten ist durch die Implementierung definiert und nicht undefiniert. Daher muss die Implementierung dokumentieren, was sie tut, und dies konsequent tun. Ein Signal kann nur ausgelöst werden, wenn die Implementierung es dokumentiert. In diesem Fall muss es immer ausgelöst werden, und Sie können dieses Verhalten verwenden.
Chris Dodd

0

Wenn zwei longWerte hinzugefügt werden, kann portabler Code den longWert in niedrige und hohe intTeile (oder in) aufteilenshort Teile, falls longdiese dieselbe Größe haben wie int):

static_assert(sizeof(long) == 2*sizeof(int), "");
long a, b;
int ai[2] = {int(a), int(a >> (8*sizeof(int)))};
int bi[2] = {int(b), int(b >> (8*sizeof(int))});
... use the 'long' type to add the elements of 'ai' and 'bi'

Die Verwendung der Inline-Assembly ist der schnellste Weg, wenn Sie auf eine bestimmte CPU abzielen:

long a, b;
bool overflow;
#ifdef __amd64__
    asm (
        "addq %2, %0; seto %1"
        : "+r" (a), "=ro" (overflow)
        : "ro" (b)
    );
#else
    #error "unsupported CPU"
#endif
if(overflow) ...
// The result is stored in variable 'a'

-1

Ich denke, dass das funktioniert:

int add(int lhs, int rhs) {
   volatile int sum = lhs + rhs;
   if (lhs != (sum - rhs) ) {
       /* overflow */
       //errno = ERANGE;
       abort();
   }
   return sum;
}

Die Verwendung von flüchtig hält den Compiler davon ab, den Test zu optimieren, weil er das glaubt sum sich dies zwischen der Addition und der Subtraktion geändert hat.

Unter Verwendung von gcc 4.4.3 für x86_64 führt die Assembly für diesen Code die Addition, die Subtraktion und den Test durch, obwohl sie alles auf dem Stapel und nicht benötigte Stapeloperationen speichert. Ich habe es sogar versuchtregister volatile int sum = aber die Montage war dieselbe.

Für eine Version mit nur int sum =(kein flüchtiges oder Register) hat die Funktion den Test nicht durchgeführt und die Addition mit nur einer leaAnweisung durchgeführt ( leaist Load Effective Address und wird häufig verwendet, um eine Addition durchzuführen, ohne das Flags-Register zu berühren).

Ihre Version ist größerer Code und hat viel mehr Sprünge, aber ich weiß nicht, welcher besser wäre .


4
-1 für den Missbrauch von volatile, um undefiniertes Verhalten zu maskieren. Wenn es "funktioniert", haben Sie immer noch "Glück".
R .. GitHub STOP HELPING ICE

@R: Wenn es nicht funktioniert, wird der Compiler nicht volatilerichtig implementiert . Ich habe nur nach einer einfacheren Lösung für ein sehr häufiges Problem bei einer bereits beantworteten Frage gesucht.
Strategie

Wo es jedoch fehlschlagen könnte, wäre ein System, dessen numerische Darstellung beim Überlauf für ganze Zahlen auf niedrigere Werte umbrochen wird.
Strategie

Dieser letzte Kommentar sollte ein "nicht" oder "nicht" enthalten.
Strategie

@nategoose, Ihre Behauptung, dass "wenn es nicht funktioniert, implementiert der Compiler Volatile nicht richtig", ist falsch. Zum einen ist es in der Zweierkomplementarithmetik immer wahr, dass lhs = sum - rhs ist, selbst wenn ein Überlauf aufgetreten ist. Selbst wenn dies nicht der Fall wäre und obwohl dieses spezielle Beispiel etwas erfunden ist, könnte der Compiler beispielsweise Code generieren, der die Addition ausführt, den Ergebniswert speichert, den Wert in ein anderes Register zurückliest und den gespeicherten Wert mit dem gelesenen Wert vergleicht Wert und stellt fest, dass sie gleich sind und daher davon ausgehen, dass kein Überlauf aufgetreten ist.
Davmac

-1

Für mich wäre die einfachste Überprüfung die Überprüfung der Vorzeichen der Operanden und der Ergebnisse.

Untersuchen wir die Summe: Der Überlauf kann in beide Richtungen + oder - nur auftreten, wenn beide Operanden das gleiche Vorzeichen haben. Und offensichtlich ist der Überlauf, wenn das Vorzeichen des Ergebnisses nicht mit dem Vorzeichen der Operanden übereinstimmt.

Ein Scheck wie dieser reicht also aus:

int a, b, sum;
sum = a + b;
if  (((a ^ ~b) & (a ^ sum)) & 0x80000000)
    detect_oveflow();

Bearbeiten: Wie Nils vorgeschlagen hat, ist dies die richtige ifBedingung:

((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)

Und seit wann die Anweisung

add eax, ebx 

führt zu undefiniertem Verhalten? In der Referenz zum Intel x86-Befehlssatz gibt es so etwas nicht.


2
Sie verpassen den Punkt hier. Ihre zweite Codezeile sum = a + bkann zu undefiniertem Verhalten führen.
Channel72

Wenn Sie während Ihres Testzusatzes die Summe a, b auf unsigned setzen, funktioniert Ihr Code übrigens.
Nils Pipenbrinck

Es ist nicht definiert, nicht weil das Programm abstürzt oder sich anders verhält. Es ist genau das, was der Prozessor tut, um das OF-Flag zu berechnen. Der Standard versucht nur, sich vor nicht standardmäßigen Fällen zu schützen, aber das bedeutet nicht, dass Sie dies nicht dürfen.
Ruslik

@Nils ja, ich wollte das machen, aber ich dachte, dass vier (usngined int)s es viel unlesbarer machen werden. (Sie wissen, Sie lesen es zuerst und versuchen es nur, wenn es Ihnen gefällt).
Ruslik

1
Das undefinierte Verhalten ist in C, nicht nach dem Kompilieren zur Assembly
phuclv
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.