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 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[]) 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.: Provavelmente você terá problemas com dispositivos de segurança do Linux. Inclua a função a seguir no seu código e a chame no início da sua função main.

#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