一. 崩溃代码:
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 stack
、where
)
#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
注意到这里的last
和result
都是空指针,有必要验证一下它们的合法性。
看到这个复杂的模板不用怕,直接丢给GPT,不过这里没必要,确实没有什么有效信息,__builtin_memmove()
,如果你C语言基础还行的话,应该能猜到它是类似于memove()
的,不清楚也没关系。
这里看不出什么有效信息,所以我们继续看参数是怎么传递过来的,特别是last
、result
这两个空的指针。
#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
也没什么好看的,都是first
、last
、result
传来传去,名字看起来也类似。
直到#6
:
这个模板函数_M_erase(iterator __position)
只接收一个__position
参数,调用了_GLIBCXX_MOVE3(__position + 1, end(), __position);
。
比对#5
、#6
,发现:
__position+1
就是first
end()
就是last
__position
就是result
。
那么,end()
(也就是last
)为空当然是很合理的事。
至于first
和result
的值,我们要再进一步分析它们的作用。
#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
就是#7
的erase()
中传入的迭代器值。
把这个_M_erase()
的代码丢给GPT,它会快速帮你分析出参数的作用:
GLIBCCXX_MOVE3
是通过将[__position+1, end()]
这个区间的元素都移动到__position
地址处,从而实现了“删除”效果。
那么就很明确为什么崩溃了,因为传入的__position
为空,__position+1
也就是0x4
地址是一个无法保障的地址,自然就会产生段错误了。
四、吸取教训
迭代器和指针一定要判空。
其实这个判空前面是一直留了个心眼的,偷懒没加,隔太久忘了。