Skip to content

Commit d24ea32

Browse files
authored
Port the .NET (Core) disassembler to ClrMd v2 (#2040)
* update and sync TraceEvent version * port the Disassembler from ClrMD 1.x to 2.x
1 parent 762b76c commit d24ea32

File tree

13 files changed

+462
-16
lines changed

13 files changed

+462
-16
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ tests/output/*
4848
artifacts/*
4949
BDN.Generated
5050
BenchmarkDotNet.Samples/Properties/launchSettings.json
51-
src/BenchmarkDotNet/Disassemblers/net461/*
51+
src/BenchmarkDotNet/Disassemblers/net462/*
5252
src/BenchmarkDotNet/Disassemblers/BenchmarkDotNet.Disassembler.*.nupkg
5353

5454
# Visual Studio 2015 cache/options directory

samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<ItemGroup>
1919
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
2020
<PackageReference Include="System.Drawing.Common" Version="4.5.1" />
21-
<PackageReference Include="System.Memory" Version="4.5.3" />
21+
<PackageReference Include="System.Memory" Version="4.5.5" />
2222
</ItemGroup>
2323
<ItemGroup>
2424
<ProjectReference Include="..\..\src\BenchmarkDotNet\BenchmarkDotNet.csproj" />

src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
<ProjectReference Include="..\BenchmarkDotNet\BenchmarkDotNet.csproj" />
1313
</ItemGroup>
1414
<ItemGroup>
15-
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.0.1" PrivateAssets="contentfiles;analyzers" />
15+
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.0.2" PrivateAssets="contentfiles;analyzers" />
1616
</ItemGroup>
1717
</Project>

src/BenchmarkDotNet.Disassembler.x64/ClrMdDisassembler.cs renamed to src/BenchmarkDotNet.Disassembler.x64/ClrMdV1Disassembler.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
namespace BenchmarkDotNet.Disassemblers
99
{
10-
internal static class ClrMdDisassembler
10+
// This Disassembler uses ClrMd v1x. Please keep it in sync with ClrMdV2Disassembler (if possible).
11+
internal static class ClrMdV1Disassembler
1112
{
1213
internal static DisassemblyResult AttachAndDisassemble(Settings settings)
1314
{

src/BenchmarkDotNet.Disassembler.x64/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static void Main(string[] args)
2424

2525
try
2626
{
27-
var methodsToExport = ClrMdDisassembler.AttachAndDisassemble(options);
27+
var methodsToExport = ClrMdV1Disassembler.AttachAndDisassemble(options);
2828

2929
SaveToFile(methodsToExport, options.ResultsPath);
3030
}

src/BenchmarkDotNet.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</PropertyGroup>
1616
<ItemGroup>
1717
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\DataContracts.cs" Link="DataContracts.cs" />
18-
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\ClrMdDisassembler.cs" Link="ClrMdDisassembler.cs" />
18+
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\ClrMdV1Disassembler.cs" Link="ClrMdV1Disassembler.cs" />
1919
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\SourceCodeProvider.cs" Link="SourceCodeProvider.cs" />
2020
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\Program.cs" Link="Program.cs" />
2121
</ItemGroup>

src/BenchmarkDotNet/BenchmarkDotNet.csproj

+2-5
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616
<ItemGroup>
1717
<PackageReference Include="CommandLineParser" Version="2.4.3" />
1818
<PackageReference Include="Iced" Version="1.17.0" />
19-
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="1.1.126102" />
19+
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.2.332302" />
2020
<PackageReference Include="Perfolizer" Version="0.2.1" />
2121
<PackageReference Include="System.Management" Version="6.0.0" />
2222
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
2323
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
2424
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
2525
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" />
26-
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.61701" />
27-
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.61" PrivateAssets="contentfiles;analyzers" />
26+
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.0.2" PrivateAssets="contentfiles;analyzers" />
2827
</ItemGroup>
2928
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
3029
<PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
@@ -47,7 +46,5 @@
4746
</ItemGroup>
4847
<ItemGroup>
4948
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\DataContracts.cs" Link="Disassemblers\DataContracts.cs" />
50-
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\SourceCodeProvider.cs" Link="Disassemblers\SourceCodeProvider.cs" />
51-
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\ClrMdDisassembler.cs" Link="Disassemblers\ClrMdDisassembler.cs" />
5249
</ItemGroup>
5350
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
using Iced.Intel;
2+
using Microsoft.Diagnostics.Runtime;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
7+
namespace BenchmarkDotNet.Disassemblers
8+
{
9+
// This Disassembler uses ClrMd v2x. Please keep it in sync with ClrMdV1Disassembler (if possible).
10+
internal static class ClrMdV2Disassembler
11+
{
12+
internal static DisassemblyResult AttachAndDisassemble(Settings settings)
13+
{
14+
using (var dataTarget = DataTarget.AttachToProcess(
15+
settings.ProcessId,
16+
suspend: false))
17+
{
18+
var runtime = dataTarget.ClrVersions.Single().CreateRuntime();
19+
20+
ConfigureSymbols(dataTarget);
21+
22+
var state = new State(runtime);
23+
24+
var typeWithBenchmark = state.Runtime.EnumerateModules().Select(module => module.GetTypeByName(settings.TypeName)).First(type => type != null);
25+
26+
state.Todo.Enqueue(
27+
new MethodInfo(
28+
// the Disassembler Entry Method is always parameterless, so check by name is enough
29+
typeWithBenchmark.Methods.Single(method => method.IsPublic && method.Name == settings.MethodName),
30+
0));
31+
32+
var disassembledMethods = Disassemble(settings, state);
33+
34+
// we don't want to export the disassembler entry point method which is just an artificial method added to get generic types working
35+
var filteredMethods = disassembledMethods.Length == 1
36+
? disassembledMethods // if there is only one method we want to return it (most probably benchmark got inlined)
37+
: disassembledMethods.Where(method => !method.Name.Contains(DisassemblerConstants.DisassemblerEntryMethodName)).ToArray();
38+
39+
return new DisassemblyResult
40+
{
41+
Methods = filteredMethods,
42+
SerializedAddressToNameMapping = state.AddressToNameMapping.Select(x => new DisassemblyResult.MutablePair { Key = x.Key, Value = x.Value }).ToArray(),
43+
PointerSize = (uint)IntPtr.Size
44+
};
45+
}
46+
}
47+
48+
private static void ConfigureSymbols(DataTarget dataTarget)
49+
{
50+
// code copied from https://github.com/Microsoft/clrmd/issues/34#issuecomment-161926535
51+
dataTarget.SetSymbolPath("http://msdl.microsoft.com/download/symbols");
52+
}
53+
54+
private static DisassembledMethod[] Disassemble(Settings settings, State state)
55+
{
56+
var result = new List<DisassembledMethod>();
57+
58+
while (state.Todo.Count != 0)
59+
{
60+
var methodInfo = state.Todo.Dequeue();
61+
62+
if (!state.HandledMethods.Add(methodInfo.Method)) // add it now to avoid StackOverflow for recursive methods
63+
continue; // already handled
64+
65+
if (settings.MaxDepth >= methodInfo.Depth)
66+
result.Add(DisassembleMethod(methodInfo, state, settings));
67+
}
68+
69+
return result.ToArray();
70+
}
71+
72+
private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings)
73+
{
74+
var method = methodInfo.Method;
75+
76+
if (method.ILOffsetMap.Length == 0 && (method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0))
77+
{
78+
if (method.IsPInvoke)
79+
return CreateEmpty(method, "PInvoke method");
80+
if (method.IL is null || method.IL.Length == 0)
81+
return CreateEmpty(method, "Extern method");
82+
if (method.CompilationType == MethodCompilationType.None)
83+
return CreateEmpty(method, "Method was not JITted yet.");
84+
85+
return CreateEmpty(method, $"No valid {nameof(method.ILOffsetMap)} and {nameof(method.HotColdInfo)}");
86+
}
87+
88+
var codes = new List<SourceCode>();
89+
if (settings.PrintSource && method.ILOffsetMap.Length > 0)
90+
{
91+
// we use HashSet to prevent from duplicates
92+
var uniqueSourceCodeLines = new HashSet<Sharp>(new SharpComparer());
93+
// for getting C# code we always use the original ILOffsetMap
94+
foreach (var map in method.ILOffsetMap.Where(map => map.StartAddress < map.EndAddress && map.ILOffset >= 0).OrderBy(map => map.StartAddress))
95+
foreach (var sharp in SourceCodeProvider.GetSource(method, map))
96+
uniqueSourceCodeLines.Add(sharp);
97+
98+
codes.AddRange(uniqueSourceCodeLines);
99+
}
100+
101+
// for getting ASM we try to use data from HotColdInfo if available (better for decoding)
102+
foreach (var map in GetCompleteNativeMap(method))
103+
codes.AddRange(Decode(map.StartAddress, (uint)(map.EndAddress - map.StartAddress), state, methodInfo.Depth, method));
104+
105+
Map[] maps = settings.PrintSource
106+
? codes.GroupBy(code => code.InstructionPointer).OrderBy(group => group.Key).Select(group => new Map() { SourceCodes = group.ToArray() }).ToArray()
107+
: new[] { new Map() { SourceCodes = codes.ToArray() } };
108+
109+
return new DisassembledMethod
110+
{
111+
Maps = maps,
112+
Name = method.Signature,
113+
NativeCode = method.NativeCode
114+
};
115+
}
116+
117+
private static IEnumerable<Asm> Decode(ulong startAddress, uint size, State state, int depth, ClrMethod currentMethod)
118+
{
119+
byte[] code = new byte[size];
120+
int bytesRead = state.Runtime.DataTarget.DataReader.Read(startAddress, code);
121+
if (bytesRead == 0 || bytesRead != size)
122+
yield break;
123+
124+
var reader = new ByteArrayCodeReader(code, 0, bytesRead);
125+
var decoder = Decoder.Create(state.Runtime.DataTarget.DataReader.PointerSize * 8, reader);
126+
decoder.IP = startAddress;
127+
128+
while (reader.CanReadByte)
129+
{
130+
decoder.Decode(out var instruction);
131+
132+
TryTranslateAddressToName(instruction, state, depth, currentMethod);
133+
134+
yield return new Asm
135+
{
136+
InstructionPointer = instruction.IP,
137+
Instruction = instruction
138+
};
139+
}
140+
}
141+
142+
private static void TryTranslateAddressToName(Instruction instruction, State state, int depth, ClrMethod currentMethod)
143+
{
144+
var runtime = state.Runtime;
145+
146+
if (!TryGetReferencedAddress(instruction, (uint)runtime.DataTarget.DataReader.PointerSize, out ulong address))
147+
return;
148+
149+
if (state.AddressToNameMapping.ContainsKey(address))
150+
return;
151+
152+
var jitHelperFunctionName = runtime.GetJitHelperFunctionName(address);
153+
if (!string.IsNullOrEmpty(jitHelperFunctionName))
154+
{
155+
state.AddressToNameMapping.Add(address, jitHelperFunctionName);
156+
return;
157+
}
158+
159+
var methodTableName = runtime.DacLibrary.SOSDacInterface.GetMethodTableName(address);
160+
if (!string.IsNullOrEmpty(methodTableName))
161+
{
162+
state.AddressToNameMapping.Add(address, $"MT_{methodTableName}");
163+
return;
164+
}
165+
166+
var methodDescriptor = runtime.GetMethodByHandle(address);
167+
if (!(methodDescriptor is null))
168+
{
169+
state.AddressToNameMapping.Add(address, $"MD_{methodDescriptor.Signature}");
170+
return;
171+
}
172+
173+
var method = runtime.GetMethodByInstructionPointer(address);
174+
if (method is null && (address & ((uint)runtime.DataTarget.DataReader.PointerSize - 1)) == 0)
175+
{
176+
if (runtime.DataTarget.DataReader.ReadPointer(address, out ulong newAddress) && newAddress > ushort.MaxValue)
177+
method = runtime.GetMethodByInstructionPointer(newAddress);
178+
}
179+
180+
if (method is null)
181+
return;
182+
183+
if (method.NativeCode == currentMethod.NativeCode && method.Signature == currentMethod.Signature)
184+
return; // in case of a call which is just a jump within the method or a recursive call
185+
186+
if (!state.HandledMethods.Contains(method))
187+
state.Todo.Enqueue(new MethodInfo(method, depth + 1));
188+
189+
var methodName = method.Signature;
190+
if (!methodName.Any(c => c == '.')) // the method name does not contain namespace and type name
191+
methodName = $"{method.Type.Name}.{method.Signature}";
192+
state.AddressToNameMapping.Add(address, methodName);
193+
}
194+
195+
internal static bool TryGetReferencedAddress(Instruction instruction, uint pointerSize, out ulong referencedAddress)
196+
{
197+
for (int i = 0; i < instruction.OpCount; i++)
198+
{
199+
switch (instruction.GetOpKind(i))
200+
{
201+
case OpKind.NearBranch16:
202+
case OpKind.NearBranch32:
203+
case OpKind.NearBranch64:
204+
referencedAddress = instruction.NearBranchTarget;
205+
return referencedAddress > ushort.MaxValue;
206+
case OpKind.Immediate16:
207+
case OpKind.Immediate8to16:
208+
case OpKind.Immediate8to32:
209+
case OpKind.Immediate8to64:
210+
case OpKind.Immediate32to64:
211+
case OpKind.Immediate32 when pointerSize == 4:
212+
case OpKind.Immediate64:
213+
referencedAddress = instruction.GetImmediate(i);
214+
return referencedAddress > ushort.MaxValue;
215+
case OpKind.Memory when instruction.IsIPRelativeMemoryOperand:
216+
referencedAddress = instruction.IPRelativeMemoryAddress;
217+
return referencedAddress > ushort.MaxValue;
218+
case OpKind.Memory:
219+
referencedAddress = instruction.MemoryDisplacement64;
220+
return referencedAddress > ushort.MaxValue;
221+
}
222+
}
223+
224+
referencedAddress = default;
225+
return false;
226+
}
227+
228+
private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method)
229+
{
230+
// it's better to use one single map rather than few small ones
231+
// it's simply easier to get next instruction when decoding ;)
232+
var hotColdInfo = method.HotColdInfo;
233+
if (hotColdInfo.HotSize > 0 && hotColdInfo.HotStart > 0)
234+
{
235+
return hotColdInfo.ColdSize <= 0
236+
? new[] { new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 } }
237+
: new[]
238+
{
239+
new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 },
240+
new ILToNativeMap() { StartAddress = hotColdInfo.ColdStart, EndAddress = hotColdInfo.ColdStart + hotColdInfo.ColdSize, ILOffset = -1 }
241+
};
242+
}
243+
244+
return method.ILOffsetMap
245+
.Where(map => map.StartAddress < map.EndAddress) // some maps have 0 length?
246+
.OrderBy(map => map.StartAddress) // we need to print in the machine code order, not IL! #536
247+
.ToArray();
248+
}
249+
250+
private static DisassembledMethod CreateEmpty(ClrMethod method, string reason)
251+
=> DisassembledMethod.Empty(method.Signature, method.NativeCode, reason);
252+
253+
private class SharpComparer : IEqualityComparer<Sharp>
254+
{
255+
public bool Equals(Sharp x, Sharp y)
256+
{
257+
// sometimes some C# code lines are duplicated because the same line is the best match for multiple ILToNativeMaps
258+
// we don't want to confuse the users, so this must also be removed
259+
return x.FilePath == y.FilePath && x.LineNumber == y.LineNumber;
260+
}
261+
262+
public int GetHashCode(Sharp obj) => obj.FilePath.GetHashCode() ^ obj.LineNumber;
263+
}
264+
}
265+
}

src/BenchmarkDotNet/Disassemblers/Exporters/DisassemblyPrettifier.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ internal static IReadOnlyList<Element> Prettify(DisassembledMethod method, Disas
3939
// first of all, we search of referenced addresses (jump|calls)
4040
var referencedAddresses = new HashSet<ulong>();
4141
foreach (var asm in asmInstructions)
42-
if (ClrMdDisassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress))
42+
if (ClrMdV2Disassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress))
4343
referencedAddresses.Add(referencedAddress);
4444

4545
// for every IP that is referenced, we emit a uinque label
@@ -72,7 +72,7 @@ internal static IReadOnlyList<Element> Prettify(DisassembledMethod method, Disas
7272
prettified.Add(new Label(label));
7373
}
7474

75-
if (ClrMdDisassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress))
75+
if (ClrMdV2Disassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress))
7676
{
7777
// jump or a call within same method
7878
if (addressesToLabels.TryGetValue(referencedAddress, out string translated))

src/BenchmarkDotNet/Disassemblers/LinuxDisassembler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ internal class LinuxDisassembler
99
internal LinuxDisassembler(DisassemblyDiagnoserConfig config) => this.config = config;
1010

1111
internal DisassemblyResult Disassemble(DiagnoserActionParameters parameters)
12-
=> ClrMdDisassembler.AttachAndDisassemble(BuildDisassemblerSettings(parameters));
12+
=> ClrMdV2Disassembler.AttachAndDisassemble(BuildDisassemblerSettings(parameters));
1313

1414
private Settings BuildDisassemblerSettings(DiagnoserActionParameters parameters)
1515
=> new Settings(

0 commit comments

Comments
 (0)