Como depurar crash do cliente de MU (análise de dump)
Transforme aquele crash misterioso do Main.exe em causa raiz usando dumps de memória, WinDbg, símbolos e leitura de stack trace, com um fluxo repetível para achar o módulo culpado e corrigir.
Um jogador abre um ticket: "o jogo fecha sozinho quando entro em Lorencia". Sem informação, isso vira adivinhação — troca driver, reinstala, tenta de novo, e o problema volta. A forma profissional de resolver é transformar o crash num artefato analisável: um dump de memória. Com o dump certo, símbol
Um jogador abre um ticket: "o jogo fecha sozinho quando entro em Lorencia". Sem informação, isso vira adivinhação — troca driver, reinstala, tenta de novo, e o problema volta. A forma profissional de resolver é transformar o crash num artefato analisável: um dump de memória. Com o dump certo, símbolos configurados e uma leitura metódica da pilha, você sai de "fecha sozinho" para "acesso a ponteiro nulo dentro de d3d9.dll chamado a partir do render de partículas na build 1.04g". Este tutorial monta esse fluxo do começo ao fim, repetível para qualquer crash do Main.exe. Todos os offsets, nomes de módulo e caminhos aparecem como exemplo e variam por season/cliente.
Pré-requisitos
Este guia assume um cliente que já roda na maioria das máquinas e crasha em situações específicas. Se você ainda está montando a base do servidor, veja antes como criar um servidor de MU Online. Depurar crash faz sentido quando o cliente já funciona no caso comum e falha na exceção.
| Requisito | Detalhe (exemplo — varia por season/cliente) |
|---|---|
| WinDbg | Do Windows SDK (WinDbg clássico ou WinDbg Preview) |
| Símbolos da Microsoft | Cache local + srv público |
| PDB do Main.exe | Da build exata, se você compila o cliente |
| Process Hacker | Para gerar dump sob demanda |
| Máquina saudável de referência | Onde o cliente roda sem crashar |
| O dump do crash | Minidump da máquina afetada |
O item que separa análise rápida de sofrimento é o PDB correto. Sem ele, você lê a pilha em offsets; com ele, em nomes de função. Se você tem o código do cliente, arquive o PDB de cada versão lançada junto ao binário — dias depois, um dump antigo sem o PDB certo é quase inútil para os frames do próprio cliente.
Etapa 1 — Capturar o dump
Sem dump não há análise. Há três formas, em ordem de praticidade para produção.
Passo 1 — WER automático (recomendado)
Configure o Windows Error Reporting para capturar todo crash do Main.exe numa pasta local. Isso pega o crash no ato, sem depender de o jogador reproduzir com uma ferramenta aberta.
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\Main.exe]
"DumpFolder"=hex(2):43,00,3a,00,5c,00,43,00,72,00,61,00,73,00,68,00,00,00
"DumpType"=dword:00000001
"DumpCount"=dword:0000000a
DumpType=1 gera minidump; 2 geraria full dump. O dump aparece na pasta configurada assim que o Main.exe fechar por exceção. Distribua esse ajuste no seu launcher para coletar dumps de jogadores que aceitarem enviar.
Passo 2 — Dump sob demanda com Process Hacker
Quando o crash é um "trava" antes de fechar, ou quando você quer o estado no momento certo: abra o Process Hacker, ache Main.exe, botão direito → Create dump file. Gera um minidump manual do processo vivo. Útil para hangs (não crashes), quando o processo não morre sozinho.
Passo 3 — MiniDumpWriteDump no próprio cliente
Se você compila o cliente, instale um handler de exceção não tratada que chame MiniDumpWriteDump. Assim o cliente gera seu próprio dump com contexto extra (versão, mapa atual, jogador) gravado junto num log. É o mais rico, porque você controla o que capturar.
// Esboço — instalar cedo, antes do resto do init
LONG WINAPI MeuHandler(EXCEPTION_POINTERS* ep) {
HANDLE h = CreateFile(L"C:\\Crash\\main_crash.dmp", GENERIC_WRITE, 0,
nullptr, CREATE_ALWAYS, 0, nullptr);
MINIDUMP_EXCEPTION_INFORMATION mei{ GetCurrentThreadId(), ep, FALSE };
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), h,
MiniDumpNormal, &mei, nullptr, nullptr);
return EXCEPTION_EXECUTE_HANDLER;
}
// SetUnhandledExceptionFilter(MeuHandler);
Etapa 2 — Preparar o WinDbg
Símbolos mal configurados fazem uma pilha legível virar sopa de offsets. Configure antes de abrir qualquer dump.
Passo 4 — Caminho de símbolos
No WinDbg, defina o cache local mais o servidor público da Microsoft e recarregue:
.sympath srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
.reload /f
Isso resolve ntdll, kernel32, d3d9, user32 e afins. Para os frames do próprio Main.exe, adicione a pasta com o PDB da build:
.sympath+ C:\MU\pdb\1.04g
.reload /f Main.exe
Passo 5 — Abrir o dump
File → Open Crash Dump, selecione o .dmp. O WinDbg carrega os módulos e para no contexto da exceção. A primeira coisa a rodar é a análise automática.
Etapa 3 — Análise automática e leitura da pilha
Passo 6 — !analyze -v
!analyze -v
Esse comando faz o trabalho pesado inicial: identifica o código da exceção, o endereço que falhou, o módulo culpado provável e imprime a pilha do thread que crashou. Leia com atenção estes campos:
| Campo | O que diz |
|---|---|
EXCEPTION_CODE | Tipo do erro (ex.: c0000005 = access violation) |
FAULTING_IP | Instrução/endereço onde estourou (ex.: Main+0x3f21a) |
MODULE_NAME | Módulo apontado como culpado |
STACK_TEXT | A pilha de chamadas até o crash |
PROCESS_NAME | Deve ser Main.exe |
c0000005 (access violation) é de longe o mais comum: o cliente tentou ler/escrever num endereço inválido — quase sempre um ponteiro nulo ou já liberado. Se o endereço que falhou é próximo de zero (ex.: 0x00000008), é ponteiro nulo com offset de campo de struct: alguém acessou objeto->campo com objeto == null.
Passo 7 — Ler a pilha manualmente
k
k imprime a pilha do thread atual. Leia de baixo para cima (mais antigo embaixo, o crash no topo). Você quer responder: a culpa é do cliente, de um DLL de terceiros ou do sistema?
# exemplo ilustrativo — varia por season/cliente
00 d3d9!CDevice::DrawPrimitive+0x2a <- topo: onde estourou
01 Main+0x3f21a <- código do cliente chamando o render
02 Main+0x41008
03 Main+0x51c30
04 kernel32!BaseThreadInitThunk <- base: início do thread
Aqui o topo está em d3d9, mas quem chamou com um argumento ruim foi Main+0x3f21a. Isso aponta para render/gráfico do cliente. Se o topo estivesse num módulo estranho (ex.: overlay_x.dll ou um antivírus), a suspeita mudaria para software de terceiros injetado.
Passo 8 — Listar módulos e achar o intruso
lm
lm lista todos os módulos carregados. Compare com os de uma máquina saudável. Módulos que só aparecem no dump problemático — overlays de gravação, injetores, hooks de antivírus — são fortes suspeitos, especialmente se aparecem na pilha.
Etapa 4 — Triagem por padrão de crash
Com pilha e módulos em mãos, classifique. A maioria dos crashes de cliente de MU cai em poucos padrões.
| Padrão na pilha | Causa provável | Direção da correção |
|---|---|---|
Topo em d3d9/d3d8 chamado pelo cliente | Recurso gráfico nulo, textura/efeito ausente | Conferir asset faltando; atualizar driver GPU |
Topo em Main.exe com c0000005 perto de zero | Ponteiro nulo no código do cliente | Localizar offset no PDB; corrigir no fonte |
| Módulo de terceiros no topo | Overlay/AV/injetor | Remover software; testar sem ele |
Stack overflow (c00000fd) | Recursão infinita | Rever loop/callback do cliente |
Heap corruption | Escrita fora de buffer | Full dump + gflags/PageHeap |
| Crash só ao carregar um mapa | Asset corrompido daquele mapa | Repatch dos arquivos daquele mapa |
Passo 9 — Do offset para a linha (com PDB)
Se o crash está no Main.exe e você tem o PDB, resolva o endereço:
ln Main+0x3f21a
ln mostra o símbolo mais próximo — nome da função e deslocamento. Com fonte, você chega à linha. Sem PDB, Main+0x3f21a ainda é útil: ele é estável para aquela build, então crashes repetidos no mesmo offset confirmam o mesmo bug, e você pode comparar relatos de vários jogadores.
Etapa 5 — Reproduzir e confirmar
Análise sem reprodução é hipótese. Tente reproduzir na máquina de referência seguindo o cenário do jogador (mesmo mapa, mesma ação). Se reproduzir, gere um dump seu e confira se cai no mesmo offset/módulo. Bater confirma a causa; não bater indica fator de ambiente (driver, overlay, DLL local do jogador).
Para crashes de heap intermitentes, ligue o PageHeap para o Main.exe com gflags antes de reproduzir — ele faz a corrupção estourar no ato da escrita inválida, não depois, deixando a pilha apontar o verdadeiro culpado.
Erros comuns e soluções
| Sintoma na análise | Causa | Solução |
|---|---|---|
| Pilha só com offsets, sem nomes | PDB/símbolos ausentes | Configurar .sympath; arquivar PDB por build |
!analyze -v aponta módulo do sistema | Culpa real é de quem chamou | Ler k e achar o frame do cliente que chamou |
| Dump vazio ou truncado | Minidump insuficiente | Gerar full dump para o caso |
| Crash não reproduz na sua máquina | Fator de ambiente do jogador | Comparar lm; suspeitar de overlay/driver/AV |
| Offset muda a cada relato | Builds diferentes entre jogadores | Padronizar versão do cliente via launcher |
| WinDbg não abre o dump | Dump de arquitetura diferente (x86/x64) | Usar WinDbg correspondente à arquitetura |
| Símbolos não baixam | Firewall bloqueia o msdl | Liberar acesso ou pré-popular o cache local |
Boas práticas para não depurar às cegas
Depuração começa antes do crash. Padronize a versão do cliente com um launcher para que todos os dumps sejam da mesma build; arquive o PDB de cada versão lançada; embuta um handler de exceção que grave dump + contexto (mapa, versão, jogador); e mantenha uma máquina de referência saudável para comparação de módulos. Com isso, cada ticket "fecha sozinho" chega já com dump e contexto, e a análise vira rotina em vez de arqueologia.
Checklist de lançamento
- WER configurado para capturar dumps do
Main.exe - Handler de exceção com
MiniDumpWriteDumpno cliente (se compila) - Contexto (versão, mapa, jogador) gravado junto ao dump
- PDB de cada build arquivado junto ao binário
- WinDbg com
.sympathpara cache local + servidor MS - Máquina de referência saudável disponível para comparação
- Fluxo
!analyze -v→k→lm→lndocumentado para a equipe - Versão do cliente padronizada via launcher
- Procedimento para o jogador enviar o dump com segurança definido
- Crashes catalogados por offset/módulo para detectar recorrência
- PageHeap/gflags disponível para caçar corrupção de heap
- Correção validada por reprodução no mesmo offset antes de fechar o caso
Perguntas frequentes
Qual a diferença entre minidump e full dump?
O minidump é pequeno (poucos MB) e contém pilhas de threads, registradores e módulos carregados, suficiente para a maioria das análises de crash. O full dump inclui toda a memória do processo, é enorme (pode passar de 1 GB) e só vale a pena quando você precisa inspecionar heap, buffers e estados de objetos que o minidump não guarda. Comece sempre pelo minidump.
Preciso do código-fonte do Main.exe para analisar o dump?
Ajuda muito, mas não é obrigatório. Sem fonte e sem símbolos (PDB), você ainda vê a pilha em termos de módulos e offsets (ex.: Main.exe+0x3F21A), identifica se o crash é no cliente, num DLL de terceiros ou no sistema, e reconhece padrões como acesso a ponteiro nulo. Com símbolos você ganha nomes de função; com fonte, a linha exata.
Onde o Windows salva os dumps de crash?
Depende da configuração. O WER (Windows Error Reporting) costuma gravar em %LOCALAPPDATA%\\CrashDumps quando habilitado por registro. Você também pode gerar sob demanda com o Process Hacker/Task Manager (Create dump file) ou programaticamente via MiniDumpWriteDump no próprio cliente. Configurar o WER para capturar o Main.exe automaticamente é o mais prático em produção.
O crash só acontece na máquina de alguns jogadores, e agora?
Crash específico de máquina quase sempre é ambiente: driver de GPU antigo, DLL de terceiros injetada (overlay, antivírus), DirectX/redistributables faltando, ou modo de compatibilidade errado. Peça o dump ao jogador, compare os módulos carregados com os de uma máquina saudável e procure o módulo estranho na pilha. Muitas vezes a correção é atualizar driver ou remover um overlay.
Como configurar símbolos no WinDbg?
Aponte o caminho de símbolos para o servidor da Microsoft mais uma pasta local de cache, por exemplo com .sympath e .reload. Isso resolve os nomes das funções do sistema (ntdll, kernel32, d3d9). Para o próprio Main.exe, você precisa do PDB correspondente àquela build; sem ele, os frames do cliente aparecem só como offsets. Guarde os PDBs de cada versão que você lança.