Le format ELF (Executable and Linkable Format)

Hugo Jacotot – David Andrawos Saad – Matthieu Le Franc

Supposons que nous ayons un programme écrit en langage C. Ce programme ne peut être utilisé sur différentes machines que si celles-ci possèdent la même architecture matérielle ainsi que le même environnement d’exécution et de compilation. Cela s’explique par le fait que le langage C est compilé en un langage machine spécifique à l’architecture matérielle, et que le programme C dépend des services fournis par les bibliothèques et le système d’exploitation. Par exemple, l’ABI fourni par le système d’exploitation définit la manière dont le programme C interagit avec son environnement, ce qui signifie qu’un programme fonctionnant sous Linux ne sera pas nécessairement compatible avec Windows.

On peut dire que cette non-portabilité constitue un obstacle. Cependant, pour résoudre ce problème, il est possible d’utiliser un format de fichier spécifique  définissant une interface entre le système et le programme tel que l’ELF, le Mach-O ou bien encore COFF… Notre choix s’est tourné sur l’ELF étant donné qu’il est directement pris en charge par le GRUB.

L’ELF (Executable and Linkable Format) est un type de format de fichier largement utilisé pour divers éléments tels que les exécutables, les bibliothèques partagées (.so), les fichiers objets (.o), les modules de noyau chargeables…

Structure d’un fichier ELF :

Le Header : 

Tout fichier ELF possède un header permettant de l’identifier. Voici la représentation des différents attributs en C : 

struct Elf32_Ehdr {
  unsigned char e_ident[EI_NIDENT]; // ELF Identification bytes
  Elf32_Half    e_type; // Type of file (see ET_* below)
  Elf32_Half    e_machine; // Required architecture for this file (see EM_*)
  Elf32_Word    e_version; // Must be equal to 1
  Elf32_Addr    e_entry; // Address to jump to in order to start program
  Elf32_Off     e_phoff; // Program header table’s file offset, in bytes
  Elf32_Off     e_shoff; // Section header table’s file offset, in bytes
  Elf32_Word    e_flags; // Processor-specific flags
  Elf32_Half    e_ehsize; // Size of ELF header, in bytes
  Elf32_Half    e_phentsize; // Size of an entry in the program header table
  Elf32_Half    e_phnum; // Number of entries in the program header table
  Elf32_Half    e_shentsize; // Size of an entry in the section header table
  Elf32_Half    e_shnum; // Number of entries in the section header table
  Elf32_Half    e_shstrndx; // Sect hdr table index of sect name string table
};

Voici les caractéristiques de notre fichier exécutable kernel.elf : 

$ readelf -h kernel.elf
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                         ELF32
  Data:                          2’s complement, little endian
  Version:                       1 (current)
  OS/ABI:                        UNIX – System V
  ABI Version:                   0
  Type:                          EXEC (Executable file)
  Machine:                       Intel 80386
  Version:                       0x1
  Entry point address:           0x10023c
  Start of program headers:      52 (bytes into file)
  Start of section headers:      28040 (bytes into file)
  Flags:                         0x0
  Size of this header:           52 (bytes)
  Size of program headers:       32 (bytes)
  Number of program headers:     4
  Size of section headers:       40 (bytes)
  Number of section headers:     16
  Section header string table index: 15

Le Corps : 

Un fichier ELF est composé de segments et de sections, dont le nombre peut être nul ou multiple. De plus, un segment peut contenir 0 ou plusieurs sections. Par ailleurs, chaque segment et section possède un header (que nous aborderons plus tard).

Les segments et sections se différencient par le fait qu’en mémoire, un segment contient les informations nécessaires à l’exécution, tandis qu’une section contient les informations nécessaires à l’édition des liens.

Prenons l’exemple de notre exécutable kernel.elf :

$ readelf -l kernel.elf

Elf file type is EXEC (Executable file)
Entry point 0x10023c
There are 4 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001000 0x00100000 0x00100000 0x00ee4 0x00ee4 R E 0x1000
  LOAD           0x002000 0x00101000 0x00101000 0x00524 0x00524 R   0x1000
  LOAD           0x003000 0x00102000 0x00102000 0x0005a 0x058a0 RW  0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

Section to Segment mapping:
  Segment Sections…
  00     .text
  01     .rodata .eh_frame
  02     .data .bss
  03

Quelques explications s’imposent concernant les différentes sections : 

.text : contient le code exécutable du programme ainsi que les instructions du cpu.

.data : peut être un ensemble de variables initialisé ou bien le fragment d’un fichier objet.

.rodata : de même que .data mais uniquement accessible en lecture (voir program header).

.eh_frame : instructions de gcc indiquant comment dépiler la pile en cas de gestion d’erreurs.

.bss : enregistre les variables non initialisées, qu’elles soient global, static ou bien encore externe.

Voici leur répartitions en mémoire ainsi que leur nécessité en fonction du point de vue : 

On peut noter que la vue de liaison traite des sections tandis que la vue d’exécution traite des segments.

Concernant notre exécutable on remarque que le premier LOAD (Code) ne contient qu’un seul segment associé à une section nommée .text. 

Le second segment LOAD contient les segments .rodata et .eh_frame. 

Le troisième LOAD  (Data) quant à lui contient deux segments ; .data et .bss. 

On remarque que GNU_STACK (Uninitialized de 02) n’est associé à aucun segment ni de section de manière implicite car il est géré dynamiquement par le système d’exploitation. L’utilité de GNU_STACK ici, est d’indiquer que la pile doit être configurée de tel sorte à avoir les autorisations en RWE.

Ainsi, on peut représenter celà sous cette forme : 

Il est important de noter que ici, l’ordre des segments n’est pas important.

Les entêtes : 

Program Header :

Un programme header joue un rôle crucial dans la description des segments d’un fichier binaire. Ces segments sont essentiels lors du processus de chargement du programme par le kernel car ils fournissent des informations sur la structure de l’exécutable.

Le program header fournit des informations sur le chargement en mémoire pour permettre la création d’une image de processus. Ainsi celà conduit à rendre le program header indispensable pour les fichiers exécutables, mais facultatif pour les fichiers objets. Concrètement, dans les fichiers objets (.o), le program header est omis, car ces fichiers doivent être liés à un exécutable plutôt que chargés directement en mémoire.

Voici la représentation en langage C de la table de program header : 

// Program header for ELF32.
struct Elf32_Phdr {
  Elf32_Word p_type; // Type of segment
  Elf32_Off  p_offset; // File offset where segment is located, in bytes
  Elf32_Addr p_vaddr; // Virtual address of beginning of segment
  Elf32_Addr p_paddr; // Physical address of beginning of segment (OS-specific)
  Elf32_Word p_filesz; // Num. of bytes in file image of segment (may be zero)
  Elf32_Word p_memsz; // Num. of bytes in mem image of segment (may be zero)
  Elf32_Word p_flags; // Segment flags
  Elf32_Word p_align; // Segment alignment constraint
};

Section Header : 

Une table de section header, représentée par la structure Elf32_Shdr, regroupe divers attributs tels que la localisation, la taille, etc… de chaque section dans un fichier binaire au format ELF.

Dans le cadre du développement de logiciel la section header peut être utilisée pour faire du débogage ou bien encore de la rétro ingénierie. Elle offre des informations cruciales sur la disposition des sections.

Par ailleurs, contrairement au segment header, la présence d’une section header n’est pas strictement obligatoire pour le bon déroulement du programme. Cela vient du fait que la section header ne lie pas la disposition de la mémoire au binaire. Cependant, une observation attentive révèle que les sections peuvent tout de même mémoriser le code et les données, offrant ainsi une flexibilité dans la structuration du programme.

Voici la structure Elf32_Shdr, une représentation en langage C de la table de section header : 

// Section header.
struct Elf32_Shdr {
  Elf32_Word sh_name; // Section name (index into string table)
  Elf32_Word sh_type; // Section type (SHT_*)
  Elf32_Word sh_flags; // Section flags (SHF_*)
  Elf32_Addr sh_addr; // Address where section is to be loaded
  Elf32_Off  sh_offset; // File offset of section data, in bytes
  Elf32_Word sh_size; // Size of section, in bytes
  Elf32_Word sh_link; // Section type-specific header table index link
  Elf32_Word sh_info; // Section type-specific extra information
  Elf32_Word sh_addralign; // Section address alignment
  Elf32_Word sh_entsize; // Size of records contained within the section
};

Sources

What’s the difference of section and segment in ELF file format

What is the role of .eh_frame segment

stacksmashing : In-depth: ELF – The Extensible & Linkable Format

Linux Kernel Foundation : [ Linux kernel ] Understanding ELF – Dissecting of Executable and Linking Format

https://llvm.org/doxygen/BinaryFormat_2ELF_8h_source.html

https://gist.github.com/CMCDragonkai/10ab53654b2aa6ce55c11cfc5b2432a4

COFF on Linux or ELF on Windows

What’s the difference of section and segment in ELF file format

Wikipedia: ELF

Très complet : 

http://www.skyfree.org/linux/references/ELF_Format.pdf

https://stackoverflow.com/a/2456882/16440965

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.