Skip to content

Add support for modules loaded at runtime #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

bkloster-sma
Copy link

I noticed that cpptrace could not resolve symbols from DLLs that were loaded with LoadLibrary after already generating at least one stacktrace. According to the Microsoft documentation for Symbol Handler Initialization, one should call SymLoadModuleEx for this case.

I've implemented a new function cpptrace::experimental::load_symbols_for_file(const std::string& name) that does this.

Benjamin Kloster added 3 commits May 22, 2025 09:42
For now, this function is only implemented for Windows when CPPTRACE_GET_SYMBOLS_WITH_DBGHELP is
defined. For other configurations, it does nothing.

load_symbols_for_file will load symbols from the given file with SymLoadModuleEx. It should be
called when a module was loaded at runtime with LoadLibrary. Otherwise, the symbols for frames
inside that loaded module will not be resolvable.
Copy link
Owner

@jeremy-rifkin jeremy-rifkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for taking the time to contribute to cpptrace and put together a thoughtful implementation! I've left some comments below

README.md Outdated
@@ -939,6 +940,12 @@ registered with cpptrace.

[jitci]: https://sourceware.org/gdb/current/onlinedocs/gdb.html/JIT-Interface.html

## Loading Libraries at Runtime

When loading libraries at runtime, e.g. with `dlopen` or `LoadLibrary`,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this isn't needed for linux/macos I believe, it'd be good to specify that this is only needed on windows for LoadLibrary

void load_symbols_for_file(const std::string& name) {
HMODULE module = GetModuleHandleA(name.c_str());
if (module == NULL) {
std::fprintf(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of printing it would be good to throw an internal_error, printing on error from a library can be problematic.

}

MODULEINFO moduleInfo;
if (0 == GetModuleInformation(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Please rephrase as if(call(...) == 0) - I try to avoid yoda conditions

}
#else

#include <cpptrace/utils.hpp>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This include isn't needed since it appears above

namespace cpptrace {
namespace experimental {

void load_symbols_for_file(const std::string& name) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole file is really meant for CPPTRACE_GET_SYMBOLS_WITH_DBGHELP stuff, it'd be better to define this for non-windows elsewhere. Alternatively, does this function make sense to have on platforms other than windows?

@bkloster-sma
Copy link
Author

Thanks for the comments!

If we make load_symbols_for_file a configuration-dependant function (i.e. it is not even declared when CPPTRACE_GET_SYMBOLS_WITH_DBGHELP is not defined), I wonder if we should change its signature and name to load_symbols_for_module(HANDLE hModule). After all, if a library user calls LoadLibrary, they already have the handle, so there would be no need for load_symbols_for_X to retrieve that handle again with GetModuleHandle.

@jeremy-rifkin
Copy link
Owner

jeremy-rifkin commented May 23, 2025

I think that's reasonable, it would simplify implementation a lot. In the header what we'd want is the load_symbols_for_module function declared only for _WIN32 and then it can be defined to do something for dbghelp or a no-op if for some reason a different back-end is configured on windows

Benjamin Kloster added 4 commits May 26, 2025 13:24
The function now takes an HMODULE handle to the DLL instead of the filename. This should make it
more explicit that this is a Windows-only function. It also reduces the (rather minor) risk of
passing a different filename into the function that doesn't actually refer to a loaded module.

It is still necessary to get the filename with GetModuleFileName, but as long as the module handle
is valid, this should rarely be an issue.
Unfortunate, because <windef.h> would have been more lightweight, but apparently expects additional
defines to be already set before being included.
Having the HMODULE data type in the public interface requires including <Windows.h>, since there
doesn't seem to be a "proper" way of forward declaring HMODULE. However, including <Windows.h>
comes with a host of problems. For one, its sheer size. For another, it defines some popular names
like `min` as macros, which breaks `std::min` and similar.

Passing a filename instead of the module handle seems *slightly* less safe due to the risk of
passing a string that doesn't refer to a module, but it's also much less hassle.
@jeremy-rifkin
Copy link
Owner

Revert to load_symbols_for_file to avoid including Windows.h

I think it should be possible to do load_symbols_for_module without including windows.h (I appreciate your thought about this #include). HMODULE is just a HANDLE which is just a void* so I think CPPTRACE_EXPORT void load_symbols_for_module(void* hModule); is good

@bkloster-sma
Copy link
Author

bkloster-sma commented May 27, 2025

Ha! I had the same idea after perusing this page. But the doc is wrong. A static assertion static_assert(std::is_same_v<HMODULE, void*>) fails, and when I trace back the definition of HMODULE in my headers, I find this in minwindef.h:

DECLARE_HANDLE(HINSTANCE);
typedef HINSTANCE HMODULE;      /* HMODULEs can be used in place of HINSTANCEs */

And DECLARE_HANDLE(HINSTANCE) expands to this:

struct HINSTANCE__ {
    int unused;
}; typedef struct HINSTANCE__* HINSTANCE

Now, if you really want to, we could still use the load_symbols_for_module(void*) signature, because HMODULE implicitly converts to void*. However, I see two arguments against it:

  1. Pretty much all pointers implicitly convert to void*, not just HMODULE. So an inattentive user could pass in a const char* to the filename, causing undefined behaviour.
  2. After writing the load_symbols_for_module(HMODULE) implementation, I realized that we need the filename anyway. So the implementation doesn't actually get simpler, we just exchange GetModuleHandle for GetModuleFileName.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants