Τι εκτυπώνει το πρόγραμμα; [Λύση]

#include <stdio.h>

int calculate(void);

int calculate(void)
{
int i;

int p[9];

for ( i = 0; i <= 3; i++ )
p[i*3+2]+=16;
}

int main(int argc, char** argv)
{

calculate();
printf("Hello, World!\n");

return 0;
}

Μεταγλωττίζουμε το πρόγραμμα της προηγούμενης εγγραφής και εκτελούμε (πλατφόρμα Linux/i386, gcc 4.0.x).

simos@home /tmp
$ gcc calculate.c -o calculate

simos@home /tmp
$ ./calculate

simos@home /tmp
$ _

(Αν η προτροπή του φλοιού (shell) φαίνεται παράξενη, χρησιμοποιώ tcsh με tcshrc).

Όπως βλέπουμε, το πρόγραμμα calculate δεν εκτυπώνει τίποτα, ενώ στον κώδικα υπάρχει αναφορά να εκτυπώσει Hello, World!. Στον κώδικα δεν υπάρχει κάποια εντολή που να παρακάμπτει την εκτύπωση του μηνύματος. Τι συμβαίνει; Μπορεί η κλήση της αθώας συνάρτησης calculate() να επηρεάσει την ροή του προγράμματος;

Ας δούμε τι γίνεται στη συνάρτηση calculate(). Όταν η μεταβλητή i παίρνει την τιμή 3, γίνεται τροποποίηση του πεδίου p[11] του πίνακα p. Ο πίνακας p διαθέτει 9 θέσεις (p[0] μέχρι p[8]), οπότε σε τι πράγμα τροποποιήσαμε την τιμή;

Η C/C++ είναι μια γλώσσα untyped, που σημαίνει μεταξύ άλλων ότι είναι καθήκον και υποχρέωση του προγραμματιστή να προσέχει να μην γράφει σε θέσεις μνήμης που δεν έχει δεσμεύσει.

Οπότε, στο παράδειγμά μας πού γράψαμε στην μνήμη;

Με την εντολή objdump -d calculate μπορούμε να δούμε το πρόγραμμα σε μορφή γλώσσας μηχανής.
Ακολουθεί απόσπασμα από τον κώδικα της συνάρτησης main().

...
80483c4: b8 00 00 00 00 mov $0x0,%eax
80483c9: 83 c0 0f add $0xf,%eax
80483cc: 83 c0 0f add $0xf,%eax
80483cf: c1 e8 04 shr $0x4,%eax
80483d2: c1 e0 04 shl $0x4,%eax
80483d5: 29 c4 sub %eax,%esp
80483d7: e8 a0 ff ff ff call 804837c <calculate>
80483dc: 83 ec 0c sub $0xc,%esp
80483df: 68 98 84 04 08 push $0x8048498
80483e4: e8 bf fe ff ff call 80482a8 <puts @plt>
80483e9: 83 c4 10 add $0x10,%esp
80483ec: b8 00 00 00 00 mov $0x0,%eax
80483f1: c9 leave
80483f2: c3 ret
80483f3: 90 nop
-- τέλος της main() --

Προσέξτε την κλήση της συνάρτησης calculate() [θέση 80483d7]. Στον δομημένο προγραμματισμό έχουμε κλήσεις συναρτήσεων/υπορουτινών που μεταφέρουν την ροή εκτέλεσης σε ένα άλλο κομμάτι προγράμματος, και μετά επιστρέφουν για να προχωρήσουν στην επόμενη εντολή. Πως θυμάται το πρόγραμμα σε εκτέλεση ποια είναι η επόμενη εντολή (στην περίπτωση μας, η εντολή στην διεύθυνση 80483dc);

Την θυμάται επειδή την τοποθετεί στην στοίβα, σε μια περιοχή μνήμης που είναι εύκολα προσβάσιμη από την καλούμενη συνάρτηση (calculate() στην περίπτωσή μας). Στο παράδειγμα

int calculate(void)
{
int i;

int p[9];

το p[11] δείχνει στην διεύθυνση μνήμης που περιέχει την επόμενη εντολή που θα εκτελέσει ο επεξεργαστής μόλις ολοκληρώσει την εκτέλεση της συνάρτησης calculate(). Ένα ενδιαφέρον σημείο είναι ότι το p[9] είναι το ίδιο με την μεταβλητή i! Δηλαδή αν τροποποιήσουμε κατά λάθος το p[9], θα αλλάξει και το i, διότι δείχνουν στην ίδια περιοχή της μνήμης.

Στο πρόγραμμά μας προσθέτουμε 16 στο p[11]. Γιατί 16; Η σωστή τιμή του p[11] είναι 0x80483dc, εμείς όμως θέλουμε να παρακάμψουμε την εκτύπωση του μηνύματος (printf()). Θέλουμε δηλαδή να πάμε στην διεύθυνση 0x80483ec (εντολή return 0;).

0x80483ec – 0x80483dc = 0x10 ή 16 στο δεκαδικό σύστημα, οπότε πρέπει να προσθέσουμε 16 στην διεύθυνση επιστροφής.

Leave a Reply

%d bloggers like this: