VMware Serverのパフォーマンス劣化を防ぐ

※注
この記事は2009年末にWizardBible Vol.49に寄稿したものである。
VMWareがクソ重くて殺したくなる時に読むべきものという記事で取り上げていただいたところ反響が大きかったようなので、こちらのブログにも掲載しておく。WizardBibleは拡張子.txtのためかSEO的に弱いようなので…

— 第1章: もう憂鬱じゃないVMware Server管理者 —

著者:金床

x0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0x
x0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0xx0xXx0x

■0x00.) はじめに

Wizard Bible vol.43にて、VMware ServerをLinux上で走らせる際の苦労につい
ての記事を書いた。その後さらに試行錯誤を繰り返していくうちにかなり納得で
きるレベルまでたどり着くことができたので、今回はWB43の続編といった位置づ
けでの記事としたい。

「どんなに暗い夜でも、必ず朝が来る」

パフォーマンスが上がらず四苦八苦し続けたVMware Serverの管理者たちに、こ
の言葉を贈りたい。

ちなみに筆者が使っているバージョンはLinux版のVMware Server 1.0系列で、
ホストOSはX86_64のCentOS5.2である。他のバージョンを使っている人には参考に
ならない点が多々あるかと思うがご了承願いたい。

■0x01.) 前回の結論と残った課題

WB43での筆者の結論は以下のようになっていた。

・すべてのゲストOSがフルにメモリを消費しても大丈夫な量のメモリをホストOSに載せておく
・ホストのメモリが実際にいくつ消費されているのかはぶっちゃけわからないので、
気にしないwww
・どうしても気になる場合は/proc/meminfoを見る
・mainMem.useNamedFile = “FALSE”は使わない
・そのほかは普通にやる(Fit all virtual…やDisable memory page trimming
など)

これらの方針通りにしばらくサーバー運用を続けたのだが、どうにも状態がよ
ろしくない。数週間から1ヶ月以上立ち上げっぱなしにしているゲストOSのパフォ
ーマンスが思わしくなく、徐々に調子が悪くなってくるのだ。特に具合が悪くな
る(あらゆる場面で遅くなる)のが、ホストOS上で巨大なファイルのコピーなど
の作業を行った後だ。つまり、ホストOSのディスクキャッシュが大きな影響を与
えているものと予想される。

ゲストOS(筆者の場合は主にLinuxだが、Windowsも同様に調子が悪くなる)を
再起動すれば調子は良くなるのだが、それではせっかくのLinuxサーバーがまるで
Windows NT4.0のようで、精神安定上よろしくない。

とはいえ再起動すれば調子よくなってしまうため、いったいどのような現象が
起こっているのかつかめず月日は流れていった。

■0x02.) .vmemファイルの読み込みが遅い

あるときゲストOSのリジュームがいつまでたっても終わらないので、.vmemファ
イルの読み込みに問題があるのでは?と見当を付けた。そこで1ヶ月以上連続稼働
しているゲストOSの.vmemファイルをddで読み込み、その読み込みの速度を測定し
てみる。するとなぜか2MB/sしか速度がでない。ハードディスクには特に問題はな
く、シーケンシャルの読み込みでは80MB/sくらいは出るのに、である。

なぜこんなに読み込みが遅いのだろうか?と考えた結果、このファイルがひど
く断片化しているのではないかという仮説にたどり着いた。というのは、起動直
後のゲストOSをサスペンド・リジュームさせた場合、比較的スムーズにリジュー
ムが完了するのに対し、長い間稼働した後のゲストOSをサスペンド・リジューム
させるとひどいことになるからだ。

また、ホストOSのディスクキャッシュの状態がリジュームのパフォーマンスに
大きな影響を与えているように感じていたが、この点について、この断片化した
.vmemファイルがディスクキャッシュにのっていない場合にひどくパフォーマン
スが悪くなるのではないかと予想した。

■0x03.) 1つめの解(/dev/shmに置く)に到達

そこで、ハードディスク上での断片化が問題なのだろうという予測のもとに、
.vmemファイルがメモリ上に作成されるようにしてみた。ホストOSにおいて/tmpを
/dev/shmへのリンクに変更した上で、vmxファイルでmainMem.useNamedFile = “F
ALSE”を記述する。すると.vmemファイルはメモリ上に隠しファイルとして作成さ
れる。メモリ上でもファイルとして断片化が発生するのかどうかはわからないが、
HDDよりもランダムアクセスが圧倒的に速いため、これで状況が改善するのでは?
と期待した。

結果は期待通りで、長い間稼働させてもパフォーマンスは安定し、サスペンド
とリジュームもうまく動いてくれるようになった。この方法の欠点は常にホスト
OSのメモリを消費するということだが、メモリをたっぷり積んでいる場合は問題
ない。手軽に実行できるのでおすすめの方法である。

この方法は/etc/vmware/configにtmpDirectory = “/dev/shm”と書き、さらにv
mxファイルにmainMem.useNamedFile = “FALSE”と書くことでも実現可能である。

後に検索によってたどり着いた多くのVMware関連のフォーラムなどをのぞいた
感じでは、この手法は一定数のユーザから支持されているようだった。

■0x04.) 断片化の度合を調べる

今回の調査では某社の気鋭のエンジニアであるINB氏に多大なる協力を頂いた。
筆者が「ファイルがどのくらい断片化しているかを調べる方法を探している」と
伝えたところ、hdparmの–fibmapオプションで可能であることを教えてくれた。
また、前項のtmpDirectoryという項目の存在を教えてくれたのも彼である。この
場を借りてお礼を申し上げる。

hdparmの–fibmapオプションは比較的新しいバージョンのhdparmに実装されて
いる。筆者の環境であるCentOS5.2に入っていたhdparmではサポートされていなか
ったので、最新版のソースをダウンロードしてコンパイルした(makeするだけで
無事にコンパイルすることができた)。

.vmemファイルに対して以下のように–fibmapオプションを使用する。

hdparm --fibmap 'Windows XP Professional.vmem'

すると以下のように結果が出力される。場合によっては非常に大量の出力が発
生するので注意が必要である。

Windows XP Professional.vmem: underlying filesystem begins at LBA 63; assuming 512 byte sectors.
byte_offset  begin_LBA    end_LBA    sectors
0  679035215  679035310         96
49152  679035319  679043510       8192
4243456  679043527  679051326       7800
8237056  679051391  679051782        392
8437760  679051791  679059982       8192
12632064  679059991  679067710       7720
(略)

数ヶ月稼働させていたゲストOSの.vmemファイルを調べてみたところ永遠に出力
が止まらないw感じだったので、wc -lにパイプしてhdparmの出力の行数だけを調
べてみることにした。このゲストに割り当てていたメモリは約650MBだったので.v
memファイルのサイズもそのくらいだったのだが、なんとhdparm –fibmapの出力
は140万行以上に及んだ。ddでシーケンシャルに同じくらいのサイズのファイルを
作成して調べてみたところわずか359行だった。…断片化ってレベルじゃねーぞ!
(`Д´)ノゴラァ

とにかくこれで「.vmemファイルがひどく断片化することがパフォーマンス劣化
の原因である」ということはほぼ確定した。そこで「vmware vmem fragmentation」
などでググってみると、案の定たくさんのウェブサイトがヒットした。このことに
もっと早く気づいていれば…。

ちなみにこの検索の段階で「filefrag」というそのまんま断片化の具合を調査
するコマンドもあることを知った。手元のUbuntuではデフォルトで入っているよ
うである。

■0x05.) 断片化の理由

なぜ.vmemファイルはここまでひどく断片化するのだろうか。WB43でも書いたよ
うに、.vmemファイルはスパースファイルとして生成される。そしてゲストOSを長
い時間稼働させていくにつれて、徐々に実際のサイズが大きくなっていく。

どうやらvmware-vmxプロセスは.vmemファイルを1MBずつの領域にわけてmmapを
行っているらしい。これは、/proc/PID/smapsをlessなどで見ることで確認するこ
とができる(PIDはvmware-vmxプロセスのプロセスID)。該当する.vmemファイル
について、Size項目が1024KBの領域が大量に見つかる。

おそらくvmware-vmxプロセスは、「しばらくアクセスされていない」等の何ら
かの基準に従って、これらの1MBの領域を個別にファイルに同期させているのだろ
う。全体で数Gにもなるようなスパースファイルである.vmemファイルに、ばらば
らの順番で1MBずつ書き込みを行っているために断片化が発生しているのだと予想
される。

■0x05.) 2つめの解(.vmemファイルをコピーして入れ替える)に到達

筆者と同じく.vmem問題にはまっている人たちのエントリが大量に見つかったの
で適当に目を通してみると、「一度サスペンドさせ、.vmemファイルをコピーして
入れ替え、リジュームする」という作戦が見つかった。断片化が起こる前に、フ
ァイル自体をきっちりHDD上に作成してしまおうという作戦である。

見た目のサイズが大きいにもかかわらず、実際には小さなスパースファイルが
あるとする。このファイルをddで読み込むことで別のファイルとしてコピーする
と、スパースファイルではない通常の(巨大な)ファイルが生成される。コピー
の際に、スパースファイル中のまだ書き込みが行われていなかった部分は、コピ
ー先のファイルではすべて0x00となる。

こんなことをして何がうれしいのかというと、コピー時に一気に大きなファイ
ルを作成するので、ファイルがHDD上にほぼシーケンシャルに作成され、断片化が
非常に少ない形で.vmemファイルを作成することができるのだ。また、このファイ
ルはすでに全体をディスク上に割り当てられているため、これ以上肥大化するこ
とがない。つまり断片化も進まない。この後いくら長くゲストOSを稼働させ続け
ようとも、まったく断片化は進行せず、安定したパフォーマンスを発揮するので
ある。

筆者の手元ではcpコマンドでのコピーではスパースファイルのままコピーされ
てしまうようだった。そのためddを使ったが、インターネット上で見た感じでは
このあたりは環境依存があるようだ(cpコマンドでうまくいっている人もいるよ
うである)。

この方法を簡単にまとめると次のようになる

・この方法は、mainMem.useNamedFile = “FALSE”を指定しない場合に使用できる
方法である。つまり、.vmemファイルが/tmp等ではなく、.vmxファイルと同じディ
レクトリに作成される場合に使う方法となる
・ゲストOSを起動したら、すぐにサスペンドさせる。
・この段階で、実際のサイズは非常に小さいが、見た目のサイズは割り当てたメ
モリの量に等しい.vmemファイルが作成される(仮にサスペンドせずこのまま使用
を続けると、ひどく断片化しながら肥大することになる)。
・ddなどを使ってこの.vmemファイルをコピーする。ここでコピー先のファイルは
実際に巨大なサイズのものになる。コピーには数十秒かかる場合もある。
・コピーが終了したら、コピー元のスパースファイルを削除する
・コピー先のファイルをリネームし、元の.vmemファイルと同じ名前にする
・ゲストOSをリジュームする
・安定稼働(*´Д`)長期実現ポワワ

この方法を使えば長期稼働させた後にも安心してサスペンド・リジュームさせ
ることができる。欠点は毎回起動後にシェルから操作が必要であることと、コピ
ーの時間(数十秒)が掛かることである。

■0x06.) 3つめの解(LD_PRELOADで.vmemファイル生成)

前項で説明した方法でほぼ問題ないのだが、毎回シェルからファイルコピーな
どの操作するのはメンドクチイと思ったので、もう少し踏み込んでみた。WBらしく、リ
バースエンジニアリングの領域に突入である。ゲストOSの実体であるvmware-vmxプロセス
の動作を一部動的にコントロールする。そして、.vmemファイルをスパースファイ
ルではなく、通常のファイルとして作成させてしまうのだ。

幸いなことにWB43にてvmware-vmxプロセスをstraceで追った際に、スパースフ
ァイルが作成される箇所は特定済みである。以下はstraceのログからの抜粋だ。

pwrite64(106, "", 1, 3774873599) = 1

このように、起動からまもなく、かなり癖のある引数でpwrite64が実行される。
4つめの引数(オフセット)が極端に大きいのに対し、たった1バイトしか書き込
みを行わないことで、この呼び出しがスパースファイルの生成であることが容易
に確認できる。この時点でファイルをシーケンシャルにきっちり作成してやれば、
前項で説明した「2つめの解」と同じ動きを実現できるはずだ。

Linuxではライブラリの呼び出しをLD_PRELOADを使いフックすることで、任意の
関数の内容を書き換え、アプリケーションの動作を変更することができる。ここ
ではpwrite64の呼び出しをフックし、スパースファイルの作成を阻止しつつ、実
際に大きなファイルを生成するようにすればいい。ただしpwrite64はvmware-vmx
の動作中で何度も使用されるため、スパースファイルの作成を行っている場合の
み、挙動を書き換えるようにする。上に書いたように4番目の引数であるオフセッ
トが極端に大きく、かつ1バイトしか書き込まない場合に挙動を変更する。このと
きpwrite64ではなく普通にwriteを呼び出し、大きな.vmemファイルをきっちり作
成する。そうでない場合はそのまま本物のpwrite64にフォワードし、戻り値もそ
のまま返すようにする。

ソースコードは以下のようになる。






#include <dlfcn.h>
#include <syslog.h>
#include <fstream>

#if defined(RTLD_NEXT)
#define REAL_LIBC RTLD_NEXT
#else
#define REAL_LIBC ((void *) -1L)
#endif

using namespace std;

static int flag1 = 0;

// pointer to the original function
static int (*original_pwrite64) (int file_descriptor, const void *buf, size_t nbyte, off64_t offset) = NULL;

// for logging
#define LOG(...) do {                 \
  syslog(LOG_INFO, __VA_ARGS__);      \
} while(0)

#define SADDR_B(target,shift)  (((target) >>  (shift)) & 0x000000ff)

static void __attribute__ ((constructor))
_constructor()
{
  // for syslog
  openlog(NULL, LOG_CONS | LOG_NDELAY | LOG_PID, LOG_USER); 

  //get address of the original function
  original_pwrite64 = (int(*)(int,const void*,size_t, off64_t)) dlsym(REAL_LIBC, "pwrite64");
}

static void __attribute__ ((destructor))
_destructor()
{
  closelog();
}

ssize_t pwrite64(int fd, const void *buf, size_t nbyte, off64_t offset)
{
int rv;
if( flag1 == 0
 && offset > ( 1024 * 1024 * 200 ) //works only if offset is larger than 200MB
 && nbyte == 1
  )
    {
    flag1 = 1;
    LOG( "pwrite64 is hooked. ARGS: nbyte=%d, offset=%d\n", nbyte, (int)offset );
    LOG( "creating .vmem file...\n" );
    int bufsize = 1024 * 1024;
    char buffer[ bufsize ];
    off64_t remain = offset + 1;

    while( remain > 0 )
        {
        if( remain > bufsize )
            {
            rv = write( fd, buffer, bufsize );
            }
        else
            {
            rv = write( fd, buffer, remain );
            }
        if( rv == -1 )
            {
            LOG( "write failed.\n" );
            return -1;
            }
        remain -= rv;
        }
    return 1;
    }
else
    {
    rv = (*original_pwrite64)(fd, buf, nbyte, offset );
    return rv;
    }
}

ソースのダウンロードはhttp://www.jumperz.net/tools/nosparse.cppから、バ
イナリのダウンロードはhttp://www.jumperz.net/tools/nosparse.soから行うこ
とができる。

このコードはチームチドリのスーパーハカーyoggy氏が作成したhook_tcp.cppをベースに
している。オリジナルのファイルはチームチドリのサイトからダウンロードできる
(http://www.t-dori.net/?hook_tcp.so)。スペシャルサンクスコ!!>Yoggyさん&チドリ

このコードをnosparse.cppという名前で保存し、次のようにコンパイルして共
有ライブラリ(.soファイル)を作成する。

g++ -Wall -fPIC  -shared -o nosparse.so nosparse.cpp -ldl

ここで注意したいのは、このコンパイルは32bitのLinuxマシンで行う必要があ
るということだ(g++の-m32オプションを使用してもよいのかもしれないが、筆者
の環境ではさまざまなヘッダファイル等が足りずうまくいかなかった)。筆者は
最初X86_64のLinux上でコンパイルしたのだが、vmware-vmxのバイナリが32bit用
のものだったためにうまくPRELOADすることができずハマった(VMware Server 1.0
系列は32bitバイナリなのである)。

32bitのマシンでコンパイルしたnosparse.soファイルをX86_64マシンに持って
くることで、問題なく動かすことができる。もちろんホストOSが32bitの場合はそ
のままで問題なくうまく動くと思われる。

コンパイルがうまくできたら、以下のようにしてvmware-vmxプロセスをシェル
から起動する。ここではnosparse.soは/root/vmware/に、vmware-vmxは/usr/lib
/vmware/bin/に、そして起動したいゲストOSのvmxファイルは/vmware/Linux/以下
にあるものと仮定する。

LD_PRELOAD=/root/vmware/nosparse.so /usr/lib/vmware/bin/vmware-vmx -x -C /vmware/Linux/Linux.vmx -@ \"\"

通常はゲストOSの起動はVMware Server Consoleか行う場合がほとんどだと思う
が、今回のテクニックを用いる場合にはこのようにシェルからゲストOS(vmware
-vmxプロセス)を起動する。このとき、CD-ROMが存在しない等のようなエラーな
どがある場合にはVMware Server Consoleに対してダイアログがポップアップする
ケースがあるため、VMware Server Consoleからも接続した状態で別ウィンドウの
シェルから起動する方法がおすすめである。

うまく起動した場合(上のコマンドを実行し、何もエラー等が出力されない場
合)、まずはじめに.vmemファイルがディスク上にほぼシーケンシャルな状態で作
成される。例えばゲストOSのメモリを2GB程度割り当てた場合には数十秒の時間を
要するので、じっと待つ。ここで時間がかかるのが欠点だが、起動後のパフォー
マンスはすこぶる安定するのでじっと待つ価値はある。

.vmemファイルを生成する際に、syslogに(通常は/var/log/messagesに)以下
のようなログが出力される。

Nov 17 18:27:06 raptor vmware-vmx[21321]: pwrite64 is hooked. ARGS: nbyte=1, offset=2017460223
Nov 17 18:27:06 raptor vmware-vmx[21321]: creating .vmem file...

.vmemファイルが無事生成されると、続いて通常と同じようにVMware Server C
onsoleにBIOS画面が表示され、続いてゲストOSの起動が始まる。ここで試しにhd
parm –fibmapしてみると、出力はわずか1000〜1500行程度に抑えられており、断
片化の防止に成功したことがわかる。

この後は普通にゲストOSを使用できる。サスペンド・リジュームを行う場合は
再度PRELOADする必要はないので(.vmemファイルはすでに完成しているので)、
コンソールから普通に操作すればOKである。

■0x07.) まとめ

今回は、LinuxをホストOSとして使うVMware Server 1.0.x系列について、サス
ペンド&リジュームや長期使用時のパフォーマンス劣化の原因が.vmemファイルの
断片化であることを特定し、またその対策として3つの案を提示した。.

vmemファイルの断片化を防止すれば、VMware Serverはすこぶる良好な使用感と
なり、非常に満足度の高い仮想化技術を提供してくれる。筆者はここ数年長い間
苦労してパフォーマンス劣化と戦ってきたが、ついに問題点を解決することがで
き、非常に晴れ晴れとした気分である。VMwareにはESXiなどもあるのでServerの
1系列にこだわる必要はないと思うかもしれないが、筆者はホストOSとしてのLin
uxの機能(iSCSI、ソフトウェアRAID、リモートから使用できる充実した管理機能
など)を必要としているのでServerにこだわっていたりする。今後もしばらくの
間は使い続けることになるだろう。

■0x08.) おまけでQ&A

なんでこのファッキンVMwareはこんなに具合悪いのマダファッカ?
.vmemファイルが断片化しているから

.vmemファイルの実際のサイズの確認方法は?
ls -lsuhあるいはduで確認が可能

.vmemファイルはいつ作成されるの?
ゲストOS起動直後

.vmemファイルはどこに作成されるの?
通常.vmxファイルと同じディレクトリだが、vmxファイルの設定でuseNamedFile
をfalseにしておくと/tmp以下の隠しファイルになる。この/tmpの位置は変更可能(本
文参照)

.vmemファイルはいつ書き込まれるの?
vmware-vmxプロセスがマターリとメモリと同期させる。数日がかりで観察する
と徐々に大きくなることが確認できる

.vmemファイルはいつ読み込まれる(役に立つ)の?
レジュームするとき。あるいはmemory page trimmingが有効な場合には、vmwa
re-vmxプロセスが判断して随時読み込む

.vmemファイルの断片化の程度はどうやって確認するの?
hdparmの最近のバージョンで hdparm –fibmap *.vmem | wc -lすればどの程度
かをつかめる。あるいはfilefragコマンドを使う

.vmemファイルの断片化を防ぐ方法は?
起動直後にサスペンドさせ、ddでファイルをコピーして入れ替え、リジューム
するか、LD_PRELOADしてスパースファイル生成を阻止する(詳細は本文参照)

.vmemファイルを使用しない方法はないの?
ない

.vmemファイルがなくなったみたいだけど?
/tmp以下に隠しファイルとして存在している。lsofでvmware-vmxプロセスを見
ると見つけることができる

サスペンドは使わない方がいい?
断片化しないように対策してあれば普通に使える

memory page trimmingは使わない方がいい?
ホストOSのメモリが足りなくなる可能性があるなら(かつ断片化を防止してい
るなら)便利に使える。メモリに余裕があるなら使わない方がいい

useNamedFile=”False”の意味は?
.vmemファイルを/tmp以下の隠しファイルにするという意味。理解していないと
地雷

ゲストOSが消費しているメモリ量の確認は?
vmware-vmxプロセスについて、/proc/PID/statusのRSSを見る。あるいはtopで
SHR項目を確認

Advertisements

Androidアプリケーションのリバースエンジニアリング

[PR]株式会社セキュアスカイ・テクノロジーのAndroidアプリケーション診断

ReversingAndroidApp

結論を先に書くと、Androidアプリケーションのリバースエンジニアリングは非常に簡単である。理由は大きく2つあり、一つはそれがJavaアプリケーションであること、もうひとつはAndroidがオープンソースであることだ(ただしJNI等を使ってC++やCのコードなどを呼び出している場合には、下層のモジュールの解析は通常のCアプリケーション同様に面倒ではないかと考えられる)。

Androidアプリケーションは.apkという拡張子でファイル単体で配布されるので、まずそれを用意する。筆者はAppMonsterというツールを使っている。このツールだと簡単にSDカードにapkファイルを保存してくれる。このエントリでは例としてテスト用のアプリケーションであるandroid1.apkを使用する。

apkファイルはZIP形式の圧縮されたファイルとなっているので、unzipコマンドで任意の場所に展開する。

root@kaldi:/android/tmp# unzip android1.apk
Archive:  android1.apk
  inflating: res/layout/main.xml     
  inflating: AndroidManifest.xml     
 extracting: resources.arsc          
 extracting: res/drawable-hdpi/icon.png  
 extracting: res/drawable-ldpi/icon.png  
 extracting: res/drawable-mdpi/icon.png  
  inflating: classes.dex             
  inflating: META-INF/MANIFEST.MF    
  inflating: META-INF/CERT.SF        
  inflating: META-INF/CERT.RSA
root@kaldi:/android/tmp# ls -l
total 36
-rw-r--r-- 1 root root  1504 Jan 20 16:45 AndroidManifest.xml
drwxr-xr-x 2 root root  4096 Jan 20 17:58 META-INF
-rw-r--r-- 1 root root 14515 Jan 20 16:46 android1.apk
-rw-r--r-- 1 root root  3780 Jan 20 16:45 classes.dex
drwxr-xr-x 6 root root  4096 Jan 20 17:58 res
-rw-r--r-- 1 root root  1488 Jan 20 16:45 resources.arsc

AndroidアプリケーションではActivityをはじめさまざまな情報がAndroidManifest.xmlにまとめられている。そのため解析対象のアプリケーションの内部構造を俯瞰するには、まずこのファイルから手を付けるのがよい。しかしAndroidでは極力モバイルデバイスの負荷を減らすためにXMLファイルはあらかじめパースされたバイナリ形式となって格納されており、このままではAndroidManifest.xmlをエディタで開いても中身を読むことができない。そこでバイナリ形式から通常のXML形式に戻すためにAXMLPrinter2.jarというツールを使用する。jarという拡張子からわかるようにこのツールはJavaアプリケーションであり、java -jarして引数にファイル名を与えると標準出力にXMLを吐いてくれる。

root@kaldi:/android/tmp# java -jar ../AXMLPrinter2.jar AndroidManifest.xml 
<?xml version="1.0" encoding="utf-8"?>
<manifest
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:versionCode="1"
 android:versionName="1.0"
 package="net.jumperz.app.android.test1"
 >
 <application
  android:label="@7F040001"
  android:icon="@7F020000"
  android:debuggable="true"
  >
  <activity
   android:label="@7F040001"
   android:name=".HelloActivity"
   >
   <intent-filter
    >
    <action
     android:name="android.intent.action.MAIN"
     >
    </action>
    <category
     android:name="android.intent.category.LAUNCHER"
     >
    </category>
   </intent-filter>
  </activity>
 </application>
 <uses-sdk
  android:minSdkVersion="4"
  >
 </uses-sdk>
</manifest>

このようにAndroidManifest.xmlを解析することで、このアプリケーションはActivityをひとつしか持たないことがすぐにわかる。

続いて実際のアプリケーションの細かな動作をコントロールしている、中心となる部分を解析する。Androidアプリケーションでは、アプリケーションの細かな挙動は開発時にはJavaのソースコード(拡張子.javaのテキストファイル)として存在する。それがOracleのJDKなどでコンパイルされ、クラスファイル(拡張子.classのバイナリファイル)となる。Androidアプリケーションとしてデプロイされる際には、アプリケーション内部で使用されるクラスファイルはすべてAndroid独自のdex形式として1つのファイルにまとめられる。このファイルはclasses.dexという名前でapkファイル内に存在している。

classes.dexファイルはバイナリファイルなので、そのままでは中身を読むことができない。そこでbaksmaliというツールを使う。baksmaliはsmaliと対となるツールであり、それぞれアイスランド語でディスアセンブラ/アセンブラという意味を持つらしい。AndroidのJVMであるDalvikという単語がアイスランド由来であることから、このような名前を付けたようだ。

baksmali/smaliともJavaで作成されたアプリケーションであり、jarファイルとして存在している。次の例のように引数にclasses.dexファイルを渡すと、out/というディレクトリを生成してその中にsmali形式のファイルを吐いてくれる。

root@kaldi:/android/tmp# mkdir smali
root@kaldi:/android/tmp# cd smali
root@kaldi:/android/tmp/smali# java -Xmx1G -jar ../../baksmali-1.2.6.jar ../classes.dex
root@kaldi:/android/tmp/smali# find . -type f
./out/net/jumperz/app/android/test1/R$drawable.smali
./out/net/jumperz/app/android/test1/R.smali
./out/net/jumperz/app/android/test1/R$id.smali
./out/net/jumperz/app/android/test1/R$layout.smali
./out/net/jumperz/app/android/test1/R$attr.smali
./out/net/jumperz/app/android/test1/R$string.smali
./out/net/jumperz/app/android/test1/HelloActivity.smali

拡張子.smaliのsmali形式のファイルはテキストファイルであり、エディタで編集することができる。以下にHelloActivity.smaliの一部を掲載する。

# virtual methods
.method public onCreate(Landroid/os/Bundle;)V
    .registers 5
    .parameter "savedInstanceState"

    .prologue
    .line 28
    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    .line 29
    const/high16 v0, 0x7f03

    invoke-virtual {p0, v0}, Lnet/jumperz/app/android/test1/HelloActivity;->setContentView(I)V

    .line 30
    const v0, 0x7f050001

    invoke-virtual {p0, v0}, Lnet/jumperz/app/android/test1/HelloActivity;->findViewById(I)Landroid/view/View;

smaliファイルの中身はDalvik VMのバイトコードをディスアセンブルしたものである。最初に見たときは「わけがわからない」と投げ出したくなるかもしれないが、Javaの開発者であれば大部分はすぐに読めるようになる(難しいアルゴリズムの実装部分などはそのまま読むのは厳しいかもしれないが)。smaliファイルに慣れるためには、まず自分で簡単なAndroidアプリケーションをJavaで書き、それを上記の方法でsmali形式に変換する。そしてJavaソースコードとsmaliファイルを行ごとに付き合わせて読んでいけばいい。おそらく30分もあれば基本的な構造についてはかなり読めるようになっているだろう。Dalvikのバイトコードやその他の説明は以下のURLが参考になる。

http://code.google.com/p/smali/w/list
http://www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html

今回、例として使用しているアプリケーションは筆者が作成したものなので、もちろん元となるJavaのソースコードが手に入る。簡単に以下の行について説明を行う。

    .line 28
    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

.line 28とあることから、元のJavaソースコードの28行目であることがわかる。該当するJavaソースコードは以下となっている。

27:    public void onCreate(Bundle savedInstanceState) {
28:        super.onCreate(savedInstanceState);

invoke-superは親クラスのメソッドを呼び出すことを意味している。次のp0とp1は呼び出しの際に使うレジスタを表している。pではじまる名前のレジスタは実行中の関数の引数(パラメータ)であり、p0が0番目(this)、p1がひとつめの引数だ。Landroid/app/Activity;はandroid.app.Activityクラスのことだ(頭のLは続く文字列がクラス名であることを意味する)。続くonCreateは呼び出す関数の名前であり、引数が(Landroid/os/Bundle;なので)android.os.Bundleクラスのインスタンスであるという意味になる。つまりthisオブジェクト(p0)の親のonCreateを、実行中の関数のひとつめの引数(p1)であるsavedInstanceStateを引数として実行する、ということになる。最後のVは戻り値がvoidであることを意味する。

実際のアプリケーション解析では、元となるJavaのソースコードは手に入らないだろう。しかしbaksmaliを使用することで、このようにある程度読みやすい形のディスアセンブルリストは手にすることができる。smali形式を読み解くことができれば、アプリケーションはあなたの前で丸裸になったようなものだ。

しかし、もっと楽をしたいと考える人もいるだろう。実はdex2jarと呼ばれるツールが存在しており、このツールを使うとclasses.dexをjar形式に変換することができる。jarファイルはJavaのクラスファイル群をZIP形式で固めたものなので、unzipすることでJavaのクラスファイルを手に入れることができる。知っている人も多いかと思うが、Javaのクラスファイルはディスアセンブルが非常に容易であり、JDやjadなどのツールを使うことでほぼ完全なJavaソースコードを手にすることができる。しかし残念なことに、Androidアプリケーションではこの前段の変換を行うdex2jarの変換精度はあまり良くないようだ。これはDalvik VMと従来のJavaVMの仕様があまりにも異なっているため仕方がないことなのだと考えられる。

それでもdex2jarとJDなどを組み合わせてJavaソースコードを手に入れることには意味がある。読みやすさではsmali形式よりもJavaソースコードの方が圧倒的に優れているので、解析する際に、目的の処理がアプリケーション全体においてだいたいどの辺り(どのクラス)に存在しているのかを探すのには非常に便利なのだ。

dex2jarは以下のようにして使用する。

root@kaldi:/android/tmp# mkdir dex2jar
root@kaldi:/android/tmp# cd dex2jar
root@kaldi:/android/tmp/dex2jar# ../../dex2jar/dex2jar.sh ../classes.dex 
version:0.0.7.8-SNAPSHOT
2 [main] INFO pxb.android.dex2jar.v3.Main - dex2jar ../classes.dex -> ../classes.dex.dex2jar.jar
Done.
root@kaldi:/android/tmp/dex2jar# jar -x < ../classes.dex.dex2jar.jar 
root@kaldi:/android/tmp/dex2jar# find . -type f
./net/jumperz/app/android/test1/R.class
./net/jumperz/app/android/test1/R$drawable.class
./net/jumperz/app/android/test1/R$string.class
./net/jumperz/app/android/test1/HelloActivity.class
./net/jumperz/app/android/test1/R$layout.class
./net/jumperz/app/android/test1/R$attr.class
./net/jumperz/app/android/test1/R$id.class

クラスファイルが生成されているので、後はJDやjadなどのツールを使ってJavaソースコードに変換すればいい。

Androidアプリケーションのリバースエンジニアリングをする場合には、目的として「解析だけしたい」場合と、「解析した上で、さらに動作を自分好みに変更する」、つまりアプリケーションの改造までを行いたい場合があるだろう。前者の場合、JDによるJavaソースコード形式への(ときに不完全な)変換で十分な場合もあるだろう。この場合、読みにくいsmali形式のファイルと格闘する必要がないかもしれない。しかし後者、つまり改造までを行いたい場合、アプリケーション内の目的の箇所を自分の意図を達成するように書き換え、ふたたびAndroidアプリケーションとして動作するよう、正しくアセンブルしなおす必要がある。smaliはこれを可能にしてくれる。非常に精度が高いディスアセンブル・アセンブルが可能なので、classes.dex -> smali -> classes.dexという変換が可能なのだ。

ここでは筆者がAndroidの参考書からコピペで作成したサンプルアプリケーションを例に、改造する例を見ていく。このアプリケーションはテキスト入力欄をひとつ持ち、そこに文字列を入力してEnterキーを押すと、その下にあるリストに文字列が追加されていく、というだけのものだ。ソースコードは以下のようになる。

package net.jumperz.app.android.test1;

import android.app.Activity;
import android.os.Bundle;
import android.widget.*;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.text.*;
import android.content.Context;

import java.util.*;

public class HelloActivity
extends Activity
implements OnKeyListener
{
private List todoItems = new ArrayList();
private ListView lv;
private EditText et;
private ArrayAdapter aa;
//----------------------------------

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView( R.layout.main );
        lv = ( ListView )findViewById( R.id.ListView01 );
        et = ( EditText )findViewById( R.id.EditText01 );

        aa = new ArrayAdapter( this, android.R.layout.simple_list_item_1, todoItems );
        lv.setAdapter( aa );

        et.setOnKeyListener( this );
    }
//----------------------------------
public boolean onKey( View view, int keyCode, KeyEvent event )
{
if( event.getAction() == KeyEvent.ACTION_DOWN )
	{
	if( keyCode == KeyEvent.KEYCODE_ENTER )
		{
		String str = et.getText().toString();
		todoItems.add( 0, str );
		aa.notifyDataSetChanged();
		et.setText( "" );
		return true;
		}
	}
return false;
}
//----------------------------------
}

このアプリケーションを改造し、Enterキーが押された際に、そのときの文字列がクリップボードにコピーされるよう動作を変更(追加)してみる。改造する際にはsmali形式のファイルを直接編集することになる。ここでは「文字列をクリップボードにコピーする」という処理をsmali形式で記述しなければならない。

先述したようにこのような場合、ゼロからsmali形式で記述する必要はなく、自分で文字列をクリップボードにコピーするアプリケーションを作成して、該当部分がどのようにsmali形式に変換されるのかを確認すればよい。処理を追加する際には、その部分をわかりやすい引数を持つ関数として独立させるのがおすすめだ。ここでは以下のような関数を使用する。

private void copyToClipboard( Activity activity, String str )
{
ClipboardManager cm =  (ClipboardManager)activity.getSystemService( Context.CLIPBOARD_SERVICE );
cm.setText( str );
}

この関数はsmali形式では次のようになる。

.method private copyToClipboard(Landroid/app/Activity;Ljava/lang/String;)V
    .registers 5
    .parameter "activity"
    .parameter "str"

    .prologue
    .line 62
    const-string v1, "clipboard"

    invoke-virtual {p1, v1}, Landroid/app/Activity;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;

    move-result-object v0

    check-cast v0, Landroid/text/ClipboardManager;

    .line 63
    .local v0, cm:Landroid/text/ClipboardManager;
    invoke-virtual {v0, p2}, Landroid/text/ClipboardManager;->setText(Ljava/lang/CharSequence;)V

    .line 64
    return-void
.end method

この部分をまるごとout/net/jumperz/app/android/test1/HelloActivity.smaliファイル内に追記することで、HelloActivityクラスにcopyToClipboardという関数が追加されることになる。非常にシンプルで簡単だ。

次に、この追加した関数を呼び出す必要がある。Javaの以下の部分で目的の文字列がstrという変数に格納されていることに注目する。

String str = et.getText().toString();

対応するsmaliファイル内の記述は以下の部分だ。

    invoke-interface {v1}, Landroid/text/Editable;->toString()Ljava/lang/String;
    move-result-object v0

ここで、対象の文字列がv0というレジスタに格納されているので、この処理の後に関数呼び出しを追記すればよい。追記する内容は以下となる。

invoke-direct {p0, p0, v0}, Lnet/jumperz/app/android/test1/HelloActivity;->copyToClipboard(Landroid/app/Activity;Ljava/lang/String;)V

HelloActivityクラス自身のメソッドを呼び出すため、invoke-directを使う。thisインスタンス(p0)の関数を、thisインスタンス(p0)と対象文字列(v0)を引数として呼び出すため、{p0, p0, v0},という記述になる。

smaliファイルをこのように書き換えたら、アセンブラであるsmaliを使って再びclasses.dexの形に変換する。

root@kaldi:/android/tmp/smali# ls
out
root@kaldi:/android/tmp/smali# java -jar ../../smali-1.2.6.jar -o classes.dex out/
root@kaldi:/android/tmp/smali# ls -l
total 8
-rw-r--r-- 1 root root 3768 Jan 21 00:21 classes.dex
drwxr-xr-x 3 root root 4096 Jan 21 00:05 out

apkファイル内のclasses.dexを、zipコマンドで入れ替える。

root@kaldi:/android/tmp/smali# zip ../android1.apk classes.dex 
updating: classes.dex (deflated 50%)

apkファイルは署名する必要があるので、jarsignerコマンドを使って署名する。まず署名関連のファイルを削除する。

root@kaldi:/android/tmp# zip -d android1.apk META-INF/*
deleting: META-INF/MANIFEST.MF
deleting: META-INF/CERT.SF
deleting: META-INF/CERT.RSA

そして再び署名する。jarsignerコマンドを使うためには.keystoreファイルの用意などが必要だが、本題ではないのでここでは細かいプロセスは省略する。Javaのキーストア関連の知識がないと少し面倒かもしれない。

root@kaldi:/android/tmp# jarsigner -verbose android1.apk test  
Enter Passphrase for keystore: 
   adding: META-INF/MANIFEST.MF
   adding: META-INF/TEST.SF
   adding: META-INF/TEST.RSA
  signing: res/layout/main.xml
  signing: AndroidManifest.xml
  signing: resources.arsc
  signing: res/drawable-hdpi/icon.png
  signing: res/drawable-ldpi/icon.png
  signing: res/drawable-mdpi/icon.png
  signing: classes.dex

これでアプリケーションの改造は終了だ。android1.apkをインストールして文字列を入力し、Enterキーを押すと、その文字列はクリップボードにコピーされる。

以下参考リンク

JD Java Decompiler
http://java.decompiler.free.fr/

AXMLPrinter2.jar
http://code.google.com/p/android4me/downloads/detail?name=AXMLPrinter2.jar&can=2&q=

Smali/Baksmali
http://code.google.com/p/smali/

dex2jar
http://code.google.com/p/dex2jar/

冒頭のイメージで、実線は頼りになる変換、破線は頼りにならない変換を示す(リバースエンジニアリング時)。この画像はcacoo.comで作成した。cacoo.com最高すぎる。Ubuntuに移行してから、SmartDrawが使えなくなったのが悩みだったのだが、完全に解消された!


ブログ開始

今年からブログをやることにする。

本当はブログ自体のウェブアプリを自作したいのだが、多忙なのでそれは来年以降の課題とする。