Une question posée sur irc est à l'origine de cette question, l'utilisateur
en question assurant que who ne retournait rien pour lui, pas même les
informations du shell courant.
Le cas a paru intéressant pour montrer comment utiliser strace en pratique.
Bon, ok, mais c'est quoi who ?
who est une commande permettant de voir quels sont les utilisateurs
loggés sur le système, sur quel terminal et depuis combien de temps.
Je suis loggé une fois sur le système depuis un tty d'où la première ligne,
et j'ai 7 terminaux d'ouverts. À chacun correspond un pts. Comme on peut le
constater il est difficile d'imaginer lancer who sans avoir de sortie, on
devrait avoir au moins une ligne correspondant au shell depuis lequel who
est lancé.
Du coup on va chercher à savoir comment fonctionne who. Et pour ça on va
lire son code source ! Bon peut-être pas en fait. who est un logiciel
libre faisant partie du projet GNU coreutils donc son code source est tout à
fait accessible. Cependant ça nous donnera des informations sur comment il
est sensé fonctionner en théorie alors que des informations dynamiques
(quelles sont effectivement les opérations effectuées) seraient
intéressantes. Ceci étant dit j'invite vraiment à prendre l'habitude de lire
le code de ce genre de programmes, c'est très instructif.
Bon, mais du coup on fait quoi ?
Et bien du coup nous allons utiliser strace. strace est un utilitaire
permettant de tracer les appels systèmes fait au noyau linux.
Un appel quoi ?
Le travail du noyau linux c'est de faire le lien entre le matériel (clavier,
écran, souris, disque dur...) et le logiciel utilisant ce matériel. Un
logiciel n'accède donc normalement jamais au matériel directement, à la place
il dépose sa demande auprès du noyau qui lui interragit avec le matériel puis
renvois la réponse. Cette demande c'est un appel système.
On fait donc des appels systèmes tout le temps puisque chaque interraction
avec quelque chose qui n'est pas logiciel en génère ! Au moment où j'écris
ces lignes par exemple j'ai des appels systèmes read() qui sont effectués
pour lire les touches tapées au clavier, des appels write() pour sauvegarder
périodiquement mon fichier sur mon disque dur, des appels recv() sans doute
aussi en arrière plan avec mon lecteur de RSS qui reçoit des informations
depuis des servers distants... Toute action non triviale passe par un appel
système ou presque.
Pour savoir quels sont les appels systèmes pour linux 64 bits on peut aller
voir dans /usr/include/asm/unistd_64.h
On peut constater que du point de vu d'un programmeur un appel système est
une fonction C comme une autre, il est tout à fait possible de les utiliser
dans un programme directement.
Bon, mais strace du coup ?
strace nous permet de tracer ces appels pour un processus donné et donc
de voir ce qui est demandé exactement au kernel et ce qu'il répond en retour.
Bon. Vu comme ça ça ne parait pas particulièrement utile, et pourtant il y a
beaucoup d'informations très intéressantes. On va voir ça par partie pour
comprendre un peu mieux ce qui se passe. L'essentiel est de ne pas chercher
à tout comprendre. Il y a beaucoup de choses qui se passent et seule une
fraction correspond à notre problème. Il ne faut pas se focaliser sur
l'incompris.
La première ligne est un appel à exceve(). Cet appel dit au système
d'exploitation « Hé, je voudrais lancer le programme /usr/bin/echo avec les
arguments "echo" et "Hello World!". » Le fait d'avoir "echo" comme argument
ne devrait pas étonner les programmeurs, si vous avez déjà récupéré les
arguments passés par la ligne de commande dans un programme vous savez que le
premier argument est le nom avec lequel le programme a été appelé.
Ensuite le système charge d'éventuelles librairies passées via LD_PRELOAD, on
voit que l'accès a échoué car nous n'en avons pas spécifié. Si vous ne savez
pas ce qu'est LD_PRELOAD je vous invite à vous renseigner dessus ; bien que
ce soit hors du propos de cet article c'est un système très sympa à
connaître.
On charge ensuite la librairies standard C en ouvrant deux librairies et lisant
le contenu des fichiers avant de les refermer (d'open() à close()). Open
renvois un file descriptor, un identifiant du fichier ouvert. C'est ce file
descriptor qu'on passe en premier argument de read, on peut donc savoir
depuis quel fichier on lit les données. Il existe 3 descripteurs de fichiers
spéciaux:
0 pour l'entrée standard, accessible seulement en lecture
1 pour la sortie standard, accessible seulement en écriture
2 pour la sortie d'erreur, accessible seulement en écriture
C'est aussi pour cette raison que lorsque l'on ouvre des fichiers les
descripteurs que l'on obtient commencent normalement à 3.
On peut aussi remarquer différents appels à mmap(). mmap() est une fonction
très utile permettant d'affecter un block de la mémoire matérielle à un
certaine addresse pour pouvoir interragir facilement avec sans reccourir à
un appel système à chaque lecture ou écriture.
On remarque aussi quelques appels à fstat(). fstat() permet d'aquérir des
informations sur un fichier (droits d'accès, taille...). Lui aussi prend un
descripteur de fichier en argument.
La partie véritablement intéressante arrive à peine quelques lignes avant la
fin:
write(1,"Hello World!\n",13HelloWorld!)=13
Voilà. On a écrit "Hello World!\n" sur la sortie standard. Tout ça pour ça.
Les lignes suivantes ne servent qu'à remballer. La raison pour laquelle cette
ligne semble cassée c'est qu'en fait les deux sorties (standard et erreur)
sont mélangées, mais on a bien deux lignes différentes en fait:
Bon. Voilà. On a vu un echo et on a eu un apperçu de ce que strace faisait.
Mais du coup, pour who?
Le cas de who
Je ne vais pas détailler autant que pour echo car beaucoup d'étapes sont
redondantes. Comme on l'a vu l'essentiel est à la fin, donc je vais commencer
par là.
Ok, donc on récupère des informations sur différents fichiers dans /dev/pts
et /etc/localtime. Un petit man 5 localtime nous en dis plus sur ce
fichier et nous apprend qu'il pointe vers un fichier spécial correspondant à
la timezone de notre ordinateur. On cherche donc à récupérer des informations
temporelles, ça fait sens avec ce qu'on voit.
Les fichiers en /dev/pts font sens également: il est courant dans le monde
unix de représenter les ressources systèmes par des fichiers et les numéros
correspondent à mes pts ouverts. On peut donc supposer qu'il vérifie s'ils
existent pour savoir si les pts correspondants existent vraiment et depuis
combien de temps.
Mais d'où tire-t-il la liste précise en premier lieu ? Je veux dire, il n'a
pas testé /dev/pts/2 par exemple, c'est donc qu'il savait déjà quoi chercher.
Comment ? Remontons encore...
On croise au passage le petit manquant de la liste: /dev/tty1:
On remarque au milieu de cette chaine de caractère (tronquée par strace pour
limiter le bruit) pts/12. Ce n'est sans doute pas une coïncidence. Toutes les
autres lignes ont également au milieu un pts/quelquechose. On dirait donc que
l'on lit des blocs de 384 octets dans un fichier binaire et que ce bloc
contient les informations sur notre pts. Un bloc binaire de taille fixe
contenant différentes informations ? En C ça serait une structure, il y a
sans doute plus d'informations dans ce bloc qu'il n'y semble. Mais dans quel
fichier sommes-nous en train de lire tout ça ?
Le file descriptor est 3, remontons jusqu'à un appel open() renvoyant 3:
open("/var/run/utmp",O_RDONLY|O_CLOEXEC)=3
Nous sommes donc en train de lire dans /var/run/utmp. Que nous dit le manuel?
Bien ! Donc ce fichier est un journal de qui est loggé et comment. On a
également un élément de réponse à notre mystère : si l'utilisateur n'est pas
loggé avec un système utilisant utmp alors il est possible que who ne le
trouve pas.
La question se pose donc de savoir si who lis d'autres fichiers ou non,
et lesquels. Facile, juste au-dessus du open() pour /var/run/utmp on trouve :
C'est le seul appel de la sorte et donc le seul fichier non trouvé. Il reste
possible que who s'arrête simplement au premier fichier qu'il trouve sans
aller plus loin, mais ce n'est pas visible dans notre première expérience.
Et notre structure alors ?
Tout est là, dans le man de utmp :
structutmp{shortut_type;/* Type of record */pid_tut_pid;/* PID of login process */charut_line[UT_LINESIZE];/* Device name of tty - "/dev/" */charut_id[4];/* Terminal name suffix,
or inittab(5) ID */charut_user[UT_NAMESIZE];/* Username */charut_host[UT_HOSTSIZE];/* Hostname for remote login, or
kernel version for run-level
messages */structexit_statusut_exit;/* Exit status of a process
marked as DEAD_PROCESS; not
used by Linux init (1 *//* The ut_session and ut_tv fields must be the same size when
compiled 32- and 64-bit. This allows data files and shared
memory to be shared between 32- and 64-bit applications. */#if __WORDSIZE == 64 && defined __WORDSIZE_COMPAT32
int32_tut_session;/* Session ID (getsid(2)),
used for windowing */struct{int32_ttv_sec;/* Seconds */int32_ttv_usec;/* Microseconds */}ut_tv;/* Time entry was made */#else
longut_session;/* Session ID */structtimevalut_tv;/* Time entry was made */#endif
int32_tut_addr_v6[4];/* Internet address of remote
host; IPv4 address uses
just ut_addr_v6[0] */char__unused[20];/* Reserved for future use */};
Voilà, je pense que ça ira pour cette fois. On a pu voir que strace même
utilisé naïvement sans options aucunes peut être utilisé pour comprendre
comment les choses marchent et on en a profité pour apprendre les principes
fondamentaux derrière who. Strace est un outil très puissant qui a
beaucoup de possibilités, n'hésitez pas à en manger !
Comment who fonctionne-t-il ?
Explication préliminaire
Une question posée sur irc est à l'origine de cette question, l'utilisateur en question assurant que who ne retournait rien pour lui, pas même les informations du shell courant.
Le cas a paru intéressant pour montrer comment utiliser strace en pratique.
Bon, ok, mais c'est quoi who ?
who est une commande permettant de voir quels sont les utilisateurs loggés sur le système, sur quel terminal et depuis combien de temps.
Exemple:
Je suis loggé une fois sur le système depuis un tty d'où la première ligne, et j'ai 7 terminaux d'ouverts. À chacun correspond un pts. Comme on peut le constater il est difficile d'imaginer lancer who sans avoir de sortie, on devrait avoir au moins une ligne correspondant au shell depuis lequel who est lancé.
Du coup on va chercher à savoir comment fonctionne who. Et pour ça on va lire son code source ! Bon peut-être pas en fait. who est un logiciel libre faisant partie du projet GNU coreutils donc son code source est tout à fait accessible. Cependant ça nous donnera des informations sur comment il est sensé fonctionner en théorie alors que des informations dynamiques (quelles sont effectivement les opérations effectuées) seraient intéressantes. Ceci étant dit j'invite vraiment à prendre l'habitude de lire le code de ce genre de programmes, c'est très instructif.
Bon, mais du coup on fait quoi ?
Et bien du coup nous allons utiliser strace. strace est un utilitaire permettant de tracer les appels systèmes fait au noyau linux.
Un appel quoi ?
Le travail du noyau linux c'est de faire le lien entre le matériel (clavier, écran, souris, disque dur...) et le logiciel utilisant ce matériel. Un logiciel n'accède donc normalement jamais au matériel directement, à la place il dépose sa demande auprès du noyau qui lui interragit avec le matériel puis renvois la réponse. Cette demande c'est un appel système.
On fait donc des appels systèmes tout le temps puisque chaque interraction avec quelque chose qui n'est pas logiciel en génère ! Au moment où j'écris ces lignes par exemple j'ai des appels systèmes read() qui sont effectués pour lire les touches tapées au clavier, des appels write() pour sauvegarder périodiquement mon fichier sur mon disque dur, des appels recv() sans doute aussi en arrière plan avec mon lecteur de RSS qui reçoit des informations depuis des servers distants... Toute action non triviale passe par un appel système ou presque.
Pour savoir quels sont les appels systèmes pour linux 64 bits on peut aller voir dans /usr/include/asm/unistd_64.h
Comme on peut le voir les appels systèmes sont numérotés, on apperçoit read() et write() que nous avons déjà évoqué.
Pour avoir plus d'information sur un appel système on peut utiliser le manuel, les pages correspondants aux appels systèmes sont dans la section 2 :
On peut constater que du point de vu d'un programmeur un appel système est une fonction C comme une autre, il est tout à fait possible de les utiliser dans un programme directement.
Bon, mais strace du coup ?
strace nous permet de tracer ces appels pour un processus donné et donc de voir ce qui est demandé exactement au kernel et ce qu'il répond en retour.
Voici un exemple sur une commande simple..
Bon. Vu comme ça ça ne parait pas particulièrement utile, et pourtant il y a beaucoup d'informations très intéressantes. On va voir ça par partie pour comprendre un peu mieux ce qui se passe. L'essentiel est de ne pas chercher à tout comprendre. Il y a beaucoup de choses qui se passent et seule une fraction correspond à notre problème. Il ne faut pas se focaliser sur l'incompris.
La première ligne est un appel à exceve(). Cet appel dit au système d'exploitation « Hé, je voudrais lancer le programme /usr/bin/echo avec les arguments "echo" et "Hello World!". » Le fait d'avoir "echo" comme argument ne devrait pas étonner les programmeurs, si vous avez déjà récupéré les arguments passés par la ligne de commande dans un programme vous savez que le premier argument est le nom avec lequel le programme a été appelé.
Ensuite le système charge d'éventuelles librairies passées via LD_PRELOAD, on voit que l'accès a échoué car nous n'en avons pas spécifié. Si vous ne savez pas ce qu'est LD_PRELOAD je vous invite à vous renseigner dessus ; bien que ce soit hors du propos de cet article c'est un système très sympa à connaître.
On charge ensuite la librairies standard C en ouvrant deux librairies et lisant le contenu des fichiers avant de les refermer (d'open() à close()). Open renvois un file descriptor, un identifiant du fichier ouvert. C'est ce file descriptor qu'on passe en premier argument de read, on peut donc savoir depuis quel fichier on lit les données. Il existe 3 descripteurs de fichiers spéciaux:
C'est aussi pour cette raison que lorsque l'on ouvre des fichiers les descripteurs que l'on obtient commencent normalement à 3.
On peut aussi remarquer différents appels à mmap(). mmap() est une fonction très utile permettant d'affecter un block de la mémoire matérielle à un certaine addresse pour pouvoir interragir facilement avec sans reccourir à un appel système à chaque lecture ou écriture.
On remarque aussi quelques appels à fstat(). fstat() permet d'aquérir des informations sur un fichier (droits d'accès, taille...). Lui aussi prend un descripteur de fichier en argument.
La partie véritablement intéressante arrive à peine quelques lignes avant la fin:
Voilà. On a écrit "Hello World!\n" sur la sortie standard. Tout ça pour ça. Les lignes suivantes ne servent qu'à remballer. La raison pour laquelle cette ligne semble cassée c'est qu'en fait les deux sorties (standard et erreur) sont mélangées, mais on a bien deux lignes différentes en fait:
Bon. Voilà. On a vu un echo et on a eu un apperçu de ce que strace faisait. Mais du coup, pour who?
Le cas de who
Je ne vais pas détailler autant que pour echo car beaucoup d'étapes sont redondantes. Comme on l'a vu l'essentiel est à la fin, donc je vais commencer par là.
Ok, donc on écrit le texte sur la sortie standard. Super, mais ça on était déjà au courant. Remontons de quelques lignes:
Ok, donc on récupère des informations sur différents fichiers dans /dev/pts et /etc/localtime. Un petit man 5 localtime nous en dis plus sur ce fichier et nous apprend qu'il pointe vers un fichier spécial correspondant à la timezone de notre ordinateur. On cherche donc à récupérer des informations temporelles, ça fait sens avec ce qu'on voit.
Les fichiers en /dev/pts font sens également: il est courant dans le monde unix de représenter les ressources systèmes par des fichiers et les numéros correspondent à mes pts ouverts. On peut donc supposer qu'il vérifie s'ils existent pour savoir si les pts correspondants existent vraiment et depuis combien de temps.
Mais d'où tire-t-il la liste précise en premier lieu ? Je veux dire, il n'a pas testé /dev/pts/2 par exemple, c'est donc qu'il savait déjà quoi chercher. Comment ? Remontons encore...
On croise au passage le petit manquant de la liste: /dev/tty1:
Mais il faut remonter plus loin pour trouver ce que l'on cherche. On trouve une série d'appels à read (entre autres choses) ressemblant à ça:
On remarque au milieu de cette chaine de caractère (tronquée par strace pour limiter le bruit) pts/12. Ce n'est sans doute pas une coïncidence. Toutes les autres lignes ont également au milieu un pts/quelquechose. On dirait donc que l'on lit des blocs de 384 octets dans un fichier binaire et que ce bloc contient les informations sur notre pts. Un bloc binaire de taille fixe contenant différentes informations ? En C ça serait une structure, il y a sans doute plus d'informations dans ce bloc qu'il n'y semble. Mais dans quel fichier sommes-nous en train de lire tout ça ?
Le file descriptor est 3, remontons jusqu'à un appel open() renvoyant 3:
Nous sommes donc en train de lire dans /var/run/utmp. Que nous dit le manuel?
Bien ! Donc ce fichier est un journal de qui est loggé et comment. On a également un élément de réponse à notre mystère : si l'utilisateur n'est pas loggé avec un système utilisant utmp alors il est possible que who ne le trouve pas.
La question se pose donc de savoir si who lis d'autres fichiers ou non, et lesquels. Facile, juste au-dessus du open() pour /var/run/utmp on trouve :
C'est le seul appel de la sorte et donc le seul fichier non trouvé. Il reste possible que who s'arrête simplement au premier fichier qu'il trouve sans aller plus loin, mais ce n'est pas visible dans notre première expérience.
Et notre structure alors ?
Tout est là, dans le man de utmp :
Voilà, je pense que ça ira pour cette fois. On a pu voir que strace même utilisé naïvement sans options aucunes peut être utilisé pour comprendre comment les choses marchent et on en a profité pour apprendre les principes fondamentaux derrière who. Strace est un outil très puissant qui a beaucoup de possibilités, n'hésitez pas à en manger !