[Mono-dev] Solving the profiler loading problem
Alex Rønne Petersen
alex at alexrp.com
Wed Sep 27 13:02:32 UTC 2017
I was recently taking a look at this bug:
The problem is that we link libmono-profiler-log.so to
libmonosgen-2.0.so, even when the mono executable is statically
linked. What happens is that, when we load libmono-profiler-log.so
into the process of a statically linked mono, libmonosgen-2.0.so is
loaded in addition to the mono executable (which already contains all
the libmono code). The dynamic linker helpfully figures out that all
of the libmono symbols that the profiler uses are already present in
the mono executable, so none of the code or data in the newly loaded
libmonosgen-2.0.so is actually used. However, this no longer holds
true when library constructors enter the mix. When libmonosgen-2.0.so
is loaded, any library constructors that it contains, or which are
present in any libraries it links to, will be executed.
C code rarely makes use of library constructors (which is why this
hasn't been a problem for a 'normally' built libmonosgen-2.0.so so
far), but they're very common in C++ code; constructors for global C++
objects are implemented with them. LLVM, for example, makes use of
them in many places. So, when we've already loaded libLLVM*.so from
the static mono executable and therefore executed the library
constructors contained within, running those library constructors
again when we load libmono-profiler-log.so just kinda breaks
Below are a few different ways we can solve this problem.
# Always build a dynamically linked mono executable
If we never statically link mono with libmonosgen-2.0.so, we can
simply keep linking libmono-profiler-log.so to libmonosgen-2.0.so and
everything will just work. When the profiler is loaded, the dynamic
linker realizes that libmonosgen-2.0.so is already loaded, so it'll do
the right thing.
The downside of this approach is that working with a dynamically
linked executable can be a pain. In particular, you need to mess with
LD_LIBRARY_PATH to get mono to use a libmonosgen-2.0.so in your
working tree rather than the one from your prefix. libtool tries to
alleviate this problem by turning the mono file in the working tree
into a script that invokes the actual executable with the right
dynamic linker setup to use the in-tree libraries. It's also possible
to use libtool --mode=execute gdb mono ... to run in-tree mono in a
If we go this route, we'd probably need to enhance
runtime/mono-wrapper to support all of our in-tree use cases (such as
running in a debugger) since nobody wants to type out the libtool
commands by hand.
There may be performance considerations with this approach, but from
what I've seen, there's already some interest in always building
libmonosgen-2.0.so as PIC to speed up the build process (no need for
building a separate static version of everything). So, I'm not too
worried about this aspect.
# Link libmono-profiler-log.so to libmonosgen-2.0.so only when needed
The idea here is that, in our build system, we can easily determine
whether we're building a statically or dynamically linked mono
executable. So, if we're building a statically linked mono (which is
the norm currently), we can simply avoid linking
libmono-profiler-log.so to libmonosgen-2.0.so. This leaves the
profiler with some undefined symbols, but the dynamic linker will sort
that out by resolving them to the symbols in the mono executable.
This is definitely the simplest approach and it's probably what I'll
end up doing at least as a temporary fix. However, it's a pretty poor
solution for third-party users of the profiler API. Those developers
cannot possibly know whether their profiler module will end up being
used with a statically or dynamically linked mono executable. So,
effectively, they have to build two separate profiler modules, one for
each case. This is wasteful for obvious reasons.
I care a great deal about making profiler development for Mono as
simple and painless as possible (see the new profiler API, for
example). This approach goes completely counter to that, so I'm not a
big fan of it.
# Pass an API vtable to a profiler module's init function
This would basically be JNI. Just like how JNI passes a JNIEnv pointer
to native functions, we'd pass a MonoEnv pointer to
mono_profiler_init_<name> (). The profiler would do all Mono API calls
through this vtable. Long term, we could even transition the entire
Mono API to use this mechanism instead of exposing functions directly
from the library.
I do like this idea since it would mean that profiler modules would
never have to explicitly link to libmonosgen-2.0.so, but there are at
least three potential issues with it:
1. Having all API calls go through a vtable could have some serious
performance implications. We use a lot of low-level functions from
libmono in the profiler (e.g. mono_memory_barrier (),
mono_hazard_pointer_get (), MONO_LLS_FOREACH, etc).
2. Macros such as MONO_LLS_FOREACH (which integrates closely with C's
break/continue statements) can't be expressed in such a vtable.
3. The log profiler uses around 160 functions from the Mono API.
That's just the amount of API functions that would need to be added to
this vtable for the log profiler to work; exposing the entire Mono API
through this vtable would be a ton of work.
This list is by no means exhaustive; there are probably solutions I
haven't thought of, and pros/cons of the above solutions that I
haven't considered. Please let me know your thoughts.
More information about the Mono-devel-list