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

Em 28/06/2010, em Linguagem C, por sergioprado
Neste post vou falar sobre a família de funções scanf(), e os cuida­dos que deve­mos ter ao utilizá-la.
Com­par­tilhe!
  • Twitter
  • Facebook
  • LinkedIn
  • del.icio.us
  • Digg
  • email
  • PDF
  • Print

Em uma biblioteca C padrão, estamos acostumados a usar as funções da família printf(), que envia 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:

"DDMMAAAAhhmmss***************", onde:

1
2
3
4
5
6
7
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

VN:F [1.9.0_1079]
Rating: 6.7/10 (3 votes cast)
Cuidados com a família de funções scanf(), 6.7 out of 10 based on 3 ratings
Compartilhe!
  • Twitter
  • Facebook
  • LinkedIn
  • del.icio.us
  • Digg
  • email
  • PDF
  • Print

Posts relacionados:

  1. Playstation 3 e o bug do ano bissexto
  2. Memory leak em linguagem C
  3. Análise estática de código
Tags:  

2 Respostas para “Cuidados com a família de funções scanf()”

  1. Erick Nogueira do Nascimento Erick Nogueira do Nascimento disse:

    Sér­gio,

    As funções da família scanf retor­nam o número de matches (casa­men­tos) bem suce­di­dos entre os cam­pos na string de for­matação e o que foi encon­trado na string de entrada, ou seja, retor­nam o número de atribuições bem suce­di­das (veja, por exem­plo: http://www.gnu.org/s/libc/manual/html_node/Formatted-Input-Functions.html#Formatted-Input-Functions).
    Com isso, para ver­i­ficar se a string de entrada sat­is­faz a string de for­matação, é só fazer o seguinte no seu exemplo:

    int r;

    if ((r = sscanf(dither, “%02d%02d%04d%02d%02d%02d%s”,
        &dia, &mes, &ano, &hora, &min, &seg, iaSe­m­ana) != 7)
       {
          // Ocor­reu algum erro
          if (r == EOF) {
             // a string de entrada ter­mi­nou antes de casar todos os cam­pos
          } else {
             // Nao con­seguiu casar algum campo
          }
       }

    Para especi­ficar o número máx­imo de car­ac­teres que podem ser lidos em um buffer, é só uti­lizar o parâmetro width (largura) do “%s”, da mesma forma que você fez em “%02d”, no seu exem­plo, ficaria “%15s”. Mais detal­hes em http://www.gnu.org/s/libc/manual/html_node/String-Input-Conversions.html#String-Input-Conversions

    Abraço.

  2. sergioprado sergioprado disse:

    É ver­dade Erick.

    Obri­gado por com­ple­men­tar o artigo!

    Um abraço,

    Ser­gio Prado

Deixe um comentário