Kontekst
Przenosimy kod C, który został pierwotnie skompilowany przy użyciu 8-bitowego kompilatora C dla mikrokontrolera PIC. Typowy idiom, który był używany, aby zapobiec cofaniu się globalnych zmiennych bez znaku (na przykład liczników błędów), jest następujący:
if(~counter) counter++;
Operator bitowy odwraca tutaj wszystkie bity, a instrukcja jest prawdziwa tylko wtedy, gdy counter
jest mniejsza niż wartość maksymalna. Co ważne, działa to niezależnie od wielkości zmiennej.
Problem
Obecnie celujemy w 32-bitowy procesor ARM za pomocą GCC. Zauważyliśmy, że ten sam kod daje różne wyniki. O ile możemy stwierdzić, wygląda na to, że bitowe uzupełnienie zwraca wartość o innym rozmiarze niż się spodziewalibyśmy. Aby to odtworzyć, kompilujemy w GCC:
uint8_t i = 0;
int sz;
sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1
sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4
W pierwszym wierszu danych wyjściowych otrzymujemy to, czego byśmy oczekiwali: i
1 bajt. Jednak bitowe uzupełnienie i
to w rzeczywistości cztery bajty, co powoduje problem, ponieważ porównania z tym teraz nie przyniosą oczekiwanych rezultatów. Na przykład, jeśli robisz (gdzie i
jest poprawnie zainicjowany uint8_t
):
if(~i) i++;
Zobaczymy i
„zawinięcie” z 0xFF z powrotem do 0x00. To zachowanie jest inne w GCC w porównaniu do tego, kiedy działało tak, jak zamierzaliśmy w poprzednim kompilatorze i 8-bitowym mikrokontrolerze PIC.
Wiemy, że możemy rozwiązać ten problem, przesyłając w ten sposób:
if((uint8_t)~i) i++;
Lub przez
if(i < 0xFF) i++;
Jednak w obu tych obejściach wielkość zmiennej musi być znana i podatna na błędy dla twórców oprogramowania. Tego rodzaju kontrole górnych granic występują w całej bazie kodu. Istnieje wiele zmiennych rozmiarach (np. uint16_t
I unsigned char
itd.) I zmieniając je w kodzie inaczej roboczej nie jest coś czekamy na.
Pytanie
Czy nasze rozumienie problemu jest prawidłowe i czy są dostępne opcje rozwiązania tego problemu, które nie wymagają ponownego odwiedzania każdego przypadku, w którym użyliśmy tego idiomu? Czy nasze założenie jest prawidłowe, że operacja taka jak bitowe uzupełnienie powinna zwrócić wynik o takim samym rozmiarze jak operand? Wygląda na to, że ulegnie to awarii, w zależności od architektury procesorów. Czuję, że biorę szalone pigułki i że C powinno być trochę bardziej przenośne niż to. Ponownie nasze rozumienie tego może być błędne.
Z pozoru może to nie wydawać się wielkim problemem, ale ten wcześniej działający idiom jest używany w setkach lokalizacji i chętnie to rozumiemy, zanim przejdziemy do drogich zmian.
Uwaga: Wydaje się, że na pozór podobne, ale nie do końca zduplikowane pytanie: Bitowa operacja na char daje 32-bitowy wynik
Nie widziałem prawdziwego sedna omawianego problemu, a mianowicie, że rozmiar wyniku bitowego uzupełnienia różni się od tego, co przekazano operatorowi.