Magaz, The Greek Linux Magazine
Magaz Logo

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

3. Το ELF

3.1 Τι είναι το ELF;

Το Εxecutable and Linking Format (ELF) αποτελεί το format που χρησιμοποιεί το linux για τα object αρχεία του. Υποστηρίζει μια πληθώρα αρχιτεκτονικών και για αυτό είναι ιδανικό για ένα multi-platform λειτουργικό όπως το linux. Παρακάτω θα γίνει μια περιγραφή των βασικών στοιχείων του ELF. Για μια πιο αναλυτική περιγραφή ανατρέξτε στο standard (pdf): ELF 1.1, ELF 1.2

Αυτό το ELF δεν έχει καμία σχέση με τον Tolkien :)

3.2 Βασική δομή του ELF

Τα βασικά συστατικά που απαρτίζουν κάθε ELF object αρχείο είναι:

  • O ELF header
  • Το Program Header Table (optional στα Relocatable Object Files)
  • Τα sections και τα segments. Τα segments αποτελούνται από ένα ή παραπάνω sections.
  • To Section Header Table (optional στα Executable Object Files)
Σχηματικά:

Τα δύο views αποτελούν διαφορετικούς τρόπους με τους οποίους το σύστημα βλέπει ένα ELF object αρχείο. Το πρώτο (linking view) χρησιμοποιείται όταν το αρχείο πρόκειται να συνδεθεί για την παραγωγή εκτελέσιμου. Η δομική μονάδα εδώ είναι το section. Το δεύτερο (execution view) χρησιμοποιείται κατά τη φόρτωση-εκτέλεση ενός εκτελέσιμου. Ο loader δε "βλέπει" πια sections αλλά φορτώνει στη μνήμη ολόκληρα segments (ομάδες από sections).

3.3 Ο ELF Header

Ο ELF Header περιέχει βασικές πληροφορίες για το object αρχείο. To πρώτο του κομμάτι (16 bytes) είναι το ELF Identification. Αυτό εκτός από τον "μαγικό αριθμό" (υπογραφή) του ELF καθορίζει

  • File Class: Aν το εκτελέσιμο είναι 32-bit ή 64-bit (πάνε τα 16-bit, πόσο μάλλον τα 8-bit!).
  • Data Encoding: Aν τα δεδομένα είναι σε LSB (little-endian) ή MSB (big-endian) μορφή.
  • Header Ver #: Την έκδοση του ELF Header.

Ακολουθούν 9 padding bytes και ύστερα αρχίζει ο κυρίως header.

  • Object File Type : Το είδος του object file (relocatable, executable, shared object).
  • Required Architecture : Η αρχιτεκτονική στην οποία πρέπει να είναι στηριγμένος ο επεξεργαστής ώστε να μπορεί να λειτουργήσει το πρόγραμμα (Intel 80386, SPARC, MIPS RS3000 κτλ)
  • Object File Version # : H εκδοση του ELF.
  • Process Entry point : H εικονική διεύθυνση της πρώτης εντολής του προγράμματος η οποία θα εκτελεστεί.
  • Program Header Table Offset : To offset του PHT στο object αρχείο.
  • Section Header Table Offset : To offset του SHT στο object αρχείο.
  • Processor Specific Flags : Σημαίες που αφορούν σε ιδιότητες του εκάστοτε επεξεργαστή.
  • ELF Header Size : To μέγεθος του ELF Header σε bytes.
  • PHT Entry Size : Το μέγεθος μιας καταχώρησης στο Program Header Table.
  • Number of PHT entries : Ο αριθμός των καταχωρήσεων στο Program Header Table σε bytes.
  • SHT Entry Size : Το μέγεθος μιας καταχώρησης στο Section Header Table σε bytes .
  • Number of SHT entries : Ο αριθμός των καταχωρήσεων στο Section Header Table.
  • Section Name String Table SHT Index : Το index στο SHT που καθορίζει ποιο section περιέχει το string table με τα ονόματα των sections.

3.4 Τα ELF Sections

Το section είναι ένα τμήμα του object αρχείου το οποίο περιέχει συγκεκριμένες και ομογενείς πληροφορίες. Για παράδειγμα, ένα section μπορεί να περιέχει τον κώδικα του προγράμματος, ένα άλλο τα δεδομένα, ένα τρίτο το string table κτλ. Τα sections ενός ELF αρχείου καθορίζονται στο Section Header Table. Αν και είναι προαιρετικός στο executable object αρχεία, πάντα περιλαμβάνεται (από όσο έχω δει). Το Section Header Table αποτελείται από μια σειρά από περιγραφείς, καθένας από τους οποίους μας δίνει πληροφορίες για ένα section:

Οι πληροφορίες που μας παρέχει η παραπάνω δομή είναι:

  • Section Name String Table Index : To offset του ονόματος του section στο string table.
  • Section Type : Το είδος του section (πχ program-defined, symbol table, dynamic linking info).
  • Section Flags : Σημαίες που καθορίζουν διάφορα χαρακτηριστικά του section (πχ allocation (αν το section φορτώνεται στη μνήμη κατα την εκτέλεση), write, execute).
  • Section Virtual Address : Η εικονική διεύθυνση στην οποία θα φορτωθεί το section στη μνήμη ή 0 αν δεν φορτώνεται.
  • Section File Offset : Το offset στο οποίο αρχίζει το section στο object αρχείο.
  • Section Size : Το μέγεθος του section σε bytes
  • Section Header Table Index Link : Κάποια sections χρειάζονται πληροφορίες από κάποιο άλλο section. To πεδίο καθορίζει το index του απαραίτητου section στο section header table. Χαρακτηριστικό παράδειγμα είναι τα sections που περιέχουν πληροφορίες για το dynamic linking. Σε αυτά, το πεδίο καθορίζει σε ποιo string table section βρίσκονται τα ονόματα του εξωτερικών συμβόλων.
  • Additional Section Info : Επιπλέον πληροφορίες, η σημασία των οποίων εξαρτάται από το είδος του section.
  • Section Address Alignment : Καθορίζει σε τι όρια πρέπει να φορτωθεί το section.
  • Fixed-Size Entry Size : Αν το section αποτελείται από κάποιον πίνακα στον οποίο κάθε καταχώρηση έχει σταθερό μέγεθος, το πεδίο αυτό δίνει το μέγεθος της κάθε καταχώρησης (πχ το symbol table section)

Υπάρχουν κάποια sections τα οποία κατά σύμβαση περιέχουν συγκεκριμένες πληροφορίες. Μια πλήρης λίστα μπορεί να βρεθεί στο standard. Τα πιο σημαντικά και κοινά είναι:

  • .text : Περιέχει τον εκτελέσιμο κώδικα του προγράμματος.
  • .data : Περιέχει τα αρχικοποιημένα δεδομένα του προγράμματος (πχ global μεταβλητές στις οποίες έχουμε δώσει αρχική τιμή).
  • .bss : Περιέχει μη αρχικοποιημένα δεδομένα του προγράμματος (πχ global μεταβλητές στις οποίες δεν έχουμε δώσει αρχική τιμή). Το section αυτό δεν καταλαμβάνει χώρο στο αρχείο αλλά δημιουργείται μόνο στη μνήμη, όπου όλα τα bytes του μηδενίζονται.
  • .dynamic : Περιέχει πληροφορίες απαραίτητες για το dynamic linking.
  • .strtab : Περιέχει strings που χρησιμοποιούνται κυριώς από το symbol table.
  • .dynstr : Περιέχει τα strings σχετικά με το dynamic linking.
  • .shstrtab : Περιέχει τα strings των ονομάτων των sections.
  • .symtab : Περιέχει τo symbol table για το αρχείο.
  • .dynsym : Περιέχει τo symbol table για τα σύμβολα που χρειάζονται για το dynamic linking.
  • .plt : Περιέχει τo procedure linkage table. Θα ασχοληθούμε με αυτό αργότερα.
  • .interp : Περιέχει το path του interpreter που θα φορτώσει το εκτελέσιμο.

String Table Section

Τα τρία sections .strtab, .dynstr και .shstrtab περιέχουν strings τα οποία χρησιμοποιούνται από κάποια άλλα sections. Η δομή τους είναι αρκετά απλή: Το πρώτο byte του section είναι '\0' και από εκεί και πέρα ακολουθεί μια σειρά από null-terminated strings. Τα strings καθορίζονται από το offset τους από την αρχή του section.

Για το παραπάνω table έχουμε:

       index/offset       String
             1            "alf"
             2              "lf"
             5             "tx"
                    κτλ

3.5 Τα ELF Segments

Τα segments αποτελούνται από ένα ή περισσότερα sections τα οποία κατά τη φόρτωση του εκτελέσιμου/shared object αρχείου έχουν κοινές ιδιότητες. Για κάθε segment υπάρχει μια καταχώρηση στο Program Header Table:

  • Segment Type : To είδος του segment και των πληροφοριών που περιέχει (πχ loadable(θα φορτωθεί στη μνήμη), dynamic linking info)
  • Segment File offset : Το offset της αρχής του segment στο αρχείο.
  • Segment Virtual Address : Η εικονική διεύθυνση στην οποία θα φορτωθεί το segment στη μνήμη.
  • Segment Physical Address : Η φυσική διεύθυνση μνήμη στην οποία θα φορτωθεί το segment. Προφανώς τα περισσότερα συστήματα αγνοούν αυτό το πεδίο.
  • Segment File Image Size : To μέγεθος που καταλαμβάνει το segment στο αρχείο.
  • Segment Memory Image Size : To μέγεθος που θα καταλαμβάνει το segment στη μνήμη (>=Segment File Image Size)
  • Segment Flags : Σημαίες που καθορίζουν ιδιότητες του segment.
  • Segment Alignment : Καθορίζει σε τι όρια πρέπει να φορτωθεί το segment.

3.6 Εκτέλεση ενός ELF executable και dynamic linking

Φόρτωση - Εκτέλεση

Σε γενικές γραμμές ακολουθούνται τα εξής βήματα:

  1. Αρχικά το λειτουργικό (μέσω του syscall exec()) διαβάζει τον header του εκτελέσιμου για να πάρει απαραίτητες πληροφορίες όπως:
    • Αν όντως πρόκειται για εκτελέσιμο που μπορεί να τρέξει στον υπολογιστή
    • Πόση μνήμη απαιτεί και τι ιδιότητες έχει κάθε τμήμα (segment) του εκτελέσιμου ( πχ read-only, executable κτλ)
  2. To λειτουργικό ελέγχει στο Program Header Table αν το εκτελέσιμο περιέχει ένα segment τύπου PT_INTERP (το οποίο περιέχει μόνο ένα section, το .interp).
    Αν δεν υπάρχει τότε:
    • Το λειτουργικό αποδίδει στη διεργασία τη μνήμη που χρειάζεται και φορτώνει τα διάφορα τμήματα στη μνήμη.
    • Γίνεται relocation (επανατοποθέτηση) αν χρειάζεται.
    • Τέλος, ο έλεγχος μεταφέρεται στο entry point του προγράμματος.
    Αν υπάρχει (συνήθως υπάρχει):
    • Διαβάζει από το segment το path του interpreter και φορτώνει τον interpreter στη μνήμη.
    • Το λειτουργικό είτε φορτώνει το εκτελέσιμο στη μνήμη και "περνάει" τη διεύθυνση του στον interpreter, είτε "περνάει" στον interpreter έναν file descriptor για το αρχείο του εκτελέσιμου και τον αφήνει να κάνει τη δουλειά. Σε κάθε περίπτωση, ο έλεγχος περνάει στον interpreter.
    • O interpreter, αν είναι ο dynamic linker ld.so (κατά 99% είναι αυτός), φορτώνει στο address space της διεργασίας τις βιβλιοθήκες που χρειάζεται το εκτελέσιμο.
    • Γίνεται relocation στο εκτελέσιμο και στις βιβλιοθήκες. Ως μέρος του relocation, διορθώνονται, αν είναι ανάγκη, οι αναφορές σε συναρτήσεις/δεδομένα των βιβλιοθηκών που φορτώθηκαν. Στο ELF αυτό μπορεί να γίνει και στο run-time.
    • Δίνεται η δυνατότητα σε κάθε shared object να εκτελέσει κάποιο κώδικα αρχικοποίησης.
    • Τέλος, ο έλεγχος μεταφέρεται στο entry point του εκτελέσιμου.

Το dynamic linking στο ELF

Τα sections τα οποία σχετίζονται με το dynamic linking σε ένα εκτελέσιμο είναι τα εξής:

.dynamic

Αυτό περιέχει πληροφορίες για το ποια shared object είναι απαραίτητα για την εκτέλεση, τη διεύθυνση του relocation table, τη διεύθυνση του symbol table που περιέχει τα εξωτερικά σύμβολα κτλ. Ο dynamic linker διαβάζει τα πεδία αυτά, φορτώνει (και επανατοποθετεί) τις βιβλιοθήκες και προσπαθεί να "επιλύσει" (resolve) τις αναφορές στα εξωτερικά σύμβολα. Με άλλα λόγια, βρίσκει τις διευθύνσεις των συμβόλων και τις διορθώνει στο εκτελέσιμο. Πρέπει να σημειωθεί πως οι βιβλιοθήκες που φορτώνει ο dynamic linker μπορεί και αυτές να απαιτούν άλλες βιβλιοθήκες, οπότε η όλη διαδικασία επαναλαμβάνεται αναδρομικά. Βέβαια, κάθε βιβλιοθήκη φορτώνεται μόνο μια φορά, άσχετα από το αν χρησιμοποιείται από πολλά ELF objects.

.got (Global Offset Table)

Το section αυτό (αφού φορτωθεί το πρόγραμμα) περιλαμβάνει τις διευθύνσεις των συμβόλων που είναι πιθανό να αλλάξουν από το relocation. Στα εκτελέσιμα περιέχει μόνο τις διευθύνσεις των εξωτερικών συμβόλων (dynamically linked), ενώ για τα shared object περιλαμβάνει τις διευθύνσεις όλων των συμβόλων (εσωτερικών και εξωτερικών). Αυτό συμβαίνει αφού σίγουρα το shared object θα επανατοποθετηθεί. Οι πρώτες τρεις καταχωρίσεις έχουν ειδική σημασία. Η πρώτη (offset 0) περιέχει τη διεύθυνση του .dynamic section. Η τρίτη περιλαμβάνει τη διεύθυνση του dynamic linker. Η τύχη της δεύτερης αγνοείται προς το παρόν.

.plt (Procedure Linkage Table)

Αυτό έχει τη μορφή:
plt_start:
        push got_start+4
        jmp [got_start+8]
        ...
jmp_func1:
        jmp [func1_offset]
        push relocation_offset1
        jmp plt_start
 
jmp_func2:
        ...
Το func1_offset είναι μια διεύθυνση μέσα στο .got section που τελικά (μετά το relocation) θα περιέχει τη διεύθυνση της συνάρτησης func1. To relocation_offset1 είναι ένα offset που καθορίζει μια καταχώρηση στο relocation table. H καταχώρηση αυτή θα έχει ως offset αλλαγής (η διεύθυνση στην οποία θα γίνει η διόρθωση) το func1_offset.

Αρχικά, η διεύθυνση func1_offset περιέχει τη διεύθυνση της επόμενης εντολής από την jmp [func1_offset]. Έτσι, ο έλεγχος πάει στην push relocation_offset1 και μετά, μέσω της jmp plt_start, στο push got_start+4, jmp [got_start+8].

Το got_start+8 είναι η τρίτη καταχώρηση στο .got section και όπως είπαμε περιλαμβάνει τη διεύθυνση του dynamic linker. Έτσι, ο έλεγχος περνάει στο dynamic linker με δύο παραμέτρους (got_start+4, relocation_offset1). Με αυτές τις πληροφορίες ο dynamic linker μπορεί να βρει τη διεύθυνση της ζητούμενης συνάρτησης και να την τοποθετήσει στη func1_offset. Την επόμενη φορά που θα κληθεί η jmp_func1, η εντολή jmp [func1_offset] θα οδηγήσει στην πραγματική συνάρτηση func1 (και όχι στο αμέσως επόμενο push).

Είναι φανερό πως αυτή η διαδικασία δεν είναι απαραίτητο να γίνει στο φόρτωμα του προγράμματος αλλά την πρώτη φορά που θα χρειαστεί να κληθεί η func1. Αυτή είναι η default συμπεριφορά και ονομάζεται lazy binding. Μπορούμε να την αλλάξουμε δίνοντας στη enviroment μεταβλητή LD_BIND_NOW μία μη null τιμή.

3.7 Εργαλεία για το ELF

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

Το πιο κοινό ίσως εργαλείο (αλλά σίγουρα όχι το καλύτερο) είναι το objdump. Το objdump είναι μέρος των binutils και μπορεί να μας δώσει σχεδόν όλες τις πληροφορίες που περιέχει ένα ELF object αρχείο. Η επιλογή των πληροφοριών που θα απεικονιστούν γίνεται με διάφορα flags. Βασικά flags είναι το -x το οποίο δείχνει σχεδόν όλες τις πληροφορίες (εκτός από αυτές που σχετίζονται με δυναμικά σύμβολα), το που δείχνει τα δυναμικά σύμβολα, το -d που κάνει disassemble τον κώδικα, το -M με το οποίο ρυθμίζουμε τον disassembler (πχ -M intel για intel σύνταξη) και το -C που ενεργοποιεί το demangling. Το κύριο μειονέκτημα του objdump είναι στον τομέα του disassembly. Το listing που παράγεται έχει ελάχιστες πληροφορίες, ειδικά αν δεν υπάρχουν σύμβολα. Παρέα με το objdump (στα binutils) έρχονται το readelf και το nm. Η αλήθεια είναι πως δεν είναι και πολύ χρήσιμα, αφού το objdump κάνει ότι τα προηγούμενα δύο μαζί και ακόμα παραπάνω.

Ένα σαφώς πιο εξελιγμένο εργαλείο είναι το ELFsh (Site: http://elfsh.devhell.org). Πρόκειται για μια scripting γλώσσα που μας επιτρέπει να διαχειριστούμε ένα ELF αρχείο. Οι δυνατότητές του ποικίλουν από μια απλή απεικόνιση των πληροφοριών του header, μέχρι και συγχώνευση δύο object αρχείων! Η "ψυχή" του προγράμματος βρίσκεται σε μια καλοσχεδιασμένη βιβλιοθήκη (libelfsh) και έτσι μπορεί να χρησιμοποιηθεί σε οποιαδήποτε άλλη εφαρμογή.

Μια πιο εύχρηστη λύση είναι o HT Editor (Site: http://hte.sourceforge.net). Παρέχει πλήρη διαχείριση του object αρχείου και έναν αρκετά καλό disassembler. Θα αναφερθούμε λίγο περισσότερο σε αυτόν στο κομμάτι για το dead-listing.

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


Valid HTML 4.01!   Valid CSS!