加入星计划,您可以享受以下权益:

  • 创作内容快速变现
  • 行业影响力扩散
  • 作品版权保护
  • 300W+ 专业用户
  • 1.5W+ 优质创作者
  • 5000+ 长期合作伙伴
立即加入
  • 正文
    • 如何开启coredump
    • GDB调试
    • GDB附着命令
    • address sanitizer工具
    • 两个案例
  • 相关推荐
  • 电子产业图谱
申请入驻 产业图谱

如何通过Core Dump和GDB快速定位程序崩溃根因:从内存溢出到死锁,一文带你玩转调试

11/18 09:12
3778
阅读需 15 分钟
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

程序在异常终止时,会触发对应的错误信号,此时操作系统会将程序的内存态内容包括程序内存、寄存器状态、调用栈等信息写入一个core文件。异常终止原因根据对应信号主要分为如下几种:

1、段错误,触发信号 SIGSEGV

包括访问空指针、数组越界、栈溢出等;

2、非法指令,触发信号SIGILL

比如把一些随机数据当成指令执行:

void (*func)() = ptr; // 将内存空间作为函数指针func(); // 触发 SIGILL

3、浮点异常,触发信号 SIGFPE

也就是除0操作;

4、非法内存访问,触发信号SIGMEM

如访问已释放的内存;

5、总线错误,触发信号 SIGBUS

比如收到异常的网络包等。

如何开启coredump

1、开启coredump:ulimit -c unlimited;

2、对于某些设置了suid的程序如网卡抓包程序,在需要开启coredump时,需要修改 /etc/sysctl.conf 文件来启用。

排查问题时,如果有core文件,使用gdb分析;否则使用dmesg分析内核日志。分析问题时,首先确认是否是OOM导致进程消失。

grep xxx /var/log/messages 获取到程序crash的地址,然后使用ldd查看外部依赖库地址基址,使用

objdump -d /lib64/libc-2.12.so --start-address=0x3ab9a7500 | head -n2000 | grep 75f62

查找crash的系统调用。

在排查问题时,coredump通常需要配合持久化日志综合分析。

GDB调试

gdb进入coredump堆栈后,bt可以展示栈帧,默认是当前栈帧也就是0栈帧。要查看对应栈帧的变量情况,可以使用f+栈帧号切换。list func可以查看对应函数的反编译源码,print p、print &p可以打印对应变量的值。

frame +数字可以切换函数帧,disassemble可以查看汇编代码。

使用print可以查看寄存器状态、函数的栈帧空间、形参的位置和值是否有问题。

info signals查看信号是否会引起段错误,info registers 命令查看寄存器状态、函数调用时的栈空间。

多线程场景,需要切换到具体的线程查看堆栈进行分析。查看所有线程:info threads

切换到对应线程,如线程2:

thread 2 //这里使用的是gdb的id,不是pid

查看当前状态:bt/where

info mutex:查看当前程序中的互斥锁信息。

dis可以查看地址的汇编指令,如:

dis -l c000000000255900 0xc000000000255900 <main+0>: push %rbp 0xc000000000255901 <main+1>: mov %rsp,%rbp 0xc000000000255904 <main+4>: sub $0x10,%rsp...

rd可以查看内存内容,如:

rd 0x7fffffffe000 32

这将从0x7fffffffe000地址开始,读取 32 个字节的内存内容。不同版本可能不一样,可以使用x命令代替,这个命令用来分析函数比较方便,打印函数堆栈内容,第一个参数一般为函数返回地址,从第二个参数开始为函数的入参:

比如分析ipv4报文,查找4500开头的内容,找到对应的地址,然后使用iphdr <栈地址>可以打印报文内容,根据偏移查找udphdr、tcphdr内容。

GDB附着命令

遇到死锁之类的,可以使用非调试手段进行定位。

附加到正在运行的线程:gdb -p pid

附加到进程:gdb attach pid

使用gdb break设置条件断点,可以抓取偶现bug。

Gdb还可以用于调试程序,p打点,watch可以设置观察点,c继续执行:

#rbp是当前函数调用栈中的基指针寄存器,向下偏移8字节指向存放金丝雀值的地方(gdb) p $rbp - 0x8  $8 = (void *) 0x7fffffff04a8#这里对canary在栈中存放的地址打数据断点(gdb) watch *0x7fffffff04a8Hardware watchpoint 2: *0x7fffffffe4a8   #触发到条件断点(gdb) cContinuing.

经验:大型工程多个.so会依赖相同的开源头文件,如果不能保证每个.so各自依赖的头文件版本一致,就可能出现栈溢出踩内存问题。

丢日志问题

可以把日志写入到一个mmap打开的文件中(mmap不支持调整文件大小,需要预先分配好空间),如果进程崩溃了系统会自动把 mmap 后的内存写入到文件里,不会丢失。

内存延迟分配

用户调用API进行内存分配的时候,操作系统并不会直接分配给用户这么多内存,而是直到用户真的访问了申请的page时产生一个page fault,然后将这个page真的分配给用户,并重新执行产生page fault的语句。所以即使使用了new,在真正使用之前是没有被真正的分配虚拟内存

所以为了加速,可以提前初始化,或者使用memset对每个页读取一个字节,使其在内存中cache。

使用htop -p可以查看进程内存占用等情况。

内存问题分析

静态检测:

gcc使用-fstack-usage选项,能输出每个函数栈的最大使用量,具体含义:

1、static: 堆栈使用量在编译时是已知的,不依赖于任何运行时条件。

2、dynamic: 堆栈使用量依赖于运行时条件,例如递归调用或基于输入数据的条件分支。

3、bounded: 堆栈使用量虽然依赖于运行时条件,但有一个可预知的上限。

动态检测:

1、使用pmap或查看/proc/pid/maps中的stack。

2、通过注册一个自定义的信号处理函数来拦截 SIGSEGV段错误信号,处理函数会收到一个 siginfo_t 结构体,其中包含错误的地址和寄存器状态等上下文信息,可以判断是否发生了栈溢出。

3、栈缓冲溢出一般主要是数组越界,使用gcc的-fstack-protector选项保护栈,可触发检测函数。如果canary值被修改,程序会认为发生了栈溢出攻击,通常会立即终止,例如通过调用 __stack_chk_fail() 函数。这个也叫做“金丝雀分析法”。

另外,使用STL容器可以减少大部分栈缓冲溢出问题。

address sanitizer工具

开address sanitizer可以进行内存错误检测,且支持多线程环境,对程序性能影响夜宵,避免coredump。

使用步骤

编译时添加-fsanitize=address选项,插入内存错误检测的相关代码。

-fno-omit-frame-pointer选项保留堆栈指针

-g选项添加调试符号和源码行号。

-O1或者更高的优化级别。

打包并链接 libasan.so。示例:

gcc -fsanitize=address -fno-omit-frame-pointer -O1 -g xx.cc -o xx

两个案例

1、netfilter回调

netfilter可以自定义增加hook点,而这些钩子函数可能修改skb报文,导致数据或者程序异常。Netfilter的五个钩子点,分别为NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN、NF_INET_FORWARD、NF_INET_LOCAL_OUT、NF_INET_POST_ROUTING。

比如,如果是经过NF_INET_LOCAL_IN之后数据异常了,那么查找挂载在NF_INET_LOCAL_IN上的钩子:

查找处理 sk_buff (skb) 的 NF_INET_LOCAL_IN 钩子的流程如下:

1、使用struct命令查看sk_buff结构体中的dev字段:

#表示 skb包 关联的网络设备struct sk_buff.dev <sk_buff对象地址>dev = 0xffff8881171a8000  

2、从net_device结构体中获取net字段:

#显示网络设备关联的网络上下文 (init_net),即当前的网络命名空间。struct net_device.nd_net <net_device结构体对象地址>nd_net = { net = 0xffffffff8322dc80 <init_net> }

3、从net结构体中获取nf.hooks_ipv4:

nf.hooks_ipv4 = {0xfff88810ba7fe00, 0xfff888810ba7ff830, 0x0, 0xffff88810ba7fe80, 0x0}

hooks_ipv4对象表示IPv4的NF (netfilter)钩子数组,五个地址对应五个回调钩子函数地址,其中NF_INET_LOCAL_IN的位置是第一个元素 (0xfff88810ba7fe00)。

4、获取 NF_INET_LOCAL_IN 钩子的条目:

nf_hook_entries显示了钩子条目,num_hook_entries = 1 表示当前有一个钩子条目,hooks = 0xffff88810ba7ff88 表示钩子条目的地址。

5、查看具体的钩子条目:

nf_hook_entry显示了钩子条目的内容,其中 hook = 0xffffffffc0e6b260 是实际的处理函数地址,然后查看处理函数的具体逻辑。

2、包版本管理问题

程序依赖了third.lib和data.h文件,data.h包含Data结构体的实现,third.lib里也依赖data.h文件。当版本更新,如Data结构体里面增加了一个字段,而data.lib因为种种原因如包管理不规范而没有重新编译和更新时,可能会引起 core dump。

为了避免这种情况,可以使用PImpl解决。PImpl 模式用于将接口和实现分离,避免直接暴露结构体的实现,进而提高了ABI二进制接口兼容性。举例:

Data.h:

class DataImpl; // 前向声明class Data {public:Data();~Data();private:std::unique_ptr<DataImpl> pImpl; // 通过指针持有实现};

data.cc:

#include "data.h"struct DataImpl{int a;double b;std::string c;};...

PImpl 模式通过将实现细节封装在私有的实现类中,使内存布局变化与代码解耦,可以随意改变DataImpl的内存布局和实现细节,而不需要担心外部代码受到影响,因为外部代码只会与Data指针交互,而不是直接访问DataImpl的成员。

相关推荐

电子产业图谱

机械转行IT狗,目前在阿里巴巴淘宝事业群。日常记录Linux应用开发、嵌入式操作系统、无线网络协议栈。刚深入使用Java,跟大家一起入门交流。