リカバリイメージの解体と再構築

フラッシュメモリストレージへの直接アクセス - urandroid の続き。

なお,たいていの Android 端末ではリカバリイメージと通常のブートイメージは同じ構造をしている*1リカバリ時であっても Linux カーネルが立ち上がり,通常のブート時と異なる /init.rc ((この init.rc では,最小限の recovery プログラム (サービス) を起動する。必要がない限り /system パーティションや SD Card をマウントしない。))を実行するだけである。つまり,端末機器からみて通常起動とリカバリ起動の駆動の仕方はかわらない。どのパーティションを kernel + root ramdisk として利用するかが違うだけである((このため,一部の機種のハッカーは実験用のカーネルやシステムをリカバリパーティションに焼きこんでテストしたりするらしい。(なお fastboot が殺されていなければわざわざ焼きこまなくてもテストできるので,制限の厳しい環境下での話) ))。

このため,以下では「リカバリイメージ」という用語を使うが,通常のブートイメージ (root ファイルシステム) についても通用する。

ちなみに手元の実機では

% adb shell cat /proc/mtd | egrep '(boot|recovery)'
dev:    size   erasesize  name
mtd0: 00640000 00020000 "boot"
mtd2: 00640000 00020000 "recovery"

このように通常ブートイメージパーティションリカバリパーティションに同じサイズが割り振られている。

リカバリイメージの取得

MTD パーティションの生データは /dev/mtd/mtd* なるキャラクタデバイスノードからひっぱってくることができる。

# cat /dev/mtd/mtd2 > /mnt/sdcard/mtd2.raw

リカバリイメージの解体

リカバリイメージの構造は HOWTO: Unpack, Edit, and Re-Pack Boot Images - Android Wiki に詳しい*2

具体的な構造は文献にゆずるとして,概略としては,ヘッダ + kernel + root ramdisk である。root ramdisk とは Linux でいう initramfs と同じようなものであり,ルートファイルシステムを RAM 上に提供する*3。RAM 上に展開されるため (root 権限であれば) ツリーをいじることは可能だが,当然ながら永続化されない。

この kernel と root ramdisk の分離を行なってくれるプログラムが split_bootimg.pl である。上記文献にリンクが存在する。

% perl split_bootimg.pl mtd2.raw

Page size: 2048 (0x00000800)
Kernel size: 2842696 (0x002b6048)
Ramdisk size: 479585 (0x00075161)
Second size: 0 (0x00000000)
Board name: 
Command line: console=ttyHSL1,115200n8 ......
Writing mtd2.raw-kernel ... complete.
Writing mtd2.raw-ramdisk.gz ... complete.

実行例のように分離時にヘッダの情報も表示してくれる。この内容はのちほど再構築で利用する重要な情報なのできちんとメモをとっておくこと。

root ramdisk の展開

一般的な Linux の initramfs と同様,root ramdisk 部分は cpio アーカイブ (の gzip 圧縮) 形式になっている*4ので cpio コマンドで展開することができる。

% mkdir ramdisk

% cd ramdisk/

% zcat ../mtd2.raw-ramdisk.gz | cpio -i
1779 blocks

% ls
battery_logo.rle  dev   init.goldfish.rc  initlogo.rle  res   system
data              etc   init.qcom.rc      logo.rle      sbin  tmp
default.prop      init  init.rc           proc          sys

Android 使いには見覚えのあるファイルツリーとなっている。(機種によってより簡素になっていることもあるだろうが) リカバリモードであろうと通常ブートモードであろうと構造にそれほどの差異はない証左である。

root ramdisk image の再生成

ramdisk image は cpio アーカイブ形式なので cpio を利用すればふたたび ramdisk image を生成できると思われる。だが Android で確実に展開させるためには Android ソース添付の mkbootfs コマンドを利用する。mkbootfs コマンドは Android ソースをビルドした場合,out/host/linux-x86/bin/ ディレクトリ等に存在する。

% mkbootfs ./ramdisk | gzip -9 - > custom-ramdisk.gz

リカバリイメージの再構築

kernel と root ramdisk を結合するには Android ソース添付の mkbootimg コマンドを利用する。mkbootimg コマンドも out/host/linux-x86/bin/ ディレクトリ等に存在する。

さきほど使用した split_bootimg.pl の雑多な出力をもとに mkbootimg を駆動すればよいのだが,一点必要となる (かもしれない) 情報,base address については出力されない。

これは,元のイメージの先頭をダンプして調べることができる。

00000000  41 4e 44 52 4f 49 44 21  48 60 2b 00 00 80 00 20  |ANDROID!H`+.... |
00000010  61 51 07 00 00 00 00 21  00 00 00 00 00 00 f0 20  |aQ.....!....... |

0x0000000c 番地から「00 80 00 20」となっている。この (リトルエンディアンの) 値((この値 ――今回の例だと 0x2000_8000―― 自体はカーネルの展開されるアドレスである。))から 0x8000 を減算した値が base address となる。つまり上記例では 0x2000_0000 である (デフォルトの base address は 0x1000_0000)。

一応,非標準の base address 時に出力するよう改変した split_bootimg.pl への差分を以下に掲載する。

--- split_bootimg.pl.orig
+++ split_bootimg.pl
@@ -183,4 +183,5 @@
     printf "Board name: $name\n";
     printf "Command line: $cmdline\n";
+    printf "Base Address: 0x%08x\n", $k_addr - 0x8000  if $k_addr != 0x10008000;
 
     # Save the values


kernel と root ramdiskアーカイブを用意して,split_bootimg.pl の出力した情報をもとに mkbootimg を実行するとリカバリイメージが生成される。

% mkbootimg \
 --kernel mtd2.raw-kernel \
 --ramdisk custom-ramdisk.gz \
 --cmdline 'console=ttyHSL1,115200n8 ......' \
 --base 0x20000000 \
 -o recovery.img


(何も手を加えず) 再構築したリカバリイメージと元の吸いだしたイメージのサイズを比較してみる。

% ls -l
-rw-r--r-- 1 ... ... 6553600 2011-04-19 14:56 mtd2.raw
-rw-r--r-- 1 ... ... 3328000 2011-04-19 15:05 recovery.img

(当然ながら) リカバリパーティションよりは小さいイメージファイルとなっている。MTD から直接イメージを吸いだした場合は,一度 split_bootimg.pl で解体して再構築しておいたほうが安全かもしれない。

イメージの書き込み

/dev/mtd/mtd2 に直接 cat 等で書きこめばうまくできるはずであるが,Android は標準で MTD パーティションへの書き込みツールが添付されている。/system/bin/flash_image がそのプログラムである((添付されていない場合,Android ソースをビルドして out/target/product/generic/ あたりからとってくる。ソースツリー自体は bootable/recovery/mtdutils/ 以下に存在する。))。

これはたとえば下記のように実行する。

# flash_image recovery recovery.img

実行結果などのログはコンソールに表示されない。


注意点として,標準の flash_image ではフラッシュディスクへの無駄な焼きこみを防ぐため,現在のイメージと先頭がある程度共通する場合は書き込みされない。具体的には先頭から flash_image.cHEADER_SIZE (デフォルトで 2048 バイト) サイズ分が一致するイメージは無視される*5。この場合,logcat 等に

header is the same, not flashing ......

のようなログが出力される。

またイメージファイルを rewind する手間をおしんでか,標準のコードでは一度先頭から HEADER_SIZE 分を zero fill して後半を書き込んだあとに,ふたたび先頭の HEADER_SIZE ぶんを書き込んでいる。意味合いはよくわからない。(2011-11-30 追記: 書き込みに失敗した場合,再度書き込めなくなる (すでに新しいイメージが書かれていると誤解される) ことを防ぐためではないかと推察。)

余談: applypatchimgdiff

実は大部分のプロダクトでは,ブートイメージからリカバリイメージを (必要に応じて) 生成して書き込んでいる。ブートイメージとリカバリイメージは (とくに kernel 部分は) 共通しているので,差分も小さなものになる。これによりアップデート時にリカバリイメージを独立して用意する必要がなくなる。

このリカバリイメージの自動生成&焼きこみプログラムは /system/etc/install-recovery.sh に存在する。以下のような単純なスクリプトである。

#!/system/bin/sh
if ! applypatch -c MTD:recovery:2048:<現在の recovery block 先頭の SHA1SUM>; then
  log -t recovery "Installing new recovery image"
  applypatch MTD:boot:<boot block の SHA1SUM> MTD:recovery \
    <パッチ後の recovery block の SHA1SUM> <パッチ後の recovery block のサイズ> \
    <boot block の SHA1SUM>:/system/recovery_from_boot.p
else
  log -t recovery "Recovery image already installed"
fi

通常 Android システムをビルドしただけではこのスクリプトは生成されない。リリースツールの build/tools/releasetools/ota_from_target_files を incremental モード((-i (--incremental_from) スイッチを使用して ota_from_target_files コマンドを起動する。))で利用すると生成されるようだ。

イメージの差分パッチである recovery_from_boot.p ファイルは imgdiff コマンドを使って生成されている((imgdiff コマンドと差分適用に使う applypatch コマンドは,ベースとなるアルゴリズムとして bsdiff を利用しているらしい。))。

さて,このようにリカバリイメージを通常ブートイメージから生成しているプロダクトの場合,/init.rc 起動スクリプトにて下記のように oneshot の service (つまり常駐せず起動時に一度だけ実行される外部プログラム) として記述されていることが多い。

service flash_recovery /system/etc/install-recovery.sh
    oneshot

このため,それらのプロダクトでは仮にカスタムリカバリイメージを焼きこんだとしても,通常システムのブート時に通常リカバリイメージが上書きされてしまう。このような事象を防ぐには,単純には /system/etc/install-recovery.sh/system/etc/recovery_from_boot.p を削除 (あるいはリネーム) しておけばよい ((そのほうが /init.rc を書き換えるより簡単であろう。))。


なお apply_patch, imgdiff そして先ほど述べた flash_image のソースは Android ソースの bootable/recovery/ 以下に存在する。MTD への読み書きにまつわるコードが多く存在するので広く勉強になる。

*1:一部ではリカバリ時も通常ブート時と同じパーティションを使う機種もあるそうだ。

*2:そもそも今回の日記はこの記事をトレースしているだけであるが。

*3:ただし initramfs は仮の root であり後に実ディスク等でマウントし直しが発生するのに対し,Android の root ramdisk はこのまま最後までルートファイルシステムとして利用される。

*4:initramfs や initrd などのキーワードで検索すればいろいろ歴史や背景を知ることができる。ramfs, rootfs and initramfs 等も参照のこと。

*5:このためリカバリイメージをちょっと改変しただけでは焼きこまれない気がするが,実際には root ramdisk をちょっといじって root ramdisk のサイズがかわるだけでヘッダ部分が変更されるので,通常の場合気にする必要はない。