概要
一部の方面で話題になっている,中華レトロゲーム機を購入したので解析をしてみた.物忘れの激しいワタシ向けのメモのようなものである.
解析のゴールを何にするのかが悩ましいところであるが,中華レトロゲーム機向けのアプリケーションの開発環境の構築方法を示せればいいかなと思う.
そうすれば大勢の発想豊かなどなたかが,なにかいいものを作ってくれるのではないかと.
イメージファイル
入手
電源を入れるとRetroFWの画面が表示された.これをキーワードにググるとRetroFWがすぐに見つかる.
リリースファイルのRetroFW_v1.2.zipをダウンロードし,中身を見てみる.
配布物の中身
zipファイルを展開すると,RetroFW.imgとバッチファイルとdd.exe,サブフォルダに各種uBoot.binとuImage.binが現れる.
バッチファイルから見えてくること
バッチファイルの中を見ると,dd.exeを使ってRetroFW.imgの中の特定の位置にuBoot.binとuImage.binを書き込む処理が書かれていることがわかる.例えばRetroGame_v1.0_S_B.batは以下のようになっている.
dd if=kernel/RetroGame_v1.0_S_B.uBoot.bin of=RetroFW.img conv=notrunc bs=512 seek=1 && dd if=kernel/RetroGame_v1.0_S_B.uImage.bin of=RetroFW.img conv=notrunc bs=1024 seek=4096
uBoot.binはファイルの先頭から512byteオフセットした位置から,uImage.binはファイルの先頭から4Mbyteオフセットした位置から書かれることがわかる.
U-Bootは電源が入ってすぐの初期の段階で動作を開始し,ハードウエアの初期化を行った後にOSの起動を行う,いわゆるブートローダーである.
これがファイルの先頭から512byteオフセットした位置に書かれるのだから,この中華レトロゲーム機で採用しているチップはmicroSDの先頭から512byteオフセットした位置(仮にLBA 1としておく)からをRAMに読み出してジャンプする仕組みを備えていることが想像できる.
更にこのU-BootはmicroSDの先頭から4Mbyteオフセットした位置に置かれているuImage.binをRAMに読み出してジャンプするように書かれていると想像できる.
イメージファイルから見えてくること
RetroFW.imgを見ると,最初の512byteがMBRであるように見える.
パーティションテーブルのわかりやすい部分だけを抜き出して,表にすると以下のようになる.
パーティションNo. | 先頭セクタ(LBA) | セクタ数 | パーティション種類 |
---|---|---|---|
#1 | 0x0000_4000 | 0x0004_0000 | Linux |
#2 | 0x0004_4000 | 0x0008_0000 | Linux swap |
#3 | 0x000c_4000 | 0x0013_c000 | FAT32 |
#4 | - | - | 空き |
上記に従いRetroFW.imgをバイナリエディタで見てみると,確かに第1パーティション(オフセット0x0080_0000から)がrootfs,第2パーティション(オフセット0x0800_0000から)がswapに見える.
しかし第3パーティション(オフセット0x1800_0000から)は,しばらくの間0で埋められている.こうなっていると,恐らくLinuxからマウントしようとすると失敗するはずで,失敗した場合の動作があらかじめ仕込まれていると考えられる.
具体的には,FAT32でマウントを失敗したらmkfsする,という動作である.もう少し気の利いた設計がしてあるなら,FAT32領域をmicroSDの末尾まで拡張してからmkfsしてくれる事も考えられる(raspbianなんかはそうだから).これは実際に動きを確認すればよいだろう.
MBRのオフセット0からのローダー部分には実行コードが配置されることになっており実際に何かかかれているが,このチップでもそうなのかは判断しづらい.
ひとまず先頭の4byte,FA B8 00 10
を逆アセンブルしてみると...
00000000 1000b8fa b loc_fffee000
とのことで,相対ジャンプ命令でしかも飛び先はマイナス方向のようである.これは実行されないような気がしてならない.これ以上突っ込んで調べるのはやめて,ランダム値を書き込んでみてどうなるか,実際に動きを確認すればよいだろう.
Linuxを使ってもっと調べる
ここまでバイナリエディタで調べたことであるが,Linux機を使えばもっと簡単に調べられる事に気づいた.
$ fdisk -l ./RetroFW.img
Disk ./RetroFW.img: 1 GiB, 1073741824 bytes, 2097152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xbb005712
Device Boot Start End Sectors Size Id Type
./RetroFW.img1 16384 278527 262144 128M 83 Linux
./RetroFW.img2 278528 802815 524288 256M 82 Linux swap / Solaris
./RetroFW.img3 802816 2097151 1294336 632M c W95 FAT32 (LBA)
調べたとおりである.せっかくなのでもう少し踏み込む.
$ sudo mount -t ext4 -o ro,loop,offset=8388608 RetroFW.img /mnt
$ file /mnt/bin/busybox
/mnt/bin/busybox: setuid ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
$ readelf -a /mnt/bin/busybox
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: MIPS R3000
Version: 0x1
Entry point address: 0x402fb0
Start of program headers: 52 (bytes into file)
Start of section headers: 800132 (bytes into file)
Flags: 0x50001007, noreorder, pic, cpic, o32, mips32
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 26
Section header string table index: 25
:
Attribute Section: gnu
File Attributes
Tag_GNU_MIPS_ABI_FP: Hard float (double precision)
MIPS32のバイナリで,FPU搭載のチップを使っているようである.
実際に購入した中華レトロゲーム機の製品名でググってみるとJZ4760というチップを使っている,という情報がありIngenic Semiconductor - Wikipediaを見るとMIPS32 rev1でFPUありの情報が得られる.
またバイナリはuClibcを使うようビルドされていることがわかる.
U-Bootのバージョン
バイナリエディタでU-Bootのバイナリを見てみると,採用しているバージョンが以下であることがわかる.
U-Boot 1.1.6 (Jul 26 2018 - 14:28:08)
1.1.6となると2006年頃のものだろうか,ずいぶん古いものを使っている.ま,ブートローダーなので古くても構わないか.
現行のリリースバージョンはv2019.07のようだが,jz4760向けの実装は見当たらずにjz4780の実装しかなさそうである.
Board: Ingenic LEPUS (CPU Speed %d MHz)
という文字列も見つけた.LEPUSというのはリファレンス・デザインボードの名称のようであるため,これをキーワードに探せばjz4760向けの実装も見つかるのではないかと思われる.
ひとまずu-boot-1.1.6-jz-20120904-r1819.patch.gzなんてものを見つけた.これにはがっつりjz4760向けの実装が含まれている.RAMが使えるようにする部分の実装は,ボードの定数由来のパラメータが含まれることがある.リファレンス・デザインにしたがったボード設計になっているかがキモになるハズ.
Linuxのバージョン
同様にバイナリエディタでuImageのバイナリを見てみると,採用しているバージョンが以下であることがわかる.
Linux-2.6.31.3
2009年頃のものだろうか,これまた古いものを使っている.
現行のstableバージョンは5.2.14のようだが,これまたjz4760向けの実装は見当たらずにjz4740, jz4770, jz4780の実装があるようだ.jz4740との違いはクロック周波数とFPUの有無,GPUの有無くらいと書かれていたので,jz4740向けのビルドで動いてしまうのかもしれない(ホントか?).
こちらもGPLだから,探せばjz4760向けの実装も見つかるのではないかと思われる.
ひとまずlinux-2.6.31.3-jz-20120904-r1448.patch.gzなんてものを見つけた.これにはがっつりjz4760向けの実装が含まれている.
標準的な開発環境
RetroFWの標準的な開発環境を探してみたところ,さっくりと見つかった.
Topic: RetroFW - Developer Support Threadに書かれてあるチュートリアルどおりに構築すれば,サンプルのIPKを作るところまであっさりできた.
開発環境構築の要点
チュートリアルに書かれてある要点は以下の通りである.
- Ubuntuが動作する環境を用意する
- BuildRootをビルドするためのパッケージを導入する
- BuildRootをビルドする
- ビルドしたツールチェインにパスを通す
チュートリアルにはVirtualBoxを使ってUbuntuの動作環境を用意していたが,試しにWindows Subsystem for LinuxのUbuntuを使ってみたところ,一部を除き問題なく構築できた.なおWSLの導入については,説明を割愛する.
次にWSLのUbuntuに,BuildRootをビルドするためのパッケージを導入する.以下の通り.
$ sudo apt install build-essential libncurses5 libncurses5-dev git python unzip bc
次にBuildRootを取得し,ビルドする.
BuildRootのバージョンは2018.02.9を使うよう指定されており,新しいバージョンがリリースされていることに気づいても使わないように記載されている(ツールチェインのバージョンに敏感だから,とある).
$ wget https://buildroot.org/downloads/buildroot-2018.02.9.tar.gz
$ tar -xzvf buildroot-2018.02.9.tar.gz
$ cd buildroot-2018.02.9
$ make menuconfig
buildrootのコンフィグレーションは,以下のようにするよう記載されている.
Target Options
Target Architecture - MIPS (little endian)
Disable soft-float
Toolchain
C library (uClibc-ng)
Enable WCHAR support
Enable C++ support
Target Packages
Graphic libraries and applications (graphic/text)
SDL
SDL_gfx
SDL_image
SDL_mixer
SDL_net
SDL_sound
SDL_TTF
あとはビルドするだけ.ツールチェイン(クロスコンパイラ)もまるごとビルドするため,結構時間がかかる.
$ export FORCE_UNSAFE_CONFIGURE=1
$ make
makeの最中にrootfsを作るのだが,WSLではこれがエラーになるようだ(fakerootがなんたら,fakedがなんたら,と出る).今回は開発環境が構築できればよく,rootfsは使わないため無視してしまう.同様の手順をVirtualBox + Ubuntu 18.04.3 LTSで試したところ,エラーが出ることなくビルドが終了した.
このあとは好みに依存するが,ツールチェインを指定のディレクトリにコピーしてPATHを通すよう書かれている.
$ mkdir /opt/rs97tools
$ cp -R output/host/* /opt/rs97tools/
$ export PATH=/opt/rs97tools/mipsel-buildroot-linux-uclibc/sysroot/usr/bin:$PATH
$ export PATH=/opt/rs97tools/bin:$PATH
これでmipsel-linux-gcc
やsdl-config --libs
が実行できれば良い,とある.
お好みでサンプルプロジェクトのビルドを試してみるのも良い.
$ mkdir /opt/rs97apps
$ cd /opt/rs97apps
$ git clone https://github.com/jbanes/rs97-commander
$ cd rs97-commander
$ make
まとめ
購入した中華レトロゲーム機ではOSとしてLinuxが動作しており,エミュレータがLinuxアプリケーションとして動作していることがわかった.
U-BootやLinuxは結構古いものを使っているが,チュートリアルが用意されているため,開発環境構築は容易である.
またSDL(1.2)に準拠したアプリケーションを書くことで,中華レトロゲーム機で動作させられそうなことがわかった.
SDL2をサポートしたRetroFW v2.0が開発中とのことで,リリースを心待ちにしている.アプリケーションの開発環境を改善すべく,longtermなLinuxカーネルをポーティングしてみるとか試してみてたけど中断してしまった.
おまけ
ビルドを試す
せっかくツールチェインが用意できたので,U-Bootのコンパイルを試みる.
U-Boot
$ wget ftp://ftp.denx.de/pub/u-boot/u-boot-1.1.6.tar.bz2
$ tar jxvf u-boot-1.1.6.tar.bz2
$ cd u-boot-1.1.6
$ gzip -cd ../u-boot-1.1.6-jz-20120904-r1819.patch.gz | patch -p1
この例を参考に,include/asm-mips
以下にあるヘッダファイルの内容を変更する.大雑把に言うとinline
や__inline__
が付加されているにも関わらずextern
されているのをstatic
に修正する.
$ make lepus_msc_config
$ vi Makefile
(SUBDIRS から examples を削除)
$ make
これでu-boot-msc.bin
ができる.動くかは試してない.
BuildRootで作られるツールチェインは,U-Bootをビルドするのにバージョンがマッチしないようだということがわかった.これは,ヘッダファイルの修正の参考にしたペイジの記載によると,ツールチェインが新しい場合にこのエラーになることが書かれているためである.
実はWSLのUbuntuでインストールできるgcc-mipsel-linux-gnu(gcc-7.4.0)でも,ビルドが通る.
$ apt install gcc-mipsel-linux-gnu
したあとに,
ifeq ($(ARCH),mips)
CROSS_COMPILE = mipsel-linux-gnu-
endif
と変更してmakeすれば良い.
.configを見たくて聞いてみたら,ソースコードを持っていないと言われた.
しかもスクラッチから作った方がいい,とか言うし.いやぁ,こっちはコンパイル,リンクまでてきているので確認して動けばそれでいいと思っている.動かなかったときのデバッグ方法を調べたうえで,仕込んでから確認しないとハマってしまうけど(LEDデバッグかなぁ).
Linux
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/linux-2.6.31.3.tar.gz
$ tar zxvf linux-2.6.31.3.tar.gz
$ cd linux-2.6.31.3
$ gzip -cd ../linux-2.6.31.3-jz-20120904-r1448.patch.gz | patch -p1
$ make lepus_defconfig
$ make
CHK include/linux/version.h
CHK include/linux/utsrelease.h
SYMLINK include/asm -> include/asm-mips
HOSTCC scripts/basic/fixdep
HOSTCC scripts/basic/docproc
HOSTCC scripts/basic/hash
CC kernel/bounds.s
In file included from include/linux/compiler.h:40:0,
from include/linux/stddef.h:4,
from include/linux/posix_types.h:4,
from include/linux/types.h:14,
from include/linux/page-flags.h:8,
from kernel/bounds.c:9:
include/linux/compiler-gcc.h:86:30: fatal error: linux/compiler-gcc6.h: No such file or directory
#include gcc_header(__GNUC__)
^
compilation terminated.
/home/yochy/linux-2.6.31.3/./Kbuild:35: recipe for target 'kernel/bounds.s' failed
make[1]: *** [kernel/bounds.s] Error 1
Makefile:977: recipe for target 'prepare0' failed
make: *** [prepare0] Error 2
というわけで,このままではコンパイルが通らない.使用しているコンパイラがgcc-6.4.0なのに対して,Linuxカーネルソースはgcc6を知らない,と言っているため.Ingenicが出している文書(Ingenic Linux Development Guide)を見ると,使用しているgccは4.1.2である.
WSLで作業を続けるには,これの64bitバイナリを作る必要がある.VirtualBoxのUbuntuなら32bitラインタイムライブラリと,ビルド済みのgcc-4.1.2を入れるだけでよいだろう.
一旦作業をVIrtualBox上のUbuntuに変え,以下のようにすると...
$ cd /opt
$ sudo tar jxvf ~/mipseltools-gcc412-glibc261.tar.bz2
$ export PATH=/opt/mipseltools-gcc412-glibc261/bin:$PATH
$ sudo dpkg --add-architecture i386
$ sudo apt install libc6:i386
これで使えるようになる.
$ make lepus_defconfig
$ make
:
TIMEC kernel/timeconst.h
Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at kernel/timeconst.pl line 373.
こんどはperlで文句を言われる.ここを参考にkernel/timeconst.pl
を書き換えれば良い.
次はこんな感じ.
$ make
:
drivers/video/jz4760_lcd.c:141: error: 'LCD_CTRL_BST_64' undeclared here (not in a function)
これは.config
で定義しているLCDの宣言に関係する.
# CONFIG_JZ4760_LCD_TOPPOLY_TD025THEA7_RGB_DELTA is not set
CONFIG_JZ4760_LCD_TOPPOLY_TD043MGEB1=y
# CONFIG_JZ4760_LCD_TRULY_TFTG320240DTSW_18BIT is not set
どれをyにすればよいのかわからないので,ひとまずここまで.LCD_CTRL_BST_64
自体はarch/mips/include/asm/mach-jz4760b/jz4760blcdc.h
にあるので,これを#include
すれば通るようになるだろう.
.configを見たくて聞いてみたら,カーネルソースを公開してもらえた.ここまでくれば,今どきのカーネルバージョンにポーティングも不可能ではないが,アプリケーションがちゃんと動くかわからないので保留かな.
v1.2.1とv1.2を比較する
RetroFW v1.2.1が出ているのに気づいたので,解析時に用いていたv1.2との差をナナメな方向から調べてみた.
結論を言ってしまうと,中身は同じものである.リリースのコメントを見ても,commit messageを見ても,中身を変えたとは書かれていない.
RetroFW.binの中身を各ゲーム機向けに書き換えるバッチファイルの名前が親切になったくらいか.
正直に言うと,RetroFW.binの0x0000_0000から0x087f_ffff(Linux領域の終端)までを切り取ってmicroSDに書き込めばFAT32領域が初期化されることなくそのままでアップデートできるからソフト入れ直す必要ないですよ,と書きたかった.残念.
U-Boot/Linux kernel
U-BootとLinux kernelは同一であった(sha256の値が同一).これ目当てでアップデートする意味はない.
RetroFW.img
RegroFW.imgの中身は異なっていた(sha256の値が異なる).
U-BootとLinux kernelが同一であることはわかっているため,MBRから求めたLinux領域に差があれば,なにかしら恩恵を受ける可能性がある.
そんなわけでRetroFW.imgのオフセット0x80_0000(LBAで0x4000セクタ)以降を比較したのだが,残念なことに内容は同一であった.
v2.0とv1.2を比較する
RetroFW v2.0が出ているのに気づいたので,解析時に用いていたv1.2との差を調べてみた.
結論から言うと,BuildRootの中身が変わっているので試す価値はあると思われる.詳しくはCHANGELOG.mdを参照されたい.
なおU-BootやLinuxカーネルは(恐らく)同一である.
パーティション構成
MBRを見ると,パーティション構成(サイズ)に若干差があったので示しておく.
パーティションNo. | 先頭セクタ(LBA) | セクタ数 | パーティション種類 |
---|---|---|---|
#1 | 0x0000_4000 | 0x0004_f800 | Linux |
#2 | 0x0005_3800 | 0x0007_0800 | Linux swap |
#3 | 0x000c_4000 | 0x0013_c000 | FAT32 |
#4 | - | - | 空き |
Linux領域が若干拡張され,その分だけswap領域が減らされている.FAT32領域の先頭セクタは変わっていない.
従ってMBRとLinux領域のみを差し替えることで,FAT32領域を初期化せずに更新可能だと思われる.
BuildRootのバージョン
一緒にリリースされているBuildRootのバージョンを確認すると,2018.02.11を使っているようである(CHANGESより).先に示したチュートリアルには2018.02.9を使うように書かれているが,バージョンを更新したようである.
v2.1とv2.0を比較する
RetroFW v2.1が出ていた.
v2.0以降は更新情報が親切に書かれているので,リンク先のリリースノートを読んで必要性の可否を判断すればいいだろう.
ちなみにFAT32領域以前を差し替えて更新しようとしたら,再初期化が走った.転送済みのあれやこれが消えてしまうので,バックアップをお忘れなく.
V2.2とv2.1を比較する
RetroFW v2.2が出ていた.
なぜか今回は更新情報がちゃんと書かれていない.Readme.mdにそれっぽいものが書かれているが,システムリカバリのQ.O.L.変更って何のことだろう(この場合のQ.O.L.ってQuarity of Lifeでいいんだろうか).
今回のイメージファイルはちょっと変わっていて,パーティションが1つしか含まれていない.
パーティションNo. | 先頭セクタ(LBA) | セクタ数 | パーティション種類 |
---|---|---|---|
#1 | 0x0000_4000 | 0x0004_f800 | Linux |
#2 | - | - | 空き |
#3 | - | - | 空き |
#4 | - | - | 空き |
microSDに書き込んで電源を入れてみたところ,当然のように初期化処理が走った.これまでの処理に比べて早く終了したように感じる.初期化処理を済ませたあとのパーティション構成は以下のようになっていた.
パーティションNo. | 先頭セクタ(LBA) | セクタ数 | パーティション種類 |
---|---|---|---|
#1 | 0x0000_4000 | 0x0004_f800 | Linux |
#2 | 0x0005_3800 | 0x0007_0800 | Linux swap |
#3 | 0x000c_4000 | 0x01c6_a000 | FAT32 |
#4 | - | - | 空き |
これまでと比較してFAT32領域が広くなった.
ところでイメージファイルのサイズが175,112,704 byteなのだけど,これが342,017(0x53801) sectorで,ぎりぎりswapファイルのパーティションに食い込んでいる.1セクタ分小さくしなかったのは理由があるんだろうか.
さてもう少しReadme.mdを読むと,libopkやopkrunが取り込まれPyMenuやSimpleMenuが使えるようになった,とある.えっと...PyMenu,SimpleMenuのことかな.
あとでもう少し見ておこうかな.