Vodič za otklanjanje grešaka prilikom razvoja programa u programskom jeziku C

Greške prilikom pisanja laboratorijskih vježbi su neminovne. Srećom, moderni alati nude pregršt mogućnosti za pravovremeno hvatanje i naknadno otklanjanje raznih grešaka koje se mogu javiti tijekom rada programa. Osim podrške alata potrebno je paziti na nekoliko stvari prilikom pisanja i prevođenja programa.

1. Rukovanje greškama u programskom jeziku C

Ispravno rukovanje greškama je prvi i osnovni korak pri razvoju stabilnih i ispravnih programa. Nadalje, ono uvelike olakšava nalaženje i otklanjanje pogrešaka u programu.

Jezgrini pozivi koriste povratne vrijednosti i "globalnu" varijablu errno kako bi dali do znanja da je došlo do pogreške [1]. Prije korištenja bilo kojeg jezgrinog poziva potrebno je proučiti dokumentaciju istoga kako bi se njegove povratne vrijednosti ispravno protumačile. Dokumentaciji bilo kojeg jezgrenog poziva najlakše je pristupiti naredbom man 2 <ime_poziva>.

Povratne vrijednosti jezgrinih poziva uvijek treba provjeravati jer ostali dijelovi često ovise o rezultatu izvršavanja jezgrinog poziva. Ako dođe do greške, jezgrin poziv stavlja kod greške u varijablu errno i preko povratne vrijednosti javlja da je došlo do greške. Svi kodovi grešaka definirani su u zaglavlju errno.h a njihovi opisi se mogu proučiti naredbom man errno. Zaglavlja err.h i string.h nude nekoliko funkcija za rad s kodovima grešaka:

  • void err(int eval, const char *fmt, ...);
    • ispisuje čitljiv opis greške pomoću koda iz varijable errno na stderr i poziva exit() s vrijednošću eval.
  • void warn(const char *fmt, ...);
    • ispisuje čitljiv opis greške pomoću koda iz varijable errno na stderr.
  • char *strerror(int errnum);
    • vraća niz znakova s opisom greške.

Primjer ispravnog provjeravanja greške prikazan je u nastavku:

// ...
p = malloc(size);
if (p == NULL)
    err(-1, "Unable to allocate memory");
fd = open(file_name, O_RDONLY, 0);
if (fd == -1)
    err(-1, "Unable to open file %s", file_name);
// ...

2. Alat GDB

Alat GDB je jedan od najpopularnijih programa za traženje grešaka u kodu (eng. debugger). Omogućava uvid u stanje procesa tijekom izvršavanja i kontrolu toka izvršavanja programa. Nećemo detaljno ulaziti u korištenje GDB-a već ćemo navesti par čestih situacija u kojima uz pomoć GDB-a brzo možemo doći do uzroka pogreške. Više detalja i uvodne literature o korištenju GDB-a možete pronaći na dodatnim poveznicama na kraju stranice.

Prije korištenja GDB-a potrebno je prevesti program s posebnom zastavicom -g koje programu dodaju metapodatke potrebe za rad GDB-a. Prevođenje bi stoga trebalo izgledati ovako:

$ gcc -Wall -Wextra -Werror -g lab1.c -o lab1

Pokretanje alata svodi se na naredbu gdb <ime_programa>, nakon čega je potrebno izvršiti naredbu run u GDB-ovom naredbenom retku. Prilikom bilo kakve greške koja bi inače srušila proces, GDB zaustavlja rad procesa i ispisuje trenutno stanje. U tom trenutku moguće je detaljnije proučavati stanje procesa, ispisivati varijable i sadržaje virtualnih adresa. Prilikom rada sa GDB-om preporuča se korištenje njegovog tekstualnog sučelja (TUI) koje uvelike olakšava rad [9]. Uključivanje tekstualnog sučelja obavlja se s pomoću zastavice -tui.

Nadalje, svaki program koji se "sruši" u pravilu stvara tzv. "core dump", posebnu datoteku koja opisuje stanje procesnog adresnog prostora u trenutku u kojem je došlo do greške [2]. GDB omogućava lagano "post-mortem" traženje grešaka učitavanjem "core dumpa", ispisom linije u kojoj je došlo do greške i ispisom raznih varijabli. Učitavanje "core dumpa" vrši se naredbom gdb <ime_programa> <core_dump>, nakon čega alat ispiše liniju u kojoj je došlo do greške. Ovdje je posebno korisna GDB-ova naredba backtrace koja ispisuje trenutno aktivan lanac poziva funkcija i njihove okvire na stogu.

"Core dump" datoteke se u pravilu stvaraju u radnom direktoriju programa. Ovdje treba naglasiti da većina Linux distribucija koristi drugi način stvaranja i rada sa "core dump" datotekama. Za daljnje upute preporučujemo pogledati poveznicu [3].

Prilikom rada s procesima koji stvaraju djecu GDB će pratiti izvršavanje procesa roditelja, dok će proces dijete nastaviti s izvršavanjem. Praćenje procesa djeteta moguće je postići izvršavanjem GDB naredbe set follow-fork-mode child. Više detalja i mogućnosti nalazi se na poveznici [4].

3. Prevođenje programa

Prevoditelji programa mogu uvelike pomoći prilikom traženja grešaka u kodu. Sve informacije dane u nastavku vezane su uz prevoditelje gcc i clang.

3.1. Zastavice upozorenja

Prevoditelji mogu uhvatiti probleme i greške u kodu prilikom prevođenja i uštedjeti vam vrijeme prilikom razvoja programa. Nažalost, ta se upozorenja u pravilu ignoriraju iako često upućuju na veće probleme u programu. Prilikom razvoja programa obavezno koristite zastavice -Wall i -Werror, a dodatno preporučujemo koristiti i -Wextra i -Wpedantic. Zastavice -Wall, -Wextra i -Wpedantic "pale" upozorenja za razne kategorije uobičajenih grešaka, dok zastavica -Werror prevoditelju nalaže da sva upozorenja tretira kao greške i prekine prevođenje programa.

3.2. AddressSanitizer

AddressSanitizer [5] je poseban podsustav prevoditelja programa koji omogućava otkrivanje grešaka vezanih uz neispravno korištenje memorije tijekom rada programa. Ovaj alat je od neprocjenjive pomoći prilikom traženja grešaka te ga je preporučljivo koristiti od samog početka razvoja programa.

Uključivanje ovog alata vrši se pomoći posebne zastavice -fsanitize=address koju je potrebno predati prevoditelju programa. Prevođenje bi stoga trebalo izgledati ovako:

$ gcc -Wall -Wextra -Werror -fsanitize=address lab1.c -o lab1

Prilikom detekcije greške, program će se zaustaviti i ispisati informacije o grešci, stanje stoga te opis radnji koje su dovele do greške. Primjer korištenja može se vidjeti na poveznici [6].

3.3. Statička analiza

Prevoditelji programa i drugi alati mogu analizirati izvorni tekst programa bez potrebe za pokretanjem. Takve statičke analize iznimno su korisne za pronalaženje rubnih slučajeva, ranjivosti i grešaka u programima.

Noviji gcc prevoditelji nude statičke analize za česte pogreške [7]. Za provođenje tih analiza potrebno je prilikom prevođenja programa predati zastavicu -fanalyze.

Prevoditelj clang za potrebe statičke analize nudi zaseban alat clang-tidy. Korištenje alata se najčešće svodi na predavanje datoteke s izvornim kodom (npr. clang-tidy lab1.c), ali alat nudi pregršt mogućnosti za biranje analiza te čak automatizirano ispravljanje izvornog koda. Za više detalja i upute o korištenju pogledajte poveznicu [8].

4. Literatura

5. Dodatne poveznice