GDB和COREDUMP

 

目录

前言

一、GDB

1.1  关于GDB

1.2 GDB的用途

1.3 详细使用方法

二、 coredump   

2.1 产生和位置

2.1.1 产生

2.1.2 指定内核转储的文件名和目录

三、gdbserver和GDB的调试结合

四、常见设备崩溃

4.1 魔术键的使用,查看当前进程和内存信息

4.2 堆内存越界。

4.3 堆内存泄漏

4.4 句柄泄漏

4.5 栈越界

4.5.1 Stack Guard

4.5.2 一些容易引起内存越界的操作

4.5.3 堆栈内部越界

4.5.4 全局变量或者动态分配的内存越界

4.6 栈溢出

4.7 oom-killer

4.8 设备卡死

五、实例


前言

编码占据了编程工作量的90%,调试占了另外的90%。计算机只会做你告诉它的事情,它不会读懂你的心思,做你想要它做的事情。即使专业的程序员也在一直制造缺陷,所以如果你的程序有问题,不必感到沮丧!

BUG复现-摘自《Debug Hacks》

1、听取收集信息;

2、确认现象、复现概率和时间;

3、分析时尽量缩小范围,也可以根据版本划分问题(版本回退),不要被表象迷惑;

4、问题原因不明时,可以怀疑硬件问题,同类错误,为BUG时刻准备好,和同事讨论,上网搜索;

5、在无法差生coredump的情况下,就只能通过添加打印来实现查找崩溃的地方。不断缩小哪里产生越界导致了访问了不该访问的地址或者访问了空指针。有时候,如果是栈上越界,可以创建一个局部变量,观察这个局部变量是在哪被某个函数给越界的。

参考来源:

http://blog.csdn.net/haoel/article/details/2879

《Debug Hacks》我的读书笔记:

https://blog.csdn.net/zhang_yin_liang/article/details/79825335

一、GDB

1.1  关于GDB

       UNIX下的程序命令行调试工具。要想用GDB调试程序,需要在编译的应用程序中加入-g的选项,如gcc -g hello.c -o hello。否则你将看不到程序的函数名和变量名,看到的全部都是运行时的内存地址。

       在学习GDB调试前,可以先学习了解到我们的函数是如何调用的:栈帧,在这个过程中我们要为函数开辟栈空间,用于临时(局部)变量的保存、现场的保护(函数的返回值和参数、调用前寄存器的状态,调用前栈帧的顶部和底部的地址),这块栈空间我们称之为函数。

       举例子,就是ESP是始终指向栈的顶部的,EBP是指向一个函数栈的栈帧顶部。所以当调用一个函数时,先将EBP的值进栈,此时ESP的指向下一个。这时将ESP的值给EBP,EBP指向新开辟的栈的位置。当调用一个函数结束时,先将EBP的值给到ESP,再出栈此时存储在栈上存储的值给EBP,此时ESP指向上一个。结束调用,回到了上一个函数的调用栈状态。

1.2 GDB的用途

  •     gdb <program> 

       program也就是你的执行文件,一般在当然目录下。

  •     gdb <program> core

       用gdb同时调试一个运行程序和core文件,core是程序非法执行后core dump后产生的文件。

  •    gdb <program> <PID>

       如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试他。program应该在PATH环境变量中搜索得到。

1.3 详细使用方法

http://blog.csdn.net/liigo/article/details/582231/

二、 coredump   

    在不清楚BUG复现的方法,或是BUG极其罕见,又或是只在特定的机器上发生的情况下,只要获取内核转储,也能够调试。

    要产生core文件,首先要设置产生core文件的大小,ulimit -c命令可以查看当前设置的core文件大小,后面可以设置core文件大小,如命令ulimit -c unlimited,这里就是设置生成的core文件无大小限制。

     然后可以按照GDB的调试步骤看看,产生的核心错误在程序的哪里出错。

2.1 产生和位置

参考来源:https://blog.csdn.net/guanyijun123/article/details/45647247

2.1.1 产生

     在终端中输入以下命令,查看内核转储是否有效。
#ulimit -c
0
-c 表示内核转储文件的大小限制,现在显示为零,表示不能用。
可以改为1G
#ulimit -c 1073741824
也可以改为无限制
#ulimit -c unlimited
上面所述的方法,只是在当前shell中生效,重启之后,就不再有效了。永久生效的办法是:
#vim /etc/profile 然后,在profile中添加:
ulimit -c 1073741824
 (但是,若将产生的转储文件大小大于该数字时,将不会产生转储文件)
或者
ulimit -c unlimited
这样重启机器后生效了。 或者, 使用source命令使之马上生效。
#source /etc/profile

2.1.2 指定内核转储的文件名和目录

       缺省情况下,内核在coredump时所产生的core文件放在与该程序相同的目录中,并且文件名固定为core。很显然,如果有多个程序产生core文件,或者同一个程序多次崩溃,就会重复覆盖同一个core文件。
我们可以通过修改kernel的参数,指定内核转储所生成的core文件的路径和文件名。
可以通过在/etc/sysctl.conf文件中,对sysctl变量kernel.core_pattern的设置。
#vim /etc/sysctl.conf 然后,在sysctl.conf文件中添加下面两句话:
kernel.core_pattern = /var/core/core%t_%e_%p
kernel.core_uses_pid = 0

保存后退出。
需要说明的是, /proc/sys/kernel/core_uses_pid。如果这个文件的内容被配置成1,即使core_pattern中没有设置%p,最后生成的core dump文件名仍会加上进程ID。
这里%e, %p分别表示:
%c 转储文件的大小上限
%e 所dump的文件名
%g 所dump的进程的实际组ID
%h 主机名
%p 所dump的进程PID
%s 导致本次coredump的信号
%t 转储时刻(由1970年1月1日起计的秒数)
%u 所dump进程的实际用户ID
可以使用以下命令,使修改结果马上生效。
#sysctl -p
请在/var目录下先建立core文件夹,然后执行a.out程序,就会在/var/core/下产生以指定格式命名的内核转储文件。查看转储文件的情况:
#ls /var/core
core_a.out_2834

三、gdbserver和GDB的调试结合

      当进程或者线程卡主的时候处理的方式。 

      远程调试环境由宿主机GDB和目标机调试stub共同构成,两者通过串口或TCP连接。使用 GDB标准程串行协议协同工作,实现对目标机上的系统内核和上层应用的监控和调试功能。调试stub是嵌入式系统中的一段代码,作为宿主机GDB和目标机调试程序间的一个媒介而存在。 就目前而言,嵌入式Linux系统中,主要有三种远程调试方法,分别适用于不同场合的调试工作:用ROM Monitor调试目标机程序、用KGDB调试系统内核和用gdbserver调试用户空间程序。这三种调试方法的区别主要在于,目标机远程调试stub 的存在形式的不同,而其设计思路和实现方法则是大致相同的。 而我们最常用的是调试应用程序。就是采用gdb+gdbserver的方式进行调试。在很多情况下,用户需要对一个应用程序进行反复调试,特别是复杂的程序。采用GDB方法调试,由于嵌入式系统资源有限性,一般不能直接在目标系统上进行调试,通常采用gdb+gdbserver的方式进行调试。

        gdbserver源码编译地址在gdb开源库中的目录下。是先编辑gdb出来,然后再编辑gdbserver的。

源码地址:http://ftp.gnu.org/gnu/gdb/

参考网址

https://blog.csdn.net/absurd/article/details/1793646

https://blog.csdn.net/backcrow/article/details/50496837

四、常见设备崩溃

       不同的设备崩溃其实有不同的方法,有时候单独用coredump很难判断出是哪里越界导致的,例如堆越界就是这样的道理,他不是崩溃在你调用的地方,而是每次崩溃都很诡异,当然也是能找到一定的规则。

4.1 魔术键的使用,查看当前进程和内存信息

kernel shortcuts

下面的Linux快捷键必须在内核中启用以后才可以使用。 
而且必须启用魔术组合键(sysrq):

启用sysrq: 
$sudo echo 1 > /proc/sys/kernel/sysrq

禁用sysrq:

$sudo echo 0 > /proc/sys/kernel/sysrq 
1. alt-sysrq-s - 同步所有已挂载的文件系统。所有缓存中的数据将被立刻写入磁盘。 
2. alt-sysrq-u - 以只读方式重新挂载所有已挂载文件系统。 
3. alt-sysrq-b - 快速重起。 不要在没有同步和卸载文件系统的情况下执行,否则会导致文件系统严重错误。 
4. alt-sysrq-s,然后 alt-sysrq-u,然后 alt-sysrq-b - 同步所有文件系统、以只读方式重新挂载所有文件系统、立刻重新启动。这是重新启动Linux的最快方式。 
5. alt-sysrq-h - 输出其他魔术组合键列表(sysrq)功能。

参考地址:

http://blog.chinaunix.net/uid-29145190-id-3877376.html

https://blog.csdn.net/jasonchen_gbd/article/details/79080576

4.2 堆内存越界。

dmalloc的使用,检测,Dmalloc只能检测越界写,但不能检测越界读。另外,Dmalloc只检测堆上用malloc系函数(而不是sbrk()或mmap())分配的内存,而无法对栈内存和静态内存进行检测。 本质上它也是通过hook malloc(), realloc(), calloc(),free()等内存管理函数,还有strcat(), strcpy()等内存操作函数,来检测内存问题。

http://dmalloc.com/

4.3 堆内存泄漏

挂钩子函数去检测泄漏,同时也需要查看该进程内存的分配在cat /proc/PID/map

C程序内存泄漏检测:mtrace ,memwatch

4.4 句柄泄漏

挂钩子函数去检测泄漏。

Linux下排查文件句柄泄漏还是挺方便的。

1.通过 ps -aux | grep 进程名 来找到进程ID

2.通过 ls -la /proc/进程ID/fd 可以棑看文件信息,文件句柄非Socket的可以查看文件路径

如果发现有同样的文件句柄打开都多次则可能存在有异常。

4.5 栈越界

在linux下,栈越界写坏返回地址会导致调用栈无法回溯,这和堆越界一样。这就导致我们直接使用coredump的bt没有办法查看崩溃时调用栈。栈越界的例子就像,一个32位的数据强制转换成64位数据去访问。

4.5.1 Stack Guard

Stack Guard 是第一个使用 Canaries 探测的堆栈保护实现,它于 1997 年作为 GCC 的一个扩展发布。最初版本的 Stack Guard 使用 0x00000000 作为 canary word。尽管很多人建议把 Stack Guard 纳入 GCC,作为 GCC 的一部分来提供堆栈保护。但实际上,GCC 3.x 没有实现任何的堆栈保护。直到 GCC 4.1 堆栈保护才被加入,并且 GCC4.1 所采用的堆栈保护实现并非 Stack Guard,而是 Stack-smashing Protection(SSP,又称 ProPolice)。

启用该选项后编译器会产生额外的代码来检测缓冲区溢出,例如栈溢出攻击。这是通过在有缺陷的函数中添加一个保护变量来实现的。这包括会调用到alloca的函数,以及具有超过8个字节缓冲区的函数。当执行到这样的函数时,保护变量会得到初始化,而函数退出时会检测保护变量。如果检测失败,会输出一个错误信息并退出程序。

-fstack-protector:

启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码。

-fstack-protector-all:

启用堆栈保护,为所有函数插入保护代码。

-fno-stack-protector:

禁用堆栈保护。

参考网址:https://blog.csdn.net/yuanbinquan/article/details/51778269  一种通过在编译中增加选项的方法去排查栈越界的方法。

https://www.linuxidc.com/Linux/2014-03/98455.htm

4.5.2 一些容易引起内存越界的操作

1:注意strcpy  sprintf  memcpy  函数目的缓冲区的大小

2:strncpy  strcpy 目的缓冲区的大小及源缓冲区是否以\0结尾

3:还要注意数组的大小、循环的次数

4:链表的头部和尾部在处理插入和删除节点时的操作

4.5.3 堆栈内部越界

  主要现象:(1):某些局部变量被修改

           (2):函数返回的时候死机

  主要原因:(1):临时变量或者数组越界

4.5.4 全局变量或者动态分配的内存越界

主要现象:

(1):全局变量被修改

(2):内存泄漏(如果动态分配的内存越界,有可能导致被越界的内存无法释放或者不能全部释放)

主要原因:

全局或者动态分配的内存越界。

4.6 栈溢出

Linux对于线程的栈大小是有限制的,默认为8M。

主要现象:(1):某些全局变量被修改

          (2):某些任务不能正常工作函数调用不正常

          (3):某些局部变量被修改

 

  主要原因:(1):线程堆栈开辟的太小

          (2):定义的太大的局部变量

          (3):函数调用太深

4.7 oom-killer

背景:

1、当物理内存和交换空间都被用完时,如果还有进程来申请内存,内核将触发OOM killer。

2、内存结构,对于物理内存内存,linux会把它分很多区(zone),zone上还有node的概念,zone下面是很多page,这些page是根据buddy分配算法组织的。

3、可以申请比物理内存实际大的内存,也就是overcommit,这样会面临一个问题,就是当真的需要这么多内存的时候怎么办—>oom killer!

4、linux会为每个进程算一个分数,最终他会将分数最高的进程kill。

措施:

保护某个进程不被内核杀掉可以这样操作。

echo -17 > /proc/$PID/oom_adj

参考:

https://segmentfault.com/a/1190000008268803?utm_source=tag-newest

https://blog.csdn.net/zhuyunier/article/details/84974405

4.8 设备卡死

解决设备卡死的办法有很多,比如上文提到的gdb server,还有魔法键也能看的。此外,也可以用strace工具。这个strace和gdb server类似。前提都是需要找到卡死的线程。

五、实例

1、由pthread_kill引起的设备崩溃(段错误),内存越界。

参考

https://www.jianshu.com/p/756240e837dd

https://libc-alpha.sourceware.narkive.com/v63gozF0/race-and-segmentation-fault-in-pthread-kill-vs-thread-teardown

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值