Traduza a função abaixo para assembly, criando um arquivo foo.s
:
int foo (int x) {
return x+1;
}
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).
unsigned char codigo[]
)
preenchido com o código de máquina visto no item anterior
(não declare esse array como uma variável local).
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"
(isto é, usando o operador &),
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).