DEV Community

Cover image for Runtime Stacktraces for Native Programming Languages
Marcelo Silva Nascimento Mancini
Marcelo Silva Nascimento Mancini

Posted on

Runtime Stacktraces for Native Programming Languages

Ever wondered what would happen if you had the debugger power at runtime?

For native language programmers, one really common problem to hit, is to dereference a null pointer. That almost always results in a crash, and this problem can get quite hard to find if you need to find inside a big project, specially if you're not this project's original developer, but, fear not, for here I am to bring clarification on how you can get useful debug information without needing to actually attach a debugger.

In Java, a solution to that already exists, being called as NullPointerException, which is pretty useful, as it shows exactly what, where and when this crash happens, and this is what we are going to achieve in this article.

Detecting a crash (Microsoft C++ Runtime Exception)

Before getting to the meaty part, whenever you crash your program, your program will basically exit, and return a negative error code, or a kill signal. THe problem about that is that you're not going to get any meaningful error message, so, the only way to understand is attaching a debugger, but that not may be a possibility every time, specially if this bug occurs at random moments. So, the first thing to do is detecting the crash, for, this, we will use the Win32 API to find it:

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
  LPTOP_LEVEL_EXCEPTION_FILTER
);
Enter fullscreen mode Exit fullscreen mode

SetUnhandledExceptionFilter function (errhandlingapi.h) — Win32 apps | Microsoft Learn — Look for more details

So, what this function does is setting a callback for when any kind of the runtime exception happens, which means you’ll have a little of information about that, for example, the code shown above 0xC..05. The first difference is now that we can actually decide if we want to continue running or handle this error.

So, let's go to the callback part:

extern (Windows) LONG TopLevelExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
{
  debug
  {
      //On D, we could simply finish here by calling throw, since it automatically get us the stack trace
      throw new Exception("Caught Exception (0x"~toHex(pExceptionInfo.ExceptionRecord.ExceptionCode)~")");

      //But continue reading to understand how to get the stack trace manually
  }
  //Here you may control whether you continue executing or not
  return EXCEPTION_CONTINUE_SEARCH;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we can get the error code by accessing (in D syntax, you can drop the -> to simply . )

Getting the stack trace information

Before jumping to directly to the code, we also need to include 2 system libraries, how this is achieved heavily depends on your programming language or build system, in D, it is possible to include the library directly from the code, which is the way I’m going to use:

debug version(Windows)
{
    pragma(lib, "psapi.lib");
    pragma(lib, "dbghelp.lib");
}
Enter fullscreen mode Exit fullscreen mode

You may also include this on your build system with -Lpsapi -Ldbghelp , directly from the code is clearer for me so it is the way I’m using. Those libraries should be found on your system since they should be on your default library path.

Now, that’s the part where you will increase a lot of usability:

extern (C) export void backtraced_Register()
{
    debug
    {
        //Initializes the .pdb reading process
        SymInitialize(GetCurrentProcess(), null, true);
        //You will want to get line information on the files
        SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_DEBUG);
        SetUnhandledExceptionFilter(&TopLevelExceptionHandler);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, for the verbose part, we are going to allocate some memory to store the stack where the exception occurred:

debug void printStackTrace()
{
    enum MAX_DEPTH = 256;
    void*[MAX_DEPTH] stack;

    HANDLE process = GetCurrentProcess();
    ushort frames = RtlCaptureStackBackTrace(0, MAX_DEPTH, stack.ptr, null);
    SYMBOL_INFO* symbol = cast(SYMBOL_INFO*) calloc((SYMBOL_INFO.sizeof) + 256 * char.sizeof, 1);
    symbol.MaxNameLen = 255;
    symbol.SizeOfStruct = SYMBOL_INFO.sizeof;

    IMAGEHLP_LINEA64 line = void;
    line.SizeOfStruct = SYMBOL_INFO.sizeof;

    DWORD dwDisplacement;
    for (uint i = 0; i < frames; i++)
    {
        //Gets the symbol from the current loop frame
        SymFromAddr(process, cast(DWORD64)(stack[i]), null, symbol);
        //Get advanced file information
        SymGetLineFromAddr64(process, cast(DWORD64)(stack[i]), &dwDisplacement, &line);

        //If you're programming in C++ or D, you may need a `demangle` function
        //That means it will make it easier to read the symbol name
        //Also, the syntax of [0..symbol.NameLen] is basically for creating a 
        char[] funcName = demangle(symbol.Name.ptr[0..symbol.NameLen]);
        auto fname = line.FileName;
        auto lnum = line.LineNumber;


        fprintf(stderr, "%s:%i - %.*s\n", fname, lnum, cast(int)funcName.length, funcName.ptr);
    }

    free(symbol);
}
Enter fullscreen mode Exit fullscreen mode

And now, with that, you will be able to print your stack trace inside the UnhandledExceptionFilter, which means you’ll get this kind of stack trace:

Result:

Caught exception (0xC0000005)
-------------------------------------------------------------------+
G:\HipremeEngine\source\hip\systems\game.d:251 - hip::systems::game::GameSystem::addScene::__lambda2
G:\HipremeEngine\modules\util\source\hip\util\concurrency.d:307 - hip::util::concurrency::HipWorkerPool::addOnAllTasksFinished
G:\HipremeEngine\modules\assets\source\hip\assetmanager.d:955 - hip::assetmanager::HipAssetManager::addOnLoadingFinish
G:\HipremeEngine\source\hip\systems\game.d:257 - hip::systems::game::GameSystem::addScene
G:\HipremeEngine\source\hip\systems\game.d:207 - hip::systems::game::GameSystem::startGame
G:\HipremeEngine\source\app.d:238 - app::gameInitialize
G:\HipremeEngine\source\app.d:214 - app::HipremeMain::__lambda4
G:\HipremeEngine\source\hip\global\gamedef.d:85 - hip::global::gamedef::loadDefaultAssets
G:\HipremeEngine\source\app.d:218 - app::HipremeMain
G:\HipremeEngine\source\app.d:296 - app::D main
G:\HipremeEngine\source\app.d:296 - _d_run_main2
G:\HipremeEngine\source\app.d:296 - _d_wrun_main
G:\HipremeEngine\tools\user\build_selector\D\ldc2-1.33.0-beta1-windows-x64\import\core\internal\entrypoint.d:32 - app::wmain
D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288 - __scrt_common_main_seh
D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288 - BaseThreadInitThunk
D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288 - RtlUserThreadStart
Enter fullscreen mode Exit fullscreen mode

You can see, you get the entire stack trace on how the execution reached this exception, which makes your program a lot more reliable now, specially if you’re dealing with other people code on a new project, or for a DLL.

Addendum: Getting stack trace from a DLL

If you’re running a DLL, specially on a system with hot code reloading, you won’t be able to get stack information (yes, once the exception handler is initialized, it works among both executable and dll code). To solve this problem, you need to load your dependency.pdb along your dependency.dllfile, this, of course, happens whenever you’re running inside your Debugger, but this is not true when you’re on a standalone executable calling LoadLibrary . For that to happen, you need some additional code, which I’ll call DebugLoadLibrary .

void* DebugLoadLibrary(const char* libName)
{
    import core.sys.windows.winbase;
    import core.sys.windows.windef;
    void* ret = LoadLibrary(libName);
    debug
    {
      import core.sys.windows.psapi;
      MODULEINFO moduleInfo;
      GetModuleInformation(GetCurrentProcess(), ret, &moduleInfo, MODULEINFO.sizeof);
      if(!SymLoadModuleEx(GetCurrentProcess(), null, libName, null, cast(ulong)moduleInfo.lpBaseOfDll, moduleInfo.SizeOfImage, null, 0))
      {
        throw new Error(format("Failed to load the DLL named ", libName, " pdb"));
      }
    }
    return ret;
}
Enter fullscreen mode Exit fullscreen mode

With that, if there is some crash happening inside your DLL file, you will get some useful information (its stack trace), so, you can easily debug it.

Top comments (0)