老程序员解 bug 有哪些通用套路?

关注者
4,435
被浏览
564,616
登录后你可以
不限量看优质回答 私信答主深度交流 精彩内容一键收藏

骨灰级大神用print就够了,比如Linux之父Linus Torvalds从不用任何debugger工具: Linus Torvalds: Debugging hell ,还有Python之父 Guido van Rossum 自称使用print语句排查90%的bug,参考 I do not use a debugger 。我也经常使用print大法(或者log),基本思路是先定位大体bug位置,比如某个函数调用,然后把关心的变量打印出来,或者在某个分支位置使用print打个tag。尤其是涉及多线程的时候,断点不一定凑效,或者bug不是100%稳定重现,只能使用print大法,触发bug时查看输出的信息。

当然,如果能用debug工具,我还是更倾向于使用debug,比如gdb、jdb、pdb,这些工具能够单步调试、监控变量等,非常方便,前面介绍了很多,这里不再重复。

如果你只有二进制文件,没有源码,或者编译时没有打开debug, strace 是非常不错的debugger工具,它能够跟踪进程的所有系统调用和信号。先看一段代码:




    
#include <stdio.h>                                                                                                             │
#include <pthread.h>                                                                                                           │
#include <unistd.h>                                                                                                            │
static void * simple_thread(void *);                                                                                           │
pthread_mutex_t mutex_1= PTHREAD_MUTEX_INITIALIZER;                                                                            │
pthread_mutex_t mutex_2= PTHREAD_MUTEX_INITIALIZER;                                                                            │
static void * simple_thread(void * dummy)                                                                                      │
{                                                                                                                              │
    pthread_mutex_lock(&mutex_2);                                                                                              │
    sleep(1);                                                                                                                  │
    pthread_mutex_lock(&mutex_1);                                                                                              │
    pthread_mutex_unlock(&mutex_1);                                                                                            │
    pthread_mutex_unlock(&mutex_2);                                                                                            │
    printf("exit simple thread\n");                                                                                            │
    return NULL;                                                                                                               │
}                                                                                                                              │
int main()                                                                                                                     │
{                                                                                                                              │
    pthread_t tid = 0;                                                                                                         │
    pthread_create(&tid, 0, &simple_thread, 0);                                                                                │
    sleep(1);                                                                                                                  │
    pthread_mutex_lock(&mutex_1);                                                                                              │
    pthread_mutex_lock(&mutex_2);                                                                                              │
    pthread_mutex_unlock(&mutex_2);                                                                                            │
    pthread_mutex_unlock(&mutex_1);                                                                                            │
    pthread_join(tid, NULL);                                                                                                   │
    printf("exit main thread\n");                                                                                              │
    return 0;                                                                                                                  │
}

先暂且不管这个程序是干什么的(其实根本没用),我们编译运行:

int32bit $ gcc -l pthread test.c
int32bit $ ./a.out

这个进程block了,什么原因呢?

另开一个终端,使用strace工具看看到底block在哪个系统调用:

由输出结果看,futex堵塞了,显然是在等待锁,也就是说,进程发生死锁了。

我们再次运行看看:

int32bit $ strace -f -e futex ./a.out
Process 104573 attached
[pid 104573] futex(0x601080, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 104572] futex(0x6010c0, FUTEX_WAIT_PRIVATE, 2, NULL

从结果发现两个线程都block在futext,因此进一步确定是由于死锁导致的。如果你的程序经常莫名其妙的卡住,可以试试strace。

另外一个例子,参考 Linux Troubleshooting with strace | Benjamin Cane

[root@laptop ~]# systemctl start vsftpd.service
 Job failed. See system logs and 'systemctl status' for details.
 [root@laptop ~]# systemctl status vsftpd.service
 vsftpd.service - Vsftpd ftp daemon
      Loaded: loaded (/lib/systemd/system/vsftpd.service; disabled)
      Active: failed since Sat, 24 Mar 2012 11:55:25 -0700; 2min 28s ago
     Process: 19356 ExecStart=/usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf (code=exited, status=1/FAILURE)
      CGroup: name=systemd:/system/vsftpd.service

问题是vsftpd服务起不来,也没有看出什么有用的日志。

使用strace启动:

[root@laptop ~]# strace -f /usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf
 <truncated output (it is seriously a lot of output)>
 [pid 19499] socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
 [pid 19499] setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
 [pid 19499] rt_sigaction(SIGCHLD, {0xf5ac00, ~[RTMIN RT_1], 0}, NULL, 8 ) = 0
 [pid 19499] rt_sigaction(SIGALRM, {0xf5abd0, ~[RTMIN RT_1], 0}, NULL, 8 ) = 0
 [pid 19499] rt_sigaction(SIGHUP, {0xf5ac00, ~[RTMIN RT_1], 0}, NULL, 8 ) = 0
 [pid 19499] rt_sigaction(SIGALRM, {0xf5abd0, ~[RTMIN RT_1], 0}, NULL, 8 ) = 0
 [pid 19499] bind(3, {sa_family=AF_INET, sin_port=htons(21), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use)
 [pid 19499] fcntl64(0, F_GETFL)         = 0x8002 (flags O_RDWR|O_LARGEFILE)
 [pid 19499] fcntl64(0, F_SETFL, O_RDWR|O_NONBLOCK|O_LARGEFILE) = 0
 [pid 19499] write(0, "500 OOPS: ", 10)  = 10
 [pid 19499] write(0, "could not bind listening IPv4 so"..., 36) = 36
 [pid 19499] write(0, "rn", 2)         = 2
 [pid 19499] exit_group(1)               = ?
 Process 19499 detached

显然是由于端口被占用导致服务起不来的。

还有最经典的内存泄露问题:

int main(int argc, char **argv)
    int *p;
    while(1) {
        p = new int;
    return 0;
}

我们使用strace运行,你会发现满屏的brk调用:

data segment地址不断增大(brk(0)是获取当前地址),说明进程不断申请内存,并且没有释放,因此可能存在内存泄露。

如果把代码改成:

#include<cstdio>
int main(int argc, char **argv)
    int *p;
    while(1) {
        printf("HelloWorld!\n");
        p = new int[10000];
        delete p;
    return 0;
}

就不会存在不断调data segment地址的问题,说明进程运行时正常的,至少说明申请和释放基本是平衡的。

还有一次经历是OpenStack cinder-volume启动失败,日志显示rbd driver初始化失败,通过strace 启动发现是由于连接不了ceph mon导致的。

好好利用strace工具,可以解决各种疑难杂症,不过strace不能定位到代码,因此发现问题后,还是需要自己去进一步分析。

如果程序跑的很慢,可以试试 perf 工具,这是一个性能调优工具,能够发现程序的性能瓶颈,定位热点代码,这篇文章 Linux下的系统性能调优工具 写得不错,可以快速入门。找到热点代码后,如果不是非你所望,可能就是程序bug了。

Linux下的系统性能调优工具 中的例子:

void longa()
   int i,j;
   for(i = 0; i < 1000000; i++)
   j=i; //am I silly or crazy? I feel boring and desperate.
 void foo2()
   int i;
   for(i=0 ; i < 10; i++)
        longa();
 void foo1()
   int i;
   for(i = 0; i< 100; i++)
      longa();
 int main(void)