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(红帽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 <[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_ppEntrymemset参数是否被截断?但是,涉及的几条指令全部使用%rax而不是%eax。顺便说一句,我无法离线重现此问题。

    所以,

    1)哪个地址有效?如果0x2c32a668有效吗?堆仅在几条指令之间被破坏了吗?以及如何解释m_ppEntry的值为0x2ab02c32a668,为什么这两个值的低4个字节相同?

    2)如果0x2ab02c32a668有效,为什么将地址传递到64位memset()中时地址被截断?在哪种情况下会发生此错误?我无法离线复制此内容。这个问题是已知错误吗?我没有通过Google找到它。

    3)或者,是否由于某些硬件或电源问题将传递给%rdimemset的4个高字节清零? (我非常不愿意相信这一点)。

    最后,感谢您对此核心发表任何评论。

    谢谢,

    最佳答案

    考虑到您提到一天的运行时间,我假设大多数情况下此代码都能正常工作。
    我同意信号值得检查,它看起来确实可疑,就像指针截断在其他地方发生一样。

    我认为这只是新问题。是否有可能有时您最终会调用超载的新运算符?
    为了完整性,m_ppEntry的声明是什么?
    我假设您使用的是no throw new,否则assert(m_ppEntry != NULL);将毫无意义。

    关于linux - x64 memset核心,传递的缓冲区地址被截断了吗?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/13724895/

    10-14 09:13