Επόμενο Προηγούμενο Περιεχόμενα
Η δημιουργία ενός εκτελέσιμου είναι μια από τις πιο
βασικές διαδικασίες σε οποιοδήποτε υπολογιστικό σύστημα.
Την εποχή του 1950-1960 τα πράγματα ήταν σχετικά "απλά".
Ο προγραμματιστής έγραφε τον αλγόριθμο σε μνημονική
γλώσσα assembly και τον μετέφραζε με το χέρι σε γλώσσα
μηχανής. Ύστερα τον περνούσε με κάποιο τρόπο (βύσματα,
διάτρητες κάρτες) στο σύστημα και προσευχόταν όλα να πάνε
καλά!
Η πρώτη προσπάθεια αυτοματοποίησης ήρθε με την δημιουργία
των assemblers. Τώρα πια ο ίδιος ο υπολογιστής έκανε την
κουραστική δουλειά της μετάφρασης από assembly σε γλώσσα
μηχανής. Οι (τεμπέληδες :) )προγραμματιστές, όμως, δεν
αρκέστηκαν σε αυτό. Ανέπτυξαν γλώσσες υψηλού επιπέδου και
δημιούργησαν compilers οι οποίοι τις μετέφραζαν σε γλώσσα
assembly. Οι assemblers που ήδη υπήρχαν ολοκλήρωναν τη
διαδικασία αλλά τα πράγματα δε σταμάτησαν ούτε εδώ!
Ακολούθησε η χρυσή εποχή του δομημένου προγραμματισμού
και των modules. Αποφασίστηκε ότι ήταν σοφό να
επαναχρησιμοποιείται ο κώδικας που υπήρχε ήδη και έτσι
έπρεπε να βρεθεί ένας τρόπος να μπορούν να συνενώνονται
κομμάτια κώδικα (σε δυαδική μορφή) που βρίσκονταν σε
διαφορετικά αρχεία.
Υπάρχουν τρία βασικά είδη 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 να μπορέσει να βρει τις διευθύνσεις των
συναρτήσεων και των δεδομένων.
Όταν ζητάμε από το λειτουργικό να εκτελέσει ένα
πρόγραμμα, γίνονται πολλά περισσότερα από όσα φαίνονται
εκ πρώτης όψεως. Σε γενικές γραμμές ακολουθούνται τα εξής
βήματα (για τα ELF εκτελέσιμα τα πράγματα διαφέρουν
λίγο):
-
Αρχικά το λειτουργικό διαβάζει τον header του
εκτελέσιμου για να πάρει απαραίτητες πληροφορίες, όπως:
-
Αν όντως πρόκειται για εκτελέσιμο που μπορεί να
τρέξει στον υπολογιστή.
-
Πόση μνήμη απαιτεί και τι ιδιότητες έχει κάθε τμήμα
(segment) του εκτελέσιμου ( πχ read-only,
executable κτλ).
-
Ποια shared objects απαιτεί το εκτελέσιμο.
-
Το λειτουργικό αποδίδει στη διεργασία τη μνήμη που
χρειάζεται και φορτώνει τα διάφορα τμήματα στη μνήμη.
-
O dynamic linker φορτώνει στο address space της
διεργασίας τις βιβλιοθήκες που χρειάζεται.
-
Γίνεται relocation στο εκτελέσιμο και τις βιβλιοθήκες.
Ως μέρος της διαδικασίας του relocation, διορθώνονται
οι αναφορές σε συναρτήσεις/δεδομένα των βιβλιοθηκών που
φορτώθηκαν. Αυτό είναι το θέμα του επόμενου τμήματος.
-
Τέλος, ο έλεγχος μεταφέρεται στο entry point του
προγράμματος. Αυτό αποτελεί τη διεύθυνση της πρώτης
εντολής που πρόκειται να εκτελεστεί.
Το relocation (επανατοποθέτηση) είναι η διαδικασία κατά
την οποία γίνονται διορθώσεις στην εικόνα ενός
προγράμματος επειδή αυτή μπορεί να τοποθετηθεί στη μνήμη
σε κάποιο αυθαίρετο σημείο. Αυτό συμβαίνει πάντα για τα
shared objects ενώ για τα εκτελέσιμα γίνεται σε πολύ
μικρότερο βαθμό. Λόγω της εικονικής μνήμης, μπορούμε να
φορτώνουμε το εκτελέσιμο πάντα στο ίδιο σημείο. Για τα
shared objects, από την άλλη, δε γίνεται να υποθέσουμε
πως δε θα υπάρχει σύγκρουση, διότι πρέπει να μπορούν να
συνυπάρχουν με οποιοδήποτε άλλο shared object.
Η διαδικασία του relocation είναι σχετικά πολύπλοκη και
εδώ θα ασχοληθούμε μόνο με ένα υποσύνολο της. Αυτό το
υποσύνολο σχετίζεται με τις διορθώσεις των αναφορών σε
σύμβολα τα οποία βρίσκονται σε βιβλιοθήκες και συνδέονται
δυναμικά με το εκτελέσιμο.
H χρήση των shared objects για dynamic linking προσφέρει
πολλά πλεονεκτήματα σε σχέση με το static linking. Μερικά
από αυτά είναι η μείωση του μεγέθους των εκτελέσιμων, οι
αυξημένες δυνατότητες για επαναχρησιμοποίηση κώδικα και η
δυνατότητα επέκτασης των εφαρμογών (πχ plug-ins). Όλα
αυτά όμως έρχονται με ένα (μικρό, ομολογουμένως) τίμημα.
Επειδή οι βιβλιοθήκες φορτώνονται σε κάποιο αυθαίρετο
σημείο της εικονικής μνήμης της διεργασίας, οι
διευθύνσεις των συναρτήσεων/δεδομένων τους δεν είναι
γνωστές από πριν. Έτσι οι κλήσεις/αναφορές σε αυτά δεν
είναι ολοκληρωμένες στο εκτελέσιμο.
Υπάρχουν διάφοροι τρόποι για να αντιμετωπιστεί το
πρόβλημα αυτό. Έτσι οι κλήσεις σε συναρτήσεις βιβλιοθηκών
μπορεί να έχουν μια από τις παρακάτω μορφές (και όχι
μόνο, αυτές είναι οι πιο βασικές):
-
call 0x???????? : Για τη διόρθωση, πρέπει σε κάθε κλήση
της συνάρτησης ο dynamic linker να αλλάξει την τιμή
στην πραγματική διεύθυνση της συνάρτησης. πχ call
0x40001000. Το πρόβλημα είναι πως αν η συνάρτηση
καλείται σε 100 διαφορετικά σημεία, πρέπει καταρχάς το
εκτελέσιμο να περιέχει πληροφορίες για όλα αυτά τα
σημεία και ο dynamic linker είναι αναγκασμένος να κάνει
100 διορθώσεις.
-
call [func1_offset] : στη διεύθυνση μνήμης func1_offset
(που είναι καθορισμένη από πριν) ο dynamic linker
τοποθετεί τη διεύθυνση της επιθυμητής συνάρτησης. Όλες
οι κλήσεις προς αυτή τη συνάρτηση διαβάζουν τη
διεύθυνση από τη συγκεκριμένη θέση μνήμης και την
καλούν έμμεσα (indirect call). Έτσι αποφεύγονται οι
πολλαπλές διορθώσεις, με ένα μικρό κόστος στην ταχύτητα
εκτέλεσης. Τα PE (Portable Executable) format που
χρησιμοποιούν τα MS Windows στηρίζεται σε αυτό το
μοντέλο.
-
call jmp_func : στη διεύθυνση jmp_func βρίσκεται μια
εντολή jmp [func_offset]. Στη διεύθυνση func_offset ο
dynamic linker τοποθετεί τη διεύθυνση της επιθυμητής
συνάρτησης. Γιατί όλη αυτή η ταλαιπωρία; Ο μηχανισμός
αυτός προσφέρει τη δυνατότητα οι διορθώσεις να γίνονται
κατά τη διάρκεια της εκτέλεσης του προγράμματος, όταν
υπάρχει ανάγκη, και όχι απαραίτητα όλες μαζί κατά τη
φόρτωση του προγράμματος. Το ELF χρησιμοποιεί μια
παραλλαγή του μοντέλου αυτού και θα το εξετάσουμε
αναλυτικότερα παρακάτω.
Επόμενο Προηγούμενο Περιεχόμενα