Laden...

Alle Aufrufe einer Methode finden, Reflection?

Letzter Beitrag vor 13 Jahren 23 Posts 6.248 Views
Alle Aufrufe einer Methode finden, Reflection?

Folgendes Problem,

ich habe mehrere Assemblies die eine Methode einer Klasse eines weiteren Assemblies callen. Nun ist es so, das mich die Callee's von allen Klassen interessieren die ein bestimmtes Interface implementieren. Alles Klassen heraus zu finden, die dieses spezielle Interface implementieren ist relativ einfach. Nun ist aber das Problem, wie finde ich heraus, wo in diesen Klassen überall die Eingangs erwähnte Methode des anderen Assembly gecalled wird, geht das mit Reflection überhaupt?

Again what learned...

Hallo rollerfreak2,

guck mal: Test Run: Determining .NET Assembly and Method References - da wird der Reflector bzw. dessen API benutzt um diese Info zu erhalten. Bei 0 beginnen kann für diese Aufgabe sehr aufwändig werden.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Hallo gfoidl,

perfekt ich denk damit bekomme ich es hin.

Again what learned...

Hab ein kleines problem, und zwar komm ich nicht an die Reflector API dll ran. Ich hab mir auch mal das zu dem Link gehörende TestRun Projekt runter geladen, aber auch dort ist nur ne Reference drin, die dll an sich nicht. Auch hab ich versucht den Reflector.exe als Referenz (5.0.0.0) einzubinden, alledings gibt es dort das Interface IAssemblyLoader gar nicht. Auch die Klasse Application gibt es im Refelctor.exe nicht. Was mache ich falsch?

[EDIT] Man sollte vorher ab und zu mal die Readme lesen. Aus IAssemblyLoader ist IAssemblyManager geworden bzw. andersherum, und Application mur ApplicationManager heißen.

Again what learned...

Hallo rollerfreak2,

guck mal: c# reflection and find all references - da gehts um was Ähnliches und vllt. findest du dort passende Anregungen.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Hi gfoidl,

bin jetzt mal soweit gekommen (mit der Reflector API) das ich alle benötigten Assemblies in den AssemblyManager geladen habe. Dann bin ich durch die besagte "externe" Assembly durch und habe mir die IMethodDeclaration gesucht die ich verfolgen will. Nun dachte ich bietet die API eine Möglichkeit alle Referenzen dieser Methode suchen zu lassen, aber ich finde leider nix. Quasi wie wenn man im Relfector auf Analyze Method klickt, und dann UsedBy. Das ist doch sicher als Funktionalität gekappselt. Jedoch finde ich auch keine Doku von der API sodass ich in der Luft hänge.

Klar ich könnte jetzt jede IInstruction jedes anderen Assembly durch fursten und schauen ob ich da einen Call finde, aber genau das müsste es ja schon so ählich geben in der API.

Again what learned...

Hallo rollerfreak2,

das Used By im Reflector hab ich auch schon gesehen. Wie das genau funktioniert weiß ich nicht, aber du könntest mit dem Reflector den Reflector betrachten 😃 - das geht, ist aber auch aufwändig.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Hi gfoidl,

das mache ich die ganze Zeit schon, leider ist das Teil obfuscated. Das macht die ganze Sache nahezu unmöglich.

Fehlermeldung:
This item is obfuscated and can not be translated.

[Edit] Mit dem detPeek von JetBrains gehts, da sieht man bisschen mehr als im Reflector. Mal schauen ob ich damit was in Erfahrung bringen kann.

Again what learned...

Hallo rollerfreak2,

ich hab mir den Reflector mal angeschaut und hat geklappt, allerdings war das noch eine alte Version. Vllt. findest du ja noch ein 5er-Version od. älter. Sonst schau dich bei den Alternativen zum Reflector um, wenn diese auch ein Used By haben gehts mit denen auch.

Sonst wirklich, wie im obigen Link auch gezeigt wird, über die MethodInstructions iterieren.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Welche Aufrufe von einer bestimmten Methode ausgeführt werden, kannst du mit folgender Erweiterungsmethode ermitteln (entstanden aus [gelöst] Zielmethode eines Call-Befehls auf CIL-Ebene ermitteln und "ausgebaut" aus dem ILWeaver vom Fast and Simple Bitmap Filter von zommi):


using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

public static class MethodBaseExtensions
{
	private static readonly OpCode[] oneByteOpCodes = new OpCode[0x100];
	private static readonly OpCode[] twoByteOpCodes = new OpCode[0x100];

	static MethodBaseExtensions()
	{
		foreach (FieldInfo fieldInfo in typeof(OpCodes).GetFields(BindingFlags.Public|BindingFlags.Static))
		{
			OpCode opCode = (OpCode)fieldInfo.GetValue(null);
			UInt16 value = (UInt16)opCode.Value;
			if (value < 0x100)
				oneByteOpCodes[value] = opCode;
			else if ((value & 0xff00) == 0xfe00)
				twoByteOpCodes[value & 0xff] = opCode;
		}
	}


	public static IEnumerable<MethodBase> GetCalls(this MethodBase methodInfo)
	{
		MethodBody methodBody = methodInfo.GetMethodBody();
		if (methodBody!=null)
		{
			byte[] ilBytes = methodBody.GetILAsByteArray();
			for (int position=0;position<ilBytes.Length;position++)
			{
				OpCode opCode = (ilBytes[position]!=0xFE) ? oneByteOpCodes[ilBytes[position]] : twoByteOpCodes[ilBytes[++position]];

				if (opCode==OpCodes.Call || opCode==OpCodes.Callvirt)
				{
					Type[] genericTypeArguments = methodInfo.DeclaringType.GetGenericArguments();
					Type[] genericMethodArguments = methodInfo.GetGenericArguments();
					yield return methodInfo.Module.ResolveMethod(BitConverter.ToInt32(ilBytes,position+1), genericTypeArguments, genericMethodArguments);
				}

				switch (opCode.OperandType)
				{
					case OperandType.InlineNone:
						break;
					case OperandType.ShortInlineBrTarget:
						position += sizeof(SByte);
						break;
					case OperandType.ShortInlineI:
					case OperandType.ShortInlineVar:
						position += sizeof(Byte);
						break;
					case OperandType.InlineI8:
						position += sizeof(Int64);
						break;
					case OperandType.ShortInlineR:
						position += sizeof(Single);
						break;
					case OperandType.InlineR:
						position += sizeof(Double);
						break;
					case OperandType.InlineVar:
						position += sizeof(UInt16);
						break;
					case OperandType.InlineBrTarget:
					case OperandType.InlineField:
					case OperandType.InlineI:
					case OperandType.InlineMethod:
					case OperandType.InlineSig:
					case OperandType.InlineString:
					case OperandType.InlineType:
						position += sizeof(Int32);
						break;
					case OperandType.InlineSwitch:
						position += sizeof(Int32) + sizeof(Int32)*BitConverter.ToInt32(ilBytes,position);
						break;
					case OperandType.InlineTok:
						position += sizeof(Int32);
						break;
					default:
						throw new NotSupportedException("Unerwarteter Operandentyp "+opCode.OperandType);
				}
			}
		}
	}
}

Mittels Reflection kannst du dir alle MethodInfos aller Typen einer Assembly ermitteln und die prüfen, ob sie eine bestimmte Methode aufrufen.
Hier mal ein Beispiel dazu:


MethodInfo mainMethodInfo = typeof(Program).GetMethod("Main");

var calledBy = Assembly.GetExecutingAssembly()
	.GetTypes()
	.SelectMany(type => type.GetMethods(BindingFlags.Public|BindingFlags.NonPublic | BindingFlags.Instance|BindingFlags.Static))
	.Where(method => method.GetCalls().Any(calledMethod => calledMethod==mainMethodInfo))
	.ToList();

Wenn es ordentlich funktioniert, können wir ja auch mal Snippet draus machen.

EDIT: So, generische Methoden/Methoden in generischen Typen sollten jetzt auch gehen. Und CallVirts auch.

Gruß,
dN!3L

Hi dN!3L,

ich wollt die Extension Method grad mal ausprobieren und hab jetzt folgende Exception bekommen.

Fehlermeldung:
Mixed mode assembly is built against version 'v2.0.50727' of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.

Dazu zu sagen ist das die zu analysierende Assembly mehrere andere Referenziert die alle mit .NET 2.0 gebaut sind. Die Exception tritt genau hier auf.


yield return methodInfo.Module.ResolveMethod(BitConverter.ToInt32(ilBytes, position + 1));

Ich hab auch mal versucht vorher alle Abhängikeiten zu laden, daher alle Assemblies die die zu analysierende Assembly referenziert. Selber Fehler.
Dann bin ich auf Mixed mode assembly exception gestoßen. Hab dann einfach in die app.config den besagten eintrag gemacht. Nun kommt jedoch folgende Exception:

Fehlermeldung:
A BadImageFormatException has been thrown while parsing the signature. This is likely due to lack of a generic context. Ensure genericTypeArguments and genericMethodArguments are provided and contain enough context.

Inner Exception:> Fehlermeldung:

An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)"

Passieren tut es wieder bei der besagten stelle und zwar wenn er die Methode IDispose auflösen will. Dann hab ich mal versucht die Überladung mit den 3 Parametern von ResolveMethod zu nehmen dabei bekomme ich zwar keine Exception, aber es wird keine Reference gefunden, da sind aber zu 100% welche drin, mit dem Reflector auch schon verifiziert.

yield return methodInfo.Module.ResolveMethod(BitConverter.ToInt32(ilBytes, position + 1), methodInfo.DeclaringType.GetGenericArguments(), methodInfo.GetGenericArguments());

Again what learned...

Das mit der BadImageFormat-Exception habe ich oben gerade angepasst. (Ich hatte ja geschrieben, dass Generics noch nicht ganz funktioniert hatte. Gelöst genau so, wie du geschrieben hast.

[...][W]enn er die Methode IDispose auflösen will [...] wird keine Reference gefunden, da sind aber zu 100% welche drin, mit dem Reflector auch schon verifiziert.

EDIT: Ui, spät gebundene Methoden wurden garnicht mit zurückgeliefert (Callvirt). Ich habe den Code oben entsprechend angepasst.

Ich wollte mal spaßeshalber alle Aufrufe einer Interface-Methode suchen. Meine Fresse, ist das kompliziert, alle Methoden zu finden, die dieses Interface/die betroffene Methode implementieren...

Hier mal mein Ergebnis: 8o


MethodInfo interfaceMethodInfo = typeof(IDisposable).GetMethod("Dispose");

var implementingMethods = Assembly.GetExecutingAssembly()
	.GetTypes()
	.Where(type => type.GetInterface(interfaceMethodInfo.DeclaringType.ToString())!=null)
	.Select(type => type.GetInterfaceMap(interfaceMethodInfo.DeclaringType))
	.SelectMany(interfaceMapping => interfaceMapping.InterfaceMethods.Select((interfaceMethod,index) => new { InterfaceMethod = interfaceMethod,TargetMethod = interfaceMapping.TargetMethods[index] }))
	.Where(methodMapping => methodMapping.InterfaceMethod==interfaceMethodInfo)
	.Select(methodMapping => methodMapping.TargetMethod)
	.Cast<MethodBase>()
	.ToList();

var calledBy = Assembly.GetExecutingAssembly()
	.GetTypes()
	.SelectMany(type => type.GetMethods(BindingFlags.Public|BindingFlags.NonPublic | BindingFlags.Instance|BindingFlags.Static))
	.Where(method => method.GetCalls().Any(calledMethod => calledMethod==interfaceMethodInfo || implementingMethods.Contains(calledMethod)))
	.ToList();

Hi dN!3L,

sieht ziemlich kompliziert aus. 😄
Habs jetzt auch mal mit der Reflector API geschafft alle callee's einer Methode zu finden. Morgen auf Arbeit probier ich die Extension Method noch mal aus.

Ein Problem hab ich jedoch noch. Und zwar ist die Methode die ich suche (alle Aufrufe davon) eine Methode eines Interfaces. Diese hat 2 Parameter. Der erste ist vom Type String und wird in den entsprechenden Assemblies immer mit Constanten übergeben.

Quasi so:


((IMyInterface)this).SpecificMethod("identifier", ....);
((IMyInterface)this).SpecificMethod("identifier1", ....);
...

Was ich jetzt noch wissen müsste ist bei dem Methoden-Call die Value's hinter dem ersten Parameter, diese stehen ja schon zur Compilezeit fest. Mit der Reflector API hab ich mich schon dusselig gesucht, aber nix gefunden wie ich daran komme.

Wie siehts mit reiner Reflection aus, gibt es da eine Möglichkeit daran zu kommen?

Again what learned...

Quasi so

Quasi so oder genau so? Also wird der Parameter direkt als String reingeschrieben oder gibt es noch eine Indirektion (z.B. Zugriff auf eine Variable, die den String enthält)?

Hm, man könnte es wie oben mit dem Durchlaufen des IL-Bytecode/den OpCodes versuchen. Allerdings hast du da erstmal das Problem, dass die Abfolge der OpCodes nichts mit dem eigentlichen Programmablauf zu tun hat/zu tun haben muss. Denn z.B. kann man ja ein und denselben Call-Befehl durch mehrere verschiedene Sprungbefehle erreichen. Da die IL-Ausführung ja stackbasiert ist, müsste man erstmal alle möglichen Pfade zum jeweiligen Call finden und gucken, was vorher für diesen Parameter auf den Stack gelegt wurde.
Wenn man mutig ist (und der String-Parameter direkt übergeben wird), könnte man auch pauschal das letzte Ldstr vor dem Call-Befehl beim Iterieren über die OpCodes zurückgeben. Es kann sein, dass das in diesem Fall funktioniert - muss aber nicht...

Gruß,
dN!3L

Quasi genau so 😄

Es gibt zwar kleine Ausnahmen, da werden die Strings aus einer Statischen Klasse ermittelt, aber generell ist das so wie beschrieben. Ich hab jetzt mal folgendes gemacht.


public static void Test1(string ident, bool flag)
{
    //nothing here
}

public static void Test2()
{
    Test1("affe", false);
}

Dann hab ich mir die MethodInfo (Test2) geholt den IL ByteCode ausgelesen. Leider bin ich nicht so bewandert wie die Parameter bzw. die Instructions dort abgelegt werden. Im vorliegenden Fall ist folgender ByteCode da für die Test2 Methode.


Array  OpCode   OperandType
_________________________
0:0     nop         InlineNode
1:114  ldstr        InlineString
2:21
3:0
4:0
5:112
6:22    ldc.i4.0    InlineNode
7:40    call          InlineMethod
8:2
9:0
10:0
11:6
12:0    nop         InlineNode
13:42  ret           InlineNode

Wo genau bekomme ich jetzt dort mein "affe" her?

Again what learned...

Ich hab jetzt mal nach einem ILReader gesucht und promt was gefunden. Read IL from MethodBase

Und siehe da ziemlich einfach und schon hat mal den string.


MethodInfo mainMethod = typeof(Program).GetMethod("Test2");
ILReader reader = new ILReader(mainMethod);
foreach (ILInstruction instruction in reader)
{
    if (instruction is InlineStringInstruction)
    {
        string param1 = ((InlineStringInstruction)instruction).String;
    }
}

und siehe da "affe" steh in param1. Ich denke mit dem ILReader ist es leichter als wenn man das selber macht.

Again what learned...

Leider bin ich nicht so bewandert wie die Parameter bzw. die Instructions dort abgelegt werden. Im vorliegenden Fall ist folgender ByteCode da für die Test2 Methode.

Wichtig sind folgende Zeilen (wenn dich der essentielle IL-Code interessiert, kompiliere im Release-Modus):


1:114  ldstr       // ersten Parameter auf den Stack ("affe", direkt als String)
6:22   ldc.i4.0    // zweiten Parameter auf den Stack (0 als Konstante, also false)
7:40   call        // Methode aufrufen
13:42  ret         // return          

Wo genau bekomme ich jetzt dort mein "affe" her?

EDIT: Da hast du ja schon selbst die Lösung gefunden. Trotzdem mein Text dazu 😃
Einfach analog zur Variante mit der Module.ResolveMethod-Methode (System.Reflection) die Module.ResolveString-Methode (System.Reflection) verwenden.
In etwa so (Das gibt den letzten String-Parameter zurück - also Achtung, wenn du mehrere String-Parameter haben solltest!):


public static IEnumerable<KeyValuePair<MethodBase,string>> GetCalls(this MethodBase methodInfo)
{
...
	byte[] ilBytes = ...
	string lastStringParam = null;
	for ...
	{
		OpCode opCode = ...

		if (opCode==OpCodes.Ldstr)
			lastStringParam = methodInfo.Module.ResolveString(BitConverter.ToInt32(ilBytes,position+1));

		if (opCode==OpCodes.Call || opCode==OpCodes.Callvirt)
		{
			int metadataToken = BitConverter.ToInt32(ilBytes,position+1);
			Type[] genericTypeArguments = methodInfo.DeclaringType.GetGenericArguments();
			Type[] genericMethodArguments = methodInfo.GetGenericArguments();
			yield return new KeyValuePair<MethodBase, string>(methodInfo.Module.ResolveMethod(metadataToken, genericTypeArguments, genericMethodArguments), lastStringParam);
		}
	...
	}
}

Es gibt zwar kleine Ausnahmen, da werden die Strings aus einer Statischen Klasse ermittel

Da dürftest du dann Module.ResolveField-Methode (System.Reflection) oder ähnliches verwenden müssen.

Gruß,
dN!3L

Hi dN!3L,

danke für die Erklärung. Ich hab jetzt auch mal die Variante mit der Statischen String Klasse probiert. Erstaunlicherweise wird die Konstante direkt in den IL "kompiliert" daher auch bei folgender Variante steht direkt "affe" als InlineString.


public static void Test3()
{
    Test1(Strings.Ident, false);
}

public static class Strings
{
   public const string Ident = "affe";
}

Grüße rollerfreak2!

Again what learned...

Erstaunlicherweise wird die Konstante direkt in den IL "kompiliert" daher auch bei folgender Variante steht direkt "affe" als InlineString.

Das liegt am const-Schlüsselwor. Dadurch wird der String eine Kompilierzeitkonstante, die dann direkt an der Verwendungsstelle eingesetzt wird.

Jo da hätte ich auch selber drauf kommen können 😦
Im Falle das const wird durch static erstetzt ist es wie du bereits erwähnt hast. Dann wird nicht die Konstante auf den Stack gepackt, sondern man hat dann dort eine InlineFieldInstruction und mit dem ILReader kann man die auch bequem auslesen. Damit sollten dann alle Unklarheiten beseitigt sein 😄

Danke für die Unterstützung!

Again what learned...

InlineFieldInstruction und mit dem ILReader kann man die auch bequem auslesen.

Du kannst dann ja auch mal etwas Code posten, wie du das mit dem ILReader machst. Denn mit einfach alle InlineStringInstructions oder InlineFieldInstructions auslesen ist es ja nicht getan 😃

Jo werde ich machen.

Again what learned...