记一次空迭代器导致的崩溃分析

一. 崩溃代码:

class EasySelect::Impl {
public:
	Impl() = default;
	std::vector<int> waitForReadable ();
	void addFd (int fd);
	void removeFd (int fd);
	void stopWait ();

private:
	std::vector<int> m_fds;
	std::mutex m_fdsMutex;
	std::mutex m_pipeMutex;
	std::pair<int, int> m_pipe;
	void openPipe ();
	void closePipe ();
};

崩溃发生在removeFd()成员函数:

void
EasySelect::Impl::removeFd (int fd)
{
	std::lock_guard<std::mutex> lock (m_fdsMutex);
	m_fds.erase (std::find (m_fds.begin(), m_fds.end(), fd));
}

正确的做法是一定要对迭代器判空的,所以应该这样:

void
EasySelect::Impl::removeFd (int fd)
{
	std::lock_guard<std::mutex> lock (m_fdsMutex);
	auto fdIt = std::find (m_fds.begin(), m_fds.end(), fd);
	if (fdIt == m_fds.end())
		return;
	m_fds.erase (fdIt);
}

二、 崩溃原因

erase()时崩溃,原因是没有对迭代器没有判空,erase()一个尾后迭代器导致崩溃。

知道了结果后自然很简单,但是在不明原因时却不太好排查。
因为总是以上帝视角来分析并不能提高我们的debug能力,所以这里以有限的视角来进行反推。

tip:不要放过每一次机会,这样在以后遇到更复杂的场景时才能应对。

三、 堆栈分析

这里用vscode + gdb来分析。

核心转储文件的生成见另一篇博客:gdb调试核心转储文件

1. 观察崩溃堆栈

$ gdb ./testPublic core

输入bt命令(或者info stackwhere

在这里插入图片描述

#0  __memcpy_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:273
#1  0x000055a8f7a03164 in std::__copy_move<true, true, std::random_access_iterator_tag>::__copy_m<int> (__first=0x4, __last=0x0, __result=0x0)
    at /usr/include/c++/12/bits/stl_algobase.h:431
#2  0x000055a8f7a02fed in std::__copy_move_a2<true, int*, int*> (__first=0x4, __last=0x0, __result=0x0) at /usr/include/c++/12/bits/stl_algobase.h:495
#3  0x000055a8f7a02dce in std::__copy_move_a1<true, int*, int*> (__first=0x4, __last=0x0, __result=0x0) at /usr/include/c++/12/bits/stl_algobase.h:522
#4  0x000055a8f7a02aed in std::__copy_move_a<true, __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > > > (__first=<error reading variable: Cannot access memory at address 0x4>, __last=non-dereferenceable iterator for std::vector, 
    __result=non-dereferenceable iterator for std::vector) at /usr/include/c++/12/bits/stl_algobase.h:529
#5  0x000055a8f7a028c9 in std::move<__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > > > (__first=<error reading variable: Cannot access memory at address 0x4>, __last=non-dereferenceable iterator for std::vector, 
    __result=non-dereferenceable iterator for std::vector) at /usr/include/c++/12/bits/stl_algobase.h:652
#6  0x000055a8f7a02232 in std::vector<int, std::allocator<int> >::_M_erase (this=0x55a8f87e22d0, __position=non-dereferenceable iterator for std::vector)
    at /usr/include/c++/12/bits/vector.tcc:179
#7  0x000055a8f7a01c0c in std::vector<int, std::allocator<int> >::erase (this=0x55a8f87e22d0, __position=non-dereferenceable iterator for std::vector)
    at /usr/include/c++/12/bits/stl_vector.h:1530
#8  0x000055a8f7a00df4 in EasySelect::Impl::removeFd (this=0x55a8f87e22d0, fd=0) at /home/sixqaq/ftp/ftp/public/src/EasySelect.cpp:46
#9  0x000055a8f7a0151f in EasySelect::Impl::closePipe (this=0x55a8f87e22d0) at /home/sixqaq/ftp/ftp/public/src/EasySelect.cpp:109
#10 0x000055a8f7a00e3a in EasySelect::Impl::stopWait (this=0x55a8f87e22d0) at /home/sixqaq/ftp/ftp/public/src/EasySelect.cpp:52
#11 0x000055a8f7a01682 in EasySelect::stopWait (this=0x7ffc4ef5ed50) at /home/sixqaq/ftp/ftp/public/src/EasySelect.cpp:132
#12 0x000055a8f7a06643 in testEasySelect () at /home/sixqaq/ftp/ftp/public/test/src/testEasySelect.cpp:24
#13 0x000055a8f7a05a14 in main () at /home/sixqaq/ftp/ftp/public/test/src/main.cpp:13

先粗看崩溃位置,发现是在std::vector<int>::erase()时失败,留意一下那些敏感参数值:error、null、none-reference之类的,包括可疑的内存地址。

在vscode终端里的堆栈信息,用Ctrl + Click都是可以跳转到崩溃代码的。

2. 作出猜想

先作出初步的猜想。

这个崩溃的是段错误,起初以为是并发访问的问题,后来验证不是。而且能够稳定复现,这也反映大概不是由于数据竞争。

这里其实也是我的失误,当看到空指针、空引用的那一刻起,就应该立刻追溯这些空值的传递。

3. 梳理上下文

#1堆栈开始看。

#1  0x000055a8f7a03164 in std::__copy_move<true, true, std::random_access_iterator_tag>::__copy_m<int> (__first=0x4, __last=0x0, __result=0x0)
    at /usr/include/c++/12/bits/stl_algobase.h:431

在这里插入图片描述

注意到这里的lastresult都是空指针,有必要验证一下它们的合法性。
看到这个复杂的模板不用怕,直接丢给GPT,不过这里没必要,确实没有什么有效信息,__builtin_memmove(),如果你C语言基础还行的话,应该能猜到它是类似于memove()的,不清楚也没关系。
在这里插入图片描述

这里看不出什么有效信息,所以我们继续看参数是怎么传递过来的,特别是lastresult这两个空的指针。

在这里插入图片描述

#0  __memcpy_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:273
#1  0x000055a8f7a03164 in std::__copy_move<true, true, std::random_access_iterator_tag>::__copy_m<int> (__first=0x4, __last=0x0, __result=0x0)
    at /usr/include/c++/12/bits/stl_algobase.h:431
#2  0x000055a8f7a02fed in std::__copy_move_a2<true, int*, int*> (__first=0x4, __last=0x0, __result=0x0) at /usr/include/c++/12/bits/stl_algobase.h:495
#3  0x000055a8f7a02dce in std::__copy_move_a1<true, int*, int*> (__first=0x4, __last=0x0, __result=0x0) at /usr/include/c++/12/bits/stl_algobase.h:522
#4  0x000055a8f7a02aed in std::__copy_move_a<true, __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > > > (__first=<error reading variable: Cannot access memory at address 0x4>, __last=non-dereferenceable iterator for std::vector, 
    __result=non-dereferenceable iterator for std::vector) at /usr/include/c++/12/bits/stl_algobase.h:529
#5  0x000055a8f7a028c9 in std::move<__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > > > (__first=<error reading variable: Cannot access memory at address 0x4>, __last=non-dereferenceable iterator for std::vector, 
    __result=non-dereferenceable iterator for std::vector) at /usr/include/c++/12/bits/stl_algobase.h:652
#6  0x000055a8f7a02232 in std::vector<int, std::allocator<int> >::_M_erase (this=0x55a8f87e22d0, __position=non-dereferenceable iterator for std::vector)
    at /usr/include/c++/12/bits/vector.tcc:179
#7  0x000055a8f7a01c0c in std::vector<int, std::allocator<int> >::erase (this=0x55a8f87e22d0, __position=non-dereferenceable iterator for std::vector)
    at /usr/include/c++/12/bits/stl_vector.h:1530

#2#3#4#5也没什么好看的,都是firstlastresult传来传去,名字看起来也类似。

直到#6

在这里插入图片描述
这个模板函数_M_erase(iterator __position)只接收一个__position参数,调用了_GLIBCXX_MOVE3(__position + 1, end(), __position);

比对#5#6,发现:

  1. __position+1就是first
  2. end()就是last
  3. __position就是result

那么,end()(也就是last)为空当然是很合理的事。
至于firstresult的值,我们要再进一步分析它们的作用。

在这里插入图片描述

#6  0x000055a8f7a02232 in std::vector<int, std::allocator<int> >::_M_erase (this=0x55a8f87e22d0, __position=non-dereferenceable iterator for std::vector)
    at /usr/include/c++/12/bits/vector.tcc:179
#7  0x000055a8f7a01c0c in std::vector<int, std::allocator<int> >::erase (this=0x55a8f87e22d0, __position=non-dereferenceable iterator for std::vector)
    at /usr/include/c++/12/bits/stl_vector.h:1530

__position就是#7erase()中传入的迭代器值。

把这个_M_erase()的代码丢给GPT,它会快速帮你分析出参数的作用:

GLIBCCXX_MOVE3是通过将[__position+1, end()]这个区间的元素都移动到__position地址处,从而实现了“删除”效果。

那么就很明确为什么崩溃了,因为传入的__position为空,__position+1也就是0x4地址是一个无法保障的地址,自然就会产生段错误了。

四、吸取教训

迭代器和指针一定要判空。

其实这个判空前面是一直留了个心眼的,偷懒没加,隔太久忘了。

相关推荐

  1. C#

    2024-04-13 06:46:06       51 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-13 06:46:06       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-13 06:46:06       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-13 06:46:06       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-13 06:46:06       20 阅读

热门阅读

  1. Fiddler的安装和使用

    2024-04-13 06:46:06       17 阅读
  2. 如何快速打开Github

    2024-04-13 06:46:06       16 阅读
  3. 【Android】【root & remount】adb su如何添加密码校验

    2024-04-13 06:46:06       19 阅读
  4. python连接mysql数据库通用类

    2024-04-13 06:46:06       17 阅读
  5. 小程序上拉触底节流处理

    2024-04-13 06:46:06       15 阅读
  6. 常用的限流算法原理与实现

    2024-04-13 06:46:06       55 阅读
  7. 【蓝桥杯】十六进制转八进制 C++实现

    2024-04-13 06:46:06       18 阅读
  8. 数据结构 -- 二叉树

    2024-04-13 06:46:06       15 阅读
  9. oracle表误删恢复

    2024-04-13 06:46:06       39 阅读