Wykrywanie podpisanego przepełnienia w C / C ++


82

Na pierwszy rzut oka to pytanie może wydawać się duplikatem pytania Jak wykryć przepełnienie liczb całkowitych? jednak w rzeczywistości jest znacząco inny.

Odkryłam, że podczas wykrywania przepełnienia całkowitą bez znaku jest dość trywialne, wykrywanie podpisane przepełnienie w C / C ++ jest rzeczywiście trudniejsze, niż sądzi większość ludzi.

Najbardziej oczywistym, ale naiwnym sposobem byłoby coś takiego:

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; 
}

Problem polega na tym, że zgodnie ze standardem C przepełnienie liczb całkowitych ze znakiem jest niezdefiniowanym zachowaniem. Innymi słowy, zgodnie ze standardem, gdy tylko spowodujesz przepełnienie ze znakiem, Twój program będzie tak samo nieprawidłowy, jak gdybyś wyłuskał wskaźnik zerowy. Nie możesz więc spowodować niezdefiniowanego zachowania, a następnie spróbuj wykryć przepełnienie po fakcie, tak jak w powyższym przykładzie sprawdzania warunku końcowego.

Mimo że powyższe sprawdzenie prawdopodobnie zadziała na wielu kompilatorach, nie możesz na to liczyć. W rzeczywistości, ponieważ standard C mówi, że przepełnienie ze znakiem całkowitym jest niezdefiniowane, niektóre kompilatory (takie jak GCC) zoptymalizują powyższe sprawdzenie, gdy ustawione są flagi optymalizacji, ponieważ kompilator zakłada, że ​​przepełnienie ze znakiem jest niemożliwe. To całkowicie przerywa próbę sprawdzenia przepełnienia.

Tak więc inny możliwy sposób sprawdzenia przepełnienia to:

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;
}

Wydaje się to bardziej obiecujące, ponieważ tak naprawdę nie dodajemy do siebie dwóch liczb całkowitych, dopóki nie upewnimy się z góry, że wykonanie takiego dodania nie spowoduje przepełnienia. W ten sposób nie powodujemy żadnego niezdefiniowanego zachowania.

Jednak to rozwiązanie jest niestety dużo mniej wydajne niż rozwiązanie początkowe, ponieważ musisz wykonać operację odejmowania, aby sprawdzić, czy operacja dodawania zadziała. I nawet jeśli nie przejmujesz się tym (małym) hitem wydajnościowym, nadal nie jestem do końca przekonany, że to rozwiązanie jest odpowiednie. Wyrażenie lhs <= INT_MIN - rhswygląda dokładnie tak, jak rodzaj wyrażenia, które kompilator może zoptymalizować, myśląc, że przepełnienie ze znakiem jest niemożliwe.

Czy jest tutaj lepsze rozwiązanie? Coś, co gwarantuje, że 1) nie spowoduje nieokreślonego zachowania i 2) nie zapewni kompilatorowi możliwości optymalizacji kontroli przepełnienia? Pomyślałem, że może być jakiś sposób na zrobienie tego przez rzutowanie obu operandów na niepodpisane i wykonywanie sprawdzeń przez zwijanie własnej arytmetyki uzupełnień do dwóch, ale nie jestem pewien, jak to zrobić.


1
Zamiast próbować wykryć, czy nie lepiej jest napisać kod, który nie ma możliwości przepełnienia?
Arun

9
@ArunSaha: Naprawdę trudno jest wykonać obliczenia i upewnić się, że nie przepełnią się, a w ogólnym przypadku nie można tego udowodnić. Zwykłą praktyką jest używanie jak największej liczby całkowitej i nadzieja.
David Thornley

6
@Amardeep: Dereferencing pustego wskaźnika jest równie niezdefiniowany jak podpisany przepełnienie. Niezdefiniowane zachowanie oznacza, że ​​zgodnie z normą wszystko może się zdarzyć. Nie można zakładać, że po przepełnieniu podpisanym system nie będzie w stanie nieprawidłowym i niestabilnym. OP wskazał na jedną konsekwencję tego: optymalizator może całkowicie legalnie usunąć kod, który wykrywa podpisane przepełnienie, gdy to nastąpi.
David Thornley

16
@Amardeep: Wspomniałem o takiej implementacji. GCC usunie kod sprawdzający przepełnienie, gdy ustawione są flagi optymalizacji. Więc zasadniczo zepsuje twój program. Jest to prawdopodobnie gorsze niż wyłuskiwanie wskaźnika zerowego, ponieważ może skutkować subtelnymi lukami w zabezpieczeniach, podczas gdy dereferencja wartości null prawdopodobnie po prostu zagrozi twojemu programowi segfault.
Channel72,

2
@Amardeep: Z pewnością wydaje mi się, że implementacje, w których, w zależności od ustawień kompilatora, przepełnienie spowodowałoby pułapkę. Byłoby miło, gdyby języki pozwalały na określenie, czy poszczególne zmienne bez znaku lub ilości powinny (1) zawijać się czysto, (2) błędnie, czy (3) robić cokolwiek wygodnego. Zauważ, że jeśli zmienna jest mniejsza niż rozmiar rejestru maszyny, wymaganie czystego zawijania ilości bez znaku może uniemożliwić wygenerowanie optymalnego kodu.
supercat

Odpowiedzi:


26

Twoje podejście do odejmowania jest poprawne i dobrze zdefiniowane. Kompilator nie może go zoptymalizować.

Innym poprawnym podejściem, jeśli masz dostępny większy typ liczby całkowitej, jest wykonanie arytmetyki w większym typie, a następnie sprawdzenie, czy wynik pasuje do mniejszego typu podczas konwersji z powrotem

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;
}

Dobry kompilator powinien przekonwertować całe dodawanie i ifinstrukcję na intdodatek o małej wielkości i pojedynczy warunkowy przepełnienie skoku i nigdy nie wykonywać większego dodawania.

Edycja: Jak wskazał Stephen, mam problem ze znalezieniem (niezbyt dobrego) kompilatora, gcc, do wygenerowania rozsądnego asm. Kod, który generuje, nie jest zbyt wolny, ale z pewnością nieoptymalny. Jeśli ktoś zna warianty tego kodu, które sprawią, że gcc zrobi dobrze, z przyjemnością je zobaczę.


1
Dla każdego, kto chce tego użyć, upewnij się, że patrzysz na moją edytowaną wersję. W oryginale głupio pominąłem obsadę long longprzed dodaniem.
R .. GitHub STOP HELPING ICE

3
Z ciekawości, czy udało ci się zdobyć kompilator do tej optymalizacji? Szybki test na kilku kompilatorach nie pokazał żadnego, który mógłby to zrobić.
Stephen Canon

2
Na x86_64 nie ma nic nieefektywnego w używaniu 32-bitowych liczb całkowitych. Wydajność jest identyczna z 64-bitowymi. Jedną z motywacji do używania typów o rozmiarze mniejszym niż natywny jest to, że jest niezwykle skuteczny w radzeniu sobie z warunkami przepełnienia lub przenoszenia (dla arytmetyki o dowolnej precyzji), ponieważ przepełnienie / przeniesienie odbywa się w bezpośrednio dostępnym miejscu.
R .. GitHub STOP HELPING ICE

2
@R., @Steven: nie, kod odejmowania podany przez OP jest nieprawidłowy, zobacz moją odpowiedź. Podaję tam również kod, który po prostu robi to z co najwyżej dwoma porównaniami. Być może kompilatory poradzą sobie z tym lepiej.
Jens Gustedt

3
To podejście nie działa na nietypowej platformie, w której sizeof(long long) == sizeof(int). C określa tylko to sizeof(long long) >= sizeof(int).
chux - Przywróć Monikę

36

Nie, twój drugi kod jest nieprawidłowy, ale jesteś blisko: jeśli ustawiłeś

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

wynikiem dodawania jest INT_MAX. ( INT_MAXjest zawsze liczbą nieparzystą). Więc to jest poprawny wpis. Ale w swojej rutynie będziesz miał INT_MAX - half == half1i przerwał. Fałszywie pozytywny.

Ten błąd można naprawić, wprowadzając <zamiast <=obu kontroli.

Ale także twój kod nie jest optymalny. Zrobiłoby to:

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;
}

Aby zobaczyć, że jest to poprawne, musisz symbolicznie dodać lhsnierówności po obu stronach, a to daje dokładnie warunki arytmetyczne, które wynikają poza granicami.


+1 za najlepszą odpowiedź. Drobne: sugeruje /* overflow will occurred */podkreślenie, że chodzi o to, aby wykryć, że wystąpiłoby przepełnienie, gdyby kod działał lhs + rhsbez wykonywania faktycznej sumy.
chux - Przywróć Monikę

16

IMHO, najbardziej wschodnim sposobem radzenia sobie z kodem C ++ wrażliwym na przepełnienie jest użycie SafeInt<T>. Jest to wieloplatformowy szablon C ++ hostowany na plexie kodu, który zapewnia gwarancje bezpieczeństwa, których tutaj potrzebujesz.

Uważam, że jest bardzo intuicyjny w użyciu, ponieważ zapewnia wiele takich samych wzorców użycia, jak normalne operacje numeryczne i wyraża przepływy powyżej i poniżej za pomocą wyjątków.


14

W przypadku gcc, z informacji o wydaniu gcc 5.0 widzimy, że teraz zawiera dodatkowo __builtin_add_overflowdo sprawdzenia przepełnienia:

Dodano nowy zestaw wbudowanych funkcji dla arytmetyki ze sprawdzaniem przepełnienia: __builtin_add_overflow, __builtin_sub_overflow i __builtin_mul_overflow oraz dla kompatybilności z clang również innymi wariantami. Te polecenia wbudowane mają dwa integralne argumenty (które nie muszą mieć tego samego typu), argumenty są rozszerzane do typu ze znakiem o nieskończonej precyzji, na nich jest wykonywany znak +, - lub *, a wynik jest przechowywany w zmiennej całkowitej wskazanej na przez ostatni argument. Jeśli przechowywana wartość jest równa wynikowi nieskończonej precyzji, funkcje wbudowane zwracają false, w przeciwnym razie true. Typ zmiennej całkowitej, w której będzie przechowywany wynik, może być inny niż typy dwóch pierwszych argumentów.

Na przykład:

__builtin_add_overflow( rhs, lhs, &result )

Możemy zobaczyć z dokumentu gcc Wbudowane funkcje do wykonywania arytmetyki z przepełnieniem sprawdzania, że:

[...] te wbudowane funkcje mają w pełni zdefiniowane zachowanie dla wszystkich wartości argumentów.

clang zapewnia również zestaw sprawdzonych wbudowanych arytmetycznych :

Clang zapewnia zestaw wbudowanych, które implementują sprawdzoną arytmetykę dla aplikacji krytycznych dla bezpieczeństwa w sposób szybki i łatwy do wyrażenia w C.

w tym przypadku wbudowany wyglądałby tak:

__builtin_sadd_overflow( rhs, lhs, &result )

Ta funkcja wydaje się być bardzo przydatna, z wyjątkiem jednej rzeczy: int result; __builtin_add_overflow(INT_MAX, 1, &result);nie mówi wprost, co jest przechowywane w przypadku resultprzepełnienia i niestety jest cicha, gdy określa niezdefiniowane zachowanie , nie występuje. Z pewnością taki był zamiar - nie ma UB. Lepiej, żeby to określiło.
chux - Przywróć Monikę

1
@chux dobra uwaga, tutaj jest napisane, że wynik jest zawsze zdefiniowany, zaktualizowałem moją odpowiedź. Byłoby raczej ironiczne, gdyby tak nie było.
Shafik Yaghmour

Ciekawe, że Twoja nowa referencja nie ma (unsigned) long long *resultdla __builtin_(s/u)addll_overflow. Z pewnością są to błędy. Zastanawia nas prawdziwość innych aspektów. IAC, dobrze to widzieć __builtin_add/sub/mull_overflow(). Mam nadzieję, że pewnego dnia dotrą do specyfikacji C.
chux - Przywróć Monikę

1
+1 to generuje znacznie lepszy assembler niż cokolwiek, co można uzyskać w standardowym C, przynajmniej nie bez polegania na optymalizatorze kompilatora, aby dowiedzieć się, co robisz. Należy wykryć, kiedy takie wbudowane są dostępne i używać standardowego rozwiązania tylko wtedy, gdy kompilator go nie zapewnia.
Alex Reinking

11

Jeśli używasz asemblera wbudowanego, możesz sprawdzić flagę przepełnienia . Inną możliwością jest użycie typu danych safeint . Polecam przeczytanie tego artykułu na temat Integer Security .


6
+1 To inny sposób powiedzenia „Jeśli C nie zdefiniuje tego, jesteś zmuszony do zachowania specyficznego dla platformy”. Tak wiele rzeczy, które można łatwo załatwić podczas montażu, jest nieokreślonych w C, tworząc góry z kretowisk w imię przenośności.
Mike DeSimone

5
Głosowałem przeciw odpowiedzi ASM na pytanie C. Jak powiedziałem, istnieją poprawne, przenośne sposoby zapisania czeku w C, które wygenerują dokładnie taki sam asm, jak napisany ręcznie. Oczywiście, jeśli ich użyjesz, wpływ na wydajność będzie taki sam i będzie miał znacznie mniejszy wpływ niż zalecany przez ciebie również bezpieczny program C ++.
R .. GitHub STOP HELPING ICE

1
@Matthieu: Jeśli piszesz kod, który będzie używany tylko w jednej implementacji i ta implementacja gwarantuje, że coś zadziała, i potrzebujesz dobrej wydajności liczb całkowitych, z pewnością możesz użyć sztuczek specyficznych dla implementacji. Nie o to jednak prosił OP.
David Thornley

3
C rozróżnia zachowanie zdefiniowane w implementacji i niezdefiniowane zachowanie z ważnych powodów, a nawet jeśli coś z UB „działa” w aktualnej wersji Twojej implementacji, nie oznacza to, że będzie działać w przyszłych wersjach. Weź pod uwagę gcc i podpisane zachowanie przepełnienia ...
R .. GitHub STOP HELPING ICE

2
Ponieważ oparłem moje -1 na twierdzeniu, że możemy uzyskać kod C, aby wygenerować identyczny asm, myślę, że sprawiedliwe jest wycofanie go tylko wtedy, gdy wszystkie główne kompilatory okażą się śmieciowe w tym względzie ..
R .. GitHub STOP HELPING ICE

6

Najszybszym możliwym sposobem jest użycie wbudowanego GCC:

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

Na x86, GCC kompiluje to do:

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

który wykorzystuje wbudowaną funkcję wykrywania przepełnienia procesora.

Jeśli nie masz nic przeciwko używaniu wbudowanych GCC, następnym najszybszym sposobem jest użycie operacji bitowych na bitach znaku. Podpisane przepełnienie dodatkowo występuje, gdy:

  • oba operandy mają ten sam znak i
  • wynik ma inny znak niż operandy.

Bit znaku ~(lhs ^ rhs)jest włączony, jeśli operandy mają ten sam znak, a bit znaku lhs ^ sumjest włączony, jeśli wynik ma inny znak niż operandy. Możesz więc dodać w postaci bez znaku, aby uniknąć niezdefiniowanego zachowania, a następnie użyć bitu znaku ~(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;
}

To kompiluje się w:

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

co jest dużo szybsze niż przesyłanie na typ 64-bitowy na maszynie 32-bitowej (z 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

Możesz mieć więcej szczęścia przy konwersji na 64-bitowe liczby całkowite i testowaniu podobnych warunków. Na przykład:

#include <stdint.h>

...

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

Możesz przyjrzeć się bliżej, jak będzie tutaj działać rozszerzenie znaku, ale myślę, że jest poprawne.


Usuń bitowe i rzutowane z instrukcji return. Są niepoprawne, jak napisano. Konwersja z większych typów całkowitych ze znakiem na mniejsze jest doskonale zdefiniowana, o ile wartość pasuje do mniejszego typu i nie wymaga jawnego rzutowania. Każdy kompilator, który wyświetla ostrzeżenie i sugeruje dodanie rzutowania po sprawdzeniu, czy wartość nie przepełnia, jest zepsutym kompilatorem.
R .. GitHub PRZESTAŃ POMÓC LODOM

@R Masz rację, po prostu lubię mówić wprost o moich obsadach. Zmienię to jednak na poprawność. Dla przyszłych czytelników, wiersz powrotu brzmi return (int32_t)(sum & 0xffffffff);.
Jonathan

2
Zauważ, że jeśli piszesz sum & 0xffffffff, sumjest niejawnie konwertowany na typ unsigned int(zakładając, że jest 32-bitowy int), ponieważ 0xffffffffma typ unsigned int. Wtedy wynik bitowego i jest an unsigned int, a jeśli sumbyłby ujemny, będzie poza zakresem wartości obsługiwanych przez int32_t. Konwersja do int32_tnastępnie ma zachowanie zdefiniowane w implementacji.
R .. GitHub STOP HELPING ICE

Zauważ, że to nie zadziała w środowiskach ILP64, w których ints są 64-bitowe.
rtx13

1

Co powiesz na:

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);
  }
}

Myślę, że to powinno działać dla każdego uzasadnionego INT_MINi INT_MAX(symetrycznego lub nie); funkcję, jak pokazano klipy, ale powinno być oczywiste, jak uzyskać inne zachowania).


+1 za przyjemne alternatywne podejście, być może bardziej intuicyjne.
R .. GitHub STOP HELPING ICE

1
Myślę, że to - result = (n1 - INT_MAX)+n2;- mogłoby się przepełnić, gdyby n1 było małe (powiedzmy 0), a n2 było ujemne.
davmac

@davmac: Hmm ... może konieczne jest wyodrębnienie trzech przypadków: zacznij od jednego for (n1 ^ n2) < 0, co na maszynie z dopełnieniem do dwóch oznaczałoby, że wartości mają przeciwny znak i mogą być dodawane bezpośrednio. Jeśli wartości mają ten sam znak, to podejście podane powyżej byłoby bezpieczne. Z drugiej strony, jestem ciekawy, czy autorzy standardu oczekiwali, że implementacje dla sprzętu typu cichy przepełnienie z uzupełnieniem dwóch będą przeskakiwać szyny w przypadku przepełnienia w sposób, który nie wymusił natychmiastowego nieprawidłowego zakończenia programu, ale spowodował nieprzewidywalne zakłócenie innych obliczeń.
supercat

0

Oczywistym rozwiązaniem jest konwersja na unsigned, aby uzyskać dobrze zdefiniowane zachowanie przepełnienia bez znaku:

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;  
} 

Zastępuje to niezdefiniowane zachowanie przepełnienia podpisem zdefiniowaną przez implementację konwersją wartości spoza zakresu między podpisanymi a niepodpisanymi, więc musisz sprawdzić dokumentację kompilatora, aby dokładnie wiedzieć, co się stanie, ale powinno być przynajmniej dobrze zdefiniowane i powinien postępować właściwie na każdej maszynie z uzupełnieniem dwójki, która nie generuje sygnałów o konwersjach, czyli prawie na każdej maszynie i kompilatorze języka C zbudowanych w ciągu ostatnich 20 lat.


Nadal przechowujesz wynik w sumformacie int. Powoduje to albo wynik zdefiniowany w implementacji, albo sygnał zdefiniowany w implementacji, który jest podnoszony, jeśli wartość (unsigned)lhs + (unsigned)rhsjest większa niż INT_MAX.
R .. GitHub STOP HELPING ICE

2
@R: o to właśnie chodzi - zachowanie jest zdefiniowane jako implementacja, a nie niezdefiniowane, więc implementacja musi udokumentować to, co robi i robić to konsekwentnie. Sygnał może być podniesiony tylko wtedy, gdy implementacja go udokumentuje, w takim przypadku zawsze musi być podniesiony i możesz użyć tego zachowania.
Chris Dodd,

0

W przypadku dodania dwóch longwartości, przenośny kod może podzielić longwartość na części niskie i wysokie int(lub na shortczęści, jeśli longma taki sam rozmiar jak 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'

Korzystanie z asemblacji wbudowanej jest najszybszym sposobem, jeśli jest przeznaczony dla określonego procesora:

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

Myślę, że to działa:

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

Użycie volatile powstrzymuje kompilator przed optymalizacją testu, ponieważ uważa, że summogło się to zmienić między dodawaniem a odejmowaniem.

Używając gcc 4.4.3 dla x86_64, assembler dla tego kodu wykonuje dodawanie, odejmowanie i testowanie, chociaż przechowuje wszystko na stosie i niepotrzebne operacje na stosie. Nawet próbowałemregister volatile int sum = ale montaż był taki sam.

W przypadku wersji z samą int sum =(brakiem ulotnym lub rejestrem) funkcja nie wykonała testu i dodała tylko jedną leainstrukcję ( leajest to Load Effective Address i jest często używana do dodawania bez dotykania rejestru flag).

Twoja wersja zawiera większy kod i dużo więcej skoków, ale nie wiem, która byłaby lepsza .


4
-1 za niewłaściwe użycie w volatilecelu maskowania niezdefiniowanego zachowania. Jeśli to „działa”, nadal masz po prostu „szczęście”.
R .. GitHub STOP HELPING ICE

@R: Jeśli to nie zadziała, kompilator nie implementuje się volatilepoprawnie. Jedyne, czego szukałem, to prostsze rozwiązanie bardzo powszechnego problemu na już udzielone pytanie.
nategoose

Jednak może zawieść system, którego reprezentacja numeryczna zawija się do niższych wartości po przepełnieniu dla liczb całkowitych.
nategoose

Ten ostatni komentarz powinien zawierać „nie” lub „nie”.
nategoose

@nategoose, twoje twierdzenie, że „jeśli to nie działa, kompilator nie implementuje poprawnie volatile” jest błędne. Po pierwsze, w arytmetyce uzupełnień do dwóch zawsze będzie prawdą, że lhs = suma - rhs, nawet jeśli wystąpiło przepełnienie. Nawet gdyby tak nie było i chociaż ten konkretny przykład jest nieco wymyślony, kompilator może na przykład wygenerować kod, który wykonuje dodawanie, przechowuje wartość wyniku, wczytuje wartość z powrotem do innego rejestru, porównuje przechowywaną wartość z odczytaną wartość i zauważa, że ​​są takie same i dlatego zakładają, że nie wystąpiło przepełnienie.
davmac

-1

Według mnie najprostszym sprawdzeniem byłoby sprawdzenie znaków operandów i wyników.

Przyjrzyjmy się sumie: przepełnienie może wystąpić w obu kierunkach, + lub -, tylko wtedy, gdy oba operandy mają ten sam znak. I, oczywiście, przepełnienie nastąpi, gdy znak wyniku nie będzie taki sam jak znak argumentów.

Więc taki czek wystarczy:

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

Edycja: jak zasugerował Nils, jest to poprawny ifstan:

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

I od kiedy instrukcja

add eax, ebx 

prowadzi do nieokreślonego zachowania? W odniesieniu do zestawu instrukcji Intel x86 nie ma czegoś takiego.


2
Nie rozumiesz tutaj. Twoja druga linia kodu sum = a + bmoże spowodować niezdefiniowane zachowanie.
Channel72,

jeśli rzucisz sumę, a i b na unsigned podczas dodawania testu, twój kod będzie działał btw ..
Nils Pipenbrinck

Jest niezdefiniowane nie dlatego, że program ulegnie awarii lub będzie się zachowywał inaczej. To jest dokładnie to, co robi procesor, aby obliczyć flagę OF. Standard stara się tylko chronić przed niestandardowymi przypadkami, ale nie oznacza to, że nie możesz tego robić.
ruslik

@Nils tak, chciałem to zrobić, ale pomyślałem, że cztery (usngined int)sekundy sprawią, że będzie to znacznie bardziej nieczytelne. (wiesz, najpierw to czytasz i próbujesz tylko wtedy, gdy ci się spodobało).
ruslik

1
niezdefiniowane zachowanie jest w C, a nie po kompilacji do asemblera
phuclv
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.