Comment débugger dans un système standard (POSIX, Unix98 -- standards(7)) ?

Rappel: dans ce qui suit nom(numéro) signifie entrée nom de la section de manuel numéro, consultable avec man numéro nom. Exemple: un socket(7) est un point d'entrée du réseau (signifie: taper man 7 socket)

Notions de processus

Un processus est la base de toute exécution. L'espace d'adressage d'un processus est entièrement séparé de tout autre processus (sauf éventuel partage de page copy-on-write, partage en lecture-exécution p.ex. pour les shared objects (so, bibliothèques partagées), options de clônage ou partage de mémoire manuel).

Un processus peut être créé par duplication puis remplacement d'exécutable (fork(2) puis exec(2)), ou par clônage (p.ex. pour des threads dont partie de l'espace d'adressage peut être partagé) via clone(2).

Tout exécutable UNIX est projeté en mémoire (mmap(2)) et donc lu à la demande. La notion de fichier est essentielle pour cette projection et le partage éventuel entre plusieurs processus pour les so.

Un processus contient des zones de code (lecture seule, exécutable: TEXT), de données (lecture seule, DATA) et de variables (BSS). De plus il contient aussi une pile et un tas (croissant dans 2 direction différentes, avec allocation automatique pour la pile, jusqu'aux limites fixées via ulimit(2) ou manuelle via brk(2) ou plus souvent malloc(3)), ainsi que tous les fichiers projetés en mémoire (y compris les so).

Pour des raisons techniques, la zone kernel est aussi mappée dans l'adressage mais pas de manière accessible en mode utilisateur. Le passage entre mode utilisateur et mode kernel est fait via des appels systèmes (section 2 du manuel); bien souvent ces appels ne sont pas directs mais via des fonctions de la bibliothèque C standard (section 3 du manuel) voire même des utilitaires (sections 1 et 8 du manuel).

Les processus communiquent entre eux par différentes techniques dépendant du degré d'isolation entre eux (IPC, mémoire partagée, fichiers, signaux, etc)

Les signaux sont des événements asynchrones générés par le système (déréférencement d'un pointeur d'une mémoire non mappée SIGSEGV; dépassement de diverses limites configurées via ulimit(2); division par zéro, etc -- voir signal(7) ou kill -l), et/ou par un processus. Certains signaux ont des significations multiples standardisées, comme par exemple SIGHUP pour recharger la configuration d'un service. Les signaux peuvent aussi être utilisés pour le temps réel ou pour une classe particulière de services I/O asynchrones. Tous les signaux peuvent être interceptés par l'application, sauf le signal SIGKILL.

Debugging

Interactif

Il y a plusieurs manières de debugger rapidement:

  • strace permet de lancer un programme en mode traçage des appels systèmes

  • ltrace permet de lancer un programme en mode traçage des appels de la bibliothèque C standard

  • gdb permet de lancer l'exécutable en mode debugging, de configurer des break points, de consulter les données, etc (il existe des surcouches graphiques à gdb).

Ces outils sont basés sur le traçage générique ptrace(2) et ne fonctionnent donc pas, pour des raisons de sécurité, avec les exécutables SUID ou SGID.

Il est aussi possible de consulter l'état d'un processus donné (sous /proc/NUMERO-DE-PROCESSUS). Notamment /proc/PID/maps contient tous les mmap(2)ping mémoires du processus: bibliothèques partagées (shared-object, so), fichiers projetés (mmap(2)), exécutables, heap, stack, etc.

Enfin, en plus de /proc/PID/fd (référence aux fichiers ouverts par ce processus), les commandes lsofs et fuser permettent d'analyser plus en détail les fichiers et/ou sockets ouverts dans le système.

Exemples:
schaefer@reliant:~$ strace -e connect telnet mud.alphanet.ch 4242 > /dev/null
[ ... ]
connect(3, {sa_family=AF_INET, sin_port=htons(4242), sin_addr=inet_addr("80.83.54.2")}, 16) = 0

schaefer@reliant:~$ cd /tmp
schaefer@reliant:/tmp$ mkdir tt
schaefer@reliant:/tmp$ cd tt
schaefer@reliant:/tmp/tt$ cat > bla.c
#include <stdlib.h>
int main(int argc, char **argv) {
   int a = 0;

   return 1/a;
}
CTRL-D

schaefer@reliant:/tmp/tt$ gcc -Wall -g bla.c -o bla
schaefer@reliant:/tmp/tt$ ./bla
Floating point exception

schaefer@reliant:/tmp/tt$ gdb bla   
(gdb) run
Starting program: /tmp/tt/bla 

Program received signal SIGFPE, Arithmetic exception.
0x080483cb in main (argc=1, argv=0xbffff474) at bla.c:5
5          return 1/a;
(gdb) where
#0  0x080483cb in main (argc=1, argv=0xbffff474) at bla.c:5
(gdb) quit

schaefer@reliant:~$ cat
CTRL-Z
schaefer@reliant:~$ jobs -l
[1]+  3632 Stopped                 cat

schaefer@reliant:~$ ls -l /proc/3632/fd
total 0
lrwx------ 1 schaefer schaefer 64 Nov  5 10:20 0 -> /dev/pts/0
lrwx------ 1 schaefer schaefer 64 Nov  5 10:20 1 -> /dev/pts/0
lrwx------ 1 schaefer schaefer 64 Nov  5 10:20 2 -> /dev/pts/0

schaefer@reliant:~$ fg %1
CTRL-C

Non interactif (post-mortem)

Lorsqu'une application termine anormalement (en général en ayant reçu un signal non traité n'étant pas un des signaux de maintenance, voir signal(7)), une image de la mémoire du processus est sauvegardée dans un fichier core. Ce fichier core peut ensuite être analysé avec gdb, dans la mesure où l'exécutable et de préférence ses sources sont également disponibles.

Il faut cependant que la génération du core soit activée via ulimit. Il y a d'autres conditions et cas particuliers décrits dans core(5). De plus, les valeurs ulimit(2) sont configurées par processus et sont héritées. Un script de démarrage est probablement une bonne solution.
schaefer@reliant:/tmp$ cat
^\Quit

schaefer@reliant:/tmp$ ulimit -c unlimited
schaefer@reliant:/tmp$ cat
^\Quit (core dumped)

schaefer@reliant:/tmp$ gdb cat core
Core was generated by `cat'.
Program terminated with signal 3, Quit.
#0  0x00e76416 in __kernel_vsyscall ()
(gdb) where
#0  0x00e76416 in __kernel_vsyscall ()
#1  0x00745e03 in read () from /lib/tls/i686/cmov/libc.so.6
#2  0x0804ce33 in ?? ()
#3  0x0804a21d in ?? ()
#4  0x0069ebd6 in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6
#5  0x08049251 in ?? ()
(ici on a utilisé CTRL-\ pour générer SIGQUIT; on voit que les symboles ne sont pas tous disponibles -- il y a de nombreuses autres séquences de contrôle qui sont reconnus par les terminaux interactifs de contrôle pour envoyer des signaux divers et variés aux processus)

Le debugging post-mortem ne fonctionne bien que si les symboles sont présents:
schaefer@reliant:/tmp$ file $(which cat)
/bin/cat: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, stripped

schaefer@reliant:/tmp$ nm $(which cat)
nm: /bin/cat: no symbols

schaefer@reliant:~$ cd /tmp/tt
schaefer@reliant:/tmp/tt$ file bla
bla: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped
schaefer@reliant:/tmp/tt$ ulimit -c unlimited
schaefer@reliant:/tmp/tt$ ls
bla  bla.c
schaefer@reliant:/tmp/tt$ ./bla 
Floating point exception (core dumped)
schaefer@reliant:/tmp/tt$ ls -lah core
-rw------- 1 schaefer schaefer 196K Nov  5 10:28 core
schaefer@reliant:/tmp/tt$ file core
core: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from './bla'
schaefer@reliant:/tmp/tt$ gdb bla core
Core was generated by `./bla'.
Program terminated with signal 8, Arithmetic exception.
#0  0x080483cb in main (argc=1, argv=0xbff024c4) at bla.c:5
5          return 1/a;
(gdb) 

On voit ci-dessus que dans le premier cas l'exécutable a été strip(1)pé (le recompiler avec l'option -g de gcc(1) et surtout ne pas lancer strip(1) dessus). Dans le deuxième cas c'est bon.

Debugging avancé

De nombreuses options peuvent être configurés dans la libc(7) pour debugger des problèmes comme la gestion de la mémoire, ou dans le dynamic linker (ld.so(8)). Des outils comme matrace(1) ou valgrind(1) permettent de debugger ces problèmes plus en détail.

Journaux

Il est d'usage que les applications produisent des entrées de log (p.ex. via syslog(3)), qui sont ensuite centralisées et triées par syslogd(8) de manière locale ou centralisée. Des outils comme logcheck(8) permettent d'analyser ces fichiers et de générer en cas de nécessité des alarmes. Enfin, logrotate(8) archive et/ou efface les anciens journaux.

On peut aussi renoncer au passage par syslogd(8) et créer des fichiers locaux, mais il faut aussi les archiver et considérer la problématique de leur accès (sécurité).

Références

-- MarcSCHAEFER - 05 Nov 2011
Topic revision: r2 - 07 Nov 2011, MarcSCHAEFER
 

Copyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding Foswiki? Send feedback