无垠之码

深度剖析代码之道


valgrind高级主题

1.客户端请求机制


Valgrind客户端请求机制是一种内嵌于用户程序(客户端)与Valgrind核心引擎(服务端)之间的通信后门。在常规运行模式下,目标程序对虚拟化环境是无感知的;而该机制赋予了程序主动交互的能力。通过向引擎传递特定的元数据或控制指令,程序可以弥补静态分析的局限性,告知引擎那些难以自动捕获的运行时上下文,从而实现更深层次的性能分析与异常诊断。

客户端请求机制的集成极其简便:仅需引入Valgrind提供的头文件,并直接调用相应的宏功能即可。该过程无需链接任何外部库,且宏在非调试环境下具有零性能损耗的特性,实现了与生产代码的无缝兼容。

1.1工作原理

Valgrind的客户端请求机制实现简洁且高效,服务端内部使用4个字节定义各个模块的请求消息,其中前2个字节为模块名称,后2个字节关联消息的处理函数,比如memcheck模块,使用MC前缀。客户端的每种功能宏函数最终都会在VALGRIND_DO_CLIENT_REQUEST_EXPR处展开,构造成汇编指令,发送到Valgrind虚拟环境执行。Valgrind的执行器,遇见__SPECIAL_INSTRUCTION_PREAMBLE这段特殊的前导汇编指令序列,会直接获取定义在rax寄存器中存储的消息类型以及参数信息,进而调用相关函数处理完成实际工作,处理完成后将返回结果放入rdx寄存器,同时返回。

#define VG_USERREQ_TOOL_BASE(a,b) ((unsigned int)(((a)&0xff) << 24 | ((b)&0xff) << 16))

typedef
   enum { 
      VG_USERREQ__MAKE_MEM_NOACCESS = VG_USERREQ_TOOL_BASE('M','C'),
      ....
      _VG_USERREQ__MEMCHECK_RECORD_OVERLAP_ERROR = VG_USERREQ_TOOL_BASE('M','C') + 256
   } Vg_MemCheckClientRequest;

#define VALGRIND_MAKE_MEM_NOACCESS(_qzz_addr,_qzz_len)           \
    VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */,      \
                            VG_USERREQ__MAKE_MEM_NOACCESS,       \
                            (_qzz_addr), (_qzz_len), 0, 0, 0)

#define __SPECIAL_INSTRUCTION_PREAMBLE                            \
                     "rolq $3,  %%rdi ; rolq $13, %%rdi\n\t"      \
                     "rolq $61, %%rdi ; rolq $51, %%rdi\n\t"

#define VALGRIND_DO_CLIENT_REQUEST_EXPR(                          \
        _zzq_default, _zzq_request,                               \
        _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5)    \
    __extension__                                                 \
    ({ volatile unsigned long int _zzq_args[6];                   \
    volatile unsigned long int _zzq_result;                       \
    _zzq_args[0] = (unsigned long int)(_zzq_request);             \
    _zzq_args[1] = (unsigned long int)(_zzq_arg1);                \
    _zzq_args[2] = (unsigned long int)(_zzq_arg2);                \
    _zzq_args[3] = (unsigned long int)(_zzq_arg3);                \
    _zzq_args[4] = (unsigned long int)(_zzq_arg4);                \
    _zzq_args[5] = (unsigned long int)(_zzq_arg5);                \
    __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE               \
                     /* %RDX = client_request ( %RAX ) */         \
                     "xchgq %%rbx,%%rbx"                          \
                     : "=d" (_zzq_result)                         \
                     : "a" (&_zzq_args[0]), "0" (_zzq_default)    \
                     : "cc", "memory"                             \
                    );                                            \
    _zzq_result;                                                  \
    })

1.2特殊宏定义

Valgrind特殊宏,依据相关功能分别定义于valgrind.h、memcheck.h、callgrind.h、helgrind.h头文件中。

valgrind.h相关宏

RUNNING_ON_VALGRIND: 判断程序环境是否运行于Valgrind模拟的CPU环境,返回Valgrind嵌套层数。
VALGRIND_DISCARD_TRANSLATIONS: 
VALGRIND_COUNT_ERRORS: 返回Valgrind发现的错误数目。
VALGRIND_MALLOCLIKE_BLOCK: 如果程序的内存管理非标准库函数malloc、new、new[],valgrind memcheck的内存追踪将失效。使用该宏追踪非标准分配器分配的内存状态。
VALGRIND_FREELIKE_BLOCK:
VALGRIND_RESIZEINPLACE_BLOCK: 通知Valgrind工具分配的块的大小已被修改,但其地址没有变化。
VALGRIND_CREATE_MEMPOOL, VALGRIND_DESTROY_MEMPOOL, VALGRIND_MEMPOOL_ALLOC, VALGRIND_MEMPOOL_FREE, VALGRIND_MOVE_MEMPOOL, VALGRIND_MEMPOOL_CHANGE, VALGRIND_MEMPOOL_EXISTS
VALGRIND_NON_SIMD_CALL: 命令Valgrind在CPU上执行指定函数。函数支持0-3个参数。
VALGRIND_PRINTF: 打印printf风格的消息至Valgrind日志文件。消息使用pid作为消息前缀。
VALGRIND_PRINTF_BACKTRACE: VALGRIND_PRINTF类似,但在打印消息后立即打印堆栈回溯信息。
VALGRIND_MONITOR_COMMAND: 执行Valgrind监控指令。指令有效返回0,否则返回-1。
VALGRIND_CLO_CHANGE: 程序中动态改变Valgrind部分参数。(新版本支持)
VALGRIND_STACK_DEREGISTER:
VALGRIND_STACK_CHANGE: 通知Valgrind之前注册过的堆栈发生改变。
VALGRIND_STACK_REGISTER: 通知Valgrind参数start和end之间是唯一的栈空间,返回的标识符作为其他VALGRIND_STACK_*函数的参数使用。Valgrind使用该信息来确定堆栈指针的更改是由于函数调用还是线程切换引起,如果堆栈指针的更改发生在注册的堆栈内,则认为是函数调用。如果堆栈指针的更改发生在未注册的堆栈内,则Valgrind将认为该更改是切换到新线程。Valgrind本身是基于操作系统的内存调试工具,可能对协程的堆栈存在一些困难或不完整。这个选项可以帮助Valgrind更准确地识别堆栈信息,从而改善Valgrind对于用户级线程包情境下的堆栈跟踪和错误报告的准确性。(Unfortunately, this client request is unreliable and best avoided)

注意:

VALGRIND_CLO_CHANGE宏提交于3a803036f719,此前版本的Valgrind不支持

valgrind --help-dyn-options:
dynamically changeable options:
    -v --verbose -q --quiet -d --stats --vgdb=no --vgdb=yes --vgdb=full
    --vgdb-poll --vgdb-error --vgdb-stop-at --error-markers --show-error-list -s
    --show-below-main --time-stamp --trace-children --child-silent-after-fork
    --scheduling-quantum --trace-sched --trace-signals --trace-symtab
    --trace-cfi --debug-dump=syms --debug-dump=line --debug-dump=frames
    --trace-redir --trace-syscalls --sym-offsets --progress-interval
    --merge-recursive-frames --vex-iropt-verbosity --suppressions --trace-flags
    --trace-notbelow --trace-notabove --profile-flags --gen-suppressions=no
    --gen-suppressions=yes --gen-suppressions=all --errors-for-leak-kinds
    --show-leak-kinds --leak-check-heuristics --show-reachable
    --show-possibly-lost --freelist-vol --freelist-big-blocks --leak-check=no
    --leak-check=summary --leak-check=yes --leak-check=full --ignore-ranges
    --ignore-range-below-sp --show-mismatched-frees --show-realloc-size-zero

memcheck.h相关宏

Valgrind Memcheck对应用程序的对每个字节内存,维护1. 访问性,2. 定义性,两种状态。访问性标识内存是不是合法的地址,可以被读/写。定义性标识内存里的值是不是已经被初始化。

VALGRIND_MAKE_MEM_NOACCESS: 内存区域标识宏
VALGRIND_MAKE_MEM_UNDEFINED: 
VALGRIND_MAKE_MEM_DEFINED: 
VALGRIND_MAKE_MEM_DEFINED_IF_ADDRESSABLE: 设置参数指定的内存区域中具有可访问性的内存为已定义(已初始化),其他保持不变
VALGRIND_CREATE_BLOCK: 为指定的内存区域添加一个描述性标签,增强Valgrind错误报告的可读性,返回一个句柄
VALGRIND_DISCARD: 释放描述性标签,输入VALGRIND_CREATE_BLOCK生成的句柄
VALGRIND_CHECK_MEM_IS_ADDRESSABLE: 
VALGRIND_CHECK_MEM_IS_DEFINED: 
VALGRIND_CHECK_VALUE_IS_DEFINED: 功能与VALGRIND_CHECK_MEM_IS_DEFINED一样,但VALGRIND_CHECK_VALUE_IS_DEFINED直接输入变量,而非变量的地址+长度
VALGRIND_DO_LEAK_CHECK: 程序运行到VALGRIND_DO_LEAK_CHECK调用处,强制Valgrind执行内存泄漏检测,类似--leak-check=full
VALGRIND_DO_ADDED_LEAK_CHECK: 与VALGRIND_DO_LEAK_CHECK功能相同,但只打印自上次内存泄漏检测以来的泄漏增量
VALGRIND_DO_CHANGED_LEAK_CHECK: 对比两次内存泄漏检测后的变化,报告泄漏增量部分和泄漏减少部分
VALGRIND_DO_QUICK_LEAK_CHECK: 类似--leak-check=summary,报告泄漏摘要
VALGRIND_COUNT_LEAKS: 返回上次泄漏检测的结果,确认泄漏数量, 可疑泄漏数量, 可达但未释放区域数量等(单位字节)
VALGRIND_COUNT_LEAK_BLOCKS: 单位块
VALGRIND_GET_VBITS: 查询参数指定的区间[zza, zza+zznbytes-1]上,每个字节在Memcheck中的已定义/未定义状态,结果写入出参zzvbits
VALGRIND_SET_VBITS: 
VALGRIND_DISABLE_ADDR_ERROR_REPORTING_IN_RANGE: 通知Valgrind关闭指定地址区间的地址错误报告,这段内存里的越界访问、未分配访问之类,Valgrind不再报错/告警
VALGRIND_ENABLE_ADDR_ERROR_REPORTING_IN_RANGE: 

注意:

  1. 可疑泄漏,当堆内存A,已经不再有指针指向其区域(肯定泄漏),另一块堆内存B,只有通过A才能间接访问到。此种情况,A属于肯定泄漏,B算入可疑泄漏
  2. 抑制数量,有些已知的、不想关心的泄漏(比如第三方库、glibc 里的一些单例缓存),可以写到suppression文件中(–suppressions=xxx),满足这些抑制规则的泄漏。

callgrind相关宏

CALLGRIND_DUMP_STATS: 命令Valgrind,将当前已收集到的开销统计(函数调用关系、指令数、cache miss等)写出到新的profile文件,然后将内部累计的计数器清零 
CALLGRIND_DUMP_STATS_AT: 功能与CALLGRIND_DUMP_STATS相同,增加理由字符串参数,写进profile文件里当描述字段,方便区分这一份dump是在什么位置/原因触发
CALLGRIND_ZERO_STATS: 
CALLGRIND_TOGGLE_COLLECT:切换收集开关[on|off],假设当前状态为on,调用后会关闭开销收集,若当前为off,调用后会开启收集。注意只是关闭记账功能,程序仍然被插桩,存在性能损失
CALLGRIND_START_INSTRUMENTATION: 关闭程序中定义的相关插桩点,无性能损失
CALLGRIND_STOP_INSTRUMENTATION: 

helgrind.h相关宏

VALGRIND_HG_MUTEX_INIT_POST: 用于自定义锁实现/锁封装场景,在锁初始化完成后调用,让Helgrind正确建立锁的元数据,后续才能更准确地判断加锁、解锁和数据竞争关系,对普通 pthread_mutex_*,Helgrind通常已有拦截,不一定需要你手动调用
VALGRIND_HG_MUTEX_LOCK_PRE: 
VALGRIND_HG_MUTEX_LOCK_POST: 
VALGRIND_HG_MUTEX_UNLOCK_PRE: 
VALGRIND_HG_MUTEX_UNLOCK_POST: 
VALGRIND_HG_MUTEX_DESTROY_PRE: 
VALGRIND_HG_SEM_INIT_POST: 与HG_MUTEX类似,自定义信号量场景
VALGRIND_HG_SEM_WAIT_POST: 
VALGRIND_HG_SEM_POST_PRE: 
VALGRIND_HG_SEM_DESTROY_PRE: 
VALGRIND_HG_BARRIER_INIT_PRE: 与HG_MUTEX类似,自定义barrier场景,目的是把barrier的初始化过程显式告知Helgrind,帮助它更准确识别同步关系、减少误报和漏报,
VALGRIND_HG_BARRIER_WAIT_PRE: 
VALGRIND_HG_BARRIER_RESIZE_PRE: 
VALGRIND_HG_BARRIER_DESTROY_PRE: 
VALGRIND_HG_CLEAN_MEMORY: 重置内存区的并发分析状态,常见于内存复用场景,如对象池、内存池、自定义分配器等,从语义上看,这个宏也可理解为“把这段内存的所有权重新交给当前线程起点”,当前线程接下来可以在没有额外同步的前提下安全初始化这块内存,而其他线程若要安全访问它,仍需要通过同步建立happens-before关系
VALGRIND_HG_CLEAN_MEMORY_HEAPBLOCK: 适用于只知道对象指针、但不知道大小的场景
VALGRIND_HG_DISABLE_CHECKING: 指示指定内存区域,不再被跟踪检查
VALGRIND_HG_ENABLE_CHECKING: 
VALGRIND_HG_GET_ABITS: 与VALGRIND_GET_VBITS类似
VALGRIND_HG_GNAT_DEPENDENT_MASTER_JOIN: 与Ada语言相关
ANNOTATE_CONDVAR_LOCK_WAIT: 标注在某个锁的保护下等待条件变量
ANNOTATE_CONDVAR_WAIT: 标注条件变量等待操作完成这一时刻
ANNOTATE_CONDVAR_SIGNAL: 告知Helgrind,该操作将唤醒,至多一个等待该条件变量的线程
ANNOTATE_CONDVAR_SIGNAL_ALL: 
ANNOTATE_HAPPENS_BEFORE: ANNOTATE_HAPPENS_BEFORE(obj)用于在发布端(生产者端)以obj为同步事件标识,声明一条先行关系的起点,使该调用之前的内存访问可与匹配的 ANNOTATE_HAPPENS_AFTER(obj)之后访问建立happens-before关系
ANNOTATE_HAPPENS_AFTER: 
ANNOTATE_HAPPENS_BEFORE_FORGET_ALL: 清除Helgrind中与obj关联的全部BEFORE历史状态,使该同步标签回到“未建立先行关系”的初始状态,后续ANNOTATE_HAPPENS_AFTER(obj)在新的 BEFORE出现前不再产生配对效果
ANNOTATE_PUBLISH_MEMORY_RANGE: 声明内存发布同步语义的注解宏,但Helgrind目前不支持其语义实现
ANNOTATE_PURE_HAPPENS_BEFORE_MUTEX: Helgrind中该宏当前未实现
ANNOTATE_NEW_MEMORY: 声明一段重新分配/新分配的内存已进入新生命周期,要求竞态检测器清除该区间的旧同步历史并将其视为当前线程新拥有的内存
ANNOTATE_PCQ_CREATE: 用于标记FIFO队列创建事件的注解宏,但Helgrind暂不支持其语义实现
ANNOTATE_PCQ_DESTROY: 
ANNOTATE_PCQ_PUT: 
ANNOTATE_PCQ_GET: 
ANNOTATE_BENIGN_RACE: 把某处数据竞争标记为可接受/良性竞争以抑制报告,但在Helgrind中该宏未实现
ANNOTATE_BENIGN_RACE_SIZED: 
ANNOTATE_IGNORE_READS_BEGIN: 临时忽略后续读访问的竞态检查,但在Helgrind中该宏未实现
ANNOTATE_IGNORE_READS_END: 
ANNOTATE_IGNORE_WRITES_BEGIN: 
ANNOTATE_IGNORE_WRITES_END: 
ANNOTATE_IGNORE_READS_AND_WRITES_BEGIN: 
ANNOTATE_IGNORE_READS_AND_WRITES_END: 
ANNOTATE_TRACE_MEMORY: 请求竞态检测器跟踪并输出对address的访问轨迹,性能原因尚未实现
ANNOTATE_THREAD_NAME: 把当前线程的名称上报给竞态检测器,提升报告可读性
ANNOTATE_RWLOCK_CREATE: 对开发者实现的锁元语注解,声明一个用户实现的读写锁对象已创建,让Helgrind从此按rwlock语义跟踪该锁的并发同步关系
ANNOTATE_RWLOCK_DESTROY: 
ANNOTATE_RWLOCK_ACQUIRED: 
ANNOTATE_RWLOCK_RELEASED: 
ANNOTATE_BARRIER_INIT: 声明一个用户实现的屏障对象已按给定参与线程数完成初始化,并指明是否允许重复初始化,但在Helgrind中该宏未实现
ANNOTATE_BARRIER_WAIT_BEFORE: 
ANNOTATE_BARRIER_WAIT_AFTER: 
ANNOTATE_BARRIER_DESTROY: 
ANNOTATE_EXPECT_RACE: 在竞态检测器测试中声明,地址参数指定的地方预期会发生数据竞争,用于校验检测器行为,单元测试,但在Helgrind中该宏未实现
ANNOTATE_NO_OP: NOP操作,在Helgrind中该宏未实现
ANNOTATE_FLUSH_STATE: 请求竞态检测器清空/刷新其内部分析状态,在Helgrind中该宏未实现

1.3用法举例

以下代码片段基本涵盖上述宏的常见使用方法:

#include <stdlib.h>
#include <string.h>

#include "debug.h"
#include "memp.h"

void definitely_loss() { int* p = malloc(sizeof(int)); }

int main() {
    if (RUNNING_ON_VALGRIND) {
        VALGRIND_PRINTF("valgrind nested: %d\r\n", RUNNING_ON_VALGRIND);
            VALGRIND_CLO_CHANGE("--leak-check=full");
            VALGRIND_CLO_CHANGE("--show-error-list=yes");
    }

    memp_t* memp = pool_create(1 << 20);
    char* name = (char*)pool_alloc(memp, 64);
    int* age = malloc(sizeof(int));
    definitely_loss();
    pool_free(memp, name);
    strncpy(name, "peter", 64);
    pool_destroy(memp);

    if (RUNNING_ON_VALGRIND) {
        VALGRIND_DO_ADDED_LEAK_CHECK;
        if (VALGRIND_COUNT_ERRORS) VALGRIND_PRINTF_BACKTRACE("valgrind errors: %d\r\n", VALGRIND_COUNT_ERRORS);
    }

    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <ucontext.h>
#include <valgrind/valgrind.h>

#define STACK_SIZE   (8*1024)

int n_ucs = 1;
int max_switchs = 10;
int n_switchs = 0;
int tid = 0;

ucontext_t *ucs;
static ucontext_t engine_uc;

void func(int arg)
{
    printf("I am thread %d\n", (int) arg);

    while (n_switchs < max_switchs) {
        int c_tid = tid;
        int n_tid = (tid + 1) % n_ucs;
        n_switchs++;
        tid = n_tid;
        printf("%d ---> %d\n", c_tid, n_tid);
        swapcontext(&ucs[c_tid], &ucs[n_tid]);
    }
}

int main(int argc, char **argv)
{
    int i;
    n_ucs = atoi(argv[1]);
    max_switchs = atoi(argv[2]);
    ucs = malloc(sizeof(ucontext_t) * n_ucs);
    int* valgrind_ret = malloc(n_ucs*sizeof(int));

    for (i = 0; i < n_ucs; i++) {
        getcontext(&ucs[i]);
        void* mystack = malloc(STACK_SIZE);
        valgrind_ret[i] = VALGRIND_STACK_REGISTER(mystack, mystack + STACK_SIZE);
        ucs[i].uc_stack.ss_sp = mystack;
        ucs[i].uc_stack.ss_size = STACK_SIZE;
        ucs[i].uc_stack.ss_flags = 0;
        ucs[i].uc_link = &engine_uc;
        makecontext(&ucs[i], (void (*)())func, 1, i);
    }

    swapcontext(&engine_uc, &ucs[tid]);

    for (i = 0; i < n_ucs; i++) {
        VALGRIND_STACK_DEREGISTER(valgrind_ret[i]);
        free(ucs[i].uc_stack.ss_sp);
    }

    free(ucs);
    free(valgrind_ret);
    return 0;
}
/* 摘自 Testing constant-timeness using Valgrind: case of the NSS library */
#include <valgrind/memcheck.h>

int compute(unsigned char secret[32]) {
    if (secret[0] == 0) {
        return 0;
    } else {
        return 1;
    }
}

int main(void) {
    unsigned char buf[32];
    for (int i = 0; i < 32; ++i)
        buf[i] = 0;
    VALGRIND_MAKE_MEM_UNDEFINED(buf, 32);
    compute(buf);
    return 0;
}

2.使用Valgrind内置gdbserver


在使用Valgrind对程序进行诊断或测试时,目标程序运行在Valgrind提供的虚拟执行环境中。因此,常规方式下无法直接使用GDB对程序进行attach调试。

为了解决这一问题,Valgrind提供了内置的调试支持机制。通过启用vgdb相关参数,Valgrind可以启动一个内置的GDB Server,使得GDB能够通过远程调试GDB remote debugging协议与被调试程序进行通信。

在这种模式下,GDB作为客户端运行在本地,Valgrind内部的GDB Server负责提供程序的断点信息,寄存器状态,内存数据等。两者之间通过vgdb工具建立通信通道,通常是管道,也可以是TCP/IP或其他方式。相比普通调试方式,这种机制不仅支持标准的GDB命令,还可以结合Valgrind提供的能力,实现更深入的分析,例如内存错误检测,内存泄漏分析,缓存行为分析。通常通过Valgrind+GDB的组合,可以在动态分析环境中实现更强大的调试能力。

用法举例

# a.本地调试
valgrind --vgdb=yes --vgdb-stop-at=startup /usr/bin/sleep 3600
gdb /usr/bin/sleep -ex 'target remote | vgdb'

# b.远程调试
valgrind --vgdb=yes --vgdb-stop-at=startup /usr/bin/sleep 3600
vgdb --port=1024
gdb -ex 'target remote 127.0.0.1:1024'

特殊命令

关于如何使用相关模块提供的特殊命令,可以参考下列文档:

  1. memcheck相关命令
  2. callgrind相关命令
  3. massif相关命令
  4. helgrind相关命令

3.函数包裹


Valgrind允许对某些指定函数的调用进行拦截,并重定向到用户提供的另一个函数。这个“包装函数”可以执行任意操作,通常包括:

  1. 检查函数参数
  2. 调用原始函数
  3. 检查原始函数的返回结果

函数包装可以对任意数量的函数进行包装,在对某个API进行监控或检测时非常有用。

int add(int x, int y){
    return x + y ;
}

gcc -fPIC -shared -o libadd.so add.c
#include "add.h"
#include <stdio.h>

int main(){
   printf("%d\r\n", add(1,1));
   return 0;
}

gcc -g -O0 main.c -L. -ladd -Wl,-rpath=/tmp/test/
#include <stdio.h>
#include "valgrind/valgrind.h"

int I_WRAP_SONAME_FNNAME_ZU(NONE,add)(int x, int y)
{
   int    result;
   OrigFn fn;
   VALGRIND_GET_ORIG_FN(fn);
   printf("foo's wrapper: args %d %d\n", x, y);
   CALL_FN_W_WW(result, fn, x,y);
   printf("foo's wrapper: result %d\n", result);
   return result;
}

gcc -shared -fPIC -o add_wrapper.so add_wrapper.c
LD_PRELOAD=./add_wrapper.so valgrind ./a.out

4.参考文献

  1. Valgrind-用户手册
  2. 协程代码Valgrind示例
  3. Testing constant-timeness using Valgrind: case of the NSS library
  4. Automated dynamic analysis for timing side-channels
  5. 性能优化之valgrind之callgrind分析瓶颈