xv6-armv7 mmu (3)

前回やった、最新式のgoで落ちるやつ、fedoraでも確認してみた。 go 1.5.4 と、ちょっと古いやつ。

実験対象は、昔作った血圧アプリ。グラフ書きのパッケージ取り寄せ先が 変更になってたので、下記のように変更。

// blood check go version
package main

import (
        "github.com/gonum/plot"
        "github.com/gonum/plot/plotter"
        "github.com/gonum/plot/plotutil"
        "encoding/csv"
        "flag"
          :

お取り寄せすると、途中でパッケージのコンパイルエラーですよ。

[sakae@fedora src]$ go get github.com/gonum/plot
# 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

該当の89行目は、配列への書き込みをしてる所。

func encodeRGBAStream(w io.Writer, img *image.RGBA) error {
        buf := make([]byte, 3*img.Rect.Dx()*img.Rect.Dy())
        var a uint16
        for i, j := 0, 0; i < len(img.Pix); i, j = i+4, j+3 {
                a = uint16(img.Pix[i+3])
                if a != 0 {
                        buf[j+0] = byte(uint16(img.Pix[i+0]) * 0xff / a)
                        buf[j+1] = byte(uint16(img.Pix[i+1]) * 0xff / a)
                        buf[j+2] = byte(uint16(img.Pix[i+2]) * 0xff / a)
                }
        }
        _, err := w.Write(buf)
        return err
}

難の変哲も無い所、レジスターの使い回しに苦労しそうには見えない。世のお悩み 相談室に問うてみる。

internal compiler error: out of fixed registers - Google 検索

出るわ、出るわ、世界で、142万件も悩みが登録されてたぞ。

落ちるのは、pdfを作っている部分なので、pdfなんていらないわいっと、該当部分を コメントアウト。

@ github.com/gonum/plot/vg/canvas.go

 16//      "github.com/gonum/plot/vg/vgpdf"
  :
281//      case "pdf":
282//              c = vgpdf.New(w, h)

そうしておいて、

$ go build -v bld.go
github.com/gonum/internal/asm
github.com/gonum/floats
  :
github.com/gonum/plot/palette
github.com/gonum/plot/plotter
github.com/gonum/plot/plotutil
command-line-arguments
# command-line-arguments
./bld.go:319: cannot use width (type float64) as type vg.Length in argument to p.Save
./bld.go:319: cannot use height (type float64) as type vg.Length in argument to p.Save

まだエラーかよ。該当部分を参照。

   :
        width := 6.0
        height := 4.0
        p.Save(width, height, "zzz.png")

Saveの所で、こけている。2年ぐらいすると仕様が変わってしまうものかなあ。 plot - GoDoc

同胞のサンプル Golearnとplotで散布図を書くでは、難しい事をやってたけど、普通に整数(ピクセル単位)で指定したら、いけたよ。 やれやれ。

Example plots ・ gonum/plot Wikiこちらがgoのプロット例。以前より進化してるな。

統計学の基礎の基礎

統計学・機械学習でよく使われる数学記号リスト

グラフと言えば、統計。犬も歩けば棒に当った!

逆引きGolang

A curated list of awesome Go frameworks, libraries and software

kmain

前回に続いて、xv6のコードを追って行く。今回は、mmuがonになって、カーネルが 本格的に活動を始めた所。

      // interrrupt vector table is in the middle of first 1MB. We use the left
      // over for page tables
      vectbl = P2V_WO ((VEC_TBL & PDE_MASK) + PHY_START);

割り込みヴェクターは中位の1M中に有るよ。ページテーブルの使い残しを 割り当てるよでいいのかな。

(gdb) macro expand (VEC_TBL & PDE_MASK) + PHY_START
expands to: (0xFFFF0000 & ((1 << 20) - 1)) + 0x80000000
(gdb) p/x (0xFFFF0000 & ((1 << 20) - 1)) + 0x80000000
$1 = 0x800f0000

マクロ展開、なかなか便利。

// add some memory used for page tables (initialization code)
void kpt_freerange (uint32 low, uint32 hi)
{
=>  while (low < hi) {
        _kpt_free ((char*)low);
        low += PT_SZ;
    }
}
(gdb) p/x low
$2 = 0xc00b1000
(gdb) p/x hi
$3 = 0xc00f0000
(gdb) info macro PT_SZ
Defined at /home/sakae/src/xv6-armv7/src/mmu.h:59
  included at /home/sakae/src/xv6-armv7/src/vm.c:6
#define PT_SZ (NUM_PTE << 2)
(gdb) p/x (NUM_PTE << 2)
$4 = 0x400

これ、1kづつ、リンクリストに繋いでる。

(gdb) s
paging_init (phy_low=2148532224, phy_hi=2281701376) at vm.c:445
445         mappages (P2V(&_kernel_pgtbl), P2V(phy_low), phy_hi - phy_low, phy_low, AP_KU);
(gdb) p/x phy_low
$6 = 0x80100000
(gdb) p/x phy_hi
$7 = 0x88000000

最初に大雑把に割り当てたのを、4K毎に再度割り当てし直し、2段階変換 方式にするんだな。

// 1:1 map the memory [phy_low, phy_hi] in kernel. We need to
// use 2-level mapping for this block of memory. The rumor has
// it that ARMv6's small brain cannot handle the case that memory
// be mapped in both 1-level page table and 2-level page. For
// initial kernel, we use 1MB mapping, other memory needs to be
// mapped as 4KB pages
void paging_init (uint phy_low, uint phy_hi)
(gdb) s
mappages (pgdir=0xc0014000, va=0xc0100000, size=133169152, pa=2148532224, ap=3) at vm.c:125
125         a = (char*) align_dn(va, PTE_SZ);
(gdb) p/x size
$8 = 0x7f00000
(gdb) p/x pa
$9 = 0x80100000

こういう事をやるとな。

// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned.
static int mappages (pde_t *pgdir, void *va, uint size, uint pa, int ap)
// Return the address of the PTE in page directory that corresponds to
// virtual address va.  If alloc!=0, create any required page table pages.
static pte_t* walkpgdir (pde_t *pgdir, const void *va, int alloc)

PDEが1次テーブルで、PTEは2次テーブルって呼んでいるんだな。 実際にPTEの下位ビットで、アクセス権とかが設定されてた。

kmem_init2

上記操作が終了すると、kmem_init2へと制御が移ってくる。 ソースは、buddy.cに置いてある。

(gdb) s
kmem_init2 (vstart=0xc0100000, vend=0xc8000000) at buddy.c:82
82          kmem.start = (uint)vstart;

これは、ヒープ領域の管理をしてるんだな。多分、きっとそうだろう。 読んでみたらそれっぽい。空きメモリーを色々なサイズ(2のべき乗単位)で 取り出せるような工夫がされてる。そして検索を素早く行うために、ビットマップも 使ってる。

初期値では、4Kブロックをどんどんと切り出して、集めているようだ。 余り、深入りしてもあれなんで、これぐらいにしておく。

user page table

カーネルをスタートする時、カーネルが使うページテーブルが初期化されてるけど、 ユーザー用のやつは、設定してる素振りが無い。

何処でやってるの? コードをブラウジング。vm.cって、バーチャルメモリーだろう って、根拠の無い希望を抱いて見てくと、TTBR0なんてコメントが出てきた。 そう、switchuvmって関数ね。

ここにBPを置いて、走らせてみると

(gdb) bt
#0  switchuvm (p=0xc00ae640 <ptable+52>) at vm.c:166
#1  0xc0025b94 in scheduler () at proc.c:341
#2  0xc0024b10 in kmain () at main.c:54
#3  0x80010520 in start () at start.c:195

此処に飛び込んできた時点で、pgdirが確定してる。

(gdb) p *p
$1 = {
  sz = 4096,
  pgdir = 0xc00dfc00,
  kstack = 0xc7fde000 "",
  state = RUNNABLE,
  pid = 1,
  parent = 0x0,
  tf = 0xc7fdefb8,
  context = 0xc7fdef88,
  chan = 0x0,
  killed = 0,
  ofile = {0x0 <repeats 16 times>},
  cwd = 0xc00ad508 <icache+52>,
  name = "initcode\000\000\000\000\000\000\000"
}

と言うことは、もっと前の段階で、ユーザー用のpgtblを設定してると思われな。 まあいい、ここへ来たんだから、先を見ておくと、

  // Switch to the user page table (TTBR0)
  void switchuvm (struct proc *p)
  {
      uint val;

B     pushcli();

      if (p->pgdir == 0) {
          panic("switchuvm: no pgdir");
      }

  =>  val = (uint) V2P(p->pgdir) | 0x00;

      asm("MCR p15, 0, %[v], c2, c0, 0": :[v]"r" (val):);
      flush_tlb();

      popcli();
  }

割り込みを禁止にして、論理アドレスから物理アドレスを求め、それをTTBR0に セットして、TLBをクリアって動作ね。

(gdb) p/x val
$2 = 0x800dfc00
(gdb) x/4 $2
0x800dfc00:     0x800df801      0x00000000      0x00000000      0x00000000

上記は、ユーザープロセスを走らせる為の卵initだけど、gdbをcontすると、 今度は本式のinitプロセスが動く

(gdb) p *p
$3 = {
  sz = 16384,
  pgdir = 0xc00df400,
  kstack = 0xc7fde000 "",
  state = RUNNING,
  pid = 1,
  parent = 0x0,
  tf = 0xc7fdefb8,
  context = 0xc7fdef88,
  chan = 0x0,
  killed = 0,
  ofile = {0x0 <repeats 16 times>},
  cwd = 0xc00ad508 <icache+52>,
  name = "init\000\000de\000\000\000\000\000\000\000"
}
(gdb) p/x val
$4 = 0x800df400
(gdb) x/4 $4
0x800df400:     0x800df001      0x00000000      0x00000000      0x00000000

そして、何回かcontすると、待望のshが生まれる

(gdb) p *p
$7 = {
  sz = 20480,
  pgdir = 0xc00dec00,
  kstack = 0xc7fdf000 "$\020\237\345$ \237\345\a",
  state = RUNNING,
  pid = 2,
  parent = 0xc00ae640 <ptable+52>,
  tf = 0xc7fdffb8,
  context = 0xc7fdff88,
  chan = 0x0,
  killed = 0,
  ofile = {0xc00acb24 <ftable+52>, 0xc00acb24 <ftable+52>, 0xc00acb24 <ftable+52>, 0x0 <repeats 13 times>},
  cwd = 0xc00ad508 <icache+52>,
  name = "sh", '\000' <repeats 13 times>
(gdb) p/x val
$8 = 0x800dec00
(gdb) x/4 $8
0x800dec00:     0x800de801      0x00000000      0x00000000      0x00000000

これで、shが動き出す。lsなんてコマンドを叩くと、sh用のtblが参照され、 やがて

(gdb) p *p
$18 = {
  sz = 16384,
  pgdir = 0xc00de400,
  kstack = 0xc7fe4000 "\b\260-\345\004\340\215\345\004\260\215\342\b\320M\342\002\020\240\343d\001\001\343",
  state = RUNNING,
  pid = 3,
  parent = 0xc00ae6bc <ptable+176>,
  tf = 0xc7fe4fb8,
  context = 0xc7fe4f88,
  chan = 0x0,
  killed = 0,
  ofile = {0xc00acb24 <ftable+52>, 0xc00acb24 <ftable+52>, 0xc00acb24 <ftable+5\
2>, 0x0 <repeats 13 times>},
  cwd = 0xc00ad508 <icache+52>,
  name = "ls", '\000' <repeats 13 times>
(gdb) p/x val
$19 = 0x800de400
(gdb) x/4 $19
0x800de400:     0x800de001      0x00000000      0x00000000      0x00000000

この時、呼び出しの歴史を辿ると

(gdb) bt
#0  switchuvm (p=0xc00ae738 <ptable+300>) at vm.c:174
#1  0xc002260c in exec (path=0x2230 "ls", argv=0xc7fe4f08) at exec.c:131
#2  0xc002753c in sys_exec () at sysfile.c:473
#3  0xc0026528 in syscall () at syscall.c:152
#4  0xc0027af4 in swi_handler (r=0xc7fe4fb8) at trap.c:12
#5  0xc0027974 in trap_swi ()
#6  0x00000354 in ?? ()

このexecの中で、ユーザーメモリーの確保と、テーブルのセットアップが 行われているんだな。

execの中でのpgtblの用意

最初

42          pgdir = 0;
(gdb)
44          if ((pgdir = kpt_alloc()) == 0) {
      // Allocate a PT page if no inital pages is available
  =>  if ((r == NULL) && ((r = kmalloc (PT_ORDER)) == NULL)) {
          panic("oom: kpt_alloc");
      }

      memset(r, 0, PT_SZ);
      return (char*) r;

kpt_alloc()は、リンクリストの頭をrに取り出してきて、メモリーを1k確保。 それを0に初期化してる。PT=ORDERは10、PT_SZは、0x400。すなわち、PTE用の 配列サイズは1024って事だな。1ページが4Kって事を思い出せば、4Mまでの メモリーをアクセス出来るとな。

(gdb) p/x r
$26 = 0xc00de400
(gdb) s
allocuvm (pgdir=0xc00de400, oldsz=0, newsz=5280) at vm.c:234
  // Allocate page tables and physical memory to grow process from oldsz to
  // newsz, which need not be page aligned.  Returns new size or 0 on error.
  int allocuvm (pde_t *pgdir, uint oldsz, uint newsz)

こうして確保したエリアに、

loaduvm (pgdir=0xc00de400, addr=0x0, ip=0xc00ad5a8 <icache+212>, offset=84, sz=5252) at vm.c:202

指定されたpgdir(PDE)で、PTEの先頭を探し出し(これが論理アドレスで表現される 空きメモリーになる)そこに、ファイルから呼び出したプログラムを格納する。

プログラムは、addrで示されるユーザーメモリの0番地から格納。プログラムの 実際の在り処はipが示していて、実際には、そこからoffset分ずれた所から、sz分 転送してねって、依頼だな。

  // Load a program segment into pgdir.  addr must be page-aligned
  // and the pages from addr to addr+sz must already be mapped.
  int loaduvm (pde_t *pgdir, char *addr, struct inode *ip, uint offset, uint sz  )

実際のデータをロードするとな。 そして、次のページをユーザースタック用に確保。サイズは決め打ちで、8k。

そして、execの中で、このユーザーイメージを確定、switchuvmへ飛んで、 今ロードしたユーザーエリアをMMU的に有効にするとな。長い道のりであったなあ。

etc

0から作るOS開発 ページングその1 ページとPTEとPDE

ページング機構2

起動するまでの長い道のり メモリ管理編(3) ページングOnの巻

Bash on Ubuntu on Windows - Release Notes