2GB超えメディアへの ipk インストールの道 – その4

何だかんだでシリーズ第4弾 (^^;
結果を先に言うと、修正に成功しました!!

Haniwa様がコメントされていた SL-C3200 では2GB超えメディアへのインストールができる件は、SHARPのサイトに書いてありました。

SL-C3100 までは、4GB以上の CF では「メモリが足りません」と表示されてインストールできないと書かれていますが、SL-C3200では修正されているようですね。

それでは続きです。

コード置き場探し

原因も対処法もおおよそ目処がたったけど、まだ問題が1つだけ残っていた。それは修正コードを置く場所。

1命令で修正できればいいが、いろいろ考えてみたものの、今回の修正には数命令が必要そうだ。当然ながら普通にコンパイルされたコードには、わざわざ無駄なスペースなどない。

どこにコードを置こうか。どっかいいところないかな? 置けそうな場所は・・・

$ LANG=C arm-linux-readelf -l qinstall.so

Elf file type is DYN (Shared object file)
Entry point 0x1214c
There are 3 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x362c4 0x362c4 R E 0x8000
LOAD 0x0362c4 0x0003e2c4 0x0003e2c4 0x02e04 0x02e04 RW 0x8000
DYNAMIC 0x038ff8 0x00040ff8 0x00040ff8 0x000d0 0x000d0 RW 0x4

Section to Segment mapping:
Segment Sections...
00 .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.rodata .rel.data .rel.got .rel.plt .init .plt .text .fini .rodata
01 .data .ctors .dtors .got .dynamic
02 .dynamic

.text と同じセグメントにロードされるセクションである必要がある。そうでないと、アドレス解決しない限りロードされるアドレスが分からないので。

ELFについてそんなに詳しくないので分からないが、.gnu.version とかって要らないんじゃないのか?

と思って、おもむろに .gnu.version を strip したら動かなくなってしまった あっかんべー
必要なセクションだったのね。

うーん。他は必要そうなセクションばかりだな。。.hash, .dynsym, .dynstr, .init, .plt, .text, .fini と全然無駄がないじゃないか (当たり前だ ^^;)

ふと、.rodata が目に止まる。あれ? .rodata って .data とかと同じセグメントじゃないんだ。ReadOnlyだからか。

.rodata に無駄な文字列とかないかな・・・と思って .rodata セクションを見てみると・・・

$ LANG=C arm-linux-readelf -x 14 qinstall.so | less

Hex dump of section '.rodata':
0x00032f04 726f6c6f 43746573 00000072 6f6c6f63 color...setColor
0x00032f14 00000000 656e694c 6e616373 00000000 ....scanLine....
:
0x00032f84 6d6f682f 00000000 303d3d69 00296425 %d).i==0..../hom
0x00032f94 616d2d74 712f316e 2f797474 616d2f65 e/matty/n1/qt-ma
0x00032fa4 6e692f32 2e332e32 2d74712f 72657473 ster/qt-2.3.2/in
0x00032fb4 682e786f 62747369 6c712f65 64756c63 clude/qlistbox.h
0x00032fc4 0000002f 6e69622f 00007772 00000000 ....rw../bin/...

お! 程よく長くてデバッグ用とおぼしき文字列を発見。開発環境のファイル名だろうし、これは潰しても問題ないでしょう。

初めに \0 で終端したダミー文字列を用意しても、52Byteあるので 13 命令も置ける。これはいけそうだ。

修正コードの作成

修正コードは以下の掛け算を置き換える形で作成する。

   26e90:       e0050293        mul     r5, r3, r2

このコードを bl 0x?????? のコードに書き換え、コール先で r5 = r2 * r3 を計算する。その結果が 0x80000000 以上だったら 0x7fffffff を r5 に代入してリターンするようなコードを書けば、うまく修正ができそうだ。

ということで、 そんな感じのコードをインラインアセンブラで書いてちゃんと動くか確認。(確認用コード

実行してみると

res:7fff0000 rlong=7fff0000
res:7fff2000 rlong=7fff2000
res:7fff4000 rlong=7fff4000
res:7fff6000 rlong=7fff6000
res:7fff8000 rlong=7fff8000
res:7fffa000 rlong=7fffa000
res:7fffc000 rlong=7fffc000
res:7fffe000 rlong=7fffe000
res:7fffffff rlong=80000000
res:7fffffff rlong=80002000
res:7fffffff rlong=80004000
res:7fffffff rlong=80006000
res:7fffffff rlong=80008000
res:7fffffff rlong=8000a000
res:7fffffff rlong=8000c000
res:7fffffff rlong=fffffffe
res:7fffffff rlong=100000000
res:7fffffff rlong=100000002

OK。rlong が 0x80000000 以上の場合は 0x7fffffff が変数に格納できるようになった。4GB越えでも大丈夫。

この確認コードをベースに作った埋め込み用コードは以下の通り。あまりARMアセンブラに精通してないので、本当はもっと短く書けるかも。

    84ec:       e0805392        umull   r5, r0, r2, r3
84f0: e3500000 cmp r0, #0 ; 0x0
84f4: 159f5008 ldrne r5, [pc, #8] ; 8504
84f8: e3550000 cmp r5, #0 ; 0x0
84fc: 459f5000 ldrmi r5, [pc, #0] ; 8504
8500: e1a0f00e mov pc, lr
8504: 7fffffff swivc 0x00ffffff

mul 命令前後を読んで調べた結果、r0 が破壊可能なレジスタだったのでワーク用に使用した。

アドレスは適当だけどリロケータブルなコードなのでそのまま埋め込める。6命令+4Byteなので、前回見つけた領域にも余裕で収まる。

ということでバイナリエディタで上記コードを埋め込んでみた。

修正コード編集前
修正コード埋め込み前

修正コード変更後
修正コード埋め込み後

文字列の先頭はダミー文字列として "/ho" だけ残した。

その後は13ロングワード分が書き換え可能な領域なので、一度すべてFFで埋めた後、作成した修正コードをリトルエンディアンで埋め込んだ。 後で埋め込んだコードがわかりやすいように、先頭にFFFFFFFFを1つだけ入れておいた。

埋め込んだコードを objdump で逆アセンブルして確認。

$ LANG=C arm-linux-objdump -D --start-address=0x32f90 --stop-address=0x32fc0 qinstall.so

 00032f90 <.rodata+0x8c>:
32f90: 006f682f rsbeq r6, pc, pc, lsr #16
32f94: ffffffff swinv 0x00ffffff
32f98: e0805392 umull r5, r0, r2, r3
32f9c: e3500000 cmp r0, #0 ; 0x0
32fa0: 159f5008 ldrne r5, [pc, #8] ; 0x32fb0
32fa4: e3550000 cmp r5, #0 ; 0x0
32fa8: 459f5000 ldrmi r5, [pc, #0] ; 0x32fb0
32fac: e1a0f00e mov pc, lr
32fb0: 7fffffff swivc 0x00ffffff
32fb4: ffffffff swinv 0x00ffffff
32fb8: ffffffff swinv 0x00ffffff
32fbc: ffffffff swinv 0x00ffffff

埋め込み完了。思惑どおりのコードになった。あとは、mul命令の代わりにこのコードをコールするだけ。
コール先のアドレスは上記 objdump の結果から、0x32f98 を呼べばよい。

修正コードのコール

前に書いたように、以下の mul 命令を bl 命令に置き換えて修正コードをコールするようにする。

  26e90:       e0050293        mul     r5, r3, r2

bl は相対ジャンプ命令なので、オフセットを計算する。このページによると、この命令があるアドレス+8からジャンプ先までのオフセットを4で割った値を24bitオフセットとして指定するらしい。

ジャンプ先は 0x32f98 なので、オフセットは (0x32f98 – (0x26e90 + 8)) / 4 = 0x003040 となる。

無条件 bl は 0xeb?????? (??はオフセット) らしいので、0xeb003040 を 0x26e90 へ書き込めばいい。

バイナリエディタでコードを書き換えて objdump で確認すると・・・

修正前

   26e88:       e51b205c        ldr     r2, [fp, -#92]
26e8c: e51b3050 ldr r3, [fp, -#80]
26e90: e0050293 mul r5, r3, r2
26e94: e5942000 ldr r2, [r4]
26e98: e5923000 ldr r3, [r2]

修正後

   26e88:       e51b205c        ldr     r2, [fp, -#92]
   26e8c:       e51b3050        ldr     r3, [fp, -#80]
   26e90:       eb003040        bl      0x32f98
   26e94:       e5942000        ldr     r2, [r4]
   26e98:       e5923000        ldr     r3, [r2]

OK。mul 命令と bl 命令の置き換え完了。コール先アドレスも正しく 0x32f98 を指しててオフセット計算も合っていたようだ。

これで修正は終了・・・。

動作確認

/opt/QtPalmtop/binlib/qinstall.so を上記の修正を施した qinstall.so へ置き換えて、再起動。
祈りながら4GB SDにインストールしてみると・・・

インストールできた!

一発で動くとは思わなかったけど、ちゃんと動いてくれたようだ。
長かっただけに感動もひとしお。

これで何も気にせずに 4GB SDを使えるよ。うん。

あとは、これを ipk 化するだけだ。
ただ、qinstall.so は SHARP 製のプログラムなので自由に配布することはできないので、バイナリパッチと言う形で、ipk をインストールした時にパッチを当てるような実装にしないといけないかな。

SL-C1000 と SL-C3000 は持ってるから qinstall.so を入手できるけど、SL-C3100 は。。。SL-C3000と同じかなぁ?

どなたか、SL-C3100で qinstall.so の md5sum を取ってもらえませんか?

以下 SL-C3000 での実行結果

$ md5sum /opt/QtPalmtop/binlib/qinstall.so
cd2629a9cd451bcd3e5198f933bb0c33 /opt/QtPalmtop/binlib/qinstall.so

これと同じ MD5 だったら同じバイナリパッチが使えるんだけど・・・。

(次回いよいよ最終回?)

2GB超えメディアへの ipk インストールの道 – その3

前回前々回の続き。

調査を進めるにつれ、SDだけでなく、CFでも同じ問題があることが判明したため、タイトルを変更した。

qinstall.so の InstallUtil::getAvailableSize() で statfs() が呼ばれていることが判明した続き。

statfs前後の処理

statfs をコールする前を確認してみる。

   26e4c:       e1a00004        mov     r0, r4
26e50: ebffa6c1 bl 0x1095c ; call QString::latin1(void) const
26e54: e24b1060 sub r1, fp, #96 ; 0x60
26e58: ebffa617 bl 0x106bc ; call statfs()

statfs を呼び出す前を見ると、第1引数(r0) に QString::latin1() の結果(おそらくパス名が入っていると思われる)が、第2引数(r1) には、fp – 96 (スタック上のローカル変数と思われる)がセットされている。

雰囲気的には以下みたいな感じかな?

unsigned long InstallUtil::getAvailableSize(QString)
{
:
struct statfs stat;
int ret;
ret = statfs(path.latin1(), &stat);
:
}

ふむふむ。

その後を追いかけてみる。(簡単なコメント付き)

   26e58:       ebffa617        bl      0x106bc            ; call statfs()
26e5c: e3500000 cmp r0, #0 ; 0x0 // 戻り値確認
26e60: 0a000008 beq 0x26e88 // 戻り値=0 (正常) なら 0x26e88 へジャンプ
26e64: e3a05000 mov r5, #0 ; 0x0
26e68: ea000009 b 0x26e94
:
:
26e88: e51b205c ldr r2, [fp, -#92] // struct statfs構造体の何かを r2 へ読み出し
26e8c: e51b3050 ldr r3, [fp, -#80] // struct statfs構造体の何かを r3 へ読み出し
26e90: e0050293 mul r5, r3, r2 // r5 = r3*r2
:
26ecc: e1a00005 mov r0, r5 // 上の掛け算結果を r0 へ
26ed0: ea000000 b 0x26ed8
26ed4: 00000800 andeq r0, r0, r0, lsl #16
26ed8: e91bac70 ldmdb fp, {r4, r5, r6, sl, fp, sp, pc} // リターン

struct statfs の何かのメンバ同士を掛け合わせたものが getAvailableSize() 関数の戻り値(r0)に入るようだ。では、statfs の何のメンバなのか。

statfs の第2引数は fp-96 のアドレスだったので、fp-96 から struct statfs 構造体が始まっている。その構造体にアドレスを当てはめると・・・

   struct statfs {
long f_type; /* fp-96 */
long f_bsize; /* fp-92 */
long f_blocks; /* fp-88 */
long f_bfree; /* fp-84 */
long f_bavail; /* fp-80 */
long f_files; /* fp-76 */
long f_ffree; /* fp-72 */
fsid_t f_fsid; /* fp-68 */
long f_namelen; /* fp-64 */
};

fp-92 は f_bsize、fp-80 は f_bavail だと分かる。

man page によると、f_bsizeが「ブロックサイズ」、f_bavail が「非スーパーユーザが使用可能な空きブロック数」らしい。これらを掛け合わせているということは、非スーパーユーザが使用可能な空きバイト数を計算しているということになる。

掛け算で使用している mul という命令は、このページによると 32bit = 32bit * 32bit の符号なし掛け算のようだ。この時点で 4GB (32bit) を越える空き容量は正しく判断できないことが分かる。さらに、4GBSDで問題が起こると言うことは、この結果を singed long として扱っていると推測できる。

また、getAvailableSize() 関数は名前の通り、空き容量をバイト数で返す関数だということも確証が得られた。

さて、これをどうしようか。

空き容量2GBの壁の検証

前々回のブログに対して、Haniwa様から 4GB SD でもインストール可能でしたとのコメントを頂いた。
今までの解析結果と推測から、空き容量が 2GB 前後に壁があるのではないかと考えられる。
ということで、4GB SDにダミーファイルを作成して、空き容量を2GB前後にしてインストール可能/不可能が変わるのかを検証してみた。

その結果・・・

  • 空き容量 2,147,450,880Byte  (0x7FFF8000 Bytes)
    → インストールOK
  • 空き容量 2,147,483,648Byte  (0x80000000Bytes)
    → インストールNG

やはり、空き容量が2GBの所に境界があるようだ。(SL-C1000, SL-C3000 で確認)

ついでに、4GBのCFでも確認してみたところ、やはり 2GB 以上の空き容量の場合はインストールできないようだ。(SL-C1000で確認)

ただし、Haniwa様がお持ちなのは SL-C3200 なので、SL-C3200 では元々 4GB メディアに対応している可能性もある。内蔵MicroDrive は6GBなので、空き容量が2GB越えに対応する必要もありそうだし。(そう考えると SL-C3000で対応済みでもよさそうだが・・・SD/CFへのインストールと本体へのインストールは容量判定処理が別なのだろうか・・・)

修正方針

qinstall.so に手を入れるとすると、修正する方法は2通りある。InstallUtil::getAvailableSize() 関数に手を入れるか、この関数を呼び出した後の判定に手を入れるか。

getAvailableSize() を呼んでいる場所を調べるため、逆アセンブルした結果を bl 0x26d20 で grep したら 13 個も見つかった。これを追うのは気が滅入りそうなので、getAvailableSize()を修正することにした。

また、空き容量が2GB以下であれば、getAvailableSize() の呼出側は正常に動作できるようなので、空き容量が2GB以上の場合は 2GB-1Byteとして返却することにしよう。そうすれば、何GBのメディアでも対応可能だし。

(もう少し続く・・・)

4GSD へ ipk インストールの道 – その2

前回の続き。

qinstall.so に InstallUtil::getAvailableSize(QString) という関数があることが分かった。この辺をなんとかすればいけるかも?

InstallUtil::getAvailableSize の抽出

まず、この関数がどこからどこまでなのかを調べてみる。開始アドレスは前回の objdump の結果から、

00026d20 g    DF .text  000001bc  Base        InstallUtil::getAvailableSize(QString)

0x26d20 だと分かっているので、そこから逆アセンブルしてみる。

$ arm-linux-objdump -d --start-address=0x26d20 qinstall.so | less
      :
00026d20 <.text+0x14bd4>:
   26d20:       e1a0c00d        mov     ip, sp
   26d24:       e92ddc70        stmdb   sp!, {r4, r5, r6, sl, fp, ip, lr, pc}
   26d28:       e24cb004        sub     fp, ip, #4      ; 0x4
   26d2c:       e1a04000        mov     r4, r0
   26d30:       e24b0020        sub     r0, fp, #32     ; 0x20
:

そういえばARMはこんな感じだったっけ。
ARMでは関数の先頭で stmdb でレジスタを保存して、関数の最後に ldmdb でレジスタを復元してリターンするのが典型的なパターン。

そのまま先を見ていくと・・・。

     : 
26ed0: ea000000 b 0x26ed8
26ed4: 00000800 andeq r0, r0, r0, lsl #16
26ed8: e91bac70 ldmdb fp, {r4, r5, r6, sl, fp, sp, pc}

あった ldmdb。

脱線するけど、初めて ARM に触ったときにこの stmdb と ldmdb の仕組みが良くできてて感心した記憶がある。
関数の入り口と出口をならべてみるとよく分かる。

   26d20:       e1a0c00d        mov     ip, sp
   26d24:       e92ddc70        stmdb   sp!, {r4, r5, r6, sl, fp, ip, lr, pc}
   26ed8:       e91bac70        ldmdb   fp, {r4, r5, r6, sl, fp, sp, pc}

r4〜r6, sl, fp は素直に保存・復元が行われて、特に lr が見事で、コール時に保存した戻りアドレスの lr を pc に戻すことでリターンを実現している。
レジスタの復元とリターンが同時にできるなんて、なんてスマートなんだ!!  しかも1命令で!! と感動したのは私だけ?? (^^;

・・・・話を戻すと、getAvailableSize の最後は 0x26ed8 で良さそう。
最適化を行うと、関数の途中にも ldmdb が出てきたりするんだけど、ジャンプを追いかけても 0x26d20〜0x26ed8 の間に収まってるみたい。

とりあえず関数の抽出ができた。

getAvailableSize の調査

本当にこの関数で statfs が呼ばれているのか調べてみる。

関数コールは bl 命令を探すだけでOK。

   26d4c:       ebffa9b6        bl      0x1142c
26d60: ebffaa05 bl 0x1157c
26d9c: ebffa7f2 bl 0x10d6c
26db8: ebffa9e3 bl 0x1154c
26dd0: ebffa995 bl 0x1142c
26de4: ebffa9e4 bl 0x1157c
26e1c: ebffa7d2 bl 0x10d6c
26e34: ebffa9c4 bl 0x1154c
26e48: ebffa9bf bl 0x1154c
26e50: ebffa6c1 bl 0x1095c
26e58: ebffa617 bl 0x106bc
26ec8: ebffa7a7 bl 0x10d6c

コール先がダブっているものを省くと、

   26d4c:       ebffa9b6        bl      0x1142c
26d60: ebffaa05 bl 0x1157c
26d9c: ebffa7f2 bl 0x10d6c
26db8: ebffa9e3 bl 0x1154c
26e50: ebffa6c1 bl 0x1095c
26e58: ebffa617 bl 0x106bc

この6つに絞られた。これを1つ1つ調べてみる。

まずは 0x1142c。このアドレスはどこかというと、

$ LANG=C arm-linux-readelf -S qinstall.so
There are 23 section headers, starting at offset 0x3918c:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
:
[10] .init PROGBITS 00010194 010194 000018 00 AX 0 0 4
[11] .plt PROGBITS 000101ac 0101ac 001fa0 04 AX 0 0 4
[12] .text PROGBITS 0001214c 01214c 020da4 00 AX 0 0 4
:

.plt セクションのようだ。.plt セクションは動的リンクしたライブラリへジャンプするためのコードが書かれたもの。

早速 0x1142c を逆アセンブルすると、 

   1142c:       e59fc004        ldr     ip, [pc, #4]    ; 0x11438
   11430:       e08fc00c        add     ip, pc, ip
   11434:       e59cf000        ldr     pc, [ip]
   11438:       0002ef98        muleq   r2, r8, pc

こんな感じのコードだった。C言語風に訳すと、

  • ip = *((unsigned int *)0x11438);
  • ip += 0x11438;
  • pc = *((unsigned int *)ip)    // ipに書かれたアドレスへジャンプ

つまりは、0x2ef98 と 0x11438 を足したアドレスに書かれているアドレスにジャンプするというもの。

足し合わせると、 0x403d0 となる。このアドレスは・・・

$ LANG=C arm-linux-readelf -S qinstall.so
There are 23 section headers, starting at offset 0x3918c:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
:
[17] .dtors PROGBITS 0003ff20 037f20 000008 00 WA 0 0 4
[18] .got PROGBITS 0003ff28 037f28 0010d0 04 WA 0 0 4
[19] .dynamic DYNAMIC 00040ff8 038ff8 0000d0 08 WA 3 0 4
:

.got セクションのようだ。.got セクションは、シンボル解決をしたアドレスが書かれるテーブル。

まぁ、白々しくこう書いたけども、.plt がジャンプコードになっていて、.got がそのジャンプアドレステーブルになっているのは ELF の動的リンクの仕組みそのもの。うん。いい勉強になるね。(また白々しい・・・)

で、例の 0x403d0 には何が書かれるかというと、

$ LANG=C arm-linux-objdump -RC qinstall.so | less
qinstall.so:     file format elf32-littlearm

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
00032f5c R_ARM_ABS32       QTextStream::width(int)
    :
000403cc R_ARM_JUMP_SLOT   QCString::QCString(char const *)
000403d0 R_ARM_JUMP_SLOT   QString::fromLatin1(char const *, int)
000403d4 R_ARM_JUMP_SLOT   QFile::exists(void) const
   :

QString::fromLatin1 のようだ。リンカがアドレス解決すると、0x403d0 に QString::fromLatin1 のアドレスが書き込まれる。
つまり、まとめると、bl  0x1142c は QString::fromLatin1 をコールしていたというわけだ。

といった感じで、残りのコール先も調べると、以下の様にになった。

   26d4c:       ebffa9b6        bl      0x1142c		; 0x403D0 QString::fromLatin1(char const *, int)
26d60: ebffaa05 bl 0x1157c ; 0x40424 QString::find(QString const &, int, bool) const
26d9c: ebffa7f2 bl 0x10d6c ; 0x40220 QStringData::deleteSelf(void)
26db8: ebffa9e3 bl 0x1154c ; 0x40418 QString::operator=(char const *)
26e50: ebffa6c1 bl 0x1095c ; 0x4011C QString::latin1(void) const
26e58: ebffa617 bl 0x106bc ; 0x40074 statfs

お、ちゃんと statfs を呼んでいるようだ。

qinstall.so 内で他に statfs を呼んでないかを調べてみる。

$ LANG=C arm-linux-objdump -d qinstall.so | grep 0x106bc
arm-linux-objdump: qinstall.so: no symbols
   26e58:    ebffa617     bl    0x106bc

うん。getAvailableSize 関数でしか呼んでないみたい。この関数でビンゴだ。

(まだまだ続く・・・)