De acordo com as Leis 12.965/2014 e 13.709/2018, que regulam o uso da Internet e o tratamento de dados pessoais no Brasil, ao me inscrever na newsletter do portal DICAS-L, autorizo o envio de notificações por e-mail ou outros meios e declaro estar ciente e concordar com seus Termos de Uso e Política de Privacidade.
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.
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.
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
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, ®s);
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 :).