Archive for March, 2008

Walking the stack of the current thread

StackWalk64 provides a convenient and platform-independent way to walk the stack of a thread. Although useful and powerful indeed, it is definitively one of the less trivial to use functions. The additional fact that the MSDN documentation for StackWalk64 is rather light does not make things easier. There is, however, a decent article on CodeProject covering the usage of StackWalk64.

Although you can walk the stack of any thread using StackWalk64, this post only deals with walking the current thread’s stack. In order to walk the stack of the current thread, you first have to obtain the CONTEXT of the current thread. The naive way to obtain this context would be to call GetThreadContext( GetCurrentThread() ) — however, as the documentation for GetThreadContext clearly states, the result of this function is undefined if used on the current thread.

Obtaining a CONTEXT

64 Bit windows provides the function RtlCaptureContext for this purpose, so obtaining the context is trivial. 32 Bit Windows, however, does not implement this function, yet it is not hard to come up with a surregate.

One way to obtain the context is to set up a SEH frame, deliberately raise an exception and obtain the context in the exception filter using GetExceptionInformation. Although that works, it is probably the slowest way to do it. An easier and quicker solution is to use inline assembly (we only have to support 32 bit as we have RtlCaptureContext for x64/IA64, so using assembly is ok). Using appropriate instructions, we can manually populate the x86 CONTEXT structure. However, as eip cannot be accessed using a mov instruction, we have to use a little trick to obtain the value of the instruction pointer — use an (otherwise useless) label:

CONTEXT Context;
ZeroMemory( &Context, sizeof( CONTEXT ) );
Context.ContextFlags = CONTEXT_CONTROL;

__asm
{
Label:
  mov [Context.Ebp], ebp;
  mov [Context.Esp], esp;
  mov eax, [Label];
  mov [Context.Eip], eax;
}

Preparing to call StackWalk64

Having obtained the context, we have to initialize the STACKFRAME64 structure for the first call. This is trivial, but platform-specific.

In order to enable StackWalk64 to use symbol information, dbghelp has to be initialized by calling SymInitialize, passing the current process handle as parameter. Unless you plan to manually call SymLoadModule64 for all affected modules, it is crucial to pass TRUE for the fInvadeProcess argument. If you pass FALSE or forget to call SymInitialize altogether (After all, the documentation of StackWalk64 does not state that calling SymInitialize is required), you will notice that StackWalk64 works on x86 builds, but fails (with last error set to 0x1E7) on x64. Also, as dbghelp is not threadsafe, it is usually a good idea to protect any calls to dbghelp by a critical section

Calling StackWalk64

After these provisions have been made, calling StackWalk64 is a snap. The only unfortnate thing is that you cannot really distinguish between StackWalk64 returning FALSE because of a failure or because of the bottom of the stack having been reached as StackWalk64 does not reliably set the last Win32 error.

The following code puts it all together. Note that only a the minimal amount of stack trace information is gathered, i.e. only the return addresses are saved in the array.

typedef struct _STACKTRACE
{
  //
  // Number of frames in Frames array.
  //
  UINT FrameCount;

  //
  // PC-Addresses of frames. Index 0 contains the topmost frame.
  //
  ULONGLONG Frames[ ANYSIZE_ARRAY ];
} STACKTRACE, *PSTACKTRACE;

/*++
  Routine Description:
    Capture the stack trace based on the given CONTEXT.

  Parameters:
    Context    Thread context. If NULL, the current context is
               used.
    Event      Event structure to populate.
    MaxFrames  Max number of frames the structure can hold.
--*/
#ifdef _M_IX86
  //
  // Disable global optimization and ignore /GS waning caused by 
  // inline assembly.
  //
  #pragma optimize( "g", off )
  #pragma warning( push )
  #pragma warning( disable : 4748 )
#endif
HRESULT CaptureStackTrace(
  __in_opt CONST PCONTEXT InitialContext,
  __in PSTACKTRACE StackTrace,
  __in UINT MaxFrames 
  )
{
  DWORD MachineType;
  CONTEXT Context;
  HRESULT Hr;
  STACKFRAME64 StackFrame;

  ASSERT( StackTrace );
  ASSERT( MaxFrames > 0 );

  if ( InitialContext == NULL )
  {
    //
    // Use current context.
    //
    // N.B. GetThreadContext cannot be used on the current thread.
    // Capture own context - on i386, there is no API for that.
    //
#ifdef _M_IX86
    ZeroMemory( &Context, sizeof( CONTEXT ) );

    Context.ContextFlags = CONTEXT_CONTROL;
    
    //
    // Those three registers are enough.
    //
    __asm
    {
    Label:
      mov [Context.Ebp], ebp;
      mov [Context.Esp], esp;
      mov eax, [Label];
      mov [Context.Eip], eax;
    }
#else
    RtlCaptureContext( &Context );
#endif  
  }
  else
  {
    CopyMemory( &Context, InitialContext, sizeof( CONTEXT ) ); 
  }

  //
  // Set up stack frame.
  //
  ZeroMemory( &StackFrame, sizeof( STACKFRAME64 ) );
#ifdef _M_IX86
  MachineType                 = IMAGE_FILE_MACHINE_I386;
  StackFrame.AddrPC.Offset    = Context.Eip;
  StackFrame.AddrPC.Mode      = AddrModeFlat;
  StackFrame.AddrFrame.Offset = Context.Ebp;
  StackFrame.AddrFrame.Mode   = AddrModeFlat;
  StackFrame.AddrStack.Offset = Context.Esp;
  StackFrame.AddrStack.Mode   = AddrModeFlat;
#elif _M_X64
  MachineType                 = IMAGE_FILE_MACHINE_AMD64;
  StackFrame.AddrPC.Offset    = Context.Rip;
  StackFrame.AddrPC.Mode      = AddrModeFlat;
  StackFrame.AddrFrame.Offset = Context.Rsp;
  StackFrame.AddrFrame.Mode   = AddrModeFlat;
  StackFrame.AddrStack.Offset = Context.Rsp;
  StackFrame.AddrStack.Mode   = AddrModeFlat;
#elif _M_IA64
  MachineType                 = IMAGE_FILE_MACHINE_IA64;
  StackFrame.AddrPC.Offset    = Context.StIIP;
  StackFrame.AddrPC.Mode      = AddrModeFlat;
  StackFrame.AddrFrame.Offset = Context.IntSp;
  StackFrame.AddrFrame.Mode   = AddrModeFlat;
  StackFrame.AddrBStore.Offset= Context.RsBSP;
  StackFrame.AddrBStore.Mode  = AddrModeFlat;
  StackFrame.AddrStack.Offset = Context.IntSp;
  StackFrame.AddrStack.Mode   = AddrModeFlat;
#else
  #error "Unsupported platform"
#endif

  StackTrace->FrameCount = 0;

  //
  // Dbghelp is is singlethreaded, so acquire a lock.
  //
  // Note that the code assumes that 
  // SymInitialize( GetCurrentProcess(), NULL, TRUE ) has 
  // already been called.
  //
  EnterCriticalSection( &DbgHelpLock );

  while ( StackTrace->FrameCount < MaxFrames )
  {
    if ( ! StackWalk64(
      MachineType,
      GetCurrentProcess(),
      GetCurrentThread(),
      &StackFrame,
      MachineType == IMAGE_FILE_MACHINE_I386 
        ? NULL
        : &Context,
      NULL,
      SymFunctionTableAccess64,
      SymGetModuleBase64,
      NULL ) )
    {
      //
      // Maybe it failed, maybe we have finished walking the stack.
      //
      break;
    }

    if ( StackFrame.AddrPC.Offset != 0 )
    {
      //
      // Valid frame.
      //
      StackTrace->Frames[ StackTrace->FrameCount++ ] = 
        StackFrame.AddrPC.Offset;
    }
    else
    {
      //
      // Base reached.
      //
      break;
    }
  }
  
  LeaveCriticalSection( &DbgHelpLock );

  return S_OK;
}
#ifdef _M_IX86
  #pragma warning( pop )
  #pragma optimize( "g", on )
#endif
Advertisements

Categories




About me

Johannes Passing, M.Sc., living in Berlin, Germany.

Besides his consulting work, Johannes mainly focusses on Win32, COM, and NT kernel mode development, along with Java and .Net. He also is the author of cfix, a C/C++ unit testing framework for Win32 and NT kernel mode, Visual Assert, a Visual Studio Unit Testing-AddIn, and NTrace, a dynamic function boundary tracing toolkit for Windows NT/x86 kernel/user mode code.

Contact Johannes: jpassing (at) acm org

Johannes' GPG fingerprint is BBB1 1769 B82D CD07 D90A 57E8 9FE1 D441 F7A0 1BB1.

LinkedIn Profile
Xing Profile
Github Profile