Mar 14, 2020

Cuidado com o vão: lidando com problemas de offsets no Mono.Cecil

Read this post in English
Lire cet article en français


Cuidado com o vão

Se há algo que aprendi em minha carreira em IT é que mais cedo ou mais tarde (normalmente mais cedo) um conhecimento mais profundo das tecnologias abstraídas por uma determinada biblioteca/framework/produto (no caso deste post CIL/Mono.Cecil respectivamente) é vital para garantir uma utilização efetiva da mesma.

Semana passada, enquanto eu trabalhava em uma funcionalidade do Cecilifier alguns testes começaram a reportar assemblies inválidos sem nenhuma modificação no código do Cecilfier (pelo menos nenhuma alteração que pudesse explicar os erros). Após investigar eu finalmente isolei o problema: mudanças nas classes de teste fizeram com que algumas instruções de desvio (branch) emitidas pelo Cecilifier ficassem com um deslocamento maior que 127 bytes.

Il possui dois grupos de instruções de desvio: versões curtas (i), que utilizam um byte para armazenar o deslocamento (possibilitando um deslocamento na faixa de -128 à 127) e versões longas (ii) que utilizam 4 bytes (o que teoricamente possibilita um deslocamento de  -2.147.483.648 a 2.147.483.647) e o Cecilifier gerava instruções do primeiro grupo independente do deslocamento usado.

O programa abaixo demonstra este cenário ao adicionar instruções nop no bloco if fazendo com que o deslocamento ultrapasse 127 bytes:
Quando executado o mesmo salva uma versão modificada (em uma pasta temporária, ou seja, %temp% no Windows e /tmp no Linux), a qual gera uma exceção quando executada (mais precisamente quando o método Foo()for jited assim que a runtime tentar executar o mesmo).
Para testar, compile o programa acima e:
  1. Execute o mesmo passando `modify 1000` na linha de comando (você pode experimentar diferentes valores):
    `Test.exe modify 1000`
  2. Agora execute a versão modificada passando `run` na linha de comando (on windows):
    `%temp%\Output.exe run`
Felizmente, Mono.Cecil oferece uma forma relativamente simples que podemos utilizar para garantir que métodos utilizarão instruções de desvio compatíveis com o deslocamento necessário. Para tanto basta utilizar o método SimplifyMacros() (definido em Mono.Cecil.Rocks.MethodBodyRocks) o qual visita todas as instruções do método passado como parâmetro substituindo instruções de deslocamento pelas versões longas respectivas, ou seja, instruções como br.s, beq.s, etc. são substituídas por suas equivalentes longas br, beq, etc (note que este método também substitui instruções tipo ldarg.x por ldarg x) garantindo assim que todos os deslocamentos são válidos.

Após finalizar as modificações no método podemos executar OptimizeMacros() para garantir que as versões mais otimizadas (do ponto de vista do número de bytes utilizados) para cada instrução será utilizado:

... garantindo que o assembly produzido será válido!



Finalisando, lembre-se que visando produzir assemblies tão compactos quanto possível, compiladores selecionam instruções de deslocamento de acordo com o deslocamento necessário; isso quer dizer que na maioria dos casos versões curtas serão utilizadas e ao modificar assemblies (inserindo instruções através do Mono.Cecil por exemplo) é possível que o deslocamento em uma ou mais destas instruções se torne inválido.

Have fun!

#monocecil #cecilifier

No comments: