xv6-armv7 mmu (2)
久しぶりに、 Big Skyさんのgolang記事を見ていたら、 みんなのGo言語(現場で使える実践テクニック)なんて本が出版された事を知った。
みんパイは、もうIoTのBasicみたいな地位になっちゃんたんで、へそを曲げて みんGoですか。新しいGoもいいかも知れないな。
こちらの方は、人気の波には逆らえず、今更ながらPythonを始めてみたらしいです。 Kivyなんていう全世界共通のGUIも出来るようですから。大勢に埋もれようって 算段らしい。
でも、オイラーはgoさ。 FreeBSDで確認したら、一応最新版の1.7.1を作れるみたい。それには、ちと古い 1.4.3だかのパッケージが必要って事で、2段ロケット方式で、1.7.1を打ち上げる とな。
cmd/api cmd/internal/obj/arm cmd/internal/obj/arm64 cmd/internal/obj/mips cmd/internal/obj/ppc64 cmd/internal/obj/s390x cmd/internal/obj/x86 cmd/asm/internal/arch cmd/asm/internal/flags cmd/asm/internal/lex cmd/asm/internal/asm cmd/internal/bio
今、こんな所をコンパイル中。arm系頑張っているな。
$ go version go version go1.7.1 freebsd/386 $ go env GOARCH="386" GOBIN="" GOEXE="" GOHOSTARCH="386" GOHOSTOS="freebsd" GOOS="freebsd" GOPATH="" GORACE="" GOROOT="/usr/local/go" GOTOOLDIR="/usr/local/go/pkg/tool/freebsd_386" CC="cc" GOGCCFLAGS="-fPIC -m32 -pthread -fmessage-length=0 -gno-record-gcc-switches" CXX="clang++" CGO_ENABLED="1"
無事にインストール出来た。どんな規模?
$ du -sh /usr/local/go 227M /usr/local/go
ちょっと試運転
$ cat t.go package main import "fmt" func main() { fmt.Println("Hello, 世界") } $ go run t.go Hello, 世界
コンパイルしてみる。
$ go build -o zz t.go $ ./zz Hello, 世界
ああ、動いているな。そしてすぐに GolangのGCを追う なんていう道草をしたくなる。悪い癖だ!
安心してたらBug踏んだ?
$ pwd /tmp/src $ go build -v bld.go github.com/gonum/internal/asm github.com/gonum/floats golang.org/x/image/math/fixed : golang.org/x/image/tiff/lzw golang.org/x/image/tiff github.com/gonum/plot/vg/vgimg bitbucket.org/zombiezen/gopdf/pdf # bitbucket.org/zombiezen/gopdf/pdf run compiler with -v for register allocation sites bitbucket.org/zombiezen/gopdf/pdf/image.go:89: internal compiler error: out of fixed registers goroutine 1 [running]: runtime/debug.Stack(0x0, 0x0, 0x0) /usr/local/go/src/runtime/debug/stack.go:24 +0x7a cmd/compile/internal/gc.Fatalf(0x8569440, 0x16, 0x0, 0x0, 0x0) /usr/local/go/src/cmd/compile/internal/gc/subr.go:165 +0x210 cmd/compile/internal/gc.Regalloc(0x39253320, 0x38908b00, 0x39253260) /usr/local/go/src/cmd/compile/internal/gc/gsubr.go:749 +0x39b
見事にバックとレースが出てきましたなあ。これって、ガベコレの祟り??
ocaml
前回、ocamlのplotを試した。その時、Xが上がっていないと、エラーを食らった。 それは当然なんだけど、コード中にWindows側を参照する設定を入れたら動いた。 何も指定しなくても、環境変数を見てくれるはずなんだけど。。。
分からないので、ソースから紐解いてみる。環境は、FreeBSD10.2です。特に ここに入っているocamlは、何もしなくても、X11がサポートされる環境に なってたので、易しいかなと思ったから。リナとかだと、X11のサポートが 有る/無しバージョンを選んでインストールするようですよ。
tar玉は効率よくxzでまとめられていたので、xz -dc で、伸張してから tarを展開しましたよ。最近は、色々な圧縮形式が有って、よう分からん。
ocaml-4.02.3/otherlibs/graph/graphics.mlを見ると
external raw_open_graph: string -> unit = "caml_gr_open_graph" let unix_open_graph arg = Sys.set_signal (sigio_signal()) (Sys.Signal_handle sigio_handler); raw_open_graph arg let (open_graph, close_graph) = match Sys.os_type with | "Unix" | "Cygwin" -> (unix_open_graph, unix_close_graph) | "Win32" -> (raw_open_graph, raw_close_graph) | "MacOS" -> (raw_open_graph, raw_close_graph) | _ -> invalid_arg ("Graphics: unknown OS type: " ^ Sys.os_type)
こんな定義がしてあった。Winでも動くんだな。で、辿って行くと、 open.cには、
/* Open the display */ if (caml_gr_display == NULL) { caml_gr_display = XOpenDisplay(display_name); if (caml_gr_display == NULL) caml_gr_fail("Cannot open display %s", XDisplayName(display_name)); caml_gr_screen = DefaultScreen(caml_gr_display); caml_gr_black = BlackPixel(caml_gr_display, caml_gr_screen); caml_gr_white = WhitePixel(caml_gr_display, caml_gr_screen); caml_gr_background = caml_gr_white; caml_gr_colormap = DefaultColormap(caml_gr_display, caml_gr_screen); }
で、XDisplayNameの引数関係は、X11系になるんだな。manを引くと
The XDisplayName function returns the name of the display that XOpenDisplay would attempt to use. If a NULL string is specified, XDisplayName looks in the environment for the display and returns the display name that XOpenDisplay would attempt to use. This makes it easier to report to the user precisely which display the program attempted to open when the initial connection attempt failed.
環境変数を使ってくれるようだ。現在の環境は
$ echo $DISPLAY localhost:10.0
127.0.0.1って所ですかね。自前のマシンのXサーバーを期待してた。けど、当然の 事ながら、そんなもんは上げていない。これが元で、エラーしてたんだな。
一応、XDisplayNameの動きを確認しておくか。
#include <stdio.h> main(){ printf("%s\n", XDisplayName( "" )); }
コンパイルすると、
t.c:(.text+0x19): undefined reference to `XDisplayName' cc: error: linker command failed with exit code 1 (use -v to see invocation)
こんなエラーになる。何をリンクしたら良いの? OpenBSDとFreeBSDのmanには、 リンクするライブラリィー情報が出てこないのよね。不親切。NetBSDは出てくる 親切OSなんだ。DISKが空いたら入れておこう。
で、当面の逃げ。先輩はどうやってたか探れば、おけ。
/usr/local/lib/ocamlへ行って、X関係をリンクしてそうなのを家捜しする。
$ for f in graphics.* > do > file $f > done graphics.a: current ar archive graphics.cma: OCaml library file (.cma) (Version 011) graphics.cmi: OCaml interface file (.cmi) (Version 017) graphics.cmx: OCaml native object file (.cmx) (Version 014) graphics.cmxa: OCaml native library file (.cmxa) (Version 013) graphics.cmxs: ELF 32-bit LSB shared object, Intel 80386, version 1 (FreeBSD), dynamically linked, not stripped graphics.mli: Mathematica 3.0 notebook
lddに引っ掛かりそうなのを、取調べ。
$ ldd ./graphics.cmxs ./graphics.cmxs: libX11.so.6 => /usr/local/lib/libX11.so.6 (0x28206000) libc.so.7 => /lib/libc.so.7 (0x2806f000) libxcb.so.1 => /usr/local/lib/libxcb.so.1 (0x28320000) librpcsvc.so.5 => /usr/lib/librpcsvc.so.5 (0x2833b000) libXau.so.6 => /usr/local/lib/libXau.so.6 (0x28344000) libpthread-stubs.so.0 => /usr/local/lib/libpthread-stubs.so.0 (0x28347000) libXdmcp.so.6 => /usr/local/lib/libXdmcp.so.6 (0x28349000)
これを見ると、 -lX11 すれば良さそう。
$ cc t.c -L/usr/local/lib -lX11
libX11が入っている場所を -L で指定して、やっとリンク出来た。 走らせてみる。
$ ./a.out localhost:10.0 $ export DISPLAY=123.234.210.234:0 $ ./a.out 123.234.210.234:0
当たり前だけど、ちゃんと動くね。
$ ocaml all.ml Exception: Graphics.Graphic_failure "Cannot open display unix:0".
間違った所が指定されてたら、それを素直に言って欲しいんですけど。。。 何か思う所が有るのかな? さっぱりわからん。
xv6-armv7 を読む
世の中には、xv6-rpiが有るようで、それを読んでる方がおられた。 xv6-rpi 掘削
オイラーは、xv6-armv7を読んでみる。(実行してみる)
Breakpoint 1, start () at start.c:169 169 _puts("starting xv6 for ARM...\n"); (gdb) n 173 set_bootpgtbl(PHY_START, PHY_START, INIT_KERN_SZ, 0); (gdb) s set_bootpgtbl (virt=2147483648, phy=2147483648, len=1048576, dev_mem=0) at star\ t.c:65 65 virt >>= PDE_SHIFT; (gdb) p/x virt $1 = 0x80000000 (gdb) p/x phy $2 = 0x80000000 (gdb) p/x len $3 = 0x100000
最初のページテーブル設定。どうやら、論理アドレスと物理アドレスを同一に 設定したいみたいだ。
65行目のわけわかめな式。暫く考えて、x += 3 みたいなのの、右シフト版の 書き方だと思い至る。シフト幅は
(gdb) info macro PDE_SHIFT Defined at /home/sakae/src/xv6-armv7/src/start.c:50 #define PDE_SHIFT 20
20ビットって事。すなわちMegaにしちゃうんだな。シフトした結果は
(gdb) p/x virt $4 = 0x800
16進数表現だと、5桁(= 20 / 4 )分、削られるとな。まるで、小学生なみの 理解だな。
続いて、forループに突入するんだけど、このループは1回実行されるだけ。 最終的には、
(gdb) n 84 kernel_pgtbl[virt] = pde; (gdb) p/x pde $7 = 0x8000040e (gdb) p/x virt $8 = 0x800 (gdb) p kernel_pgtbl $9 = (uint32 *) 0x80014000 <_kernel_pgtbl>
こんな書き込みがなされました。
次のページテーブルセットは、こんな要求
(gdb) p/x virt $11 = 0xc0000000 (gdb) p/x phy $12 = 0x80000000 (gdb) p/x len $13 = 0x100000
先ほどはすっ飛ばしてしまったけど、
(gdb) 74 pde |= (AP_KO << 10) | PE_CACHE | PE_BUF | KPDE_TYPE; (gdb) 81 if (virt < NUM_UPDE) {
81行目のNUM_UPDEで、ユーザーテーブルに書くかカーネル用テーブルに書くか 選択してる。その境界は一体幾つになってるの?
(gdb) info macro NUM_UPDE Defined at /home/sakae/src/xv6-armv7/src/mmu.h:56 included at /home/sakae/src/xv6-armv7/src/start.c:5 #define NUM_UPDE (1 << (UADDR_BITS - PDE_SHIFT)) (gdb) macro expand NUM_UPDE expands to: (1 << (28 - 20)) (gdb) p/x (1 << (28 - 20)) $15 = 0x100
指定された論理アドレスが小さければ、ユーザー用ページに書かれるとな。 具体的には、0x10000000 以下のアドレスですな。この値を表示させると、 256Mまでは、ユーザーエリアだよって事になりました。
テーブルに埋め込むフラグは、後回しにして、先に流れだけを追ってみる。
start () at start.c:177 177 vectbl = P2V_WO ((VEC_TBL & PDE_MASK) + PHY_START); (gdb) 179 if (vectbl <= (uint)&end) { (gdb) p/x vectbl $17 = 0xc00f0000
次から、set_bootpgtblが4つ実行されてる。初回
(gdb) p/x virt $18 = 0xffff0000 (gdb) p/x phy $19 = 0x80000000 (gdb) p/x len $20 = 0x100000
2回目
(gdb) p/x virt $21 = 0x1c000000 (gdb) p/x phy $22 = 0x1c000000 (gdb) p/x len $23 = 0x1000000
こんな調子で、実行されて、変換テーブルが用意される。 細かい事は、マクロの定義を見る方が速い。どこにあるか調べると、ハードと言うか ボードべったりと言う事で、device/versatile_pb.hの中。最初、表層だけを 対象にgrepしててHitせず、マクロのinfoを見て、定義場所を知ったと言うお粗末。
// the VerstatilePB board can support up to 256MB memory. // but we assume it has 128MB instead. During boot, the lower // 64MB memory is mapped to the flash, needs to be remapped // the the SDRAM. We skip this for QEMU #define PHYSTOP (0x08000000 + PHY_START) #define BSP_MEMREMAP 0x04000000 #define DEVBASE1 0x1c000000 #define DEVBASE2 0x2c000000 #define DEV_MEM_SZ 0x01000000 #define VEC_TBL 0xFFFF0000 :
そして、最後は load_pgtlbで、それを有効にする。
(gdb) n 135 val = (uint)kernel_pgtbl | 0x00; (gdb) n 136 asm("MCR p15, 0, %[v], c2, c0, 1": :[v]"r" (val):); (gdb) p val $39 = 2147565568 (gdb) p/x val $40 = 0x80014000
カーネル用テーブルをコプロに教えこんでる。
そして、ユーザー用のテーブル 0x80018000 を教えこんで、mmuをイネーブル。 アドレス変換用のキャッシュをクリア。
以後、MMUが働いているんで、スタックを再設定、bssをクリアして、 本式のカーネルである、kmainに突入してく。
MMU フラグ
上で出てきた、テーブルの設定は、こんな風になってた。
(gdb) 74 pde |= (AP_KO << 10) | PE_CACHE | PE_BUF | KPDE_TYPE;
これらが定義されてるのは、mmu.hの中。簡単な説明が載ってる。
例えば、
// access permission for page directory/page table entries. #define AP_NA 0x00 // no access #define AP_KO 0x01 // privilaged access, kernel: RW, user: no access #define AP_KUR 0x02 // no write access from user, read allowed #define AP_KU 0x03 // full access // domain definition for page table entries #define PE_CACHE (1 << 3)// cachable #define PE_BUF (1 << 2)// bufferable #define PE_TYPES 0x03 // mask for page type #define KPDE_TYPE 0x02 // use "section" type for kernel page directory #define UPDE_TYPE 0x01 // use "coarse page table" for user page directory #define PTE_TYPE 0x02 // executable user page(subpage disable)
これ以上細かい事は、ARMのマニュアルを見てくれって事だな。
論理/物理アドレスの変換テーブルは2つ設定出来るようだけど、どうやって切り替え るの? 素直に考えると、石がシステムモードかユーザーモードかで、自動選択って手 を思い付くけど。
ARM LinuxはTTBR1を使っているのかが、その答えのようなんで、重点的にマニュアルを見ておけ(ok)。 ヒントは、TTBCRの下位3ビットで表れる、Nの設定ね。
あと、ページテーブルの設定値の意味がようやく分かった。1エントリーで、1メガ分 を担当。だから、4096エントリー有れば、32Bitの論理アドレス全部を指定出来る。 じゃ、64Bitの石の場合はどうするの?
1エントリーで4Gを担当させるって言う大盤振る舞いを仮定しても、更にその上に 32Bit有るんで、そんなののエントリーを定義するって、絶望的に思える。 なんか、ハード的な仕掛けで、この範囲とかってような事をするのだろうか? 疑問は尽きないな。
ARMの正式なマニュアルは英文だけど、軟弱者は日本語おkって事で、 ARM1136JF-S and ARM1136J-S Technical Reference Manual - DDI0211DJ.pdfあたりを ブラウジングしています。
ページテーブルベースレジスタは以下のように選択されます。 1. N = 0 の場合には、常に変換テーブルベースレジスタ 0 が使用されます。 これがリセット時のデフォルトです。この機能には、ARMv5 以前のプロセッサと 下位互換性があります。 2. N > 0 の場合、仮想アドレスのビット [31:32-N] がすべて 0 であれば、変換 テーブルベースレジスタ 0 が使用されます。それ以外の場合は変換テーブルベー スレジスタ 1 が使用されます。 N には 0 ~ 7 の範囲内の値が指定される必要があります。
マニュアルから勝手に引いちゃったけど、こういう事なのね。論理アドレスがちいさ かったら、TTBR0が使われるとな。31:32-Nって、それを思い起こさせるような設定が 出てきていたな。
今回見たのは、カーネルがちゃんと動く為の、仮mmuの設定だそうだ。ちゃんとした 設定は、テーブルを2段構成にして、かゆい所に手が届くようにするとか。
64Bitのマシンだと、メモリーが広大な台地となるので、テーブルが2段ぐらいでは 追いつかず、4段構成になるそうな。
上でのトレースでも見たけど、4096エントリーを持つテーブル(配列)で、実際に 有効な値が入っているのは、2-30ぐらいですから。超疎な使い方になってます。