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 очень очень неохотно верит в это).
Наконец, оцениваются любые комментарии к этому ядру.
Спасибо,
Гэри Ху