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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
#include<stdio.h>
int
a1=
10
,a2=
20
;
void
f2
(
)
{
printf
(
"
X
"
);
}
void
f1
(
)
{
int
i;
for
(
i=
0
;i<
100
;i++
)
{
if
(
i %
20
==
0
)
a1++
;
f2
(
);
}
}
void
main
(
)
{
char
*
str =
"
Bonjour, bonne journée....
"
;
f1
(
);
puts
(
str);
printf
(
"
hello %d
\n
"
,a1);
}
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.