标签 调试 下的文章

为函数添加enter和exit级trace

日常开发中,我们为了辅助程序调试常常在每个函数的出入口(entry/exit)增加Trace,一般我们多用宏来实现这些Trace语句,例如:

#ifdef XX_DEBUG_
#define TRACE_ENTER() printf("Enter %s\n", __FUNCTION__)
#define TRACE_EXIT() printf("Exit %s\n", __FUNCTION__)
#else
#define TRACE_ENTER()
#define TRACE_EXIT()
#endif

有了TRACE_ENTER和TRACE_EXIT后,你就可以在你的函数中使用它们了。例如:
void foo(…) {
    TRACE_ENTER();
    … …
    TRACE_EXIT();
}

这样你就可以很容易看到函数的调用关系。不过这种手法用起来却不轻松。首先你需要在每个函数中手工加入TRACE_ENTER和TRACE_EXIT,然后再利用XX_DEBUG_宏控制其是否生效。特别是对于初期未添加函数级Enter/Exit Trace的项目,后期加入工作量很大。

不过Gcc给我们提供了另外一种方便的手法:使用GCC的-finstrument-functions选项。-finstrument-functions使得GCC在生成代码时自动为每个函数在入口和出口生成__cyg_profile_func_enter和__cyg_profile_func_exit两个函数调用。我们要做的就是给出一份两个函数的实现即可。最简单的实现莫过于打印出被调用函数的地址了:

/* func_trace.c */
__attribute__((no_instrument_function))
void __cyg_profile_func_enter(void *this_fn, void *call_site) {
    printf("enter func => %p\n", this_fn);
}

__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void *this_fn, void *call_site) {
    printf("exit func <= %p\n", this_fn);
}

我们将这两个函数放入libfunc_trace.so:gcc -fPIC -shared -o libfunc_trace.so func_trace.c

我们为下面例子添加enter/exit级Trace:

/* example.c */
static void foo2() {

}

void foo1() {
    foo2();
}

void foo() {
    chdir("/home/tonybai");
    foo1();
}

int main(int argc, const char *argv[]) {
    foo();
    return 0;
}

$ gcc -g example.c -o example -finstrument-functions
$ LD_PRELOAD=libfunc_trace.so example
enter func => 0×8048524
enter func => 0x80484e5
enter func => 0x80484b2
enter func => 0×8048484
exit func <= 0×8048484
exit func <= 0x80484b2
exit func <= 0x80484e5
exit func <= 0×8048524

不过只输出函数地址很难让人满意,根据这些地址我们无法得知到底对应的是哪个函数。那我们就尝试一下将地址转换为函数名后再输出,这方面GNU依旧给我们提供了工具,它就是addr2line。addr2line是binutils包中的一个工具,它可以根据提供的地址在可执行文件中找出对应的函数名、对应的源码文件名以及行数。我们改造一下func_trace.c中的两个函数的实现:

/* func_trace.c */
static char path[PATH_MAX];

__attribute__((constructor))
static void executable_path_init() {
    char    buf[PATH_MAX];

    memset(buf, 0, sizeof(buf));
    memset(path, 0, sizeof(path));

#ifdef _SOLARIS_TRACE
    getcwd(buf, PATH_MAX);
    sprintf(path, "%s/%s", buf, getexecname());
#elif _LINUX_TRACE
    readlink("/proc/self/exe", path, PATH_MAX);
#else
    #error "The OS has not been supported!"
#endif
}

__attribute__((no_instrument_function))
void __cyg_profile_func_enter(void *this_fn, void *call_site) {
    char buf[PATH_MAX];
    char cmd[PATH_MAX];

    memset(buf, 0, sizeof(buf));
    memset(cmd, 0, sizeof(cmd));

    sprintf(cmd, "addr2line %p -e %s -f|head -1", this_fn, path);

    FILE *ptr = NULL;
    memset(buf, 0, sizeof(buf));

    if ((ptr = popen(cmd, "r")) != NULL) {
        fgets(buf, PATH_MAX, ptr);
        printf("enter func => %p:%s", this_fn, buf);
    }

    (void) pclose(ptr);
}

__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void *this_fn, void *call_site) {
    char buf[PATH_MAX];
    char cmd[PATH_MAX];

    memset(buf, 0, sizeof(buf));
    memset(cmd, 0, sizeof(cmd));

    sprintf(cmd, "addr2line %p -e %s -f|head -1", this_fn, path);

    FILE *ptr = NULL;
    memset(buf, 0, sizeof(buf));

    if ((ptr = popen(cmd, "r")) != NULL) {
        fgets(buf, PATH_MAX, ptr);
        printf("exit func <= %p:%s", this_fn, buf);
    }

    (void) pclose(ptr);
}

在我的Ubuntu 10.04下,我们编译和执行

$ gcc -D_LINUX_TRACE -fPIC -shared -o libfunc_trace.so func_trace.c
$ gcc -g example.c -o example -finstrument-functions
$ LD_PRELOAD=libfunc_trace.so example
$ example
enter func => 0×8048524:main
enter func => 0x80484e5:foo
enter func => 0x80484b2:foo1
enter func => 0×8048484:foo2
exit func <= 0×8048484:foo2
exit func <= 0x80484b2:foo1
exit func <= 0x80484e5:foo
exit func <= 0×8048524:main

关于这个实现,还有几点要说道说道:
首先libfunc_trace.so是动态链接到你的可执行程序中的,那么如何获取addr2line所需要的文件名是一个问题;另外考虑到可执行程序中可能会调用chdir这样的接口更换当前工作路径,所以我们需要在初始化时就得到可执行文件的绝对路径供addr2line使用,否则会出现无法找到可执行文件的错误。在这里我们利用了GCC的__attribute__扩展:
__attribute__((constructor))

这样我们就可以在main之前就将可执行文件的绝对路径获取到,并在__cyg_profile_func_enter和__cyg_profile_func_exit中直接引用这个路径。

在不同平台下获取可执行文件的绝对路径的方法有不同,像Linux下可以利用"readlink /proc/self/exe"获得可执行文件的绝对路径,而Solaris下则用getcwd和getexecname拼接。

再总结一下,如果你想使用上面的libfunc_trace.so,你需要做的事情有:
1、将编译好的libfunc_trace.so放在某路径下,并export LD_PRELOAD=PATH_TO_libfunc_trace.so/libfunc_trace.so
2、你的环境下需要安装binutils的addr2line
3、你的应用在编译时增加-finstrument_functions选项。

我已经将这个小工具包放到了Google Code上,有兴趣的朋友可以在这里下载完整源码包(20110715更新:支持输出函数所在源文件路径以及所在行号,前提编译你的程序时务必加上-g选项)。

又遇字节序问题

今天上午处理了一个线上产品的故障。分析来分析去,最后定位问题还是出在字节序转换的环节上。

其实测试组早在产品上线前就曾报告了这个问题,但是对应的开发人员并未对该问题进行深入地分析,而是有些草率地将该问题归结为客户端模拟器的实现不符合标准。因为这位同事比较资深,所以当时我也没有给予足够关注。

产品今天凌晨上线,9点左右业务量开始增大,这个问题立即就被我们在现场的运维人员发现,还好我们的系统是集群式的,运维同事及时的将线上有问题的版本停掉,用其他服务器支撑起了全部业务,躲过一劫。

我们还是回到这个问题上来。经验告诉我们:严重的问题往往都是由极其简单的错误导致的。这次也不例外!问题的直接原因就是:多调用了一次htonl。的确就是这么简单,但如果继续深入下去,我们还能得到一些收获。

当产品运行在x86服务器上,这个问题就会暴露出来,但是在Sun Sparc服务器上,该产品运行良好。我们分析后的结论是:这是由于在两种体系结构上htonl的实现不同而导致的。

我们先来做个试验,看下面的代码和执行结果:

/* testhtonl.c */
#include "stdio.h"
#include "arpa/inet.h"

int main() {
    unsigned int a = 0×12345678;
    unsigned int b = htonl(a);

    printf("0x%x\n", b);
    printf("0x%x\n", htonl(b));
   
    return 0;
}

将上面代码分别在x86和Sparc上编译运行。在x86上(ubuntu 10.04 Gcc 4.4.3 x86)运行的结果如下:
0×78563412
0×12345678

而在Sparc上(Solaris 10 for Sparc, Gcc 3.4.6)运行的结果如下:
0×12345678
0×12345678

由此我们可以看出,htonl这个接口并不总等价于字节序转换。在Sparc这种Big-endian体系结构的平台上,htonl相当于直接将参数值返回;而在x86这样的little-endian体系结构平台上,htonl则是等价于一个reverse_byte_order接口,每次调用都会把输入参数的byte order倒转后的结果返回。

还回到我们的那个问题中:多调了一次htonl在Sparc平台上没有什么影响;但是在x86平台上,我们得到了相反字节序的结果,导致故障的出现。

这不是我们第一次遇到字节序问题了,不过却是第一次在线上产品中遇到,上一次是在开发过程中遇到的。这次发生的问题并不仅仅是技术上的问题,更多的是在工作的严谨性和工作态度上出现问题了。对我来说,这是一个很值得吸取的教训。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats