Introdução
A maioria das CPUs modernas é compatível entre si porque compartilha a mesma ISA1. A ISA mais comum hoje é a x86_64, uma extensão de 64 bits da antiga arquitetura x86. A necessidade de operações mais complexas e rápidas, especialmente na aritmética de ponto flutuante, levou a extensões como SSE2 e AVX3.
Essas extensões têm várias versões que permitem ao processador interpretar diferentes conjuntos de opcodes. O SSE chega à versão 4.2, além de uma versão 4a exclusiva da AMD. O AVX chega ao AVX-512, mas o conjunto de extensões mais comum em CPUs COTS4 é o AVX-2.
Ao compilar software, é possível definir uma flag que determina se o código de máquina resultante será compatível com um conjunto específico de extensões. Essa escolha pode levar a um desempenho melhor, porque as extensões conseguem realizar determinadas operações usando menos ciclos de CPU.
Porém, CPUs mais antigas, perfeitamente capazes de executar software moderno, podem ter chegado ao mercado antes de as extensões mais recentes estarem disponíveis. Portanto, software compilado com essas instruções mais novas simplesmente não consegue ser executado nelas.
Detectando suporte a extensões
A descrição dessas extensões está incluída nas flags de CPUID. A forma mais fácil de verificar se sua CPU suporta instruções específicas é pelo CPU-Z:
Você também pode simplesmente tentar executar o software. Ele pode mostrar um pop-up dizendo que não é compatível, ou apenas travar silenciosamente. Verifique o Visualizador de Eventos do Windows em busca do código de erro 0xC000001D, que indica que uma instrução ilegal foi tentada.
Processo de patching
Vou descrever o processo de patching que usei para contornar essas instruções no jogo Days Gone. Este é o meu método e não é necessariamente o ideal, mas funciona. Vale notar que até compradores legítimos do jogo não conseguiam executá-lo em processadores um pouco mais antigos — ao patchear algumas instruções, consegui torná-lo compatível.
Ao executar o jogo, ocorre um CTD5 imediato e o processo desaparece do Gerenciador de Tarefas. Não há avisos nem indicações do problema.
Configurando o depurador
Carregue o jogo no x64dbg e defina um breakpoint em EXCEPTION_ILLEGAL_INSTRUCTION. Essa exceção é gerada quando o processador encontra um opcode desconhecido:
Execute o jogo até que esse breakpoint seja atingido. Você pode encontrar outros breakpoints; continue até chegar ao da instrução inválida. Processadores diferentes podem ter conjuntos de extensões disponíveis diferentes e, por isso, interpretar opcodes diferentes.
Observe como a barra de status inferior mostra que a execução parou no breakpoint EXCEPTION_ILLEGAL_INSTRUCTION. Agora estamos em um opcode não suportado.
Primeira tentativa: usar NOP
Geralmente tento primeiro aplicar NOP6 à instrução (botão direito → Binary → Fill with NOPs). Isso pode funcionar ou não, dependendo da instrução e do software. Jogos costumam usar essas instruções para decodificar conteúdo de vídeo, como créditos ou cutscenes.
No entanto, simplesmente aplicar NOP à instrução pode quebrar o software. Nesse caso, precisamos substituir essas instruções por equivalentes que usem opcodes disponíveis na nossa CPU.
Encontrando instruções equivalentes
Uma busca pela instrução pminuw me levou a este recurso útil, que fornece substitutos compatíveis com SSE2:
Implementação de PMINUW compatível com SSE2
; xmm0 = entrada|saída ; xmm1 = entrada ; xmm7 = temporário movdqa xmm7, xmm0 ; Move xmm0 para o temporário psubusw xmm7, xmm1 ; Subtração com saturação sem sinal psubw xmm0, xmm7 ; Subtração (sem saturação; aqui nunca haveria overflow)
Isso significa que podemos substituir nossa instrução por essas três para obter compatibilidade com SSE2. Porém, a instrução atual tem apenas 5 bytes (66:0F383AD1) e precisamos de 3×4 = 12 bytes para a substituição. Como fazer isso caber?
Usando code caves
Podemos saltar para outro local que comporte essas instruções e então saltar de volta para continuar a execução. Primeiro, vamos calcular o espaço necessário:
movdqa : 4 bytes
psubusw : 4 bytes
psubw : 4 bytes
jmp : ≤5 bytes
Precisamos de aproximadamente 17 bytes de espaço livre. Procure uma "code cave" — uma área não utilizada na seção executável. Faça uma busca por um padrão de 17 bytes CC consecutivos ou navegue pelo arquivo para encontrar uma área vazia:
Abra o primeiro resultado e escreva as instruções de substituição. Ao final, adicione um jmp de volta à instrução seguinte à que substituímos. Lembre-se de ajustar os registradores e verificar se o registrador temporário não está sendo usado em outro lugar:
Concluindo o patch
Substitua a instrução original por um jmp para a nossa code cave:
É isso. Repita até que não haja mais breakpoints. Em Days Gone, substituí 12 instruções pminuw e 2 instruções pmaxud. Algumas instruções não tinham substituições fáceis de encontrar, então simplesmente apliquei NOP nelas — e funcionou.
Resumo das etapas
- Identifique o problema: verifique o CPU-Z para ver os conjuntos de instruções suportados ou procure o código
0xC000001Dno Visualizador de Eventos do Windows - Configure o depurador: carregue o executável no x64dbg e defina um breakpoint em
EXCEPTION_ILLEGAL_INSTRUCTION - Encontre instruções não suportadas: execute até o breakpoint ser atingido para identificar o opcode problemático
- Tente NOP primeiro: às vezes, simplesmente preencher a instrução com NOPs funciona
- Encontre instruções equivalentes: procure alternativas compatíveis com SSE2 para as instruções SSE4.x
- Localize code caves: procure espaço vazio (bytes
CC) no executável - Patche a code cave: escreva as instruções substitutas seguidas de um salto de volta
- Substitua a instrução original: altere-a para saltar à sua code cave
- Repita: continue até que todas as instruções ilegais sejam patcheadas
Jogos que exigem instruções SSE4.1/4.2
Lista compilada por mafia47 (alguns jogos também exigem AVX)
| Jogo | Status |
|---|---|
| Sunset Overdrive | Ainda não há correção |
| Journey | Correção: desative a interpolação de tecido (somente na versão mais recente) |
| Metal Slug XX | Ainda não há correção |
| Beyond: Two Souls | Jogável com Intel SDE7 |
| Detroit: Become Human | Correção disponível |
| Control Ultimate Edition | Ainda não há correção |
| Yakuza 3, 4, 5, 6 | Ainda não há correção |
| Yakuza: Like a Dragon | Ainda não há correção |
| Horizon Zero Dawn | Correção apenas para Steam v1.0.9.3 |
| Days Gone | Correção apenas para v1.0 |
| Little Nightmares | Jogável com Intel SDE |
| Unravel Two | Jogável com Intel SDE |
| Death Stranding | Ainda não há correção |
| Cyberpunk 2077 | Correção para GOG v1.22 (requer testes) |
| Red Dead Redemption 2 | Correção apenas para a versão crackeada |
| Nioh 2 | Ainda não há correção |
| God Eater 3 | Ainda não há correção |
| Assassin's Creed Origins | Correção para v1.51 (não para CPU Phenom) |
| Assassin's Creed Odyssey | Correção para v1.5.3 (não para CPU Phenom) |
| Assassin's Creed Valhalla | Ainda não há correção |
| Immortals Fenyx Rising | Ainda não há correção |
| Far Cry 5 | Corrigido pelos desenvolvedores na versão mais recente; sem correção para versões antigas |
| Dead Rising 4 | Ainda não há correção |
| Crash Bandicoot 4 | Ainda não há correção |
| Saints Row The Third Remastered | Ainda não há correção |
| DOOM Eternal | Correção apenas para exe sem Denuvo (não para CPU Phenom) |
| Need for Speed Heat | Ainda não há correção |
| NBA 2K21 | Ainda não há correção |
| Shadow Man Remastered | Jogável com Intel SDE |
| Biomutant | Ainda não há correção |
| Outriders | Ainda não há correção |
| Puyo Puyo Tetris 2 | Corrigido pelos desenvolvedores |
| Diablo II Resurrected | Ainda não há correção |
Considerações finais
Tenho certeza de que existem abordagens melhores, seja automatizando com scripts, seja usando plugins especializados para depuradores. Também estou trabalhando em um emulador genérico que poderia nos ajudar a continuar usando CPUs mais antigas, porém perfeitamente capazes: isso seria ideal porque uma emulação JIT não altera o software original e emularia apenas as instruções necessárias, gerando (esperamos) uma sobrecarga mínima.
Footnotes
-
Instruction Set Architecture é o conjunto de instruções que um processador consegue entender e executar. ↩
-
Streaming SIMD Extensions; SIMD significa Single Instruction, Multiple Data e permite operações paralelas sobre vários pontos de dados. ↩
-
Advanced Vector Extension fornece registradores mais largos (de 256 e 512 bits) para ainda mais capacidade de processamento paralelo. ↩
-
Commercial Off-The-Shelf é hardware padrão de consumo, em oposição a processadores especializados ou personalizados. ↩
-
Crash To Desktop: o aplicativo é encerrado abruptamente, sem uma caixa de diálogo de erro. ↩
-
NOP (No Operation) é uma instrução que não faz nada. Preencher código com NOPs remove sua funcionalidade na prática, preservando o alinhamento dos bytes. ↩
-
Intel Software Development Emulator pode emular instruções ausentes, mas com sobrecarga significativa de desempenho. ↩