Debug applicatif avec Eris Linux

Publié par cpb
Oct 07 2026

Une fois l’application métier développée, le container correspondant construit et déployé sur une carte (ou un groupe de cartes) de test, il est souvent nécessaire de passer à une étape moins réjouissante : le debug !

Analyser le comportement d’une application fonctionnant dans un container, s’exécutant lui-même dans une machine distante – qui est prévue pour empêcher toute intrusion pour des raisons de sécurité – est assez compliqué. Toutefois Eris Linux nous propose deux méthodes simples pour suivre l’exécution de notre code applicatif. (D’autres méthodes verront probablement le jour, cet article sera modifié en conséquence quand ce sera le cas).

Le premier moyen d’analyse de l’exécution d’un programme sous Linux est de récupérer les messages qu’il peut envoyer sur la sortie standard (stdout) et la sortie interactive (stderr). La seconde méthode consistera à exécuter le programme sous le contrôle d’un débogueur distant. Commençons par récupérer les messages de sortie d’un exécutable.

Quand le programme s’exécute dans un container sur un système embarqué, le moyen le plus simple est d’envoyer les messages sur une machine distante, où l’on pourra les récupérer et les étudier.

À noter : pour que les méthodes décrites ci-dessous fonctionnent, vous devrez utiliser au minimum Eris Linux version 1.2.0. Si vous avez des systèmes dans une version précédente, ils peuvent se mettre à jour automatiquement si vous avez coché les cases « Automatic system update » et « Automatic reboot after update » de la configuration « Group setup » .

Programme d’exemple

Je vous conseille de commencer par télécharger le repository suivant, qui contient les exemples que nous avons déjà utilisés dans l’article sur le développement applicatif : https://github.com/eris-linux/eris-linux-containers. Ce dépôt étant amené à changer régulièrement, je vous propose de vérifier que vous êtes bien sur le bon tag.

[~]$ git  clone  https://github.com/eris-linux/eris-linux-containers
Cloning into 'eris-linux-containers'...
  [...]

[~]$ cd  eris-linux-containers/

[eris-linux-containers]$ git  checkout  1.1.0
Note: switching to '1.1.0'.
  [...]

Pour tester notre première possibilité de debug, je vous propose d’utiliser le programme C suivant, qui écrit des messages sur ses sorties régulièrement.

$ cat test-debug-stdio/test-debug-stdio.c 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	int i = 0;
	for (;;) {
		fprintf(stderr, "This message goes to stderr (%d)\n", i);
		fprintf(stdout, "This message goes to stdout (%d)\n", i);
		sleep(1);
		if (++i > 5) {
			fflush(stdout);
			i = 0;
		}
	}
	return 0;
}

Je place ce fichier dans un sous-répertoire test-debug-stdio/ qui contient également le fichier Makefile suivant :

$ cat test-debug-stdio/Makefile

APP = test-debug-stdio

OBJS = test-debug-stdio.o

CFLAGS += -Wall -g
LDFLAGS =


all: $(APP)

$(APP): $(OBJS)
	$(CC) $(CFLAGS) $(LDFLAGS)  $^ -o $@ $(LDLIBS)

%o: %c $(INC)
	$(CC) -c $(CFLAGS) $<

clean:
	rm -rf out *.o $(APP)

Et le fichier Dockerfile :

[eris-linux-containers (master)]$ cat test-debug-stdio/Dockerfile 


################### Build container #########################################
FROM  alpine:latest AS build
RUN apk add --no-cache build-base file
WORKDIR /src
COPY test-debug-stdio.c Makefile .
RUN make


################### Final container #########################################
FROM alpine:latest
COPY --from=build /src/test-debug-stdio  /test-debug-stdio
CMD ["/test-debug-stdio"]

Le Makefile et le Dockerfile sont directement adaptés de l’exemple 05-hello-world-in-c.

Nous compilons le programme applicatif et créons un container avec la commande :

$ ./create-container  arm64  test-debug-stdio/  test-debug-stdio

Le container est « uploadé » sur le serveur Eris Linux et déployé vers un système de test grâce aux onglets « My devices » et « My containers » du Device Manager.

Redirections des messages

Dans l’onglet « My devices » nous cliquons ensuite sur le bouton « Setup » (en bas à droite de l’écran) correspondant au Device (pas au groupe). La fenêtre suivante s’ouvre :

Fig. 1 – Fenêtre de setup d’un device.

Dans le bas de cette fenêtre, des cases à cocher permettent d’activer certaines fonctionnalités de débogage pour l’équipement concerné :

  • redirection des messages sur stdout et stderr vers un port UDP distant,
  • utilisation d’un débogueur GDB distant.

Nous activons la première case et remplissons les deux champs qui apparaissent avec l’adresse IP de la machine qui recevra les traces (votre poste de développement) et un numéro de port de base UDP. Par défaut ce numéro de port est 10001, ce qui signifie que les messages du container se trouvant dans le slot numéro 1 seront redirigés vers ce port, les messages du slot 2 vers le port 10002, ceux du slot 3 vers le port 10003 et ceux du slot 4 vers le port UDP 10004.

Fig. 2 – Configuration de la redirection de messages.

Sur la machine se trouvant à l’adresse indiquée nous allons recevoir les messages redirigés. Pour les visualiser, on peut employer divers utilitaires. L’un des plus courants est socat. On le lance ainsi :

$ socat udp-recv: <port-recept> <dest-redirection>

Comme on souhaite visualiser les messages et pas les réémettre, on indique STDOUT pour <dest-redirection>

[~]$ socat  -u  UDP-RECV:10001  STDOUT  
<27>Sep 24 12:09:45 eris-1[668]: This message goes to stderr (5)
<30>Sep 24 12:09:45 eris-1[668]: This message goes to stdout
<27>Sep 24 12:09:46 eris-1[668]: This message goes to stderr (0)
<27>Sep 24 12:09:47 eris-1[668]: This message goes to stderr (1)
<27>Sep 24 12:09:48 eris-1[668]: This message goes to stderr (2)
<27>Sep 24 12:09:49 eris-1[668]: This message goes to stderr (3)
<27>Sep 24 12:09:50 eris-1[668]: This message goes to stderr (4)
<27>Sep 24 12:09:51 eris-1[668]: This message goes to stderr (5)
<30>Sep 24 12:09:51 eris-1[668]: This message goes to stdout (0)
<30>Sep 24 12:09:51 eris-1[668]: This message goes to stdout (1)
<30>Sep 24 12:09:51 eris-1[668]: This message goes to stdout (2)
<30>Sep 24 12:09:51 eris-1[668]: This message goes to stdout (3)
<30>Sep 24 12:09:51 eris-1[668]: This message goes to stdout (4)
<27>Sep 24 12:09:51 eris-1[668]: This message goes to stderr (5)
<27>Sep 24 12:09:52 eris-1[668]: This message goes to stderr (1)
<27>Sep 24 12:09:53 eris-1[668]: This message goes to stderr (2)
<27>Sep 24 12:09:54 eris-1[668]: This message goes to stderr (3)
<27>Sep 24 12:09:55 eris-1[668]: This message goes to stderr (4)
<27>Sep 24 12:09:56 eris-1[668]: This message goes to stderr (5)
<30>Sep 24 12:09:56 eris-1[668]: This message goes to stdout (0)
<30>Sep 24 12:09:56 eris-1[668]: This message goes to stdout (1)
<30>Sep 24 12:09:56 eris-1[668]: This message goes to stdout (2)
<30>Sep 24 12:09:56 eris-1[668]: This message goes to stdout (3)
<30>Sep 24 12:09:56 eris-1[668]: This message goes to stdout (4)
<30>Sep 24 12:09:56 eris-1[668]: This message goes to stdout (5)
<27>Sep 24 12:09:57 eris-1[668]: This message goes to stderr (0)
[...]

Nous voyons les messages sur stderr qui arrivent chaque seconde, ceux sur stdout sont mis en buffer et envoyés uniquement lors du fflush(), c’est un comportement tout-à-fait normal pour des sorties redirigées.

Utilisation de Gdb

Le GNU debugger Gdb est très utile pour suivre finement l’exécution de certaines portions de code, examiner les variables, le contenu de la mémoire ou de la pile, ou analyser le comportement des threads.

On l’emploie habituellement par le biais d’un environnement de développement intégré (IDE) qui offre la possibilité de placer un point d’arrêt d’un clic de souris ou de visualiser le contenu d’une variable en la survolant. Dans l’exemple ci-dessous je vais me contenter d’une utilisation beaucoup plus fruste en ligne de commande pour être facilement répétable et adaptable à votre environnement de travail.

Lorsque nous sélectionnons la case « Remote GDB on container » deux zones de saisie apparaissent :

Fig. 3 – Configuration du debug distant.

La première case sélectionne le slot dans lequel se trouve le container à déboguer. En général on va indiquer un slot vide, et c’est lorsqu’on y insèrera un container que le debug commencera.

La seconde zone indique un port TCP/IP à utiliser pour la connexion au débogueur local.

Le principe de fonctionnement est le suivant.

Un petit agent nommé gdbserver, installé dans l’image Eris Linux est présent sur l’équipement embarqué et écoute sur le port TCP/IP indiqué ci-dessus. Il est capable de piloter l’application se trouvant dans le container placé dans le slot mentionné. gdbserver n’est pas installé dans le container applicatif mais dans Eris Linux lui-même. Lorsqu’un slot est placé en mode Gdb, Eris identifie le processus applicatif correspondant puis y attache gdbserver.

Depuis le PC de développement, on fait tourner le débogueur Gdb complet, qui peut se connecter sur l’agent gdbserver et lui envoyer des commandes.

Point important : sur la machine de développement (PC), on doit utiliser une version de Gdb capable de comprendre les instructions assembleur et le format binaire de l’équipement embarqué. Sur certains systèmes, la version gdb installée ne connaît que le format x86.

Je vous recommande d’installer les packages suivants :

  • sur distribution Debian, Ubuntu ou une distribution WSL basée sur Debian ou Ubuntu : le package gdb-multiarch installé avec la commande sudo apt install gdb-multiarch,
  • sur distribution Fedora, le package gdb standard (installé avec dnf install gdb) est suffisant, il connait les cibles embarquées supportées par Eris.
  • sur distribution Arch, le package gdb standard fonctionne également pour les cibles embarquées. On l’installe avec pacman -S gdb.

J’utiliserai pour la suite des exemples de cet article gdb-multiarch, à remplacer par gdb sur Fedora ou Arch Linux.

Attention : le mode de débogage utilisant Gdb doit rester une fonctionnalité temporaire de développement. Gdbserver ne fournit aucun mécanisme d’authentification. Le port de debug ne doit donc être accessible que depuis un réseau de développement maîtrisé et le mode de débogage doit être désactivé dès que la session de mise au poiunt est achevée.

Quelques instants après avoir rechargé mon container, je vois les deux premières lignes de traces arriver sur socat, c’est normal gdbserver n’est démarré qu’au bout de quelques dixièmes de secondes, pour être sûr que le container a bien lancé l’application.

                    [~]$ socat  -u  UDP-RECV:10001  STDOUT
                    2026/10/06 13:19:25 socat[16746] W address is opened in read-write mode but only supports read-only
                    <30>Oct  6 11:21:06 eris-1[667]: This message goes to stdout (0)
                    <27>Oct  6 11:21:06 eris-1[667]: This message goes to stderr (0)

Dans un autre terminal je démarre gdb-multiarch en me plaçant au préalable dans le répertoire des sources de l’exécutable. Une fois le prompt de gdb affiché, je lui indique que la machine à déboguer se trouve à l’adresse IP de la cible (192.168.3.6 visible sur le Device Manager) et nous attend sur le port TCP/IP 15000.

[eris-linux-containers]$ cd  test-debug-stdio/

[test-debug-stdio)]$ ls
Dockerfile  Makefile  test-debug-stdio.c

[test-debug-stdio]$ gdb-multiarch 
  [...]
(gdb) target  remote  192.168.3.6:15000
Remote debugging using 192.168.3.6:15000
Reading /test-debug-stdio from remote target...
⚠️ warning: File transfers from remote targets can be slow. Use "set sysroot" to access files locally instead.
Reading /test-debug-stdio from remote target...
Reading symbols from target:/test-debug-stdio...
Reading /lib/ld-musl-aarch64.so.1 from remote target...
Reading symbols from target:/lib/ld-musl-aarch64.so.1...
   [...]
0x0000007fa8c0bd04 in ?? () from target:/lib/ld-musl-aarch64.so.1

Nous voyons que gdb récupère le code exécutable depuis la cible. Pour une application plus volumineuse, il sera généralement plus rapide de conserver localement une copie non strippée de l’exécutable et de lancer directement gdb-multiarch ./test-debug-stdio.

gdb nous indique que le programme est suspendu dans une fonction de la bibliothèque C (probablement printf) dont il n’a pas le nom (c’est la signification de ??()). Demandons-lui de placer un point d’arrêt au début de la boucle, puis de s’y rendre.

(gdb) list  main
2	#include <stdio.h>
3	#include <stdlib.h>
4	#include <unistd.h>
5	
6	int main(void)
7	{
8		int i = 0;
9		for (;;) {
10			fprintf(stderr, "This message goes to stderr (%d)\n", i);
11			fprintf(stdout, "This message goes to stdout (%d)\n", i);
(gdb) break  10
Breakpoint 1 at 0x55666808ec: file test-debug-stdio.c, line 10.
(gdb) continue
Continuing.

Breakpoint 1, main () at test-debug-stdio.c:10
10			fprintf(stderr, "This message goes to stderr (%d)\n", i);

Nous sommes arrêtés sur la ligne 10. Nous allons l’exécuter :

(gdb) next

Sur l’autre terminal, nous voyons le message apparaitre :

          <27>Oct  6 11:24:44 eris-1[667]: This message goes to stderr (1)

11			fprintf(stdout, "This message goes to stdout (%d)\n", i);
(gdb) next

Cette fois pas de message, c’est normal, les données écrites sur stdout patientent dans le buffer.

12			sleep(1);
(gdb) next
13			if (++i > 5) {
(gdb) next

Breakpoint 1, main () at test-debug-stdio.c:10
10			fprintf(stderr, "This message goes to stderr (%d)\n", i);

Nous voici revenus au début de la boucle. Nous pouvons refaire un tour complet simplement avec :

(gdb) continue
Continuing.

Breakpoint 1, main () at test-debug-stdio.c:10
10			fprintf(stderr, "This message goes to stderr (%d)\n", i);

Une ligne est bien apparue sur l’autre terminal :

          <27>Oct  6 11:39:51 eris-1[667]: This message goes to stderr (2)

Et nous sommes à nouveau au début de la boucle. Nous pouvons regarder la valeur de la variable i.

(gdb) print  i
$1 = 3

Le $1 correspond au numéro de l’évaluation d’expression. La valeur est bien 3. Modifions-la :

(gdb) set  var  i=300
(gdb) print  i
$2 = 300
(gdb) next

En avançant d’une ligne on voit bien apparaître :

          <27>Oct  6 11:43:00 eris-1[667]: This message goes to stderr (300)

En revanche l’exécution de la ligne suivante est silencieuse, car on n’a pas encore atteint le fflush()

11			fprintf(stdout, "This message goes to stdout (%d)\n", i);
(gdb) next
12			sleep(1);

On peut restaurer la valeur précédente de i :

(gdb) set var i=3

(gdb) continue

En exécutant à quelques reprises continue on voit les traces suivantes :

          <27>Oct  6 11:46:30 eris-1[667]: This message goes to stderr (4)
          <27>Oct  6 11:46:31 eris-1[667]: This message goes to stderr (5)
          <30>Oct  6 11:46:32 eris-1[667]: This message goes to stdout (0)
          <30>Oct  6 11:46:32 eris-1[667]: This message goes to stdout (1)
          <30>Oct  6 11:46:32 eris-1[667]: This message goes to stdout (2)
          <30>Oct  6 11:46:32 eris-1[667]: This message goes to stdout (300)
          <30>Oct  6 11:46:32 eris-1[667]: This message goes to stdout (4)
          <30>Oct  6 11:46:32 eris-1[667]: This message goes to stdout (5)

Nous pouvons bien piloter notre application en pas-à-pas, consulter, et même modifier le contenu de sa mémoire.

Lorsque nous quittons le débogueur, l’application reprend son exécution normale :

(gdb) quit
A debugging session is active.

	Inferior 1 [process 5348] will be detached.

Quit anyway? (y or n) y
Detaching from program: target:/test-debug-stdio, process 5348
Ending remote debugging.
[Inferior 1 (process 5348) detached]
          <27>Oct  6 11:54:43 eris-1[667]: This message goes to stderr (0)
          <27>Oct  6 11:54:44 eris-1[667]: This message goes to stderr (1)
          <27>Oct  6 11:54:45 eris-1[667]: This message goes to stderr (2)
          <27>Oct  6 11:54:46 eris-1[667]: This message goes to stderr (3)
          <27>Oct  6 11:54:47 eris-1[667]: This message goes to stderr (4)
          <27>Oct  6 11:54:48 eris-1[667]: This message goes to stderr (5)
          <27>Oct  6 11:54:49 eris-1[667]: This message goes to stderr (0)
          <30>Oct  6 11:54:49 eris-1[667]: This message goes to stdout (0)
          <30>Oct  6 11:54:49 eris-1[667]: This message goes to stdout (1)
          <30>Oct  6 11:54:49 eris-1[667]: This message goes to stdout (2)
          <30>Oct  6 11:54:49 eris-1[667]: This message goes to stdout (3)
          <30>Oct  6 11:54:49 eris-1[667]: This message goes to stdout (4)
          <30>Oct  6 11:54:49 eris-1[667]: This message goes to stdout (5)
          <27>Oct  6 11:54:50 eris-1[667]: This message goes to stderr (1)
          <27>Oct  6 11:54:51 eris-1[667]: This message goes to stderr (2)
          <27>Oct  6 11:54:52 eris-1[667]: This message goes to stderr (3)
          <27>Oct  6 11:54:53 eris-1[667]: This message goes to stderr (4)
          <27>Oct  6 11:54:54 eris-1[667]: This message goes to stderr (5)

Conclusion

Nous avons vu deux méthodes pour déboguer une application s’exécutant dans un container. Pour l’instant nous nous sommes limités au C/C++, mais d’autres méthodes pour d’autres langages seront disponibles dans l’avenir.

Nous avons déjà remarqué qu’Eris Linux permettait de développer du code applicatif sans nécessiter l’installation de chaîne de cross-compilation. Nous voyons à présent que la même image applicative peut être utilisée normalement ou déboguée à distance sans embarquer d’outils spécifiques de debug dans le container.

Eris Linux est un projet vivant, en cours d’évolution, et vos interrogations, retours et commentaires sont les bienvenus. N’hésitez pas à me contacter si vous avez des questions ou des suggestions d’amélioration.

Précédent :

URL de trackback pour cette page