O objetivo deste trabalho é implementar em C uma função
cria_func
, que recebe o endereço de uma função
f
e a descrição de um conjunto de parâmetros.
A função cria_func
deverá gerar, dinamicamente, o código de uma "nova versão"
de f
. e gravar na região de memória especifidada.
Leia com atenção o enunciado do trabalho e as instruções para a entrega.
Em caso de dúvidas, não invente. Pergunte!
O propósito de gerarmos dinamicamente uma "nova versão" de uma função f
é podermos "amarrar" valores pré-determinados a um ou mais dos parâmetros
de f
.
Dessa forma, não precisaremos passar esses valores como argumentos quando chamarmos
a nova versão gerada.
Considere, por exemplo, um exemplo trivial: uma função que retorna o produto de seus dois parâmetros:
int mult (int x, int y);A função
cria_func
nos permite criar dinamicamente uma nova função,
baseada em mult
,
que sempre retorna o valor de seu parâmetro multiplicado por 10.
Para criar essa nova função, cria_func
amarra
o segundo parâmetro de mult
a um valor fixo (10).
Ou seja, cria_func
constrói, em tempo de execução, o código de uma nova função que chama mult
,
passando dois argumentos: o primeiro é argumento recebido por essa nova função, e o segundo é
o valor constante 10.
A função cria_func
deve ter o protótipo
void cria_func (void* f, DescParam params[], int n, unsigned char codigo[]);onde
f
tem o endereço da função original a ser chamada
pelo código gerado,
o array params
contém a descrição dos parâmetros para
chamar essa função,
n
é o número de parâmetros descritos por
params
e
codigo é um vetor onde deverá ser gravado o código gerado.
O número mínimo de parâmetros é 1, e o máximo é 3!
O tipo DescParam
é definido da seguinte forma:
typedef enum {INT_PAR, PTR_PAR} TipoValor; typedef enum {PARAM, FIX, IND} OrigemValor; typedef struct { TipoValor tipo_val; /* indica o tipo do parametro (inteiro ou ponteiro) */ OrigemValor orig_val; /* indica a origem do valor do parametro */ union { int v_int; void* v_ptr; } valor; /* define o valor ou endereço do valor do parametro (quando amarrado/indireto) */ } DescParam;O campo
orig_val
indica se o parâmetro deve ser "amarrado" ou
não; ele pode conter os seguintes valores:
PARAM
: o parâmetro não é amarrado, ou seja, deve ser recebido pela
nova função e repassado à função origin.
FIX
: o parâmetro deve ser amarrado a um valor constante,
fornecido no campo valor
.
IND
: o parâmetro deve ser amarrado a uma variável,
cujo endereço é fornecido no campo valor
. Isto é, deve ser
passado à função original o valor corrente dessa variável.
O arquivo cria_func.h
contém as definições acima, e
pode ser obtido AQUI. O trabalho deve seguir
estritamente as definições constantes nesse arquivo.
O programa abaixo usa cria_func
para criar dinamicamente uma nova versão de mult
que multiplica seu parâmetro por 10, e depois chama essa função para obter as dezenas de 1 a 100:
#include <stdio.h> #include "cria_func.h" typedef int (*func_ptr) (int x); int mult(int x, int y) { return x * y; } int main (void) { DescParam params[2]; func_ptr f_mult; int i; unsigned char codigo[500]; params[0].tipo_val = INT_PAR; /* o primeiro parãmetro de mult é int */ params[0].orig_val = PARAM; /* a nova função repassa seu parämetro */ params[1].tipo_val = INT_PAR; /* o segundo parâmetro de mult é int */ params[1].orig_val = FIX; /* a nova função passa para mult a constante 10 */ params[1].valor.v_int = 10; cria_func (mult, params, 2, codigo); f_mult = (func_ptr) codigo; for (i = 1; i <=10; i++) { printf("%d\n", f_mult(i)); /* a nova função só recebe um argumento */ } return 0; }
Na variação do programa mostrada abaixo, obtemos as dezenas de 1 a 100 amarrando o primeiro parâmetro a uma variável e o segundo parâmetro ao valor constante 10. Neste caso, não passamos nenhum argumento para a função gerada dinamicamente.
Note que devemos passar, na descrição do primeiro parâmetro, o endereço da variável à qual o parâmetro está amarrado:
#include <stdio.h> #include "cria_func.h" typedef int (*func_ptr) (); int mult(int x, int y) { return x * y; } int main (void) { DescParam params[2]; func_ptr f_mult; int i; unsigned char codigo[500]; params[0].tipo_val = INT_PAR; /* a nova função passa para mult um valor inteiro */ params[0].orig_val = IND; /* que é o valor corrente da variavel i */ params[0].valor.v_ptr = &i; params[1].tipo_val = INT_PAR; /* o segundo argumento passado para mult é a constante 10 */ params[1].orig_val = FIX; params[1].valor.v_int = 10; cria_func (mult, params, 2, codigo); f_mult = (func_ptr) codigo; for (i = 1; i <=10; i++) { printf("%d\n", f_mult()); /* a nova função não recebe argumentos */ } //libera_func(f_mult); <== removido return 0; }
No exemplo a seguir, criamos uma nova versão da função de comparação de
bytes memcmp
,
da biblioteca padrão de C.
A função memcmp
recebe duas strings e um número n,
e compara os n primeiros bytes das duas strings, retornando 0 se são iguais.
int memcmp(const void *s1, const void *s2, size_t n);Em outras palavras, podemos usar
memcmp
para verificar se as duas
strings fornecidas possuem um mesmo prefixo, de tamanho n
.
Podemos usar cria_func
para criar uma versão de memcmp
que testa se uma dada string possui um mesmo prefixo que uma string pré-determinada
(ou seja, amarrada),
Veja este exemplo de uso abaixo:
#include <stdio.h> #include <string.h> #include "cria_func.h" typedef int (*func_ptr) (void* candidata, size_t n); char fixa[] = "quero saber se a outra string é um prefixo dessa"; int main (void) { DescParam params[3]; func_ptr mesmo_prefixo; char s[] = "quero saber tudo"; int tam; unsigned char codigo[500]; params[0].tipo_val = PTR_PAR; /* o primeiro parâmetro de memcmp é um ponteiro para char */ params[0].orig_val = FIX; /* a nova função passa para memcmp o endereço da string "fixa" */ params[0].valor.v_ptr = fixa; params[1].tipo_val = PTR_PAR; /* o segundo parâmetro de memcmp é também um ponteiro para char */ params[1].orig_val = PARAM; /* a nova função recebe esse ponteiro e repassa para memcmp */ params[2].tipo_val = INT_PAR; /* o terceiro parâmetro de memcmp é um inteiro */ params[2].orig_val = PARAM; /* a nova função recebe esse inteiro e repassa para memcmp */ cria_func (memcmp, params, 3, codigo); mesmo_prefixo = (func_ptr) codigo; tam = 12; printf ("'%s' tem mesmo prefixo-%d de '%s'? %s\n", s, tam, fixa, mesmo_prefixo (s, tam)?"NAO":"SIM"); tam = strlen(s); printf ("'%s' tem mesmo prefixo-%d de '%s'? %s\n", s, tam, fixa, mesmo_prefixo (s, tam)?"NAO":"SIM"); return 0; }
A função cria_func
deve ser implementada em C,
mas ela deve gerar, no espaço de memória do vetor parâmetro codigo,
um trecho de código em linguagem de máquina que corresponde à nova função.
Ao retornar da função cria_func
seu programa irá chamar
o trecho de código contigo no vetor codigo usando um type cast como
nos exemplos acima.
Para facilitar a criação do código da nova função, você pode utilizar uma variação da instrução call (a instrução call indireto), onde o endereço da função a ser chamada está em um registrador. Por exemplo, a instrução abaixo faz uma chamada para a função cujo endereço foi armazenado no registrador %rax:
call *%rax
De forma geral, cria_func
irá percorrer o array com a descrição
dos parâmetros e gerar um código em linguagem de máquina que:
params
;
Atenção: lembre-se que a localização (registradores) dos parâmetros não amarrados, recebidos pela função gerada dinamicamente, pode não ser a mesma para a chamada à função original. Cuidado para não perder o valor desses parâmetros!
call
(indireto);
Você deve criar um arquivo fonte chamado cria_func.c
contendo as função cria_func
e funções
auxiliares, se for o caso. Esse arquivo não deve
conter uma função main
nem depender de arquivos de cabeçalho
além de cria_func.h
e dos cabeçalhos das bibliotecas padrão!
Para testar o seu programa, crie um outro arquivo, por exemplo teste.c
,
contendo a função main
. Crie seu programa executável, por exemplo
teste
, com a linha
gcc -Wall -Wa,--execstack -o teste cria_func.c teste.c(sem a opção execstack , o sistema operacional abortará o seu programa, por tentar executar um código armazenado na área de dados).
Para testar seu programa, use técnica de TDD (Test Driven Design), na qual testes são escritos antes do código. O propósito é garantir ao desenvolvedor (você) ter um bom entendimento dos requisitos do trabalho antes de implementar o programa. Com isto a automação de testes é praticada desde o início do desenvolvimento, permitindo a elaboração e execução contínua de testes de regressão. Desta forma fortalecemos a criação de um código que nasce simples, testável e próximo aos requisitos do trabalho. Os passos gerais para seguir tal técnica:
Recomendamos fortemente uma implementação gradual, desenvolvendo e testando passo-a-passo cada nova funcionalidade implementada.
Comece, por exemplo, com um esqueleto que declare como variável local da main um vetor unsigned char de tamanho suficientemente grande, coloca um código bem conhecido nessa região. Teste a chamada a essa função gerada dinamicamente. Para obter um código "bem conhecido" você pode compilar um arquivo assembly contendo uma função bem simples (que, por exemplo, retorna o valor do seu parâmetro) usando:
minhamaquina > gcc -c code.sA opção -c é usada para compilar e não gerar o executável. Depois de compilar, veja o código de máquina gerado usando:
minhamaquina > objdump -d code.oA seguir, implemente a geração dinamica do código, e comece a testá-la. Por exemplo, comece usando
cria_func
para gerar um código que chama uma
função que retorna o valor de seu único parâmetro inteiro. Teste primeiro sem amarrar o parâmetro, e
depois o amarrando a um valor fixo e ao valor de uma variável.
Vá então aumentando gradualmente o número e os tipos de parâmetros tratados, testando combinações diferentes. Você pode usar os exemplos dados no enunciado do trabalho, mas faça também outros testes!
Leia com atenção as instruções para entrega a seguir e siga-as estritamente. Atenção para os nomes e formatos dos arquivos!
Devem ser entregues via EAD dois arquivos:
Coloque no início do arquivo fonte, como comentário, os nomes dos integrantes do grupo da seguinte forma:
/* Nome_do_Aluno1 Matricula Turma */ /* Nome_do_Aluno2 Matricula Turma */
Lembre-se que esse arquivo não deve conter a função main
!
Esse relatório deve explicar o que está funcionando e o que não está funcionando. Mostre exemplos de testes executados com sucesso e os que resultaram em erros (se for o caso).
Coloque também no relatório os nomes dos integrantes do grupo.
Coloque na área de texto da tarefa do EAD os nomes e turmas dos integrantes do grupo.