Programmation C · L2 · Section 5/12
Unix et pointeurs avancés
Progression
#Unix, pointeurs avancés et outillage système
Nous quittons la bibliothèque standard pour dialoguer directement avec le noyau : appels système POSIX, gestion des erreurs par errno, signaux, puis les qualifications de pointeurs qui changent ce que le compilateur a le droit de supposer (const, restrict, volatile). Le chapitre se referme sur la chaîne d’outils de build et la production d’une bibliothèque partagée chargée dynamiquement.
#1. Appels système et erreurs
Contrairement aux fonctions de la bibliothèque, les appels système demandent un service au noyau et peuvent échouer pour des raisons extérieures au programme (fichier absent, permissions, disque plein) :
1#include <fcntl.h>2#include <unistd.h>3#include <stdio.h>4#include <string.h>5#include <errno.h>6 7int copie_brute(const char *src) {8 int fd = open(src, O_RDONLY);9 if (fd < 0) {10 fprintf(stderr, "open %s : %s\n", src, strerror(errno));11 return -1;12 }13 char tampon[4096];14 ssize_t n;Le contrat à retenir : presque tous les appels renvoient -1 en cas d’échec et positionnent errno, qui n’est fiable que immédiatement après l’échec. errno est une variable modifiable par tout appel : sauvegardez-la avant d’afficher autre chose. Notez aussi que write peut écrire moins que demandé (signaux, tampons du noyau) : la boucle d’écriture n’est pas de la paranoia, c’est le contrat.
ssize_t est signé (compte ou -1), size_t non signé : la conversion explicite (size_t)(n - ecrit) n’a lieu qu’après avoir vérifié n > 0.
#2. Signaux : terminer proprement
Un SIGINT (Ctrl-C) interrompt le processus là où il se trouve. Pour libérer des ressources ou finir une écriture avant de partir, on installe un gestionnaire avec sigaction :
1#include <signal.h>2#include <stdbool.h>3 4static volatile sig_atomic_t arrete = false; /* seul type sûr à partager avec un handler */5 6static void sur_sigint(int signo) {7 (void)signo;8 arrete = true; /* on ne fait rien d'autre dans un handler */9}1struct sigaction sa = {0};2sa.sa_handler = sur_sigint;3sigemptyset(&sa.sa_mask);4sa.sa_flags = 0; /* pas de SA_RESTART : read() doit revenir pour qu'on voie le drapeau */5sigaction(SIGINT, &sa, NULL);6 7while (!arrete) {8 ssize_t n = read(STDIN_FILENO, tampon, sizeof tampon);9 if (n < 0 && errno == EINTR) continue; /* interrompu par le signal : on reboucle */10 /* ... traitement ... */11}12/* nettoyage ici, hors du handler */Trois règles non négociables : dans un gestionnaire, seules les opérations sûres en contexte asynchrone sont permises (affecter un volatile sig_atomic_t en est l’archétype ; printf ou malloc en sont exclus) ; volatile est indispensable pour que le compilateur relise réellement la variable à chaque tour de boucle ; sans SA_RESTART, les appels lents reviennent avec EINTR, qu’il faut gérer.
#3. Qualifier les pointeurs : const, restrict, volatile
const T *p: promesse de lecture seule à traversp. Elle protège l’appelant (la fonction ne modifiera pas sa donnée), pas le monde entier : la donnée peut changer par ailleurs.T *restrict p: engagement que, pendant la durée de vie dep, la zone pointée n’est accessible que viap. C’est une information d’aliasing pour l’optimiseur ; la violer est un comportement indéfini. C’est ce qui permet de vectorisermemcpy-like :
1void copie_octets(unsigned char *restrict dst, const unsigned char *restrict src, size_t n) {2 for (size_t i = 0; i < n; ++i) dst[i] = src[i];3}Si dst et src se chevauchaient, l’appel serait invalide : le contrat restrict l’interdit.
volatile T *p: chaque accès doit réellement avoir lieu, dans l’ordre écrit. À réserver aux variables partagées avec un gestionnaire de signal, un autre thread ou du matériel mémoire-mappé. Sur du code ordinaire,volatilen’apporte ni atomicité ni thread-safety.
#4. Un hexdump minimal, complet et vérifiable
Assembler les briques : lecture brute, affichage en hexadécimal et ASCII, gestion d’erreur :
1/* hexdump.c : ./hexdump fichier */2#include <stdio.h>3#include <stdint.h>4 5void dump_hex(const unsigned char *bytes, size_t len, unsigned long offset) {6 for (size_t i = 0; i < len; ++i) {7 if (i % 16 == 0) printf("%08lX ", offset + i);8 printf("%02X ", bytes[i]);9 if (i % 16 == 15 || i + 1 == len) putchar('\n');10 }11}Jalon observable :
1printf 'ABC' > /tmp/trois.txt2cc -std=c17 -Wall -Wextra -Wpedantic hexdump.c -o hexdump3./hexdump /tmp/trois.txt100000000 41 42 43#5. Outillage de build et bibliothèque partagée
Sur un binaire, nm liste les symboles, objdump -d désassemble :
1nm hello | grep puts # U puts : symbole non résolu, fourni par la bibliothèque C2objdump -d hello | head # le désassemblage de la fonction mainUne bibliothèque partagée se compile en code indépendant de la position, puis se charge à l’exécution :
1cc -std=c17 -Wall -Wextra -fPIC -shared mathutil.c -o libmathutil.so2cc -std=c17 -Wall -Wextra chargeur.c -ldl -o chargeur # ancien ordre de liaison : -ldl après les sources3LD_LIBRARY_PATH=. ./chargeur1/* chargeur.c */2#include <dlfcn.h>3#include <stdio.h>4 5int main(void) {6 void *lib = dlopen("./libmathutil.so", RTLD_NOW);7 if (!lib) { fprintf(stderr, "dlopen : %s\n", dlerror()); return 1; }8 int (*double_it)(int) = dlsym(lib, "double_it");9 if (!double_it) { fprintf(stderr, "dlsym : %s\n", dlerror()); dlclose(lib); return 1; }10 printf("double_it(21) = %d\n", double_it(21));11 dlclose(lib);12 return 0;13}dlopen renvoie un pointeur opaque propriété de l’appelant : il faut exactement un dlclose. dlsym renvoie void * ; l’affectation directe à un pointeur de fonction est une extension POSIX acceptée, un cast explicite reste le plus portable.
#Atelier
- Écrivez
hexdump.ccomplet (affichage hexadécimal et colonne ASCII, gestion des arguments, code de sortie non nul si le fichier est illisible) et vérifiez le jalon ci-dessus. - Écrivez
libmathutil.soexposantint double_it(int), puischargeur.c; le jalon estdouble_it(21) = 42. - Instrumentez le chargeur : appelez
dlsymsur un nom inexistant et vérifiez que le message d’erreur dedlerrors’affiche et que le programme quitte avec le code 1.
Pourquoi LD_LIBRARY_PATH=.
Par défaut le chargeur dynamique cherche les bibliothèques dans les chemins système. LD_LIBRARY_PATH=. ajoute le répertoire courant le temps d’une exécution, sans installation. Alternative propre au développement : lier avec -Wl,-rpath,'$ORIGIN' pour que le binaire cherche à côté de lui-même.