今月のバグ:キャッシュフローの問題でSamsungのスマホアプリがクラッシュ

Table of Contents

今月のバグ:キャッシュフローの問題でSamsungのスマホアプリがクラッシュ

サムスンにとって、今年の夏は芳しいものではなかった。Galaxy Note 7スマートフォンに爆発性のバッテリーが同梱されていたため、世界中でリコールが発生した。

そして、Note 7、Galaxy S7、S7 Edgeに搭載されている高性能なExynos 8890プロセッサは、一見奇妙なクラッシュを引き起こし、アプリの動作を阻害しています。ヌルポインタから不正命令例外まで、あらゆるクラッシュがランダムに発生し、エンジニアを何ヶ月も悩ませてきました。

そして今、彼らはついに事件を解決した。

ソフトウェア開発ツールキット「Mono」で作成されたアプリは、コード自体は問題ないにもかかわらず、Samsungの最新Android端末上で不正命令エラーにより無差別にクラッシュしていました。ゲームキューブ・Wiiエミュレータ「Dolphin」やPSPエミュレータ「PPSSPP」も同様にクラッシュしていました。

これらの不具合は、オンデマンドでジャストインタイム(JIT)生成されるコードに関連していました。MonoはJITコンパイラを使用して、アプリのポータブルバイトコードをハンドヘルドデバイス上のネイティブARM命令に変換します。DolphinとPPSSPPも同様の方法で、ゲームのPowerPCまたはMIPS実行ファイルを基盤CPU上で実行します。自己書き換えコードを含むプログラムは、Exynos 8890上で動作しなくなる危険性があるようです。

ARM アーキテクチャでは、命令キャッシュとデータ キャッシュが分割されているため、JIT エンジンはプロセッサの命令キャッシュをクリアして、新しく生成された命令が確実にロードされ実行されるようにする必要があります。

Mono のエンジニアは、I キャッシュから 128 バイトのブロックをフラッシュするときに 64 バイトしかクリアされず、プロセッサ コアが古くて一致しないコードを実行し、実行中のアプリケーションがクラッシュする可能性があることに気付きました。

Exynos 8890システムオンチップは8つのコアを搭載しています。ARM設計のCortex-A53コアが4つと、Samsung設計のM1コアが4つです。これらはARMのbig.LITTLE方式で配置されています。つまり、一時的に高い処理能力が必要な場合に4つの強力なM1コアを、通常の作業には4つの軽量なA53コアを使用します。アプリ内のスレッドは、必要な処理量に応じて、bigコアまたはLITTLEコア間を移動します。

A53の命令キャッシュライン幅は64バイトで、キャッシュは64バイトのブロック単位でフラッシュ・置換されます。一方、M1の命令キャッシュラインは128バイトです。これは問題です。

おっと、その場しのぎの…先月のサムスンのチップ設計者への滑り

GCC を使用して構築されたアプリ (少なくとも Mono の場合) は、次の疑似コードのような関数を使用して、コアの命令キャッシュをフラッシュします。

void __clear_cache (char *アドレス、size_t サイズ)
{ 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);
}

__clear_cacheアプリケーションが初めてを使用する際、CPUコアのキャッシュライン幅をプロセッサから直接読み取り、 に格納しますcache_line_size。次に、キャッシュをフラッシュする際に、クリアする必要があるメモリをループし、プロセッサに命令キャッシュを1キャッシュラインずつダンプするように指示します。

つまり、アプリがA53で起動する場合、64バイトブロックで命令キャッシュをクリアし、64バイト単位でループ処理を実行することになります。M1で起動する場合は、128バイトブロックを使用します。

さて、M1で動作していたアプリをA53に移行すると、命令キャッシュを128バイトブロック単位でクリアすることになります。しかし実際には、より小さなコアは最初の64バイトのみをクリアし、__clear_cache残りは次の128バイトブロックにスキップします。その結果、キャッシュ内に古いコードが残り、プログラムが混乱してクラッシュすることになります。

Mono、Dolphin、PPSSPP は、この問題を回避するためにコードにパッチを適用しました。

LLVMコンパイラとGoogleのV8 JavaScriptエンジンでビルドされたソフトウェアは、GCC生成コードほど深刻な影響を受けません。これは、各インクリメントの直前にCPUの命令キャッシュ幅を要求するためです。Monoも同様の処理を行っています。デバイス内で最小の命令キャッシュ幅を算出し、それをそのまま使用します。

残念ながら、CPUからIキャッシュ幅を取得して次のラインをフラッシュする処理はアトミックではないため、ユーザー空間から完全に解決するのは困難です。ループ中にスレッドが異なるキャッシュライン幅を持つコアに再スケジュールされ、メモリがスキップされる可能性があります。Monoのアプローチは、少なくともこの問題に対して耐性があります。

適切な解決策の 1 つは、オペレーティング システム カーネルにパッチを適用して、CPU の I キャッシュ ライン幅の読み取りで常にハードウェアの最小サイズが返されるようにし、無効な命令を削除するときにバイトが残らないようにすることです。

一方で、これは厳密にはサムスンのせいではありません。初期のbig.LITTLEコアであるCortex-A15の技術リファレンスマニュアルには、次のように記されています。

Cortex-A15プロセッサのL1キャッシュは64バイトのラインで構成されています。ただし、他のプロセッサでは、Cortex-A15プロセッサとは異なるキャッシュライン長をサポートするキャッシュを搭載している場合があります。

つまり、ソフトウェアエンジニアは、キャッシュライン幅がデバイスごとに異なる可能性があることに注意する必要があるということです。しかし、SamsungのExynos 8890に搭載されているCortex-A53の技術マニュアルには、他のコアのキャッシュライン幅が異なることについては一切触れられていません。ARMシステムオンチップの世界では、上記の問題を回避するために、パッケージ内ではIキャッシュラインの幅を一定に保つのが慣例となっています。ARMはCortex-Aの設計では確かにそうしています(まあ、A7とA15ではそうではありませんでしたが、それ以降はそうでした)。

つまり、サムスンはもっとよく知っているべきだった、あるいは少なくとももう少し警告を与えるべきだった、という結論になるだろう。®

Discover More