DynamoRIO
DynamoRIO eXtension utilities

The drx DynamoRIO Extension provides various utilities for instrumentation and sports a BSD license, as opposed to the drutil Extension which also contains instrumentation utilities but uses an LGPL 2.1 license.

Setup

To use drx with your client simply include this line in your client's CMakeLists.txt file:

use_DynamoRIO_extension(clientname drx)

That will automatically set up the include path and library dependence.

The drx_init() function may be called multiple times; subsequent calls will be nops and will return true for success. This allows a library to use drx without coordinating with the client over who invokes drx_init().

Soft Kills

A common scenario with multi-process applications is for a parent process to directly kill child processes. This is problematic for most dynamic tools as this leaves no chance for each child process to output the results of its instrumentation. On Windows, drx provides a feature called "soft kills" to address this situation. When enabled, this feature monitors system calls that terminate child processes, whether directly or through job objects. When detected, it notifies the client, who is expected to then notify the target process via a nudge. Typically this nudge will perform instrumentation output and then terminate its process, allowing the parent to simply skip its own termination request. The nudge handler should normally handle multiple requests, as it is not uncommon for the parent to kill each child process through multiple mechanisms.

Buffer Filling API

The drx library also demonstrates a minimalistic buffer API. Its API is currently in flux. These buffers may contain traces of data gathered during instrumentation, such as memory traces, instruction traces, etc. Note that per-thread buffers are used for all implementations. There currently exist three types of buffers.

Trace Buffer

The trace buffer notifies the user when the buffer fills up and allows the client to write the contents to disk or to a pipe, etc. Note that writing multiple fields of a struct to the buffer runs the risk of the client being notified that the buffer is filled before the entire struct has been written. In order to circumvent this limitation, either write the element at the highest offset in the struct first, so that the user never sees an incompletely-written struct, or if this is not possible, allocate a buffer whose size is a multiple of the size of the struct.

Circular Buffer

This circular buffer will wrap around when it becomes full, and is used when a client might only need to remember the most recent portion of a sequence of events instead of recording an entire trace of events. This circular buffer can be any size, but is specially optimized for a buffer size of 65336.

Fast Circular Buffer

The only special case mentioned in Circular Buffer is a buffer of size 65336. Because the buffer is this size exactly, we can align it to a 65336 byte boundary, and increment only the bottom two bytes of the base pointer. By this method we are able to wrap around on overflow.

Note that this buffer is very good for homogeneous writes, such as in the sample client bbuf (see Sample Tools), where we only write app_pc sized values. Since the buffer cannot be a different size, when using a structure it is a good idea to increment buf_ptr to a size that evenly divides the size of the buffer.

Using the Buffer API

There is a single API for modifying the buffer which is compatible with each of these buffer types. The user must generally load the buffer pointer into a register, perform some store operations relative to the register, and then finally update the buffer pointer to accommodate these stores. Using offsets for subsequent fields in a structure is the most efficient method, but please note the warning in Trace Buffer, where one should either allocate an integer multiple of the size of the struct, or always write the last field of a struct first.

/* load the buffer pointer into reg_ptr */
drx_buf_insert_load_buf_ptr(drcontext, buf, bb, inst, reg_ptr);
/* Store whatever is in the scratch reg to the buffer at offset 0, and then
* to offset 8.
*/
drx_buf_insert_buf_store(drcontext, buf, bb, inst, reg_ptr,
drx_buf_insert_buf_store(drcontext, buf, bb, inst, reg_ptr,
/* We wrote 16 bytes (8 bytes of scratch1 and 8 bytes of scratch2), so we
* increment the buffer pointer by that amount, using reg_tmp as a temporary
* scratch register.
*/
drx_buf_insert_update_buf_ptr(drcontext, buf, bb, inst, reg_ptr,
reg_tmp, sizeof(reg_t)*2);

Manually Modifying the Buffer

It is possible to manually modify the buffer without calling drx_buf_insert_buf_store(). The provided store routines are for convenience only, to ensure that an app translation is set for each instruction. If a user writes to the buffer without using the provided operations, please make sure an app translation is set.

DR_API INSTR_INLINE opnd_t opnd_create_reg(reg_id_t r)
DR_EXPORT bool drx_buf_insert_buf_store(void *drcontext, drx_buf_t *buf, instrlist_t *ilist, instr_t *where, reg_id_t buf_ptr, reg_id_t scratch, opnd_t opnd, opnd_size_t opsz, short offset)
#define OPSZ_PTR
Definition: dr_ir_opnd.h:234
@ DR_REG_NULL
Definition: dr_ir_opnd.h:298
DR_EXPORT void drx_buf_insert_update_buf_ptr(void *drcontext, drx_buf_t *buf, instrlist_t *ilist, instr_t *where, reg_id_t buf_ptr, reg_id_t scratch, ushort stride)
DR_EXPORT void drx_buf_insert_load_buf_ptr(void *drcontext, drx_buf_t *buf, instrlist_t *ilist, instr_t *where, reg_id_t buf_ptr)