zvrba/ tips/ c++ dtrace

Using DTrace SDT provider with C++

UPDATE One of my blog readers has pointed me to a much better way of accomplishing the same thing. Thanks!

The DTrace SDT provider allows the developer to put probe points into the source code. When the probe fires, the dtrace utility shows the arguments with which the probe has been invoked. Akin to printf() debugging, but much more powerful - the probes may be turned on or off during the program execution.

Let's say that you have testprov.d file with some probes defined:

provider testprov {
  probe some_event(int);
};

A program that wants to signal occurence of some_event would do so as follows:

#include <sys/sdt.h>

void f(void)
{
  // some code
  DTRACE_PROBE1(testprov, some_event, 42);
}

The problem arises when you have a C++ program, for example something as

#include <sys/sdt.h>

namespace some_namespace {
  void f()
  {
    DTRACE_PROBE1(testprov, some_event, 42);
  }
}

If you follow the procedure described in the DTrace manual:

$ dtrace -G -32 -s testprov.d src1.o src2.o ...
$ CC -o testprov testprov.o src1.o src2.o ..

you will get linking errors referring to some mangled C++ names containing __dtrace inside. This is the typical definition of a DTRACE_PROBE macro, taken from sys/sdt.h file:

#define DTRACE_PROBE(provider, name) {            \
extern void __dtrace_##provider##___##name(void); \
  __dtrace_##provider##___##name();               \
}

When compiled in a C++ program, this will declare an external function with C++ linkage. But DTrace expects C linkage! Changing the extern declaration in the macro to

extern "C" void __dtrace_##provider##___##name(void);

doesn't work either, because the C++ compiler gives an error saying that linkage declarations can only be specified at the top-level scope. The only other way to specify linkage is to declare function with C linkage before it is used, and to remove the internal declaration.

Thus, I have copied the sys/sdt.h file to my working directory and changed all DTRACE_PROBE macros to read similar to [notice the deleted extern function prototype]:

#define DTRACE_PROBE(provider, name) {            \
  __dtrace_##provider##___##name();               \
}

In addition, I have defined another macro

#define DLINKAGE(provider, name) extern void __dtrace_##provider##___##name(...)

that produces just the function prototype, but does not invoke it. The small example program listed above now looks like [some additional stuff has been added so that it can be compiled]:

// This includes sdt.h with DTRACE macros modified as explained above.
#include "sdt.h"

#define DLINKAGE(provider, name) extern void __dtrace_##provider##___##name(...)

extern "C" {
  DLINKAGE(testprov, some_event);
}

namespace some_namespace {
  void f()
  {
    DTRACE_PROBE1(testprov, some_event, 42);
  }
}

int main()
{
  some_namespace::f();
  return 0;
}

Save this file into main.c, compile it and link it as follows:

$ CC -c main.cc
$ dtrace -G -32 -s testprov.d main.o
$ CC testprov.o main.o -o main

If you run the program nothing will happen. Now write a small D script to test whether the thing works [save into dscript.d]:

testprov$target:::some_event
{
  printf("some_event fired, value=%d\n", arg0);
}

And run as follows; the role of the c++filt program is to demangle ugly C++ names into something readable. This is what you should get:

$ dtrace -s dscript.d -c ./main | c++filt
dtrace: script 'dscript.d' matched 1 probe
dtrace: pid 13050 has exited
CPU     ID                    FUNCTION:NAME
0  38977 void some_namespace::f():some_event some_event fired, value=42