x64 memset核心,是否通过缓冲区地址截断?

时间:2012-12-05 13:59:09

标签: linux debugging gcc assembly gdb

1。问题背景

最近我们的一个在线搜索服务器上发生了核心转储。由于尝试写入无效地址,核心发生在memset(),因此接收到SIGSEGV信号。以下信息来自dmsg:

is_searcher_ser[17405]: segfault at 000000002c32a668 rip 0000003da0a7b006 rsp 0000000053abc790 error 6

我们的在线服务器的环境如下:

  • 操作系统:RHEL 5.3
  • 内核:2.6.18-131.el5.custom,x86_64(64位)
  • GCC:4.1.2 20080704(Red Hat 4.1.2-44)
  • Glibc:glibc-2.5-49.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$rax0x2c32a668。从汇编分析和离线测试,$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

奇怪的是,更多是:当核心时,0x2c32a668m_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

1 个答案:

答案 0 :(得分:1)

我假设大部分时间这段代码工作正常,因为你提到有一天正在运行。 我同意信号值得检查,看起来确实像指针截断正在其他地方发生。

我认为只有其他事情可能是新问题。有没有可能有时你最终会调用一个重载的新运算符? 同样为了完整性,m_ppEntry的声明是什么? 我假设你正在使用no throw new,否则assert(m_ppEntry != NULL);将毫无意义。