Lekce 3 - Céčko a Linux - Debugging
V minulé lekci, Céčko a Linux – Makefile, jsme se věnovali makefile. Již umíme překládat své programy v terminálu a umíme si to i usnadnit.
Dnes se podíváme na další, velmi důležité téma. Určitě se vám již stalo, že jste měli v programu někde chybu a procházeli jste ho znovu a znovu... Ale pořád ji nemohli najít. Je to tak častý problém, že byla vytvořena spousta nástrojů, které nám ji mají usnadnit. Říká se jim ladící programy a nejdůležitější z nich je tzv. debugger (doslovný překlad je „odvšivovač“).
indent
Začneme zlehka a podíváme se nejdříve na tento jednoduchý nástroj pro
formátování kódu. Nainstalovat ho můžete příkazem
sudo apt-get install indent
a detailní popis jeho používání
najdete v manuálových stránkách (man indent
). Věřím
(doufám), že většina z vás píše pěkně odsazený kód. K čemu tedy
takový program? Čas od času se můžete dostat ke kódu, který buď z
nějakého důvodu ztratil původní formátování (přišel vám přes FB)
nebo autor používá výrazně jiný způsob odsazování a vás to jednoduše
"štve". V takovém případě není nic jednoduššího, než si nechat
přeformátovat kód k obrazu svému.
Malá ukázka - máme třeba takovýto nepěkný kód (představte si, že má 10 000 řádků). V tomhle by se vám nechtěla hledat chyba, že?
#include <stdio.h> #include <stdlib.h> int main (int argc,char argv[]){int a=b=c=1; printf("Výpočet: %d\n",(a+a+b)*(++c+c+++c);return 4;}
Spustíme tedy indent soubor.c
. Toto nastavení nám kód
zformátuje do klasického Unixového stylu. Pokud nemáte rádi odsazení
tabulátory, použijte přepínač -nut
(indent -nut soubor.c
). Ten řekne indentu, že nechceme
tabulátory, ale mezery. Výhoda tabulátorů je možnost nastavit si v editoru
jejich šířku. Někdo má radši odsazení větší, někdo menší (já
preferuji šířku dvě mezery).. Tato vlastnost může být ale teoreticky i
nevýhoda - například v prohlížečích má tabulátor standardně šířku 8
mezer, takže kód s tabulátory může mít opravdu obrovské odsazení.
Po použití indent bude náš kód vypadat nějak takto:
#include <stdio.h> #include <stdlib.h> int main (int argc, char argv[]) { int a = b = c = 1; printf ("Výpočet: %d\n", (a + a + b) * (++c + c++ + c); return 4; }
Není to samozřejmě dokonalé (nelíbí se mi například větší odsazení returnu), ale na rychlé zpřehlednění nečitelného kódu to stačí. Indent se dá samozřejmě (jako všechny Linuxové nástroje) dopodrobna nastavit, takže s trochou trpělivosti si můžete vytvořit vlastní konfiguraci, která bude formátovat kód přesně podle vašich představ.
Ještě poznámka - můžete si vytvořit vlastní "profil" indentu, který
bude automaticky použit při každém spuštění. Stačí jen ve svém
domovském adresáři vytvořit soubor indent.pro
a napsat do něj
použité přepínače. Jejich význam najdete v manuálových stránkách.
splint
Další nástroj, na který se dnes podíváme, slouží k analýze kódu - vypíše nám možné problémy. Máme sice k dispozici chybové hlášky překladače, ale jak brzy uvidíme, ty ne vždy stačí. Podíváme se na následující kód - takhle nějak by mohl vypadat kód začátečníka, co si někde něco přečetl a teď se sám snaží vyzkoušet si práci se znaky...
#include <stdio.h> #include <stdlib.h> int main () { char c; while (c != 'x'); { c = getchar(); if (c = 'x') return 0; switch (c) { case '\t': printf("Zadal jsi tabulátor.\n"); default: printf("%c", c); } } return 0; }
Zkusíme ho přeložit..
$ gcc prog.c -o prog
Když přeložíme tento kód, překladač nám nevrátí žádnou chybu. Přesto ale kód nebude vůbec fungovat. Zkušenější programátor jistě ví proč, ale začátečník bude ztracený. Překladač nehlásí chybu, ale program nefunguje. Co s tím? V prvním díle jsme mluvili o parametrech překladače a jak bychom měli správně překládat. Zkusíme to tedy znovu.
$ gcc -std=c99 -Wall -Wextra -pedantic prog.c -o prog
Tentokrát již náš překladač upozornil na dva problémy - v cyklu
while
používáme proměnnou c
bez inicializace a v
podmínce máme místo porovnávání přiřazení.
warning: ‘c’ is used uninitialized in this function while (c != 'x'); warning: suggest parentheses around assignment used as truth value if (c = 'x')
Varování samotné ale není příliš detailní a ani nám neříká, co
bychom s tím měli dělat. Zkusíme splint prog.c
(výpis je
dlouhý, takže si to každý zkuste sám, abyste přesně viděli, co splint
vypíše). Splint provede analýzu kódu a upozorní nás na následující
problémy:
- proměnná c použita před inicializací
- hrozí nekonečná smyčka
- přiřazení int do char: c = getchar() - hrozí ztráta informace
- přiřazení v podmínce
- "fall trough case" - "propadnutí" casem (chybí break, pokud přijde \t, vykoná se i default)
Ke každému problému je vypsán detailní popis, proč je to problém a jak
by se dal vyřešit. Kromě toho se i dozvíme, jak toto varování vypnout.
Řekl bych, že splint se může občas docela hodit - zvláště pokud máme
dlouhý kód. Opravený kód si
můžete stáhnout.
GDB
Překladač ani splint nám nehlásí žádné chyby, ale program stejně nefunguje tak, jak jsme chtěli. Co s tím? Zavoláme si na pomoc debugger. Co to ale vlastně je?
Debugger je nástroj, který nám v zásadě umožňuje krokovat (provádět náš kód po jednotlivých příkazech) a u toho sledovat hodnoty proměnných. Existuje celá řada debuggerů a umí celou řadu věcí. My se dnes podíváme na klasický gdb, který je takový nepsaný standard pro debuggování programů v C/C++, ale zvládá i mnoho dalších. Je také často základ různých grafických prostředí (dnes se podíváme na ddd, příště na Code::Blocks, dále například Nemiver).
Většina debuggerů umožňuje přeskakovat bloky kódu, co nás nezajímají (víme, že tam nemůže být chyba), pomocí tzv. breakpointů. Breakpoint je místo, kde se zastaví vykonávání a odtud typicky dále jdeme po řádcích a sledujeme, jak program funguje. Existuje velké množství možností a módů. My se dnes podíváme na debuggování jen rámcově, abychom věděli, jak debugger spustit a použít. Detailnější postupy a triky si snad ukážeme někdy v budoucnu.
První věc, kterou musíme udělat, když chceme použít debugger, je
přeložit náš kód s parametrem -g
. Ten, jak už víme, přidá
debuggovací informace, bez kterých je debugger téměř nepoužitelný. Pro
ukázku použijeme následující kód.
#include <stdio.h> #include <stdlib.h> int main () { int sum = 0; for(int i = 0; i -= 1000; i++) sum += i; printf("Součet: %d\n", sum); return 0; }
Je mi jasné, že naprostá většina z vás na první pohled uvidí, kde je problém. Ale předpokládejme, že ne. Jsme například unavení nebo nastal nějaký zkrat a my jsme si jistí, že -= je ten správný operátor. Zkusíme program přeložit - žádné chyby. Natěšení tedy program spustíme a co se nestane?

Chvilku na výsledek nechápavě hledíme... Suma čísel od 1 do 1000 přece
není -34394132! Koukneme na kód, ale chybu nevidíme. Celé to pročítat a
hledat chybu se nám nechce, takže spustíme debugger. Stačí napsat v
terminálu gdb soubor
. GDB vypíše nějaké informace o sobě a
čeká na další příkazy.

GDB můžeme ukončit zadáním klasického "q" nebo "quit". Zadáním "help" se nám zobrazí témata nápovědy. Když napíšeme help téma (např. help breakpoints), vypíší se nám konkrétní příkazy a jejich význam. Program spustíme jednoduše napsáním "run". Kdybychom to ale teď udělali, tak jen proběhne, ukáže nám výstup a návratovou hodnotu, ale nic dalšího se nedozvíme. Nastavíme tedy breakpoint a spustíme program.
break main run
Jak vidíte, breakpoint můžeme snadno nastavit na začátek funkce. Program
poběží, než se dostane k breakpointu (v našem případě než je zavolána
funkce main) a tam se zastaví a vypíše řádek, který bude zpracovávat.
Nyní můžeme vypsat obsah proměnné příkazem display. Zkusíme to
display sum
. Vidíme, že proměnná obsahuje nějakou náhodnou
hodnotu - ještě se neprovedlo přiřazení nuly. Krok provedeme příkazem
n
(zkratka pro next). Vidíme, že se vypsal další řádek
(číslo na začátku je číslo řádku) a pod ním hodnota proměnné sum.

Vidíme, že příkaz display sum
ve skutečnosti zapíná
zobrazování proměnné, nemusíme ho tedy psát pokaždé znovu. Vykonáme
další řádek - teď by měla být inicializovaná proměnná i. Pro jistotu
si ji zkontrolujeme (display i
). Hned vidíme problém - i by mělo
být nula, ale je -1000. Pro zajímavost můžeme pokračovat dál.

Vidíme, že suma se počítá správně a že k i je sice s každou iterací
přičtena jednička, ale zároveň je odečten tisíc. Vypneme debugger,
chvíli budeme nechápavě kroutit hlavou nad tím, jak jsme mohli udělat
takovou chybu a nakonec ji opravíme. Hotovo, problém vyřešen.
GDB toho umí samozřejmě mnohem mnohem více, ale pro ukázku to stačí. Ještě uvedu několik užitečných příkazů a podíváme se na DDD.
list název_fce
- vypíše zdrojový kód funkcelist 10,50
- vypíše řádky 10 až 50print proměnná
- pokud chceme jen jednou vypsat proměnnou a nechceme ji hned sledovatinfo display
- zobrazí, které proměnné sledujemeundisplay číslo
- zruší sledování, číslo získáme pomocí info displaycontinue
- program pokračuje do dalšího breakpointu (pokud není, tak do konce)
DDD
Dále se krátce podíváme na DDD (Data Display Debugger), což je vlastně jen grafická nástavba pro debugger. Kromě GDB umí DDD pracovat s množstvím dalších a to je také jedna z jeho hlavních výhod. Je ale dnes již poněkud zastaralý (například chybějící podpora UTF-8), ale stále hojně užívaný a dobrá ukázka. V některém budoucím článku se podíváme na Nemiver, což je pěkná, moderní nástavba pro GDB, určená specificky pro GNOME. Uživatelé KDE, se mohou podívat například na KDbg. Další alternativou může být například Insight.
V DDD můžeme používat stejné příkazy (a klidně je zadávat ručně),
ale můžeme také využít připravené GUI. Výhoda může být, že neustále
vidíme svůj kód a nemusíme znát všechny příkazy nazpaměť. Takhle
nějak vypadá DDD po spuštění (ddd program
).

Tak... Podíváme se na nějaké základní ovládání. Breakpoint můžeme vytvořit kliknutím (a držením) pravého tlačítka na řádek.

Jednoduché, že? Všimněme si, že na řádku se nám objevila krásná "stopka". Když na ní klikneme (zas pravým tlačítkem), můžeme ji odstranit, vypnout nebo případně otevřít vlastnosti (tam můžeme nastavit například podmínku - o těch ale v tomto díle mluvit nebudeme).

Program spustíme tlačítkem run. Všimněme si, že v příkazové řádce ve spodní části obrazovky se objevují stejné výpisy, jako jsme viděli při práci s GDB. Také se objevila sympaticky zelená šipka, která ukazuje na řádek, který má být vykonán.

Dále nás bude určitě zajímat, jak sledovat proměnnou. Opět je to jednoduché - stačí na ni kliknout pravým tlačítkem a vybrat si příslušný příkaz. Já se rozhodl, že ji chci sledovat pomocí display. Nahoře se objevila nová část okna a v té má proměnná.
Nyní můžeme krokovat programem. Já udělal několik kroků, ale pak jsem si řekl, že se mi opravdu nechce proklikávat se celým cyklem (který bude mít o hodně více než původně zamýšlených 1000 iterací). Nastavil jsem si tedy další breakpoint (na řádek za cyklem).

Teď jen klikneme na cont (continue) a program poběží do dalšího breakpointu. V sum se nám ukázala finální hodnota. Když teď vybereme next, proběhne printf. Když bychom vybrali znovu cont, program proběhne až do konce.

A to je pro dnešek vše.
V příští lekci, Céčko a Linux - Code::Blocks, se podíváme na práci v Code::Blocks. K pokročilejším debuggovacím technikám se vrátíme v některém z budoucích dílů.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 36x (719 B)
Aplikace je včetně zdrojových kódů v jazyce C