← Voltar aos artigosCarga e armazenamento no ARM: LDR, STR e seu primeiro laço

Carga e armazenamento no ARM: LDR, STR e seu primeiro laço

No artigo anterior vimos a arquitetura ARM por cima: banco de registradores, modos e o conjunto de instruções. Mas arquitetura só "cola" quando a gente suja a mão. Neste texto vamos exercitar a parte que mais assusta quem chega do C ou do Java: como o ARM acessa memória. Spoiler — o ARM segue o modelo load/store, e isso muda tudo.

Load/store: só LDR e STR tocam a memória

Em arquiteturas CISC (x86, por exemplo) você faz add eax, [ebx] — soma direto lendo da memória. O ARM não. Todas as instruções de processamento de dados (add, sub, and, mov…) operam entre registradores ou com immediatos. Para ler ou escrever memória existem exatamente duas portas:

  • LDRload: memória → registrador.
  • STRstore: registrador → memória.

Isso parece limitação, mas é o que torna o conjunto enxuto e o ciclo de instrução previsível — exatamente o que o arm-jitter emula quando decodifica cada opcode. Se você veio do hello world no arm-box, já usou ldr r1, =msg sem perceber: era um LDR buscando o endereço da string numa literal pool.

Os modos de endereçamento do LDR/STR

O endereço de memória é calculado a partir de um registrador base (r0 no nosso exemplo) mais um offset. Há três sabores, e vale decorar porque eles aparecem em quase todo programa real:

ldr   r1, [r0]          @ offset 0 (sem deslocamento)
ldr   r1, [r0, #8]      @ pré-indexado: usa r0+8 (r0 NÃO muda)
ldr   r1, [r0, #8]!     @ pré-indexado com writeback: r0 = r0+8
ldr   r1, [r0], #8      @ pós-indexado: usa r0, DEPOIS r0 = r0+8

O pós-indexado ([r0], #4) é o truque que usaremos para varrer um vetor: a gente lê o elemento e já empurra o ponteiro para o próximo, numa instrução só.

Nosso objetivo: somar um vetor no arm-box

Vamos escrever um programa que soma os elementos de um vetor em .data e encerra com a soma como exit code — assim dá para conferir o resultado com echo $?. O arm-box roda binários ARMv5–ARMv7, então montamos com -march=armv5te (a mesma ISA do hello world).

.syntax unified
.text
.global _start

.data
nums:
    .word 3, 7, 1, 9, 4
len = (. - nums) / 4
result:
    .word 0

.text
_start:
    ldr   r0, =nums        @ r0 = ponteiro para o vetor
    mov   r1, #0           @ r1 = soma acumulada
    mov   r2, #0           @ r2 = índice i
    ldr   r3, =len         @ r3 = total de elementos

loop:
    cmp   r2, r3           @ i == len?
    bge   done             @ sim -> termina
    ldr   r4, [r0], #4     @ r4 = nums[i]; r0 += 4 (próximo)
    add   r1, r1, r4       @ soma += nums[i]
    add   r2, r2, #1       @ i++
    b     loop             @ volta

done:
    ldr   r5, =result
    str   r1, [r5]         @ armazena a soma na memória (STR!)
    mov   r0, r1           @ código de saída = soma
    mov   r7, #1           @ syscall 1 = exit
    svc   #0

Passo a passo do que está acontecendo:

  • len = (. - nums) / 4 é calculado em tempo de link: a diferença de endereço dividida por 4 dá a quantidade de palavras de 32 bits.
  • O laço usa cmp + bge (branch if greater or equal) como um while (i < len).
  • ldr r4, [r0], #4 é o coração do varredor: lê a palavra e já avança o ponteiro. É por isso que não precisamos de r2 para calcular endereços — o ponteiro anda sozinho.
  • str r1, [r5] é o nosso único STR: prova que a soma realmente voltou para a memória, não ficou só no registrador.

Montando, ligando e rodando

Geramos um ELF estático idêntico ao do hello world:

arm-none-eabi-as -march=armv5te sum.s -o sum.o
arm-none-eabi-ld -static -o sum.elf sum.o

Rodamos com o --check para garantir que o JIT e o interpretador IR do arm-jitter concordam, e pedimos o código de saída:

java -jar target/armbox-1.0-SNAPSHOT.jar --check sum.elf
echo $?   # 24  (3+7+1+9+4)

Se aparecer 24, parabéns: você acabou de ler e escrever memória de verdade num núcleo ARM emulado em Java. O --check não reclamou, então JIT e interpretador calcularam a mesma soma.

Por que isso importa para o curso

Carga e armazenamento são a fronteira entre a CPU e o mundo exterior. Todo acesso a arrays, structs, pilha de chamadas e até argumentos de função passa por LDR/STR. Dominar os modos de endereçamento (especialmente o writeback) é o que separa quem "copia assembly de stackoverflow" de quem entende o que o processador faz. No próximo texto vamos abrir o objdump desse ELF e decodificar instruções ARM à mão, bit a bit — a mesma leitura que o decodificador do arm-jitter faz. Até lá!