(xv6)kernelmemfsとmemide.o

memide.o

Fake IDE disk; stores blocks in memory. Useful for running kernel without scratch disk. とあるとおり、Makefileでkernelをビルドする際にこいつを差し替えてる感じ。GAIAのをみるとわかるように実機で動かす場合はちょっと面倒な処理が必要かも。

Makefile

xv6-mips

MEMFSOBJS = $(filter-out ide.o,$(OBJS)) memide.o
kernelmemfs: $(MEMFSOBJS) entry.o initcode kernel.ld fs.img
    $(LD) $(LDFLAGS) -T kernel.ld -o kernelmemfs entry.o  $(MEMFSOBJS) -b binary initcode fs.img
    $(OBJDUMP) -S kernelmemfs > kernelmemfs.asm
    $(OBJDUMP) -t kernelmemfs | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > kernelmemfs.sym

xv6-gaia

# We use memfs as default because our CPU architecture has no disk.
MEMFSASMS = $(filter-out _ide.s,$(ASMS)) _memide.s
kernelmemfs: $(MEMFSASMS) initcode _min-rt fs.img
    ./tools/gen_binary_blobs 0x80002000 initcode _min-rt fs.img
    $(AS) $(ASFLAGS) -c -o _kernelmemfs -e 0x80002000 -start _start $(MEMFSASMS) _binary_blobs.s $(UCCLIBS) -f __UCC_HEAP_START
    ./tools/gen_binary_blobs `ruby -e "print open('_kernelmemfs').size + 0x80002000"` initcode _min-rt fs.img
    $(AS) $(ASFLAGS) -c -o _kernelmemfs -e 0x80002000 -start _start $(MEMFSASMS) _binary_blobs.s $(UCCLIBS) -f __UCC_HEAP_START
    cat _kernelmemfs initcode _min-rt fs.img > kernelmemfs
    rm _kernelmemfs
    ./tools/attach_boot_header kernelmemfs

MIPS HardwareにおけるKSEG0, 1, 2のアドレス変換とTLB

KSEG0, 1におけるマッピングはハードとkernelがどう連携しているのか調査した。結論としては、ハード側でTLBにVirtualAddressを渡す前にチェック

例1)
  • pc(virtual address)の上位2 or 3bitが
    • 100 : KSEG0 (2.0G - 2.5G)
    • 101 : KSEG1 (2.5G - 3.0G)
    • 11 : KSEG2 (3.0G - 4.0G)

その結果をみてTLBを使うかどうか判断して、PFN (Physical Frame Number) or PPN(Physical Page Number)を算出

wire tlb_use_at_idle = fetch_state == FETCH_IDLE && (if_pc[31] == 1'b0 || if_pc[31:30] == 2'b11);

wire [19:0] pfn_at_idle = 
    (~(tlb_use_at_idle))?   { 3'b0, if_pc[28:12] } :
    (micro_check_matched)?  micro_check_result[39:20] :
                            tlb_ram_fetch_result[39:20];

aoR3000/pipeline_if.v at b459a2326825652e8d2f8e542a7dddf94e4c25ea · alfikpl/aoR3000 · GitHub

例2)

直接比較

    // If instuction memory is unmapped, just pass
    // it through after removing the offsets
    if( (inst_virtual_address >= `KSEG0_START) &&
      (inst_virtual_address <= `KSEG0_END)) begin
      inst_physical_address = (inst_virtual_address - `KSEG0_START);
    end
    else if( (inst_virtual_address >= `KSEG1_START) &&
         (inst_virtual_address <= `KSEG1_END)) begin
      inst_physical_address = (inst_virtual_address - `KSEG1_START);
    end
    else begin

https://alexander.soto.io/mips-processor

GAIAとxv6におけるbootloader

SRAM

GAIAで使ってる基盤は4MB(222)のSRAMを積んでる。 rom.vhd は実際には sram.vhd をラップしてるに過ぎず、いわゆるDRAMは一切使用していない。(BlockRamというのもSRAMの一種らしい?)

sram.vhdの中では、入ってきたアドレスの上位10bitは見ておらず、下位22bitだけを見ている。(これが単純な仮想アドレス変換として機能している?ページテーブルやTLBの実装等は見当たらないため)

xv6

nyuichi/xv6

Makefile

kernelmemfs: $(MEMFSASMS) initcode _min-rt fs.img
    ./tools/gen_binary_blobs 0x80002000 initcode _min-rt fs.img
    $(AS) $(ASFLAGS) -c -o _kernelmemfs -e 0x80002000 -start _start $(MEMFSASMS) _binary_blobs.s $(UCCLIBS) -f __UCC_HEAP_START
    ./tools/gen_binary_blobs `ruby -e "print open('_kernelmemfs').size + 0x80002000"` initcode _min-rt fs.img
    $(AS) $(ASFLAGS) -c -o _kernelmemfs -e 0x80002000 -start _start $(MEMFSASMS) _binary_blobs.s $(UCCLIBS) -f __UCC_HEAP_START
    cat _kernelmemfs initcode _min-rt fs.img > kernelmemfs
    rm _kernelmemfs
    ./tools/attach_boot_header kernelmemfs
  • 公式のようなリンカスクリプトは使っていない。よくみるとハードコーディングで 0x80002000 を使っており、これがkernelが配置されている先頭仮想アドレスである。 0x8010_0000 を使ってないのは、無駄に0 - 1MB空間を空けないようにするため?
  • そして、GAIAではプロセスごとのアドレス空間切り替えをどうやって実装しているのか。MIPSでいうASID的なものが見当たらないが...

memlayout.h

// Memory layout

#define EXTMEM   0x2000             // Start of extended memory
#define PHYSTOP  (4*1024*1024)      // Top physical memory
#define DEVSPACE 0xFE000000         // Other devices are at high addresses

// Key addresses for address space layout (see kmap in vm.c for layout)
#define KERNBASE 0x80000000         // First kernel virtual address
#define KERNLINK (KERNBASE+EXTMEM)  // Address where kernel is linked

// [0x80000000, 0x80001FFF] is special memory mapped area.
// Kernel MUST guarantee that this memory area is directly mapped to the physical same area.
#define VAENABLE   0x80001200
#define PDADDR     0x80001204
#define INTHANDLER 0x80001100
#define INTENABLE  0x80001104
#define EPC        0x80001108
#define CAUSE      0x8000110C
#define SERIAL     0x80001000
#define SERIALWE   0x80001004

ここで、だいたい分かる。このへんのアドレス設定はGAIAのアーキテクチャと密結合な感じで、GAIAのソースコード内にもがっつりハードコーディングで書かれている。

* EXTMEM  : 物理アドレスのどこにkernel先頭が配置されてるのか示す。
* PHYSTOP : 詰んでるSRAMの容量に合わせる(実質的な物理メモリ容量)
* その他  : I/Oで使うメモリマップドのアドレス。例えば、シリアル通信は0x80001000, 0x80001004。uart.cで使用している

mips移植を考える場合

xv6-mips では上記のような考慮はされていないため、適宜パラメタを書き換える必要がある。特にEXTMEMはkernellをSRAMに載せるにしろDRAMに載せるにしろ書き換えないとまともに動かない。オリジナルのxv6, xv6-mipsでは memlayout.hはシンプルで、定数は各クラスファイル内に書かれているケースが多い。(uart.c, mips.hなど)

// Memory layout

#define EXTMEM  0x100000            // Start of extended memory
#define PHYSTOP 0xE000000           // Top physical memory
#define DEVSPACE 0xFE000000         // Other devices are at high addresses

// Key addresses for address space layout (see kmap in vm.c for layout)
#define KERNBASE 0x80000000         // First kernel virtual address
#define KERNLINK (KERNBASE+EXTMEM)  // Address where kernel is linked

#define V2P(a) (((uint) (a)) - KERNBASE)
#define P2V(a) (((void *) (a)) + KERNBASE)

#define V2P_WO(x) ((x) - KERNBASE)    // same as V2P, but without casts
#define P2V_WO(x) ((x) + KERNBASE)    // same as P2V, but without casts

xv6のbootloader

f:id:varmil:20171106122940p:plain

xv6_translate/chapter1.md at master · msyksphinz/xv6_translate · GitHub

original UNIX v6

  1. ROMに保存されているブートストラップローダプログラムが、ルートディスクのブロック番号0にあるブートストラッププログラムをメモリのアドレス0に読み込んで実行する。
  2. ブートストラッププログラムはルートディスクのファイルシステムから /unix や /rkunixといったカーネルプログラム本体をメモリのアドレス0に読み込んで実行する。
  3. カーネルがシステムの初期化を行う

電源ON〜kernelをディスクにロード、PCを設定するところまで (1, 2)

わかりやすい

msyksphinz.hatenablog.com

kernel動作開始からinit前まで(3: 主に仮想メモリの初期化)

kernel.ld (リンカスクリプト)

ENTRY(_start)

ENTRY(start)とは、プログラムの実行をstartというシンボルの位置から開始するという意味です。startは.text : { start = . ; ...によって、機械語命令を格納する.textセクションの先頭に設定されます。

_startの位置からしばらく続くBYTE()やLONG()の列によって、所定の機械語命令を直接.textセクションに書き込んでいます。この命令をC風に書くと、だいたい次のようになります。

GNU linker scriptでhello world - Qiita

entry.S

1032 # By convention, the _start symbol specifies the ELF entry point.
1033 # Since we haven’t set up virtual memory yet, our entry point is
1034 # the physical address of ’entry’.
1035 .globl _start
// コンパイル時には仮想アドレスとして定義されるが、entryはページングが動作していない状態でスタートするので、物理アドレスに変換している?
// ここはちょっとトリッキーだ。entryの場所を仮想アドレスの場所(0x8000_0000以上)に設定するのではなく、無理矢理0x0000_0000の付近に設定している。
// これにより、最初はページングハードウェアがONになっていなくても、entryを読み込んで実行が開始される。
1036 _start = V2P_WO(entry)
1037
1038 # Entering xv6 on boot processor, with paging off.
1039 .globl entry
1040 entry:
// ページング機構をONにするプログラムだと思う。それ以上は調べていない。
1041 # Turn on page size extension for 4Mbyte pages
1042 movl %cr4, %eax
1043 orl $(CR4_PSE), %eax
1044 movl %eax, %cr4
============================================================================
// entrypgdirの詳細は以下
1311 pde_t entrypgdir[NPDENTRIES] = {
1312 // Map VA’s [0, 4MB) to PA’s [0, 4MB)
1313 [0] = (0) | PTE_P | PTE_W | PTE_PS,         // 0x0-0x400000 --> 0x00000に変換
1314 // Map VA’s [KERNBASE, KERNBASE+4MB) to PA’s [0, 4MB)
1315 [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,   // 0x8000_0000-0x8040_0000 を 0x0000000に変換
1316 };
============================================================================
1045 # Set page directory
1046 movl $(V2P_WO(entrypgdir)), %eax
// cr3に設定することにより、ページング変換はこの場所から変換を開始するようになる。
1047 movl %eax, %cr3
// ページングを有効にする。それ以上は調べていない。
1048 # Turn on paging.
1049 movl %cr0, %eax
1050 orl $(CR0_PG|CR0_WP), %eax
1051 movl %eax, %cr0
1052
// スタックを有効にする
1053 # Set up the stack pointer.
1054 movl $(stack + KSTACKSIZE), %esp
1055
1056 # Jump to main(), and switch to executing at
1057 # high addresses. The indirect call is needed because
1058 # the assembler produces a PC−relative instruction
1059 # for a direct jump.
// mainは0x8000_0000以上の場所になる。従って、ここからはページングハードウェアを利用して仮想アドレス変換が始まる。
// ここから先は、ページングハードウェアによって、変換されるが、mainは0x8010_0000に存在しているつもりが、0x0000_0000に変換される。
1060 mov $main, %eax
1061 jmp *%eax
1062
1063 .comm stack, KSTACKSIZE

MITのxv6を読もう - 第1章 entryはどのような構造? - - FPGA開発日記

main.c

// Boot page table used in entry.S and entryother.S.
// Page directories (and page tables), must start on a page boundary,
// hence the "__aligned__" attribute.  
// Use PTE_PS in page directory entry to enable 4Mbyte pages.
__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
  // Map VA's [0, 4MB) to PA's [0, 4MB)
  [0] = (0) | PTE_P | PTE_W | PTE_PS,
  // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
  [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};

entry用のページテーブルはmain.cに定義されている。 (main.cのその部分だけ以下に載せておきます。ただmain.c全体でも115行しかありません。)

Program Memoryの書き換え

そもそもどのタイミングでプログラムがロードされるのか(xv6カーネルから)

そして、Data MemoryとProgram Memoryをどうやって区別しているのか(See Mips Run)

f:id:varmil:20171105231749p:plain

See MIPS Run - Dominic Sweetman - Google ブックス

D-Cacheにまずプログラムが読み込まれて、そいつをI-Cacheに同期するみたいな感じ? synciという命令があるらしい。

下はGAIAのI-Cache実装時のlog add instruction cache · nyuichi/GAIA3@bea8cb9 · GitHub

GAIAはModified Harvard Architectureっぽいぞ

GAIA3/top.vhd at 67bbdfb4c949258f91d3f3750064ac1fc40295ac · nyuichi/GAIA3 · GitHub

このへん。RAMというのが主記憶っぽい。i-cache, d-cacheは単純にRAMのサブセットとしてのキャッシュ(FPGAのBlockRAMというのを使ってる。ちょっと調べた感じSRAMと同じくらい早い?)。SRAMFPGAのハードウェアPINを使っていて、RAMのキャッシュとして機能している。(RAMに組み込まれている感じ)

さらに、メモリ(RAM)では、「req(uest) - gr(a)nt」ステートを使用している。これはInstructionとDataを1つの物理メモリで扱うために必要なもの。例えばi-cacheがミスしてRAMに問い合わせている(BrockRAMにFETCHした結果を書き込んでいる)最中に、d-cacheが同様にページフォールトした場合、後者は前者のFETCHが終了するまで待たされなければならない。それを実現する仕組み。(でもこれって単純にwireを両者分用意しておけば、こんな面倒なことせずにいけるんでは...)

そもそも1つの物理メモリのみを使用しているのは、kernel(OS)を動かす場合、そのほうがハードウェア側がシンプルになるから?補助記憶装置(kernelとかユーザプログラムが置いてある場所)から主記憶にプログラム展開する際に、MIPSでは「Program Memoryにloadするのか、Data Memoryにloadするのか」区別するような命令がないためか。

ROM (GAIA)

これが結構重要で、実機で動かす場合のbootloaderをどこに実装しているのかと思ったら、ここにあった。逆アセンブルしないと何やってるのかわからないが、 bootloader_prog_silent ってのが一番最近に実装されてる。しかし、実際に使われてるのは bootloader_prog ってやつ。

GAIA3/data.vhd at master · nyuichi/GAIA3 · GitHub

-Makefile編- xv6 (mips) コードリーディング

QEMUで実行できる形に最適化されているので、実機(FPGA)で動作させる場合はコードの手直しが必要かもしれない。

xv6.img, fs.img

QEMUで実行する際に

qemu-nox: fs.img xv6.img
    $(QEMU) -nographic $(QEMUOPTS)

となるが、これは下記のようなコマンドとして実行される

qemu-system-mipsel -nographic -hdb fs.img -kernel kernel -smp 1 -m 256 -M mips

hdb   : block device file
kernel: Use bzImage as kernel image
smp   : the number of simulation processor
m     : RAM size (MB)
M     : Set the emulated machine type

original vs. mips

MIPS版では、bootblock(Master Boot Record)を使わない。実機で動かす場合はこの辺の手直しが必要?

original

xv6.img: bootblock kernel fs.img
    dd if=/dev/zero of=xv6.img count=10000
    dd if=bootblock of=xv6.img conv=notrunc
    dd if=kernel of=xv6.img seek=1 conv=notrunc

bootblock: bootasm.S bootmain.c
    $(CC) $(CFLAGS) -fno-pic -O -nostdinc -I. -c bootmain.c
    $(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c bootasm.S
    $(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o
    $(OBJDUMP) -S bootblock.o > bootblock.asm
    $(OBJCOPY) -S -O binary -j .text bootblock.o bootblock
    ./sign.pl bootblock

mips

xv6.img: kernel fs.img
    dd if=/dev/zero of=xv6.img count=10000
    dd if=kernel of=xv6.img seek=1 conv=notrunc

じゃあどうやってxv6-mipsは動作しているのか?

結論から書くと、xv6-mipsQEMU上での動作しかサポートしていない。QEMUはあくまでエミュレータなので、BIOSにあたるもの(MBRセクタ)がなくても問題ないらしい。実機ではおそらく何かしら

There currently is no MIPS BIOS file for QEMU (see firmware). However if passed a -kernel argument qemu will not call the firmware at all, so this does no harm at all. Since QEMU 0.8.1 this workaround is obsolete, a missing BIOS file triggers only a warning message.

Firmware

The QEMU distribution does not contain any firmware for MIPS. This is only a minor problem as unlike on a real system QEMU's virtual hardware is mostly initialized after a reset. At least some existing firmware for MIPS Malta also works with QEMU, e.g. RedBoot. There is also a mmon port to QEMU.

https://www.linux-mips.org/wiki/QEMU#MIPS_BIOS_not_found_on_startup https://www.linux-mips.org/wiki/QEMU#Firmware

そもそもファームウェアとは?

x86でいうBIOSのように、ハードウェアとソフトウェアの間にあるもの。電源投入と同時に実行される。主な働きはハードウェアを初期化して記憶装置からブートローダーを呼び出すこと。 ハード的に見たら、ProgramCounterが0に初期化された後、すぐ実行される位置に格納されているProgram、という感じなのかな?実装がよくわからない。

Firmware is the system software residing in a non-volatile memory, typically a ROM, EPROM or today often a flash memory. What is called firmware for the MIPS systems is relatively close to what is called the BIOS for the PC sector. There is a small number of firmware implementations in use on MIPS only however in a large number of variants.

ARC
Common Firmware Environment (CFE)
Cobalt Firmware
CoLo
IDT/sim, IDT/boot
mmon tiny monitor program for MIPS VR4300
PMON
PMON 2000
RedBoot
U-Boot
YAMON

実機でのfirmware実装のヒント

UTのCPU実験、GAIA上で動作させたxv6がヒントになるかも。カーネルビルドの最後にヘッダ部分に4bytesのサイズを書き込んでる。この辺はプロセッサ回路がどう設計されてるかで、firmware実装も変わってきそうな気がする。

kernelmemfs: $(MEMFSASMS) initcode _min-rt fs.img
    ./tools/gen_binary_blobs 0x80002000 initcode _min-rt fs.img
    $(AS) $(ASFLAGS) -c -o _kernelmemfs -e 0x80002000 -start _start $(MEMFSASMS) _binary_blobs.s $(UCCLIBS) -f __UCC_HEAP_START
    ./tools/gen_binary_blobs `ruby -e "print open('_kernelmemfs').size + 0x80002000"` initcode _min-rt fs.img
    $(AS) $(ASFLAGS) -c -o _kernelmemfs -e 0x80002000 -start _start $(MEMFSASMS) _binary_blobs.s $(UCCLIBS) -f __UCC_HEAP_START
    cat _kernelmemfs initcode _min-rt fs.img > kernelmemfs
    rm _kernelmemfs
    ./tools/attach_boot_header kernelmemfs

xv6/Makefile at 94d02ecb2b9492365574f2166cf29ce5a170abf5 · nyuichi/xv6 · GitHub

TODO

  • JTAG-UARTってDE10-Lite+MIPS+xv6で使えるのか。使えなかったらシリアルポート付きFPGAを購入する必要あり?

http://blog.goo.ne.jp/sim00/e/236d685cc29873571051270075449cac

Nios II/eでHello, world! (Quartus IIとQsys) - FPGAがさっぱり分からない人がDE0でFPGAを勉強する

x86のVGA操作について - FPGA開発日記

  • UARTってそもそもなに?(実装)

UART in VHDL and Verilog for an FPGA

  • harvard architectureだとData MemoryとProgram Memoryが別れているけど、kernelからどうやってProgram領域にデータロードするのか

(UNIXv6を読むと)textセグメント、dataセグメントに分けて考えているが、恐らくこれがそのままProgramMemory、DataMemoryに対応づくのだと思う。両Memoryは必ずしも物理的に別個のデバイスでなくとも問題ない。(ただし、回路設計上、アドレスバス、データバスは別々に接続されてなければならない。でないとRISC的な5段パイプラインが実現できないため)

  • User Programとは?

Makefileをみると、ユーザプログラム(cat, mkdir, echo...)は最初の段階でroot inode dirに書き込んでいるようにみえる。fork & execの過程でどうやって主記憶(ディスク)からProgramMemoryへ読み込まれるのだろうか。そもそもMIPS命令セットには「ProgramMemoryへ書き込み」などという命令はないのだから、やはり物理メモリは1本で、ProgramMemoryと呼ばれるものはあくまでキャッシュにすぎない。というイメージだろうか?

UNIXではコンテキストスイッチによってマルチプロセスを実現させる。レジスタは本数も少なく、容量も僅かなのでコンテキストスイッチの際、そのまま退避させたり復帰させたりするのが容易。しかし、物理メモリ上のデータはでかいのでいちいち退避させず、物理アドレスと仮想アドレスの対応関係を実行時に変換する。

各プロセスの仮想アドレス空間のアドレス範囲は同じでありオーバーラップしているのが一般的である。これを多重仮想記憶と呼ぶ。MMUは現に実行中のプロセスの仮想空間のみを認識する。コンテキストスイッチでプロセスを切り替える際、MMUに対して仮想アドレス空間の切り替えも指示する必要があるが、その方式はアーキテクチャによって様々である。

なお、アーキテクチャによっては、多重仮想記憶がオーバーラップしていると捉えず、全仮想空間がフラットに並んだ巨大な仮想空間を想定することもある。この場合、仮想空間識別番号が巨大な仮想空間のアドレスの一部と考えられる。もっとも、これは単にモデル化の手法が違うだけで実装に大きな違いがあるわけではない。実際、各ユーザープロセスが自分の仮想空間識別番号以外の仮想空間にアクセスすることはできない。

R3000の場合、TLBでPIDを識別する6bit情報を持っているので、仮想アドレス+PIDで物理アドレスマッピングする感じ

f:id:varmil:20171105145606p:plain http://www.cqpub.co.jp/dwm/contents/0074/dwm007401330.pdf

  • xv6-mipsにおける、bootloaderではbootasm.Sを使ってない?

YES.使ってない。そもそもbootasmはx86の命令セットを使用しているのでMIPSでは動作しない。

  • "unix v6 bootloader" v6はbootloaderも併せ持つようだが、どういう実装になっているのか
  • xv6-MIPSリンカスクリプトは?
  • メモリマップドのベースアドレスはどうやって決めている?MIPSの仕様?
  • MIPS R4000, 3000, 2000の違い(xv6-mipsはR4000基準のため)