C/Błędy
"Dennis Ritchie designed C to be close to the hardware with an assumption in its design was that the programmers using the language were well trained and professionals, using processes that professional do, such as code reviews." Clem Cole[1]
Definicje
edytujBłąd (oprogramowania, programu) (ang. bug) jest to usterka programu komputerowego powodująca jego nieprawidłowe działanie[2]
Sygnał (ang. signal) jest to informacja przesyłana do procesu.
Wyjątek (ang. exception) jest to przewidziany przez programistę błąd programu [3]
Niezdefiniowane zachowanie ( ang. undefined behavior UB)[4][5]
Unspecified behavior[6]
Skutek uboczny, efekt uboczny (ang. side effect) – dowolny efekt wyrażenia lub wywołania funkcji, który wykracza poza zwrócenie wartości, np. interakcja z systemem operacyjnym lub zmiana wartości zmiennej globalnej. Innymi słowy skutek uboczny następuje, gdy modyfikowany zostaje jakiś stan programu poza swoim lokalnym środowiskiem, to znaczy następuje zauważalna interakcja ze „światem zewnętrznym”, oprócz zwracania wartości.[7]
Typy
edytujZe względu na przyczynę błędy dzieli się na trzy główne typy:[8]
- błędy składniowe – nie pozwalają na kompilację programu, jak np. literówka w wywołaniu zmiennej. Są one najczęściej dość łatwe do usunięcia, zazwyczaj wynikają z drobnych pomyłek programisty.
- błędy semantyczne (znaczeniowe) – część błędów semantycznych można wychwycić już w momencie kompilacji, np. próbę odwołania się do nieistniejącej funkcji, lecz inne mogą ujawnić się dopiero w trakcie wykonywania.
- błędy logiczne – nie przerywają kompilacji, lecz powodują niewłaściwe działanie warstwy logicznej jak np. niepoprawne wyznaczanie pozycji gracza (które także może być spowodowane literówką – jak np. wpisanie znaku „+” zamiast „-”). Ten typ błędów jest znacznie trudniejszy do wykrycia i usunięcia; często błąd tkwi w jednym źle zapisanym znaku, lecz programista musi do tego znaku sam dojść (w przypadku błędu składniowego znak jest wskazywany przez komunikat kompilatora).
Ze względu na moment wykrycia błędy dzielimy na:
- błędy wykryte przy kompilacji ( buildtime)
- błędy wykryte przy uruchomieniu ( runtime)
- błędy niewykryte, czyli ukryte ( program kompiluje się poprawnie i uruchamia się bez komunikatów o błędach)
Przykłady typowych błędów[9][10]
Błędy przy kompilacji
edytujBłędy przy uruchamianiu
edytujBłędy przy uruchamianiu programu (błędy wykonania, ang. run-time errors)
- Naruszenie ochrony pamięci (ang. Segmentation fault) mogą być spowodowane przez:[11]
- przepełnieniem stosu (ang. stack overflow) [12]
- dostęp do tablicy z użyciem indeksu poza jego zakresem (Index > IndexMax). W C nie ma wymagania sprawdzania zakresu tablic, trzeba to robić samemu.
- użycie
scanf("%d", x);
zamiastscanf("%d", &x);
[13] - przepełnienie bufora[14][15][16]
- przepełnienie bufora stosu[17]
- przepełnienie bufora sterty
- Błędy z użyciem wskaźników: wycieki pamięci ( ang. memory leak ).
- Błędy przy obsłudze plików
- Błędy w obliczeniach numerycznych
- Błędy związane z łańcuchami
Naruszenie ochrony pamięci
edytujPrzepełnienie stosu
edytujMoże być spowodowane:
- rekursją: nieskończoną lub zbyt głęboką
- rozmiarem zmiennych statycznych (stosu)
Rekursja
edytujProgram powoduje nieskończoną rekursję:
#include <stdio.h>
int foo() {
return foo();
}
int main ()
{
foo();
return 0;
}
Program kompiluje się bez problemów:
gcc c.c -Wall
ale jego uruchomienie powoduje błąd:
./a.out Naruszenie ochrony pamięci
Rozmiar zmiennych
edytujPoniższy program tworzy styczne tablice o coraz większym rozmiarze aż do powstania błędu związanego z dostępem do pamięci:
#include <stdio.h>
void TestArray(int length)
{
double n[length]; /* n is an array of 10 integers */
int i;
/* initialize elements of array n to 0 */
for ( i = 0; i < length; i++ )
{
n[ i ] = i *100.0; /* set element at location i to i + 100 */
}
n[1]=n[1]; // to remove warning: variable ‘n’ set but not used [-Wunused-but-set-variable
// http://stackoverflow.com/questions/37538/how-do-i-determine-the-size-of-my-array-in-c
// length = sizeof(n)/sizeof(n[0])
printf(" length = %d , size = %ld bytes = %ld kB \n",length, length*sizeof(double), length*sizeof(double)/1000);
}
int main ()
{
int i;
for (i=1; i<1000000; i++)
TestArray(1000*i);
return 0;
}
Tablice statyczne są zapisywane w stosie więc następuje przepełnienie stosu[18] : Wynik:
length = 1043000 , size = 8344000 bytes = 8344 kB length = 1044000 , size = 8352000 bytes = 8352 kB length = 1045000 , size = 8360000 bytes = 8360 kB length = 1046000 , size = 8368000 bytes = 8368 kB length = 1047000 , size = 8376000 bytes = 8376 kB Naruszenie ochrony pamięci
Co dziwne największa możliwa do utworzenia tablica ma rozmiar 8376 kB, co jest większe niż ustalony maksymalny rozmiar stosu:
ulimit -s
otrzymujemy
8192
Potencjalne rozwiązania problemu:
- zmiana zmiennych na dynamiczne
- zmiana limitu wielkości stosu
- zmiana algorytmu
Przepełnienie bufora
edytujProgram wczytuje znaki
/*
http://www.tenouk.com/Bufferoverflowc/Bufferoverflow1.html
The output, when the input is: 12345678 (8 bytes), the program run smoothly.
When the input is: 123456789 (9 bytes), then :
* the following will be displayed when compiled with Microsoft Visual C++ 6.0.
* In Linux the “Segmentation fault” message will be displayed and the program terminates.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// theoretically reserve 5 byte of buffer plus the
// terminating NULL....should allocate 8 bytes = 2 double words,
// to overflow, need more than 8 bytes...
// so, if more than 8 characters input by user,
// there will be access violation, segmentation fault etc.
char mybuffer[5];
// a prompt how to execute the program...
if (argc < 2)
{
printf("strcpy() NOT executed....\n");
printf("Syntax: %s <characters>\n", argv[0]);
exit(0);
}
// copy the user input to mybuffer, without any bound checking
// a secure version is srtcpy_s()
strcpy(mybuffer, argv[1]);
printf("mybuffer content= %s\n", mybuffer);
// you may want to try strcpy_s()
printf("strcpy() executed...\n");
return 0;
}
Kompilujemy program bez problemów:
gcc b.c -Wall
Uruchamiamy:
./a.out
Wynik:
strcpy() NOT executed.... Syntax: ./a.out <characters>
Zapomnieliśmy wprowadzić dane, program dopomina się o nie. Ponownie uruchamiamy:
./a.out 1234
Wynik:
mybuffer content= 1234 strcpy() executed...
bez błędów.
./a.out 12345678
mybuffer content= 12345678 strcpy() executed...
Wprowadzenie więcej niż 8 znaków:
./a.out 123456789
powoduje błąd:
mybuffer content= 123456789 strcpy() executed... *** stack smashing detected ***: ./a.out terminated Przerwane
Zobacz również:
Przykłady
edytujZapobieganie
edytuj- standardy kodowania, formatowanie kodu
- MISRA_C – standard budowania aplikacji w języku C stworzony przez Motor Industry Software Reliability Association (MISRA) utworzony w celu zapewnienia przejrzystości i przenośności kodu źródłowego, głównie z myślą o systemach mikroprocesorowych programowanych w C. Standard MISRA C został stworzony z myślą o przemyśle samochodowym. Obecnie jednak jest podstawowym standardem tworzenia oprogramowania we wszelkich systemach, które wymagają dużej niezawodności m.in. przemysł lotniczy, kolejowy, medyczny, telekomunikacyjny oraz militarny.
- CERT Coding Standards
Przestrzeń nazw
edytujPrzestrzeń nazw (ang. namespace) nie istnieje w c w taki sposób jak w C++, ale można używać prefiksów[21]
Przykłady uzycia:
- biblioteka stb[22]
- stb_image_write.h ( file name )
- STBI_THREAD_LOCAL ( name )
- static void *stbi__malloc_mad4(int a, int b, int c, int d, int add)
Dokumentacja
edytuj- ręczna:
- komentarze
- znaczące nazwy funkcji i zmiennych
- ASCII art[23]
- automatyczna
Zobacz również
- Programowanie_w_systemie_UNIX: dokumentacja
- cytowanie prac [26]
Usuwanie niepotrzebnego kodu
edytujZnajdowanie martwego kodu
- cppcheck
- Squish Coco[27]
- kompilacja z gcc -Wunreachable-code ( usunięta opcja w nowym kopilatorze)
- Frama-C
Zmniejszanie zakresu zmiennej
edytuj- ang. Reduce Scope Of Variable
Bezpieczne programowanie
edytujW C nie ma automatycznego:
- sprawdzania zakresów indeksu tablicy (ang. no bounds checks on array) to prowadzi do błędu Off by one
- wskaźników (ang. pointer references)
Funkcje, których należy unikać dotyczą: [28]
- operacji na łańcuchach
niebezpieczna | opis ryzyka | bezpieczna |
---|---|---|
gets() | najbardziej niebezpieczna | fgets(buf, size, stdin) |
strcpy | bardzo niebezpieczna | strncpy, strlcpy |
strcat | bardzo niebezpieczna | strncat |
sprintf | bardzo niebezpieczna | snprintf asprintf |
scanf | bardzo niebezpieczna | Use precision specifiers, or do your own parsing. |
sscanf | bardzo niebezpieczna | Use precision specifiers, or do your own parsing. |
fscanf | bardzo niebezpieczna | Use precision specifiers, or do your own parsing. |
vfscanf | bardzo niebezpieczna | Use precision specifiers, or do your own parsing. |
vsprintf | bardzo niebezpieczna | Use vsnprintf instead, or use precision specifiers. |
vscanf | bardzo niebezpieczna | Use precision specifiers, or do your own parsing. |
vsscanf | bardzo niebezpieczna | Use precision specifiers, or do your own parsing. |
streadd | bardzo niebezpieczna | Make sure you allocate 4 times the size of the source parameter as the size of the destination. |
strecpy | bardzo niebezpieczna | Make sure you allocate 4 times the size of the source parameter as the size of the destination. |
strtrns | Risky | Manually check to see that the destination is at least the same size as the source string. |
realpath | Very risky (or less, depending on the implementation) | Allocate your buffer to be of size MAXPATHLEN. Also, manually check arguments to ensure the input argument is no larger than MAXPATHLEN. |
syslog | Very risky (or less, depending on the implementation) | Truncate all string inputs at a reasonable size before passing them to this function. |
getopt | Very risky (or less, depending on the implementation) | Truncate all string inputs at a reasonable size before passing them to this function. |
getopt_long | Very risky (or less, depending on the implementation) | Truncate all string inputs at a reasonable size before passing them to this function. |
getpass | Very risky (or less, depending on the implementation) | Truncate all string inputs at a reasonable size before passing them to this function. |
getchar | Moderate risk | Make sure to check your buffer boundaries if using this function in a loop. |
fgetc | Moderate risk | Make sure to check your buffer boundaries if using this function in a loop. |
getc | Moderate risk | Make sure to check your buffer boundaries if using this function in a loop. |
read | Moderate risk | Make sure to check your buffer boundaries if using this function in a loop. |
bcopy | Low risk | Make sure that your buffer is as big as you say it is. |
fgets | Low risk | Make sure that your buffer is as big as you say it is. |
memcpy | Low risk | Make sure that your buffer is as big as you say it is. |
snprintf | Low risk | Make sure that your buffer is as big as you say it is. |
strccpy | Low risk | Make sure that your buffer is as big as you say it is. |
strcadd | Low risk | Make sure that your buffer is as big as you say it is. |
strncpy | Low risk | Make sure that your buffer is as big as you say it is. |
vsnprintf | Low risk | Make sure that your buffer is as big as you say it is. |
słowa kluczowe:
Wykrywanie i usuwanie
edytujOgólne zasady:
- wg Erica Lipperta[29]
testy programu
edytujLiczbę błędów można też ograniczyć przeprowadzając testy programu.
Komunikaty o błędach
edytuj
Analiza kodu
edytujObsługa błędów i wyjątków
edytujObsługa błędów w języku C ( ang. error handling)[30]
- funkcje
- perror
- strerror
- makra
- globalne zmienne
- errno
- kontrola zwracancyh przez funkcje wartosci ( ang. function return values )
- EXIT_STATUS
Źródła
edytuj- ↑ quora: Why-are-programs-written-in-C-and-C-so-frequently-vulnerable-to-overflow-attacks
- ↑ błąd w wikipedii
- ↑ Z Lipiński : Programowanie w Cpp
- ↑ undefined behavior (UB) w ang. wikipedii
- ↑ nayuki : undefined-behavior-in-c-and-cplusplus-programs
- ↑ Unspecified behaviorw ang. wikipedii
- ↑ Skutek uboczny w wikipedii
- ↑ Błąd_(informatyka) w wikipedii, zobacz również wersję ang
- ↑ programowanie-pułapki-jezyka-c
- ↑ c errors
- ↑ wikipedia: Naruszenie ochrony pamięci
- ↑ W Sikora-Kobyliński, M Czępiński, G Kulewski : Ostatnia linia obrony przed atakiem
- ↑ ...definition of "Segmentation Fault" - Where is...
- ↑ Przepełnienie bufora w wikipedii
- ↑ Open Web Application Security Project
- ↑ Smashing The Stack For Fun And Profit by Aleph One
- ↑ Journey to the Stack, Part I by Gustavo Duarte Mar 10th, 2014
- ↑ Przepełnieniestosu w ang wikipedii
- ↑ Time-of-check_to_time-of-use w ang. wikipedii
- ↑ List_of_software_bugs in eng. wikipedia
- ↑ stackoverflow question: namespaces-in-c
- ↑ stb single-file public domain libraries for C/C++ by Sean T. Barrett.
- ↑ Explaining Code using ASCII Art by John Regehr, Professor of Computer Science, University of Utah, USA
- ↑ Doxygen Magically Turns Source Code into Documentation by Carla Schroder
- ↑ sphinx-doc
- ↑ citing-papers-in-software by Fredrik Johansson
- ↑ find-unused-functions-c-code-coverage using Squish Coco
- ↑ 28,0 28,1 Protect your code through defensive programming by Gary McGraw, John Viega
- ↑ eric lippert : how-to-debug-small-programs
- ↑ delftstack : c-handling-errors by Naila Saad Siddiqui Oct-20, 2022
- ↑ Simiński: Programowanie w C++
- ↑ libc: Errors in Floating-Point Calculations
- ↑ opengroup: fenv.h
- ↑ libc: FP Exceptions