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

* update and sync TraceEvent version

* port the Disassembler from ClrMD 1.x to 2.x
This commit is contained in:
Adam Sitnik 2022-07-18 11:10:16 +02:00 коммит произвёл GitHub
Родитель 762b76c368
Коммит d24ea32447
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 462 добавлений и 16 удалений

2
.gitignore поставляемый
Просмотреть файл

@ -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

Просмотреть файл

@ -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" />

Просмотреть файл

@ -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>

Просмотреть файл

@ -7,7 +7,8 @@ using System.Linq;
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)
{

Просмотреть файл

@ -24,7 +24,7 @@ namespace BenchmarkDotNet.Disassemblers
try
{
var methodsToExport = ClrMdDisassembler.AttachAndDisassemble(options);
var methodsToExport = ClrMdV1Disassembler.AttachAndDisassemble(options);
SaveToFile(methodsToExport, options.ResultsPath);
}

Просмотреть файл

@ -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>

Просмотреть файл

@ -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" />
<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" />
@ -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>

Просмотреть файл

@ -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;
}
}
}

Просмотреть файл

@ -39,7 +39,7 @@ namespace BenchmarkDotNet.Disassemblers.Exporters
// 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
@ -72,7 +72,7 @@ namespace BenchmarkDotNet.Disassemblers.Exporters
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))

Просмотреть файл

@ -9,7 +9,7 @@ namespace BenchmarkDotNet.Disassemblers
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(

Просмотреть файл

@ -0,0 +1,183 @@
using Microsoft.Diagnostics.Runtime;
using Microsoft.Diagnostics.Symbols;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespace BenchmarkDotNet.Disassemblers
{
internal static class SourceCodeProvider
{
private static readonly Dictionary<SourceFile, string[]> SourceFileCache = new Dictionary<SourceFile, string[]>();
private static readonly Dictionary<SourceFile, string> SourceFilePathsCache = new Dictionary<SourceFile, string>();
internal static IEnumerable<Sharp> GetSource(ClrMethod method, ILToNativeMap map)
{
var sourceLocation = method.GetSourceLocation(map.ILOffset);
if (sourceLocation == null)
yield break;
for (int line = sourceLocation.LineNumber; line <= sourceLocation.LineNumberEnd; ++line)
{
var sourceLine = ReadSourceLine(sourceLocation.SourceFile, line);
if (sourceLine == null)
continue;
var text = sourceLine + Environment.NewLine
+ GetSmartPointer(sourceLine,
start: line == sourceLocation.LineNumber ? sourceLocation.ColumnNumber - 1 : default(int?),
end: line == sourceLocation.LineNumberEnd ? sourceLocation.ColumnNumberEnd - 1 : default(int?));
yield return new Sharp
{
Text = text,
InstructionPointer = map.StartAddress,
FilePath = GetFilePath(sourceLocation.SourceFile),
LineNumber = line
};
}
}
private static string GetFilePath(SourceFile sourceFile)
=> SourceFilePathsCache.TryGetValue(sourceFile, out string filePath) ? filePath : sourceFile.Url;
private static string ReadSourceLine(SourceFile file, int line)
{
if (!SourceFileCache.TryGetValue(file, out string[] contents))
{
// GetSourceFile method returns path when file is stored on the same machine
// otherwise it downloads it from the Symbol Server and returns the source code ;)
string wholeFileOrJustPath = file.GetSourceFile();
if (string.IsNullOrEmpty(wholeFileOrJustPath))
return null;
if (File.Exists(wholeFileOrJustPath))
{
contents = File.ReadAllLines(wholeFileOrJustPath);
SourceFilePathsCache.Add(file, wholeFileOrJustPath);
}
else
{
contents = wholeFileOrJustPath.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
}
SourceFileCache.Add(file, contents);
}
return line - 1 < contents.Length
? contents[line - 1]
: null; // "nop" can have no corresponding c# code ;)
}
private static string GetSmartPointer(string sourceLine, int? start, int? end)
{
Debug.Assert(start is null || start < sourceLine.Length);
Debug.Assert(end is null || end <= sourceLine.Length);
var prefix = new char[end ?? sourceLine.Length];
var index = 0;
// write offset using whitespaces
while (index < (start ?? prefix.Length))
{
prefix[index] =
sourceLine.Length > index &&
sourceLine[index] == '\t'
? '\t'
: ' ';
index++;
}
// write smart pointer
while (index < prefix.Length)
{
prefix[index] = '^';
index++;
}
return new string(prefix);
}
}
internal static class ClrSourceExtensions
{
// TODO Not sure we want this to be a shared dictionary, especially without
// any synchronization. Probably want to put this hanging off the Context
// somewhere, or inside SymbolCache.
private static readonly Dictionary<PdbInfo, ManagedSymbolModule> s_pdbReaders = new Dictionary<PdbInfo, ManagedSymbolModule>();
private static readonly SymbolReader symbolReader = new SymbolReader(TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath };
internal static SourceLocation GetSourceLocation(this ClrMethod method, int ilOffset)
{
var reader = GetReaderForMethod(method);
if (reader == null)
return null;
return reader.SourceLocationForManagedCode((uint)method.MetadataToken, ilOffset);
}
internal static SourceLocation GetSourceLocation(this ClrStackFrame frame)
{
var reader = GetReaderForMethod(frame.Method);
if (reader == null)
return null;
return reader.SourceLocationForManagedCode((uint)frame.Method.MetadataToken, FindIlOffset(frame));
}
private static int FindIlOffset(ClrStackFrame frame)
{
ulong ip = frame.InstructionPointer;
int last = -1;
foreach (ILToNativeMap item in frame.Method.ILOffsetMap)
{
if (item.StartAddress > ip)
return last;
if (ip <= item.EndAddress)
return item.ILOffset;
last = item.ILOffset;
}
return last;
}
private static ManagedSymbolModule GetReaderForMethod(ClrMethod method)
{
ClrModule module = method?.Type?.Module;
PdbInfo info = module?.Pdb;
ManagedSymbolModule reader = null;
if (info != null)
{
if (!s_pdbReaders.TryGetValue(info, out reader))
{
string pdbPath = info.Path;
if (pdbPath != null)
{
try
{
reader = symbolReader.OpenSymbolFile(pdbPath);
}
catch (IOException)
{
// This will typically happen when trying to load information
// from public symbols, or symbol files generated by some weird
// compiler. We can ignore this, but there's no need to load
// this PDB anymore, so we will put null in the dictionary and
// be done with it.
reader = null;
}
}
s_pdbReaders[info] = reader;
}
}
return reader;
}
}
}

Просмотреть файл

@ -31,7 +31,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.3" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Mono.Cecil" Version="0.11.1" />

Просмотреть файл

@ -15,7 +15,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.3" />
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="xunit" Version="2.4.1" />