1

I've got simple C++ Windows program that prints stack trace. Tried different codes I found on internet and still getting same results. When optimizations are off, things get displayed correctly. Once I turn them on(or switch from Debug build to Release build in VS), specifically I use /O2(but doesn't work with other optimization flags too), only main function gets printed on screen even though there should be more functions on stack. I thought that maybe that function gets inlined but I think that this isn't the case, I can turn any optimization setting on, like force inline anywhere possible etc. and just works fine, once I turn on /O2, /O1 etc. it shows just main. PDB files are generated so that shouldn't cause any problems.

I'd appreciate any help, or if anyone knows some other way/library to show stack trace.

Output with optimizations off, you can see func2 and func3 displayed correctly:

Line: 78 | Function name: func2 | Module name: D:\ConsoleApplication3\Debug\ConsoleApplication3.exe
Line: 83 | Function name: func3 | Module name: D:\ConsoleApplication3\Debug\ConsoleApplication3.exe
Line: 88 | Function name: main | Module name: D:\ConsoleApplication3\Debug\ConsoleApplication3.exe
Line: 78 | Function name: invoke_main | Module name: D:\ConsoleApplication3\Debug\ConsoleApplication3.exe
Line: 288 | Function name: __scrt_common_main_seh | Module name: D:\ConsoleApplication3\Debug\ConsoleApplication3.exe
Line: 331 | Function name: __scrt_common_main | Module name: D:\ConsoleApplication3\Debug\ConsoleApplication3.exe
Line: 17 | Function name: mainCRTStartup | Module name: D:\ConsoleApplication3\Debug\ConsoleApplication3.exe
Line: 0 | Function name: BaseThreadInitThunk | Module name: C:\WINDOWS\System32\KERNEL32.DLL
Line: 0 | Function name: RtlGetFullPathName_UEx | Module name: C:\WINDOWS\SYSTEM32\ntdll.dll
L

Output with /O2 flag, func2 and func3 are missing:

Line: 88 | Function name: main | Module name: D:\ConsoleApplication3\Release\ConsoleApplication3.exe
Line: 288 | Function name: __scrt_common_main_seh | Module name: D:\ConsoleApplication3\Release\ConsoleApplication3.exe
Line: 0 | Function name: BaseThreadInitThunk | Module name: C:\WINDOWS\System32\KERNEL32.DLL
Line: 0 | Function name: RtlGetFullPathName_UEx | Module name: C:\WINDOWS\SYSTEM32\ntdll.dll
Line: 0 | Function name: RtlGetFullPathName_UEx | Module name: C:\WINDOWS\SYSTEM32\ntdll.dll

This is code I am currently using, this runs only on x86:

#include <windows.h>
#include <iostream>
#include <dbghelp.h>

void stacktrace()
{
    DWORD machine = IMAGE_FILE_MACHINE_I386;

    HANDLE process = GetCurrentProcess();
    HANDLE thread = GetCurrentThread();
    CONTEXT context = {};
    context.ContextFlags = CONTEXT_FULL;
    RtlCaptureContext(&context);

    SymInitialize(process, NULL, TRUE);
    SymSetOptions(SYMOPT_LOAD_LINES);

    STACKFRAME frame = {};
    frame.AddrPC.Offset = context.Eip;
    frame.AddrPC.Mode = AddrModeFlat;
    frame.AddrFrame.Offset = context.Ebp;
    frame.AddrFrame.Mode = AddrModeFlat;
    frame.AddrStack.Offset = context.Esp;
    frame.AddrStack.Mode = AddrModeFlat;

    while (StackWalk(machine, process, thread, &frame, &context, NULL, SymFunctionTableAccess, SymGetModuleBase, NULL))
    {
        DWORD64 functionAddress;
        std::string moduleName;
        std::string functioName;
        std::string file;
        unsigned int _line = 0;

        functionAddress = frame.AddrPC.Offset;

        DWORD moduleBase = SymGetModuleBase(process, frame.AddrPC.Offset);
        char moduleBuff[MAX_PATH];
        if (moduleBase && GetModuleFileNameA((HINSTANCE)moduleBase, moduleBuff, MAX_PATH))
        {
            moduleName = moduleBuff;
        }

        char symbolBuffer[sizeof(IMAGEHLP_SYMBOL) + 255];
        PIMAGEHLP_SYMBOL symbol = (PIMAGEHLP_SYMBOL)symbolBuffer;
        symbol->SizeOfStruct = (sizeof IMAGEHLP_SYMBOL) + 255;
        symbol->MaxNameLength = 254;

        if (SymGetSymFromAddr(process, frame.AddrPC.Offset, NULL, symbol))
        {
            functioName = symbol->Name;
        }

        DWORD  offset = 0;
        IMAGEHLP_LINE line;
        line.SizeOfStruct = sizeof(IMAGEHLP_LINE);

        if (SymGetLineFromAddr(process, frame.AddrPC.Offset, &offset, &line))
        {
            file = line.FileName;
            _line = line.LineNumber;
        }

        std::cout
            << "Line: " << _line 
            << " | Function name: " << functioName
            << " | Module name: " << moduleName
            << std::endl;
    }

    

    SymCleanup(process);
}

void func2()
{
    stacktrace();
}

void func3()
{
    func2();
}

int main()
{
    func3();
    system("pause");
    return 0;
}
Michaelt LoL
  • 452
  • 5
  • 10
  • 7
    Yes, that's what optimizations do. Among (many) other things, they get rid of those pesky stack frames that slow your program down. Inlining, tail recursion, tons of things in a C++ optimizer will alter the stack trace. Debugging your program and running optimizations are mutually exclusive. Either compile in "debug" mode and use your debugger, or compile with optimizations and accept that the compiler just made mincemeat of your beautiful call stack. There's no in between. – Silvio Mayolo Jun 13 '22 at 17:58
  • That makes sense, thank you for answer. But I saw games that are written in C++ and show stack trace when they crash and I don't believe that they have optimizations disabled, that would be terrible. Maybe they use some other way to display it? – Michaelt LoL Jun 13 '22 at 18:02
  • @MichaeltLoL - The games probably don't have empty functions like `func3() { func2(); }`. There is no need for a stackframe there, because the function doesn't use it. – BoP Jun 13 '22 at 18:06
  • If the stack frame does nothing, it's going to get optimized. If it ends in a tail call, that might also get optimized. Trying to "trick" the optimizer with small trivial functions like this isn't going to work; it's smarter than you when it comes to C++ (take it from someone who's tried). If you're trying to experiment with printing stack traces from WinAPI, then just don't use optimizations. It's only going to muddy the waters unless you take the time to *really* understand your compiler's optimizer. – Silvio Mayolo Jun 13 '22 at 18:11
  • @BoP you were right, after I added something to func3(), it got displayed there. But when I inline it, it obviously dissapears from there. – Michaelt LoL Jun 13 '22 at 18:13
  • @SilvioMayolo I undestand. Unfortunately I'm not just playing with it, I need it for my game because it's really hard to debug when you don't even know what happened on client's side, obviously they won't install Visual Studio just to help me debug it. – Michaelt LoL Jun 13 '22 at 18:18
  • If it's a problem that occurs in debug mode as well, then just use debug mode for debugging (that's what it's for, after all). If it's a problem that only occurs in release mode and can't be reproduced in debug mode, then it's likely you've hit [undefined behavior](https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior) somewhere and it's tripping up the optimizer. In that case, stack traces won't do you any good and you need to be using a safety or correctness tool like valgrind to find the underlying UB (which may not even be at the crash site) – Silvio Mayolo Jun 13 '22 at 18:20
  • 1
    you can `-fno-omit-frame-pointer`. Not sure of the MSVC command for that. – Taekahn Jun 13 '22 at 18:52
  • It was really caused just by that I was testing it on empty code. When I put it in my actual game, it shows everything correctly even with Release and all optimizations on. – Michaelt LoL Jun 13 '22 at 19:25
  • When @SilvioMayolo says the optimizer "gets rid of those pesky stack frames", he doesn't mean that it gets rid of the information the stack trace uses to find stack frames, it means that it optimizes the code so there are fewer function calls on the stack at a time, either because they were never there (inlining) or because they got removed when no longer needed (tail call). You can use `declspec(noinline)` to prevent the optimizer from doing one of those things, which will make your code a little slower but make your call stacks closer to the source code. – Ben Voigt Jun 13 '22 at 21:35
  • Whatever degree of optimization you do or don't use, the call stack is showing you the actual final structure of the code that's executing. – Ben Voigt Jun 13 '22 at 21:35
  • *"I saw games that are written in C++ and show stack trace when they crash"* - Do you actually *want* a stack trace, rather than something that is actually useful: [Minidump Files](https://learn.microsoft.com/en-us/windows/win32/debug/minidump-files). No need to deploy debug symbols, and you'll instantly get **vastly** more useful information. And you don't need to write a single line of code: [Collecting User-Mode Dumps](https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps). – IInspectable Jun 14 '22 at 16:17

0 Answers0