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

Em 28/06/2010, em Linguagem C, por Sergio Prado

Em uma bib­lioteca C padrão, esta­mos acos­tu­ma­dos a usar as funções da família printf(), que envia para um stream de saída uma string for­matada. Este stream de saída pode ser um arquivo, pon­teiro para uma string ou mesmo um terminal.

Já a tam­bém pre­sente família de funções scanf() faz o processo inverso, proces­sando uma entrada de dados de acordo com um for­mato especi­fi­cado. Esta entrada de dados pode vir de um arquivo, pon­teiro para uma string ou mesmo a entrada padrão do terminal.

Alguns pro­tóti­pos 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 com­pleta destas funções você pode encon­trar aqui e aqui.

Mas quando usamos estas funções? Basi­ca­mente quando temos uma entrada for­matada, e quer­e­mos ler e car­regar esta entrada for­matada em uma ou mais var­iáveis. Podem ser dados dig­i­ta­dos em um teclado matri­cial ou mesmo vin­dos de um pro­to­colo de comunicação.

Vamos pen­sar no seguinte prob­lema: temos dois dis­pos­i­tivos se comu­ni­cando através de uma inter­face RS232, e o pro­to­colo de comu­ni­caçao especi­fica que o dis­pos­i­tivo con­fig­u­rado como Mas­ter pode con­fig­u­rar remo­ta­mente o dis­pos­i­tivo Slave. O dis­pos­i­tivo Mas­ter pode, por exem­plo, enviar um comando de atu­al­iza­ção de data/hora para o Slave.

Para efeitos de estudo, vamos igno­rar aqui cam­pos comuns de um pro­to­colo de comu­ni­cação, como STX, ETX, tipo de reg­istro e check­sum. Os cam­pos cor­re­spon­dentes às infor­mações de data/hora terão o seguinte for­mato, em ASCII:

“DDM­MAAAAhh­mmss***************”, 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)

Exem­plo: “28062010204501Segunda-feira  ”.

Como podemos desen­volver uma rotina em C para que o dis­pos­i­tivo Slave possa tratar este campo e con­fig­u­rar cor­re­ta­mente seu RTC? Uma das for­mas mais comuns é ler campo a campo e fazer a con­ver­são, con­forme 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 fun­ciona per­feita­mente, porém ficou grande, repet­i­tiva e de difí­cil manutenção. Usando a função sscanf, podemos reduzí-la a ape­nas 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 deter­mi­nado for­mato e setar as var­iáveis de data/hora. A solução é muito boa, mas com algu­mas armadil­has que pre­cisamos tomar cuidado.

Os prin­ci­pais prob­le­mas com a família de funções scanf() estão rela­ciona­dos à segu­rança. Podemos dizer que esta função é efi­ciente, mas burra. Se a string pas­sada não pos­suir exata­mente o for­mato dese­jado, o com­por­ta­mento da função será indefinido, que poderá ir de sim­ples­mente não ler cor­re­ta­mente os dados for­mata­dos, até gerar um Buffer Over­flow ou Seg­men­ta­tion Fault durante sua execução.

Veja alguns testes exe­cu­ta­dos em uma máquina Linux:

1. Exe­cução com os cam­pos corretos:

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

2. Enviando “aci­den­ta­mente” uma letra no primeiro car­ac­tere do campo “dia”

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

3. Esque­cendo 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 incon­sistên­cia que tive­mos nos exem­plo 2 e 3, quando não fize­mos algu­mas ver­i­fi­cações nos dados antes de usar a função sscanf().

Outro prob­lema comum é o Buffer Over­flow. No nosso caso veja que esta­mos lendo o dia da sem­ana e sal­vando na var­iável diaSe­m­ana. E se esta var­iável não tiver um tamanho sufi­ciente para con­ter a string, vamos cair no prob­lema de buffer over­flow. E em sis­temas com memória pro­te­gida, o sis­tema opera­cional pode lançar um erro de Seg­men­ta­tion Fault (erro de acesso inde­v­ido à uma região de memória pro­te­gida). Pre­cisamos garan­tir que a função scanf(), ao ler a string for­matada, não ultra­passe os lim­ites de tamanho das var­iáveis, prin­ci­pal­mente quando se trata de strings.

A função scanf() é mais um exem­plo de uma fer­ra­menta que dá poderes extras ao pro­gra­mador C, mas que se não for bem uti­lizada, pode ter efeitos indesejados.

Um abraço,

Ser­gio Prado

VN:F [1.9.13_1145]
Rat­ing: 6.7/10 (3 votes cast)
Cuida­dos com a família de funções scanf(), 6.7 out of 10 based on 3 ratings

Sem posts relacionados.

Tags:  
  • Erick Nogueira do Nascimento

    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.

    VA:F [1.9.13_1145]
    Rating: 0.0/5 (0 votes cast)
    • http://www.sergioprado.org ser­gio­prado

      É ver­dade Erick.

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

      Um abraço,

      Ser­gio Prado

      VA:F [1.9.13_1145]
      Rating: 0.0/5 (0 votes cast)
  • http://www.fdavid.com.br Fran­cis David

    Posso usar o scanf para retornar, por exem­plo, o e-mail de uma string, sem saber exata­mente sua posi­cao ?
    e.g: “ini­cio da string asdf gfd ert teset@dd.com masis oiiuytr­rwe ddff outro@hotmail.com.br final da string”

    VA:F [1.9.13_1145]
    Rating: 0.0/5 (0 votes cast)
  • Cas­sio

    Muito Bom!!! Parabéns!

    VA:F [1.9.13_1145]
    Rating: 0.0/5 (0 votes cast)