Wednesday, December 1, 2010

Intercepting Linux system calls: Part II

[Update] This code can now be found on github (ntrace)

Continuing from the first part of this discussion, I thought it would be best to first show what strace does and how LD_PRELOAD compares to that before continuing on to a full-blown example of using one over the other.

strace
strace is a utility that either attaches to an already running process or spawns an entirely new process and monitors that process' system calls and signals. This is done via ptrace and thus provides an almost scary level of control over the process being monitored. This is precisely why I prefer to use strace over LD_PRELOAD in most cases.

LD_PRELOAD
LD_PRELOAD is an environment variable that can contain a custom library to be loaded before others when invoking a program. This allows for behaviors such as intercepting library calls (e.g. libc) and invoking custom behavior around or instead of the intended target code. LD_PRELOAD does not allow a user (without extended effort) to poke at the registers and data sections of a running process or to receive signals destined to the process.

To give an idea of how you might use each of these in context, lets say that you have a program that will print updates to the screen whenever it receives events internally. As a matter of black-boxing that application, you would like to know how often these events are occurring but you do not have access to the source code and there is no timing information other than some text output to the screen. For example:

ezpz@sandbox:~/blog > ./event_handler
updated again
updated again
updated again
updated again
...

All you can see is a stream of notifications. What you'd like to see, however, is some indication of time with (or instead of) the meaningless update. strace is perfect for this as it gets notified each time a write is about to occur and prints a timestamp along with that data. Here is an example that prefixes traced calls with a seconds/microseconds timestamp and only responds to the write system call.

The command is:

strace -ttt -e trace=write -o strace.log ./event_handler

That command produces a log file with the following contents:

1291244916.592714 write(1, "updated again\n"..., 14) = 14
1291244917.024000 write(1, "updated again\n"..., 14) = 14
1291244917.217090 write(1, "updated again\n"..., 14) = 14
1291244917.354294 write(1, "updated again\n"..., 14) = 14
...

Now you can easily see when each event fires and with a simple script could determine with what frequency these events most often occur.

Similarly, you can use LD_PRELOAD to do this. In this particular case there is significantly more work to be done (another reason to opt for strace first) but bear with me for the illustration.

In the LD_PRELOAD case you need to provide an shared library that can be loaded and used in place of the libraries you want to modify. Let's set that up: first, the interception code:

#define _GNU_SOURCE
#include <sys/time.h>
#include <stdio.h>
#include <dlfcn.h>

/*
 * Use the same signature as libc's puts when defining our interceptor
 * and function pointer
 *  int puts(const char *s);
 */
typedef int (*libc_puts)(const char *);

/*
 * Provide our version of puts here
 */
int puts (const char *s) {
    /* grab the actual puts */
    libc_puts original = dlsym (RTLD_NEXT, "puts");

    /* prefix the output with our timestamp */
    struct timeval tv;
    gettimeofday (&tv, 0);
    printf ("[%ld.%06ld] ", tv.tv_sec, tv.tv_usec);

    /* pass the arguments through to the libc call */
    return original(s);
}

Once that is built you can use the shim by

ezpz@sandbox:~/blog > LD_PRELOAD=./shim.so ./event_handler

which yields

ezpz@sandbox:~/blog > LD_PRELOAD=./shim.so ./event_handler
[1291246609.278321] updated again
[1291246609.709342] updated again
[1291246609.902186] updated again
[1291246610.039169] updated again
...

So you have an equally viable solution in either case. The point here was not necessarily to show why either of the two of these methods is better than the other in this contrived example but to expose the fact that you can do similar things with each. The strace solution here does not allow for injecting your own code into a running process but it does capture the exact thing you want without the overhead of having to build a shared library to get it done.

I'll continue next time with why you might need to modify program behavior to get data that is just not available with strace.

No comments :

Post a Comment