GBAプログラミングメモ

更新履歴

はじめに

このメモは、筆者が卒業研究においてGBA上で動作するOSを作成する過程に残したメモを連ねたものである。「Linuxから目覚めるぼくらのゲームボーイ!」を読んで得られる知識については触れられていないのであしからず。

GBA特有のメモだけでなく、アセンブラやリンカなどの話題にも及ぶ。

BIOS関連

プログラム開始アドレス

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の例外ベクタアドレスは、すべて同一のアドレスに飛ぶようになっている。

GBAのマルチブートモードは初期状態でSVCモード?

とはいってもMappy VMでのお話だが。.mbファイルを読ませると、CPSRのモードビットがSVRを示している(0x600000d3)。ちなみに実機ではSYSモードだった(0x8000001F)。

権限昇格(swi 0xd40000)とシステムコール

ソフトウェア割り込みを発生させる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を使ってシステムコールを呼ぶようにした場合、ソフトウェア割り込みからその終了までの流れは以下の通り。

  1. ソフトウェア割り込み(SVCモードに移行)
  2. GBA BIOSによる処理(SYSモードに移行、0x2002040番地へ移動。カーネルモード*4のはじまり)
  3. コンテキストの保存
  4. システムコールの実体を実行
  5. スケジューリング
  6. コンテキストの復元(GBA BIOSにリターン。カーネルモードの終わり)
  7. GBA BIOSによる処理の続き(SVCモードに移行)
  8. ユーザプログラムの続き(USR、あるいはSYSに移行)

ここで困るのが、スタックの扱いである。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言語側でそのラベルを参照してあれこれやれる。

任意の位置にコードを配置

自分がやりたかったことは

  1. スタートアップコード(GBAはここから読みはじめる)
  2. ソフトウェア割り込み処理コード(0x202040番地に配置したいコード)
  3. そのほかのコード(上記以外のコード、データ)

この3つのコードをそれぞれ0x200000番地、0x202040番地、2のコードの終端に配置したいということだ。しかし、スタートアップコードの長さが0x2040より大きいとき、0x202040番地にソフトウェア割り込み処理コードをおくと、スタートアップコードが破壊されてしまうので、あらかじめ0x202040番地周辺を開けておくか、実行時に再配置するかしないといけない。

しかし、再配置のアプローチはリロケーションを行うコードを追加しなければならず、出来ればそれは避けたかった(とりあえず面倒だからだ)。

というわけで、以下のようなリンカスクリプトを書いて解決。

SECTION {
  .text 0x2000000 : {
     crt.o(.text)
     . = 0x0002040;
     *(.text)
  }
}

ARMアセンブリにおけるジャンプ命令の制約

てきとうにぐぐったらGBA開発をやってるっぽいひとの2004/08/02の日記を発見。ちょいと引用すると

参考までにARMにおける関数呼び出しのジャンプ命令について。

bl命令に与えるオペレータで重要なのは範囲ではなくて、b命令にあたえる即値の大きさね。mainがある場所は0x2000000より後ろなわけだから、b mainはb 0x2??????に置換されると。で、0x2??????は余裕で24ビットを越えてしまうわけで、つまりb命令じゃだめというわけ。

アセンブリ(for ARM)

最も単純なCRT

ただ単に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に格納される。

ツール

gcc with newlib for ARM

UNIX上でのクロスgccのビルド with newlibが詳しい。

最適化

アセンブリをそのまま機械語に落とし込んでほしいソースを最適化オプションつきでコンパイルするとはまることが多い(最適化によってコードが変わってしまい、不都合が起きる)。ただし、ハードウェアアクセス部分など、速度が重要な局面では最適化を施すことによって精度が得られる場合もある。

VisualBoyAdvance

GBA界では*.gba(あるいは*.bin)というのがいわゆるROMカードリッジの生データで、*.mbというファイルはマルチブートモード用の実行データらしい。今まで動かなかったぼくらのゲームボーイ!のサンプルの*.bin形式のファイルを、試しに*.mbにリネームしてVisualBoyAdvanceに投げたら動いた。びっくり。

objdump

アドレス指定を行っている行にかかれたコメントは単純に即値を16進に直したものではなく、レジスタなどと組み合わせてアドレッシングを行っている場合はその計算結果になっている。

エミュレータでデバッグ

当然ながら独自BIOSで動作しているエミュレータはswi 0xd40000しても0x2002040番地には飛ばない。swi 0xd40000したときに0x2002040に飛ぶのは仕様ではなく(おそらく)偶然だからだ。だからそのままだとこのへんのデバッグは実機上でやらざるを得ない*7

が、ほとんどのエミュレータは外部BIOSファイルを読み込めるようになっている。そこに実機からアレしたソレを読ませれば、実機さながらの動作をしてくれる。

エミュレータにはデバッグ機能が搭載されていることが多く、ブレークポイントの設置やステップ実行なんかができるものもあり、開発に非常に役立つ(特にMappy VMは素晴らしい)。

オプティマイズのブートケーブルはLinuxでどのようなデバイスに見えるのか

とりあえず挿すと、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の関数を使ってブートケーブルの通信プロトコルにしたがってコマンドを発行してあげれば見事に通信成功、となる。

BDFフォントをC言語の配列に変換する

そんな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"");

参考リンク

転送関係

オプティマイズ
ブートケーブルUSBとか売ってる
GBA を UNIX マシンにつないで遊ぶページ
クロスコンパイラの作り方と、GBAへのプログラム転送のためのugbabtc

リファレンス

Dev'rs GBA Dev FAQs
GBAプログラミングのFAQ
ARMメモ
ARM7TDMIのデータシート和訳
GBATEK
GBAのスペックについて
CowBiteのスペック
エミュレータCowBiteのスペック
NWSOS
NWSOSと、OS作りのノウハウとか?
GNU リンカ LDの使いかた
ローレベルのプログラミングには必須知識

エミュレータ

Mappy VM
デバッガが充実しているエミュレータ(LinuxでもWINEで動作)
VisualBoyAdvance Homepage
再現性の高いエミュレータ(Linux版もある)

GBA上で動くOS

JaysOS
GBA上で動作するOS
UNIX on the Game Boy Advance
GBA上で動くUNIX

ARMプロセッサ対応組み込み向けオープンソースOS

Copyright © ymkn <ymkn@jibunstyle.net>