xv6-mipsリンカスクリプトとentry

リンカスクリプト

OUTPUT_FORMAT("elf32-tradlittlemips", "elf32-tradlittlemips", "elf32-tradlittlemips")
OUTPUT_ARCH(mips)
ENTRY(_start)

この _start はentry.Sに定義されている。

# By convention, the _start symbol specifies the ELF entry point.
.globl _start
_start:
  mfc0   $t0, $COP0_STATUS
  # Disable interrput
  li     $t1, STATUS_IE
  or     $t0, $t1, $t1
  xor    $t0, $t0, $t1
  # Setup IM registers preparing for enabling interrput
  ori    $t0, $t0, STATUS_IM
  # COP0
  lui    $t1, STATUS_CU0 >> 16
  or     $t0, $t0, $t1
  mtc0   $t0, $COP0_STATUS
  
  # Set up the stack pointer.
  la  $sp, stack + KSTACKSIZE

  # Jump to main()
  la     $t0, main
  jr     $t0
  nop

ここの最後で main() へ飛ぶ。memfsの仕組みがよくわかってないが、

xv6のI/O, PIC, IRQ, uart

プロセッサ側とポートを合わせなきゃいけないものと、kernel(ソフト側)で好き勝手に決めればいいものの境界線を探っている。xv6-mipsで定義している

mips.h

static int io_port_base = 0xb4000000;

これはなぜこのアドレスなのかわからない。inb outbuchar *へキャストしているのが関係ある? →QEMUのISAバスが Phys 0x1400_0000 に紐付いているから。 0xb400_0000 - 0x1400_0000 = 0xA0000000 (kseg1の先頭)となる。

uart 0x3F8

COM (hardware interface) - Wikipedia

これは仕様で決まってるっぽい

mips interrupt mask i/O

CPUで、「一体どのハードウェアからinterruptがきたのか」をどうやって判別するのか調べている。ついでにMemory-mapped I/Oの理解もきちんと出来ていないので。「こっからここまではデバイス用I/Oメモリ!」って決めるのは誰?CPUなのかkernelなのか、はたまたデバイスなのか。

f:id:varmil:20171113150735p:plain

https://courses.cs.washington.edu/courses/cse378/07au/lectures/L20-Interrupt.pdf

f:id:varmil:20171113150716p:plain

http://jjc.hydrus.net/cs61c/handouts/interrupts1.pdf

メモリマップドI/Oの入出力ポートとアドレスの割付はハードウェア(address decoder, address code decoderなどと呼ばれる)で決め打ち

このへんを見ると、わかるがI/OメモリマップはCPUの外にあるハードウェア(address decoderという回路)で制御しているっぽい。CPUが出力する物理アドレスを見て、それが特定のアドレス(I/Oにマップされている)の場合、向き先をデバイスに向ける。

CPUに内蔵されているデバイスであれば、ボードが違っても同じアドレスに見えるはずだよね。でも、デバイスを外付けした場合は、ボードの設計によるんだ。この場合は、ボードの仕様書にメモリマップが書かれているはずだよ。

I/Oポートベース(qemuだとPhys0x1400_0000)もハードウェアで好きに設定してよい

(xv6-mipsだと)ベースアドレスからのオフセットで、各周辺デバイスへのメモリマップを行う。COM1を 0x3f8 にしているのもPC/AT互換機のボード設定であり、MIPSおれおれボードで必ずしもこのアドレスに設定する必要はない。(あくまで、address decoderの回路とkernelの設定を一致させればよいだけ)

https://i.stack.imgur.com/rBbh1.png

cpu - Difference between port mapped and memory mapped access? - Super User

The hardware of the system is arranged so that devices on the address bus will only respond to particular addresses which are intended for them, while all other addresses are ignored. This is the job of the address decoding circuitry, and that establishes the memory map of the system.

Memory-mapped I/O - Wikipedia

自作CPUではPICはいらない

パタヘネ本にある通り、ハードウェア側でgeneral exception handler entry point addressを決め打ちする(0x8000_0180)。例外ベクタは使用しない。PICも必要ない。

DMA (I/O) Controller

I/Oを特に工夫しない通常のデータの流れだと、I/Oデバイスからレジスタファイルへデータ転送(lw)→レジスタファイルからRAMへデータ転送(sw) というのが読み出したいbyte分繰り返されることになり、その間CPUは他の仕事をできない。

これを、I/Oデバイス(DMA Controller)⇔RAM間で直接データ転送できるようにするのがDMAである。DMA Controller or I/O Controllerは回路であり、いわゆるマザボ等のハードウェア側で対応しなければ使用できない。

バスとは

単なる概念。Data Busは、ReadData[32], WriteData[32]に分かれていたりする。その集合概念

(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