Falha na segmentação (leitura de imagem)

1. Falha na segmentação (leitura de imagem)

Pedro Victor
Nerdiarretado

(usa Arch Linux)

Enviado em 16/05/2019 - 22:01h

Saudações amigos (as)!

Estou desenvolvendo um programa que leia uma imagem ppm, porém no final da leitura, diz que existe uma falha de segmentação. Alguém sabe me ajudar por qual motivo está acontecendo isso.
Caso alguém queria baixar a imagem padrão (https://raw.githubusercontent.com/eltonvs/ppm-processor/master/lena.ppm)



#include <stdio.h>
#include <string.h>
#include <stdlib.h>


typedef struct {
unsigned char red, green, blue;
}pixel;

int main (int argc, char** argv) {
FILE *imagemAtual;
FILE *imagemNova;

char codigo[3]; // Variável para verificar se a imagem ppm é P3
char nomeImagem[50];
int i, j; // Variáveis que faram o controle dos laços for
int larg, alt, max;

// Leitura da imagem
printf("Digite o nome da imagem:\t");
scanf("%s", nomeImagem);
imagemAtual = fopen (nomeImagem, "r");

if (imagemAtual== NULL) {
printf("Ocorreu algum erro ao abrir a imagem %s\n", nomeImagem);
exit(1);
}

// Código da imagem
fscanf(imagemAtual, "%s", codigo);
if(strcmp(codigo, "P3") != 0 ) // Verificação do arquivo, para ver se o mesmo é PPM
{
printf("Código não suportado!\n%s\n",codigo);
fclose(imagemAtual);
return 0;
}

// Cabeça da Imagem
fscanf(imagemAtual, "%i", &larg);
fscanf(imagemAtual, "%i", &alt);
fscanf(imagemAtual, "%i", &max);

printf("Largura = %i | Altura = %i | MAX = %i\n", larg, alt, max);

// Saída da Imagem
imagemNova = fopen("teste.ppm", "wb");
if (imagemNova == NULL) {
printf("Erro ao abrir a imagem\n");
return 0;
}

// ALTURA X LARGURA
pixel **RGB = (pixel**)malloc(alt*sizeof(pixel*));
for (i = 0; i < larg; i++)
{
RGB[i] = (pixel*)malloc(larg*sizeof(pixel));
}



// Leitura das informações que estão na matriz da imagem original
for (i = 0; i < larg; i++)
{
for ( j = 0; j < alt; j++)
{
fscanf(imagemAtual, "%c", &RGB[i][j].red);
fscanf(imagemAtual, "%c", &RGB[i][j].green);
fscanf(imagemAtual, "%c", &RGB[i][j].blue);
}
}


// Gravar dados
fprintf(imagemNova, "P3\n%i %i\n%i", alt, larg, max);
for (i= 0;i < larg; i++)
{
for (j= 0;j < alt; j++)
{
fprintf(imagemNova, "%c", RGB[i][j].red);
fprintf(imagemNova, "%c", RGB[i][j].green);
fprintf(imagemNova, "%c", RGB[i][j].blue);
}
}



// FIM DESSA DESGRAÇA
fclose(imagemAtual);
fclose(imagemNova);
return 0;



}



  


2. Re: Falha na segmentação (leitura de imagem)

Paulo
paulo1205

(usa Ubuntu)

Enviado em 17/05/2019 - 10:53h

Seria bom você verificar se todas as chamadas a fscanf() foram bem sucedidas. Minha suspeita é de que alguma delas venha a falhar, e isso comprometa de modo negativo os valores de altura e largura.

Eu não vi no seu programa nenhuma provisão para tratar comentários no arquivo PPM. Se um comentário aparecer no arquivo, há uma grande chance de você deixar as invocações de fscanf() em maus lençóis.


... “Principium sapientiae timor Domini, et scientia sanctorum prudentia.” (Proverbia 9:10)


3. Re: Falha na segmentação (leitura de imagem)

Pedro Victor
Nerdiarretado

(usa Arch Linux)

Enviado em 17/05/2019 - 12:44h

paulo1205 escreveu:
Eu não vi no seu programa nenhuma provisão para tratar comentários no arquivo PPM. Se um comentário aparecer no arquivo, há uma grande chance de você deixar as invocações de fscanf() em maus lençóis.


Olá Paulo.
Poderia me explicar melhor essa questão dos comentários na imagem, não entendi muito bem. Desde já muito obrigado pela ajuda



4. Re: Falha na segmentação (leitura de imagem)

Paulo
paulo1205

(usa Ubuntu)

Enviado em 18/05/2019 - 19:06h

A descrição do formato PPM está em http://netpbm.sourceforge.net/doc/ppm.html. Existem dois formatos, na verdade, que você pode reconhecer pelas assinaturas "P6" ou "P3", que indicam respectivamente o formato completo ou o formato “simples” (“plain”). Em ambos os casos, o cabeçalho do PPM — isto é: tudo o que vem desde a assinatura até imediatamente antes dos componentes RGB de cada pixel — pode conter comentários, que são blocos de texto que não afetam o formato da imagem, mas podem dar informações adicionais sobre ela, tal como o nome do arquivo original, data de criação ou edição, assinatura do programa usado para manipulá-la etc.

O próprio exemplo mostrado no site de arquivo PPM de formato simples (com assinatura “P3”, e que é o mesmo formato que você está usando) inclui um comentário, como você pode ver na transcrição abaixo.

P3
# feep.ppm
4 4
15
0 0 0 0 0 0 0 0 0 15 0 15
0 0 0 0 15 7 0 0 0 0 0 0
0 0 0 0 0 0 0 15 7 0 0 0
15 0 15 0 0 0 0 0 0 0 0 0


No exemplo acima a segunda linha é um comentário, ocorrendo logo após a assinatura e antes do tamanho da imagem e da escala usada para os componentes RGB. O conteúdo do comentário parece ser um nome de arquivo.

Pelo que eu vi no seu programa, ele não conseguiria tratar o arquivo de exemplo mostrado acima, pois ele não dá margem para o aparecimento de comentários no cabeçalho.


Na verdade, todas as operações de leitura do seu programa estão sujeitas a problemas. Um bom programa deve procurar se proteger contra situações indesejáveis durante a leitura de dados, mas seu programa não faz isso.

Mesmo que você tivesse considerado que pode haver variações no cabeçalho devidas à possível presença de comentários em qualquer posição entre a assinatura e o fim do cabeçalho, ainda podem ocorrer situações de arquivos com formato diferente (por exemplo, um PPM com assinatura “P6” em lugar de “P3”), arquivos corrompidos, arquivos truncados etc. Você não deve assumir que as operações de leitura serão bem sucedidas, porque uma eventual falha pode deixar com valor indevido uma variável que será usada num ponto mais a diante no programa, e esse valor indevido pode provocar efeitos deletérios.

Aqui, eu vou analisar partes do seu código, mostrando como robustecê-lo contra erros. Mas antes, quero lhe dar uma orientação geral: sempre que você usar uma função da família de scanf() (que inclui a própria, mas também fscanf(), sscanf(), vscanf() etc.), verifique o valor retornado pela função, comparando-o com a quantidade de conversões contidas na string de formatação. Se o valor for retornado for menor, eis um sinal de alerta de que chegaram dados que não atendiam o formato esperado, e isso quase sempre indica que você não deve usar as variáveis que deveriam ter sido modificadas em decorrência das atribuições, mas provavelmente não o foram ou o foram de modo incompleto.

  // Leitura da imagem
printf("Digite o nome da imagem:\t");
scanf("%s", nomeImagem);


Na invocação acima, além do problema geral de não confirmar se a conversão foi bem-sucedida ou não, você tem três outros problemas, a saber:

  • Você não diz qual a quantidade máxima de caracteres que podem ser salvos em nomeImagem, de modo que se o usuário digitar mais do 49 caracteres (visto que o array foi declarado com 50 caracteres, e o último caráter deveria ser um byte nulo), o programa pode acabar invadindo áreas de memória alocadas para outra variáveis, estruturas de controle usadas durante a execução do programa, ou mesmo memória que não foi de modo nenhum reservada para o programa, e os efeitos disso não-raramente são desastrosos.

  • A conversão "%s" descarta espaços no começo da digitação e para quando encontra o primeiro caráter de espaço após o início da conversão, o que impede o uso de nomes de arquivos que porventura contenham espaços.

  • Admitindo que o usuário encerrou a digitação do nome do arquivo apertando a tecla Enter, o caráter correspondente a essa tecla é interpretado na categoria de espaço em branco, interrompendo a conversão que já tenha sido iniciada, mas permanecendo no buffer de leitura até a próxima operação de leitura. Dependendo de qual operação seja essa, a presença desse (e de outros) espaços em branco pode produzir efeitos indesejados, se você não tomar os devidos cuidados.

Uma forma melhor de fazer essa mesma leitura, tratando o problema geral e os três específicos da string de formatação contendo apenas a conversão "%s" seria a seguinte.
  int a=0, b=0;  // Contadores de caracteres consumidos durante a leitura da linha (ver abaixo).
if(scanf("%49[^\n]%n%*1[\n]%n", nomeImagem, &a, &b)!=1 || a<=0 || b<=a){
/*
Explicação da string de formatação:
• "%49[^\n]" é uma conversão de string (conversão base: "%["), que pode ser formada por
um máximo de 49 caracteres (mais um 50º para guardar o byte nulo que marca o fim da
string) do conjunto de caracteres que exclui a marca de fim de linha ('\n'). A essa conversão
corresponde o ponteiro ‘nomeImagem’ (como você sabe, quando o nome de um array é
usado numa expressão que não seja sua declaração e não seja aplicando os operadores
“sizeof” ou “&” diretamente sobre ele, o array decai para ponteiro para seu primeiro elemento).
• "%n" não é uma conversão (logo, não incrementa o valor de retorno de scanf()), mas provoca
alteração do valor da variável cujo ponteiro lhe corresponda na lista de argumentos de scanf().
No caso desta primeira ocorrência de "%n", a quantidade de caracteres consumida por scanf()
até o momento será armazenada na variável inteira ‘a’.
• "%*1[\n]" não é uma conversão porque o asterisco indica que não existe um ponteiro associ-
ado na lista de argumentos (logo também não incrementa o valor de retorno de scanf()), mas
de resto o comportamento é parecido com o de "%[". Neste caso, especificamente, a função
procura por uma sequência de comprimento 1 formada por caracteres do conjunto composto
apenas pelo caráter '\n'. Ou seja: essa é uma forma de garantir que o primeiro caráter após o
o nome do arquivo será justamente a marca de fim de linha. Se for, essa marca será consu-
mida (i.e. não vai impactar operações de leitura futuras); se não for (por exemplo: por causa
de um nome digitado com 50 caracteres ou mais, ou em razão de algum erro de leitura), a
função scanf() vai interromper o processamento prematuramente, não passando para a etapa
seguinte, que é o...
• ... segundo "%n", que é uma maneira de verificar que realmente havia uma marca de fim de
após o nome do arquivo, e que ela foi consumida por scanf(), fazendo com que o valor de ‘b’
fique maior do que o valor que já foi atribuído a ‘a’.
*/
// Contudo, se cair neste ponto, ou falhou a conversão do nome (scanf(...)<=1, e a==0, porque não
// modificada) ou faltou a marca de fim de linha (b, não modificado, logo igual a 0, portanto b<=a).
fprintf(stderr, "Nome de aquivo inválido. Saindo do programa.\n");
exit(1);
}
// Deste ponto em diante é seguro usar o nome lido.


Eventualmente, em lugar de apenas indicar erro e sair, você poderia querer um tratamento mais sofisticado, como distinguir entre erro de leitura e erro de digitação ou, neste último caso, permitir que o usuário tente novamente. Tais diferentes casos podem ser percebidos por variações no valor de retorno de scanf() (EOF indicando erro de leitura, valores não-negativos mas menores do que o número de conversões indicando sucesso parcial, e valor positivo igual ao número de conversões indicando sucesso total), e mais os valores de ‘a’ e ‘b’ (ou outras, dependendo da complexidade da string de formatação) como forma de garantir que mesmo eventuais separadores serão devidamente consumidos, sem espirrar para operações de leitura futuras.

Se você achar a string de formatação acima cabalística demais, saiba, em primeiro lugar, que eu concordo, mas isso decorre do fato de que scanf() é possivelmente a função mais complexa da biblioteca padrão do C, ao mesmo tempo muito poderosa e difícil de aprender a usar devidamente. Nunca é demais indicar que você leia bem sua documentação.

Mas, além da baixa legibilidade, a string de formatação acima tem outro problema, que é o de conter um número fixo (49), que depende do tamanho de um array declarado em outra parte do programa (que eu nem transcrevi nesta resposta). E se eu quisesse mudar o tamanho usado na declaração do array? Teria de varrer todo o programa encontrando associações indiretas como essa. E se eu quisesse ler um arquivo com nome maior do que 49 caracteres? Teria de localizar não apenas a declaração, para alterar ali o tamanho para alguma coisa maior (lembrando do elemento a mais para o byte nulo), mas também todas as outras operações envolvendo o array em que o tamanho possa fazer diferença.

Por essas e outras complicações, muita gente (inclusive eu) prefere, na hora de ler strings, usar outras funções em lugar de scanf(), tais como fgets() ou getline(). Mas, como não existe almoço grátis, essas funções também têm sua própria dose de idiossincrasias, especialmente se você quiser fazer uma detecção de erros detalhada.

  imagemAtual = fopen (nomeImagem, "r");

if (imagemAtual== NULL) {
printf("Ocorreu algum erro ao abrir a imagem %s\n", nomeImagem);
exit(1);
}

// Código da imagem
fscanf(imagemAtual, "%s", codigo);
if(strcmp(codigo, "P3") != 0 ) // Verificação do arquivo, para ver se o mesmo é PPM
{
printf("Código não suportado!\n%s\n",codigo);
fclose(imagemAtual);
return 0;
}


Aqui há alguns problemas. Os mais óbvios são que você não testou o valor de retorno de fscanf() e não limitou o tamanho da string na conversão "%s", de modo que você está vulnerável a corrupção de memória e falha de segmentação se simplesmente mandar abrir um arquivo cuja primeira palavra tenha 3 ou mais caracteres que não sejam considerados espaços.

Ainda há outras falhas associadas ao uso de "%s". O fato de tal conversão ignorar espaços que apareçam antes de a conversão começar permite que um arquivo que não comece realmente com a assinatura “P3” nas duas primeiras posições do arquivo ainda fosse interpretado como válido (por exemplo, se esse arquivo começar com uma quantidade arbitrária de caracteres considerados como espaços, e o primeiro texto diferente de espaços só ocorra bem lá para frente, como na décima, milésima ou mesmo bilionésima posição do arquivo).

Para além disso, há dois outros problemas relacionados à sinalização de erro, não graves, mas que vale a pena apontar e aprender. Um deles é jogar a mensagem de erros na saída padrão, através de printf(). Como você deve saber, o C possui um canal separado para indicações de erro, chamado de “saída padrão de erros”, ou stderr, e essa separação se dá para que mensagens decorrentes da execução normal e esperada, quando tudo vai bem, não se confundam com mensagens decorrentes de operações que falharam, inclusive permitindo redirecionamentos para locais diferentes. Por isso, convém que você façam como o resto dos programadores do mundo, e jogue suas mensagens de erro no canal destinado para tais mensagens, como eu fiz no código que postei acima. O outro problema é você encerrar uma execução que deu errado com o código de retorno 0, pois esse valor é convencionalmente usado para indica execução bem-sucedida (note que eu, no código que postei acima, chamo “exit(1)” no caso de erro, para que o programa use esse 1 como código indicador de falha).

Considerando tudo isso, eis uma forma mais ou menos óbvia de corrigir os problemas acima.
  int a=0, b=0, c=0;
if(
// Primeiro testa se a função retorna uma conversão bem-sucedida.
fscanf(imagemAtual, " %n%2s%n %n", &a, codigo, &b, &c)!=1 ||
// Depois testa para ver que nenhum espaço foi consumido antes da string.
a!=0 ||
// Depois testa que a string teve tamanho exatamente igual a 2 e que depois dela houve ao menos um espaço.
(b-a)!=2 || c<=b
// Por fim, garante que a assinatura está correta.
strcmp(codigo, "P3")!=0
){
fprintf(stderr, "Tipo de arquivo não reconhecido. Saindo do programa.\n");
return 1;
}


Mas há um jeito mais simples de fazer, que dispensa o array codigo e o uso de strcmp(): é usar fscanf() para procurar por texto arbitrário, e não por conversões ou coisa parecida.
  int a=0;
// A chamada abaixo só altera o valor de ‘a’ se conseguir consumir exatamente um caráter 'P', seguido
// de um caráter '3'. O espaço em branco na strings de formatação até pode não consumir nada, mas
// somente se o próximo caráter (ou falta dele, no caso do fim do arquivo) tornar o formato do arquivo
// inaceitável para o programa.
if(fscanf(imagemAtual, "P3 %n", &a)!=0 || a<3){
fprintf(stderr, "Tipo de arquivo não reconhecido. Saindo do programa.\n");
return 1;
}



// Cabeça da Imagem
fscanf(imagemAtual, "%i", &larg);
fscanf(imagemAtual, "%i", &alt);
fscanf(imagemAtual, "%i", &max);


Essas leituras (e as que vem depois, que eu não mostrei) também têm de ser tratadas com cuidado, testando-se o valor de retorno de cada uma, para se ter certeza de que o arquivo não está corrompido e que os valores lidos fazem sentido. Mas, mais do que isso, antes e depois de cada uma das leituras acima pode haver um ou mais cabeçalhos, e você tem de incluir código que saiba se desvencilhar deles, já que não agregam informação visual ao PPM (ou que os retenha, se você preferir).

Eu fico muito tentado a lhe mostrar o código para descartar os cabeçalhos, mas penso que será um bom exercício para você se conseguir fazê-lo por conta própria.

Uma dica, porém, que lhe pode ser útil é saber usar a função ungetc().


... “Principium sapientiae timor Domini, et scientia sanctorum prudentia.” (Proverbia 9:10)






Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts