W jaki sposób uzyskałem wartość większą niż 8 bitów z 8-bitowej liczby całkowitej?


118

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 charnie powinno być nigdy niezdefiniowanym zachowaniem tak długo, jak sizeof(char) < sizeof(int). Ale to nie wyjaśnia, w jaki sposób cuzyskuje się niemożliwą wartość. Jak 8-bitowa liczba całkowita może cprzechowywać 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.


61
„Zdaję sobie sprawę, że zgodnie ze specyfikacją C ++ przepełnienia ze znakiem są niezdefiniowane”. -- Dobrze. Aby być precyzyjnym, nie tylko wartość jest niezdefiniowana, ale zachowanie jest. Pozorowanie uzyskania fizycznie niemożliwych wyników jest ważną konsekwencją.

@hvd Jestem pewien, że ktoś ma wyjaśnienie, w jaki sposób typowe implementacje C ++ powodują takie zachowanie. Być może ma to coś wspólnego z wyrównaniem lub jak przebiega printf()konwersja?
rliu

Inni zajęli się głównym problemem. Mój komentarz jest bardziej ogólny i dotyczy podejść diagnostycznych. Wierzę, że jednym z powodów, dla których znalazłeś tę taką zagadkę, jest nieokreślone przekonanie, że było to możliwe. Oczywiście nie jest to niemożliwe, więc zaakceptuj to i spójrz jeszcze raz
Tim X

@TimX - obserwowałem zachowanie i oczywiście wyciągnąłem wniosek, że nie jest to niemożliwe w tym sensie. Moje użycie tego słowa odnosiło się do 8-bitowej liczby całkowitej posiadającej 9-bitową wartość, co z definicji jest niemożliwe. Fakt, że tak się stało, sugeruje, że nie jest on traktowany jako wartość 8-bitowa. Jak mówili inni, jest to spowodowane błędem kompilatora. Jedyną pozorną niemożliwością jest tutaj 9-bitowa wartość w 8-bitowej przestrzeni, a tę pozorną niemożliwość wyjaśnia fakt, że przestrzeń jest faktycznie „większa” niż podawana.
Niepodpisany

Właśnie przetestowałem to na mojej maszynie i wynik jest taki, jaki powinien być. c: -120 c: -121 c: -122 c: -123 c: -124 c: -125 c: -126 c: -127 c: -128 c: 127 c: 126 c: 125 c: 124 c: 123 c: 122 c: 121 c: 120 c: 119 c: 118 c: 117 A moje środowisko to: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Odpowiedzi:


111

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 cjest zdefiniowane jako int8_ti int8_tpromuje do int, c--to ma wykonać odejmowanie c - 1w intarytmetyce i przekonwertować wynik z powrotem na int8_t. Odejmowanie w intnie 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).


Nie opisałbym tego jako „błąd”. Ponieważ przepełnienie ze znakiem powoduje niezdefiniowane zachowanie, kompilator ma pełne prawo założyć, że tak się nie stanie i zoptymalizować pętlę, aby zachować wartości pośrednie cw szerszym typie. Przypuszczalnie tak właśnie się tutaj dzieje.
Mike Seymour,

4
@MikeSeymour: Jedyne przepełnienie dotyczy (niejawnej) konwersji. Przepełnienie podczas konwersji podpisanej nie ma nieokreślonego zachowania; daje jedynie wynik zdefiniowany w implementacji (lub podnosi sygnał zdefiniowany w implementacji, ale wydaje się, że tak się nie dzieje). Różnica w definiowaniu między operacjami arytmetycznymi i konwersjami jest dziwna, ale tak definiuje to standard języka.
Keith Thompson

2
@KeithThompson To coś, co różni się między C i C ++: C pozwala na sygnał zdefiniowany w implementacji, C ++ nie. C ++ mówi po prostu: „Jeśli typ docelowy jest podpisany, wartość pozostaje niezmieniona, jeśli można ją przedstawić w typie docelowym (i szerokości pola bitowego); w przeciwnym razie wartość jest zdefiniowana w ramach implementacji”.

Tak się składa, że ​​nie mogę odtworzyć dziwnego zachowania na g ++ 4.8.0.
Daniel Landau,

2
@DanielLandau Patrz komentarz 38 w tym błędzie: „Naprawiono w wersji 4.8.0”. :)

15

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ć cw sposób podobny do c = c - 1. Tutaj wartość cpo prawej jest promowana do typu int, a następnie następuje odejmowanie. Ponieważ cnależy do zakresu int8_t, odejmowanie to nie spowoduje przepełnienia, ale może dać wartość spoza zakresu int8_t. Po przypisaniu tej wartości następuje konwersja z powrotem do typuint8_t dzięki czemu wynik pasuje z powrotem do c. W przypadku spoza zakresu konwersja ma wartość określoną w implementacji. Jednak wartość spoza zakresu int8_tnie 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, że int8_tpowstaje 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_tlub char. 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ą printftak, jak jest. Specyfikator konwersji %iw printfnie ma pojęcia, że ​​argument pochodzi z int8_tobliczeń; po prostu pracuje z plikiemint argument.


To jest klarowne wyjaśnienie.
David Healy

Kompilator tworzy dobry kod z wyłączonym optymalizatorem. Dlatego wyjaśnienia z użyciem „reguł” i „definicji” nie mają zastosowania. To błąd w optymalizatorze.

14

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 - 1a wartości pozostały w zakresie [-128 ... 127]:

c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127  // woop
c: 126
c: 125
c: 124
c: 123
c: 122

Freaky ey? Nie wiem zbyt wiele o tym, co kompilator robi z wyrażeniami typu i++lub i--. Prawdopodobnie promuje zwracaną wartość do an inti 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.


4
Ze względu na integralne promocje c = c - 1oznacza c = (int8_t) ((int)c - 1. Przekształcenie wartości spoza zakresu intna int8_tma zdefiniowane zachowanie, ale wynik jest zdefiniowany w implementacji. Właściwie, czy nie c--ma też wykonywać tych samych konwersji?

12

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ż volatilewymuszasz użycie dla niej pamięci, a tym samym uzyskasz oczekiwane wartości w zakresie.


1
Oh wow. Zapomniałem, że skompilowany zespół będzie przechowywał zmienne lokalne w rejestrach, jeśli to możliwe. Wydaje się, że jest to najbardziej prawdopodobna odpowiedź, printfnie przejmując sizeofsię wartościami formatu.
rliu

3
@roliu Uruchom g ++ -O2 -S code.cpp, a zobaczysz montaż. Ponadto printf () jest zmienną funkcją argumentową, więc argumenty, których ranga jest mniejsza niż int, będą promowane do typu int.
nr

@nos Chciałbym. Nie byłem w stanie zainstalować programu ładującego UEFI (w szczególności rEFInd), aby uruchomić archlinux na mojej maszynie, więc od dawna nie kodowałem narzędziami GNU. Dojdę do tego ... w końcu. Na razie to tylko C # w VS i próbuję zapamiętać C / nauczyć się C ++ :)
rliu

@rollu Uruchom go na maszynie wirtualnej, np. VirtualBox
nos

@nos Nie chcę wykoleić tematu, ale tak, mógłbym. Mógłbym też po prostu zainstalować Linuksa z bootloaderem BIOS-u. Jestem po prostu uparty i jeśli nie mogę go uruchomić z bootloaderem UEFI, prawdopodobnie w ogóle go nie uruchomię: P.
rliu

11

Kod asemblera ujawnia problem:

:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
sub ebx, 1
call    printf
cmp ebx, -301
jne loop

mov esi, -45
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
xor eax, eax
call    printf

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:

mov eax, ebx
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
add ebx, 1
not eax
movsx   ebp, al                 ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp

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:

movzx   eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx   edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2   ;"c: %i\n"
mov esi, edx

Więc jest to błąd w optymalizatorze.


4

Użyj %hhdzamiast %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.


1
Jestem w stanie odtworzyć oryginalne zachowanie w moim systemie przy użyciu g++ -O3. Zmiana %ina %hhdniczego nie zmienia.
Keith Thompson

3

Myślę, że dzieje się to poprzez optymalizację kodu:

for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

Kompilator używa int32_t izmiennej zarówno dla, jak ii c. Wyłącz optymalizację lub wykonaj bezpośrednie przesyłanie printf("c: %i\n", (int8_t)c--);


Następnie wyłącz optymalizację. lub zrób coś takiego:(int8_t)(c & 0x0000ffff)--
Vsevolod

1

cjest sam w sobie zdefiniowany jako int8_t, ale podczas działania ++lub --powyżej int8_tjest niejawnie konwertowany jako pierwszy, inta wynik operacji zamiast tego wewnętrzna wartość c jest drukowana za pomocą printf, co jestint .

Zobacz aktualną wartość z cpo całej pętli, zwłaszcza po ostatnim ubytku

-301 + 256 = -45 (since it revolved entire 8 bit range once)

jego poprawna wartość, która przypomina zachowanie -128 + 1 = 127

czaczyna używać intpamięci rozmiaru, ale drukowane jest int8_ttak samo, jak podczas drukowania jako siebie, używając tylko 8 bits. Wykorzystuje wszystko, 32 bitsgdy jest używany jakoint

[Błąd kompilatora]


0

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ż

printf("c: %i\n", c);

„c” to wartość 8-bitowa, dlatego nie jest możliwe, aby kiedykolwiek pomieścić liczbę tak dużą, jak -300.
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.