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

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

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

Aug 15 17:52:02 ucas-pc2 kernel: [ 6857.318149] show_signal_msg: 44 callbacks suppressed
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]
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 <c5> fe 6f 01 c5 fe 6f 49 e0 c5 fe 6f 51 c0 c5 fe 6f 59 a0 48 81 e9 
Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.973254] configure success,  NodeID: 100, isRelayNode: 0.
Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.975100] channel: 0, netlink_pid: 360 Registered!
Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.975517] configure success,  NodeID: 100, isRelayNode: 0.
Aug 15 17:52:47 ucas-pc2 kernel: [ 6901.997209] channel: 1, netlink_pid: 361 Registered!
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函数,里面内容如下:

/*
 * Print out info about fatal segfaults, if the show_unhandled_signals
 * sysctl is set:
 * 如果设置了show_unhandled_signals sysctl,请打印有关致命段错误的信息:
 */
static inline void
show_signal_msg(struct pt_regs *regs, unsigned long error_code,
		unsigned long address, struct task_struct *tsk)
{
	if (!unhandled_signal(tsk, SIGSEGV))
		return;

	if (!printk_ratelimit())
		return;

	printk("%s%s[%d]: segfault at %lx ip %p sp %p error %lx",
		task_pid_nr(tsk) > 1 ? KERN_INFO : KERN_EMERG,
		tsk->comm, task_pid_nr(tsk), address,
		(void *)regs->ip, (void *)regs->sp, error_code);

	print_vma_addr(KERN_CONT " in ", regs->ip);

	printk(KERN_CONT "\n");
}

通过里面的内容基本可以断定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中)发现如下内容:

/*
 * Page fault error code bits:
 *
 *   bit 0 ==	 0: no page found	1: protection fault
 *   bit 1 ==	 0: read access		1: write access
 *   bit 2 ==	 0: kernel-mode access	1: user-mode access
 *   bit 3 ==				1: use of reserved bit detected
 *   bit 4 ==				1: fault was an instruction fetch
 */
enum x86_pf_error_code {

	PF_PROT		=		1 << 0,
	PF_WRITE	=		1 << 1,
	PF_USER		=		1 << 2,
	PF_RSVD		=		1 << 3,
	PF_INSTR	=		1 << 4,
};

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

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


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

gdb无效时使用dmesg调试

1. 打开coredump配置:

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

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

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

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

coredump行不通了,还有个dmesg。一般异常崩溃的程序,内核都会在日志里记录一下崩溃信息,直接在终端运行dmesg查看即可。关于dmesg的更多介绍,请参考Linux内核日志查看之dmesg命令简介_guotianqing的博客-优快云博客_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的地方是什么函数:

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

打印出来的函数是free。

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

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

#include <iostream>
#include <memory.h>

using namespace std;

int main()
{
    char buf[1024] = {0};
    char src[5] = "abcd";
    memcpy(buf,src,-1);
    return 0;
}

输出如下: 

[18582.902548] usb 2-2.1: SerialNumber: 000650268328
[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"
[18846.812513] memcpySegfault[8502]: segfault at 7fff66c78ffb ip 00007ff0d026b961 sp 00007fff67476228 error 4 in libc-2.31.so[7ff0d0102000+178000]
[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 <c5> 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中该位置的函数:

ok@u20:~/test$ nm -D /lib/x86_64-linux-gnu/libc-2.31.so |grep 0000000000162
0000000000162c20 T fattach
0000000000162c40 T fdetach
0000000000162c60 T getmsg
0000000000162c80 T getpmsg
0000000000162ca0 T isastream
0000000000162be0 T posix_spawn
0000000000162c00 T posix_spawnp
0000000000162cc0 T putmsg
0000000000162ce0 T putpmsg
0000000000162b10 T sched_getaffinity
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的博客-优快云博客

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值