OSなしのCPUで、どうやってCのmain関数の実行にたどりつくのか、というのは概念としては知っているけど、個々のCPUで手順は異なる。
“OSなしのCPUで”などと書いたけど、OSの立ち上げだって同じ話だったりする。ブートローダーというキーワードで片付けられたりするけど。
ということで、電源投入からそれなりに正しい手順でmain関数を実行するまでのコードを作った。
https://github.com/mgwsuzuki/v6pi/tree/master/base
それなりに正しい手順だけど、テスト目的程度のレベルである。
ベクタテーブル
CPUは命令を実行している間に割り込みが入ったとき、ある特定のアドレスにジャンプしたり、特定のアドレスを読み出してそこにジャンプしたりする。このような動作を例外処理と呼び、割り込みは例外処理の1つである。
CPUの例外要因は1つではない。そのため、例外要因ごとに所望の処理ルーチンにジャンプするのを効率化するために、例外要因ごとに番号をつけ、それをインデックスとしたテーブルを作る。これをベクタテーブルなどと呼ぶ。
テーブルの中身はRasPiに使われているARMでは命令そのものである。ただ、CPUごとに実装はそれぞれでジャンプ先のアドレスだったりするものもある。
このテーブルのベースアドレスは0x0か、0xffff_0000を選べる。0xffff_0000はMMUを使ったOSで使用することを想定している。初期状態は0x0である。
ARMでは以下のようなテーブル定義となっている。
開始アドレス | 例外/割り込み名 |
---|---|
0x0000_0000 | リセット |
0x0000_0004 | 未定義命令 |
0x0000_0008 | ソフトウェア割り込み |
0x0000_000c | プリフェッチアボート |
0x0000_0010 | データアボート |
0x0000_0014 | 予約 |
0x0000_0018 | 割り込み要求 |
0x0000_001c | 高速割り込み要求 |
このテーブルには命令を記述する必要があり、要するにアセンブラの命令を記述すればよい。ということでvector.sには次のようなコードを記述している。1命令あたり4byteなので、ぴったり8つの要素があるベクタテーブルとなる。
_start:
ldr pc, htbl_reset
ldr pc, htbl_undef
ldr pc, htbl_swi
ldr pc, htbl_pabt
ldr pc, htbl_dabt
ldr pc, htbl_unused
ldr pc, htbl_irq
ldr pc, htbl_fiq
“htblなんとか”から始まるラベルは次のようになっている。
htbl_reset:
.word reset_handler
htbl_undef:
.word undef_handler
htbl_swi:
.word swi_handler
htbl_pabt:
.word pabt_handler
htbl_dabt:
.word dabt_handler
htbl_unused:
.word unused_handler
htbl_irq:
.word irq_handler
htbl_fiq:
.word fiq_handler
“なんとかhandler”は例外処理ハンドラの開始アドレスである。つまり、ldr命令で”なんとかhandler”のアドレスをプログラムカウンタに読み込ませてジャンプする。
ちなみに、ldr命令ではなくてb命令でジャンプさせる方法もある。が、32MBより離れたアドレスに分岐できないし、後で説明する回避できない副作用があるため用いない。
リセットハンドラ
リセットハンドラの処理は以下のようになっている。
ベクタテーブルのコピー
名前の通り、電源投入直後やCPUがリセットされたときに実行されるコードである。vector.sでは次のとおり。
mov r0, #0x8000
mov r1, #0x0000
ldmia r0!, {r2, r3, r4, r5, r6, r7, r8, r9}
stmia r1!, {r2, r3, r4, r5, r6, r7, r8, r9}
ldmia r0!, {r2, r3, r4, r5, r6, r7, r8, r9}
stmia r1!, {r2, r3, r4, r5, r6, r7, r8, r9}
やっていることは、0x8000から32byte分のデータを0x0にコピーしている。
実は、_startラベルから始まるベクタテーブルは、アドレス0x8000に配置されている。0x8000は何かというと、RasPiのARMが起動したときに実行が開始されるアドレスなのである。0x0でない。
これはRasPiに搭載されているSoC(及びメーカー配布のブートローダー)がそうなっているため、どうしようもない。だからベクタテーブルを0x8000に配置すれば電源投入直後だけはしのげる。
かといって、ARMのベクタテーブルの開始アドレスはやはり0x0である。なので、0x8000にあるままでは正しく動作しない。ということで、上記のコードで0x8000にあるベクタテーブルを0x0へコピーしている。
ここにベクタテーブルをb命令で記述できない理由がある。b命令はプログラムカウンタをベースとした相対アドレスで分岐先アドレスを決定するため、命令を別のアドレスに移動すると分岐先アドレスが狂ってしまうのである。
ldr命令もプログラムカウンタをベースとした相対アドレスで読み出しアドレスを決定するけど、読み出し先データも矛盾なくコピーするので問題ない。そして、読み出し先に書いてあるジャンプ先アドレスは絶対アドレスなので問題なくジャンプできる。
モードとスタックポインタの設定
ARMはCPUの動作モードして以下のようなものがある。
- アボート
- 高速割り込み要求
- 割り込み要求
- スーパバイザ
- システム
- 未定義
- ユーザ
これらはcpsr(レジスタ)の[4:0]に値を書き込むことで変更できる。といっても、書き込み権限のないモードになってしまうと書き込めない。
もうひとつ大切なことは、各モードごとにいくつかのレジスタが切り替わる、バンクレジスタとなっていて、スタックポインタもそれに含まれる。
現状のv6piではCPUモードを切り替えるようなレベルではないため、スーパバイザのみ設定しているが、例えば割り込みを使うようになったらそのモードにおけるスタックポインタを正しく設定する必要がある。
mov r0, #(0x13|0xc0)
msr cpsr_c, r0
mov sp, #0x10000000
なお、上のコードでは割り込みを禁止している。
メモリの初期化
ブートローダーによって、SDカードから実行したいプログラムはすでにメモリに読み込まれているが、これを実行する前の準備が必要である。
1つは初期化データのコピー、もうひとつはBSS領域をゼロクリアすることである。
初期化データはプログラムが実行される前にメモリにセットされている値だが、これは.textの後ろに配置されていて、これを.dataの領域にコピーする必要がある。
ldr r1, =__text_end__
ldr r2, =__data_start__
ldr r3, =__data_end__
1: cmp r2, r3
ldrlo r0, [r1], #4
strlo r0, [r2], #4
blo 1b
このおかげで初期化データをプログラムが変更しても、リセットすることでもとの値へ復活させることができる。
bss領域は、初期値がゼロでなければいけない。ということでゼロクリアする。
mov r0, #0
ldr r1, =__bss_start__
ldr r2, =__bss_end__
2: cmp r1, r2
strlo r0, [r1], #4
blo 2b
なお、”__“で始まるシンボルはmemmapに記述されており、リンカの処理で値が確定する。
mainをコール
ここでようやくmain関数をコールすることができる。
bl main
書きながら気が付いたが、main関数から戻ってきたときの処理が入っていない。後で無限ループを追加しておこう。
その他のハンドラ
リセット以外のハンドラを記述するためのテンプレートである。現在は何も使っていないのですべて無限ループとなっているが、必要に応じて記述する。
undef_handler:
b undef_handler
swi_handler:
b swi_handler
pabt_handler:
b pabt_handler
dabt_handler:
b dabt_handler
unused_handler:
b unused_handler
irq_handler:
b irq_handler
fiq_handler:
b fiq_handler