Criando Funções Com Número Variável De Argumentos Em C

Introdução

Dentre as funções mais usuais do C, a printf() é aquela que tem definição um pouco estranha. Essa função, diferentemente das outras, recebe um número variável de argumentos, isto é, a quantidade de parâmetros passados para a função não tem um valor fixo. Esse tipo de comportamento pode ser adquirido também por funções dos usuários utilizando macros definidas no arquivo cabeçalho <stdarg.h>.

Funções com número variável de argumentos são muito comuns e úteis em programação, pois possuem um comportamento flexível e dinâmico por não limitar o número de parâmetros. Além disso, através do uso de strings de formatos, esse dinamismo é ainda maior por causa da facilidade de recuperar e converter parâmetros de vários tipos.

Hoje aprenderemos como criar esse tipo de função e também a forma correta de usá-las para se evitar, em alguns casos, bugs famosos como vulnerabilidades de format string.

Declarando uma função varargs

Funções varargs são aquelas com número de parâmetros variável. Para se declarar essas funções, utilizamos os três pontos como último parâmetro na declaração da mesma:

void va_minha_funcao(char *primeiro_parametro, ...) ;

É obrigatório ter pelo menos um parâmetro com nome, nesse caso o primeiro_parametro. Veja que agora essa função receberá um número indeterminado de parâmetros de qualquer tipo. Alguns exemplos de chamadas dessa função são:

va_minha_funcao("aaa", "bbb") ;
va_minha_funcao("aaa", "bbb", 3.5, -478) ;
va_minha_funcao("aaa") ;
va_minha_funcao() ; # Chamada incorreta. Deve haver pelo menos um parâmetro.

Observe que todos os parâmetros antes do três pontos devem ser passados obrigatoriamente, e é por isso que a última chamada de va_minha_funcao() está incorreta.

A seguinte declaração está incorreta:

void va_minha_funcao(...) ;

É preciso declarar pelo menos um parâmetro com nome. Um modo correto de declaração é:

void va_minha_funcao(char *nome, int max, ...) ;

Nesse caso podemos afirmar que essa função recebe no mínimo dois parâmetros, o primeiro um ponteiro e o segundo um inteiro. Os demais parâmetros podem ser de qualquer tipo.

As macros va_start(), va_arg(), va_end() e o tipo va_list

Antes de criarmos nossa função varargs temos que incluir a biblioteca <stdarg.h>

# include <stdarg.h>

Feito isso, teremos acesso a um tipo especial, o va_list, que é usado pelas macros va_start(), va_arg() e va_end() [1].

A va_start() deve ser chamada obrigatoriamente antes de qualquer tentativa de acesso aos parâmetros. Sua definição é a seguinte:

# include <stdarg.h>
void va_start(va_list ap, parmN)

Primeiro va_start() inicializa o parâmetro especial ap para ser usado corretamente por va_arg(). A chamada dessa macro é obrigatória. O parâmetro parmN é o nome do parâmetro mais à direita da nossa função, isso é, parmN é o nome do parâmetro antes dos três pontos.  Exemplo, se declararmos uma função de cálculo de média como:

void va_media_alunos(char *nome_aluno, int qtd_notas, ...) ;

A chamada à va_start() deve ser feita assim:

void va_media_alunos(char *nome_aluno, int qtd_notas, ...) {
     va_list ap ;
     /* ... */
     va_start(ap, qtd_notas) ;
     /* ... */
}

Uma vez inicializados, os parâmetros podem ser acessados pela macro va_arg().

# include <stdarg.h>
TIPO va_arg(va_list ap, TIPO);

Essa macro recebe um tipo no segundo parâmetro indicando o tipo do parâmetro atual sendo analisado.  Esse TIPO nada mais é que os tipos primitivos  de C (char, double, int, etc), ponteiros (char *, double *, int *, etc) e até estruturas. Desse modo, devemos saber previamente qual o tipo de cada parâmetro que nossa função recebe.

À medida que chamamos va_arg(), um novo parâmetro é retornado. O primeiro parâmetro é aquele logo após os três pontos. Além disso, essa macro não sabe quando o último parâmetro é encontrado, ficando a cargo do programador criar algum método para percorrer os argumentos corretamente (ex: usando um contador ou passando o NULL como último argumento).

No caso da nossa função de média, ela ficaria assim:

double va_media_alunos(char *nome_aluno, int qtd_notas, ...) {
     va_list ap ;
     double nota_final = 0;
     int i ;

     va_start(ap, qtd_notas) ;

     for(i=0; i < qtd_notas; i++) {
          nota_final += va_arg(ap, double) ;
     }

    /* Após o processamento dos parâmetros, va_end() deve ser chamada. */
    va_end(ap) ;

     /* ... */
}

A função va_media_alunos() pode receber parâmetros de qualquer tipo, porém ao se utilizar o parâmetro double em va_arg(), ela só tratará corretamente os do tipo double. Toda vez que va_arg() acessa um parâmetro, ela primeiramente o converte para double antes de retorná-lo. O detalhe é que se o tipo passado não for um double, o valor retornado é indefinido e portanto passível de ocorrência de erros de conversão de tipos, como visto no post passado em [2].

Para se ter um maior controle sobre os parâmetros e seus tipos, temos que utilizar strings de formato.

Minha primeira função varargs

A seguir um pequeno programa que utiliza a função va_media_alunos():

/*
 * [va_media.c]
 * Programa exemplo que utiliza uma função de média
 * com número de parâmetros variável.
 *
 * [Autor]
 * Marcos Paulo Ferreira (Daemonio)
 * undefinido gmail com
 * https://daemoniolabs.wordpress.com
 *
 * [Compilação]
 * $ gcc -o va_media va_media.c
 *
 * [Uso]
 * $ ./va_media
 *
 * Versão 1.0 by daemonio @ Sun Sep  9 11:34:54 BRT 2012
 */
#include <stdio.h>
#include <stdlib.h>

/* Necessário para os va_* */
#include <stdarg.h>

/* Protótipo da função. */
double va_media_alunos(char *, int , ...) ;

double va_media_alunos(char *nome_aluno, int qtd_notas, ...) {
    va_list ap ; /* tipo especial. */
    double nota_final ; /* nota final. */
    int i;

    /* Linha obrigatória. Aqui iniciamos os
     * parâmetros para serem usados. */
    va_start(ap, qtd_notas) ;

    nota_final=0 ;
    for(i=0; i < qtd_notas; i++) {
        /* va_arg: retorna um parâmetro a cada chamada.
         * O parâmetro é convertido para double. */
        nota_final += va_arg(ap, double) ;
    }

    /* Faz uma média */
    nota_final /= qtd_notas ; 

    /* Salvar nota final indexando pelo nome, por exemplo. */
    // salvar_nota_final(nome_aluno, nota_final) ;

    /* Após o processamento dos parâmetros, va_end() deve ser chamada. */
    va_end(ap) ;

    return nota_final ;
}

int main(int argc, char **argv) {
    double nota ;

    /* Exemplos de execução. */

    nota = va_media_alunos("Pedro", 6, 1.2, 1.8, .6, 20.0, 30.0, 29.0) ;
    printf("Nome %s e nota final: %lf\n", "Pedro", nota) ;

    nota = va_media_alunos("Maria", 5, 0.2, 0.4, 1, 20.0, 29.0) ;
    printf("Nome %s e nota final: %lf\n", "Maria", nota) ;

    nota = va_media_alunos("Madruga", 2, 10.0, 5.0) ;
    printf("Nome %s e nota final: %lf\n", "Madruga", nota) ;

    nota = va_media_alunos("Eu",    6, 2.0, 2.0, 2.0, 24.0, 30.0, 30.0) ;
    printf("Nome %s e nota final: %lf\n", "Eu", nota) ;

    return 0;
}
/* EOF */

Compilando:

$ gcc -o va_media va_media.c

O programa em si não exige entradas:

$ ./va_media
Nome Pedro e nota final: 13.766667
Nome Maria e nota final: 6.120001
Nome Madruga e nota final: 7.500000
Nome Eu e nota final: 15.000000

Funções com strings de formato

Funções da família *printf() utilizam caracteres especiais, começados com %, para se obter e converter os parâmetros passados de uma maneira fácil e organizada. A boa notícia é que podemos incluir essas strings de formato em nossas funções também.

As funções declaradas como v*printf() (ex: vprintf(), vfprintf(), etc) recebem uma string de formato e também um parâmetro va_list. Essas funções extraem e convertem os parâmetros de va_list de acordo com o que é passado na string de formato.

$ man 3 printf
# include <stdarg>

 int vprintf(const char *format, va_list ap);
 int vfprintf(FILE *stream, const char *format, va_list ap);
 int vsprintf(char *str, const char *format, va_list ap);
 int vsnprintf(char *str, size_t size, const char *format, va_list ap);

O parâmetro format recebe um buffer contendo strings de formato (%c, %d, %f, %lf, etc). O parâmetro, do tipo va_list, aponta para a lista variável de parâmetros da nossa função. O comportamento de cada uma dessas funções é equivalente ao das funções que estamos acostumados. Por exemplo, a função vsprintf() funciona do mesmo modo que sprintf() sendo que a única diferença está em como os parâmetros são passados. A vsprintf() aceita parâmetros via tipo va_list e por outro lado, em sprintf(), os parâmetros são passados diretamente na própria chamada da função.

É bastante comum servidores criarem funções de log ou de aviso que aceitam string de formato. Um exemplo disso é a função send_error() abaixo:

#include <stdio.h>
#include <stdarg.h>

int send_error(int sockfd, char *fmt, ...) {
    va_list ap ;
    char buffer[BUFSIZ] ;

    va_start(ap, fmt) ;

    vsnprintf(buffer, BUFSIZ, fmt, ap) ;

    /* envia dados */
    write(sockfd, buffer, strlen(buffer)) ;

    va_end(ap) ;
}

Seu uso é semelhante ao de funções como printf().

    send_error(1, "Erro: usuário `%s' inválido (ID: %d).\n", "daemonio", 6667) ;

A saída será:

Erro: usuário `daemonio' inválido (ID: 6667).

Um detalhe muito importante é: NÃO passe para fmt um buffer que o usuário tem controle. Entenda o motivo a seguir.

Vulnerabilidade de Format String

Se um usuário mal intencionado tem controle sobre a string de formato, ele provavelmente conseguirá controlar o fluxo de seu programa com a finalidade de executar comandos nocivos ao sistema.

Vulnerabilidades de string de formato são conhecidas há bastante tempo e os primeiros exploits saíram no fim da década de 90. Mesmo assim, apesar de ser conhecida há bastante tempo, muitas aplicações hoje em dia ainda estão vulneráveis a este tipo de ameaça.

A ameaça existe quando o programador utiliza dados do usuário em parâmetros que aceitam string de formato. Veja um exemplo de programa vulnerável:

/* [vulfmt.c] */

#include <stdio.h>

int main(int argc, char **argv) {
    char *minha_senha_secreta="pipapaparapo" ;

    printf(argv[1]) ; /* ERRO!! O modo correto é: printf("%s", argv[1]); */

    putchar('\n') ;

    return 0 ;
}

Compile:

$ cc -o vulfmt vulfmt.c

Agora execute:

$ ./vulfmt stringqualquer
stringqualquer

$ ./vulfmt stringqualquer%x
stringqualquerbfbba1c4

$ ./vulfmt %x%x%x%x%x%x%s
bfa3bc94bfa3bca047c97f7d47e103c41000804843bpipapaparapo

O que aconteceria se msg contivesse caracteres como %c, %x e %p? A resposta é que eles seriam interpretados como strings de formato. Desse modo, um atacante tem acesso a valores acima do frame pointer de printf(). Alguns desses valores não passam de lixo, porém é possível recuperar valores de variáveis importantes, como a senha do programa acima. Outro valor importante é o endereço de retorno da função que chamou printf(), pois alterando-se esse endereço, um atacante conseguirá executar dados arbitrários no programa, comprometendo a segurança do sistema.

A dica que fica para se evitar dores de cabeça é sempre passar a string de formato diretamente para as funções, sem utilizar um buffer controlado pelo usuário para isso.

Não entrarei em detalhes sobre esse assunto. Mais detalhes na referência [3].

Conclusão

Funções com número variável de argumentos tornam o programa mais flexível e dinâmico, pois um número de parâmetros passados para elas não é fixo. Com um simples uso das macros va_start(), va_arg(), va_end() e o tipo especial va_list, conseguimos criar esse tipo de função.

Quando mais controle sobre os dados é necessário, o programador pode optar por utilizar strings de formato, iguais àquelas usadas pela printf(). Funções da família v*printf() recebem uma string de formato e um parâmetro va_list e fazem todo o trabalho pesado, como extrair e converter os parâmetros.

Vimos que quando um usuário controla a string de formato dessas funções, ele poderá controlar o fluxo do programa e executar comandos nocivos para o sistema. A solução para esse problema é simples: sempre passe a string de formato diretamente, sem utilizar um buffer controlado pelo usuário para isso.

Referências

[1] 9.9. Variable numbers of arguments by Mike Banahan, Declan Brady and Mark Doran (Acessado em: Setembro/2012)
http://publications.gbdirect.co.uk/c_book/chapter9/stdarg.html

[2] Conversão de Tipos em C (Parte I) by Daemonio (Acessado em: Setembro/2012)
https://daemoniolabs.wordpress.com/2012/09/02/conversao-de-tipos-em-c-parte-i/

[3] Format String by scut (team teso) (Acessado em: Setembro/2012)
http://crypto.stanford.edu/cs155old/cs155-spring08/papers/formatstring-1.2.pdf

[4] 25.2 Writing a “varargs” Function by Steve Summit (Acessado em: Setembro/2012)
http://www.eskimo.com/~scs/cclass/int/sx11b.html

[5] How does printf handle its arguments? by stackoverflow (Acessado em: Setembro/2012)
http://stackoverflow.com/questions/2433295/how-does-printf-handle-its-arguments

4 pensamentos sobre “Criando Funções Com Número Variável De Argumentos Em C

  1. Quando esse código é executado, cada versão da função func é escolhida, e chamada, de acordo com as correspondências entre as listas de parâmetros. Você vai conseguir usar essa capacidade, que é uma grande característica do C , uma vez que você encare sobrecarga de função como uma solução para muitos dos problemas de programação. Por exemplo, se você cria uma função para inicializar um módulo, você pode ter uma chamada diferente, para a mesma função, dependendo da característica do parâmetro que é passado; um string, um inteiro, um ponto flutuante, e assim por diante.

Deixe um comentário