Skompiluj, spusti a vypíš stack trace — v céčku

Vývojári konzolových aplikácií v Jave to majú jednoduché. Ak zhlobia program, ktorý padne na chybovom stave, teda na výnimke, ktorú nik neodchytí, do konzoly sa im radostne vypíše stack trace, teda štrúdlik postupnosti volaní smerujúcej k riadku, na ktorom vznikla pôvodná chyba.

novotnyr@x64> java ThrowUp

Exception in thread "main" java.lang.NullPointerException
        at ThrowUp.main(ThrowUp.java:4)

V tomto prípade vieme rovno otvoriť ThrowUp.java, a zistiť, že na štvrtom riadku sa zjavne volá metóda na objekte, ktorý je v skutočnosti null.

Ako na to v céčku? Nebolo by super, keby binárky v tomto jazyku po páde vypľuli niečo podobné ako stack trace?

V predošlom článku sme si ukázali mechanizmus ako to dosiahnuť. Skúsme to však rozšíriť použitím návratových kódov a trochou skriptovania v shelli.

Signály padnutých programov

Ak sa program pokúša pristúpiť k pamäti, ktorá nie je platná, Linux okamžite vyhodí signál č. 11. Neplatný prístup môže znamenať snahu prečítať neplatnú pamäť, napr. snahou pristúpiť k premennej na adrese 0 (obvykle v prípade, keď máme smerník na štruktúru, ktorý však ukazuje na NULL), alebo snahu o zápis do pamäte, ktorá programu nepatrí.

Signály sa ideologicky podobajú na výnimky v objektovo-orientovaných programoch (v skutočnosti však vznikli omnoho skôr, dávno pred OOP). Operačný systém môže pomocou nich signalizovať programu výskyt špeciálnej situácie. Samotné signalizovanie prerušuje aktuálny beh programu. Niektoré signály môžu spôsobiť okamžité ukončenie behu programu, niektoré zase môžu zodpovedať zmenám v prostredí mimo programu, na ktoré môže program zareagovať rozumným spôsobom.

V unixovských operačných systémoch je každý signál jednoznačne identifikovaný číslom. Na linuxovských systémoch má prvých 32 signálov presne dohodnuté významy a vyššie čísla sú ponechané na jednotlivé programy.

kill nevraždí, ale signalizuje

Typické použitie je v príkaze kill. Ak chcete zabiť proces s PID 25, folklórom je

kill 25

V skutočnosti posiela kill v štandardnom zápise príslušnému procesu signál SIGTERM (číslo 15.)

Zoznam signálov a ich kódov na Debiane môžete získať výpisom

cat /usr/include/asm/signal.h | grep '^#define SIG'

Popis možno nájsť napr. na LinuxJournale.

Signály v bashi

Shelly majú známu vlastnosť: pomocou premennej $? je možné zistiť exit code, teda kód stavu, s ktorým skončil naposledy spustený program. Dohoda hovorí, že nula zodpovedá stavu „zbehol som bez problémov“ a akékoľvek iné číslo indikuje chybu.

Táto dohoda sa odráža aj v céčkových programoch: číslo, ktoré vráti procedúra main() sa použije ako návratový kód.

Shell bash túto funkcionalitu obohacuje: ak program padol na neobslúženom signáli, návratový kód bude mať hodnotu 128 + číslo signálu.

Skúste si pustiť program, ktorý padá na Segmentation fault:

novotnyr@x64> ./segfaultdemo
Segmentation fault

a spýtať sa na návratový kód

novotnyr@x64> echo $?   
139

Segfault má kód 11, čiže 128 + 11 dáva 139.

Finálny skript

Finálny skript bude ideologicky nasledovný:

  • nastavme ulimit -c na neobmedzenú hodnotu
  • skompilujme zdrojáky do binárky
  • spustime binárku
  • otestujme návratový kód: ak je 139, znamená to, že program segfaultol
    • následne otestujme, či existuje core dump
    • ak áno, tak ho spracujme gdb
    • a následne zmažme, aby nestrašil pri ďalšom behu

Hľa, výsledok:

#!/bin/sh
BINARY=main
SIGSEGV=139
COREDUMP=core

ulimit -c unlimited
gcc -Wall -Wextra -g -o $BINARY main.c modul.c
./$BINARY
if [ "$?" = "$SIGSEGV" ] ;
then
        if [ -f $COREDUMP ] ;
        then
                gdb -batch $BINARY $COREDUMP
                rm $COREDUMP
        fi
fi

Názov binárky, číslo signálu a názov súboru s core dumpom je kvôli prehľadnosti vyhodený do premenných na začiatku skriptu.

Skript môžeme pomenovať run, následne ho chmodnúť, aby sa dal spustiť

chmod +x ./run

A následný cyklus vývoja bude

./run

Typicky zlý beh bude vyzerať nasledovne:

novotnyr@x64> ./run
main.c: In function ‘main’:
main.c:12: warning: unused parameter ‘argc’
main.c:12: warning: unused parameter ‘argv’
Segmentation fault (core dumped)

warning: Can't read pathname for load map: Input/output error.
Core was generated by `./main'.
Program terminated with signal 11, Segmentation fault.
#0  0x0000000000400be3 in pridajKluc (sentinel=0x0, dlzka_databazy=-1,
    nazovAktualnehoKluca=0x7fff08898c80 ";",
    nazovAktualnejHodnotyKluca=0x7fff08898cb0 "\300\214\211\b\377\177",
    aktualnyListKomentarov=0x6f70b0) at modul.c:111
111         new_kluc->next = sentinel->next;

Literatúra

  1. Signal 11 while compiling kernel
  2. Catching core dumps in a shells cript

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *