Επόμενο Προηγούμενο Περιεχόμενα
Το Εxecutable and Linking Format (ELF) αποτελεί το format
που χρησιμοποιεί το linux για τα object αρχεία του.
Υποστηρίζει μια πληθώρα αρχιτεκτονικών και για αυτό είναι
ιδανικό για ένα multi-platform λειτουργικό όπως το linux.
Παρακάτω θα γίνει μια περιγραφή των βασικών στοιχείων του
ELF. Για μια πιο αναλυτική περιγραφή ανατρέξτε στο
standard (pdf):
ELF 1.1, ELF
1.2
Αυτό το ELF δεν έχει καμία σχέση με τον Tolkien :)
Τα βασικά συστατικά που απαρτίζουν κάθε 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).
Ο 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.
Το 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"
κτλ
Τα 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.
Φόρτωση - Εκτέλεση
Σε γενικές γραμμές ακολουθούνται τα εξής βήματα:
-
Αρχικά το λειτουργικό (μέσω του syscall exec())
διαβάζει τον header του εκτελέσιμου για να πάρει
απαραίτητες πληροφορίες όπως:
-
Αν όντως πρόκειται για εκτελέσιμο που μπορεί να
τρέξει στον υπολογιστή
-
Πόση μνήμη απαιτεί και τι ιδιότητες έχει κάθε τμήμα
(segment) του εκτελέσιμου ( πχ read-only,
executable κτλ)
-
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 τιμή.
Η δομή του 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.
Επόμενο Προηγούμενο Περιεχόμενα