Wytropiłem wyjątkowo paskudny błąd ukrywający się za tym małym klejnotem. Zdaję sobie sprawę, że zgodnie ze specyfikacją C ++ przepełnienia ze znakiem są niezdefiniowanym zachowaniem, ale tylko wtedy, gdy przepełnienie występuje, gdy wartość jest rozszerzana do szerokości bitowej sizeof(int)
. Jak rozumiem, zwiększanie wartości a char
nie powinno być nigdy niezdefiniowanym zachowaniem tak długo, jak sizeof(char) < sizeof(int)
. Ale to nie wyjaśnia, w jaki sposób c
uzyskuje się niemożliwą wartość. Jak 8-bitowa liczba całkowita może c
przechowywać wartości większe niż jej szerokość w bitach?
Kod
// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>
int main()
{
int8_t c = 0;
printf("SCHAR_MIN: %i\n", SCHAR_MIN);
printf("SCHAR_MAX: %i\n", SCHAR_MAX);
for (int32_t i = 0; i <= 300; i++)
printf("c: %i\n", c--);
printf("c: %i\n", c);
return 0;
}
Wynik
SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128 // <= The next value should still be an 8-bit value.
c: -129 // <= What? That's more than 8 bits!
c: -130 // <= Uh...
c: -131
...
c: -297
c: -298 // <= Getting ridiculous now.
c: -299
c: -300
c: -45 // <= ..........
Sprawdź to na ideone.
c++
gcc
undefined-behavior
Bez podpisu
źródło
źródło
printf()
konwersja?Odpowiedzi:
To jest błąd kompilatora.
Chociaż uzyskanie niemożliwych wyników dla niezdefiniowanego zachowania jest ważną konsekwencją, w rzeczywistości nie ma niezdefiniowanego zachowania w twoim kodzie. Dzieje się tak, że kompilator uważa, że zachowanie jest niezdefiniowane i odpowiednio optymalizuje.
Jeśli
c
jest zdefiniowane jakoint8_t
iint8_t
promuje doint
,c--
to ma wykonać odejmowaniec - 1
wint
arytmetyce i przekonwertować wynik z powrotem naint8_t
. Odejmowanie wint
nie powoduje przepełnienia, a konwertowanie wartości całkowitych spoza zakresu na inny typ całkowity jest prawidłowe. Jeśli typ docelowy jest podpisany, wynik jest zdefiniowany w ramach implementacji, ale musi to być poprawna wartość dla typu docelowego. (A jeśli typ docelowy jest bez znaku, wynik jest dobrze zdefiniowany, ale to nie ma zastosowania w tym przypadku).źródło
c
w szerszym typie. Przypuszczalnie tak właśnie się tutaj dzieje.Kompilator może mieć błędy, które są inne niż niezgodności ze standardem, ponieważ istnieją inne wymagania. Kompilator powinien być kompatybilny z innymi wersjami samego siebie. Można również oczekiwać, że będzie w pewien sposób kompatybilny z innymi kompilatorami, a także będzie zgodny z niektórymi przekonaniami na temat zachowania, które posiadają większość jego użytkowników.
W tym przypadku wydaje się, że jest to błąd zgodności. Wyrażenie
c--
powinno działaćc
w sposób podobny doc = c - 1
. Tutaj wartośćc
po prawej jest promowana do typuint
, a następnie następuje odejmowanie. Ponieważc
należy do zakresuint8_t
, odejmowanie to nie spowoduje przepełnienia, ale może dać wartość spoza zakresuint8_t
. Po przypisaniu tej wartości następuje konwersja z powrotem do typuint8_t
dzięki czemu wynik pasuje z powrotem doc
. W przypadku spoza zakresu konwersja ma wartość określoną w implementacji. Jednak wartość spoza zakresuint8_t
nie jest prawidłową wartością zdefiniowaną w ramach implementacji. Implementacja nie może „zdefiniować”, że typ 8-bitowy nagle przechowuje 9 lub więcej bitów. Wartość, która ma być zdefiniowana w ramach realizacji, oznacza, żeint8_t
powstaje coś z zakresu , a program jest kontynuowany. Standard C pozwala zatem na zachowania takie jak arytmetyka nasycenia (powszechna w procesorach DSP) lub zawijanie (architektury głównego nurtu).Kompilator używa szerszego bazowego typu maszyny podczas manipulowania wartościami małych typów całkowitych, takich jak
int8_t
lubchar
. Gdy wykonywana jest arytmetyka, wyniki, które są poza zakresem typu małej liczby całkowitej, mogą być wiarygodnie wychwytywane w tym szerszym typie. Aby zachować zewnętrznie widoczne zachowanie, że zmienna jest typu 8-bitowego, szerszy wynik należy obciąć do zakresu 8-bitowego. Aby to zrobić, wymagany jest wyraźny kod, ponieważ lokalizacje pamięci maszyny (rejestry) są szersze niż 8 bitów i są zadowolone z większych wartości. W tym przypadku kompilator zaniedbał normalizację wartości i po prostu przekazał jąprintf
tak, jak jest. Specyfikator konwersji%i
wprintf
nie ma pojęcia, że argument pochodzi zint8_t
obliczeń; po prostu pracuje z plikiemint
argument.źródło
Nie mogę tego zmieścić w komentarzu, więc zamieszczam to jako odpowiedź.
Z jakiegoś bardzo dziwnego powodu
--
sprawcą jest operator.Przetestowałem kod opublikowany w Ideone i zastąpiłem
c--
go,c = c - 1
a wartości pozostały w zakresie [-128 ... 127]:Freaky ey? Nie wiem zbyt wiele o tym, co kompilator robi z wyrażeniami typu
i++
lubi--
. Prawdopodobnie promuje zwracaną wartość do anint
i przekazuje ją. To jedyny logiczny wniosek, do jakiego mogę dojść, ponieważ w rzeczywistości otrzymujesz wartości, które nie mieszczą się w 8-bitach.źródło
c = c - 1
oznaczac = (int8_t) ((int)c - 1
. Przekształcenie wartości spoza zakresuint
naint8_t
ma zdefiniowane zachowanie, ale wynik jest zdefiniowany w implementacji. Właściwie, czy niec--
ma też wykonywać tych samych konwersji?Wydaje mi się, że podstawowy sprzęt nadal używa rejestru 32-bitowego do przechowywania tego int8_t. Ponieważ specyfikacja nie narzuca zachowania w przypadku przepełnienia, implementacja nie sprawdza przepełnienia i pozwala również na przechowywanie większych wartości.
Jeśli oznaczysz zmienną lokalną, ponieważ
volatile
wymuszasz użycie dla niej pamięci, a tym samym uzyskasz oczekiwane wartości w zakresie.źródło
printf
nie przejmującsizeof
się wartościami formatu.Kod asemblera ujawnia problem:
EBX powinien być zakończony po dekrementacji FF lub tylko BL powinien być używany z pozostałą częścią EBX wyczyszczoną. Ciekawe, że używa sub zamiast dec. -45 jest całkowicie tajemniczy. Jest to odwrócenie bitów 300 i 255 = 44. -45 = ~ 44. Gdzieś jest połączenie.
Przechodzi dużo więcej pracy przy użyciu c = c - 1:
Następnie używa tylko dolnej części RAX, więc jest ograniczone do -128 do 127. Opcje kompilatora "-g -O2".
Bez optymalizacji tworzy poprawny kod:
Więc jest to błąd w optymalizatorze.
źródło
Użyj
%hhd
zamiast%i
! Powinien rozwiązać twój problem.To, co tam widzisz, jest wynikiem optymalizacji kompilatora połączonych z poleceniem printf, aby wydrukował liczbę 32-bitową, a następnie wypchnął (podobno 8-bitową) liczbę na stos, który jest naprawdę wielkości wskaźnika, ponieważ tak działa push opcode w x86.
źródło
g++ -O3
. Zmiana%i
na%hhd
niczego nie zmienia.Myślę, że dzieje się to poprzez optymalizację kodu:
Kompilator używa
int32_t i
zmiennej zarówno dla, jaki
ic
. Wyłącz optymalizację lub wykonaj bezpośrednie przesyłanieprintf("c: %i\n", (int8_t)c--);
źródło
(int8_t)(c & 0x0000ffff)--
c
jest sam w sobie zdefiniowany jakoint8_t
, ale podczas działania++
lub--
powyżejint8_t
jest niejawnie konwertowany jako pierwszy,int
a wynik operacji zamiast tego wewnętrzna wartość c jest drukowana za pomocą printf, co jestint
.Zobacz aktualną wartość z
c
po całej pętli, zwłaszcza po ostatnim ubytkujego poprawna wartość, która przypomina zachowanie
-128 + 1 = 127
c
zaczyna używaćint
pamięci rozmiaru, ale drukowane jestint8_t
tak samo, jak podczas drukowania jako siebie, używając tylko8 bits
. Wykorzystuje wszystko,32 bits
gdy jest używany jakoint
[Błąd kompilatora]
źródło
Myślę, że stało się tak, ponieważ twoja pętla będzie trwać, dopóki int i nie osiągnie 300, a c stanie się -300. Ostatnia wartość to ponieważ
źródło