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
…
}
上述代码的汇编代码为:
…
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
…
在核心转储中,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
…
对于上述GDB分析,我们知道memset()
的地址已在重定位PLT表中得到解决。也就是说,第一个jmpq *0x771b92(%rip)
将直接跳转到函数memset()
的第一条指令。此外,该程序已在线运行了近一天,memset()
的重定位地址应该已经提前解决。
2。怪异现象
此核心触发=> 0x0000003da0a7b006 <+854>: mov %rdx,-0x8(%rdi)
中的memset()
指令。实际上,这是memset()
中将0
设置在缓冲区右侧开始位置的指令,该缓冲区是memset()
的第一个参数。
核心时,在第0帧中,$rdi
的值为0x2c32a670
,$rax
为0x2c32a668
。从汇编分析和离线测试,$rax
应保留memset
的源缓冲区,即memset()
的第一个参数。
因此,在我们的示例中,$rax
应与m_ppEntry
的地址相同,其值存储在this
对象中(this
指针存储%rbx
之前的memset
中的m_ppEntry
之前的0x2ab02c32a668
。但是,info files
的值为0x2c32a668
。
然后使用0x2ab02c32a668
GDB命令进行检查,地址memset
确实无效(未映射),地址m_ppEntry
是有效地址。
第3。为什么这很奇怪?
这个核心的奇怪之处在于:如果memset
的实际地址已经解决(非常非常可能),那么在将指针值放入{{}的操作之间只有很少的指令。 1}}并尝试$rax
它。实际上,寄存器m_ppEntry
(保持传递的缓冲区地址)的值在这些指令期间根本不会改变。那么,$rax
如何不等于$rax
?
奇怪的是,更多是:当核心时,0x2c32a668
(m_ppEntry
)的值实际上是0x2ab02c32a668
的低4字节的值( m_ppEntry
)。如果两个值之间确实存在某种关系,那么传递给memset
的{{1}}参数是否会被截断?但是,所涉及的几条指令都使用%rax
,而不是%eax
。顺便说一下,我无法离线重现此问题。
所以,
1)哪个地址有效?如果0x2c32a668
有效?堆只是在几条指令之间损坏了吗?以及如何解释m_ppEntry
的值是0x2ab02c32a668
,以及为什么这两个值的低4字节是相同的?
2)如果0x2ab02c32a668
有效,为什么地址在传入64位memset()
时会被截断?在哪种情况下会发生此错误?我无法离线重现这个。这个问题是一个已知的错误吗?我没有通过谷歌找到它。
3)或者,是否由于某些硬件或电源问题导致传递给%rdi
的{{1}}的4个更高字节归零? (我非常不愿意相信这一点)。
最后,对此核心的任何评论都表示赞赏。
谢谢,
Gary Hu
答案 0 :(得分:1)
我假设大部分时间这段代码工作正常,因为你提到有一天正在运行。 我同意信号值得检查,看起来确实像指针截断正在其他地方发生。
我认为只有其他事情可能是新问题。有没有可能有时你最终会调用一个重载的新运算符?
同样为了完整性,m_ppEntry的声明是什么?
我假设你正在使用no throw new,否则assert(m_ppEntry != NULL);
将毫无意义。