[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home] [volume] [parte]


Capitolo 568.   Formato ELF

Il formato ELF è il contenitore di un programma che non si trova necessariamente nello stato di poter essere eseguito. Il formato ELF si distingue per la presenza di un'intestazione che si trova obbligatoriamente all'inizio del file; quindi, il contenuto del file è affiancato da una serie di tabelle che lo descrivono in base a vari criteri.

568.1   Sezioni e segmenti

Per semplificare la descrizione di un formato ELF, lo si può immaginare composto da sezioni, il cui scopo è quello di descrivere tutto ciò che compone il programma, e da segmenti, con i quali si descrive in che modo il programma deve essere rappresentato in memoria ed eseguito. L'informazione relativa alle sezioni è indispensabile quando deve intervenire un «collegatore» (linker); l'informazione data dai segmenti riguarda l'avvio del programma.

Se si vuole abbandonare questo tipo di rappresentazione astratta, il formato ELF lo si può vedere come un involucro del codice eseguibile e dei dati inizializzati, contenente un'intestazione di riconoscimento (che si trova obbligatoriamente all'inizio del file) e da una serie di tabelle, più o meno concatenate tra di loro, alcune delle quali possono essere facoltative, in base al contesto per il quale il file oggetto è predisposto.

Tabella 568.1. Componenti principali che descrivono un formato ELF.

Tabella Descrizione
ELF header È l'intestazione del file e deve trovarsi necessariamente all'inizio dello stesso. Contiene poi i riferimenti alla tabella dei segmenti (program header table) e a quella delle sezioni (section header table).
program header table È la tabella dei segmenti da caricare in memoria, con le informazioni necessarie a procedere in tal senso. La presenza di questa tabella è obbligatoria in un file oggetto eseguibile.
section header table È la tabella delle sezioni.
string table È la tabella delle stringhe, a cui fanno riferimento le altre tabelle quando devono indicare una stringa di qualunque tipo.
symbol table È la tabella dei simboli associati a varie parti del contenuto. La tabella dei simboli, per indicare i nomi dei simboli, deve fare riferimento alla tabella delle stringhe.

568.2   Intestazione ELF

L'intestazione ELF è il componente più importante del formato, in quanto la sua presenta è obbligatoria. L'intestazione consente di identificare un file ELF come tale e di raggiungere le tabelle delle sezioni e dei segmenti, da cui poi si arriva al contenuto rimanente del file.

Tabella 568.2. Intestazione ELF secondo l'architettura x86, in particolare con le informazioni necessarie a produrre un file eseguibile.

Nome mnemonico Dimensione
x86-32
Dimensione
x86-64
Descrizione
e_ident[0]
e_ident[1]
e_ident[2]
e_ident[3]
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
Impronta di identificazione del formato: deve corrispondere a 7F16, E, L, F.
e_ident[4]
8 bit 8 bit Definisce la classe del file: 0116 rappresenta un file oggetto a 32 bit; 0216 rappresenta invece un file a 64 bit.
e_ident[5]
8 bit 8 bit Definisce la codifica dei dati: 0116 rappresenta un formato LSB, ovvero little endian; 0216 formato MSB, ovvero big endian. Sia 0116, sia 0216, si riferiscono a una rappresentazione numerica dei valori negativi attraverso il complemento a due.
e_ident[6]
8 bit 8 bit Definisce la versione dell'intestazione (inizialmente esiste solo la versione 0116).
e_ident[7]
e_ident[8]
e_ident[9]
e_ident[10]
e_ident[11]
e_ident[12]
e_ident[13]
e_ident[14]
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
8 bit
Questi byte definiscono informazioni di importanza minore e di solito vengono lasciati a 0016.
e_ident[15]
8 bit 8 bit Dichiara la dimensione in byte della sequenza di identificazione. Il valore obbligato per questo byte è 1016, ovvero 1610.
e_type
16 bit 16 bit Definisce il tipo di file oggetto. Un file oggetto rilocabile ha il codice 0116; un file oggetto eseguibile ha il codice 0216.
e_machine
16 bit 16 bit Definisce il tipo di architettura. Il codice 0316 si riferisce al tipo Intel.
e_version
32 bit 32 bit Definisce la versione del file oggetto (inizialmente esiste solo la versione 0000000116).
e_entry
32 bit 64 bit Contiene l'indirizzo a cui occorre passare il controllo per l'esecuzione del programma.
e_phoff
32 bit 64 bit program header table offset
Contiene lo scostamento, rispetto all'inizio del file, necessario per raggiungere il primo byte della tabella che descrive i segmenti da caricare in memoria. Tale tabella è nota come program header table ed è obbligatoria la sua presenza in un file oggetto eseguibile.
e_shoff
32 bit 64 bit section header table offset
Contiene lo scostamento, rispetto all'inizio del file, necessario per raggiungere il primo byte della tabella che descrive le sezioni. Tale tabella è nota come section header table.
e_flags
32 bit 32 bit Contiene degli indicatori specifici per il tipo di microprocessore. Nel caso dell'architettura x86-32 può contenere semplicemente valori a zero.
e_ehsize
16 bit 16 bit ELF header size
Contiene la dimensione dell'intestazione ELF.
e_phentsize
16 bit 16 bit program header entry size
Definisce la dimensione di una voce descrittiva di un segmento, nella tabella dei segmenti. Tutte le voci di tale tabella hanno la stessa dimensione.
e_phnum
16 bit 16 bit program header number
Definisce la quantità di voci contenute nella tabella di descrizione dei segmenti.
e_shentsize
16 bit 16 bit section header entry size
Definisce la dimensione di una voce descrittiva di una sezione, nella tabella delle sezioni. Tutte le voci di tale tabella hanno la stessa dimensione.
e_shnum
16 bit 16 bit section header number
Definisce la quantità di voci contenute nella tabella di descrizione delle sezioni.
e_shstrndx
16 bit 16 bit section header string index
Definisce l'indice, all'interno della tabella delle sezioni, che identifica la voce che fa riferimento alla tabella delle stringhe.

568.3   Descrizione dei segmenti

La descrizione dei segmenti, necessaria per mettere in esecuzione un programma, è contenuta nella tabella program header, composta da un array di voci, di dimensione uniforme, ognuna delle quali descrive un segmento. Si raggiunge la prima voce di questo array con lo scostamento indicato nell'intestazione ELF (e_phoff), quindi, sapendo la dimensione di ogni voce (e_phentsize) e la quantità di queste (e_phnum), è possibile scandire anche le altre.

Tabella 568.3. Descrizione di una voce nella tabella dei segmenti, secondo l'architettura x86-32.

Nome mnemonico Dimensione Descrizione
p_type
32 bit Definisce il tipo di operazione da compiere. La situazione più semplice è costituita da codice eseguibile e dati da caricare in memoria: 0116.
p_offset
32 bit Definisce lo scostamento, dall'inizio del file, necessario a raggiungere il primo byte del segmento.
p_vaddr
32 bit Definisce l'indirizzo assoluto, nell'ambito della memoria virtuale, dove il primo byte del segmento deve trovarsi in memoria, una volta caricato.
p_paddr
32 bit Equivale al campo p_vaddr, ma si riferisce alla «memoria fisica». In un sistema GNU/Linux questo valore è sempre uguale a p_vaddr.
p_filesz
32 bit Definisce la dimensione del segmento nel file e in casi particolari può essere pari a zero.
p_memsz
32 bit Definisce la dimensione del segmento rappresentato in memoria e in casi particolari può essere pari a zero.
p_flags
32 bit Definisce degli indicatori che descrivono i permessi del segmento: 1 = esecuzione; 2 = scrittura; 4 = lettura. Per avere permessi multipli si sommano i permessi elementari. Generalmente, il segmento di una porzione di codice dispone di permessi di accesso in lettura e in esecuzione, mentre quello di un'area di dati, consente normalmente la lettura e la scrittura.
p_align
32 bit Definisce l'allineamento in memoria, a blocchi del valore indicato, il quale a sua volta deve essere una potenza di due.

Tabella 568.4. Descrizione di una voce nella tabella dei segmenti, secondo l'architettura x86-64.

Nome mnemonico Dimensione Descrizione
p_type
32 bit Definisce il tipo di operazione da compiere. La situazione più semplice è costituita da codice eseguibile e dati da caricare in memoria: 0116.
p_flags
32 bit Definisce degli indicatori che descrivono i permessi del segmento: 1 = esecuzione; 2 = scrittura; 4 = lettura. Per avere permessi multipli si sommano i permessi elementari. Generalmente, il segmento di una porzione di codice dispone di permessi di accesso in lettura e in esecuzione, mentre quello di un'area di dati, consente normalmente la lettura e la scrittura.
p_offset
64 bit Definisce lo scostamento, dall'inizio del file, necessario a raggiungere il primo byte del segmento.
p_vaddr
64 bit Definisce l'indirizzo assoluto, nell'ambito della memoria virtuale, dove il primo byte del segmento deve trovarsi in memoria, una volta caricato.
p_paddr
64 bit Equivale al campo p_vaddr, ma si riferisce alla «memoria fisica». In un sistema GNU/Linux questo valore è sempre uguale a p_vaddr.
p_filesz
64 bit Definisce la dimensione del segmento nel file e in casi particolari può essere pari a zero.
p_memsz
64 bit Definisce la dimensione del segmento rappresentato in memoria e in casi particolari può essere pari a zero.
p_align
64 bit Definisce l'allineamento in memoria, a blocchi del valore indicato, il quale a sua volta deve essere una potenza di due.

568.4   Definizione manuale di un formato ELF

Un programma eseguibile in formato ELF deve contenere l'intestazione ELF e la descrizione dei segmenti; le sezioni possono anche mancare del tutto. Viene mostrato un esempio di programma banale (non fa altro che restituire il valore 77) in cui tutto, anche ciò che costituisce il formato ELF, viene definito nel sorgente. Il programma in sé trae spunto dal documento A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux di Brian Raiter, come annotato alla fine del capitolo. Si osservi che nel sorgente le sezioni non vengono indicate affatto.

.code32
.globl _start
#
file_begin:
#
# ELF header.
#
elf_header_begin:
    .byte    0x7F           # e_ident
    .byte    'E', 'L', 'F'  # 
    .byte    1              #
    .byte    1              #
    .byte    1              #
    .byte    0, 0, 0, 0     #
    .byte    0, 0, 0, 0     #
    .byte    16             #
    #
    .short   2              # e_type        2 = executable file
    .short   3              # e_machine     3 = 386
    .int     1              # e_version     1 = current version
    .int     _start         # e_entry           start address
    .int     (program_header_begin - file_begin)
                            # e_phoff           program header offset
    .int     0              # e_shoff       0 = no section header table
    .int     0              # e_flags           no flags
    .short   (elf_header_end - elf_header_begin)
                            # e_ehsize          ELF header size
    .short   (program_header_end - program_header_begin)
                            # e_phentsize       program header entry size
    .short   1              # e_phnum           program header entries
    .short   0              # e_shentsize   0 = no section header table
    .short   0              # e_shnum           section header entries
    .short   0              # e_shstrndx    0 = undefined
elf_header_end:
#
# Program header table, with just one entry.
#
program_header_begin:
    .int     1              # p_type        1 = segment to be loaded
    .int     0              # p_offset          segment's offset
    .int     0x08048000     # p_vaddr           segment's virtual address
    .int     0x08048000     # p_paddr           segment's physical address
    .int     (file_end - file_begin)
                            # p_filesz          file image size
    .int     (file_end - file_begin)
                            # p_memsz           memory image size
    .int     5              # p_flags       5 = read + execute
    .int     0x1000         # p_align           segment's memory alignment
program_header_end:
#
# Program code.
#
_start:
    mov $77, %ebx
    mov $1, %eax
    int $0x80
#
file_end:

Il sorgente scritto nel formato adatto a GNU AS va compilato così:

as -o elf_test.o elf_test.s[Invio]

ld --oformat binary -o elf_test elf_test.o[Invio]

Come si vede, GNU ld viene usato in modo da produrre un formato binario, puro e semplice, ovvero un file privo di formato, dato che è già tutto incluso nella descrizione del programma stesso.

Si mostra anche il sorgente adatto a NASM, dove va annotata anche l'origine, ovvero l'indirizzo in cui tutto deve essere caricato in memoria:

    bits 32
    org  0x08048000
;
file_begin:
;
; ELF header.
;
elf_header_begin:
    db       0x7F           ; e_ident
    db       'E', 'L', 'F'  ; 
    db       1              ;
    db       1              ;
    db       1              ;
    db       0, 0, 0, 0     ;
    db       0, 0, 0, 0     ;
    db       16             ;
    ;
    dw       2              ; e_type        2 = executable file
    dw       3              ; e_machine     3 = 386
    dd       1              ; e_version     1 = current version
    dd       _start         ; e_entry           start address
    dd       (program_header_begin - file_begin)
                            ; e_phoff           program header offset
    dd       0              ; e_shoff       0 = no section header table
    dd       0              ; e_flags           no flags
    dw       (elf_header_end - elf_header_begin)
                            ; e_ehsize          ELF header size
    dw       (program_header_end - program_header_begin)
                            ; e_phentsize       program header entry size
    dw       1              ; e_phnum           program header entries
    dw       0              ; e_shentsize   0 = no section header table
    dw       0              ; e_shnum           section header entries
    dw       0              ; e_shstrndx    0 = undefined
elf_header_end:
;
; Program header table, with just one entry.
;
program_header_begin:
    dd       1              ; p_type        1 = segment to be loaded
    dd       0              ; p_offset          segment's offset
    dd       0x08048000     ; p_vaddr           segment's virtual address
    dd       0x08048000     ; p_paddr           segment's physical address
    dd       (file_end - file_begin)
                            ; p_filesz          file image size
    dd       (file_end - file_begin)
                            ; p_memsz           memory image size
    dd       5              ; p_flags       5 = 1 (execute) + 4 (read)
    dd       0x1000         ; p_align           segment's memory alignment
program_header_end:
;
; Program code.
;
_start:
    mov ebx, 77
    mov eax, 1
    int 0x80
;
file_end:

In questo caso, la compilazione non richiede altro che NASM, il quale produce direttamente il formato binario voluto:

nasm -f bin -o elf_test elf_test.s[Invio]

chmod +x elf_test[Invio]

I valori che rappresentano scostamenti e dimensioni del codice, sono calcolati attraverso il compilatore, facendo riferimento alle etichette che delimitano le varie porzioni del sorgente. Ecco come si presenta il programma eseguibile dal punto di vista di Objdump:

objdump -x elf_test[Invio]

ELF_test:     file format elf32-i386
ELF_test
architecture: i386, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x08048054

Program Header:
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x00000060 memsz 0x00000060 flags r-x

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
SYMBOL TABLE:
no symbols

568.5   Esempio più complesso

Viene mostrato un esempio più complesso, composto sempre da una sola voce nella tabella dei segmenti; in particolare viene definita una variabile inizializzata, incorporata nel segmento del codice, che nel sorgente appare in fondo. Viene mostrata solo la versione per GNU AS, trattandosi del programma per il calcolo del fattoriale, già descritto in un altro capitolo.

.code32
.globl _start
#
file_begin:
#
# ELF header.
#
elf_header_begin:
    .byte    0x7F           # e_ident
    .byte    'E', 'L', 'F'  # 
    .byte    1              #
    .byte    1              #
    .byte    1              #
    .byte    0, 0, 0, 0     #
    .byte    0, 0, 0, 0     #
    .byte    16             #
    #
    .short   2              # e_type        2 = executable file
    .short   3              # e_machine     3 = 386
    .int     1              # e_version     1 = current version
    .int     _start         # e_entry           start address
    .int     (program_header_begin - file_begin)
                            # e_phoff           program header offset
    .int     0              # e_shoff       0 = no section header table
    .int     0              # e_flags           no flags
    .short   (elf_header_end - elf_header_begin)
                            # e_ehsize          ELF header size
    .short   (program_header_end - program_header_begin)
                            # e_phentsize       program header entry size
    .short   1              # e_phnum           program header entries
    .short   0              # e_shentsize   0 = no section header table
    .short   0              # e_shnum           section header entries
    .short   0              # e_shstrndx    0 = undefined
elf_header_end:
#
# Program header table, with just one entry.
#
program_header_begin:
    .int     1              # p_type        1 = segment to be loaded
    .int     0              # p_offset          segment's offset
    .int     0x08048000     # p_vaddr           segment's virtual address
    .int     0x08048000     # p_paddr           segment's physical address
    .int     (file_end - file_begin)
                            # p_filesz          file image size
    .int     (file_end - file_begin)
                            # p_memsz           memory image size
    .int     5              # p_flags       5 = read + execute
    .int     0x1000         # p_align           segment's memory alignment
program_header_end:
#
# Program code.
#
_start:
    mov   op1, %esi        # ESI contiene il valore di cui si vuole
                           # calcolare il fattoriale.
    push  %esi             # f_fact (ESI) ==> EAX
    call  f_fact           #
    add   $4, %esp         #
    mov   %eax, %ebx       # Restituisce il valore del fattoriale,
    mov   $1, %eax         # ammesso che sia abbastanza piccolo
    int   $0x80            # da poter essere rappresentato come
                           # valore di uscita.
#
# Fattoriale di un numero senza segno.
# f_fatt (a) ==> EAX
# EAX = a!
#
f_fact:
    enter $4, $0
    pusha
    #
    mov   8(%ebp), %edi    # Valore di cui calcolare il fattoriale.
    #
    cmp   $1, %edi         # Il fattoriale di 1 è 1.
    jz    f_fact_end_1     #
    #
    mov   %edi, %esi       # ESI contiene il valore di cui si vuole
    dec   %esi             # il fattoriale, ridotto di una unità.
    #
    push  %esi             # f_fact (ESI) ==> EAX
    call  f_fact           #
    add   $4, %esp         #
    mul   %edi             # EDX:EAX = EAX*EDI
    mov   %eax, -4(%ebp)   # Salva il risultato.
    jmp   f_fact_end_X     # Conclude la funzione.
    #
f_fact_end_1:
    popa                   # Conclude la funzione con EAX = 1.
    mov $1, %eax           #
    leave                  #
    ret                    #
f_fact_end_X:
    popa                   # Conclude la funzione con EAX pari
    mov -4(%ebp), %eax     # al valore salvato nella variabile
    leave                  # locale.
    ret                    #
#
# Initialized data.
#
op1:    .int    5
#
file_end:

568.6   Riferimenti


Appunti di informatica libera 2008 --- Copyright © 2000-2008 Daniele Giacomini -- <appunti2 (ad) gmail·com>


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome formato_elf.htm

[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home]

Valid ISO-HTML!

CSS validator!

Gjlg Metamotore e Web Directory