O cliente do TOTVS Linha RM é .NET Framework 4.6, WinForms, com quase 600 DLLs DevExpress e um auto-atualizador próprio. É a categoria mais difícil que existe para o Wine. Mesmo assim ele instala, abre, desenha a tela de login e autentica de verdade contra o servidor. E aí, ao montar a tela principal, morre assim:

Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory.
   at RM.MDI.MainForm.LoadModule(String moduleClassName)
   ...
wine: Unhandled page fault on execute access to 0000000000000000 at address 0000000000000000

Page fault no endereço zero num aplicativo gerenciado é chamada em ponteiro de função nulo. O palpite óbvio é o suspeito óbvio: gráfico, tema, DevExpress. Perdemos horas trocando componentes nativos por esse caminho — e pioramos o quadro: instalar gdiplus, riched20, vcrun2019, msxml6 e windowscodecs nativos fez o aplicativo quebrar antes da tela de login. A configuração que funciona é Wine puro + .NET Framework real, com tudo forçado de volta ao builtin.

O que resolveu não foi tentativa e erro. Foi ler o que o programa faz.

O método: pare de adivinhar, leia o IL

O winedbg só mostra pilha nativa e não serve aqui. As fontes úteis de pilha gerenciada, em ordem de preferência:

  1. A saída padrão do aplicativo. O RM usa Serilog e despeja a pilha no stdout do processo Wine. Como o log vem com acentuação quebrada, grep puro falha — use strings antes: strings wine.log | grep -aE "Exception| at ".
  2. O relatório de erro do próprio produto. Quase todo ERP tem um. No RM é o botão ... da caixa de erro → Feedback do UsuárioGerar. Gera um arquivo local (use esse, nunca o “Reportar para o fabricante”), que é base64 de um gzip de um objeto serializado. Para ler: base64 -d relatorio | gunzip | strings | sed -n '/Call Stack Information/,/^$/p'.
  3. %LOCALAPPDATA%\Temp\RM.el, que o mesmo mecanismo escreve.

A pilha nomeia o método. Se o nome estiver ofuscado — e estará —, dois detalhes salvam: nomes de lambda vazam (<GetBindingsPriority>b__6_0 no meio de um PLDCHGNPHOB...) e valores de campo string não são ofuscados.

Daí em diante é reflexão pura. Um utilitário de ~120 linhas carrega a DLL, pega MethodBody.GetILAsByteArray(), percorre os opcodes e resolve cada token com Module.ResolveMember(). Ele imprime todas as chamadas, constantes e literais do método. Compila com o csc que o próprio .NET Framework instala dentro do prefixo: C:\windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe.

Vinte minutos de IL responderam o que horas de tentativa e erro não responderam.

Defeito 1 — o Wine não cria Tcpip\Linkage

A pilha do primeiro erro:

System.NullReferenceException
   at RM.Lib.RMSNetUtils.<ofuscado>()          <- GetBindingsPriority
   at RM.Lib.RMSNetUtils.GetTopIpv4Ip(String hostName)
   at RM.Lib.RMSNetUtils.GetFirstIpv4Ip()
   at RM.Lib.LicenceServer.RMSLicenseClientBase..ctor()
   at RM.Lib.LicenceServer.RMSLicenseClientFactory.GetImplementation()

O IL mostrou exatamente o que ele faz:

Registry.LocalMachine.OpenSubKey("SYSTEM\CurrentControlSet\Services\Tcpip\Linkage")
        .GetValue("Bind")                      // REG_MULTI_SZ, entradas "\Device\{GUID}"
  → para cada GUID, tirando o prefixo "\Device\":
Registry.LocalMachine.OpenSubKey("SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\" + GUID)
        .GetValue("IPAddress")                 // REG_MULTI_SZ
        .GetValue("DhcpIPAddress")             // REG_SZ

É a ordem de binding das placas de rede do Windows, usada pelo cliente de licença para descobrir o IPv4 da máquina. Essas chaves não existem num prefixo Wine — só existe Tcpip\Parameters. OpenSubKey devolve null, GetValue no nulo estoura o NullReferenceException, a fábrica de licença devolve null, e a tela principal chama método em referência nula. O page fault no endereço zero está a cinco quadros de distância da causa.

A correção mora inteira dentro do prefixo. O GUID a usar é o que o próprio Wine expõe em NetworkInterface.Id — algo como {00000002-0000-0000-0000-4E6574446576} para a primeira placa:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Tcpip\Linkage]
"Bind"=hex(7):  ; REG_MULTI_SZ com "\Device\{GUID}"

[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{GUID}]
"IPAddress"=hex(7):      ; REG_MULTI_SZ com o IP da máquina
"SubnetMask"=hex(7):
"DefaultGateway"=hex(7):
"DhcpIPAddress"="192.168.0.10"
"EnableDHCP"=dword:00000001

Aplicando com wine regedit arquivo.reg, o erro de licença desaparece — e o aplicativo passa a carregar módulos de verdade (o consumo de memória do processo salta de 240 MB para 480 MB).

E quebra de novo, mais adiante.

Defeito 2 — o Wine não valida cbSize e estoura 432 bytes

A pilha seguinte, tirada do relatório local:

System.NullReferenceException - Object reference not set to an instance of an object.
|RM.Lib.WinForms.MessageBoxExForm | SystemParametersInfo(Int32 uiAction, Int32 uiParam,
                                                         NONCLIENTMETRICS& ncMetrics, Int32 fWinIni)|
|System.Windows.Forms.Form        | OnCreateControl()                                               |

O IL do chamador, e os metadados do P/Invoke, contam a história inteira:

  • uiAction é 0x29 = SPI_GETNONCLIENTMETRICS;
  • a struct NONCLIENTMETRICS declarada pela aplicação é truncada: só cbSize e lfCaptionFont, 68 bytes — ela só quer a fonte da barra de título;
  • o DllImport está com CharSet = Auto, que em NT resolve para SystemParametersInfoW;
  • o retorno é jogado em Font.FromLogFont(...), e logo depois há um Marshal.GetLastWin32Error() — ou seja, o código já espera que a chamada falhe.

E falha mesmo, no Windows. Escrevemos uma sonda que preenche um buffer de 8 KB com 0xAA, chama a API e mede até onde ela escreveu. O mesmo binário, nos dois lados:

cbSizeWindows 11Wine 11.0
68 (o que a aplicação passa)FALSE, 4 bytesTRUE, 500 bytes 🔴
500 (tamanho pré-Vista)TRUE, 500TRUE, 500
504 (tamanho completo)TRUE, 504TRUE, 504
200 (inválido, controle)FALSE, 4TRUE, 500 🔴

O Windows valida o cbSize e recusa a chamada, deixando a struct intacta. O Wine não valida: devolve sucesso e escreve os 500 bytes da NONCLIENTMETRICSW num buffer de 68 — 432 bytes além do fim. Como o marshaller do .NET aloca esse buffer na pilha do stub, o estouro esmaga o quadro de retorno. O efeito é aleatório e distante: às vezes a caixa de mensagem aparece, às vezes vem NullReferenceException, às vezes AccessViolationException, às vezes o page fault no zero.

E como MessageBoxExForm é a caixa de mensagem padrão do produto, qualquer mensagem que ele precise exibir corrompe a memória do processo.

No fonte do Wine (dlls/win32u/sysparams.c), o único teste era este:

case SPI_GETNONCLIENTMETRICS:
{
    NONCLIENTMETRICSW *nm = ptr;
    int padded_border;

    if (!ptr) return FALSE;
    /* ...preenche tudo, sempre... */

O patch

Três linhas, nos dois pontos que tratam o parâmetro (a variante normal e a ...ForDpi):

     if (!ptr) return FALSE;
+    if (nm->cbSize != sizeof(NONCLIENTMETRICSW) &&
+        nm->cbSize != FIELD_OFFSET(NONCLIENTMETRICSW, iPaddedBorderWidth))
+        return FALSE;

Compilar o Wine com isso é um configure --enable-archs=i386,x86_64 e um make. Numa máquina de 20 threads levou 8 minutos. Depois do patch, a sonda devolve, byte a byte, o mesmo que o Windows: cbSize=68FALSE, 4 bytes escritos.

🔑 A lição generaliza muito além deste caso. O padrão perigoso não é a API que falha — é a API que deveria falhar e “falha” com sucesso. Código que no Windows morre de forma inofensiva (return FALSE, struct zerada, resultado ignorado) vira estouro de buffer no Wine. Diante de AccessViolation ou page fault em zero num aplicativo .NET que não tem uma linha de código nativo, suspeite disso antes de suspeitar de tema, fonte ou biblioteca gráfica.

Defeito 3 — o atualizador não roda, e isso trava a própria instalação

Sintoma estranho: o instalador baixa 1,9 GB, a barra chega ao fim, a janela fecha… e a pasta de destino fica vazia. Duas vezes seguidas, idêntico.

O log do aplicativo, mais tarde, entregou o mecanismo:

INICIANDO PROGRAMA DE ATUALIZAÇÃO
ARGUMENTOS: APPPATH="C:\TOTVS\RM.NET" APPID="C:\TOTVS\RM.NET\RM.exe" TEMP="...\RMSUpdate"
TERMINANDO A EXECUÇÃO DA APLICAÇÃO

O produto não copia os próprios arquivos. Ele baixa tudo para uma área de preparo (%LOCALAPPDATA%\Temp\RMSUpdate), dispara um executável separado — o RM.Update.exe — e se encerra de propósito para que esse atualizador possa substituir arquivos em uso. O instalador usa exatamente o mesmo componente no passo final. Sob Wine o atualizador não completa, e por isso os dois falham no mesmo ponto, do mesmo jeito silencioso.

Contorno: fazer a cópia que ele faria.

cp -a "$PREFIX/drive_c/users/$USER/AppData/Local/Temp/RMSUpdate/." \
      "$PREFIX/drive_c/TOTVS/RM.NET/"
rm -rf "$PREFIX/drive_c/users/$USER/AppData/Local/Temp/RMSUpdate"

A área de preparo traz tudo, inclusive o RM.exe.config já apontando para o servidor correto. E limpar a pasta é obrigatório: se ela ficar, o aplicativo detecta atualização pendente e repete o ciclo — em versões anteriores, travando em anon_pipe_read.

A receita curta

  1. Wine com o patch de cbSize acima. Sem ele o resto não se sustenta.
  2. Prefixo em Windows 10, 64 bits. Se você usar winetricks, confira o winver depois: há verbos que rebaixam o prefixo para XP sem avisar, e o registro passa a dizer 5.2/3790.
  3. .NET Framework real (winetricks -q dotnet48). O wine-mono não serve — quebra com Assertion: should not be reached at marshal-ilgen.c antes de imprimir uma linha.
  4. Nada de componentes nativos além disso. gdiplus, riched20, msxml6, windowscodecs, vcrun* pioram; force tudo ao builtin com WINEDLLOVERRIDES="gdiplus=b;riched20=b;msxml6=b;windowscodecs=b;msvcp140=b;vcruntime140=b;msvcr120=b".
  5. Crie as chaves Tcpip\Linkage e Tcpip\Parameters\Interfaces\{GUID}.
  6. Instale e, quando o instalador sair sem copiar, aplique a área de preparo na mão.

Duas armadilhas de bancada que custam caro:

  • 🔴 Sondar a árvore de acessibilidade (UI Automation) mata o processo. Foi o que derrubou o instalador três vezes seguidas, e passamos um bom tempo acusando o instalador. O erro no log é err:ole:get_typeinfo_for_iid Failed to load typelib for {618736e0-3c3d-11cf-810c-00aa00389b71}IID_IAccessible.
  • 🔴 WINEDEBUG=-all apaga o log da própria aplicação. Duas rodadas foram gastas culpando o instalador por um log vazio que era obra nossa.

O que isto significa — e o que não significa

Significa que roda: instala, autentica contra o servidor e monta a tela principal completa, com ribbon, módulos e componentes DevExpress renderizados.

Não significa que virou plataforma. Três pontos, sem rodeios:

  • Não é homologado. ERP é sistema de registro; rodar fora do ambiente suportado pelo fabricante é bancada, não produção.
  • A automação é viável, mas mais lenta. Aqui nós erramos primeiro e vale contar: concluímos que o UI Automation “derrubava o processo” sob Wine. Não derrubava. Era a corrupção de memória do defeito 2 matando o processo no evento seguinte — e a sonda, que estava por perto, levou a culpa. Com o Wine corrigido, o UIA lê a árvore inteira, com os mesmos AutomationId do Windows, BoundingRectangle real e valor de célula. O que continua cego: o AT-SPI (a pilha de acessibilidade do Linux não enxerga aplicativo Wine), o MSAA (TYPE_E_CANTLOADLIBRARY) e, por algum motivo que não investigamos, a faixa do ribbon. A desvantagem real é o custo: sem um servidor residente, cada leitura sobe um processo novo e leva ~1 min, contra segundos no Windows. 🔑 A lição vale além do Wine: antes de dizer “a ferramenta X derruba o app”, pergunte se o app já não estava corrompido antes de X entrar.
  • O ganho real quase nunca está na tela. Boa parte do que se faz na interface de um ERP também sai por API (SOAP/REST). Migrar o cliente de plataforma resolve um problema que, na maioria das vezes, não precisava ser resolvido pela tela.

O valor do exercício é outro, e é o que fica: um sintoma pode mentir por cinco quadros de pilha. Trocar componentes por palpite não converge. Ler o que o programa realmente chama, medir dos dois lados e só então corrigir — isso converge, e ainda deixa um patch que serve para qualquer aplicativo que caia no mesmo buraco.