Linux : au cœur du processus de construction

Liran B.H

Traducteur

Chrtophe

Ce tutoriel de Liran B.H va vous faire une présentation succincte du format exécutable ELF utilisé par Linux/Unix.

Vous allez découvrir les informations de débogage, ainsi que des outils pratiques de manipulation et analyse de fichiers ELF.

Commentez

- Titre : Linux : au cœur du processus de construction

- Auteur : Liran B.H

- Traducteur : Chrtophe

- Parution : 9 février 2018

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2018 Liran B.H. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 1. Introduction

- 2. Les en-têtes Section

- 2-1. .bss et .data

- 3. Les informations de débogage

- 4. Autres outils utiles

- 5. Notes de la rédaction

1 - Introduction

Le format ELF (Executable and Linking Format) est le format de fichier standard pour les fichiers exécutables, les fichiers objets, les bibliothèques partagées et les core dumps sous Linux/Unix. Le format d'information de débogage le plus récent, compatible avec ELF, est DWARF 5.

Si vous n'écrivez pas un nouveau compilateur ou un débogueur, il n'est pas nécessaire de comprendre chaque bit de ces formats, mais il y a quelques concepts et quelques outils qui peuvent vous aider à écrire et à gérer votre code.

Prenons un exemple de code simple et compilons-le :

1. #include<stdio.h>

3. int a1=10,a2=20;

5. void f2()

6. {

7. printf("X");

8. }

10. void f1()

11. {

12. int i;

13. for(i=0;i<100;i++)

14. {

15. if(i % 20 == 0)

16. a1++;

17. f2();

18. }

19. }

21. void main()

22. {

23. char *str = "Bonjour, bonne journée....";

24. f1();

25. puts(str);

26. printf("hello %d\n",a1);

27. }

Compilons le code avec les options par défaut :

# gcc -o app ./test.c

Nous pouvons maintenant obtenir les informations du fichier ELF avec l'outil readelf :

# readelf -a ./app

On voit alors :

- l'en-tête ELF : utilisé par la commande file pour afficher les informations générales et l'architecture matérielle du code ;

- les en-têtes Section : le code, les données, les chaînes de caractères et plus ;

- les en-têtes Program : les en-têtes pour les bibliothèques dynamiques, la pile, etc., avec leurs permissions (Lecture/Écriture/Exécution) ;

- les sections dynamiques ;

- et plus…

2 - Les en-têtes Section

Section Headers:
[Nr] Name              Type             Address           Offset    Size              EntSize          Flags  Link  Info  Align
[ 0]                   NULL             0000000000000000  0000000   0000000000000000  0000000000000000           0     0     0
[ 1] .interp           PROGBITS         0000000000400238  00000238  000000000000001c  0000000000000000   A       0     0     1
[ 2] .note.ABI-tag     NOTE             0000000000400254  00000254  0000000000000020  0000000000000000   A       0     0     4
[ 3] .note.gnu.build-i NOTE             0000000000400274  00000274  0000000000000024  0000000000000000   A       0     0     4
[ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298  000000000000001c  0000000000000000   A       5     0     8
[ 5] .dynsym           DYNSYM           00000000004002b8  000002b8  0000000000000090  0000000000000018   A       6     1     8
[ 6] .dynstr           STRTAB           0000000000400348  00000348  0000000000000062  0000000000000000   A       0     0     1
[ 7] .gnu.version      VERSYM           00000000004003aa  000003aa  000000000000000c  0000000000000002   A       5     0     2
[ 8] .gnu.version_r    VERNEED          00000000004003b8  000003b8  0000000000000030  0000000000000000   A       6     1     8
[ 9] .rela.dyn         RELA             00000000004003e8  000003e8  0000000000000018  0000000000000018   A       5     0     8
[10] .rela.plt         RELA             0000000000400400  00000400  0000000000000060  0000000000000018  AI       5    24     8
[11] .init             PROGBITS         0000000000400460  00000460  000000000000001a  0000000000000000  AX       0     0     4
[12] .plt              PROGBITS         0000000000400480  00000480  0000000000000050  0000000000000010  AX       0     0     16
[13] .plt.got          PROGBITS         00000000004004d0  000004d0  0000000000000008  0000000000000000  AX       0     0     8
[14] .text             PROGBITS         00000000004004e0  000004e0  00000000000002a2  0000000000000000  AX       0     0     16
[15] .fini             PROGBITS         0000000000400784  00000784  0000000000000009  0000000000000000  AX       0     0     4
[16] .rodata           PROGBITS         0000000000400790  00000790  000000000000001c  0000000000000000   A       0     0     4
[17] .eh_frame_hdr     PROGBITS         00000000004007ac  000007ac  0000000000000054  0000000000000000   A       0     0     4
[18] .eh_frame         PROGBITS         0000000000400800  00000800  0000000000000174  0000000000000000   A       0     0     8
[19] .init_array       INIT_ARRAY       0000000000600e10  00000e10  0000000000000008  0000000000000000  WA       0     0     8
[20] .fini_array       FINI_ARRAY       0000000000600e18  00000e18  0000000000000008  0000000000000000  WA       0     0     8
[21] .jcr              PROGBITS         0000000000600e20  00000e20  0000000000000008  0000000000000000  WA       0     0     8
[22] .dynamic          DYNAMIC          0000000000600e28  00000e28  00000000000001d0  0000000000000010  WA       6     0     8
[23] .got              PROGBITS         0000000000600ff8  00000ff8  0000000000000008  0000000000000008  WA       0     0     8
[24] .got.plt          PROGBITS         0000000000601000  00001000  0000000000000038  0000000000000008  WA       0     0     8
[25] .data             PROGBITS         0000000000601038  00001038  0000000000000018  0000000000000000  WA       0     0     8
[26] .bss              NOBITS           0000000000601050  00001050  0000000000000008  0000000000000000  WA       0     0     1
[27] .comment          PROGBITS         0000000000000000  00001050  0000000000000034  0000000000000001  MS       0     0     1
[28] .shstrtab         STRTAB           0000000000000000  00001084  00000000000000fc  0000000000000000           0     0     1

Dans chaque section, le système de construction place différentes entités. Les sections importantes sont :

- .text : le code compilé ;

- .data : les données initialisées ;

- .bss : les données non initialisées.

2-1 - .bss et .data

Pour voir la différence, regardons un code simple :

int arr[1000];

void main()
{
arr[0]=1;
arr[1]=2;

....
}

Si nous déclarons un tableau global sans initialisation, le système de construction le place dans la section .bss :

Sortie de readelf :

Nous pouvons voir la taille des sections lors du chargement de l'application en mémoire, notamment la section .bss. Ci-dessous la taille du fichier ELF :

Developer@:~/testapp$ ls -l
total 16
-rw-rw-r-- 1 developer developer  287 fev 7 18:22 a.c
-rwxrwxr-x 1 developer developer 8848 fev 7 18:26 app

Mais si nous ajoutons quelques valeurs pour initialiser le tableau :

int arr[1024] = {1,2};

void main()
{
char *str = "Bonjour, passez une bonne journée.....";
...
}

Nous pouvons constater que le tableau est maintenant dans la section .data :

Et le fichier ELF est maintenant plus gros :

Developer@:~/testapp$ ls -l
total 16
-rw-rw-r-- 1 developer developer   285 fev 7 18:32 a.c
-rwxrwxr-x 1 developer developer 12992 fev 7 18:32 app

Cela signifie que le fichier ELF contient tout le tableau, même si nous n'avons initialisé que deux éléments.

Donc, si le même programme déclare une taille de tableau de 1 000 000 :

int arr[1000000] = {1,2};

void main()
{
char *str = "Bonjour, passez une bonne journée.....";
...
}

La taille est maintenant de :

Developer@:~/testapp$ ls -l
total 16
-rw-rw-r-- 1 developer developer     288 fev 7 18:32 a.c
-rwxrwxr-x 1 developer developer 4008896 fev 7 18:32 app

Le fichier ELF est maintenant plein de zéros.

3 - Les informations de débogage

Les informations de débogage sont placées dans le fichier ELF pour permettre au débogueur de savoir quelle ligne du source correspond au code machine. Le débogueur charge le programme et utilise les informations de débogage pour savoir où placer les points d'arrêt. Il remplace l'instruction machine par un trap (int 3 sur les CPU x86), ce qui provoque une exception CPU et le code d'origine est remis pour la reprise après l'exception.

Si nous compilons le code avec les informations de débogage, nous verrons que la taille devient plus grande :

developer@:~/testapp$ gcc -o app1 ./a.c
developer@:~/testapp$ gcc -g3 -o app2 ./a.c
developer@:~/testapp$ ls -l
total 48
-rw-rw-r-- 1 developer developer     290 fev 7 19:04 a.c
-rwxrwxr-x 1 developer developer    8824 fev 7 19:04 app1
-rwxrwxr-x 1 developer developer   29864 fev 7 19:04 app2

Et les nouvelles sections :

4 - Autres outils utiles

nm : liste les symboles avec les adresses :

developer@:~/testapp$ nm app2
0000000000601048 D a1
000000000060104c D a2
0000000000601050 B __bss_start
0000000000601050 b completed.7585
0000000000601038 D __data_start
0000000000601038 W data_start
.....

objdump : affiche les informations sur un fichier objet : file :

- les en-têtes (-x) ;

- les informations de débogage (-g) ;

- désassemblage (-d) ;

- désassemblage avec le code source (-S) ;

- et plus…

# objdump -S ./app2
...
void main()
{
400626:    55                       push   %rbp
400627:    48 89 e5                 mov    %rsp,%rbp
40062a:    48 83 ec 10              sub    $0x10,%rsp
char *str = "hello have a good day.....";
40062e:    48 c7 45 f8 f4 06 40     movq   $0x4006f4,-0x8(%rbp)
400635:    00
f1();
400636:    b8 00 00 00 00           mov    $0x0,%eax
40063b:    e8 87 ff ff ff           callq  4005c7 <f1>
puts(str);

addr2line : affiche le numéro de ligne du code source à partir de l'adresse connue.

Cet outil est très utile si le programme a planté et que nous avons écrit un gestionnaire d'erreur pour afficher l'adresse ayant généré celle-ci (compteur de programme). Nous pouvons utiliser cet outil avec un fichier ELF contenant des informations de débogage (le programme a pu être strippé - voir ci-dessous) :

developer@:~/testapp$ addr2line -e app2 0x400630
/home/developer/testapp/./a.c:25

strip : retire du fichier ELF les symboles et les informations de débogage.

Avant de déployer notre application, nous pouvons supprimer tous les symboles et les informations de débogage sans avoir à la recompiler.

developer@:~/testapp$ strip -o appstripped ./app
developer@:~/testapp$ ls -l
total 44
-rw-rw-r-- 1 developer developer   290 dec  7 19:04 a.c
-rwxrwxr-x 1 developer developer 29864 dec  7 19:05 app
-rwxrwxr-x 1 developer developer  6336 dec  7 21:17 appstripped

objcopy : copie et traduit des fichiers objets.

Vous pouvez utiliser objcopy pour copier du contenu d'un fichier ELF vers un autre, par exemple si vous voulez copier uniquement les informations de débogage dans un fichier séparé.

# gcc -g3 -o app ./a.c
# objcopy --only-keep-debug app app.debug
# strip -s ./app

Et vous pouvez ajouter plus tard les informations de débogage :

# objcopy --add-gnu-debuglink app.debug app
# gdb ./app

size : affiche la taille des sections.

# size ./app
text       data        bss        dec        hex    filename
1594        576          8       2178        882    ./app

5 - Notes de la rédaction

La rédaction remercie Liran B.H pour son autorisation de traduction de :

http://devarea.com/linux-inside-the-build-process/

La rédaction remercie également Chrtophe pour sa traduction ainsi que Jlliagre et f-leb pour leur relecture.