Non-native languages

As DTrace became popular, many language interpreters got USDT probes. Some of them adopted them in upstream, some probes are provided by the binaries custom packages shipped with operating system. The basic pair of probes provided by most language interpreters are function entry and exit probes which provide name of the function, line number and file name. For example, Perl can be traced that way:

# echo 'use Data::Dumper; 
      map { Dumper($_, ", ") }  ("Hello", "world");' | 
    dtrace -n '
        perl$target:::sub-entry {
            trace(copyinstr(arg0));  trace(copyinstr(arg1)); trace(arg2);
        }  ' -c 'perl -- -'

# stap -e '
        probe process("/usr/lib64/perl5/CORE/libperl.so").mark("sub__entry") {
            printdln(" : ", user_string($arg1), user_string($arg2), $arg3);
        }
    ' -c $'perl -e \'use Data::Dumper; 
                     map { Dumper($_, ", ") }  ("Hello", "world");\''

Note that we had to use stdin as a script in DTrace example. That happened because DTrace cannot parse -c option value in shell-like manner.

Language interpreters provide not only function entry probes, here are other examples of supplied probes:

Here are list of availability of that probes in various interpreters shipped as binary packages. If you lack them, you may want to rebuild interpreters with some configure option like --enable-dtrace.

Interpreter CentOS Solaris
Python 2 Python has never accepted DTrace patches into upstream. However, it was implemented by Solaris developers for Python 2.4, and being ported to Fedora's and CentOS python. Only function-related probes are supplied: function-entry and function-return.
Python 3 Like Python 2, Python 3 in CentOS (if installed from EPEL) supports function__entry and function__return probes. In addition to that, SystemTap supplies example python3 tapset. Python 3 is supplied as FOSS (unsupported) package in Solaris 11 and has line probe, instance creation and garbage-collector related probes.
Starting with Python 3.6, DTrace probes function entry and exit probes, garbage collector probes and line are supported by vanilla interpreter
PHP5 Doesn't support USDT tracing but can it be enabled via --enable-dtrace switch when it is built from source. PHP supports tracing functions, exceptions and errors, VM opcodes execution and file compiling from scratch. Its probes will be discussed in the following section, Web applications.
Ruby 2 Supports multiple probes including object creation, method entry and exit points and garbage collector probes in Ruby 2.0 in CentOS or Ruby 2.1 as FOSS package in Solaris 11.
Perl 5 Supports subroutine probes sub-entry and sub-return (see examples above).
Go Go is pretty close to native languages in Linux, so you can attach probes directly to its functions while backtraces show correct function names. Differences in type system between C-based languages and Go, however prevents SystemTap from accessing arguments. Go has experimental support for Solaris so it is not considered as a target for DTrace.
Erlang Neither EPEL nor Solaris package feature USDT probes in BEAM virtual machine, but they are supported in sources, so building with --with-dynamic-trace option enables various probes including function-boundary probes.
Node.JS Node.JS is not supplied as OS packages, while binaries from official site doesn't have USDT enabled in Linux or simply not working in Oracle Solaris (only in Illumos derivatives). Building from sources, however adds global network-related probes like http-server-request. Function boundary tracing is not supported.

Most interpreted language virtual machines still rely on libc to access basic OS facilities like memory allocation, but some may use their own: i.e. PyMalloc in Python, Go runtime is OS-independent in Go language. For example let's see how malloc calls may be cross-referenced with Python code in yum and or pkg package managers using SystemTap or DTrace. We will need to attach to function entry and exit points to track "virtual" python backtrace and malloc call to track amount of allocated bytes. This approach is implemented in the following couple of scripts:

  Script file pymalloc.d

#!/usr/sbin/dtrace -qCZs

BEGIN {
    self->depth = 0;
}

foo$target::: {
    /* This probe is just a workaround for -xlazyload */
}

python$target:::function-entry {
    func_stack[self->depth] = arg1;
    file_stack[self->depth] = arg0;
    
    self->depth++;
}
python$target:::function-return {
    self->depth--;
}

pid$target::malloc:entry 
/ func_stack[self->depth] != 0 / {
    @mallocs[copyinstr(func_stack[self->depth]),
             copyinstr(file_stack[self->depth])] = sum(arg0);
}

  Script file pymalloc.stp

#!/usr/bin/env stap

@define libc %( "/lib64/libc.so.6" %)

global file_stack, func_stack, mallocs, thread_depth

probe python.function.entry {
    thread_depth[tid()]++;
    
    depth = thread_depth[tid()];
    file_stack[tid(), depth] = filename;
    func_stack[tid(), depth] = funcname;
}

probe python.function.return {
    thread_depth[tid()]--;
}

probe process(@libc).function("_int_malloc") {
    depth = thread_depth[tid()];
    mallocs[file_stack[tid(), depth],
            func_stack[tid(), depth]] <<< $bytes;
}

Note

We have used non-existent foo provider in DTrace example because like JVM, Python is linked with -xlazyload linker flag, so we apply same workaround to find probes that we used in Java Virtual Machine section.

Arguments and local variables are also inaccessible directly by SystemTap or DTrace when program in non-native language is traced. That happens because they are executed within virtual machine which has its own representation of function frame which is different from CPU representation: languages with dynamic typing are more likely to keep local variables in a dict-like object than in a stack. These frame and dict-like objects, however, are usually implemented in C and available for dynamic tracing. All that you have to do is to provide their layout.

Let's see how this can be done for Python 3 in Solaris and Linux. If you try to get backtrace of program interpreted by Python 3, you will probably see function named PyEval_EvalCodeEx which is responsible for evaluation of code object. Code object itself has type PyCodeObject and passed as first argument of that function. That structure has fields like co_firstlineno, co_name and co_filename. Last two fields not just C-style strings but kind of PyUnicodeObject –- an object which represents strings in Python 3. It have multiple layouts, but we rely on the simplest one: compacted ASCII strings. That may not be true for all string objects in the program, but that works fine for objects produced by the interpreter itself like code objects.

DTrace cannot recognize type information from Python libraries, but it supports struct definitions in the code. We will use it to provide PyCodeObject and PyUnicodeObject layouts in a separate file pycode.h. DTrace syntax is pretty much like C syntax, so these definitions are almost copy-and-paste from Python sources. Here is an example of DTrace scripts which trace python program execution:

  Script file pycode.h

#ifndef PY_CODE_H
#define PY_CODE_H

/**
 * This is forward definitions taken from Include/object.h and Include/code.h 
 * to support extraction of Python 3.4 interpreter state
 */

typedef long        ssize_t;

typedef struct _object {
    /* _PyObject_HEAD_EXTRA */
    ssize_t ob_refcnt;
    struct PyObject *ob_type;
} PyObject;

/* Bytecode object */
typedef struct _code {
    PyObject base;
    int co_argcount;        /* #arguments, except *args */
    int co_kwonlyargcount;  /* #keyword only arguments */
    int co_nlocals;         /* #local variables */
    int co_stacksize;       /* #entries needed for evaluation stack */
    int co_flags;           /* CO_..., see below */
    PyObject *co_code;      /* instruction opcodes */
    PyObject *co_consts;    /* list (constants used) */
    PyObject *co_names;     /* list of strings (names used) */
    PyObject *co_varnames;  /* tuple of strings (local variable names) */
    PyObject *co_freevars;  /* tuple of strings (free variable names) */
    PyObject *co_cellvars;      /* tuple of strings (cell variable names) */
    /* The rest doesn't count for hash or comparisons */
    unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */
    PyObject *co_filename;  /* unicode (where it was loaded from) */
    PyObject *co_name;      /* unicode (name, for reference) */
    int co_firstlineno;     /* first source line number */
    PyObject *co_lnotab;    /* string (encoding addr<->lineno mapping) See
                   Objects/lnotab_notes.txt for details. */
    void *co_zombieframe;     /* for optimization only (see frameobject.c) */
    PyObject *co_weakreflist;   /* to support weakrefs to code objects */
} PyCodeObject;

/**
 * Compact ASCII object from Python3 -- data starts after PyUnicodeObject -- only if compact, ascii
 * and ready flags are set
 */
typedef struct _unicode {
    PyObject  base;
    ssize_t   length;
    ssize_t   hash;
    char      flags[4];
    void*     wstr;
} PyUnicodeObject;

#endif

  Script file pycode.d

#!/usr/sbin/dtrace -Cs

#include "pycode.h"

#define GET_Py3_STRING(obj) (((PyUnicodeObject*) copyin((uintptr_t) obj,                     \
                                           sizeof(PyUnicodeObject)))->flags[0] & 0xE0)       \
            ? copyinstr(((uintptr_t) obj) + sizeof(PyUnicodeObject)) : ""   

foo$target::: {}

pid$target::PyEval_EvalCodeEx:entry {
    self->co = (PyCodeObject*) copyin(arg0, sizeof(PyCodeObject));

    trace(GET_Py3_STRING(self->co->co_filename));
    trace(GET_Py3_STRING(self->co->co_name));
    trace(self->co->co_firstlineno);
}

Note

Similar mechanism is used in so-called ustack helpers in DTrace. That allows to build actual backtraces of Python or Node.JS programs when you use jstack() action.

SystemTap can extract type information directly from DWARF section of shared libraries so all we need to do to achieve same effect in it is to use @cast expression:

  Script file pycode.stp

#!/usr/bin/env stap

@define PYTHON3_LIBRARY %( "/usr/lib64/libpython3.4m.so.1.0" %)

function get_py3_string:string(uo: long) {
    flags = user_uint32(&@cast(uo, "PyASCIIObject", @PYTHON3_LIBRARY)->state);
    if(flags & 0xE0) {
        size = &@cast(0, "PyASCIIObject", @PYTHON3_LIBRARY)[1]
        return user_string(uo + size);
    }
    
    return "???";
}

probe process(@PYTHON3_LIBRARY).function("PyEval_EvalCodeEx") {
    code = $_co;
    if(code) {
        printf("%s %s:%d\n",
               get_py3_string(@cast(code, "PyCodeObject", @PYTHON3_LIBRARY)->co_name),
               get_py3_string(@cast(code, "PyCodeObject", @PYTHON3_LIBRARY)->co_filename),
               @cast(code, "PyCodeObject", @PYTHON3_LIBRARY)->co_firstlineno);
    }
}

References