Så fik jeg tid til at skrive lidt om Mono.Cecil igen. Sidst skrev jeg hvordan man lavede en reference til en anden assembly, og skrev den “modificerede” assembly, med den nye reference, ned på disken. Dog er der ikke meget ved kun at lave referencer til andre assemblies, dette bliver lidt kedelig i længden. Jeg vil i dette indlæg forklare hvordan man kan kalde metoderne i den nylige referencerede assembly.
For at kunne lave et sådanne hack generelt, hvor injecter metode kald ind i andre assemblies, kræves der at man har lidt styr på de mere lavtstående dele af C#, nemlig IL kode. Dog er det minimalt hvor meget der bliver brugt i dette indlæg, så det er lige til overkomme, men vil man lave noget mere avanceret så vil jeg anbefale at man nærstudere MSIL lidt mere. Hvis du vil læse mere om MSIL opkoderne som bliver brugt længere nede, før du læser videre, så kan du tage et kig på
- call
- ldstr
Disse to opkoder bliver injectet via Mono.Cecils ILProcessor. Jeg har ikke kunne finde noget dokumentation på hvordan man bruger ILProcessoren, da Mono.Cecils dokumentation er skrevet til en ældre version end den seneste version af Mono.Cecil. Så den eneste dokumentation er her. Hvilket vil sige er lidt af en by i Rusland, hvis man gerne vil have lidt inspiration til hvad det kan bruges til
. Dog kan jeg gå så langt og sige at man nemt kan få skabt sig et overblik via førnævnte FAQ, og lidt tid i tænkeboksen.
Jeg vil bygge lidt videre på min sidste artikel, hvor jeg har udvidet koden lidt,
Mono.Cecil.TypeDefinition type = lib.MainModule.Types.Where(x => x.Name == "Writer").Single();
Mono.Cecil.MethodDefinition method = type.Methods.Where(x => x.Name == "WriteLine").Single();
Mono.Cecil.MethodReference reference = source.MainModule.Import(method);
Mono.Cecil.TypeDefinition globalType = source.MainModule.Types.Where(x => x.Name == "Global").Single();
Mono.Cecil.MethodDefinition applicaionErrorMethod = globalType.Methods.Where(x => x.Name == "Application_Error").Single();
Mono.Cecil.Cil.ILProcessor processor = applicaionErrorMethod.Body.GetILProcessor();
Mono.Cecil.Cil.Instruction call = processor.Create(Mono.Cecil.Cil.OpCodes.Call, reference);
Mono.Cecil.Cil.Instruction str = processor.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"h");
processor.InsertBefore(applicaionErrorMethod.Body.Instructions[0], str);
processor.InsertAfter(applicaionErrorMethod.Body.Instructions[0], call);
Koden er faktisk lige til, dog har jeg brugt lang tid på at skrive den, da jeg ikke synes at der er meget dokumentation omkring på nettet, som fortæller hvordan man injecter et funktionskald. De fleste af de små snippets/spørgsmål/svar som flyder omkring på nettet, beskriver hvordan man gør dette med de ældre versioner af Mono.Cecil, som gør brug af en CilWorker til at injecte med. Denne metode er blevet byttet ud med førnævnte IlProcessor. Dette er faktisk den største forskel. Dog ligger der ikke et samlet eksempel på nettet på hvordan man laver et inject til en ekstern defineret metode. Jeg vil faktisk våge og påstå at min er det eneste fulde eksempel!! Hvilket jeg synes er lidt ærgeligt, da det er lidt sjovt at sidde og nørkle med.
Det der tog længst tid for mig at regne ud, var hvordan man brugte den injectede assembly reference. For man skulle ved første øjekast tro at man “bare” importerede den fundne metode og efterfølgende brugte den fundne metode i selve ILProcessoren:
...
Mono.Cecil.MethodDefinition method = type.Methods.Where(x => x.Name == "WriteLine").Single();
source.MainModule.Import(method);
...
Mono.Cecil.Cil.ILProcessor processor = applicaionErrorMethod.Body.GetILProcessor();
Mono.Cecil.Cil.Instruction call = processor.Create(Mono.Cecil.Cil.OpCodes.Call, method);
Mono.Cecil.Cil.Instruction str = processor.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"h");
processor.InsertBefore(applicaionErrorMethod.Body.Instructions[0], str);
processor.InsertAfter(applicaionErrorMethod.Body.Instructions[0], call);
Dog skal man bruge den returnerede “Mono.Cecil.MethodReference reference = …” videre i forløbet, ellers ved Mono.Cecil ikke hvad for assembly metoden referer til. Det jeg gjorde i første artikel, var altså bare en side af injectningen, hvor man fandt og tilknyttede referencen til den assembly som man vil modificere. Dog skal man bruge den returnerede reference, for at kompileren ikke brokker sig. Dejligt med compiletime tjek!
Det der sker i koden er at man først finder den metode som man vil injecte
Mono.Cecil.TypeDefinition type = lib.MainModule.Types.Where(x => x.Name == "Writer").Single();
Mono.Cecil.MethodDefinition method = type.Methods.Where(x => x.Name == "WriteLine").Single();
Mono.Cecil.MethodReference reference = source.MainModule.Import(method);
Her finder jeg altså metoden “WriteLine” i den statiske klasse “Writer”. Denne MetodeDefinition bruger jeg til at Importere hele den tilhørende assembly, og den returnerede MethodReference, gemmer jeg. Denne reference skal jeg bruge når jeg rent faktisk skal injecten.
Injecten har jeg besluttet mig for at lave i web applikationens Global.Application_Error metode. At finde denne metode er simpel
Mono.Cecil.TypeDefinition globalType = source.MainModule.Types.Where(x => x.Name == "Global").Single();
Mono.Cecil.MethodDefinition applicaionErrorMethod = globalType.Methods.Where(x => x.Name == "Application_Error").Single();
og dejlig letlæselig også takket være Linq.
Jeg har nu
- importeret den assembly som indeholder den metode som jeg vil kalde
- fundet metoden som injecten skal ske i
Nu kan jeg begynde at injecte:
Mono.Cecil.Cil.ILProcessor processor = applicaionErrorMethod.Body.GetILProcessor();
Mono.Cecil.Cil.Instruction call = processor.Create(Mono.Cecil.Cil.OpCodes.Call, reference);
Mono.Cecil.Cil.Instruction str = processor.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"h");
processor.InsertBefore(applicaionErrorMethod.Body.Instructions[0], str);
processor.InsertAfter(applicaionErrorMethod.Body.Instructions[0], call);
For at injecte fx kald i eksisterende kode, skal man først have fat i den kontekst som man vil injecte i. Vi vil gerne injecte noget kode Application_Error metoden, derfor skal vi have fat i kroppen af denne metode. Kroppen har Mono.Cecil pænt pakket ind til os, og de har endda gjort det nemt for os at injecte kode ind med ILProcessor’en. Jeg frygtede, før jeg gik igang, at man selv skulle til at flytte rundt på IL opkoder, og derved nemt ødelægge det eksisterende, men dette behøves man ikke med ILProcessoren. Den hjælper os nemlig. Både med at injecte, men også med at oprette MSIL instruktioner. Med Create metoden laver man instruktioner som skal injectes, og med InsertBefore, og InsertAfter, injecter man. Dette gør at koden er meget nemmere at læse. Man kan derfor fokusere mere på at skrive god og letlæseligt kode, og mindre på at forstå, og huske hvad MSIL kode der skal konstrueres.
Selve oprettelsen af det IL kode der skal injectes sker altså gennem ILProcessorens Create metode.
Mono.Cecil.Cil.Instruction call = processor.Create(Mono.Cecil.Cil.OpCodes.Call, reference);
Mono.Cecil.Cil.Instruction str = processor.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"h");
Læg mærke til at jeg bruger WriteLine metodens reference, og ikke selve dens MethodDefinition. Jeg opretter to instruktioner. En som sørger for at kalde metoden, og en som sørger for at lægge metodens paramteren på stakken. For at injecte kaldet indsætter jeg disse to instruktioner i Application_Error’ens krop. Jeg vil gerne indsætte kaldet først i metoden
processor.InsertBefore(applicaionErrorMethod.Body.Instructions[0], str);
processor.InsertAfter(applicaionErrorMethod.Body.Instructions[0], call);
Her smider jeg strengen, “h”, på stakken og laver et fald til min WriteLine metode.
Rækkefølgen af indsættelsen er ikke hel ligegyldig. Hvis du ikke er inde i hvordan metodekald sker i de “mere primitive” sprog, så skal man, før man kalder metoden, smide alle metodens parametre på stakken, og derefter kalde metoden. Call koden sørger derefter for at tage de argumenter som den skal bruge fra stakken og oprette en ny stak, som den bruger til at udføre sin handling. Efter endt kald rykkes dens kaldstak ned, og en eventuel returværdi smides på kalderens stak, som kalderen kan vælge at bruge i dens videre forløb.
Tilsidst skal vi have gemt den modificerede assembly
source.MainModule.Write("WebApplicationModified.dll");
Der er nu gemt en modificeret assembly med Writer.WriteLine(“h”) kaldet i Application_Error metoden. Du kan se dette ved at inspicere den gemte dll med ILSpy. For at sikre sig at man ikke ødelagt og lavet en korrupt dll, kan man køre PEVerify på den. PEVerify tjekker om alt er som det skal være i den nyoprettede assembly. Har man injectet noget galt og usammenhængede MSIL kode, skal PEVerify nok fortælle dig det. Dette kunne fx være at man havde glemt at smide en parameter på stakken før man kalder en given metode.
Det var alt for nu.