Magaz, The Greek Linux Magazine
Magaz Logo

Επόμενο  Προηγούμενο  Περιεχόμενα

2. Διαδικασία Δημιουργίας και Φόρτωσης Εκτελέσιμου

2.1 Εισαγωγή

Η δημιουργία ενός εκτελέσιμου είναι μια από τις πιο βασικές διαδικασίες σε οποιοδήποτε υπολογιστικό σύστημα. Την εποχή του 1950-1960 τα πράγματα ήταν σχετικά "απλά". Ο προγραμματιστής έγραφε τον αλγόριθμο σε μνημονική γλώσσα assembly και τον μετέφραζε με το χέρι σε γλώσσα μηχανής. Ύστερα τον περνούσε με κάποιο τρόπο (βύσματα, διάτρητες κάρτες) στο σύστημα και προσευχόταν όλα να πάνε καλά!

Η πρώτη προσπάθεια αυτοματοποίησης ήρθε με την δημιουργία των assemblers. Τώρα πια ο ίδιος ο υπολογιστής έκανε την κουραστική δουλειά της μετάφρασης από assembly σε γλώσσα μηχανής. Οι (τεμπέληδες :) )προγραμματιστές, όμως, δεν αρκέστηκαν σε αυτό. Ανέπτυξαν γλώσσες υψηλού επιπέδου και δημιούργησαν compilers οι οποίοι τις μετέφραζαν σε γλώσσα assembly. Οι assemblers που ήδη υπήρχαν ολοκλήρωναν τη διαδικασία αλλά τα πράγματα δε σταμάτησαν ούτε εδώ! Ακολούθησε η χρυσή εποχή του δομημένου προγραμματισμού και των modules. Αποφασίστηκε ότι ήταν σοφό να επαναχρησιμοποιείται ο κώδικας που υπήρχε ήδη και έτσι έπρεπε να βρεθεί ένας τρόπος να μπορούν να συνενώνονται κομμάτια κώδικα (σε δυαδική μορφή) που βρίσκονταν σε διαφορετικά αρχεία.

2.2 Η πορεία ενός bit: Από την πηγαίο κώδικα στο τελικό εκτελέσιμο

Υπάρχουν τρία βασικά είδη object αρχείων:

  • Relocatable Object File: Περιέχουν δεδομένα και κώδικα και είναι κατάλληλα για τη δημιουργία executable ή shared object αρχείων με τη διαδικασία του linking (Κατάληξη: ".o", ".obj" )
  • Executable Object File: Αρχεία κατάλληλα για εκτέλεση (Κατάληξη: στα Unix συστήματα συνήθως καμία, ".bin", ".exe")
  • Shared Object File: Μπορούν να γίνουν link με άλλα Shared Object ή Relocatable Object Files για να δημιουργήσουν Executable Files Επιπλέον μπορούν να συνδεθούν δυναμικά με το εκτελέσιμο κατά τη διάρκεια της φόρτωσης του. (Κατάληξη: ".so", ".dll") ????
Το παρακάτω σχήμα δείχνει συνοπτικά τα στάδια που περνάει ένα πρόγραμμα από τη στιγμή της δημιουργίας του μέχρι την εκτέλεση.

Το παραπάνω πρόγραμμα αποτελείται από δύο modules (Relocatable Object File 1 και 2). Επιπλέον, χρησιμοποιεί δύο "βιβλιοθήκες" (Shared Object File 1 και 2). Η πρώτη συνδέεται στατικά στο πρόγραμμα μας, δηλαδή ο κώδικας της συγχωνεύεται στο τελικό object αρχείο. Η δεύτερη συνδέεται δυναμικά. Στην περίπτωση αυτή, στο στάδιο του linking δε γίνεται συγχώνευση κώδικα, αλλά εισάγονται πληροφορίες ώστε όταν φορτωθεί το πρόγραμμα ο dynamic linker να μπορέσει να βρει τις διευθύνσεις των συναρτήσεων και των δεδομένων.

2.3 Το φόρτωμα του προγράμματος

Όταν ζητάμε από το λειτουργικό να εκτελέσει ένα πρόγραμμα, γίνονται πολλά περισσότερα από όσα φαίνονται εκ πρώτης όψεως. Σε γενικές γραμμές ακολουθούνται τα εξής βήματα (για τα ELF εκτελέσιμα τα πράγματα διαφέρουν λίγο):

  1. Αρχικά το λειτουργικό διαβάζει τον header του εκτελέσιμου για να πάρει απαραίτητες πληροφορίες, όπως:
    • Αν όντως πρόκειται για εκτελέσιμο που μπορεί να τρέξει στον υπολογιστή.
    • Πόση μνήμη απαιτεί και τι ιδιότητες έχει κάθε τμήμα (segment) του εκτελέσιμου ( πχ read-only, executable κτλ).
    • Ποια shared objects απαιτεί το εκτελέσιμο.
  2. Το λειτουργικό αποδίδει στη διεργασία τη μνήμη που χρειάζεται και φορτώνει τα διάφορα τμήματα στη μνήμη.
  3. O dynamic linker φορτώνει στο address space της διεργασίας τις βιβλιοθήκες που χρειάζεται.
  4. Γίνεται relocation στο εκτελέσιμο και τις βιβλιοθήκες. Ως μέρος της διαδικασίας του relocation, διορθώνονται οι αναφορές σε συναρτήσεις/δεδομένα των βιβλιοθηκών που φορτώθηκαν. Αυτό είναι το θέμα του επόμενου τμήματος.
  5. Τέλος, ο έλεγχος μεταφέρεται στο entry point του προγράμματος. Αυτό αποτελεί τη διεύθυνση της πρώτης εντολής που πρόκειται να εκτελεστεί.

2.4 Relocation - Εναλλακτικοί τρόποι για τη διόρθωση των αναφορών στο dynamic linking.

Το relocation (επανατοποθέτηση) είναι η διαδικασία κατά την οποία γίνονται διορθώσεις στην εικόνα ενός προγράμματος επειδή αυτή μπορεί να τοποθετηθεί στη μνήμη σε κάποιο αυθαίρετο σημείο. Αυτό συμβαίνει πάντα για τα shared objects ενώ για τα εκτελέσιμα γίνεται σε πολύ μικρότερο βαθμό. Λόγω της εικονικής μνήμης, μπορούμε να φορτώνουμε το εκτελέσιμο πάντα στο ίδιο σημείο. Για τα shared objects, από την άλλη, δε γίνεται να υποθέσουμε πως δε θα υπάρχει σύγκρουση, διότι πρέπει να μπορούν να συνυπάρχουν με οποιοδήποτε άλλο shared object.

Η διαδικασία του relocation είναι σχετικά πολύπλοκη και εδώ θα ασχοληθούμε μόνο με ένα υποσύνολο της. Αυτό το υποσύνολο σχετίζεται με τις διορθώσεις των αναφορών σε σύμβολα τα οποία βρίσκονται σε βιβλιοθήκες και συνδέονται δυναμικά με το εκτελέσιμο.

H χρήση των shared objects για dynamic linking προσφέρει πολλά πλεονεκτήματα σε σχέση με το static linking. Μερικά από αυτά είναι η μείωση του μεγέθους των εκτελέσιμων, οι αυξημένες δυνατότητες για επαναχρησιμοποίηση κώδικα και η δυνατότητα επέκτασης των εφαρμογών (πχ plug-ins). Όλα αυτά όμως έρχονται με ένα (μικρό, ομολογουμένως) τίμημα. Επειδή οι βιβλιοθήκες φορτώνονται σε κάποιο αυθαίρετο σημείο της εικονικής μνήμης της διεργασίας, οι διευθύνσεις των συναρτήσεων/δεδομένων τους δεν είναι γνωστές από πριν. Έτσι οι κλήσεις/αναφορές σε αυτά δεν είναι ολοκληρωμένες στο εκτελέσιμο.

Υπάρχουν διάφοροι τρόποι για να αντιμετωπιστεί το πρόβλημα αυτό. Έτσι οι κλήσεις σε συναρτήσεις βιβλιοθηκών μπορεί να έχουν μια από τις παρακάτω μορφές (και όχι μόνο, αυτές είναι οι πιο βασικές):

  1. call 0x???????? : Για τη διόρθωση, πρέπει σε κάθε κλήση της συνάρτησης ο dynamic linker να αλλάξει την τιμή στην πραγματική διεύθυνση της συνάρτησης. πχ call 0x40001000. Το πρόβλημα είναι πως αν η συνάρτηση καλείται σε 100 διαφορετικά σημεία, πρέπει καταρχάς το εκτελέσιμο να περιέχει πληροφορίες για όλα αυτά τα σημεία και ο dynamic linker είναι αναγκασμένος να κάνει 100 διορθώσεις.

  2. call [func1_offset] : στη διεύθυνση μνήμης func1_offset (που είναι καθορισμένη από πριν) ο dynamic linker τοποθετεί τη διεύθυνση της επιθυμητής συνάρτησης. Όλες οι κλήσεις προς αυτή τη συνάρτηση διαβάζουν τη διεύθυνση από τη συγκεκριμένη θέση μνήμης και την καλούν έμμεσα (indirect call). Έτσι αποφεύγονται οι πολλαπλές διορθώσεις, με ένα μικρό κόστος στην ταχύτητα εκτέλεσης. Τα PE (Portable Executable) format που χρησιμοποιούν τα MS Windows στηρίζεται σε αυτό το μοντέλο.

  3. call jmp_func : στη διεύθυνση jmp_func βρίσκεται μια εντολή jmp [func_offset]. Στη διεύθυνση func_offset ο dynamic linker τοποθετεί τη διεύθυνση της επιθυμητής συνάρτησης. Γιατί όλη αυτή η ταλαιπωρία; Ο μηχανισμός αυτός προσφέρει τη δυνατότητα οι διορθώσεις να γίνονται κατά τη διάρκεια της εκτέλεσης του προγράμματος, όταν υπάρχει ανάγκη, και όχι απαραίτητα όλες μαζί κατά τη φόρτωση του προγράμματος. Το ELF χρησιμοποιεί μια παραλλαγή του μοντέλου αυτού και θα το εξετάσουμε αναλυτικότερα παρακάτω.

Επόμενο  Προηγούμενο  Περιεχόμενα


Valid HTML 4.01!   Valid CSS!