碰到一个dangling pointer导致进程core的问题,此类问题很常见,从上图m_vRelativePosition.y
和z
的异常值可以猜测,m_targetDomArea
指向的内存被踩坏了,或是m_targetDomArea
指向的对象已被销毁,其内存可能已被重新分配。
这里不去讨论m_targetDomArea
野的原因(熟悉这块逻辑的同事已经在上图指出具体原因),只去研究导致core的直接原因是什么。
首先通过gdb查看具体是哪个signal导致的core:
最常见的signal SIGSEGV, Segmentation fault,段错误。
以及core在哪一行:
float fDist = Magnitude(m_targetDomArea->GetPosition() -vPos);
SEGV
This very popular signal is generated when a program makes an invalid memory reference. A memory reference may be invalid because the referenced page doesn’t exist (e.g., it lies in an unmapped area somewhere between the heap and the stack), the process tried to update a location in read-only memory (e.g., the program text segment or a region of mapped memory marked read-only), or the process tried to access a part of kernel memory while running in user mode (Section 2.1). In C, these events often result from dereferencing a pointer containing a bad address (e.g., an uninitialized pointer) or passing an invalid argument in a function call. The name of this signal derives from the term segmentation violation.
——引用自《The Linux Programming Interface》
段错误意味着访问了非法地址,而core位置对应的代码看似只有一次对m_targetDomArea
指针的访问,那么m_targetDomArea
是导致core的非法地址吗?
可以看到m_targetDomArea
的值为0x8fe3498
,而导致core的非法地址是0x82b,不是它。
查看源码发现Magnitude()
是内联函数,会不会是core在这个函数里?但这个函数只是对函数参数做一些简单的算术运算,并没有指针解引用之类的操作,而传入的参数又是个栈上的临时变量,地址肯定是OK的,所以不是core在这里。
只看源码已经无法断定core的具体原因,进一步查看core在汇编的哪一行:
可以看到最后执行的汇编指令确实有一次内存访问:(%rax)
,即访问寄存器rax所存储的内存地址对应的内存,将这块内存存储的值放入rax寄存器。那么猜测rax寄存器的值应该就是非法地址0x82b,验证一下:
那么为什么会访问这个地址?仔细观察后面几行汇编,发现有一处函数调用call *%rax
。
普通的函数调用应该是固定的函数地址,因为在链接期间函数的地址就都已经通过重定位确定好了,类似这样:
但这里却是动态计算出来的一个函数地址(保存在rax寄存器中),想想什么情况下函数地址是需要经过计算得出的?没错,虚函数。根据对象中的虚表指针找到虚表,在加上函数的偏移找到函数真正的地址。
那只能是GetPosition()了,查看源码发现这确实是一个虚函数:
到这里基本可以下结论了,m_targetDomArea
指向的内存被踩,或者销毁后被其他对象重新利用,导致虚表指针的值错掉了,进而在通过虚表指针计算虚函数地址的时候得出一个非法的地址0x82b
,最终访问这个地址触发段错误,进程core掉。
还可以进一步验证一下,mov (%rax),%rax
的下一行mov %rdx,%rdi
,应该是为调用GetPosition()
准备隐藏的参数this
指针(按照函数调用约定,rdi
寄存器用于传入第一个参数),即m_targetDomArea
:
可以看到用于给rdi
赋值的rdx
的值就是m_targetDomArea
。
知识点:
1. gdb中通过$_siginfo
查看导致core的具体原因以及非法地址。
2. 使用gdb指令disassemble加选项/m
可以同时对照查看C++源码及其对应的汇编指令,很方便:
3. 如果调式机器上的源码路径与可执行文件调试信息中的路径不一致,还需要使用set substitute-path from to设置替代路径,以便gdb找到正确的源码。
(gdb) set substitute-path /src/path/in/binary/ /src/path/on/machine/
4. 虚函数。
5. 各寄存器在函数调用中的用途。