Εισαγωγή στην εκσφαλμάτωση προγραμμάτων GCC με DDD και GDB

Ο DDD (Data Display Debugger) είναι ένα γραφικό front-end του GDB (Gnu DeBugger). Η σύντομη αυτή εισαγωγή παρουσιάζει τις λειτουργίες των DDD και GDB ώστε να μπορέσετε να τους χρησιμοποιήσετε άμεσα. Στη συνέχεια μπορείτε να μελετήσετε με την ησυχία σας ολόκληρα τα εγχειρίδια, ανάλογα με τις ανάγκες σας. Η εισαγωγή αυτή βασίζεται στο sample session του εγχειριδίου χρήσης του DDD.

Ξεκινούμε με ένα υποδειγματικό πρόγραμμα sample.c του οποίου ο πηγαίος κώδικας φαίνεται παρακάτω:

 /* sample.c -- Sample C program to be debugged with DDD */

#include <stdio.h>
#include <stdlib.h>

static void shell_sort(int a[], int size)
{
int i, j;
int h = 1;

do {
h = h * 3 + 1;
} while (h <= size);

do {
h /= 3;
for (i = h; i < size; i++)
{
int v = a[i];
for (j = i; j >= h && a[j - h] > v; j -= h)
a[j] = a[j - h];
if (i != j)
a[j] = v;
}
} while (h != 1);
}

int main(int argc, char *argv[])
{
int *a;
int i;

a = (int *)malloc((argc - 1) * sizeof(int));
for (i = 0; i < argc - 1; i++)
a[i] = atoi(argv[i + 1]);

shell_sort(a, argc);

for (i = 0; i < argc - 1; i++)
printf("%d ", a[i]);
printf("\n");

free(a);
return 0;
}

Το πρόγραμμα αυτό παρουσιάζει ένα σφάλμα. Κανονικά το πρόγραμμα sample πρέπει να ταξινομεί και να εμφανίζει τα ορίσματά του όπως στο ακόλουθο παράδειγμα:

 $ ./sample 8 7 5 4 1 3
1 3 4 5 7 8
$ _

Όμως, με ορισμένα ορίσματα το πρόγραμμα δε δουλεύει σωστά:

 $ ./sample 8000 7000 5000 1000 4000
1000 1913 4000 5000 7000
$ _

Ο αριθμός των ορισμάτων είναι σωστός, τα ορίσματα φαίνονται ταξινομημένα αλλά μερικά από αυτά έχουν μεταβληθεί, π.χ. το 1913 αντικατέστησε το 8000.

Θα προσπαθήσουμε να βρούμε το σφάλμα με τη βοήθεια του DDD. Πρώτα πρέπει να μεταγλωττίσουμε το πρόγραμμα sample.c για εκσφαλμάτωση, θέτοντας την επιλογή -g :

 $ gcc -g -o sample sample.c
$ _

Τώρα,  μπορούμε να καλέσουμε το DDD με όρισμα το εκτελέσιμο αρχείο sample:

 $ ddd sample
Εμφανίζεται η διεπαφή του DDD. Το παράθυρο Source Window περιέχει το πηγαίο κώδικα του προγράμματος sample.c; μπορούμε να κινηθούμε με το Scroll Bar.

Κλήση DDD

Ο οθόνη τερματικού Debugger Console (στο κάτω μέρος) περιέχει πληροφορίες για την έκδοση του DDD καθώς και τη προτροπή του GDB:

 GNU DDD Version 3.3.9, by Dorothea Lutkehaus and Andreas Zeller.
 Copyright 1995-1999 Technische Universitat Braunschweig, Germany.
Copyright 1999-2001 Universitat Passau, Germany.
Copyright 2001-2004 Universitat des Saarlandes, Germany.
Reading symbols from sample...done.
(gdb) _

Το πρώτο πράγμα που θα κάνουμε είναι να βάλουμε ένα Breakpoint, ώστε να σταματήσουμε την εκτέλεση στο σημείο που μας ενδιαφέρει. Επιλέγουμε τη γραμμή 31 του πηγαίου κώδικα. Το πεδίο ορισμάτων Argument field (): τώρα περιέχει τη γραμμή (sample.c:31). Στη γραμμή εργαλείων επιλέγουμε το Break για τη δημιουργία του Breakpoint στη θεση που φαίνεται στο Argument Field. Θα πρέπει να εμφανιστεί ένα κόκκινο σημάδι STOP στη γραμμή 31. Σημειώστε οτι στην οοθόνη τερματικού Debugger Console εμφανίζεται η αντίστοιχη εντολή που θα έπρεπε να εισαχθεί στο GDB.

 Reading symbols from sample...done.
(gdb) break sample.c:31
Breakpoint 1 at 0x8048666: file sample.c line 31_
(gdb) _

Επίσης, από τη γραμμή Menu αν επιλέξουμε το View => Machine Code Window μπορούμε να εμφανίσουμε και το κώδικα του προγράμματος σε γλώσσα assembly. Και εκεί σημειώνεται το Breakpoint. Μέσω του Menu =>View μπορείτε να επιλέγετε τα παράθυρα που θέλετε να βλέπετε.

Το επόμενο βήμα είναι η εκτέλεση του προγράμματος για να ελέγξουμε τη συμπεριφορά του. Στο Menu επιλέγουμε Program => Run. Εμφανίζεται ένα παράθυρο διαλόγου όπου μας επιτρέπεται να εισάγουμε τα ορίσματα της γραμμής εντολών του main.

Εκτέλεση DDD

Στο παράθυρo Run with Arguments εισάγουμε τα δεδομένα που προκάλεσαν τη λανθασμένη συμπεριφορά του προγράμματοςδηλαδή, 8000 7000 5000 1000 4000. Στη συνέχεια επιλέγουμε το πλήκτρo Run.

Ο GDB μέσω του DDD τώρα εκτελεί το πρόγραμμα. Η εκτέλεση σταματά στο Breakpoint, όπως ανακοινώνεται στο παράθυρο τερματικού του GDB.

(gdb) break sample.c:31
Breakpoint 1 at 0x8048666: file sample.c, line 31.
(gdb) run 8000 7000 5000 1000 4000
Starting program: sample 8000 7000 5000 1000 4000

Breakpoint 1, main (argc=6, argv=0xbffff918) at sample.c:31
(gdb) _

Η τρέχουσα εντολή προς εκτέλεση σημειώνεται με το πράσινο βέλος:

 => a = (int *)malloc((argc - 1) * sizeof(int));

Τώρα μπορούμε να εξετάσουμε τα περιεχόμενα (τιμές) των μεταβλητών. Για την εξέταση απλής μεταβλητής (δηλαδή βαθμωτού μεγέθους), απλά κινούμε το ποντίκι πάνω από το όνομα της μεταβλητής και το αφήνουμε εκεί για λίγο. Σύντομα εμφανίζεται ένα μικρό pop-up window με τη τιμή της μεταβλητής. Δοκιμάστε το με το όρισμα argc για να δείτε τη τιμή του (6). Η τοπική μεταβητή a δεν έχει αρχικοποιηθεί ακόμη. Το πιθανότερο είναι οτι θα δείτε μια μη-έγκυρη τιμή δείκτη σε ακέραιο.

Για να εκτελέσουμε τη τρέχουσα γραμμή επιλέγουμε το κουμπί Next στο Command Tool. Αν δεν βλέπετε το Command Tool επιλέξτε στο Menu View => Command Tool. Το πράσινο βέλος προχωρά μια θέση. Οι εντολές του GDB φαίνονται στο παράθυρο τερματικού. Τώρα μετακινείστε πάλι το δείκτη του ποντικιού στο a για να δείτε τη τιμή του που αρχικοποιήθηκε σε ένα πραγματικό δείκτη.

Τιμή DDD

Για την εξέταση απλών στοιχείων του πίνακα, εισάγουμε το όνομά τους στο Argument Field.Για παράδειγμα, εισάγουμε a[0] (αφού πρώτα καθαρίσουμε το πεδίο κάνοντας click στο ():) και στη συνέχεια από τη γραμμή εργαλείων επιλέγουμε το Print. Η τιμή εμφανίζεται στο παράθυρο τερματικού. Έτσι θα δόυμε
(gdb) print a[0]
$1 = 0
(gdb) _
    
ή κάποια άλλη τιμή (σημειώστε οτι ο πίνακας απλά ορίστηκε, δεν έχουν περάσει οι τιμές στα στοιχεία του πίνακα).

Για να δούμε όλα τα στοιχεία του πίνακα μαζί, πρέπει να χρησιμοποιήσουμε ένα ειδικό τελεστή του GDB. Αφού ο πίνακας a  ορίζεται δυναμικά, ο GDB δεν γνωρίζει το μέγεθός του εκ των προτέρων. Το μέγεθος του πίνακα πρέπει να δηλωθεί ρητά με τον τελεστή @. Στο Argument Field εισάγουμε a[0]@(argc - 1)και στη συνέχεια επιλέγουμε Print. Εμφανίζονται τα πρώτα argc - 1 στοιχεία του a, δηλαδή

(gdb) print a[0]@(argc - 1)
$2 = {0, 0, 0, 0, 0}
(gdb) _
    
Αν κατά λάθος δοκιμάσουμε a@(argc - 1) θα δούμε τις διεθύνσεις των στοιχείων του πίνακα a, αντί για τις τιμές τους. Αντί να χρησιμοποιήσουμε το Print κάθε φορά που θέλουμε να δούμε τις τρέχουσες τιμές του a, μπορούμε επίσης να εφανίζουμε το a αυτόματα. Έχοντας το a[0]@(argc - 1) ακόμη στι Argument Field, επιλλέγουμε το Display. Τώρα τα περιεχόμενα του πίνακα εμφανίζονται στο Data Window. Με την επιλογή Rotate τα εμφανίζουμε οριζόντια ή κατακόρυφα.

Display DDD

Τώρα προχωρούμε στην ανάθεση τιμών στα στοιχεία του πίνακα a:

 => for (i = 0; i < argc - 1; i++)
a[i] = atoi(argv[i + 1]);

Επιλέγουμε το πλήκτρο Next και βλέπουμε τη σταδιακή εκχώρηση τιμών. Τα στοιχεία που μεταβάλλονται φωτίζονται.

Για να περάσουμε την εκτέλεση όλου του βρόχου, χρησιμοποιούμε το πλήκτρο Until. Ο GDB εκτελεί το πρόγραμμα μέχρι να βρεθεί σε αριθμό γραμμής μεγαλύτερο από τον τρέχοντα, δηλαδή εκτός βρόχου. Έτσι καταλήγουμε στη κλήση της συνάρτησης shell_sort

 => shell_sort(a, argc);

Σε αυτό το σημείο, τα περιεχόμενα του a πρέπει να είναι 8000 7000 5000 1000 4000. Επιλέγουμε πάλι Nextκαι προσπερνάμε το shell_sort. Ο DDD σταματά στο

 => for (i = 0; i < argc - 1; i++)
printf("%d ", a[i]);

και βλέπουμε οτι μετά το τέλος του shell_sort τα περιεχόμενα του a είναιe 1000, 1913, 4000, 5000, 7000, δηλαδή το shell_sortκάπως χάλασε τα περιεχόμενα του a.

Για να βρούμε τι έγινε ξαναεκτελούμε το πρόγραμμα. Αυτή τη φορά δε θα σταματήσουμε στην αρχικοποίηση, αφού γίνεται σωστά, αλλά θα πάμε κατ' ευθείαν στη κλήση του shell_sort. Διαγράφουμε το παλιό breakpoint επιλέγοντας το και πιέζοντας το Clear. Μετά δημιουργούμε νέο breakpoint στη γραμμή 35 ακριβώς πριν τη κλήση του shell_sort. Για τη νέα εκτέλεση επίλέγουμε Program => Run Again.

Για μια ακόμη φορά ο DDD σταματά στη γραμμή της κλήσης του shell_sort:

 => shell_sort(a, argc);

Τώρα θα εξετάσουμε πιο προσεκτικά τι συμβαίνει μέσα στο shell_sort. Επιλέγουμε Step για να μπούμε μέσα στη κλήση του shell_sort. Βρισκόμαστε τη πρώτη εκτελέσιμη γραμμή της συνάρτησης 

 => int h = 1;

ενώ το παράθυρο τερματικού μας ενημερώνει για τη συνάρτηση που μόλις μπήκαμε:

 (gdb) step
shell_sort (a=0x8049878, size=6) at sample.c:9
(gdb) _

Διαπιστώνουμε οτι η συνάρτηση shell_sort έχει κληθεί με δύο ορίσματα, τη διεύθυνση του πίνακα ακεραίων a=0x8049878) και τον ακέραιο size=6, δηλαδή το μέγεθος του πίνακα. Αυτή η πληροφόρηση λέγεται και  stack frame display, δηλαδή εμφανίζεται η περίληψη της στοίβας της καλούμενης συνάρτησης. Για να δούμε το σύνολο της στοίβας των συναρτήσεων επιλέγουμε από το Menu το Status => Backtrace. Επιλέγοντας μια γραμμή της στοίβας (ή με τα πλήκτρα Up και Down) μπορούμε να δούμε όλη τη στοίβα. Τα περιεχόμενα κάθε stack frame εμφανίζονται στο παράθυρο τερματικού του GDB.  

Backtrace DDD

Ας δούμε τώρα αν τα ορίσματα του shell_sort είναι σωστά. Επιστρέφουμε στο σωστό stack frame, εισάγουμε στο argument field την έκφραση a[0]@sizeκαι επιλέγουμε Print:

 (gdb) print a[0] @ size
$4 = {8000, 7000, 5000, 1000, 4000, 1913}
(gdb) _

Έκπληξη! Από πού ήρθε το 1913 ? Η απάντηση είναι απλή: το μέγεθος του πίνακα  a, όπως πέρασε στη συνάρτηση shell_sort με το όρισμα size, είναι λάθος: ο πίνακας φαίνεται πιό μεγάλος κατά μια θέση. Η τιμή 1913 είναι μια τυχαία τιμή που βρίσκεται στη μνήμη μετά το τέλος του πίνακα a. Και αυτή η τιμή ταξινομέιται κανονικά

Σημειώστε οτι αντί για Print θα μπορούσαμε να είχαμε επιλέξει Display. Επίσης θα μπορούσαμε να είχαμε εισάγει τις αντίστοιχες εντολές κατ' ευθείαν στο παράθυρο τερματικού του GDB.

Για να βεβαιωθούμε οτι βρήκαμε το σφάλμα, μπορούμε να θέσουμε στο size τη σωστή τιμή του. Στο πηγαίο κώδικα επιλέγουμς το size και στη συνέχεια, από τη γραμμή εργαλείων επιλέγουμε Set. Εμφανίζεται ένα παράθυρο διαλόγου, όπου εισάγουμε τη τιμή και επιλέγουμε OK ή Apply. Στο παράθυρο τερματικού εμφανίζεται και η σύνταξη της αντίστοιχης εντολής GDB.

Set DDD

Στη συνέχεια επιλέγουμε Finish και η εκτέλεση συνεχίζεται μέχρι την έξοδο από τη συνάρτηση shell_sort:

 (gdb) set variable size = 5
(gdb) finish
Run till exit from #0 shell_sort (a=0x8049878, size=5) at sample.c:9
0x80486ed in main (argc=6, argv=0xbffff918) at sample.c:35
(gdb) _
Πετύχαμε! Στο Data Window εμφανίζονται τώρα οι σωστές τιμές 1000, 4000, 5000, 7000, 8000.

Finish DDD

Μπορούμε να επιβεβαιώσουμε οτι αυτές είναι οι τιμές που τελικά θα εμφανίσει το πρόγραμμά μας στη πρότυπη έξοδο (stdout) αν συνεχίσουμε την εκτέλεση του προγράμματος. Επιλέγουμε τπ πλήκτρο Cont για τη συνέχιση της εκτέλεσης. Στο παράθυρο τερματικού του GDB βλέπουμε

 (gdb) cont
1000 4000 5000 7000 8000

Program exited normally.
(gdb) _

Το μήνυμα Program exited normally. είναι από το GDB; σημαίνει οτι τελείωσε η εκτέλεση του προγράμματος sample.

Τώρα που βρήκαμε το σφάλμα, μπορούμε να διορθώσουμε το πηγαίο κώδικα. Διορθώνουμε το αρχείο sample.c, ώστε η γραμμή

 shell_sort(a, argc);

να μεταβληθεί σε 

 shell_sort(a, argc - 1);

Η διόρθωση μπορεί να γίνει είτε μέσω του συνηθισμένου μας editor (σε ξεχωριστό παράθυρο τερματικού) ή από το Menu, Source => Edit Source. Στη συνέχεια μεταγλωττίζουμε πάλι και παράγουμε το νέο εκτελέσιμο sample

 $ gcc -g -o sample sample.c
$ _

και πιστοποιουμε την ορθότητα μέσω του Menu, Program => Run Again .

 (gdb) run
`sample' has changed; re-reading symbols.
Reading in symbols...done.
Starting program: sample 8000 7000 5000 1000 4000
1000 4000 5000 7000 8000

Program exited normally.
(gdb) _

Μια ενδιαφέρουσα δυνατότητα του DDD είναι η ταυτόχρονη παρακολούθηση της εκτέλεσης του προγράμματος, τόσο σε επίπέδο πηγαίου κώδικα αλλά και σε assembly (μνημονική γλώσσα μηχανής). Αυτό μπορεί να γίνει με την επιλογή Menu, View => Machine Code Window. Τα βήματα του προγράμματος ακολουθύνται και στα δύο παράθυρα, οπότε μπορούμε να μελετήσουμε την αντιστοιχία εντολών γλώσσας υψηλού επιπέδου με αυτές γλώσσας μηχανής.

O DDD έχει αρκετές ακόμη επιλογές, όπως για παράδειγμα παρακολούθηση των καταχωρητών (Menu, Status => Registers), δημιουργία και παρακολούθηση Σημάτων (Menu, Status => Signals), lπαρακολούθηση τιμών μεταβλητών (Watch),  έλεχγος περιεχομένων τμημάτων μνήμης (Menu, Data => Memory) και πολλά άλλα.  

Κλείνουμε το DDD από το Menu, με Program => Exit ή Ctrl+Q ή απλά κλείνοντας το παράθυρο.