------------------------------------------------------------------------------- Integer overflow (short) introduction (c) Marcin Ulikowski ------------------------------------------------------------------------------- Każdy typ danych w języku C może być traktowany jako liczba. Każdy typ danych ma określoną długość wyrażoną w bitach lub bajtach. Dla przykładu typ char, czyli typ znakowy ma długość 8 bitów, czyli 1 bajta. Typ integer, który jest typem liczbowym ma długość 32 bitów lub 4 bajtów. Wartość jaką może przenosić dany typ jest ograniczona jego długością oraz od tego czy jest ze znakiem (signed) czy bez (unsigned). Polecam zapoznać się z małym plikiem /usr/include/limits.h aby dowiedzieć się o limitach dla konkretnych typów danych. Poniższy kod pokaże rozmiary podstawowych typów. Należy jednak wiedzieć, że chociaż poniższe typy mają różną długość to na stosie/stercie zajmują zawsze po 32bity (4bajty) co można łatwo sprawdzić używając gdb. Oczywiście przy założeniu, że używamy CPU Intel IA32. [elceef@osiris ~]$ cat sizes.c #include int main(void) { char a; short b; int c; printf("char (%d bits)\n", sizeof(a) * 8); printf("short (%d bits)\n", sizeof(b) * 8); printf("int (%d bits)\n", sizeof(c) * 8); return 0; } [elceef@osiris ~]$ gcc -Wall -o sizes sizes.c [elceef@osiris ~]$ ./sizes char (8 bits) short (16 bits) int (32 bits) W kolejnych przykładach będziemy używać typu char zamiast integer. Łatwiej zrozumieć problem kiedy będziemy operować na małych liczbach oraz na małych zakresach. Natomiast w przypadku typu integer i innych typów całkowitych cała zasada "działania" pozostaje bez zmian. Zmienne typu signed (ze znakiem) ------------------------------------------------------------------------------ [elceef@osiris ~]$ cat example1.c #include #include int main(int argc, char *argv[]) { char number; if (argc < 2) exit(1); number = atoi(argv[1]); printf("number = %d\n", number); number++; printf("number + 1 = %d\n", number); return 0; } [elceef@osiris ~]$ gcc -Wall -o example1 example1.c [elceef@osiris ~]$ ./example1 127 number = 127 number + 1 = -128 Dlaczego po zwiększeniu 127 o 1 liczba zmieniła znak na przeciwny? Ponieważ został przekroczony zakres liczb dodatnich. Zmienna char którą zadeklarowaliśmy była domyślnie ze znakiem (signed). Kiedy miała wartość 127 binarnie wyglądała w ten sposób: 01111111 Pierwszy bit czyli 0 oznacza znak dodatni (jest to bit znaku). Kolejne jedynki to już wartość samej zmiennej czyli 127. Oto jak wygląda liczba po dodaniu jedynki: 01111111 + 00000001 ---------- 10000000 Widać, że pierwszy bit zmienił wartość na 1, czyli liczba ma teraz znak ujemny. Jest to także najmniejsza liczba ujemna jaką można zapisać używając tego typu danych (signed char). Należy zwrócić uwagę, że teraz siedem zer oznacza liczbę -128 ze względu na inny zakres liczb ujemnych w stosunku do dodatnich. Liczby ujemne mają zakres <-128;-1>, natomiast dodatnie <0;127> dlatego siedem jedynek będzie oznaczało wartość -1. Dla pewności przykłady: 00000000 - 00000001 ---------- 11111111 = (-1) 01111111 + 00000010 ---------- 10000001 = (-127) Podsumowując typ signed char może przenosić liczby z zakresu <-128;127> Jest to 256 liczb czyli 2^8. Zmienne typu unsigned (bez znaku) ------------------------------------------------------------------------------ [elceef@osiris ~]$ cat example2.c #include #include int main(int argc, char *argv[]) { unsigned char number; if (argc < 2) exit(1); number = atoi(argv[1]); printf("number = %d\n", number); number++; printf("number + 1 = %d\n", number); return 0; } [elceef@osiris ~]$ gcc -Wall -o example2 example2.c [elceef@osiris ~]$ ./example2 255 number = 255 number + 1 = 0 Zmienna typu unsigned char miała wartość 255. Po zwiększeniu o 1 zmieniła wartość na 0, ponieważ został przekroczony limit. Limit = 255 lub 11111111 Wartość = 256 lub 100000000 Wynik = 0 lub 00000000 Do obliczenia wyniku takiej operacji możemy posłużyć się dosyć znanym wzorem: wartosc % (limit + 1) = wynik czyli 256 % (255 + 1) = 0 lub inny przykład 257 % (255 + 1) = 1 Limit = 255 lub 11111111 Wartosc = 257 lub 100000001 Wynik = 1 lub 00000001 Typ unsigned char może przenosić liczby z zakresu <0;255> Jest to 256 liczb czyli 2^8 Integer overflows chociaż trudne do wykrycia nie stanowią same w sobie dużego zagrożenia. Jednak czasami mogą prowadzić do innych błędów, najczęściej przepełnień bufora powodując zapis w obszarach pamięci o krytycznym znaczeniu. Wykorzystanie ujemnej zmiennej jako argumentu malloc() nie spowoduje błędu, a jedynie zaalokowanie kilkubajtowego bufora, który może zostać nadpisany.