É realmente interessante como a internet democratizou a informação e tem ajudado muita gente (entendam aqui muitos profissionais de TI... hehehe) a melhorarem profissionalmente. Uma dos mecanismos mais democráticos e livres que a internet introduziu, ou popularizou, foi o "Grupo de discussão", e nesta semana um me em especial me ajudou muito em um problema que eu não estava conseguindo resolver.
Bem, o problema era em um código fonte em "C" que provocava estouro de memória. Problema muito simples por sinal, algo realmente bem básico, mas que me perturbou por um dia inteiro ao passo que no final do dia eu me rendi e postei em um fórum.
Bem, vamos ao problema:
Eu havia escrito uma rotina composta por basicamente duas funções auxiliares e uma função principal, que pode ser descrito abaixo:
/**
* teste.c
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void funcion_2(const char *);
int function_1(const char *);
void funcion_2(const char *arquivo){
function_1(arquivo);
}
int function_1(const char *arquivo){
FILE *fp;
char* linha;
fp = fopen (arquivo, "r");
if (fp == NULL) {
printf ("Houve um erro ao abrir o arquivo.\n");
return 1;
}
while( fgets(linha, 1024, fp) ){
printf( "Nome: %s\n", strtok(linha, ";"));
printf( "Mail: %s\n", strtok(NULL , ";"));
}
fclose(fp);
return 0;
}
int main()
{
funcion_2("/tmp/mail.txt");
function_1("/tmp/mail.txt");
exit(0);
}
/*** FIM teste.c ***/
/* mail.txt */
Maria da Silva;maria@email.com.br
Joao Abrantes;joao@email.com.br
Nelson Jobin;nelson@email.com.br
Jose Sarney;jose@email.com.br
Adriano de Souza;adriano@email.com.br
/* Fim mail.txt */
Eu li, vi um erro básico de inicialização de variável, todavia não consegui ver o motivo de um comportamento estranho que o programa tinha, era ele:
- Quando eu executava a chamada da function_1() diretamente pela função main() o aplicativo executava perfeitamente bem...
- Quando eu executava a chamada da function_2(), que por sua vez faz somente uma chamada a function_1() o programa quebrava retornando uma menssagem de "Falha de segmentação".
Isso me intrigou... Como pode uma simples chamada quebrar o programa? Foi então que recorri a lista, e de pronto vieram diversas respostas, e algumas realmente foram muito boas, tanto que estou compilando aqui para sempre me recordar do erro, e também da solução.
Bem, dentre as respostas vou destacar as do amigo Marcio Gil, que diz:
Amigo Charly,
Para você entender realmente o que aconteceu, você deve entender o funcionamento da "pilha de chamada" (em inglês "call stack").
A pilha é um espaço único em um processo (um programa em execução ou uma thread em um programa multi-processo), que serve para guardar os parâmetros, dados de retorno, variáveis locais, entre outras coisas durante a execução de uma função. Quando a função retorna, todo o espaço utilizado na pilha pela função é "descartado"; embora os dados permaneçam lá, estes dados serão sobre-escritos tão logo outra função seja executada. Na linguagem C++ também ocorre a execução dos destrutores dos objetos armazenados na pilha, fato irrelevante se você trabalha com C puro.
Para entender como funciona uma pilha, recomendo esta leitura:
http://www.ime.usp.br/~pf/algoritmos/aulas/pilha.html
Para mais informações a respeito da pilha de chamada:
http://pt.wikipedia.org/wiki/Pilha_de_chamada
Para entender mais profundamente o funcionamento da pilha:
http://www.codeproject.com/KB/cpp/exceptionhandler.aspx
Agora que você sabe tudo sobre a pilha, fica fácil entender o que aconteceu: "function_1" avança menos na pilha e, por acaso, no local da pilha onde a variável 'linha' está armazenado existe um valor que equivale a um endereço "válido", entre aspas pois os dados lidos pelo fgets serão armazenados sabe-lá-Deus-aonde em algum espaço de memória do seu programa, provavelmente apagando qualquer informação essencial. Então seu programa parece funcionar perfeitamente, mas ele vai travar aleatoriamente depois aparentemente sem explicação alguma.
Já "funcion_2" avança mais na pilha, então a variável 'linha' vai conter outro lixo que, por acaso, não é um endereço "válido", ou melhor, que não pertence ao espaço de memória do seu programa.
Logo ocorre uma falha de segmentação que poderia perfeitamente ter ocorrido também ao se chamar "function_1" diretamente, mas que, absolutamente por acaso, não ocorreu.
Quando você declara
char linha[1025];
como algum colega sugeriu, você está reservando 1025 caracteres na pilha de execução e linha aponta para este local seguro.
Só para complementar, saiba que esta sua questão é uma das maiores fontes de dor de cabeça no mundo da programação em C/C++.
Imagine um sistema travando sem explicação. Então você pode ir cercando o problema até descobrir aonde acontece... para seu desespero o erro acontece mas você não encontra nada de errado.
Na base da tentativa e erro você modifica a parte que está dando erro e o problema acontece em outro lugar. Logo você descobre que se depurar o sistema o erro não acontece, ou acontece outro problema diferente. E agora?
Eu mesmo já experimentei este tipo de cenário. Quando finalmente encontrei a raiz do problema, após inúmeras revisões de código, estava em um local totalmente diferente.
Uma solução pode ser utilizar alguma ferramenta de detecção de vazamentos de memória, como o Valgrind ou o CodeGuard. Este tipo de ferramenta pode detectar que você está utilizando um ponteiro não inicializado, mas nem sempre podem detectar todos as possíveis causas de corrupção de memória.
Mas a melhor defesa contra este tipo de problema é redobrar a atenção.
Marcio Gil.
Outra colaboração muito interessante foi a do Mauro Cordon, que disse:
Ola,
So completando o que disse o Marcio (se me permitir rs):
Uma pratica muito boa na programação C/C++, claro, se você deseja fazer programas e sistemas consistentes, e ter duas versões: uma versão debug e uma versão release (ou final).
-Na versão debug, você deve rechear seu código com assertivas onde você testa os limites de uso de memória, por exemplo a[x], você deve saber se x não passa dos limites de a, e se a e valido. Pra isso, requer que você tenha controle sobre algumas funções como: malloc/new, de preferência, tenha suas próprias funções, e essas funções chamam malloc/new. faz um array com toda memória alocada, com seus tamanhos, programa que alocou, etc. Depois as verificações ficam melhores (tem um livro de um analista da Microsoft que não me lembro o nome que tem umas referencias ótimas sobre essa pratica).
-Na versão debug ainda, seu programa chega ser 10 a 20 vezes maior que a versão final, porem quando você desligar a diretiva de compilação que indica ser DEBUG, todo código de verificação vai pro espaço, e seu código fica enxuto e a maior parte dos erros foram sanados.
-Sabe os tamanho de cada dado e super importante, e ai usar funções que ajudam na defesa disso. por exemplo: não usar strcpy mas strncpy, nao usar sprintf, mas snprintf, etc.
Mauro Cordon.·.
Bem, com esse pequeno exemplo, podemos perceber o quanto é fácil escrevermos aplicativos falhos, vulneráveis e suscetíveis a ataques. Fica aqui o alerta!
Nenhum comentário:
Postar um comentário