Aller au contenu principal

Programmation C · L2 · Section 5/12

Unix et pointeurs avancés

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#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) :

cc

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 :

cc

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}
cc

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 à travers p. 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 de p, la zone pointée n’est accessible que via p. C’est une information d’aliasing pour l’optimiseur ; la violer est un comportement indéfini. C’est ce qui permet de vectoriser memcpy-like :
cc

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, volatile n’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 :

cc

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 :

shsh

1printf 'ABC' > /tmp/trois.txt2cc -std=c17 -Wall -Wextra -Wpedantic hexdump.c -o hexdump3./hexdump /tmp/trois.txt
code

100000000  41 42 43

#5. Outillage de build et bibliothèque partagée

Sur un binaire, nm liste les symboles, objdump -d désassemble :

shsh

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 main

Une bibliothèque partagée se compile en code indépendant de la position, puis se charge à l’exécution :

shsh

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=. ./chargeur
cc

1/* 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

  1. Écrivez hexdump.c complet (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.
  2. Écrivez libmathutil.so exposant int double_it(int), puis chargeur.c ; le jalon est double_it(21) = 42.
  3. Instrumentez le chargeur : appelez dlsym sur un nom inexistant et vérifiez que le message d’erreur de dlerror s’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.

Quand la valeur de errno est-elle fiable ?
Quand la valeur de errno est-elle fiable ?