I deal mostly withy managed code, and while Visual Studio is a great environment, I often find myself in situations where a lightweight, command-line style debugger is all I really need to get to what i want. Mdbg is such a debugger, with a 4 MB working set, it often fires up in just under a second.
Mdbg has support for symbol server but not for source server, and when you’re dealing with 6 released versions and all kinds of service packs and qfes, you really don’t want to go spelunking to find the source code. Since Mdbg supports extensions, I decided to write an extension that could find the source automatically using the DbgHelp APIs implemented by DbgHelp.dll (docs available here). I decided to share the code and some of the experience hoping it will be useful to others.
I based my implementation on the sample posted by Mike Stall (available here), and I decided to call my command “ls” for LoadSource, so I started off creating a Class Library project, added references to MdbgCore.dll, and started off with the following skeleton:
[CommandDescription(CommandName = "ls", ShortHelp = "Load Sources from source server", MinimumAbbrev = 2, LongHelp = @" Usage: ls [frames] load sources using the SRC* source server")] public static void LoadSourceCmd(string arguments) { }
The support for symbol server works just as one would expect, so in my command I assume that the user has already taken care of getting the right symbols loaded so the path information is already available. The API in MdbgCore is fairly straightforward, I didn’t need docs to find what I needed, just a few hops using intellisense.
What I do is look at the Path in the SourcePostion of the CurrentFrame, look it up using the FileLocator, and if I find that the file is not available, I will go and fetch it using the source server. If the source server returns a location the value of fileLocation and the contents of the FileLocator will be updated. Regardless, I then execute the Show command:
MDbgFrame currentFrame = CommandBase.Debugger.Processes.Active.Threads.Active.CurrentFrame; stringsourceFile = currentFrame.SourcePosition.Path; stringfileLocation = CommandBase.Shell.FileLocator.GetFileLocation(sourceFile); if (!File.Exists(fileLocation)) { fileLocation = GetPathFromSourceServer(arguments, currentFrame, sourceFile); if (File.Exists(fileLocation)) { CommandBase.Shell.FileLocator.Associate(sourceFile, fileLocation);
} } IMDbgCommand show; stringargs; CommandBase.Shell.Commands.ParseCommand("sh", outshow, outargs); show.Execute(string.Empty);
Since DbgHelp is a native code library, calling from managed required me to write a bunch of tedious P/Invoke code, I will share that code but won’t say much about it as there isn’t anything very interesting about it. I also removed most of the error checking from the snippets below to make it more readable, don’t think I do that in real life, the attached code should have plenty of error checking!
DbgHelp requires a initialization, so I addded a static initializer that I call at the beginning of the GetPathFromSourceServer method so I can initialize if I haven’t already. Before initializing one can also set options using the SymSetOptions API, but other than that it’s just a straight call to SymInitializeW to which i pass the handle of the current process:
IntPtr hProcess = Process.GetCurrentProcess().Handle; Dbghelp.SymInitializeW(hProcess, null, false);
After that, we need to make sure DbgHelp has loaded the module we’re finding sources for. I added a static cache where I track which modules I already loaded so I don’t end up loading a module twice. So I first check my cache and if it comes back empty, I will load the module with a call to SymLoadModuleExW and save the result in my cache:
MDbgModule module = currentFrame.Function.Module; string symbolFile = module.SymbolFilename; ulong baseAddress; CorModule corModule = module.CorModule; baseAddress = (ulong)corModule.BaseAddress; uint size = (uint)corModule.Size; string moduleName = Path.GetFileNameWithoutExtension(sourceFile); baseAddress = Dbghelp.SymLoadModuleExW(hProcess, IntPtr.Zero, symbolFile, moduleName, baseAddress, size, (Dbghelp.MODLOAD_DATA*)null, 0); moduleCache.Add(symbolFile, baseAddress);
At this point there really isn’t much left for us to do, our last API call is the real deal, the one that finds the source code and brings a copy of it down, and all of that work is just a simple call to SymGetSourceFileW:
StringBuilder path = new StringBuilder(MAX_PATH * 8); Dbghelp.SymGetSourceFileW(hProcess, baseAddress, IntPtr.Zero, sourceFile, path, (uint)path.Capacity); return path.ToString();
After playing with the new command, I discovered that my command and Visual Studio (which uses the same core implementation as DbgHelp) were putting files in different places and having two separate caches was really irritating me, so I decided to fix that. Unfortunately, after calling the SymSetHomeDirectoryW API to point DbgHelp to the Visual Studio location (which on my Win7 box happens to be “%LOCALAPPDATA%\SourceServer”), my files were being put under a subfolder of that same location, called “src”. Thanks to a suggestion from Pat Styles, I found that if the SYMOPT_FLAT_DIRECTORY option was set, then the “src” folder would not be appended when calling SymSetHomeDirectoryW. So I added these few lines before the call to SymInitializeW and my problem was solved:
Dbghelp.SymSetOptions(Dbghelp.SYMOPT.SYMOPT_FLAT_DIRECTORY); string localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\SourceServer"; Dbghelp.SymSetHomeDirectoryW(hProcess, localApplicationData); Dbghelp.SymSetOptions((Dbghelp.SYMOPT)0);
You can download the sources here, they contain two sets of sln/csproj files that share the same code, you can use LoadSource2.0.sln with Visual Studio 2008, and LoadSource.sln Visual Studio 2010 (note that you can also just use msbuild.exe to build the LoadSource.dll, so Visual Studio isn’t required).
0 comments:
Post a Comment