[この記事は Dmitry Malykhanov、デベロッパー アドボケートによる Android Developers Blog の記事 "Android changes for NDK developers" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。]
他の Android プラットフォームの改善と合わせて、Android M と N のダイナミック リンカーのコード記述要件が厳しくなりました。これは、クロスプラットフォームでコンパイル可能なきれいなネイティブ コードをロードできるようにするためです。最新の Android リリースにスムーズに移行できるように、このルールや推奨事項に従ってアプリケーションのネイティブ コードを記述することが必要になります。
個別の変更点については、以下で詳しく説明します。いずれも、ネイティブ コードのロードに関する問題を回避した結果、あるいは問題を回避するステップに伴う変更です。
必須ツール: NDK の各アーキテクチャには、toolchains/ の下に <arch>-linux-android-readelf バイナリ(たとえば、arm-linux-androideabi-readelf や i686-linux-android-readelf)というファイルがありますが、ここで使用している基本的な検査だけであれば、readelf はどのアーキテクチャに対しても使用できます。Linux では、readelf を使うには「binutils」パッケージ、scanelf を使うには「pax-utils」パッケージのインストールが必要です。
アプリをアップデートしても、ユーザーは一貫した操作性を提供されるべきです。また、プラットフォームの変更に対応するために、デベロッパーが緊急アップデートを行なわなければならないのは適切ではありません。その理由から、プライベートな C/C++ シンボルの利用を控えることをおすすめします。プライベート シンボルのテストは、すべての Android 端末が合格する必要のある互換性テストスイート(CTS)に含まれていません。プライベート シンボルが存在しない可能性や、動作が異なる可能性があります。そのため、プライベート シンボルを使用するアプリは、特定の端末や将来のリリースで動作しなくなる可能性が高くなります。Android 6.0 Marshmallow で OpenSSL が BoringSSL に切り替えられた際に、多くのデベロッパーがこれを経験しました。
この変更によるユーザーへの影響を軽減するために、Google Play でインストール率の高いアプリで多く使われており、しばらくの間サポートが可能ないくつかのライブラリを特定しました。これには、libandroid_runtime.so、libcutils.so、libcrypto.so、libssl.so などが含まれています。また、移行に当てられる時間を長くとれるように、これらのライブラリは一時的にサポートいたします。ただし、将来のリリースでコードが動作しなくなるという警告を見かけたら、すぐに修正をしていただくようお願いいたします。
発生する恐れのある問題: API 24 以降、ダイナミック リンカーはプライベート ライブラリをロードしないため、アプリケーションがロードできなくなります。
解決策: パブリック API のみを使うようにネイティブ コードを書き直します。短期的な回避策として、複雑な依存性のないプラットフォーム ライブラリ(libcutils.so)をプロジェクトにコピーすることができます。長期的な解決策としては、関連するコードをプロジェクト ツリーにコピーする必要があります。SSL / Media / JNI 内部 / バインダ API には、ネイティブ コードからアクセスするべきではありません。必要な場合は、ネイティブ コードから適切なパブリック Java API のメソッドを呼び出します。
パブリック ライブラリの完全なリストは、NDK 内で参照できます。
注: SSL/crypto は特殊なケースです。アプリケーションは、プラットフォームの libcrypto および libssl ライブラリを直接使用してはいけません。これは古いプラットフォームにも該当します。既知の脆弱性から保護されるよう、すべてのアプリケーションで GMS セキュリティ プロバイダを使用してください。
解決策: ビルド時にセクション ヘッダーを取り除くステップを行わないようにします。
テキストの再配置を行う一般的な理由は、位置に依存した手書きのアセンブラの存在です。これは一般的ではありません。さらに詳しい診断を行うには、ドキュメントに記載されている scanelf ツールを使用してください。
scanelf ツールを利用できない場合は、代わりに readelf を使用して基本的なチェックを行うことも可能です。その場合、TEXTREL エントリか TEXTREL フラグを探します。どちらかが見つかれば十分です(TEXTREL エントリに対応する値は関係なく、通常は 0 です。TEXTREL エントリが存在するだけで、.so にテキストの再配置が含まれていることがわかります)。次の例では、両方が存在しています。
注: 技術的には、共有オブジェクトに TEXTREL エントリ / フラグがあっても、実際のテキストの再配置は含まれていない場合も考えられます。これは NDK では発生しませんが、Android のダイナミック リンカーはこのエントリ / フラグに基づいて判断しているため、独自に ELF ファイルを生成している場合は、テキストの再配置を含むことを宣言している ELF ファイルを生成していないことを確認してください。
発生する恐れのある問題: 再配置は、強制的にコードページを書き込み可能にし、メモリ内のダーティー ページ数を無駄に増加させます。Android K(API 19)以降、ダイナミック リンカーはテキストの再配置について警告してきましたが、API 23 以降では、テキストの再配置を含むコードをロードできなくなります。
解決策: アセンブラを位置に依存しないように書き直し、テキストの再配置が不要になるようにします。詳しい説明は、Gentoo のドキュメントをご覧ください。
API 23 より前は、Android のダイナミック リンカーが必要なライブラリを探す際に、フルパスを無視してベース名(最後の「/」以降)だけを使用していました。API 23 以降では、実行時リンカーが DT_NEEDED を正確に解釈するため、端末上の正確な場所にライブラリが存在しない場合、ライブラリをロードできません。
さらに悪いことに、ビルドホスト上のファイルを指す DT_NEEDED エントリを挿入するバグがあるビルドシステムも存在します。その場合、端末上でライブラリを見つけることはできません。
発生する恐れのある問題: API 23 より前では、DT_NEEDED エントリのベース名が使われました。API 23 以降では、Android ランタイムは指定されたパスを使用してライブラリをロードしようとしますので、パスが端末上に見つかりません。SONAME ではなく、ビルド ホスト上のパスを使用するという問題があるサードパーティ製のツールチェーンやビルドシステムも存在します。
解決策: 必要なすべてのライブラリが SONAME だけによって参照されていることを確認してください。端末によって場所が異なる可能性があるため、ライブラリは実行時リンカーに探させてロードさせるようにします。
発生する恐れのある問題: 名前空間の衝突により、実行時に誤ったライブラリがロードされる可能性があります。その場合、必要なシンボルが見つからない、または利用しようとしているライブラリとは違う ABI 非互換のライブラリが使われるため、クラッシュが発生します。
解決策: 最新の NDK は、デフォルトで正しい SONAME を生成します。最新の NDK を利用していることと、(
最新の NDK で正しくビルドしたクロスプラットフォームのコードは、Android N で問題なく動作します。正しいバイナリを生成できるように、ネイティブ コード ビルドを見直してみることをおすすめします。
Posted by Yuichi Araki - Developer Relations Team
他の Android プラットフォームの改善と合わせて、Android M と N のダイナミック リンカーのコード記述要件が厳しくなりました。これは、クロスプラットフォームでコンパイル可能なきれいなネイティブ コードをロードできるようにするためです。最新の Android リリースにスムーズに移行できるように、このルールや推奨事項に従ってアプリケーションのネイティブ コードを記述することが必要になります。
個別の変更点については、以下で詳しく説明します。いずれも、ネイティブ コードのロードに関する問題を回避した結果、あるいは問題を回避するステップに伴う変更です。
必須ツール: NDK の各アーキテクチャには、toolchains/ の下に <arch>-linux-android-readelf バイナリ(たとえば、arm-linux-androideabi-readelf や i686-linux-android-readelf)というファイルがありますが、ここで使用している基本的な検査だけであれば、readelf はどのアーキテクチャに対しても使用できます。Linux では、readelf を使うには「binutils」パッケージ、scanelf を使うには「pax-utils」パッケージのインストールが必要です。
プライベート API(API 24 以降で強制)
ネイティブ ライブラリは、パブリック APIのみを使用しなければならず、NDK 以外のプラットフォーム ライブラリにリンクしてはいけません。このルールは API 24 以降で強制となり、アプリケーションは NDK 以外のプラットフォーム ライブラリをロードできなくなります。このルールを強制するのはダイナミック リンカーなので、コードがどのような手段でロードしようとしても、パブリックでないライブラリにアクセスすることはできません。System.loadLibrary(...)、DT_NEEDED エントリ、dlopen(...) の直接呼び出しは、いずれも同じように失敗します。アプリをアップデートしても、ユーザーは一貫した操作性を提供されるべきです。また、プラットフォームの変更に対応するために、デベロッパーが緊急アップデートを行なわなければならないのは適切ではありません。その理由から、プライベートな C/C++ シンボルの利用を控えることをおすすめします。プライベート シンボルのテストは、すべての Android 端末が合格する必要のある互換性テストスイート(CTS)に含まれていません。プライベート シンボルが存在しない可能性や、動作が異なる可能性があります。そのため、プライベート シンボルを使用するアプリは、特定の端末や将来のリリースで動作しなくなる可能性が高くなります。Android 6.0 Marshmallow で OpenSSL が BoringSSL に切り替えられた際に、多くのデベロッパーがこれを経験しました。
この変更によるユーザーへの影響を軽減するために、Google Play でインストール率の高いアプリで多く使われており、しばらくの間サポートが可能ないくつかのライブラリを特定しました。これには、libandroid_runtime.so、libcutils.so、libcrypto.so、libssl.so などが含まれています。また、移行に当てられる時間を長くとれるように、これらのライブラリは一時的にサポートいたします。ただし、将来のリリースでコードが動作しなくなるという警告を見かけたら、すぐに修正をしていただくようお願いいたします。
$ readelf --dynamic libBroken.so | grep NEEDED
0x00000001 (NEEDED) Shared library: [libnativehelper.so]
0x00000001 (NEEDED) Shared library: [libutils.so]
0x00000001 (NEEDED) Shared library: [libstagefright_foundation.so]
0x00000001 (NEEDED) Shared library: [libmedia_jni.so]
0x00000001 (NEEDED) Shared library: [liblog.so]
0x00000001 (NEEDED) Shared library: [libdl.so]
0x00000001 (NEEDED) Shared library: [libz.so]
0x00000001 (NEEDED) Shared library: [libstdc++.so]
0x00000001 (NEEDED) Shared library: [libm.so]
0x00000001 (NEEDED) Shared library: [libc.so]
発生する恐れのある問題: API 24 以降、ダイナミック リンカーはプライベート ライブラリをロードしないため、アプリケーションがロードできなくなります。
解決策: パブリック API のみを使うようにネイティブ コードを書き直します。短期的な回避策として、複雑な依存性のないプラットフォーム ライブラリ(libcutils.so)をプロジェクトにコピーすることができます。長期的な解決策としては、関連するコードをプロジェクト ツリーにコピーする必要があります。SSL / Media / JNI 内部 / バインダ API には、ネイティブ コードからアクセスするべきではありません。必要な場合は、ネイティブ コードから適切なパブリック Java API のメソッドを呼び出します。
パブリック ライブラリの完全なリストは、NDK 内で参照できます。
platforms/android-API/usr/lib
以下をご覧ください。 注: SSL/crypto は特殊なケースです。アプリケーションは、プラットフォームの libcrypto および libssl ライブラリを直接使用してはいけません。これは古いプラットフォームにも該当します。既知の脆弱性から保護されるよう、すべてのアプリケーションで GMS セキュリティ プロバイダを使用してください。
セクション ヘッダーの欠落(API 24 以降で強制)
各 ELF ファイルのセクション ヘッダーには、追加情報が含まれています。ダイナミック リンカーがサニティ チェックを行う際にこのヘッダーを使うため、ヘッダーの存在が必須となります。バイナリを難読化してリバース エンジニアリングを防ぐために、このヘッダーを取り除こうとするデベロッパーもいます(取り除いた情報は広く入手できるツールを使って再構築できるため、この方法は実際にはあまり役に立ちません)。$ readelf --header libBroken.so | grep 'section headers'
Start of section headers: 0 (bytes into file)
Size of section headers: 0 (bytes)
Number of section headers: 0
$
解決策: ビルド時にセクション ヘッダーを取り除くステップを行わないようにします。
テキストの再配置(API 23 以降で強制)
API 23 以降、共有オブジェクトにテキストの再配置を含めることはできません。つまり、コードはそのままロードし、変更せずに使用する必要があります。このようなアプローチによって、ロード時間が短くなり、セキュリティも向上します。テキストの再配置を行う一般的な理由は、位置に依存した手書きのアセンブラの存在です。これは一般的ではありません。さらに詳しい診断を行うには、ドキュメントに記載されている scanelf ツールを使用してください。
$ scanelf -qT libTextRel.so
libTextRel.so: (memory/data?) [0x15E0E2] in (optimized out: previous simd_broken_op1) [0x15E0E0]
libTextRel.so: (memory/data?) [0x15E3B2] in (optimized out: previous simd_broken_op2) [0x15E3B0]
[skipped the rest]
scanelf ツールを利用できない場合は、代わりに readelf を使用して基本的なチェックを行うことも可能です。その場合、TEXTREL エントリか TEXTREL フラグを探します。どちらかが見つかれば十分です(TEXTREL エントリに対応する値は関係なく、通常は 0 です。TEXTREL エントリが存在するだけで、.so にテキストの再配置が含まれていることがわかります)。次の例では、両方が存在しています。
$ readelf --dynamic libTextRel.so | grep TEXTREL
0x00000016 (TEXTREL) 0x0
0x0000001e (FLAGS) SYMBOLIC TEXTREL BIND_NOW
$
注: 技術的には、共有オブジェクトに TEXTREL エントリ / フラグがあっても、実際のテキストの再配置は含まれていない場合も考えられます。これは NDK では発生しませんが、Android のダイナミック リンカーはこのエントリ / フラグに基づいて判断しているため、独自に ELF ファイルを生成している場合は、テキストの再配置を含むことを宣言している ELF ファイルを生成していないことを確認してください。
発生する恐れのある問題: 再配置は、強制的にコードページを書き込み可能にし、メモリ内のダーティー ページ数を無駄に増加させます。Android K(API 19)以降、ダイナミック リンカーはテキストの再配置について警告してきましたが、API 23 以降では、テキストの再配置を含むコードをロードできなくなります。
解決策: アセンブラを位置に依存しないように書き直し、テキストの再配置が不要になるようにします。詳しい説明は、Gentoo のドキュメントをご覧ください。
無効な DT_NEEDED エントリ(API 23 以降で強制)
ライブラリの依存性(ELF ヘッダーの DT_NEEDED エントリ)は絶対パスでも構いませんが、ライブラリがシステムのどこにインストールされるかを制御できない Android では、絶対パスは意味をなしません。DT_NEEDED エントリは必要となるライブラリの SONAME と同じである必要があり、実行時にライブラリを探す作業はダイナミック リンカーに委ねられます。API 23 より前は、Android のダイナミック リンカーが必要なライブラリを探す際に、フルパスを無視してベース名(最後の「/」以降)だけを使用していました。API 23 以降では、実行時リンカーが DT_NEEDED を正確に解釈するため、端末上の正確な場所にライブラリが存在しない場合、ライブラリをロードできません。
さらに悪いことに、ビルドホスト上のファイルを指す DT_NEEDED エントリを挿入するバグがあるビルドシステムも存在します。その場合、端末上でライブラリを見つけることはできません。
$ readelf --dynamic libSample.so | grep NEEDED
0x00000001 (NEEDED) Shared library: [libm.so]
0x00000001 (NEEDED) Shared library: [libc.so]
0x00000001 (NEEDED) Shared library: [libdl.so]
0x00000001 (NEEDED) Shared library:
[C:\Users\build\Android\ci\jni\libBroken.so]
$
発生する恐れのある問題: API 23 より前では、DT_NEEDED エントリのベース名が使われました。API 23 以降では、Android ランタイムは指定されたパスを使用してライブラリをロードしようとしますので、パスが端末上に見つかりません。SONAME ではなく、ビルド ホスト上のパスを使用するという問題があるサードパーティ製のツールチェーンやビルドシステムも存在します。
解決策: 必要なすべてのライブラリが SONAME だけによって参照されていることを確認してください。端末によって場所が異なる可能性があるため、ライブラリは実行時リンカーに探させてロードさせるようにします。
SONAME の欠落(API 23 以降で使用)
すべての ELF 共有オブジェクト(「ネイティブ ライブラリ」)に、SONAME(共有オブジェクト名)属性が必要です。デフォルトで NDK ツールチェーンはこの属性を追加します。そのため、この属性が存在しないということは、誤って別のツールチェーンを設定しているか、ビルドシステムの設定が誤っているかのどちらかです。SONAME がない場合、代わりにファイル名が利用されるので、実行時に誤ったライブラリがロードされるなどの問題が発生する可能性があります。$ readelf --dynamic libWithSoName.so | grep SONAME
0x0000000e (SONAME) Library soname: [libWithSoName.so]
$
発生する恐れのある問題: 名前空間の衝突により、実行時に誤ったライブラリがロードされる可能性があります。その場合、必要なシンボルが見つからない、または利用しようとしているライブラリとは違う ABI 非互換のライブラリが使われるため、クラッシュが発生します。
解決策: 最新の NDK は、デフォルトで正しい SONAME を生成します。最新の NDK を利用していることと、(
-soname
リンカー オプションによって)不適切な SONAME エントリを生成するようにビルドシステムを設定していないことを確認します。 最新の NDK で正しくビルドしたクロスプラットフォームのコードは、Android N で問題なく動作します。正しいバイナリを生成できるように、ネイティブ コード ビルドを見直してみることをおすすめします。
Posted by Yuichi Araki - Developer Relations Team