Podpisano do konwersji bez znaku w C - czy zawsze jest bezpieczna?

135

Załóżmy, że mam następujący kod C.

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

Jakie niejawne konwersje mają tutaj miejsce i czy ten kod jest bezpieczny dla wszystkich wartości ui i? (Bezpieczne, w tym sensie, że chociaż wynik w tym przykładzie przepełni się do jakiejś ogromnej liczby dodatniej, mogę rzucić go z powrotem na liczbę int i uzyskać prawdziwy wynik.)

Cwick
źródło

Odpowiedzi:

223

Krótka odpowiedź

Twoje izostanie przekonwertowane na liczbę całkowitą bez znaku przez dodanie UINT_MAX + 1, a następnie dodawanie zostanie przeprowadzone z wartościami bez znaku, co spowoduje duże result(w zależności od wartości ui i).

Długa odpowiedź

Zgodnie ze standardem C99:

6.3.1.8 Zwykłe konwersje arytmetyczne

  1. Jeśli oba operandy mają ten sam typ, dalsza konwersja nie jest potrzebna.
  2. W przeciwnym razie, jeśli oba operandy mają typy liczb całkowitych ze znakiem lub oba mają typy całkowite bez znaku, operand o typie mniejszej liczby całkowitej konwersji jest konwertowany na typ operandu z wyższą rangą.
  3. W przeciwnym razie, jeśli operand, który ma typ liczby całkowitej bez znaku, ma rangę większą lub równą randze typu drugiego operandu, operand z typem liczby całkowitej ze znakiem jest konwertowany na typ operandu z typem liczby całkowitej bez znaku.
  4. W przeciwnym razie, jeśli typ operandu z typem liczby całkowitej ze znakiem może reprezentować wszystkie wartości typu operandu z typem liczby całkowitej bez znaku, wówczas operand z typem liczby całkowitej bez znaku jest konwertowany na typ operandu z typem liczby całkowitej ze znakiem.
  5. W przeciwnym razie oba operandy są konwertowane na typ liczby całkowitej bez znaku odpowiadającego typowi operandu z typem liczby całkowitej ze znakiem.

W twoim przypadku mamy jeden unsigned int ( u) i signed int ( i). Odnosząc się do (3) powyżej, ponieważ oba operandy mają tę samą rangę, twoja iwola musi zostać przekonwertowana na liczbę całkowitą bez znaku.

6.3.1.3 Liczby całkowite ze znakiem i bez znaku

  1. Gdy wartość o typie całkowitym jest konwertowana na inny typ liczby całkowitej inny niż _Bool, jeśli wartość może być reprezentowana przez nowy typ, pozostaje niezmieniona.
  2. W przeciwnym razie, jeśli nowy typ jest bez znaku, wartość jest konwertowana przez wielokrotne dodawanie lub odejmowanie o jedną wartość większą niż maksymalna wartość, która może być reprezentowana w nowym typie, dopóki wartość nie znajdzie się w zakresie nowego typu.
  3. W przeciwnym razie nowy typ jest podpisany i nie można w nim przedstawić wartości; wynik jest zdefiniowany w ramach implementacji lub generowany jest sygnał zdefiniowany w implementacji.

Teraz musimy odwołać się do (2) powyżej. Twoja izostanie przekonwertowana na wartość bez znaku przez dodanie UINT_MAX + 1. Wynik będzie więc zależał od tego, jak UINT_MAXzdefiniowano w Twojej implementacji. Będzie duży, ale się nie przepełni, bo:

6.2.5 (9)

Obliczenie obejmujące operandy bez znaku nigdy nie może się przepełnić, ponieważ wynik, którego nie można reprezentować przez wynikowy typ liczby całkowitej bez znaku, jest zmniejszany modulo liczba, która jest o jeden większa niż największa wartość, która może być reprezentowana przez wynikowy typ.

Bonus: Pół-WTF konwersji arytmetycznej

#include <stdio.h>

int main(void)
{
  unsigned int plus_one = 1;
  int minus_one = -1;

  if(plus_one < minus_one)
    printf("1 < -1");
  else
    printf("boring");

  return 0;
}

Możesz użyć tego linku, aby wypróbować to online: https://repl.it/repls/QuickWhimsicalBytes

Bonus: Efekt uboczny konwersji arytmetycznej

Reguły konwersji arytmetycznej można wykorzystać do uzyskania wartości UINT_MAXpoprzez zainicjowanie wartości bez znaku -1, np .:

unsigned int umax = -1; // umax set to UINT_MAX

Gwarantuje to przenośność niezależnie od podpisanej reprezentacji numeru systemu ze względu na opisane powyżej reguły konwersji. Zobacz to pytanie SO, aby uzyskać więcej informacji: Czy bezpieczne jest użycie -1 do ustawienia wszystkich bitów na true?

Ozgur Ozcitak
źródło
Nie rozumiem, dlaczego nie może po prostu podać wartości bezwzględnej, a następnie traktować ją jako bez znaku, tak jak w przypadku liczb dodatnich?
Jose Salvatierra
7
@ D.Singh Czy możesz wskazać niewłaściwe części w odpowiedzi?
Shmil The Cat,
W przypadku konwersji ze znakiem na bez znaku dodajemy maksymalną wartość wartości bez znaku (UINT_MAX +1). Podobnie, jaki jest łatwy sposób konwersji z niepodpisanego na podpisany? Czy musimy odjąć podaną liczbę od wartości maksymalnej (256 w przypadku znaku bez znaku)? Na przykład: 140 po konwersji na liczbę ze znakiem staje się -116. Ale 20 staje się samymi 20. Więc jakaś łatwa sztuczka?
Jon Wheelock,
@JonWheelock patrz: stackoverflow.com/questions/8317295/…
Ozgur Ozcitak
24

Konwersja ze podpisanego na niepodpisany niekoniecznie musi po prostu kopiować lub reinterpretować reprezentację podpisanej wartości. Cytując standard C (C99 6.3.1.3):

Gdy wartość o typie całkowitym jest konwertowana na inny typ liczby całkowitej inny niż _Bool, jeśli wartość może być reprezentowana przez nowy typ, pozostaje niezmieniona.

W przeciwnym razie, jeśli nowy typ jest bez znaku, wartość jest konwertowana przez wielokrotne dodawanie lub odejmowanie o jedną wartość większą niż maksymalna wartość, która może być reprezentowana w nowym typie, dopóki wartość nie znajdzie się w zakresie nowego typu.

W przeciwnym razie nowy typ jest podpisany i nie można w nim przedstawić wartości; wynik jest zdefiniowany w ramach implementacji lub generowany jest sygnał zdefiniowany w implementacji.

Dla reprezentacji dopełnienia tych dwóch, która jest obecnie prawie uniwersalna, reguły odpowiadają reinterpretacji bitów. Ale dla innych reprezentacji (znak i wielkość lub dopełnienie jedynki), implementacja C musi nadal zorganizować ten sam wynik, co oznacza, że ​​konwersja nie może po prostu skopiować bitów. Na przykład (unsigned) -1 == UINT_MAX, niezależnie od reprezentacji.

Ogólnie konwersje w C są definiowane do działania na wartościach, a nie na reprezentacjach.

Aby odpowiedzieć na pierwotne pytanie:

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

Wartość i jest konwertowana na bez znaku int, co daje UINT_MAX + 1 - 5678. Ta wartość jest następnie dodawana do wartości 1234 bez znaku, otrzymując UINT_MAX + 1 - 4444.

(W przeciwieństwie do przepełnienia bez znaku, przepełnienie ze znakiem wywołuje niezdefiniowane zachowanie. Zawijanie jest powszechne, ale nie jest gwarantowane przez standard C - a optymalizacje kompilatora mogą siać spustoszenie w kodzie, który przyjmuje nieuzasadnione założenia).


źródło
5

Nawiązując do Biblii :

  • Twoja operacja dodawania powoduje, że int zostanie przekonwertowany na int bez znaku.
  • Zakładając reprezentację dopełnienia do dwóch i typy o jednakowej wielkości, wzór bitowy się nie zmienia.
  • Konwersja z unsigned int na signed int zależy od implementacji. (Ale obecnie prawdopodobnie działa tak, jak oczekujesz na większości platform).
  • Zasady są nieco bardziej skomplikowane w przypadku łączenia różnych rozmiarów ze znakiem i bez znaku.
smh
źródło
3

Kiedy dodawana jest jedna zmienna bez znaku i jedna ze znakiem ze znakiem (lub dowolna operacja binarna), obie są niejawnie konwertowane na bez znaku, co w tym przypadku dałoby ogromny wynik.

Jest więc bezpieczny w tym sensie, że wynik może być ogromny i błędny, ale nigdy się nie zawiedzie.

Mats Fredriksson
źródło
Nie prawda. 6.3.1.8 Zwykłe konwersje arytmetyczne Jeśli zsumujesz int i unsigned char, ten ostatni jest konwertowany na int. Jeśli zsumujesz dwa znaki bez znaku, zostaną one zamienione na int.
2501
3

Podczas konwersji z podpisanego na niepodpisany istnieją dwie możliwości. Liczby, które były pierwotnie dodatnie, pozostają (lub są interpretowane jako) tę samą wartość. Liczba, która była pierwotnie ujemna, będzie teraz interpretowana jako większe liczby dodatnie.

Tim Ring
źródło
1

Jak już wcześniej udzielono, możesz bez problemu przesyłać między podpisanymi i niepodpisanymi. Wielkość obramowania dla liczb całkowitych ze znakiem to -1 (0xFFFFFFFF). Spróbuj dodać i odjąć od tego, a przekonasz się, że możesz rzucić z powrotem i sprawdzić, czy jest poprawne.

Jeśli jednak zamierzasz przesyłać w tę iz powrotem, zdecydowanie radzę nazwać zmienne tak, aby było jasne, jakiego typu są, np .:

int iValue, iResult;
unsigned int uValue, uResult;

Zbyt łatwo jest się rozproszyć ważniejszymi sprawami i zapomnieć, która zmienna jest jakiego typu, jeśli zostaną nazwane bez podpowiedzi. Nie chcesz rzutować na niepodpisany, a następnie używać go jako indeksu tablicy.

Taylor Price
źródło
0

Jakie niejawne konwersje mają tutaj miejsce,

i zostanie przekonwertowany na liczbę całkowitą bez znaku.

i czy ten kod jest bezpieczny dla wszystkich wartości u oraz i?

Bezpieczeństwo w sensie bycia dobrze zdefiniowanym tak (patrz https://stackoverflow.com/a/50632/5083516 ).

Reguły są zwykle napisane trudnymi do odczytania standardami, ale zasadniczo niezależnie od reprezentacji użytej w liczbie całkowitej ze znakiem, liczba całkowita bez znaku będzie zawierać uzupełnienie do 2 reprezentacji liczby.

Dodawanie, odejmowanie i mnożenie będą działać poprawnie na tych liczbach, dając w wyniku kolejną liczbę całkowitą bez znaku zawierającą liczbę uzupełnioną do dwóch reprezentującą „wynik rzeczywisty”.

dzielenie i rzutowanie na większe typy liczb całkowitych bez znaku da dobrze zdefiniowane wyniki, ale wyniki te nie będą reprezentacjami „rzeczywistego wyniku” z dopełnieniem 2.

(Bezpieczne, w tym sensie, że chociaż wynik w tym przykładzie przepełni się do jakiejś ogromnej liczby dodatniej, mogę rzucić go z powrotem na liczbę int i uzyskać prawdziwy wynik.)

Podczas gdy konwersje ze znakiem na bez znaku są zdefiniowane przez standard, odwrotność jest zdefiniowana przez implementację, zarówno gcc, jak i msvc definiują konwersję w taki sposób, że otrzymasz "rzeczywisty wynik" podczas konwersji liczby uzupełnienia do 2 przechowywanej w liczbie całkowitej bez znaku z powrotem na liczbę całkowitą ze znakiem . Spodziewam się, że znajdziesz inne zachowanie tylko w mało znanych systemach, które nie używają uzupełnień do 2 dla liczb całkowitych ze znakiem.

https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html#Integers-implementation https://msdn.microsoft.com/en-us/library/0eex498h.aspx

plugwash
źródło
-17

Straszne odpowiedzi w bród

Ozgur Ozcitak

Kiedy przesyłasz ze znaku podpisanego na niepodpisany (i odwrotnie), wewnętrzna reprezentacja liczby nie zmienia się. Zmiana polega na tym, jak kompilator interpretuje bit znaku.

To jest całkowicie błędne.

Mats Fredriksson

Kiedy dodawana jest jedna zmienna bez znaku i jedna ze znakiem ze znakiem (lub dowolna operacja binarna), obie są niejawnie konwertowane na bez znaku, co w tym przypadku dałoby ogromny wynik.

To też jest złe. Bez znaku int mogą być promowane do int, jeśli mają taką samą precyzję ze względu na wypełnienie bitów w typie bez znaku.

smh

Twoja operacja dodawania powoduje, że int zostanie przekonwertowany na int bez znaku.

Źle. Może tak, a może nie.

Konwersja z unsigned int na signed int zależy od implementacji. (Ale obecnie prawdopodobnie działa tak, jak oczekujesz na większości platform).

Źle. Jest to niezdefiniowane zachowanie, jeśli powoduje przepełnienie lub wartość zostaje zachowana.

Anonimowy

Wartość i jest konwertowana na unsigned int ...

Źle. Zależy od dokładności int względem wartości int bez znaku.

Taylor Price

Jak już wcześniej udzielono, możesz bez problemu przesyłać między podpisanymi i niepodpisanymi.

Źle. Próba zapisania wartości poza zakresem liczby całkowitej ze znakiem powoduje niezdefiniowane zachowanie.

Teraz mogę wreszcie odpowiedzieć na pytanie.

Jeśli dokładność int będzie równa unsigned int, u zostanie podniesione do int ze znakiem, a otrzymasz wartość -4444 z wyrażenia (u + i). Teraz, jeśli u i ja mamy inne wartości, możesz uzyskać przepełnienie i niezdefiniowane zachowanie, ale z tymi dokładnymi liczbami otrzymasz -4444 [1] . Ta wartość będzie miała typ int. Ale próbujesz zapisać tę wartość w unsigned int, aby następnie został rzutowany na int bez znaku, a wartość, którą otrzyma wynik, będzie (UINT_MAX + 1) - 4444.

Jeśli dokładność wartości typu unsigned int będzie większa niż liczba int, int ze znakiem zostanie podwyższona do liczby int bez znaku, dając wartość (UINT_MAX + 1) - 5678, która zostanie dodana do drugiej liczby int bez znaku 1234. Czy u i i inne wartości, które powodują, że wyrażenie wykracza poza zakres {0..UINT_MAX} wartość (UINT_MAX + 1) zostanie dodana lub odjęta, dopóki wynik NIE znajdzie się w zakresie {0..UINT_MAX) i nie wystąpi żadne niezdefiniowane zachowanie .

Co to jest precyzja?

Liczby całkowite mają bity wypełniające, bity znaku i bity wartości. Liczby całkowite bez znaku nie mają oczywiście bitu znaku. Ponadto gwarantuje się, że znak bez znaku nie będzie zawierał bitów wypełniających. Liczba bitów wartości, które ma liczba całkowita, określa jej dokładność.

[Gotchas]

Samo makro rozmiar makra nie może służyć do określenia dokładności liczby całkowitej, jeśli obecne są bity wypełniające. A rozmiar bajtu nie musi być oktetem (osiem bitów), jak określono w C99.

[1] Przelew może wystąpić w jednym z dwóch punktów. Albo przed dodaniem (podczas promocji) - gdy masz int bez znaku, który jest zbyt duży, aby zmieścić się w int. Przepełnienie może również wystąpić po dodaniu, nawet jeśli unsigned int znajdował się w zakresie int, po dodaniu wynik może nadal przepełniać.

Elite Mx
źródło
6
„Niepodpisane liczby int mogą być promowane na liczby int”. Nie prawda. Nie występuje promocja liczb całkowitych, ponieważ typy mają już ranking> = int. 6.3.1.1: „Rząd dowolnego typu liczby całkowitej bez znaku będzie równy rządowi odpowiedniego typu liczby całkowitej ze znakiem, jeśli taki istnieje”. i 6.3.1.8: „W przeciwnym razie, jeśli operand, który ma typ liczby całkowitej bez znaku, ma rangę większą lub równą randze typu drugiego operandu, wówczas operand z typem liczby całkowitej ze znakiem jest konwertowany na typ operandu z liczbą całkowitą bez znaku rodzaj." obie gwarantują, że intsą konwertowane unsigned intna zwykłe konwersje arytmetyczne.
CB Bailey,
1
6.3.1.8 Występuje tylko po promocji liczb całkowitych. W akapicie otwierającym jest napisane „W przeciwnym razie promocje w liczbach całkowitych są wykonywane na obu operandach. WTEDY poniższe reguły są stosowane do promowanych operandów”. Przeczytaj więc zasady promocji 6.3.1.1 ... "Obiekt lub wyrażenie o typie całkowitoliczbowym, którego ranga konwersji liczb całkowitych jest mniejsza lub równa rangi int i unsigned int" oraz "Jeśli int może reprezentować wszystkie wartości oryginalny typ, wartość jest konwertowana na int ".
Elite Mx,
1
6.3.1.1 Promocja typu Integer używana do konwersji niektórych typów liczb całkowitych, które nie są, intlub unsigned intdo jednego z tych typów, w których coś jest typu unsigned intlub intjest oczekiwane. W TC2 dodano „lub równe”, aby umożliwić wyliczone typy rang konwersji równe intlub unsigned intkonwertowane na jeden z tych typów. Nigdy nie było zamierzone, aby opisywana promocja zmieniała się między unsigned inta int. Wspólne określanie typu między unsigned inti intjest nadal regulowane przez 6.3.1.8, nawet po TC2.
CB Bailey,
19
Umieszczanie złych odpowiedzi podczas krytykowania złych odpowiedzi innych nie brzmi jak dobra strategia na znalezienie pracy ... ;-)
R .. GitHub STOP HELPING ICE
6
Nie głosuję za usunięciem, ponieważ ten poziom zła w połączeniu z arogancją jest zbyt zabawny
MM