Android NDKで使えないシステムコール・ライブラリ関数一覧
Android NDKでCのコードを書いていると、普段のCプログラミングでは悩まないことで悩むことがあります。たとえば、AndroidのlibcはGoogle製でPOSIXに準拠していません。他のUnix系環境であれば必ず実装されているライブラリ関数が存在しないなどの罠があるため、メジャーなツールをビルドするのにもconfigure;makeが素直に通らなかったりします。
それだけでなく、Android NDKが提供する開発環境にも問題があります。特に、NDKで配られているヘッダファイルとビルド用の共有ライブラリで対応が取れていないのは頭痛のタネです。どういうことかというと、ヘッダファイルに定義されているシステムコールを使おうと思ったらリンカエラーが出ることがあります。
また、システムコールの一部については、カーネルレベルでは正しく実装されているもののlibcにインターフェース実装がなく、呼びだすのに不便だったりもします。
本稿では、この混乱した状況を多少なりとも整理してみます。根性系の調査結果なので、抜け漏れに気づいた方はぜひ教えてください。
Android NDKとは
まずAndroid NDKについて軽く紹介しておきます。
Android NDK(Native Development Kit)はAndroid用のC/C++開発環境で、中身はAndroid向けのクロスコンパイラ、ヘッダファイル、ライブラリの環境一式です。x86、ARM、MIPS各アーキテクチャ用の実行バイナリや共有ライブラリを作れます。
Androidアプリの開発はJavaで記述し、OSの機能へのアクセスはAndroid SDKが提供するJavaインターフェースを呼び出すのが標準的です。しかし、ネイティブコードを共有ライブラリの形で作成し、これをJavaから呼び出すこともできます。C/C++の資産を活用したり性能を稼いだりする場合のために提供されているのがNDKというわけです。
bionic C library概要
AndroidのカーネルはLinuxがベースになっています。LinuxのノウハウがそのままAndroidで利用できるとすればNDKのメリットも大きくなります。
ところが、ここで問題になるのが冒頭で紹介したlibcの問題です。Androidでは各種Linuxディストビューションで一般的なglibcは採用されておらず、bionic C libraryと呼ばれるGoogle製のlibcが使われています。Googleがわざわざ独自のlibcを作った理由は筆者の理解では2点、性能面とライセンスの問題です。
この2点のうち、性能面はわかりやすい理由でしょう。モバイルOSの場合、サーバー機と比べれば貧弱なCPUと少ないメモリで動作する必要がありますから、オーバースペックなglibcを嫌ってシンプルなlibcを作り直すという選択も理解できます。
もう一点はライセンス上の問題です。筆者も理解度が低いのですが、スマートフォンベンダーやチップセットベンダーが一部のソフトウェア(デバイスドライバ類?)をクローズドソースにすることを許すために、ユーザー空間からGPL・LGPLのソフトウェアを排除する必要があったようです。glibcもuClibcもLGPLなので、新規実装するしか無かったというのも説得力があります。
そんなわけで、多くのソースコードをBSD系から輸入しつつ、Linux系カーネルのシステムコールを呼び、一部は完全に新規実装したキメラ的なプロダクトがbionic C libraryです。今回指摘する内容もこのような出自によるところが大きそうです。
ヘッダファイル中でコメントアウトされている関数一覧
bionic C libraryではモバイルOSとして不要な機能はバッサリ削ってあります。ざっくり言うと、アカウント管理、システム管理などに関連する関数はヘッダファイル中でコメントアウトされており、実体も存在しません。
以下はヘッダファイル中でコメントアウトしてあるシステムコール・ライブラリ関数の一覧です。
- ctermid(3) - 制御端末名の取得
- cuserid(3) - プログラムを実行しているユーザー名を取得する
- endgrent(3) - グループファイルエントリの取得
- endhostent(3)- ネットワーク上のホストのエントリを取得する
- endnetent(3) - ネットワークエントリを取得する
- endnetgrent(3) - ネットワーク・グループのエントリを操作する
- endprotoent(3) - プロトコルのエントリを取得する
- execvpe(3) - ファイルを実行する
- freehostent(3) - ネットワークホストの名前とアドレスの取得
- getdomainname(2) - NIS ドメイン名の取得・設定をする
- getgrent(3) - グループファイルエントリの取得
- getgrgid_r(3) - グループファイルエントリの取り出し
- getgrnam_r(3) - グループファイルエントリの取り出し
- gethostbyaddr_r(3) - ネットワーク上のホストのエントリを取得する
- gethostbyname2_r(3) - ネットワーク上のホストのエントリを取得する
- gethostent_r(3) - ネットワーク上のホストのエントリを取得する
- getipnodebyaddr(3) - ネットワークホストの名前とアドレスの取得
- getipnodebyname(3) - ネットワークホストの名前とアドレスの取得
- getlogin_r(3) - このセッションにログインしているユーザー名を取得する
- getnetbyaddr_r(3) - ネットワークエントリを取得する (リエントラント版)
- getnetbyname_r(3) - ネットワークエントリを取得する (リエントラント版)
- getnetent(3) - ネットワークエントリを取得する
- getnetent_r(3) - ネットワークエントリを取得する (リエントラント版)
- getnetgrent(3) - ネットワーク・グループのエントリを操作する
- getprotobyname_r(3) - プロトコル エントリを取得する (リエントラント版)
- getprotobynumber_r(3) - プロトコル エントリを取得する (リエントラント版)
- getprotoent(3) - プロトコルのエントリを取得する
- getprotoent_r(3) - プロトコル エントリを取得する (リエントラント版)
- getpwent(3) - パスワードファイルのエントリの取得
- getpwnam_r(3) - パスワードファイルのエントリの取得
- getpwuid_r(3) - パスワードファイルのエントリの取得
- getsid(2) - セッション ID を取得する
- getsubopt(3) - 文字列中のサブオプション引き数の解釈を行う
- innetgr(3) - ネットワーク・グループのエントリを操作する
- on_exit(3) - プロセスが正常に終了した際に呼ばれる関数を登録する
- pivot_root(2) - root ファイルシステムを変更する
- setdomainname(2) - NIS ドメイン名の取得・設定をする
- setfsgid(2) - ファイルシステムのチェックに用いられるグループ ID を設定する
- setfsuid(2) - ファイルシステムのチェックに用いられるユーザ ID を設定する
- setgrent(3) - グループファイルエントリの取得
- sethostent(3) - ネットワーク上のホストのエントリを取得する
- sethostname(2) - ホスト名の取得・設定をする
- setnetent(3) - ネットワークエントリを取得する
- setnetgrent(3) - ネットワーク・グループのエントリを操作する
- setprotoent(3) - プロトコルのエントリを取得する
- setpwent(3) - パスワードファイルのエントリの取得
また、また、下記のセマフォや共有メモリに関するヘッダファイル、および対応するライブラリ関数は存在しません。
- <sys/sem.h> /* SysV semaphores */
- <sys/shm.h> /* SysV shared memory segments */
- <sys/msg.h> /* SysV message queues */
- <sys/ipc.h> /* General IPC definitions */
これらが削られている理由はNDKのdocs/system/libc/SYSV-IPC.htmlにも書いてありますが、セマフォなどのシステムグローバルなリソースのリークがあった場合に、システムリブート以外の方法で解放できないのがリスクだから、ということのようです。
ヘッダファイルに定義があるのにリンクできない関数一覧
Android NDKで開発していると、ヘッダファイルにプロトタイプ宣言が存在するのにリンカがエラーを出すことがあります。例えば次のような状況になります。
$ arm-linux-androideabi-gcc -Wall /tmp/bcmp-test.c /Users/hnw/Development/arm-android-19-toolchain/bin/../lib/gcc/arm-linux-androideabi/4.6/../../../../arm-linux-androideabi/bin/ld: /var/folders/_6/384fllzd5ys3mjqgk1xfmrnc0000gp/T//ccUhYkjo.o: in function main:bcmp-test.c(.text+0x34): error: undefined reference to 'bcmp' collect2: ld returned 1 exit status $
<strings.h>にbcmpの定義があるのにリンクできないのは理不尽な気がしますが、実際NDKのlibc.soにはbcmpが含まれていないので仕方がありません。このようなシステムコール・ライブラリ関数は以下の通りです。
- atexit(3) - プロセスが正常終了した時に呼び出される関数を登録する
- bcmp(3) - バイト列を比較する
- getw(3) - ワード(int)の入出力
- malloc_usable_size(3) - obtain size of block of memory allocated from heap
- mlockall(2) - メモリのロックとロック解除を行う
- munlockall(2) - メモリのロックとロック解除を行う
- pvalloc(3) - アラインメントされたメモリの割り当てを行う
- rindex(3) - 文字列中の文字の位置を示す
また、下記のロケール関連およびワイドキャラクタ系のライブラリ関数も同様の状況です。これらはヘッダ内に「サポートしてないけどlibstdc++-v3のコンパイルを通すために定義してある」的なことが書いてありますが、われわれ一般人からするとライブラリをビルドしたらコメントアウトしておいて欲しい気がします。
- localeconv(3) - 数値に関する書式情報を得る
- mblen(3) - 次のマルチバイト文字のバイト数を返す
- mbtowc(3) - マルチバイト列をワイド文字に変換する
- towctrans(3) - ワイド文字の変換
- wctomb(3) - ワイド文字をマルチバイト列に変換する
- wctrans(3) - ワイド文字変換マッピング
これらはconfigureが混乱する原因になることがあります。例えば<locale.h>の存在チェックに成功するとロケール関連をデフォルトで有効にするものがありますので、明示的にconfigureオプションで無効にする必要があったりします。
NDKのlibcには存在しないためリンクエラーになるシステムコール一覧
システムコールはカーネル側で実装されており、カーネル側はほぼLinuxであるため、AndroidではLinuxのシステムコールの多くが呼び出せるはずです。しかし、実際には呼び出せないシステムコールが数多く存在します。これは、対応するCの関数がlibcに実装されていないためです。(例:「ftruncate64 linker error on NDK r8b」)
このようなシステムコールを網羅的にリストアップする方法は思いつかなかったのですが、近いリストとして、Nexus5実機(Android 4.4)のlibcでは実装されているけれども、NDKのlibcには無いシステムコールを取り出してみました。
- faccessat(2) - ユーザのファイルへのアクセス権をチェックする
- fgetxattr(2) - 拡張属性の値を取得する
- flistxattr(2) - 拡張属性の名前リストを得る
- fremovexattr(2) - 拡張属性を削除する
- fsetxattr(2) - 拡張属性の値を設定する
- ftruncate64(2) - 指定した長さにファイルを切り詰める
- getsid(2) - セッション ID を取得する
- getxattr(2) - 拡張属性の値を取得する
- lgetxattr(2) - 拡張属性の値を取得する
- listxattr(2) - 拡張属性の名前リストを得る
- llistxattr(2) - 拡張属性の名前リストを得る
- lremovexattr(2) - 拡張属性を削除する
- lsetxattr(2) - 拡張属性の値を設定する
- mlockall(2) - メモリのロックとロック解除を行う
- munlockall(2) - メモリのロックとロック解除を行う
- perf_event_open(2) - set up performance monitoring
- personality(2) - プロセスを実行するドメインを設定する
- pread64(2) - 指定したオフセットでファイルディスクリプタを読み書きする
- pwrite64(2) - 指定したオフセットでファイルディスクリプタを読み書きする
- readahead(2) - 前もってファイルをページ・キャッシュに読み込む
- removexattr(2) - 拡張属性を削除する
- sched_getaffinity(2) - スレッドの CPU affinity マスクを設定・取得する
- sched_setaffinity(2) - スレッドの CPU affinity マスクを設定・取得する
- setxattr(2) - 拡張属性の値を設定する
- signalfd(2) - シグナル受け付け用のファイルディスクリプタを生成する
- signalfd4(2) - シグナル受け付け用のファイルディスクリプタを生成する
- swapoff(2) - ファイル/デバイスへのスワップを開始/停止する
- swapon(2) - ファイル/デバイスへのスワップを開始/停止する
- tgkill(2) - スレッドにシグナルを送る
- timerfd_create(2) - ファイルディスクリプタ経由で通知するタイマー
- timerfd_gettime(2) - ファイルディスクリプタ経由で通知するタイマー
- timerfd_settime(2) - ファイルディスクリプタ経由で通知するタイマー
- unshare(2) - プロセス実行コンテキストの一部を分離する
このリストは「arm-linux-androideabi-nm -D」でシンボルを取り出し、Linux環境で一般的と思われるシステムコール・ライブラリコールのみ抽出したものですので、過不足があるかもしれません。
どうやらAndroid NDK付属のlibc.so(android-19プラットフォームなので、Android 4.4に対応)はかなり古いもののようで、bionic C libraryのリポジトリ上では2010年頃に修正されている内容が反映されていなかったりします。一方で、Nexus 5実機のlibc.soには多くの修正が反映されているようです。bionic C libraryの改善は続けるけど、Android NDKプログラミングでは後方互換性のために古いインターフェースで頑張ってね、というメッセージなのかもしれません。
libcで実装されていないシステムコールを直接呼び出す方法
ところで、システムコールの実体がカーネルに実装されていれば、libcに実装がなくても自分でCインターフェースを実装することができます。例えば、getsid(2)であれば次のようにして呼び出すことができます。
#ifdef __BIONIC__ #includepid_t getsid(pid_t pid); pid_t getsid(pid_t pid) { return syscall(__NR_getsid, pid); } #endif
syscall(2)システムコールは任意のシステムコールを呼び出すためのシステムコールで、第一引数にはシステムコール番号を渡します。ここで使われている__NR_getsidなどの各システムコール番号は<asm/unistd.h>で大量に定義されており、これを使って任意のシステムコールを呼び出すことができます。
ただし、システムコール番号が定義されていても、正しく呼び出せる保証はありません。システムコールの実装本体はカーネルにあり、カーネル次第では未実装のこともあります。未実装の場合にはシステムコールがerrnoとしてENOSYSを返すことに注意してください。
とはいえ、カーネルバージョンに対応したシステムコールであれば大抵うまく動く印象です。筆者はNexus 5(Android 4.4, Linux 3.4.0)でprocess_vm_readv(2)を呼び出すことができました。
まとめ
- Android NDKでは多くの環境で利用できるライブラリ関数であっても使えないものがあります
- アカウント管理関連、システム管理関連、ロケール関連、ワイドキャラクタ系、セマフォ、共有メモリなど
- Android NDKのヘッダファイル、NDKのlibc、実機のlibcで関数の対応が取れていないことがあります
- libcで実装されていなくても、syscall(2)を使えば任意のシステムコールを呼び出すことが可能です
本稿の内容は、筆者が各種Unix系ライブラリ・ツールをAndroid NDKでビルドしたときに気になった点をまとめたものです。makeがうまく通らないときなどに役立つと想像しています。
ちなみに、Android NDKのlibc.soはプラットフォームandroid-9(Android 2.3相当)からandroid-19(Android 4.4相当)までほとんど変わっていないようです。ですから、上記内容はプラットフォームバージョンによらず共通の内容と言えそうです。