Debugging d'une application
Technique de debugging
Il existe une multitude de chemins pour debugger une application:
- Try and Error
- Revue de code (code walk through)
- Traçage (Tracing:
printf
,syslog
, 7-segment, …) - Debugging à l’aide d’un debugger
- Ligne de commande, par exemple GDB (GNU Debugger)
- Graphique, par exemple DDD (Data Display Debugger) ou VSCode
Il est important de choisir le bon outil, en fonction du problème à analyser. Le traçage est probablement le plus simple et le plus facile à mettre en œuvre. Les debuggers apportent un grand confort lors du développement, mais il faut savoir qu’ils peuvent perturber le comportement du code et qu’il est souvent difficile de les utiliser lorsque les cibles ont été déployées chez les clients.
Tracing
Technique très simple, il suffit d’ajouter des printf
à l’intérieur du code aux
endroits sensibles. Il permet d’afficher l’état de variables (.../fibonacci
)
int value = 100;
if (/*condition*/) {
value++;
printf ("if statement\n");
} else {
printf ("else statement\n");
}
printf ("Value decimal:%d, hex:0x%x\n", value, value);
Cette technique peut être amélioré avec une compilation conditionnelle (.../tracing
)
#ifdef DEBUG
#define TRACE(x) printf x
#else
#define TRACE(X)
#endif
TRACE(("Value decimal:%d, hex:0x%x\n", value, value));
D’autres améliorations sont encore possible: log-file, exécution conditionnelle, syslog, etc.
Debugging avec GDB
Le debugger GNU, généralement appelée simplement GDB, est le debugger standard pour le système GNU, et par conséquent pour Linux. Il s’agit d’un debugger portable qui fonctionne sur de nombreux systèmes similaires à Unix et fonctionne pour de nombreux langages de programmation, tel qu’Ada, C/C++, FreeBasic ou Fortran.
Démarrer le programme avec GDB
gdb <program_with_symbols>
Ce type de debugging peut être exécuté indifféremment sur la cible ou sur la machine hôte.
Note
le programme doit être compilé avec l’option de debugging (gcc -g
).
Debugging avec GDB - quelques commandes
Commandes de base:
list - show the program code
break - set breakpoint (to address or source-line no)
del break - delete a breakpoint
run - start program
continue - continue execution after breakpoint
step - step (into) instruction
next - step over instruction
print - show variable contents
x/i - display memory content as instruction
bt - backtrace: show execution stack
frame - select a stack frame
quit - leave the debugger
Quelques guides :
Debugging à distance avec GDB / GDB Server
GDB propose un mode distance (remote), utilisé principalement lors de debugging de systèmes embarqués. On parle de debugging à distance, lorsque GDB fonctionne sur la machine hôte et le programme étant debuggé sur la cible. Dans ce cas, GDB communiquera avec la cible soit par une interface série, soit par une interface Ethernet/IP via le protocole série ou TCP/IP.
Démarrer une session de debugging à distance est également simple
-
Sur la machine cible
gdbserver <[hostname]:port> <program_without_symbols>
-
Sur la machine hôte
<path>/<target>-gdb <program_with_symbols> > set sysroot <shared_library_path> > target remote <targetname:port> > break main > continue
Pour le NanoPi NEO Plus2
- path (toolchain) →
/buildroot/output/host/usr/bin
- target →
aarch64-buildroot-linux-gnu
- shared_library_path →
/buildroot/output/staging
Debugging avec DDD
Étant donné que GDB ne dispose pas d’interface graphique telle que les IDE, l’utilisation d’un front-end externe est utile. Il en existe beaucoup. DDD en est un, simple et pratique.
Appeler DDD:
ddd <program_with_symbols>
Au démarrage, DDD affiche plusieurs fenêtres différentes, pa exemple :
- Data window
- Source window
- GDB console
Les commandes GDB peuvent être invoquées soit en pressant sur les boutons dans la fenêtre de commandes, soit en tapant la commande l’intérieur de la console GDB.
Debugging à distance avec DDD
Le debugging à distance avec DDD est réalisé de manière similaire au debugging à distance avec GDB utilisant le GDB Server sur la cible.
Démarrer une session de debugging à distance est également simple :
-
Sur la cible
gdbserver <[hostname]:port> <program_without_symbols>
-
Sur la machine hôte
ddd --debugger <path>/<target>-gdb <program_with_symbols> > set sysroot <shared library path> > target remote <targetname:port> > break main > continue
Pour le NanoPi NEO Plus2
- path (toolchain) →
/buildroot/output/host/usr/bin
- target →
aarch64-buildroot-linux-gnu
- shared_library_path →
/buildroot/output/staging
Debugging avec Eclipse
Eclipse est un environnement de développement intégré Il propose toute une série d’outils très puissants pour le développement d’applications. Il offre entre autres une interface graphique très performante pour le debugging d’applications native et distante.
Debugging avec VS-Code
VS-Code est un éditeur très puissant. Il propose toute une série d’extensions pour le développement de logiciels. Dans ce catalogue, l’extension «C/C++» offre des services pour le debugging d’applications à distance via GDB, laquelle s’intègre avec le GUI de VS-Code.
Configuration de l’infrastructure de debugging
Core dumps
Un core dump est un fichier contenant une image de la mémoire du processus à l’instant du crash. Il est généré par le noyau Linux lorsque l’exécution d’un programme lève un signal pour lequel aucune routine de traitement n’a préalablement été enregistrée.
Le core dump est enregistré dans le répertoire et sous le nom de fichier
spécifié dans /proc/sys/kernel/core_pattern
, généralement
core.<pid>
La création de core dumps doit être autorisée. Pour cela, il suffit d’utiliser la commande :
ulimit -c <size|unlimited>
Les core dumps sont un désastre pour les applications, mais ils peuvent être une bénédiction dans la recherche du problème pour le développeur. Il suffit d’appeler GDB pour debugger un core dump :
-
Sur la cible:
gdb <program-name> <core-file> > bt (backtrace)
-
Sur la machine hôte:
sudo <path><target>-gdb <program-name> <core-file> > bt (backtrace)
Appel du programme (.../core_dumps
)
# ./app
Segmentation fault (core dumped)
Informations fournies par le debugger
$ sudo /buildroot/output/host/usr/bin/aarch64-buildroot-linux-gnu-gdb app core
...
Core was generated by `./app'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000400534 in access_data () at core_dumps.c:31
31 *p=10;
(gdb) bt
#0 0x0000000000400534 in access_data () at core_dumps.c:31
#1 0x000000000040056c in call (n=0) at core_dumps.c:37
#2 0x0000000000400568 in call (n=1) at core_dumps.c:36
#3 0x0000000000400568 in call (n=2) at core_dumps.c:36
#4 0x0000000000400568 in call (n=3) at core_dumps.c:36
#5 0x0000000000400568 in call (n=4) at core_dumps.c:36
#6 0x0000000000400568 in call (n=5) at core_dumps.c:36
#7 0x0000000000400568 in call (n=6) at core_dumps.c:36
#8 0x0000000000400568 in call (n=7) at core_dumps.c:36
#9 0x0000000000400568 in call (n=8) at core_dumps.c:36
#10 0x0000000000400568 in call (n=9) at core_dumps.c:36
#11 0x0000000000400568 in call (n=10) at core_dumps.c:36
#12 0x00
Backtrace
Un backtrace est une impression de la pile d’exécution d’un thread ou processus.
Il peut être initié à tout moment en appelant les fonctions
backtrace(...)
et backtrace_symbols_fd(...)
.
Ces fonctions seront généralement incluses dans la routine de traitement des erreurs de segmentation.
Lors de l’impression de la pile d’exécution, les adresses absolues correspondant
aux de lignes de code des fonctions appelées sont affichées. Pour convertir ces
adresses en nom des fichiers sources et numéros de lignes, il suffit d’appeler
l’outil addr2line
:
addr2line -e <name-of-executable-with-debug-symbols> <absolute-address>
Note
Ces fonctions ne sont pas disponibles dans toutes les bibliothèques, par exemple µClib
L’exemple de backtrace affiche les infos suivantes lors du crash (.../backtrace
)
# ./app
backtrace() returned 17 addresses
./app[0x40070c]
linux-vdso.so.1(__kernel_rt_sigreturn+0x0)[0xffffb43f9578]
./app[0x400748]
./app[0x400780]
./app[0x40077c]
./app[0x40077c]
./app[0x40077c]
./app[0x40077c]
./app[0x40077c]
./app[0x40077c]
./app[0x40077c]
./app[0x40077c]
./app[0x40077c]
./app[0x40077c]
./app[0x4007ac]
/lib64/libc.so.6(__libc_start_main+0xe4)[0xffffb4281fac]
./app[0x40062c]
#
Pour obtenir le nom du fichier et la ligne de code correspondant à une adresse, il suffit d’appeler l’outil addr2line comme suit :
aarch64-none-linux-gnu-addr2line -e app 0x400704
/workspace/src/01_environment/backtrace/main.c:37
aarch64-none-linux-gnu-addr2line -e app 0x400740
/workspace/src/01_environment/backtrace/main.c:46
aarch64-none-linux-gnu-addr2line -e app 0x400774
/workspace/src/01_environment/backtrace/main.c:52
System calls
strace
est un outil pour tracer les appels système du noyau Linux.
Pour obtenir la liste des appels, il suffit de lancer l’application sous le contrôle
de strace
:
strace <program-name>
Un appel typique peut donner : (../system_calls
)
# strace ./app_a
execve("./app_a", ["./app_a"], [/* 17 vars */]) = 0
...
open("/sys/class/thermal/thermal_zone0/temp", O_RDONLY) = 3
read(3, "46000\n", 50) = 6
close(3) = 0
...
+++ exited with 0 +++
#
Étant donné que les informations affichées peuvent être très nombreuses, il est parfois bon de les limiter en spécifiant un ensemble des appels système intéressants:
strace -e trace=<set of calls> <program>
strace -e trace=openat,close,mmap,munmap ./app_a
Pour plus de détails, voir man strace
ou http://en.wikipedia.org/wiki/strace
Memory leaks
valgrind est un outil pour debugger, effectuer du profilage de code et mettre en évidence des fuites mémoires.
Pour analyser le code, il suffit de lancer le programme sous le contrôle de valgrind :
valgrind --leak-check=full <program-name>
Pour plus de détails, voir «man valgrind» ou la homepage valgrind ou http://en.wikipedia.org/wiki/Valgrind
Un appel typique peut donner: (.../memory_leaks
)
# valgrind --leak-check=full ./app
==291== Memcheck, a memory error detector
==291== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==291== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==291== Command: ./app
==291==
==291==
==291== HEAP SUMMARY:
==291== in use at exit: 63,760 bytes in 3,985 blocks
==291== total heap usage: 4,000 allocs, 15 frees, 64,000 bytes allocated
==291==
==291== 63,760 (16 direct, 63,744 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 3
==291== at 0x4848400: malloc (in /usr/lib/valgrind/vgpreload_memcheck-arm64-linux.so)
==291== by 0x4006B7: alloc2 (mem_leaks.c:65)
==291== by 0x40077F: alloc (mem_leaks.c:82)
==291== by 0x40081B: main (mem_leaks.c:98)
==291==
==291== LEAK SUMMARY:
==291== definitely lost: 16 bytes in 1 blocks
==291== indirectly lost: 63,744 bytes in 3,984 blocks
==291== possibly lost: 0 bytes in 0 blocks
==291== still reachable: 0 bytes in 0 blocks
==291== suppressed: 0 bytes in 0 blocks
==291==
==291== For lists of detected and suppressed errors, rerun with: -s
==291== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
#
LTTng
LTTng (Linux Trace Toolkit - next generation) est un outil moderne pour la corrélation de traces prises dans le noyau et/ou l’espace utilisateur.
LTTng utilise la même base de temps pour le traçage d’événements en espace utilisateur et en espace noyau.
LTTng est intégré dans le noyau Linux depuis la version 2.6.38
LTTng est intégré dans l’IDE Eclipse depuis la version Juno
Plus de détails sous https://lttng.org
PROC file system
Le noyau Linux fournit énormément d’informations sur le système et son
comportement par l’intermédiaire du proc file system /proc/<...>
Par exemple /proc/cpuinfo
, /proc/meminfo
, /proc/1/maps
Pour obtenir l’information, il suffit d’utiliser la commande cat
# cat /proc/1/maps
00400000-004b3000 r-xp 00000000 b3:02 16 /bin/busybox
004c2000-004c3000 r--p 000b2000 b3:02 16 /bin/busybox
004c3000-004c4000 rw-p 000b3000 b3:02 16 /bin/busybox
004c4000-004c5000 rw-p 00000000 00:00 0
2e127000-2e148000 rw-p 00000000 00:00 0 [heap]
ffff96afb000-ffff96c4c000 r-xp 00000000 b3:02 192 /lib/libc-2.30.so
ffff96c4c000-ffff96c5b000 ---p 00151000 b3:02 192 /lib/libc-2.30.so
ffff96c5b000-ffff96c5f000 r--p 00150000 b3:02 192 /lib/libc-2.30.so
ffff96c5f000-ffff96c61000 rw-p 00154000 b3:02 192 /lib/libc-2.30.so
ffff96c61000-ffff96c65000 rw-p 00000000 00:00 0
ffff96c65000-ffff96c77000 r-xp 00000000 b3:02 215 /lib/libresolv-2.30.so
ffff96c77000-ffff96c87000 ---p 00012000 b3:02 215 /lib/libresolv-2.30.so
ffff96c87000-ffff96c88000 r--p 00012000 b3:02 215 /lib/libresolv-2.30.so
ffff96c88000-ffff96c89000 rw-p 00013000 b3:02 215 /lib/libresolv-2.30.so
ffff96c89000-ffff96c8b000 rw-p 00000000 00:00 0
ffff96c8b000-ffff96caa000 r-xp 00000000 b3:02 186 /lib/ld-2.30.so
ffff96cb4000-ffff96cb8000 rw-p 00000000 00:00 0
ffff96cb8000-ffff96cb9000 r--p 00000000 00:00 0 [vvar]
ffff96cb9000-ffff96cba000 r-xp 00000000 00:00 0 [vdso]
ffff96cba000-ffff96cbb000 r--p 0001f000 b3:02 186 /lib/ld-2.30.so
ffff96cbb000-ffff96cbd000 rw-p 00020000 b3:02 186 /lib/ld-2.30.so
ffffdd2e9000-ffffdd30a000 rw-p 00000000 00:00 0 [stack]