(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
GAIAとxv6におけるbootloader
SRAM
GAIAで使ってる基盤は4MB(222)のSRAMを積んでる。 rom.vhd
は実際には sram.vhd
をラップしてるに過ぎず、いわゆるDRAMは一切使用していない。(BlockRamというのもSRAMの一種らしい?)
sram.vhd
の中では、入ってきたアドレスの上位10bitは見ておらず、下位22bitだけを見ている。(これが単純な仮想アドレス変換として機能している?ページテーブルやTLBの実装等は見当たらないため)
xv6
nyuichi/xv6
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
xv6_translate/chapter1.md at master · msyksphinz/xv6_translate · GitHub
original UNIX v6
- ROMに保存されているブートストラップローダプログラムが、ルートディスクのブロック番号0にあるブートストラッププログラムをメモリのアドレス0に読み込んで実行する。
- ブートストラッププログラムはルートディスクのファイルシステムから /unix や /rkunixといったカーネルプログラム本体をメモリのアドレス0に読み込んで実行する。
- カーネルがシステムの初期化を行う
電源ON〜kernelをディスクにロード、PCを設定するところまで (1, 2)
わかりやすい
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)
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と同じくらい早い?)。SRAMもFPGAのハードウェア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
ってやつ。
-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
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-mipsはQEMU上での動作しかサポートしていない。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
http://blog.goo.ne.jp/sim00/e/236d685cc29873571051270075449cac
Nios II/eでHello, world! (Quartus IIとQsys) - FPGAがさっぱり分からない人がDE0で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で物理アドレスとマッピングする感じ
http://www.cqpub.co.jp/dwm/contents/0074/dwm007401330.pdf
- xv6-mipsにおける、bootloaderではbootasm.Sを使ってない?