ALAN JAHNEL / Engenharia de software

Artigos

GBA em Assembly — 2. Bits, registradores e memória

Aprenda a acompanhar valores em Assembly ARM: bits, endereços, leituras, escritas, comparações e máscaras, com exercícios passo a passo.

No primeiro capítulo, vimos que escrever em um endereço pode alterar uma variável, configurar o vídeo ou mudar uma imagem. Agora vamos entender o que está sendo escrito e como a CPU chega a esse resultado.

O objetivo é conseguir acompanhar um trecho curto de Assembly usando papel e lápis. Os exemplos deste capítulo são exercícios de leitura, não arquivos completos para montar. A primeira ROM executável aparece no próximo capítulo.

Roteiro deste capítulo

Antes de começar: depois do capítulo 1, você deve conseguir distinguir CPU, ROM, RAM e memória de vídeo. Este capítulo ainda usa exercícios no papel, não um programa executável.

  1. Use a tabela de números para comparar escritas diferentes do mesmo valor. Não é necessário converter um endereço inteiro à mão.
  2. Acompanhe as duas instruções aritméticas: escreva r1 = 20, depois risque 20 e escreva 15 após a subtração.
  3. Siga o exemplo da RAM linha por linha. Faça anotações separadas para r0, r1 e coins; compare com a tabela.
  4. Estude a comparação e os exemplos de máscaras de botões só depois de entender essa distinção. Consulte uma instrução desconhecida pelo link de referência próximo dela.

Ponto de conferência: depois de carregar 20, subtrair 5 e gravar, coins vale 15. Se você omitir a gravação, a RAM continua com 20. Explique o motivo antes de seguir ao capítulo 3. Nenhuma saída de terminal é esperada neste capítulo.

Um número pode ser escrito de três maneiras

A referência reúne as regras de números, sinal e constantes imediatas.

Um bit guarda um de dois valores: zero ou um. Um byte reúne oito bits. Com oito bits podemos representar 256 combinações, que, interpretadas como números sem sinal, vão de 0 a 255.

DecimalBinário com oito bitsHexadecimal
0000000000x00
1000000010x01
15000011110x0F
16000100000x10
255111111110xFF

Não são três espécies de número. São três formas de escrever o mesmo valor. Hexadecimal usa os dígitos de 0 a 9 e as letras A a F; cada dígito corresponde a quatro bits. Assim, dois dígitos descrevem um byte. Essa relação facilita enxergar máscaras de bits e endereços.

Uma palavra, no contexto da CPU ARM desta série, tem 32 bits, ou quatro bytes. Uma meia palavra tem 16 bits, ou dois bytes. Quando alguém disser apenas “escrever na memória”, pergunte também quantos bytes serão escritos.

Registradores: os valores que a CPU tem à mão

Consulte os nomes dos registradores, MOV e ADD, SUB e RSB para acompanhar as operações deste trecho.

Usaremos r0 a r12 para valores temporários. Outros nomes importantes são sp, o ponteiro da pilha; lr, que recebe o endereço de retorno de uma chamada; e pc, associado ao ponto de execução.

Imagine este código de alto nível:

C
int moedas = 20;
moedas = moedas - 5;

Para estudar apenas a conta, podemos manter o saldo em r1:

Assembly ARM
mov r1, #20
sub r1, r1, #5

mov coloca um valor no destino. sub destino, origem, valor calcula a subtração e grava o resultado no destino. O # marca um valor imediato, escrito na própria instrução. Depois das duas linhas, r1 contém 15.

O nome r1 não torna esse valor uma moeda. Se outra rotina sobrescrever r1, o saldo desaparece dali. Para conservar uma variável do jogo, vamos reservar espaço na RAM.

Guardar um endereço é diferente de ler seu conteúdo

Use a seção LDR e STR para comparar as formas com = e com colchetes; as reservas de memória estão em diretivas do assembler.

Este é um dos detalhes mais importantes do curso:

Assembly ARM
ldr r0, =coins
ldr r1, [r0]
sub r1, r1, #5
str r1, [r0]

Na sintaxe do GNU assembler que usamos, ldr r0, =coins é uma pseudoinstrução: o assembler escolhe como colocar o endereço representado por coins em r0. Ela não lê o saldo. A linha seguinte, com colchetes, lê a palavra armazenada naquele endereço.

Depois fazemos a conta em r1 e gravamos o resultado de volta. Sem o último str, a RAM continuaria com o saldo antigo.

Depois da linhar0r1Conteúdo de coins na RAM
AntesIndefinido para este exercícioIndefinido20
ldr r0, =coinsEndereço de coinsIndefinido20
ldr r1, [r0]Mesmo endereço2020
sub r1, r1, #5Mesmo endereço1520
str r1, [r0]Mesmo endereço1515

No jogo, a reserva terá esta forma:

Assembly ARM
.section .bss
.balign 4
coins: .space 4

As linhas com ponto são diretivas para as ferramentas de construção. Não são instruções executadas pela CPU. .space 4 reserva quatro bytes; .balign 4 solicita alinhamento em um múltiplo de quatro. O linker decide o endereço concreto de coins. Reservar espaço na BSS não faz o hardware iniciar esse espaço em zero: nosso código de inicialização terá esse trabalho.

A largura de cada acesso importa

A tabela de cargas e armazenamentos por largura explica ldrb, ldrh, ldr e suas formas de escrita.

OperaçãoLeituraEscritaUso nesta série
8 bitsldrbstrbConceito de byte; não usaremos para desenhar
16 bitsldrhstrhControle de vídeo, botões e pixels
32 bitsldrstrVariáveis da partida

O ARM7TDMI exige cuidado com alinhamento: nossas palavras ficam em múltiplos de quatro, e meias palavras, em múltiplos de dois. Não use um acesso desalinhado como atalho para juntar bytes.

Além disso, as regiões do GBA têm regras próprias. Na VRAM, uma escrita de byte não se comporta como uma escrita comum de byte em RAM. Vamos escrever pixels com strh, respeitando o formato e a largura esperados.

O GBA usa a organização little-endian. Se uma palavra contém 0x12345678, seus quatro bytes, do menor endereço para o maior, aparecem como 78 56 34 12. O valor não foi invertido: essa é a ordem dos bytes na memória. Ao ler a palavra com a largura correta, a CPU recompõe o valor.

Como uma comparação vira um desvio

Para cada condição, consulte CMP e TST, flags e sufixo S e a tabela de condições com e sem sinal.

Em uma linguagem de alto nível, você escreveria “se o saldo for menor que 20, não compre”. No nosso Assembly:

Assembly ARM
cmp r1, #20
blo sem_dinheiro
sub r1, r1, #20

cmp compara como uma subtração, mas não guarda o resultado em r1. Ela atualiza indicadores da CPU chamados flags, no registrador de status. blo consulta esses indicadores e desvia se o primeiro valor for menor, interpretando os valores sem sinal.

sem_dinheiro é um label: um nome dado a um ponto do código. Um label não é uma função por si só; ele só marca uma posição. O exemplo acima é incompleto e serve para ler a condição.

CondiçãoSignificado que usaremos
eqIgual
neDiferente
lo / hsMenor / maior ou igual, sem sinal
lt / geMenor / maior ou igual, com sinal

Os mesmos bits podem representar um número com sinal ou sem sinal. A escolha da condição determina a interpretação da comparação. Um valor de 32 bits com todos os bits em um pode representar -1 com sinal ou 4294967295 sem sinal.

Algumas instruções recebem um sufixo de condição: subne só subtrai se a condição “diferente” estiver satisfeita. Outras recebem s, como subs, para atualizar as flags. Precisamos observar qual instrução atualizou as flags antes de depender delas.

Bits também representam escolhas independentes

Os detalhes de AND, ORR, EOR, BIC e MVN e dos deslocamentos ficam na referência.

Uma variável pode guardar um saldo inteiro. Já um registrador de hardware frequentemente reúne várias opções em bits separados. Para verificar uma opção, usamos uma máscara.

Assembly ARM
tst r1, #16
bne direita_pressionada

tst calcula um AND lógico apenas para atualizar as flags. A máscara 16 tem um único bit ligado, o bit 4. Se esse bit também estiver ligado em r1, o resultado não será zero. O desvio será tomado. No capítulo de controles, esse bit representará o botão direito depois de normalizarmos a leitura do hardware.

AND seleciona bits; OR reúne bits; NOT inverte bits. Deslocar um valor quatro posições para a esquerda, lsl #4, multiplica por 16 quando o resultado cabe na largura disponível. Vamos usar isso para converter células do tabuleiro em pixels.

Pequeno exercício de mesa

A preservação de registradores e retornos é explicada em BL e BX e PUSH e POP.

Considere coins = 20 na RAM. A CPU carrega o valor em r1, soma 5 e chama outra rotina antes de gravar. Essa rotina muda r1 para 3. O que será salvo se agora executarmos str r1, [r0], supondo que r0 foi preservado?

Será salvo 3. Registradores não pertencem automaticamente a uma variável. Precisamos estabelecer quais uma rotina pode modificar e quais deve preservar. A pilha vai ajudar a guardar valores e endereços de retorno; veremos esse uso ao desenhar retângulos.

Para rever a sintaxe, consulte as diretivas ARM do GNU assembler. A referência ARM7TDMI da Arm descreve a CPU; não é necessário lê-la inteira para seguir o curso.

Anterior: como o hardware funciona. Próximo: preparar o ambiente e executar a primeira ROM.

Voltar ao início