不可能なバグの物語:big.LITTLEとキャッシュ

誰かがマルチコアという言葉を発音するとき、私たちは無意識にSMPを意味します。 ARMまでbig.LITTLEが発表されるまで、これは最近まで正常に機能していました。 ARM big.LITTLEアーキテクチャは、 AMPアーキテクチャの最初の大量生産例であり、後で見るように、マルチコアプログラミングの複雑さの水準をさらに高めます。



不可能なバグの物語



それはすべて、ヨーロッパのサムスンの携帯電話でExynosチップセットが使用するプロセッサを搭載した携帯電話からのエラーメッセージで始まりました。 ソフトウェアを使用して作成されたアプリケーションは、完全にランダムな場所でSIGILL



でクラッシュしSIGILL



た。 何が起こっているのかを合理的に説明することはできず、有効なプロセッサー命令でフォール自体が発生しました。 これにより、命令キャッシュがクリアされていないとすぐに疑われました。



キャッシュフラッシュのすべてのJITコードを確認した後、 __clear_cache



正しく呼び出したことが確認されました。 これにより、他の仮想マシンまたはコンパイラがARM64のキャッシュをフラッシュする方法を確認し、Cortex A53仕様のタイプミス/パッチのリストを見つけました。 ARMからのこれらの問題の説明はあいまいで読みにくいため、回避策を見つけようとしました。 しかし、ここでも何もうまくいきませんでした。



それから我々は反対側に行った。 それとも、問題はシグナルハンドラにありますか? いや ユーザー空間での不器用なCPUエミュレーション? いや 壊れたlibc



実装? いいね 機器の故障? これをいくつかのデバイスで再現しました。 運が悪いのか、それともカルマ? はい!



私たちの何人かは、目の前にあるこのような驚くべきパズルで眠りに落ちることができず、アプリケーションのダンプを見続けました。 しかし、面白いことが1つありました。不良アドレスは、常にメモリダンプの3行目または4行目にありました。







これが私たちの唯一の手がかりであり、非常に理解しにくいエラーになると、事故について話すことはできません。 メモリダンプは16バイトで調整されましたが、 SIGILL



常に 0x40-0x7f



または0xc0-0xff



範囲で発生しました 。 したがって、アロケータの動作を確認しやすいようにメモリスナップショットをフォーマットしました。



 $ grep SIGILL *.log custom_01.log:E/mono (13964): SIGILL at ip=0x0000007f4f15e8d0 custom_02.log:E/mono (13088): SIGILL at ip=0x0000007f8ff76cc0 custom_03.log:E/mono (12824): SIGILL at ip=0x0000007f68e93c70 custom_04.log:E/mono (12876): SIGILL at ip=0x0000007f4b3d55f0 custom_05.log:E/mono (13008): SIGILL at ip=0x0000007f8df1e8d0 custom_06.log:E/mono (14093): SIGILL at ip=0x0000007f6c21edf0 [...]
      
      





これを使用して、最初の適切な仮説が策定されました。キャッシュフラッシュは、各128バイトブロックの最上位64バイトで常に発生しました。 低レベルのプログラミングを扱っている場合、これらの数値はすぐにキャッシュラインのサイズを思い出させます。 その瞬間から、すべてが意味をなし始めました。



以下は、 libgcc



キャッシュを libgcc



方法の擬似コードです。



 void __clear_cache (char *address, size_t size) { static int cache_line_size = 0; if (!cache_line_size) cache_line_size = get_current_cpu_cache_line_size (); for (int i = 0; i < size; i += cache_line_size) flush_cache_line (address + i); }
      
      





上記の例では、 get_current_cpu_cache_line_size



はキャッシュラインのサイズを返すプロセッサー命令であり、 get_current_cpu_cache_line_size



は指定されたアドレスのキャッシュラインをクリアします。



当時、この関数の独自の実装を使用していたため、個別に起動し、プロセッサでキャッシュラインのサイズを表示することにしました。 そして突然、128と64を印刷しました。それが実際にあったことを再確認しました。 その後、このプロセッサのディレクトリを取得しましたが、古いコア(大きな)のキャッシュラインは128バイトで、最下位(LITTLE)の64でした。



最初に__clear_cache



が128バイトの命令キャッシュラインを持つ大きなカーネルで呼び出され、その後LITTLEコアの1つで呼び出され、リセット時に他のすべてをスキップできることが判明しました。 どこも簡単です。 キャッシュを削除し、機能しました。



結論



一部のARM big.LITTLEプロセッサには、さまざまなサイズのキャッシュラインを持つコアが含まれている場合があります。 すべてのコアは対称であると想定されています。



さらに悪いことに、ARM命令セットでさえこれに対応できていません。 抜け目のない読者は、各呼び出しでキャッシュラインを計算するだけではユーザーコードでは不十分であると推測する場合があります。プロセスが一方のコアで開始し、他方で特定のキャッシュラインサイズで__clear_cache



を実行する場合があります。 したがって、すべてのコア間のグローバルな最小キャッシュラインサイズを把握する必要があります。 Mono: Pull Requestの修正を以下に示します。 修正を既に借用している他のプロジェクト: DolphinPPSSPP




All Articles