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

Feb 22, 2019

Aprendendo Mono.Cecil de uma maneira mais fácil

Read this post in english

A muito tempo atrás eu trabalhava com uma biblioteca para manipulação de bytecode Java chamada ASM. Um dos grandes diferenciais da mesma era sua excelente documentação bem como um aplicativo (asmifier) que, dado um trecho de código Java gerava um um programa com chamadas a API da biblioteca ASM que produzem bytecode equivalente ao do trecho de código usado.

Algum tempo depois (por volta de 2010 ~ 2011) o foco da minha carreira mudou para dotnet e eu comecei a usar uma biblioteca para manipulação de assemblies .NET (equivalente ao ASM) chamada Mono.Cecil e não demorou muito para eu entender que uma ferramente similar ao asmifier para o mundo .Net seria de grande valor.

Dada a complexidade inerente do problema (manipulação de MS IL), a documentação disponível precária do Mono.Cecil e meu interesse em aprender uma tecnologia emergente (naquela época) chamada MS Roslyn e assim nasceu Cecilifier.

Por razões que não são importantes agora eu acabei por diminuir o ritmo do mesmo algumas vezes mas nunca o abandonei por completo; ao contrário, o projeto progrediu de uma forma muito, muito, muito lenta com algumas paradas durante este período (veja o diagrama abaixo de você estiver curioso):


A boa notícia é que ano passado eu retomei o mesmo, adicionando algumas funcionalidades, corrigindo bugs, reorganizando o código, etc. Com isso eu planejava esperar até que o código atingisse um nível maior de qualidade antes de torná-lo open source mas eu concluí que mesmo no estado atual ele pode ser bastante útil; dessa forma tornei o repositório git público e implementei/configurei um site web.

Mesmo entendendo que este site torne o projeto mais acessível e útil atualmente o mesmo esta hospedado no gcp consumindo créditos gratuitos oferecidos pelo Google. Infelizmente estes créditos se esgotarão rapidamente, assim sendo, para permitir que o site continue disponível eu lancei uma campanha no Patreon com o objetivo de cobrir os custos de hospedagem, obter um domínio, etc. Outra formade contribuir é enviando bitcoins para 34e55WVni9nMFCiugo8d1bfwu9pLNzPH6n :)

Caso o montante mensal arrecadado não seja suficiente para cobrir tais custos eu irei remover o site mas o desenvolvimento deverá continuará, dependendo apenas do interesse tanto da comunidade como o meu :)

Se você gostou do projeto e vê o site como uma parte importante considere tornar-se um patrão. É claro que toda e qualquer forma de contribuição é bem vinda. Sinta-se a vontade em reportar problemas, enviar Merge Requests, ajudar outros usuários, adicionar mais testes, etc.

Certamente o site é uma das partes que eu dediquei menor esforço (mesmo porque este foi o recurso adicionado mais recentemente). Assim sendo esta é uma parte do projeto que se beneficiaria muito de desenvolvedores web com mais experiência.
Se desejar você pode ler mais detalhes a respeito do projeto (incluindo lista de  funcionalidades, limitações entre outras) no README incluso no repositório.

Finalizando envie-me uma mensagem no twitter contando se você esta utilizando o projeto, o que você acha, sugestões, comentários, etc (é sempre bom saber que o meu esforço esta sendo útil).

Espero que o mesmo seja útil.

Divirtam-se.

Adriano

Making it easier to getting started with Mono.Cecil

Leia este post em Português

A long time ago I was working with byte code manipulation in the black side of the force, i.e, in Java ;) using a library called ASM. In order to learn how to use this library (and also help raising developer productivity) one could use a tool called asmifier (and the related Bytecode Outline Eclipse plugin) which is able to take Java code snippets and generate calls to the ASM library to produce the equivalent byte code of the compiled code snippet.

Later, when I focused on dotnet development and started using Mono.Cecil to implement various tools to manipulate .Net assemblies (around 2010 ~ 2011) it did not took me too long to realize that something similar to asmifier in the .Net world could be of great value; add to that the implicit complexity of the problem domain, the sparse documentation available for Mono.Cecil and my interest in learning how to use a cool, emerging technology (at that time) called MS Roslyn and Cecilifier was born.

Due to reasons that are not important I've put the project aside a couple of times but never gave up or fully abandoned it.. instead it progressed very, very, very slowly with some stall periods in its development (check timeline bellow if you are curious):


The good news is that last year I resumed the work, added some missing features and fixed some bugs. I was planning to wait the code to reach some level of maturity before open sourcing it but I came to the conclusion that even in its current state it may be useful so I've finally opened its git repo and deployed a site.

As of today this web site is hosted in gcp, running of free credits; in order to keep it running (not necessarily in gcp) I've started a campaign in Patreon; in case this endeavor proves unsuccessful (i.e, it does not rise enough money) I'll shutdown the gcp server but development of the project is only tied to interest of both the community and my own. So if you think the web site is useful, please, consider donating. Another way you you can contribute is by sending bitcoins to 34e55WVni9nMFCiugo8d1bfwu9pLNzPH6n :)

Depending on the amount of money I raise monthly, I'll consider getting a custom domain; the excess will be used to cover hardware & beer costs as well as an incentive to put more effort in the development of the project.

[EDIT]

I removed the patreon option (didn't get any :) but you can still support me if you want to, through github sponsor

[/EDIT]


Of course you can also contribute by reporting issues, sending merge requests, engaging in discussions, helping users, etc.

The website is the part of the project to which I have dedicate the least amount of time/energy (it was one the last additions also) so it certainly could use some love :). If you are a web developer and would like to help, drop me a line.

If you use it, please drop me a message in twitter and tell me what you think, what improvements you would like to see, etc.

Finally, you can find more details (including features, limitations and more) in the README file in the git repo.

Have fun.

Adriano

Feb 6, 2019

Éviter les copies de données cachées en C#

Read this post in English

Leia este post em Português

Je voulais remercier tout ceux qui m'ont aidé avec la correction de ce texte (sans ordre particulier) : Charles Beauchemin, Vladimir Nachbaur.


Est ce que vous voyez un problème dans l’extrait de code ci-dessous ? Indice : Il y en a au moins un, lié à l’utilisation de Value Types & la performance.


D’accord, rendons le problème un peu plus évident en introduisant une erreur, laquelle, en supposant que le code en question a des tests unitaires, fera probablement remarquer le comportement immédiatement:


Quel serait le résultat maintenant ?
Si vous avez répondu :
M() : 0
M() : 1
M() : 2

Vous serez probablement surpris de voir :

M() : 0
M() : 0
M() : 0

C'est maintenant évident que quelque chose n’est pas correct.

Le problème est que le champ s a eté déclaré readonly, ce qui fait que le compilateur publie des copies défensives avant chaque appel de méthode sur ce type (dans notre cas s.M()); ça signifie que la méthode M() va ajouter le champ counter en une copie de la structure originale (au lieu de s), ce qui explique le comportement observé.

Notez que dans la première version du code (dans lequel la structure n’est pas modifiable), il n'est pas facile de détecter que quelque chose n'est pas correcte. Même dans la dernière version du code ce n'est pas clair pourquoi on n’observe jamais l’incrément de s.counter.

Pour comprendre ce qui se passe, examinons le code IL généré pour la fonction Main() (vous pouvez utiliser le site sharplab.io pour expérimenter avec le code) :


Maintenant, c’est un peu plus facile de remarquer les copies supplémentaires qui se produisent dans les offsets IL_0001 & IL_0006, puis IL_000f & IL_0014, et enfin IL_001d & IL_0022. Notez qu’après la copie du contenu de s dans la variable située dans l’emplacement 1, cette adresse est chargée sur la pile et le méthode M() est appelée.

Malheureusement, avec C#, c’est relativement facile de tomber dans d’autres “pièges” ; le prochain exemple montre deux autres cas : les paramètres in et les variables locales ref readonly:

La question naturelle qui vient est : Est-ce que c’est possible de s’éviter cette copies ? Si oui, comment ? La réponse dépend de la version du langage C# que vous utilisez :
  • < 7.3: Le seul moyen que je connaisse est de supprimer le modificateur readonly du champ (s dans notre exemple)
  • >= 7.3: Vous pouvez déclarer sa struct comme readonly afin de s’assurer que la même n’est pas modifiable donc le compilateur n'a pas à émettre de copies défensives (au cas où vous essaierait de modifier l’état de la structure, le compilateur émettra une erreur).
Il reste une question : à quel point ces copies sont-elles mauvaises? À mon avis la réponse dépend de comment elles affectent ton programme : la performance (i) et/ou comportement (ii) et aussi leurs exigences de performance.

Dans le cas de comportements incorrects, je crois que le consensus est que le développeur doit trouver et corriger des copies ; déjà dans les cas où ces copies n’affectent pas la performance, je dirais que le développeur doit analyser les avantages/inconvénients avant d'investir le temps pour se retrouver/corriger ces copies. Cela étant dit, si vous utilisez Visual Studio vous pouvez installer le plugin ErrorProne.NET que facilitera beaucoup la localisationde ces problèmes (Jetbrains Rider a un plugin intégré pour faire la même chose).

Finalement, si vous êtes intéressé à mieux comprendre la plate-forme.Net , je vous recommande de suivre ce blog

Amusez-vous,

Adriano

Evitando cópias de dados em C#

Read this post in English

Lire ce post en Francais

Você nota algum problema no trecho de código abaixo? Dica: existe pelo menos um, relacionado ao uso de Value Types & performance.

Ok, vamos tornar o problema um pouco mais óbvio introduzindo um erro, o qual, assumindo-se que o código em questão possui testes unitários, provavelmente faria com que o comportamento fosse notado imediatamente. Qual seria o resultado do programa agora ?

Se você respondeu:
M() : 0
M() : 1
M() : 2

Você provavelmente ficará surpreso ao ver:
M() : 0
M() : 0
M() : 0

Agora ficou evidente que algo está errado.

O problema com este programa é que o campo s foi declarado como readonly o que faz com que o compilador emita cópias defensivas antes da chamada de qualquer método do tipo (no nosso caso s.M()); isso significa que o método M() incrementará o campo counter em uma cópia da estrutura original (ao invés de s) o que explica o comportamento observado.

Note que na primeira versão do programa (em que a estrutura em questão não é mutável) não é fácil detectar que algo pode não estar correto. Mesmo na última versão do programa não é claro o porquê nunca observamos o incremento de s.counter.

Para entender o que está ocorrendo vamos olhar o código IL gerado para o método Main() (você pode usar o sharplab.io para experimentar com o código):
Agora ficou mais fácil observar as cópias extras que ocorrem nos offsets IL_0001 & IL_0006 e depois em IL_000f & IL_0014 e finalmente em IL_001D & IL_0022. Note que após copiar o conteúdo de s para a variável local localizada no slot 1 o endereço da mesma é carregado na pilha e a seguir o método M() é executado.

Infelizmente em C# é relativamente fácil cair em outras “armadilhas”; O exemplo abaixo apresenta mais dois casos: parâmetros in e variáveis local ref readonly:

A pergunta natural que surge é: é possível evitar estas cópias? Como? A resposta depende da versão da linguagem que você está usando:
  • < 7.3: A única maneira que conheço é remover o modificador readonly do campo (s no nosso exemplo)
  • >= 7.3: Neste caso você pode declarar sua struct como readonly de forma a garantir que a mesma não é mutável; desta forma o compilador não precisará emitir cópias defensivas (caso o código tente modificar o estado da estrutura o compilador emitirá um erro).
Resta uma questão: quão ruim estas cópias extras são? Na minha opinião a resposta depende de como as mesmas afetam seu programa; performance (i) e/ou comportamento (ii) e também dos seus requisitos quanto a performance.

No caso de comportamento incorreto acredito ser consenso que o desenvolvedor deve encontrar/corrigir estas cópias; já em cenários em que tais cópias afetam apenas a performance eu diria que o desenvolvedor deve analisar os prós/contras antes de investir tempo para encontrar/remover tais cópias (para um número de aplicativos o impacto em performance causado por estas cópias extras é desprezível). Dito isso, se você desenvolve usando o Visual Studio você pode instalar o ErrorProne.NET que irá facilitar em muito a localização de problemas deste tipo.

Finalmente, se você se interessa em entender a plataforma .Net mais a fundo recomendo acompanhar este blog

Have fun!

Adriano