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