zoukankan      html  css  js  c++  java
  • Debug Hacks中文版——深入调试的技术和工具

    关键词:gdb、strace、kprobe、uprobe、objdump、meminfo、valgrind、backtrace等。

     

    《Debugs Hacks中文版——深入调试的技术和工具》这本书是Miracle Linux一些同事合作,主要关注Linux下的调试技术和工具。

    本文章以此书为蓝本进行总结,进行适当的补充。

    下面以Debug hacks地图将内容组织如下:

    0. 通用技术

      《objdump》, 《strace》, 《kprobes》, 《uprobes》, systemtap, oprofile, 《valgrind》, 《/proc/meminfo》, 《/proc/<pid>/maps

    1. 程序异常结束应对方法

      1.1 应用问题 

        内存非法访问SEGV类型问题分析,可以通过core+gdb离线分析通过watch分析非法内存访问,《利用backtrace()/backtrace_symbols()栈回溯》。

        《调试器GDB的基本使用方法

      1.2 内核问题

        1.2.1 内核转储分析

          如何设置内核转储如何分析内核转储文件

          空指针引用,链表破坏等导致的Kernel Panic分析

          死循环,自旋锁,信号量等导致的内核停止响应问题

          实时进程停止响应

        1.2.2 其他分析

          《一个内存Oops解读》:不同架构的Oops差异很大,尤其涉及到体系架构相关的寄存器、栈信息等。

    2. 程序不结束异常

      2.1 应用问题

        用top查看负载是否过高?负载不高,则进行《应用程序死锁停止响应分析》;负载高,则进行《应用程序死循环停止响应分析》。

      2.2 内核问题

        设置内核WDT检测异常

        通过SysRq分析问题

    1. 使用strace寻找故障线索

    strace用于跟踪系统调用, 并显示输入输出情况.

    strace -o filename将strace结果保存到filename.

    strace -f cmd跟踪fork()之后的进程.

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
      FILE *fp;
    
      fp = fopen("/etc/shadow", "r");
      if(fp == NULL)
      {
        printf("Error!
    ");
        return EXIT_FAILURE;
      }
      return EXIT_SUCCESS;
    }

    编译如上代码, 使用strace ./st1执行结果如下:

    execve("./st1", ["./st1"], [/* 81 vars */]) = 0...
    open("/etc/shadow", O_RDONLY)           = -1 EACCES (Permission denied)-----------------找出问题的根源,是因为权限问题.
    fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
    write(1, "Error!
    ", 7Error!------------------------------------------------------------控制台输出内容
    )                 = 7
    exit_group(1)                           = ?
    +++ exited with 1 +++

    1.1  显示系统调用地址

    strace -i显示对应系统调用地址:

    [00007f53cb39e777] execve("./st1", ["./st1"], [/* 81 vars */]) = 0...
    [00007f5265e9a040] open("/etc/shadow", O_RDONLY) = -1 EACCES (Permission denied)
    [00007f5265e99c34] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
    [00007f5265e9a2c0] write(1, "Error!
    ", 7Error!
    ) = 7
    [00007f5265e6f748] exit_group(1)        = ?
    [????????????????] +++ exited with 1 +++

    1.2 显示系统调用相关时间

    strace -ttstrace -ttt显示每个系统调用执行的绝对时间, 只是格式不同.

    09:10:35.411774 execve("./st1", ["./st1"], [/* 81 vars */]) = 0
    ...
    09:10:35.416745 open("/etc/shadow", O_RDONLY) = -1 EACCES (Permission denied)
    09:10:35.416842 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
    09:10:35.416925 write(1, "Error!
    ", 7Error!
    ) = 7
    09:10:35.417019 exit_group(1)           = ?
    09:10:35.417161 +++ exited with 1 +++
    
    
    1537060239.606438 execve("./st1", ["./st1"], [/* 81 vars */]) = 0
    ...
    1537060239.609897 open("/etc/shadow", O_RDONLY) = -1 EACCES (Permission denied)
    1537060239.609989 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
    1537060239.610065 write(1, "Error!
    ", 7Error!
    ) = 7
    1537060239.610160 exit_group(1)         = ?
    1537060239.610330 +++ exited with 1 +++

    还有一种strace -r计算上一次系统调用开始到本次系统调用开始时间之间的差值:

         0.000000 execve("./st1", ["./st1"], [/* 81 vars */]) = 0
    ...
         0.000074 open("/etc/shadow", O_RDONLY) = -1 EACCES (Permission denied)
         0.000086 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
         0.000076 write(1, "Error!
    ", 7Error!
    )   = 7
         0.000095 exit_group(1)             = ?
         0.000146 +++ exited with 1 +++

    strace -T则显示每个系统调用总开始到结束的耗时.

    execve("./st1", ["./st1"], [/* 81 vars */]) = 0 <0.000439>...
    open("/etc/shadow", O_RDONLY)           = -1 EACCES (Permission denied) <0.000037>
    fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0 <0.000029>
    write(1, "Error!
    ", 7Error!
    )                 = 7 <0.000039>
    exit_group(1)                           = ?

    strace -c显示系统调用耗时的统计信息, 包括耗时百分比time, 总耗时seconds, 系统调用平均耗时usecs/call, 总次数calls, 错误次数errors, 系统调用名称syscall.

    % time     seconds  usecs/call     calls    errors syscall
    ------ ----------- ----------- --------- --------- ----------------
     20.35    0.000046           7         7           mmap
     17.70    0.000040          10         4           mprotect
     15.04    0.000034          34         1           munmap
     11.95    0.000027           9         3         1 open
      7.96    0.000018           6         3         3 access
      7.96    0.000018          18         1           execve
      5.75    0.000013          13         1           write
      3.98    0.000009           3         3           fstat
      3.98    0.000009           3         3           brk
      2.21    0.000005           5         1           read
      2.21    0.000005           3         2           close
      0.88    0.000002           2         1           arch_prctl
    ------ ----------- ----------- --------- --------- ----------------
    100.00    0.000226                    30         4 total

    strace -c只显示内核中CPU耗时; 如下strace -w -c显示从系统调用开始到结束的耗时, 更加准确.

    % time     seconds  usecs/call     calls    errors syscall
    ------ ----------- ----------- --------- --------- ----------------
     31.18    0.000386         386         1           execve
     15.67    0.000194          28         7           mmap
     10.18    0.000126          32         4           mprotect
      7.19    0.000089          30         3         1 open
      6.38    0.000079          26         3         3 access
      5.90    0.000073          24         3           fstat
      5.82    0.000072          24         3           brk
      4.60    0.000057          57         1           munmap
      4.28    0.000053          53         1           arch_prctl
      3.72    0.000046          23         2           close
      2.99    0.000037          37         1           write
      2.10    0.000026          26         1           read
    ------ ----------- ----------- --------- --------- ----------------
    100.00    0.001238                    30         4 total

    下面对sleep(3)分别使用两个命令进行对比, 可以看出区别如下.

    sudo strace -w -c -p `pidof st2`
    strace: Process 6277 attached
    ^Cstrace: Process 6277 detached
    % time     seconds  usecs/call     calls    errors syscall
    ------ ----------- ----------- --------- --------- ----------------
     99.16   93.011518     3000372        31           nanosleep
      0.83    0.781669      781669         1           restart_syscall
      0.00    0.003852         120        32        32 open
      0.00    0.003817         119        32           write
    ------ ----------- ----------- --------- --------- ----------------
    100.00   93.800856                    96        32 total
    
    sudo strace -c -p `pidof st2` strace: Process 6277 attached ^Cstrace: Process 6277 detached % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 41.00 0.007169 54 132 132 open 29.53 0.005164 39 131 nanosleep 29.11 0.005091 39 132 write 0.35 0.000062 62 1 restart_syscall ------ ----------- ----------- --------- --------- ---------------- 100.00 0.017486 396 132 total

    可以看出从用户角度strace -w -p更加准确, nanosleep()耗时基本上为3秒; 但是从内核角度来说, nanosleep并没有实际占用3秒, 而是39微秒.

    综上所述,如果要分析单个系统调用的性能strace -T比较合适; 如果要分析系统调用统计信息strace -w -c比较合适.

    1.3 attach到已有进程

    如果进程已经运行, 可以通过strace -p `pidof st2`来附着到st2进行系统调用跟踪.

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
      FILE *fp;
    
      while(1){
        fp = fopen("/etc/shadow", "r");
        if(fp == NULL)
        {
          printf("Error!
    ");
        } else
          close(fp);
        sleep(3);
      }
      return EXIT_SUCCESS;
    }

    运行输出Error之后, 通过strace附着到st2上, 并且打印系统调用耗时.

    可以看出Error的原因, 并且可以看出sleep(3)的实际耗时.

    strace: Process 6231 attached
    restart_syscall(<... resuming interrupted nanosleep ...>) = 0 <0.637061>
    open("/etc/shadow", O_RDONLY)           = -1 EACCES (Permission denied) <0.000117>
    write(1, "Error!
    ", 7)                 = 7 <0.000051>
    nanosleep({3, 0}, 0x7ffea3ee5560)       = 0 <3.000289>
    open("/etc/shadow", O_RDONLY)           = -1 EACCES (Permission denied) <0.000107>
    write(1, "Error!
    ", 7)                 = 7 <0.000060>

    1.4 strace -e高级功能

    通过设置strace -e expr, 可以对监控的系统调用进行过滤.

    2. valgrind使用方法

    valgrind是一款用于内存调试、内存泄漏检测以及性能分析的软件开发工具。
    valgrind可以用于检测内存泄漏, 访问非法内存地址, 访问已释放内存区域, 内存双重释放, 非法栈空间操作等问题.

    2.1 检测内存泄露

    内存泄露是申请的内存, 没有被释放. 造成可用内存越来越小, 从而引起内存紧张.

    #include <stdio.h>
    #include <stdlib.h>
    #include <malloc.h>
    
    int main()
    {
      char *p = malloc(10);
      return EXIT_SUCCESS;
    }

    通过valgrind --leak-check=yes ./test1, 得到如下结果.

    ==9218== Memcheck, a memory error detector
    ==9218== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
    ==9218== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
    ==9218== Command: ./test1
    ==9218== 
    ==9218== 
    ==9218== HEAP SUMMARY:--------------------------------------------------------------------------堆的使用情况.
    ==9218==     in use at exit: 10 bytes in 1 blocks
    ==9218==   total heap usage: 1 allocs, 0 frees, 10 bytes allocated------------------------------1次分配, 0次释放, 就是问题的根源.
    ==9218== 
    ==9218== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
    ==9218==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==9218==    by 0x400537: main (test1.c:7)-------------------------------------------------------具体申请的点,也即泄漏点.
    ==9218== 
    ==9218== LEAK SUMMARY:--------------------------------------------------------------------------泄露类型, 以及每种泄露情况.
    ==9218==    definitely lost: 10 bytes in 1 blocks
    ==9218==    indirectly lost: 0 bytes in 0 blocks
    ==9218==      possibly lost: 0 bytes in 0 blocks
    ==9218==    still reachable: 0 bytes in 0 blocks
    ==9218==         suppressed: 0 bytes in 0 blocks
    ==9218== 
    ==9218== For counts of detected and suppressed errors, rerun with: -v
    ==9218== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

    2.2 检测对非法内存地址的访问

    地址的越界操作, 也即堆非法地址访问造成的问题比较隐蔽. 内存踩踏造成的影响, 也比较发散.

    #include <stdio.h>
    #include <stdlib.h>
    #include <malloc.h>
    
    int main()
    {
      char *p = malloc(10);
      p[10] = 1;
      free(p);
      return EXIT_SUCCESS;
    }

    执行valgrind ./test2结果如下:

    ==9265== Memcheck, a memory error detector
    ==9265== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
    ==9265== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
    ==9265== Command: ./test2
    ==9265== 
    ==9265== Invalid write of size 1--------------------------------------------------------------错误类型是, 无效的一字节写.
    ==9265==    at 0x400584: main (test2.c:8)-----------------------------------------------------错误发生地点.
    ==9265==  Address 0x520404a is 0 bytes after a block of size 10 alloc'd-----------------------发生错误的地址.
    ==9265==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==9265==    by 0x400577: main (test2.c:7)
    ==9265== 
    ==9265== 
    ==9265== HEAP SUMMARY:------------------------------------------------------------------------可以看出堆的使用没有问题, 申请的内存被正确的释放了.
    ==9265==     in use at exit: 0 bytes in 0 blocks
    ==9265==   total heap usage: 1 allocs, 1 frees, 10 bytes allocated
    ==9265== 
    ==9265== All heap blocks were freed -- no leaks are possible
    ==9265== 
    ==9265== For counts of detected and suppressed errors, rerun with: -v
    ==9265== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

    2.3 访问已释放的区域

    访问已释放的内存同样也可能造成一些未知的错误, 造成一些不可理解的错误.

    #include <stdio.h>
    #include <stdlib.h>
    #include <malloc.h>
    
    int main()
    {
      char *x = malloc(sizeof(int));
    
      free(x);
      int a = *x + 1;
    
      return EXIT_SUCCESS;
    }

    执行valgrind ./test4结果如下:

    ==9341== Memcheck, a memory error detector
    ==9341== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
    ==9341== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
    ==9341== Command: ./test4
    ==9341== 
    ==9341== Invalid read of size 1-------------------------------------------------------------读一字节错误.
    ==9341==    at 0x40058C: main (test4.c:10)--------------------------------------------------错误发生位置.
    ==9341==  Address 0x5204040 is 0 bytes inside a block of size 4 free'd----------------------表明操作的地址只想一个已经被释放的内存区域, 下面是详细的申请位置和释放位置.
    ==9341==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==9341==    by 0x400587: main (test4.c:9)
    ==9341==  Block was alloc'd at
    ==9341==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==9341==    by 0x400577: main (test4.c:7)
    ==9341== 
    ==9341== 
    ==9341== HEAP SUMMARY:
    ==9341==     in use at exit: 0 bytes in 0 blocks
    ==9341==   total heap usage: 1 allocs, 1 frees, 4 bytes allocated
    ==9341== 
    ==9341== All heap blocks were freed -- no leaks are possible
    ==9341== 
    ==9341== For counts of detected and suppressed errors, rerun with: -v
    ==9341== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

    2.4 内存双重释放

    内存的双重释放问题在程序执行时, 已经可以暴露. 或者通过gdb + ulimit -c unlimited去分析.

    #include <stdio.h>
    #include <stdlib.h>
    #include <malloc.h>
    
    int main()
    {
      char *x = malloc(sizeof(int));
    
      free(x);
      free(x);
    
      return EXIT_SUCCESS;
    }

    但是valgrind ./test5也提供了详细的信息,  

    ==9636== Memcheck, a memory error detector
    ==9636== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
    ==9636== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
    ==9636== Command: ./test5
    ==9636== 
    ==9636== Invalid free() / delete / delete[] / realloc()------------------------------------非正常释放
    ==9636==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)----产生错误的位置, 下面是正常的申请和释放位置.
    ==9636==    by 0x400593: main (test5.c:10)
    ==9636==  Address 0x5204040 is 0 bytes inside a block of size 4 free'd
    ==9636==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==9636==    by 0x400587: main (test5.c:9)
    ==9636==  Block was alloc'd at
    ==9636==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==9636==    by 0x400577: main (test5.c:7)
    ==9636== 
    ==9636== 
    ==9636== HEAP SUMMARY:---------------------------------------------------------------------堆的使用情况, 一个申请两个释放.
    ==9636==     in use at exit: 0 bytes in 0 blocks
    ==9636==   total heap usage: 1 allocs, 2 frees, 4 bytes allocated
    ==9636== 
    ==9636== All heap blocks were freed -- no leaks are possible
    ==9636== 
    ==9636== For counts of detected and suppressed errors, rerun with: -v
    ==9636== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

    2.5 非法栈操作

    非法栈操作在正常情况下不显示错误, 导致一些不可预测的问题.
    #include <stdio.h>
    #include <stdlib.h>
    #include <malloc.h>
    
    int main()
    {
      int a;
    
      int *p = &a;
      p -= 0x80;
      *p = 1;
    
      return EXIT_SUCCESS;
    }

     将p指向的地址向前移动512字节, 这个地址已经在栈之外了. 结果如下:

    ==10026== Memcheck, a memory error detector
    ==10026== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
    ==10026== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
    ==10026== Command: ./test6
    ==10026== 
    ==10026== Invalid write of size 4-------------------------------------------------非法的写, 超出栈的区域.
    ==10026==    at 0x400571: main (in /home/al/debug_hacks/valgrind/test6)-----------产生非法写的位置.
    ==10026==  Address 0xffefff8ac is on thread 1's stack
    ==10026==  500 bytes below stack pointer------------------------------------------相对于栈指针的偏移.
    ==10026== 
    ==10026== 
    ==10026== HEAP SUMMARY:
    ==10026==     in use at exit: 0 bytes in 0 blocks
    ==10026==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
    ==10026== 
    ==10026== All heap blocks were freed -- no leaks are possible
    ==10026== 
    ==10026== For counts of detected and suppressed errors, rerun with: -v
    ==10026== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

    2.6 不对称释放

    不对称释放, 也即free释放的内存并不是malloc()分配的.

    #include <stdio.h>
    #include <stdlib.h>
    #include <memory.h>
     
    int main()
    {
      char szTest[100] = {0};
      char *p = szTest;
      free(p);
    
      return 0;
    }
    执行结果如下:
    ==11548== Memcheck, a memory error detector
    ==11548== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
    ==11548== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
    ==11548== Command: ./test7
    ==11548== 
    ==11548== Invalid free() / delete / delete[] / realloc()---------------------------------------不正确的释放free().
    ==11548==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==11548==    by 0x4005DD: main (test7.c:10)----------------------------------------------------异常出现地点.
    ==11548==  Address 0xffefffa50 is on thread 1's stack
    ==11548==  in frame #1, created by main (test7.c:7)
    ==11548== 
    ==11548== 
    ==11548== HEAP SUMMARY:
    ==11548==     in use at exit: 0 bytes in 0 blocks
    ==11548==   total heap usage: 0 allocs, 1 frees, 0 bytes allocated------------------------------没有malloc()的free().
    ==11548== 
    ==11548== All heap blocks were freed -- no leaks are possible
    ==11548== 
    ==11548== For counts of detected and suppressed errors, rerun with: -v
    ==11548== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

    3. 利用backtrace()/backtrace_symbols()栈回溯

    利用backtrace()获取当前线程调用栈,然后通过backtrace_symbols()将地址转化为一个字符串数组。从而实现了用户空间的栈回溯。

    在头文件"execinfo.h"中声明了三个函数用于获取当前线程的函数调用堆栈。

    #include <execinfo.h>
    int backtrace(void **buffer, int size);
    char **backtrace_symbols(void *const *buffer, int size);
    void backtrace_symbols_fd(void *const *buffer, int size, int fd);

    int backtrace(void **buffer, int size)
    该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针数组;参数 size 用来指定buffer中可以保存多少个void* 元素。

    函数返回值是实际获取的指针个数,最大不超过size大小在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈frame有一个返回地址。

    注意某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会使无法正确解析堆栈内容。


    char ** backtrace_symbols (void *const *buffer, int size)
    backtrace_symbols()将从backtrace函数获取的信息转化为一个字符串数组。

    参数buffer应该是从backtrace函数获取的数组指针,size是该数组中的元素个数(backtrace的返回值),函数返回值是一个指向字符串数组的指针,它的大小同buffer相同。

    每个字符串包含了一个相对于buffer中对应元素的可打印信息。它包括函数名,函数的偏移地址,和实际的返回地址。

    现在,只有使用ELF二进制格式的程序和苦衷才能获取函数名称和偏移地址。在其他系统,只有16进制的返回地址能被获取。另外,你可能需要传递相应的标志给链接器,以能支持函数名功能(比如,在使用GNU ld的系统中,你需要传递(-rdynamic))。

    backtrace_symbols()生成的字符串都是malloc出来的,但是不要最后一个一个的free,因为backtrace_symbols是根据backtrace给出的call stack层数,一次性的malloc出来一块内存来存放结果字符串的,所以,像上面代码一样,只需要在最后,free backtrace_symbols()的返回指针就OK了。

    这一点backtrace的manual中也是特别提到的。

    注意:如果不能为字符串获取足够的空间函数的返回值将会为NULL


    void backtrace_symbols_fd (void *const *buffer, int size, int fd)

    backtrace_symbols_fd()与backtrace_symbols()函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行.它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况。

    #include <execinfo.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
     
    #define SIZE 1000
    
    void trace(int signo)
    {
        int j, nptrs;
    
        void *buffer[SIZE];
        char **strings;
     
        printf("signo: %d
    ", signo);
    
        nptrs = backtrace(buffer, SIZE);
        printf("backtrace() returned %d addresses
    ", nptrs);
    
        /* The call backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO)
         *               would produce similar output to the following: */
    
        strings = backtrace_symbols(buffer, nptrs);
        if (strings == NULL) {
            perror("Backtrace:");
            exit(EXIT_FAILURE);
        }
    
        for (j = 0; j < nptrs; j++)
            printf("%s
    ", strings[j]);
    
        free(strings);
    
        if (SIGSEGV == signo || SIGQUIT == signo) {
            exit(0);
        }
    }
     
    void segfault(void)
    {
        int *p = NULL;
        *p = 1;
    }
     
    int main(int argc, char *argv[])
    {
        signal(SIGSEGV, trace);
        signal(SIGINT, trace);
        signal(SIGQUIT, trace);
    
        while (1) {
            sleep(1);
            if (time(0) % 7 == 0) {
                segfault();
            }
        }
    
        return 0;
    }

    编译的时候 需要打开-g -rdynamic -fexceptions选项,

    gcc -g -rdynamic -fexceptions backtrace.c -o backtrace

    然后执行./backtrace得到如下结果:

    ./backtrace(trace+0x4b) [0x400b21]
    /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7fc4a97b54b0]
    ./backtrace(segfault+0x10) [0x400c12]
    ./backtrace(main+0x85) [0x400ca0]
    /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fc4a97a0830]
    ./backtrace(_start+0x29) [0x400a09]

    除了trace()函数,最接近的是segfault函数。

    通过addr2line获取在文件中位置。

    addr2line 0x400c12 -e backtrace -afs
    0x0000000000400c12
    segfault
    backtrace.c:43

    还可以通过objdump -SC backtrace,查看详细信息。可以看出0x400C12对应的内容是*p = 1;。

    4. objdump使用说明

    objdump命令是用查看目标文件或者可执行的目标文件的构成的gcc工具。

    objdump是gcc工具,用来查看编译后目标文件的组成。
    常用命令:
    objdump -x obj:以某种分类信息的形式把目标文件的数据组成输出;<可查到该文件的的所有动态库>
    objdump -t obj:输出目标文件的符号表()
    objdump -h obj:输出目标文件的所有段概括()
    objdump -j ./text/.data -S obj:输出指定段的信息(反汇编源代码)
    objdump -S obj:输出目标文件的符号表(), 当gcc -g时打印更明显

    objdump -j .text -Sl stack1 | more
    -S 尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。PS:需要objdump和源码统一目录下。
    -l 用文件名和行号标注相应的目标代码,仅仅和-d、-D或者-r一起使用使用-ld和使用-d的区别不是很大,在源码级调试的时候有用,要求编译时使用了-g之类的调试编译选项。
    -j name 仅仅显示指定section的信息。

    5. 应用程序死锁停止响应分析

    如果锁使用的不好,会造成应用程序停止相应。

    gcc -g astall.c -o astall -lpthread编译如下代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    int cnt = 0;
    
    void cnt_reset(void)
    {
      pthread_mutex_lock(&mutex);
      cnt = 0;
      pthread_mutex_unlock(&mutex);
    }
    
    
    void *thr(void)
    {
      while(1) {
        pthread_mutex_lock(&mutex);
        if(cnt > 2)
          cnt_reset();
        else
          cnt++;
        pthread_mutex_unlock(&mutex);
    
        printf("%d
    ", cnt);
        sleep(1);
      }
    }
    
    int main(void)
    {
      pthread_t tid;
    
      pthread_create(&tid, NULL, thr, NULL);
      pthread_join(tid, NULL);
    
      return EXIT_SUCCESS;
    }

     执行程序3秒后,程序就会停止响应。

    此时通过top -p `pidof astall`可以看出,进程并没有死循环,而是在睡眠状态。

    通过ps ax -L | grep astall可以看出两个线程都处于Sl+状态,也即都在睡眠中。

    此时可以通过sudo gdb -p `pidof astall` attach到此进程。

    可以看到当前有两个线程在执行.

    分别查看两个线程的栈信息,线程1的睡眠点符合预期在pthread_join();线程2的睡眠点在pthread_mutex_lock(),这就是问题的根源。

    从thread 2的栈回溯start_thread()->thr()->cnt_reset()->pthread_mutex_lock()可以看出,锁死现场。

    还可以编写gdb 脚本来记录pthread_mutex_lock()/pthread_mutex_unlock()被调用是栈信息:

    gdb astall -x debug.cmd

    set pagination off
    set logging file debug.log
    set logging overwrite
    set logging on
    
    start
    set $addr1 = pthread_mutex_lock
    set $addr2 = pthread_mutex_unlock
    b *$addr1
    b *$addr2
    
    while 1
        c
        if $pc != $addr1 && $pc != $addr2
            quit
        end
        bt full
    end

    6. 应用程序死循环停止响应分析

    有时候应用进入死循环,这时候可以通过top简单确认。

    如果CPU占用率很高,则说明很可能进入死循环。

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    int cnt = 0;
    
    void cnt_reset(void)
    {
      while(1){};
    }
    
    
    void *thr(void)
    {
      while(1) {
        if(cnt > 2)
          cnt_reset();
        else
          cnt++;
    
        printf("%d
    ", cnt);
        sleep(1);
      }
    }
    
    int main(void)
    {
      pthread_t tid;
    
      pthread_create(&tid, NULL, thr, NULL);
      pthread_join(tid, NULL);
    
      return EXIT_SUCCESS;
    }

    用top可以看出astall2占用率接近100%。

    再用ps查看,可以两个进程,一个进程死循环,另一个进程在睡眠。

     

    分别查看两个线程的栈信息,可以明显的看出thread 1处于睡眠状态;thread 2处于while(1),也即问题根源。

    6. uprobe使用

    uprobe是和kprobe类似的调试方法,编译内核时通过CONFIG_UPROBE_EVENT=y来使能该特性。

    6.1 uprobe介绍

    和kprobe类似,使用时不需要通过current_tracer来激活,而是检测点通过/sys/kernel/debug/tracing/uprobe_events设置,通过/sys/kernel/debug/tracing/events/uprobes/<EVENT>/enabled来使能。

    然而,和kprobe不同的是,使用时需要用户自己计算探测点在用户态文件中的偏移,可以通过nm等工具,这还是有点麻烦的。

    可以通过/sys/kernel/debug/tracing/uprobe_profile来查看某一检测事件命中的总数和没有命中的总数。第一列是事件名称,第二列是事件命中的次数,第三列是事件miss-hits的次数。

    6.2 uprobe示例

    uprobe的使用和kprobe的不同在于不能设置符号,而是偏移地址。
    #include <stdlib.h>
    #include <stdio.h>
    
    int count = 0;
    
    int do_sth()
    {
        printf("current count = %d
    ", count);
        return count++;
    }
    
    int main(int argc, char* argv[])
    {
        int i = 0;
        while(1)
        {
            i = do_sth();
        };
    
        return 0;
    }

    获取函数的偏移方法:1.通过objdump找到函数对应地址A1;2.在maps查看程序对应的加载地址A2。A1-A2就是探测点函数的偏移地址。

    # cat /proc/`pgrep  bash`/maps | grep /bin/bash | grep r-xp
       00400000-004e1000 r-xp 00000000 08:01 786439                             /bin/bash
    # objdump -T /bin/zsh | grep -w free
       00000000004ab500 g    DF .text    0000000000000009  Base        free

    使用如下:

    echo > /sys/kernel/debug/tracing/trace
    
    echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_events
    echo 'r:myretprobe do_sys_open ret=$retval' >> /sys/kernel/debug/tracing/kprobe_events
    echo 'p:do_sth /home/al/debug_hacks/uprobe/loop_print:0x526 %ip %ax' > /sys/kernel/debug/tracing/uprobe_events 
    echo 'r:do_sth_exit /home/al/debug_hacks/uprobe/loop_print:0x526 %ip %ax' >> /sys/kernel/debug/tracing/uprobe_events 
    
    echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
    echo 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable
    echo 1 > /sys/kernel/debug/tracing/events/uprobes/do_sth/enable
    echo 1 > /sys/kernel/debug/tracing/events/uprobes/do_sth_exit/enable
    
    echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
    echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable
    echo 0 > /sys/kernel/debug/tracing/events/uprobes/do_sth/enable
    echo 0 > /sys/kernel/debug/tracing/events/uprobes/do_sth_exit/enable
    
    echo > /sys/kernel/debug/tracing/kprobe_events
    echo > /sys/kernel/debug/tracing/uprobe_events 
    
    cat /sys/kernel/debug/tracing/trace
    参考文档:

    7. systemtap

    8. oprofile

    配置编译环境和下载源码:

     sudo apt install binutils-dev libiberty-dev libpopt-dev -y  

     wget https://nchc.dl.sourceforge.net/project/oprofile/oprofile/oprofile-1.3.0/oprofile-1.3.0.tar.gz

    然后./configure配置,sudo make install安装。
     
  • 相关阅读:
    完全图解scrollLeft,scrollWidth,clientWidth,offsetWidth 获取相对途径,滚动图片
    Input的size,width,maxlength属性
    Linux,VI命令详解
    Javascript 第十章
    Javascript 第七章
    IE css hack
    Javascript 第九章
    js中document.documentElement 和document.body 以及其属性
    关于xmlhttp.status == 0的问题
    Javascript 第八章
  • 原文地址:https://www.cnblogs.com/arnoldlu/p/9649229.html
Copyright © 2011-2022 走看看