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

  2. Use gcc -c -o foo.o 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. Declare um array de bytes (unsigned char codigo[]) como uma variável local de sua main, 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.


    Seu programa deve converter o endereço do array para um endereço de função. Para isso, declare o tipo "ponteiro para uma função que recebe um int e retorna um int", conforme abaixo:

    typedef int (*funcp) (int x);
    
    Na sua função main atribua o endereço do array a uma variável desse tipo:
    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:
    i = (*f)(10);
    
    Faça isso no seu programa, imprimindo a seguir o valor da variável i para poder verificar se o seu código de máquina foi realmente executado.

    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 a opção -Wa,--execstack, 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. Traduza agora a função abaixo para assembler,

    int foo (int x) {
      return add(x);
    }
    

  5. Observe o código de máquina da nova função foo com o objdump. Note que o código gerado para a instrução call é
    e8 00 00 00 00 
    
    Nessa instrução, o byte e8 representa o código de call, e os quatro bytes seguintes (os bytes 00 neste exemplo), representam o deslocamento da função chamada (add) em relação à instrução seguinte ao call (isto é, a diferença entre os dois endereços: o endereço de add e o endereço da instrução seguinte ao call).

    Esse deslocamento é armazenado em little endian, e pode ser um valor negativo ou positivo, dependendo do endereço da função chamada ser "menor" ou "maior" que o da instrução seguinte ao call.

    Note que o deslocamento NÃO está correto no arquivo .o, pois o endereço da função chamada somente será conhecido no passo de linkedição (a função não está definida no módulo). O montador então preenche a posição do deslocamento com um valor "default", que será depois "corrigido" pelo linkeditor.

  6. Declare agora a função add no seu arquivo C (o arquivo que contém a main):

    int add (int x) {
      return x+1;
    }
    
    e modifique o programa para que ele preencha no array codigo o código de máquina da nova função foo.

    Como você pode obter o endereço de add "programaticamente", seu programa C pode calcular qual deve ser o deslocamento correspondente à chamada de add no seu código de máquina.

    Lembre-se que o seu código está armazenado no espaço reservado para o array e, portanto, o endereço do início do código é o próprio endereço do array. Se, por exemplo, a instrução call começa na posição n do array, a próxima instrução começará na posição n+5 (pois a instrução call tem 5 bytes).

    Modifique agora seu programa C, fazendo com que ele "corrija" a instrução call, preenchendo os quatro bytes após o opcode e8 com o deslocamento calculado. Lembre-se que esse valor deve estar em little-endian!

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

  7. Substitua agora no seu arquivo assembly a instrução call add por jmp add. Observe que o código de máquina dessa instrução é

    e9 00 00 00 00
    
    Assim como num call, o byte e9 representa o código de jmp e os quatro bytes seguintes são o deslocamento, ou diferença, entre o endereço de destino do "jump" e o endereço da próxima instrução. Isto vale para todos os tipos de "jump" (incondicional e condicional).

Obs.: É provável que haja problemas caso declare seu vetor codigo[] como variável global pois um mecanismo de segurança mais recente do Linux impede que seja executado código nesta área de memória de forma trivial. Uma solução simples para contornar este problema é declarar o vetor codigo[] como uma variável local de sua main, ou seja, ocupando espaço no registro de ativação da main. Uma outra opção mais complicada, é usar o trecho de código abaixo, incluindo a função a seguir no seu código e chamando-a no início da sua função main. Você deve passar como parâmetro para a função execpage o endereço do vetor codigo e o seu tamanho (sizeof codigo).

#include <stdint.h>
#include <sys/mman.h>
#include <unistd.h>

#define PAGE_START(P) ((intptr_t)(P)&~(pagesize-1))
#define PAGE_END(P) (((intptr_t)(P)+pagesize-1)&~(pagesize-1))

/*
 * The execpage() function shall change the specified memory pages
 * permissions into executable.
 *
 * void *ptr  = pointer to start of memory buff
 * size_t len = memory buff size in bytes
 *
 * The function returns 0 if successful and -1 if any error is encountered.
 * errono may be used to diagnose the error.
 */
int execpage(void *ptr, size_t len) {
	int ret;

	const long pagesize = sysconf(_SC_PAGE_SIZE);
	if (pagesize == -1)
		return -1;

	ret = mprotect((void *)PAGE_START(ptr),
		 PAGE_END((intptr_t)ptr + len) - PAGE_START(ptr),
		 PROT_READ | PROT_WRITE | PROT_EXEC);
	if (ret == -1)
		return -1;

	return 0;
}

#undef PAGE_START
#undef PAGE_END