- O Mac Classic II chegou perto de escrever em um endereço incorreto por causa de um bug de salto fora do intervalo na ROM, mas a CPU Motorola MC68030 real executou uma instrução não documentada e alterou o registrador A1, evitando o travamento
- O problema apareceu na emulação do Classic II no MAME quando o endereçamento de 32 bits foi ativado, exibindo Sad Mac e Chimes of Death; com endereçamento de 24 bits, parecia inicializar normalmente
- O caminho
InstallSoundIntHandler na ROM aplicava o BoxFlag 17 do Classic II a uma tabela de desvios com 16 entradas e saltava para 0x40A43B94, posição que originalmente ficava no meio de uma instrução MOVEA.L
- A sequência de bytes
0C EC 08 A9 00 04 parecia algo como CAS D1,D2,$0004(A4), mas era uma instrução inválida da família CAS com bits reservados ativados; no 68030 real, ela alterava A1 de FFFF8FBA para 40A4BBB2
- A verificação com três ROMs customizadas em um Classic II real mostrou que, ao remover essa instrução e substituí-la por NOP, o hardware também falhava com Sad Mac; até descobrir o comportamento correto, o MAME contornou a inicialização com um patch na ROM
Falha de inicialização do Classic II revelada no MAME
- O MAME é mais conhecido como emulador de jogos de arcade, mas também oferece suporte à emulação de modelos de Mac baseados em 68000
- Durante uma modificação no MAME para ajustar o recurso que chama o depurador com a combinação command+power, foi descoberto um comportamento estranho no Classic II
- O Classic II tem um botão físico de interrupção e também um microcontrolador “Egret” 68HC05 que cuida de teclado, mouse etc.
- O MacsBug contém código para enviar o comando que ativa a combinação command+power
- O Classic II inicializava normalmente em endereçamento de 24 bits, mas ao ativar o endereçamento de 32 bits necessário para carregar o MacsBug, surgiam o Sad Mac e os Chimes of Death
- Segundo a documentação da Apple Tech Info Library, o código Sad Mac
0000000F indica exceção, e 00000001 indica erro de barramento
- Em Macs 68k, erro de barramento normalmente significa acesso a endereço inválido, como tentar acessar uma placa de expansão inexistente
Rastreamento da ROM: caminho de inicialização da interrupção de som
- No depurador do MAME, foi colocado um breakpoint no endereço do handler de erro de barramento
0x40A026F0, e o caminho de chamadas foi rastreado usando o mapa de símbolos da ROM
- Esse endereço aparece como
GenExcps no mapa da ROM
- A rotina comum de tratamento de erro aparece com o nome
ToDeepShit
- A instrução que provocava o erro de barramento era o código a seguir em
0x40A43B9C
move.b #$90, ($1c00,A1)
- Esse código estava dentro da rotina identificada no mapa da ROM como
InstallSoundIntHandler e estava relacionado à inicialização da interrupção de som do Classic II
- O Classic II, na verdade, é estruturalmente mais próximo da linha Macintosh LC do que do Macintosh Classic original, usando uma gate array EAGLE semelhante ao chip V8 do LC
- É por isso que nomes como
V8SndIntPatch1 aparecem no código da ROM
- O EAGLE inclui funcionalidades equivalentes a uma versão reduzida do Apple Sound Chip (ASC)
Tabela de desvios incorreta e o problema do registrador A1
V8SndIntPatch1 verifica os atributos de hardware usando o seletor gestaltHardwareAttr do trap Gestalt, isto é, 'hdwr'
- Se o bit
gestaltHasASC não estiver presente, ele retorna
- No Classic II,
gestaltHasASC está definido
- Em seguida, o código lê o valor de
BoxFlag em 0xCB3 na RAM e o coloca em D0
- No Classic II, o valor de
BoxFlag é 0x11, ou seja, 17
- O desvio problemático dobrava D0 e o usava como offset de salto relativo ao PC
add.w d0,d0
jmp loc_40A43B72(pc,d0.w)
- Como cada instrução
BRA.S ocupa 2 bytes, a estrutura dobrava o valor de BoxFlag para usá-lo como índice na tabela de desvios
- Porém, a tabela real tinha apenas 16 entradas, correspondentes aos BoxFlags 0 a 15; o valor 17 do Classic II ficava fora do intervalo
- O destino calculado do salto era
0x40A43B72 + 0x22 = 0x40A43B94
- Essa posição não era o início de uma instrução pretendida, mas o meio de uma instrução
MOVEA.L
- A
MOVEA.L original deveria carregar um endereço correto em A1, mas como o salto caiu no meio da instrução, A1 manteve o valor 0xFFFF8FBA usado no cálculo anterior do salto relativo
- Em seguida, quando
move.b #$90, ($1c00,A1) era executada, o endereço de escrita virava 0xFFFF8FBA + 0x1C00 = 0xFFFFABBA, um endereço inválido no Classic II
Comportamento não documentado do 68030
- A sequência de bytes alcançada pelo salto fora do intervalo era a seguinte
0C EC 08 A9 00 04
- Nem o IDA nem o GNU objdump conseguiram desmontar corretamente a instrução nessa posição
- Ao executar a mesma sequência de bytes no MacsBug de um Macintosh IIci, ela foi exibida como algo parecido com
CAS.W D1,D2,$0004(A4), mas o valor de A1 mudava após a execução
- A1 passava de
0xFFFF8FBA para um valor que parecia um endereço de RAM
- Foi observado que o novo valor de A1 variava conforme os valores originais de A1, A7 e do contador de programa
- Segundo o Motorola M68000 Family Programmer’s Reference Manual, a primeira word parece uma instrução CAS, mas na segunda word há 3 bits que deveriam ser 0 e estão definidos como 1
- Portanto, essa instrução não era um CAS válido documentado, mas uma instrução inválida com bits reservados ativados
- O 68030 real não tratava essa instrução como exceção de illegal instruction e executava um ciclo de barramento read-modify-write sobre A4+4
- Se A4 fosse configurado com um endereço inválido, o MacsBug mostrava erro de barramento durante o read-modify-write em A4+4
- A1 também era alterado, embora um CAS normal não devesse modificá-lo
Verificação em hardware real do Classic II
- Para testar a hipótese, foi comprado e reparado um Classic II fabricado em 1991, e a ROM foi substituída por uma EEPROM SST29EE010 programável
- Capacitores SMD em Macs antigos podem vazar substâncias corrosivas, então eles foram removidos e reparados antes de ligar o equipamento
- Em vez de usar o CRT e a placa analógica, a placa lógica foi acionada separadamente, e a imagem foi verificada com um conversor VGA baseado em Raspberry Pi Pico
- Foi inserido em um espaço vazio da ROM um código em assembly 68030 para mostrar o valor de A1 na tela, e três ROMs customizadas foram produzidas
- ROM customizada 1: substitui a
MOVE.B em 0x40A43B9C, que causava Sad Mac no MAME, pelo código de exibição de A1
- ROM customizada 2: substitui a instrução parecida com CAS em
0x40A43B94, destino do salto fora do intervalo, pelo código de exibição de A1
- ROM customizada 3: substitui a instrução parecida com CAS em
0x40A43B94 por NOP
- A ROM customizada 1 mostrou que esse código realmente é executado no hardware real e que A1 no ponto em que o MAME travava vale
0x40A4BBB2
- Esse valor é um endereço de ROM, então não é adequado como destino de escrita, mas mesmo assim a tentativa de escrita não gera erro de barramento
- A ROM customizada 2 mostrou que, imediatamente antes do salto fora do intervalo, A1 tinha o mesmo valor visto no MAME:
0xFFFF8FBA
- O salto fora do intervalo realmente acontece
- Isso bate com a hipótese de que a instrução parecida com CAS altera A1 em seguida
- A ROM customizada 3 mostrou que, ao remover a instrução parecida com CAS, o Classic II real também falha com Sad Mac
- Nesse teste, o hardware apresentou o mesmo Sad Mac até em modo de 24 bits
- O fato de o MAME não gerar erro de barramento para essa escrita incorreta no modo de 24 bits significava que ele estava sendo mais permissivo do que o hardware real
Problemas que continuam no MAME e na emulação
- O comportamento descoberto é um caso em que uma instrução MC68030 não documentada executa um ciclo de barramento read-modify-write e ainda altera o registrador A1
- O bug na ROM do Classic II ficou escondido graças a esse comportamento, e por isso a máquina real funcionava normalmente
- O mesmo trecho de código também foi encontrado em ROMs mais novas do Macintosh IIvx, mas ali a tabela de saltos foi ampliada e o caso do Classic II salta diretamente para RTS
- Por causa disso, parece provável que ainda não exista um emulador ou implementação clonada 100% perfeita do Motorola MC68030
- Dá para criar um pequeno código que execute essa instrução e verifique o resultado em A1 para distinguir um 68030 físico de um emulador
- Até que o comportamento exato da instrução seja esclarecido, o MAME permite a inicialização aplicando um patch no bug da ROM do Classic II
- Separadamente, a combinação command+power funciona em um Classic II real quando o MacsBug está instalado, mas ainda não funciona no MAME
1 comentários
Opiniões no Hacker News
O comportamento descoberto no MC68030 talvez seja menos uma instrução “real” criada intencionalmente pelos projetistas da CPU e mais o resultado de a lógica interna da CPU ter sido executada por acaso, com entradas don’t-care, diante de uma instrução ilegal
Normalmente, a CPU deveria detectar uma instrução ilegal e gerar uma exceção, mas parece que em certos casos isso não acontece
Nas páginas 8-9 de https://www.nxp.com/docs/en/reference-manual/MC68030UM.pdf, diz-se que uma instrução ilegal é definida pelo padrão de bits da “primeira word”
Esta instrução tem 3 words, a primeira word é normal, e os bits estranhos estão na segunda word; portanto, é bem provável que o 68030 não tenha validado a segunda word e tenha simplesmente seguido em frente com a lógica de implementação da instrução CAS
Os códigos de instrução “válidos” eram apenas combinações úteis o suficiente para serem documentadas, enquanto as combinações “inválidas” eram inúteis ou sem sentido
Visto assim, códigos de instrução ilegais/misteriosos não são uma exceção surpreendente, mas algo quase inevitável
Normalmente, eu esperaria que os bits 0 que não têm significado na documentação fossem completamente ignorados
Mas, no 68030, talvez esses bits precisem ser 0; e, como nos opcodes ilegais do 6502, quando esses bits são ligados, alguma lógica hardwired de outra instrução pode acabar sendo ativada
CAS sempre foi uma dor de cabeça, e provavelmente foi a instrução que mais recebeu relatos de bugs em emulação
No antigo King of Fighters também havia um bug que verificava “errado” o carry flag da instrução SBCD, usando isso para diminuir o timer do round e encerrar o round
Mesmo sendo um comportamento não documentado, se os flags de estado aritmético em operações BCD não forem emulados, o timer de round do KOF fica circulando de 00 para 99 indefinidamente e nunca termina
A SNK realmente era formada por deuses do chip 68000
É uma jornada realmente longa
Hoje em dia não tenho a paciência de descer tão fundo na toca do coelho como o autor, mas me identifico muito com a sensação de realização que vem da diferença entre achar que se sabe algo e realmente saber
Mesmo tomando muito tempo, quando se vai até o fim é realmente recompensador
Mesmo mais de 30 anos depois, sempre me surpreende como a UI do debugger do Mac conseguia ser tão eficaz em uma resolução de tela tão pequena
Dá mesmo para sentir o nível de artesanato ali
Quase toda CPU tem instruções não documentadas, e o 68k não é exceção
A questão é que, na época, a maioria das pessoas com interesse suficiente e conhecimento de baixo nível estava concentrada em x86/PC, e dá para dizer que x86/PC era uma arquitetura muito mais aberta e estável do que o lado da Apple
O microcódigo do 8088 e do 8086 foi desassemblado alguns anos atrás e bastante estudado, e sei que também houve tentativas de simulação em nível de transistor
A estrutura do espaço de opcodes x86 também já foi explorada em detalhes em documentos como estes
http://ref.x86asm.net/geek.html
https://gist.github.com/seanjensengrey/f971c20d05d4d0efc0781...
Ainda não sabemos exatamente o que essa instrução faz
Em testes limitados, o valor resultante de A1 parecia variar conforme o valor original de A1, o valor de A7 e o contador de programa, mas não tenho certeza
Se alguém criar um programa que experimente vários valores de registradores e conteúdos de memória para inferir o comportamento exato, a emulação também poderá ficar mais precisa
Por enquanto, até alguém considerar que vale a pena investigar, o MAME está corrigindo esse bug da ROM para fazer o Classic II inicializar
Pessoalmente, acho que vale a pena esclarecer isso em nome de uma emulação precisa
Não sou familiarizado com 68k, mas os bits da instrução dão pistas. A hipótese é que os bits 5:3 da segunda word parecem outro campo de modo; em vez de o modo 000 selecionar o registrador Dn, o 101 selecionaria novamente (d16, An), e o campo Dc contendo 001 parece ser interpretado como A1
O 68030 era uma CPU de uso geral bastante boa na época e também foi usado no Amiga 3000, Atari Falcon, Sun 3, NeXT Cube etc.
Na demoscene, muita gente ainda se mantinha nos computadores domésticos de 8 bits, e quem usava sistemas domésticos de 16 bits se concentrava no Atari e no Amiga
O PC e o x86 só começaram a decolar de verdade em casa depois que VGA e placas de som passaram a fazer parte da configuração padrão de um PC
Quando eu era criança, tinha um Amiga 2000 com processador 68000
Fiquei muito empolgado quando ouvi falar do 68020 ou do 68030, e depois foi a mesma coisa com as arquiteturas RISC
Quando eu fazia o Amiga dizer frases como “Hello, how are you?” com aquela voz robótica, meus amigos ficavam maravilhados como se eu tivesse ido à Lua
Eu jamais teria imaginado que, menos de 40 anos depois, estaríamos conversando com computadores em linguagem natural por meio de LLMs e automatizando praticamente qualquer coisa que quiséssemos usando Python, VS Code e LLMs como ferramentas
É uma época realmente insana
Sinto falta da família 68000
Eram chips realmente excelentes
Era claro e lógico, como toda boa engenharia deveria ser, então dava para entender imediatamente o que estava acontecendo só de passar os olhos
Em contraste, assembly x86 parece, na maior parte, uma bagunça pura, como um monte de truques espertos saídos de uma noite mal dormida
Subtrair um registrador dele mesmo para obter 0 é demais
Fico curioso se isso era uma proteção contra cópia para impedir que rodasse em determinados sistemas, ou se era um fenômeno que acontece em todos os 68030
A tabela de saltos envolvida aqui não tinha uma entrada para aquela máquina, e parece que os responsáveis pela ROM provavelmente esqueceram completamente de adicionar a nova entrada, mas, por puro acaso, funcionou mesmo sem a entrada na tabela
Nos Macs de hoje, algo assim parece impossível daqui para frente
A documentação técnica atual da Apple é péssima
Eles não expõem, por meio de conectores, um barramento de sistema no qual o usuário pudesse encaixar placas e desenvolvedores terceiros precisassem interagir diretamente com o hardware, como antigamente
Nos Macs modernos, há USB e Thunderbolt, que programas em espaço de usuário podem lidar
Claro, não nego que a documentação de algumas APIs recentes do macOS seja muito ruim
Quando você chega a uma página antiga com um degradê azul no cabeçalho e o texto “Apple documentation archive”, sabe que encontrou algo legal
O espaço de endereçamento é muito maior e a maioria dos endereços não é mapeada, então uma operação de memória em um endereço lixo geralmente falharia, e uma instrução inválida provavelmente também falharia
O bug em si, de ler uma tabela de saltos com um índice fora do intervalo, ainda pode acontecer em software moderno, mas é mais provável que o processo morra do que continue rodando como aqui
Como observação, WebAssembly tem um único espaço de endereçamento linear e todos os endereços podem ser lidos e escritos, então esse tipo de bug é mais possível
Se o endereço lixo estiver dentro do intervalo, dá para fazer uma leitura, uma escrita ou um CAS incorretos e ainda seguir em frente
Ainda assim, uma instrução inválida como a do texto faria o carregamento do módulo WASM falhar, então não é exatamente igual a esse problema na ROM do Mac
Fico curioso se o 040/060 também suporta essa “instrução não documentada”
A1 não foi tocado de forma alguma e, embora eu não tenha testado mais, talvez ela simplesmente seja tratada como um CAS normal
Quando executei essa instrução passo a passo pela primeira vez no MacsBug de um LC 475, houve um erro de sistema, mas depois disso ficou tudo bem
Foi apenas um teste muito rápido exatamente com essa word de instrução