Ce chapitre décrit le fournisseur Function Boundary Tracing (FBT), qui fournit des sondes associées à l'entrée et au retour de la plupart des fonctions du noyau de Solaris. Cette fonction constitue l'unité fondamentale du texte du programme. Dans un système bien conçu, chaque fonction effectue une opération discrète précise sur un ou plusieurs objets similaires spécifiés. Par conséquent, même sur les plus petits systèmes Solaris, FBT fournit approximativement 20 000 sondes.
À l'instar d'autres fournisseurs DTrace, FBT n'a d'effets sur les sondes que s'il est activé de manière explicite. Lorsqu'il est activé FBT provoque uniquement des effets sur les fonctions sondées. Puisque l'implémentation de FBT est spécifique à l'architecture du jeu d'instructions, FBT a été implémenté sur une plate-forme SPARC et sur une plate-forme x86. Pour chaque jeu d'instructions, il existe un petit nombre de fonctions qui n'en appellent aucune autre, optimisées par le compilateur et que FBT ne peut pas instrumenter. On les appelle les fonctions terminales. Les sondes correspondant à ces fonctions ne sont pas présentes dans DTrace.
L'utilisation efficace des sondes FBT requiert une bonne connaissance de l'implémentation du système d'exploitation. Par conséquent, il est recommandé d'utiliser FBT uniquement lors du développement du logiciel de noyau ou lorsque d'autres fournisseurs ne suffisent pas. D'autres fournisseurs DTrace, dont syscall, sched, proc et io, peuvent être utilisés pour répondre à la plupart des questions portant sur l'analyse du système, sans qu'aucune connaissance en matière d'implémentation du système d'exploitation ne soit nécessaire.
FBT fournit une sonde à la limite de la plupart des fonctions du noyau. La limite d'une fonction est franchie lors de l'entrée dans la fonction et lors du retour de celle-ci. FBT fournit ainsi deux rôles à chaque fonction du noyau : l'un à l'entrée dans la fonction, l'autre au retour de la fonction. Ces sondes sont appelées entry et return, respectivement. Le nom de la fonction et le nom du module sont spécifiés comme faisant partie de la sonde. Toutes les sondes FBT précisent un nom de fonction et un nom de module.
Les arguments des sondes entry sont identiques à ceux de la fonction du noyau du système d'exploitation correspondant. Il est possible d'accéder à ces arguments dans un mode de saisie en utilisant le tableau args[]. Il est possible d'accéder à ces arguments en tant que int64_t en utilisant arg0 .. Variables argn.
Une fonction donnée ne dispose que d'un seul point d'entrée mais peut présenter de nombreux points différents lors du retour vers le programme appelant. Généralement, vous vous intéressez à la valeur renvoyée par une fonction ou au fait que la fonction est renvoyée vers tous les chemins et pas uniquement le chemin de retour spécifique. FBT collecte donc plusieurs sites de retour d'une fonction dans une sonde unique return. Si le chemin de retour précis présente un intérêt, étudiez la valeur args[0] de la sonde return qui indique le décalage (en octets) de l'instruction de retour dans le texte de la fonction.
Si la fonction a une valeur de retour, celle-ci est stockée dans args[1]. Dans le cas contraire, args[1] n'est pas défini.
Vous pouvez utiliserFBT pour explorer facilement l'implémentation du noyau. Le script suivant, donné à titre d'exemple, enregistre le premier ioctl(2) d'un processus xclock quelconque puis vient après le chemin de code suivant dans le noyau :
/* * To make the output more readable, we want to indent every function entry * (and unindent every function return). This is done by setting the * "flowindent" option. */ #pragma D option flowindent syscall::ioctl:entry /execname == "xclock" && guard++ == 0/ { self->traceme = 1; printf("fd: %d", arg0); } fbt::: /self->traceme/ {} syscall::ioctl:return /self->traceme/ { self->traceme = 0; exit(0); }
Exécuter ce script engendre une sortie identique à l'exemple suivant :
# dtrace -s ./xioctl.d dtrace: script './xioctl.d' matched 26254 probes CPU FUNCTION 0 => ioctl fd: 3 0 -> ioctl 0 -> getf 0 -> set_active_fd 0 <- set_active_fd 0 <- getf 0 -> fop_ioctl 0 -> sock_ioctl 0 -> strioctl 0 -> job_control_type 0 <- job_control_type 0 -> strcopyout 0 -> copyout 0 <- copyout 0 <- strcopyout 0 <- strioctl 0 <- sock_ioctl 0 <- fop_ioctl 0 -> releasef 0 -> clear_active_fd 0 <- clear_active_fd 0 -> cv_broadcast 0 <- cv_broadcast 0 <- releasef 0 <- ioctl 0 <= ioctl |
La sortie montre qu'un processus xclock a appelé ioctl() sur un descripteur de fichier qui semble être associé à un socket.
Vous pouvez également utiliser FBT pour comprendre les pilotes de noyau. Par exemple, le pilote ssd(7D) dispose de nombreux chemins de code par l'intermédiaire desquels EIO est susceptible d'être renvoyé. FBT peut facilement être utilisé pour déterminer le chemin de code précis ayant engendré une condition d'erreur, comme illustré dans l'exemple suivant :
fbt:ssd::return /arg1 == EIO/ { printf("%s+%x returned EIO.", probefunc, arg0); }
Pour plus d'informations sur les retours de EIO, il est possible de procéder au suivi spéculatif des sondesfbt puis d'exécuter commit()(ou discard()) en fonction de la valeur de retour d'une fonction spécifique. Pour de plus amples informations sur le suivi spéculatif, reportez-vous au Chapitre13Suivi spéculatif.
Sinon, vous pouvez utiliser FBT pour comprendre les fonctions appelées au sein d'un module spécifié. L'exemple suivant répertorie toutes les fonctions appelées dans UFS :
# dtrace -n fbt:ufs::entry'{@a[probefunc] = count()}' dtrace: description 'fbt:ufs::entry' matched 353 probes ^C ufs_ioctl 1 ufs_statvfs 1 ufs_readlink 1 ufs_trans_touch 1 wrip 1 ufs_dirlook 1 bmap_write 1 ufs_fsync 1 ufs_iget 1 ufs_trans_push_inode 1 ufs_putpages 1 ufs_putpage 1 ufs_syncip 1 ufs_write 1 ufs_trans_write_resv 1 ufs_log_amt 1 ufs_getpage_miss 1 ufs_trans_syncip 1 getinoquota 1 ufs_inode_cache_constructor 1 ufs_alloc_inode 1 ufs_iget_alloced 1 ufs_iget_internal 2 ufs_reset_vnode 2 ufs_notclean 2 ufs_iupdat 2 blkatoff 3 ufs_close 5 ufs_open 5 ufs_access 6 ufs_map 8 ufs_seek 11 ufs_addmap 15 rdip 15 ufs_read 15 ufs_rwunlock 16 ufs_rwlock 16 ufs_delmap 18 ufs_getattr 19 ufs_getpage_ra 24 bmap_read 25 findextent 25 ufs_lockfs_begin 27 ufs_lookup 46 ufs_iaccess 51 ufs_imark 92 ufs_lockfs_begin_getpage 102 bmap_has_holes 102 ufs_getpage 102 ufs_itimes_nolock 107 ufs_lockfs_end 125 dirmangled 498 dirbadname 498 |
Si vous connaissez l'objectif ou les arguments d'une fonction de noyau, vous pouvez utiliser FBT pour comprendre de quelle manière et pour quelle raison la fonction est appelée. Par exemple, putnext(9F) prend un pointeur dans une structure queue(9S) comme premier membre. Le membre q_qinfo de la structure queue est un pointeur sur une structure qinit(9S). Le membre qi_minfo de la structure qinit dispose d'un pointeur sur une structure module_info(9S), qui contient le nom du module dans son membre mi_idname. L'exemple suivant rassemble ces informations en utilisant la sonde FBT dans putnext pour procéder au suivi des appels putnext(9F) par nom de module :
fbt::putnext:entry { @calls[stringof(args[0]->q_qinfo->qi_minfo->mi_idname)] = count(); }
Exécuter le script ci-dessus engendre une sortie similaire à l'exemple suivant :
# dtrace -s ./putnext.d ^C iprb 1 rpcmod 1 pfmod 1 timod 2 vpnmod 2 pts 40 conskbd 42 kb8042 42 tl 58 arp 108 tcp 126 ptm 249 ip 313 ptem 340 vuid2ps2 361 ttcompat 412 ldterm 413 udp 569 strwhead 624 mouse8042 726 |
Vous pouvez également utiliser FBT pour déterminer le temps écoulé dans une fonction donnée. L'exemple suivant montre comment déterminer les programmes appelants des routines de délai DDI drv_usecwait(9F) et delay(9F).
fbt::delay:entry, fbt::drv_usecwait:entry { self->in = timestamp } fbt::delay:return, fbt::drv_usecwait:return /self->in/ { @snoozers[stack()] = quantize(timestamp - self->in); self->in = 0; }
Il est particulièrement intéressant d'exécuter ce script, donné à titre d'exemple, pendant l'initialisation. Le Chapitre36Suivi anonyme décrit la procédure permettant de réaliser un suivi anonyme pendant l'initialisation du système. Lors de la réinitialisation, une sortie similaire à l'exemple suivant s'affichera vraisemblablement :
# dtrace -ae ata`ata_wait+0x34 ata`ata_id_common+0xf5 ata`ata_disk_id+0x20 ata`ata_drive_type+0x9a ata`ata_init_drive+0xa2 ata`ata_attach+0x50 genunix`devi_attach+0x75 genunix`attach_node+0xb2 genunix`i_ndi_config_node+0x97 genunix`i_ddi_attachchild+0x4b genunix`devi_attach_node+0x3d genunix`devi_config_one+0x1d0 genunix`ndi_devi_config_one+0xb0 devfs`dv_find+0x125 devfs`devfs_lookup+0x40 genunix`fop_lookup+0x21 genunix`lookuppnvp+0x236 genunix`lookuppnat+0xe7 genunix`lookupnameat+0x87 genunix`cstatat_getvp+0x134 value ------------- Distribution ------------- count 2048 | 0 4096 |@@@@@@@@@@@@@@@@@@@@@ 4105 8192 |@@@@ 783 16384 |@@@@@@@@@@@@@@ 2793 32768 | 16 65536 | 0 kb8042`kb8042_wait_poweron+0x29 kb8042`kb8042_init+0x22 kb8042`kb8042_attach+0xd6 genunix`devi_attach+0x75 genunix`attach_node+0xb2 genunix`i_ndi_config_node+0x97 genunix`i_ddi_attachchild+0x4b genunix`devi_attach_node+0x3d genunix`devi_config_one+0x1d0 genunix`ndi_devi_config_one+0xb0 genunix`resolve_pathname+0xa5 genunix`ddi_pathname_to_dev_t+0x16 consconfig_dacf`consconfig_load_drivers+0x14 consconfig_dacf`dynamic_console_config+0x6c consconfig`consconfig+0x8 unix`stubs_common_code+0x3b value ------------- Distribution ------------- count 262144 | 0 524288 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 221 1048576 |@@@@ 29 2097152 | 0 usba`hubd_enable_all_port_power+0xed usba`hubd_check_ports+0x8e usba`usba_hubdi_attach+0x275 usba`usba_hubdi_bind_root_hub+0x168 uhci`uhci_attach+0x191 genunix`devi_attach+0x75 genunix`attach_node+0xb2 genunix`i_ndi_config_node+0x97 genunix`i_ddi_attachchild+0x4b genunix`i_ddi_attach_node_hierarchy+0x49 genunix`attach_driver_nodes+0x49 genunix`ddi_hold_installed_driver+0xe3 genunix`attach_drivers+0x28 value ------------- Distribution ------------- count 33554432 | 0 67108864 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3 134217728 | 0 |
Lorsqu'une fonction finit par en appeler une autre, le compilateur peut s'engager dans une optimisation des appels terminaux, au cours de laquelle la fonction appelée réutilise le cadre de pile du programme appelant. Cette procédure est le plus souvent utilisée dans l'architecture SPARC, où le compilateur réutilise la fenêtre d'enregistrement du programme appelant dans la fonction appelée, afin de réduire la pression dans la fenêtre d'enregistrement.
La présence de cette optimisation provoque le déclenchement de la fonction d'appel de la sonde return avant la sonde entry de la fonction appelée. Ce tri peut s'avérer confus. Par exemple, si vous souhaitez enregistrer toutes les fonctions appelées à partir d'une fonction donnée ainsi que toutes les fonctions appelées par cette même fonction, vous pouvez utiliser le script suivant :
fbt::foo:entry { self->traceme = 1; } fbt:::entry /self->traceme/ { printf("called %s", probefunc); } fbt::foo:return /self->traceme/ { self->traceme = 0; }
Toutefois, si foo() se termine par un appel terminal optimisé, la fonction d'appel terminal et par conséquent toutes les fonctions appelées par cette dernière ne seront pas capturées. Il est impossible d'annuler l'optimisation du noyau de manière dynamique à la volée et DTrace ne donne aucune information erronée à propos de la structure du code. Par conséquent, vous devez savoir à quel moment l'optimisation des appels terminaux peut être utilisée.
L'optimisation des appels terminaux est susceptible d'être utilisée dans un code source similaire à l'exemple suivant :
return (bar());
ou dans un code source similaire à ce qui suit :
(void) bar(); return;
À l'inverse, un code source de fonction se terminant comme dans l'exemple suivant ne peut pas avoir son appel à bar() optimisé, car l'appel à bar() n'est pas un appel terminal :
bar(); return (rval);
Vous pouvez déterminer si un appel a fait l'objet d'une optimisation d'appels terminaux en ayant recours à la technique suivante :
Lors de l'exécution de DTrace, procédez au suivi de arg0 de la sonde return en question. arg0 contient le décalage de l'instruction de retour de la fonction.
Après l'arrêt de DTrace, utilisez mdb(1) pour observer la fonction. Si le décalage ayant fait l'objet d'un suivi contient un appel à une autre fonction au lieu d'une instruction à renvoyer de la fonction, cela signifie que l'appel a fait l'objet d'une optimisation d'appels terminaux.
En raison de l'architecture du jeu d'instructions, l'optimisation d'appels terminaux est beaucoup plus courante sur les systèmes SPARC que sur les systèmes x86. L'exemple suivant utilise mdb pour découvrir l'optimisation d'appels terminaux dans la fonction dup() du noyau :
# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}' |
Lorsque cette commande est en cours d'exécution, exécutez un programme qui effectue un dup(2), tel qu'un processus bash. La commande ci-dessus doit fournir une sortie similaire à l'exemple suivant :
dup+0x10 ^C |
À présent, étudiez la fonction avec mdb :
# echo "dup::dis" | mdb -k dup: sra %o0, 0, %o0 dup+4: mov %o7, %g1 dup+8: clr %o2 dup+0xc: clr %o1 dup+0x10: call -0x1278 <fcntl> dup+0x14: mov %g1, %o7 |
La sortie montre que dup+0x10 est un appel à la fonction fcntl() et non une instruction ret. Par conséquent, l'appel à fcntl() constitue un exemple d'optimisation d'appels terminaux.
Vous pouvez rencontrer des fonctions qui semblent entrer mais n'être jamais renvoyées ou inversement. Ces fonctions rares sont généralement des routines d'assemblage codées manuellement qui se connectent à la partie centrale d'autres fonctions d'assemblage codées manuellement. Ces fonctions ne doivent pas empêcher l'analyse : La fonction connectée à l'entrée doit toujours renvoyer au programme appelant de la fonction connectée au retour. Cela signifie que si vous activez toutes les sondes FBT vous devez voir l'entrée dans une fonction et le retour d'une autre fonction à la même profondeur de pile.
Certaines fonctions ne peuvent pas être instrumentées par FBT. La nature exacte des fonctions instrumentables sont spécifiques à l'architecture du jeu d'instructions.
Les fonctions ne créant pas de cadre de pile sur les systèmes x86 ne peuvent pas être instrumentées par FBT. Étant donné que le jeu d'enregistrement pour les systèmes x86 est extraordinairement petit, la plupart des fonctions doivent placer les données sur la pile et par conséquent, créer un cadre de pile. Toutefois, certaines fonctions x86 ne créent pas de cadre de pile et ne peuvent donc pas être instrumentées. Les chiffres réels varient, mais en règle générale, moins de cinq pour cent des fonctions ne peuvent pas être instrumentées sur la plate-forme x86.
Les routines terminales codées manuellement dans un langage d'assemblage sur les systèmes SPARC ne peuvent pas être instrumentées par FBT. La majorité du noyau est écrit en C et toutes les fonctions écrites en C peuvent être instrumentées par FBT.
FBT fonctionne en modifiant de manière dynamique le texte de noyau. Étant donné que les points d'arrêt fonctionnent également en modifiant le texte de noyau, si un point d'arrêt du noyau est placé à un site d'entrée ou de retour avant le chargement de DTrace, FBT refuse de fournir une sonde pour la fonction, même si le point d'arrêt du noyau est supprimé ultérieurement. Si le point d'arrêt du noyau est placé après le chargement de DTrace, le point d'arrêt du noyau et la sonde DTrace correspondront au même point du texte. Dans cette situation, le point d'arrêt se déclenche en premier, puis la sonde se déclenche lorsque le programme de débogage effectue une reprise sur le noyau. Il est recommandé de ne pas utiliser les points d'arrêt simultanément avec DTrace. Si des points d'arrêt sont requis, utilisez l'action breakpoint() de DTrace à la place.
Le noyau Solaris peut charger et décharger dynamiquement des modules de noyau. Lorsque FBT est chargé et qu'un module est chargé de manière dynamique, FBT fournit automatiquement de nouvelles sondes associées au nouveau module. Si un module chargé dispose de sondes FBT dont l'activation a été annulée, le module peut être déchargé ; les sondes correspondantes seront détruites lors du déchargement du module. Si un module chargé dispose de sondes FBT activées, le module est considéré occupé et ne peut pas être déchargé.
Le fournisseur FBT utilise le mécanisme de stabilité pour décrire ses stabilités, comme illustré dans le tableau suivant. Pour plus d'informations sur le mécanisme de stabilité, reportez-vous au Chapitre39Stabilité.
Élément |
Stabilité des noms |
Stabilité des données |
Classe de dépendance |
---|---|---|---|
Fournisseur |
En cours d'évolution |
En cours d'évolution |
ISA |
Module |
Privé |
Privé |
Inconnu |
Fonction |
Privé |
Privé |
Inconnu |
Nom |
En cours d'évolution |
En cours d'évolution |
ISA |
Arguments |
Privé |
Privé |
ISA |
Étant donné que FBT expose l'implémentation du noyau, aucun élément le concernant n'est stable et le nom du module et de la fonction ainsi que le stabilité des données sont explicitement privées. La stabilité des données pour le fournisseur et le nom sont en cours d'évolution mais les autres stabilités de données sont privées : il s'agit d'artefacts de l'implémentation actuelle. La classe de dépendance de FBT est ISA : lorsque FBT est disponible sur toutes les architectures de jeux d'instructions, il n'y a aucune garantie que FBT sera disponible sur de futures architectures de jeux d'instructions arbitraires.