-
-
Notifications
You must be signed in to change notification settings - Fork 955
/
ClrMdV2Disassembler.cs
355 lines (302 loc) · 16.9 KB
/
ClrMdV2Disassembler.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Filters;
using Microsoft.Diagnostics.Runtime;
using Microsoft.Diagnostics.Runtime.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using BenchmarkDotNet.Portability;
namespace BenchmarkDotNet.Disassemblers
{
// This Disassembler uses ClrMd v2x. Please keep it in sync with ClrMdV1Disassembler (if possible).
internal abstract class ClrMdV2Disassembler
{
// Translating an address to a method can cause AV and a process crash (https://github.com/dotnet/BenchmarkDotNet/issues/2070).
// It was fixed in https://github.com/dotnet/runtime/pull/79846,
// and most likely will be backported to 7.0.2 very soon (https://github.com/dotnet/runtime/pull/79862).
protected static readonly bool IsVulnerableToAvInDac = !RuntimeInformation.IsWindows() && Environment.Version < new Version(7, 0, 2);
internal 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, settings.TargetFrameworkMoniker);
if (settings.Filters.Length > 0)
{
FilterAndEnqueue(state, settings);
}
else
{
ClrType 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 void FilterAndEnqueue(State state, Settings settings)
{
Regex[] filters = GlobFilter.ToRegex(settings.Filters);
foreach (ClrModule module in state.Runtime.EnumerateModules())
foreach (ClrType type in module.EnumerateTypeDefToMethodTableMap().Select(map => state.Runtime.GetTypeByMethodTable(map.MethodTable)).Where(type => type is not null))
foreach (ClrMethod method in type.Methods.Where(method => method.Signature != null))
{
if (method.NativeCode > 0)
{
if (!state.AddressToNameMapping.TryGetValue(method.NativeCode, out _))
{
state.AddressToNameMapping.Add(method.NativeCode, method.Signature);
}
}
if (CanBeDisassembled(method))
{
foreach (Regex filter in filters)
{
if (filter.IsMatch(method.Signature))
{
state.Todo.Enqueue(new MethodInfo(method,
depth: settings.MaxDepth)); // don't allow for recursive disassembling
break;
}
}
}
}
}
private DisassembledMethod[] Disassemble(Settings settings, State state)
{
var result = new List<DisassembledMethod>();
DisassemblySyntax syntax = (DisassemblySyntax)Enum.Parse(typeof(DisassemblySyntax), settings.Syntax);
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, syntax));
}
return result.ToArray();
}
private static bool CanBeDisassembled(ClrMethod method) => method.ILOffsetMap.Length > 0 && method.NativeCode > 0;
private DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings, DisassemblySyntax syntax)
{
var method = methodInfo.Method;
if (!CanBeDisassembled(method))
{
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);
}
foreach (var map in GetCompleteNativeMap(method, state.Runtime))
{
codes.AddRange(Decode(map, state, methodInfo.Depth, method, syntax));
}
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 IEnumerable<Asm> Decode(ILToNativeMap map, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax)
{
ulong startAddress = map.StartAddress;
uint size = (uint)(map.EndAddress - map.StartAddress);
byte[] code = new byte[size];
int totalBytesRead = 0;
do
{
int bytesRead = state.Runtime.DataTarget.DataReader.Read(startAddress + (ulong)totalBytesRead, new Span<byte>(code, totalBytesRead, (int)size - totalBytesRead));
if (bytesRead <= 0)
{
throw new EndOfStreamException($"Tried to read {size} bytes for {currentMethod.Signature}, got only {totalBytesRead}");
}
totalBytesRead += bytesRead;
} while (totalBytesRead != size);
return Decode(code, startAddress, state, depth, currentMethod, syntax);
}
protected abstract IEnumerable<Asm> Decode(byte[] code, ulong startAddress, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax);
private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method, ClrRuntime runtime)
{
if (!TryReadNativeCodeAddresses(runtime, method, out ulong startAddress, out ulong endAddress))
{
startAddress = method.NativeCode;
endAddress = ulong.MaxValue;
}
ILToNativeMap[] sortedMaps = method.ILOffsetMap // CanBeDisassembled ensures that there is at least one map in ILOffsetMap
.Where(map => map.StartAddress >= startAddress && map.StartAddress < endAddress) // can be false for Tier 0 maps, EndAddress is not checked on purpose here
.Where(map => map.StartAddress < map.EndAddress) // some maps have 0 length (they don't have corresponding assembly code?)
.OrderBy(map => map.StartAddress) // we need to print in the machine code order, not IL! #536
.Select(map => new ILToNativeMap()
{
StartAddress = map.StartAddress,
// some maps have EndAddress > codeHeaderData.MethodStart + codeHeaderData.MethodSize and contain garbage (#2074). They need to be fixed!
EndAddress = Math.Min(map.EndAddress, endAddress),
ILOffset = map.ILOffset
})
.ToArray();
if (sortedMaps.Length == 0)
{
// In such situation ILOffsetMap most likely describes Tier 0, while CodeHeaderData Tier 1.
// Since we care about Tier 1 (if it's present), we "fake" a Tier 1 map.
return new[] { new ILToNativeMap() { StartAddress = startAddress, EndAddress = endAddress } };
}
else if (sortedMaps[0].StartAddress != startAddress || (sortedMaps[sortedMaps.Length - 1].EndAddress != endAddress && endAddress != ulong.MaxValue))
{
// In such situation ILOffsetMap most likely is missing few bytes. We just "extend" it to avoid producing "bad" instructions.
return new[] { new ILToNativeMap() { StartAddress = startAddress, EndAddress = endAddress } };
}
return sortedMaps;
}
private static DisassembledMethod CreateEmpty(ClrMethod method, string reason)
=> DisassembledMethod.Empty(method.Signature, method.NativeCode, reason);
protected static bool TryReadNativeCodeAddresses(ClrRuntime runtime, ClrMethod method, out ulong startAddress, out ulong endAddress)
{
if (method is not null
&& runtime.DacLibrary.SOSDacInterface.GetCodeHeaderData(method.NativeCode, out var codeHeaderData) == HResult.S_OK
&& codeHeaderData.MethodSize > 0) // false for extern methods!
{
// HotSize can be missing or be invalid (https://github.com/microsoft/clrmd/issues/1036).
// So we fetch the method size on our own.
startAddress = codeHeaderData.MethodStart;
endAddress = codeHeaderData.MethodStart + codeHeaderData.MethodSize;
return true;
}
startAddress = endAddress = 0;
return false;
}
protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD, State state, bool isIndirectCallOrJump, int depth, ClrMethod currentMethod)
{
var runtime = state.Runtime;
if (state.AddressToNameMapping.ContainsKey(address))
return;
var jitHelperFunctionName = runtime.GetJitHelperFunctionName(address);
if (!string.IsNullOrEmpty(jitHelperFunctionName))
{
state.AddressToNameMapping.Add(address, jitHelperFunctionName);
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);
method = WorkaroundGetMethodByInstructionPointerBug(runtime, method, newAddress);
}
}
else
{
method = WorkaroundGetMethodByInstructionPointerBug(runtime, method, address);
}
if (method is null)
{
if (isAddressPrecodeMD || !IsVulnerableToAvInDac)
{
var methodDescriptor = runtime.GetMethodByHandle(address);
if (!(methodDescriptor is null))
{
if (isAddressPrecodeMD)
{
state.AddressToNameMapping.Add(address, $"Precode of {methodDescriptor.Signature}");
}
else
{
state.AddressToNameMapping.Add(address, $"MD_{methodDescriptor.Signature}");
}
return;
}
}
if (!IsVulnerableToAvInDac)
{
var methodTableName = runtime.DacLibrary.SOSDacInterface.GetMethodTableName(address);
if (!string.IsNullOrEmpty(methodTableName))
{
state.AddressToNameMapping.Add(address, $"MT_{methodTableName}");
return;
}
}
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);
}
protected void FlushCachedDataIfNeeded(IDataReader dataTargetDataReader, ulong address, byte[] buffer)
{
if (!RuntimeInformation.IsWindows())
{
if (dataTargetDataReader.Read(address, buffer) <= 0)
{
// We don't suspend the benchmark process for the time of disassembling,
// as it would require sudo privileges.
// Because of that, the Tiered JIT thread might still re-compile some methods
// in the meantime when the host process it trying to disassemble the code.
// In such case, Tiered JIT thread might create stubs which requires flushing of the cached data.
dataTargetDataReader.FlushCachedData();
}
}
}
// GetMethodByInstructionPointer sometimes returns wrong methods.
// In case given address does not belong to the methods range, null is returned.
private static ClrMethod WorkaroundGetMethodByInstructionPointerBug(ClrRuntime runtime, ClrMethod method, ulong newAddress)
=> TryReadNativeCodeAddresses(runtime, method, out ulong startAddress, out ulong endAddress) && !(startAddress >= newAddress && newAddress <= endAddress)
? null
: method;
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) => HashCode.Combine(obj.FilePath, obj.LineNumber);
}
}
}