INF1018 - Software Básico

Código de Máquina

  1. Traduza a função abaixo para assembly, criando um arquivo foo.s.

    int foo (int x) {
      return x+1;
    }
    

    Atenção: Utilize, em sua tradução, o prólogo e o epílogo abaixo. Mesmo que, neste caso, isso não seja necessário, precisaremos do alinhamento da pilha mais tarde (o prólogo faz esse alinhamento).

    .text
    .global foo
    foo: 
      pushq %rbp
      movq  %rsp, %rbp
    
      /* seu código aqui */
    
      leave
      ret
    

  2. Use o comando gcc -c foo.s para traduzir seu programa para linguagem de máquina (o gcc vai gerar um arquivo foo.o).

    Veja qual o código de máquina que seu programa gera, usando o comando objdump -d foo.o (a opção -d do objdump faz um "disassembly" do arquivo objeto).

  3. Escreva agora um programa em C como descrito a seguir. Esse programa deve criar um array de bytes global (unsigned char codigo[]) preenchido com o código de máquina visto no item anterior. Lembre-se que a saída do objdump mostra os códigos das instruções em hexadecimal. Use esses valores para preencher o array.

    Note que o conteúdo do array é o código de máquina de uma função. Nosso objetivo é "chamar" essa função! Para isso, precisamos converter o endereço do array em um endereço de função, armazenar esse endereço em um "ponteiro para uma função", e usar esse ponteiro para realizar a chamada.

    Para realizar a conversão para um ponteiro de função adequado à chamada, precisamos declarar um tipo "ponteiro para uma função que recebe um int e retorna um int", e atribuir o endereço do array a uma variável desse tipo:

    typedef int (*funcp) (int);
    
    funcp f = (funcp)codigo;
    
    O ponteiro f armazena agora o endereço da função, ou seja, o endereço inicial do código da função, armazenado na memória. Você pode então usar f para chamar essa função como se fosse uma função C, fazendo, por exemplo:
    int i;
    i = (*f)(10);
    
    Faça isso no seu programa, lembrando-se de imprimir o valor de i, para confirmar que a função foi chamada.

    Deve ser necessário compilar seu programa com

    gcc -Wall -Wa,--execstack -o seuprograma seuprograma.c

    para permitir a execução do seu código de máquina (sem essa opção, o sistema operacional abortará o seu programa, por tentar executar um código armazenado na área de dados). Execute o programa resultante e verifique a sua saída.

  4. Observe agora o seguinte programa:
    #include 
    typedef int (*funcp) (int);
    
    int add(int x) {
      return x+1;
    }
    
    int foo(funcp f, int x) {
      return (*f)(x);
    }
    
    int main() {
      int i = foo(add, 10);
      printf("%d\n", i);
      return 0;
    }
    
    Repare que a nova função foo recebe um ponteiro para uma função (isto é, um endereço), e chama essa função passando para ela o seu segundo parâmetro. Note que você pode obter, em C, o endereço de uma função usando o seu nome! Execute esse programa, e observe o resultado.

  5. Traduza agora a nova função foo para assembly. Para fazer essa tradução, você vai precisar usar uma instrução assembly que permita chamar a função cujo endereço foi passado como argumento (f). Uma opção é usar uma variação da instrução call, onde o endereço da função a ser chamada está em um registrador. Por exemplo:
    call *%rax
    
    Obtenha o código de máquina da nova função foo com objdump -d .
  6. Modifique o programa, eliminando a declaração de foo em C, e criando um array codigo com o código de máquina de foo. Novamente, precisamos converter o endereço do array em um endereço de função, armazenar esse endereço em um "ponteiro para uma função", e usar esse ponteiro para realizar a chamada.

    Você vai precisar de um tipo diferente para armazenar o endereço da nova foo: "ponteiro para uma função que recebe um ponteiro para função e um int, e retorna um int". Esse tipo pode ser declarado como abaixo:

    typedef int (*funcp2) (funcp, int);
    

    Execute agora o seu programa, e verifique se tudo funciona corretamente!