2024
Objectifs du module:
Modalités:
Partie Programmation
Partie Système
Evaluation
Système de classe inversée. Pour chaque séance :
Avant la séance
Pendant la séance:
Attention ! Cela ne fonctionne que si vous travaillez sérieusement avant la séance.
Hypothèse: les étudiants suivant ce cours sont des adultes responsables.
Les supports de cours à étudier pour chaque séance sont disponibles dans plusieurs formats:
Pour vous aider, vous avez à votre disposition:
man 2 <fonction>
ou
man 3 <fonction>
)Les IA génératives sont de super outils qui peuvent améliorer la productivité des développeurs. Il existe des plugins qui s’intègrent directement dans certains IDE et qui permettent de générer du code à la place du développeur. Si ces outils peuvent être utiles pour un développeur dans une entreprise, ils sont à proscrire dans le cadre de vos études.
Le but des modules que vous suivez est de vous apprendre à programmer et à comprendre certains concepts ou problèmes. Si une IA écrit le code à votre place, vous ne pouvez pas être confrontés à ces problèmes/concepts et vous n’êtes pas capable de vérifier que le code généré est correct.
Par ailleurs, lorsqu’on vous évalue (que ce soit lors d’un DM, ou TP noté), vos enseignants évaluent votre travail, pas celui d’une IA. Si vous rendez du code généré par une IA, il est possible que d’autres étudiants fassent de même, et la probabilité pour que vous rendiez le même code (ou du code similaire) est forte. Ceci s’apparente à la triche, ce qui est fortement sanctionné à Télécom SudParis (0/20 au module, impossibilité de passer un éventuel CF2, potentielles sanctions supplémentaires décidées par la DF)
Il convient donc de désactiver ces éventuels plugins dans vos IDE, que ce soit pendant les TP, ou les DM. Vous êtes bien sûr libres de les réactiver pour votre projets perso.
Les corrigés des différents TP sont disponibles en ligne et dans certains modules, des enregistrements vidéo des TP viennent compléter les ressources que nous proposons aux étudiants pour les aider. Il arrive que des étudiants se contentent de copier-coller les corriger, puis compiler et tester, avant de passer à l’exercice suivant. Ces étudiants ne sont donc pas confrontés aux problèmes qu’on souhaite montrer et n’apprennent pas à les résoudre par eux-même.
Il convient donc de faire les TP par vous même. Si vous êtes bloqués, posez des questions à l’enseignant qui vous mettra sur la voie. En faisant cela, vous progresserez et serez capable de faire les TP sans avoir le corrigé (ce qui sera utile lors du TP noté final, mais également dans votre vie professionnelle future).
L’entraide est bien sûr encouragée dans nos enseignements. Lorsque vous ne comprenez pas un concept ou lorsque vous révisez, nhésitez pas solliciter vos camarades. Par contre, lorsqu’il s’agit d’un travail évalué (DM, TP noté, etc.), cette entraide est contre-productive. Notre but est d’évaluer vos compétences et vous faire progresser. Récupérer le code d’un camarade, ou partager votre code avec eux (que ce soit en s’envoyant des fichiers, via un répo git, ou via une messagerie type discord) est très risqué. En cas de triche, tous les tricheurs (qu’ils aient récupéré le code d’un camarde, ou partagé le leur) auront 0/20 au module.
D’une manière générale, vous avez tous un bon niveau en informatique et vous devriez tous pouvoir valider ce module si vous y travaillez. Nous sommes conscients du fait que votre emploi du temps est parfois très chargé. Mais vous pouvez le faire par vous même sans tricher.
Fichier *.c
/* hello_world.c */
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv) {
("Hello World!\n");
printfreturn EXIT_SUCCESS;
}
Compilation/execution:
$ gcc hello_world.c -o hello_world -Wall -Werror
$ ./hello_world
Hello World!
#include <stdio.h>
indiquent que le programme
a besoin des outils stdio
. Il s’agit donc d’un équivalent
du import package
de Javaprintf("message\n");
return EXIT_SUCCESS;
à la fin du main
permet de spécifier le code retour du programme (accessible depuis le
shell en faisant echo $?
). En cas d’erreur, on peut
retourner EXIT_FAILURE
à la place de
EXIT_SUCCESS
.Pour les types simples, déclaration identique à Java:
int var1;
int var2, var3, var4;
int var5 = 42;
Types disponibles:
int
, short
,
long
, long long
float
, double
char
Pour les entiers: possibilité de préfixer le type par
unsigned
. Les variables sont alors non-signées (ie.
positives).
La taille d’une variable entière (ie. le nombre de bits/octets)
dépend de l’implémentation. Le standard C ne spécifie que la taille
minimum. Ainsi, un int
doit faire au moins 16 bits, alors
que la plupart des implémentations modernes utilisent 32 bits pour les
int
. Il convient donc de ne pas se reposer sur ces types
lorsqu’on a besoin d’un nombre précis de bits/octets.
Pour cela, il est préférable d’utiliser les types fournis par
stdint.h
: uint8_t
(8 bits),
uint16_t
(16 bits), uint32_t
(32 bits), ou
uint64_t
(64 bits).
Comme en Java, les variables déclarées dans une fonction sont locales à la fonction (elles disparaissent donc dès la sortie de la fonction). Les variables déclarées en dehors d’une fonction sont globales: elles sont accessibles depuis n’importe quelle fonction.
La liste des opérateurs disponibles est à peu près la même qu’en Java:
+
, -
, *
,
/
, \%
=
, +=
, -=
,
*=
, /=
, \%=
++
, {-
}<
, <=
,
>
, >=
, ==
,
!=
!
, &&
,
||
Mais également:
sizeof n
: donne le nombre d’octets qui constitue une
variable/un type n
Possibilité de travailler sur des champs de bits.
<<
, >>
|
, AND :&
, XOR : ^
,
NOT : ~
<<=
, >>=
,
|=
, &=
, ^=
,
~=
/* bits.h */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main(int argc, char** argv) {
uint32_t v = 1;
int i;
/* l'operateur << decale vers la gauche */
for(i=0; i<32; i++) {
/* v << i decale les bits de v de i places vers la gauche
* c'est equivalent à calculer v*(2^i)
*/
("v<<%d = %u\n", i, v<<i);
printf}
= 5;
v /* v | 3 effectue un OU logique entre les bits de v et la representation binaire de 3
* 101 | 11 = 111 (7)
*/
("%u | %u = %u\n", v, 3, v|3);
printf
/* v & 3 effectue un ET logique entre les bits de v et la representation binaire de 3
* 101 & 11 = 001 (1)
*/
("%u & %u = %u\n", v, 3, v&3);
printf
/* v ^ 3 effectue un XOR logique entre les bits de v et la representation binaire de 3
* 101 ^ 011 = 110 (6)
*/
("%u ^ %u = %u\n", v, 3, v^3);
printf
/* ~v effectue un NON logique des bits de v
* ~ 00...00101 = 11..11010 (4294967290)
*/
("~%u = %u\n", v, ~v);
printf
return EXIT_SUCCESS;
}
Lorsqu’on opère un décalage (avec <<
) sur une
valeur signée (par exemple, un int
), le bit de signe n’est
pas modifié par le décalage. Par exemple, si les bits d’un
int a
sont à
1010 0000 0000 0000 0000 0000 0000 0000
, le résultat de
a >> 1
est
1001 0000 0000 0000 0000 0000 0000 0000
.
Comme en Java:
for(i=0; i<n; i++) { ... }
while(cond) {... }
do { ... } while(cond);
if (cond) { ... } else { ... }
printf("%d exemple de %f format \n", v1, v2);
scanf("%d-%f", &v1, &v2);
/* formats.c */
#include <stdio.h>
int main(int argc, char** argv) {
int v;
("Entrez la valeur de v:\n");
printf("%d", &v);
scanf("v = %d (en decimal)\n", v);
printf("v = %u (en decimal non signe)\n", v);
printf("v = %x (en hexadecimal)\n", v);
printf("v = %o (en octal)\n", v);
printf("v = %c (en ASCII)\n", v);
printf
double a;
("%lf", &a);
scanf("a = %f (en flottant)\n", a);
printf("a = %lf (en flottant double precision)\n", a);
printf("a = %e (en notation scientifique)\n", a);
printf
char *chaine = "Bonjour";
("chaine = %s\n", chaine);
printf("chaine = %p (adresse)\n", chaine);
printf
("On peut aussi afficher le caractère %%\n");
printf}
Déclaration:
(type_param1 param1, type_param2 param2) {
type_retour nom_fonc/* déclaration des variables locales */
/* instructions à exécuter */
}
En C, il est d’usage de nommer les fonctions en minuscule, en
séparant les mots par _
. Cette convention se nomme Snake case.
Par exemple, l’équivalent en C de la fonction Java
calculerLeMinimum()
(qui utilise la convention de nommage
camel case) sera
calculer_le_minimum()
.
char
, short
, int
,
long long
(signés par défaut, non signés si préfixés de
unsigned
)float
(4 octets), double
(8 octets)Une structure est une définition d’un nouveau type de données
Remarque: les sous-types d’une structure s’appellent des champs
Définition d’une nouvelle structure avec :
struct nom_de_la_structure {
;
type1 nom_du_champs1;
type2 nom_du_champs2...
};
Par exemple:
struct nombre_complexe {
int partie_reelle;
int partie_imaginaire;
};
Par convention, les noms de structures commencent par une minuscule en C
Une structure peut aussi être composée à partir d’une autre structure, comme dans cet exemple:
struct point {
int x;
int y;
};
struct segment {
struct point p1;
struct point p2;
};
En revanche, une structure ne peut pas être composée à partir d’elle-même. À titre d’illustration, l’exemple suivant n’est pas correct:
struct personnage {
struct personnage ami;
int point_de_vie;
};
Cette construction est impossible car il faudrait connaître la taille
de la structure personnage
pour trouver la taille de la
structure personnage.
struct nombre_complexe z1, z2, z3;
/* partie_relle de z prend la valeur 0 */
/* partie_imaginaire de z prend la valeur 1 */
struct nombre_complexe i = { 0, 1 };
/* autre solution : */
struct nombre_complexe j = { .partie_reelle=0, .partie_imaginaire=1 };
L’initialisation d’une variable de type structure est différente lorsque la variable est déclarée globalement ou localement. On vous rappelle qu’une variable globale si elle est déclarée en dehors de toute fonction. Sinon, on dit qu’elle est locale.
Lorsqu’une variable de type structure est déclarée en tant que variable globale sans être initialisée, le compilateur initialise chacun de ces champs à la valeur 0. En revanche, lorsqu’une structure est déclarée en tant que variable locale dans une fonction sans être initialisée, ces champs prennent une valeur aléatoire.
Par exemple, dans :
struct nombre_complexe i;
void f() {
struct nombre_complexe j;
}
Les champs de i
sont initialisés à 0 alors que ceux de
j
prennent une valeur aléatoire.
On peut aussi partiellement initialiser une structure comme dans l’exemple suivant :
struct nombre_complexe j = { 1 };
Dans ce cas, le champs partie_relle
prend la valeur 1 et
le champs partie_imaginaire
prend soit la valeur 0 si la
variable est globale, soit une valeur aléatoire si la variable est
locale à une fonction.
struct point {
int x;
int y;
};
struct ligne {
struct point p1;
struct point p2;
};
void f() {
struct point p;
struct ligne l;
.x = 42;
p.y = 17;
p.p1.x = 1;
l.p1.y = 2;
l.p2 = p; /* copie p.x/p.y dans l.p2.x/l.p2.y */
l
("[%d %d]\n", p.x, p.y);
printf}
[taille_du_tableau]; type_des_elements nom_de_la_variable
int a[5]; /* tableau de 5 entiers */
double b[12]; /* tableau de 12 nombres flottants */
struct point c[10]; /* tableau de 10 structures points */
int d[12][10]; /* tableau de 10 tableaux de 12 entiers */
/* => d est une matrice 12x10 */
n
du tableau tab
se
fait avec tab[n]
void f() {
int x[3];
int y[3];
int i;
/* 0 est le premier élément, 2 est le dernier */
for(i=0; i<3; i++) {
[i] = i;
x[i] = x[i] * 2;
y}
}
struct point {
int x;
int y;
};
struct triangle {
struct point sommets[3];
};
void f() {
struct triangle t;
for(i=0; i<3; i++) {
.sommets[i].x = i;
t.sommets[i].y = i * 2;
t}
}
On ne peut pas accéder à la taille d’un tableau
Lors d’un accès en dehors des bornes du tableau, l’erreur est silencieuse :
c’est une erreur, mais elle n’est pas signalée immédiatement
=> parmi les erreurs les plus fréquentes (et les plus difficiles à repérer) en C
void f() {
int x[3];
[4] = 42; /* Erreur silencieuse !!! */
x/* Écriture à un emplacement aléatoire en mémoire */
/* le bug pourra apparaître n'importe quand */
}
[taille] = { e0, e1, e2, ... }; type_element nom_variable
Par exemple:
int x[6] = { 1, 2, 3, 4, 5, 6 };
Comme pour les structures, on peut partiellement initialiser un tableau
int x[6] = { 1, 1, 1 };
En l’absence d’initialisation : * Si le tableau est une variable globale, chaque élément est initialisé à 0 * Sinon, chaque élément est initialisé à une valeur aléatoire
Lorsqu’on initialise un tableau lors de sa déclaration, on peut omettre la taille du tableau. Dans ce cas, la taille du tableau est donnée par la taille de la liste d’initialisation.
int x[] = { 1, 2, 3 }; /* tableau à trois éléments */
int y[6] = { 1, 2, 3 }; /* tableau à six éléments, avec les trois premiers initialisés */
struct point {
int x;
int y;
};
struct triangle {
struct point sommets[3];
};
struct triangle t = {
{ 1, 1 },
{ 2, 3 },
{ 4, 9 }
};
Une chaîne de caractère est simplement un tableau de caractères
terminé par le caractère '\0'
(c’est à dire le nombre
zéro)
char yes[] = "yes";
est équivalent à
char yes[] = { 'y', 'e', 's', '\0' };
Passage par valeur : l’argument est copiée de l’appelé vers l’appelant
=> l’argument et sa copie sont deux variables différentes
Passage par référence : une référence vers l’argument de l’appelant est donné à l’appelé
=> l’appelant et l’appelé partagent la même donnée
type nom[]
, sans la taille/* le x de f et le x du main sont deux variables distinctes */
/* le fait qu'elles aient le même nom est anecdotique */
void f(int x) {
= 666;
x ("f : x = %d\n", x); /* f : x = 666 */
printf}
int main() {
int x = 42;
(x); /* x est copié dans f */
f/* => le x de main n'est donc pas modifié par f */
("g : x = %d\n", x); /* g : x = 42 */
printfreturn 0;
}
struct point {
int x;
int y;
};
void f(struct point p) {
.x = 1;
p("(%d, %d)\n", p.x, p.y); /* => (1, 2) */
printf}
int main() {
struct point p = { -2, 2 };
(p); /* p est copié dans f */
f("(%d, %d)\n", p.x, p.y); /* => (-2, 2) */
printfreturn 0;
}
void print(int x[], int n) {
for(int i=0; i<n; i++) {
("%d ", x[i]);
printf}
("\n");
printf}
int main() {
int tab[] = { 1, 2, 3 };
(tab, 3); /* => 1 2 3 */
print(tab);
f(tab, 3); /* => 1 42 3 */
print
return 0;
}
/* x est une référence vers le tableau original */
void f(int x[]) {
[1] = 42; /* => modifie l'original */
x}
struct nom { type_1 champs_1; type_2 champs_2; ... };
struct nom var = { v1, v2 };
var.champs_i
int tab[] = { 1, 2, 3 };
tab[i]
Beaucoup de concepts sont les même qu’en Java
Interface d’un module: partie publique accessible d’autres modules
Implémentation d’un module: partie privée
Bibliothèque (en anglais: library) : regroupe un ensemble de modules
Deux fichiers par module. Par exemple, pour le module
mem_alloc
:
mem_alloc.h
(fichier
d’entête / header)
mem_alloc.c
mem_alloc.h
:
#include "mem_alloc.h"
mem_alloc.h
mem_alloc
(depuis le module
main
)
mem_alloc.h
:
#include "mem_alloc.h"
mem_alloc.h
mem_alloc
mem_alloc
/* mem_alloc.h */
#include <stdlib.h>
#include <stdint.h>
#define DEFAULT_SIZE 16
typedef int64_t mem_page_t;
struct mem_alloc_t {
/* [...] */
};
/* Initialize the allocator */
void mem_init();
/* Allocate size consecutive bytes */
int mem_allocate(size_t size);
/* Free an allocated buffer */
void mem_free(int addr, size_t size);
/* mem_alloc.c */
#include "mem_alloc.h"
struct mem_alloc_t m;
void mem_init() { /* ... */ }
int mem_allocate(size_t size) {
/* ... */
}
void mem_free(int addr, size_t size) {
/* ... */
}
#include "mem_alloc.h"
int main(int argc, char**argv) {
();
mem_init/* ... */
}
Compilation en trois phases:
exécutable
Le préprocesseur transforme le code source pour le compilateur
#define N 12
substitue N
par
12
dans le fichier#if <cond> ... #else ... #endif
permet la
compilation conditionnelle#ifdef <var> ... #else ... #endif
(ou l’inverse:
#ifndef
) permet de ne compiler que si var
est
défini (avec #define
)#include "fichier.h"
inclue (récursivement) le fichier
"fichier.h"
gcc -E mem_alloc.c
La directive #if
permet, par exemple, de fournir
plusieurs implémentations d’une fonction. Cela peut être utilisé pour
des questions de portabilité.
#if __x86_64__
void foo() { /* implementation pour CPU intel 64 bits */ }
#elif __arm__ /* équivalent à #else #if ... */
void foo() { /* implementation pour CPU ARM */ }
#else
void foo() {
printf("Architecture non supportée\n");
abort();
#endif
Il y a deux syntaxes pour la directive #include
: *
#include <fichier>
: le préprocesseur cherche
fichier
dans un ensemble de répertoires systèmes
(/usr/include
par exemple) *
#include "fichier"" le préprocesseur cherche
fichier` dans
le répertoire courant, puis dans les répertoires systèmes.
On utilise donc généralement #include "fichier"
pour
inclure les fichiers d’entête définis par le programme, et
#include <fichier>
pour les fichiers d’entête du
système (stdio.h
, stdlib.h
, etc.)
Compilation : transformation des instructions C en instructions “binaires”
gcc -c mem_alloc.c
mem_alloc.o
Edition de liens : regroupement des fichiers objets pour créer un exécutable
gcc -o executable mem_alloc.o module2.o [...] moduleN.o
Règles de compilations
mem_alloc.c
, il est nécessaire de regénérer le fichier
objet (mem_alloc.o
), et de regénérer l’exécutable. Il n’est
toutefois pas nécessaire de recompiler les modules utilisant le module
modifié.mem_alloc.h
), il est nécessaire de recompiler le module,
ainsi que tous les modules utilisant le module modifié. Une fois que
tous les fichiers objets (les fichiers *.o
) concernés ont
été regénérés, il faut refaire l’édition de liens.Lorsque le nombre de module devient élevé, il devient difficile de
savoir quel(s) module(s) recompiler. On automatise alors la chaîne de
compilation, en utilisant l’outil make
.
Puisque la compilation se fait en 3 phases, 3 types d’erreurs peuvent survenir: * une erreur du préprocesseur (ie. une macro est mal écrite):
$ gcc -c foo.c
foo.c:1:8: error: no macro name given in #define directive
#define
^
$ gcc -c erreur_compil.c
erreur_compil.c: In function ‘f’:
erreur_compil.c:3:1: error: expected ‘;’ before ‘}’ token
}
^
$ gcc -o plip main.o
main.o : Dans la fonction « main » :
main.c:(.text+0x15) : référence indéfinie vers « mem_init »
collect2: error: ld returned 1 exit status
Les fichiers objets et exécutables sont sous le format ELF (Executable and Linkable Format)
Ensemble de sections regroupant les symboles d’un même type:
.text
contient les fonctions de l’objet.data
et .bss
contiennent les données
initialisées (.data
) ou pas (.bss
).symtab
contient la table des symbolesLors de l’édition de liens ou du chargement en mémoire, les sections de tous les objets sont fusionnés
La liste complète des sections du format ELF est disponible dans la
documentation (man 5 elf
).
La table des symboles contient la liste des
fonctions/variables globales (ou statiques) définies ou utilisées dans
le fichier. L’outil nm
permet de consulter cette table. Par
exemple:
$ nm mem_alloc.o
0000000000000000 C m
0000000000000007 T mem_allocate
0000000000000012 T mem_free
0000000000000000 T mem_init
$ nm main.o
0000000000000000 T main
U mem_init
Pour chaque symbole, nm
affiche l’adresse (au sein d’une
section), le type (donc, la section ELF), et le nom du symbole.
Ces informations sont également disponible via la commande
readelf
:
$ readelf -s mem_alloc.o
Table de symboles « .symtab » contient 12 entrées :
Num: Valeur Tail Type Lien Vis Ndx Nom
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS mem_alloc.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000001 0 OBJECT GLOBAL DEFAULT COM m
9: 0000000000000000 7 FUNC GLOBAL DEFAULT 1 mem_init
10: 0000000000000007 11 FUNC GLOBAL DEFAULT 1 mem_allocate
11: 0000000000000012 14 FUNC GLOBAL DEFAULT 1 mem_free
L’utilitaire objdump
permet lui aussi d’examiner la
table des symboles:
$ objdump -t mem_alloc.o
mem_alloc.o: format de fichier elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 mem_alloc.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 O *COM* 0000000000000001 m
0000000000000000 g F .text 0000000000000007 mem_init
0000000000000007 g F .text 000000000000000b mem_allocate
0000000000000012 g F .text 000000000000000e mem_free
Une variable déclarée dans une fonction peut être
int var;
ou int var2 = 17;
static
static int var = 0;
Puisqu’une variable locale statique est allouée au chargement du programme, elle apparaît dans la liste des symboles :
$ nm plop.o
0000000000000000 T function
0000000000000000 d variable_locale_static.1764
Ici, le symbole variable_locale_static.1764
correspond à
la variable variable_locale_static
déclarée
static
dans la fonction function
. Le suffixe
.1764
permet de différencier les variables nommées
variable_locale_static
déclarées dans des fonctions
différentes.
Une variable déclarée dans le fichier fic.c
en dehors
d’une fonction peut être:
int var;
ou int var2 = 17;
extern
extern int var;
fic.c
static
static int var = 0;
Les variables globales (déclarées extern
,
static
, ou “normales”) se retrouvent dans la table des
symboles de l’objet, mais dans des sections ELF différentes :
$ nm plop.o
0000000000000000 T function
U var_extern
0000000000000000 D var_globale
0000000000000004 d var_static_globale
La variable var_extern
(déclarée avec
extern int var_extern;
) est marquée "U"
(undefined). Il s’agit donc d’une référence à un symbole
présent dans un autre objet.
La variable var_globale
(déclarée avec
int var_globale = 12;
) est marquée "D"
(The symbol is in the initialized data section). Il s’agit donc
d’une variable globale initialisée .
La variable var_static_globale
(déclarée avec
static int var_static_globale = 7;
) est marquée
“d
” (The symbol is in the initialized data
section). Il s’agit donc d’une variable globale “interne”. Il n’est
donc pas possible d’accèder à cette variable depuis un autre objet:
$ gcc plop.o plip.o -o executable
plip.o : Dans la fonction « main » :
plip.c:(.text+0xa) : référence indéfinie vers « var_static_globale »
collect2: error: ld returned 1 exit status
#include "mem_alloc.h"
, puis
utilisation des fonctions-l
, par
exemple: -lmemory
libmemory.so
ou
libmemory.a
Avantages/inconvénients des bibliothèques statiques:
- Taille de l’exécutable important (puisqu’il inclut la bibliothèque);
- En cas de nouvelle version d’une bibliothèque (qui corrige un bug par exemple), il faut recompiler toutes les applications utilisant la bibliothèque;
- Duplication du code en mémoire;
+ L’exécutable incluant une bibliothèque statique fonctionne “tout seul” pas besoin d’autres fichiers).
Avantages/inconvénients des bibliothèques dynamiques:
+ Taille de l’exécutable réduite (puisqu’il n’inclut qu’une référence à la bibliothèque);
+ En cas de nouvelle version d’une bibliothèque (qui corrige un bug par exemple), pas besoin de recompiler les applications utilisant la bibliothèque;
+ Une instance du code en mémoire est partageable par plusieurs processus;
- L’exécutable incluant une bibliothèque dynamique ne fonctionne pas “tout seul”: il faut trouver toutes les bibliothèques dynamiques nécessaires.
Les “dépendances” dues aux bibliothèques dynamiques sont visibles
avec ldd
:
$ ldd executable
linux-vdso.so.1 (0x00007fff9fdf6000)
libmem_alloc.so (0x00007fb97cb9f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb97c7ca000)
/lib64/ld-linux-x86-64.so.2 (0x0000555763a9b000)
Il existe 2 types de bibliothèques
libmemory.**a**
ar rcs libmemory.a mem_alloc.o mem_plip.o mem_plop.o [...]
libmemory.**so**
gcc -shared -o libmemory.so mem_alloc.o mem_plip.o mem_plop.o [...]
-fPIC
:gcc -c mem_alloc.c -fPIC
Pour exécuter un programme utilisant une bibliothèque dynamique, le
système doit charger en mémoire le programme ainsi que la biblbiothèque.
Si la bibliothèque n’est pas installée dans un répertoire standard
(typiquement dans /usr/lib
), il peut être nécessaire
d’indiquer où trouver cette bibliothèque grâce à la variable
d’environnement LD_LIBRARY_PATH
.
$ ./mon_programme
mon_programme: error while loading shared libraries: libtruc.so: cannot open shared object file: No such file or directory
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/trahay/libs/libtruc/
$ ./mon_programme
It works !
L’avantage d’une bibliothèque statique est qu’il n’est nécessaire de connaître son emplacement qu’au moment d’édition de liens. Une fois l’exécutable crée, celui-ci inclue la bibliothèque statique. On peut donc déplacer l’exécutable, supprimer la bibliothèque statique, recopier l’exécutable sur une autre machine sans empêcher son exécution.
Toutefois, lorsqu’une bibliothèque statique est mise à jour (par exemple pour corriger un bug ou une faille de sécutité), il est nécessaire de recompiler tous les programmes utilisant cette bibliothèque. Pour des bibliothèques très utilisées (par exemple, la libc), cela peut être long et on risque d’oublier de recompiler certains programmes.
A l’inverse, si une bibliothèque dynamique est mise à jour, cette mise à jour est directement disponible pour toutes les applications utilisant la bibliothèque. Ceci explique pourquoi la plupart des distributions Linux reposent aujourd’hui sur des bibliothèques dynamiques plutot que statiques.
Attention, si une bibliothèque dynamique est mise à jour et que son ABI (Application Binary Interface) change (par exemple si la signature d’une fonction change), il sera nécessaire de recompiler les applications utilisant cette biblliothèque.
Organisation classique d’un projet:
src/
module1/
module1.c
module1.h
module1_plop.c
module2/
module2.c
module2.h
module2_plip.c
doc/
manual.tex
Besoin d’utiliser des flags
:
-Idir
indique où chercher des fichiers
.h
gcc -c main.c -I../memory/
-Ldir
indique à l’éditeur de lien où trouver des
bibliothèquesgcc -o executable main.o -L../memory/ -lmem_alloc
A l’exécution:
LD_LIBRARY_PATH
contient les répertoires où
chercher les fichiers .so
export LD_LIBRARY_PATH=.:../memory
Par défaut, le compilateur va chercher les fichiers d’entête dans un
certain nombre de répertoires. Par exemple, gcc
cherche
dans:
/usr/local/include
<libdir>/gcc/<target>/version/include
/usr/<target>/include
/usr/include
L’option -I
ajoute un répertoire à la liste des
répertoires à consulter. Vous pouvez donc utiliser plusieurs fois
l’option -I
dans une seule commande. Par exemple:
gcc -c main.o -Imemory/ -Itools/ -I../plop/
De même, l’éditeur de liens va chercher les bibliothèques dans un
certain nombre de répertoires par défaut. La liste des répertoires
parcourus par défaut par ld
(l’éditeur de lien utilisé par
gcc
) est dans le fichier /etc/ld.so.conf
. On y
trouve généralement (entre autre):
/lib/<target>
/usr/lib/<target>
/usr/local/lib
/usr/lib
/lib32
/usr/lib32
Si la variable LD_LIBRARY_PATH
est mal positionnée, vous
risquez de tomber sur ce type d’erreur au lancement de
l’application:
$ ./executable
./executable: error while loading shared libraries: libmem_alloc.so: cannot open \
shared object file: No such file or directory
executable
, j’ai besoin de
mem_alloc.o
et main.o
”executable
, il faut lancer la commande
gcc -o executable mem_alloc.o main.o
”Makefile
, ensemble de règles
sur deux lignes:cible : dependance1 dependance2 ... dependanceN`
<TAB>commande
make
make
make
parcourt le fichier
Makefile
du répertoire courant et tente de produire la
première cible.make cible
.make
s’arrêteMakefile
et des
date/heure de dernière modification des fichiers: si un fichier de
l’arbre est plus récent que la cible à générer, tout le chemin entre le
fichier modifié et la cible est regénéré.Makefile
Voici un exemple de fichier Makefile
:
: executable
all
: mem_alloc.o main.o
executable-o executable main.o mem_alloc.o
gcc
.o: mem_alloc.c mem_alloc.h
mem_alloc-c mem_alloc.c
gcc
.o: main.c mem_alloc.h
main-c main.c
gcc
emacs
,
lorsque vous éditez un fichier nommé Makefile
, les
tabulations sont colorisées en rose par défaut.Si vous utilisez des espaces à la place de la tabulation, la commande
make
affiche le message d’erreur suivant:
Makefile:10: *** missing separator (did you mean TAB instead of 8 spaces?). Arrêt.
make
(sans argument) génère la
première cible, on ajoute généralement une premier cible “artificielle”
all
décrivant l’ensemble des exécutables à générer:all: executable1 executable2
Dans ce cas, seule la cible est spécifiée. Il n’y a pas d’action à effectuer.
clean
clean
pour “faire le ménage” (supprimer les exécutables et
les fichiers .o
):clean:
<TAB>rm -f executable1 executable2 *.o
Makefile
, on peut utiliser
certaines notations symboliques:
$@
désigne la cible de la règle$^
désigne l’ensemble des dépendances$<
désigne la première dépendance de la liste$?
désigne l’ensemble des dépendances plus récentes que
la cibleMakefile
. Par exemple:=executable
BIN=mem_alloc.o main.o
OBJETS=-Wall -Werror -g
CFLAGS=-lm
LDFLAGS
: $(BIN)
all
: $(OBJETS)
executable-o executable main.o mem_alloc.o $(LDFLAGS)
gcc
.o: mem_alloc.c mem_alloc.h
mem_alloc-c mem_alloc.c $(CFLAGS)
gcc
.o: main.c mem_alloc.h
main-c main.c $(CFLAGS)
gcc
:
clean-f $(BIN) $(OBJETS) rm
make
n’est qu’une partie
de la chaîne de configuration et de compilation.autoconf
/automake
ou
Cmake
sont fréquemment utilisés pour écrire des
“proto-makefiles” * On peut faire référence à n’importe quel octet de l’espace mémoire
grace à son adresse * Adresse mémoire virtuelle codée sur \(k\) bits1 * donc \(2^{k}\) octets accessibles (de
00...00
à 11...11
)
0x1001
est stocké l’octet
0x41
char
(le caractère
A
)int
(par exemple
l’entier 0x11**41**2233
)Les valeurs préfixées par 0x
sont représentées en
hexadécimal (en base 16). Ainsi, 0x200d
correspond au
nombre qui s’écrit 200D
en base 16, soit le nombre \(2 \times 16^3
+ 0\times16^2 + 0\times16^1 + 13\times16^0 = 8205\) écrit en
base 10.
La notation hexadécimale est couramment utilisée pour représenter des
octets car deux chiffres en hexadécimal permettent de coder 256 (soit
\(2^8\)) valeurs différentes. On peut
donc représenter les 8 bits d’un octet avec deux chiffres hexadécimaux.
Par exemple, 0x41
représente l’octet
0100 0001
.
Les processeurs équipant les ordinateurs modernes sont généralement
de type x86_64
. Pour ces processeurs, les adresses
virtuelles sont codées sur 64 bits. Un processus peut donc adresser
\(2^{64}\) octets (16 Exaoctets ou 16 x
1024 Pétaoctets) différents.
ARM est une autre architecture de processeur très répandue puisqu’elle équipe la plupart des smartphones. Jusqu’à très récemment, les processeurs ARM fonctionnaient en 32 bits. Un processus pouvait donc accéder à \(2^{32}\) octets (4 Gigaoctets). Les processeurs ARM récents sont maintenant 64 bits, ce qui permet à un processus d’utiliser une plus grande quantité de mémoire.
&var
désigne l’adresse de var
en
mémoire%p
dans printf
:("adresse de var: %p\n", &var); printf
affiche:
adresse de var: 0x7ffe8d0cbc7f
Il est possible de manipuler l’adresse de n’importe quel objet en C, que ce soit une variable, le champ d’une structure, ou une case d’un tableau.
Le programme suivant:
#include <stdio.h>
#include <stdlib.h>
struct point{
float x;
float y;
float z;
int id;
};
int main() {
char var='A';
("adresse de var: %p\n", &var);
printf
struct point p = {.x = 2.5, .y = 7.2, .z=0, .id=27};
("adresse de p: %p\n", &p);
printf("adresse de p.x: %p\n", &p.x);
printf("adresse de p.y: %p\n", &p.y);
printf("adresse de p.z: %p\n", &p.z);
printf("adresse de p.id: %p\n", &p.id);
printf
char tab[] = "hello";
("adresse de tab[2] = %p\n", &tab[2]);
printfreturn EXIT_SUCCESS;
}
peut donner cet affichage:
adresse de var: 0x7ffe44d7c6df
adresse de p: 0x7ffe44d7c6c0
adresse de p.x: 0x7ffe44d7c6c0
adresse de p.y: 0x7ffe44d7c6c4
adresse de p.z: 0x7ffe44d7c6c8
adresse de p.id: 0x7ffe44d7c6cc
adresse de tab[2] = 0x7ffe44d7c6b2
type* nom_variable;
type
désigne le type de la donnée “pointée”char* pa;
crée un pointeur sur une donnée de
type char
:// pour l'exemple, les adresses sont codees sur 32 bits
char a = 'A'; // a est stocke a l'adresse 0x0000FFFF
// la valeur de a est 0x41 ('A')
char* pa = &a; // pa est une variable de 32 bits stockee
// aux adresses 0xFFFB a 0xFFFE
// la valeur de pa est 0x0000FFFF (l'adresse de a)
On peut ensuite manipuler l’adresse de a
(0xFFFF
) ou la valeur de pa
(0xFFFF
) indifféremment:
("&a = \%p\n", &a); // affiche 0xFFFF
printf("pa = \%p\n", pa); // affiche 0xFFFF
printf("&pa = \%p\n", &pa); // affiche 0xFFFB, soit l'adresse de pa printf
Un pointeur étant une variable comme les autres, on peut donc stocker son adresse dans un pointeur. Par exemple:
char a = 'A'; // a est stockee a l'adresse 0xFFFF et contient 0x41 ('A' ou 65)
char* pa = &a; // pa est stockee a l'adresse 0xFFFB et contient 0xFFFF (l'adresse de a)
char** ppa = &pa; // ppa est stockee a l'adresse 0xFFF7 et contient 0xFFFB (l'adresse de pa)
Quand vous déclarez un pointeur, initialisez-le immédiatement, soit
avec l’adresse d’une variable, soit avec la valeur NULL
(définie dans stdlib.h
) qui est la valeur pointant sur
“rien”. Dit autrement, ne laissez jamais une variable
pointeur avec un contenu non initialisé.
Les opérateurs +
, -
, ++
, et
--
sont utilisables sur des pointeurs, mais avec
précaution.
Incrémenter un pointeur sur type
aura pour effet
d’ajouter sizeof(type)
à la valeur du pointeur. Par
exemple:
char* pa = &a; // pa vaut 0xFFFF
--; // enleve sizeof(char) (c'est a dire 1) a pa
pa// donc pa vaut 0xFFFE
char**ppa = &pa // ppa vaut 0xFFFB
--; // enleve sizeof(char*) (c'est a dire 4) a ppa
ppa// donc ppa vaut 0xFFF7
= ppa - 2; // soustrait 2*sizeof(char*) (donc 8) a ppa
ppa // ppa vaut 0xFFEF
Sur https://codecast.france-ioi.org/, vous pouvez visualiser le contenu de la mémoire d’un programme. Pour cela, saisissez le code source du programme, cliquez sur “compiler”, puis exécutez le programme pas à pas en cliquant sur “next expression”. {`A} chaque instant, le contenu de la mémoire est représenté en bas de la page. Essayez avec ce programme:
#include <stdio.h>
int main() {
//! showMemory(cursors=[a, pa, ppa], start=65528)
char a = 'A';
char* pa= &a;
char** ppa = &pa;
("a = %d, &a=%p\n", a, &a);
printf("pa = %p, &pa=%p\n", pa, &pa);
printf("ppa = %p, &ppa=%p\n", ppa, &ppa);
printf
--; // enleve sizeof(char)=1 a pa
pa--; // enleve sizeof(char*) a ppa
ppa
("a = %d, &a=%p\n", a, &a);
printf("pa = %p, &pa=%p\n", pa, &pa);
printf("ppa = %p, &ppa=%p\n", ppa, &ppa);
printf
= ppa - 2; // enleve 2*sizeof(char*) a ppa
ppa ("ppa = %p, &ppa=%p\n", ppa, &ppa);
printf
return 0;
}
* ptr
exemple:
char a = 'A'; // valeur 0x41 (cf. codage ASCII)
char* pa = &a;
("pa = %p\n", pa); // affiche "pa = 0xFFFF"
printf("*pa = %c\n", *pa); // affiche "*pa = A"
printf*pa = 'B'; // modifie l'emplacement memoire 0xFFFF
// (donc change la valeur de a)
("a = %c\n", a); // affiche "a = B" printf
A partir d’un pointeur, on peut donc afficher 3 valeurs différentes:
printf("pa = %p\n", pa);
– affiche la valeur du
pointeur (ici, l’adresse 0x1001
)printf("*pa = %c\n", *pa);
– affiche la valeur
“pointée” par pa
(ici, la valeur de a
:
'A'
)printf("&pa = %c\n", &pa);
– affiche l’adresse
du pointeur (ici, l’adresse 0x1004
)Lorsqu’un pointeur ptr
contient l’adresse d’une
structure, l’accès au champ c
de la structure peut se faire
en déréférençant le pointeur, puis en accédant au champ :
(*ptr).c
Cela devient plus compliqué lorsque la structure contient un pointeur
(p1
) vers une structure qui contient un pointeur
(p2
) sur une structure. La syntaxe devient rapidement
indigeste: (*(*(*ptr).p1).p2).c
Pour éviter cette notation complexe, on peut utiliser l’opérateur
->
qui combine le déréférencement du pointeur et l’accès
à un champ. Ainsi, ptr->c
est équivalent à
(*ptr).c
. On peut donc remplacer la notation
(*(*(*ptr).p1).p2).c
par
ptr->p1->p2->c
.
Un pointeur doit être systématiquement initialisé au moment de sa définition
fopen
étudié au
CI5)Un free sur un pointeur doit être systématiquement suivi d’une mise à
NULL
de ce pointeur.
Lorsqu’une fonction retourne l’adresse d’une variable, il faut que cette adresse reste valide après la fin de la fonction ! Le programme suivant illustre ce problème.
#include <stdio.h>
#include <stdlib.h>
int* get_address(int value);
void do_something(int value);
int main(int argc, char**argv) {
// exemple de représentation mémoire du programme.
// Au démarrage:
// 0x7fffffffd6f0: | | <-- rbp (main)
// ...
// 0x7fffffffd6dc: | a = 42 |
// 0x7fffffffd6d0: | v = 0 |
// 0x7fffffffd6dc: | v = 0 |
// 0x7fffffffd6c0: | | <-- rsp (main)
int a = 42;
/* Avant l'appel à get_value, le sommet de la pile est à
* l'adresse %rsp (par exemple 0x7fffffffd6c0)
*/
int* v = get_address(a);
/* v récupère l'adresse de la variable locale x (0x7fffffffd6a8) */
//
// 0x7fffffffd6f0: | | <-- rbp (main)
// ...
// 0x7fffffffd6dc: | a = 42 |
// 0x7fffffffd6d0: | v = ffd6a8 |
// 0x7fffffffd6dc: | v = 7ffff ---------------------|
// 0x7fffffffd6c0: | | <-- rsp (main) |
// ... |
// le reste a été désalloué, mais la mémoire |
// contient toujours les données |
// 0x7fffffffd6b0: | | |
// 0x7fffffffd6a8: | x = 42 <---------------------|
("v=%p, *v=%d\n", v, *v); // affiche "v=0x7fffffffd6a8, *v=42"
printf
(a);
do_something
("v=%p, *v=%d\n", v, *v); // affiche "v=0x7fffffffd6a8, *v=7"
printf
return EXIT_SUCCESS;
}
/* Lors de l'appel à get_value, une nouvelle "frame" est créée et
* la base de la pile (%rbp) 0x7fffffffd6b0
*/
int* get_address(int value) {
/* La variable x est stockée à l'adress %rbp-4 = 0x7fffffffd6a8 */
int x = value;
// 0x7fffffffd6f0: | | <-- rbp (main)
// ...
// 0x7fffffffd6dc: | a = 42 |
// 0x7fffffffd6d0: | v = 0 |
// 0x7fffffffd6dc: | v = 0 |
// 0x7fffffffd6c0: | | <-- rsp (main)
// ...
// 0x7fffffffd6b0: | | <-- rbp (get_address)
// 0x7fffffffd6a8: | x = 42 |
return &x; // valeur retournée: 0x7fffffffd6a8
}
/* Lors de l'appel à get_value, une nouvelle "frame" est créée et
* la base de la pile (%rbp) 0x7fffffffd6b0
* Il se trouve que cette adresse est la même que lors de l'appel à get_address
*/
void do_something(int value) {
/* La variable a est stockée à l'adress %rbp-4 = 0x7fffffffd6a8 */
int a = 7;
//
// 0x7fffffffd6f0: | | <-- rbp (main)
// ...
// 0x7fffffffd6dc: | a = 42 |
// 0x7fffffffd6d0: | v = ffd6a8 |
// 0x7fffffffd6dc: | v = 7ffff -------------------|
// 0x7fffffffd6c0: | | <-- rsp (main) |
// ... |
// 0x7fffffffd6b0: | | |
// 0x7fffffffd6a8: | a = 7 <---------------------|
// ici a=7 vient écraser l'ancienne valeur de x, et donc la valeur
// de *v
}
Si un tableau est un argument de fonction
void f(int x[])
<=>
void f(int* x)
tab[i]
réécrit en *(tab + i)
tab = 0x1000
et i=5
tab[i]
calcule
0x1000 + (5*sizeof(int)) = 0x1000 + 0x14 = 0x1014
sizeof(tab)
donne la taille d’un pointeur&tab
donne l’adresse de
int[] tab
, donc &tab != tab
Si un tableau est une variable locale ou globale
int tab[3]; // alloue 3 int
tab
est le nom de cet espace mémoireint tab[] = { 1, 2, 3 }; // idem + initialisation
int tab[]; // interdit !
sizeof(tab)
renvoie la taille du tableauSi un tableau est une variable locale ou globale (suite)
&tab
donne l’adresse du tableau
&tab == &tab[0]
car tab
et tab[0]
désignent les mêmes emplacements mémoirestab
est implicitement transtypé vers son
pointeur au besoinint* tab2 = tab;
réécrit en
int* tab2 = &tab
if(tab == &tab)
récrit en
if(&tab == &tab)
f(tab)
réécrit en f(&tab)
printf("%p %p\n", tab, &tab);
réécrit en
printf("%p %p\n", &tab, &tab);
tab[i]
réécrit en (&tab)[i]
puis en
*(&tab + i)
*(tab + i)
réécrit en *(&tab + i)
Rappel:
void f(int* px) {
*px = 666; // la variable pointee par px est modifiee
}
int main() {
int x = 42;
(&x); // l'adresse de x est donnee à f
f// => le x de main est modifié par f
("x = \%d\n", x); // la nouvelle valeur de x : 666
printfreturn EXIT_SUCCESS;
}
void* malloc(size_t nb_bytes);
nb_bytes
octets et retourne un pointeur sur la
zone allouéechar* str = malloc(sizeof(char)* 128);
NULL
en cas d’erreur (par ex: plus assez de
mémoire)Attention ! Risque de “fuite mémoire” si la mémoire allouée n’est jamais libérée
void*
Le void*
renvoyé par malloc
signifie que la
fonction retourne un pointeur vers n’importe quel type de donnée. Ce
pointeur (qui est donc une adresse) vers void
peut être
converti en pointeur (une adresse) vers int
ou tout autre
type.
Vérifiez systématiquement si malloc
vous a renvoyé
NULL
et, si c’est le cas, arrêtez votre programme. Une
manière simple et lisible de faire cela est d’utiliser la macro
assert
(définie dans assert.h
) comme dans
l’exemple suivant :
char* str = malloc(sizeof(char)* 128);
(str); assert
Lorsque l’on déclare une variable (un int
, un tableau,
une structure, ou toute autre variable) depuis une fonction
foo
, l’espace mémoire de cette variable est réservé sur la
pile. Lorsque l’on sort de foo
, la pile est “nettoyée” et
l’espace réservé pour les variables locales est libéré.
Lorsque l’on alloue de la mémoire avec malloc
depuis une
fonction foo
, la mémoire est allouée sur le tas.
Lorsque l’on sort de la fonction foo
, l’espace mémoire
réservé reste accessible. Si on “perd” l’emplacement de cette zone
mémoire, elle devient donc inaccessible, mais reste réservée: c’est une
fuite mémoire.
Si la fuite mémoire fait “perdre” quelques octets à chaque appel de
la fonction foo
, la mémoire de la machine risque, à terme,
d’être remplie de zones inutilisées. Le système d’exploitation n’ayant
plus assez de mémoire pour exécuter des processus devra donc en tuer
quelques uns pour libérer de la mémoire.
void free(void* ptr);
malloc
est situé à l’adresse
ptr
A chaque fois que vous faites free
sur un pointeur,
pensez à remettre ensuite ce pointeur à NULL
(pour être sûr
que vous n’avez pas un pointeur qui pointe sur une zone de mémoire
libérée). Dit autrement, tout “free(ptr);
” doit être suivi
d’un “ptr = NULL;
”.
&var
)type
: type* ptr;
ptr++
)NULL
*ptr
ptr->champ
ptr[i]
int* ptr = malloc(sizeof(int)*5);
* désallocation:
free(ptr);
FILE*
: type “opaque” désignant un fichier ouvert1 Pour être exact, il s’agit de la bibliothèque standard (la libc)
Plus précisement, FILE*
désigne un flux. Ce
flux peut être un fichier, mais également des flux standards
(stdin
, stdout
, ou stderr
), des
tubes, des sockets, etc.
FILE* fopen(char* fichier, char* mode);
mode
: mode d’ouverture
"r"
: lecture seule"w"
: écriture seule"r+"
ou "a"
: écriture seule (ajout)"a+"
: lecture et écriture (ajout)int fclose(FILE* f);
* Complète les opérations et
ferme le fichier
Après appel à la fonction fclose
, f
(le
FILE*
) devient inutilisable : le pointeur pointe vers une
zone mémoire qui a peut être été libérée par fclose
. Il
convient donc de ne plus utiliser le fichier !
int fprintf(FILE* f, char* format, ...);
printf
, mais écrit dans le fichier
f
size_t fwrite(void* ptr, size_t size, size_t nmemb, FILE* f);
size
\(\times\)nmemb
octets situés à
l’adresse ptr
dans f
Quelle est la différence entre printf
et
fwrite
? La fonction fprintf
écrit un ensemble
de caractères ASCII dans le fichier, alors que fwrite
écrit
un ensemble de bits.
Ainsi, pour écrire la valeur 17
dans un fichier en
ASCII, on peut exécuter:
(f, "%d", 12); fprintf
Le fichier contiendra donc les octets 0x31
(le caractère
'1'
), et 0x32
('2'
).
Pour écrire la valeur 12
en binaire, on peut
exécuter:
int n=12;
(&n, sizeof(int), 1, f); fwrite
Le fichier contiendra donc les octets 0x0C
0x00
0x00
0x00
, c’est à dire les
4 octets d’un int
dont la valeur est 12
.
Puisqu’un FILE*
désigne en fait un flux, on peut
utiliser fprintf
pour écrire sur la sortie standard
d’erreur :
(stderr, "Warning: flux capacitor overlow !\n"); fprintf
Voici un programme montrant l’utilisation de primitives de lecture et d’écriture:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main(int argc, char**argv) {
if (argc != 3) {
(stderr, "USAGE = %s fichier_source fichier_destination\n", argv[0]);
fprintfreturn EXIT_FAILURE;
}
/* open the input and output files */
FILE*f =fopen(argv[1], "r");
(f);
assertFILE*f_out =fopen(argv[2], "w");
(f_out);
assertchar line[1024];
int lineno = 1;
/* read the input file line by line */
while(fgets(line, 1024, f)) {
/* write the line to the output file */
(f_out, "%s", line);
fprintf++;
lineno}
/* close the files */
(f);
fclose(f_out);
fclosereturn 0;
}
int fscanf(FILE* f, char* format, ...);
scanf
, mais lit depuis le fichier
f
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* f);
nmemb
\(\times\)size
octets et les
stocke à l’adresse ptr
fread
renvoie une valeur < nmemb
si la
fin du fichier (EOF
) est atteintechar* fgets(char* s, int size, FILE* f);
size
caractères et les stocke dans
s
size
si lit n
ou
EOF
Ces 3 fonctions sont généralement utilisées chacune dans un cas précis:
fscanf
est utilisée pour lire des valeurs et les
stocker dans des variables. Par exemple:int a;
float b;
(f, "%d\t%f\n", &a, &b); fscanf
fread
est utilisée pour charger le contenu d’une (ou de
plusieurs) structure(s):struct s {
int a;
float b;
char c;
};
struct s tab[10];
(tab, sizeof(struct s), 10, f); fread
fgets
est utilisée pour lire un fichier ligne par
ligne:char line[1024];
int lineno = 1;
while(fgets(line, 1024, f)) {
("line %d: %s\n", lineno, line);
printf++;
lineno}
Position dans le fichier à laquelle la prochaine opération aura lieu
Avance à chaque opération de lecture/écriture
long ftell(FILE *stream);
int fseek(FILE *f, long offset, int whence);
offset
octets depuis
whence
vaut
SEEK_SET
)whence
vaut
SEEK_CUR
)whence
vaut
SEEK_END
)But: comprendre l’exécution d’un programme
12
? Ca devrait être 17, non
?Selon une étude1, un développeur passe 50 % de son temps à debugger des programmes. Apprendre à debugger efficacement est donc nécessaire si vous souhaitez réduire la durée de cette activité pénible.
1 T. Britton et al. Reversible debugging software. University of Cambridge-Judge Business School, 2013, Technical Report.
printf("entrée dans foo(n=%d)\n", n);
dans le
code source$ ./sigsegv
Debut du programme
Erreur de segmentation
$ gdb ./sigsegv
[...]
(gdb) run
Starting program: ./sigsegv
Debut du programme
Program received signal SIGSEGV, Segmentation fault.
0x000000000040050b in main (argc=1, argv=0x7fffffffdd68) at sigsegv.c:7
7 *ptr=5;
(gdb) print ptr
$1 = (int *) 0x0
-g
gdb
:
gdb ./programme
r
(ou run
)Ctrl+C
q
(ou quit
)Vous trouverez sur https://www-inf.telecom-sudparis.eu/COURS/CSC4103/Supports/?page=annexe-gdb
un récapitulatif des principales commandes gdb
.
gdb
est capable de faire du reverse debugging.
En enregistrement ce que fait le programme, gdb
peut
“exécuter” le programme en arrière. Cela peut être utile pour détecter
quand une variable a été modifiée.
Si vous terminez le TP rapidement, prenez le temps de regarder les vidéos suivantes qui ouvrent plein de perspectives : Reverse debugging.
Il existe d’autres debuggers que gdb
, mais les principes
restent les même. Si vous utilisez un IDE comme VS Code, celui-ci
propose sans doute un debugger intégré.
gdb
peut être étendu avec des sur-couches comme gef, ou avec des interfaces
graphiques
La commande bt
(ou backtrace
)
#x
avec la
commande frame [x]
. (gdb) bt
#0 baz (a=2) at backtrace.c:7
#1 0x0000000000400581 in bar (n=5, m=3) at backtrace.c:15
#2 0x00000000004005ae in foo (n=4) at backtrace.c:21
#3 0x0000000000400559 in baz (a=5) at backtrace.c:9
[...]
La backtrace permet d’examiner l’état actuel du processus, ainsi que l’enchaînement d’appels de fonctions qui a mené à cet état.
(gdb) bt
#0 baz (a=2) at backtrace.c:7
#1 0x0000000000400581 in bar (n=5, m=3) at backtrace.c:15
#2 0x00000000004005ae in foo (n=4) at backtrace.c:21
#3 0x0000000000400559 in baz (a=5) at backtrace.c:9
[...]
(gdb) frame
#0 baz (a=2) at backtrace.c:7
7 if(a<=2)
(gdb) print a
$1 = 2
(gdb) frame 1
#1 0x0000000000400581 in bar (n=5, m=3) at backtrace.c:15
15 return baz(m-1);
(gdb) print m
$2 = 3
Ici, gdb
nous indique que le programme est arrêté dans
la fonction baz
, à la ligne 7 du fichier
backtrace.c
. Cette fonction a été appelée
(frame #1
) par la fonction bar
à la ligne 15.
La fonction bar
a été appelée par foo
à la
ligne 31 (cf la frame #2
).
En sélectionnant une frame
, on peut examiner l’état des
variables locales au site d’appel.
p [var]
(ou print [var]
)
[var]
display [var]
[var]
à chaque arrêt du
programme[var]
peut être une variable (eg.
p n
)[var]
peut être une expression (eg.
p ptr->next->data.n
)Il est possible de choisir le format d’affichage:
p/d [var]
affiche la valeur décimale de
[var]
p/u [var]
affiche la valeur décimale (non signée) de
[var]
p/x [var]
affiche la valeur hexadécimale de
[var]
p/o [var]
affiche la valeur octale de
[var]
p/t [var]
affiche la valeur binaire de
[var]
p/a [var]
affiche la valeur [var]
sous
forme d’adressep/c [var]
affiche la valeur [var]
sous
forme de caractèrep/f [var]
affiche la valeur [var]
sous
forme de flottantgdb
peut également afficher la valeur d’un registre. Par
exemple p $eax
affiche la valeur du registre
eax
.
Une fois le programme lancé, possibilité d’exécuter les instructions une par une:
n
(ou next
) : exécute la prochaine
instruction, puis arrête le programme.s
(ou step
) : idem. Si l’instruction est
un appel de fonction, ne descend pas dans la fonction.c
(ou continue
) : continue l’exécution du
programme (arrête le mode pas à pas)b fichier.c:123
Après avoir définis les points d’arrêt, on laisse le programme
s’exécuter (avec la commande continue
). Lorsque le
programme atteint un des points d’arrêt, le débugger le met en pause et
donne la main au développeur afin qu’il puisse examiner l’état du
programme.
Par exemple:
$ gdb ./programme
[...]
(gdb) b bar
Breakpoint 1 at 0x400569: file programme.c, line 13.
(gdb) b backtrace.c:9
Breakpoint 2 at 0x40054c: file programme.c, line 9.
(gdb) r
Starting program: programme
Debut du programme
Breakpoint 1, bar (n=11, m=9) at backtrace.c:13
13 if(m<2)
(gdb) p n
$1 = 11
(gdb) p m
$2 = 9
(gdb) c
Continuing.
Breakpoint 2, baz (a=8) at backtrace.c:9
9 return foo(a-1);
(gdb) p a
$3 = 8
(gdb) c
Continuing.
Breakpoint 1, bar (n=8, m=6) at backtrace.c:13
13 if(m<2)
(gdb)
Il est également possible de définir des points d’arrêt conditionnels. Par exemple la commande
(gdb) b bar if n == 0
n’arrêtera l’exécution du programme en entrant dans la fonction
bar
que si n
est égal à 0
.
watch x
ptr
ne devrait pas être NULL
!
Qui a fait ça ?”Voici un exemple d’utilisation de la commande watch
$ gdb ./watch
[...]
(gdb) watch n
Hardware watchpoint 2: n
(gdb) c
Continuing.
Hardware watchpoint 2: n
Old value = 0
New value = 1
main (argc=1, argv=0x7fffffffdd68) at watch.c:7
7 for(i=0; i<1000; i++) {
(gdb) c
Continuing.
Hardware watchpoint 2: n
Old value = 1
New value = 2
main (argc=1, argv=0x7fffffffdd68) at watch.c:7
7 for(i=0; i<1000; i++) {
(gdb) p i
$1 = 17
[...]
valgrind [programme]
Valgrind peut détecter l’utilisation de variables non initialisées.
Par exemple, la non initialisation de n
dans instructions
suivantes est détectée par valgrind:
int n;
("%d$\n", n); printf
$ valgrind ./exemple_valgrind
==1148== Memcheck, a memory error detector
==1148== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==1148== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
==1148== Command: ./exemple_valgrind
==1148==
==1148== Conditional jump or move depends on uninitialised value(s)
==1148== at 0x4E7F2D3: vfprintf (vfprintf.c:1631)
==1148== by 0x4E86AC8: printf (printf.c:33)
==1148== by 0x400504: foo (exemple_valgrind.c:6)
==1148== by 0x400529: main (exemple_valgrind.c:13)
==1148==
==1148== Use of uninitialised value of size 8
==1148== at 0x4E7C06B: _itoa_word (_itoa.c:179)
==1148== by 0x4E7F87C: vfprintf (vfprintf.c:1631)
==1148== by 0x4E86AC8: printf (printf.c:33)
==1148== by 0x400504: foo (exemple_valgrind.c:6)
==1148== by 0x400529: main (exemple_valgrind.c:13)
==1148==
[...]
==1148==
5
==1148==
==1148== HEAP SUMMARY:
==1148== in use at exit: 0 bytes in 0 blocks
==1148== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==1148==
==1148== All heap blocks were freed -- no leaks are possible
==1148==
==1148== For counts of detected and suppressed errors, rerun with: -v
==1148== Use --track-origins=yes to see where uninitialised values come from
==1148== ERROR SUMMARY: 8 errors from 8 contexts (suppressed: 0 from 0)
Valgrind détecte également les fuites mémoire. Lorsqu’une
zone mémoire allouée avec malloc()
n’est pas libérée (avec
free()
), la zone mémoire peut être ``perdue’’. L’effet peut
être grave si la fuite mémoire survient fréquemment. Par exemple, un
serveur web qui perdrait quelques octets lors du traitement d’une
requête web, pourrait perdre plusieurs gigaoctets de mémoire après le
traitement de millions de requêtes.
Valgrind détecte ce type de fuites mémoire. Pour obtenir des
informations sur l’origine de la fuite, on peut utiliser l’option
--leak-check=full
:
$ valgrind --leak-check=full ./exemple_valgrind2
==1572== Memcheck, a memory error detector
==1572== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==1572== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
==1572== Command: ./exemple_valgrind2
==1572==
85823552
==1572==
==1572== HEAP SUMMARY:
==1572== in use at exit: 1,024 bytes in 1 blocks
==1572== total heap usage: 2 allocs, 1 frees, 2,048 bytes allocated
==1572==
==1572== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1572== at 0x4C2BBCF: malloc (vg_replace_malloc.c:299)
==1572== by 0x40054E: main (exemple_valgrind2.c:7)
==1572==
==1572== LEAK SUMMARY:
==1572== definitely lost: 1,024 bytes in 1 blocks
==1572== indirectly lost: 0 bytes in 0 blocks
==1572== possibly lost: 0 bytes in 0 blocks
==1572== still reachable: 0 bytes in 0 blocks
==1572== suppressed: 0 bytes in 0 blocks
==1572==
==1572== For counts of detected and suppressed errors, rerun with: -v
==1572== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Outre valgrind, vous pouvez utiliser des sanitizers pour repérer des soucis dans votre code:
AddressSanitizer
: détecte les accès hors-tableau, et
l’utilisation des pointeurs après la libération
-fsanitize=address
Undefined
: détecte divers comportements non-définis,
notamment avec les pointeurs
-fsanitize=undefined
Memory
: détecte des accès à de la mémoire non
initialisée
-fsanitize=memory
Ces sanitizers sont généralement implémentés dans les principaux
compilateurs (gcc, clang, visual C/C++). En fonction de votre
compilateurs ou plateforme, il est possible que certains sanitizers ne
soient pas disponibles. Consultez le manuel (man gcc
) !
foo
désigne l’adresse de la fonction
int foo(int a, char b);
int (*function_ptr)(int a, char b) = foo;
function_ptr(12, 'f'); // appelle la fonction foo
1 Oui, c’est sans rapport avec le debugging, mais pour équilibrer les séances, nous avons préféré ne pas aborder cette notion lors du cours sur les pointeurs :-)
(*nom_pointeur)
NULL
int
qui ne peut pointer que
sur une valeur de type int
, un pointeur sur
int (*f)(double, char, int)
ne peut pointer que sur une
fonction dont le prototype est
int f(double, char, int);
Exemple:
#include <stdio.h>
#include <stdlib.h>
double add(double a, double b) {
return a+b;
}
double substract(double a, double b) {
return a-b;
}
int main(int argc, char**argv) {
double n, m;
("%lf", &n);
scanf("%lf", &m);
scanf// declare a function pointer named "operation"
double (*operation)(double, double) = NULL;
if(n < m) {
/* operation points to the add function */
= add;
operation } else {
/* operation points to the substract function */
= substract;
operation }
/* call the function pointed to by operation */
double result = operation(n, m);
("Result of the operation: %lf\n", result);
printf
return EXIT_SUCCESS;
}
On peut définir un type (à l’aide du mot-clé typedef
)
correspondant à un pointeur de fonction. Par exemple:
typedef double (*op_function)(double, double);
définit le type op_function
. On peut donc ensuite
déclarer un pointeur de fonction de ce type en faisant:
op_function operation;
struct component {
char* plugin_name;
void (*init_value)(struct value*v);
void (*compute_value)(struct value*v);
void (*print_value)(struct value*v);
};
Un plugin implémentant ce service allouera la structure et désignera ses fonctions comme callback pour le service.
pid_t getpid();
pid_t getppid();
Voici un exemple de programme affichant son PID et son PPID:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char**argv) {
("Current process ID: %d\n", getpid());
printf("Current parent process ID: %d\n", getppid());
printfreturn EXIT_SUCCESS;
}
Ce programme donne pour résultat:
$ ./print_id
Current process ID: 17174
Current parent process ID: 25275
Lorsque le processus parent (P1
) d’un processus
(P2
) meurt, le processus fils est rattaché au processus au
processus initial de PID 1. Le PPID de P2
devient donc
1.
int system(const char* cmd);
cmd
Voici un exemple de programme utilisant la fonction
system
:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char**argv) {
int ret_val;
char* cmd="ps f";
("Running command '%s'\n", cmd);
printf("-----------\n");
printf= system(cmd);
ret_val ("-----------\n");
printf("system returned %d\n", ret_val);
printfreturn EXIT_SUCCESS;
}
Ce programme donne pour résultat:
$ ./system
Running command 'ps f'
-----------
PID TTY STAT TIME COMMAND
16847 pts/1 Ss+ 0:00 bash
17076 pts/1 S 0:01 _ okular chap.pdf
8199 pts/0 Ss+ 0:00 bash
25275 pts/7 Ss 0:00 bash
8174 pts/7 Sl 0:14 _ emacs contenu.tex
17540 pts/7 S+ 0:00 _ ./system
17541 pts/7 S+ 0:00 _ sh -c ps f
17542 pts/7 R+ 0:00 _ ps f
-----------
system returned 0
pid_t fork();
Voici un exemple d’utilisation de fork
:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char**argv) {
("I am process %d. My PPID: %d\n", getpid(), getppid());
printf
= fork();
pid_t ret_val if(ret_val == 0) {
("I'm the child process. PID=%d, PPID=%d\n", getpid(), getppid());
printf(1);
sleep} else if (ret_val>0) {
("I'm the parent process. PID=%d, PPID=%d\n", getpid(), getppid());
printf} else {
("Fork failed\n");
printf}
return EXIT_SUCCESS;
}
Lors du fork
, le processus est intégralement dupliqué.
Le processus fils possède donc la même mémoire que le processus père: on
retrouve donc les variables affectées dans le processus père avant le
fork.
Après le fork, les espaces mémoire des deux processus deviennent
dissociés: si le processus fils (resp. le père) modifie la variable
value
, il ne modifie que sa copie de la variable, et le
processus père (resp. le fils) ne voit pas la modification. Par exemple,
le programme suivant:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char**argv) {
int value = 1;
("[%d] Before forking. value = %d\n", getpid(), value);
printf= fork();
pid_t ret_val if(ret_val == 0) {
++;
value("[%d] After forking. value = %d\n", getpid(), value);
printf} else if (ret_val>0) {
(1);
sleep("[%d] After forking. value = %d\n", getpid(), value);
printf} else {
("Fork failed\n");
printf}
return EXIT_SUCCESS;
}
donnera, lors de son exécution, le résultat suivant:
$ ./fork_variable
[13229] Before forking. value = 1
[13230] After forking. value = 2
[13229] After forking. value = 1
exec
execlp
, execvp
, execve
,
execle
, execlp
, etc.Voici un exemple d’utilisation de execlp
:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char**argv) {
("I am process %d. My PPID: %d\n", getpid(), getppid());
printf
= fork();
pid_t ret_val if(ret_val == 0) {
("I'm the child process. PID=%d, PPID=%d\n", getpid(), getppid());
printf("ps", "ps", "-l", NULL);
execlp("This is printed only if execlp fails\n");
printf();
abort} else if (ret_val>0) {
("I'm the parent process. PID=%d, PPID=%d\n", getpid(), getppid());
printf(1);
sleep}
return EXIT_SUCCESS;
}
exec
est une famille de fonction permettant de remplacer
l’image du processus courant: l’ensemble de l’espace mémoire est effacé
et remplacé par l’image du procegramme exécuté.
Les paramètres peuvent être passés sous la forme d’une liste de
paramètres (avec les fonctions execl*
), ou sous la forme
d’un tableau de chaînes de caractères (avec les fonctions
execv*
).
pid_t wait(int *status);
status
permet de connaître la cause du
décès.pid_t waitpid(pid_t pid, int *wstatus, int options);
Voici un exemple d’utilisation de la fonction wait
:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char**argv) {
("I am process %d. My PPID: %d\n", getpid(), getppid());
printf
= fork();
pid_t ret_val if(ret_val == 0) {
("I'm the child process. PID=%d, PPID=%d\n", getpid(), getppid());
printf(1);
sleepreturn 17;
} else if (ret_val>0) {
("I'm the parent process. PID=%d, PPID=%d\n", getpid(), getppid());
printfint status;
= wait(&status);
pid_t pid
int exit_status = WEXITSTATUS(status);
("The child process %d ended with exit status %d\n", pid, exit_status);
printf
} else {
("Fork failed\n");
printf}
return EXIT_SUCCESS;
}
La fonction wait
retourne le PID du processus fils qui
s’est terminé et remplit la variable entière status
. Cette
variable peut ensuite être utilisée pour obtenir des informations sur la
terminaison du processus fils.
Par exemple, la macro WEXITSTATUS(status)
retourne le
code de retour du processus fils. Ce programme donnera donc :
$ ./exemple_wait
I am process 21088. My PPID: 20960
I'm the parent process. PID=21088, PPID=20960
I'm the child process. PID=21089, PPID=21088
The child process 21089 ended with exit status 17
La fonction waitpid
est une variante de
wait
. Elle permet d’attendre un processus fils précis, ou
de tester (sans bloquer le processus appelant) la terminaison d’un
processus.
Rôles d’un système d’exploitation:
Cloisonnement entre le mode utilisateur et le mode noyau
User mode
Kernel mode
2 méthodes:
Lorsqu’une interruption est reçue, le processeur suspend l’exécution du thread, bascule en mode noyau, et appelle la routine traitant l’interruption. Lorsque la routine se termine, le processeur rebascule en mode utilisateur est reprend l’exécution du thread.
Un appel système consiste à appeler une fonction exécuté en mode
noyau. Le passage du mode utilisateur au mode noyau peut se faire en
générant une interruption logicielle particulière (par exemple sur les
processeurs ARM ou les processeurs x86 32 bits), ou en exécutant une
instruction particulière (par exemple, l’instruction
syscall
sur les processeurs x86 64 bits) qui a un effet
équivalent. Le kernel exécute alors la fonction correspondant au numéro
de l’appel système demandé. Lorsque l’appel système se termine, on sort
du traitant d’interruption, et le processeur rebascule en mode
utilisateur.
La commande strace
intercepte et affiche les appels
systèmes d’un programme:
$ strace echo "coucou"
execve("/bin/echo", ["echo", "coucou"], [/* 54 vars */]) = 0
brk(NULL) = 0x25d2000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f619cc01000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("tls/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
[...]
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
write(1, "coucou\n", 7coucou
) = 7
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
Certains cadres de pages sont référencés par plusieurs processus.
Cela est possible par exemple si les processus ne font que des accès en
lecture à la page. Il s’agit typiquement du code des bibliothèques
partagées (libc.so
par exemple) qui peuvent être chargées
par plusieurs processus.
Sémaphore:
sem_open("/CLE", O_CREAT, S_IRWXU, nb_jetons);
sem_t*
/
sem_open("/CLE", 0);
sem_t
sem_unlink(const char *name)
sem_wait(sem_t* sem)
sem_post(sem_t* sem)
L’opération sem_wait
est bloquante tant qu’il n’y a pas
de jeton. L’opération sem_post
est, elle, non-bloquante, et
permet de débloquer un des processus en attente d’un jeton.
Voici un exemple d’utilisation d’un sémaphore. Le progamme
exemple_sem_init
crée un sémaphore, qui peut ensuite être
utilisé par le programme exemple_sem
. Le programme
exemple_sem_unlink
détruit un sémaphore.
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char**argv) {
*sem;
sem_t
if (argc != 3) {
(stderr, "USAGE = %s cle valeur\n", argv[0]);
fprintf(EXIT_FAILURE);
exit}
char*cle=argv[1];
int valeur = atoi(argv[2]);
/* Creation et initialisation du semaphore */
= sem_open(cle, O_CREAT, S_IRWXU, valeur);
sem if (sem == SEM_FAILED) {
("sem_open failed");
perror(EXIT_FAILURE);
exit}
("Initialisation OK\n");
printfint sval = -1;
/* recupere le nombre de jetons */
if(sem_getvalue(sem, &sval) <0) {
("sem_getvalue failed");
perror(EXIT_FAILURE);
exit}
("sval = %d\n", sval);
printf
return EXIT_SUCCESS;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <semaphore.h>
int main(int argc, char**argv) {
*sem;
sem_t
if (argc != 2) {
(stderr, "USAGE = %s cle\n", argv[0]);
fprintf(EXIT_FAILURE);
exit}
char*cle=argv[1];
/* Creation et initialisation du semaphore */
= sem_open(cle, 0);
sem if (sem == SEM_FAILED) {
("sem_open");
perror(EXIT_FAILURE);
exit}
("Ouverture OK\n");
printf
("On prend un jeton...\n");
printf(sem);
sem_wait("Jeton obtenu.\n");
printf(5);
sleep
("On relache le jeton\n");
printf(sem);
sem_post("Jeton relache\n");
printf
return EXIT_SUCCESS;
}
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char**argv) {
*sem;
sem_t
if (argc != 2) {
(stderr, "USAGE = %s cle\n", argv[0]);
fprintf(EXIT_FAILURE);
exit}
char*cle=argv[1];
if(sem_unlink(cle) < 0){
("sem unlink failed");
perror();
abort}
return EXIT_SUCCESS;
}
Rappel (CSC3102)
int kill(pid_t pid, int sig);
sig
au processus pid
sig
?
9
): pas portable (dépend de
l’OS)SIGKILL
) définie dans
signal.h
Voici un exemple de programme utilisant la fonction
kill
:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char**argv) {
if(argc != 2) {
(stderr, "usage: %s PID\n", argv[0]);
fprintfreturn EXIT_FAILURE;
}
= atoi(argv[1]);
pid_t pid int signo = SIGKILL;
("Sending signal %d to %d\n", signo, pid);
printf(pid, signo);
kill
return EXIT_SUCCESS;
}
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
struct sigaction
est une structure de la forme:struct sigaction {
void (*sa_handler)(int); // pointeur sur la fonction à appeler
void (*sa_sigaction)(int, siginfo_t *, void *);
;
sigset_t sa_maskint sa_flags;
void (*sa_restorer)(void);
};
Il est possible d’utiliser sigaction
pour “intercepter”
tout signal sauf les signaux SIGKILL
et
SIGSTOP
struct sigaction
La valeur prise par sa_handler
est: * l’adresse d’une
fonction (par ex: void signal_handler(int signo)
* Le
paramètre signo
est le numéro du signal reçu * la valeur
SIG_DFL
pour restaurer l’action par défaut (tuer le
processus) * la valeur SIG_IGN
pour ignorer le signal: à la
réception de ce signal, aucune action ne sera effectuée
Sauf cas d’usages particuliers, les autres champs de la structure
sigaction
sont à mettre à 0
.
oldact
La fonction sigaction
modifie le comportant du processus
lorsqu’il reçoit le signal signum
. Si oldact
n’est pas NULL
, l’ancien comportement y est stocké.
Si la fonction traitant le signal manipule des variables globales, il
est conseillé de les déclarer volatile
. Par exemple:
volatile int var;
Lorsqu’une variable est déclarée volatile
, le
compilateur limite les optimisations faites sur cette variable. Par
exemple, le compilateur ne met pas en cache (dans un registre) la
variable.
Si une fonction (par exemple foo
) qui manipule la
variable var
non volatile
est interrompue par
un traitant de signal (sig_handler
) modifiant
var
, la modification de la variable risque de ne pas être
“vue” par foo
qui travaille sur une copie en cache de la
variable. La fonction foo
risque donc de travailler sur une
version obsolète de la variable.
Voici un exemple de programme utilisant sigaction
pour
intercepter un signal:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
/* Function to call upon signal reception */
void signal_handler(int signo) {
("Received: signal %d\n", signo);
printf}
int main(int argc, char**argv) {
if(argc != 2) {
(stderr, "usage: %s signo\n", argv[0]);
fprintfreturn EXIT_FAILURE;
}
/* Initialize the sigaction structure */
int signo = atoi(argv[1]);
struct sigaction s;
.sa_handler = signal_handler;
s
/* Install the signal handler */
("Installing signal handler for signal %d\n", signo);
printfint retval = sigaction(signo, &s, NULL);
if(retval<0) {
("sigaction failed");
perror();
abort}
/* Wait to receive signals */
while(1) {
("[%d] Sleeping...\n", getpid());
printf(1);
sleep}
return EXIT_SUCCESS;
}
int pause( );
int alarm(unsigned int s);
SIGALRM
après s
secondes Pour programmer une alarme avec une granularité plus fine, utilisez
la fonction setitimer
. Cette fonction permet de programmer
des alarmes périodiques (qui se répètent) avec une granularité de
l’ordre de la microseconde.
Les 3 sous-sections suivantes présentent des notions pour les étudiant·e·s Ninja qui ont vraiment envie d’aller encore plus loin.
sigsetjmp
et
siglongjmp
Permet de faire un “saut (goto
) non local”
int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);
env
sigsetjmp
Ces fonctions peuvent être utile pour la gestion des signaux :
sigsetjmp
permet de définir l’endroit du code où revenir
après l’appel à siglongjmp
.signal_handler
); appeler siglngjmp
.Elles sont également utilisées pour mettre en place des mécanismes d’exception.
Exemple : suivez le lien pour visualiser cet exemple tiré du livre “Développement système sous Linux” de Christophe BLAESS. FYI, ce livre est disponible à la médiathèque.
libuv permet de faire de la programmation événementielle
libuv
.libuv
is not much different from
directly using the BSD socket interface, some things are easier, all are
non-blocking, but the concepts stay the same. In addition libuv offers
utility functions to abstract the annoying, repetitive and low-level
tasks like setting up sockets using the BSD socket structures, DNS
lookup, and tweaking various socket parameters.” (extrait du
chapitre Networking de la documentation libuv).Pour récupérer des exemples, c’est ici.
Il existe aussi d’autres bibliothèques, mais qui nous semblent moins pertinentes (vu leurs dernières dates de mise à jour): libevent et libev.
Cet extrait de l’exemple de tcp-echo-server
de
libuv
(cf. libuv-1.48.0/docs/code/tcp-echo-server/main.c
)
illustre le problème de lisibilité:
void echo_write(uv_write_t *req, int status) {
if (status) {
(stderr, "Write error %s\n", uv_strerror(status));
fprintf}
(req);
free_write_req}
void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
if (nread > 0) {
*req = (write_req_t*) malloc(sizeof(write_req_t));
write_req_t ->buf = uv_buf_init(buf->base, nread);
req((uv_write_t*) req, client, &req->buf, 1, echo_write);
uv_writereturn;
}
// ...
}
void on_new_connection(uv_stream_t *server, int status) {
// ...
*client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
uv_tcp_t (loop, client);
uv_tcp_initif (uv_accept(server, (uv_stream_t*) client) == 0) {
((uv_stream_t*) client, alloc_buffer, echo_read);
uv_read_start}
// ...
}
Quand une nouvelle connexion est établie avec ce serveur, la
fonction on_new_connection
est appelée. Cette fonction
appelle la fonction uv_read_start
qui contient un paramètre
echo_read
, i.e. la fontion à appeler quand la lecture sur
la socket par uv_read_start
sera terminée.
Et echo_read
appelle la fonction
uv_write
avec le paramètre echo_write
, i.e. la
fonction à appeler quand l’écriture sur la socket sera
terminée.
Ce serait bien d’avoir une seule fonction qui enchaîne ces 3 codes. Cela supposerait une fonction capable de relâcher la main en plein milieu de son exécution et reprendre à cet endroit quand on lui redonnerait la main. C’est la notion de coroutine.
Hélas, cette notion n’est pas implémentée en langage C. Mais, il existe des contournements, cf. articles que nous vous invitons à lire :
Commande | Description |
---|---|
gdb [prog] |
Charge le programme [prog] dans GDB. |
gdb --args [prog] [arg1 arg2 ...] |
Charge le programme [prog] dans GDB avec les
paramètres[arg1 arg2 ...] |
gdb [prog] [core-file] |
Charge le core-dump du programme [prog] dans GDB. |
q ou quit |
Quitte GDB. |
Commande | Description |
---|---|
r ou run |
Lance l’exécution du programme chargé dans gdb. |
r [arg1 arg2 ...] |
Lance l’exécution du programme chargé dans gdb avec les paramètres
[arg1 arg2 ...] . |
set [args arg1 arg2 ...] |
Sélectionne la liste des arguments ([arg1 arg2 ...] )
pour le prochain programme à démarrer. |
Commande | Description |
---|---|
p [var] ou print [var] |
Affiche la valeur de la variable [var] . |
p/x [var] |
Affiche la valeur hexadécimale de la variable
[var] . |
display [var] |
Affiche la valeur de la variable [var] à chaque arrêt
du programme. |
bt ou backtrace |
Affiche la pile d’appel. |
frame |
Affiche la stack frame courante. |
frame [x] |
Sélectionne la stack frame [x] . |
l ou list |
Affiche la portion de code autour de la stack frame sélectionnée. |
Commande | Description |
---|---|
b [pos] ou break [pos] |
Positionne un breakpoint à l’endroit [pos] .
[pos] peut être un nom de fonction, un numéro de ligne (du
fichier courant), un nom de fichier+numéro de ligne
(break mon_fichier:57 ), etc. |
clear [pos] |
Supprime le breakpoint positionné à l’endroit
[pos] . |
d [num] ou delete [num] |
Supprime le breakpoint numéro [num] . |
w [var] ou watch [var] |
Surveille l’état de la variable [var] . |
Commande | Description |
---|---|
n ou next |
Avance d’un pas. |
s ou step |
Avance d’un pas. Si l’instruction à exécuter est un appel de fonction, ne descend pas dans la fonction. |
c ou continue |
Continue l’exécution jusqu’au prochain breakpoint. |
Commande | Description |
---|---|
disassemble [function] |
Affiche le code assembleur de la fonction
[function] . |
info registers |
Affiche la valeur des registres. |
p \$[register] |
Affiche la valeur d’un registre. Exemple:
print \$eax . |
Commande | Description |
---|---|
record |
Démarre l’enregistrement du comportement du processus. |
record stop |
Arrête l’enregistrement. |
rs ou reverse-step |
Comme step, mais en arrière. |
rn ou reverse-next |
Comme next, mais en arrière. |
rc ou reverse-continue |
Comme continue, mais en arrière. |
set can-use-hw-watchpoints 0 |
Permet d’utiliser des watchpoints en reverse debugging. |
Commande | Description |
---|---|
break [pos] if [cond] |
Arrête le processus à [pos] si la condition
[cond] est vérifiée. |
up |
Remonte d’une stack frame dans la pile. |
down |
Descend d’une stack frame dans la pile. |
attach [pid] |
Attache GDB au processus de pid [pid] . |
detach |
Détache le processus de GDB. |
set env [var]=[value] |
Affecte la valeur [value] à la variable d’environnment
[var] . |
# In order to reuse this Makefile
# Modify the project name => modify the binary name
# Add all the .o dependencies in OBJ
# That's all folk :)
PROJECT=main
BIN=$(PROJECT)
OBJ=main.o
# compilation flags
CFLAGS=-Wall -Werror -g
# link flags
LDFLAGS=
CC=gcc
Echo=@echo [$(PROJECT)]:
ifndef VERBOSE
Verb := @
endif
# Tells make that 'all' and 'clean' are "virtual" targets (that does not
# generate a file)
.PHONY: all clean
all: $(BIN)
$(BIN): $(OBJ)
$(Echo) Linking $@
$(Verb) $(CC) $(LDFLAGS) -o $@ $^
# generic rule: to generate foo.o, we need foo.c
%.o: %.c
$(Echo) Compiling $<
$(Verb) $(CC) $(CFLAGS) -c "$<" -o "$@"
# you can add specific compilation rules here
# you can invoke "make clean" to delete all the generated files
clean:
$(Echo) Cleaning compilation files
$(Verb) rm -f $(OBJ) $(BIN)