Blog.nechutny.net

Blog o webu a IT.

Časté chyby při programování v C

K sepsání tohoto textu mě dovedly často se opakující dotazy spolužáků k školním projektům na FIT VUT. Opakují se dotazy na věci, které by druháci již měli dávno vědět.


Indexování argumentů (a polí obecně)

Základní chyba, kterou někteří natvrdlí jedinci stále ignorují i přes opakované upozornění. Zápis int main(int argc, char* argv[]) vám do argc umístí počet prvků v argv. Číslo začíná na jedničce - nultým prvkem je (až na pár velmi obskurních případů) název spouštěného programu. Pokud aplikaci spustíte pomocí ./app arg1 tak bude hodnota argc = 2, argv[0] bude obsahovat řetězec "app", argv[1] = "arg1" a argv[2] bude NULL pointer.

V případě přístupu k argv[3] dojde k nespecifikovanému chování. Může vám to smazat všechno porno studijní materiály, přivolat UFO, nebo ukončit aplikaci a nemůžete říct ani "ň", že vás nikdo nevaroval.

Pamět počítače si můžete představit, jako velký list čtverečkovaného papíru. Argv ukazuje na daný čtvereček, kde začínánají hodnoty argumentů. Pro argv[1] se pointerovou aritmetikou posune na začátek hodnoty druhého argumentu a ta se přečte. V případě argv[3] pro argc=2 dojde k nedefinovanému chování, které většinou vypadá následovně: dostáváme se za část vyhrazené paměti pro danou proměnnou a nastává "střelba do nohy". Přistupujeme k paměti, v které mohou být uložena data jiné aplikace a každý slušný operační systém vás klepne přes prsty - vyhodí Segfault. Existuje vždy šance, že za těmito daty bude následovat další alokovaná část pro vaši aplikaci a pak se bude aplikace na první pohled chovat normálně. Při dalším spuštění tomu už tak nemusí být, jelikož moderní operační systémy obsahují z důvodu bezpečnosti randomizaci paměti.

Stručně: Kontrola if(argc > n) nám zajišťuje pouze bezpečný přístup k prvkům argv[0]argv[n+1].


Za textem mám nějaký bordel

Další častý problém. Ukládáte do nějakého pole znaků řetězec a při výpisu pomocí printf("%s",var); dostanete onen řetězec následovaný náhodnými znaky, často ještě doprovozené o segfault. Text je v jazyce C pole bajtů, kdy každý bajt reprezentuje jeden ASCII znak. Takový řetězec "Hello" pak zabere v paměti 5+1 bajtů. Za znaky se uloží zarážka tzv. null-terminator, která sdělí, že už dál řetězec nepokračuje. V případě výpisu této chyby je jasné, že tam chybí.

Řešením je uložit za řetězec tuto nulu. Z důvodu přehlednosti kódu je lepší použít hodnotu '\0' místo klasické nuly. Při pohledu na kód je pak každému jasné, že se jedná o ukončení řetězce. Ukázkou řešení pak je třeba char foo[16]; foo[0] = 'H'; foo[1] = 'e'; foo[2] = foo[3] = 'l'; foo[4] = 'o'; foo[5] = '\0';

Dva řetězce přijdou do baru. Barman se ptá: "Co si dáte?" První řetězec říká: "Já bych si dal gin s tonikem.#MV*()>SDk+￿!^￿ ￿￿&￿@P&￿]JEA￿" Druhý komentuje: "Omluvte mého přítele, není null-terminated."

Segfault, SIGSEGV...

Šaháte kam nemáte. Mimo pamět, kterou máte vyhrazenou viz předchozí sekce Indexování argumentů. Důvodů může být několik. Neinicializovaný pointer, špatná alokace (třeba jste zrovna zapomněli alokovat pamět pro délku textu + 1 pro '\0'), nebo špatný index pole. Odhalení místa chyby je poměrně jednoduché. Pomůže nám v tom přepínač -g pro kompilátor gcc, který zajistí přiložení ladících informací do výsledné binárky. Následně už pak jen stačí při spuštění před příkaz hodit valgrind.

Pokud překládáte svůj program s gcc -std=c99 -Wall -Wextra -pedantic app.c -o app, pak postačí drobná změna na gcc -std=c99 -Wall -Wextra -pedantic -g app.c -o app. V případě Makefile přidáme do CFLAGS -g.

Aplikaci pak spustíme pod valgrindem příkazem valgrind ./app [argumenty programu]. Valgrind provádí kontrolu paměti a v případě špatné práce nás na ni upozorní. Díky debug informacím nám sdělí i přesný řádek zdrojového programu a call stack. Pokud pak na takovém řádku objevíme argv[5], nebo array[i], tak víme, že index pole je mimo rozsah.

Další chybou může být špatná velikost alokace. I u projektů v druhém ročníků se často vyskytovala alokace paměti pro pointer na strukturu, místo struktury. Mohlo za to zadání

typedef struct tElem {
    struct tElem *ptr;
    int data;
} *tElemPtr;
a následná alokace malloc(sizeof(tElemPtr)). Správným řešením je malloc(sizeof(struct tElem)) Na takové případy je třeba dávat pozor.


Čtení vstupu

Pro čtění vstupu z stdin lze použít hned několik funkcí. Ať už klasické scanf/fscanf, tak i třeba číst po znacích pomocí (f)getc. Čtení po znacích vyžaduje o něco delší kód, ale umožnuje občas více optimalizovat zpracování vstupních dat a ihned reagovat. U scanf je třeba si dát pozor na velikost načítaného řetězce - měl by být vždy omezen na velikost proměnné, do které se ukládá. Vždy při psaní "%s" zvažte, zda by nebylo lepší napsat "%255s".

Teoretickým řešením může být také "%ms", kdy fscanf provede alokaci paměti, aby se do ní vešel celý načtený řetězec. Problémem ovšem je, že se jedná o rozšíření GNU a není součástí standardu C99, ani C11 - tedy je lepší ho nepoužívat.

Stejně nebezpečnou funkcí z hlediska buffer overflow je také gets, jelikož načítá data dokuď nenarazí na znak konce řádku, nebo souboru. Takový řádek může být samozřejmě delší, než naše alokovaná pamět. Naštěstí v C11 je již tato funkce odebrána. Správnou volbou funkce pro tento případ je fgets, která má jako druhý argument specifikaci maximální délky.


Použití správných funkcí

Převod řetězce na číslo: použít atol, nebo strtol? Když se podíváte podrobně na popis těchto funkcí, tak to bude asi jasné. Při předhozením vstupu "aaa" funkci atol dostaneme hodnotu 0, kterou nejsme schopni rozlišit od předhození řetezce "0". Naopak u funkce strol můžeme provést kontrolu pomocí druhého parametru.

Práce s řetězci: použít strcpy, nebo strncpy? strcat, nebo strncat? Pokud víme maximální délku řetězce, tak je vždy lepší volit funkci s "n" v názvu. Vyhneme se tak možným problémům s null-terminate.