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ぐらいですから。超疎な使い方になってます。