Endereçamento
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.

Endereçamento em IA-16

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:
mov [bx], ax ; Correto!
mov [bx+si], ax ; Correto!
mov [bp+di], ax ; Correto!
mov [bp+si], ax ; Correto!
mov [bx+di + 0xa1], ax ; Correto!
mov [si], ax ; Correto!
mov [0x1a], ax ; Correto!
mov [dx], ax ; ERRADO!
mov [bx+bp], ax ; ERRADO!
mov [si+di], ax ; ERRADO!

Endereçamento em IA-32

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:
mov [edx], eax ; Correto!
mov [ebx+ebp], eax ; Correto!
mov [esi+edi], eax ; Correto!
mov [esp+ecx], eax ; Correto!
mov [ebx*4 + 0x1a], eax ; Correto!
mov [ebx + ebp*8 + 0xab12cd34], eax ; Correto!
mov [esp + ebx*2], eax ; Correto!
mov [0xffffaaaa], eax ; Correto!
mov [esp*2], eax ; ERRADO!
SIB é sigla para Scale, Index and Base. Que são os três valores usados para calcular o endereço efetivo.

Endereçamento em x86-64

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:
mov [rbx], rax ; Correto!
mov [ebx], rax ; Correto!
mov [r15 + r10*4], rax ; Correto!
mov [r15d + r10d*4], rax ; Correto!
mov [r10 + r15d], rax ; ERRADO!
mov [rsp*2], rax ; ERRADO!
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:
mov [rel my_label], rax
; OU:
default rel
mov [my_label], rax
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.

Truque do NASM

Cuidado para não se confundir em relação ao fator de escala. Veja por exemplo esta instrução 64-bit:
mov [rbx*3], rax
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:
mov [rbx + rbx*2], rax
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:
mov [rsi + rbx*3], rax
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.

Instrução LEA

lea registrador, [endereço]
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:
assembly.asm
main.c
bits 64
global assembly
assembly:
mov rbx, 5
mov rcx, 10
lea eax, [rcx + rbx*2 + 5]
ret
#include <stdio.h>
int assembly(void);
int main(void)
{
printf("Resultado: %d\n", assembly());
return 0;
}