Skip to content

Commit 4924f0e

Browse files
authored
Fix few new disassembler bugs caused by update to ClrMd v2 (#2075)
* don't assume that HotSize is always provided (microsoft/clrmd#1036) * handle partial reads * fetch the method size on our own, don't rely on HotCold and ILToNativeMap.EndAddress info numbers * workaround GetMethodByInstructionPointer bug
1 parent cab43e1 commit 4924f0e

File tree

1 file changed

+99
-32
lines changed

1 file changed

+99
-32
lines changed

src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using BenchmarkDotNet.Filters;
22
using Iced.Intel;
33
using Microsoft.Diagnostics.Runtime;
4+
using Microsoft.Diagnostics.Runtime.Utilities;
45
using System;
56
using System.Collections.Generic;
7+
using System.IO;
68
using System.Linq;
79
using System.Text.RegularExpressions;
810

@@ -66,16 +68,29 @@ private static void FilterAndEnqueue(State state, Settings settings)
6668

6769
foreach (ClrModule module in state.Runtime.EnumerateModules())
6870
foreach (ClrType type in module.EnumerateTypeDefToMethodTableMap().Select(map => state.Runtime.GetTypeByMethodTable(map.MethodTable)).Where(type => type is not null))
69-
foreach (ClrMethod method in type.Methods.Where(method => CanBeDisassembled(method) && method.Signature != null))
70-
foreach (Regex filter in filters)
71+
foreach (ClrMethod method in type.Methods.Where(method => method.Signature != null))
72+
{
73+
if (method.NativeCode > 0)
7174
{
72-
if (filter.IsMatch(method.Signature))
75+
if (!state.AddressToNameMapping.TryGetValue(method.NativeCode, out _))
7376
{
74-
state.Todo.Enqueue(new MethodInfo(method,
75-
depth: settings.MaxDepth)); // don't allow for recursive disassembling
76-
break;
77+
state.AddressToNameMapping.Add(method.NativeCode, method.Signature);
7778
}
7879
}
80+
81+
if (CanBeDisassembled(method))
82+
{
83+
foreach (Regex filter in filters)
84+
{
85+
if (filter.IsMatch(method.Signature))
86+
{
87+
state.Todo.Enqueue(new MethodInfo(method,
88+
depth: settings.MaxDepth)); // don't allow for recursive disassembling
89+
break;
90+
}
91+
}
92+
}
93+
}
7994
}
8095

8196
private static DisassembledMethod[] Disassemble(Settings settings, State state)
@@ -96,8 +111,7 @@ private static DisassembledMethod[] Disassemble(Settings settings, State state)
96111
return result.ToArray();
97112
}
98113

99-
private static bool CanBeDisassembled(ClrMethod method)
100-
=> !(method.ILOffsetMap.Length == 0 && (method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0));
114+
private static bool CanBeDisassembled(ClrMethod method) => method.ILOffsetMap.Length > 0 && method.NativeCode > 0;
101115

102116
private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings)
103117
{
@@ -128,9 +142,10 @@ private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State
128142
codes.AddRange(uniqueSourceCodeLines);
129143
}
130144

131-
// for getting ASM we try to use data from HotColdInfo if available (better for decoding)
132-
foreach (var map in GetCompleteNativeMap(method))
133-
codes.AddRange(Decode(map.StartAddress, (uint)(map.EndAddress - map.StartAddress), state, methodInfo.Depth, method));
145+
foreach (var map in GetCompleteNativeMap(method, state.Runtime))
146+
{
147+
codes.AddRange(Decode(map, state, methodInfo.Depth, method));
148+
}
134149

135150
Map[] maps = settings.PrintSource
136151
? codes.GroupBy(code => code.InstructionPointer).OrderBy(group => group.Key).Select(group => new Map() { SourceCodes = group.ToArray() }).ToArray()
@@ -144,14 +159,25 @@ private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State
144159
};
145160
}
146161

147-
private static IEnumerable<Asm> Decode(ulong startAddress, uint size, State state, int depth, ClrMethod currentMethod)
162+
private static IEnumerable<Asm> Decode(ILToNativeMap map, State state, int depth, ClrMethod currentMethod)
148163
{
164+
ulong startAddress = map.StartAddress;
165+
uint size = (uint)(map.EndAddress - map.StartAddress);
166+
149167
byte[] code = new byte[size];
150-
int bytesRead = state.Runtime.DataTarget.DataReader.Read(startAddress, code);
151-
if (bytesRead == 0 || bytesRead != size)
152-
yield break;
153168

154-
var reader = new ByteArrayCodeReader(code, 0, bytesRead);
169+
int totalBytesRead = 0;
170+
do
171+
{
172+
int bytesRead = state.Runtime.DataTarget.DataReader.Read(startAddress + (ulong)totalBytesRead, new Span<byte>(code, totalBytesRead, (int)size - totalBytesRead));
173+
if (bytesRead <= 0)
174+
{
175+
throw new EndOfStreamException($"Tried to read {size} bytes for {currentMethod.Signature}, got only {totalBytesRead}");
176+
}
177+
totalBytesRead += bytesRead;
178+
} while (totalBytesRead != size);
179+
180+
var reader = new ByteArrayCodeReader(code, 0, (int)size);
155181
var decoder = Decoder.Create(state.Runtime.DataTarget.DataReader.PointerSize * 8, reader);
156182
decoder.IP = startAddress;
157183

@@ -204,7 +230,15 @@ private static void TryTranslateAddressToName(Instruction instruction, State sta
204230
if (method is null && (address & ((uint)runtime.DataTarget.DataReader.PointerSize - 1)) == 0)
205231
{
206232
if (runtime.DataTarget.DataReader.ReadPointer(address, out ulong newAddress) && newAddress > ushort.MaxValue)
233+
{
207234
method = runtime.GetMethodByInstructionPointer(newAddress);
235+
236+
method = WorkaroundGetMethodByInstructionPointerBug(runtime, method, newAddress);
237+
}
238+
}
239+
else
240+
{
241+
method = WorkaroundGetMethodByInstructionPointerBug(runtime, method, address);
208242
}
209243

210244
if (method is null)
@@ -222,6 +256,13 @@ private static void TryTranslateAddressToName(Instruction instruction, State sta
222256
state.AddressToNameMapping.Add(address, methodName);
223257
}
224258

259+
// GetMethodByInstructionPointer sometimes returns wrong methods.
260+
// In case given address does not belong to the methods range, null is returned.
261+
private static ClrMethod WorkaroundGetMethodByInstructionPointerBug(ClrRuntime runtime, ClrMethod method, ulong newAddress)
262+
=> TryReadNativeCodeAddresses(runtime, method, out ulong startAddress, out ulong endAddress) && !(startAddress >= newAddress && newAddress <= endAddress)
263+
? null
264+
: method;
265+
225266
internal static bool TryGetReferencedAddress(Instruction instruction, uint pointerSize, out ulong referencedAddress)
226267
{
227268
for (int i = 0; i < instruction.OpCount; i++)
@@ -255,26 +296,52 @@ internal static bool TryGetReferencedAddress(Instruction instruction, uint point
255296
return false;
256297
}
257298

258-
private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method)
299+
private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method, ClrRuntime runtime)
300+
{
301+
if (!TryReadNativeCodeAddresses(runtime, method, out ulong startAddress, out ulong endAddress))
302+
{
303+
startAddress = method.NativeCode;
304+
endAddress = ulong.MaxValue;
305+
}
306+
307+
ILToNativeMap[] sortedMaps = method.ILOffsetMap // CanBeDisassembled ensures that there is at least one map in ILOffsetMap
308+
.Where(map => map.StartAddress >= startAddress && map.StartAddress < endAddress) // can be false for Tier 0 maps, EndAddress is not checked on purpose here
309+
.Where(map => map.StartAddress < map.EndAddress) // some maps have 0 length (they don't have corresponding assembly code?)
310+
.OrderBy(map => map.StartAddress) // we need to print in the machine code order, not IL! #536
311+
.Select(map => new ILToNativeMap()
312+
{
313+
StartAddress = map.StartAddress,
314+
// some maps have EndAddress > codeHeaderData.MethodStart + codeHeaderData.MethodSize and contain garbage (#2074). They need to be fixed!
315+
EndAddress = Math.Min(map.EndAddress, endAddress),
316+
ILOffset = map.ILOffset
317+
})
318+
.ToArray();
319+
320+
if (sortedMaps.Length == 0)
321+
{
322+
// In such situation ILOffsetMap most likely describes Tier 0, while CodeHeaderData Tier 1.
323+
// Since we care about Tier 1 (if it's present), we "fake" a Tier 1 map.
324+
return new[] { new ILToNativeMap() { StartAddress = startAddress, EndAddress = endAddress }};
325+
}
326+
327+
return sortedMaps;
328+
}
329+
330+
private static bool TryReadNativeCodeAddresses(ClrRuntime runtime, ClrMethod method, out ulong startAddress, out ulong endAddress)
259331
{
260-
// it's better to use one single map rather than few small ones
261-
// it's simply easier to get next instruction when decoding ;)
262-
var hotColdInfo = method.HotColdInfo;
263-
if (hotColdInfo.HotSize > 0 && hotColdInfo.HotStart > 0)
332+
if (method is not null
333+
&& runtime.DacLibrary.SOSDacInterface.GetCodeHeaderData(method.NativeCode, out var codeHeaderData) == HResult.S_OK
334+
&& codeHeaderData.MethodSize > 0) // false for extern methods!
264335
{
265-
return hotColdInfo.ColdSize <= 0
266-
? new[] { new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 } }
267-
: new[]
268-
{
269-
new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 },
270-
new ILToNativeMap() { StartAddress = hotColdInfo.ColdStart, EndAddress = hotColdInfo.ColdStart + hotColdInfo.ColdSize, ILOffset = -1 }
271-
};
336+
// HotSize can be missing or be invalid (https://github.com/microsoft/clrmd/issues/1036).
337+
// So we fetch the method size on our own.
338+
startAddress = codeHeaderData.MethodStart;
339+
endAddress = codeHeaderData.MethodStart + codeHeaderData.MethodSize;
340+
return true;
272341
}
273342

274-
return method.ILOffsetMap
275-
.Where(map => map.StartAddress < map.EndAddress) // some maps have 0 length?
276-
.OrderBy(map => map.StartAddress) // we need to print in the machine code order, not IL! #536
277-
.ToArray();
343+
startAddress = endAddress = 0;
344+
return false;
278345
}
279346

280347
private static DisassembledMethod CreateEmpty(ClrMethod method, string reason)

0 commit comments

Comments
 (0)