INF1018 - Software Básico (2023.2)
Segundo Trabalho

Gerador Dinâmico de Funções

ERRATA

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.

Instruções Gerais


Leia com atenção o enunciado do trabalho e as instruções para a entrega. Em caso de dúvidas, não invente. Pergunte!

Amarrando Parâmetros

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.




Especificação das funções

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:

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.




Um exemplo de uso

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;
}



Outro exemplo de uso

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;
}


Implementação

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:

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!

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:


Dicas

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.s
A 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.o
A 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!




Entrega

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:

  1. o arquivo fonte cria_func.c

    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!

  2. um arquivo texto (não envie um .doc ou .docx), chamado relatorio.txt, contendo um pequeno relatório que descreva os testes realizados.

    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.