Cuidados com a família de funções scanf()

- por Sergio Prado

Categorias: Linguagem C Tags: ,

Em uma biblioteca C padrão, estamos acostumados a usar as funções da família printf(), que enviam para um stream de saída uma string formatada. Este stream de saída pode ser um arquivo, ponteiro para uma string ou mesmo um terminal.

Já a também presente família de funções scanf() faz o processo inverso, processando uma entrada de dados de acordo com um formato especificado. Esta entrada de dados pode vir de um arquivo, ponteiro para uma string ou mesmo a entrada padrão do terminal.

Alguns protótipos desta família de funções:

1
2
3
int scanf(const char *format, ...);
int fscanf (FILE *file, const char *format, ...);
int sscanf (const char *str, const char *format, ...);

Uma descrição bem mais completa destas funções você pode encontrar aqui e aqui.

Mas quando usamos estas funções? Basicamente quando temos uma entrada formatada, e queremos ler e carregar esta entrada formatada em uma ou mais variáveis. Podem ser dados digitados em um teclado matricial ou mesmo vindos de um protocolo de comunicação.

Vamos pensar no seguinte problema: temos dois dispositivos se comunicando através de uma interface RS232, e o protocolo de comunicaçao especifica que o dispositivo configurado como Master pode configurar remotamente o dispositivo Slave. O dispositivo Master pode, por exemplo, enviar um comando de atualização de data/hora para o Slave.

Para efeitos de estudo, vamos ignorar aqui campos comuns de um protocolo de comunicação, como STX, ETX, tipo de registro e checksum. Os campos correspondentes às informações de data/hora terão o seguinte formato, em ASCII:

1
2
3
4
5
6
7
8
9
10
11
"DDMMAAAAhhmmss***************", onde:
 
DD.............dia (2 caracteres)
MM.............mes (2 caracteres)
AAAA...........ano (4 caracteres)
hh.............hora (2 caracteres)
mm.............minuto (2 caracteres)
ss.............segundo (2 caracteres)
********.......dia da semana (15 caracteres)
 
Exemplo: "28062010204501Segunda-feira  ".

Como podemos desenvolver uma rotina em C para que o dispositivo Slave possa tratar este campo e configurar corretamente seu RTC? Uma das formas mais comuns é ler campo a campo e fazer a conversão, conforme abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void setaDataHora(const char *dthr)
{
    int dia, mes, ano, hora, min, seg;
    char diaSemana[30];
    char aux[5];
 
    /* dia */
    strncpy(aux, dthr, 2);
    aux[2] = 0;
    dia = atoi(aux);
 
    /* mes */
    strncpy(aux, &dthr[2], 2);
    aux[2] = 0;
    mes = atoi(aux);
 
    /* ano */
    strncpy(aux, &dthr[4], 4);
    aux[4] = 0;
    ano = atoi(aux);
 
    /* hora */
    strncpy(aux, &dthr[8], 2);
    aux[2] = 0;
    hora = atoi(aux);
 
    /* minuto */
    strncpy(aux, &dthr[10], 2);
    aux[2] = 0;
    min = atoi(aux);
 
    /* segundo */
    strncpy(aux, &dthr[12], 2);
    aux[2] = 0;
    seg = atoi(aux);
 
    /* dia da semana */
    strcpy(diaSemana, &dthr[14]);
 
    /* configurar aqui data/hora no hardware */
    ...
}

Esta função funciona perfeitamente, porém ficou grande, repetitiva e de difícil manutenção. Usando a função sscanf, podemos reduzí-la a apenas uma linha:

1
2
3
4
5
6
7
8
9
10
11
12
void setaDataHora(const char *dthr)
{
    int dia, mes, ano, hora, min, seg;
    char diaSemana[30];
    char aux[5];
 
    sscanf(dthr, "%02d%02d%04d%02d%02d%02d%s",
           &dia, &mes, &ano, &hora, &min, &seg, diaSemana);
 
    /* configurar aqui data/hora no hardware */
    ...
}

Veja que usamos a função sscanf() para escanear a string “dthr” de acordo com determinado formato e setar as variáveis de data/hora. A solução é muito boa, mas com algumas armadilhas que precisamos tomar cuidado.

Os principais problemas com a família de funções scanf() estão relacionados à segurança. Podemos dizer que esta função é eficiente, mas burra. Se a string passada não possuir exatamente o formato desejado, o comportamento da função será indefinido, que poderá ir de simplesmente não ler corretamente os dados formatados, até gerar um Buffer Overflow ou Segmentation Fault durante sua execução.

Veja alguns testes executados em uma máquina Linux:

1. Execução com os campos corretos:

1
2
3
4
// Buffer recebido no protocolo:
"28062010204501Segunda-feira  "
// Saida
"Data/hora = 28/06/2010 20:45:01 Segunda-feira"

2. Enviando “acidentamente” uma letra no primeiro caractere do campo “dia”

1
2
3
4
// Buffer recebido no protocolo:
"A8062010204501Segunda-feira  "
// Saida:
"Data/hora = 00/13019084/-1076429016 134513192:-1076429004:12939273 r�"

3. Esquecendo de enviar o campo “segundos”:

1
2
3
4
// Buffer recebido no protocolo:
"280620102045Segunda-feira  "
// Saida
"Data/hora = 28/06/2010 20:45:13090825 ��"

Veja a inconsistência que tivemos nos exemplo 2 e 3, quando não fizemos algumas verificações nos dados antes de usar a função sscanf().

Outro problema comum é o Buffer Overflow. No nosso caso veja que estamos lendo o dia da semana e salvando na variável diaSemana. E se esta variável não tiver um tamanho suficiente para conter a string, vamos cair no problema de buffer overflow. E em sistemas com memória protegida, o sistema operacional pode lançar um erro de Segmentation Fault (erro de acesso indevido à uma região de memória protegida). Precisamos garantir que a função scanf(), ao ler a string formatada, não ultrapasse os limites de tamanho das variáveis, principalmente quando se trata de strings.

A função scanf() é mais um exemplo de uma ferramenta que dá poderes extras ao programador C, mas que se não for bem utilizada, pode ter efeitos indesejados.

Um abraço,

Sergio Prado

Faça um Comentário

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.