#include <stdio.h>
int assembly(void);
int main(void)
{
printf("Resultado: %d\n", assembly());
return 0;
}
Entendendo o acesso à memória RAM na prática
O processador acessa dados da memória principal usando o que é chamado de endereço de memória. Para o hardware da memória RAM o endereço nada mais é que um valor numérico que serve como índice para indicar qual byte deve ser acessado na memória. Imagine a memória RAM como uma grande array com bytes sequenciais, onde o endereço de memória é o índice de cada byte. Esse "índice" é chamado de endereço físico (physical address).
Porém o acesso a operandos na memória principal é feito definindo alguns fatores que, após serem calculados pelo processador, resultam no endereço físico que será utilizado a partir do barramento de endereço (address bus) para acessar aquela região da memória. Do ponto de vista do programador são apenas algumas somas e multiplicações.
O endereçamento de um operando também pode ser chamado de endereço efetivo, ou em inglês, effective address.
Não tente ler ou modificar a memória com nossa PoC ainda. No final do tópico eu falo sobre a instrução LEA que pode ser usada para testar o endereçamento.
No código de máquina da arquitetura IA-16 existe um byte chamado ModR/M que serve para especificar algumas informações relacionadas ao acesso de (R)egistradores e/ou (M)emória. O endereçamento em IA-16 é totalmente especificado nesse byte e ele nos permite fazer um cálculo no seguinte formato:
REG + REG + DESLOCAMENTO
Onde REG
seria o nome de um registrador e DESLOCAMENTO
um valor numérico também somado ao endereço. Os registradores BX, BP, SI e DI
podem ser utilizados. Enquanto o deslocamento é um valor de 8 ou 16 bits.
Nesse cálculo um dos registradores é usado como base, o endereço inicial, e o outro é usado como índice, um valor numérico a ser somado à base assim como o deslocamento. Os registradores BX e BP
são usados para base enquanto SI e DI
são usados para índice. Perceba que não é possível somar base+base
e nem índice+índice
.
Alguns exemplos para facilitar o entendimento:
Em IA-32 o código de máquina tem também o byte SIB que é um novo modo de endereçamento. Enquanto em IA-16 nós temos apenas uma base e um índice, em IA-32 nós ganhamos também um fator de escala. O fator de escala é basicamente um número que irá multiplicar o valor de índice.
O valor do fator de escala pode ser 1, 2, 4 ou 8.
O registrador de índice pode ser qualquer um dos registradores de propósito geral exceto ESP.
O registrador de base pode ser qualquer registrador geral.
O deslocamento pode ser de 8 ou 32 bits.
Exemplos:
SIB é sigla para Scale, Index and Base. Que são os três valores usados para calcular o endereço efetivo.
Em x86-64 segue a mesma premissa de IA-32 com alguns adendos:
É possível usar registradores de 32 ou 64 bit.
Os registradores de R8 a R15 ou R8D a R15D podem ser usados como base ou índice.
Não é possível mesclar registradores de 32 e 64 bits em um mesmo endereçamento.
O byte ModR/M tem um novo endereçamento RIP + deslocamento. Onde o deslocamento é necessariamente de 32 bits.
Exemplos:
Na sintaxe do NASM para usar um endereçamento relativo ao RIP deve-se usar a keyword rel para determinar que se trata de um endereço relativo. Também é possível usar a diretiva default rel para setar o endereçamento como relativo por padrão. Exemplo:
Na configuração padrão do NASM o endereçamento é montado como um endereço absoluto (default abs). Mais à frente irei abordar o assunto de Position-independent executable (PIE) e aí entenderemos qual é a utilidade de se usar um endereço relativo ao RIP.
Cuidado para não se confundir em relação ao fator de escala. Veja por exemplo esta instrução 64-bit:
Apesar de 3 não ser um valor válido de escala o NASM irá montar o código sem apresentar erros. Isso acontece porque ele converteu a instrução para a seguinte:
Ele usa RBX tanto como base como também índice e usa o fator de escala 2. Resultando no mesmo valor que se multiplicasse RBX por 3. Esse é um truque do NASM que pode levar ao erro, por exemplo:
Dessa vez acusaria erro já que a base foi explicitada. Lembre-se que os fatores de escala válidos são 1, 2, 4 ou 8.
A instrução LEA, sigla para Load Effective Address, calcula o endereço efetivo do segundo operando e armazena o resultado do cálculo em um registrador. Essa instrução pode ser útil para testar o cálculo do effective address e ver os resultados usando nossa PoC, conforme exemplo abaixo: