您的位置:首页 > 运维架构 > Linux

linux 下C/C++程序常用调试方法(gdb)

2014-12-02 09:13 477 查看

不管是在开发或者运行过程中,调试保证程序正常运行最基本的手段,熟悉这些调试方式,方便我们更快的定位程序问题所在,提高开发效率。
一 程序正常运行调试
(1) 直接使用gdb
开发过程中最常用的方式,我们可以在其过程中给程序添加断点,监视等辅助手段,监控其行为是否与我们设计相符,比如:



(2) 程序已经运行,通过attach附加到进程



二 程序中断后调试
首先简单介绍下linux 下的信号:
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信 号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
我们正是利用linux的信号机制,按第一种方法进行处理。
以SIGSEGV为例,其他类似。触发信号而触发此信号最根本的原因是试图访问未分配的内存,或者试图往没有写权限的地址写入数据。
(1) 通过backtrace,backtrace_symbols输出函数调用堆栈;
void dump(int __signal)
{
const int __max_stack_flow = 20;
void* __array[__max_stack_flow];
char** __strings;
size_t __size = backtrace(__array,__max_stack_flow);
printf("backtrace() returned %d addresses\n", (int)__size);
__strings = backtrace_symbols(__array,__size);
if(NULL == __strings)
{
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
fprintf (stderr,"obtained %zd stack frames.nm", __size);
for (size_t __i = 0; __i < __size; ++__i)
{
printf("%s\n", __strings[__i]);
}
//	This __strings is malloc(3)ed by backtrace_symbols(), and must be freed here
free (__strings);
exit(0);
}


· 比如说,我们需要对SIGSEGV进行处理,那么只需要调用signal(SIGSEGV, dump),那么当产生SIGSEGV中断时,就会触发dump调用,打印出堆栈,默认设置最大堆栈数为20,如果实际的堆栈大于20,仅仅显示最近的20层。
测试代码:
void segv_fun()
{
unsigned char* __ptr = 0x00;
*__ptr = 0x00;
}
void register_signal(int __signal)
{
signal(__signal, dump);
}
void TestDump::test_signal_segv()
{
printf("TestDump::test_signal SIGSEGV\n");
register_signal(SIGSEGV);
segv_fun();
}


运行输出:



从运行结果可以看到,程序SIGSEGV中断触发了dump函数,dump打印出了6帧,
第一帧:/easy_main(_Z4dumpi+0x26) [0x402896] 是在执行dump;
第二帧:/lib64/libc.so.6() [0x332ae329a0] 是调用libc的库函数;
第三帧:./easy_main(_ZN8TestDump5myRunEPKcb+0x5f) [0x402d2f],这个看起来有点奇怪,这一串貌似可以看出来一些信息,TestDump?不能很快定位。不用急,如果出现这个请看看,我们可以借助addr2line,通过地址转换到对应文件的行数。



再看看源代码文件,
void segv_fun()
{
unsigned char* __ptr = 0x00;
*__ptr = 0x00;
}


这下就知道问题所在之处了吧!
第N帧
......

(2) 分析core文件
如果不对信息进行任何接管,那么程序中断后,会产生一个core文件,core文件是当时程序中断时的内存的一个镜像,利用它可以还原场景。如果没有产生core 文件,请检查ulimit 参数,比如我的设置是这样,



那么需要设置它的太小,单位为blocks,一般来说1 blocks = 1k,也就是1024bytes.



现在我已将它设置为1M,那么如果程序占用内存小于1M的话,core文件是一个完整的内存镜像,大于1M也会保留最近的1M的内存信息。



由输出结果可知,第一次执行时,没有生成core, 设置ulimit 值后,便产生了core dump了(segmentation fault core dumped).
接下来我们通过core 文件来定位错误信息。



显然,结果显示了产生中断的详细代码以及文件所在行数,这样是不是更方便呢!

(3) 结合(1),中断时启动gdb调试,
稍微改变下代码,捕捉到信号后,获取当前进程的参数,执行命令行。
void dump_for_gdb(int __signal)
{
const int __max_buf_size = 512;
char __buf[__max_buf_size] = {};
char __cmd[__max_buf_size] = {};
FILE* __file;
snprintf(__buf, sizeof(__buf), "/proc/%d/cmdline", getpid());
if(!(__file = fopen(__buf, "r")))
{
exit(0);
}
fclose(__file);
if(__buf[strlen(__buf) - 1] == '\n')	//	warning: multi-character character constant [-Wmultichar]
{
__buf[strlen(__buf) - 1] = '\0';	//	warning: multi-character character constant [-Wmultichar]
}
snprintf(__cmd, sizeof(__cmd), "gdb %s %d",__buf, getpid());
system(__cmd);
exit(0);
}
void register_signal_for_gdb(int __signal)
{
signal(__signal, dump_for_gdb);
}
void TestDump::test_dump_for_gdb()
{
printf("TestDump::test_dump_for_gdb SIGSEGV\n");
register_signal_for_gdb(SIGSEGV);
segv_fun2();
}


输出结果



当然,我们可以把这些东西整合起来,比如在项目最终上线后,我们希望这个操作更加简单,因为到了运营阶段,操作者可能不是开发者,而是运维人员,我们希望用更简单,直接的方式,把这些信息提取出来,那就需要更进一步的工作了。我们之前采用的方法是:把dump的堆栈信息写的文件中,然后使用shell读取这些堆栈信息,病使用addr2line转化到具体的文件行数或者函数并保存最终文件。这样运维人员只需要把最终的文件给开发人员,便可分析定位问题了。
目前所常用的就这些了,如果有更好的方式,欢迎补充。

引用代码:
git@github.com:yuyunliuhen/easy.git
https://github.com/yuyunliuhen/easy/blob/master/src/base/easy_dump.h
https://github.com/yuyunliuhen/easy/blob/master/src/test/easy_test_dump.cc

参考:

Linux
信号signal处理机制

Linux下的段错误产生的原因及调试方法

通过PID获取进程相关信息,如cmdline


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: