Medidas anti-emulação de clássicos do NES

Table of Contents

1 INTRO

Alguns de vocês podem lembrar uma série de jogos de Game Boy Advance que surgiram no curso do ano de 2004. Em rígido constraste com os cartuchos cinza-escuro de rótulos coloridos, um conjunto de cartuchos cinza-claro com rótulos simples contendo jogos portados do Nintendo Entertainment System original foi lançado. Nomeado de Classic NES Series nos EUA, estes jogos eram interessantes por uma série de razões.

De uma perspectiva de emulação do GBA, os jogos eram especialmente interessantes. O jogo médio de Game Boy Advance é extremamente defeituoso 1, e a própria plataforma contém um número de salvaguardas para impedir que jogos travem. Como resultado, emuladores tendem a ser compatíveis-com-os-defeitos do hardware original a fim de garantir que os jogos realmente funcionem. Porém, a Classic NES Series vai acime e além do jogo médio na tentativa de garantir que não rode em emuladores.

Se você tentou carregar algum jogo em algum emulador antigo, provavelmente foi confrontado com uma tela Game Pak Error, como visto acima. Como logo veremos, estes jogos exploram diversos truques e comportamentos indefinidos que tornam sua emulação mais difícil. Isto parece ser uma tentativa deliberada de dissuadir a cópia destes jogos. Pelo interesse da acurácia, eu diligentemente investiguei, implementei e narrei todas as coisas incomuns que descobri que esses jogos fazem.

2 Truque 1: Espelhamento de Memória

O primeiro truque que os jogos lançam envolve a disposição da memória do GBA. O GBA tem um espaço endereçamento de memória liso (sem segmentação), porém os oito bits superiores do endereço sinalizam o barramento para qual dispositivo deve-se ter acesso naquele momento. 00 é o BIOS, 02 a RAM principal, 03 a RAM do chip, etc. Porém, desde que somente os oito bits superiores sinalizam o dispositivo, e a maioria dos dispositivos têm um espaço de endereçamento bem limitado (menos de 16 MiB), bits no meio dos 8 bits superiores e os bits inferiores que sinalizam o endereço dentro do dispositivo não têm nenhum propósito definido.

Por exemplo, a RAM principal é de 256 KiB. Isto iguala a 18 bits de espaço de endereçamento. Isto significa que os endereços dessa região de memória vão de 02000000 até 0203FFFF, deixando tudo de 02040000 até 02FFFFFF não endereçado. Num dispositivo ARM típico, acessar endereços inválidos resulta em algo chamado aborto de dados. Porém, o GBA não suporta abortos de dados, e o que acontece neste caso particular é interessante. Dado que os 8 bits superiores são usados para selecionar o dispositivo, e os 18 inferiores são usados para endereçamento no dispositivo, existem 6 bits no meio que não são usados. Estes bits não usados são simplesmente ignorados. Isto significa que se você tentar acessar qualquer coisa além das regiões válidas de memória na RAM principal, os bits superiores são efetivamente mascarados e você fica com um endereço de memória válido mais uma vez. Alguns emuladores referem-se a isso como a memória espelhada.

O que a Classic NES Series faz com a memória espelhada não é particularmente especial: ela copia código para a RAM principal e daí salta para um destes endereços-espelho. Isto tende a confundir alguns emuladores, mas nunca foi um problema em mGBA, devido a como ele implementa regiões de memória. Porém, este é de longe o menos problemático truque que a Classic NES Series usa.

3 Truque 2: Código na VRAM

O que os jogos fazem na sequência é bem mais interessante: eles começam a copiar dados na RAM de vídeo, o que por si só é perfeitamente normal, mas então saltam a execução para estes dados copiados na VRAM: era código copiado para uma região da RAM que geralmente é reservada a gráficos, e então executá-lo dali. A primeira vez que vi um jogo fazer isso, eu pensei ter feito algo de muito errado. Saltar para um endereço inválido é um sintoma comum de um defeito na emulação azedando, e tende a acontecer quando copiando sobre os endereços ou a memória que está sendo executada. Sob inspeção aprofundada, eu descobri que se permitisse que o jogo de fato executasse código na VRAM, ele não iria quebrar, e parecia relativamente estável. Existiam diversos problemas restantes, mas esta foi claramente uma tática para descartar a emulação. Usar VRAM para algo que ela não foi claramente pretendida definitivamente me desconcertou de primeira, mas uma vez que eu deixei os jogos fazerem isso, eu caí em mais alguns problemas.

4 Truque 3: Registradores STM para DMA

O próximo truque que eles empregam é um uso bastante heterodoxo das instruções STM. STM, que significa "store multiple" 2, é uma classe de instruções projetada para empacotar os valores de múltiplos registradores da CPU em memória consecutiva. Existem quatro variedades de instruções STM: decremento posterior, decremento anterior, incremento posterior, incremento anterior. O "decremento posterior" refere-se à organização do endereço de memória que ele empacota: os valores são armazenados um por vez, decrementando o endereço pelo tamanho de uma palavra após armazenar cada palavra. Superficialmente, isto parece significar que a ordem em que as escritas de memória ocorrem seria decrementada. Se você está guardando os valores A, B e C, você acaba com CBA na memória, então pode muito bem começar com A, dado que é o endereço inicial, e seguir seu caminho reverso.

Porém, como Martin Korth escreveu sobre, o processador na realidade reconhece de antemão qual será o endereço do registrador final e daí os escreve na mesma ordem como se estivese incrementando em vez de decrementando. Portanto, apesar do fato que a memória termina em CBA, o processador escreve C primeiro. Um emulador precisaria fzer uma contagem de quantos registradores tem de ser armazenados de antemão, o que pode ser mais lento. Agora, em geral, a ordem em que a memória pareceria irrelevante, especialmente em um processador de núcleo simples, onde as escritas na memória podem ser assumidas como atômicas. Para a RAM principal, isto pode ser correto. (Dado que a escrita é realizada em uma instrução, uma DMA não pode preemptar a CPU no meio das escritas.) Porém, os jogos da Classic NES Series estão empregando um truque brilhante com elas: estabelecer uma transferência de DMA em uma instrução.

Transferências de DMA são usadas para copiar memória de uma região a outra eficientemente, geralmente o do Game Pak para a RAM principal, ou da RAM principal para os FIFOs de áudio. Existem três registradores consecutivos na região da memória dos registradores de E/S mapeada na memória por canal de DMA (e existem quatro canais de DMA) que podem ser escritos para estabelecer uma transferência de DMA. Geralmente um jogo estabelecerá a fonte e o destino escrevend a conta e os bits de controle de DMA com ou um armazenamento final de 32 pits ou dois armazenamentos de 16 bits para cada metade do registrador de controle.

O que os jogos da Classic NES Series fazem algumas vezes é mais brilhante: dado que estes três registradores são consecutivos na memória, eles usam as instruções STMIA e STMDA para guardar os três valores de uma vez. STMIA é o caso fácil: escreva um registrador, incremente, escreva o próximo registrador, incrementa, escreve o registrador de controle, incrementa. STMDA é um pouco diferente: dado que ele está decrementando, um emulador desavisado pode escrever os bits de controle antes dos endereços, resultando numa transferência incorreta de DMA. Apesar do fato que A, B e C são escritos como CBA, e o endereço inicial é o de A, A precisa ser escrito por último. Eu precisei fazer uma contagem populacional do número de registradores sendo escritos, e ajustar a compensação inicial da escrita para fazer com que a ordenação funcionasse corretamete. Após consertar estas operações para o que o hardware aguarda, as transferências foram feitas corretamente.

5 Truque 4: Mascaramento do Tipo de Save

Os truques não terminam por aqui, porém. O próximo truque é um pouco menos esperto, porém, e existem outros jogos que empregam este truque também. Num cartucho de Game Boy Advance, pode existir um de diversos mecanismos diferentes de salvar. Alguns jogos usam um save-de-bateria ou de outra forma NVRAM que é endereçável por bytes. Estes existem no bloco 0E da memória, e podem ser salvos normalmente. Outros cartuchos usam memória Flash na mesma região, e usam um protocolo padrão para queimar bytes na Flash, ou apagar regiões para reprogramação. Um terceiro tipo é EEPROM, que existe na extremidade superior da região de memória do Game Pak, na região 0D. Estes usam um protocolo de nível de bit manipulado por transferências de DMA para enviar séries de bits para a EEPROM para programação. Porém, cada jogo só pode ter um destes tipos de save, e o cabeçalho do cartucho não especifica qual é o tipo de save que o cartucho terá. Diversos emuladores, mGBA incluso, tentam auto-detectar o tipo de save esperando até que o jpgo tenta interagir com um deles, e determina o tipo de save com base nisso. Porém, alguns jogos, inclusos os da Classic NES Series, tapeiam esses emuladores tentando acessar o tipo errado primeiro. Por exemplo, todos esses jogos usam EEPROM, mas fingem usar a SRAM. Se eles detectam que as escritas na SRAM realmente tiveram sucesso, eles apresentam a tela de erro de Game Pak, como vista acima. Este truque é relativamente fácil de contra-atacar, e o emulador confere de antemão o código do jogo. Se ele detecta um código associado a um jogo da Classic NES Series, ele força o tipo de save para EEPROM.

6 Truque 5: Abuso do Prefetch

O próximo truque que estes jogos empregam foi o mais difícil para eu desvendar. Me custou muitos dias para eu descobrir apropriadamente, e exigiu uma mudança de nível realmente muito baixo no núcleo do laço de emulação. Processadores em hardware têm diversos processos de estágios para executar instruções, chamado linha de produção (vulgo pipeline). Cada estágio executa uma tarefa diferente tal que cada porção de um circuito da CPU pode ser mantida ocupada enquanto outra parte está à parte fazendo seu próprio passo. O pipeline é projetado de tal forma que quando uma instrução está pronta em um estágio e move-se para o próximo estágio, a instrução seguinte pode imediatamente preencher o espaço agora vago. O ARM7TMDI, o processador do Game Boy Advance, tem uma pipeline que tem três estágis relevantes para uma emulação precisa: busca, decodificação e execução. No estágio de busca, o barramento de memória é inquirido acerca da memória associada a ele com uma instrução. Esta é então passada para o estágio de decodificação, onde o processador descobre que instrução é. Finalmente, o processador de fato executa a instrução. Um interpretador ingênuo pode mesclar todos estes três estágios, ou por hipotéticas razões de velocidade, ou meramente por uma ideia mal-formada de como os processadores funcionam. mGBA realmente estava assumindo que os estágios de decodificação e execução eram combinados até recentemente. Porém, uma observação importante foi feita enquanto se cavoucava pelos códigos dos jogos da Classic NES Series: o jogo estava modificando uma instrução que estava muito perto em proximidade aonde o código já estava sendo executado. O seguinte código em assembly é extração da VRAM do Classic NES Metroid.

06000260:  E3A01000     mov r1, #0
06000264:  E28FE008     add lr, pc, #8
06000268:  E51F0010     ldr r0, [$06000260]
0600026C:  E58E0000     str r0, [lr, #0]
06000270:  E3A010FF     mov r1, #255
06000274:  E3A010FF     mov r1, #255

O que este código faz é bem simples. Ele guarda 0 no registrador r1, então carrega a palavra em 06000260 no registrador r0, guarda ele no endereço 06000274. Então salva 255 no registrador r1 e finalmente … bem, eu menti um bocadinho. Note que a última instrução desse bloco de assembly é o próprio endereço que está sendo armazenado duas instruções antes. O valor que está sendo salvo neste endereço é a instrução que guardará 0 em r1, em vez de 255. Então o que esse código faz? A resposta depende de quão longo é seu pipeline.

O que é imperativo entender que está acontecendo neste bloco de código é notar que, uma vez que as instruções foram buscadas pelo pipeline, mudar a memória que guarda aquele endereço é irrelevante. Isto é semelhante a como a coerência de cache funciona, mas é ainda mais rigoroso. isto significa que se sua pipeline é longa o bastante, a instrução que entra na pipeline durante a escrita é aquela que guarda 255. Se é muito curta, guarda 0. Acontece que o jogo falha em iniciar se encontra o valor 0 no registrador r1, mas executa sem problemas se é 255. Ao notar isso, eu tive que estender a pipeline emulada no mGBA para incluir um estágio postiço entre a execução e a busca. Na pipeline do ARM7TDMI real, existe um estágio de decodificação entre esses dois. Porém, eu li erroneamente o manual e não notei que este estágio existia separadamente. Adicionando um novo estágio à pipeline no interpretador, repentinamente os jogos da Classic NES Series games funcionam!

Metroid's start screen

7 Truque 6: Irregularidades na FIFO de Áudio

Há ainda mais uma complicação, porém: enquanto os jogos estavam jogáveis, o áudio estava muito horrivelmente defeituoso. Isto custou bem pouca depuração, mas mais uma vez eram estes jogos fazendo algo que é único à Classic NES Series e foi portanto implementado de maneira bem levemente errada em razão de subespecificação. O Game Boy Advance tem seis canais de áudio: quatro canais de áudio gerados proceduralmente, que são um superconjunto funcional daqueles encontrados no Game Boy original, e dois canais de áudio PCM. Até onde eu vi, os jogos da Classic NES Series só usavam um canal, e era um dos canais PCM. Os canais de áudio PCM operam tendo uma pequena FIFO interna que inicia uma transferência DMA quando chega abaixo de um certo ponto. Jogos configuram esta para escrever 32 bits por vez para os registradores E/S associados com cada canal. Desde que os canais PCM são de apenas 8 bits de largura, escrever 32 bits é de fato quatro samples. O que os Classic NES Series fazem é um pouco diferente: escreve apenas 16 bits por vez, para uma metade do registrador em vez do registrador completo. Desde que eu assumi que os jogos só escreveriam 32 bits por vez, isto fez o emulador acabar escrevendo os dois samples requisitados pelo jogo bem como dois samples vazios por escrita. Este simples lapso causou um áudio completamente mutilado nos jogos. Após adicionar um simples conserto, os jogos agora parecem rodar sem problemas.

8 Sucesso, Mas Por Que o Problema?

Eu não estou realmente certo de por que a Nintendo foi tão longe com estes jogos, considerando eles são apenas ports de jogos NES. Emuladores NES cheios de recursos existem há muitos anos, com os bons surgindo já em 1997. Embora seja verdade que os jogos da série Classic NES eram novos para serem jogados em um dispositivo portátil, emular o Game Boy Advance em outros dispositivos portáteis estaria há anos de distância. Além disso, embora esses problemas certamente tenham barrado sua emulação em vários projetos, essas proteções parecem ser exclusivas da série Classic NES. Não estou certo de por que eles colocaram todo esse esforço para trazer problemas para desenvolvedores de emuladores neste caso específico, mas isto certamente levou a várias noites muito longas para mim, tentando caminhar pelo código do jogo, uma função por vez, até as coisas darem visivelmente errado

Porém, após consertar todos esses problemas, os jogos agora devem estar 100% jogáveis. Diversos dos consertos entraram na versão 0.1.0, mas alguns dos maiores não foram concebidos até um tempo depois. Os jogos estarão completamente suportados na 0.2.0 quando for lançado, e já são jogáveis agora nos lançamentos noturnos.

9 META

Footnotes:

1

no original, "buggy" - cheio de bugs

2

literalmente, "múltipla armagenzagem"

Author: Tradutor Bastardo

Created: 2020-04-04 sáb 03:17

Validate