Manuel de suivi dynamique Solaris

Chapitre 20 Fournisseur fbt

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.

Sondes

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.

Arguments des sondes

Sondes entry

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.

Sondes return

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.

Exemples

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

Optimisation des appels terminaux

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 :

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.

Fonctions d'assemblage

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.

Limitations du jeu d'instructions

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.

Limitations x86

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.

Limitations SPARC

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.

Interaction du point d'arrêt

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.

Chargement des modules

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é.

Stabilité

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.