Skip to content

Port the .NET (Core) disassembler to ClrMd v2 #2040

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ tests/output/*
artifacts/*
BDN.Generated
BenchmarkDotNet.Samples/Properties/launchSettings.json
src/BenchmarkDotNet/Disassemblers/net461/*
src/BenchmarkDotNet/Disassemblers/net462/*
src/BenchmarkDotNet/Disassemblers/BenchmarkDotNet.Disassembler.*.nupkg

# Visual Studio 2015 cache/options directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="System.Drawing.Common" Version="4.5.1" />
<PackageReference Include="System.Memory" Version="4.5.3" />
<PackageReference Include="System.Memory" Version="4.5.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BenchmarkDotNet\BenchmarkDotNet.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
<ProjectReference Include="..\BenchmarkDotNet\BenchmarkDotNet.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.0.1" PrivateAssets="contentfiles;analyzers" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.0.2" PrivateAssets="contentfiles;analyzers" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

namespace BenchmarkDotNet.Disassemblers
{
internal static class ClrMdDisassembler
// This Disassembler uses ClrMd v1x. Please keep it in sync with ClrMdV2Disassembler (if possible).
internal static class ClrMdV1Disassembler
{
internal static DisassemblyResult AttachAndDisassemble(Settings settings)
{
Expand Down
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet.Disassembler.x64/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static void Main(string[] args)

try
{
var methodsToExport = ClrMdDisassembler.AttachAndDisassemble(options);
var methodsToExport = ClrMdV1Disassembler.AttachAndDisassemble(options);

SaveToFile(methodsToExport, options.ResultsPath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\DataContracts.cs" Link="DataContracts.cs" />
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\ClrMdDisassembler.cs" Link="ClrMdDisassembler.cs" />
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\ClrMdV1Disassembler.cs" Link="ClrMdV1Disassembler.cs" />
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\SourceCodeProvider.cs" Link="SourceCodeProvider.cs" />
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\Program.cs" Link="Program.cs" />
</ItemGroup>
Expand Down
7 changes: 2 additions & 5 deletions src/BenchmarkDotNet/BenchmarkDotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.4.3" />
<PackageReference Include="Iced" Version="1.17.0" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="1.1.126102" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.2.332302" />
<PackageReference Include="Perfolizer" Version="0.2.1" />
<PackageReference Include="System.Management" Version="6.0.0" />
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" />
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.61701" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we get this dependency from ClrMD now (and it uses newer version)

<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.61" PrivateAssets="contentfiles;analyzers" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.0.2" PrivateAssets="contentfiles;analyzers" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
Expand All @@ -47,7 +46,5 @@
</ItemGroup>
<ItemGroup>
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\DataContracts.cs" Link="Disassemblers\DataContracts.cs" />
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\SourceCodeProvider.cs" Link="Disassemblers\SourceCodeProvider.cs" />
<Compile Include="..\BenchmarkDotNet.Disassembler.x64\ClrMdDisassembler.cs" Link="Disassemblers\ClrMdDisassembler.cs" />
</ItemGroup>
</Project>
265 changes: 265 additions & 0 deletions src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
using Iced.Intel;
using Microsoft.Diagnostics.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;

namespace BenchmarkDotNet.Disassemblers
{
// This Disassembler uses ClrMd v2x. Please keep it in sync with ClrMdV1Disassembler (if possible).
internal static class ClrMdV2Disassembler
{
internal static DisassemblyResult AttachAndDisassemble(Settings settings)
{
using (var dataTarget = DataTarget.AttachToProcess(
settings.ProcessId,
suspend: false))
{
var runtime = dataTarget.ClrVersions.Single().CreateRuntime();

ConfigureSymbols(dataTarget);

var state = new State(runtime);

var typeWithBenchmark = state.Runtime.EnumerateModules().Select(module => module.GetTypeByName(settings.TypeName)).First(type => type != null);

state.Todo.Enqueue(
new MethodInfo(
// the Disassembler Entry Method is always parameterless, so check by name is enough
typeWithBenchmark.Methods.Single(method => method.IsPublic && method.Name == settings.MethodName),
0));

var disassembledMethods = Disassemble(settings, state);

// we don't want to export the disassembler entry point method which is just an artificial method added to get generic types working
var filteredMethods = disassembledMethods.Length == 1
? disassembledMethods // if there is only one method we want to return it (most probably benchmark got inlined)
: disassembledMethods.Where(method => !method.Name.Contains(DisassemblerConstants.DisassemblerEntryMethodName)).ToArray();

return new DisassemblyResult
{
Methods = filteredMethods,
SerializedAddressToNameMapping = state.AddressToNameMapping.Select(x => new DisassemblyResult.MutablePair { Key = x.Key, Value = x.Value }).ToArray(),
PointerSize = (uint)IntPtr.Size
};
}
}

private static void ConfigureSymbols(DataTarget dataTarget)
{
// code copied from https://github.com/Microsoft/clrmd/issues/34#issuecomment-161926535
dataTarget.SetSymbolPath("http://msdl.microsoft.com/download/symbols");
}

private static DisassembledMethod[] Disassemble(Settings settings, State state)
{
var result = new List<DisassembledMethod>();

while (state.Todo.Count != 0)
{
var methodInfo = state.Todo.Dequeue();

if (!state.HandledMethods.Add(methodInfo.Method)) // add it now to avoid StackOverflow for recursive methods
continue; // already handled

if (settings.MaxDepth >= methodInfo.Depth)
result.Add(DisassembleMethod(methodInfo, state, settings));
}

return result.ToArray();
}

private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings)
{
var method = methodInfo.Method;

if (method.ILOffsetMap.Length == 0 && (method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0))
{
if (method.IsPInvoke)
return CreateEmpty(method, "PInvoke method");
if (method.IL is null || method.IL.Length == 0)
return CreateEmpty(method, "Extern method");
if (method.CompilationType == MethodCompilationType.None)
return CreateEmpty(method, "Method was not JITted yet.");

return CreateEmpty(method, $"No valid {nameof(method.ILOffsetMap)} and {nameof(method.HotColdInfo)}");
}

var codes = new List<SourceCode>();
if (settings.PrintSource && method.ILOffsetMap.Length > 0)
{
// we use HashSet to prevent from duplicates
var uniqueSourceCodeLines = new HashSet<Sharp>(new SharpComparer());
// for getting C# code we always use the original ILOffsetMap
foreach (var map in method.ILOffsetMap.Where(map => map.StartAddress < map.EndAddress && map.ILOffset >= 0).OrderBy(map => map.StartAddress))
foreach (var sharp in SourceCodeProvider.GetSource(method, map))
uniqueSourceCodeLines.Add(sharp);

codes.AddRange(uniqueSourceCodeLines);
}

// for getting ASM we try to use data from HotColdInfo if available (better for decoding)
foreach (var map in GetCompleteNativeMap(method))
codes.AddRange(Decode(map.StartAddress, (uint)(map.EndAddress - map.StartAddress), state, methodInfo.Depth, method));

Map[] maps = settings.PrintSource
? codes.GroupBy(code => code.InstructionPointer).OrderBy(group => group.Key).Select(group => new Map() { SourceCodes = group.ToArray() }).ToArray()
: new[] { new Map() { SourceCodes = codes.ToArray() } };

return new DisassembledMethod
{
Maps = maps,
Name = method.Signature,
NativeCode = method.NativeCode
};
}

private static IEnumerable<Asm> Decode(ulong startAddress, uint size, State state, int depth, ClrMethod currentMethod)
{
byte[] code = new byte[size];
int bytesRead = state.Runtime.DataTarget.DataReader.Read(startAddress, code);
if (bytesRead == 0 || bytesRead != size)
yield break;

var reader = new ByteArrayCodeReader(code, 0, bytesRead);
var decoder = Decoder.Create(state.Runtime.DataTarget.DataReader.PointerSize * 8, reader);
decoder.IP = startAddress;

while (reader.CanReadByte)
{
decoder.Decode(out var instruction);

TryTranslateAddressToName(instruction, state, depth, currentMethod);

yield return new Asm
{
InstructionPointer = instruction.IP,
Instruction = instruction
};
}
}

private static void TryTranslateAddressToName(Instruction instruction, State state, int depth, ClrMethod currentMethod)
{
var runtime = state.Runtime;

if (!TryGetReferencedAddress(instruction, (uint)runtime.DataTarget.DataReader.PointerSize, out ulong address))
return;

if (state.AddressToNameMapping.ContainsKey(address))
return;

var jitHelperFunctionName = runtime.GetJitHelperFunctionName(address);
if (!string.IsNullOrEmpty(jitHelperFunctionName))
{
state.AddressToNameMapping.Add(address, jitHelperFunctionName);
return;
}

var methodTableName = runtime.DacLibrary.SOSDacInterface.GetMethodTableName(address);
if (!string.IsNullOrEmpty(methodTableName))
{
state.AddressToNameMapping.Add(address, $"MT_{methodTableName}");
return;
}

var methodDescriptor = runtime.GetMethodByHandle(address);
if (!(methodDescriptor is null))
{
state.AddressToNameMapping.Add(address, $"MD_{methodDescriptor.Signature}");
return;
}

var method = runtime.GetMethodByInstructionPointer(address);
if (method is null && (address & ((uint)runtime.DataTarget.DataReader.PointerSize - 1)) == 0)
{
if (runtime.DataTarget.DataReader.ReadPointer(address, out ulong newAddress) && newAddress > ushort.MaxValue)
method = runtime.GetMethodByInstructionPointer(newAddress);
}

if (method is null)
return;

if (method.NativeCode == currentMethod.NativeCode && method.Signature == currentMethod.Signature)
return; // in case of a call which is just a jump within the method or a recursive call

if (!state.HandledMethods.Contains(method))
state.Todo.Enqueue(new MethodInfo(method, depth + 1));

var methodName = method.Signature;
if (!methodName.Any(c => c == '.')) // the method name does not contain namespace and type name
methodName = $"{method.Type.Name}.{method.Signature}";
state.AddressToNameMapping.Add(address, methodName);
}

internal static bool TryGetReferencedAddress(Instruction instruction, uint pointerSize, out ulong referencedAddress)
{
for (int i = 0; i < instruction.OpCount; i++)
{
switch (instruction.GetOpKind(i))
{
case OpKind.NearBranch16:
case OpKind.NearBranch32:
case OpKind.NearBranch64:
referencedAddress = instruction.NearBranchTarget;
return referencedAddress > ushort.MaxValue;
case OpKind.Immediate16:
case OpKind.Immediate8to16:
case OpKind.Immediate8to32:
case OpKind.Immediate8to64:
case OpKind.Immediate32to64:
case OpKind.Immediate32 when pointerSize == 4:
case OpKind.Immediate64:
referencedAddress = instruction.GetImmediate(i);
return referencedAddress > ushort.MaxValue;
case OpKind.Memory when instruction.IsIPRelativeMemoryOperand:
referencedAddress = instruction.IPRelativeMemoryAddress;
return referencedAddress > ushort.MaxValue;
case OpKind.Memory:
referencedAddress = instruction.MemoryDisplacement64;
return referencedAddress > ushort.MaxValue;
}
}

referencedAddress = default;
return false;
}

private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method)
{
// it's better to use one single map rather than few small ones
// it's simply easier to get next instruction when decoding ;)
var hotColdInfo = method.HotColdInfo;
if (hotColdInfo.HotSize > 0 && hotColdInfo.HotStart > 0)
{
return hotColdInfo.ColdSize <= 0
? new[] { new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 } }
: new[]
{
new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 },
new ILToNativeMap() { StartAddress = hotColdInfo.ColdStart, EndAddress = hotColdInfo.ColdStart + hotColdInfo.ColdSize, ILOffset = -1 }
};
}

return method.ILOffsetMap
.Where(map => map.StartAddress < map.EndAddress) // some maps have 0 length?
.OrderBy(map => map.StartAddress) // we need to print in the machine code order, not IL! #536
.ToArray();
}

private static DisassembledMethod CreateEmpty(ClrMethod method, string reason)
=> DisassembledMethod.Empty(method.Signature, method.NativeCode, reason);

private class SharpComparer : IEqualityComparer<Sharp>
{
public bool Equals(Sharp x, Sharp y)
{
// sometimes some C# code lines are duplicated because the same line is the best match for multiple ILToNativeMaps
// we don't want to confuse the users, so this must also be removed
return x.FilePath == y.FilePath && x.LineNumber == y.LineNumber;
}

public int GetHashCode(Sharp obj) => obj.FilePath.GetHashCode() ^ obj.LineNumber;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal static IReadOnlyList<Element> Prettify(DisassembledMethod method, Disas
// first of all, we search of referenced addresses (jump|calls)
var referencedAddresses = new HashSet<ulong>();
foreach (var asm in asmInstructions)
if (ClrMdDisassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress))
if (ClrMdV2Disassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress))
referencedAddresses.Add(referencedAddress);

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

if (ClrMdDisassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress))
if (ClrMdV2Disassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress))
{
// jump or a call within same method
if (addressesToLabels.TryGetValue(referencedAddress, out string translated))
Expand Down
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Disassemblers/LinuxDisassembler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal class LinuxDisassembler
internal LinuxDisassembler(DisassemblyDiagnoserConfig config) => this.config = config;

internal DisassemblyResult Disassemble(DiagnoserActionParameters parameters)
=> ClrMdDisassembler.AttachAndDisassemble(BuildDisassemblerSettings(parameters));
=> ClrMdV2Disassembler.AttachAndDisassemble(BuildDisassemblerSettings(parameters));

private Settings BuildDisassemblerSettings(DiagnoserActionParameters parameters)
=> new Settings(
Expand Down
Loading