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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

《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!\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

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

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

1.1  显示系统调用地址

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

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

1.2 显示系统调用相关时间

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

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

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

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

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

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

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

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
20.35 0.000046 mmap
17.70 0.000040 mprotect
15.04 0.000034 munmap
11.95 0.000027 open
7.96 0.000018 access
7.96 0.000018 execve
5.75 0.000013 write
3.98 0.000009 fstat
3.98 0.000009 brk
2.21 0.000005 read
2.21 0.000005 close
0.88 0.000002 arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.000226 total

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

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
31.18 0.000386 execve
15.67 0.000194 mmap
10.18 0.000126 mprotect
7.19 0.000089 open
6.38 0.000079 access
5.90 0.000073 fstat
5.82 0.000072 brk
4.60 0.000057 munmap
4.28 0.000053 arch_prctl
3.72 0.000046 close
2.99 0.000037 write
2.10 0.000026 read
------ ----------- ----------- --------- --------- ----------------
100.00 0.001238 total

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

sudo strace -w -c -p `pidof st2`
strace: Process attached
^Cstrace: Process detached
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
99.16 93.011518 nanosleep
0.83 0.781669 restart_syscall
0.00 0.003852 open
0.00 0.003817 write
------ ----------- ----------- --------- --------- ----------------
100.00 93.800856 total

sudo strace -c -p `pidof st2`
strace: Process attached
^Cstrace: Process detached
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
41.00 0.007169 open
29.53 0.005164 nanosleep
29.11 0.005091 write
0.35 0.000062 restart_syscall
------ ----------- ----------- --------- --------- ----------------
100.00 0.017486 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(){
fp = fopen("/etc/shadow", "r");
if(fp == NULL)
{
printf("Error!\n");
} else
close(fp);
sleep();
}
return EXIT_SUCCESS;
}

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

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

strace: Process  attached
restart_syscall(<... resuming interrupted nanosleep ...>) = <0.637061>
open("/etc/shadow", O_RDONLY) = - EACCES (Permission denied) <0.000117>
write(, "Error!\n", ) = <0.000051>
nanosleep({, }, 0x7ffea3ee5560) = <3.000289>
open("/etc/shadow", O_RDONLY) = - EACCES (Permission denied) <0.000107>
write(, "Error!\n", ) = <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();
return EXIT_SUCCESS;
}

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

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

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

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

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

执行valgrind ./test2结果如下:

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

2.3 访问已释放的区域

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

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

执行valgrind ./test4结果如下:

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

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也提供了详细的信息,

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

2.5 非法栈操作

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

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

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

2.6 不对称释放

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

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

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\n", signo); nptrs = backtrace(buffer, SIZE);
printf("backtrace() returned %d addresses\n", 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 = ; j < nptrs; j++)
printf("%s\n", strings[j]); free(strings); if (SIGSEGV == signo || SIGQUIT == signo) {
exit();
}
} void segfault(void)
{
int *p = NULL;
*p = ;
} int main(int argc, char *argv[])
{
signal(SIGSEGV, trace);
signal(SIGINT, trace);
signal(SIGQUIT, trace); while () {
sleep();
if (time() % == ) {
segfault();
}
} return ;
}

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

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

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

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

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

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

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

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 = ; void cnt_reset(void)
{
pthread_mutex_lock(&mutex);
cnt = ;
pthread_mutex_unlock(&mutex);
} void *thr(void)
{
while() {
pthread_mutex_lock(&mutex);
if(cnt > )
cnt_reset();
else
cnt++;
pthread_mutex_unlock(&mutex); printf("%d\n", cnt);
sleep();
}
} int main(void)
{
pthread_t tid; pthread_create(&tid, NULL, thr, NULL);
pthread_join(tid, NULL); return EXIT_SUCCESS;
}

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

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

从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
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 = ; void cnt_reset(void)
{
while(){};
} void *thr(void)
{
while() {
if(cnt > )
cnt_reset();
else
cnt++; printf("%d\n", cnt);
sleep();
}
} int main(void)
{
pthread_t tid; pthread_create(&tid, NULL, thr, NULL);
pthread_join(tid, NULL); return EXIT_SUCCESS;
}

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

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

Debug Hacks中文版——深入调试的技术和工具-LMLPHP

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 = ; int do_sth()
{
printf("current count = %d\n", count);
return count++;
} int main(int argc, char* argv[])
{
int i = ;
while()
{
i = do_sth();
}; return ;
}

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

# cat /proc/`pgrep  bash`/maps | grep /bin/bash | grep r-xp
   -004e1000 r-xp :                              /bin/bash
# objdump -T /bin/zsh | grep -w free
   00000000004ab500 g    DF .text      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 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
echo > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable
echo > /sys/kernel/debug/tracing/events/uprobes/do_sth/enable
echo > /sys/kernel/debug/tracing/events/uprobes/do_sth_exit/enable echo > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
echo > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable
echo > /sys/kernel/debug/tracing/events/uprobes/do_sth/enable
echo > /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

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

然后./configure配置,sudo make install安装。
 
05-08 07:55