Linux线上系统程序debug思路及方法

       很多程序长期在线上系统跑着,可能跑着跑着就coredump了,而这种bug比较难复现,这个问题估计困扰不少同行朋友,这里记录一下我的一些思路,如有不对之处,欢迎指正。

1、coredump文件

       这个方法很基础了,相信大家都知道,具体步骤如下:

ulimit -c unlimited
ulimit -c unlimited' >> /etc/profile
service apport stop
sysctl -w kernel.core_pattern=/var/log/%e.core.%p

       这几个命令也不用多说,主要是开启coredump以及指定coredump保存的路径,当coredump文件产生时,用gdb指定二进制文件和coredump文件进去就可以调试了,比如bt查看调用堆栈、查看寄存器、反汇编、查看变量等等。这个方法在开发环境比较合适,在线上环境就要小心了,原因是coredump文件很大,一两G都有可能,如果程序出现重复或者多次coredump那用不了多长时间硬盘就满了,硬盘满了之后一些重要的日志(比如nginx的访问日志)就无法写入了,所以这种方法在硬盘够大或者只是偶尔出现coredump的情况下比较合适。

2、screen+gdb

       如果在关闭coredump的情况下,还有几种方法可以使用,这里先介绍screen+gdb的方法,这种方法也比较简单,就是用gdb attach到可能出现coredump的进程,然后等待coredump的出现就可以在线调试了,但是得结合screen来使用,因为我们上线上服务器一般都是ssh上去的,这个ssh连接可能比较容易断掉,如果这个ssh连接被断掉了,那之前gdb attach就白费了,所以结合screen来用就可以了。步骤:首先ssh连接服务器,其次screen打开一个shell,然后gdb attach到指定进程,最后就是等待coredump的出现了。也不是死等,就是每半天或者过一段时间来看看有没有产生coredump就行。

       这个方法优点就是能直接在线调试,比较容易找出bug原因,但也缺点也是有的,因为gdb上去之后肯定会影响程序的性能,二是发生coredump之后程序就卡在gdb那里了,如果是nginx之类的程序,那可能一些请求就被超时了。这个方法就酌情使用吧。

3、内核日志+反汇编

       如果没有开启coredump,而程序又死了,也没法gdb attach上去调试。这种情况下,可以分析一下dmesg,比如下面有一个产生coredump的例子:

raise_coredump.c:

#include <stdio.h>

void func(char *p)
{
    *p = 'p';
}

int main(int argc, char *argv[])
{
    char *a;
    int  b;
    char *p = NULL;

    a = "aa";
    b = 22;

    func(p);

    return 0;
}
上面代码中func函数修改了字符串参数p的第一个字符为p,但它并没有判断参数p是否为NULL,所以在main函数中调用func函数并传一个NULL指针进去,肯定coredump,下面是编译运行结果:

root@jusse ~/develop/debug_coredump# dmesg -c

root@jusse ~/develop/debug_coredump# gcc -Wall -g -o raise_coredump ./raise_coredump.c
./raise_coredump.c: In function ‘main’:
./raise_coredump.c:11:10: warning: variable ‘b’ set but not used [-Wunused-but-set-variable]
./raise_coredump.c:10:11: warning: variable ‘a’ set but not used [-Wunused-but-set-variable]

root@jusse ~/develop/debug_coredump# ./raise_coredump       
Segmentation fault

root@jusse ~/develop/debug_coredump# 
可见coredump已经产生了,调试方法就是dmesg和反汇编:


如图所示,先是用dmesg看看内核日志,ip后面跟的就是指令寄存器的值,也就是当前正在执行指令的地址,有了这个地址我们就可以用objdump来反汇编看看是哪条指令引起的coredump了,如果编译时没有优化也可以直接用addr2line来看是哪行代码出了问题,如果嫌objdump+grep麻烦的话,也可以用gdb来反汇编:


可以看出gdb的反汇编也能直接显示是在哪个函数以及相应的指令了。(这种方法应该不合适于程序加载地址随机的情况,待验证)。gdb的这个方式更适合于像调试nginx这种多进程,比如其中一个进程coredump重启了,我们可以dmesg拿到地址之后,gdb attach到nginx的master进程中,再disassemble就可以查看相应的汇编指令了。

4、breakpad

       breakpad是google搞出来的一个东西,如果产生coredump,那breakpad将收集调用堆栈信息,然后用它自己的格式保存到文件中,我们可以把这个文件拿到本地分析。这种方法优点是能收集产生coredump时的调用堆栈,而且文件比较小,可以拿到本地分析,比较适合线上系统,缺点是收集的信息比较少,而且在C语言里用的话还需要把它的C++库进行一次封装,比较麻烦。有兴趣可以看看:http://blog.csdn.net/wpc320/article/details/8291296

5、处理SIGSEGV信号

       其实,我重点是想介绍这种方式,而且据了解,很多人也已经采用这种方式。原理就是:因为coredump主要是由于一些非法操作导致产生SIGSEGV信号而引起的,所以在我们的程序中注册SIGSEGV信号,当进程收到SIGSEGV信号时,在我们的信号处理函数中调用gdb来attach到自己的进程,然后就可以通过gdb来收集自己想要的信息了。例子如下:

segmentfault_handler.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>

void segment_fault_handler(int sig)
{
    char gdb_cmd[64] = {0};
    printf("segment_fault_handler\n");
    
    //组合gdb命令参数,gdb.cmd文件是gdb的命令脚本,gdb的输出重定向到gdb_debug.log日志文件中
    snprintf(gdb_cmd, sizeof(gdb_cmd)-1, "gdb -q -p %d -x ./gdb.cmd >gdb_debug.log 2>&1", getpid());
    system(gdb_cmd);
    signal(SIGSEGV, SIG_DFL);
}

void func(char *p)
{
    *p = 'p';//产生coredump
}

int main(int argc, char *argv[])
{
    printf("main start\n");
    char *p = NULL;

    //注册SIGSEGV信号
    if (signal(SIGSEGV, segment_fault_handler) == SIG_ERR) {
        perror("signal error: ");
    }

    func(p);

    while (1) {
        sleep(10);
    }

    return 0;
}

编译运行:


如图所示,注册了SIGSEGV信号处理函数之后,在处理函数中是可以启gdb来收集进程死之前的一些关键信息的,因为gdb的-x参数是指定命令脚本,所以就可以根据自己的需要来修改命令脚本就行了。这种方式感觉还不错,你也可以试试。

如有其他更好的调试方法,欢迎指教~


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值