Zopfcode

かつてない好奇心をあなたに。

リセットとフリーズで解析する電子辞書リバエン記

f:id:puhitaku:20211120163623p:plain

諸行無常という言葉がある。常なるものはなく、何事も変化し続けるという意味だ。これは私にも、枯れたツールである電子辞書にも例外なく当てはまる。

1年と少し前、私は「電子辞書は組み込み Linux の夢を見るか?」という記事を公開した。

www.zopfco.de

この記事を書いた時は独りだった私に、Brain Hackers という電子辞書ハックのコミュニティができた。これがまず最初の変化だった。

そしてそれから半年と経たない2021年2月、次なる変化が訪れた。SHARP Brain の新モデル、PW-x1 シリーズ*1が発売された時のことだ。この記事ではその PW-x1 シリーズの解析と、困難を突破して Linux の動作に至るまでの顛末をご紹介する。

(本記事は、Kernel/VM 探検隊 online part4 で発表した同タイトルのスライドを記事にしたものです。)

speakerdeck.com

ハードとソフトの刷新

型番の一新や追加アプリの非互換のように、大きな変化を窺わせる事前情報はあった。しかしながら、何事も「中身」を見ずして確信には変えられない。その思いから、PW-x1 が発売された当日にヨドバシ AKIBA へと駆け込んだ。販売員は驚いていた。

「発売日当日に電子辞書をお買い求め頂いたお客様は初めてです」

思わず互いに笑みがこぼれた。

新世代の PW-B1 を手に入れた私は、開封から数分も経たずにその保証期間を終わらせ、ガワの「中身」を観察した。思わず声が上がった。

システムの根底ともいえる SoC が、前世代の i.MX283 から i.MX7ULP に変更されていたのである。大急ぎでセットアップした YouTube Live で基板を流しつつ、あれこれ考察した。半導体業界やソフトウェア産業の見地から、いかようにでも理由が考察できた。

自作バイナリを何とかして動かす

ハードウェアが変化したのであれば、次はソフトウェアを確認したい。この電子辞書で U-Boot や Linux を動かすには、何よりもまず自作バイナリが動かないと始まらない。ちなみにこの電子辞書では μITRON ベースの RTOS が動いていることが後にほぼ確定するが、この時は知る由もなかった。

まず、以前の世代では動いたアプリが、この新世代では認識すらされないことはすぐ判明した。発売から日を置いてリリースされた追加アプリを購入して、早速解析した。すると、実行ファイルの先頭には EXE (PE) のヘッダーも、ELF のヘッダーもなかった。0ワード目から機械語列が並んでいたのである。

f:id:puhitaku:20211118164715p:plain:w500
当時の実際の会話ログ

PW-B1 には NXP i.MX7ULP が搭載されているので、Arm のバイナリが動く。Arm の機械語をそのまま見ると、命令 4 Bytes の最後の Byte が E9 のように E で始まるものが多いためにすぐ察しがつく。後にobjdump でそれらしい機械語列が出たことで確定診断となった。

f:id:puhitaku:20211118165751p:plain
Arm 向けにビルドした u-boot.bin の先頭部分(買ったアプリのバイナリではない)
オレンジで囲んだ 4 Byte 目に `ex` が多いことがわかる。

では手でアセンブリを書けば自作バイナリが動かせるのではないかということで、以下のような「無限ループするだけ」のアセンブリを実装した。

.text
    .align 2
    .global _start

_start:
loop:
    nop
    b loop

as で ELF にしたものから .text セクションだけを手動で切り出し、AppMain.bin というファイル名で SD カードに配置。するとアプリメニューにちゃんと表示された。…恐る恐る実行すると、画面がアプリメニューのまま静止し、画面開閉以外にほぼ反応しない状態になった。

無限ループのプログラムなので、カーネルが何らかの割り込みをする以外の時間はリソースを食い尽くした状態になる。つまり「完全フリーズではないがほぼ無反応状態」は実験成功を意味している。ここでガッツポーズ!

続けて、無限ループではなく「即座に return する」、つまり mov pc, lr で呼び出し元に戻るだけのコードを動かした。すると、謎の緑の画像が出て操作ができなくなった。

.text
    .align 2
    .global _start

_start:
loop:
    mov pc, lr

f:id:puhitaku:20211118192239j:plain
「謎の画像」が出た画面

さらに、0x00000000 という明らかに不正な仮想アドレスにジャンプするコードも動かした。こちらは予想通りというか、ページフォルトハンドラによるものと思われる本体のリセットがかかった。

.text
    .align 2
    .global _start

_start:
loop:
    ldr r0, =0x00000000
    bx r0

「無限ループ」「謎の画像」「リセット」はいずれもまるで実用的ではない動作だが、自作のバイナリが動かせることが確認できた。

ループ・画像・リセットを頼りに U-Boot 起動の可能性を探る

さて、ここで Linux ポーティングの第一歩として、前段となるブートローダーの U-Boot を動かしたい*2。U-Boot を動かすには、普通以下2つの条件を満たす必要がある。

  • MMU (Memory Management Unit) と割り込みを無効化できる
  • MMU がいなくなった物理アドレス空間で U-Boot にジャンプできる

これらの動作実験にあたり、バイナリが上手く動いたか否かを知るための I/O が必要だが、この時点で利用できる I/O はひとつたりともなかった。フレームバッファがどこにあるのかわからない。OS が持つ描画 API がわからない。JTAG もどこに出ているのかわからない。UART(いわゆるシリアル通信)を引き出す箇所は回路を調べて発見したが、そもそも SoC の UART がどの仮想アドレスにあるのかがわからない。

どうすればよいのか…本当に頭を抱えたが、ここでふとあることをひらめいた。前節の実験では、「無限ループ」「謎の画像」「リセット」を発生させることができた。これらを組み合わせて「処理が成功したら謎の画像を出し、ダメならリセット」といった風に実装すれば、1 bit の情報が取り出せるのではないか? と思い付き、早速実行に移すことにした。

MMU を無効化する

上述の通り、U-Boot へは MMU を切ってからジャンプする。MMU の無効化はコプロセッサ書き込み命令 mcr で行うため、これが実行できるかを確かめなければならない。MMU の動作を司る SCTSR を読み書きするには特権モードが必須なため、特権モードでない時にアクセスすると未定義命令例外が発生してリセットしてしまう*3

そこで、「処理が上手く行ったら return して謎の画像を出し、上手く行かなかったらリセットされる」という 1bit の情報が取り出せる以下のコードを書いた。

.text
    .align 2
    .global _start

_start:
    mrc p15, 0, r10, c1, c0, 0
    mov pc, lr

普通の OS でいうカーネル部分以外はユーザーモードで動くのが普通なので、ほとんどダメ元で動かした。するとなんと、リセットされることなく謎の画像が出たからビックリ。追加アプリは特権で動いていることが判明した。

これでまず、条件 No. 1 はクリアできた。

メモリに置いたバイナリへ物理アドレスでジャンプする

仮想アドレスに置いた U-Boot に MMU を切って ジャンプする以上は、仮想アドレスと物理アドレスの完璧な対応を発見する必要がある。そこで、以下の3つの実験を行った*4

  1. Brain で実行できる最大のバイナリサイズを調べる
  2. 128 MiB ある DRAM の物理アドレスを 1. で調べた最大サイズごとに区切り、1. のバイナリに着地できる物理アドレスはどこにあるか大まかに探す
    1. のバイナリのうちどこに着地したのか、2. で得た範囲を二分探索して正確に特定する

実験1

実験1で使うバイナリを図解したのが以下の図だ。ほとんどは NOP で埋まっていて、末尾にちょこっと mov pc, lr つまり「謎の画像」を出す処理が入っている。この実験によって、DRAM のどれくらいの領域を自分のバイナリで埋められるかがわかる。

f:id:puhitaku:20211120061513p:plain

実験1の結果、バイナリサイズは最大ぴったり 15 MiB であることが判明し、それ以上は1命令 (4 Bytes) たりとも増える余地はなかった。

実験2

実験2で使うバイナリを図解したのが以下の図だ。先程よりも随分複雑なバイナリになっている。

f:id:puhitaku:20211120061810p:plain

このバイナリは「MMU を切って特定の物理アドレスに飛ぶコードと NOP」および「NOP とリセットが発生するコード」が組み合わさっている。いずれも 64 KiB の長さを持っていて、これは以下の図のように仮想メモリ上でのバイナリが実は物理では連続でなかった場合に備えている。

f:id:puhitaku:20211120062104p:plain

DRAM の領域を、先程実験1で発見したサイズごとの区画に分け、各区画のど真ん中にジャンプして当たるかどうかを調べる。リセットが発生すれば当たりとなるが、最終的に「リセット」を「無限ループ」に差し替えて動作がそのとおり変化するかを確認すれば、自作バイナリの上に飛んでいることは確定となる。

f:id:puhitaku:20211120063112p:plain

実験2の結果、DRAM のかなり末尾に近い 0x67800000(末端より 15 MiB の区間の真ん中)に飛ぶと自作バイナリに着地することがわかった。

実験3

実験3では、実験2のバイナリを改変した以下のようなバイナリを準備する。

f:id:puhitaku:20211120062459p:plain

このバイナリでは、先程の 64 KiB 区間たちの半分が「リセット」で、もう半分が「無限ループ」になっている。自作バイナリの上に飛んでくることは判明しているので、このどちらが発生するかを見て二分探索を繰り返せば最終的に区間が確定できる。

実験の結果、112番めの区間に飛んできていることが判明した。

f:id:puhitaku:20211120062746p:plain

実験まとめ

3つの実験結果をまとめると、以下の事実が判明した。

  1. 最大のバイナリサイズはぴったり 15 MiB
  2. 自作バイナリは DRAM の末尾 113 MiB 〜 128 MiB のあたりにある
  3. 物理アドレス 0x67800000 にジャンプすると、自作バイナリのうち 64 KiB で区切った112番目の箇所、オフセットにして 0x700000 の場所に飛んでくる

ライブしながら試行錯誤したのでこの3つで5時間くらいかかったが、最終的に実行ファイルのアドレスと物理アドレスの対応が1つ発見できた。

f:id:puhitaku:20211118225128j:plain
YouTube Live で物理アドレスの対応が確定した瞬間のスクリーンショット

U-Boot を動かす

幸いにも自作バイナリと物理アドレスの対応がわかったので、U-Boot を動かす作業に取り掛かった。初めての自作バイナリ実行から3日目のことである。

i.MX7ULP の評価ボードに対応する defconfig と device tree から余計そうな要素を削り、先程判明したオフセットでバイナリに配置。本当に動くのか??その結果は????

キターーーー!!!!

翌週の3月15日にはついに U-Boot shell が操作できるところまで到達した。

f:id:puhitaku:20211118230019p:plain
U-Boot shell まで到達した瞬間

コミュニティで生まれた進歩

冒頭で紹介したように、現在は Brain Hackers というコミュニティで一緒に解析をする仲間がいる。彼らの解析結果も非常に多くの情報をもたらした。Linux の話に進む前に、コミュニティで起きた進歩をご紹介しよう。

回路の詳細な解析が進む

Brain Hackers が発足してから、@sgch07 による回路解析のクオリティの高さ・部品剥がしの綺麗さにはいつも感嘆していた。そこで、PW-B1 についても同様に解析をお願いするため、私が追加で購入した PW-B1 を 6月に @sgch07 に寄贈した。

f:id:puhitaku:20211120070545p:plain
回路解析レポートの一部

3週間ほどすると、表面の部品やその結線、型番のよくわからない部品に至るまで詳細なレポートが出来上がっていた。後々の IOMUX*5 や Linux の device tree の整備では大いに役立つことだろう。

メモリダンプを吸う・元の OS の解析が進む

U-Boot のおかげで DRAM も SD カードのファイルシステムも好きに読み書きできるようになったことで、元の OS のメモリダンプを解析することが可能になった。

f:id:puhitaku:20211118235150p:plain:w500
メモリダンプを吸いし者たちの会話

6月に本機種を購入した @pepepper_cpp から発見が相次いだ。彼はまずこのダンプから fopen や fclose といった元の OS が持つ libc の関数を次々に発見していった。これによって元の OS 上でも SD が読み書き可能になったので、@sgch07 が元の OS を生かしたままメモリダンプを吸い、追加の libc 関数を発見することにも成功した。

この時期にもなると、「大きなバイナリに U-Boot を埋め込む」というテクニックはもはや古いものとなっていた。メモリダンプが吸えるようになったのと時を同じくして仮想メモリのページテーブル解析や libc の知見も次々に上がってきたため、SD から直接 u-boot.bin を読み出してメモリに置いてジャンプする、という過去機種と遜色ないブートが可能になっていた。

Linux を動かす

前回記事では Linux 自体を動かすのはさほど時間がかからなかったが、今回は Linux の完全なブートに期間を要した。起動初期の printk は表示されるものの、すぐにブートが停止してしまうのだ。画面をタッチしたりキーを押したりするのに応じてログが出るので割り込み関係と当たりをつけたものの、6月当時は私も他メンバーもうまく解析が進まずお預けとなってしまった。

4ヶ月ほど経った10月に @pepepper_cpp から「Mailbox Unit *6あたりの割り込みが怪しい」「今自分たちがコンパイルしているバージョンの Linux には i.MX7ULP の MU のドライバが入っていない」と報告が上がり、Linux ポーティングは新展開を迎えた。早速 NXP の upstream を見る。より新しいコードでは MU のドライバが確かに device tree に追加されているではないか!

こちらに切り替えたものを push して、 @pepepper_cpp から嬉しい報告が舞い込んでくるまでに時間はかからなかった。

f:id:puhitaku:20211119001854p:plain

Linux の起動が可能になったのは本当に最近で、ゆっくりとデバッグできるような長時間稼働も実現していないため、UART 以外の I/O はまだほとんど動いていない。現在も解析は日進月歩で進んでいるので、近いうちの進展をお待ちいただきたい。

まとめと今後

新モデルの Brain で動いていた「未知の OS」は、後に μITRON 系のプロプライエタリな RTOSらしいことが解析で判明した。公にある知見は当然ほとんど無く、購入当初から本記事の執筆時点まで大変な苦労を強いられた。

しかしながら、何一つ I/O らしい I/O もない中、「リセット」と「フリーズ」だけで難しい箇所を突破し、メモリダンプ、libc、果ては Linux まで爆発的に知見が増えていったのは本当に感動的だ。前回記事を書いた頃にはなかった Brain Hackers というコミュニティがあることで、自分だけでなく仲間たちの知見と協力を得つつ、時には励まし合いつつ進むことができた。

本記事を執筆した2021年11月現在でもなお、Linux の完全な安定動作には至っていない。さらに、GUI を含む自作アプリを元の OS で動かせるほどの知見も蓄積できてはいない。しかしこれらは明らかに不可能ではない。今後の我々の解析と開発に是非ご期待頂きたい。

*1:x には A, B, H, J, S が入る

*2:Linux へ直接飛ばないのかという意見もあると思われるが、Linux より遥かに小さい U-Boot を動かす方が簡単で、後でハックの足場にもできるので良い

*3:PW-x1 の場合、未定義命令例外や abort が発生すると即座にリセットする実装が入っているようだ

*4:MMU のページテーブルを解析するコードはこの時まだなく、MMU を切ってから読みに行くにしても PC (program counter) が指すアドレスはもはや正しくないためパイプラインに入っている数個の命令しか実行する余地が残されていない・よってアドレスの対応は力技で見つけるしかない

*5:i.MX7ULPでは1つの物理ピンの機能を自在に切り替える機能があり、I/O の multiplexer であることから IOMUX と呼ばれる

*6:i.MX7ULP には Cortex-A7 と Cortex-M4 の異種コアが同居していて、Mailbox Unit を通じて互いにデータをやり取りする