use*_*089 27 linux debugging assembly gcc gdb
1.问题背景
最近在我们的一个在线搜索服务器上发生了核心转储.memset()由于尝试写入无效地址,核心发生,因此接收到SIGSEGV信号.以下信息来自dmsg:
is_searcher_ser[17405]: segfault at 000000002c32a668 rip 0000003da0a7b006 rsp 0000000053abc790 error 6
我们的在线服务器的环境如下:
以下是相关的代码段:
CHashMap<…>::CHashMap(…)
{
…
typedef HashEntry *HashEntryPtr;
m_ppEntry = new HashEntryPtr[m_nHashSize]; // m_nHashSize is 389 when core
assert(m_ppEntry != NULL);
memset(m_ppEntry, 0x0, m_nHashSize*sizeof(HashEntryPtr)); // Core in this memset() invocation
…
}
Run Code Online (Sandbox Code Playgroud)
上面代码的汇编代码是:
…
0x000000000091fe9e <+110>: callq 0x502638 <_Znam@plt> // new HashEntryPtr[m_nHashSize]
0x000000000091fea3 <+115>: mov 0xc(%rbx),%edx // Get the value of m_nHashSize
0x000000000091fea6 <+118>: mov %rax,%rdi // Put m_ppEntry pointer to %rdi for later memset invocation
0x000000000091fea9 <+121>: mov %rax,0x20(%rbx) // Store the pointer to m_ppEntry member variable(%rbx holds the this pointer)
0x000000000091fead <+125>: xor %esi,%esi // Generate 0
0x000000000091feaf <+127>: shl $0x3,%rdx // m_nHashSize*sizeof(HashEntryPtr)
0x000000000091feb3 <+131>: callq 0x502b38 <memset@plt> // Call the memset() function
…
Run Code Online (Sandbox Code Playgroud)
在核心转储中,程序集memset@plt是:
(gdb) disassemble 0x502b38
Dump of assembler code for function memset@plt:
0x0000000000502b38 <+0>: jmpq *0x771b92(%rip) # 0xc746d0 <memset@got.plt>
0x0000000000502b3e <+6>: pushq $0x53
0x0000000000502b43 <+11>: jmpq 0x5025f8
End of assembler dump.
(gdb) x/ag 0x0000000000502b3e+0x771b92
0xc746d0 <memset@got.plt>: 0x3da0a7acb0 <memset>
(gdb) disassemble 0x3da0a7acb0
Dump of assembler code for function memset:
0x0000003da0a7acb0 <+0>: cmp $0x1,%rdx
0x0000003da0a7acb4 <+4>: mov %rdi,%rax
…
Run Code Online (Sandbox Code Playgroud)
对于上面的GDB分析,我们知道memset()在重定位PLT表中解析了地址.也就是说,第一个jmpq *0x771b92(%rip)将直接跳转到第一个函数指令memset().此外,该程序已经运行了近一天,重新安置地址memset()应该已经提前解决了.
2.奇怪的现象
这个核心射向指令=> 0x0000003da0a7b006 <+854>: mov %rdx,-0x8(%rdi)中memset().实际上这是memset()设置0缓冲区右边开始位置的指令,它是第一个参数memset().
当芯,在帧0中,值的$rdiIS 0x2c32a670,和$rax是0x2c32a668.从汇编分析和离线测试,$rax应该保持源缓冲区memset,即第一个参数memset().
因此,在我们的示例中,$rax应该与地址相同m_ppEntry,其值在稍后归零之前首先存储在this对象中(this指针存储在其中%rbx)memset.但是,价值m_ppEntry是0x2ab02c32a668.
然后使用info filesGDB命令检查,地址0x2c32a668确实无效(未映射),并且地址0x2ab02c32a668是有效地址.
3.为什么这很奇怪?
这个核心的奇怪之处在于:如果memset已经解决了真实地址(非常非常可能),那么在将指针值放入的操作m_ppEntry和memset对它的尝试之间只有很少的指令.实际上,$rax在这些指令期间,寄存器(保持传递的缓冲区地址)的值根本不会改变.那么,怎么能m_ppEntry不等于$rax?
什么是奇怪的更多的是:当核心价值$rax(0x2c32a668)实际上是低4个字节的值m_ppEntry(0x2ab02c32a668).如果两个值之间确实存在某种关系,那么m_ppEntry参数是否memset被截断?但是,所涉及的几个指令都是使用的%rax,而不是%eax.顺便说一下,我无法离线重现此问题.
所以,
1)哪个地址有效?如果0x2c32a668有效?堆只是在几条指令之间损坏了吗?以及如何解释它的值m_ppEntry是什么0x2ab02c32a668,以及为什么这两个值的低4字节是相同的?
2)如果0x2ab02c32a668有效,为什么地址在传入64位时会被截断memset()?在哪种情况下会发生此错误?我无法离线重现这个.这个问题是一个已知的错误吗?我没有通过谷歌找到它.
3)或者,是否由于某些硬件或电源问题使4个更高的字节%rdi传递给memset归零?(我非常不愿意相信这一点).
最后,对此核心的任何评论都表示赞赏.
谢谢,
加里胡
鉴于您提到一天的运行情况,我假设大多数情况下这段代码都能正常工作。我同意信号值得检查,它看起来确实可疑,就像指针截断发生在其他地方。
我认为唯一的另一件事可能是新的问题。有时您是否有可能最终调用一个重载的 new 运算符?另外为了完整性, m_ppEntry 的声明是什么?我假设你使用的是 no throw new 否则就assert(m_ppEntry != NULL);没有意义了。