Some Thread Internals

[Previous] [Next]

So far, I've explained how to implement a thread function and how to have the system create a thread to execute that function. In this section, we'll look at how the system pulls this off.

Figure 6-1 shows what the system must do to create and initialize a thread. Let's look closely at this figure to understand exactly what's going on. A call to CreateThread causes the system to create a thread kernel object. This object has an initial usage count of 2. (The thread kernel object is not destroyed until the thread stops running and the handle returned from CreateThread is closed.) Other properties of the thread's kernel object are also initialized: the suspension count is set to 1, the exit code is set to STILL_ACTIVE (0x103), and the object is set to the nonsignaled state.

Once the kernel object has been created, the system allocates memory, which is used for the thread's stack. This memory is allocated from the process's address space since threads don't have an address space of their own. The system then writes two values to the upper end of the new thread's stack. (Thread stacks always build from high memory addresses to low memory addresses.) The first value written to the stack is the value of the pvParam parameter that you passed to CreateThread. Immediately below it is the pfnStartAddr value that you also passed to CreateThread.

click to view at full size.

Figure 6-1. How a thread is created and initialized

Each thread has its own set of CPU registers, called the thread's context. The context reflects the state of the thread's CPU registers when the thread last executed. The set of CPU registers for the thread is saved in a CONTEXT structure (defined in the WinNT.h header file). The CONTEXT structure is itself contained in the thread's kernel object.

The instruction pointer and stack pointer registers are the two most important registers in the thread's context. Remember that threads always run in the context of a process. So both these addresses identify memory in the owning process's address space. When the thread's kernel object is initialized, the CONTEXT structure's stack pointer register is set to the address of where pfnStartAddr was placed on the thread's stack. The instruction pointer register is set to the address of an undocumented (and unexported) function called BaseThreadStart. This function is contained inside the Kernel32.dll module (which is also where the CreateThread function is implemented). Figure 6-1 shows all of this.

Here is what BaseThreadStart basically does:

 VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) { _ _try { ExitThread((pfnStartAddr)(pvParam)); } _ _except(UnhandledExceptionFilter(GetExceptionInformation())) { ExitProcess(GetExceptionCode()); } // NOTE: We never get here. } 

After the thread has completely initialized, the system checks to see whether the CREATE_SUSPENDED flag was passed to CreateThread. If this flag was not passed, the system decrements the thread's suspend count to 0 and the thread can be scheduled to a processor. The system then loads the actual CPU registers with the values that were last saved in the thread's context. The thread can now execute code and manipulate data in its process's address space.

Because a new thread's instruction pointer is set to BaseThreadStart, this function is really where the thread begins execution. BaseThreadStart's prototype makes you think that the function receives two parameters, but this implies that the function is called from another function, which is not true. The new thread simply comes into existence and starts executing here. BaseThreadStart believes that it was called from another function because it has access to two parameters. But access to these parameters works because the operating system explicitly wrote the values to the thread's stack (which is how parameters are normally passed to a function). Note that some CPU architectures pass parameters using CPU registers instead of the stack. For these architectures, the system initializes the proper registers correctly before allowing the thread to execute the BaseThreadStart function.

When the new thread executes the BaseThreadStart function, the following things happen:

  • A structured exception handling (SEH) frame is set up around your thread function so that any exceptions raised while your thread executes get some default handling by the system. (See Chapters 23, 24, and 25 for more information about structured exception handling.)
  • The system calls your thread function, passing it the pvParam parameter that you passed to the CreateThread function.
  • When your thread function returns, BaseThreadStart calls ExitThread, passing it your thread function's return value. The thread kernel object's usage count is decremented and the thread stops executing.
  • If your thread raises an exception that is not handled, the SEH frame set up by the BaseThreadStart function handles the exception. Usually, this means that a message box is presented to the user and that when the user dismisses the message box, BaseThreadStart calls ExitProcess to terminate the entire process, not just the offending thread.

Notice that within BaseThreadStart, the thread calls either ExitThread or ExitProcess. This means that the thread cannot ever exit this function; it always dies inside it. This is why BaseThreadStart is prototyped as returning VOID—it never returns.

Also, your thread function can return when it's done processing because of BaseThreadStart. When BaseThreadStart calls your thread function, it pushes its return address on the stack so your thread function knows where to return. But BaseThreadStart is not allowed to return. If it didn't forcibly kill the thread and simply tried to return, an access violation would almost definitely be raised because there is no function return address on the thread's stack and BaseThreadStart would try to return to some random memory location.

When a process's primary thread is initialized, its instruction pointer is set to another undocumented function called BaseProcessStart. This function is almost identical to BaseThreadStart and looks something like this:

 VOID BaseProcessStart(PPROCESS_START_ROUTINE pfnStartAddr) { _ _try { ExitThread((pfnStartAddr)()); } _ _except(UnhandledExceptionFilter(GetExceptionInformation())) { ExitProcess(GetExceptionCode()); } // NOTE: We never get here. } 

The only real difference is that there is no reference to the pvParam parameter. When BaseProcessStart begins executing, it calls the C/C++ run time library's startup code, which initializes and then calls your main, wmain, WinMain, or wWinMain function. When your entry-point function returns, the C/C++ run-time library startup code calls ExitProcess. So for a C/C++ application, the primary thread never returns to the BaseProcessStart function.



Programming Applications for Microsoft Windows
Programming Applications for Microsoft Windows (Microsoft Programming Series)
ISBN: 1572319968
EAN: 2147483647
Year: 1999
Pages: 193

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net