Mar 14, 2020

Mind the Gap: dealing with offset issues in Mono.Cecil


Lire cet article en français
Leia este post em Português


Mind the Gap

If there's one thing I've learned in my IT career is that, sooner or later (more often than not the former), a deeper knowledge about the various technologies abstracted by a library/framework will be required in order to use it efficiently and/or solve issues (in the context of this post Mono.Cecil abstracts various CIL aspects).

Last week while I was working on a Cecilifier feature, out of the blue I started getting invalid assemblies without changing any code in Cecilifier itself. After some head scratching I've figured it out: changes to test classes leading to some of the branches being more than 127 apart from each other.

IL (intermediate language) has two families of branch instructions: short i and long ii forms. In short (pun intended), instructions that take the short form uses one byte as the offset (ranging from -128 ~ 127) to the target of the branch whereas the ones based on the long form takes 4 bytes (a much wider range) and Cecilifier was emitting the former type of branches irrespective to the offset between the branch instruction and its target.

To make it more concrete the following program simulates this scenario by naively adding a bunch of nop instructions inside the if statement leading to the offset of the branch (to the end of the if) to overflow:
When executed it saves a modified version of itself (to your temp folder, i.e, %temp% on Windows / /tmp on Linux) which throws an exception as soon as the affected method is jited (in the example when we try to execute Foo() method).

You can build the program above and:
  1. Execute it passing `modify 1000` as its command line argument (you can try different values for the number of nops):
    `Test.exe modify 1000`
  2. After it finishes, run the modified version passing`run`  as the command line argument (on windows):
    `%temp%\Output.exe run`
Luckily, Mono.Cecil provides a relatively easy way to ensure that branch instructions compatible with the offsets will be used through SimplifyMacros() extension method (defined in Mono.Cecil.Rocks.MethodBodyRocks) which goes over a method's body instructions replacing the ones encoded using the short form of the opcodes with the respective long form ones; this means that branch instructions such as br.s, beq.s, etc. are replaced with their long form counterparts (br, beq, etc) in the same way that opcodes like ldarg.x are replaced with ldarg x and so on. Since those instructions uses offsets that are 4 bytes long it is very unlikely (almost impossible) that targets will be outside the valid range.

After finishing doing the modifications to a method body you can call OptimizeMacros() to ensure all instructions uses the most efficient (space wise) possible encoding by taking the long form of instructions and replacing them with the short form versions whenever possible.

Armed with these methods we can change our program to (note lines #51 & #58):
Now the assembly produced is no longer invalid!




As a last note keep in mind that in general in order to minimize assembly size, compilers emit instructions taking the least space possible (which will be the short form for offesets smaller than 128). That is nice and cool but this means that in the vast majority of the cases the short form of the branch instructions will be used and inserting a singe IL instruction in a method's body may cause short forms of branch instructions to fall out of valid offsets leading to exceptions at runtime.

Have fun.

#monocecil #cecilifier

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

Attention à l’écart: Faire face aux compensations dans Mono.Cecil

Read this post in English
Leia este post em Português

Attention à l’écart

S'il y a une chose que j'ai appris dans ma carrière informatique, c'est que tôt ou tard (le plus souvent la première), une connaissance plus approfondie des différentes technologies abstraites par les bibliothèques / frameworks sera nécessaire pour les utiliser efficacement et / ou résoudre des problèmes (dans le contexte de cet article, Mono.Cecil abstrait divers aspects CIL). 

La semaine dernière, alors que je travaillais sur une fonctionnalité en Cecilifier, j'ai commencé à obtenir des assemblys invalides sans aucune modification du code Cecilfier lui-même. Après quelques enquêtes j'ai compris le motif: en changeant le code de les classes de testes il y a des instructions de branchement que était plus de 128 bytes de son cibles.

Dans IL il y a deux formats des instructions de branchement : les branches courtes i et les branches longues ii. En bref, le format court utilise un octet (de -128 à 127) pour le décalage tandis que les autres utilisent 4 bytes (une plage beaucoup plus large) et Cecilifier était produit des instructions sur le format courtes indistinctement de le décalage.

Plus concrètement, le programme suivant ajoute naïvement une série d'instructions nop dans le bloc if menant au décalage de la branche (jusqu'à la fin du if) à déborder:

Lorsqu'il est exécuté, il enregistre une version modifiée d'elle-même, (dans votre dossier temp, c'est-à-dire %temp% sous Windows / /tmp sous Linux), laquelle lève une exception dès que la méthode affectée est jited (dans l'exemple, lorsque nous essayons d'exécuter la méthode Foo()).

Heureusement Mono.Cecil offre un moyen relativement simple de s'assurer que tous les branchements ont le décalage correct grâce à la méthode SimplifyMacros() (définie dans Mono.Cecil.Rocks.MethodBodyRocks) qui visite toutes les instructions de la méthode remplaçant celles qui utilisent le format court des opcodes avec le format long respectif; cela signifie que les instructions de branchement telles que br.s, beq.s, etc. sont remplacées par leurs équivalents longs br, beq, etc de le même manière que les opcodes ldarg.x sont remplacées par darg x. Puisque ces instructions utilisent des décalages de 4 octets, il est très improbable que les cibles se trouvent en dehors de la plage valide.

Après avoir fait des modifications dans la méthode, vous pouvez appeler OptimizeMacros() pour s'assurer que toutes les instructions utilisent le format le plus efficiente possible en remplaçant des instructions qui utilisent le format long par les équivalents en format court quand c’est possible.

En connaissant ces méthodes nous pouvons changer nos programmes comme suit :

Maintenant l'assembly produit n'est plus invalide !




Finalement, rappelle toi que dans  le but de produire les petites assemblys ,en général, les compilateurs émettent des instructions les plus petites possible (ou, les instructions en format court). C’est bien mais ça signifie aussi qu’en changeant une assembly (avec Mono.Cecil) c'est possible qu’une instruction de branchement devienne invalide quand on ajoute quelques instructions.


Amuse-toi.

#monocecil #cecilifier