Android のバックアップファイルを展開する

Android 4.0 以降?でサポートされた adb backup で生成されるファイルを展開してみた。

AOSP とか読まずにネットで拾った情報からだが,

  • 先頭にヘッダが入る
  • ヘッダ以降は,データ群を tar 形式でまとめたものを zlib 形式で圧縮 (アルゴリズム: RFC 1951 Deflate, フォーマット: RFC 1950) したもの

という構造らしい。

# 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 ファイルの構造はわからなかった。

Pythontarfile モジュールを使えば 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()

*1:ご承知のとおり,各 apk ごとに (厳密には shared とかあるので違うけど) ユニークな uid / gid が割り振られる。でも当然ながら違う端末・インストール状況では同じアプリケーションが違う uid / gid となる。なので,ひょっとすると uid / gid 情報を失っても問題ないのかな?知っている人は教えてください。

Acer Iconia TAB A500 に Stock ROM 3.2 をインストール

基本的に個別の端末については言及しないつもりでいたのだが,ネタもないことであるし,これからは書いていくことにする。
A500 にはさまざまなカスタム ROM がでまわっているのでそれを使えばまぁそれほど不自由はない (らしい) のだが,システムがどのように構成されているのかということを捉えるうえでも*1日本版オフィシャル ROM 3.2 (Honeycomb) をインストールすることにする。

A500 のパーティション構成

たとえば下記の p1 に対応するブロックデバイスノードは /dev/block/mmcblk0p1 のようになる。実機が手元にないので間違っているかもしれない。

アップデータの抜き出し

システムアップデータのダウンロードをおこなう (そしてアップデータの適用まではおこなわない) と,/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.zipAMSS.zip((AMSS.zip は現状ではダミーファイルとなっている。)) とほかいくつかのファイルがある。update.zipAndroid におけるいわゆる 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.zipMETA-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: APX モードに移行しホスト端末から (添付された) ファームウェアの書き込みを行う
    • APX モードでの書き込みには A500 の UID が必要 (lsusb -v 等で取得できる iSerial の値らしい)
  • 方法 2: 公式の鍵で署名された不安定な 3.0.1 firmware*5update.zip を一度適用して,そこから CWM の書き込みなりなんなりをおこなう

*1:まぁそれは建前で単にカスタム ROM を漁ったり入れ替えたりするのが面倒なだけだけど。

*2:後述するように p4 はブートローダのアップデート用にも用いられるようだ。

*3:ディレクトリ名はうろ覚えなので違うかも。

*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.pk8testkey.x509.pem*1 を署名用鍵・証明書として利用する。

testkey.pk8
RSA 秘密鍵 (の PKCS #8 形式)
testkey.x509.pem
対応する X.509 公開鍵証明書

サンプルなのでこのようなテスト用キーを利用したが,本来であれば自分用の鍵・証明書を development/tools/make_keykeytool を利用して作成するべきである。
まずはじめに,秘密鍵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=

...... (後略)

アルゴリズムがいささかややこしいが,

  1. まず SHA1 digest の演算器を初期化する
  2. META-INF/MANIFEST.MFSHA1 digest を計算し,SHA1-Digest-Manifest: 属性として出力する
  3. 『digest 演算器のステートはそのまま』
  4. 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.SFRSA 暗号鍵と 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.zipwhole.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.pk8testkey.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+10000U+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);
    }
}

*1:とはいえ,ゆるい UTF-8 デコーダなら (サロゲートペアを除いて) 問題なくデコードできるであろう。厳密にはバイトシーケンスから一度 UTF-16 に変換し,ふたたび UTF-8 に変換するほうがよいだろう。

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 などの言語では引数は関数の実行前に評価されてしまうよね。
  • ("&&" や "||" のような) 演算子は組込関数のシンタックスシュガーにすぎない。よってそれらは制御構造として働くこともできる。
    訳注: たとえば foo() || bar()foo() が真の場合 bar() は評価 (実行) されない。なので制御構造的に使える。また実際には &&|| に対応する組込関数が存在するわけではない。
いくつかの例:
  • クォートされた文字列とクォートされていない文字列に区別はない。
    文字列中にホワイトスペースを含めたいときにはクォーテーションは必要になる。
    以下の式はすべて同じ文字列として評価される。
"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() にて定義))
    • 実際のアップデータスクリプトのパース・実行はこのアップデートバイナリによっておこなわれる
      • よって edify アップデートスクリプトを利用する場合,update.zip アーカイブの中に META-INF/com/google/android/update-binary バイナリを組み入れておく必要がある
      • 一部機能はアップデートバイナリ側ではなくホスト側 (recovery プロセス) によって実装されている
    • アップデートバイナリのソースは bootable/recovery/updater/ 以下に存在する
  • ベンダ提供のもの
    • 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,
path1, path2, ...)
パーミッションを設定 再帰
getprop(key) getprop
file_getprop(filename, key) ファイルが対象の getprop
write_raw_image(filename, partition) イメージファイルを MTD パーティションに書き込む
apply_patch(src_file, target_file, target_sha1, target_size,
sha1_1, patch_1, ...)
差分の適用
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_patterwhole_str の部分文字列なら真 ("t") を返す。

stdout(expr_a, expr_b, expr_c, ...)

全引数を標準出力に出力する*3

戻り値
空文字列 ("")
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)

プログレスバーを表示する (後述)。
portion01.0 の小数値。sec はバーが伸びるのにかかる時間 (秒数; 整数値)。
(ホスト側で実行)

戻り値
portion
show_progress(0.3, 10);
set_progress(frac)

プログレスバーの進捗を設定する (後述)。
frac01.0 の小数値。
(ホスト側で実行)

戻り値
frac
set_progress(0.8);
delete(path1, path2, ...)

指定された全パス(ファイルのみ)を削除する。

戻り値
正常に削除できた個数
delete("/tmp/foo.txt", "/tmp/bar.bin");
delete_recursive(path1, path2, ...)

指定された全パス(ディレクトリ含む)を再帰的に削除する。

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_filetarget_file には MTD:partition_name 記法が使えるようだ (未確認)。
target_file に「-」を指定すると src_file を上書きする。すでに target_sha1target_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");

*1:カンマ・セミコロンや一部制御構造のシンタックスシュガーによって C ――というより ALGOL――の見た目を醸し出しているが,じっさいには LISP により近い。

*2:前述のようにユーザ定義関数は定義できない。

*3:recovery updater 環境下では意味がないと思う。

*4:Androidソースコードを取得してそのまま generic product をビルドしても生成はされないのでひと工夫する必要がある。

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

フラッシュメモリストレージへの直接アクセス - 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 のサイズがかわるだけでヘッダ部分が変更されるので,通常の場合気にする必要はない。

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.apkres/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/ あたりが細かい仕様までまとまっていると感じた (ちゃんと探していないが)。


以下補足。

  • 無圧縮 zip 形式であることについて
    • frameworks/base/libs/utils/ZipFileRO.cpp を利用して ZIP ファイルを読んでいるが,このコード自体には展開能力がないため無圧縮である必要がある。frameworks/base/libs/utils/ZipUtils.cpp に ZIP 展開機能がインプリメントされているぽいのでそちらも使ってくれればよかったのに*2
  • ディレクトリ名について
    • partXX のような例示が多いが,別にそのような名前である必要はない。spec.txt にきちんと書いてあればよい。
  • 画像ファイル名について
    • 「連番で」などの紹介が多いが,要は ZIP 書庫に格納されている順番なので,なんでもよい。というか連番で用意してもアーカイバが辞書順に格納してくれなかったらうまくいかないと思われる。
  • 画像ファイル形式について
    • PNG 形式で作成している例が多いが skia ライブラリを利用してイメージを読み込んでいるので,JPEG, GIF あたりも対応しているようだ (未確認)。Windows BMPICO 形式にも対応している感じ。

*1:つまり,標準 Android ソースツリーからあまり手を加えていない場合,両方のプロパティを指定する必要はない。

*2:とはいえ,画像自体が圧縮されているので書庫自体を圧縮する意味はない。