você está aqui: Home  → Arquivo de Mensagens

Instrumentando e monitorando aplicativos

Colaboração: Evgueni Dodonov

Data de Publicação: 27 de Abril de 2006

Esta dica tem como objetivo descrever alguns dos métodos para análise de desempenho e funcionamento de aplicativos para Linux/BSD/Unix em geral.

Existem inúmeras técnicas para fazer avaliação automática de desempenho de aplicativos, vou descrever as três mais desconhecidas porém as mais poderosas de todas.

1. interceptação de chamadas de funções

A primeira técnica consiste em interceptação de chamadas de funções através de modificação automática do código fonte. É possível fazer esta modificação de várias maneiras, vou descrever a mais símples -- utilizar o SED.

O seguinte script vai interceptar todas as funções de leitura de dados, escrevendo uma mensagem:

  #!/bin/sh
  #
  # DEBUG.SH script
  #
  cat > debug.h << EOF
  #ifndef _MPIDEBUG_H
  #define _MPIDEBUG_H
  
  #include <stdio.h>
  
  #define _read(fd, buf, size) {\
  printf("Estou lendo %d bytes do arquivo %d na posição %p\n",
  size, fd, buf);\
  read(fd, buf, size);\
  }
  
  #endif
  EOF
  
  cat << EOF
  /* Generated with EugeniDebug script */
  #include "debug.h"
  
  EOF
  
  cat $z | sed e 's/\(read\)/_&/g'

Salvando isso como um arquivo e executando com

  # sh debug.sh arquivo.c > novo_arquivo.c

no arquivo novo_arquivo.c teremos o código instrumentado automaticamente. Este código, após compilado, executará o macro "_read" para toda função "read" do código original. Com isto, dá para fazer inúmeras coisas, só ilustrei a mais símples de todas.

2. dlopen() e dlsym()

A segunda técnica não precisa nem de modificação do código fonte. Ela funciona com aplicativos compilados dinamicamente e intercepta as chamadas de funções usando as funções "dlopen()" e "dlsym()".

Vamos fazer o seguinte arquivo:

  /* libdebug.c */
  
  #include <dlfcn.h>
  #include <stdio.h>
  
  static int (*_read)(int fd, void *buf, size_t size);
  
  void _init(void)
  {
  void *libc_handler = RTLD_NEXT;
  _read = dlsym(ibc_handler, "read");
  }
  
  int read(int fd, void *buf, size_t size)
  {
  printf("Estou lendo %d bytes do arquivo %d na posição %p\n",
  size, fd, buf);
  return _read(fd, buf, size);
  }

Compilando este código com seguinte comando:

  # gcc -D_GNU_SOURCE -DPIC -fPIC -D_REENTRANT libdebug.c
  # ld -shared -o libdebug.o -ldl -lc

teremos uma biblioteca libdebug.so.

Agora, colocando esta biblioteca na variável de ambiente LD_PRELOAD, todas as funções "read" dos aplicativos serão interceptadas:

  # LD_PRELOAD=`pwd`/libdebug.so ./a.out

3. função ptrace()

A 1a técnica necessita de modificação do código fonte. A 2a necessita de aplicativos compilados dinamicamente. Uma 3a técnica possibilita interceptar qualquer função do sistema, mesmo para aplicativos compilados estaticamente. Para isto, é utilizada a função ptrace().

A função permite utilizar breakpoints no contexto do kernel, interceptando qualquer chamada do sistema de um aplicativo. Esta técnica é mais complicada e necessita de alguns conhecimentos de arquitetura do Linux e dos registros do processador (EAX, EBX, etc).

O seguinte código interceptará a função "open" utilizando a função ptrace():

  /* debug.c */
  #include <stdio.h>
  #include <sys/ptrace.h>
  
  /* Função para recuperar texto dos registros */
  /* OBS: Eu sei que é complicado de entender mesmo :) */
  void getdata(pid_t child, long addr,
  char *str, int len)
  {   char *laddr;
  int i, j;
  union u {
  long val;
  char chars[sizeof(long)];
  }data;
  i = 0;
  j = len / sizeof(long);
  laddr = str;
  while(i < j) {
  data.val = ptrace(PTRACE_PEEKDATA,
  child, addr + i * 4,
  NULL);
  memcpy(laddr, data.chars, sizeof(long));
  ++i;
  laddr += sizeof(long);
  }
  j = len % sizeof(long);
  if(j != 0) {
  data.val = ptrace(PTRACE_PEEKDATA,
  child, addr + i * 4,
  NULL);
  memcpy(laddr, data.chars, j);
  }
  str[len] = '\0';
  }
  
  /* Processa um syscall interceptado */
  void process_syscall (long syscall, int pid)
  {
  struct user_regs_struct regs;
  switch(syscall)
  {
  case SYS_open:
  {
  /* Interceptamos o open() */
  char str[256];
  ptrace(PTRACE_GETREGS, pid, NULL, &regs);
  getdata(pid, regs.ebx, str, 256);
  printf("Tentativa de abrir %s!\n", str);
  break;
  }
  default: break;
  }
  }
  
  int main(int argc, char **argv)
  {
  int pid;
  
  if (argc < 2)
  {
  fprintf(stderr, "Uso: %s <comando>\n", argv[0]);
  exit(1);
  }
  
  /* Vamos criar um filho para executar o aplicativo proriamente dito */
  pid = fork();
  if (pid == 0)
  {
  /* Program */
  char *command;
  char **params;
  int i;
  command = (char *)malloc(strlen(argv[1]) * sizeof(char));
  params = (char **)malloc((argc) * sizeof(char*) + 1);
  strncpy(command, argv[1], strlen(argv[1]));
  
  for (i=1; i < argc; i++)
  {
  params[i-1] = (char *)malloc(strlen(argv[i]));
  strncpy(params[i-1], argv[i], strlen(argv[i]));
  }
  
  /* Vamos dizer ao kernel que o aplicativo será monitorado */
  ptrace(PTRACE_TRACEME, 0, NULL, NULL);
  execve(command, params, env);
  perror("execve");
  exit(1);
  }
  if (pid > 0)
  {
  /* Enquanto o aplicativo está em execução */
  while(wait(NULL) >= 0)
  {
  long orig_eax;
  /* Interceptar o contexto do programa */
  orig_eax = ptrace(PTRACE_PEEKUSER, pid, 4 * ORIG_EAX, NULL);
  /* Processar a syscall */
  process_syscall(orig_eax, pid);
  /* Voltar o fluxo de execução ao programa rodando */
  ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }
  }
  
  
  return 0;
  }

Depois disto, é só compilar:

  # gcc -o debug debug.c

e rodar alguma coisa

  # ./debug /bin/cat /etc/passwd

Obviamente, somente as chamadas do sistema conhecidos pelo kernel podem ser interceptadas.

Bem, é isso :).



Veja a relação completa dos artigos de Evgueni Dodonov

 

 

Opinião dos Leitores

Seja o primeiro a comentar este artigo
*Nome:
Email:
Me notifique sobre novos comentários nessa página
Oculte meu email
*Texto:
 
  Para publicar seu comentário, digite o código contido na imagem acima
 


Powered by Scriptsmill Comments Script