143 строки
11 KiB
C#
143 строки
11 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License - see LICENSE file in this repo.
|
|
namespace Microsoft.SqlServer.Utils.Misc.SQLCallStackResolver {
|
|
public static class ModuleInfoHelper {
|
|
private static readonly Regex rgxPDBName = new(@"^(?<pdb>.+)(\.pdb)$", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
|
|
private static readonly Regex rgxFileName = new(@"^(?<module>.+)\.(dll|exe)$", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
|
|
|
|
/// <summary>
|
|
/// Parse the input and return a set of resolved Symbol objects
|
|
/// </summary>
|
|
public async static Task<Dictionary<string, Symbol>> ParseModuleInfoAsync(List<StackDetails> listOfCallStacks, CancellationTokenSource cts) {
|
|
var syms = new Dictionary<string, Symbol>();
|
|
bool anyTaskFailed = false;
|
|
await Task.Run(() => Parallel.ForEach(listOfCallStacks.Where(c => c.Callstack.Contains(",")).Select(c => c.CallstackFrames), lines => {
|
|
if (cts.IsCancellationRequested) return;
|
|
Contract.Requires(lines.Length > 0);
|
|
foreach (var line in lines) {
|
|
if (cts.IsCancellationRequested) return;
|
|
string moduleName = null, pdbName = null;
|
|
|
|
// foreach line, split into comma-delimited fields
|
|
var fields = line.Split(',');
|
|
// only attempt to process further if this line does look like it has delimited fields
|
|
if (fields.Length >= 3) {
|
|
Guid pdbGuid = Guid.Empty;
|
|
foreach (var field in fields.Select(f => f.Trim().TrimEnd('"').TrimStart('"'))) {
|
|
// for each field, attempt using regexes to detect file name and GUIDs
|
|
if (Guid.TryParse(field, out Guid tmpGuid)) pdbGuid = tmpGuid;
|
|
if (string.IsNullOrEmpty(moduleName)) {
|
|
var matchFilename = rgxFileName.Match(field);
|
|
if (matchFilename.Success) moduleName = matchFilename.Groups["module"].Value;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(pdbName)) {
|
|
var matchPDBName = rgxPDBName.Match(field);
|
|
if (matchPDBName.Success) pdbName = matchPDBName.Groups["pdb"].Value;
|
|
}
|
|
}
|
|
|
|
_ = int.TryParse(fields[fields.Length - 1], out int pdbAge); // assumption is that last field is pdbAge
|
|
pdbName = string.IsNullOrEmpty(pdbName) ? moduleName : pdbName; // fall back to module name as PDB name
|
|
|
|
// check if we have all 3 details
|
|
if (!string.IsNullOrEmpty(pdbName) && pdbAge != int.MinValue && pdbGuid != Guid.Empty) {
|
|
lock (syms) {
|
|
if (!syms.TryGetValue(moduleName, out Symbol existingSym)) syms.Add(moduleName, new Symbol() { ModuleName = moduleName, PDBName = pdbName + ".pdb", PDBAge = pdbAge, PDBGuid = pdbGuid.ToString("N") });
|
|
else if (!(existingSym.ModuleName == moduleName && existingSym.PDBName == pdbName + ".pdb" && existingSym.PDBAge == pdbAge && existingSym.PDBGuid == pdbGuid.ToString("N"))) {
|
|
anyTaskFailed = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
return cts.IsCancellationRequested ? new Dictionary<string, Symbol>() : (anyTaskFailed ? null : syms);
|
|
}
|
|
|
|
public async static Task<(Dictionary<string, Symbol>, List<StackDetails>)> ParseModuleInfoXMLAsync(List<StackDetails> listOfCallStacks, CancellationTokenSource cts) {
|
|
var syms = new Dictionary<string, Symbol>();
|
|
bool anyTaskFailed = false;
|
|
await Task.Run(() => Parallel.ForEach(listOfCallStacks, currItem => {
|
|
var latestMappedModuleNames = new Dictionary<string, string>();
|
|
if (cts.IsCancellationRequested) return;
|
|
var outCallstack = new StringBuilder();
|
|
// sniff test to allow for quick exit if input has no XML at all
|
|
if (currItem.Callstack.Contains("<frame")) {
|
|
// use a multi-line regex replace to reassemble XML fragments which were split across lines
|
|
currItem.Callstack = Regex.Replace(currItem.Callstack, @"(?<prefix>\<frame[^\/\>]*?)(?<newline>(\r\n|\n))(?<suffix>.*?\/\>\s*?$)", @"${prefix}${suffix}", RegexOptions.Multiline);
|
|
if (cts.IsCancellationRequested) return;
|
|
// ensure that each <frame> element starts on a newline
|
|
currItem.Callstack = Regex.Replace(currItem.Callstack, @"/\>\s*\<frame", "/>\r\n<frame");
|
|
if (cts.IsCancellationRequested) return;
|
|
// next, replace any pre-post stuff from the XML frame lines
|
|
currItem.Callstack = Regex.Replace(currItem.Callstack, @"(?<prefix>.*?)(?<retain>\<frame.+\/\>)(?<suffix>.*?)", "${retain}");
|
|
if (cts.IsCancellationRequested) return;
|
|
|
|
foreach (var line in currItem.Callstack.Split('\n')) {
|
|
if (cts.IsCancellationRequested) return;
|
|
if (!string.IsNullOrWhiteSpace(line) && line.StartsWith("<frame")) { // only attempt further formal XML parsing if a simple text check works
|
|
try {
|
|
using var sreader = new StringReader(line);
|
|
using var reader = XmlReader.Create(sreader, new XmlReaderSettings() { XmlResolver = null });
|
|
if (reader.Read()) {
|
|
// seems to be XML; process attributes only if all 3 are there
|
|
var moduleNameAttributeVal = reader.GetAttribute("module");
|
|
if (string.IsNullOrEmpty(moduleNameAttributeVal)) moduleNameAttributeVal = reader.GetAttribute("name");
|
|
var moduleName = Path.GetFileNameWithoutExtension(moduleNameAttributeVal);
|
|
var addressAttributeVal = reader.GetAttribute("address");
|
|
ulong addressIfPresent = string.IsNullOrEmpty(addressAttributeVal) ? ulong.MinValue : Convert.ToUInt64(addressAttributeVal, 16);
|
|
var rvaAttributeVal = reader.GetAttribute("rva");
|
|
ulong rvaIfPresent = string.IsNullOrEmpty(rvaAttributeVal) ? ulong.MinValue : Convert.ToUInt64(rvaAttributeVal, 16);
|
|
ulong calcBaseAddress = ulong.MinValue;
|
|
if (rvaIfPresent != ulong.MinValue && addressIfPresent != ulong.MinValue) calcBaseAddress = addressIfPresent - rvaIfPresent;
|
|
var pdbGuid = reader.GetAttribute("guid");
|
|
var pdbAge = reader.GetAttribute("age");
|
|
string uniqueModuleName;
|
|
// createe a map of the last mapped module names to handle cases when the frame is "truncated" and the above PDB details are not available
|
|
if (pdbGuid != null && pdbAge != null) {
|
|
uniqueModuleName = $"{pdbGuid.Replace("-", string.Empty).ToUpper()}{pdbAge}";
|
|
if (latestMappedModuleNames.ContainsKey(moduleName)) latestMappedModuleNames[moduleName] = uniqueModuleName;
|
|
else latestMappedModuleNames.Add(moduleName, uniqueModuleName);
|
|
} else {
|
|
if (!latestMappedModuleNames.TryGetValue(moduleName, out uniqueModuleName)) {
|
|
anyTaskFailed = true;
|
|
return;
|
|
}
|
|
}
|
|
lock (syms) {
|
|
if (syms.TryGetValue(uniqueModuleName, out var existingEntry)) {
|
|
if (ulong.MinValue == existingEntry.CalculatedModuleBaseAddress) existingEntry.CalculatedModuleBaseAddress = calcBaseAddress;
|
|
} else syms.Add(uniqueModuleName, new Symbol() { PDBName = reader.GetAttribute("pdb").ToLower(), ModuleName = moduleName, PDBAge = int.Parse(pdbAge), PDBGuid = Guid.Parse(pdbGuid).ToString("N").ToUpper(), CalculatedModuleBaseAddress = calcBaseAddress });
|
|
}
|
|
string rvaAsIsOrDerived = null;
|
|
if (ulong.MinValue != rvaIfPresent) rvaAsIsOrDerived = rvaAttributeVal;
|
|
else if (ulong.MinValue != addressIfPresent && ulong.MinValue != syms[uniqueModuleName].CalculatedModuleBaseAddress)
|
|
rvaAsIsOrDerived = "0x" + (addressIfPresent - syms[uniqueModuleName].CalculatedModuleBaseAddress).ToString("X");
|
|
|
|
if (string.IsNullOrEmpty(rvaAsIsOrDerived)) { throw new NullReferenceException(); }
|
|
|
|
var frameNumHex = string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:x2}", int.Parse(reader.GetAttribute("id")));
|
|
// transform the XML into a simple module+offset notation
|
|
outCallstack.AppendFormat($"{frameNumHex} {uniqueModuleName}+{rvaAsIsOrDerived}{Environment.NewLine}");
|
|
continue;
|
|
}
|
|
} catch (Exception ex) {
|
|
if (!(ex is ArgumentNullException || ex is NullReferenceException || ex is XmlException || ex is FormatException)) { throw; }
|
|
}
|
|
}
|
|
|
|
// pass-through this line as it is either non-XML, 0-length or whitespace-only
|
|
outCallstack.AppendLine(line);
|
|
}
|
|
currItem.Callstack = outCallstack.ToString();
|
|
}
|
|
}));
|
|
|
|
return cts.IsCancellationRequested ? (new Dictionary<string, Symbol>(), new List<StackDetails>()) : (anyTaskFailed ? null : syms, listOfCallStacks);
|
|
}
|
|
}
|
|
}
|