1. 問題の背景
最近、オンライン検索サーバーの 1 つでコア ダンプが発生しました。コアはmemset()
、無効なアドレスに書き込もうとしたために発生し、SIGSEGV 信号を受信しました。次の情報は dmsg からのものです。
is_searcher_ser[17405]: segfault at 000000002c32a668 rip 0000003da0a7b006 rsp 0000000053abc790 error 6
弊社オンラインサーバーの環境は以下の通りです。
- OS:RHEL5.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 <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)
最初は function の最初の命令に直接ジャンプしますmemset()
。さらに、このプログラムはオンラインでほぼ 1 日実行されていたため、移転先の住所はmemset()
以前に解決されているはずでした。
2. 奇妙な現象
このコア=> 0x0000003da0a7b006 <+854>: mov %rdx,-0x8(%rdi)
は、memset()
. 実際には、これは の最初のパラメータである をバッファの右開始位置にmemset()
設定する命令です。0
memset()
cored の場合、フレーム 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
。2 つの値の間に何らかの関係がある場合、m_ppEntry
渡されたパラメーターはmemset
切り捨てられますか? ただし、関連するいくつかの命令はすべて%rax
ではなく を使用します%eax
。ところで、この問題をオフラインで再現することはできません。
そう、
1) 有効な住所はどれですか? 0x2c32a668
有効な場合は? いくつかの命令の間でヒープが破損していませんか? m_ppEntry
の値がであると言い換える方法と0x2ab02c32a668
、この 2 つの値の下位 4 バイトが同じなのはなぜですか?
2)0x2ab02c32a668
が有効な場合、64 ビットに渡されるときにアドレスが切り捨てられるのはなぜmemset()
ですか? このエラーはどのような状況で発生しますか? これをオフラインで再現することはできません。この問題は既知のバグですか? Googleで見つけられませんでした。
%rdi
3) または、渡された上位 4 バイトをmemset
ゼロにするのは、ハードウェアまたは電源の問題によるものですか? (私はこれを信じるのが非常に気が進まない)。
最後に、このコアに関するコメントをお待ちしております。
ありがとう、
ゲイリー・フー