C++

在函数入口出口插桩

C++插桩

Posted by bbkgl on July 17, 2020

千里共如何

微风吹兰杜

知乎上有个问题 如何快速地在每个函数入口处加入相同的语句?

问题好像已经蛮久了,最近工作中需要做一个插桩的工具,然后结合蓝色大大的回答,这里我讲个可能更可用,更简单的方法。

因为clang也支持,所以实际上还能用在android上和ios上。。。这样一想可以做的事情就很多了23333。

插桩

毫无疑问,像蓝色说的那样,我们可以利用clang/gcc的 -finstrument-functions 编译参数,使得每个函数都会在出口和入口会依次执行 __cyg_profile_func_enter 函数和 __cyg_profile_func_exit 函数。

于是可以先编写这么一个hook.cpp文件:

// hook.cpp
#include <cstdio>

extern "C" void __attribute__((constructor)) traceBegin(void) {}

extern "C" void __attribute__((destructor)) traceEnd(void) {}

extern "C" void __cyg_profile_func_enter(void *func, void *caller) {
    fprintf(stdout, "enter func: 0x%016lx father: 0x%016lx\n", func, caller);
}

extern "C" void __cyg_profile_func_exit(void *func, void *caller) {
    fprintf(stdout, "exit func: 0x%016lx father: 0x%016lx\n", func, caller);
}

测试用的main.cpp文件:

// main.cpp
#include <cstdio>

void bbb() {
    
}

void aaa() {
    bbb();
}

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

首先将hook.cpp编译成一个.o文件或者.a文件。。。

g++ hook.cpp -c

然后链接到要被插桩的项目/代码中:

g++ main.cpp hook.o -g -finstrument-functions

然后执行./a.out

enter func: 0x000055f09eac2861 father: 0x00007fb7f24e62e1
enter func: 0x000055f09eac281f father: 0x000055f09eac2889
enter func: 0x000055f09eac27ed father: 0x000055f09eac2840
enter func: 0x000055f09eac27b0 father: 0x000055f09eac2809
exit func: 0x000055f09eac27b0 father: 0x000055f09eac2809
exit func: 0x000055f09eac27ed father: 0x000055f09eac2840
exit func: 0x000055f09eac281f father: 0x000055f09eac2889
exit func: 0x000055f09eac2861 father: 0x00007fb7f24e62e1

就能把函数地址给打印出来。

获取符号名

显然前面显示出来的结果不是很讨人喜欢,我们希望打印的不是地址而是函数符号。

有个蛮有用的函数 dladdr,在进行backtrace的时候会用到。

DLADDR(3)                              Linux Programmer's Manual                              DLADDR(3)

NAME
       dladdr, dladdr1 - translate address to symbolic information

SYNOPSIS
       #define _GNU_SOURCE
       #include <dlfcn.h>

       int dladdr(void *addr, Dl_info *info);

       int dladdr1(void *addr, Dl_info *info, void **extra_info, int flags);

       Link with -ldl.

DESCRIPTION
       The  function dladdr() determines whether the address specified in addr is located in one of the
       shared objects loaded by the calling application.  If it is, then dladdr()  returns  information
       about  the  shared  object  and  symbol  that  overlaps addr.  

这个函数能将地址转换成对应的符号。。。正好满足我们的要求。

于是将hook.cpp进行一下修改:

// hook.cpp
#include <cstdio>
#include <dlfcn.h>

extern "C" void __attribute__((constructor)) traceBegin(void) {}

extern "C" void __attribute__((destructor)) traceEnd(void) {}

extern "C" void __cyg_profile_func_enter(void *func, void *caller) {
    Dl_info info1, info2;
    if (dladdr(func, &info1) & dladdr(caller, &info2))
        fprintf(stdout, "enter func: %s father: %s\n", info1.dli_sname, info2.dli_sname);
}

extern "C" void __cyg_profile_func_exit(void *func, void *caller) {
    Dl_info info1, info2;
    if (dladdr(func, &info1) & dladdr(caller, &info2))
        fprintf(stdout, "exit func: %s father: %s\n", info1.dli_sname, info2.dli_sname);
}

同样编译成.a或者.o:

g++ hook.cpp -c

然后链接到要被插桩的项目/代码main.cpp中(注意链接dl库并使用-rdynamicl编译参数):

g++ main.cpp hook.o -g -finstrument-functions -ldl -rdynamic

然后执行./a.out

enter func: main father: __libc_start_main
enter func: _Z3barv father: main
enter func: _Z3zoov father: _Z3barv
enter func: _Z3foov father: _Z3zoov
exit func: _Z3foov father: _Z3zoov
exit func: _Z3zoov father: _Z3barv
exit func: _Z3barv father: main
exit func: main father: __libc_start_main

这样看来就很熟悉啦,不过因为这里打印的C++的函数符号与实际编写的不太一样,也要进行一下转换。

修改一下hook.cpp:

// hook.cpp
#include <cstdio>
#include <dlfcn.h>
#include <cxxabi.h>

inline const char *get_funcname(const char *src) {
        int status = 99;
        const char *f = abi::__cxa_demangle(src, nullptr, nullptr, &status);
        return f == nullptr ? src : f;
}

extern "C" void __attribute__((constructor)) traceBegin(void) {}

extern "C" void __attribute__((destructor)) traceEnd(void) {}

extern "C" void __cyg_profile_func_enter(void *func, void *caller) {
    Dl_info info1, info2;
    if (dladdr(func, &info1) & dladdr(caller, &info2)) {
        fprintf(stdout, "enter func: %s father: %s\n", get_funcname(info1.dli_sname), get_funcname(info2.dli_sname));
    }
}

extern "C" void __cyg_profile_func_exit(void *func, void *caller) {
    Dl_info info1, info2;
    if (dladdr(func, &info1) & dladdr(caller, &info2)) {
        fprintf(stdout, "exit func: %s father: %s\n", get_funcname(info1.dli_sname), get_funcname(info2.dli_sname));
    }
}

重复上面的编译流程,就能得到如下结果了。

enter func: main father: __libc_start_main
enter func: bar() father: main
enter func: zoo() father: bar()
enter func: foo() father: zoo()
exit func: foo() father: zoo()
exit func: zoo() father: bar()
exit func: bar() father: main
exit func: main father: __libc_start_main