In the days of Windows XP, a Windows Service was more or less an ordinary executable running in the same session as all other executables. Debugging it was fairly simple, although you did have to deal with the complication that it was started by the Service Control Manager.
Today, Windows Services run in a mysterious Session 0, which is difficult to work with. If your Service is written in C++, you'll find that it can be problematic to deal with bugs, particularly crashes, that occur during Service startup.
In this article, I explain why that is and demonstrate a simple set of techniques for dealing with this problem. I hope that the relative simplicity of all this will reduce your frustration level when debugging your Windows Services.
The Problem Statement
Services handle huge amounts of the workload involved in keeping Windows humming along, and much of the stuff they do can drastically change the way the operating system behaves which means any surface area they expose represents a real security issue.As I mentioned, in the days of Windows XP, Windows Services ran in the same environment as normal executables. When Windows Services are normal executables, they can be bombarded with Windows Messages, COM requests, DDE, all the normal IPC stuff that Windows uses. Malformed messages can crash services or get them to behave improperly. It's generally just not a good way to do things it would be like keeping the keys to the family jewels on a hook in your entry hall. Figure 1 shows just a small fraction of the services running on my Windows 7 laptop. If you are a black hat hacker, you can't help but drool a bit at what you see there:
Figure 1: A sampling of maybe 120 services on an Windows 7 System.
In the Vista era, all services were moved to a special Session 0. (See Sessions, Desktops and Windows Stations for discussion of these terms.) Or to be more accurate, everything else was moved out of Session 0. This move drastically limited the ability of user mode programs to interact with services mostly for the good.
In general, executables running in Session 0 don't communicate with your desktop session no windows messages, for example. This makes them much more secure. But it also makes it hard to debug them. I'll give some explanation shortly showing why this is, and how we get around it.
Debugging a Service
Unlike a normal application, I don't start a service by entering its name on the command line, or by calling
CreateProcess. Instead I need to rely on the Service Control Manager to start and stop the process by having my process respond to some very specific commands.
This doesn't fit very well into the normal debugging paradigm we normally expect the debugger to actually start the program in question. But with Windows Services, we must go to the Services plugin of the Management Console and tell it to start the service the app then starts without much involvement from us.
To debug that app after it has started, we need to invoke the Debug|Attach to Process function, which brings up the dialog shown in Figure 2:
Figure 2: Attach to process dialog.
By checking the Show Processes From All Users checkbox, I can see my service, select it, and attach it to the debugger. I'm now free to set breakpoints, watchpoints, examine variables, and do all the other things that I need to debug an app. Things are just the way I want to them to be.
So What's the Problem?
It seems like we have a pretty reasonable way to debug a service, right? As it happens, I work on an app that runs as a Windows Service, and this app spends a lot of time at start-up figuring out how it is configured. From time to time, things go wrong during that startup and my state is incorrect. Even worse, there are times when that startup code crashes.
As a toy example, here is some code that I might execute in the startup of a service. It runs at the start of
PreMessageLoop(), a good place to do initialization of a service:
CRegKey key; DWORD checks_per_second = 0; LONG err = key.Open(HKEY_LOCAL_MACHINE,L"SOFTWARE\\mrn\\mrnService", KEY_READ); if ( err == ERROR_SUCCESS ) key.QueryDWORDValue(L"ChecksPerSecond",checks_per_second); int time_in_ms = 1000 / checks_per_second;
It turns out that I don't check for the proper opening of the registry key and I had previously inadvertently stored the key in
HKLM/SOFTWARE/mrn/mrnService on my Windows 7 system. I should have created it in
HKLM/SOFTWARE/WOw6432Node/mrn/mrnService. As a result, the key
open failed, and
checks_per_scond value was left at 0. (I don't check for illegal values of the key even if it is read properly a second representative error.)
So, when I attempted to start this service, I got a dialog box from the Service Control Manager that looked something like Figure 3:
Figure 3: Error from starting a service.
This would seem like a good point to attach to the service and start debugging, but you can forget about it the service has already crashed and is gone.
Where is JIT When I Need It?
What I really need here is the normal popup that I see on a dev system when a crash occurs the one that asks if I would like to debug the troubled process. Why am I not seeing it?
If you are quick on the draw, Process Explorer actually shows you what is going on. In the screen shot below, you can see that my service,
mrnService.exe, is caught by the Windows Error Reporting tool, which normally brings up just that dialog:
Figure 4: Windows error reporting tools.
The problem is that this is all happening in Session 0, which does not have the ability to interact with my desktop. So Windows Error Reporting pops up a dialog and quickly realizes that there is nobody home to click on it. It simply closes up shop and kills the errant process. I have no opportunity to catch this in progress.
(This capture also highlights part of the difficulty in debugging services the process has been started by
svchost.exe, not Visual Studio. The lifecycle of a service requires this, like it or not.)
A Reasonable Solution
It's not quite true that Session 0 has no opportunity to communicate with your desktop. Windows has a Remote Desktop API that allows for just the type of communications we would like. In particular, I can use the
WTSSendMessage function to popup a message on my desktop when the service enters that crucial startup phase. The resulting message box gives me an opportunity to attach to the service and start debugging before it has done anything of importance.