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



    不管是在开发或者运行过程中,调试保证程序正常运行最基本的手段,熟悉这些调试方式,方便我们更快的定位程序问题所在,提高开发效率。
 一 程序正常运行调试
    (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转化到具体的文件行数或者函数并保存最终文件。这样运维人员只需要把最终的文件给开发人员,便可分析定位问题了。
目前所常用的就这些了,如果有更好的方式,欢迎补充。

引用代码:


参考:

Linux 信号signal处理机制




郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。