Gravando o código de funções em arquivos

É possível gravar o código de máquina de uma função de um programa já compilado  em um arquivo, e ler esse arquivo em outro programa e executar essa função?

SIM!

Parece até mágica, mas o curioso é que tudo funciona! Só devemos tomar alguns cuidados para saber onde começa e termina uma função, e lembrarmos que a função gravada não será linkeditada, logo ela só pode acessar parâmetros – nenhum acesso a variáveis ou funções externas é permitido, ou teremos um erro de execução (access violation). Vamos ver como fazer isso. Primeiro, o código de um programa que grava uma função em um arquivo. A função que faz essa mágica é gravaFuncao. Há duas funções que serão gravadas: multiplica e multiplicaOtimizada. São funções para multiplicar matrizes tiradas desse post. Até aqui não há mistério: o endereço de uma função é o endereço do início de seu código. Para saber onde ela termina, basta olhar para o endereço da função imediatamente seguinte a ela no programa fonte. Para saber seu tamanho, basta subtrair os ponteiros (convertendo para const char* pois o C++ não permite aritmética de ponteiros para função).  Sabendo o tamanho da função, basta escrever o seu código em um arquivo!

Código Fonte do programa que grava uma função:

#include <cstdlib>
#include <iostream>

using namespace std;

typedef const char* EnderecoFuncao;

void gravaFuncao( const char* nomeArquivo, EnderecoFuncao funcao, EnderecoFuncao funcaoSeguinte ) {
  FILE *arquivo = fopen( nomeArquivo, "wb" );
  int tamanhoFuncao = funcaoSeguinte - funcao;

  if( arquivo == NULL ) {
    cout << "Erro ao abrir arquivo " << nomeArquivo << endl;
    exit( 0 );
  }

  fwrite( funcao, 1, tamanhoFuncao, arquivo );
  fclose( arquivo );

  cout << "Funcao gravada no arquivo \"" << nomeArquivo << "\"." << endl;
  cout << "Tamanho da funcao: " << tamanhoFuncao << " bytes." << endl << endl;
}

const int N = 1000;

struct Matrizes {
  double a[N][N], b[N][N], c[N][N];
};

void multiplica( Matrizes& m ) {
  for( int i = 0; i < N; i++ )
    for( int j = 0; j < N; j++ )
      for( int k = 0; k < N; k++ )
        m.c[i][j] += m.a[i][k] * m.b[k][j];
}   

void multiplicaOtimizada( Matrizes& m ) {
  for( int i = 0; i < N; i++ )
    for( int k = 0; k < N; k++ )
      for( int j = 0; j < N; j++ )
        m.c[i][j] += m.a[i][k] * m.b[k][j];
}  

void fim() {} 

int main(int argc, char *argv[])
{
  gravaFuncao( "funcao1.dat", (EnderecoFuncao) multiplica, (EnderecoFuncao) multiplicaOtimizada );
  gravaFuncao( "funcao2.dat", (EnderecoFuncao) multiplicaOtimizada, (EnderecoFuncao) fim ); 

  system("PAUSE");
  return 0;
}

Saída:

Funcao gravada no arquivo "funcao1.dat".
Tamanho da funcao: 160 bytes.

Funcao gravada no arquivo "funcao2.dat".
Tamanho da funcao: 208 bytes.

Detalhes:

Note que é preciso uma função vazia chamada fim para podermos obter o final da função multiplicaOtimizada. E obviamente, é necessário um typecast para EnderecoFuncao para podermos chamar gravaFuncao (EnderecoFuncao é apenas um typedef para const char*).

Vale dizer também que as funções gravadas, apesar de muito parecidas, têm tamanhos diferentes. Isso ocorre porque a segunda função é bastante otimizada pelo compilador após a inversão do loop em j com o loop em k.

Pronto! Já temos dois arquivos com o código das funções acima.  Tudo o que temos de fazer agora é ler esse código e executá-lo passando a estrutura com as matrizes. Mostrarei como fazer isso na sequência deste post…

“Don’t tell me what I can’t do”

Bookmark and Share

Post to Twitter

3 Comments

  1. Bruno Buss says:

    Nossa! Já tinha tentado fazer isso uma vez, mas não tive essa sacada de pegar o endereço da função e da “próxima função”.

    O otimizador não pode tentar trocar as funções de lugar, a ordem em que elas ficam, na hora de gerar o código de máquina?

    Estou bastante curioso para saber como é feito a chamada da função =]
    Meu chute… seria alocar um espaço em memória (char[] ?), “ler” a função do arquivo e jogar os bytes nesta área e chamar a função como se chamaria por uma function pointer? Uma dúvida que tenho é… esse espaço deveria ser na heap ou na stack (ou tanto faz)?

    SOs com sistemas de proteção de memória W^X (Write or Execute) talvez impliquem com isso…

    Mas muito legal mesmo, seria uma forma de implementar plug-ins em um programa =]

  2. Zimbrão says:

    Que eu saiba, o otimizador não muda as funções de lugar pois isso não traz nenhum ganho de desempenho, mas…
    A chamada é quase isso, exceto que você tem de alocar o espaço via API para poder dar permissão de execução a essa área de memória – heap e pilha não têm permissão de execução.
    Agora, para implementar plug-ins acho melhor o mecanismo de carga de bibliotecas dinâmicas durante a execução… esse exemplo aqui é só devaneio! É só para reafirmar a frase do post: “Don’t tell me what I can’t do” rsrs

  3. Rufor says:

    Hi,
    Everything dynamic and very positively! :)
    Have a nice day

Leave a Reply