• 从libc-2.27.so[7fd68b298000+1e7000]崩溃回溯程序段错误segfault


    近期在外场试验过程中出现了一次进程退出的问题(在西安地面测试中也出现过一次),但是因为该问题不复现,后面就一直记着但没有进行闭环,也不清楚是异常退出还是因为资源消耗问题被系统杀死。这次回来之后有必要进行闭环控制

    在家里使用普通电脑跑了70+h的测试,发现该问题还是不复现。于是联系西工大任务计算机人员获取系统日志(/var/log/目录下)syslog和kernlog,发现当时时间点的日志信息如下:

    1. Aug 15 17:52:02 ucas-pc2 kernel: [ 6857.318149] show_signal_msg: 44 callbacks suppressed
    2. Aug 15 17:52:02 ucas-pc2 kernel: [ 6857.318152] actor1_node[2230]: segfault at 55b74e933fee ip 00007fd68b426d7d sp 00007fd6657a04e8 error 4 in libc-2.27.so[7fd68b298000+1e7000]
    3. Aug 15 17:52:02 ucas-pc2 kernel: [ 6857.318158] Code: c3 4c 8d 14 11 4d 39 d1 0f 82 03 ff ff ff 0f 18 89 00 ff ff ff 0f 18 89 c0 fe ff ff 0f 18 89 80 fe ff ff 0f 18 89 40 fe ff ff fe 6f 01 c5 fe 6f 49 e0 c5 fe 6f 51 c0 c5 fe 6f 59 a0 48 81 e9
    4. Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.973254] configure success, NodeID: 100, isRelayNode: 0.
    5. Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.975100] channel: 0, netlink_pid: 360 Registered!
    6. Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.975517] configure success, NodeID: 100, isRelayNode: 0.
    7. Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.997209] channel: 1, netlink_pid: 361 Registered!
    8. Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.997452] channel: 2, netlink_pid: 362 Registered!

    因为无法复现,所以没有办法拿到core文件,于是整个网络搜索原因。里面的核心问题有3个:glibc的错误、段错误和错误代码4。

    搜索输出信息,在/arch/x86/mm/fault.c中里面找到了show_signal_msg函数,里面内容如下:

    1. /*
    2. * Print out info about fatal segfaults, if the show_unhandled_signals
    3. * sysctl is set:
    4. * 如果设置了show_unhandled_signals sysctl,请打印有关致命段错误的信息:
    5. */
    6. static inline void
    7. show_signal_msg(struct pt_regs *regs, unsigned long error_code,
    8. unsigned long address, struct task_struct *tsk)
    9. {
    10. if (!unhandled_signal(tsk, SIGSEGV))
    11. return;
    12. if (!printk_ratelimit())
    13. return;
    14. printk("%s%s[%d]: segfault at %lx ip %p sp %p error %lx",
    15. task_pid_nr(tsk) > 1 ? KERN_INFO : KERN_EMERG,
    16. tsk->comm, task_pid_nr(tsk), address,
    17. (void *)regs->ip, (void *)regs->sp, error_code);
    18. print_vma_addr(KERN_CONT " in ", regs->ip);
    19. printk(KERN_CONT "\n");
    20. }

    通过里面的内容基本可以断定dmesg中打印出来的段错误信息就是出自这个函数,但是为了保证绝对的正确性,可以通过更改内核源码,在里面添加一个打印信息来判断是否由这个函数输出。

    我们这里的error_code为4,意思是用户态程序内存读出错,error 由三个bit位组成,它的具体含义如下(对于linux 2.x版本来说是3bit构成的):

    • bit2: 值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
    • bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
    • bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址

    对于linux 3.0之后的版本,error_code的参数由5 bit表示,(/arch/x86/mm/fault.c中)发现如下内容:

    1. /*
    2. * Page fault error code bits:
    3. *
    4. * bit 0 == 0: no page found 1: protection fault
    5. * bit 1 == 0: read access 1: write access
    6. * bit 2 == 0: kernel-mode access 1: user-mode access
    7. * bit 3 == 1: use of reserved bit detected
    8. * bit 4 == 1: fault was an instruction fetch
    9. */
    10. enum x86_pf_error_code {
    11. PF_PROT = 1 << 0,
    12. PF_WRITE = 1 << 1,
    13. PF_USER = 1 << 2,
    14. PF_RSVD = 1 << 3,
    15. PF_INSTR = 1 << 4,
    16. };

    再结合https://blog.csdn.net/SweeNeil/article/details/83926778可以进行排查。

    通过上面基本确定是内存访问越界造成的。最后排查发现是memcpy()中要复制的字节数量为-1造成的。这个也是一系列机缘巧合造成的,但是从侧面说明了没有进行返回值判断处理,需要进行代码加固。


    下面说一下可复现的错误的调试过程。

    gdb无效时使用dmesg调试

    1. 打开coredump配置:

    在运行进程的终端执行:ulimit -c unlimited,该方法只能临时打开当前终端的内核转储,详细方法请参考:内核转储-coredump简介_guotianqing的博客-CSDN博客_内核转储

    程序崩溃时产生了core文件,但是gdb打印的情况如下:

    1. Program terminated with signal SIGSEGV, Segmentation fault.
    2. #0  0x00007eff6effd9fd in ?? ()
    3. [Current thread is 1 (LWP 19887)]
    4. (gdb) bt
    5. #0  0x00007eff6effd9fd in ?? ()
    6. #1  0x000055e3e8345330 in ?? ()
    7. #2  0x635027382a97e600 in ?? ()
    8. #3  0x000055e3e8343f60 in ?? ()
    9. #4  0x000055e3e7a99f67 in ?? ()
    10. #5  0x000055e3e8345100 in ?? ()
    11. #6  0x635027382a97e600 in ?? ()
    12. #7  0x000055e3e8345100 in ?? ()
    13. #8  0x00007ffcd31007e0 in ?? ()
    14. #9  0x0000000000000000 in ?? ()
    15. (gdb) 

    这就尴尬了。我编译程序时加了-g呀,怎么是这样呢?经查,出现这种情况可能是在编译时加了-O2选项,或者是程序链接的库没有-g编译。没办法,由于当时机器上没有源码,只能进一步查查原因。

    coredump行不通了,还有个dmesg。一般异常崩溃的程序,内核都会在日志里记录一下崩溃信息,直接在终端运行dmesg查看即可。关于dmesg的更多介绍,请参考Linux内核日志查看之dmesg命令简介_guotianqing的博客-CSDN博客_dmesg 日志

    刚崩溃时查看的话,最后一行应该就是。我的显示如下:

    [1745123.819693] godserver[29078]: segfault at 1ff8 ip 00007ff3736949fd sp 00007fff9f148ba0 error 4 in libc-2.27.so[7ff3735fd000+1e7000]

    其中,后面的3个奇葩数字分别为出错时的地址1ff8(用处不大)、发生错误时指令的地址00007ff3736949fd、堆栈指针00007fff9f148ba0.

    error number为4,意思是用户态程序内存读出错。到这里,可以先分析一下,哪些操作会导致内存读崩溃:

    • 操作null指针
    • 把未初始化或非法指针交给函数处理
    • 读内存越界
    • 读取非法内存

    这样范围很大,不可能一点一点看代码找吧。其实,根据现有的信息,我们还是可以再做些努力的。那就从libc入手吧。注意两个地址:

    崩溃时指令地址ip:00007ff3736949fd和崩溃库的基地址:7ff3735fd000,两者相减,其实就得到了崩溃时库的位置:9 79FD。

    下面我们来看一下相对代码段地址为979FD的地方是什么函数:

    1. # -t为打印文件的符号表入口,具体可查看man page
    2. # -T为打印文件的动态符号表入口
    3. % objdump -tT /lib/x86_64-linux-gnu/libc.so.6 | grep 979
    4. 00000000000979c0 g    DF .text    0000000000000e31 (GLIBC_2.2.5) cfree
    5. 00000000000979c0 g    DF .text    0000000000000e31  GLIBC_2.2.5 __libc_free
    6. 00000000000979c0 g    DF .text    0000000000000e31  GLIBC_2.2.5 free

    打印出来的函数是free。

    结合之前的分析,读内存非法,调用free确实可能导致这个崩溃。很可能是free了非法的地址,即free了一个无效的指针。到这里,为了验证一下自己的猜想,可以写个free无效指针的测试程序,运行一下,看看它是不是也会导致dmesg记录这样一条日志。c++中可以导致底层free的函数也不多,如free,delete, 智能指针的析构等。可以重点查一下它们。经过添加日志等方式定位,我的程序是在退出时,一个类析构时free了无效的指针。本节上面部分原文链接:https://blog.csdn.net/guotianqing/article/details/109729623

    同上,本次我们写一个memcpy异常的一个测试:

    1. #include
    2. #include
    3. using namespace std;
    4. int main()
    5. {
    6. char buf[1024] = {0};
    7. char src[5] = "abcd";
    8. memcpy(buf,src,-1);
    9. return 0;
    10. }

    输出如下: 

    1. [18582.902548] usb 2-2.1: SerialNumber: 000650268328
    2. [18583.471480] audit: type=1400 audit(1661333115.794:40): apparmor="DENIED" operation="capable" profile="/usr/sbin/cups-browsed" pid=7193 comm="cups-browsed" capability=23 capname="sys_nice"
    3. [18846.812513] memcpySegfault[8502]: segfault at 7fff66c78ffb ip 00007ff0d026b961 sp 00007fff67476228 error 4 in libc-2.31.so[7ff0d0102000+178000]
    4. [18846.812559] Code: c3 4c 8d 14 11 4d 39 d1 0f 82 03 ff ff ff 0f 18 89 00 ff ff ff 0f 18 89 c0 fe ff ff 0f 18 89 80 fe ff ff 0f 18 89 40 fe ff ff fe 6f 01 c5 fe 6f 49 e0 c5 fe 6f 51 c0 c5 fe 6f 59 a0 48 81 e9

    按照上面的思路,使用ip-base=7ff0d026b961 - 7ff0d0102000,得到169961,我们查看一下glibc.so中该位置的函数:

    1. ok@u20:~/test$ nm -D /lib/x86_64-linux-gnu/libc-2.31.so |grep 0000000000162
    2. 0000000000162c20 T fattach
    3. 0000000000162c40 T fdetach
    4. 0000000000162c60 T getmsg
    5. 0000000000162c80 T getpmsg
    6. 0000000000162ca0 T isastream
    7. 0000000000162be0 T posix_spawn
    8. 0000000000162c00 T posix_spawnp
    9. 0000000000162cc0 T putmsg
    10. 0000000000162ce0 T putpmsg
    11. 0000000000162b10 T sched_getaffinity
    12. 0000000000162b80 T sched_setaffinity

    使用nm -D或者objdump -tT都不能找到是memcpy函数。这可能是memcpy中调用了其他函数造成的。上面他那个用例是比较巧,刚好free函数起始点在979开头的段。

    继续换工具:addr2line工具是一个可以将指令的地址和可执行映像转换为文件名、函数名和源代码行数的工具。这在内核执行过程中出现崩溃时,可用于快速定位出出错的位置,进而找出代码的bug。

    addr2line -e /lib/x86_64-linux-gnu/libc-2.31.so -fCi 0x169961

     这个函数有点懵圈啊,和预想的不一样啊。下源码吧,好像也没啥用,因为是一个大文件夹。

    上面判断可能出错的是__nss_database_lookup函数,此时我们再反汇编libc-2.31.so,找到对应函数的汇编代码,查到对应偏移地址:

    objdump -d /lib/x86_64-linux-gnu/libc-2.31.so |grep __nss_database_lookup >5.txt

    大概是对的,但是这个函数貌似和memcpy没啥关系啊。后面就搞不懂了。本文先这样吧,等后面有更好的方式了再补吧。

    参考:从libc-2.27.so[7ff3735fd000+1e7000]崩溃回溯程序段错误segfault_guotianqing的博客-CSDN博客

     Linux段错误Segfault内核层面分析 - it610.com

  • 相关阅读:
    【GlobalMapper精品教程】023:Excel数据通过相同字段连接到属性表中(气温降水连接到气象台站)
    ROS话题(Topic)通信:自定义msg - 例程与讲解
    快速排序的简单理解
    电脑重装系统后如何给系统磁盘扩容空间
    [SLAM] IMU预积分推导
    LVGL - RV1109 LVGL UI刷新效率优化-02
    offline RL | ABM:从 offline dataset 的好 transition 提取 prior policy
    Go基础——基础语法
    开源 RPA 和并行处理的力量
    chrome NET::ERR_CERT_INVALID 使用thisisunsafe 解决浏览器发生了什么
  • 原文地址:https://blog.csdn.net/jinking01/article/details/126508176