Ότι θέλατε να ρωτήσετε για την C...απλά δεν το ξέρετε ακόμα!

Αυτό το άρθρο, το οποίο είναι και το πρώτο μου σε αυτό το blog, σηματοδοτεί την αρχή μίας σειράς άρθρων από μέρος μου, όπου θα γράφω για διάφορα που αφορούν την γλώσσα προγραμματισμού C. Μιας και η συγγραφή αυτών των άρθρων γίνεται για το Linux Users Group του TΕΙ Κρήτης, μια ομάδα προγραμματιστών (ωω, ελάτε τώρα, όλοι λατρεύετε τον προγραμματισμό, απλά δεν το έχετε καταλάβει ακόμα :) σκέφθηκα ότι θα ήταν καλό να υπάρχει μία σειρά άθρων που βουτάνε στα βαθιά όσον αφορά την C.

HelloWorld.c


Ξεκινώντας λοιπόν, θα ήθελα να επισκεφθούμε το πρώτο πρόγραμμα που γράφει κάθε νεοφώτιστος προγραμματιστής όταν δοκιμάζει ή μαθαίνει μία νέα γλώσσα: Το "Hello World". Βαρετό, μπορεί να σκεφτήκατε ήδη. Και εγώ το ίδιο θα έλεγα στην θέση σας. Αλλά αφήστε με να το αιτιολογίσω. Σε αυτό το άρθρο, δεν θα συγγράψουμε απλά ένα πρόγραμμα "Hello World", αλλά θα δούμε και πως λειτουργεί η C και ο μεταγλωτιστής της (compiler) μελετώντας το. Τώρα, όσον αφορά το γιατί το "Hello world", τότε η απάντηση έχει δύο σκέλη:
  1. Είναι το πιο απλό πρόγραμμα πάνω στο οποίο μπορούμε να μελετήσουμε κάποια χαρακτηριστικά της C. Επίσης το μέγεθος του μας επιτρέπει να αναφερθούμε σε κάποια κύρια χαρακτηριστικά της C χωρίς να πλατειάζουμε και να αναλωνόμαστε σε πάρα πολλές λεπτομέρειες υλοποίησης.
  2. Είναι ίσως η καλύτερη εισαγωγή σε μία γλώσσα κατά τη γνώμη πολλών επιστημόνων υπολογιστών, και κυρίως του Dennis Ritchie. O λόγος για κάτι τέτοιο είναι απλός: Το "Hello world" είναι αρκετά απλό για την κατανόηση όσον αφορά αρχάριους, ενώ δε, είναι και αρκετά πλούσιο σε πληροφορίες όσον αφορά την σύνταξη και την σημασιολογία μιας γλώσσας για ένα έμπειρο προγραμματιστή.


Χωρίς άλλες καθυστερήσεις, ήρθε η ώρα της αποκάλυψης της συγγραφής του "Hello world" στην C:

#include <stdio.h>

int main(void)
{
    printf("Hello world!\n");
    return 0;
}

Ας προσπαθήσουμε τώρα να αναλύσουμε το κώδικα μας (όχι δεν είναι τόσο τρομακτικό όσο ακούγεται.)

  • Η πρώτη γραμμή, που περιέχει το #include <stdio.h>  είναι μία οδηγία για τον προεπεξεργαστή της C (preprocessor directive). Η συγκεκριμένη οδηγία λέει στον προεπεξεργαστή: Πήγαινε και βρες το αρχείο stdio.h και εισήγαγε το κείμενο του αρχείου αυτού εδώ. Ο λόγος για τον οποίο κάνουμε κάτι τέτοιο είναι απλός: Πιο κάτω καλούμε την συνάρτηση printf() της βασικής βιβλιοθήκης της C. Προτού γίνει μία κλίση, είναι απαραίτητο ο μεταγλωτιστής να γνωρίζει κάποια κύρια χαρακτηριστικά της συνάρτησης (κυρίως την υπογραφή της, δηλαδή τι παραμέτρους δέχεται, και τι τύπο δεδομένων επιστρέφει). Για να το κάνει αυτό πρέπει να γνωρίζει εκ των προτέρων την υπογραφή της συνάρτησης. 
  • Με το να εισάγουμε το αρχείο ολόκληρο στην θέση της οδηγίας βεβαιωνόμαστε κατά αυτόν τον τρόπο ότι έχει εισαχθεί το πρωτότυπο της συνάρτησης (function prototype). Η C από τεχνικής άποψης, όσον αφορά τους Compilers της και συγκεκριμένα την διαδικασία του Parsing, ακολουθεί την λογική top-down parsing, κάτι που σημαίνει ότι για να μπορέσει να κάνει μεταγλωτίσει τον κώδικα ο μεταγλωτιστής, πρέπει να έχει δει πιο πάνω τουλάχιστον μία φορά πληροφορίες για την συνάρτηση. 
  • Από την στιγμή λοιπόν που ο κώδικας που περιέχει πληροφορίες για την printf() έχει εισαχθεί και ο μεταγλωτιστής τον συναντάει πιο πριν από την πραγματική κλίση της συνάρτησης, τότε το πρόγραμμα είναι εντάξει.
  • Μία παρατήρηση: Πολύ απλά γνωρίζουν ότι τα αρχεία .h είναι αρχεία κεφαλίδων χωρίς να γνωρίζουν περισσότερα πράγματα για αυτές. Κάπου εδώ θα είναι χρήσιμο να πούμε ότι τα αρχεία αυτά, τουλάχιστον όσον αφορά την "στάνταρτ" βιβλιοθήκη της C, περιέχουν αρκετά πρωτότυπα συναρτήσεων, μαζί με διάφορα άλλα στοιχεία όπως είναι αναγνωριστικά τύπων, μεταβλητές, κλπ. Τουλάχιστον όσον αφορά συστήματα που χρησιμοποιούν την υποδομή μεταγλωτιστών GCC, η βασική βιβλιοθήκη της C είναι στο σύστημα και ονομάζεται glibc. Για περισσότερες πληροφορίες δείτε το C Standard Library. Σημείωση: Για να βρείτε, και να εξετάσετε τα αρχεία αυτά ενώ βρίσκεστε σε περιβάλλον gnu/linux μπορείτε να γράψετε στο terminal: whereis <header_file> π.χ whereis stdio.h. 
  • Η επόμενη γραμμή περιέχει την υπογραφή (και αρχή του ορισμού) της συνάρτησης main(). Η συνάρτηση main() είναι μία ειδική συνάρτηση, που ορίζει την αρχή της εκτέλεσης του προγράμμματος μας. 
  • Μία παρατήρηση: Έχουμε ορίσει τιμή επιστροφής ακέραιο, και αυτό διότι, όπως όταν εκτελείται μία συνάρτηση και μετά τερματιστεί μπορεί να γυρνάει μία τιμή στην καλούσα ρουτίνα, έτσι και σε επίπεδο διεργασιών (προγραμμάτων) όταν μία διεργασία τελείωσει την εκτέλεση της γυρνάει μία τιμή στην καλούσα διεργασία (δλδ το λειτουργικό σύστημα). Παρά το γεγονός ότι πολλοί στο περιβάλλον windows μπορεί να το αγνοούσαν, στα περιβάλλοντα unix/linux αυτή η δυνατότητα είναι ιδιαιτέρως χρήσιμη, καθώς μπορούμε να μελετήσουμε την τιμή επιστροφής γράφωντας στο terminal echo $?. Κάτι τέτοιο αποδεικνύεται χρήσιμο σε διάφορες καταστάσεις όπως π.χ το shell scripting, όπου για παράδειγμα, μπορεί το script να ακολουθεί διαφορετικό branch σε περίπτωση που η διεργασία δεν τερματιστεί ομαλά.
  • Η επόμενη γραμμή περιέχει μία κλίση σε μία συνάρτηση της βασική βιβλιοθήκης της C: την printf(). Η printf() τυπώνει μορφοποιημένο κείμενο στο ειδικό αρχείο που καθορίζει την στάνταρτ έξοδο (stdout).
  • Η επόμενη τιμή επιστρέφει την τιμή 0 στο λειτουργικό σύστημα όπως συζητήσαμε πιο πριν. Παρά το γεγονός ότι δεν υπάρχει κάποια τυποποίηση συνήθως η τιμή 0 ορίζει ομαλή εκτέλεση και τερματισμό της διεργασίας, ενώ οτιδήποτε άλλο είναι κωδικός σφάλματος.

Είναι όμως αυτό αρκετό;


Γράψαμε το κείμενο αυτό σε ένα επεξεργαστή κειμένου και το αποθηκεύσαμε στον υπολογιστή μας ως hello.c. Είναι όμως αυτό αρκετό;

Πολύ από εσάς δεν θα διστάσουν να συμφωνήσουν. Εγώ όμως θα διαφωνίσω μαζί σας. Και αυτό γιατί: Μέχρι τώρα αυτό που γράψαμε είναι απλά ένα απλό κείμενο. Τίποτα παραπάνω. Το μόνο που το ξεχωρίζει με ένα αντίστοιχο αρχείο κειμένου που μπορεί να περιέχει ένα ποίημα, είναι ότι είναι γραμμένο σε συγκεκριμένο αλφάβητο (ASCII) και σε συγκεκριμένη γραμματική δομή (C).

Παρά αυτήν την διαφορά το αρχείο μας δεν είναι τίποτα άλλο παρά (άχρηστα;) byte στην μονάδα αποθήκευσης του υπολογιστή μας, όσο δεν έχουμε ένα μεταγλωτιστή. Και αυτό διότι, ο μεταγλωτιστής είναι που πέρνει σαν είσοδο το αρχείο, και παράγει ένα εκτελέσιμο αρχείο, σε γλώσσα μηχανής. Χωρίς μεταγλωτιστή, το αρχείο μας είναι πολύ απλά ένα άχρηστο (σχεδόν) αρχείο κειμένουν και τίποτα περισσότερο.

Τί γίνεται όμως από την στιγμή που θα μεταγλωτιστεί;


Χωρίς να μπω σε λεπτομέρειες τεχνολογίας μεταγλωτιστών (αυτό είναι 1) πολύ σύνθετο, 2) απαιτεί τουλάχιστον ένα 3-4 φορές μεγαλύτερο άρθρο από αυτό), θα προσπαθίσω να δείξω τι παράγει ο μεταγλωτιστής και να κλείσω το άρθρο, γιατί κάπου εδώ φαντάζομαι θα έχετε αρχίσει να κουράζεστε και εσείς :)

Η έξοδος ενός μεταγλωτιστή είναι (...............σκόπιμη καθυστέρηση προκειμένου να κορυφωθεί η αγωνία!): ένα εκτελέσιμο αρχείο. (Κάπου εδώ κάνω κινήσεις να αποφύγω "εισερχόμενες" ντομάτες).

Τι είναι όμως αυτά τα περιβόητα εκτελέσιμα αρχεία;

Ο υπολογιστής, (όπως γνωρίζουμε όλοι) καταλαβαίνει μόνο μία γλώσσα: την γλώσσα μηχανής. Η γλώσσα μηχανής αποτελείται από bit & byte. Εσωτερικά ένα εκτελέσιμο αρχείο μοιάζει κάπως έτσι:
  1. 0000000 457f 464c 0101 0001 0000 0000 0000 0000
  2. 0000010 0002 0003 0001 0000 8310 0804 0034 0000
  3. 0000020 06c4 0000 0000 0000 0034 0020 0007 0028
  4. 0000030 001e 001b 0006 0000 0034 0000 8034 0804
  5. 0000040 8034 0804 00e0 0000 00e0 0000 0005 0000

Αγνοήστε την πρώτη στήλη που είναι το memory offset (απόσταση από την αρχή του προγράμματος), όλα τα υπόλοιπα είναι απλά byte. Δεν μοιάζουν; Κι όμως. Είναι byte, απλά σε δεκαεξαδική αναπαράσταση (hex). Όσον αφορά την δεκαεξαδική αναπαράσταση δύο hex symbols αντιστοιχούν σε ένα byte. Δηλαδή το FF σε δυαδικό είναι 11111111 αφού το F είναι το 15 στο δεκαδικό και το 1111 σε δυαδικό.

Στην πραγματικότητα δεν είναι έτσι ακριβώς τα πράγματα, γιατί παίζει ρόλο και το endianness του επεξεργαστή  αλλά αυτό είναι αρκετά κοντά για εμάς για λόγους κατανόησης.

Σημείωση: Για αυτούς που αισθάνονται την ανάγκη να δουν ένα ολόκληρο πρόγραμμα σε machine code έχω ανεβάσει τον κώδικα του helloworld εδώ --> http://pastebin.com/00r9iujE

Και τι παίζει με την Assembly;

Κάπου εδώ σας ακούω να διαμαρτύρεστε: "-Και τι παίζει με την Assembly; -Μου είχαν πει ότι θα έχει Assembly! -Είναι αιρετικός! Κάψτε τον!"

Μισό λεπτό αγαπητοί μου. Η assembly είναι σχεδόν το ίδιο πράγμα με το machine code.  Για την ακρίβεια είναι ανθρώπινα μνημονικά για εντολές του επεξεργαστή. Γιατί για πολλούς ανθρώπους το  mov ebp, esp λέει πολύ περισσότερα πράγματα από το 457f 464c (τυχαίο παράδειγμα)

ΣΗΜΕΙΩΣΗ: Μπορεί εμάς τους ανθρώπους να μας βολεύει η Assembly, αλλά για τον υπολογιστή δεν σημαίνει τίποτα (θυμηθείτε ότι ο υπολογιστής μιλάει μόνο σε 1 και 0 και όχι σε λέξεις) . Για αυτό όταν ένας άνθρωπος γράφει με το χέρι Assembly, είναι ανάγκη ο κώδικας να περαστεί από assembler και να παραχθεί κώδικας μηχανής (δηλαδή να γίνει μετάφραση του mov ebp, esp σε 457f 464c

Για όποιον ενδιαφέρεται να δει κώδικα assembly, εδώ είναι ο κώδικας ενός helloworld, σε x86 assembly, μεταγλωτισμένο από τον clang/llvm --> http://ecksit.wordpress.com/2011/01/01/hello-world-in-llvm/

Δεν υπάρχουν σχόλια:

Δημοσίευση σχολίου