Hello World est l'un des premiers programmes que nous écrivons dans n'importe quel langage de programmation.
Pour C, bonjour le monde semble simple et court:
#include <stdio.h> void main() { printf("Hello World!\n"); }
Le programme étant si court, il devrait être élémentaire d'expliquer ce qui se passe «sous le capot».
Voyons d'abord ce qui se passe lors de la compilation et de la liaison:
gcc --save-temps hello.c -o hello
--save-temps
ajouté pour que gcc laisse
hello.s
, un fichier de code d'assembly.
Voici l'exemple de code assembleur que j'ai reçu:
.file "hello.c" .section .rodata .LC0: .string "Hello World!" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp movl $.LC0, %edi call puts popq %rbp ret
Comme vous pouvez le voir dans la liste des assembleurs, ce n'est pas
printf
qui est appelé, mais
puts
. La fonction
puts
est également définie dans le fichier
stdio.h
et s'engage à imprimer une ligne et un saut de ligne.
Eh bien, nous avons compris quelle fonction notre code appelle réellement. Mais où les mises sont-elles mises en œuvre?
Pour déterminer quelle bibliothèque implémente les mises, nous utilisons
ldd
, qui affiche les dépendances de la bibliothèque, et
nm
, qui affiche les caractères du fichier objet.
$ ldd hello libc.so.6 => /lib64/libc.so.6 (0x0000003e4da00000) $ nm /lib64/libc.so.6 | grep " puts" 0000003e4da6dd50 W puts
La fonction est située dans une bibliothèque appelée
libc
et située dans
/lib64/libc.so.6
sur mon système (Fedora 19). Dans mon cas,
/lib64
est un lien symbolique sur
/usr/lib64
, et
/usr/lib64/libc.so.6
est un lien symbolique sur
/usr/lib64/libc-2.17.so
. Ce fichier contient toutes les fonctions.
Nous découvrons la version de
libc
en exécutant le fichier comme s'il était exécutable:
$ /usr/lib64/libc-2.17.so GNU C Library (GNU libc) stable release version 2.17, by Roland McGrath et al. ...
Par conséquent, notre programme appelle la fonction
glibc
version 2.17 de la
glibc
. Voyons maintenant ce que
puts
fonction
glibc-2.17
dans
glibc-2.17
.
Le code glibc est difficile à naviguer en raison de l'utilisation généralisée des macros et scripts de préprocesseur. En regardant le code, nous voyons ce qui suit dans
libio/ioputs.c
:
weak_alias (_IO_puts, puts)
Dans la glibc, cela signifie que lors de l'appel de
_IO_puts
,
_IO_puts
est en fait appelé. Cette fonction est décrite dans le même fichier, et la partie principale de la fonction ressemble à ceci:
int _IO_puts (str) const char *str; {
J'ai jeté toutes les ordures autour du défi important pour nous.
_IO_sputn
est maintenant notre maillon actuel dans la chaîne d'appels Hello World. Nous trouvons une définition, ce nom est une macro définie dans
libio/libioP.h
, qui appelle une autre macro, qui encore une fois ... L'arbre des macros contient ce qui suit:
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
Qu'est-ce qui se passe ici? Développons toutes les macros pour regarder le code final:
((*(struct _IO_jump_t **) ((void *) &((struct _IO_FILE_plus *) (((_IO_FILE*)(&_IO_2_1_stdout_)) ) )->vtable+(((_IO_FILE*)(&_IO_2_1_stdout_)) )->_vtable_offset))->__xsputn ) (((_IO_FILE*)(&_IO_2_1_stdout_)), str, len)
Les yeux me font mal. Permettez-moi de vous expliquer ce qui se passe ici. Glibc utilise jump-table pour appeler des fonctions. Dans notre cas, la table se trouve dans une structure appelée
_IO_2_1_stdout_
, et la fonction dont nous avons besoin s'appelle
__xsputn
.
La structure est déclarée dans le fichier
libio/libio.h
:
extern struct _IO_FILE_plus _IO_2_1_stdout_;
Et dans le fichier
libio/libioP.h
y a des définitions de la structure, de la table et de son champ:
struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; };
Si nous creusons encore plus, nous voyons que la table
_IO_2_1_stdout_
initialisée dans le fichier
libio/stdfiles.c
, et les implémentations
libio/stdfiles.c
des fonctions de la table sont définies dans
libio/fileops.c
:
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS); # define _IO_new_file_xsputn _IO_file_xsputn
Tout cela signifie que si nous utilisons la table de saut associée à
stdout
, nous
_IO_new_file_xsputn
par appeler la fonction
_IO_new_file_xsputn
. Déjà plus proche, non? Cette fonction jette des données dans des tampons et appelle
new_do_write
lorsque le contenu du tampon peut être
new_do_write
. Voici à quoi ressemble
new_do_write
:
static _IO_size_t new_do_write (fp, data, to_do) _IO_FILE *fp; const char *data; _IO_size_t to_do; { _IO_size_t count; .. count = _IO_SYSWRITE (fp, data, to_do); .. return count; }
Bien sûr, la macro est appelée. Par le même mécanisme de table de saut que nous avons vu pour
__xsputn
,
__write
est
__write
. Pour les fichiers
__write
,
__write
_IO_new_file_write
sur
_IO_new_file_write
. Cette fonction est finalement appelée. Regardons-la?
_IO_ssize_t _IO_new_file_write (f, data, n) _IO_FILE *f; const void *data; _IO_ssize_t n; { _IO_ssize_t to_do = n; _IO_ssize_t count = 0; while (to_do > 0) {
Enfin, une fonction qui appelle quelque chose qui ne commence pas par un trait de soulignement! La fonction d'
write
est connue et définie dans
unistd.h
. Il s'agit d'un moyen assez standard d'écrire des octets dans un fichier à l'aide d'un descripteur de fichier. La fonction d'
write
est définie dans la glibc elle-même, nous devons donc trouver le code.
J'ai trouvé le code d'
write
dans
sysdeps/unix/syscalls.list
. La plupart des appels système enveloppés dans la glibc sont générés à partir de ces fichiers. Le fichier contient le nom de la fonction et les arguments qu'elle prend. Le corps de la fonction est créé à partir d'un modèle d'appel système commun.
# File name Caller Syscall name Args Strong name Weak names ... write - write Ci:ibn __libc_write __write write ...
Lorsque le code glibc appelle l'
write
(
__libcwrite
ou
__write
), syscall se produit dans le noyau. Le code du noyau est beaucoup plus lisible que la glibc. Le point d'entrée pour l'
write
syscall est dans
fs/readwrite.c
:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) { struct fd f = fdget(fd); ssize_t ret = -EBADF; if (f.file) { loff_t pos = file_pos_read(f.file); ret = vfs_write(f.file, buf, count, &pos); if (ret >= 0) file_pos_write(f.file, pos); fdput(f); } return ret; }
Tout d'abord, la structure correspondant au descripteur de fichier est trouvée, puis la fonction
vfs_write
est
vfs_write
partir du sous-système de système de fichiers virtuel (vfs). La structure dans notre cas correspondra au fichier
stdout
. Jetez un œil à
vfs_write
:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { ssize_t ret;
La fonction délègue l'exécution de la fonction d'
write
appartenant à un fichier particulier. Sous Linux, cela est souvent implémenté dans le code du pilote, vous devez donc savoir quel pilote est appelé dans notre cas.
J'utilise Fedora 19 avec Gnome 3 pour des expériences, ce qui signifie notamment que mon terminal est
gnome-terminal
par défaut. Exécutez ce terminal et procédez comme suit:
~$ tty /dev/pts/0 ~$ ls -l /proc/self/fd total 0 lrwx------ 1 kos kos 64 okt. 15 06:37 0 -> /dev/pts/0 lrwx------ 1 kos kos 64 okt. 15 06:37 1 -> /dev/pts/0 lrwx------ 1 kos kos 64 okt. 15 06:37 2 -> /dev/pts/0 ~$ ls -la /dev/pts total 0 drwxr-xr-x 2 root root 0 okt. 10 10:14 . drwxr-xr-x 21 root root 3580 okt. 15 06:21 .. crw--w---- 1 kos tty 136, 0 okt. 15 06:43 0 c--------- 1 root root 5, 2 okt. 10 10:14 ptmx
La commande
tty
imprime le nom d'un fichier lié à l'entrée standard, et comme vous pouvez le voir dans la liste des fichiers dans
/proc
, le même fichier est associé à la sortie et au flux d'erreur. Ces fichiers de périphériques dans
/dev/pts
sont appelés pseudo-terminaux, plus précisément, ce sont des pseudo-terminaux esclaves. Lorsqu'un processus écrit un pseudo-terminal sur esclave, les données vont au pseudo-terminal maître. Le pseudo-terminal maître est un périphérique
/dev/ptmx
.
Le pilote du pseudo-terminal se trouve dans le noyau Linux dans le
drivers/tty/pty.c
:
static void __init unix98_pty_init(void) {
Lors de l'écriture dans
pts
,
pty_write
est
pty_write
, qui ressemble à ceci:
static int pty_write(struct tty_struct *tty, const unsigned char *buf, int c) { struct tty_struct *to = tty->link; if (tty->stopped) return 0; if (c > 0) { c = tty_insert_flip_string(to->port, buf, c); if (c) { tty_flip_buffer_push(to->port); tty_wakeup(tty); } } return c; }
Les commentaires aident à comprendre que les données sont dans la file d'attente d'entrée du pseudo-terminal maître. Mais qui lit cette ligne?
~$ lsof | grep ptmx gnome-ter 13177 kos 11u CHR 5,2 0t0 1133 /dev/ptmx gdbus 13177 13178 kos 11u CHR 5,2 0t0 1133 /dev/ptmx dconf 13177 13179 kos 11u CHR 5,2 0t0 1133 /dev/ptmx gmain 13177 13182 kos 11u CHR 5,2 0t0 1133 /dev/ptmx ~$ ps 13177 PID TTY STAT TIME COMMAND 13177 ? Sl 0:04 /usr/libexec/gnome-terminal-server
Le processus
gnome-terminal-server
génère tous les
gnome-terminal
et crée de nouveaux pseudo-terminaux. C'est lui qui écoute le pseudo-terminal maître et qui finira par recevoir nos données qui sont
"Hello World"
. Le serveur
gnome-terminal
reçoit la chaîne et l'affiche à l'écran. En général, il n'y avait pas assez de temps pour une analyse détaillée de
gnome-terminal
:)
Conclusion
Le parcours général de notre ligne «Hello World»:
0. hello: printf("Hello World") 1. glibc: puts() 2. glibc: _IO_puts() 3. glibc: _IO_new_file_xsputn() 4. glibc: new_do_write() 5. glibc: _IO_new_file_write() 6. glibc: syscall write 7. kernel: vfs_write() 8. kernel: pty_write() 9. gnome_terminal: read() 10. gnome_terminal: show to user
Cela ressemble à un
petit buste pour une opération aussi simple. C'est bien que seuls ceux qui le veulent vraiment le voient.