Les programmeurs qui développent en C ou C++ ont tous passé de longs moments à débugger leurs programmes. Cet article présente les principaux outils disponibles sous Linux pour les aider dans cette tâche fastidieuse. En plus des outils "classiques" comme GNU gdb ou DDD, des outils plus spécialisés comme Electric Fence sont également présentés. Bien que cet article s'adresse en priorité aux programmeurs débutants, les vieux routards pouront également y trouver leur bonheur.
GNU gdb
GNU gdb est LE programme de débuggage sous Linux (ainsi que sur bon nombre d'autres plate-formes). En dépis de son interface en mode texte, bien qu'il existe des surcouches graphiques, comme DDD ou KDevelop que nous verrons plus loin, il est extrêmement puissant et permet de venir à bout des bugs les plus corriaces et les plus incrustés dans vos programmes.
Présentation générale :
Un débugueur comme gdb permet de visualiser ce qui se passe dans
votre programme, soit au cours de son exécution, soit au moment
où il a crashé dans le cas où gdb est utilisé
pour faire une analyse post-mortem (nous reviendrons sur ce terme plus
loin).
gdb permet quatre principaux types d'actions pour déceler les
problèmes :
- Lancer un programme en spécifiant les paramètres appropriés.
- Demander au programme de s'arrêter dans des circonstances bien
précises.
- Examiner ce qui s'est passé, une fois le programme terminé.
- Changer l'état d'exécution du programme (comme la valeur
d'une variable) pour corriger les résultats.
La version actuelle de gdb (5.0) permet de débugger les programmes
écrits en C, C++, Modula-2, Chill (un "dialecte" du LISP), Fortran
et Java. Il existe également des patchs pour supporter l'ADA et
l'Objective-C.
Notons au passage que pour être pleinement exploitable par gdb,
un programme doit être compilé avec l'option '-g', voire '-g3';
d'autre part les optimisations (-O/-O2/etc) doivent être désactivées
dans la mesure du possible.
Pour lancer gdb, il suffit de tapper 'gdb' au prompt shell, mais le plus souvent on lance gdb avec quelques paramètres : soit 'gdb ./monprog' pour spécifier directement le programme cible (ce qui évite de taper 'file ./monprog' dès l'apparition du prompt gdb), soit 'gdb ./monprog core' pour faire une analyse post-mortem à partir d'un fichier core. Il est également possible d'utiliser gdb sur un programme qui est déjà en cours d'exécution : 'gdb ./monprog 2345' pour attacher gdb au processus dont le PID est 2345. Attention, gdb recherche les fichiers core avant les PID, dans ce dernier cas, vous ne devez pas avoir de fichier core ayant le même nom que le PID du processus dans le répertoire courant.
Voici la liste des commandes gdb les plus utilisées :
break [fichier_source:]fonction
Met un point d'arrêt au début de la
fonction spécifiée. Lorsque l'exécution du programme
rencontre un de ces points d'arrêt, le programme est stoppé
et le programmeur peut entrer d'autres commandes au prompt du débogueur.
L'exécution pourra ensuite être reprise grâce à
la commande 'cont'. A la place d'un nom de fonction, on peut directement
passer un numéro de ligne dans le code source à la commande
'break'.
condition numéro test
Permet de poser une condition sur un point d'arrêt
précédement défini par la commande 'break'.
Par exemple, 'condition 1 i==10' n'activera le point d'arrêt
numéro 1 que dans le cas où la variable i vaut 10.
run [liste de paramètres]
Démarre l'exécution du programme avec
les éventuels paramètres spécifiés.
bt
Backtrace : demande à gdb d'afficher le contenu
de la pile du programme. Synonyme de la commande 'where'.
print expression
Affiche la valeur d'une expression. En particulier,
cette commande peut être utilisée pour afficher la valeur
d'une variable.
cont
Demande la reprise de l'exécution d'un programme
stoppé.
next
Exécute la ligne suivante dans un programme
suspendu. Si cette ligne contient des appels de fonctions, elles
seront exécutées sans être elles-mêmes tracées.
nexti
Idem que la commande 'next', mais le pas
de progression n'est plus la ligne de programme, mais l'opcode assembleur
(commande à réserver pour les cas les plus désespérés).
set [option] [paramètres]
Cette option permet de changer la plus grande partie
des paramètres de gdb ; parmi les principales options disponibles
:
environment : permet de changer une variable
d'environnement du programme exécuté.
language : permet de changer le langage
source par défaut.
variable : permet de changer à la
volée le contenu d'une variable.
step
Idem que la commande 'next', mais les éventuelles
fonctions appelées seront elles aussi tracées.
stepi
Idem que la commande 'step', mais le pas
de progression n'est plus la ligne de programme, mais l'opcode assembleur.
help [nom]
Donne des informations sur la commande donnée
en paramètre, ou une page d'aide générique si aucun
paramètre n'est mentionné.
quit
Quitte gdb.
Il est important de noter que la ligne de commande de gdb utilise la
bibliothèque GNU readline et que par conséquent tous les
raccourcis clavier courrants (^A, ^E, ^K, etc...) ainsi que le mécanisme
d'historique (flèches curseur, ^R) sont disponibles.
En cas de gros problème, voire de doute sur le code généré
par le compilateur, gdb permet également de désassembler
le code d'une fonction. Bien évidemment, il est rare d'en arriver
à une telle extrémité ;-)
(gdb) disassemble Double
Dump of assembler code for function Double:
0x8048480 <Double>: push
%ebp
0x8048481 <Double+1>: mov %esp,%ebp
0x8048483 <Double+3>: mov 0x8(%ebp),%eax
0x8048486 <Double+6>: add %eax,%eax
0x8048488 <Double+8>: leave
0x8048489 <Double+9>: ret
End of assembler dump.
DDD & KDevelop
DDD et KDevelop sont les 2 interfaces graphiques les plus pratiques de gdb. Par rapport à KDevelop, DDD fait figure d'ancétre, mais celui-ci est très convivial, donc très simple à utiliser et peu gourmand en ressources.
DDD : Fenêtre principale
Cependant, DDD ne reste qu'une interface graphique au dessus de gdb,
ce qui pourra laisser les plus exigeants sur leur faim. Heureusement, KDevelop
comble un manque important : c'est un véritable environnement de
développement intégrant un éditeur avec colorisation
du code, un système de gestion de projets et enfin une interface
de débugage.
Bien que KDevelop soit une application KDE, elle fonctionne très
bien avec simplement les bibliothèques Qt et KDElibs, c'est à
dire que vous n'aurez pas toutes les applications KDE à installer
si vous désirez utiliser KDevelop; seules les bibliothèques
de base sont suffisantes. D'autre part, KDevelop permet sans aucun problème
de développer et débugguer des applications GNOME ;-)
Bref, si la ligne de commande de gdb vous rebute, si vous ne maîtrisez
pas totalement Emacs et que vous ne supportez pas vi, alors KDevelop
est certainement fait pour vous. KDevelop intéressera également
les développeurs qui découvrent le monde Linux alors qu'ils
étaient habitués aux environnements de développement
intégrés du monde Windows.
Les problèmes de plomberie
Vous avez déjà probablement eu a retrouver des "fuites" de mémoires, c'est-à-dire à trouver des zones mémoires qui ont été allouées, mais qui ne sont pas désallouées par la suite. Dans cette partie, nous allons faire un tour d'horizon des divers outils disponibles sous Linux qui peuvent aider dans cette recherche.
Vous savez déjà probablement qu'une zone de mémoire allouée est automatiquement restituée au système d'exploitation lors de la terminaison du processus. Dans ce cas, pourquoi s'obstiner à rechercher ces fuites ? Celles qui sont situées dans les programmes qui n'allouent dynamiquement que peu de mémoire et dont le temps de fonctionnement est très cours (comme par exemple, les commandes ls ou ps) ne prettent pas à conséquences bien que ce soit un style de programmation relativement peu esthétique. Par contre, si des fuites surviennent dans des programmes qui utilisent beaucoup de mémoire ou qui sont sensés tourner pendant longtemps, alors ce problème devient réelement génant. Lequel d'entre vous n'a pas déjà eu à redémarrer Netscape ou son serveur X (voire les deux) parce qu'il occupait plus de 100 Mo en mémoire ?
Les fuites ne sont pas les seuls problèmes liés à
l'allocation mémoire. Les langages C et C++ n'effectuant pas de
tests pour vérifier si l'on accéde bien toujours à
des zones mémoires correctes, qu'elles aient été allouées
dynamiquement (par un appel à la fonction malloc()) ou
non (tableau statique). Dans ce cas, cela se terminera dans la plupart
des cas par un résultat erroné ou par une erreur de segmentation
(voire l'un suivi de l'autre).
Heureusement, il existe sous Linux quelques outils qui permettent de
grandement simplifier la chasse aux bugs.
Electric Fence
Electric Fence (litéralement "cloture électrique") est
un outil très simple, mais il permet néanmoins d'intercepter
un grand nombre de problèmes grâce à un minimum d'efforts.
Plus particulièrement, Electric Fence rend possible la détection
des erreurs suivantes :
- accès en dehors des zones allouées par la fonction
malloc()
("buffer overruns" et "buffer underruns")
- accès à une zone mémoire retournée au
système par un appel à la fonction free().
- détection des problèmes d'alignement.
A la différence des autres outils similaires, Electric Fence
détecte non seulement les tentatives d'écritures en dehors
des zones allouées, mais également les tentatives de lecture.
C'est, en grande partie, ce qui fait l'intérêt de cet outil.
Electric Fence se présente sous la forme d'une bibliothèque
avec laquelle vous devrez "linker" votre programme. Vous pouvez
choisir d'utiliser la bibliothèque libefence explicitement lors
de l'édition de liens finale en ajoutant l'option '-lefence'
lors de l'appel à la commande gcc, cependant il est beaucoup plus
pratique de forcer l'édition de liens au moment de l'exécution
grâce à la variable d'environnement
LD_PRELOAD. Cela
peut se faire simplement à partir de la ligne de commande :
# LD_PRELOAD=libefence.so.0.0 ./monprog
ou bien depuis gdb grace à la directive 'set environment
LD_PRELOAD=libefence.so.0.0'.
Voyons concrétement avec un exemple comment s'utilise Electric
Fence. Considérons le petit programme suivant :
|
#include <stdio.h>
#include <errno.h> int main (int argc, char **argv) {
if (! (p = (int *) malloc(10*sizeof(int)))) {
|
(Les lecteurs attentifs auront certainement déjà vu qu'il y à une erreur dans le code, mais merci de ne rien dire pour le moment afin de ne pas me gacher mon effet de surprise que je réserve pour plus tard ;-)
On compile ce programme grâce à la commande :
$ gcc -g eftest1.c -o eftest1
puis on l'exécute :
$ ./eftest1
Fin du test
$
Tout s'est donc passé comme prévu ; du moins en apparence,
car cela ne veut pas dire que le programme fonctionne correctement. Pour
nous en convaincre relançons ce même programme, mais cette
fois en utilisant Electric Fence :
$ LD_PRELOAD=libefence.so.0.0 ./eftest1
Electric Fence 2.0.5 Copyright (C) 1987-1998 Bruce Perens.
Segmentation fault.
$
On note au passage qu'Electric Fence a bien été trouvé comme l'indique le message (Ne vous laissez pas impressionner par le fait que la dernière version d'Electric Fence date de 1998; cela est tout simplement dû au fait que cet utilitaire remplit parfaitement son rôle et qu'aucun bug n'y a été découvert depuis cette date). Mais on voit surtout que le programme a planté avant la fin à cause d'une erreur de segmentation. Pour y voir plus clair revoyons la scène avec gdb (à défaut de ralenti).
$ gdb -q ./eftest1
(gdb) set environment LD_PRELOAD=libefence.so.0.0
(gdb) run
Starting program: /home/vincent/articles/./eftest1
Electric Fence 2.0.5 Copyright (C) 1987-1998 Bruce Perens.
Program received signal SIGSEGV, Segmentation fault.
0x8048520 in main (argc=1, argv=0xbffffd44) at eftest1.c:12
12 p[i]=i;
(gdb) print i
$1 = 10
A ce stade, on en sait déjà un peu plus. D'une part, on sait précisément quelle ligne du programme a provoquée l'erreur ; d'autre part, on a pu déterminer que la variable i valait 10 au moment du plantage. A partir de là, l'erreur est évidente : l'appel de malloc() a alloué un segment de 10 mots machine, alors que la boucle for essaye d'en utiliser 11 (de 0 à 10, cela fait bien 11, pas 10).
Pour cet exemple, on a utilisé le mode de fonctionnement par
défaut de Electric Fence, à savoir les problèmes de
dépassements par le haut (buffer overruns). Du fait de sa conception,
Electric Fence ne permet pas de détecter en une seule exécution
les dépassements par le haut et par le bas.
En fait Electric Fence utilise le hardware gérant la mémoire
virtuelle (la MMU) pour placer une page inaccessible après
(ou avant selon l'option utilisée) chaque plage de mémoire
allouée. De la sorte, lorsque le programme tente de lire ou d'écrire
cette page inaccessible, une erreur de segmentation est déclenchée.
A partir de ce moment, il est trival de trouver l'instruction fautive grâce
à gdb, comme l'a montré l'exemple précédent.
De même une zone mémoire qui a été libérée
par un appel de la fonction free() sera rendue inaccessible, de sorte qu'une
tentative d'accès après libération provoque un plantage
du programme.
En ce qui concerne son utilisation, Electric Fence peut être
utilisé pour simplement générer un fichier core
qui sera utilisé pour une analyse post-mortem; cette méthode
est cependant déconseillée, car du fait qu'Electric Fence
alloue systèmatiquement 2 pages mémoire (soit 8 Ko sur les
architectures i386), le programme débugué nécessitera
une quantité de mémoire considérablement plus importante.
|
A titre de référence, voici la liste des variables d'environnement succeptibles d'être utilisées pour modifier le comportement d'Electric Fence :
EF_DISABLE_BANNER
Si cette variable a une valeur différente
de zéro, alors Electric Fence n'affichera pas de message lors de
son démarrage. L'utilisation de cette fonctionnalité est
extrêmement déconseillée si vous linkez votre programme
statiquement, car vous risquer d'oublier d'enlever Electric Fence dans
la version en production de votre programme, avec les problèmes
de performances que cela comporte.
EF_PROTECT_BELOW
Par défaut (comme le montre la figure 1 qui
ne devrait pas être bien loin si elle ne s'est pas perdue au département
PAO), Electric Fence bloque l'accès à la page qui suit la
zone allouée. Par conséquent si l'on tente d'accéder
en dehors de la zone allouée par le bas, le problème ne sera
pas détecté. Pour palier à ce problème, lorsque
la variable EF_PROTECT_BELOW a une valeur non nulle, le mécanisme
inverse est utilisé, c'est-à-dire que c'est la première
page qui est bloquée, et la zone mémoire allouée est
placée au début de la deuxième page (voir la Figure
2).
|
EF_PROTECT_FREE
Si cette variable est positionnée, Electric
Fence ne retournera pas la mémoire désallouée avec
free() au système, mais au contraire, il la rendra totalement inaccessible
au programme. Cette fonctionnalité est très utile si vous
suspectez que le programme que vous tentez de débugger essaye d'accéder
à des zones mémoire précédemment désallouées.
Attention, cette fonctionnalité décuple les besoins en mémoire
de l'application, vu que la mémoire allouée est rendue inutilisable.
EF_ALLOW_MALLOC_0
Par défaut, Electric Fence intercepte les
appels à la fonction malloc() avec une taille nulle, car ils sont
le plus souvent dus à un bug (quel pourrait bien être l'intérêt
d'allouer une zone de taille nulle?!). Cependant cette fonctionnalité
peut être désactivée en donnant une valeur non-nulle
à la variable d'environnement EF_ALLOW_MALLOC_0.
EF_ALIGNMENT
Ce paramètre permet de changer la valeur
de l'alignement des zones mémoire retournées par Electric
Fence. En quoi est-ce important? Tout simplement parce que par défault,
les tailles des zones mémoires sont des multiples de la taille des
mots mémoire, soit 4 octets (32 bits) sur la plupart des architectures
ou 8 octets (64 bits) sur les Alpha et UltraSparc. Etant donné que
chaque zone mémoire est un multiple de 4 octets, un dépassement
de moins de 3 octets ne sera pas intercepté par Electric Fence.
Pour vous convaincre (si si, je vois bien à votre air dubitatif
que vous n'étes pas totalement convaincu), modifions l'exemple eftest1.c
précédent de la manière suivante :
|
#include <stdio.h>
#include <errno.h> int main (int argc, char **argv) {
if (! (p = (char *) malloc(10*sizeof(char))))
{
|
Comme vous pouvez le voir, la seule modification a consisté à
remplacer le tableau d'"int" par un tableau de "char".
Maintenant, compilons et exécutons ce programme :
# gdb -q ./eftest2
(gdb) set environment LD_PRELOAD=libefence.so.0.0
(gdb) run
Starting program: /home/vincent/articles/./eftest2
Electric Fence 2.0.5 Copyright (C) 1987-1998 Bruce Perens.
Fin du test
Program exited normally.
Surprise... Cette fois l'erreur de dépassement n'a pas été
trouvée par Electric Fence, car lorsque l'allocation de 10 octets
a été demandé, c'est en fait un nombre entier de mots
machine de 4 octets qui ont été alloués (3 dans notre
exemple), ce qui nous fait 12 caractères. Le code de l'exemple n'accédant
qu'aux 11 premiers, aucune erreur n'a été détectée.
Par contre, si l'on tente d'accéder au delà de ces 12 octets,
alors l'erreur sera interceptée; vous pouvez le tester par vous-même
en remplaçant le "10" de la boucle "for" par "12".
Alors comment faire pour détecter les problèmes d'allocation
mémoire dont la taille est inférieure à la taille
d'un mot machine? Heureusement, Electric Fence fourni un mécanisme
pour contourner ce problème : la variable EF_ALIGNMENT
peut être utilisée pour définir sur combien d'octets
l'alignement des zones allouées sera fait. Pour détecter
les dépassements d'un seul octet, il suffit donc de positionner
cette variable à 1. Pourquoi n'est ce pas fait par défaut
? Tout simplement à cause du fait que la fonction malloc()
est sensée retourner une zone mémoire alignée sur
un mot machine et que tous les processeurs ne sont pas capables d'accéder
à des données qui ne respectent pas scrupuleusement cette
contrainte d'alignement (les processeurs Alpha par exemple).
(gdb) set environment EF_ALIGNMENT=1
(gdb) run
Starting program: /home/vincent/articles/./eftest2
Electric Fence 2.0.5 Copyright (C) 1987-1998 Bruce Perens.
Program received signal SIGSEGV, Segmentation fault.
0x804857c in main (argc=1, argv=0xbffffcc4) at eftest2.c:13
13
p[i]=i;
(gdb)
Cette fois, en enlevant la contrainte d'alignement, le problème
est bien intercepté.
|
Lorsqu'Electric Fence est utilisé pour débugger un programme,
les besoins en mémoire de celui-ci sont décuplés,
et il se peut que vous atteignez rapidement les limites de la mémoire
virtuelle de votre machine. Si vous rencontrez ce problème, il est
facile d'y remédier temporairement grâce à un fichier
de swap supplémentaire (oui : un fichier, pas une partition)
Supposons que vous estimiez à 80 Mo votre besoin de mémoire supplémentaire. Il vous suffit de : 1 - Créer un fichier vide de 80 Mo : # dd if=/dev/zero of=/tmp/extraswap count=80 bs=1M 80+0 records in 80+0 records out 2 - Le "formatter" avec la commande mkswap : # mkswap /tmp/extraswap Setting up swapspace version 1, size = 83881984 bytes 3 - Monter le fichier créé : # swapon /tmp/extraswap A ce stade, vous pouvez utiliser la commande 'free' pour vérifier
que la taille de votre espace de swap a bien augmenté de 80 Mo.
Lorsque vous n'aurez plus besoin de ce swap supplémentaire, vous
pourrez le désactiver grâce à la commande 'swapoff
/tmp/extraswap', puis effacer le fichier.
|
strace & ltrace
Ces 2 outils ne sont pas aussi génériques que ceux que nous venons de présenter, et leur intérêt est souvent plus limité pour débugger un programme dont vous êtes l'auteur. Cependant, ils ont pour particularité (à la différence des outils présentés jusqu'à présent) d'être extrêmement utiles pour déterminer ce qui se passe dans un programme pour lequel vous ne disposez pas du code source.
strace
Cet outil permet de visualiser les appels systèmes
effectués par le programme tracé. Notons au passage que le
système Solaris comporte également un outil similaire, nommé
'truss' dans les anciennes versions et 'trace' dans les versions plus récentes.
Essayons strace sur le programme d'exemple eftest2 dont nous
avons donné le code source un peu plus haut :
# strace ./eftest2
execve("./eftest2", ["./eftest2"], [/* 25 vars */]) = 0 brk(0) = 0x8049840 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40016000 open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=38822, ...}) = 0 old_mmap(NULL, 38822, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000 close(3) = 0 open("/lib/libc.so.6", O_RDONLY) = 3 fstat(3, {st_mode=S_IFREG|0755, st_size=1057576, ...}) = 0 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\224\314"..., 4096) = 4096 [...] brk(0x8049cb8) = 0x8049cb8 brk(0x804a000) = 0x804a000 [...] ioctl(1, TCGETS, {B9600 opost isig icanon echo ...}) = 0 write(1, "Fin du test\n", 12) = 12 [...] close(3) = 0 munmap(0x40017000, 4096) = 0 _exit(0) = ? # |
Outre les premières lignes qui correspondent à l'initialisation du processus et des bibliothèques dynamiques qu'il utilise, on voit les appels à brk (appel système utilisé par la fonction malloc) et à write (correspondant à la fonction printf).
Ce type d'outil est par exemple très utile si l'on cherche à connaître les fichiers d'initialisation lus par un programme, auquel cas il suffira de rechercher les occurences de l'appel système 'read'.
ltrace
Le fonctionnement de ltrace est relativement similaire à
celui de strace, à la différence près que
ltrace montre non pas les appels systèmes, mais les appels aux fonctions
de bibliothèques. Voici par exemple les informations fournies par
ltrace sur le même programme d'exemple.
# ltrace ./eftest2
__libc_start_main(0x08048610, 1, 0xbffffd14, 0x080483ec, 0x080486dc <unfinished ...> __monstartup(0x080484f0, 0x080486f8, 0xbffffcb8, 0x4004f4a8, 0x401221e8) = 0 atexit(0x08048470) = 0 __register_frame_info(0x08049730, 0x08049828, 0xbffffcb8, 0x4004f4a8, 0x401221e8) = 0x40122e60 mcount(0x08049744, 0xbffffcc8, 0xbffffce8, 0x4003dbcc, 1) = 0x40120a58 malloc(10) = 0x08049cb0 printf("Fin du test\n"Fin du test ) = 12 exit(0) = <void> _mcleanup(0x401221e8, 0x40015ec0, 1, 0, 0x4004f360) = 0x08049ca8 __deregister_frame_info(0x08049730, 0x400163f8, 0xbffffc6c, 0x400e8121, 0x08049848) = 0x08049828 +++ exited (status 0) +++ # |
La plupart des appels mis en évidence sont en fait internes au fonctionnement de la libc, mais l'on voit tout de même les appels des 2 fonctions de bibliothèques faits par notre programme : malloc() et printf().
Maintenant, non seulement, vous n'avez plus aucune excuse pour avoir
des fuites mémoire dans vos programmes, mais vous étes également
armés pour corriger les bugs de tout autre programme que vous pourrez
être amené à utiliser.
|
GNU gdb :
http://sources.redhat.com/gdb/ Electric Fence : Disponible en paquet Debian sous le nom 'electric-fence'. Sources : ftp://ftp.perens.com/pub/ElectricFence/ KDevelop : http://www.kdevelop.org/ gcc-checker : http://www.gnu.org/software/checker/checker.html glib : ftp://ftp.gtk.org/pub/gtk/v1.2/ strace : http://www.wi.leidenuniv.nl/~wichert/strace/ ltrace : ftp://ftp.debian.org/debian/dists/unstable/main/source/utils/ltrace_0.3.10.tar.gz |
Utilisateur de GNU/Linux depuis 1993, Vincent Renardias
est activement impliqué dans son développement depuis 1996
: Développeur de la distribution Debian, auteur de la traduction
Française de l'environnement GNOME, créateur du groupe d'utilisateurs
Linux de Marseille (PLUG). Il continue aujourd'hui activement a promouvoir
le système GNU/Linux en tant que responsable technique de la société
Echo, créatrice du portail voila.fr.
Vincent Renardias <vincent@echo.fr> |