Fritz is a Windows NT system developer specializing in networked and distributed applications for Nims Associates Inc. in Denver, Colorado. He can be contacted at [email protected].
Even in this brave new world of point-and-shoot debugging, some problems are still best resolved by a bit of printf()-style output. Case in point: Windows NT services, applications, similar to UNIX daemons but written using a special API, that run in the background without any user interface. The standard method of developing and debugging a service is to first write it as an application (generally a 32-bit Console App), and call printf() to get state output on the fly. However, this method is useless when the process is actually running as a service. This is particularly vexing when the problem being tested or debugged depends upon the service running in a different security context than the client or the impersonation of a user other than System (the default service account). Debugging both from an interface such as Developer Studio is not valid because both would be running as the logged-in user.
The Win32 API provides a neat solution for such sophisticated problems that scream out for low-tech solutions -- the Debug API. This set of functions provides a number of useful tools for both the debugger and the debugged. Additionally, since these functions are supported by the operating system, it doesn't matter whether the program being debugged has been compiled or optimized in Debug or Release mode, nor does it make any difference what language or tool you opt to use. As long as you can call the API you should be able to use the functions. As an added benefit, once you've got everything proven, you can leave the debugging output code in the program since it has little effect unless a debugger is present. This is a great way to look into programs when they are running in place, particularly when those odd bugs show up in, shall we say, bulletproof code.
The only addition that needs to be made to the application being debugged is the inclusion of the OutputDebugString() API call. As you might expect, this function sends a string to the debugger. In my own code I create a temporary string via sprintf() with a descriptive message possibly containing error information like the result from GetLastError().
If you are using the Microsoft Developers Studio, you can see the output from OutputDebugString() in the Output window while debugging a project, but using DevStudio is unwieldy for many purposes. Fortunately, writing a lightweight debugger is easy using the Debug API. In fact, the sample source code contains a debugger that is only 170 lines long, and could be shorter if you don't care about some debugging events.
The first step for using this tool as a window into the secret lives of services is to make sure that the debugging process has the Debug Privilege. This setting is only available to users and groups that are permitted to debug programs from the User Manager. By default, this only includes the local Administrators group. To add other users and groups:
- Run User Manager.
- Select "User Rights" from the "Policies" menu.
- Check the "Show Advanced User Rights" box.
- Find "Debug programs" in the "Rights" drop-down box.
- Add users and groups as necessary.
Listing One (at the end of this article) shows a fairly straightforward means of making sure that the process' access token is modified. Once a process has modified its token privileges, it is able to debug other processes, even those that are running in other security contexts.
The next and perhaps most obvious step is that there must be a process to debug. The easiest way to debug a process is to run it from the debugger specifying the DEBUG_PROCESS flag in the dwCreationFlags parameter of CreateProcess(). This method is not effective for debugging services, however, since they are run via the operating system. Not to fear, DebugActiveProcess() is here! The trick to this API call is to get the process ID (PID) from the Task Manager for the process that you want to debug. The process's handle is needed in order to read any output strings so an additional call to OpenProcess() is necessary. The startup code for the debugger looks like Listing Two.
At this point, as far as NT is concerned, you have just become a debugger. This means that debug events generated by the target process can now be captured and interpreted. To do this, the debugging process enters a loop for the remainder of its life. The top of the loop uses WaitForDebugEvent() to block until a debug event is available or for a designated time-out period. When a debug event is available the information returned in the DEBUG_EVENT structure dictates what happens next. For my purposes, only the OUTPUT_DEBUG_STRING_EVENT events required more than casual handling, though I did add a line of code in the EXCEPTION_DEBUG_EVENT handling block to allow the debugged program to handle exceptions. Had I not done this, the application would terminate due to an unhandled exception error. If the debugger uses DebugActiveProcess() the first events that come through will represent the current state of the process: a CREATE_PROCESS_DEBUG_EVENT, one CREATE_THREAD_DEBUG_EVENT event per thread in the target process, one LOAD_DLL_DEBUG_EVENT event for each currently loaded DLL, and finally a single EXCEPTION_DEBUG_EVENT event, which should not be passed back to the target application, to indicate that the process is being restarted. Subsequent events will be generated by the application itself. It is interesting to note that Windows NT 4.0 does not supply pointers to DLL file names within the debugged application for LOAD_DLL_DEBUG_EVENT events when using DebugActiveProcess(). When debugging a child process, the pointers provided are to addresses within the process symbol table and can be extracted only by digging through the image. To allow the target process to proceed after posting a debug event the debugger calls the ContinueDebugEvent() function to conclude the event handling.
Reading the strings generated by the OutputDebugString() call deserves a bit of additional discussion. The pointer returned in the OUTPUT_DEBUG_STRING_INFO structure is not to a string but to an offset within the target process that can then be read, via ReadProcessMemory(); see Listing Three.
Examples
Listing Four is an application that can be used to demonstrate utility of OutputDebugString(). If run by itself, it is singularly uninteresting, however, once you have its PID and run qdebug (the demonstration tool, which is available electronically; see "Resource Center," page 3) with the PID as a command line parameter, you see much more action. Figure 1 is the app dbme.exe (also available electronically) running alongside the debugger.
Still not impressed? Try running the DBME program via the Windows NT schedule service using the "at" command. Since the schedule service runs using the System account privileges, the DBME application is off limits from ordinary users, including those in the Administrators group. To prove this, try to terminate the process from the Task Manager once it's been run by Schedule. You will see an "Access is denied" message. Once again, qdebug with the PID as a command line parameter will attach and show the text being sent via OutputDebugString(). Fun, huh? Quitting the debugger will in turn kill the DBME application, which brings us to...
Drawbacks and Caveats
There are two big gotchas with the debugging API: You can attach to a running process but you can not detach without terminating the debugged process; and not surprisingly, debugging an application slows its performance. Think about it: App calls OutputDebugString(), which then checks IsDebuggerPresent(). If the debugger is present, it posts the event to the debugger and halts the calling thread; a bunch of system plumbing ensues. The debugger receives the event, in the case of OutputDebugString(), it opens the target process and reads data out; more system plumbing, then the app is restarted. Slow? You bet! Use of these techniques is only recommended during debugging and testing or for nonperformance-critical tools (are there such things?).
Conclusion
The functions provided in this API provide easy language- and tool-independent methods for solving otherwise complex debugging challenges. The demonstration tool qdebug is useful for working with services and other applications that may not be able to output data to a console. If you want to tinker, an obvious extension to this tool is to log the output to a file. If this feature were added, qdebug could become a useful alternative to logging via the NT Event Log, with the possible performance issues kept in mind.
DDJ
Listing One
// Get token for this processHANDLE token; if(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token) == false) { printf("OpenProcessToken Failed: 0x%X\n", GetLastError()); exit(-1); } </p> //Get LUID for shutdown privilege} TOKEN_PRIVILEGES tkp; if(LookupPrivilegeValue(NULL, "SeDebugPrivilege", &tkp.Privileges[0].Luid) == false) { printf("LookupPrivilegeValue failed: 0x%X\n", GetLastError()); exit(-1); } </p> tkp.PrivilegeCount = 1; tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; </p> if(AdjustTokenPrivileges(token, false, &tkp, 0, NULL, NULL) == false) { printf("AdjustTokenPrivileges Failed: 0x%X\n", GetLastError()); exit(-1); }
Listing Two
HANDLE hProc = INVALID_HANDLE_VALUE;if(isdigit(sCL[0]) == 0) { // Command line is text, so run it STARTUPINFO rStartup; ZeroMemory(&rStartup, sizeof(rStartup)); rStartup.cb = sizeof(rStartup); PROCESS_INFORMATION rProc; char aCL[2048]; CopyMemory(aCL, sCL, sCL.GetLength()); if(CreateProcess(NULL, aCL, NULL, NULL, true, DEBUG_PROCESS, NULL, NULL, &rStartup, &rProc) == false) { printf("Could not start process commandline: %s\n", sCL); exit(-1); } hProc = rProc.hProcess; </p> printf("Attempting to debug: %s\n", aCL); } else { // debugging a running process via the PID int iPid = atoi(sCL); if(DebugActiveProcess(iPid) == false) { printf("Error: DebugActiveProcess: %d\n", GetLastError()); exit(-2); } hProc = OpenProcess(PROCESS_ALL_ACCESS, false, iPid); if(hProc == NULL) { printf("Error OpenProcess: %d\n", GetLastError()); exit(-3); } }
Listing Three
case OUTPUT_DEBUG_STRING_EVENT: { char *aBuf = new char[deDebugger.u.DebugString.nDebugStringLength]; DWORD dwRead; ReadProcessMemory(hProc, deDebugger.u.DebugString.lpDebugStringData, aBuf, deDebugger.u.DebugString.nDebugStringLength, &dwRead); printf("%s", aBuf); delete aBuf; break; }
Listing Four
/* DBME.C a demo tool that uses OutputDebugString(). */#include <windows.h> #include <stdio.h> </p> int main(int argc, char *argv[]) { printf("Process ID = %ld\n", GetCurrentProcessId()); while(1) { OutputDebugString("I'm a little teapot\n"); Sleep(1000); OutputDebugString("Short and stout\n"); Sleep(1000); OutputDebugString("Here is my handle\n"); Sleep(1000); OutputDebugString("Here is my spout\n\n"); Sleep(1000); } return 0; exit(0); }
Copyright © 1998, Dr. Dobb's Journal