Skip to content

Commit aa810aa

Browse files
Support reading naot crashinfo via module export (#5528)
SOS supports reading NAOT crash information using a JSON encoding mechanism. The mechanism relies on passing the serialized exception address via a [FailFast exception parameter](https://github.com/dotnet/runtime/blob/67837792faa88beb3f44105dfa7a3a430961ff12/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs#L322-L332) along with a special code. SOS can read this data via `!crashinfo` as well as `!clrma`. As of dotnet/runtime#117832, NAOT can serialize exception data when `ExceptionHandling.RaiseAppDomainUnhandledExceptionEvent` is called. This PR adds the corresponding SOS functionality to locate the [g_CrashInfoBuffer](https://github.com/dotnet/runtime/blob/67837792faa88beb3f44105dfa7a3a430961ff12/src/coreclr/nativeaot/Runtime/DebugHeader.cpp#L261) global as an alternate mechanism in the event the exception does not contain the address of the buffer. Since this requires reading module exports, this change introduces a configurable module enumeration mechanism via the `!crashinfo` command. --------- Co-authored-by: Michal Strehovský <[email protected]>
1 parent 496f7d7 commit aa810aa

File tree

15 files changed

+428
-19
lines changed

15 files changed

+428
-19
lines changed

src/Microsoft.Diagnostics.DebugServices.Implementation/CrashInfoService.cs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,202 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.IO;
8+
using System.Runtime.InteropServices;
89
using System.Linq;
910
using System.Text;
1011
using System.Text.Json;
1112
using System.Text.Json.Serialization;
1213

1314
namespace Microsoft.Diagnostics.DebugServices.Implementation
1415
{
16+
public sealed class CrashInfoModuleService : ICrashInfoModuleService
17+
{
18+
private readonly IServiceProvider Services;
19+
20+
public CrashInfoModuleService(IServiceProvider services)
21+
{
22+
Services = services ?? throw new ArgumentNullException(nameof(services));
23+
}
24+
public ICrashInfoService Create(ModuleEnumerationScheme moduleEnumerationScheme)
25+
{
26+
return CreateCrashInfoServiceFromModule(Services, moduleEnumerationScheme);
27+
}
28+
29+
private static bool CreateCrashInfoServiceForModule(IModule module, out ICrashInfoService crashInfoService)
30+
{
31+
crashInfoService = null;
32+
if (module == null || module.IsManaged)
33+
{
34+
return false;
35+
}
36+
if ((module as IExportSymbols)?.TryGetSymbolAddress(CrashInfoService.DOTNET_RUNTIME_DEBUG_HEADER_NAME, out ulong headerAddr) != true)
37+
{
38+
Trace.TraceInformation($"CrashInfoService: {CrashInfoService.DOTNET_RUNTIME_DEBUG_HEADER_NAME} export not found in module {module.FileName}");
39+
return false;
40+
}
41+
IMemoryService memoryService = module.Services.GetService<IMemoryService>();
42+
if (!memoryService.Read<uint>(ref headerAddr, out uint headerValue) ||
43+
headerValue != CrashInfoService.DOTNET_RUNTIME_DEBUG_HEADER_COOKIE ||
44+
!memoryService.Read<ushort>(ref headerAddr, out ushort majorVersion) || majorVersion < 3 || // .NET 8 and later
45+
!memoryService.Read<ushort>(ref headerAddr, out ushort minorVersion))
46+
{
47+
Trace.TraceInformation($"CrashInfoService: .NET 8+ {CrashInfoService.DOTNET_RUNTIME_DEBUG_HEADER_NAME} not found in module {module.FileName}");
48+
return false;
49+
}
50+
51+
Trace.TraceInformation($"CrashInfoService: Found {CrashInfoService.DOTNET_RUNTIME_DEBUG_HEADER_NAME} in module {module.FileName} with version {majorVersion}.{minorVersion}");
52+
53+
if (!memoryService.Read<uint>(ref headerAddr, out uint flags) ||
54+
memoryService.PointerSize != (flags == 0x1 ? 8 : 4) ||
55+
!memoryService.Read<uint>(ref headerAddr, out uint _)) // padding
56+
{
57+
Trace.TraceError($"CrashInfoService: Failed to read DotNetRuntimeDebugHeader flags or padding in module {module.FileName}");
58+
return false;
59+
}
60+
61+
Trace.TraceInformation($"CrashInfoService: Target is {memoryService.PointerSize * 8}-bit");
62+
63+
headerAddr += (uint)memoryService.PointerSize; // skip DebugEntries array
64+
65+
// read GlobalEntries array
66+
if (!memoryService.ReadPointer(ref headerAddr, out ulong globalEntryArrayAddress) || globalEntryArrayAddress == 0)
67+
{
68+
Trace.TraceError($"CrashInfoService: Unable to read GlobalEntry array");
69+
return false;
70+
}
71+
uint maxGlobalEntries = (majorVersion >= 4 /*.NET 9 or later*/) ? CrashInfoService.MAX_GLOBAL_ENTRIES_ARRAY_SIZE_NET9_PLUS : CrashInfoService.MAX_GLOBAL_ENTRIES_ARRAY_SIZE_NET8;
72+
for (int i = 0; i < maxGlobalEntries; i++)
73+
{
74+
if (!memoryService.ReadPointer(ref globalEntryArrayAddress, out ulong globalNameAddress) || globalNameAddress == 0 ||
75+
!memoryService.ReadPointer(ref globalEntryArrayAddress, out ulong globalValueAddress) || globalValueAddress == 0 ||
76+
!memoryService.ReadAnsiString(CrashInfoService.MAX_GLOBAL_ENTRY_NAME_CHARS, globalNameAddress, out string globalName))
77+
{
78+
break; // no more global entries
79+
}
80+
81+
if (!string.Equals(globalName, "g_CrashInfoBuffer", StringComparison.Ordinal))
82+
{
83+
continue; // not the crash info buffer
84+
}
85+
86+
ulong triageBufferAddress = globalValueAddress;
87+
Span<byte> buffer = new byte[CrashInfoService.MAX_CRASHINFOBUFFER_SIZE];
88+
if (memoryService.ReadMemory(triageBufferAddress, buffer, out int bytesRead) && bytesRead > 0 && bytesRead <= CrashInfoService.MAX_CRASHINFOBUFFER_SIZE)
89+
{
90+
// truncate the buffer to the null terminated string in the buffer
91+
int nullTerminatorIndex = buffer.IndexOf((byte)0);
92+
if (nullTerminatorIndex >= 0)
93+
{
94+
buffer = buffer.Slice(0, nullTerminatorIndex);
95+
Trace.TraceInformation($"CrashInfoService: Found g_CrashInfoBuffer in module {module.FileName} with size {buffer.Length} bytes");
96+
if (buffer.Length > 0)
97+
{
98+
crashInfoService = CrashInfoService.Create(0, buffer, module.Services.GetService<IModuleService>());
99+
return true;
100+
}
101+
else
102+
{
103+
Trace.TraceError($"CrashInfoService: g_CrashInfoBuffer is empty in module {module.FileName}");
104+
}
105+
}
106+
else
107+
{
108+
Trace.TraceError($"CrashInfoService: g_CrashInfoBuffer is not null terminated in module {module.FileName}");
109+
}
110+
}
111+
else
112+
{
113+
Trace.TraceError($"CrashInfoService: ReadMemory({triageBufferAddress}) failed in module {module.FileName}");
114+
}
115+
break;
116+
}
117+
return false;
118+
}
119+
120+
private static unsafe ICrashInfoService CreateCrashInfoServiceFromModule(IServiceProvider services, ModuleEnumerationScheme moduleEnumerationScheme)
121+
{
122+
if (moduleEnumerationScheme == ModuleEnumerationScheme.None)
123+
{
124+
return null;
125+
}
126+
127+
if (moduleEnumerationScheme == ModuleEnumerationScheme.EntryPointModule ||
128+
moduleEnumerationScheme == ModuleEnumerationScheme.EntryPointAndEntryPointDllModule)
129+
{
130+
// if the above did not locate the crash info service, then look for the DotNetRuntimeDebugHeader
131+
IModule entryPointModule = services.GetService<IModuleService>().EntryPointModule;
132+
if (entryPointModule == null)
133+
{
134+
Trace.TraceError("CrashInfoService: No entry point module found");
135+
return null;
136+
}
137+
if (CreateCrashInfoServiceForModule(entryPointModule, out ICrashInfoService crashInfoService))
138+
{
139+
return crashInfoService;
140+
}
141+
if (moduleEnumerationScheme == ModuleEnumerationScheme.EntryPointAndEntryPointDllModule)
142+
{
143+
// if the entry point module did not have the crash info service, then look for a library with the same name as the entry point
144+
string fileName = entryPointModule.FileName;
145+
OSPlatform osPlatform = entryPointModule.Target.OperatingSystem;
146+
if (fileName == null)
147+
{
148+
Trace.TraceError("CrashInfoService: Entry point module has no file name");
149+
return null;
150+
}
151+
if (osPlatform == OSPlatform.Windows)
152+
{
153+
if (fileName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
154+
{
155+
fileName = fileName.Substring(0, fileName.Length - 4) + ".dll"; // look for a dll with the same name
156+
}
157+
else
158+
{
159+
Trace.TraceError($"CrashInfoService: Unexpected entry point module file name {fileName}");
160+
}
161+
}
162+
else if (osPlatform == OSPlatform.Linux)
163+
{
164+
fileName += ".so"; // look for a .so with the same name
165+
}
166+
else if (osPlatform == OSPlatform.OSX)
167+
{
168+
fileName += ".dylib"; // look for a .dylib with the same name
169+
}
170+
else
171+
{
172+
Trace.TraceError($"CrashInfoService: Unsupported operating system {osPlatform}");
173+
return null;
174+
}
175+
foreach (IModule speculativeAppModule in services.GetService<IModuleService>().GetModuleFromModuleName(fileName))
176+
{
177+
if (CreateCrashInfoServiceForModule(speculativeAppModule, out crashInfoService))
178+
{
179+
return crashInfoService;
180+
}
181+
}
182+
}
183+
}
184+
else if (moduleEnumerationScheme == ModuleEnumerationScheme.All)
185+
{
186+
// enumerate all modules and look for the crash info service
187+
foreach (IModule module in services.GetService<IModuleService>().EnumerateModules())
188+
{
189+
if (CreateCrashInfoServiceForModule(module, out ICrashInfoService crashInfoService))
190+
{
191+
return crashInfoService;
192+
}
193+
}
194+
}
195+
else
196+
{
197+
Trace.TraceError($"CrashInfoService: Unknown module enumeration scheme {moduleEnumerationScheme}");
198+
}
199+
200+
return null;
201+
}
202+
}
203+
15204
public class CrashInfoService : ICrashInfoService
16205
{
17206
/// <summary>
@@ -24,6 +213,13 @@ public class CrashInfoService : ICrashInfoService
24213
/// </summary>
25214
public const uint FAST_FAIL_EXCEPTION_DOTNET_AOT = 0x48;
26215

216+
public const uint MAX_CRASHINFOBUFFER_SIZE = 8192;
217+
public const uint MAX_GLOBAL_ENTRIES_ARRAY_SIZE_NET8 = 6;
218+
public const uint MAX_GLOBAL_ENTRIES_ARRAY_SIZE_NET9_PLUS = 8;
219+
public const uint MAX_GLOBAL_ENTRY_NAME_CHARS = 32;
220+
public const uint DOTNET_RUNTIME_DEBUG_HEADER_COOKIE = 0x48444e44; // 'DNDH'
221+
public const string DOTNET_RUNTIME_DEBUG_HEADER_NAME = "DotNetRuntimeDebugHeader";
222+
27223
private sealed class CrashInfoJson
28224
{
29225
[JsonPropertyName("version")]

src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ public IModule CreateModule(int moduleIndex, ulong imageBase, ulong imageSize, s
194194
return new ModuleFromAddress(this, moduleIndex, imageBase, imageSize, imageName);
195195
}
196196

197+
/// <summary>
198+
/// Gets the entrypoint module for the target. This is the first module in the sorted list of modules.
199+
/// </summary>
200+
public virtual IModule EntryPointModule
201+
{
202+
get
203+
{
204+
// The entry point module is the first module in the sorted list of modules.
205+
// This is not necessarily the same as the entry point module in the target.
206+
IModule[] modules = GetSortedModules();
207+
return modules.Length > 0 ? modules[0] : null;
208+
}
209+
}
210+
197211
#endregion
198212

199213
/// <summary>

src/Microsoft.Diagnostics.DebugServices.Implementation/SpecialDiagInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private ulong SpecialDiagInfoAddress
8484
}
8585
}
8686

87-
public static ICrashInfoService CreateCrashInfoService(IServiceProvider services)
87+
public static ICrashInfoService CreateCrashInfoServiceFromException(IServiceProvider services)
8888
{
8989
EXCEPTION_RECORD64 exceptionRecord;
9090

src/Microsoft.Diagnostics.DebugServices.Implementation/TargetFromDataReader.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,13 @@ public TargetFromDataReader(DataTarget dataTarget, OSPlatform targetOS, IHost ho
7474
});
7575

7676
// Add optional crash info service (currently only for Native AOT on Linux/MacOS).
77-
_serviceContainerFactory.AddServiceFactory<ICrashInfoService>((services) => SpecialDiagInfo.CreateCrashInfoService(services));
77+
_serviceContainerFactory.AddServiceFactory<ICrashInfoService>((services) => SpecialDiagInfo.CreateCrashInfoServiceFromException(services));
7878
OnFlushEvent.Register(() => FlushService<ICrashInfoService>());
7979

80+
// Add the crash info service factory which lookup the DotNetRuntimeDebugHeader from modules
81+
_serviceContainerFactory.AddServiceFactory<ICrashInfoModuleService>((services) => new CrashInfoModuleService(services));
82+
OnFlushEvent.Register(() => FlushService<ICrashInfoModuleService>());
83+
8084
Finished();
8185
}
8286

src/Microsoft.Diagnostics.DebugServices/ICrashInfoService.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,39 @@ public enum CrashReason
1717
InternalFailFast = 3,
1818
}
1919

20+
public enum ModuleEnumerationScheme
21+
{
22+
/// <summary>
23+
/// Enumerate no modules
24+
/// </summary>
25+
None,
26+
27+
/// <summary>
28+
/// Enumerate only the entry point module
29+
/// </summary>
30+
EntryPointModule,
31+
32+
/// <summary>
33+
/// Enumerate only the entry point module and the entry point DLL module
34+
/// </summary>
35+
EntryPointAndEntryPointDllModule,
36+
37+
/// <summary>
38+
/// Enumerate all modules in the target
39+
/// </summary>
40+
All
41+
}
42+
43+
public interface ICrashInfoModuleService
44+
{
45+
/// <summary>
46+
/// Create a crash info service
47+
/// </summary>
48+
/// <param name="moduleEnumerationScheme">module enumeration scheme</param>
49+
/// <returns>ICrashInfoService</returns>
50+
ICrashInfoService Create(ModuleEnumerationScheme moduleEnumerationScheme);
51+
}
52+
2053
/// <summary>
2154
/// Crash information service. Details about the unhandled exception or crash.
2255
/// </summary>

src/Microsoft.Diagnostics.DebugServices/IModuleService.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,13 @@ public interface IModuleService
5757
/// <returns>IModule</returns>
5858
/// <exception cref="ArgumentNullException">thrown if imageBase or imageSize is 0</exception>
5959
IModule CreateModule(int moduleIndex, ulong imageBase, ulong imageSize, string imageName);
60+
61+
/// <summary>
62+
/// Gets the entrypoint module for the target.
63+
/// </summary>
64+
IModule EntryPointModule
65+
{
66+
get;
67+
}
6068
}
6169
}

src/Microsoft.Diagnostics.DebugServices/MemoryServiceExtensions.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Text;
56
using System.Diagnostics;
67
using System.IO;
78
using System.Runtime.InteropServices;
9+
using System.Runtime.CompilerServices;
810

911
namespace Microsoft.Diagnostics.DebugServices
1012
{
@@ -71,6 +73,22 @@ public static bool ReadDword(this IMemoryService memoryService, ulong address, o
7173
return false;
7274
}
7375

76+
public static bool Read<T>(this IMemoryService memoryService, ref ulong address, out T value) where T : unmanaged
77+
{
78+
Span<byte> buffer = stackalloc byte[Unsafe.SizeOf<T>()];
79+
if (memoryService.ReadMemory(address, buffer, out int bytesRead))
80+
{
81+
if (bytesRead == Unsafe.SizeOf<T>())
82+
{
83+
value = MemoryMarshal.Read<T>(buffer);
84+
address += (ulong)Unsafe.SizeOf<T>();
85+
return true;
86+
}
87+
}
88+
value = default;
89+
return false;
90+
}
91+
7492
/// <summary>
7593
/// Return a pointer sized value from the address.
7694
/// </summary>
@@ -98,6 +116,45 @@ public static bool ReadPointer(this IMemoryService memoryService, ulong address,
98116
return false;
99117
}
100118

119+
public static bool ReadPointer(this IMemoryService memoryService, ref ulong address, out ulong value)
120+
{
121+
bool ret = memoryService.ReadPointer(address, out value);
122+
if (ret)
123+
{
124+
address += (ulong)memoryService.PointerSize;
125+
}
126+
return ret;
127+
}
128+
129+
public static bool ReadAnsiString(this IMemoryService memoryService, uint maxLength, ulong address, out string value)
130+
{
131+
StringBuilder sb = new();
132+
byte[] buffer = new byte[maxLength];
133+
value = null;
134+
if (memoryService.ReadMemory(address, buffer, out int bytesRead) && bytesRead > 0)
135+
{
136+
// convert null terminated ANSI char array to a string
137+
for (int i = 0; i < buffer.Length; i++)
138+
{
139+
// Read the string one character at a time
140+
char c = (char)buffer[i];
141+
if (buffer[i] == 0) // Stop at null terminator
142+
{
143+
value = sb.ToString();
144+
break; // Stop reading at null terminator
145+
}
146+
if (c < 0x20 || c > 0x7E) // Unexpected characters
147+
{
148+
break;
149+
}
150+
// Append the character to the string
151+
sb.Append(c);
152+
}
153+
}
154+
return !string.IsNullOrEmpty(value);
155+
}
156+
157+
101158
/// <summary>
102159
/// Create a stream for all of memory.
103160
/// </summary>

0 commit comments

Comments
 (0)