Traduza a função abaixo para assembly, criando um arquivo foo.s
:
int foo (int x) {
return x+1;
}
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).
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.
Traduza agora a função abaixo para assembler,
int foo (int x) {
return add(x);
}
foo
com
o objdump
.
Note que o código gerado para a instrução call
é
e8 00 00 00 00Nessa 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.
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!
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