碧玉妆成一树高
万条垂下绿丝绦
前言
实习的时候有个需求,需要实现一个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, ifunw_local_addr_space
is passed inas
, thenarg
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 ofarg
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文档。
-
获得一块被展开的空间:
unw_addr_space_t unw_create_addr_space(unw_accessors_t *ap, int byteorder)
-
获得当前机器状态的上下文:
int unw_getcontext(unw_context_t *ucp);
-
跟踪目标进程,可选参数很多,这里用的attach和detach:
long int ptrace (enum __ptrace_request __request, ...)
-
根据进程ID得到一个进程信息的
void *
指针,对应之前说的那个arg
:void *_UPT_create (pid_t)
-
对进程堆栈展开,并将信息赋给一个cursor,也就是之前介绍的:
int unw_init_remote(unw_cursor_t *c, unw_addr_space_t as, void *arg)
-
从寄存器中获得某些变量的地址:
unw_get_reg(unw_cursor_t *cp, unw_regnum_t reg, unw_word_t *valp)
-
获取cursor所在栈的函数名字,最后的offp是一个偏移量:
int unw_get_proc_name(unw_cursor_t *cp, char *bufp, size_t len, unw_word_t *offp)
-
将编译器内部函数名转化成外部函数名:
char *name = abi::__cxa_demangle(bufp, nullptr, nullptr, &status)
-
跳到下一个层栈,如果是栈顶则返回0:
int unw_step(unw_cursor_t *cp)
-
最后就是销毁环境:
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。
我这里的输出结果是: