Подтвердить что ты не робот

X64 memset core, передается адрес буфера усеченного?

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 <[email protected]>  // 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 <[email protected]> // Call the memset() function
…

В дампе ядра сборка [email protected]:

(gdb) disassemble 0x502b38
Dump of assembler code for function [email protected]:
    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) на самом деле является значением менее 4 байтов m_ppEntry (0x2ab02c32a668). Если действительно существует какая-то взаимосвязь между этими двумя значениями, параметр m_ppEntry, переданный в memset, усечен? Однако задействованные несколько инструкций используют %rax, а не %eax. Кстати, я не могу воспроизвести эту проблему в автономном режиме.

Итак,

1) Какой адрес действителен? Если 0x2c32a668 действительно? Является ли куча повреждена только между несколькими инструкциями? И как перефразировать, что значение m_ppEntry равно 0x2ab02c32a668, и почему низкие 4 байта этого двух значений одинаковы?

2) Если 0x2ab02c32a668 допустимо, почему адрес усечен при передаче в 64-разрядный memset()? При каких условиях эта ошибка возникнет? Я не могу воспроизвести это офлайн. Является ли эта проблема известной ошибкой? Я не нашел его через Google.

3) Или, из-за какой-то аппаратной или энергетической проблемы, чтобы 4 байта с более высоким значением %rdi передавались на memset с нулевым значением? (Im очень очень неохотно верит в это).

Наконец, оцениваются любые комментарии к этому ядру.

Спасибо,

Гэри Ху

4b9b3361

Ответ 1

Я предполагаю большую часть времени, когда этот код работает нормально, учитывая ваше упоминание о том, как работает один день. Я согласен, что сигналы стоит проверить, это выглядит подозрительно, как усечение указателя происходит где-то в другом месте.

Только я думаю, что это может быть проблема с новым. Возможно ли, что иногда вы можете позвонить перегруженному новому оператору? Также для полноты, что такое объявление m_ppEntry? Я предполагаю, что вы используете никакой бросок, иначе assert(m_ppEntry != NULL); будет бессмысленным.