DynamoRIO Client 代码编写

最近在研究DBI,就弄了个笔记。

本文提到的官方的示例工具:示例工具

DynamoRIO构成

DynamoRIO为用户提供了丰富的应用程序编程接口(API),用于构建DynamoRIO客户端。DynamoRIO客户端是一个与DynamoRIO耦合的库,用于共同操作输入程序二进制文件。我们这次要写的东西就是这个Client。

client.png

例子:empty

这是一个最简单的、什么也不做的Client,因此其代码非常简单。虽然简单,其包含一个Client必须包含的因素。

0. 头文件

#include "dr_api.h"

1. dr_client_main

每一个Client都必须包含dr_client_main函数。这个函数里面做初始化、接收参数、绑定事件之类的操作。

DR_EXPORT void
dr_client_main(client_id_t id, int argc, const char *argv[])
{
    /* empty client */
    dr_set_client_name("DynamoRIO Sample Client 'empty'", "http://dynamorio.org/issues");
    dr_register_exit_event(event_exit);
}

可以看到,这个函数至少应该包含2个部分

  1. 设置Client名称和支持信息;
  2. 注册退出事件。

其中,注册事件是是使用DynamoRIO的核心内容。下面会详细介绍。

2. event_exit

该函数就是所需注册的回调函数。

static void
event_exit(void)
{
    /* empty client */
}

不同的事件具有不同的回调函数参数列表,具体函数参数可以查看API文档。我这里列出一些常用的事件及其相关的函数参数列表。

头文件

  1. dr_api.h是核心API,提供基础功能,如注册回调函数、访问基本块和指令列表等。每个Client都需要使用这个函数。实际上,该头文件引用了很多头文件,例如dr_events.h
  2. drmgr.h是drmgr的API列表,提供drmgr的各项二次封装功能,同时也有对原生DynamoRIO API的一些修改。

事件模型

事件模型是一种编程和设计模式,用于处理系统中的异步事件和状态变化。其允许应用在特定事件发生的时候进行特定的操作或相应。事件模型一般在前端、网络编程等需要异步相应的领域使用,DynamoRIO也是用这样的方式,在动态分析二进制代码时在特定的事件处执行自定义的指令。

组分

  1. 事件(Event):发生的操作或者状态的变化。比如二进制动态分析进入基本块、离开基本块、发生PC跳转、初始化线程、线程结束等。
  2. 事件源(EventSource):产生事件的对象。
  3. 事件监听器(EventListener):监听特定的事件并响应这些事件的处理函数等。
  4. 事件分发器(EventDispatcher):将事件从事件源传递给相应的事件监听器。

工作流程

  1. 注册监听器:将事件监听器注册到事件源,以便在特定事件发生时获得通知。在DynamoRIO中,可以使用dr_register_<specific>_event(注册事件)和dr_unregister_<specific>_event来注册函数的回调,其参数是所需注册的回调函数。一般在dr_client_main中注册回调函数。
  2. 事件的实际发生和实际回调:在事件实际发生的时候,事件源会通知事件调度器,事件调度器调用相应的事件监听器,执行预先注册的回调函数的处理逻辑。DynamoRIO中,事件发生时会调用已经注册的相应的回调函数。

     > [!NOTE]
     > Client可以为同一事件注册多个回调。DynamoRIO按照**注册顺序的倒序**排列事件回调。也就是说,第一个注册的回调是最后收到事件通知的。
     >
     > DynamoRIO也支持多个Client,每个Client都可以为同一事件注册。DynamoRIO会根据客户端的优先级(参见部署API中的多个Client和`dr_register_client`)调用每个Client的`dr_client_main`函数。
    
     > [!CAUTION]
     > 确保Client的回调函数与其他Client的回调函数兼容是Client的责任。
    

例子:memtrace_simple

在这里,我们看下DynamoRIO提供的memtrace_simple工具。遵循从简单到困难的过程,还有一个复杂版的memtrace_x86,该版本优化了性能,之后再看。

1. dr_client_main

给出了一个加强版的主函数。

DR_EXPORT void
dr_client_main(client_id_t id, int argc, const char *argv[])
{
    // PART 1
    /* We need 2 reg slots beyond drreg's eflags slots => 3 slots */
    drreg_options_t ops = { sizeof(ops), 3, false };
    dr_set_client_name("DynamoRIO Sample Client 'memtrace'",
                       "http://dynamorio.org/issues");

    if (argc > 1) {
        if (argc == 2 && strcmp(argv[1], "-log_to_stderr") == 0)
            log_to_stderr = true;
        else {
            dr_fprintf(STDERR,
                       "Error: unknown options: only -log_to_stderr is supported\n");
            dr_abort();
        }
    }

    // PART 2
    if (!drmgr_init() || drreg_init(&ops) != DRREG_SUCCESS || !drutil_init() ||
        !drx_init())
        DR_ASSERT(false);

    // PART 3
    /* register events */
    dr_register_exit_event(event_exit);
    if (!drmgr_register_thread_init_event(event_thread_init) ||
        !drmgr_register_thread_exit_event(event_thread_exit) ||
        !drmgr_register_bb_app2app_event(event_bb_app2app, NULL) ||
        !drmgr_register_bb_instrumentation_event(NULL /*analysis_func*/,
                                                 event_app_instruction, NULL))
        DR_ASSERT(false);

    // PART 4
    client_id = id;
    mutex = dr_mutex_create();

    tls_idx = drmgr_register_tls_field();
    DR_ASSERT(tls_idx != -1);
    /* The TLS field provided by DR cannot be directly accessed from the code cache.
     * For better performance, we allocate raw TLS so that we can directly
     * access and update it with a single instruction.
     */
    if (!dr_raw_tls_calloc(&tls_seg, &tls_offs, MEMTRACE_TLS_COUNT, 0))
        DR_ASSERT(false);

    /* make it easy to tell, by looking at log file, which client executed */
    dr_log(NULL, DR_LOG_ALL, 1, "Client 'memtrace' initializing\n");
}

我将代码分为了4个部分。

  • PART 1

    • 设置寄存器槽位,定义需要的寄存器数量,这里因为他要存options,所以需要2个额外的寄存器,加上eflags占一个,一共3个。
    • 设置Client名称。
    • 处理命令行参数。
  • PART 2

    • 初始化各种东西:drmgr、drreg、drutil、drx。这里先不介绍,之后专门有介绍。
  • PART 3

    • 注册事件:dr_register_exit_eventdrmgr_register_thread_init_eventdrmgr_register_thread_exit_eventdrmgr_register_bb_app2app_eventdrmgr_register_bb_instrumentation_event。注意,这里的部分事件是需要首先初始化上述drmgr工具才能使用的。
  • PART 4

    • memtrace的主要逻辑。
    • 创建互斥锁:因为在trace的过程中有全局的计数器,为了保证多线程正确性。
    • 分配TLS内存:TLS内存就是Thread-local Storage,线程局部存储。使用drmgr_register_tls_field() 注册一个线程局部存储字段,然后用dr_raw_tls_calloc()分配实际的TLS段和偏移量。实际上,这里的注册TLS和PART 3的注册事件类似,也需要unregister的操作。
    • 日志。

2. event_exit

static void
event_exit(void)
{
    dr_log(NULL, DR_LOG_ALL, 1, "Client 'memtrace' num refs seen: " SZFMT "\n", num_refs);
    if (!dr_raw_tls_cfree(tls_offs, MEMTRACE_TLS_COUNT))
        DR_ASSERT(false);

    if (!drmgr_unregister_tls_field(tls_idx) ||
        !drmgr_unregister_thread_init_event(event_thread_init) ||
        !drmgr_unregister_thread_exit_event(event_thread_exit) ||
        !drmgr_unregister_bb_app2app_event(event_bb_app2app) ||
        !drmgr_unregister_bb_insertion_event(event_app_instruction) ||
        drreg_exit() != DRREG_SUCCESS)
        DR_ASSERT(false);

    dr_mutex_destroy(mutex);
    drutil_exit();
    drmgr_exit();
    drx_exit();
}
  • dr_raw_tls_cfree是一个函数,用于释放之前使用dr_raw_tls_calloc分配的原始线程本地存储(TLS)空间。这个函数是为了管理那些通过dr_raw_tls_calloc分配的TLS空间,在不再需要这些空间时进行清理。

3. event_thread相关

static void
event_thread_init(void *drcontext)
{
    per_thread_t *data = dr_thread_alloc(drcontext, sizeof(per_thread_t));
    DR_ASSERT(data != NULL);
    drmgr_set_tls_field(drcontext, tls_idx, data);

    /* Keep seg_base in a per-thread data structure so we can get the TLS
     * slot and find where the pointer points to in the buffer.
     */
    data->seg_base = dr_get_dr_segment_base(tls_seg);
    data->buf_base =
        dr_raw_mem_alloc(MEM_BUF_SIZE, DR_MEMPROT_READ | DR_MEMPROT_WRITE, NULL);
    DR_ASSERT(data->seg_base != NULL && data->buf_base != NULL);
    /* put buf_base to TLS as starting buf_ptr */
    BUF_PTR(data->seg_base) = data->buf_base;

    data->num_refs = 0;

    if (log_to_stderr) {
        data->logf = stderr;
    } else {
        /* We're going to dump our data to a per-thread file.
         * On Windows we need an absolute path so we place it in
         * the same directory as our library. We could also pass
         * in a path as a client argument.
         */
        data->log = log_file_open(client_id, drcontext, NULL /* using client lib path */,
                                  "memtrace",
#ifndef WINDOWS
                                  DR_FILE_CLOSE_ON_FORK |
#endif
                                      DR_FILE_ALLOW_LARGE);
        data->logf = log_stream_from_file(data->log);
    }
    fprintf(data->logf, "Format: <data address>: <data size>, <r/w/opcode>, <tid>\n");
}

static void
event_thread_exit(void *drcontext)
{
    per_thread_t *data;
    memtrace(drcontext); /* dump any remaining buffer entries */
    data = drmgr_get_tls_field(drcontext, tls_idx);
    dr_mutex_lock(mutex);
    num_refs += data->num_refs;
    dr_mutex_unlock(mutex);
    if (!log_to_stderr)
        log_stream_close(data->logf); /* closes fd too */
    dr_raw_mem_free(data->buf_base, MEM_BUF_SIZE);
    dr_thread_free(drcontext, data, sizeof(per_thread_t));
}
  • 分配线程本地数据(TLS)

    • 使用 dr_thread_alloc 分配一个 per_thread_t 结构体,用于存储与当前线程相关的数据。
    • 通过 drmgr_set_tls_field 将这个数据与DynamoRIO的线程本地存储(TLS)索引关联起来。
  • 初始化缓冲区

    • 获取DynamoRIO的段基地址,存储在 data->seg_base
    • 分配一个内存缓冲区 data->buf_base,大小为 MEM_BUF_SIZE,用于存储内存引用信息。
    • data->buf_base 的地址设置为 BUF_PTR(data->seg_base),即TLS中的缓冲区指针。
  • 设置日志

exit不再赘述。

4. event_bb_app2app

/* We transform string loops into regular loops so we can more easily
 * monitor every memory reference they make.
 */
static dr_emit_flags_t
event_bb_app2app(void *drcontext, void *tag, instrlist_t *bb, bool for_trace,
                 bool translating)
{
    if (!drutil_expand_rep_string(drcontext, bb)) {
        DR_ASSERT(false);
        /* in release build, carry on: we'll just miss per-iter refs */
    }
    if (!drx_expand_scatter_gather(drcontext, bb, NULL)) {
        DR_ASSERT(false);
    }
    return DR_EMIT_DEFAULT;
}

将字符串操作循环(如使用rep前缀的字符串操作指令)转换成常规循环,以便更容易地监控每一次内存引用。

[!NOTE]

这里给出了一个处理SIMD指令的全新思路:将原本的聚合指令拆分为简单指令。虽然性能会下降,但是分析和插桩的精度会变好。

但是,一般的SIMD指令不会处理控制用的内存数据。因此部分分析可以不对其进行拆分以提高性能。

5. event_app_instruction

/* For each memory reference app instr, we insert inline code to fill the buffer
 * with an instruction entry and memory reference entries.
 */
static dr_emit_flags_t
event_app_instruction(void *drcontext, void *tag, instrlist_t *bb, instr_t *where,
                      bool for_trace, bool translating, void *user_data)
{
    int i;

    /* Insert code to add an entry for each app instruction. */
    /* Use the drmgr_orig_app_instr_* interface to properly handle our own use
     * of drutil_expand_rep_string() and drx_expand_scatter_gather() (as well
     * as another client/library emulating the instruction stream).
     */
    instr_t *instr_fetch = drmgr_orig_app_instr_for_fetch(drcontext);
    if (instr_fetch != NULL &&
        (instr_reads_memory(instr_fetch) || instr_writes_memory(instr_fetch))) {
        DR_ASSERT(instr_is_app(instr_fetch));
        instrument_instr(drcontext, bb, where, instr_fetch);
    }

    /* Insert code to add an entry for each memory reference opnd. */
    instr_t *instr_operands = drmgr_orig_app_instr_for_operands(drcontext);
    if (instr_operands == NULL ||
        (!instr_reads_memory(instr_operands) && !instr_writes_memory(instr_operands)))
        return DR_EMIT_DEFAULT;
    DR_ASSERT(instr_is_app(instr_operands));

    for (i = 0; i < instr_num_srcs(instr_operands); i++) {
        const opnd_t src = instr_get_src(instr_operands, i);
        if (opnd_is_memory_reference(src)) {
#ifdef AARCH64
            /* TODO i#5844: Memory references involving SVE registers are not
             * supported yet. To be implemented as part of scatter/gather work.
             */
            if (opnd_is_base_disp(src) &&
                (reg_is_z(opnd_get_base(src)) || reg_is_z(opnd_get_index(src)))) {
                if (!reported_sg_warning) {
                    dr_fprintf(STDERR,
                               "WARNING: Scatter/gather is not supported, results will "
                               "be inaccurate\n");
                    reported_sg_warning = true;
                }
                continue;
            }
#endif
            instrument_mem(drcontext, bb, where, src, false);
        }
    }

    for (i = 0; i < instr_num_dsts(instr_operands); i++) {
        const opnd_t dst = instr_get_dst(instr_operands, i);
        if (opnd_is_memory_reference(dst)) {
#ifdef AARCH64
            /* TODO i#5844: Memory references involving SVE registers are not
             * supported yet. To be implemented as part of scatter/gather work.
             */
            if (opnd_is_base_disp(dst) &&
                (reg_is_z(opnd_get_base(dst)) || reg_is_z(opnd_get_index(dst)))) {
                if (!reported_sg_warning) {
                    dr_fprintf(STDERR,
                               "WARNING: Scatter/gather is not supported, results will "
                               "be inaccurate\n");
                    reported_sg_warning = true;
                }
                continue;
            }
#endif
            instrument_mem(drcontext, bb, where, dst, true);
        }
    }

    /* insert code to call clean_call for processing the buffer */
    if (/* XXX i#1698: there are constraints for code between ldrex/strex pairs,
         * so we minimize the instrumentation in between by skipping the clean call.
         * As we're only inserting instrumentation on a memory reference, and the
         * app should be avoiding memory accesses in between the ldrex...strex,
         * the only problematic point should be before the strex.
         * However, there is still a chance that the instrumentation code may clear the
         * exclusive monitor state.
         * Using a fault to handle a full buffer should be more robust, and the
         * forthcoming buffer filling API (i#513) will provide that.
         */
        IF_AARCHXX_ELSE(!instr_is_exclusive_store(instr_operands), true))
        dr_insert_clean_call(drcontext, bb, where, (void *)clean_call, false, 0);

    return DR_EMIT_DEFAULT;
}
  • drmgr_orig_app_instr_for_fetchdrmgr_orig_app_instr_for_operands指的是获取原始的指令,而不是修改过的指令(例如上述的将SIMD指令转换为一般指令的过程),以确保监控代码能够正确地插入和执行。
  • instr_get_srcinstr_get_dst是获取指令的第几个源/目标操作数。

    • 特例:对于AArch64架构,如果内存引用涉及SVE寄存器,目前不支持散/聚操作。如果遇到这种情况,输出警告并跳过处理。
  • 使用dr_insert_clean_call函数在bb(指令列表)中的where位置插入一个清洁调用,以处理内存引用信息的缓冲区。

    • 特例:代码插入的约束:ldrexstrex指令对是ARM架构中用于实现原子操作的指令对。在这两个指令之间插入代码可能会干扰它们的原子性和独占状态。

      • IF_AARCHXX_ELSE(!instr_is_exclusive_store(instr_operands), true):这是一个宏,用于检查当前指令是否是一个独占存储指令(即strex)。如果是在AArch64架构上,并且当前指令不是独占存储指令,或者在其他架构上,这个条件将评估为真。
      • instr_is_exclusive_store(instr_operands):这个函数检查instr_operands是否是一个独占存储指令。

6. 插桩函数

/* insert inline code to add an instruction entry into the buffer */
static void
instrument_instr(void *drcontext, instrlist_t *ilist, instr_t *where, instr_t *instr)
{
    /* We need two scratch registers */
    reg_id_t reg_ptr, reg_tmp;
    /* we don't want to predicate this, because an instruction fetch always occurs */
    instrlist_set_auto_predicate(ilist, DR_PRED_NONE);
    if (drreg_reserve_register(drcontext, ilist, where, NULL, &reg_ptr) !=
            DRREG_SUCCESS ||
        drreg_reserve_register(drcontext, ilist, where, NULL, &reg_tmp) !=
            DRREG_SUCCESS) {
        DR_ASSERT(false); /* cannot recover */
        return;
    }
    insert_load_buf_ptr(drcontext, ilist, where, reg_ptr);
    insert_save_type(drcontext, ilist, where, reg_ptr, reg_tmp, (ushort)instr_get_opcode(instr));
    insert_save_size(drcontext, ilist, where, reg_ptr, reg_tmp, (ushort)instr_length(drcontext, instr));
    insert_save_pc(drcontext, ilist, where, reg_ptr, reg_tmp, instr_get_app_pc(instr));
    insert_save_tid(drcontext, ilist, where, reg_ptr, reg_tmp, (uint)dr_get_thread_id(drcontext));
    insert_update_buf_ptr(drcontext, ilist, where, reg_ptr, sizeof(mem_ref_t));
    /* Restore scratch registers */
    if (drreg_unreserve_register(drcontext, ilist, where, reg_ptr) != DRREG_SUCCESS ||
        drreg_unreserve_register(drcontext, ilist, where, reg_tmp) != DRREG_SUCCESS)
        DR_ASSERT(false);
    instrlist_set_auto_predicate(ilist, instr_get_predicate(where));
}

/* insert inline code to add a memory reference info entry into the buffer */
static void
instrument_mem(void *drcontext, instrlist_t *ilist, instr_t *where, opnd_t ref,
               bool write)
{
    /* We need two scratch registers */
    reg_id_t reg_ptr, reg_tmp;
    if (drreg_reserve_register(drcontext, ilist, where, NULL, &reg_ptr) !=
            DRREG_SUCCESS ||
        drreg_reserve_register(drcontext, ilist, where, NULL, &reg_tmp) !=
            DRREG_SUCCESS) {
        DR_ASSERT(false); /* cannot recover */
        return;
    }
    /* save_addr should be called first as reg_ptr or reg_tmp maybe used in ref */
    insert_save_addr(drcontext, ilist, where, ref, reg_ptr, reg_tmp);
    insert_save_type(drcontext, ilist, where, reg_ptr, reg_tmp,
                     write ? REF_TYPE_WRITE : REF_TYPE_READ);
    insert_save_size(drcontext, ilist, where, reg_ptr, reg_tmp,
                     (ushort)drutil_opnd_mem_size_in_bytes(ref, where));
    insert_save_tid(drcontext, ilist, where, reg_ptr, reg_tmp,
                     (uint)dr_get_thread_id(drcontext));
    insert_update_buf_ptr(drcontext, ilist, where, reg_ptr, sizeof(mem_ref_t));
    /* Restore scratch registers */
    if (drreg_unreserve_register(drcontext, ilist, where, reg_ptr) != DRREG_SUCCESS ||
        drreg_unreserve_register(drcontext, ilist, where, reg_tmp) != DRREG_SUCCESS)
        DR_ASSERT(false);
}

过程:关闭自动推断->插桩->开启自动推断。

  • 预留寄存器,用于接下来的保存信息用。其中,reg_ptr用于保存指向内存引用缓冲区的指针,reg_tmp用于临时存储数据。
  • 记录各种信息:

    • 调整寄存器大小:将寄存器大小调整为目标数据的大小。
    • 加载值到寄存器:MINSERT(ilist, where, XINST_CREATE_load_int(drcontext, opnd_create_reg(scratch), OPND_CREATE_INT16(type))); 这行代码创建一个指令,将 type 值加载到 scratch 寄存器中。XINST_CREATE_load_int 是 DynamoRIO 提供的宏,用于创建加载立即数到寄存器的指令。
    • 存储值到内存:MINSERT(ilist, where, XINST_CREATE_store_2bytes(drcontext, OPND_CREATE_MEM16(base, offsetof(mem_ref_t, type)), opnd_create_reg(scratch))); 这行代码创建一个指令,将 scratch 寄存器中的值(即 type)存储到内存中的 mem_ref_t 结构的 type 成员位置。OPND_CREATE_MEM16 创建一个 16 位的内存操作数,offsetof(mem_ref_t, type) 计算 type 成员在 mem_ref_t 结构中的偏移量。
  • 解除预留寄存器。

各种组件

drmgr(Dynamic Runtime Manager)

drmgr模块是DynamoRIO中的一个重要Extension组件,它提供了一个更高层次、更易于使用的接口,用于管理代码插桩、回调注册和事件处理等功能。drmgr模块简化了插件的开发,提供了许多底层细节,使得开发者可以更专注于实际的插桩逻辑。

drmgr会替换使用的DynamoRIO基本函数为自己的版本。

  • 回调管理:提供了管理不同类型回调函数的机制,包括基本块插桩、模块加载、线程创建和退出、应用退出等事件。
  • 顺序保证:允许插件指定回调函数的优先级,以控制多个插件之间的回调执行顺序,避免回调之间的冲突和不一致。
  • 扩展性:通过提供一致的API接口,使得新的回调类型能够轻松集成到系统中,增强了插件的扩展能力。
  • 多插件协调:可以协调多个插件之间的交互,确保插件能够安全地共享DynamoRIO的资源和信息。

drreg(DynamoRIO Register Management Extension)

drreg是DynamoRIO的一个寄存器管理扩展,主要作用是在多个插桩组件之间进行寄存器的选择、保留和使用。 它作为一个中介,协调不同插桩组件对寄存器的需求,确保寄存器使用不会发生冲突。

drreg提供了一系列的API,允许用户请求对特定寄存器的独占使用权,并在必要时溢出或恢复应用程序的值。 它基于一种预留模型,用户可以预留一个寄存器,在预留期间,如果需要,drreg会自动保存应用程序的值,并在适当的时候恢复这个值。 此外,drreg还提供了关于寄存器保留和恢复状态的信息查询功能。

drutil(DynamoRIO Instrumentation Utilities)

drutil 提供了一系列的函数,主要用于内存追踪和指令分析。

  • drutil_insert_get_mem_addr:在指定位置插入指令,以确定并存储内存引用的地址到指定的寄存器中。
  • drutil_insert_get_mem_addr_ex:与 drutil_insert_get_mem_addr 类似,但提供了一个额外的参数来指示是否使用了 scratch 寄存器。
  • drutil_opnd_mem_size_in_bytes:返回内存引用的大小(以字节为单位)。
  • drutil_expand_rep_string:将单指令字符串循环(使用 reprepne 前缀)展开成常规循环,以简化内存使用分析。
  • drutil_expand_rep_string_ex:与 drutil_expand_rep_string 类似,但提供了额外的信息,如是否进行了展开,以及展开循环中的字符串指令。
  • drutil_instr_is_stringop_loop:检查给定的指令是否是字符串操作循环。

drwarp(DynamoRIO Function Wrapping and Replacing Extension)

drwrap 是 DynamoRIO 的一个扩展库,提供了函数包装(Function Wrapping)和替换(Function Replacing)的功能。允许用户在不修改原始程序代码的情况下,插入自定义的代码来包装或替换程序中的函数调用。

  • 函数包装(Function Wrapping)

    • 在目标函数调用前后插入自定义的回调函数。这意味着可以在函数执行前后执行特定的代码。可以为每个被包装的函数指定一个pre-function callback和一个post-function callback。
  • 函数替换(Function Replacing)

    • drwrap_replace对目标代码进行替换。替换之后的代码作为App的代码运行。但是每个地址的指令只允许被替换一次(开启override)
    • drwrap_replace_native可以将原始函数修改为App中已有的函数。替换函数必须在返回前调用 drwrap_replace_native_fini,否则控制将丢失,后续应用程序代码将不再受 DynamoRIO 控制。

基本概念

基本块(Basic Block)

基本块是指一段顺序执行的代码,除了入口处没有其他转入分支,除了出口处没有其他转出分支,因此其在没有分支或跳转的情况下连续执行。在DynamoRIO中,基本块是插桩的基本单位,在DynamoRIO中简称bb。Client在bb的入口、出口、中间某个地方插入代码。基本块由指令列表instrlist_t组成,可以通过回调函数和API函数来遍历和操作这些基本块。

轨迹(Trace)

轨迹是DynamoRIO动态形成的频繁执行的基本块序列,即所谓的“热代码”。轨迹是基于执行计数器和回溯头(trace heads)构建的,这些回溯头通常是循环的入口点或现有轨迹的出口点。轨迹事件允许客户端仅针对频繁执行的代码路径进行插桩,这可以提高效率,因为只有热代码路径会被单独插桩和优化,而不是全量的代码。

  • 一旦一个trace被创建,它是由一系列基本块(basic blocks)组成的,这些基本块是按照特定的顺序连接在一起的。这句话的意思是,一旦这个trace结构形成,你不能改变这些基本块的顺序或者替换其中的某些基本块。不能在trace执行后插入新的控制流指令(如跳转或分支)来改变执行流程,也不能移除或替换trace中的任何基本块。如果需要对trace中的代码进行修改,必须在基本块被添加到trace之前或者在构建trace的过程中进行。这通常是通过在基本块事件回调中修改基本块来实现的,而不是直接修改trace。
  • 不能改变每个组件块的终止控制流指令。每个基本块(basic block)的最后一个指令通常是一个控制流指令,比如跳转(jmp)、条件跳转(conditional jump)、函数调用(call)或返回(ret)。这些指令决定了程序执行的下一个位置,是程序流程控制的关键部分,因此在基本块被添加到trace时就已经确定,不能在trace层面上被更改。如果需要在基本块结束时执行一些操作,不能修改现有的控制流指令来实现。
  • 不能添加应用程序控制流指令。不能在trace中插入新的控制流指令,如跳转(jmp)、条件跳转(jcc)、函数调用(call)或返回(ret)等。这些控制流指令会改变程序的执行路径,而DynamoRIO要求在trace创建时保持应用程序的原始控制流不变。
  • 不能添加、修改系统调用指令、中断指令。

将基本块添加到新trace时,基本块回调会被调用,其for_trace参数设置为true。如果要更改组成trace的基本块,可以使用dr_register_end_trace_event或在基本块回调中修改继续地址。

元指令与非元指令

  • 元指令主要用于监控和分析,例如记录函数调用、跟踪变量值或注入日志语句。
  • 非元指令的改变直接影响应用程序的行为,这在元插桩阶段通常较少见,但在某些复杂的调试或分析场景中可能会用到。

drmgr的5个阶段

DynamoRIO Extension之一drmgr对App基本块的作用分为5个阶段。这5个阶段是顺序执行的。

  1. App代码转换(app2app):这个阶段主要是首先将App的代码根据Client注册的事件,将App中的部分函数或指令修改为指定的函数或指令。只有这个阶段可以对原始App代码进行修改操作。
  2. 分析(Analysis):这个阶段不允许对代码进行修改或添加,除了添加标签指令,因此只能做二进制代码分析。这个阶段可以用标签(note)对指令进行标记,然后在后续阶段读取该标签并使用其信息。标签可以自定义数据结构,但是需要使用内存分配空间。注意绑定指令和解绑指令以及最后的free标签的空间。
  3. 插桩(Instrumentation):这个阶段的目的是将监控代码(如性能计数器、日志记录调用等)插入到应用程序代码中。允许开发者在应用程序代码的特定点插入额外的元指令。只能立即在传递给回调的指令之前插入元指令,不能在之前的非元指令之前或后续的非元指令之后插入。此外,不允许插入新的非元指令或更改现有的非元指令。
  4. 插桩代码转换(instru2instru):这个阶段主要是对第3阶段的插桩进行优化和修改操作。允许插入元指令,但不允许插入非元指令。
  5. 元插桩分析和转换(meta-instrumentation analysis and transformations):这个阶段主要是对已经插入到应用程序中的监控代码(元代码)进行分析和进一步转换的过程。允许插入元指令(meta instructions)和非元指令(non-meta instructions),提供了对指令流的广泛控制。

自动推断(auto predicate)

自动推断(auto predicates)是DynamoRIO中的一个特性,它允许动态二进制插桩(DBI)工具在插入的代码中自动处理条件分支。这意味着,当在基本块(basic block)中插入新的指令时,DynamoRIO会尝试将这些新指令放置在原始程序中条件分支指令的相应条件下执行,以保持原始程序的控制流逻辑。自动谓词不会改变原始程序的控制逻辑,它们只是确保插入的代码在正确的条件下执行,以保持程序语义的一致性。

  • 内存引用记录:代码的目的是记录所有内存引用,包括读取和写入操作。这些操作通常与程序的控制流无关,无论条件分支的结果如何,内存访问都应该被记录。因此,插入的代码需要在每个内存访问指令执行时都执行,而不是仅在条件为真时执行。
  • 跨基本块插入:在处理内存引用时,可能需要在多个基本块中插入代码,以确保所有相关的内存访问都被记录。关闭自动谓词推断可以确保插入的代码不会因为基本块的分支条件而遗漏。
  • 工具逻辑的独立性:工具的逻辑是独立的,它不依赖于原始程序的控制流条件。例如,即使原始程序中的某个条件分支从未被执行,工具仍然需要记录所有可能的内存访问。关闭自动谓词推断可以确保工具逻辑的执行不受影响。
  • 性能考虑:虽然自动谓词可以提高性能,但在这种情况下,记录内存访问的开销可能远大于条件分支检查的开销。因此,关闭自动谓词推断可以减少工具逻辑的复杂性,同时对性能的影响可接受。

发表新评论