C/C++获取程序堆栈信息

profile

Posted by bbkgl on December 1, 2019

碧玉妆成一树高

万条垂下绿丝绦

前言

实习的时候有个需求,需要实现一个profile工具,工具的功能(简述)有:

  • 根据进程ID和服务器ip获取程序中C++的堆栈信息
  • 根据进程ID和服务器ip获取程序中Lua的堆栈信息
  • 将堆栈调用信息绘制成火焰图

目前打算使用libunwind获得C++的堆栈信息。

libunwind

这玩意特别特别少,特别是相关接口使用的例子,我只找到了一个,然后其他人全部抄的这一个,而且他还是获取当前程序堆栈的,也就是必须在被 profiling 的程序中调用libunwind接口才能获取到堆栈信息。。。还不是中文的,国内关于libunwind的资料真的少

说实话,我觉得这样和现在的需求不符,我们需要是获取其他进程堆栈信息。

然后就只能啃libunwind文档了,发现了两个接口:

unw_init_local(unw_cursor_t *c, unw_context_t *ctxt)
unw_init_remote(unw_cursor_t *c, unw_addr_space_t as, void *arg)

很显然,第一个是在当前程序堆栈为unw_cursor_t指针进行初始化,第二个是利用“远程”程序堆栈为unw_cursor_t指针进行初始化,区别就在于第三个参数了,所以第三个参数一定带有其他进程的信息。

来看看文档中关于arg参数的介绍:

The arg void-pointer tells the address space exactly what entity should be unwound. For example, if unw_local_addr_space is passed in as, then arg needs to be a pointer to a context structure containing the machine-state of the initial stack frame. However, other address-spaces may instead expect a process-id, a thread-id, or a pointer to an arbitrary structure which identifies the stack-frame chain to be unwound. In other words, the interpretation of arg is entirely dependent on the address-space in use; libunwind never interprets the argument in any way on its own.

翻译一下的话,意思就是arg是用来告知“是哪里的堆栈信息应该被解开”,举个例子,如果unw_local_addr_space 是通过as来告知是哪里的堆栈信息要解开,那么arg需要被一个指针来指向一个存有机器状态的空间。然而如果是其他的程序的堆栈空间,则更期望arg指向了一个存有进程id,线程id或者其他堆栈信息的空间。换句话说,需要用户自己来指定这些信息。

说的云里雾里的,可能是自己英语水平太渣了,大概就是arg中需要存有目标进程的信息,目标进程就是要被“解堆栈”的进程,后面那个解释不解释我也不懂了就。

后面在github上直接搜unw_init_remote这个关键词,找到了别人的调用方法。

调用格式是这样的:

void rctx = _UPT_create(pid);   // pid就是目标进程id
unw_init_remote(&cursor, addr_space, rctx)

那怎么绑定进程就解决了。

主要接口介绍

这里只讲这里需要用到的接口,其他的可以自行参考libunwind文档

  1. 获得一块被展开的空间:

     unw_addr_space_t unw_create_addr_space(unw_accessors_t *ap, int byteorder)
    
  2. 获得当前机器状态的上下文:

     int unw_getcontext(unw_context_t *ucp);
    
  3. 跟踪目标进程,可选参数很多,这里用的attach和detach:

     long int ptrace (enum __ptrace_request __request, ...)
    
  4. 根据进程ID得到一个进程信息的void *指针,对应之前说的那个arg

     void *_UPT_create (pid_t)
    
  5. 对进程堆栈展开,并将信息赋给一个cursor,也就是之前介绍的:

     int unw_init_remote(unw_cursor_t *c, unw_addr_space_t as, void *arg)
    
  6. 从寄存器中获得某些变量的地址:

      unw_get_reg(unw_cursor_t *cp, unw_regnum_t reg, unw_word_t *valp)
    
  7. 获取cursor所在栈的函数名字,最后的offp是一个偏移量:

     int unw_get_proc_name(unw_cursor_t *cp, char *bufp, size_t len, unw_word_t *offp)
    
  8. 将编译器内部函数名转化成外部函数名:

     char *name = abi::__cxa_demangle(bufp, nullptr, nullptr, &status)
    
  9. 跳到下一个层栈,如果是栈顶则返回0:

     int unw_step(unw_cursor_t *cp)
    
  10. 最后就是销毁环境:

    int _UPT_resume (unw_addr_space_t, unw_cursor_t *, void *);
        
    void _UPT_destroy (void *);    
    

简单例子

被测试展开的程序1, test.cpp:

#include <cstdio>
#include <unistd.h>


void ddd(int n) {
    if (n == 0) sleep(3000);
    else ddd(n - 1);
}

void ccc() {
    ddd(10);
}

void bbb() {
    ccc();
}

void aaa() {
    bbb();
}

int main() {
    aaa();
    return 0;
}

获取堆栈信息程序, backtrace.cpp:

#include <iostream>
#include <libunwind.h>
#include <libunwind-ptrace.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <cxxabi.h>
#include <cstdio>

int wait4stop(pid_t pid) {
    int status = 99;
    do {
        if (waitpid(pid, &status, 0) == -1 || WIFEXITED(status) || WIFSIGNALED(status))
            return 0;
    } while(!WIFSTOPPED(status));
    return 1;
}

void get_backtrace(pid_t pid) {
    unw_cursor_t cursor, resume_cursor;
    unw_context_t context;
    unw_word_t ip, sp, off;

    unw_addr_space_t addr_space = unw_create_addr_space(&_UPT_accessors, __BYTE_ORDER__);
    if (!addr_space) 
        std::cerr << "Failed to create address space" << std::endl;
    
    unw_getcontext(&context);

    if (-1 == ptrace(PTRACE_ATTACH, pid, nullptr, nullptr))
        std::cerr << "Failed to ptrace" << std::endl;

    if (!wait4stop(pid))
        std::cerr << "wait SIGSTOP of ptrace failed" << std::endl;

    void *rctx = _UPT_create(pid);

    if (rctx == nullptr) 
        std::cerr << "Failed to _UPT_create" << std::endl;

    if (unw_init_remote(&cursor, addr_space, rctx))
        std::cerr << "unw_init_remote failed" << std::endl;

    resume_cursor = cursor;

    const size_t bufflen = 256;
    char *buff = new char[bufflen];

    do {
        char *name = "23333";
        unw_get_reg(&cursor, UNW_REG_IP, &ip);
        unw_get_reg(&cursor, UNW_REG_SP, &sp);

        // 获取函数名字
        if (0 == unw_get_proc_name(&cursor, buff, bufflen, &off)) {
            int status = 99;
            printf("%s---", buff);
            // 如果符号表中没有找到buff的函数名
            if ((name = abi::__cxa_demangle(buff, nullptr, nullptr, &status)) == 0)
                name = buff;
        }
        printf("%s.\n", name);
    } while (unw_step(&cursor) > 0);
    
    delete[] buff;
    _UPT_resume(addr_space, &resume_cursor, rctx);
    _UPT_destroy(rctx);

    // 然后是将进程结束中断
    ptrace(PTRACE_DETACH, pid, nullptr, nullptr);
}

int main(int argc, char **argv) {
    if (argc < 2) 
        std::cerr << "please input pid" << std::endl;

    pid_t pid = std::atoi(argv[1]);
    get_backtrace(pid);
    return 0;
}

需要做的就是首先执行程序test.cpp,然后得到进程id,最后执行backtrace.cpp,参数是test.cpp的进程id。

我这里的输出结果是:

20200112222659.png