Introduction
Let's say that you've been looking at the DTrace Toolkit, and you notice that the author has included this BEGIN probe in just about every script:
dtrace:::BEGIN
{
printf("Tracing... Hit Ctrl-C to end.\n");
}
It occurs to you that the author could save a lot of typing if there were an action that would perform that specific printf(). You decide that you want to add this action to DTrace, and you choose to call it the brendan() action. This tutorial describes the steps you would need to take.
Userland changes
There are both userland and kernel components to this. We'll start with the userland components.
usr/src/lib/libdtrace/common/dt_open.c
First, you need to declare the keyword for the new action. You'll do this in the _dtrace_globals[] array in usr/src/lib/libdtrace/common/dt_open.c. This table is described elsewhere; the entry for the brendan() action would look like this:
{ "brendan", DT_IDENT_ACTFUNC, 0, DT_ACT_BRENDAN, DT_ATTR_STABCMN, DT_VERS_1_7,
&dt_idops_func, "void()" }
This is similar to the entry for printf(). The differences are that we're using a different ID for this action (DT_ACT_BRENDAN), that the DTrace version number (DT_VERS_1_7) is newer, and that the function prototype matches the proposed brendan() action.
With respect to the version number, we need to increment the minor number to reflect the fact that we've introduced a new reserved word, thus possibly breaking existing scripts. The DTrace versions are listed in the same file, usr/src/lib/libdtrace/common/dt_open.c:
/*
* The version number should be increased for every customer visible release
* of Solaris. The major number should be incremented when a fundamental
* change has been made that would affect all consumers, and would reflect
* sweeping changes to DTrace or the D language. The minor number should be
* incremented when a change is introduced that could break scripts that had
* previously worked; for example, adding a new built-in variable could break
* a script which was already using that identifier. The micro number should
* be changed when introducing functionality changes or major bug fixes that
* do not affect backward compatibility -- this is merely to make capabilities
* easily determined from the version number. Minor bugs do not require any
* modification to the version number.
*/
#define DT_VERS_1_0 DT_VERSION_NUMBER(1, 0, 0)
#define DT_VERS_1_1 DT_VERSION_NUMBER(1, 1, 0)
#define DT_VERS_1_2 DT_VERSION_NUMBER(1, 2, 0)
#define DT_VERS_1_2_1 DT_VERSION_NUMBER(1, 2, 1)
#define DT_VERS_1_2_2 DT_VERSION_NUMBER(1, 2, 2)
#define DT_VERS_1_3 DT_VERSION_NUMBER(1, 3, 0)
#define DT_VERS_1_4 DT_VERSION_NUMBER(1, 4, 0)
#define DT_VERS_1_4_1 DT_VERSION_NUMBER(1, 4, 1)
#define DT_VERS_1_5 DT_VERSION_NUMBER(1, 5, 0)
#define DT_VERS_1_6 DT_VERSION_NUMBER(1, 6, 0)
#define DT_VERS_1_7 DT_VERSION_NUMBER(1, 7, 0)
#define DT_VERS_LATEST DT_VERS_1_7
#define DT_VERS_STRING "Sun D 1.7"
const dt_version_t _dtrace_versions[] = {
DT_VERS_1_0, /* D API 1.0.0 (PSARC 2001/466) Solaris 10 FCS */
DT_VERS_1_1, /* D API 1.1.0 Solaris Express 6/05 */
DT_VERS_1_2, /* D API 1.2.0 Solaris 10 Update 1 */
DT_VERS_1_2_1, /* D API 1.2.1 Solaris Express 4/06 */
DT_VERS_1_2_2, /* D API 1.2.2 Solaris Express 6/06 */
DT_VERS_1_3, /* D API 1.3 Solaris Express 10/06 */
DT_VERS_1_4, /* D API 1.4 Solaris Express 2/07 */
DT_VERS_1_4_1, /* D API 1.4.1 Solaris Express 4/07 */
DT_VERS_1_5, /* D API 1.5 Solaris Express 7/07 */
DT_VERS_1_6, /* D API 1.6 */
DT_VERS_1_7, /* D API 1.7 */
0
};
usr/src/lib/libdtrace/common/dt_impl.h
We've also introduced the value DT_ACT_BRENDAN, so we need to define it. The other actions are defined as follows in usr/src/lib/libdtrace/common/dt_impl.h:
#define DT_ACT_PRINTF DT_ACT(0) /* printf() action */ #define DT_ACT_TRACE DT_ACT(1) /* trace() action */ #define DT_ACT_TRACEMEM DT_ACT(2) /* tracemem() action */ #define DT_ACT_STACK DT_ACT(3) /* stack() action */ [ ... ]
so we add DT_ACT_BRENDAN to the end of this list:
#define DT_ACT_UADDR DT_ACT(27) /* uaddr() action */ #define DT_ACT_SETOPT DT_ACT(28) /* setopt() action */ #define DT_ACT_BRENDAN DT_ACT(29) /* brendan() action */
The above is essentially all of the bookkeeping necessary to set up the brendan() action, at least in userland. (We'll see one more piece of bookkeeping once we move into the kernel.) Now we move on to actually doing something. We need to do two things: first, we need to tell the compiler how to handle the brendan() action, and then we need to tell DTrace how to consume the brendan() action.
usr/src/lib/libdtrace/common/dt_cc.c
The first step comes in
usr/src/lib/libdtrace/common/dt_cc.c, the compiler driver for DTrace. The first step is to tell dt_compile_fun() what function to call for DT_ACT_BRENDAN, and the second step is to define that function. The addition to dt_compile_fun() is straightforward:
static void
dt_compile_fun(dtrace_hdl_t *dtp, dt_node_t *dnp, dtrace_stmtdesc_t *sdp)
{
switch (dnp->dn_expr->dn_ident->di_id) {
case DT_ACT_BREAKPOINT:
dt_action_breakpoint(dtp, dnp->dn_expr, sdp);
break;
case DT_ACT_BRENDAN:
dt_action_brendan(dtp, dnp->dn_expr, sdp);
break;
case DT_ACT_CHILL:
dt_action_chill(dtp, dnp->dn_expr, sdp);
break;
case DT_ACT_CLEAR:
dt_action_clear(dtp, dnp->dn_expr, sdp);
break;
[ ... ]
For dt_action_brendan(), we can follow the model set by another action with a similar function prototype, dt_action_breakpoint(), for example:
static void
dt_action_breakpoint(dtrace_hdl_t *dtp, dt_node_t *dnp, dtrace_stmtdesc_t *sdp)
{
dtrace_actdesc_t *ap = dt_stmt_action(dtp, sdp);
ap->dtad_kind = DTRACEACT_BREAKPOINT;
ap->dtad_arg = 0;
}
dt_stmt_action() allocates a new dtrace_actdesc_t structure and links it into a list. dt_action_breakpoint() does nothing more than set the type of action this is(DTRACEACT_BREAKPOINT) and zero out the action argument. (See dt_action_normalize() for a different example of setting these two values, ap->dtad_kind to DTRACEACT_LIBACT and ap->dtad_arg to either DT_ACT_NORMALIZE or DT_ACT_DENORMALIZE.)
Based on the above, dt_action_brendan() is defined simply as:
static void
dt_action_brendan(dtrace_hdl_t *dtp, dt_node_t *dnp, dtrace_stmtdesc_t *sdp)
{
dtrace_actdesc_t *ap = dt_stmt_action(dtp, sdp);
ap->dtad_kind = DTRACEACT_BRENDAN;
ap->dtad_arg = 0;
}
usr/src/uts/common/sys/dtrace.h
Note that this leads to our last bit of bookkeeping, which is to define DTRACEACT_BRENDAN. We do this inusr/src/uts/common/sys/dtrace.h:
/* * DTrace Actions * * The upper byte determines the class of the action; the low bytes determines * the specific action within that class. The classes of actions are as * follows: * * [ no class ] <= May record process- or kernel-related data * DTRACEACT_PROC <= Only records process-related data * DTRACEACT_PROC_DESTRUCTIVE <= Potentially destructive to processes * DTRACEACT_KERNEL <= Only records kernel-related data * DTRACEACT_KERNEL_DESTRUCTIVE <= Potentially destructive to the kernel * DTRACEACT_SPECULATIVE <= Speculation-related action * DTRACEACT_AGGREGATION <= Aggregating action */ #define DTRACEACT_NONE 0 /* no action */ #define DTRACEACT_DIFEXPR 1 /* action is DIF expression */ #define DTRACEACT_EXIT 2 /* exit() action */ #define DTRACEACT_PRINTF 3 /* printf() action */ #define DTRACEACT_PRINTA 4 /* printa() action */ #define DTRACEACT_LIBACT 5 /* library-controlled action */ #define DTRACEACT_BRENDAN 6 /* brendan() action */
usr/src/lib/libdtrace/common/dt_consume.c
This leaves us with our last userland modification, which is to specify how to consume the brendan() action. We do this inusr/src/lib/libdtrace/common/dt_consume.c in the function dt_consume_cpu():
static int
dt_consume_cpu(dtrace_hdl_t *dtp, FILE *fp, int cpu, dtrace_bufdesc_t *buf,
dtrace_consume_probe_f *efunc, dtrace_consume_rec_f *rfunc, void *arg)
{
dtrace_epid_t id;
size_t offs, start = buf->dtbd_oldest, end = buf->dtbd_size;
[ ... ]
for (offs = start; offs < end; ) {
dtrace_eprobedesc_t *epd;
[ ... ]
for (i = 0; i < epd->dtepd_nrecs; i++) {
dtrace_recdesc_t *rec = &epd->dtepd_rec[i];
dtrace_actkind_t act = rec->dtrd_action;
[ ... ]
if (act == DTRACEACT_BRENDAN) {
if (dt_printf(dtp, fp, "Tracing... Hit Ctrl-C "
"to end.\n") < 0)
return (-1);
goto nextrec;
}
}
[ ... ]
offs += epd->dtepd_size;
[ ... ]
}
The above is a skeleton of the function, enough to get a feel for what's going on. The outer loop consumes all of the data in this buffer for this particular CPU. The data for each enabled probe are consumed in the inner loop. For the brendan action, we merely print our desired string and move on to the next record. (Something interesting to note here is that the brendan() action isn't making use of any data that have come back from the kernel. We'll see later that, in this particular case, we'll need to act as if we're returning data out of the kernel in order to force this action to have an effect. For a useful action, there would likely be data coming out of the kernel, so there would be no need to maintain this pretext.)
Kernel changes
So far we've discussed changes in userland. We now need to make the kernel changes necessary for the brendan() action.
usr/src/uts/common/dtrace/dtrace.c
We need to make changes to two functions, dtrace_ecb_action_add() and dtrace_probe() (the epicenter of DTrace.)
First, in dtrace_ecb_action_add(), we need to specify the size of the data that we're going to be returning out of the kernel, where these data are going to be consumed as described above. In this particular case, we're not returning any data out of the kernel. But we need to act as if we are, otherwise the code to consume the brendan() action won't run. And in the case of a real action, we would most likely be sending data out of the kernel, so we would need this step.
Skeletally, the pertinent code in this function looks like this:
static int
dtrace_ecb_action_add(dtrace_ecb_t *ecb, dtrace_actdesc_t *desc)
{
[ ... ]
if (DTRACEACT_ISAGG(desc->dtad_kind)) {
[ ... ]
} else {
[ ... ]
switch (desc->dtad_kind) {
[ ... ]
case DTRACEACT_BRENDAN:
size = sizeof (uint64_t);
break;
dtrace_probe() is the function that is called when a probe fires. Here is a skeletal view of dtrace_probe():
void
dtrace_probe(dtrace_id_t id, uintptr_t arg0, uintptr_t arg1,
uintptr_t arg2, uintptr_t arg3, uintptr_t arg4)
{
[ ... ]
for (ecb = probe->dtpr_ecb; ecb != NULL; ecb = ecb->dte_next) {
dtrace_predicate_t *pred = ecb->dte_predicate;
[ ... ]
if (pred != NULL) {
dtrace_difo_t *dp = pred->dtp_difo;
int rval;
rval = dtrace_dif_emulate(dp, &mstate, vstate, state);
if (!(*flags & CPU_DTRACE_ERROR) && !rval) {
[ ... ]
continue;
}
}
for (act = ecb->dte_action; !(*flags & CPU_DTRACE_ERROR) &&
act != NULL; act = act->dta_next) {
[ ... ]
switch (act->dta_kind) {
case DTRACEACT_BRENDAN:
continue;
Essentially, all dtrace_probe() is doing is looping over the ECB's (enabling control blocks, link to be added when that section is written) for this probe, evaluating the predicate associated with each, and if the predicate evaluates to true, performing each action specified for the ECB. (The predicate code is included here for general interest, not because it relates specifically to implementing the brendan() action.) Note that there's something a bit odd here in that we do nothing for this action, which is generally not going to be the case. A typical action would either change state (e.g., the breakpoint action) or prepare data to be returned to userland (e.g., the stack() action.)
The code outline above masks an important detail. Within that innermost loop, there actions are broken across two case statements. The first applies to those actions which take no arguments, or more specifically, which take no arguments that need to be evaluated within the kernel. (The stack(), ustack(), and jstack() actions are handled in this case statement, but the (optional) arguments to these actions are integers and interpreted in userland.) After this first case statement, we have the following, which evaluates code related to the argument(s) of the action (in the case of an action like printf(), dtrace_dif_emulate() would return a pointer to the resultant string, which is copied into the ECB):
dp = act->dta_difo;
ASSERT(dp != NULL);
val = dtrace_dif_emulate(dp, &mstate, vstate, state);
Test Suite
The authors of DTrace have provided a test suite, and it's suggested that every change to DTrace include a test suite script (or scripts) to test the added functionality. Three types of files can be found in the test suite, D scripts (tst.foo.d), ksh scripts (tst.foo.ksh), and output files (tst.foo.d.out and/or tst.foo.ksh.out.) Scripts are run using either dtrace or ksh as appropriate. If there is a corresponding output file, the output of the script must match in order to pass. If there is not output file, a 0 exit value is a pass.
usr/src/cmd/dtrace/test/tst/common/misc/tst.brendan.d
The test script for the above is very simple:
#pragma D option quiet
BEGIN
{
brendan();
exit(0);
}
usr/src/cmd/dtrace/test/tst/common/misc/tst.brendan.d.out
The corresponding output file for the above is as follows:
Tracing... Hit Ctrl-C to end.
usr/src/pkgdefs/SUNWdtrt/prototype_com
And for the sake of completeness, it should be noted that entries for tst.brendan.d and tst.brendan.d.out should be added to the package prototype file for the DTrace Test Suite package, as these are new files.
Conclusion
And that concludes the tutorial. Aside from the details of gathering data for an action that does useful work (as opposed to simply poking fun at the author of the DTrace Toolkit, the usefulness of which is dubious), the above is a complete outline of the steps needed to add an action to DTrace.