[TUTORIAL] Crie jogos para o NES - Parte 3: Entendendo o comando de compilação, sistemas númericos e introdução ao Assembly 6502
Caso você não viu as outras partes os links estão aqui
Parei com os tutoriais por um tempo porque a escola estava um pouco puxada :>
Na parte 2 finalizamos com um código compilado, mas você simplesmente só copiou o código e executou o comando de compilação, vamos finalmente começar a destrinchar oque fizemos.
Conteúdos
1 - Entendendo o comando de compilação
2 - Sistema binário e sistema hexadecimal
2.1 - Sistema binário
2.2 - Sistema hexadecimal
3 - Por que não usar o sistema decimal?
4 - Introdução a código Assembly 6502
5 - Diretivas Assembly 6502
1 - Entendendo o comando de compilação
No ultimo tutorial utilizamos o comando cl65 --target nes hellones.asm --verbose -o hellones.nes para compilar o nosso código para um arquivo binário de NES, o cl65 é o compilador/linker do cc65, usado para o processador MOS 6502(no qual a CPU do NES é baseada). Ele combina várias etapas de uma vez só: compilação de C/Assembly, montagem(assembly), linkagem e geração do arquivo final, mas ainda está muito superficial, então vamos detalhar.
cl65 é o comando principal da toolchain(caso não saiba oque é uma toolchain, é um conjunto de ferramentas de programação para desenvolvimento de um software falando de uma forma bem resumida) do cc65, ele chama outras ferramentas internamente no caso, vai chamar o ca65, que é o assembler, ele monta o código assembly em objetos .o, tambem vai chamar o ld65 que é o linker(linka os objetos e bibliotecas e cria o executável final), possivelmente possa chamar o cc65 que é o compilador C, no caso no nosso código não há código C, e não vai chama-lo.
O --target nes define o alvo de compilação, basicamente dizer qual sistema a saída será compativel, no caso configurando para o NES, o cc65 usa isso para escolher o conjunto de bibliotecas padrão e rotinas de inicialização para o NES e configurar o formatador do linker correto, que gera um header de NES e organiza a mémoria ROM/VRAM. O hellones.asm vai ser o nosso arquivo de entrada, ou melhor o nosso código =), então o cc65 vai passar para o assembler ca65, que transforma seu código em objeto binário .o interno. A flag --verbose aativa o modo detalhado, vai fazer o compilador imprimir informações extras sobre oque está fazendo, incluindo cada passo de compilação, montagem, etc. Útil para depuração ou entender como a ROM está sendo organizada. -o hellones.nes, -o significa output, ou seja o arquivo de saída, hellones.nes é o nome do arquivo final da ROM.
2 - Sistema binário e sistema hexadecimal
Vamos utilizar bastante o sistema binário e o sistema hexadecimal para representar números, então vou explicar os dois.
2.1 - Sistema binário
O sistema binário utiliza apenas dois o 0 e o 1, o número zero seria por exemplo 00000000, nesse caso eu utilizei 8 algarismos para contar, mas isso pode variar, nesse caso utilizei 8 digitos pois a CPU do NES é uma CPU 8 bits, lembrando que bit significa binary digit, ou seja 8 bits, seriam 8 digitos binários como escrito anteriormente, se fosse 4 bits seria 0000, agora o número 1 decimal representado em binário seria 00000001, e o número 2 seria 00000010, o número 3 seria 00000011, resumidamente pegamos o digito 0 mais a direita, e trocamos por 1, se não existe um digito 0 mais a direita, isso quer dizer que não existe mais digitos a direita ou que você chegou no limite(11111111), no caso que não existe mais digitos a direita do digito 1 seria assim 00001111, ou por exemplo 00111111, como não há mais digitos a direita do primeiro digito 1, você vai pegar o digito anterior ao primeiro digito 1, trocar ele por 1, e os digitos em diante vão receber o valor de zero, basicamente estamos adicionando uma nova casa binária, isso seria equivalente a por exemplo quando estamos contando com números decimais, 6, 7, 8, 9, não tem como mais você contar, então você vai resetar o digito para 0, e você vai começar a contar do outro lado tambem, então 9 vira 10, depois 11, 12.. 19, 9 vira 0 denovo e você incrementa o digito de valor maior para virar então 2, ficando 20.
Aqui há uma tabelinha para ajudar a entender
| Decimal | Binário |
|---|---|
| 0 | 00000000 |
| 1 | 00000001 |
| 2 | 00000010 |
| 3 | 00000011 |
| 4 | 00000100 |
| 5 | 00000101 |
| 6 | 00000110 |
| 7 | 00000111 |
| 8 | 00001000 |
| 9 | 00001001 |
| 10 | 00001010 |
| 11 | 00001011 |
| 12 | 00001100 |
| 13 | 00001101 |
| 14 | 00001110 |
| 15 | 00001111 |
| 16 | 00010000 |
| ... |
2.2 - Sistema hexadecimal
O sistema hexadecimal utiliza 16 simbolos diferentes, enquanto o decimal utiliza 0-9, e o binário 0-1, o hexadecimal utiliza: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F.
A representa 10, B representa 11, C representa 12, D representa 13, E representa 14
geralmente utilizamos 0x na frente para indicar que está sendo representado de forma hexadecimal, as vezes não as vezes sim.
hexadecimal acaba sendo decimal só que com mais simbolos.
| Decimal | Hexadecimal |
|---|---|
| 0 | 0x0000 |
| 1 | 0x0001 |
| 2 | 0x0002 |
| 3 | 0x0003 |
| 4 | 0x0004 |
| 5 | 0x0005 |
| 6 | 0x0006 |
| 7 | 0x0007 |
| 8 | 0x0008 |
| 9 | 0x0009 |
| 10 | 0x000A |
| 11 | 0x000B |
| 12 | 0x000C |
| 13 | 0x000D |
| 14 | 0x000E |
| 15 | 0x000F |
| 16 | 0x0010 |
| ... |
3 - Por que não usar o sistema decimal?
É uma boa pergunta, e precisamos compreender isso bem. Na verdade podemos escolher usar hexadecimal, binário ou decimal geralmente quando quisermos (a não ser que o seu software te restringe a utilizar eles), Tem vezes que vamos utilizar, hexadecimal, binário ou decimal e vou dizer quando usar cada um, embora é facil para o computador ler binário, para nós nem tanto, não precisamos nos preocupar com otimização de números geralmente porque apenas existe binário em hardware, decimal e hexadecimal acaba apenas sendo uma representação visual. As unicas vezes que vamos utilizar binário é quando realmente faz sentido, no NES por exemplo, algumas partes da memória que a CPU vai usar para coisas especificas podem ser compartilhadas, ela pode pegar um byte(espaço na memória de 8 bits) e fazer flags para enviar informações, exemplo, pode haver um byte para configuração, e cada bit vai ser uma configuração especifica, onde 0 seria desligado e 1 seria ligado, como ele só precisa saber se estar ligado e desligado, é besteira utilizar um byte para cada configuração, então ele utiliza o mesmo byte para essas configurações, outro exemplo que eu consigo dá é quando você precisa modificar algum bit especifico na hora de fazer alguma operação lógica, bitwise ou aritmética, com isso fica mais fácil de visualizar oque está sendo mudado, que em decimal apenas pareceria ser um número mágico.
Agora para os números hexadecimais a história é um pouco diferente, ele é feito para justamente nos ajudar a entender de forma fácil, sim parece contraintuitivo porque podemos usar utilizar decimal para isso, e é verdade, mas vamos entender porque pode ser possivelmente melhor do que usar decimal, o binário é a base dos computadores, o hexadecimal é perfeitamente alinhado ao binário, porque 1 dígito hexadecimal = 4 bits, já o decimal não se encaixa em potencias de 2, 1 byte cabe 2 digitos hexadecimal (0x00 a 0xFF), em decimal o mesmo byte vai de 0 a 255(limite de 8 bits) oque não "casa" diretamente. Então, se você quiser escrever binário para hexadecimal, é prático. Em decimal, precisa fazer divisão por 2, pesos de 10, etc.
Nota: geralmente você não vai escrever um conversor de binário para hexadecimal, a não ser que por exemplo você queira mostrar a quantidade de pontos do jogador :P, mas as vezes é mais fácil você entender um sistema teoricamente utilizando números hexadecimais do que decimais já que eles vão contra a natureza do binário.
4 - Introdução a código Assembly 6502
Em código assembly, os comandos são chamados de instruções, alguns recebem argumentos, outros não. Primeiramente vamos entender oque são registradores da CPU, existem 6 deles, dentre eles:
| Registrador | Bits |
|---|---|
| A(Accumulator ou Acumulador) | 8 bits |
| X(Index X) | 8 bits |
| Y(Index Y) | 8 bits |
| PC(Program Counter) | 16 bits |
| SP(Stack Pointer) | 8 bits |
| P(Status Register | 8 bits |
Mas por enquanto apenas vamos abordar os registradores A, X e Y.
Registradores são pequenas áreas da memória com apenas alguns bytes, são ultra rápidas, e tem a caracteristica de poder realizar operações diretamente nelas(soma, subtrair, comparar, etc).
O NES tem aproximadamente 56 instruçoes, na realidade aproximadamente 151(depois vou explicar porque isso). A primeira instrução que vou apresentar aqui é o INX, INX: Increment X register, ou simplesmente: Incrementa o valor de X
Por exemplo esse código:
INX
INX é a instrução que queremos usar, vai simplesmente incrementar, ou melhor adicionar um, em Javascript seria equivalente a:
x++
Também temos a instrução DEX, que vai decrementar o valor de X
Por exemplo esse código:
DEX
em Javascript o equivalente seria:
x--
Um exemplo de código com várias instruções
INX
INX
INX
INX
DEX
DEX
o valor final de X será 2, com 4 INX ficou valor de 4, e com 2 DEX decrementamos duas vezes ficando 2.
equivalente em Javascript:
x++
x++
x++
x++
x--
x--
Outra instrução que quero apresentar é o LDX: Loads a value into memory X, ou simplesmente carregar um valor na memória de X.
Por exemplo esse código
LDX #$07
LDX é a instrução que queremos usar, #$07 é o valor hexadecimal que nesse caso em decimal é 7 mesmo kk, depois vou explicar oque o # e o $ fazem, por enquanto ignorem sua existência.
Então ele vai pegar o byte da memória do registrador X e colocar o valor de 7, isso em Javascript seria basicamente:
x = 7
invés de realizamos uma operação aritmética, simplesmente carregamos o valor diretamente.
também podemos fazer isso com o registrador Y, ele praticamente faz a mesma coisa que o registrador X faz, porem usando dois registradores podemos ter dois valores carregados ao mesmo tempo.
LDY #$05
LDX #$07
DEY
DEY
INX
INX
INX
o equivalente em Javascript seria:
y = 5
x = 7
y--
y--
x++
x++
X++
No final disso tudo o Y vai ter o valor de 3 (ou em hexadecimal 0x03), e o X o valor de 10 (ou em hexadecimal 0x0A)
Na proxima parte vamos aprender mais instruções, e vamos começar a ver como o código que copiamos e colamos da parte 2 realmente funciona, quaisquer dúvidas podem perguntar a vontade.