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 <[email protected]>
0x0000000000502b3e <+6>: pushq $0x53
0x0000000000502b43 <+11>: jmpq 0x5025f8
End of assembler dump.
(gdb) x/ag 0x0000000000502b3e+0x771b92
0xc746d0 <[email protected]>: 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
GDB命令进行检查,地址0x2c32a668
确实无效(未映射),并且地址0x2ab02c32a668
是有效地址。3.为什么很奇怪?
该内核的怪异之处在于:如果
memset
的真实地址已经被解析(非常有可能),那么在将指针值放入m_ppEntry
的操作与尝试对其进行memset
的操作之间只有很少的指令。实际上,在这些指令期间,寄存器$rax
的值(保存传递的缓冲区地址)根本没有改变。因此,m_ppEntry
如何不等于$rax
呢?更奇怪的是:当为核心时,
$rax
(0x2c32a668
)的值实际上是m_ppEntry
(0x2ab02c32a668
)的低4个字节的值。如果两个值之间确实存在某种关系,传递给m_ppEntry
的memset
参数是否被截断?但是,涉及的几条指令全部使用%rax
而不是%eax
。顺便说一句,我无法离线重现此问题。所以,
1)哪个地址有效?如果
0x2c32a668
有效吗?堆仅在几条指令之间被破坏了吗?以及如何解释m_ppEntry
的值为0x2ab02c32a668
,为什么这两个值的低4个字节相同?2)如果
0x2ab02c32a668
有效,为什么将地址传递到64位memset()
中时地址被截断?在哪种情况下会发生此错误?我无法离线复制此内容。这个问题是已知错误吗?我没有通过Google找到它。3)或者,是否由于某些硬件或电源问题将传递给
%rdi
的memset
的4个高字节清零? (我非常不愿意相信这一点)。最后,感谢您对此核心发表任何评论。
谢谢,
胡
最佳答案
考虑到您提到一天的运行时间,我假设大多数情况下此代码都能正常工作。
我同意信号值得检查,它看起来确实可疑,就像指针截断在其他地方发生一样。
我认为这只是新问题。是否有可能有时您最终会调用超载的新运算符?
为了完整性,m_ppEntry的声明是什么?
我假设您使用的是no throw new,否则assert(m_ppEntry != NULL);
将毫无意义。
关于linux - x64 memset核心,传递的缓冲区地址被截断了吗?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/13724895/