このメモは、筆者が卒業研究においてGBA上で動作するOSを作成する過程に残したメモを連ねたものである。「Linuxから目覚めるぼくらのゲームボーイ!」を読んで得られる知識については触れられていないのであしからず。
GBA特有のメモだけでなく、アセンブラやリンカなどの話題にも及ぶ。
MultiBootモードからの起動の場合、0x02000000番地から始まるわけだが、通常のROMカードリッジからの起動の場合、開始アドレスは0x08000000番地で、まずカートリッジ情報*1が並び、その後ゲーム命令が始まるようになっている。
GBAにおけるハードウェア割り込みはIRQ例外として扱われる。IRQ例外が発生するとARMはIRQ例外モードに移行し、PCとステータスレジスタの値をバンクレジスタに待避して、IRQ例外ベクタアドレスである0x00000018番地へ飛ぶ。しかし、GBAのメモリ空間の先頭16KBはBIOSに割り当てられているので、任意の命令を例外ベクタアドレスに書き込むことができない。その代わりGBAではBIOSで何らかの処理を行った後、0x03007ffc番地に処理を移すため、ユーザはそこにハードウェア割り込みに対する処理を書くことができる。しかし、ハードウェア割り込み以外の例外が発生したときの流れはいまいち不明瞭。
ソフトウェア割り込みを発生させるにはSWI命令を使うのたが、この命令が発行されるとGBAはBIOSの組み込みルーチンを実行する。具体的にはSWI命令の引数に応じてBIOS内にある組み込みルーチンのアドレステーブルを参照し、組み込みルーチンのアドレスを得てそれを実行する。
なお、未定義命令、プリフェッチアボート、データアボート、予約、FIQの例外ベクタアドレスは、すべて同一のアドレスに飛ぶようになっている。
とはいってもMappy VMでのお話だが。.mbファイルを読ませると、CPSRのモードビットがSVRを示している(0x600000d3)。ちなみに実機ではSYSモードだった(0x8000001F)。
ソフトウェア割り込みを発生させるSWI命令は、GBAではBIOS組み込み関数のコールのためにフックされている。当初はこの命令の途中で、ハードウェア割り込みと同様にユーザ定義例外ハンドラ*2へジャンプしてくれるものと考えていたが、BIOSのダンプを眺めてみる限りそのような動作はしないようである。
しかしながら、BIOS組み込み関数アドレスをロードするルーチンはBIOS組み込み関数番号の範囲をチェックしないため、BIOS組み込み関数番号に巨大な値を与えれば任意のアドレスへ処理をとばせるのではないかという指摘があり。一瞬心躍らせたのだが、BIOS組み込み関数のアドレス計算は(組み込み関数ベースアドレス + SWI命令のコメントフィールド上位8ビット * 4)であるため、どうあがいても(組み込み関数ベースアドレス + 255 * 4)までしかとばすことができず、ユーザが自由に使用できるRAM領域(0x02000000番地)までは届かない。
さらに、未定義命令を利用するという案もだめ。未定義命令もBIOSにきちんとフックされており*3、↑で書いたようにOS開発者のの処理が入り込める余地はない。
しかし、uClinux port to the GBA HOWTOによれば、swi 0xd40000を発行すると特権モードのまま処理をRAM空間のアドレスに移すことができるという。
実際にswi 0xd40000はどこを読むかというと、0xd4 * 4 + 0x1c8(BIOS関数アドレステーブルの先頭) = 0x518の値を読みにいく。0x518の値は0x02002040で、見事RAM空間の範囲内である。あとは何らかの方法で0x2002040番地に任意のプログラムを配置すればよいというわけである。
swi 0xd40000命令を発行すると、プロセッサの動作モードはSVCになる。そして、GBA BIOSがCPSRを書き換えてSYSモードに移行する。そして初めて処理が0x2002040へ移る。
さて、このSYSモードだが、いちおう特権モードの仲間であるものの、他の特権モードとはちょっと違う。一番大きな違いは、バンクレジスタを持たないことである。つまり、レジスタとスタックはUSRモードと共有である。privileged user modeとも呼ばれるように、SYSモードは「特権のあるUSRモード」と同義であるのだ。
で、swi 0xd40000を使ってシステムコールを呼ぶようにした場合、ソフトウェア割り込みからその終了までの流れは以下の通り。
ここで困るのが、スタックの扱いである。USRモードとSYSモードのspは同じレジスタであるため、同じスタックポインタを共有してしまう。SVCモードのsp_svcに別のスタックのポインタを割り当てても、それが使われるのはGBA BIOSの中だけである。結果的に、カーネルモードとユーザモードのスタックを別にすることができないので、そこは気合で何とかする必要がある。
マルチブートによって転送できる最小プログラムサイズは 256 バイトであるとの事だった。また,最大プログラムサイズは 256KB (0x40000) ではなく 256KB より 192 byte 小さい 0x3FF40 バイトとの事だった(ヘッダサイズ分小さい?)。
リンカスクリプト*5 gccでは初期化済み変数は.dataセクション、非初期化変数は.bssセクション、定数データは.rodataセクションに格納されるので、以下のようなリンカスクリプトになる。
OUTPUT_ARCH(arm)
SECTIONS {
.text 0x2000000 : { *(.text) }
.data : { *(.data) }
.rodata : { *(.rodata*) }
.bss : { *(.bss) }
}
なお、arm(GBA)用のものである。
スタック用のメモリ領域として100番地から200番地を予約したい、ということがよくある。そういう場合、リンカスクリプトで予約したいアドレスの開始位置にラベルを付けておく。そうすればC言語側でそのラベルを参照してあれこれやれる。
自分がやりたかったことは
この3つのコードをそれぞれ0x200000番地、0x202040番地、2のコードの終端に配置したいということだ。しかし、スタートアップコードの長さが0x2040より大きいとき、0x202040番地にソフトウェア割り込み処理コードをおくと、スタートアップコードが破壊されてしまうので、あらかじめ0x202040番地周辺を開けておくか、実行時に再配置するかしないといけない。
しかし、再配置のアプローチはリロケーションを行うコードを追加しなければならず、出来ればそれは避けたかった(とりあえず面倒だからだ)。
というわけで、以下のようなリンカスクリプトを書いて解決。
SECTION {
.text 0x2000000 : {
crt.o(.text)
. = 0x0002040;
*(.text)
}
}
てきとうにぐぐったらGBA開発をやってるっぽいひとの2004/08/02の日記を発見。ちょいと引用すると
参考までにARMにおける関数呼び出しのジャンプ命令について。
- devkitadvは出来る限りbl命令を使おうとします。最もCPUコストが少ないからです。
- しかし、bl命令は相対オフセット24bit、つまり32MBの領域までしかジャンプできないため、ROM(0x08000000)とIWRAM(0x03000000)を相互に行き来することが出来ません。
- そこでbx命令を使います。bx命令は32bit領域にアクセスできるためどこからどこへでもOKです。少しCPUコストがかかります。
- devkitadvにbx命令でジャンプするように強制するためには、グローバル変数(これは最適化されません)に関数ポインタを一度代入してから使うことになります。上で挙げたコードになります。
- __attribute__ ( (section ("".data.iwram""), long_call) )でbx命令になるかと思ったらなりませんでした。残念です。
bl命令に与えるオペレータで重要なのは範囲ではなくて、b命令にあたえる即値の大きさね。mainがある場所は0x2000000より後ろなわけだから、b mainはb 0x2??????に置換されると。で、0x2??????は余裕で24ビットを越えてしまうわけで、つまりb命令じゃだめというわけ。
ただ単にmainを一番はじめに呼ぶcrt*6ファイル
.text b main
アセンブリ言語におけるラベル付き処理をC言語から関数のように呼び出すには、.globl指示詞を用いる(参考)。
--- func.s ---
.globl func
func:
; 何らかの処理
--- main.c ---
int main() {
func();
return 0;
}
非常にシンプルなファイル構成によるテスト。
crt.S(C言語関数のmainに飛ぶだけ)
.text
b main
dots.s(GBAのLCDの左上隅(0, 0)にr0の色(GBAの15bitBGR形式)でドットを打つ)
.arm
.text
.globl dots
dots:
mov r1, #0x4000000
ldr r2, =0xF03
strh r2, [ r1 ]
mov r3, #0x6000000
strh r0, [ r3 ]
test.c(dots.sのdots関数(サブルーチンと呼ぶべき?)をよけいな引数をたくさんつけて呼び出す)
extern void dots(int color, int a, int b, int c, int d, int e);
int main() {
dots(255, 1, 2, 3, 4, 5);
while(1);
return 0;
}
上記4つのファイルを以下のMakefileでMake。オプションなどは必要最低限のもの。
AS=as-arm
CC=gcc-arm
LD=ld-arm
LS=dots.ls
ELF=test.out
OUT=test.mb
DUMPSFX=.dump.s
OBJCOPY=objcopy-arm
OBJDUMPARM=objdump-arm -D -b binary -m arm --adjust-vma=0x02000000
all:
${AS} -o crt.o crt.S
${AS} -o dots.o dots.s
${CC} -c -o test.o test.c
${LD} -o ${ELF} -T ${LS} crt.o test.o dots.o
${OBJCOPY} -O binary test.out test.mb
${OBJDUMPARM} ${OUT} > ${OUT}${DUMPSFX}
最終的にできあがる、GBAのMultiBootモードで実行可能なバイナリファイルの逆アセンブル:
test.mb: ファイル形式 binary
セクション .data の逆アセンブル:
02000000 <.data>:
2000000: eaffffff b 0x2000004
2000004: e1a0c00d mov ip, sp
2000008: e92dd800 stmdb sp!, {fp, ip, lr, pc}
200000c: e24cb004 sub fp, ip, #4 ; 0x4
2000010: e24dd008 sub sp, sp, #8 ; 0x8
2000014: e3a03004 mov r3, #4 ; 0x4
2000018: e58d3000 str r3, [sp]
200001c: e3a03005 mov r3, #5 ; 0x5
2000020: e58d3004 str r3, [sp, #4]
2000024: e3a000ff mov r0, #255 ; 0xff
2000028: e3a01001 mov r1, #1 ; 0x1
200002c: e3a02002 mov r2, #2 ; 0x2
2000030: e3a03003 mov r3, #3 ; 0x3
2000034: eb000000 bl 0x200003c
2000038: eafffffe b 0x2000038
200003c: e3a01301 mov r1, #67108864 ; 0x4000000
2000040: e59f2008 ldr r2, [pc, #8] ; 0x2000050
2000044: e1c120b0 strh r2, [r1]
2000048: e3a03406 mov r3, #100663296 ; 0x6000000
200004c: e1c300b0 strh r0, [r3]
2000050: 00000f03 andeq r0, r0, r3, lsl #30
test.cにおけるmain関数は0x2000004~0x2000038まで。dots.sは0x200003c~0x2000050までである。0x2000034のbl命令でdots.sの処理に飛んでいるのが分かる。
で、コレを見てみると、はじめの4つの引数は直接r0~r3に代入されて、それ以降の引数はすべてスタックに積まれていることが分かる。ちなみにさらに少ない引数、さらに多い引数でも試したが、同様。
ということはC言語側からアセンブリ言語で書かれた処理を呼ぶとき、引数は4つまでにしておいた方が楽であろう。実際4つ以上の引数をとる関数はあまり書かないと思われるので、実質、引数の受け渡しはr0~r3に注目しておけばいいことになる。
ただし、コンパイラに何らかの命令を与えることでこの辺は調整可能なのかも知れない。調べてないので分からないが。
C言語関数→アセンブリラベル、あるいはその逆のとき、4つめまでの引数はr0~r3で渡される。それ以上の引数はスタックにつまれる。後ろの引数ほど下に(アドレス的には後ろ)つまれる。
hoge_func(1, 2, 3, 4, 5, 6, 7); r0 : 1 r1 : 2 r2 : 3 r3 : 4
hoge_funcをよぶ直前のspが0x1008のとき、
0x1000 : 5 0x1004 : 6 0x1008 : 7
hoge_func内ではspは0x1000を指している。
なお、戻り値はr0に格納される。
UNIX上でのクロスgccのビルド with newlibが詳しい。
アセンブリをそのまま機械語に落とし込んでほしいソースを最適化オプションつきでコンパイルするとはまることが多い(最適化によってコードが変わってしまい、不都合が起きる)。ただし、ハードウェアアクセス部分など、速度が重要な局面では最適化を施すことによって精度が得られる場合もある。
GBA界では*.gba(あるいは*.bin)というのがいわゆるROMカードリッジの生データで、*.mbというファイルはマルチブートモード用の実行データらしい。今まで動かなかったぼくらのゲームボーイ!のサンプルの*.bin形式のファイルを、試しに*.mbにリネームしてVisualBoyAdvanceに投げたら動いた。びっくり。
アドレス指定を行っている行にかかれたコメントは単純に即値を16進に直したものではなく、レジスタなどと組み合わせてアドレッシングを行っている場合はその計算結果になっている。
当然ながら独自BIOSで動作しているエミュレータはswi 0xd40000しても0x2002040番地には飛ばない。swi 0xd40000したときに0x2002040に飛ぶのは仕様ではなく(おそらく)偶然だからだ。だからそのままだとこのへんのデバッグは実機上でやらざるを得ない*7。
が、ほとんどのエミュレータは外部BIOSファイルを読み込めるようになっている。そこに実機からアレしたソレを読ませれば、実機さながらの動作をしてくれる。
エミュレータにはデバッグ機能が搭載されていることが多く、ブレークポイントの設置やステップ実行なんかができるものもあり、開発に非常に役立つ(特にMappy VMは素晴らしい)。
とりあえず挿すと、Linuxは新しいUSBデバイスを発見してくれた。が、USBメモリなどを挿したときのようになんらかのデバイスドライバがロードされるとかそういう動作はない。Linux君は「なんかあたらしいUSBデバイスささったみたい」としか言ってくれないのだ。
オプティマイズのWebには、ブートケーブルのデバイスドライバ(バイナリのみ)と、GBAへプログラムを転送するツールのソースコードが置いてある。しかし、これらは残念なことにWindows用である。
書籍「Linuxから目覚めるぼくらのゲームボーイ!」にはその転送プログラムのソースコードを参考に作られたLinux版の転送プログラムが添付されている。ソースコードを眺めてみると、どうやらlibusbを使ってブートケーブルとやりとりしているらしい。
そこでlibusbのソースを入手して眺めてみる。linux依存部分であると思われるlinux.cの usb_os_open関数では、なんらかのファイルを開いているような記述かある。
snprintf(filename, sizeof(filename) - 1, ""%s/%s/%s"", usb_path, dev->bus->dirname, dev->device->filename); dev->fd = open(filename, O_RDWR);
usb_pathはgetenv(""USB_DEVFS_PATH"")の戻り値(/proc/bus/usb)、dev->bus->dirnameはバス番号(っていうのか?)、dev->device->filenameはデバイス番号(?)である。それぞれlsusbしたときに表示されるBusとDeviceの右にかかれた数字である。
そのファイルに対してioctlしてreadとwriteを行っているようだ。このへんの面倒なことをlibusbは面倒見てくれちゃってるわけである。このlibusbの関数を使ってブートケーブルの通信プロトコルにしたがってコマンドを発行してあげれば見事に通信成功、となる。
そんなRubyスクリプト。8x8フォント専用。
#!/usr/bin/ruby
# BDF -> c header file for GBA
begin
print(""unsigned char char8x8[][8] = {\n"");
cn = 0
while (line = STDIN.readline) do
if /^ENCODING (.+)$/ === line
cn = $1.to_i
elsif line.chomp == 'BITMAP'
ary = []
while (line = STDIN.readline) do
break if line.chomp == 'ENDCHAR'
ary << '0x' + line.chomp
end
print(""\t{ #{ary.join(', ')} }, // ASCII code #{cn.to_s}\n"")
end
end
rescue EOFError
end
print(""};\n"");
jibunstyle.net>