HOOKIT - Callflow for C/C++ for Linux


JPROF, our Java Profiler, captures entry and exit events from method calls and uses these events to trigger the recording of various profiling statistics. Hookit is an attempt at extending the JPROF profiler so that the same can be done for compiled programs and libraries written in C. The end goal is to be able to gather callflow information from standalone C applications, Java native method calls, and calls to system libraries.

Generating Entry/Exit Events in C

Profiling in Java relies heavily on the Java Virtual Machine (JVM) notifying the profiler of when interesting events occur. The JVM also provides large amounts of useful profiling information and is conveniently implemented on all major platforms. C applications lack a similar centralized mechanism of control. However, with the appropriate tools, compiled C code can be made to act very much like a Java application (as far as the profiler is concerned).

In order to be able to get any useful profiling done, some mechanism of signaling when interesting stuff happens is needed. GCC provides some compiler options that will embed special instrumentation calls at the beginning and end of every function. All that remains is to provide an implementation of these instrumentation functions that will pass control to a profiler. All code that needs to be profiled should be (re)compiled with the -finstrument-functions option.

#gcc -finstrument-functions -c foo.c

This option causes the compiler to add a couple of function calls right after the entry and right before the exit of every function in the code. The inserted functions are documented along with the -finstrument-functions option in the GCC man page.
void __cyg_profile_func_enter (void * this_fn, void * call_site);
void __cyg_profile_func_exit (void * this_fn, void * call_site);

In both cases this_fn is the entry point address of the function being entered or exited and call_site is the address that the function will return to when it has finished executing. Since these arguments are provided by the compiler, no platform specific code is needed at this step.

The Hookit library provides implementations of these instrument functions for interacting with the JPROF profiler. This allows for the creation of instrumented code by simply recompiling the application with the instrument option and linking to libhookit.a.


Wrapping System Libraries

It is possible to generate entry/exit events for library calls by rebuilding the library with the Hookit instrumentation. However, some functions have special purposes and arguments that can provide useful profiling information. For example, malloc is called with a size specifying how much memory is requested. Intercepting malloc and free calls with a wrapper library allows the profiler to monitor memory allocations.

The simplest way to use a wrapper library is to recompile the application with the library calls wrapped directly. In order to compile a wrapper function directly, the linker must be told what is going on and given the appropriate wrapper implementation.
#gcc -Wl,--wrap -Wl,--malloc -o foo foo.c malloc_wrapper.c

This command will cause the foo.c code to link to the malloc wrapper found in malloc_wrapper.c instead of linking directly to the libc implementation. The wrapper function is called __wrap_malloc and references the real malloc function with the symbol __real_malloc (see the ld man page for details on the –wrap option). Currently the Hookit code provides a libwrapper that provides a malloc and free wrapper implementation. In order to use libwrapper, the code must be linked with options similar to the following:

#gcc -o foo foo.c -lwrapper -Wl,--wrap -Wl,--malloc -Wl,--wrap -Wl,--free



Entry/Exit Events for Standalone C Applications

In order for a standalone C application to be profiled with Hookit, two things are needed:

For example, to build a program called foo with source file foo.c with the Performance Inspector libraries in the directory /opt/Dpiperf/lib, the following command might be used.
#gcc -finstrument-functions -o foo foo.c -L/opt/Dpiperf/lib -lhookit -ldl

In the context of makefiles, the following variables would need to be modified in order to build the program foo:
CFLAGS = -finstrument
LDADD = L/opt/Dpiperf/lib -lhookit -ldl

Libc provides default implementations of some of the functions found in libHookit. It is important that if libc is linked to explicitly, that libHookit appear before libc in the link order (otherwise entry/exit events will not be generated).

To run the program, the desired options need to be set in the JPROFOPTS environment variable (these are the normal JPROF options with the exception that the ehi option must be present for Hookit events to work) and the JPROF library must be in the systems search path. One possible way to achieve this would be:

#export LD_LIBRARY_PATH=/opt/Dpiperf/lib
#export JPROFOPTS=generic,ehi

The last command should run the program and dump the JPROF profiling information.

Java Applications with Native Libraries

In order to profile the internals of a native library called by a Java application, the library should be built with the desired options from the sections on memory profiling and entry/exit profiling. Once built, the steps for invoking the profiler are a little different than what is done for standalone applications. In the case of entry/exit profiling, there is a symbol conflict that must be resolved by preloading the native library. A good way to get this preloading to occur is to set some environment variables. Assuming the library to be profiled is called and resides in the /usr/share/foo/ directory, the following variables should cause it to be preloaded at runtime.

#export LD_LIBRARY_PATH=/usr/share/foo

Regardless of which kind of profiling is being done, the profiler is started via command line arguments to the JVM. The standard options apply. In order to do entry/exit profiling on a Java application called Foo making calls to, a command like the following might be used:

#java Xrunjprof:generic,ehi Foo


Miscellaneous Notes

Hookit is currently unable to parse command line arguments passed to the application being profiled. However, all the normal command line arguments may be passed to Hookit via the JPROFOPTS environment variable. The rtdriver interface should be fully functional as well. In the event that Hookit does not support a given option, it is simply ignored. When attempting to profile native code, the options passed through the JVM should be observed by Hookit as well.

It should be noted that the Hookit code relies on the dl library and thus will likely not function correctly if an application is compiled with the Hookit instrumentation and the static compiler option.

Hookit can be configured to use other profiler libraries by setting the HOOKIT_PROFILER environment variable. Whatever library is chosen as the profiler must implement the three functions listed in the next section on bootstrapping JPROF. Similarly the HOOKIT_DEBUG environment variable may be used to display debug output from Hookit. In some cases the HOOKIT_PROFILING variable may be used to enable or disable profiling. However this is often overridden by the profiler itself.

Bootstrapping JPROF

JPROF is implemented as a dynamically linked library and is normally initialized by a call from the JVM when the application is started. Since there is no JVM when executing a C application, Hookit must jumpstart JPROF on its own. It is possible to initialize Hookit when the instrumented module is loaded by adding a special compiler attribute to the prototype:

void hookit_init (void) __attribute__((constructor));

If this is done, the compiler will ensure that the function is run at load time before main. However, this can cause problems in the event that an instrumented library is being preloaded (as is done for profiling JNI calls). Instead, Hookit checks to see if it has been initialized when hookit_worker is first called and calls hookit_init as needed. The hookit_init function attempts to load three needed functions from the profiler library:

void HookitStart(int ** OnOffSwitch, void (*hookit_workder)(int), void *));
void XJNICALL JVM_Event(JVMPI_Event * e);
void HookitFinish (void);

HookitStart is called by hookit_init at initialization time and provides the code that actually initializes JPROF without the JVM. A link is also established between JPROF and Hookit via the OnOffSwitch that allows JPROF to control whether or not profiling is actually triggered. The hookit_worker function is also (optionally) passed to JPROF to allow for runtime patching of the code (currently not implemented).

The Hookit implementations of the instrument functions inserted by the compiler make calls to the hookit_worker function at every function entry or exit.

void hookit_worker (int event, void * addr);

The hookit_worker function is responsible for packing the entry point address and the event type (entry or exit) into the JVMPI_Event format recognized by JPROF (basically packages C events as Java events). This new event object is then passed on to the JVM_Event function in JPROF.

JPROF also needs to be notified when the application has completed execution. This is normally done by the JVM. To accomplish this for standalone C applications, the Hookit library provides a function called hookit_cleanup with compiler attributes requesting that this function is run after main but before program termination completes.

void hookit_cleanup (void) __attribute__((destructor));

The cleanup function calls HookitFinish to signify JPROF that the program is terminating. JPROF then cleans things up, flushes any logging information to disk, and discontinues profiling. Finally, hookit_cleanup unloads the profiler library and exits.

Handling Hookit Events in JPROF

When a Hookit event enters JPROF it still needs some work before it can be routed though the statistics gathering algorithms. Most notably, the addresses need to be resolved to human readable names and the JPROF data structures need to be updated so that they are aware of the newly discovered symbols.

In order to do the address-to-name resolution, JPROF makes calls to the A2N library (also provided with Performance Inspector). With the introduction of Hookit, A2N was updated to be able to load process memory maps from the operating system as needed. Currently these maps are retrieved via the /proc file system for increased easy and portability. However, this could alternatively be done via the dl_iterate_phdr function (see /usr/include/link.h for details). During the initialization phase, Hookit notes which process it is and notifies A2N to prepare to do name resolution for the current process ID.

The JPROF data structures are geared for Java applications and expect a program to be organized by classes and methods. In order to be consistent with this paradigm, Hookit treats program modules (DLL's, etc) as classes and functions within each module as the methods belonging to these classes. Module base addresses and function entry point addresses are used for the unique class and method id's (respectively). It is assumed that the JVM generates these ids based on some sort of memory address. Therefore any method for generating these ids based on an object's virtual memory address should prove to be unique. Each time an entry or exit event occurs, Hookit ensures that the class and method data structures are updated appropriately before attempting to log statistics information for the event. In this way Hookit discovers the program structure on the fly rather than when signaled by the JVM at class load time.

Dynamically loaded modules provide a special challenge for name resolution since the symbols may not be available during program initialization. One possible approach for handling this would be to reinitialize A2N if a lookup fails. A less naive approach would be to detect when a dynamic library is being loaded and to insert its memory mapping information into A2N's database at load time. However, this approach proves to be somewhat complex. Currently Hookit handles this case the “naive” way, reloading the memory map and symbols in the event of a failure (only one reload attempt occurs per failure).

To handle name resolution, class and method generation, etc., Hookit passes events to a special preprocessing routine before handing them off to the normal tree building code. The reh_hookit function performs all of these tasks and then passes the completed “Java event” to the rehmethods function to record profiling information associated with the event.

void reh_hookit (JVMPI_Event * e, thread_t * tp);

Hookit events don't really fall into the normal classifications of Java events since they aren't Java events. For this reason Hookit events are referred to as “Functions” in the log output. The formatting convention is to print F:module_name.function_name(). For example:


Note that the function signature is unspecified. This is simply because retrieving signatures for compiled functions is not readily done and not considered to be worth the effort.

Java and all Java-based trademarks are trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.
IBM is a trademark of International Business Machines Corporation in the United States, other countries, or both
Other company, product or service names may be trademarks or service marks of others.
MMX, Pentium, and ProShare are trademarks or registered trademarks of Intel Corporation in the United States, other countries, or both.