using System; using System.Collections.Generic; using System.ComponentModel; // For Win32Excption; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using Symbols; using Dia2Lib; using Address = System.UInt64; using Utilities; using System.Reflection; namespace Symbols { /// /// A symbol reader represents something that can FIND pdbs (either on a symbol server or via a symbol path) /// Its job is to find a full path a PDB. Then you can use OpenSymbolFile to get a SymbolReaderModule and do more. /// public unsafe class SymbolReader : IDisposable { /// /// Opens a new SymbolReader. All diagnostics messages about symbol lookup go to 'log'. /// public SymbolReader(TextWriter log, string nt_symbol_path = null) { SymbolPath = nt_symbol_path; if (SymbolPath == null) SymbolPath = SymPath._NT_SYMBOL_PATH; log.WriteLine("Created SymbolReader with SymbolPath {0}", nt_symbol_path); // TODO FIX NOW. the code below does not support probing a file extension directory. // we work around this by adding more things to the symbol path var symPath = new SymPath(SymbolPath); var newSymPath = new SymPath(); foreach (var symElem in symPath.Elements) { newSymPath.Add(symElem); if (!symElem.IsSymServer) { var probe = Path.Combine(symElem.Target, "dll"); if (Directory.Exists(probe)) newSymPath.Add(probe); probe = Path.Combine(symElem.Target, "exe"); if (Directory.Exists(probe)) newSymPath.Add(probe); } } var newSymPathStr = newSymPath.ToString(); // log.WriteLine("Morphed Symbol Path: {0}", newSymPathStr); this.m_log = log; SymbolReaderNativeMethods.SymOptions options = SymbolReaderNativeMethods.SymGetOptions(); SymbolReaderNativeMethods.SymSetOptions( SymbolReaderNativeMethods.SymOptions.SYMOPT_DEBUG | // SymbolReaderNativeMethods.SymOptions.SYMOPT_DEFERRED_LOADS | SymbolReaderNativeMethods.SymOptions.SYMOPT_LOAD_LINES | SymbolReaderNativeMethods.SymOptions.SYMOPT_EXACT_SYMBOLS | SymbolReaderNativeMethods.SymOptions.SYMOPT_UNDNAME ); m_currentProcess = Process.GetCurrentProcess(); // Only here to insure processHandle does not die. TODO get on safeHandles. m_currentProcessHandle = m_currentProcess.Handle; bool success = SymbolReaderNativeMethods.SymInitializeW(m_currentProcessHandle, newSymPathStr, false); if (!success) { // This captures the GetLastEvent (and has to happen before calling CloseHandle() m_currentProcessHandle = IntPtr.Zero; throw new Win32Exception(); } m_callback = new SymbolReaderNativeMethods.SymRegisterCallbackProc(this.StatusCallback); success = SymbolReaderNativeMethods.SymRegisterCallbackW64(m_currentProcessHandle, m_callback, 0); Debug.Assert(success); } // These routines find a PDB based on something (either an DLL or a pdb 'signature') /// /// Finds the symbol file for 'exeFilePath' that exists on the current machine (we open /// it to find the needed info). Uses the SymbolReader.SymbolPath (including Symbol servers) to /// look up the PDB, and will download the PDB to the local cache if necessary. /// /// This routine looks in the EXEFile to find the PDB Guid signature and then calls FindSymbolFilePath /// /// returns null if the pdb can't be found. /// public string FindSymbolFilePathForModule(string dllFilePath) { Debug.Assert(!IsDisposed); try { dllFilePath = BypassSystem32FileRedirection(dllFilePath); if (File.Exists(dllFilePath)) { using (var peFile = new PEFile.PEFile(dllFilePath)) { string pdbName; Guid pdbGuid; int pdbAge; // TODO we get the NGEN pdb if we can. Is this what we want in general? if (peFile.GetPdbSignature(out pdbName, out pdbGuid, out pdbAge, true)) { string fileVersionString = null; var fileVersion = peFile.GetFileVersionInfo(); if (fileVersion != null) fileVersionString = fileVersion.FileVersion; // TODO FIX NOW should this be here? m_log.WriteLine("Exe {0} has pdb {1} GUID {2} age {3}", dllFilePath, pdbName, pdbGuid, pdbAge); return FindSymbolFilePath(pdbName, pdbGuid, pdbAge, dllFilePath, fileVersionString); } else m_log.WriteLine("File does not have a codeview debug signature."); } } else m_log.WriteLine("File does not exist."); } catch (Exception e) { m_log.WriteLine("Failure opening PE file: {0}", e.Message); } m_log.WriteLine("[Failed to find PDB file for {0}]", dllFilePath); return null; } /// /// Find the complete PDB path, given just the simple name (filename + pdb extension) as well as its 'signature', /// which uniquely identifies it (on symbol servers). Uses the SymbolReader.SymbolPath (including Symbol servers) to /// look up the PDB, and will download the PDB to the local cache if necessary. /// /// A Guid of 0, means 'unknown' and will match the first PDB that matches simple name. Thus it is unsafe. /// /// Returns null if the PDB could not be found /// /// /// The name of the PDB file (we only use the file name part) /// The GUID that is embedded in the DLL in the debug information that allows matching the DLL and the PDB /// Tools like BBT transform a DLL into another DLL (with the same GUID) the 'pdbAge' is a small integers /// that indicates how many transformations were done /// If you know the path to the DLL for this pdb add it here. That way we can probe next to the DLL /// for the PDB file. /// This is an optional string that identifies the file version (the 'Version' resource information. /// It is used only to provided better error messages for the log. public string FindSymbolFilePath(string pdbSimpleName, Guid pdbIndexGuid, int pdbIndexAge, string dllFilePath = null, string fileVersion = "") { Debug.Assert(!IsDisposed); SymbolReaderNativeMethods.SymFindFileInPathProc FindSymbolFileCallBack = delegate(string fileName, IntPtr context) { Debug.Assert(context == IntPtr.Zero); Guid fileGuid = Guid.Empty; int fileAge = 0; int dummy = 0; if (!SymbolReaderNativeMethods.SymSrvGetFileIndexesW(fileName, ref fileGuid, ref fileAge, ref dummy, 0)) { m_log.WriteLine("Failed to look up PDB signature for {0}.", fileName); return true; // continue searching. } bool matched = (pdbIndexGuid == fileGuid && pdbIndexAge == fileAge); if (!matched) { if (pdbIndexGuid == Guid.Empty) { m_log.WriteLine("No PDB Guid provided, assuming an unsafe PDB match for {0}", fileName); matched = true; } else m_log.WriteLine("PDB File {0} has Guid {1} age {2} != Desired Guid {3} age {4}", fileName, fileGuid, fileAge, pdbIndexGuid, pdbIndexAge); } return !matched; // you return false when you match, true to continue searching }; StringBuilder pdbFullPath = new StringBuilder(260); bool foundPDB = SymbolReaderNativeMethods.SymFindFileInPathW(m_currentProcessHandle, null, // Search path pdbSimpleName, ref pdbIndexGuid, // ID (&GUID) pdbIndexAge, // ID 2 0, // ID 3 SymbolReaderNativeMethods.SSRVOPT_GUIDPTR, // Flags pdbFullPath, // output FilePath FindSymbolFileCallBack, IntPtr.Zero); // Context for callback string pdbPath = null; if (foundPDB) { pdbPath = pdbFullPath.ToString(); goto Success; } // TODO is ONLY looking in the cache the right policy? Hmmm... if ((Flags & SymbolReaderFlags.CacheOnly) == 0) { // We check these last because they may be hostile PDBs and we have to ask the user about them. if (dllFilePath != null) // Check next to the file. { string pdbPathCandidate = Path.ChangeExtension(dllFilePath, ".pdb"); // Also try the symbols.pri\retail\dll convention that windows and devdiv use if (!File.Exists(pdbPathCandidate)) pdbPathCandidate = Path.Combine( Path.GetDirectoryName(dllFilePath), @"symbols.pri\retail\dll\" + Path.GetFileNameWithoutExtension(dllFilePath) + ".pdb"); if (File.Exists(pdbPathCandidate)) { if (!FindSymbolFileCallBack(pdbPathCandidate, IntPtr.Zero)) { if (CheckSecurity(pdbPathCandidate)) { pdbPath = pdbPathCandidate; goto Success; } } } } // If the pdbPath is a full path, see if it exists if (pdbSimpleName.IndexOf('\\') > 0 && File.Exists(pdbSimpleName)) { if (!FindSymbolFileCallBack(pdbSimpleName, IntPtr.Zero)) { if (CheckSecurity(pdbSimpleName)) { pdbPath = pdbSimpleName; goto Success; } } } // TODO does this belong here? m_log.WriteLine("Last chance, looking for PDB in private builds."); pdbPath = FindPdbInPrivateBuilds(pdbSimpleName, fileVersion, FindSymbolFileCallBack); if (pdbPath != null) goto Success; } string where = ""; if ((Flags & SymbolReaderFlags.CacheOnly) != 0) where = " in local cache"; m_log.WriteLine("Failed to find PDB {0}{1}.\r\n GUID {2} Age {3} Version {4}", pdbSimpleName, where, pdbIndexGuid, pdbIndexAge, fileVersion); return null; Success: m_log.WriteLine("Successfully found PDB {0}\r\n GUID {1} Age {2} Version {3}", pdbPath, pdbIndexGuid, pdbIndexAge, fileVersion); // If the PDB is on a network share, copy it to the local sym return CacheFileLocally(pdbPath, pdbIndexGuid, pdbIndexAge); } // Once you have a file path to a PDB file, you can open it with this method /// /// Given the path name to a particular PDB file, load it so that you can resolve symbols in it. /// /// The name of the PDB file to open. /// The SymbolReaderModule that represents the information in the symbol file (PDB) public SymbolModule OpenSymbolFile(string symbolFilePath) { Debug.Assert(!IsDisposed); var ret = new SymbolModule(this, symbolFilePath); return ret; } // Various state that controls symbol and source file lookup. /// /// The symbol path used to look up PDB symbol files. Set when the reader is initialized. /// public string SymbolPath { get; set; } /// /// The paths used to look up source files. defaults to _NT_SOURCE_PATH. /// public string SourcePath { get { if (m_SourcePath == null) { m_SourcePath = Environment.GetEnvironmentVariable("_NT_SOURCE_PATH"); if (m_SourcePath == null) m_SourcePath = ""; } return m_SourcePath; } set { m_SourcePath = value; m_parsedSourcePath = null; } } /// /// Where symbols are downloaded if needed. Derived from symbol path /// public string SymbolCacheDirectory { get { if (m_SymbolCacheDirectory == null) m_SymbolCacheDirectory = new SymPath(SymbolPath).DefaultSymbolCache; return m_SymbolCacheDirectory; } set { m_SymbolCacheDirectory = value; } } /// /// The place where source is downloaded from a source server. /// public string SourceCacheDirectory { get { if (m_SourceCacheDirectory == null) m_SourceCacheDirectory = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), "SrcCache"); return m_SourceCacheDirectory; } set { m_SourceCacheDirectory = value; } } /// /// Is this symbol reader limited to just the local machine cache or not? /// public SymbolReaderFlags Flags { get; set; } /// /// Cache even the unsafe pdbs to the SymbolCacheDirectory. TODO: is this a hack? /// public bool CacheUnsafePdbs; /// /// We call back on this when we find a PDB by probing in 'unsafe' locations (like next to the EXE or in the Built location) /// If this function returns true, we assume that it is OK to use the PDB. /// public Func SecurityCheck { get; set; } /// /// A place to log additional messages /// public TextWriter Log { get { return m_log; } } /// /// Note that all SymbolReaderModules returned by 'OpenSymbolFile' become invalid after disposing of the SymbolReader. /// /// public void Dispose() { if (m_currentProcessHandle != IntPtr.Zero) { // Can't do this in the finalizer as the handle may not be valid then. SymbolReaderNativeMethods.SymCleanup(m_currentProcessHandle); m_currentProcessHandle = IntPtr.Zero; m_currentProcess.Close(); m_currentProcess = null; } } public bool IsDisposed { get { return m_currentProcess == null; } } ~SymbolReader() { // TODO FIX NOW, need to call SymCleanup, but I need m_currentProcessHandle to be valid. } #region private private string m_SourcePath; internal List ParsedSourcePath { get { if (m_parsedSourcePath == null) { m_parsedSourcePath = new List(); foreach (var path in SourcePath.Split(';')) { var normalizedPath = path.Trim(); if (normalizedPath.EndsWith(@"\")) normalizedPath = normalizedPath.Substring(0, normalizedPath.Length - 1); if (Directory.Exists(normalizedPath)) m_parsedSourcePath.Add(normalizedPath); else m_log.WriteLine("Path {0} in source path does not exist, skipping.", normalizedPath); } } return m_parsedSourcePath; } } internal List m_parsedSourcePath; private bool CheckSecurity(string pdbName) { if (SecurityCheck == null) { m_log.WriteLine("Found PDB {0}, however this is in an unsafe location.", pdbName); m_log.WriteLine("If you trust this location, place this directory the symbol path to correct this."); return false; } if (!SecurityCheck(pdbName)) { m_log.WriteLine("Found PDB {0}, but failed securty check.", pdbName); return false; } return true; } /// /// This is an optional routine. It is already the case that if you find a PDB on a symbol server /// that it will be cached locally, however if you find it on a network path by NOT using a symbol /// server, it will be used in place. This is annoying, and this routine makes up for this by /// mimicking this behavior. Basically if pdbPath is not a local file name, it will copy it to /// the local symbol cache and return the local path. /// private string CacheFileLocally(string pdbPath, Guid pdbGuid, int pdbAge) { try { var fileName = Path.GetFileName(pdbPath); // Use SymSrv conventions in the cache if the Guid is non-zero, otherwise we simply place it in the cache. var localPdbDir = SymbolCacheDirectory; if (pdbGuid != Guid.Empty) { var pdbPathPrefix = Path.Combine(SymbolCacheDirectory, fileName); // There is a non-trivial possibility that someone puts a FILE that is named what we want the dir to be. if (File.Exists(pdbPathPrefix)) { // If the pdb path happens to be the SymbolCacheDir (a definate possibility) then we would // clobber the source file in our attempt to set up the target. In this case just give up // and leave the file as it was. if (string.Compare(pdbPath, pdbPathPrefix, StringComparison.OrdinalIgnoreCase) == 0) return pdbPath; m_log.WriteLine("Removing file {0} from symbol cache to make way for symsrv files.", pdbPathPrefix); File.Delete(pdbPathPrefix); } localPdbDir = Path.Combine(pdbPathPrefix, pdbGuid.ToString("N") + pdbAge.ToString()); } else if (!CacheUnsafePdbs) return pdbPath; if (!Directory.Exists(localPdbDir)) Directory.CreateDirectory(localPdbDir); var localPdbPath = Path.Combine(localPdbDir, fileName); var fileExists = File.Exists(localPdbPath); if (!fileExists || File.GetLastWriteTimeUtc(localPdbPath) != File.GetLastWriteTimeUtc(pdbPath)) { if (fileExists) m_log.WriteLine("WARNING: overwriting existing file {0}.", localPdbPath); m_log.WriteLine("Copying {0} to local cache {1}", pdbPath, localPdbPath); File.Copy(pdbPath, localPdbPath, true); } return localPdbPath; } catch (Exception e) { m_log.WriteLine("Error trying to update local PDB cache {0}", e.Message); } return pdbPath; } private bool StatusCallback( IntPtr hProcess, SymbolReaderNativeMethods.SymCallbackActions ActionCode, ulong UserData, ulong UserContext) { bool ret = false; switch (ActionCode) { case SymbolReaderNativeMethods.SymCallbackActions.CBA_SRCSRV_INFO: case SymbolReaderNativeMethods.SymCallbackActions.CBA_DEBUG_INFO: var line = new String((char*)UserData).Trim(); m_log.WriteLine(Regex.Replace(line, @"\p{C}+", String.Empty)); ret = true; break; default: // messages.Append("STATUS: Code=").Append(ActionCode).AppendLine(); break; } return ret; } /// /// We may be a 32 bit app which has File system redirection turned on /// Morph System32 to SysNative in that case to bypass file system redirection /// internal string BypassSystem32FileRedirection(string path) { var winDir = Environment.GetEnvironmentVariable("WinDir"); if (winDir != null) { var system32 = Path.Combine(winDir, "System32"); if (path.StartsWith(system32, StringComparison.OrdinalIgnoreCase)) { if (Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432") != null) { var sysNative = Path.Combine(winDir, "Sysnative"); var newPath = Path.Combine(sysNative, path.Substring(system32.Length + 1)); if (File.Exists(newPath)) path = newPath; } } } return path; } private static bool? m_WinBuildsExist; private static bool WinBuildsExist { get { if (!m_WinBuildsExist.HasValue) m_WinBuildsExist = SymPath.ComputerNameExists("winbuilds"); return m_WinBuildsExist.Value; } } private static bool? m_CpvsbuildExist; private static bool CpvsbuildExist { get { if (!m_CpvsbuildExist.HasValue) m_CpvsbuildExist = SymPath.ComputerNameExists("cpvsbuild"); return m_CpvsbuildExist.Value; } } /// /// This function is useful only internally in Microsoft. It is useful for builds that are not /// published on the symbol server. It knows about windows conventions as well as devdiv conventions /// private string FindPrivateBuildPdbSearchPath(string fileVersion, string arch) { // See if it is a windows version style // FileVersion 6.2.7996.0 (fbl_pac_dev2.110504-2214) // \\winbuilds\release\FBL_PAC_DEV2\7996.0.110504-2214\x86fre\bin\Indexes // \\winbuilds\release\FBL_PAC_DEV2\7996.0.110504-2214\x86fre\symbols.pri\retail\dll Match m = Regex.Match(fileVersion, @"(\d+)\.(\d+)\.(\d+)\.(\d+).*\((\w+)\.([\d-]+)\)"); if (m.Success && !fileVersion.Contains("win7_")) { if (!WinBuildsExist) { m_log.WriteLine("WinBuilds for arch {0} not accessable. No private build lookup done.", arch); return null; } var dailyNum = m.Groups[3].Value; var rebuildNum = m.Groups[4].Value; var branch = m.Groups[5].Value; var time = m.Groups[6].Value; var buildType = "fre"; var dropPath = string.Format(@"\\winbuilds\release\{0}\{1}.{2}.{3}\{4}{5}", branch, dailyNum, rebuildNum, time, arch, buildType); if (!Directory.Exists(dropPath)) { m_log.WriteLine("Windows Drop Path {0} does not exist", dropPath); return null; } /* var symServPath = Path.Combine(dropPath, @"bin\Indexes"); if (Directory.Exists(symServPath)) return "SRV*" + symServPath; */ var rawPdbs = Path.Combine(dropPath, @"symbols.pri\retail"); if (Directory.Exists(rawPdbs)) { m_log.WriteLine("Found Windows symbols {0}.", rawPdbs); return rawPdbs; } else m_log.WriteLine("Could not pdbs at {0}.", dropPath); } // See if it is a devdiv version style // FileVersion 11.0.40506.2 built by: MAIN // \\cpvsbuild\drops\dev11\Main\raw\40506.02\binaries.x86ret\Symbols.pri\retail\exe\devenv.pdb m = Regex.Match(fileVersion, @"(\d+)\.(\d+)\.(\d+)\.(\d+).*built by: *(\w+)"); if (m.Success) { if (!CpvsbuildExist) { m_log.WriteLine("Cpvsbuild for arch {0} not accessable. No private build lookup done.", arch); return null; } m_log.WriteLine("File version {0} matches devdiv schema.", fileVersion); var releaseNum = m.Groups[1].Value; var dailyNum = m.Groups[3].Value; var rebuildNum = m.Groups[4].Value; var branch = m.Groups[5].Value; var buildType = "ret"; var dropPath = string.Format(@"\\cpvsbuild\drops\dev{0}\{1}\raw\{2}.{3}\binaries.{4}{5}", releaseNum, branch, dailyNum, rebuildNum.PadLeft(2, '0'), arch, buildType); var rawPdbs = Path.Combine(dropPath, @"symbols.pri\retail"); if (Directory.Exists(rawPdbs)) { m_log.WriteLine("Found DevDiv symbols {0}.", rawPdbs); return rawPdbs; } else m_log.WriteLine("Could not find private build {0}.", dropPath); } return null; } internal string FindPdbInPrivateBuilds(string pdbSimpleName, string fileVersion, SymbolReaderNativeMethods.SymFindFileInPathProc FindSymbolFileCallBack) { if (string.IsNullOrEmpty(fileVersion)) return null; var arches = new string[] { "x86", "amd64" }; foreach (var arch in arches) { var buildPath = FindPrivateBuildPdbSearchPath(fileVersion, arch); if (buildPath == null) continue; pdbSimpleName = Path.GetFileNameWithoutExtension(pdbSimpleName); var fullPath = Path.Combine(buildPath, @"dll\" + pdbSimpleName + ".pdb"); if (!File.Exists(fullPath)) { fullPath = Path.Combine(buildPath, @"exe\" + pdbSimpleName + ".pdb"); if (!File.Exists(fullPath)) continue; } if (FindSymbolFileCallBack(fullPath, IntPtr.Zero)) continue; return fullPath; } return null; } internal Process m_currentProcess; // keep to insure currentProcessHandle stays alive internal IntPtr m_currentProcessHandle; // TODO really need to get on safe handle plan internal SymbolReaderNativeMethods.SymRegisterCallbackProc m_callback; internal TextWriter m_log; private string m_SymbolCacheDirectory; private string m_SourceCacheDirectory; #endregion } /// /// A symbolReaderModule represents a single PDB. You get one from SymbolReader.OpenSymbolFile /// It is effecively a managed interface to the Debug Interface Access (DIA) see /// http://msdn.microsoft.com/en-us/library/x93ctkx8.aspx for more. I have only exposed what /// I need, and the interface is quite large (and not super pretty). /// public unsafe class SymbolModule { /// /// The path name to the PDB itself /// public string PdbPath { get { return m_pdbPath; } } /// /// This is the EXE associated with the Pdb. It may be null or an invalid path. It is used /// to help look up source code (it is implicitly part of the Source Path search) /// public string ExePath { get; set; } /// /// Finds a (method) symbolic name for a given relative virtual address of some code. /// Returns an empty string if a name could not be found. /// public string FindNameForRva(uint rva) { System.Threading.Thread.Sleep(0); // Allow cancelation. if (m_symbolsByAddr == null) return ""; IDiaSymbol symbol = m_symbolsByAddr.symbolByRVA(rva); if (symbol == null) { Debug.WriteLine(string.Format("Warning: address 0x{0:x} not found.", rva)); return ""; } var ret = symbol.name; if (ret == null) { Debug.WriteLine(string.Format("Warning: address 0x{0:x} had a null symbol name.", rva)); return ""; } if (symbol.length == 0) { Debug.WriteLine(string.Format("Warning: address 0x{0:x} symbol {1} has length 0", rva, ret)); return ""; } // TODO FIX NOW, should not need to do this hand-unmangling. if (ret.Contains("@")) { // TODO relativel inefficient. string unmangled = null; symbol.get_undecoratedNameEx(0x1000, out unmangled); if (unmangled != null) ret = unmangled; if (ret.StartsWith("@")) ret = ret.Substring(1); if (ret.StartsWith("_")) ret = ret.Substring(1); var atIdx = ret.IndexOf('@'); if (0 < atIdx) ret = ret.Substring(0, atIdx); } m_lastSymbolNameRet = ret; return ret; } /// /// Fetches the source location (line number and file), given the relative virtual address (RVA) /// of the location in the executable. /// public SourceLocation SourceLocationForRva(uint rva) { m_reader.m_log.WriteLine("SourceLocationForRva: looking up RVA {0:x} ", rva); uint fetchCount; IDiaEnumLineNumbers sourceLocs; m_session.findLinesByRVA(rva, 0, out sourceLocs); IDiaLineNumber sourceLoc; sourceLocs.Next(1, out sourceLoc, out fetchCount); if (fetchCount == 0) { m_reader.m_log.WriteLine("SourceLocationForRva: No lines for RVA {0:x} ", rva); return null; } var buildTimeSourcePath = sourceLoc.sourceFile.fileName; var lineNum = (int)sourceLoc.lineNumber; var sourceFile = new SourceFile(this, sourceLoc.sourceFile); var sourceLocation = new SourceLocation(sourceFile, (int)sourceLoc.lineNumber); return sourceLocation; } /// /// Managed code is shipped as IL, so RVA to NATIVE mapping can't be placed in the PDB. Instead /// what is placed in the PDB is a mapping from a method's meta-data token and IL offset to source /// line number. Thus if you have a metadata token and IL offset, you can again get a source location /// public SourceLocation SourceLocationForManagedCode(uint methodMetaDataToken, int ilOffset) { m_reader.m_log.WriteLine("SourceLocationForManagedCode: Looking up method token {0:x} ilOffset {1:x}", methodMetaDataToken, ilOffset); IDiaSymbol methodSym; m_session.findSymbolByToken(methodMetaDataToken, SymTagEnum.SymTagFunction, out methodSym); if (methodSym == null) { m_reader.m_log.WriteLine("SourceLocationForManagedCode: No symbol for token {0:x} ilOffset {1:x}", methodMetaDataToken, ilOffset); return null; } uint fetchCount; IDiaEnumLineNumbers sourceLocs; IDiaLineNumber sourceLoc; // TODO FIX NOW, this code here is for debugging only turn if off when we are happy. m_session.findLinesByRVA(methodSym.relativeVirtualAddress, (uint)(ilOffset + 256), out sourceLocs); for (int i = 0; ;i++) { sourceLocs.Next(1, out sourceLoc, out fetchCount); if (fetchCount == 0) break; if (i == 0) m_reader.m_log.WriteLine("SourceLocationForManagedCode: source file: {0}", sourceLoc.sourceFile.fileName); m_reader.m_log.WriteLine("SourceLocationForManagedCode: ILOffset {0:x} -> line {1}", sourceLoc.relativeVirtualAddress - methodSym.relativeVirtualAddress, sourceLoc.lineNumber); } // End TODO FIX NOW debugging code // For managed code, the 'RVA' is a 'cumulative IL offset' (amount of IL bytes before this in the module) // Thus you find the line number of a particular IL offset by adding the offset within the method to // the cumulative IL offset of the start of the method. m_session.findLinesByRVA(methodSym.relativeVirtualAddress + (uint)ilOffset, 256, out sourceLocs); sourceLocs.Next(1, out sourceLoc, out fetchCount); if (fetchCount == 0) { m_reader.m_log.WriteLine("SourceLocationForManagedCode: No lines for token {0:x} ilOffset {1:x}", methodMetaDataToken, ilOffset); return null; } var sourceFile = new SourceFile(this, sourceLoc.sourceFile); int lineNum; // FEEFEE is some sort of illegal line number that is returned some time, It is better to ignore it. // and take the next valid line for(;;) { lineNum = (int)sourceLoc.lineNumber; if (lineNum != 0xFEEFEE) break; sourceLocs.Next(1, out sourceLoc, out fetchCount); if (fetchCount == 0) break; } var sourceLocation = new SourceLocation(sourceFile, lineNum); return sourceLocation; } /// /// Returns a list of all source files referenced in the PDB /// public IEnumerable AllSourceFiles() { IDiaEnumTables tables; m_session.getEnumTables(out tables); IDiaEnumSourceFiles sourceFiles; IDiaTable table = null; uint fetchCount = 0; for (; ; ) { tables.Next(1, ref table, ref fetchCount); if (fetchCount == 0) return null; sourceFiles = table as IDiaEnumSourceFiles; if (sourceFiles != null) break; } var ret = new List(); IDiaSourceFile sourceFile = null; for (; ; ) { sourceFiles.Next(1, out sourceFile, out fetchCount); if (fetchCount == 0) break; ret.Add(new SourceFile(this, sourceFile)); } return ret; } /// /// The PdbIndex a unique identifier that is used to relate the DLL and its PDB. /// public Guid PdbIndexGuid { get { return m_session.globalScope.guid; } } public int PdbIndexAge { get { return (int)m_session.globalScope.age; } } /// /// The symbol reader this SymbolModule was created from. /// public SymbolReader SymbolReader { get { return m_reader; } } #region private // TODO FIX NOW use or remove public enum NameSearchOptions { nsNone, nsfCaseSensitive = 0x1, nsfCaseInsensitive = 0x2, nsfFNameExt = 0x4, // treat as a file path nsfRegularExpression = 0x8, // * and ? wildcards nsfUndecoratedName = 0x10, // A undecorated name is the name you see in the source code. }; // TODO FIX NOW REMOVE public IEnumerable FindChildrenNames() { return FindChildrenNames(m_session.globalScope); } // TODO FIX NOW REMOVE public IEnumerable FindChildrenNames(IDiaSymbol scope, string name = null, NameSearchOptions searchOptions = NameSearchOptions.nsNone) { var syms = FindChildren(m_session.globalScope, name, searchOptions); var ret = new List(); foreach (var sym in syms) ret.Add(sym.name); return ret; } // TODO FIX NOW REMOVE private IEnumerable FindChildren(IDiaSymbol scope, string name = null, NameSearchOptions searchOptions = NameSearchOptions.nsNone) { IDiaEnumSymbols symEnum; m_session.findChildren(scope, SymTagEnum.SymTagNull, name, (uint)searchOptions, out symEnum); uint fetchCount; var ret = new List(); for (; ; ) { IDiaSymbol sym; symEnum.Next(1, out sym, out fetchCount); if (fetchCount == 0) break; SymTagEnum symTag = (SymTagEnum)sym.symTag; Debug.WriteLine("Got " + sym.name + " symTag " + symTag + " token " + sym.token.ToString("x")); if (symTag == SymTagEnum.SymTagFunction) { if (sym.token != 0) { var sourceLocation = SourceLocationForManagedCode(sym.token, 0); if (sourceLocation != null) Debug.WriteLine("Got Line " + sourceLocation.LineNumber + " file " + sourceLocation.SourceFile); } } if (symTag == SymTagEnum.SymTagCompiland) { var children = (List)FindChildren(sym, name, searchOptions); Debug.WriteLine("got " + children.Count + " children"); } ret.Add(sym); } return ret; } internal SymbolModule(SymbolReader reader, string pdbFilePath) { m_pdbPath = pdbFilePath; this.m_reader = reader; IDiaDataSource source = DiaLoader.GetDiaSourceObject(); source.loadDataFromPdb(pdbFilePath); source.openSession(out m_session); m_session.getSymbolsByAddr(out m_symbolsByAddr); } internal void LogManagedInfo(string pdbName, Guid pdbGuid, int pdbAge) { // Simply rember this if we decide we need it for source server support m_managedPdbName = pdbName; m_managedPdbGuid = pdbGuid; m_managedPdbAge = pdbAge; } // returns the path of the PDB that has source server information in it (which for NGEN images is the PDB for the managed image) internal string SourceServerPdbPath { get { if (m_managedPdbName == null) return PdbPath; if (!m_managedPdbPathAttempted) { m_managedPdbPathAttempted = true; m_managedPdbPath = m_reader.FindSymbolFilePath(m_managedPdbName, m_managedPdbGuid, m_managedPdbAge); } if (m_managedPdbPath == null) return PdbPath; return m_managedPdbPath; } } private string m_managedPdbName; private Guid m_managedPdbGuid; private int m_managedPdbAge; private string m_managedPdbPath; private bool m_managedPdbPathAttempted; // As a speed optimization we check the last symbol we got IDiaSymbol m_lastSymbol; string m_lastSymbolName; string m_lastSymbolNameRet; uint m_lastSymbolStartRva; uint m_lastSymbolEndRva; uint m_lastSymbolRva; bool m_lastSymbolVerified; private string CheckCache(uint rva) { if (m_lastSymbol == null) return null; if (!m_lastSymbolVerified) { // If we are not close, don't bother. if (!(m_lastSymbolRva < rva && rva <= m_lastSymbolRva + 0x100)) { m_lastSymbol = null; return null; } m_lastSymbolStartRva = m_lastSymbol.relativeVirtualAddress; m_lastSymbolEndRva = m_lastSymbolStartRva + (uint)m_lastSymbol.length; for (; ; ) { if (!(m_lastSymbolStartRva <= rva && rva < m_lastSymbolEndRva)) { m_lastSymbol = null; return null; } var testSym = m_symbolsByAddr.symbolByRVA(m_lastSymbolEndRva - 1); Debug.WriteLine(string.Format("Validating {0} start 0x{1:x} end 0x{2:x}", m_lastSymbolName, m_lastSymbolStartRva, m_lastSymbolEndRva)); if (testSym == null) { m_lastSymbol = null; return null; } if (testSym.name == m_lastSymbolName) { Debug.WriteLine("Hit Cache after validation."); m_lastSymbolVerified = true; return m_lastSymbolNameRet; } Debug.WriteLine(string.Format("Had to adjust symbol {0} from 0x{1:x} down to rva 0x{2:x}", m_lastSymbolName, m_lastSymbolEndRva, testSym.relativeVirtualAddress)); if (m_lastSymbolEndRva <= testSym.relativeVirtualAddress) { Debug.WriteLine("We did not reduce the range! Giving up."); m_lastSymbol = null; return null; } m_lastSymbolEndRva = testSym.relativeVirtualAddress; } } if (m_lastSymbolStartRva <= rva && rva < m_lastSymbolEndRva) { Debug.WriteLine("Hit Cache."); return m_lastSymbolNameRet; } return null; } internal SymbolReader m_reader; IDiaSession m_session; IDiaEnumSymbolsByAddr m_symbolsByAddr; string m_pdbPath; #endregion // TODO use or remove. internal void GetSourceServerStream() { var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().ManifestModule.FullyQualifiedName); var pdbStrExe = Path.Combine(dir, "pdbstr.exe"); var cmd = Command.Run(Command.Quote(pdbStrExe) + "-s:srcsrv -p:" + m_pdbPath); string output = cmd.Output; } } [Flags] public enum SymbolReaderFlags { /// /// No options this is the common case, where you want to look up everything you can. /// None = 0, /// /// Only fetch the PDB if it lives in the symbolCacheDirectory (is local an is generated). /// This will generate NGEN pdbs unless the NoNGenPDBs flag is set. /// CacheOnly = 1, /// /// No NGEN PDB generation. /// NoNGenPDB = 2, } /// /// A source file represents a source file from a PDB. This is not just a string /// because the file has a build time path, a checksum, and it needs to be 'smart' /// to copy down the file if requested. /// public class SourceFile { public string BuildTimeFilePath { get; private set; } public bool HasChecksum { get { return m_hash != null; } } /// /// This may fetch things from the source server, and thus can be very slow, which is why it is not a property. /// /// public string GetSourceFile() { var log = m_symbolModule.m_reader.m_log; // Did we build on this machine? if (File.Exists(BuildTimeFilePath)) { if (ChecksumMatches(BuildTimeFilePath)) { log.WriteLine("Found in build location."); return BuildTimeFilePath; } } // Try the source server var ret = GetSourceFromSrcServer(); if (ret != null) { log.WriteLine("Got source from source server."); return ret; } // Try _NT_SOURCE_PATH var locations = m_symbolModule.m_reader.ParsedSourcePath; log.WriteLine("_NT_SOURCE_PATH={0}", m_symbolModule.m_reader.SourcePath); // If we know the exe path, add that to the search path. if (m_symbolModule.ExePath != null) { var exeDir = Path.GetDirectoryName(m_symbolModule.ExePath); if (Directory.Exists(exeDir)) { // Add directories up the path, we stop somewhat arbitrarily at 3 for (int i = 0; i < 3; i++) { locations.Insert(0, exeDir); log.WriteLine("Adding Exe path {0}", exeDir); exeDir = Path.GetDirectoryName(exeDir); if (exeDir == null) break; } } } var curIdx = 0; for (; ; ) { var sepIdx = BuildTimeFilePath.IndexOf('\\', curIdx); if (sepIdx < 0) break; curIdx = sepIdx + 1; var tail = BuildTimeFilePath.Substring(sepIdx); foreach (string location in locations) { var probe = location + tail; log.WriteLine("Probing {0}", probe); if (File.Exists(probe)) { if (ChecksumMatches(probe)) { log.WriteLine("Success {0}", probe); return probe; } else log.WriteLine("Found file {0} but checksum mismatches", probe); } } } log.WriteLine("[Could not find source for {0}]", BuildTimeFilePath); return null; } #region private private string GetSourceFromSrcServer() { string ret = null; lock (this) { // To allow for cancelation we run this on another thread // This is a hack until I can stop using the ugly non-thread safe APIs. if (s_sourceServerCommandInProgress) { while (s_sourceServerCommandInProgress) System.Threading.Thread.Sleep(100); } s_sourceServerCommandInProgress = true; var thread = new System.Threading.Thread(delegate() { ret = m_GetSourceFromSrcServer(BuildTimeFilePath); s_sourceServerCommandInProgress = false; }); thread.Start(); // wait, allowing cancelation. while (s_sourceServerCommandInProgress) System.Threading.Thread.Sleep(100); } return ret; } private bool ChecksumMatches(string filePath) { if (!HasChecksum) return true; byte[] checksum = ComputeHash(filePath); if (checksum.Length != m_hash.Length) return false; for (int i = 0; i < checksum.Length; i++) if (checksum[i] != m_hash[i]) return false; return true; } private static bool s_sourceServerCommandInProgress; unsafe private string m_GetSourceFromSrcServer(string buildTimeSourcePath) { // Currently we are very inefficient loading and unloading constantly. ulong imageBase = 0x10000; var sb = new StringBuilder(260); var reader = m_symbolModule.m_reader; reader.m_log.WriteLine("[Searching source server for {0}]", buildTimeSourcePath); ulong imageBaseRet = SymbolReaderNativeMethods.SymLoadModuleExW( reader.m_currentProcessHandle, IntPtr.Zero, m_symbolModule.SourceServerPdbPath, null, (ulong)imageBase, 0, null, 0); // TODO FIX NOW is this a hack? // Allow it to find exes next to the current assembly var origPath = Environment.GetEnvironmentVariable("PATH"); var newPath = origPath; // TODO FIX NOW search exhaustively. (ELFIX) var progFiles = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); if (progFiles == null) progFiles = Environment.GetEnvironmentVariable("ProgramFiles"); if (progFiles != null) { var VSDir = Path.Combine(progFiles, @"Microsoft Visual Studio 11.0\Common7\IDE"); if (!Directory.Exists(VSDir)) VSDir = Path.Combine(progFiles, @"Microsoft Visual Studio 10.0\Common7\IDE"); var tfexe = Path.Combine(VSDir, "tf.exe"); if (File.Exists(tfexe)) newPath = VSDir + ";" + newPath; } var curAssembly = System.Reflection.Assembly.GetExecutingAssembly(); var curAssemblyDir = Path.GetDirectoryName(curAssembly.ManifestModule.FullyQualifiedName); reader.m_log.WriteLine("Adding {0} to the path", curAssemblyDir); newPath = curAssemblyDir + ";" + newPath; Environment.SetEnvironmentVariable("PATH", newPath); var setHomeRet = SymbolReaderNativeMethods.SymSetHomeDirectoryW(reader.m_currentProcessHandle, reader.SourceCacheDirectory); Debug.Assert(!(setHomeRet == IntPtr.Zero)); var ret = SymbolReaderNativeMethods.SymGetSourceFileW(reader.m_currentProcessHandle, imageBase, IntPtr.Zero, buildTimeSourcePath, sb, sb.Capacity); SymbolReaderNativeMethods.SymUnloadModule64(reader.m_currentProcessHandle, imageBase); // TODO worry about exceptions. Environment.SetEnvironmentVariable("PATH", origPath); if (!ret) { reader.m_log.WriteLine("Source Server for {0} failed", buildTimeSourcePath); return null; } var retVal = sb.ToString(); reader.m_log.WriteLine("Source Server downloaded {0}", retVal); return retVal; } unsafe internal SourceFile(SymbolModule module, IDiaSourceFile sourceFile) { m_symbolModule = module; BuildTimeFilePath = sourceFile.fileName; // 0 No checksum present. // 1 CALG_MD5 checksum generated with the MD5 hashing algorithm. // 2 CALG_SHA1 checksum generated with the SHA1 hashing algorithm. m_hashType = sourceFile.checksumType; if (m_hashType != 1 && m_hashType != 0) { // TODO does anyone use SHA1? Debug.Assert(false, "Unknown hash type"); m_hashType = 0; } if (m_hashType != 0) { // MD5 is 16 bytes // SHA1 is 20 bytes m_hash = new byte[16]; uint bytesFetched; fixed (byte* bufferPtr = m_hash) sourceFile.get_checksum((uint)m_hash.Length, out bytesFetched, out *bufferPtr); Debug.Assert(bytesFetched == 16); } } private byte[] ComputeHash(string filePath) { System.Security.Cryptography.MD5CryptoServiceProvider crypto = new System.Security.Cryptography.MD5CryptoServiceProvider(); using (var fileStream = File.OpenRead(filePath)) return crypto.ComputeHash(fileStream); } SymbolModule m_symbolModule; uint m_hashType; byte[] m_hash; #endregion } /// /// A SourceLocation represents a point in the source code. That is the file and the line number. /// public class SourceLocation { public SourceFile SourceFile { get; private set; } public int LineNumber { get; private set; } #region private internal SourceLocation(SourceFile sourceFile, int lineNumber) { SourceFile = sourceFile; LineNumber = lineNumber; } #endregion } } #region private classes namespace Dia2Lib { /// /// The DiaLoader class knows how to load the msdia100.dll (the Debug Access Interface) (see docs at /// http://msdn.microsoft.com/en-us/library/x93ctkx8.aspx), without it being registered as a COM object. /// Basically it just called the DllGetClassObject interface directly. /// /// It has one public method 'GetDiaSourceObject' which knows how to create a IDiaDataSource object. /// From there you can do anything you need. /// internal static class DiaLoader { /// /// Load the msdia100 dll and get a IDiaDataSource from it. This is your gateway to PDB reading. /// public static IDiaDataSource GetDiaSourceObject() { var diaSourceClassGuid = new Guid("{B86AE24D-BF2F-4AC9-B5A2-34B14E4CE11D}"); var comClassFactory = (IClassFactory)DllGetClassObject(diaSourceClassGuid, typeof(IClassFactory).GUID); object comObject = null; Guid iDataDataSourceGuid = typeof(IDiaDataSource).GUID; comClassFactory.CreateInstance(null, ref iDataDataSourceGuid, out comObject); return (comObject as IDiaDataSource); } #region private [ComImport, ComVisible(false), Guid("00000001-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IClassFactory { void CreateInstance([MarshalAs(UnmanagedType.Interface)] object aggregator, ref Guid refiid, [MarshalAs(UnmanagedType.Interface)] out object createdObject); void LockServer(bool incrementRefCount); } // Methods [return: MarshalAs(UnmanagedType.Interface)] [DllImport("msdia100.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] internal static extern object DllGetClassObject( [In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid); #endregion } } #endregion