Android のバックアップファイルを展開する
Android 4.0 以降?でサポートされた adb backup
で生成されるファイルを展開してみた。
AOSP とか読まずにネットで拾った情報からだが,
という構造らしい。
# backup2tar.py import sys, zlib buflen = 4096 f = open(sys.argv[1], 'rb') try: if f.readline() != "ANDROID BACKUP\x0a": raise IOError, "Invalid backup file" if f.readline() != "1\x0a": raise IOError, "Invalid version number" if f.readline() != "1\x0a": raise IOError, "Invalid version number" if f.readline() != "none\x0a": raise IOError, "Encrypted backup file is not supported" z = zlib.decompressobj() while True: buf = f.read(buflen) if len(buf) <= 0: break decompressed = z.decompress(buf) sys.stdout.write(decompressed) decompressed = z.flush() sys.stdout.write(decompressed) sys.stdout.flush() finally: f.close()
tar による展開まではやっていないので,
$ python backup2tar.py backup_file | tar xvf -
のように tar コマンドと組み合わせて使う。
tar アーカイブの中身は,基本的に対象パッケージの /data/data
ディレクトリの内容物をまとめたものであるようだ。ただし _manifest
ファイルの構造はわからなかった。
Python の tarfile
モジュールを使えば tar 部分までやってやれなくはないのだが,uid / gid 等の情報*1を失うのがイヤなので,とりあえず tar 形式に変換する部分まで作った。
逆に tar アーカイブからバックアップ形式に変換するスクリプトはこちら。
# tar2backup.py import sys, zlib buflen = 4096 f = open(sys.argv[1], 'rb') try: sys.stdout.write("ANDROID BACKUP\x0a1\x0a1\x0anone\x0a") z = zlib.compressobj(9) while True: buf = f.read(buflen) if len(buf) <= 0: break compressed = z.compress(buf) sys.stdout.write(compressed) compressed = z.flush() sys.stdout.write(compressed) sys.stdout.flush() finally: f.close()
Acer Iconia TAB A500 に Stock ROM 3.2 をインストール
基本的に個別の端末については言及しないつもりでいたのだが,ネタもないことであるし,これからは書いていくことにする。
A500 にはさまざまなカスタム ROM がでまわっているのでそれを使えばまぁそれほど不自由はない (らしい) のだが,システムがどのように構成されているのかということを捉えるうえでも*1日本版オフィシャル ROM 3.2 (Honeycomb) をインストールすることにする。
アップデータの抜き出し
システムアップデータのダウンロードをおこなう (そしてアップデータの適用まではおこなわない) と,/cache/fota_dn/
ディレクトリ*3以下に B992.....zip
のような16進数の羅列のファイルが保存される。
末尾が zip であるが,実際には暗号化されたファイルとなっており,そのままでは zip アーカイバでは展開できない。
「暗号化」といっても単に全バイト 0x12
で XOR されているだけなので,たとえば下記のような簡単なプログラムで復号できる。
#include <stdio.h> int main(int argc, char *argv[]) { int c; while ((c = getchar()) != EOF) putchar(c ^ 0x12); return 0; }
復号したファイルを展開すると,中に update.zip
と AMSS.zip
((AMSS.zip
は現状ではダミーファイルとなっている。)) とほかいくつかのファイルがある。update.zip
が Android におけるいわゆる update.zip
となっている。
公式 3.2 アップデートで注意が必要なところ
そもそも 3.2 にアップデートするためだけに update.zip
を抽出する必要は本来はない。だが,公式版 3.2 にそのままアップデートすると下記のような弊害を生ずる。
- (現時点で) 3.2 でブートした状態で root を取得する手段がない (ほんと?詳しくないからよく知らない)
- 新ブートローダがインストールされてしまう
- この新ブートローダでは公式の boot や recovery しかブートすることができない
update.zip
に含まれる bootloader.blob
というファイルが新ブートローダでのイメージであり,標準のアップデートスクリプトではこれが書きこまれてしまう((スクリプトでは /dev/block/mmcblk0p4
(/cache
パーティション) に書き込んでいるが,おそらくそこに特定パターン (MSM-RADIO-UPDATE
か?) のデータが書きこまれている場合,ブート時にブートローダに書きこむようになっているのだろう。))。
よって update.zip
の META-INF/com/google/android/updater-script
に下記のようなパッチをあてる。
--- updater-script.orig +++ updater-script @@ -2,7 +2,6 @@ format("ext4", "EMMC", "/dev/block/mmcblk0p3", "0"); mount("ext4", "EMMC", "/dev/block/mmcblk0p3", "/system"); mount("ext4", "EMMC", "/dev/block/mmcblk0p8", "/data"); -package_extract_dir("recovery", "/system"); package_extract_dir("system", "/system"); symlink("ash", "/system/bin/sh"); symlink("toolbox", "/system/bin/cat", "/system/bin/chmod", @@ -37,7 +36,6 @@ set_perm(1002, 1002, 0440, "/system/etc/dbus.conf"); set_perm(1014, 2000, 0550, "/system/etc/dhcpcd/dhcpcd-run-hooks"); set_perm(0, 2000, 0550, "/system/etc/init.goldfish.sh"); -set_perm(0, 0, 0544, "/system/etc/install-recovery.sh"); set_perm_recursive(0, 0, 0755, 0555, "/system/etc/ppp"); set_perm_recursive(0, 2000, 0755, 0644, "/system/vendor"); set_perm(0, 0, 0644, "/system/vendor/fake.txt"); @@ -54,8 +52,5 @@ package_extract_dir("calibration", "/tmp/"); run_program("/system/bin/sh", "/tmp/calibration.sh"); package_extract_file("flexrom.img", "/dev/block/mmcblk0p6"); -package_extract_file("bootloader.blob","/tmp/bootloader.blob"); unmount("/cache"); -format("ext4","EMMC","/dev/block/mmcblk0p4","0"); -run_program("/system/bin/dd","if=/tmp/bootloader.blob","of=/dev/block/mmcblk0p4"); unmount("/system"); \ No newline at end of file
ついでに recovery パーティションの上書きを防ぐため,/system/etc/install-recovery.sh
の駆動まわりも抑制してある。
個人的にはさらに /system/bin/su
あたりも仕込んでおきつつ set_perm(0, 0, 04755, "/system/bin/su");
も追加した。
編集した後ふたたび update.zip
にまとめなおして CWM recovery から適用した(update.zip
の署名がベンダのものではなくなってしまうので,署名を無視することができる CWM など公式以外のリカバリからインストールする必要がある)。これで無事ブートローダはさわらずに 3.2 の stock ROM をインストールすることができた。
(旧) ブートローダについて
新ブートローダでは boot パーティションや recovery パーティションの署名をチェックして,きちんと署名されていないイメージではブートできないようになっているらしい (伝聞*4 )。
ここでは 3.1 以前で利用されていた旧ブートローダについて解説する。
旧ブートローダは boot パーティションや recovery パーティションを読み込む際に,そのパーティションのチェックサムを算出し,あらかじめ (/dev/block/mmcblk0p7
に) 格納されているチェックサムと異なる場合には,エラーを出して読み込まない (すなわちブートしない) ようになっている。
チェックサムのアルゴリズムは不明だが,実はチェックサム格納エリアに「特殊なパターン」を書きこんでおくと,ブート時にブートローダがパーティションのチェックサムを算出し書きこんでくれるという (ある意味) 親切な設計になっていた (というのも,そもそも 3.1 以前の純正アップデータでもその機構を利用していたから)。
この機構を利用して,ブート時チェックサム自動計算モードにしてくれるプログラムがいわゆる「itsmagic」である (http://forum.xda-developers.com/showthread.php?t=1121543 参照)。
A500 用の CWM では,(カスタム) アップデータ適用時やリカバリ時などに自動的に itsmagic を実行してくれるので,現在ではユーザが意識して起動する必要はなくなっている (推測)。
itsmagic を解析したところ,「特殊なパターン」として /dev/block/mmcblk0p7
の 132 バイト目から 64 バイト分に
00 fb 30 94 99 01 4f 97 2e 4c 2b a5 18 6b dd 06
という16バイト×4 を書き込んでいるようだ (適当な解析結果なので間違っている可能性はある)。どのエリアがどのパーティションに対応しているのかはわからない。
おまけ
公式 3.2 にあげてしまった場合,http://forum.xda-developers.com/showthread.php?t=1276227 の手順に従えば旧来の ROM 等に戻すことができる,らしい。
*1:まぁそれは建前で単にカスタム ROM を漁ったり入れ替えたりするのが面倒なだけだけど。
*2:後述するように p4 はブートローダのアップデート用にも用いられるようだ。
*4:http://forum.xda-developers.com/showthread.php?p=18522844#post18522844 参照
*5:公式の鍵で署名されていることから,おそらく純正 3.0.1 firmware updater にもブートローダが含まれていたのだろう。アップデータスクリプトをちらりとみてみたが,froyo 時代の標準アップデータスクリプトと異なり,リビジョンチェックをもともと行なっていないようで,公式 ROM でも ダウングレードできるようだ。
APK ファイルの署名の仕様
APK ファイル (や一部の jar ファイル) において利用される「署名」の内部仕様について解説し,およびアルゴリズム的に OpenSSL 等を利用して自力で署名をおこなってみる。誰得企画であるが,せっかく調査した (そして署名つき JAR ファイルの仕様のひどさに吃驚した) のでメモとして残しておく。
はじめにざっくりいうと,APK ファイルの署名の仕様は Java における JAR ファイルの署名 (のサブセット) に独自仕様を追加したものである。
build/tools/signapk/SignApk.java を読むのが一番わかりやすい。
以下のサンプルでは build/target/product/security/ に存在する testkey.pk8 と testkey.x509.pem*1 を署名用鍵・証明書として利用する。
サンプルなのでこのようなテスト用キーを利用したが,本来であれば自分用の鍵・証明書を development/tools/make_key や keytool を利用して作成するべきである。
まずはじめに,秘密鍵が PKCS #8 形式のままでは,OpenSSL でのとりまわしが不便なので,openssl pkcs8
コマンドで一般的な PEM 形式に変換しておく。
$ openssl pkcs8 -in testkey.pk8 -inform DER -nocrypt -out testkey.pem
APK ファイルなどを解凍すればわかるが,アーカイブ内の
- META-INF/MANIFEST.MF
- META-INF/CERT.SF
- META-INF/CERT.RSA
これらのファイルが署名にまつわるファイルである。
普通に署名する方法
あまり本文書の守備範囲ではないが,一応書いておく。
コマンドラインから APK ファイル等を署名するには,AOSP の build/tools/signapk/ 以下をビルドして生成されるコマンド signapk.jar を用いる。
$ java -jar signapk.jar Usage: signapk [-w] publickey.x509[.pem] privatekey.pk8 input.jar output.jar
-wオプションを付与すると後述するアーカイブ全体の署名も埋め込むことになる。
もちろん Java に付属する jarsigner を利用してもよい*2。ただ,jarsigner ではアーカイブ全体の署名はおこなうことはできない。
META-INF/MANIFEST.MF の構造と生成
ファイルを覗いてみると以下のような構造になっている。
Manifest-Version: 1.0 Created-By: 1.0 (Android) Name: res/layout/main.xml SHA1-Digest: Xal5w1XkBBgw1JtbLohBa8RxDDk= Name: res/drawable-ldpi/icon.png SHA1-Digest: i7vxaosoiS+9HzKB7ZgIsXMYRLY= Name: AndroidManifest.xml SHA1-Digest: 8Op8I2+2AKZZS9CpAzTnwi7zidU= ...... (後略)
各ファイルの SHA1 digest (の BASE64 形式) を格納しているだけである。
後述するスクリプトで生成した例。
$ perl make-manifest.pl hello > MANIFEST.MF $ cat MANIFEST.MF Signature-Version: 1.0 Created-By: 1.0 (Android SignApk) Name: AndroidManifest.xml SHA1-Digest: 8Op8I2+2AKZZS9CpAzTnwi7zidU= Name: resources.arsc SHA1-Digest: iAvBaNTIddA6lRk1v+SAf8IKH+M= ......
おおもとは各エントリは Java の Map のキー列挙順になってしまうので,この Perl スクリプトによる出力 (辞書順) とは順序が異なってしまう。あくまでコンセプトコードとしてみること。
META-INF/CERT.SF の構造と生成
はっきりいって (署名という意味では) 存在意義のまるでないファイル。
2011-08-19 追記: JAR ファイルの仕様としては複数人が別々に署名することを想定して .SF ファイル (signature file) を用意したようです。また,必ず CERT.SF / .RSA という名前である必要はありません*3。もともと別々の JAR ファイルを一つに結合した場合などには便利かもしれません (が,Android プログラミングにおいて登場することはないでしょう)。
META-INF/MANIFEST.MF さえあれば再生成できる (RSA 暗号鍵などは必要ない)。
Signature-Version: 1.0 Created-By: 1.0 (Android) SHA1-Digest-Manifest: sGz/74W/RDIlQzFdV1n7XqlvR3Y= Name: res/layout/main.xml SHA1-Digest: aQj8hpWCMdsFwVuVLtqolo9seCQ= Name: AndroidManifest.xml SHA1-Digest: h+gwFCRgDwq0hbqvYNt1UyLYiHg= ...... (後略)
アルゴリズムがいささかややこしいが,
- まず SHA1 digest の演算器を初期化する
- META-INF/MANIFEST.MF の SHA1 digest を計算し,SHA1-Digest-Manifest: 属性として出力する
- 『digest 演算器のステートはそのまま』
- 各 MANIFEST.MF のエントリの SHA1 digest を計算して SHA1-Digest: 属性として出力していく
末尾に付した make-cert-sf.pl のコード (あるいは SignApk.java) を読むほうがわかりやすと思う。
$ perl make-cert-sf.pl MANIFEST.MF > CERT.SF $ cat CERT.SF Created-By: 1.0 (Android SignApk) SHA1-Digest-Manifest: mJEA5BvICy4RRe2EzFxbYp/ndT0= Name: AndroidManifest.xml SHA1-Digest: h+gwFCRgDwq0hbqvYNt1UyLYiHg= Name: resources.arsc SHA1-Digest: z0OgdzNF/68Zor8TmNbhE97X2S0= ......
META-INF/CERT.RSA の構造と生成
上述の META-INF/CERT.SF を RSA 暗号鍵と X.509 公開鍵証明書で署名して PKCS #7 形式で表現したものが META-INF/CERT.RSA である。
openssl pkcs7 コマンドには署名する機能はないので openssl smime コマンドをなぜか使う。
$ openssl smime -sign -inkey testkey.pem -signer testkey.x509.pem -in CERT.SF -outform DER -noattr > CERT.RSA
既存の META-INF/CERT.RSA の解析
ちなみに openssl pkcs7 コマンドを使うと署名 META-INF/CERT.RSA に用いられた (署名者の公開鍵) 証明書を抜き出すことができる (署名に必要な秘密鍵は当然抜き出すことはできない)。
てっとりばやく誰が署名したのかを見るには -noout -print_certs オプションをつけて実行する。
$ openssl pkcs7 -inform DER -in META-INF/CERT.RSA -noout -print_certs subject=/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com issuer=/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com
より詳細に証明書をみたい場合は -text オプションをつける。
$ openssl pkcs7 -inform DER -in META-INF/CERT.RSA -noout -print_certs -text Certificate: Data: Version: 3 (0x2) Serial Number: 93:6e:ac:be:07:f2:01:df Signature Algorithm: sha1WithRSAEncryption Issuer: C=US, ST=California, L=Mountain View, O=Android, OU=Android, CN=Android/emailAddress=android@android.com Validity Not Before: Feb 29 01:33:46 2008 GMT Not After : Jul 17 01:33:46 2035 GMT Subject: C=US, ST=California, L=Mountain View, O=Android, OU=Android, CN=Android/emailAddress=android@android.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public Key: (2048 bit) Modulus (2048 bit): ...... (後略)
証明書自体を PEM 形式で抜き出すには -print_certs オプションだけもちいる。
$ openssl pkcs7 -inform DER -in META-INF/CERT.RSA -print_certs subject=/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com issuer=/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com -----BEGIN CERTIFICATE----- MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD ...... (後略)
この (BEGIN CERTIFICATE 以降の) 証明書は署名に利用した testkey.x509.pem と同一である (あたりまえ)。
アーカイブ全体の署名
ここまでに述べた仕様は Java 署名付き JAR ファイルのサブセット*4仕様であるが,Android APK の署名独自仕様がある。それがアーカイブ全体の署名である。
ただしオプショナルであり,実際にはアプリケーションパッケージ (APK) 等では利用されていない。現状では OTA アップデータアーカイブの update.zip においてのみ利用されている*5。
アーカイブ全体の署名は,簡単にいうと (署名自体を除く) アーカイブ全体を RSA 暗号鍵と X.509 公開鍵証明書で署名して PKCS #7 形式で ZIP ファイルのコメント欄に格納している。
ZIP のコメント欄の仕様は,EOCD レコード以降にそのまま格納されているだけである*6が,アーカイブの署名は具体的には下記のように格納されている。
...... (前略) {central directory file header} ...... (中略) {EOCD record} (end of central directory) "signed by SignApk" [00] (comment; not checked) {signature in PKCS#7 format} [XX] [XX] (signature offset from file end in 2 bytes) [FF] [FF] [XX] [XX] (comment block size in 2 bytes)
なぜこのようなやや複雑な構造になっているかというと,署名検査ルーチンがアーカイブの末尾のみ読み込んで簡便に署名部分にアクセスできるようにである (後述する cut-wfsig.pl を参照のこと)。
この全体署名の算出のためには,まずは対象となる update.zip を2バイト*7切り詰めて,それへの RSA 署名を算出する。
$ ls -l update.zip -rw-r--r-- 1 urandroid 159336 2011-08-04 15:49 update.zip $ cp update.zip body.zip $ truncate -s 159334 body.zip # 2バイト少なくする $ ls -l body.zip -rw-r--r-- 1 urandroid 159334 2011-08-04 16:37 body.zip $ openssl smime -sign -inkey testkey.pem -signer testkey.x509.pem -in body.zip -outform DER -noattr -binary > whole.sig
あとは body.zip に whole.sig のサイズを2バイトで埋め込み,先ほどのフォーマットに従って署名を格納すれば,アーカイブ全体署名が完成する。
スクリプト群
make-manifest.pl
ファイルツリーから META-INF/MANIFEST.MF を生成するスクリプト。
#!perl use strict; use warnings; use File::Find; use File::Spec::Functions qw( splitpath splitdir ); use Digest::SHA; use MIME::Base64; my $CRLF = "\r\n"; print "Signature-Version: 1.0", $CRLF; print "Created-By: 1.0 (Android SignApk)", $CRLF, $CRLF; my $target_dir = shift; find(\&each_node, $target_dir); exit; sub each_node { my $file = $_; return if ! -f $file; my ($vol, $d, $f) = splitpath $File::Find::name; my @dirs = grep { $_ ne '' } splitdir $d; my $name = join '/', @dirs, $f; $name =~ s{\A \Q$target_dir\E /? }{}xms; return if $name eq 'META-INF/MANIFEST.MF'; return if $name eq 'META-INF/CERT.SF'; return if $name eq 'META-INF/CERT.RSA'; printf "Name: %s%s", $name, $CRLF; printf "SHA1-Digest: %s%s", sha1_for_file($file), $CRLF; print $CRLF; } sub sha1_for_file { my $file = shift; my $sha = Digest::SHA->new("SHA1"); $sha->addfile($file); return encode_base64($sha->digest(), ""); }
make-cert-sf.pl
META-INF/MANIFEST.MF から META-INF/CERT.SF を生成するスクリプト。
#!perl use strict; use warnings; use Digest::SHA; my $manifest = shift; my $CRLF = "\r\n"; print "Signature-Version: 1.0", $CRLF; print "Created-By: 1.0 (Android SignApk)", $CRLF; my $md = Digest::SHA->new('SHA1'); $md->addfile($manifest); print "SHA1-Digest-Manifest: ", base64($md->digest), $CRLF, $CRLF; open my $file, '<', $manifest or die $!; while (my $line = <$file>) { chomp $line; last if $line =~ m{\A \s* \z}xmso; } my $name = ""; while (my $line = <$file>) { $line =~ s{ [\r\n]+ $}{}gxmso; # custom chomp if ($line eq "") { $md->add($CRLF); print "Name: $name", $CRLF; print "SHA1-Digest: ", base64($md->digest), $CRLF, $CRLF; next; } die $line if $line !~ m{^ (\S+) \s* : \s* (.*?) \s* $}xmso; my ($key, $data) = ($1, $2); if (lc $key eq 'name') { $name = $data; } $md->add("$key: $data" . $CRLF); } close $file; exit; use MIME::Base64; sub base64 { return encode_base64($_[0], ""); }
cut-wfsig.pl
アーカイブ全体への署名が埋め込まれた update.zip 等から署名単体を切り出すスクリプト。PKCS #7 形式なので openssl pkcs7 コマンドでいろいろみることができる。
#!perl use strict; use warnings; use autodie; my $buf; my $filename = shift; open my $file, '<', $filename; seek $file, -6, 2; read $file, $buf, 2; my $offset_sig = unpack 'v', $buf; printf {*STDERR} "signature offset = %d\n", $offset_sig; seek $file, -$offset_sig, 2; read $file, $buf, $offset_sig - 6; close $file; print $buf;
*1:ちなみにこれらの鍵や証明書 testkey.pk8 と testkey.x509.pem は ClockworkMod Recovery などのカスタムリカバリーで update.zip の署名チェックに用いられる鍵・証明書である。
*2:この場合,鍵・証明書ペアではなくキーストアを利用することになる。
*3:Android においても署名ファイルの名前は CERT.SF / .RSA に決め打ちにはなっていない。また,検証側では RSA だけでなく DSA もサポートするようだ。(ただし PGP はサポートしていない)
*4:マニフェストに限定された属性しか利用されていない点と,デジタル署名に RSA しか使えない点でサブセットである。
*5:逆に,update.zip においてはこの全体署名しか署名チェックされない。META-INF/CERT.RSA などはアーカイブ内に存在していなくても構わない。
*6:もちろん EOCD 自体にコメント長などを指定しておく必要がある。
*7:ZIP ファイルのコメント長部分に該当する。
DEX 版 strings
DEX ファイルから可読文字列を抽出するコードを,ちょっとした必要があって書いた。
もちろん classes.dex に strings をかけるだけでも目的は達成されるが,DEX ファイルには文字列識別子を集めたセクションが存在するので,そこをダンプするとより正確に文字列だけを抽出することができる。
DEX ファイルのフォーマットについては AOSP 公式文書として http://source.android.com/tech/dalvik/dex-format.html を参照すればよい。その他 Dalvik VM のバイトコードなどについても公式の http://source.android.com/tech/dalvik/index.html からいくつか資料を読むことができる。
DEX ファイルでは文字列は MUTF-8 という UTF-8 をもとにした形式で保存されている。
- かならず1〜3バイトのエンコーディングとなっている (4バイト以上のものは使用されていない)
- そのかわり U+10000 〜 U+10FFFF の範囲のユニコード文字はサロゲートペアを用いてエンコードされる
- 文字列中の U+0000 (NUL) は2バイト (0xC0 0x80) で表記される
- 逆に単一バイトの NUL (0x00) は文字列終端子として用いられる
前半2つは,Java 由来のバイトコードらしく UTF-16 を基本としていることを示している。
後半2つは,末尾終端子としての 0 とは別に,文字列中に 0 (NUL) を含むことができることを示している。
逆に,これらの特徴をもつため厳密な意味では UTF-8 としては不正なシーケンスをとりうる *1。
本プログラムでは,面倒だったので,MUTF-8 エンコーディングの文字列をそのまま出力している。(サロゲートペアを含まない) 通常の範囲内の文字列であれば問題なく端末やエディタで見ることができるであろう。
#!perl use strict; use warnings; use autodie; open my $file, '<', shift; parse_handle($file); close $file; exit; sub parse_handle { my ($file) = @_; my $buffer; # seek $file, 0, 0; # magic # read $file, $buffer, 8; seek $file, 56, 0; # string_ids_size read $file, $buffer, 4; my $string_ids_size = unpack 'V', $buffer; read $file, $buffer, 4; my $string_ids_off = unpack 'V', $buffer; for my $i (0 .. $string_ids_size - 1) { seek $file, $string_ids_off + 4 * $i, 0; read $file, $buffer, 4; my $string_data_off = unpack 'V', $buffer; seek $file, $string_data_off, 0; my $str = parse_string_data_item($file); print $str, "\n"; } } sub parse_string_data_item { my ($file) = @_; my $buffer; my $utf16_size = parse_uleb128($file); my $str = q{}; while (1) { read $file, $buffer, 1; my $c = unpack 'C', $buffer; last if $c == 0; $str .= $buffer; } return $str; } sub parse_uleb128 { my ($file) = @_; my $n = 0; my $buffer; while (1) { read $file, $buffer, 1; my $c = unpack 'C', $buffer; $n = ($n << 8) + ($c & 0x7f); return $n if ! ($c & 0x80); } }
edify script 文法大全
注意: 以下の記述は Froyo (2.2) 時点のソースに基づく
リカバリモードのアップデータ (update.zip
) で使われるアップデータスクリプトは Donut (1.6) 以降 edify というミニスクリプトが採用されている。
edify の文法はおおまかに次のような特徴をもつ。
- 一見 C 風*1だが異なる部分も多い
- 型はおもに文字列。ほかに BLOB (ファイルの中身) と NULL も一応ある (NULL はほとんど使われていない)
- 変数はない
- ユーザ定義の関数やプロシージャもない
文法については bootable/recovery/edify/README
にて説明されている。
bootable/recovery/edify/README
の訳
donut 以降では edify という新しい簡易スクリプト言語でアップデートスクリプトを書く。
edify は以前のスクリプトシステム amend にぱっと見似ているが異なるものである。
edify の簡潔な概要:
- スクリプト全体が (訳注: 最終的に) 単一の「式」として評価される。
- すべての「式」の値は文字列である。
- 文字列リテラルはダブルクォートで表記される。
\n
,\t
,\"
,\\
のエスケープは適宜解釈される。
また\x4a
のような16進エスケープも解釈される (訳注: 10進エスケープは解釈されない)。 - 英数字, コロン, アンダーバー, スラッシュ, ピリオドのみからなるトークンは文字列として解釈されるのでダブルクォートは必要ない。
- ただし以下の単語は予約語である。
if then else endif
クォーテーションなしでは特別な意味をもつ。
(もちろんダブルクォートされたものは単に文字列リテラルとなる) - 真偽値として評価される場合,空文字列は「偽」であり,その他の文字列は「真」である。
(訳注: 慣例的に文字列 "t
" を「真」として返す関数が多い) - すべての (訳注: 組込) 関数は実際には (LISP 的意味合いで) マクロである。関数本体がどの引数を実際に評価するかコントロールすることができる。このことは関数が制御構造としても振る舞えることを示している。
訳注:foo(bar(), baz())
みたいな関数foo()
があった際,bar()
とbaz()
は実際に必要なときに評価されるということ。
もっとわかりにくいか。実際に存在する関数で具体例を書くと,ifelse("t", bar(), baz());
このコードではbar()
は評価されるけどbaz()
は評価されない。C などの言語では引数は関数の実行前に評価されてしまうよね。
いくつかの例:
- クォートされた文字列とクォートされていない文字列に区別はない。
文字列中にホワイトスペースを含めたいときにはクォーテーションは必要になる。
以下の式はすべて同じ文字列として評価される。
"a b" a + " " + b "a" + " " + "b" "a\x20b" a + "\x20b" concat(a, " ", "b") "concat"(a, " ", "b")
最後の例からもわかるように,関数名もただの文字列にすぎない。しかしながら関数名は単一の文字列「リテラル」でなくてはならない。
以下は間違い:
("con" + "cat")(a, " ", b) # 文法エラー
ifelse()
組込関数は3つの引数 (訳注: 後述のように2つでもよい) をとる。1つ目の引数の真偽によって2つ目か3つ目かの必ずどちらかの引数だけ評価する。if
/else
文のように見えるシンタックスシュガーも用意されている。
# 以下はすべて同じ意味 ifelse(something(), "yes", "no") if something() then yes else no endif if something() then "yes" else "no" endif
else 部分はオプショナルである。
if something() then "yes" endif # something() が偽の場合, # 評価値は偽となる ifelse(condition(), "", abort()) # condition() が偽のときのみ # abort() が呼ばれる
最後の例は以下と同義である。
assert(condition())
&&
と||
演算子は同時に使用することができる。いずれも右辺式は式の真偽値が必要となる場合だけ評価される。式全体の値は最後に評価された式の値となる:
file_exists("/data/system/bad") && delete("/data/system/bad") file_exists("/data/system/missing") || create("/data/system/missing") get_it() || "xxx" # get_it() の評価値が真の場合はそれを返す # さもなくば "xxx" を返す
- 「
;
」の目的はもちろん命令文((訳注: 原文は imperative statements。訳出が難しかったので訳者個人の考えを書く。edify 言語系自体は関数型言語的である。だがセミコロン演算子 (;
) を用いると,命令型プログラミング (≒手続き型言語) のように逐次実行的な記述を (機能的にも,見た目的にも) 行うことができる。というようなことをいいたいのではないかと思う。))の機能を提供するためである。だがこの演算子はどこでも使うことができる。
式全体の値は右辺の値となる:
concat(a;b;c, d, e;f) # 評価値は "cdf"
より有用な例:
ifelse(condition(), (first_step(); second_step();), # 2つ目の「;」はオプショナル alternative_procedure())
訳注: ifelse()
の2番目の引数は演算子の結合順位的には必ずしもカッコで囲う必要がない気がする
訳者による補足
文法を規定するレキサやパーサの設定は edify/lexer.l
および edify/parser.y
を読めばわかる。
- 「
#
」以降はコメントとなり無視される。 - ダブルクォーテーションされた文字列は途中で改行していても継続する。
- 空文字列以外は「真」として評価される。ホワイトスペースや改行や "
0
" も真である。 - ほとんどの組込関数において,整数値として評価される場合,基数 10 の
strtol()
を利用している。このため,これらの引数において8進数表記 (0123
) や16進数表記 (0xbeaf
) は使えない。 - BLOB (ファイルの中身) 型は評価時に文字列化されない。文字列等が必要な場面 (たいていの関数の引数など) で BLOB を与えるとエラーとなる。
### 演算子 expr + expr # 文字列として結合 expr == expr # 文字列として比較 expr != expr # 文字列として比較 expr && expr # 論理積; 値を返す; 左辺が偽の場合,右辺は評価されない expr || expr # 論理和; 値を返す; 左辺が真の場合,右辺は評価されない ! expr # 論理否定 ### 制御構造 if cond_expr then expr else expr endif # elsif 系はない; 自力でネストさせる必要あり if cond_expr then expr endif # else を省略した場合,cond_expr が偽のときの # 式の値は cond_expr の評価値となる # つまり偽のときは戻り値は実質 "" となる
組込関数
アップデータで利用するさまざまな機能が組込関数として実装してある*2。組込関数自体は RegisterFunction(name, method)
((bootable/recovery/edify/expr.c (353)
)) 関数で定義できるので,それで検索すればどのような組込関数が提供されているかわかる。
組込関数は以下の3箇所で登録されている。
- 言語系に組み込まれているもの((
bootable/recovery/edify/expr.c (384): RegisterBuiltins()
にて定義)) - アップデートバイナリ (
updater-binary
) に組み込まれているもの((bootable/recovery/updater/install.c (1006): RegisterInstallFunctions()
にて定義)) - ベンダ提供のもの
BoardConfig.mk
などでTARGET_RECOVERY_UPDATER_LIBS
に定義されたライブラリに記述されているもの- 各ライブラリに
Register_libname()
なる関数を実装し,その中で上述のRegisterFunction(name, method)
を呼び出して組込関数を登録するbootable/recovery/updater/Android.mk
や HTC のベンダライブラリlibrecovery_updater_htc
(のソースdevice/htc/common/updater/recovery_updater.c
) を参照のこと
言語系の組込関数
ifelse(cond, on_true, on_false) |
|
ifelse(cond, on_true) |
|
abort([msg]) |
abort |
assert(cond_a, cond_b, cond_c, ...) |
assert |
concat(expr_a, expr_b, expr_c, ...) |
文字列として連結 |
is_substring(search_pattern, whole_str) |
部分文字列判定 |
stdout(expr_a, expr_b, expr_c, ...) |
標準出力に出力 |
sleep(seconds) |
sleep |
less_than_int(expr_a, expr_b) |
expr_a < expr_b |
greater_than_int(expr_a, expr_b) |
expr_a > expr_b |
アップデートバイナリの組込関数
ファイルパスを受け取るほとんどの関数は,物理的にその時点でアクセス可能なパスである必要がある。つまり update.zip
アーカイブ内のファイルは対象とならない。ただし package_extract_dir()
, package_extract_file()
関数の第1引数はアーカイブ内のファイルパスを示す。
mount(type, location, mount_point) |
マウントする |
is_mounted(mount_point) |
マウントされているか判定 |
unmount(mount_point) |
アンマウントする |
format(type, location) |
MTD パーティションをフォーマットする |
show_progress(portion, sec) |
プログレスバーの表示 |
set_progress(frac) |
プログレスバーの進捗セット |
delete(path1, path2, ...) |
ファイル削除 |
delete_recursive(path1, path2, ...) |
ファイル・ディレクトリ削除 |
package_extract_dir(package_path, target_path) |
update.zip からディレクトリを展開 |
package_extract_file(package_path, target_path) |
update.zip からファイルを展開 |
package_extract_file(package_path) |
update.zip からファイルを展開して返す |
symlink(src, target1, target2, ...) |
シンボリックリンクを張る |
set_perm(uid, gid, mode, path1, path2, ...) |
パーミッションを設定 |
set_perm_recursive(uid, gid, dir_mode, file_mode, |
パーミッションを設定 再帰版 |
getprop(key) |
getprop |
file_getprop(filename, key) |
ファイルが対象の getprop |
write_raw_image(filename, partition) |
イメージファイルを MTD パーティションに書き込む |
apply_patch(src_file, target_file, target_sha1, target_size, |
差分の適用 |
apply_patch_check(file, sha1_1, sha2_1, ...) |
差分適用前チェック |
apply_patch_space(bytes) |
差分適用に必要な空き容量のチェック |
read_file(filename) |
ファイルを読み込み返す |
sha1_check(data) |
SHA1 の計算 |
sha1_check(data, sha1_hex, sha1_hex, ...) |
SHA1 のチェック |
ui_print(str1, str2, ...) |
recovery UI に表示 |
run_program(prog, arg1, arg2, ...) |
外部コマンドの実行 |
組込関数詳説
ifelse(cond, on_true, on_false)
略
ifelse(cond, on_true)
略
abort([msg])
評価した時点でアボートする。recovery updater 環境では,UI にメッセージ msg
を出力する。
assert(cond_a, cond_b, cond_c, ...)
与えられた全 cond
が真でない場合,アボートする。"assert failed: 偽となった部分" のようなメッセージをアボート出力する。
concat(expr_a, expr_b, expr_c, ...)
全引数を結合した文字列を返す。
is_substring(search_pattern, whole_str)
search_patter
が whole_str
の部分文字列なら真 ("t"
) を返す。
sleep(seconds)
指定された秒数 sleep する。
- 戻り値
seconds
less_than_int(expr_a, expr_b)
両辺が数値として評価される。expr_a < expr_b
なら真を返す。
greater_than_int(expr_a, expr_b)
両辺が数値として評価される。expr_a > expr_b
なら真を返す。
mount(type, location, mount_point)
指定されたパーティションを指定したファイルタイプでマウントする。マウントオプションはつけることができない。
type
に "MTD"
を指定すると,location
に MTD パーティション名 (userdata
など) を指定することができる。ただしこの場合,ファイルタイプは yaffs2 固定となる。
マウントの前にあらかじめ mkdir(mount_point, 0755);
しておいてくれる。またマウントに失敗しても fatal error とはならない。
- 戻り値
- 成功時は
mount_point
,失敗時は偽 (""
) - 例
mount("MTD", "system", "/system");
mount("vfat", "/dev/block/mmcblk0", "/sdcard");
is_mounted(mount_point)
現在マウントされているかどうかを返す。
- 戻り値
- 真の場合は
mount_point
,偽の場合は空文字列 (""
) - 例
is_mounted("/system")
unmount(mount_point)
指定されたマウントポイントをアンマウントする。
実は umount()
の成否はわからない
- 戻り値
is_mounted()
でない場合は偽 (""
),is_mounted()
の場合はmount_point
- 例
unmount("/system");
format(type, location)
指定された MTD パーティションをフォーマットする。
type
は "MTD" のみサポート。
- 戻り値
location
- 例
format("MTD", "system");
show_progress(portion, sec)
プログレスバーを表示する (後述)。
portion
は 0
〜 1.0
の小数値。sec
はバーが伸びるのにかかる時間 (秒数; 整数値)。
(ホスト側で実行)
- 戻り値
portion
- 例
show_progress(0.3, 10);
set_progress(frac)
プログレスバーの進捗を設定する (後述)。
frac
は 0
〜 1.0
の小数値。
(ホスト側で実行)
- 戻り値
frac
- 例
set_progress(0.8);
delete(path1, path2, ...)
指定された全パス(ファイルのみ)を削除する。
- 戻り値
- 正常に削除できた個数
- 例
delete("/tmp/foo.txt", "/tmp/bar.bin");
package_extract_dir(package_path, target_path)
指定されたZIP内ディレクトリを指定されたディレクトリに展開する。
ctime や mtime は必ず 2008/8/1 になるようだ。
- 戻り値
- 真偽値
- 例
package_extract_dir("system", "/system");
package_extract_file(package_path, target_path)
指定された ZIP 内ファイルを指定されたファイル名で展開する。
- 戻り値
- 真偽値
- 例
package_extract_file("hoge.txt", "/tmp/hoge.txt");
package_extract_file(package_path)
指定された ZIP 内ファイルを展開し内容を返す。
戻り値が VAL_BLOB
タイプなので sha1_check()
か apply_patch()
で使うしかない。
- 戻り値
- ファイルの内容 (
VAL_BLOB
タイプ); 失敗時はVAL_NULL
。 - 例
package_extract_file("hoge.txt")
symlink(src, target1, target2, ...)
target1
等が既存の場合は削除してシンボリックリンクを貼り直してくれる。
symlink(2)
と同様の挙動を示すことに注意。つまり,相対パスの場合,各ターゲットからの相対パスとなる (カレントディレクトリは関係ない)。
- 戻り値
- 空文字列 (
""
; 偽) - 例
symlink("busybox", "/system/bin/ls", "/system/bin/ps");
set_perm(uid, gid, mode, path1, path2, ...)
指定されたパス群にパーミッション, オーナ, グループを設定する。
めずらしく基数 0 の strtoul()
を使用しているので,引数には8進数記法(0777
等)や16進数記法(0x1ff
等)も使えるはず。
- 戻り値
- 空文字列 (
""
; 偽) - 例
set_perm(0, 0, 04755, "/system/bin/su");
set_perm_recursive(uid, gid, dir_mode, file_mode, path1, path2, ...)
指定されたパス群を再帰的にパーミッション等を設定する。ディレクトリとファイルで別のパーミッションを指定することが可能。
- 戻り値
- 空文字列 (
""
; 偽) - 例
set_perm_recursive(0, 0, 0755, 0644, "/system");
getprop(key)
getprop コマンドと同様。
- 例
getprop("ro.product.model")
file_getprop(filename, key)
指定されたファイルをプロパティファイルとみなしてプロパティ値を取得する。
- 例
file_getprop("/system/build.prop", "ro.build.id")
write_raw_image(filename, partition)
指定された MTD パーティションにファイルの内容を書きこむ。
- 戻り値
- 成功時
partition
- 例
write_raw_image("/sdcard/boot.img", "boot");
apply_patch(src_file, target_file, target_sha1, target_size, sha1_1, patch_1, ...)
指定された src_file
をもとに patch を差分適用して target_file
を生成する (applypatch
コマンドと同様)。
patch_1
等は VAL_BLOB
タイプである必要がある。つまり read_file()
や package_extract_file()
の戻り値を使う必要がある。
applypatch
と同じく src_file
や target_file
には MTD:partition_name
記法が使えるようだ (未確認)。
target_file
に「-
」を指定すると src_file
を上書きする。すでに target_sha1
の target_file
が存在する際はなにもしない。
- 戻り値
- 真偽値
- 例
apply_patch("foo.txt", "bar.txt", "cafe....", 1024, "beaf....", package_extract_file("patch.p"));
apply_patch_check(file, sha1_1, sha2_1, ...)
指定されたファイルの SHA1SUM に一致する引数があるか調べる。
- 戻り値
- 真偽値
- 例
apply_patch_check("foo.txt", "cafe....", "beaf....")
apply_patch_space(bytes)
bytes
バイト分の空き容量を /cache
パーティションに用意する。
- 戻り値
- 真偽値 (用意できなかった場合,偽)
- 例
apply_patch_space(65536);
read_file(filename)
ファイルを読み込んで内容を返す。ファイルを読み込めなかった場合アボートする。
ただし戻り値は VAL_BLOB
型なので sha1_check()
か apply_patch()
で使うしかない
- 戻り値
- ファイルの内容 (
VAL_BLOB
型) - 例
read_file("/system/etc/hosts")
sha1_check(data)
data
の SHA1SUM を算出して返す。
data
は通常の文字列型の値でも VAL_BLOB
型の値でもどちらでもよい。
- 戻り値
- SHA1 digest 値
- 例
assert( sha1_check(read_file("/system/etc/hosts")) == "..." );
sha1_check(data, sha1_hex1, sha1_hex2, ...)
data
の SHA1SUM を算出し,後続の sha1_hex
にマッチするものがあるかどうか調べる。
- 戻り値
- 真偽値
- 例
sha1_check(read_file("/system/etc/hosts"), "a1..", "3e..");
ui_print(str1, str2, ...)
recovery UI に文字列を出力する。
(ホスト側で実行)
- 戻り値
- 結合された文字列
- 例
ui_print("Hello, World!\n\nThis is test.", "foo bar");
run_program(prog, arg1, arg2, ...)
外部プログラムを実行する。
- 戻り値
- 終了コード
- 例
run_program("/system/bin/sleep", "3");
show_progress()
と set_progress()
について
プログレスバーまわりは update-binary
側ではなく,ホスト側 (recovery
) で実際に処理をおこなっているので,挙動を知るにはホスト側のコード((show_progress()
はホスト側のコード install.c (123): try_update_binary()
で駆動されている。実際の表示にまつわるコードは ui.c (358): ui_show_progress()
に存在する。set_progress()
はホスト側のコード install.c (132): try_update_binary()
で駆動されている。実際の表示にまつわるコードは ui.c (371): ui_set_progress()
に存在する。))を読む必要がある。
両者の違いはわかりにくい。show_progress(portion, sec)
はこれから行う作業量の全体に対するだいたいの割合 (portion
) と見積もっている時間 (sec
) を指定する((そうするとプログレスバーが sec
秒をかけて portion
分徐々に伸長していく。))。set_progress(frac)
はその作業の途中でどこまで作業が進んだかを指定する。複数 show_progress()
を設定する場合,その portion
の和は 1.0
にするべきである。
実例をあげる。
### 20秒ほどかかる作業; 全体の50% show_progress(0.5, 20); ... # ちょっとした作業 set_progress(0.3); ... # ちょっとした作業 set_progress(0.7); ... # ちょっとした作業 (以下略) set_progress(1.0); ### 5秒ほどかかる作業; 全体の20% show_progress(0.2, 5); set_progress(0.2); set_progress(0.4); set_progress(0.6); set_progress(1.0); ### 10秒ほどかかる作業; 全体の30% show_progress(0.3, 10); set_progress(0.1); set_progress(0.9); set_progress(1.0);
show_progress()
すると前回のプログレスバーの最終端までジャンプするので,実際には set_progress(1.0)
は (最後のものを除いて) 必要ない。
また細かいことであるが,show_progress()
の portion
の和を 1.0
にするようにと書いたが,実際のアップデートプロセスの前にアップデートパッケージ (update.zip
) の検証フェーズがあり,その検証フェーズが全体の 25% であるとデフォルトで設定((VERIFICATION_PROGRESS_FRACTION = 0.25
, TIME = 60
))されている。よって,show_progress(0.4, 10);
のように指定した場合,指定したフェーズのプログレスバーは全体の 40% 分伸長するのではなく 40% * 75% = 30% 分伸長する。なんにしても portion
の総和が 1.0
となるように設計するべきであることには変わらない。
apply_patch()
について
apply_patch( "source.img", "target.img", "cafe...", 1024, "babe...", "patch1.p", "beef...", "patch2.p" );
のようなスクリプトになっていた場合,
- すでに
target.img
が存在し,その SHA1SUM がcafe...
の場合,パッチ適用はスキップされる (終了; 真値) source.img
の SHA1SUM がbabe...
の場合,patch1.p
を適用してtarget.img
を生成する。適用後の SHA1SUM がcafe...
の場合,終了。違う場合,エラー終了 (偽値)source.img
の SHA1SUM がbeef...
の場合,patch2.p
を適用してtarget.img
を生成する。適用後の SHA1SUM がcafe...
の場合,終了。違う場合,エラー終了 (偽値)- 適用可能なパッチがない場合,エラー (偽値)
順番にパッチを当てたりするわけではないことに注意が必要である。ひとつパッチを当てた時点で,要求する target の SHA1SUM にならなければエラーとなる。
スクリプト例
Android 標準ビルドキットで生成*4したアップデータスクリプトを以下に掲載する。build/tools/releasetools/ota_from_target_files
という (Python 製) ツールの def WriteFullOTAPackage()
で生成されている。
assert(!less_than_int(1305679443, getprop("ro.build.date.utc"))); assert(getprop("ro.product.device") == "generic" || getprop("ro.build.product") == "generic"); show_progress(0.500000, 0); format("MTD", "system"); mount("MTD", "system", "/system"); package_extract_dir("recovery", "/system"); package_extract_dir("system", "/system"); symlink("toolbox", "/system/bin/cat", "/system/bin/chmod", "/system/bin/chown", "/system/bin/cmp", "/system/bin/date", "/system/bin/dd", "/system/bin/df", "/system/bin/dmesg", "/system/bin/getevent", "/system/bin/getprop", "/system/bin/hd", "/system/bin/id", "/system/bin/ifconfig", "/system/bin/iftop", "/system/bin/insmod", "/system/bin/ioctl", "/system/bin/ionice", "/system/bin/kill", "/system/bin/ln", "/system/bin/log", "/system/bin/ls", "/system/bin/lsmod", "/system/bin/mkdir", "/system/bin/mount", "/system/bin/mv", "/system/bin/nandread", "/system/bin/netstat", "/system/bin/newfs_msdos", "/system/bin/notify", "/system/bin/printenv", "/system/bin/ps", "/system/bin/reboot", "/system/bin/renice", "/system/bin/rm", "/system/bin/rmdir", "/system/bin/rmmod", "/system/bin/route", "/system/bin/schedtop", "/system/bin/sendevent", "/system/bin/setconsole", "/system/bin/setprop", "/system/bin/sleep", "/system/bin/smd", "/system/bin/start", "/system/bin/stop", "/system/bin/sync", "/system/bin/top", "/system/bin/umount", "/system/bin/vmstat", "/system/bin/watchprops", "/system/bin/wipe"); set_perm_recursive(0, 0, 0755, 0644, "/system"); set_perm_recursive(0, 2000, 0755, 0755, "/system/bin"); set_perm(0, 3003, 02750, "/system/bin/netcfg"); set_perm(0, 3004, 02755, "/system/bin/ping"); set_perm(0, 2000, 06750, "/system/bin/run-as"); set_perm(1002, 1002, 0440, "/system/etc/dbus.conf"); set_perm(1014, 2000, 0550, "/system/etc/dhcpcd/dhcpcd-run-hooks"); set_perm(0, 2000, 0550, "/system/etc/init.goldfish.sh"); set_perm(0, 0, 0544, "/system/etc/install-recovery.sh"); set_perm_recursive(0, 0, 0755, 0555, "/system/etc/ppp"); set_perm_recursive(0, 2000, 0755, 0755, "/system/xbin"); set_perm(0, 0, 06755, "/system/xbin/librank"); set_perm(0, 0, 06755, "/system/xbin/procmem"); set_perm(0, 0, 06755, "/system/xbin/procrank"); set_perm(0, 0, 06755, "/system/xbin/su"); set_perm(0, 0, 06755, "/system/xbin/tcpdump"); show_progress(0.200000, 0); show_progress(0.200000, 10); assert(package_extract_file("boot.img", "/tmp/boot.img"), write_raw_image("/tmp/boot.img", "boot"), delete("/tmp/boot.img")); show_progress(0.100000, 0); unmount("/system");
リカバリイメージの解体と再構築
フラッシュメモリストレージへの直接アクセス - 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.c
の HEADER_SIZE
(デフォルトで 2048 バイト) サイズ分が一致するイメージは無視される*5。この場合,logcat 等に
header is the same, not flashing ......
のようなログが出力される。
またイメージファイルを rewind する手間をおしんでか,標準のコードでは一度先頭から HEADER_SIZE
分を zero fill して後半を書き込んだあとに,ふたたび先頭の HEADER_SIZE
ぶんを書き込んでいる。意味合いはよくわからない。(2011-11-30 追記: 書き込みに失敗した場合,再度書き込めなくなる (すでに新しいイメージが書かれていると誤解される) ことを防ぐためではないかと推察。)
余談: applypatch
と imgdiff
実は大部分のプロダクトでは,ブートイメージからリカバリイメージを (必要に応じて) 生成して書き込んでいる。ブートイメージとリカバリイメージは (とくに 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 のサイズがかわるだけでヘッダ部分が変更されるので,通常の場合気にする必要はない。
bootanimation について
bootanimation の機構
/init.rc
の下記部分でブートロゴアニメーションサービスを起動している。
service bootanim /system/bin/bootanimation user graphics group graphics disabled oneshot
/system/bin/bootanimation
のソースは,Android のソースツリーの frameworks/base/cmds/bootanimation/
ディレクトリに存在する (git ツリー)。
bootanimation の抑制
上記ソース (の [https://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=cmds/bootanimation/bootanimation_main.cpp;hb=HEAD:title=bootanimation_main.cpp]
) を読むと debug.sf.nobootanimation
というプロパティが 0 以外であれば起動が抑制されることがわかる。なのでこのプロパティを /data/local.prop
, /system/build.prop
あたりで指定してやればよい。
であるが,/system/etc/init.goldfish.sh
にて ro.kernel.android.bootanim
プロパティが 0 の際に当該プロパティを setprop にて設定しているので,ro.kernel.android.bootanim=0
を指定するほうが行儀がよいようだ*1。(2011-04-21 追記:ただし ro.*
プロパティなので /default.prop
に指示子がある場合そちらが優先されてしまう)
bootanimation のカスタマイズ
ソースによれば
/data/local/bootanimation.zip
が存在する場合,そのアニメーションを使う。/system/media/bootanimation.zip
が存在する場合,そのアニメーションを使う。- さもなくば
/system/framework/framework-res.apk
のres/assets/images/android-logo-mask.png
およびres/assets/images/android-logo-shine.png
を利用した簡易アニメーションを行う。
のようになっている。
なので,/data/local/bootanimation.zip
を用意してやればカスタマイズしたアニメーションを利用できる。(どうしてもっということであれば framework-res.apk
の中身を変更すれば簡易アニメーションもカスタマイズできるであろうが,bootanimation.zip
を用意する手間とシステムファイル書き換えのリスクを天秤にかけたらとてもおすすめはできない)
bootanimation.zip
の作り方については「bootanimation.zip
desc.txt
」あたりをキーワードにして Google 検索すればいくらかでてくる。英語だが http://forum.sdx-developers.com/themes/(how-to)-creating-a-bootanimation-zip/ あたりが細かい仕様までまとまっていると感じた (ちゃんと探していないが)。
以下補足。