|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection.Runtime.General;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Internal.Metadata.NativeFormat;
using Internal.NativeFormat;
using Internal.Runtime;
using Internal.Runtime.Augments;
using Internal.Runtime.TypeLoader;
using Internal.TypeSystem;
using Debug = System.Diagnostics.Debug;
using ReflectionExecution = Internal.Reflection.Execution.ReflectionExecution;
namespace Internal.StackTraceMetadata
{
/// <summary>
/// This helper class is used to resolve non-reflectable method names using a special
/// compiler-generated metadata blob to enhance quality of exception call stacks
/// in situations where symbol information is not available.
/// </summary>
internal static class StackTraceMetadata
{
/// <summary>
/// Module address-keyed map of per-module method name resolvers.
/// </summary>
private static PerModuleMethodNameResolverHashtable _perModuleMethodNameResolverHashtable;
/// <summary>
/// Eager startup initialization of stack trace metadata support creates
/// the per-module method name resolver hashtable and registers the runtime augment
/// for metadata-based stack trace resolution.
/// </summary>
internal static void Initialize()
{
_perModuleMethodNameResolverHashtable = new PerModuleMethodNameResolverHashtable();
RuntimeAugments.InitializeStackTraceMetadataSupport(new StackTraceMetadataCallbacksImpl());
}
[Intrinsic]
[AnalysisCharacteristic]
internal static extern bool StackTraceHiddenMetadataPresent();
/// <summary>
/// Locate the containing module for a method and try to resolve its name based on start address.
/// </summary>
public static unsafe string GetMethodNameFromStartAddressIfAvailable(IntPtr methodStartAddress, out string owningTypeName, out string genericArgs, out string methodSignature, out bool isStackTraceHidden, out int hashCodeForLineInfo)
{
IntPtr moduleStartAddress = RuntimeAugments.GetOSModuleFromPointer(methodStartAddress);
int rva = (int)((byte*)methodStartAddress - (byte*)moduleStartAddress);
foreach (NativeFormatModuleInfo moduleInfo in ModuleList.EnumerateModules())
{
if (moduleInfo.Handle.OsModuleBase == moduleStartAddress)
{
PerModuleMethodNameResolver resolver = _perModuleMethodNameResolverHashtable.GetOrCreateValue(moduleInfo.Handle.GetIntPtrUNSAFE());
if (resolver.TryGetStackTraceData(rva, out var data))
{
isStackTraceHidden = data.IsHidden;
if (data.OwningType.IsNil)
{
Debug.Assert(data.Name.IsNil && data.Signature.IsNil);
Debug.Assert(isStackTraceHidden);
owningTypeName = null;
genericArgs = null;
methodSignature = null;
hashCodeForLineInfo = 0;
return null;
}
(owningTypeName, genericArgs, string methodName, methodSignature) = MethodNameFormatter.FormatMethodName(resolver.Reader, data.OwningType, data.Name, data.Signature, data.GenericArguments);
hashCodeForLineInfo = (int)VersionResilientHashCode.CombineThreeValuesIntoHash((uint)data.OwningType.ToIntToken(), (uint)((Handle)data.Name).ToIntToken(), (uint)((Handle)data.Signature).ToIntToken());
return methodName;
}
}
}
isStackTraceHidden = false;
// We haven't found information in the stack trace metadata tables, but maybe reflection will have this
if (ReflectionExecution.TryGetMethodMetadataFromStartAddress(methodStartAddress,
out MetadataReader reader,
out TypeDefinitionHandle typeHandle,
out MethodHandle methodHandle))
{
if (StackTraceHiddenMetadataPresent())
{
foreach (CustomAttributeHandle cah in reader.GetTypeDefinition(typeHandle).CustomAttributes)
{
if (cah.IsCustomAttributeOfType(reader, ["System", "Diagnostics"], "StackTraceHiddenAttribute"))
{
isStackTraceHidden = true;
break;
}
}
foreach (CustomAttributeHandle cah in reader.GetMethod(methodHandle).CustomAttributes)
{
if (cah.IsCustomAttributeOfType(reader, ["System", "Diagnostics"], "StackTraceHiddenAttribute"))
{
isStackTraceHidden = true;
break;
}
}
}
(owningTypeName, genericArgs, string methodName, methodSignature) = MethodNameFormatter.FormatMethodName(reader, typeHandle, methodHandle);
hashCodeForLineInfo = (int)VersionResilientHashCode.CombineTwoValuesIntoHash((uint)((Handle)typeHandle).ToIntToken(), (uint)((Handle)methodHandle).ToIntToken());
return methodName;
}
owningTypeName = null;
genericArgs = null;
methodSignature = null;
hashCodeForLineInfo = 0;
return null;
}
private static unsafe (string, int) GetLineNumberInfo(IntPtr methodStartAddress, int offset, int hashCode)
{
foreach (NativeFormatModuleInfo module in ModuleList.EnumerateModules())
{
if (!module.TryFindBlob((int)ReflectionMapBlob.BlobIdStackTraceLineNumbers, out byte* lineNumbersBlob, out uint cbLineNumbersBlob)
|| !module.TryFindBlob((int)ReflectionMapBlob.BlobIdStackTraceDocuments, out byte* documentsBlob, out uint cbDocumentsBlob))
{
continue;
}
ExternalReferencesTable externalReferences = default(ExternalReferencesTable);
externalReferences.InitializeCommonFixupsTable(module);
var reader = new NativeReader(lineNumbersBlob, cbLineNumbersBlob);
var parser = new NativeParser(reader, 0);
var hashtable = new NativeHashtable(parser);
var lookup = hashtable.Lookup(hashCode);
NativeParser entryParser;
while (!(entryParser = lookup.GetNext()).IsNull)
{
IntPtr foundMethod = externalReferences.GetFunctionPointerFromIndex(entryParser.GetUnsigned());
if (foundMethod != methodStartAddress)
continue;
uint numEntries = entryParser.GetUnsigned();
int currentLineNumber = 0;
int currentNativeOffset = 0;
int documentIndex = entryParser.GetSigned();
for (uint i = 0; i < numEntries; i++)
{
int nativeOffsetDelta = entryParser.GetSigned();
int oldDocumentIndex = documentIndex;
if (i > 0 && nativeOffsetDelta == 0)
{
documentIndex = entryParser.GetSigned();
nativeOffsetDelta = entryParser.GetSigned();
}
if (currentNativeOffset + nativeOffsetDelta > offset)
{
documentIndex = oldDocumentIndex;
break;
}
int lineNumberDelta = entryParser.GetSigned();
currentNativeOffset += nativeOffsetDelta;
currentLineNumber += lineNumberDelta;
}
int documentOffset = ((int*)documentsBlob)[documentIndex];
byte* documentAddress = documentsBlob + documentOffset;
string documentName = System.Text.Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(documentAddress));
return (documentName, currentLineNumber);
}
}
return (null, 0);
}
public static unsafe DiagnosticMethodInfo? GetDiagnosticMethodInfoFromStartAddressIfAvailable(IntPtr methodStartAddress)
{
IntPtr moduleStartAddress = RuntimeAugments.GetOSModuleFromPointer(methodStartAddress);
int rva = (int)((byte*)methodStartAddress - (byte*)moduleStartAddress);
foreach (NativeFormatModuleInfo moduleInfo in ModuleList.EnumerateModules())
{
if (moduleInfo.Handle.OsModuleBase == moduleStartAddress)
{
PerModuleMethodNameResolver resolver = _perModuleMethodNameResolverHashtable.GetOrCreateValue(moduleInfo.Handle.GetIntPtrUNSAFE());
if (resolver.TryGetStackTraceData(rva, out var data))
{
if (data.OwningType.IsNil)
{
Debug.Assert(data.Name.IsNil && data.Signature.IsNil);
return null;
}
return new DiagnosticMethodInfo(
resolver.Reader.GetString(data.Name),
MethodNameFormatter.FormatReflectionNotationTypeName(resolver.Reader, data.OwningType),
FormatAssemblyName(resolver.Reader, data.OwningType)
);
}
}
}
// We haven't found information in the stack trace metadata tables, but maybe reflection will have this
if (ReflectionExecution.TryGetMethodMetadataFromStartAddress(methodStartAddress,
out MetadataReader reader,
out TypeDefinitionHandle typeHandle,
out MethodHandle methodHandle))
{
return new DiagnosticMethodInfo(
reader.GetString(reader.GetMethod(methodHandle).Name),
MethodNameFormatter.FormatReflectionNotationTypeName(reader, typeHandle),
FormatAssemblyName(reader, typeHandle)
);
}
return null;
}
private static string FormatAssemblyName(MetadataReader reader, Handle handle)
{
switch (handle.HandleType)
{
case HandleType.TypeDefinition:
TypeDefinition typeDef = reader.GetTypeDefinition(handle.ToTypeDefinitionHandle(reader));
TypeDefinitionHandle enclosingTypeDef = typeDef.EnclosingType;
if (!enclosingTypeDef.IsNil)
return FormatAssemblyName(reader, enclosingTypeDef);
return FormatAssemblyName(reader, typeDef.NamespaceDefinition);
case HandleType.TypeReference:
TypeReference typeRef = reader.GetTypeReference(handle.ToTypeReferenceHandle(reader));
return FormatAssemblyName(reader, typeRef.ParentNamespaceOrType);
case HandleType.TypeSpecification:
TypeSpecification typeSpec = reader.GetTypeSpecification(handle.ToTypeSpecificationHandle(reader));
return FormatAssemblyName(reader, typeSpec.Signature);
case HandleType.TypeInstantiationSignature:
TypeInstantiationSignature typeInst = reader.GetTypeInstantiationSignature(handle.ToTypeInstantiationSignatureHandle(reader));
return FormatAssemblyName(reader, typeInst.GenericType);
case HandleType.NamespaceDefinition:
NamespaceDefinition nsDef = reader.GetNamespaceDefinition(handle.ToNamespaceDefinitionHandle(reader));
return FormatAssemblyName(reader, nsDef.ParentScopeOrNamespace);
case HandleType.NamespaceReference:
NamespaceReference nsRef = reader.GetNamespaceReference(handle.ToNamespaceReferenceHandle(reader));
return FormatAssemblyName(reader, nsRef.ParentScopeOrNamespace);
case HandleType.ScopeDefinition:
return FormatAssemblyName(reader, handle.ToScopeDefinitionHandle(reader));
case HandleType.ScopeReference:
return FormatAssemblyName(reader, handle.ToScopeReferenceHandle(reader));
default:
return "<unknown>";
}
}
private static string FormatAssemblyName(MetadataReader reader, ScopeDefinitionHandle handle)
{
ScopeDefinition scopeDef = reader.GetScopeDefinition(handle);
return $"{reader.GetString(scopeDef.Name)}, Version={scopeDef.MajorVersion}.{scopeDef.MinorVersion}.{scopeDef.BuildNumber}.{scopeDef.RevisionNumber}";
}
private static string FormatAssemblyName(MetadataReader reader, ScopeReferenceHandle handle)
{
ScopeReference scopeRef = reader.GetScopeReference(handle);
return $"{reader.GetString(scopeRef.Name)}, Version={scopeRef.MajorVersion}.{scopeRef.MinorVersion}.{scopeRef.BuildNumber}.{scopeRef.RevisionNumber}";
}
/// <summary>
/// This hashtable supports mapping from module start addresses to per-module method name resolvers.
/// </summary>
private sealed class PerModuleMethodNameResolverHashtable : LockFreeReaderHashtable<IntPtr, PerModuleMethodNameResolver>
{
/// <summary>
/// Given a key, compute a hash code. This function must be thread safe.
/// </summary>
protected override int GetKeyHashCode(IntPtr key)
{
return key.GetHashCode();
}
/// <summary>
/// Given a value, compute a hash code which would be identical to the hash code
/// for a key which should look up this value. This function must be thread safe.
/// This function must also not cause additional hashtable adds.
/// </summary>
protected override int GetValueHashCode(PerModuleMethodNameResolver value)
{
return GetKeyHashCode(value.ModuleAddress);
}
/// <summary>
/// Compare a key and value. If the key refers to this value, return true.
/// This function must be thread safe.
/// </summary>
protected override bool CompareKeyToValue(IntPtr key, PerModuleMethodNameResolver value)
{
return key == value.ModuleAddress;
}
/// <summary>
/// Compare a value with another value. Return true if values are equal.
/// This function must be thread safe.
/// </summary>
protected override bool CompareValueToValue(PerModuleMethodNameResolver value1, PerModuleMethodNameResolver value2)
{
return value1.ModuleAddress == value2.ModuleAddress;
}
/// <summary>
/// Create a new value from a key. Must be threadsafe. Value may or may not be added
/// to collection. Return value must not be null.
/// </summary>
protected override PerModuleMethodNameResolver CreateValueFromKey(IntPtr key)
{
return new PerModuleMethodNameResolver(key);
}
}
/// <summary>
/// Implementation of stack trace metadata callbacks.
/// </summary>
private sealed class StackTraceMetadataCallbacksImpl : StackTraceMetadataCallbacks
{
public override DiagnosticMethodInfo TryGetDiagnosticMethodInfoFromStartAddress(nint methodStartAddress)
{
return GetDiagnosticMethodInfoFromStartAddressIfAvailable(methodStartAddress);
}
public override string TryGetMethodStackFrameInfo(IntPtr methodStartAddress, int offset, bool needsFileInfo, out string owningType, out string genericArgs, out string methodSignature, out bool isStackTraceHidden, out string fileName, out int lineNumber)
{
string methodName = GetMethodNameFromStartAddressIfAvailable(methodStartAddress, out owningType, out genericArgs, out methodSignature, out isStackTraceHidden, out int hashCode);
if (needsFileInfo)
{
(fileName, lineNumber) = GetLineNumberInfo(methodStartAddress, offset, hashCode);
}
else
{
fileName = null;
lineNumber = 0;
}
return methodName;
}
}
/// <summary>
/// Method name resolver for a single binary module
/// </summary>
private sealed class PerModuleMethodNameResolver
{
/// <summary>
/// Start address of the module in question.
/// </summary>
private readonly IntPtr _moduleAddress;
/// <summary>
/// Dictionary mapping method RVA's to tokens within the metadata blob.
/// </summary>
private readonly StackTraceData[] _stacktraceDatas;
/// <summary>
/// Metadata reader for the stack trace metadata.
/// </summary>
public readonly MetadataReader Reader;
/// <summary>
/// Publicly exposed module address property.
/// </summary>
public IntPtr ModuleAddress { get { return _moduleAddress; } }
/// <summary>
/// Construct the per-module resolver by looking up the necessary blobs.
/// </summary>
public unsafe PerModuleMethodNameResolver(IntPtr moduleAddress)
{
_moduleAddress = moduleAddress;
TypeManagerHandle handle = new TypeManagerHandle(moduleAddress);
ModuleInfo moduleInfo;
if (!ModuleList.Instance.TryGetModuleInfoByHandle(handle, out moduleInfo))
{
// Module not found
return;
}
NativeFormatModuleInfo nativeFormatModuleInfo = moduleInfo as NativeFormatModuleInfo;
if (nativeFormatModuleInfo == null)
{
// It is not a native format module
return;
}
byte *metadataBlob;
uint metadataBlobSize;
byte *rvaToTokenMapBlob;
uint rvaToTokenMapBlobSize;
if (nativeFormatModuleInfo.TryFindBlob(
(int)ReflectionMapBlob.EmbeddedMetadata,
out metadataBlob,
out metadataBlobSize) &&
nativeFormatModuleInfo.TryFindBlob(
(int)ReflectionMapBlob.BlobIdStackTraceMethodRvaToTokenMapping,
out rvaToTokenMapBlob,
out rvaToTokenMapBlobSize))
{
Reader = new MetadataReader(new IntPtr(metadataBlob), (int)metadataBlobSize);
int entryCount = *(int*)rvaToTokenMapBlob;
_stacktraceDatas = new StackTraceData[entryCount];
PopulateRvaToTokenMap(handle, rvaToTokenMapBlob + sizeof(int), rvaToTokenMapBlobSize - sizeof(int));
}
}
/// <summary>
/// Construct the dictionary mapping method RVAs to stack trace metadata tokens
/// within a single binary module.
/// </summary>
/// <param name="handle">Module to use to construct the mapping</param>
/// <param name="pMap">List of RVA - token pairs</param>
/// <param name="length">Length of the blob</param>
private unsafe void PopulateRvaToTokenMap(TypeManagerHandle handle, byte* pMap, uint length)
{
Handle currentOwningType = default;
MethodSignatureHandle currentSignature = default;
ConstantStringValueHandle currentName = default;
ConstantStringArrayHandle currentMethodInst = default;
int current = 0;
byte* pCurrent = pMap;
while (pCurrent < pMap + length)
{
byte command = *pCurrent++;
if ((command & StackTraceDataCommand.UpdateOwningType) != 0)
{
currentOwningType = Handle.FromIntToken((int)NativePrimitiveDecoder.ReadUInt32(ref pCurrent));
Debug.Assert((command & StackTraceDataCommand.IsStackTraceHidden) != 0 ||
currentOwningType.HandleType is HandleType.TypeDefinition or HandleType.TypeReference or HandleType.TypeSpecification);
}
if ((command & StackTraceDataCommand.UpdateName) != 0)
{
currentName = new Handle(HandleType.ConstantStringValue, (int)NativePrimitiveDecoder.DecodeUnsigned(ref pCurrent)).ToConstantStringValueHandle(Reader);
}
if ((command & StackTraceDataCommand.UpdateSignature) != 0)
{
currentSignature = new Handle(HandleType.MethodSignature, (int)NativePrimitiveDecoder.DecodeUnsigned(ref pCurrent)).ToMethodSignatureHandle(Reader);
currentMethodInst = default;
}
if ((command & StackTraceDataCommand.UpdateGenericSignature) != 0)
{
currentSignature = new Handle(HandleType.MethodSignature, (int)NativePrimitiveDecoder.DecodeUnsigned(ref pCurrent)).ToMethodSignatureHandle(Reader);
currentMethodInst = new Handle(HandleType.ConstantStringArray, (int)NativePrimitiveDecoder.DecodeUnsigned(ref pCurrent)).ToConstantStringArrayHandle(Reader);
}
void* pMethod = ReadRelPtr32(pCurrent);
pCurrent += sizeof(int);
Debug.Assert((nint)pMethod > handle.OsModuleBase);
int methodRva = (int)((nint)pMethod - handle.OsModuleBase);
_stacktraceDatas[current++] = new StackTraceData
{
Rva = methodRva,
IsHidden = (command & StackTraceDataCommand.IsStackTraceHidden) != 0,
OwningType = currentOwningType,
Name = currentName,
Signature = currentSignature,
GenericArguments = currentMethodInst,
};
static void* ReadRelPtr32(byte* address)
=> address + *(int*)address;
}
Debug.Assert(current == _stacktraceDatas.Length);
Array.Sort(_stacktraceDatas);
}
/// <summary>
/// Try to resolve method name based on its address using the stack trace metadata
/// </summary>
public bool TryGetStackTraceData(int rva, out StackTraceData data)
{
if (_stacktraceDatas == null)
{
// No stack trace metadata for this module
data = default;
return false;
}
int index = Array.BinarySearch(_stacktraceDatas, new StackTraceData() { Rva = rva });
if (index < 0)
{
// Method RVA not found in the map
data = default;
return false;
}
data = _stacktraceDatas[index];
return true;
}
public struct StackTraceData : IComparable<StackTraceData>
{
private const int IsHiddenFlag = 0x2;
private readonly int _rvaAndIsHiddenBit;
public int Rva
{
get => _rvaAndIsHiddenBit & ~IsHiddenFlag;
init
{
Debug.Assert((value & IsHiddenFlag) == 0);
_rvaAndIsHiddenBit = value | (_rvaAndIsHiddenBit & IsHiddenFlag);
}
}
public bool IsHidden
{
get => (_rvaAndIsHiddenBit & IsHiddenFlag) != 0;
init
{
if (value)
_rvaAndIsHiddenBit |= IsHiddenFlag;
}
}
public Handle OwningType { get; init; }
public ConstantStringValueHandle Name { get; init; }
public MethodSignatureHandle Signature { get; init; }
public ConstantStringArrayHandle GenericArguments { get; init; }
public int CompareTo(StackTraceData other) => Rva.CompareTo(other.Rva);
}
}
}
}
|