Guia de rastreamento dinâmico Solaris

Capítulo 20 Provedor fbt

Este capítulo descreve o provedor Function Boundary Tracing ( FBT), que fornece testes associados à entrada e ao retorno da maioria das funções no kernel do Solaris. A função é a unidade fundamental de texto do programa. Em um sistema bem projetado, cada função realiza uma operação discreta e bem definida em um objeto especificado ou uma série de objetos semelhantes. Portanto, inclusive nos menores sistemas Solaris, o FBT fornecerá na ordem de 20.000 testes.

Semelhante a outros provedores do DTrace, o FBT não tem efeito de teste quando não é ativado explicitamente. Quando ativado, o FBT apenas induz um efeito de teste em funções testadas. Embora a implementação do FBT seja altamente específica da arquitetura do conjunto de instruções, o FBT foi implementado em plataformas SPARC e x86. Para cada conjunto de instruções, existe um pequeno número de funções que não chamam outras funções, e são altamente otimizadas pelo compilador (chamadas de funções em folha) que não podem ser instrumentadas pelo FBT. Testes destas funções não estão presentes no DTrace.

O uso efetivo de testes do FBT requer o conhecimento da implementação do sistema operacional. Portanto, recomenda-se o uso do FBT somente quando você estiver desenvolvendo o software do kernel ou quanto outros provedores não são suficientes. Outros provedores do DTrace, incluindo syscall, sched, proc e io, podem ser usados para responder à maioria das questões de análise do sistema sem requerer o conhecimento da implementação do sistema operacional.

Testes

FBT fornece um teste no limite da maioria das funções do kernel. O limite de uma função é ultrapassado ao entrar na função e retornar da função. O FBT, portanto, fornece duas funções para cada função no kernel: uma na entrada para a função e uma no retorno da função. Estes testes são chamados de entry e return, respectivamente. O nome da função e o nome do módulo são especificados como parte do teste. Todos os testes do FBT especificam um nome de função e um nome de módulo.

Argumentos de teste

Testes entry

Os argumentos de testes entry são os mesmos argumentos da função do kernel do sistema operacional correspondente. Estes argumentos podem ser acessados em forma de digitação, usando a matriz args[]. Estes argumentos podem ser acessados como int64_t usando o arg0 .. variáveis argn.

Testes return

Enquanto uma determinada função possui somente um ponto de entrada, ela pode ter muitos pontos diferentes onde ela retorna para o seu chamador. Você está geralmente interessado no valor que uma função retornou ou no fato de a função ter retornado em vez do caminho de retorno específico que foi tomado. O FBT , portanto, coleta os vários locais de retorno de uma função em um único teste return. Se o caminho de retorno exato for do seu interesse, examine o valor args[0] do teste return, que indica o deslocamento (em bytes) da instrução de retorno no texto da função.

Se a função tiver um valor de retorno, este valor será armazenado em args[1]. Se uma função não tiver um valor de retorno, args[1] não é definida.

Exemplos

Você pode usar o FBT para explorar facilmente a implementação do kernel. O script de exemplo seguinte registra o primeiro ioctl(2) de qualquer processo xclock e, em seguida, segue o caminho de código subseqüente através do kernel:

/*
 * 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);
}

Executar este script acima resultará numa saída semelhante ao exemplo seguinte:


# 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

A saída mostra que um processo xclock chamou ioctl() em um descritor de arquivo que parece estar associado a um soquete.

Você também pode usar o FBT ao tentar entender os drivers do kernel. Por exemplo, o driver ssd(7D) possui muitos caminhos de código, pelos quais EIO pode ser retornado. O FBT pode facilmente ser usado para determinar o caminho de código exato que resultou em uma condição de erro, como mostrado no exemplo seguinte:

fbt:ssd::return
/arg1 == EIO/
{
	printf("%s+%x returned EIO.", probefunc, arg0);
}

Para obter mais informações sobre qualquer retorno de EIO, talvez você queira rastrear especulativamente todos os testes de fbt e usar commit()(ou discard()) com base no valor de retorno de uma função específica. Consulte o Capítulo 13Rastreio especulativo para obter mais detalhes sobre rastreios especulativos.

Alternativamente, você pode usar o FBT para entender as funções chamadas em um módulo especificado. O exemplo seguinte lista todas as funções chamadas em 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

Se você souber a finalidade ou os argumentos de uma função do kernel, use o FBT para entender como ou por que a função está sendo chamada. Por exemplo, putnext(9F) considera um ponteiro para uma estrutura queue(9S) como seu primeiro membro. O membro q_qinfo da estrutura queue é um ponteiro para uma estrutura qinit(9S). O membro qi_minfo da estrutura qinit tem um ponteiro para uma estrutura module_info(9S), que contém o nome do módulo em seu membro mi_idname. O exemplo seguinte reúne suas informações usando o teste do FBT em putnext para controlar chamadas putnext(9F) por nome de módulo:

fbt::putnext:entry
{
	@calls[stringof(args[0]->q_qinfo->qi_minfo->mi_idname)] = count();
}

Executar o script acima resultará numa saída semelhante ao exemplo seguinte:


# 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

Você também pode usar o FBT para determinar o tempo gasto em uma função específica. O exemplo seguinte mostra como determinar os chamadores das rotinas de atraso DDI drv_usecwait(9F) e 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;
}

Este script de exemplo é particularmente interessante de ser executado durante a reinicialização. O Capítulo 36Rastreio anônimo descreve o procedimento para realizar o rastreio anônimo durante a inicialização do sistema. Durante a reinicialização, você talvez veja uma saída semelhante ao exemplo seguinte:


# 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

Otimização de chamada de laço

Quando uma função termina ao chamar outra função, o compilador pode se envolver na otimização de chamada de laço, na qual a função que está sendo chamada reutiliza o quadro de pilha do chamador. Este procedimento é mais usado na arquitetura SPARC, onde o compilador reutiliza a janela do registro do chamador na função que está sendo chamada para minimizar a pressão da janela do registro.

A presença desta otimização faz com que o teste return da função que está chamando seja acionado antes do teste entry da função chamada. Esta ordem pode causar um pouco de confusão. Por exemplo, se você quisesse registrar todas as funções de uma função específica e quaisquer funções que esta função chama, teria que usar o script seguinte:

fbt::foo:entry
{
	self->traceme = 1;
}

fbt:::entry
/self->traceme/
{
	printf("called %s", probefunc);
}

fbt::foo:return
/self->traceme/
{
	self->traceme = 0;
}

Entretanto, se foo() terminar em uma chamada de laço otimizada, a função de chamada de laço, e portanto quaisquer funções que ela chama, não seriam capturadas. O kernel não pode ser dinamicamente desotimizado durante o uso, e o DTrace não quer entrar em uma situação falsa sobre como o código é reestruturado. Portanto, você deve estar ciente de quando a otimização de chamada de laço deve ser usada.

A otimização de chamada de laço deve ser usada em um código-fonte semelhante ao exemplo seguinte:

	return (bar());

Ou em um código-fonte semelhante ao exemplo seguinte:

	(void) bar();
	return;

Contrariamente, o código-fonte da função que termina como o exemplo seguinte não pode ter a sua chamada à bar() otimizada, pois a chamada à bar() não é uma chamada de laço:

	bar();
	return (rval);

Você pode determinar se uma chamada sofreu otimização de chamada de laço usando a técnica seguinte:

Devido à arquitetura do conjunto de instruções, a otimização de chamada de laço é muito mais comum em sistemas SPARC do que em sistemas x86. O exemplo seguinte usa mdb para descobrir a otimização de chamada de laço na função dup() do kernel:


# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}'

Enquanto este comando está sendo executado, execute um programa que realize um dup(2), tal como um processo bash. O comando acima deve fornecer uma saída semelhante ao exemplo seguinte:


dup+0x10
^C

Agora examine a função com 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

A saída mostra que dup+0x10 é uma chamada para a função fcntl() e não uma instrução ret. Portanto, a chamada à fcntl() é um exemplo de otimização de chamada de laço.

Funções de composição

Você talvez observe funções que parecem entrar mas nunca retornar ou vice-versa. Funções tão raras são geralmente rotinas de composição codificadas à mão que se ramificam para o meio das funções de composição codificadas à mão. Estas funções não devem impedir a análise: a função ramificada-para ainda devem retornar para o chamador da função ramificada-de. Ou seja, se você ativar todos os testes de FBT, verá a entrada para uma função e o retorno de outra função na mesma profundidade da pilha.

Limitações do conjunto de instruções

Algumas funções não podem ser instrumentadas pelo FBT. A natureza exata das funções não-instrumentáveis é específica da arquitetura do conjunto de instruções.

Limitações de x86

As funções que não criam um quadro de pilha em sistemas x86 não podem ser instrumentadas por FBT. Como o conjunto de registros de x86 é extraordinariamente pequeno, muitas funções devem colocar dados na pilha e, portanto, criar um quadro de pilha. Entretanto, algumas funções de x86 não criam um quadro de pilha e, portanto, não podem ser instrumentadas. Os números reais variam, mas geralmente menos de 5% das funções não podem ser instrumentadas na plataforma x86.

Limitações de SPARC

As rotinas em folha codificadas à mão em linguagem de composição em sistemas SPARC não podem ser instrumentadas por FBT. A maior parte do kernel é escrita em C, e todas as funções escritas em C podem ser instrumentadas por FBT.

Interação de ponto de interrupção

FBT funciona modificando dinamicamente o texto do kernel. Como os pontos de interrupção do kernel também funcionam modificando o texto do kernel, se um ponto de interrupção do kernel for colocado em um local de entrada ou de retorno antes de carregar o DTrace, o FBT se recusará a fornecer um teste da função, mesmo se o ponto de interrupção do kernel for subseqüentemente removido. Se o ponto de interrupção do kernel for colocado depois de carregar o DTrace, o ponto de interrupção do kernel e o teste do DTrace corresponderão ao mesmo ponto no texto. Nesta situação, o ponto de interrupção disparará primeiro e, em seguida, o teste será acionado quando o depurador continuar o kernel. É recomendado que os pontos de interrupção do kernel não sejam usados ao mesmo tempo que o DTrace. Se os pontos de interrupção forem necessários, use a ação breakpoint () do DTrace.

Carregamento de módulo

O kernel do Solaris pode carregar e descarregar dinamicamente os módulos do kernel. Quando o FBT é carregado e um módulo é dinamicamente carregado, o FBT fornece automaticamente novos testes associados ao novo módulo. Se um módulo carregado possuir testes FBT não-ativados, o módulo pode ser descarregado; os testes correspondentes serão destruídos quando o módulo for descarregado. Se um módulo carregado tiver testes FBT ativados, o módulo será considerado ocupado, e não poderão ser descarregados.

Estabilidade

O provedor FBT usa o mecanismo de estabilidade do DTrace para descrever suas estabilidades, conforme mostrado na tabela seguinte. Para obter mais informações sobre o mecanismo de estabilidade, consulte o Capítulo 39Estabilidade.

Elemento 

Estabilidade de nome 

Estabilidade de dados 

Classe de dependência 

Provedor 

Desenvolvendo 

Desenvolvendo 

ISA

Módulo 

Privada 

Privada 

Desconhecida 

Função 

Privada 

Privada 

Desconhecida 

Nome 

Desenvolvendo 

Desenvolvendo 

ISA

Argumentos 

Privada 

Privada 

ISA

Como o FBT expõe a implementação do kernel, nada sobre seu nome Estável — e do Módulo e da Função e a estabilidade dos dados é explicitamente Privado. A estabilidade dos dados de Provedor e Nome são Desenvolvendo, mas todas as outras estabilidades de dados são Privadas: elas são artefatos da implementação atual. A classe de dependência de FBT é ISA: embora o FBT esteja disponível em todas as arquiteturas do conjunto de instruções atual, não há garantia de que o FBT esteja disponível em futuras arquiteturas de conjunto de instruções arbitrárias.