Αποσφαλμάτωση στις γλώσσες C/C++ και ευκολίες του vim

Περιεχόμενα

  1. Αποσφαλμάτωση
  2. Ευκολίες του vim

Αποσφαλμάτωση

"Ο υπολογιστής έχει ένα καλό: Κάνει ακριβώς Ο,ΤΙ ΤΟΥ ΛΕΜΕ.
Επίσης έχει και ένα κακό: Κάνει ΑΚΡΙΒΩΣ ό,τι του λέμε!"

― Στέφανος Σταμάτης

Οι μεταγλωττιστές βρίσκουν τα συντακτικά λάθη στα προγράμματά μας. Δυστυχώς, στην Πληροφορική υπάρχει κάτι σαν θεώρημα, που λέει ότι δεν θα κατασκευαστεί ποτέ ένας μεταγλωττιστής που θα βρίσκει λογικά λάθη. Ωστόσο υπάρχουν τρόποι να πολεμήσουμε τα σφάλματα (bugs) ενός προγράμματός μας, να κάνουμε δηλαδή αποσφαλμάτωση (debugging). Οι δύο παρακάτω υποενότητες αφορούν τις γλώσσες προγραμματισμού C και C++.

Πρόληψη με assert-ions

Ως γνωστόν, η πρόληψη είναι καλύτερη από τη θεραπεία. Αυτό δεν θα μπορούσε παρά να ισχύει και για τα σφάλματα ενός προγράμματός μας. Για αυτό είναι καλό όταν θεωρούμε κάτι δεδομένο στον κώδικά μας, να το εξασφαλίζουμε με τη συνάρτηση assert() του assert.h. Π.χ. έστω η παρακάτω συνάρτηση:
	int  get_value (int array[], int i)
	{
		return  array[i];
	}
Η συνάρτηση είναι απλή και φαίνεται να κάνει μια χαρά τη δουλειά της ― επιστρέφει το i-οστό στοιχείο του πίνακα array. Όμως για να προλάβουμε τυχόν segmentation fault εν τη γενέσει τους, λόγω κακής κλήσης της συνάρτησης, καλό είναι να ξαναγράψουμε το παραπάνω, εφόσον μιλάμε για τη γλώσσα C, ως εξής:
	#include <assert.h>

	int  get_value (int array[], int i)
	{
		assert(i >= 0);
		return  array[i];
	}
Αν παραβιαστεί η συνθήκη της assert(), το πρόγραμμα τερματίζει άμεσα, τυπώνοντας και τη γραμμή του πηγαίου αρχείου στην οποία απέτυχε η assertion. Περισσότερες πληροφορίες υπάρχουν στην ενότητα Β6 του Παραρτήματος Β του βιβλίου των Kernighan και Ritchie «Η Γλώσσα Προγραμματισμού C». Στη C++ αντί για την assert(), καλό είναι να χρησιμοποιούμε εξαιρέσεις.

Προειδοποιήσεις του gcc/g++

Κατά τη μεταγλώττιση, μπορούμε να ζητήσουμε από τον μεταγλωττιστή, μέσω παραμέτρων, να μας τυπώνει κάποιες προειδοποιήσεις, οι οποίες ίσως αποδειχθούν εξαιρετικά χρήσιμες. Παρακάτω υπάρχουν μερικές παράμετροι προειδοποιήσεων, που ισχύουν και για τους δύο μεταγλωττιστές gcc και g++. Για περισσότερες πληροφορίες man gcc ή info gcc. Τέλος η παράμετρος -s (το `s' προέρχεται από το `strip') απογυμνώνει το εκτελέσιμο από πληροφορίες αποσφαλμάτωσης και έτσι το αρχείο αυτό γίνεται πιο μικρό. Εξυπακούεται ότι η -s δεν θα πρέπει να χρησιμοποιείται μαζί με τις παραμέτρους αποσφαλμάτωσης (όπως η -g3).

Αποσφαλματωτής gdb

"The debugger isn't a substitute for good thinking. But, in some cases, thinking isn't a substitute for a good debugger either. The most effective combination is good thinking and a good debugger."
― Steve McConnell, Code Complete

Αναλυτικές οδηγίες για τη χρήση του gdb υπάρχουν στο εγχειρίδιο χρήσης του (man gdb ή info gdb), καθώς και στην Ενότητα 3 του Unix Programming Tools. Εδώ θα αναφερθούν εν συντομία κάποιες οδηγίες.

Έστω ότι θέλουμε να αποσφαλματώσουμε ένα αρχείο πηγαίου κώδικα test.c. Τότε το μεταγλωττίζουμε με την παράμετρο -g3-g):
gcc -g3 -o test test.c
Αντί για gcc μπορούμε να έχουμε και τον g++, αν μιλάμε για κώδικα C++. Η διαδικασία παραμένει ακριβώς η ίδια.

Έπειτα ξεκινάμε τον αποσφαλματωτή:
gdb test
Θα εμφανιστεί μια γραμμή εντολών. Τώρα μπορούμε να ορίσουμε κάποια breakpoints, δηλαδή κάποια σημεία στα οποία θέλουμε να σταματήσει η εκτέλεση του προγράμματός μας. Με την εντολή
    b main
υποδεικνύουμε να σταματήσει το πρόγραμμα, όταν ξεκινήσει η main. Αντί της main, μπορούμε να βάλουμε οποιοδήποτε όνομα συνάρτησης. (Μάλιστα αν πατήσουμε Tab, καθώς γράφουμε το όνομα μιας συνάρτησης, γίνεται συνήθως auto-complete.) Με την
    b 10
δημιουργείται ένα breakpoint στη γραμμή 10 του αρχείου πηγαίου κώδικα. Αν θέλουμε να βάλουμε ένα breakpoint σε συγκεκριμένη γραμμή, συγκεκριμένου αρχείου, γράφουμε b αρχείο:γραμμή. Είναι σημαντικό να βάλουμε τουλάχιστον ένα breakpoint, γιατί αν δεν υπάρχει κανένα, το πρόγραμμα θα εκτελεστεί χωρίς να σταματήσει πουθενά. Επίσης, τα breakpoints σε γραμμές που είναι κενές, ή περιέχουν μόνο σχόλια ή δηλώσεις (και όχι εντολές) δεν δουλεύουν. Για να τρέξουμε το πρόγραμμα, γράφουμε
    r
ή run. Μετά την r μπορούμε να γράψουμε τα ορίσματα της γραμμής εντολών για το πρόγραμμά μας, εφόσον υπάρχουν. Με τη
    n
ή next, εκτελούμε την εμφανιζόμενη εντολή. Αν η εμφανιζόμενη εντολή περιέχει κλήση συνάρτησης, αντί για n μπορούμε να γράψουμε
    s
που είναι το λεγόμενο step, δηλαδή «μπες μέσα στη συνάρτηση». Αν δεν κάνουμε step και γράψουμε n, η συνάρτηση θα εκτελεστεί, θα επιστρέψει και θα συνεχίσουμε με την επόμενη γραμμή. Σχετικές εντολές είναι η
    u
ή until, με την οποία τελειώνουμε τους βρόχους, καθώς και η
    f
ή finish, με την οποία τελειώνουμε με την εκτέλεση της τρέχουσας συνάρτησης (δηλαδή της συνάρτησης μέσα στην οποία βρισκόμαστε).
Συνοπτικά με τις n, s, u και f προχωράμε στο πρόγραμμά μας, με διαφορετικούς όμως τρόπους.
Η σημαντικότερη εντολή πάντως είναι η print, αφού εμφανίζει τις τρέχουσες τιμές των μεταβλητών. Π.χ. μπορούμε να γράψουμε
    p Y[i]
Μπορούμε επίσης να τυπώσουμε οποιαδήποτε έκφραση της C (ή της C++). Π.χ.
    p (1+x*5)/2.0
ακόμα και
    p 1+1
Αν θέλουμε μετά από κάποιο breakpoint που συνέβη, να συνεχίσει το πρόγραμμα κανονικά γράφουμε
    c
ή continue.

Αξίζει να σημειωθεί ότι σε μερικά συστήματα είναι διαθέσιμη η εφαρμογή ddd, η οποία είναι μια εύχρηστη γραφική απεικόνιση του gdb. Μία ακόμα βολική εκδοχή του gdb είναι ο gdbtui. Ο gdbtui υπάρχει μαζί σχεδόν με όλες τις εκδόσεις του gdb και χρησιμοποιείται ακριβώς όπως ο τελευταίος. (Δεν υπάρχει στα μηχανήματα Sun του Τμήματος.) Τέλος, ένας άλλος αποσφαλματωτής, που εστιάζει στον έλεγχο της διαχείρισης μνήμης, είναι το valgrind, το οποίο είναι όμως διαθέσιμο μόνο για Linux. Κάποιες οδηγίες χρήσης του υπάρχουν εδώ. Φυσικά, όπου αναφέρεται ο g++, μπορούμε κάλλιστα να χρησιμοποιήσουμε τον gcc, εφόσον μεταγλωττίζουμε αρχεία *.c.

Segmentation fault

Στις περιπτώσεις που το πρόγραμμά μας παθαίνει segmentation fault (σφάλμα κατάτμησης), η τακτική που ακολουθούμε αρχικά για βρούμε το πού συνέβη το σφάλμα, έχει να κάνει με τα εξής βήματα:

Ευκολίες του vim