Outils

Pour le cours de programmation système, nous travaillerons sous Linux avec l’éditeur de code Visual Studio Code / VSCode.

Pour compiler, nous utiliserons GCC (GNU Compiler Collection) ; Clang (C language family frontend for LLVM) fonctionne aussi.

Les commandes d’installation des extensions VSCode sont données dans les consignes. Les deux extensions utilisées sont :

Installation sur votre machine perso

Sous Linux ou macOS

Paquets à installer :

  • Debian/Ubuntu :

    sudo apt update
    sudo apt install build-essential gdb valgrind clang-format strace htop hwloc xxd netcat-openbsd
    
  • Fedora :

    sudo dnf group install development-tools
    sudo dnf install gdb valgrind clang-tools-extra strace htop hwloc xxd nmap-ncat libasan libubsan
    
  • Arch :

    sudo pacman -S base-devel gdb valgrind clang strace htop hwloc tinyxxd openbsd-netcat
    
  • macOS (avec Homebrew) : configuration non testée. valgrind, gdb et strace ne sont pas disponibles sur les versions récentes de macOS (en particulier sur Apple Silicon) : utilisez -fsanitize=address (voir Analyse à l’exécution) et lldb à la place ; -fsanitize=address n’y détecte pas les fuites mémoire, utilisez pour cela leaks --atExit -- ./mon_prog. Attention, gcc y est un alias de clang (le GCC de Homebrew s’appelle gcc-14, gcc-15, etc.) :

    brew install gcc clang-format
    

Sous Windows

C’est un cours de programmation système sous Unix. La plupart des TP (fichiers POSIX, mémoire, processus, threads, sockets) ne compileront pas nativement sous Windows : fork, unistd.h, sys/socket.h ou /proc n’y existent pas.

Vous pouvez au choix :

Dans les quatre cas, faites-le avant de venir en cours ; sinon, utilisez les PC des salles.

Compilation

La compilation d’un programme C se déroule en 4 étapes :

  1. Prétraitement – le préprocesseur traite les directives comme #include, #define et produit du C « pur », sans macros ni includes (fichier .i, normalement temporaire) ;

  2. Compilation – le compilateur transforme ce fichier en assembleur (fichier .s) ;

  3. Assemblage – l’assembleur (as) transforme le code assembleur en code machine dans un fichier objet .o ;

  4. Édition de liens (linking) – l’éditeur de liens assemble les objets et les bibliothèques (dont la libc) en un programme final.

Chaîne de compilation : prog.c, préprocesseur (option -E), prog.i, compilateur (option -S), prog.s, assembleur (option -c), prog.o, éditeur de liens avec la libc, exécutable prog.

En pratique, ces étapes sont enchaînées automatiquement pour produire un exécutable :

# avec gcc, éventuellement gcc-VERSION
gcc -std=c2x -Wall -Wextra -pedantic -g mon_prog.c -o mon_prog
# ou avec clang, éventuellement clang-VERSION
clang -std=c2x -Wall -Wextra -pedantic -g mon_prog.c -o mon_prog
# puis exécution avec
./mon_prog

Options fréquentes :

Option

Effet

-Wall

Active les avertissements de base

-Wextra

Avertissements supplémentaires

-pedantic

Avertit des écarts à la norme choisie par -std

-std=c2x

Utilisation de la norme C23

-g

Ajoute les informations de debug pour gdb ou valgrind

-O2/-O3

Active les optimisations

-o fichier

Définit le nom du fichier de sortie

-DNOM=valeur

Définit la macro NOM, comme #define NOM valeur

-pthread

Compile et lie avec la bibliothèque des threads POSIX

-fsanitize=...

Ajoute des vérifications à l’exécution (voir ci-dessous)

Analyse à l’exécution

Pour détecter les problèmes de mémoire, en particulier lors de l’utilisation de pointeurs (fuites, accès invalides, etc.), deux outils peuvent être utilisés :

  • valgrind (compiler avec -g pour inclure les informations de debug) :

    # compilation
    gcc -std=c2x -Wall -Wextra -pedantic -g mon_prog.c -o mon_prog
    # exécution d'un programme avec détection des fuites mémoire
    valgrind ./mon_prog
    
    # pour un rapport plus détaillé avec les emplacements dans le code source
    valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes -s ./mon_prog
    

    Si le LEAK SUMMARY affiché vers la fin indique des octets definitely lost ou indirectly lost, il y a une fuite mémoire (la catégorie still reachable, mémoire encore accessible à la fin du programme, n’est pas forcément un bug) :

    ==340142== HEAP SUMMARY:
    ==340142==     in use at exit: 400 bytes in 1 blocks
    ==340142==   total heap usage: 3 allocs, 2 frees, 2,448 bytes allocated
    ==340142==
    ==340142== LEAK SUMMARY:
    ==340142==    definitely lost: 400 bytes in 1 blocks
    ==340142==    indirectly lost: 0 bytes in 0 blocks
    ==340142==      possibly lost: 0 bytes in 0 blocks
    ==340142==    still reachable: 0 bytes in 0 blocks
    ==340142==         suppressed: 0 bytes in 0 blocks
    

    Si toute la mémoire allouée a été libérée, le HEAP SUMMARY affiche autant d’allocations (allocs) que de libérations (frees), et valgrind conclut par « All heap blocks were freed » :

    ==336877== HEAP SUMMARY:
    ==336877==     in use at exit: 0 bytes in 0 blocks
    ==336877==   total heap usage: 3 allocs, 3 frees, 2,448 bytes allocated
    ==336877==
    ==336877== All heap blocks were freed -- no leaks are possible
    

    D’autres erreurs peuvent être affichées lors de l’exécution.

  • AddressSanitizer et UndefinedBehaviorSanitizer : activés par les options -fsanitize=address,undefined -fno-sanitize-recover=all de gcc :

    # compilation
    gcc -std=c2x -Wall -Wextra -pedantic -g -fsanitize=address,undefined -fno-sanitize-recover=all mon_prog.c -o mon_prog
    # exécution
    ./mon_prog
    

    Si AddressSanitizer:DEADLYSIGNAL s’affiche à répétition, faites Ctrl+C et relancez (peut planter plusieurs fois, ne devrait pas arriver avec les versions récentes des outils).

    Le message en cas d’erreur est clair avec AddressSanitizer :

    =================================================================
    ==331863==ERROR: LeakSanitizer: detected memory leaks
    
    Direct leak of 400 byte(s) in 1 object(s) allocated from:
        #0 0x7d0628d2c30f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
        #1 0x5b257cd9d21e in main /home/user/mon_prog.c:4
        #2 0x7d0628029d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    
    SUMMARY: AddressSanitizer: 400 byte(s) leaked in 1 allocation(s).
    

    La ligne #1 indique le fichier et la ligne de l’allocation (mon_prog.c:4), grâce à l’option -g.

    Contrairement à valgrind, les sanitizers détectent aussi les accès hors limites des tableaux dans le stack, par exemple :

    1#include <stdio.h>
    2
    3int main(void) {
    4    int tab[5];
    5    tab[10] = 4;
    6    printf("tab[10] = %d\n", tab[10]);
    7    return 0;
    8}
    

    À l’exécution, c’est ici UndefinedBehaviorSanitizer (UBSan, activé par la partie undefined de -fsanitize=address,undefined) qui signale l’erreur :

    mon_prog.c:5:8: runtime error: index 10 out of bounds for type 'int [5]'
    

    valgrind (Memcheck), lui, ne détecte pas ce débordement : il ne connaît pas les limites des tableaux dans le stack ou globaux, seulement celles des blocs alloués dans la heap.

Avertissement

Ne lancez pas valgrind sur un exécutable compilé avec -fsanitize : les deux outils ne fonctionnent pas ensemble. Recompilez sans -fsanitize avant de lancer valgrind.

Pour les programmes multithreads (voir Partie 5 - Threads), l’outil helgrind de valgrind détecte les data races (programme compilé sans -fsanitize) :

gcc -std=c2x -Wall -Wextra -pedantic -g -pthread mon_prog.c -o mon_prog
valgrind --tool=helgrind ./mon_prog

ThreadSanitizer (option -fsanitize=thread, incompatible avec -fsanitize=address) détecte aussi les data races, sans valgrind.

Formatage automatique

Pour maintenir un code lisible, cohérent et conforme à de bonnes pratiques, les outils de formatage automatique comme clang-format sont très utiles. Ils permettent d’uniformiser le style de code entre plusieurs projets/groupes/classes, de gagner du temps en évitant les discussions inutiles liées à la mise en forme du code et d’éviter les commits sur git avec uniquement des changements de format.

clang-format lit les règles de formatage dans le fichier .clang-format. Ce fichier et le fichier .vscode/settings.json (formatage à l’enregistrement) sont fournis dans les consignes.

Une fois la configuration en place, le code suivant :

#include <stdio.h>

int main(    void     )
{
int a=    0;
            int b   = 4 ;

for   (  int i = 0;
i< 10;   ++ i)
{


printf(
    "%d",        i
);
}
return 0           ;
}

sera automatiquement formaté en :

#include <stdio.h>

int main(void) {
    int a = 0;
    int b = 4;

    for (int i = 0; i < 10; ++i) {

        printf("%d", i);
    }
    return 0;
}

Pour l’utiliser dans la ligne de commande :

clang-format -i p1/*.c

L’option -i applique les modifications directement dans les fichiers.

Debug

Il existe plusieurs debuggers en ligne de commande, comme gdb ou lldb.

VSCode propose un debugger pratique et rapide à utiliser pour un fichier :

Autres outils

Documentation

Pour générer les templates de docstring Doxygen, utilisez l’extension Doxygen Documentation Generator.

# installation depuis le terminal :
code --install-extension cschlosser.doxdocgen

Ou depuis la palette de VSCode (Ctrl+P) :

ext install cschlosser.doxdocgen

Ensuite, pour ajouter une docstring à une fonction, tapez /** puis appuyez sur Entrée :

// 1 taper /** (le */ s'ajoute automatiquement)
/** */
int my_function(int a, float b) {
    // ...
}

// 2 appuyer sur "Entrée"
/**
* @brief
*
* @param a
* @param b
* @return int
*/
int my_function(int a, float b) {
    // ...
}

Ensuite, quand vous passerez le curseur sur la fonction, vous verrez les informations sur ce que fait la fonction et les différents paramètres.

Affichage des erreurs

Error Lens permet d’afficher les erreurs directement sur la ligne concernée. Il peut être envahissant si vous utilisez un correcteur orthographique et nommez vos variables en français : il signalera toutes les variables écrites sans accent.

Compilation de projets : Make et CMake

Les exercices du cours tiennent en un seul fichier : une commande gcc suffit. Pour un projet de plusieurs fichiers, on automatise la compilation :

  • Make lit un fichier Makefile qui décrit comment compiler chaque fichier et ne recompile que les fichiers modifiés ;

  • CMake génère ces Makefile à partir d’un fichier CMakeLists.txt plus simple à écrire, et s’intègre dans VSCode avec l’extension CMake Tools.

Analyse statique

Des outils d’analyse statique lisent le code sans l’exécuter et signalent des bugs que le compilateur ne voit pas (pointeur NULL déréférencé, fuite sur un chemin d’erreur, variable non initialisée…) : cppcheck et clang-tidy.

# --suppress : masque les messages inutiles sur les includes système
cppcheck --enable=all --language=c --quiet --suppress=missingIncludeSystem --suppress=checkersReport p1/p1e3.c

Outils en ligne de commande

  • strace : affiche les appels système faits par un programme (voir l”introduction) ;

  • xxd, hexdump, od : affichent le contenu d’un fichier binaire octet par octet (voir Partie 2 - Fichiers et l’exercice p2e5) ;

  • /proc/PID/maps : affiche les zones mémoire d’un processus (voir Partie 3 - Mémoire) ;

  • htop, ps, pstree : affichent les processus en cours (voir Partie 4 - Processus) ;

  • nc (netcat) : client ou serveur TCP minimal, pour tester un serveur sans écrire de client (nc 127.0.0.1 8080) ou un client sans écrire de serveur (nc -l 8080) (voir Partie 6 - Sockets) ;

  • ss -tlnp : liste les ports TCP en écoute et les programmes associés (voir Partie 6 - Sockets).