Artigos
Assembly do GBA: referência de instruções e sintaxe
Consulte instruções ARM7TDMI, registradores, flags, condições, endereçamento, pilha e diretivas, com exemplos e links para as explicações da série.
Este artigo é uma referência para consultar enquanto você acompanha a série Do hardware ao jogo e o Tower Defense em Assembly. Ele explica o que as instruções leem, o que modificam e como a sintaxe se relaciona com a CPU e com a memória.
O alvo é o Game Boy Advance, com CPU ARM7TDMI, arquitetura ARMv4T e instruções no estado ARM, usando GNU assembler. O Game Boy clássico e o Game Boy Color usam outra CPU: os comandos daqui não são uma referência para esses consoles. Thumb também existe no GBA, mas tem formatos e restrições diferentes dos exemplos ARM desta série.
A referência cobre todas as famílias de instruções e diretivas que aparecem nos seis exemplos e no Tower Defense didático. Também apresenta algumas operações complementares. As variantes especiais e os detalhes completos de codificação ficam nos manuais indicados ao final. Salvo indicação contrária, os exemplos usam registradores comuns, sem os comportamentos especiais de instruções que escrevem em pc.
Como consultar
Cada link abaixo leva diretamente à seção correspondente. Nos outros artigos, os links de referência aparecem junto do conceito ou do código que os utiliza. Você pode abrir a referência em outra aba para conservar a posição no tutorial.
| O que você encontrou no código | Onde consultar |
|---|---|
#, =, colchetes, chaves, comentários | Sintaxe |
r0 a r15, sp, lr, pc, CPSR | Registradores |
| Hexadecimal, negativos e constantes grandes | Números e imediatos |
mov, moveq, movne | MOV |
ldr, str, ldrh, strh e ldr =nome | LDR e STR |
[r0], [r0, #4], [r0], #4, ! | Endereçamento |
add, sub, rsb, subs | ADD, SUB e RSB |
mul | MUL |
and, orr, eor, bic, mvn, orrs | Operações de bits |
lsl, lsr, asr, ror | Deslocamentos |
N, Z, C, V e sufixo s | Flags |
cmp, cmpeq, tst | CMP e TST |
eq, ne, lo, hs, lt, ge, hi, le | Condições |
b, beq, bne, 1b, 1f | B e labels |
bl, blne, bx, bxne, bx lr | BL e BX |
push, pop, pilha e preservação | PUSH e POP |
msr cpsr_c, #0xdf | MRS e MSR |
.equ, .space, .section, .if, .ltorg | Diretivas do assembler |
MEMORY, SECTIONS, .bss, cabeçalho | Linker e cabeçalho |
as, ld, objcopy, nm, objdump | Ferramentas de construção |
| Chamada de BIOS e outras famílias | SWI e BIOS, outras instruções |
Os blocos curtos deste artigo são exemplos de leitura e de sintaxe, não ROMs completas. Endereços simbólicos como coins precisam ser definidos e organizados pelo programa que os utiliza. Para executar um programa inteiro, use os seis exemplos completos ou a primeira ROM explicada passo a passo.
Sintaxe
Uma linha pode conter um label, uma operação, seus operandos e um comentário. Aqui está uma linha real que pode aparecer dentro de uma rotina:
paint: strh r1, [r0], #2 @ Escrever um pixel e avancar dois bytes.
| Parte | Significado |
|---|---|
paint: | Dá um nome a esta posição do código; não executa uma operação |
strh | Nome da instrução, também chamado mnemônico |
r1 | Registrador que contém o valor a escrever |
[r0] | Acesso à memória no endereço contido em r0 |
, #2 depois dos colchetes | Atualiza o endereço após a escrita |
@ ... | Comentário na sintaxe ARM do GNU assembler |
#20 significa o valor imediato 20. =coins, em ldr r0, =coins, pede ao assembler que carregue o valor do símbolo; não significa ler a variável. {r4, lr} é uma lista de registradores, usada em operações como push. O ponto em .equ ou .section indica uma diretiva das ferramentas, não uma instrução da CPU.
Não existe uma regra única de quantidade de operandos. add normalmente tem destino e duas entradas; cmp tem duas entradas e não guarda um resultado numérico; b recebe um destino de execução. Não interprete toda primeira posição como um destino.
Nos nomes ARM, sufixos acrescentam informações: em strh, h é meia palavra; em subne, ne é uma condição; em subs, s solicita atualização das flags. São coisas diferentes. Para os comandos usados aqui, o GNU assembler aceita, por exemplo, addeq e addseq: a segunda forma soma sob condição EQ e atualiza as flags.
Registradores
Os registradores da CPU guardam valores de 32 bits. Os nomes não determinam se o valor é uma posição, um endereço ou uma cor: o programa dá esse significado aos bits.
| Nome | Papel |
|---|---|
r0 a r12 | Valores e endereços de trabalho |
r13, ou sp | Ponteiro da pilha |
r14, ou lr | Endereço de retorno produzido por uma chamada com bl |
r15, ou pc | Registrador relacionado ao fluxo de execução |
cpsr | Flags, estado ARM/Thumb, modo da CPU e controle de interrupções |
Ler uma palavra da RAM exige colocar um endereço em um registrador e usar uma instrução de carga. Uma soma comum trabalha nos registradores; não existe neste conjunto uma forma add [endereco], #1 para incrementar diretamente uma variável de memória.
No estado ARM, a leitura usual de pc por uma instrução considera o pipeline e corresponde ao endereço da instrução atual mais oito bytes. Por isso uma desmontagem pode mostrar uma carga relativa a pc. Isso não quer dizer que o programa pulou duas instruções. Para saltos e retornos, use os padrões descritos em B e labels e BL e BX.
Não confunda estes registradores com KEYINPUT, VCOUNT ou DISPCNT: esses são registradores do hardware periférico, acessados por endereços de I/O. r0 pode conter o endereço de DISPCNT, mas não se torna o próprio DISPCNT.
Números e imediatos
20 e 0x14 representam o mesmo número. 0x anuncia hexadecimal; os dígitos vão de 0 a 9 e A a F. Um byte tem oito bits, uma meia palavra tem 16 e uma palavra ARM tem 32.
Uma palavra com os bits 0xFFFFFFFF representa 4.294.967.295 quando interpretada sem sinal e -1 em complemento de dois com sinal. Os registradores não carregam uma etiqueta de tipo: a operação e a condição escolhidas determinam a interpretação.
As contas comuns produzem um resultado de 32 bits. Somar um a 0xFFFFFFFF deixa zero nos 32 bits do destino; as flags, quando solicitadas, ajudam a identificar carry e overflow. O V de overflow com sinal não é a mesma coisa que o C de carry sem sinal.
Nem toda constante de 32 bits cabe no campo imediato de uma instrução ARM. Em muitas operações de processamento de dados, o imediato é formado por oito bits com uma rotação em passos pares. Portanto, “é um número pequeno no texto” ou “cabe em um registrador” não basta para garantir a codificação.
mov r0, #20
ldr r1, =0x12345678
No segundo caso, a pseudoinstrução deixa o assembler escolher uma sequência adequada para a constante. Ele pode usar uma instrução equivalente de movimento ou uma leitura de um literal próximo ao código. Esse mecanismo é explicado em LDR e STR e .ltorg.
MOV
mov destino, origem coloca no destino uma cópia do valor da origem. A origem pode ser outro registrador ou um imediato que a ferramenta consiga codificar.
mov r0, #20
mov r1, r0
mov r2, r1, lsl #1
Ao final, r0 e r1 contêm 20; r2 contém 40. O deslocamento participa da operação que produz o valor de r2; não altera r1.
O mov comum não lê nem escreve uma variável na RAM e não modifica as flags. movs atualiza as flags aplicáveis à operação lógica. moveq e movne executam o movimento apenas quando a condição está satisfeita. Essas condições usam flags anteriores, não fazem uma comparação implícita com a origem do movimento.
O GNU assembler pode transformar algumas constantes em uma forma equivalente, como mvn. Para saber o que foi realmente emitido, consulte a desmontagem do ELF.
LDR e STR
ldr destino, [endereco] carrega uma palavra da memória. str origem, [endereco] armazena uma palavra. A direção é importante: em str, o primeiro operando é a origem do valor, e a memória é o destino.
ldr r0, =coins
ldr r1, [r0]
add r1, r1, #5
str r1, [r0]
A primeira linha coloca em r0 o endereço representado por coins. A segunda lê o saldo. A terceira calcula o novo saldo. Só a quarta altera a variável na RAM. Se o saldo era 20, passa a ser 25.
ldr r0, =coins é uma pseudoinstrução, reconhecida pelo assembler. O sinal = distingue esse pedido da leitura com colchetes. Se o símbolo representa um endereço, carregamos um endereço; se representa uma constante definida com .equ, carregamos essa constante.
| Instruções | Largura | Comportamento da leitura |
|---|---|---|
ldr / str | 32 bits | Lê ou escreve uma palavra |
ldrh / strh | 16 bits | ldrh completa com zeros os bits superiores do destino |
ldrb / strb | 8 bits | ldrb completa com zeros os bits superiores do destino |
ldrsh | 16 bits | Estende o sinal até 32 bits |
ldrsb | 8 bits | Estende o sinal até 32 bits |
Se r1 contém 0x12345678, strh r1, [r0] escreve somente 0x5678. Não há uma instrução strsh: escrever uma largura menor descarta os bits superiores, independentemente da interpretação com sinal.
Na RAM, nossos acessos de palavra ficam alinhados em quatro bytes; os de meia palavra, em dois. Não suponha que acessos desalinhados se comportem como uma leitura comum. No GBA, a região também importa: a VRAM tem restrições próprias, e uma escrita de byte não se comporta como um armazenamento isolado de byte em RAM. Os exemplos desenham com strh.
O GBA usa little-endian: 0x12345678 ocupa, em endereços crescentes, os bytes 78 56 34 12. Isso descreve a organização dos bytes, não uma inversão do valor aritmético. As formas de carga e armazenamento usadas nesta série não atualizam NZCV.
Endereçamento
Um endereço mede bytes. Incrementar um ponteiro em um não significa automaticamente avançar uma palavra ou um pixel.
Considere cada linha da tabela de forma independente, começando com r0 = 0x02000000:
| Forma | Endereço acessado | r0 depois |
|---|---|---|
str r1, [r0] | 0x02000000 | 0x02000000 |
str r1, [r0, #4] | 0x02000004 | 0x02000000 |
str r1, [r0, #4]! | 0x02000004 | 0x02000004 |
str r1, [r0], #4 | 0x02000000 | 0x02000004 |
O ! solicita atualização da base na forma pré-indexada. Na forma pós-indexada, depois dos colchetes, a atualização acontece após o acesso. Isso modifica o registrador da base, não uma variável que por acaso existisse no endereço antigo.
Para palavras, certas formas também permitem um índice em registrador com deslocamento:
ldr r2, [r0, r1, lsl #2]
strh r4, [r5], #2
Na primeira linha, o índice é multiplicado por quatro para selecionar uma palavra. Na segunda, escrevemos um pixel e avançamos dois bytes. As formas de endereçamento não são idênticas para todas as larguras: no ARM7TDMI, não copie o índice escalado de ldr diretamente para uma forma de ldrh ou strh.
Você usa esses padrões no desenho de retângulos: o ponteiro percorre pixels consecutivos e depois salta o espaço que falta até a próxima linha.
ADD, SUB e RSB
| Operação | Cálculo |
|---|---|
add r0, r1, r2 | r0 = r1 + r2 |
sub r0, r1, r2 | r0 = r1 - r2 |
rsb r0, r1, r2 | r0 = r2 - r1 |
rsb inverte a ordem da subtração. É útil para calcular 240 - largura ou negar um valor com 0 - valor.
mov r1, #12
add r0, r1, #4
sub r2, r1, #4
rsb r3, r1, #0
Os resultados são 16 em r0, 8 em r2 e -12, na representação de 32 bits, em r3. r1 permanece 12.
Sem s, essas operações não atualizam as flags. adds, subs e rsbs atualizam NZCV. Um laço pode usar subs contador, contador, #1 seguido de bne para repetir enquanto o resultado não for zero. O sufixo condicional em subne tem outra função: escolher se a subtração será executada.
No Tower Defense, rsblt r2, r2, #0 nega uma diferença apenas se ela for negativa, de acordo com a comparação anterior. Para os pequenos valores de posição usados ali, isso permite calcular o valor absoluto. O menor inteiro com sinal de 32 bits é um caso especial: seu positivo não cabe nessa mesma representação.
MUL
mul destino, entrada1, entrada2 multiplica dois registradores e guarda os 32 bits inferiores do produto. Não recebe um imediato na posição dos multiplicandos.
mov r1, #80
mov r5, #240
mul r6, r1, r5
r6 passa a conter 19.200. Esse é o início do cálculo da posição de uma linha na imagem.
O ARM7TDMI tem restrições de operandos que não devem ser substituídas pelas regras de um ARM mais recente. No formato ARM usado aqui, mantenha o destino diferente do primeiro multiplicando: mul r6, r1, r5 atende a essa restrição. Os exemplos usam mul sem atualizar flags.
Para um produto completo de 64 bits, existem outras instruções, como umull e smull, resumidas na tabela de operações complementares. Um produto de 32 bits que transborda não gera automaticamente uma exceção do jogo.
AND, ORR, EOR, BIC e MVN
Essas operações trabalham em cada posição dos bits. Elas não interpretam o valor como “botão”, “estado” ou “cor”.
| Instrução | Resultado | Uso típico |
|---|---|---|
and r0, r1, r2 | r1 AND r2 | Manter apenas os bits selecionados por uma máscara |
orr r0, r1, r2 | r1 OR r2 | Reunir opções ou campos de bits |
eor r0, r1, r2 | r1 XOR r2 | Alternar bits selecionados |
bic r0, r1, r2 | r1 AND (NOT r2) | Limpar os bits selecionados |
mvn r0, r1 | NOT r1 | Inverter os 32 bits |
Considere apenas os quatro bits inferiores para visualizar a diferença:
valor = 1100 mascara = 1010
AND = 1000
OR = 1110
XOR = 0110
BIC = 0100
No exemplo dos botões, mvn troca a convenção ativa em zero por bits um para botões pressionados. Um and com 0x03FF elimina os bits que não pertencem aos dez controles. Depois, bic remove os botões que já estavam pressionados no quadro anterior.
Na forma comum, essas instruções não atualizam flags. As formas com s, como orrs, atualizam N e Z, podem atualizar C conforme o deslocamento do operando e preservam V. Por exemplo, orrs r0, r0, r1 permite testar se dois valores eram ambos zero: o resultado só será zero nesse caso. Esse padrão aparece na verificação de fim da onda do Tower Defense.
Deslocamentos
Um deslocamento pode compor o segundo operando de uma instrução. O ARM tem um circuito de deslocamento associado a essas operações; não é necessário modificar o registrador de origem antes de usá-lo.
| Operação | Efeito |
|---|---|
lsl #n | Move bits para a esquerda, colocando zeros à direita |
lsr #n | Move bits para a direita, colocando zeros à esquerda |
asr #n | Move bits para a direita, repetindo o bit de sinal à esquerda |
ror #n | Rotaciona: os bits que saem à direita retornam à esquerda |
mov r0, #3
mov r1, r0, lsl #4
add r2, r1, r0, lsl #1
r1 recebe 48. r2 recebe 48 + 6, ou 54. r0 continua 3. Nos números usados para coordenadas, lsl #4 equivale a multiplicar por 16, desde que o resultado não ultrapasse a largura disponível.
Para valores sem sinal, lsr pode representar uma divisão por potência de dois descartando o resto. Para negativos, asr preserva o sinal e não equivale em todos os casos à divisão de uma linguagem que arredonda em direção a zero. Existe também rrx, uma rotação de um bit que envolve a flag C.
As quantidades zero e 32 têm particularidades de codificação e efeito. Os exemplos introdutórios usam deslocamentos positivos pequenos; para outras quantidades, confira a instrução e o formato no manual.
Flags e sufixo S
As flags principais ficam no CPSR:
| Flag | O que informa |
|---|---|
| N | O bit 31 do resultado é um |
| Z | O resultado é zero |
| C | Carry em soma ou ausência de empréstimo em subtração; em operações lógicas, pode vir do deslocamento |
| V | O resultado aritmético não cabe no intervalo com sinal de 32 bits |
N não significa, sozinho, que uma comparação com sinal deu “menor”: o overflow também precisa ser considerado. É por isso que LT usa N != V.
mov r0, #0
subs r0, r0, #1
Depois da subtração, r0 = 0xFFFFFFFF, N vale um, Z vale zero, C vale zero porque houve empréstimo e V vale zero porque -1 cabe com sinal.
cmp e tst atualizam flags sem exigir um s escrito no final. Já sub e subs são formas diferentes. ldr, str e mov comum não apagam automaticamente o resultado de uma comparação anterior.
A condição sempre observa as flags mais recentes, não necessariamente a comparação visualmente mais próxima no código. Uma instrução com s entre cmp e bne pode mudar o significado do desvio. Uma chamada de função também pode modificar flags; não dependa delas depois de chamar uma rotina sem uma convenção explícita.
CMP e TST
cmp a, b atualiza NZCV como se calculasse a - b, mas descarta o resultado numérico. Os dois operandos permanecem intactos.
cmp r1, #20
blo sem_dinheiro
sub r1, r1, #20
Se r1 for menor que 20 sem sinal, a execução vai para o label sem_dinheiro. Caso contrário, segue e subtrai o custo. A comparação sozinha não implementa um bloco if: o desvio ou uma instrução condicional utiliza seu resultado.
tst a, mascara atualiza as flags lógicas como um AND, sem guardar o AND em um registrador. Usamos principalmente Z para saber se algum bit selecionado estava ligado:
tst r1, #16
bne direita_pressionada
Isso verifica o bit 4. Com uma máscara que possui vários bits um, resultado diferente de zero significa pelo menos um desses bits ligado, não necessariamente todos. Para testar todos, faça o AND em um destino e compare com a máscara.
cmpeq é uma comparação condicional. O padrão abaixo verifica se as duas coordenadas coincidem com um ponto:
cmp r0, #6
cmpeq r1, #2
bne fora_do_ponto
Se X não é 6, a segunda comparação não executa e o primeiro resultado diferente continua valendo. Se X é 6, a segunda comparação decide se Y é 2. Assim, o desvio acontece se qualquer coordenada estiver diferente.
Condições
No estado ARM, as instruções usadas aqui podem receber códigos de condição. A mesma condição aparece em bne, movne, strne ou bxne: o efeito da instrução só ocorre quando as flags satisfazem o código.
| Sufixo | Teste nas flags | Interpretação após cmp a, b |
|---|---|---|
eq | Z = 1 | Iguais |
ne | Z = 0 | Diferentes |
cs / hs | C = 1 | a >= b, sem sinal |
cc / lo | C = 0 | a < b, sem sinal |
hi | C = 1 e Z = 0 | a > b, sem sinal |
ls | C = 0 ou Z = 1 | a <= b, sem sinal |
ge | N = V | a >= b, com sinal |
lt | N != V | a < b, com sinal |
gt | Z = 0 e N = V | a > b, com sinal |
le | Z = 1 ou N != V | a <= b, com sinal |
mi / pl | N = 1 / N = 0 | Bit de sinal um / zero |
vs / vc | V = 1 / V = 0 | Houve / não houve overflow com sinal |
al | Sempre | Forma usual quando não há sufixo condicional |
Considere r0 = 0xFFFFFFFF e cmp r0, #0: bhi seria tomado porque o valor é maior sem sinal; blt também seria tomado porque o valor representa -1 com sinal. Não há contradição: são interpretações diferentes dos mesmos bits.
Nos exemplos de movimento com coordenadas negativas temporárias, usamos condições com sinal. No Tower Defense, certas verificações de faixa usam uma comparação sem sinal que também rejeita -1, pois ele aparece como um valor enorme nessa interpretação.
Uma condição falsa evita o efeito da instrução, mas não significa custo zero de execução. Não estime o tempo de um quadro apenas contando comandos que mudam valores.
B e labels
b destino muda o fluxo para o label indicado, sem criar um endereço de retorno em lr. A forma condicional, como bne, só desvia quando a condição é verdadeira. Sem desvio, a execução segue para a instrução seguinte.
mov r2, #3
repeat:
subs r2, r2, #1
bne repeat
O decremento acontece três vezes. O programa não reconhece uma estrutura especial de laço: reconhece subtrações, flags e mudanças no fluxo.
O GNU assembler também oferece labels numéricos reutilizáveis:
b 1f
1: mov r0, #3
2: subs r0, r0, #1
bne 2b
1f procura a próxima definição de 1: à frente; 2b procura a definição mais recente de 2: para trás. O b em 2b significa backward, não o botão B. Esses nomes são resolvidos pelas ferramentas antes da execução.
Um label nomeado não cria automaticamente um escopo de função. Se o código não desviar ou retornar, ele pode continuar executando o que estiver depois desse label. A regra dos labels locais está na documentação do GNU assembler.
BL e BX
bl rotina desvia e coloca em lr o endereço para continuar depois da chamada. No estado ARM usado aqui, esse retorno aponta para a instrução seguinte ao bl. A rotina costuma retornar com bx lr.
double_value:
add r0, r0, r0
bx lr
Esse fragmento dobra o valor em r0 e volta ao chamador. Não existe um objeto de função protegendo os registradores: o contrato precisa dizer o que entra, o que sai e o que pode ser alterado.
bx registrador salta para o endereço contido no registrador e pode trocar o estado ARM/Thumb conforme o bit 0 do destino. Ele não cria um retorno em lr. Os endereços de código e o estado escolhido precisam respeitar o alinhamento correspondente.
Uma rotina que chama outra com bl sobrescreve seu próprio lr. Por isso, quando precisa voltar ao chamador original, deve preservá-lo, normalmente na pilha. Já b fill_rect, no final de paint_hero, encaminha a execução sem substituir lr: fill_rect retorna diretamente ao chamador de paint_hero.
blne new_game é uma chamada condicional. bxne lr é um retorno antecipado condicional. Nenhum deles faz por conta própria uma comparação; ambos usam as flags existentes.
PUSH e POP
A pilha fica na RAM e é controlada por sp. Nos exemplos, ela cresce para endereços menores. push reserva espaço e escreve os registradores; pop lê os valores e devolve esse espaço.
outer:
push {r4, lr}
mov r4, #20
bl inner
pop {r4, lr}
bx lr
Se sp era 0x03007F00, o push de dois registradores o leva a 0x03007EF8. A lista ocupa oito bytes, quatro por registrador. Em endereços crescentes ficam r4 e depois lr; a ordem é determinada pelos números dos registradores, não por uma ideia de “copiar o texto da lista para trás”. O pop correspondente restaura os valores e o ponteiro original.
No ARM, essas formas são representadas por operações de armazenamento e carga múltiplos, equivalentes aos padrões stmdb sp! e ldmia sp!. {r5-r8, lr} representa cinco registradores, portanto 20 bytes.
Todos os caminhos de retorno precisam restaurar o que foi empilhado. Retornar antes do pop deixa a pilha desequilibrada. Além disso, a lista deve preservar o que o contrato da rotina exige, não apenas lr.
Os exemplos têm convenções locais, incluindo passagem da cor em r4. Isso não é o contrato padrão de qualquer função C. Ao misturar linguagens, respeite a ABI escolhida, inclusive os registradores preservados e o alinhamento de oito bytes da pilha nas interfaces públicas AAPCS. Não presuma que toda chamada interna desta série já foi escrita para essa integração.
MRS e MSR
mrs lê um registrador de status para um registrador comum. msr escreve campos selecionados desse status. São operações sobre o estado da CPU, diferentes de escrever em um registrador de vídeo por um endereço de memória.
mrs r0, cpsr
msr cpsr_c, #0xdf
No startup dos exemplos, msr cpsr_c, #0xdf define o campo de controle: modo System, estado ARM, IRQ e FIQ mascaradas. Os cinco bits inferiores de 0xDF valem 11111, o código do modo System; os bits I e F valem um. O bit T vale zero.
O sufixo _c seleciona o campo de controle, não as flags NZCV. Escrever esse campo exige o contexto de privilégio apropriado. Os programas usam polling e não instalam tratadores de interrupção; copiar esse startup para um programa que depende de IRQ pode impedir que ele funcione.
Não use msr para alternar casualmente entre ARM e Thumb: a troca de estado segue os mecanismos apropriados, como bx. A instrução acima é uma configuração inicial no estado ARM.
Diretivas do assembler
As diretivas orientam o GNU assembler. Algumas selecionam a linguagem, outras definem símbolos ou colocam dados em seções. Elas não são operações que a CPU executa como uma soma ou uma leitura.
| Diretiva | Função neste projeto |
|---|---|
.syntax unified | Seleciona a sintaxe unificada aceita pelo GNU assembler |
.cpu arm7tdmi | Seleciona o alvo e restringe as instruções disponíveis |
.arm | Emite código para o estado ARM; não troca sozinha o estado da CPU em execução |
.equ NOME, valor | Define um símbolo para um valor; não reserva uma variável na RAM |
.section nome, "ax" | Escolhe uma seção; a marca alocação e x, código executável |
.global nome | Torna um símbolo visível para ligação e outras ferramentas |
.space n | Reserva n bytes, interpretados conforme a seção e a ligação |
.balign n | Avança até um alinhamento em múltiplos de n bytes |
.word valor | Emite um valor de 32 bits como dado |
.hword valor | Emite um valor de 16 bits como dado |
.byte valor | Emite um valor de oito bits como dado |
.ltorg | Emite o conjunto pendente de literais da seção naquele ponto |
.if expressao / .endif | Inclui código conforme uma expressão na montagem |
.ifndef NOME | Inclui o bloco se o símbolo ainda não estiver definido |
.error "mensagem" | Interrompe a montagem com erro |
Uma constante de hardware e uma variável têm papéis diferentes:
.equ KEYINPUT, 0x04000130
.section .bss
.balign 4
.global coins
coins: .space 4
KEYINPUT é um nome para um endereço já determinado pelo hardware. coins marca quatro bytes que o linker vai posicionar na RAM. Nesse projeto, a BSS é NOLOAD; reservar esse espaço não faz a CPU preenchê-lo com zero. O boot precisa limpar as variáveis.
.ltorg é importante quando ldr =valor depende de um literal alcançável pelo código. Literais são dados: não deixe o fluxo cair diretamente sobre eles. Nos exemplos, um retorno ou um desvio encerra o caminho de execução antes da área emitida.
As condições de montagem também são diferentes das condições do jogo. .if LESSON >= 2 escolhe o que entra no arquivo antes de a ROM existir. cmp seguido de bne decide algo durante a execução. Consulte as diretivas ARM e o manual geral do assembler para outras formas.
Linker e cabeçalho
O assembler cria instruções, dados e referências. O linker organiza essas partes e resolve endereços entre seções e arquivos.
Item do gba.ld | Significado |
|---|---|
OUTPUT_FORMAT / OUTPUT_ARCH | Formato ELF e arquitetura do resultado |
ENTRY(_start) | Ponto de entrada declarado no ELF |
MEMORY, ORIGIN, LENGTH | Regiões e tamanhos disponíveis |
SECTIONS | Regras que posicionam as seções de entrada |
KEEP(*(.header)) | Preserva a seção que reservamos para o cabeçalho |
.text / .rodata | Instruções e dados constantes organizados na ROM |
.bss (NOLOAD) | Espaço para estado sem bytes iniciais correspondentes na ROM |
ALIGN(4) | Alinha a posição em quatro bytes |
__bss_start, __bss_end | Símbolos dos limites usados pelo laço de limpeza |
ASSERT | Falha a ligação se uma condição do layout não for atendida |
O ponto . usado no script do linker representa sua posição de organização. Não é o mesmo que o ponto que inicia uma diretiva do assembler, embora os arquivos usem notações parecidas.
O cabeçalho é um requisito do formato de cartucho GBA. Reservamos seus bytes no começo da ROM, deixamos uma instrução de desvio na entrada e usamos rom.py para preencher seus campos e checksum. ENTRY(_start) sozinho não faz o console procurar um símbolo textual: no arquivo binário, o hardware encontra bytes e instruções em posições específicas.
O nome .header é uma convenção do projeto; quem a coloca na posição adequada é o linker. O código Python de cabeçalho roda no computador de desenvolvimento. Nenhum comando do linker ou do Python vira automaticamente uma instrução executada pela CPU do GBA. A documentação de SECTIONS do GNU ld detalha essa organização.
Ferramentas de construção
Os nomes abaixo são comandos do terminal, não comandos da linguagem Assembly:
| Comando | Trabalho |
|---|---|
arm-none-eabi-as | Monta o texto .s em um objeto .o |
arm-none-eabi-ld | Liga objetos e organiza endereços no ELF |
arm-none-eabi-objcopy -O binary | Extrai o conteúdo binário da ROM |
python3 rom.py arquivo.gba | Preenche e verifica o cabeçalho do exemplo |
arm-none-eabi-nm -n arquivo.elf | Lista símbolos em ordem de endereço |
arm-none-eabi-objdump -d arquivo.elf | Desmonta instruções para inspeção |
arm-none-eabi-objdump -h arquivo.elf | Mostra seções |
-mcpu=arm7tdmi seleciona o alvo. -g inclui informações úteis para depuração. -o escolhe o arquivo de saída. -T gba.ld seleciona o script do linker. No Tower Defense, --defsym LESSON=2 define um símbolo na montagem para escolher uma etapa.
O .elf conserva informações que ajudam a entender os endereços e nomes. O .gba contém o cartucho que abrimos no emulador. Mudar o .s sem repetir a construção não altera uma ROM antiga que continua no disco.
SWI e BIOS
swi provoca uma exceção de software. No GBA, serviços da BIOS podem ser chamados por esse mecanismo, seguindo a convenção de identificação do serviço, argumentos e estado ARM ou Thumb apropriados. Não é uma chamada de sistema operacional de um computador pessoal.
Nenhum dos seis exemplos depende de chamadas swi: a espera de vídeo é feita por consulta a VCOUNT. Um serviço da BIOS que aguarda interrupções exige que a configuração correspondente esteja preparada. Não substitua uma espera por polling por uma chamada de BIOS sem verificar esses requisitos.
O primeiro passo para experimentar BIOS deve ser um programa pequeno, com a convenção da função documentada e um resultado observável. A referência de instruções explica como a exceção é solicitada; a documentação da plataforma explica o serviço que a atende.
Outras instruções ARM7TDMI
Estas famílias complementam as que aparecem nos tutoriais. A tabela é um mapa para consulta posterior, não uma recomendação para acrescentar todas ao primeiro jogo.
| Família | Função |
|---|---|
adc | Soma os operandos e o carry atual |
sbc | Subtrai o operando e o empréstimo representado por 1 - C |
rsc | Subtração invertida com o mesmo tratamento de empréstimo |
cmn | Atualiza flags como uma soma, descartando o resultado |
teq | Atualiza flags lógicas como XOR, descartando o resultado |
mla | Multiplica e soma um acumulador, com resultado de 32 bits |
umull / smull | Multiplicação longa sem sinal / com sinal, gerando dois registradores de resultado |
umlal / smlal | Multiplicação longa com acumulação em 64 bits |
ldm / stm | Carrega / armazena uma lista de registradores, com variantes de ordem e atualização de endereço |
swp / swpb | Troca um valor entre registrador e memória, em palavra / byte |
O ARM7TDMI não tem sdiv, udiv, movw, movt, blx ou os blocos it de arquiteturas posteriores. Divisão exige uma rotina, um serviço apropriado da BIOS ou uma transformação matemática válida para o caso. Para chamar alternando ARM e Thumb nesta CPU, use os padrões de interworking adequados a ARMv4T.
Não existe uma instrução dedicada nop no núcleo ARMv4T com a codificação moderna de NOP. O assembler pode aceitar a grafia e emitir uma operação equivalente, como um movimento de um registrador para ele mesmo. Conferir o alvo e a desmontagem evita confundir a sintaxe aceita pela ferramenta com uma nova capacidade da CPU.
Exemplo comentado
Leia este fragmento supondo que coins já foi definido na RAM e contém 20:
spend_once:
ldr r0, =coins
ldr r1, [r0]
cmp r1, #5
bxlo lr
sub r1, r1, #5
str r1, [r0]
bx lr
| Passo | O que muda |
|---|---|
Carregar =coins | r0 recebe um endereço; o saldo ainda não foi lido |
Ler [r0] | r1 recebe 20 |
| Comparar com 5 | As flags descrevem a subtração; r1 continua 20 |
| Retorno condicional | Não retorna, pois 20 não é menor que 5 sem sinal |
| Subtrair | r1 passa a ser 15; a RAM ainda guarda 20 |
| Armazenar | A RAM passa a guardar 15 |
| Retornar | O programa continua pelo endereço em lr |
Se o saldo inicial fosse 3, a rotina retornaria antes da subtração. Ela não preserva r0, r1 ou as flags e não informa sucesso em um registrador separado: essas escolhas fazem parte do contrato deste fragmento.
Esse exercício reúne a distinção essencial da série: uma constante pode representar um endereço, um registrador pode conter esse endereço, e uma instrução com colchetes acessa o conteúdo da memória. Em um endereço de I/O, essa mesma operação interage com o hardware.
Erros frequentes
| Sintoma ou engano | O que conferir |
|---|---|
“Mudei r1, mas o saldo não mudou” | Faltou gravar com str? |
“Usei ldr =coins e recebi um número enorme” | Você carregou um endereço, não o saldo |
“bne usa a comparação errada” | Alguma instrução ou chamada modificou as flags? |
“-1 parece maior que zero” | A comparação foi sem sinal? |
| “O pixel seguinte aparece no lugar errado” | O endereço avançou dois bytes, e o fim da linha foi tratado? |
| “A função não volta ao lugar certo” | Uma chamada interna sobrescreveu lr? |
| “O reinício se repete enquanto seguro Start” | A regra usa estado atual em vez de nova pressão? |
“.space 4 não começou em zero” | Reservar espaço não inicializa a RAM durante a execução |
| “Uma instrução encontrada na internet não monta” | Ela pertence a ARM7TDMI e ao estado ARM selecionado? |
| “Montou, mas não executa corretamente” | Além da sintaxe, confira layout, entrada, cabeçalho, estado da CPU e acessos ao hardware |
Referências oficiais
O ARM7TDMI Technical Reference Manual, DDI 0210C descreve o modelo da CPU e resume as instruções. Use a parte de instruções ARM e condições, observando a arquitetura do alvo. Folhas de referência de ARM mais recente podem incluir operações inexistentes no GBA.
O manual do GNU assembler para ARM define a sintaxe da ferramenta usada aqui; suas páginas de diretivas e símbolos e labels locais ajudam a interpretar o texto do programa. O manual de scripts do GNU ld trata da ligação.
Para continuar aprendendo por experiências executáveis, volte ao índice dos nove capítulos. Para aplicar as operações em uma partida, siga o artigo principal de Tower Defense.