Chaîne de compilation et exécutables C
Chaîne de compilation et exécutables C
La licence confère à l’utilisateur un droit d’usage sur le document consulté ou téléchargé, totalement ou en partie, dans les
conditions définies ci-après et à l’exclusion expresse de toute utilisation commerciale.
Le droit d’usage défini par la licence autorise un usage dans un cadre académique, par un utilisateur donnant des cours
dans un établissement d’enseignement secondaire ou supérieur et à l’exclusion expresse des formations commerciales et
notamment de formation continue. Ce droit comprend :
Tout autre usage que ceux prévus par la licence est soumis à autorisation préalable et expresse de l’auteur :
[Link]@[Link]
Ce qu'on va apprendre :
écrire du C propre
ce que fait un compilateur
ce que fait un éditeur de lien
comment sont construits les exécutables, à l'octet près
Les types les plus petits ayant au moins la taille demandée (utile
sur un système imaginaire ayant des bytes de 10 bits par exemple)
:
int_least8_t / uint_least8_t : 10 bits
int_least16_t / uint_least16_t : 20 bits
…
int main()
{
uint32_t a= 0x44332211;
uint8_t *p = (uint8_t *) &a;
printf("premier octet de a = %x\n", *p);
return 0;
}
Inclusions réciproques
si deux headers se référencent mutuellement,
comment les écrire ?
exemple :
/* a.h */
#include "b.h"
/* b.h */
#include "a.h"
Inclusions réciproques
si deux headers se référencent mutuellement,
comment les écrire ?
exemple :
/* a.h */
#ifndef A_H
#define A_H
#include "b.h"
#endif
/* b.h */
#ifndef B_H
#define B_H
#include "a.h"
#endif
page 12 23/09/23 SE203 ⓒ A. Polti & S. Tardieu
4ème commandement de l'UE
Type de main en C
quel est le type de main ?
pourquoi est-ce vraiment important ?
Attention : volatile n’est plus une solution acceptable pour synchroniser des
variables dès lors que plusieurs cœurs sont impliqués !
// Utilisable ainsi :
*gpio_config = 20;
uint32_t value = *gpio_config;
// Autre possibilité
#define GPIO_REG (*(volatile uint32_t *) 0xA300020)
GPIO_REG = 20;
value = GPIO_REG;
On a vu
quelques rappels importants de C
On va voir maintenant
ce que fait exactement une chaîne de compilation
ce que fait un compilateur
écrire du C propre
à quoi sert un éditeur de lien
: fichier de déclarations
Compilation : unité de compilation
: script de link
pré-processeur compilateur C assembleur linker
a.h
Exemple : GCC
GCC : GNU Compiler Collection
compilateurs
pré-processeurs
driver (gcc -v / gcc -###)
binutils
éditeur de lien (linker) : ld / gold / collect2
assembleur : as
objcopy, objdump, gprof, strip, readelf, nm, size
GNU Debugger : gdb
Bibliothèque C
glibc, eglibc
newlib, tinylibc, dietlibc
klibc
page 25 23/09/23 SE203 ⓒ A. Polti & S. Tardieu
Chaîne de compilation : GCC
Pré-processeur : cpp
processeur texte
interprète les directives de compilation
#include
#define / #undef
#ifdef / #ifndef
#if
...
produit un fichier C pré-processé (.i, .ii pour le C++)
on peut voir le fichier résultant avec gcc -E
-Dname / -Dname=value : définit des macros
-Uname : annule la définition de macros
-iquote dir :
ajoute dir à la liste où sont cherchés les headers utilisateurs
-isystem dir :
ajoute dir à la liste où sont cherchés les headers systèmes
-I dir :
ajoute dir à la liste où sont cherchés les headers
-nostdinc :
limite la recherche des headers à « . » et aux répertoires spécifiés par -I et -iquote
cross compilation : le compilateur produit du code pour une cible différente de la machine où il
s'exécute
compilation du
exécution du compilateur exécution du code nom
compilateur
x86 x86 x86 natif
x86 x86 ARM cross
x86 SH x86 crossback
x86 SH SH crossed native
x86 SH PPC canadian cross
un cross-compilateur se comporte comme un compilateur natif
certaines cibles ont des options particulières :
gcc --target-help pour les connaître
On a vu
quelques rappels importants de C
les chaînes de compilation
On va voir maintenant
ce que fait un compilateur
écrire du C propre
à quoi sert un éditeur de lien
Code
nous utiliserons la définition suivante de code :
suite d'instructions destinées à un ordinateur
exemples de code :
code C : programme en langage C
code machine : suite de mots binaires directement exécutables par un
processeur
bytecode : suite de mots exécutables par une machine virtuelle
pseudocode : suite d'instructions à effectuer, facilement compréhensibles
par des humains
Processus de compilation
Génération de code
Analyse
(étudiée ici)
Arbre sémantique
Étapes
La définition d'un langage intermédiaire facilite la réutilisation :
M parties frontales et N générateurs de code permettent d'écrire M + N + 1
fragments plutôt que M × N
Frontal (front-end)
La partie frontale d'un compilateur
transforme le code en un arbre syntaxique
construit les associations sémantiques
vérifie la syntaxe et la sémantique du code
Exemple : GCC
GCC (GNU Compiler Collection) utilise un langage intermédiaire :
gimple
GCC génère un fichier assembleur temporaire
L'optimisation se fait à chaque niveau
Il est facile de rajouter :
un nouveau langage
une nouvelle cible
Génération de code
But : permettre à chaque sous-programme présent dans l'arbre
d'être appelé
Moyens :
pour chaque instruction, générer du code effectuant les bonnes opérations
générer du code pour l'entrée (prologue) et la sortie (épilogue) du sous-
programme
Code machine
Le code machine
est simple : xy n'existe pas
a peu d'arguments : f(u, v, w, x, y, z) est impossible
est peu structuré : for(i=0; i<10; i++) n'est pas représentable
simplement
Constructions ternaires
classiquement, chaque opération est transformée en une suite de
constructions généralement ternaires, car cela :
correspond aux possibilités usuelles d'un microprocesseur
permet d'optimiser indépendamment chaque instruction
permet d'unifier les sous-expressions communes
Constructions usuelles
sur les architectures load / store :
une instruction ne peut manipuler que des registres
l'accès à la mémoire se fait par les instructions LDR et STR
a: ...
... f();
goto a; ...
devient : devient :
a: ...
... bl f
b a ...
Constructions usuelles
Les boucles sont généralement ré-écrites selon la structure
suivante :
Constructions usuelles
Exemple : for (i=0;i<10;i++) {...}
1:i = 0
2:
3 : if NOT(i < 10) goto 7
4 : ...
5:i = i + 1
6 : goto 3
7:
Constructions usuelles
Exemple : while(c) {...}
1:
2:
3 : if NOT(c) goto 7
4 : ...
5:
6 : goto 3
7:
Constructions usuelles
Exemple : do {...} while(c)
1:
2 : goto 4
3 : if NOT(c) goto 7
4 : ...
5:
6 : goto 3
7:
À vous de travailler :
exercice 1 : facile
exercice 2 : moyen
Exercice 2
Traduire en assembleur ARM le code suivant :
// Global variables
uint32_t *a;
uint32_t *b;
uint32_t *c;
…
*a += *c;
*b += *c;
Exercice 2 (suite)
Comparez avec ce que produit GCC, ainsi :
// Global variables
uint32_t *a;
uint32_t *b;
uint32_t *c;
On a vu
quelques rappels importants de C
les chaînes de compilation
ce que fait un compilateur
On va voir maintenant
comment écrire du C propre
à quoi sert un éditeur de lien
déclaration :
association nom ↔ type
exemple : extern int a;
empêche l'initialisation : extern int a=2;
instanciation / définition :
déclaration + allocation de la mémoire pour stocker la variable
exemple : int a;
exemple : int a=3;
page 53 23/09/23 SE203 ⓒ A. Polti & S. Tardieu
Compléments de C
conséquence :
une variable ne doit être instanciée qu'une seule fois, dans un fichier
source .c
exception : symboles weak, qu'on verra après
déclaration :
association nom ↔ (type de retour, types des paramètres)
exemple : int foo(int, char);
définition :
déclaration + code du corps de la fonction
exemple : int foo(int a, char b) { return a+b; }
page 56 23/09/23 SE203 ⓒ A. Polti & S. Tardieu
Compléments de C
Déclaration et définition des fonctions
le compilateur peut générer du code utilisant une fonction même si sa
définition et/ou son adresse ne sont pas encore connues
il n'a besoin que de savoir
quels arguments lui passer
comment récupérer la valeur de retour
bref, connaître seulement sa déclaration et les conventions d'appel (ABI)
l'adresse à laquelle sauter pour exécuter la définition de la fonction est prise
temporairement nulle
et sera rectifiée à l'édition de lien
conséquence :
une fonction ne doit être définie qu'une seule fois, dans un fichier source .c
exemples :
static int a;
static void foo() { … }
Fonctions inline
pour optimiser la vitesse d'exécution
évite les prologues / épilogues / appels
permet des optimisations entre appelant et appelé (arguments constants, …)
pas toujours possible
en C99 :
le plus simple : static inline void foo() {…}
nécessite au moins -O1 et dépend de la taille de la fonction
forcer un inline : __attribute__((always_inline))
empêcher un inline : __attribute__ ((noinline))
Les fonctions static inline seront dans cette UE les seules fonctions
à pouvoir être définies dans des headers.
Variables locales
auto :
allouées / désallouées automatiquement
généralement dans un registre
stockées temporairement sur la pile (spilled) s’il n’y a pas assez de
registres disponibles
static :
allouées et initialisées au lancement du programme
durent pendant toute la durée du programme
Exemple : GCC
gcc -Ox : optimisations
-Os/-Oz : taille / taille en plus agressif
-O0 : aucune optimisation
-O1 ... -O3 : de plus en plus d'optimisations
-O4 ... -O999 : identique à -O3
-Og : permet un débug plus simple
-Ofast : le plus rapide, quitte à ne pas être complètement conforme (par
exemple dans les calculs sur flottants)
On compilera toujours en
-O1 ou -Og : code simple à lire
-O2 : standard, mais code plus compliqué à
suivre
Les warnings
warning : quand le compilateur ne peut pas décider de lui même si
un morceau de code est valide ou non
pas là pour faire joli
implique obligatoirement une intervention humaine
la plupart des projets sérieux obligent une compilation sans warnings
flags :
gcc -Wall : active tous les warnings facilement corrigeables
gcc -Wextra : active des warnings supplémentaires
gcc -Werror : traite les warnings comme des erreurs
On a vu
quelques rappels importants de C
les chaînes de compilation
comment écrire du C propre
On va voir maintenant
ce que fait un éditeur de lien
Principe
les fichiers objets
peuvent contenir des références vers des symboles externes : les adresses de
ces symboles sont temporairement nulles
ont un code logé temporairement à l'adresse 0
le linker se chargera de :
allouer à tous les symboles une adresse finale
remplacer les références externes (adresses temporaires nulles) par les
adresses finales
Exemple
00000000 <main>: Avant link
#include <stdio.h> 0: e92d4008 push {r3, lr}
4: e59f3010 ldr r3, [pc, #16] ; 1c <main+0x1c>
const char *mesg = "Hello World!"; 8: e5930000 ldr r0, [r3]
c: ebfffffe bl 0 <printf>
int main() { 10: e3a00000 mov r0, #0
printf(mesg); 14: e8bd4008 pop {r3, lr}
return 0; 18: e12fff1e bx lr
} 1c: 00000000 .word 0x00000000
fichiers d'entrée :
*.o
*.a
ldscript
gcc -flto
On sait maintenant :
écrire du C propre
ce que fait un compilateur
à quoi sert un éditeur de lien
On va voir
comment sont architecturés les exécutables
comment piloter l'éditeur de lien pour produire
l'exécutable qu'on veut.
selon leur destination, les exécutables n'ont pas à fournir les même
informations → différents formats !
page 78 23/09/23 SE203 ⓒ A. Polti & S. Tardieu
Anatomie d'un exécutable
les objets :
contiennent du code
ainsi que beaucoup d'informations auxiliaires (sections, infos de débug, infos
de relocation, …)
.bss Données non initialisées (ou initialisées à zéro). Le système initialise cette zone au
NOBITS A, W
.sbss lancement de l'exécution avec des zéros. Peut aussi servir à la pile et au tas
.heap A, W Le tas.
La pile, généralement après .heap. Peut être combiné avec .heap pour former une
.stack
section .bss_stack
.init Initialisation du processus. Le système exécute cette section avant d'appeler main.
PROGBITS A, EX
.ini Utilisé par la libC pour initialiser des variables globales.
Terminaison du processus. Le système exécute cette section à la fin du processus si
.fini PROGBITS A, EX
celui-ci s'est correctement terminé.
.ctors PROGBITS A, W Pointeurs vers des fonctions à appeler au démarrage du programme
.dtors PROGBITS A, W Pointeurs vers des fonctions à appeler à la fin du programme
Global Offset Table : permet de référencer les variables globales des bibliothèques
.got / .got2 PROGBITS A, W
dynamiques
Procedure Linkage Table : permet de référencer les fonctions des bibliothèques
.plt PROGBITS A, EX
dynamiques (lazy binding)
[Link] none
[Link] REL (sauf bib Informations de relocations pour le code, les données et les constantes
[Link] dyn)
int32_t x = 34;
int32_t y;
const char mesg[] = "Hello World!";
int main() {
static uint8_t z;
uint16_t t;
y = 12;
z = z + 1;
t = y+z;
printf(mesg);
printf("x = %d, y = %d, z = %d, t = %d\n",
x, y, z, t);
$ arm-none-eabi-gcc -c t.c
$ arm-none-eabi-objdump -f t.o
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000000b8 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 00000000 00000000 000000ec 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000001 00000000 00000000 000000f0 2**0
ALLOC
3 .rodata 00000032 00000000 00000000 000000f0 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000001e 00000000 00000000 00000130 2**0
CONTENTS, READONLY
5 .[Link] 0000002a 00000000 00000000 0000014e 2**0
CONTENTS, READONLY
SYMBOL TABLE:
00000000 l df *ABS* 00000000 t.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .rodata 00000000 .rodata
00000000 l .bss 00000001 z.5152
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 x
00000000 g O .bss 00000004 y
00000000 g O .rodata 0000000d mesg
00000000 g F .text 000000b8 main
00000000 *UND* 00000000 printf
*UND* : un symbole pour l'instant non défini, qui sera résolu plus
tard - du moins on l'espère.
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000000b8 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 00000000 00000000 000000ec 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000001 00000000 00000000 000000f0 2**0
ALLOC
3 .rodata 00000032 00000000 00000000 000000f0 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000001e 00000000 00000000 00000130 2**0
CONTENTS, READONLY
5 .[Link] 00000030 00000000 00000000 0000014e 2**0
CONTENTS, READONLY
int main()
{
p[0] = 'H';
printf("%s", p);
return 0;
}
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
EXIDX 0x012488 0x00012488 0x00012488 0x00008 0x00008 R 0x4
LOAD 0x000000 0x00000000 0x00000000 0x12494 0x12494 R E 0x10000
LOAD 0x012494 0x00022494 0x00022494 0x009c0 0x00a20 RW 0x10000
Outils ELF
objdump : examen du contenu d'objets
objdump -f : infos générales
objdump -h : les sections
objdump -p : les segments
objdump -t : la table des symboles
objdump -x : tous les headers
objdump -d : désassemble les sections de code
objdump -S : désassemble les sections de code en
mixant avec le source C (s'il contient des
informations de débug)
Outils ELF
readelf : idem, mais que pour ELF
readelf -h : infos générales
readelf -S : les section
readelf -l : les segments
readelf -s : la table des symboles
readelf -d : les sections dynamiques
complémentaire d'objdump
On sait maintenant :
écrire du C propre
ce que fait un compilateur
à quoi sert un éditeur de lien
comment sont architecturés les exécutables
On va voir
comment piloter l'éditeur de lien pour produire
l'exécutable qu'on veut.
data
Les segments sont créés text
MEMORY
permet de décrire le layout de la mémoire. Optionnel.
chaque entrée est appelée « région »
exemple :
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (wx) : ORIGIN = 0x40000, LENGTH = 4M
}
SECTION
permet de décrire comment fusionner les sections des objets
d'entrée :
// LDSCRIPT
SECTIONS
{
.text : {
*(.text)
} > rom
.data 0x2000 : {
*(.data)
} AT> rom
.bss 0x3000 : {
*(.bss) *(COMMON)
}
}
page 109 23/09/23 SE203 ⓒ A. Polti & S. Tardieu
Linkers et linker scripts
Commandes :
ENTRY(symbol) :
définit "symbol" comme étant le point d'entrée du programme
usuellement : _start
symboles et affectations :
crée un symbole et lui donne une valeur
exemple : etext = 0x1000;
Exemple :
// LDSCRIPT
start_of_RAM = 0x1000;
start_of_TEXT = .text;
sizeof_of_TEXT = sizeof (.text);
// init.c
extern uint8_t start_of_RAM, end_of_TEXT,
sizeof_of_TEXT;
.
location counter
modification : SECTIONS
à l'intérieur d'une section : offset par {
rapport au début de la section . = 0x100;
.text: {
à l'extérieur d'une section : adresse *(.text)
(VMA) absolue . = 0x200;
}
. = 0x500
affecté à un symbole : .data: {
symbole = VMA(.) *(.data)
. += 0x600;
}
ne peut jamais revenir en arrière }
page 112 23/09/23 SE203 ⓒ A. Polti & S. Tardieu
Linkers et linker scripts
// LDSCRIPT
MEMORY {
rom (rx) : ORIGIN = 0x00000000, LENGTH = 256K
ram (!rx): ORIGIN = 0x00200000, LENGTH = 1M
} // init.c
SECTIONS { extern char _etext, _data,
.text : { _edata, _bstart, _bend;
*(.text)
_etext = . ; static uint8_t *src = &_etext;
} > rom static uint8_t *dst = &_data;
.data : AT (ADDR(.text)+SIZEOF(.text)) { // ROM has placed .data at end of .text.
_data = . ; // Copy .data to RAM
*(.data) while (dst < &_edata)
_edata = . ; *dst++ = *src++;
} > ram
LENGTH(region) :
renvoie la longueur d'une région
ADDR(section) :
renvoie la VMA d'une section
LOADADDR(section) :
renvoie la LMA d'une section
SIZEOF(section) :
renvoie la taille d'une section
page 114 23/09/23 SE203 ⓒ A. Polti & S. Tardieu
Derniers détails
crt0.s
en charge de préparer l'espace d'exécution
prépare la pile et positionne le pointeur de pile
met le bss à zéro
recopie les données de la ROM vers la RAM
en utilisant les symboles exportés par le linker
si on est sûr que data et/ou bss sont vides, ce n'est pas la peine de
les initialiser
symboles spéciaux : etext, edata, end
manipule les sections selon leur LMA lors de la génération d'une image binaire
peut modifier des sections au passage :
strip
relocations
Conversion de formats
inversement, il est possible de transformer un fichier
binaire quelconque en une section ELF (.data par défaut) :
objcopy -I binary -O elf64-little -B i386 input_file output_file
À vous de travailler :
exercice 1 : facile
exercice 2 : plus compliqué
Exercice 1 :
1) Sur un PC Linux x86_64 actuel et par adresses croissantes, dans
quel ordre sont stockées les sections suivantes : text, data*,
rodata*, bss, pile et tas au moment de l’exécution ?
Exercice 2 :
1) Compilez sans édition de lien ce code-ci ([Link] pour ARM
avec une chaîne récente, et avec les optimisations suivantes : Os, O0, O1
et O2.
Pour chaque niveau d'optimisation, justifiez la taille des sections de
données que vous obtenez.
3) Remplacez const char mesg[] par const char *mesg. puis par
const char * const mesg. Expliquez les différences dans le code
généré et les sections de données par rapport à la question 2.
Exercice 2 : indications
Cet exercice est destiné à vous faire manipuler gcc et objdump. Il
a l'air simple, mais c'est un véritable jeu de piste, complexe, et qui
prend du temps. Vous devrez avancer pas à pas, compiler,
examiner à la loupe les différentes sections, tester, recommencer.
N'hésitez pas à poser des questions au fur et à mesure par mail.
La licence confère à l’utilisateur un droit d’usage sur le document consulté ou téléchargé, totalement ou en partie, dans les
conditions définies ci-après et à l’exclusion expresse de toute utilisation commerciale.
Le droit d’usage défini par la licence autorise un usage dans un cadre académique, par un utilisateur donnant des cours
dans un établissement d’enseignement secondaire ou supérieur et à l’exclusion expresse des formations commerciales et
notamment de formation continue. Ce droit comprend :
Tout autre usage que ceux prévus par la licence est soumis à autorisation préalable et expresse de l’auteur :
[Link]@[Link]