Open Design Computer Project

オリジナルCPUから作る本格的自作コンピューター

ユーザ用ツール

サイト用ツール


software:porting_xv6

新アーキテクチャに xv6 を移植

mist32 向けに xv6 を移植します。

方針

移植の方針としては、x86 依存な部分を削除・コメントアウトして、コンパイラにエラーを吐かせてどんどん書き換えていく。

よくわからんとか思ったら、サイトの textbook や、TRICKS ファイルなどを読んでみよう。(x86 をある程度知っていて、コードが読めるなら、大した量でもないのでコード読んだり動かしたほうがはやい。)

mist32 へ移植したコードの diff

1000行書き換えるだけで動きました。予想以上に少ない。

https://github.com/techno/xv6-mist32/commits/mist32

Makefile

TOOLPREFIX に、クロスコンパイル用のコマンドプレフィックスを指定する。

ブートローダーとかが用意されてるけど、今回は kernel の ELF を吐いてくれればいいので、まずディスクイメージを出力する部分を削除。qemu もないし、エミュレーターもとりあえず make で実行するつもりはないので、その部分を削除。

$ make kernel で、カーネルの ELF が出てくる。また、$ make kernelmemfs で IDE ディスクを使わない kernel を吐いてくれる。RAM の上に擬似的なファイルシステムを構築してくれる。(RAMdisk 的な感じ)

今回は kernelmemfs を使って移植してみる。ただし、Makefile に一部問題があるようで、現状の xv6 で xv6memfs.img, kernelmemfs を出力させるには、以下の修正が必要。

diff --git a/Makefile b/Makefile
index 20cb884..e2d9196 100644
--- a/Makefile
+++ b/Makefile
@@ -121,8 +121,8 @@ kernel: $(OBJS) entry.o entryother initcode kernel.ld
 # great for testing the kernel on real hardware without
 # needing a scratch disk.
 MEMFSOBJS = $(filter-out ide.o,$(OBJS)) memide.o
-kernelmemfs: $(MEMFSOBJS) entry.o entryother initcode fs.img
-       $(LD) $(LDFLAGS) -Ttext 0x100000 -e main -o kernelmemfs entry.o  $(MEMFSOBJS) -b binary initcode entryother fs.img
+kernelmemfs: $(MEMFSOBJS) entry.o entryother initcode kernel.ld fs.img
+       $(LD) $(LDFLAGS) -T kernel.ld -o kernelmemfs entry.o  $(MEMFSOBJS) -b binary initcode entryother fs.img
        $(OBJDUMP) -S kernelmemfs > kernelmemfs.asm
        $(OBJDUMP) -t kernelmemfs | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > kernelmemfs.sym

移植のポイント

entry.S

kernel のエントリーポイントはこのファイル。Multiboot Header がついてるけど、ブートローダーがまともに用意されてるアーキテクチャなら良いが、mist32 はそうではないので削除した。

あとは、x86 の機械語をだいたいそのまま翻訳していけば良い。

main.c

main() にずらーっと関数が並んでいる。上から見ていって、辿っていくのが良い。ただし、mpinit(), lapicinit(), ioapicinit(), startothers() などは、マルチコア x86 前提のコードなので、とりあえずシングルコアで実行するのであれば、ごっそり消して良い。mpinit() は、ncpu = 1 や ismp = 0 と cpus[] をちょっと初期化すればそれで終わり。それ以外はファイルごと削除で実行しなくてもよい。

seginit() は、後述する cpu-local storage を、どうにかしてここで設定するだけで OK. セグメントの設定とかは削除して構わない。

consoleinit(), uartinit() まで辿り着けば、cprintf() が使える。UART や画面出力は、アーキテクチャに応じて移植すれば良い。

timerinit() はタイマーの設定である、コンテキストスイッチに使うインターバルタイマーがあればそれで良い。10ms になってる。

userinit() で、最初のユーザープロセスが作られる。まだ実行はしない。

momain() でスケジューラーが立ち上がり、プロセスが実行され、起動する。

x86.h (<arch>.h)

とりあえず、x86.h は移植するなら当たり前だけど使わない。移植するターゲット向けのファイルを新しく作れば良い。

アセンブラで実行しなければいけない命令の関数定義が主な内容。各アーキテクチャで必要な物を追加していく。

trapframe は、後述するが、trap 時に構築されるスタックフレームの構造体である。逆にユーザープロセスを init するときに trapframe をスタック上に構築して、trapret している。つまり、割り込み時に退避しなければいけない物を並べておく。

vm.c

とりあえず、仮想メモリ関連のフラグが沢山使われているので、全部置き換える。

その他の関数は、適時意味を理解して、移植先の都合の良いように移植していく。セグメントは完全無視の方向で。

static struct kmap kmap[]

kmap は、カーネル領域のメモリレイアウトを定義する。memlayout.h を参照すると良い。

カーネル領域は、基本的に 0x80000000 から開始される。以下の順で配置されている。(x86 の物理メモリマップ)

  • Memory mapped I/O 領域 (BIOS とか)
  • text + rodata
  • data
  • Memory mapped I/O 領域 (PCI とか)

移植する CPU 向けに、お好みでやればいいと思う。そのままでも動くなら、そのままにしておいたほうが楽。x86 みたいにレイアウトがややこしいアーキはないだろうし… PHYSTOP が結構大きめの値をとっているので(200MB超)、そんなにメモリが乗ってないアーキテクチャなのであれば少なくすること。値を少なくするだけでほとんど問題ない。

proc.h

struct cpu, struct proc

CPU とプロセスの状態を保存しておく構造体を定義してある。さらに、その情報は x86 特有の GS レジスタを利用して格納されている。

extern struct cpu *cpu asm("%gs:0");       // &cpus[cpunum()]                                                                                                                                                                                 
extern struct proc *proc asm("%gs:4");     // cpus[cpunum()].proc 

つまりこれは、GS レジスタで指定されているセグメントの “アドレス 0” に &cpus[cpunum()] が保存されていて、“アドレス 4” に cpus[cpunum()].proc が保存されているということ。

x86 のようなセグメント・レジスタを持っているプロセッサは少ないと思うので、何らかの Thread-local な情報を保存するための何かを用意する必要がある。xv6 の実装は Linux pthread を参考にしているらしいが、この資料が参考になるかも知れない。SPARC は、g7 レジスタに持たせているらしい。スレッドローカルだが普段は使われないようなレジスタがあれば、有効的に使えるかも知れない。

少なくとも、extern struct proc * で asm を使って実装するのは x86 以外では難しそうで、マクロも cpu, proc が構造体宣言と被っていて使えないので、関数呼び出しににして呼んでる側も cpu(), proc() として、インライン関数として実装してしまうのが良さそう。ただし、volatile にしないとコンパイラにがっつり消去される (戻り値がスタックにキャッシュされたりする) ので注意。まあ、普通にメモリにグローバル変数として持たせてもいいんだけどね…

struct context

プロセスが、コンテキストスイッチされるときに、この構造体に退避される。caller-saved ではないレジスタを並べておけば良い。

proc.c

一番ややこしい部分。

userinit(), allocproc(), scheduler()

userinit() で最初のプロセスが作られる。プロセスを作るために allocproc() を呼んでいる。その後スケジューラーに投入され、scheduler() 内で switchuvm() したあとに、swtch() でレジスタの内容を入れ替え、コンテキストが切り替わる。

作られて allocproc() されたばかりのプロセスは、swtch() → forkret() → trapret() のように遷移する。xv6 のオリジナル実装は、それがうまく行くように、スタック上に trapframe と context を allocproc() で構築してある。

なので、移植する場合にはそのアーキテクチャでうまく行くように、レジスタやスタックをセットする必要があるが、x86 のようにスタックを使えば全てがうまく行くアーキテクチャばかりではないのでうまくいかないだろう。リンクレジスタなどを持っているプロセッサでは、なかなか難しい。場合によっては swtch からの戻りを、リンクレジスタを使った return ではなく branch 系命令でこなして、リンクレジスタにはその先の trapret のポインタを入れておくとか、そういう対応が必要。

userinit() では、ページテーブルを構築して、ユーザースタックとフラグレジスタ、initcode のエントリを trapframe に設定している。trapret() から initcode へ return するように trapframe を構築することでユーザー権限に落ちて、スタックが切り替わり、割り込みが有効になり、更にその先 exec システムコールを発行している。

このあたりの移植は、x86 べったりに書かれているので、非常に苦痛。

vectors.pl

割り込みベクタに登録される関数を自動生成する。割り込み番号を push して、alltraps() を呼んでいる。

trapasm.S

alltraps()

vectors.pl に書かれた関数から飛んでくる、共通の割り込みエントリ関数。 ここから、更に trap() を呼んでいるが、この関数ではその引数である trapframe の構築を行っている。

すべてのレジスタを push して、スタック上に trapframe を構築している。なので、最後に引数として esp を積んでいる。どうやって渡すかは自由だけれど、大人しくこのままやったほうがいいと思う。

trapret()

割り込みから return する関数。

alltraps() で積んだ trapframe をすべて取り除いて、その前の割り込みベクタで積んである trapno と errcode も取り除いている。

エラーコードは何のためにあるかというと、一般保護例外が起きると、自動的に積まれるため。エラーコードがレジスタ渡しなら、そもそも trapframe に必要ない。

console.c

cprintf の可変長引数 (varargs) の実装はオリジナル実装なので、気をつけましょう。つまり、varargs でスタックを使わない場合があるアーキテクチャなどは、実装し直す必要がある。

とはいえ、stdarg.h は libc がなくても gcc であれば使えるので、それを使って実装しなおせばよいだろう。(オリジナル実装は標準ライブラリが全くない状態でも動作するように考えられてると思われる。)

spinlock.c

ロックを実装している。xchg を使ってロックを確保しているが、swap や test and set などの命令を実装しているなら、それで置き換えれば良い。

それ以外にも、pushcli, popcli の移植が必要。TRICKS も参照。

syscall.c

syscall() は、すべてのシステムコールのエントリ関数。システムコールの戻り値を trapframe の戻り値を返すレジスタに相当する部分に格納させて、呼び出し元にちゃんと戻り値が渡るようになってる。

問題は、argint() である。システムコールの引数を取得する関数であるが、xv6 はシステムコールの引数をスタックに積んでいる。つまり、普通の関数呼び出しの call が int 40h に変わっただけである。(Linux の場合は x86 でもレジスタにシステムコール引数を格納している) スタックを使って引数を読もうとしているが、mist32 は関数呼び出しもレジスタ渡しの引数であるし、同じように統一したい。なので、trapframe の引数が格納されるレジスタから取得する方法に切り替える必要がある。もし、すべてレジスタで引数の受け渡しができるなら fetchint() は不要。

mkfs.c

このファイルは、ホストで実行される mkfs という実行ファイルのソースである。mkfs は、ホスト上でファイルシステムを構築する。

気をつけなければいけないのは、エンディアンである。エンディアンが x86 のリトルエンディアンをターゲットに作られているので、ビッグエンディアンのマシンへ移植する場合には、それを考慮する必要がある。エンディアンを変換する関数として xint と xshort が用意されているので、これの順番を逆にすればよい。元のコードはよく考えると変に思えるが、ホストのエンディアンに関わらず、リトルエンディアンでメモリに出力されるような実装になっている。

elf.h

ELF_MAGIC が、リトルエンディアンで定義されている。ビッグエンディアンのマシンに移植するのであれば、ここを書き換えないと exec に失敗して init も立ち上がらない。

exec.c

exec システムコールの動作であるが、コンパイルできてしまうが x86 に依存しているところがあるので要注意。引数 (argc, argv) と fake return PC の受け渡しをスタックで行っている部分がある。ここを、スタックを使わず trapframe を使うことで、レジスタ渡しでも実現可能である。その先の、スタックコピーや ustack のアロケーションのサイズも変更しなければいけないので注意。

printf.c

ユーザーランドの printf です。cprintf と同様、stdarg.h を使っていない独自実装なので、実装し直しましょう。

software/porting_xv6.txt · 最終更新: 2013/12/18 15:58 by hktechno