ベアメタルRustでセルフリロケーション

背景

(詳しい人には釈迦に説法な内容なので読み飛ばしてください。)

自作OSや自作VMMをやっている際に問題になるのが、ブートストラップをどうするかです。通常U-BootやUEFIなどのブートローダからOSのローダが起動してKernelを展開する、もしくはKernelが直接起動するようになっています。

MMUを持つCPUで動く汎用Kernelは0xFFFF...から始まるいわゆるHigh Memory Addressで動くことが大半で、このページテーブルはKernelの起動初期かローダが作成します。そのため、Kernelのアドレッシングは(ASLRなどありますが)静的でも動きますが、このページテーブルができる前は物理アドレスで関数や変数のアドレスを指定します。

組み込み系などあらかじめ動く環境が決まっている環境は、ローダが置かれるアドレスをリンカスクリプトで指定してビルドすればバイナリにアドレスが適切に埋め込まれます。一方で汎用OSでは動作環境によってRAMが存在しているアドレスが異なり、ロードされて初めて決定することも多いです。

そのため、特にローダはどのアドレスにも配置されても実行できるようにする必要があります。そしてローダの中でメモリマップを把握してKernelで使用するメモリ領域を決定しロードしていきます。

コードレベルでどのアドレスにロードされても動くようにするには固定のアドレスを持たないなどの対策で実現できますが、実際の機械語で固定アドレスを使用しない様にするには手間がかかります。グローバル変数や関数などのアドレスを持っておかないと参照できません。

最近のCPUアーキテクチャではPC相対というアドレッシングモードがサポートされています。これはアドレスを指定する際に現在実行している命令のPC(Program Counter)からの相対値をオペランドとして機械語に埋め込んでおき、実行時にCPUがPCの値を元に実際のアドレスを決定します。実現にはいくつか手段がありますがx86_64やArm、RISC-Vが対応しています。(x86(32bit)はこの方式をサポートしておらず、関数呼び出しでスタックからアドレスを取り出すなどします。)このようなPC相対アドレスを使用してどのアドレスに配置されても動作するようにしたプログラムをPIC(Position Independent Code)/PIE(Position Independent Executable)といいます。

PC相対があるから全て解決かというとそうではありません。関数ポインタを保持しているグローバル変数などは、実行開始時から有効なアドレスを保持していなければなりません。正確には分割コンパイルなどで、いつ参照されるかコンパイラからは判断できず、かつ初期化コードを自動的に挿入できない言語仕様のためです。これらは relaセクションに実行時にベースアドレスを書き込む場所が列挙して保存されていて、実行前にローダやCRTが適切に設定します。

RustでフラットバイナリでPIEがしたい

自作OS(https://github.com/PG-MANA/Methylenix)をRISC-V向けにポートしていて、その際にロードをどうするかを考えていました。x86_64はGRUB2からMultiboot経由で立ち上げていてELFを適切に配置してくれます(そろそろどうにかしたい)。AArch64はUEFIから立ち上げることを前提にLoaderを作って、KernelのELFを読み込んでページテーブルを作成するようにしています。

RISC-Vでどうするか考えたのですが、U-Bootから立ち上がるようにしたいため、ELF形式かPE形式(bootefi)でローダを作ることを考えました。ELF形式ではELFのプログラムヘッダに書かれているアドレスにロードしてしまいます(https://github.com/u-boot/u-boot/blob/v2026.01/lib/elf.c#L100)。これでは動的に読み込むアドレスを決定できません。U-BootはUEFIアプリケーションを(完全ではないものの)動かせるようになっていて、UEFIの仕様に従い動的再配置が行えるようになっています。RISC-Vでもサポートされているようですが、RustでRISC-V向けUEFIアプリケーションを作る方法がまだない(多分target.jsonを書けばいける)のと、PE形式に頼るのが負けた気分になったので断念しました。

というわけで、残るはFlat Binary、すなわちメモリに展開して一切の前処理を行わずに先頭アドレスにジャンプして実行する方式を取ることにしました。これはLinux KernelのImageなどでも採用されています。(AArch64のImageはPEファイルとしても機能するようになっています。)

出力をFlat Binaryにするには、リンカスクリプトで、OUTPUT_FORMAT(binary)を指定するとできます。

また、PIEな機械語を生成するために以下のような.cargo/config.tomlを書きます。

[target.'cfg(all(target_arch = "riscv64", target_os = "none"))']
rustflags = [
    "-C", "code-model=medium",
    "-C", "relocation-model=pie",
    "-C", "no-redzone=yes",
    "-C", "panic=abort",
    "-C", "link-arg=-Tloader.ld",
]

これでできたLoaderのバイナリをU-Bootで実行します。$kernel_addr_rには環境ごとに違うアドレスが入っています。

fatload nvme 0 $kernel_addr_r loader
go $kernel_addr_r

結果はまあ…コケるわけです。(コケたときの例外情報を取り忘れました…)

原因追求

今回コケた命令のアドレスは0x84004364でした。0x84000000$kernel_addr_rの値なので、コケた命令は0x4364ということになります。デバッグのために出力形式をELFに戻して、objdumpでみてみます。

00000000000042e6 <core::fmt::write>:
    42e6: 7159          addi    sp, sp, -0x70
    42e8: f486          sd      ra, 0x68(sp)
    42ea: f0a2          sd      s0, 0x60(sp)
    42ec: eca6          sd      s1, 0x58(sp)
    42ee: e8ca          sd      s2, 0x50(sp)
    42f0: e4ce          sd      s3, 0x48(sp)
    42f2: e0d2          sd      s4, 0x40(sp)
    42f4: fc56          sd      s5, 0x38(sp)
    42f6: f85a          sd      s6, 0x30(sp)
    42f8: f45e          sd      s7, 0x28(sp)
    42fa: f062          sd      s8, 0x20(sp)
    42fc: ec66          sd      s9, 0x18(sp)
    42fe: 89b6          mv      s3, a3
    4300: 892e          mv      s2, a1
    4302: 0016f593      andi    a1, a3, 0x1
    4306: 8a2a          mv      s4, a0
    4308: 14059763      bnez    a1, 0x4456 <core::fmt::write+0x170>
    430c: 00064403      lbu     s0, 0x0(a2)
    4310: 16040763      beqz    s0, 0x447e <core::fmt::write+0x198>
    4314: 4c01          li      s8, 0x0
    4316: 01893a83      ld      s5, 0x18(s2)
    431a: 08000b13      li      s6, 0x80
    431e: 0c000b93      li      s7, 0xc0
    4322: 60000cb7      lui     s9, 0x60000
    4326: 020c8c93      addi    s9, s9, 0x20
    432a: a025          j       0x4352 <core::fmt::write+0x6c>
    432c: 004c1513      slli    a0, s8, 0x4
    4330: 00a985b3      add     a1, s3, a0
    4334: 6188          ld      a0, 0x0(a1)
    4336: 6590          ld      a2, 0x8(a1)
    4338: e052          sd      s4, 0x0(sp)
    433a: e44a          sd      s2, 0x8(sp)
    433c: e866          sd      s9, 0x10(sp)
    433e: 858a          mv      a1, sp
    4340: 9602          jalr    a2
    4342: 14051063      bnez    a0, 0x4482 <core::fmt::write+0x19c>
    4346: 0c05          addi    s8, s8, 0x1
    4348: 0004c403      lbu     s0, 0x0(s1)
    434c: 8626          mv      a2, s1
    434e: 12040863      beqz    s0, 0x447e <core::fmt::write+0x198>
    4352: 03841513      slli    a0, s0, 0x38
    4356: 00160493      addi    s1, a2, 0x1
    435a: 00054a63      bltz    a0, 0x436e <core::fmt::write+0x88>
    435e: 8552          mv      a0, s4
    4360: 85a6          mv      a1, s1
    4362: 8622          mv      a2, s0
    4364: 9a82          jalr    s5

jalr s5は戻り先をリンクレジスタに格納してレジスタの値にジャンプする命令なので、s5レジスタの値が壊れていると予想します。関数の先頭に向かってたどるとld s5, 0x18(s2)があり、s2レジスタの指す先から0x18バイト先からロードしているようです。そして戻っていくとmv s2, a1とあるように、このレジスタは引数として渡されているようです。

core::fmt::writeを呼んでいるのは自作したprint関数です。

0000000000002ba6 <kernel_loader::print::print>:
    2ba6: 86ae          mv      a3, a1

0000000000002ba8 <.Lpcrel_hi26>:
    2ba8: 00b71597      auipc   a1, 0xb71
    2bac: f4058593      addi    a1, a1, -0xc0
    2bb0: 6198          ld      a4, 0x0(a1)
    2bb2: c739          beqz    a4, 0x2c00 <.Lpcrel_hi27+0x40>
    2bb4: 862a          mv      a2, a0
    2bb6: 0016f513      andi    a0, a3, 0x1
    2bba: e919          bnez    a0, 0x2bd0 <.Lpcrel_hi27+0x10>
    2bbc: 00858513      addi    a0, a1, 0x8

0000000000002bc0 <.Lpcrel_hi27>:
    2bc0: 00002597      auipc   a1, 0x2
    2bc4: 31058593      addi    a1, a1, 0x310
    2bc8: 00001317      auipc   t1, 0x1
    2bcc: 71e30067      jr      0x71e(t1) <core::fmt::write>

下からa1を追いかけるとすぐ上にauipc命令とaddi命令があります。auipc命令は現在のPCとオペランドを12ビットシフトした値を加算してレジスタに格納する命令です。つまりここ、「objdump上」ではa1 = 0x2bc0 + (0x2 << 12) = 0x4bc0ということになります。これがPC相対アドレッシングで、実際の動作環境によって格納される値は変わります。先程動作させた環境では0x84002bc0 + (0x2 << 12)になるはずです。objdumpでは0ベースで表示しているので、先程の計算結果になります。次のaddi命令は加算命令で、a1 = a1 + 0x310 = 0x4bc0 + 0x310 = 0x4ed0が実行結果になります。

さて、このアドレスをreadelfなどで参照すると匿名変数になっていました。

182: 0000000000004ed0    48 OBJECT  LOCAL  HIDDEN     2 anon.8ebadb4c8da7aa972c6ef32b2da398b1.26.llvm.17646884786561777633

これでは埒が明かないので、Rustにアセンブリを吐かせる方法を使って、アセンブリを出して比較します。

_RNvNtCs4S0rFkNLftr_13kernel_loader5print5print:
   mv      a3, a1
.Lpcrel_hi51:
   auipc   a1, %pcrel_hi(.L_MergedGlobals)
   addi    a1, a1, %pcrel_lo(.Lpcrel_hi51)
   ld      a4, 0(a1)
   beqz    a4, .LBB10_8
   mv      a2, a0
   andi    a0, a3, 1
   bnez    a0, .LBB10_3
   addi    a0, a1, 8
.Lpcrel_hi52:
   auipc   a1, %pcrel_hi(.Lanon.07d1e03e94b7c3c3fc7ae6cf24a924da.76)
   addi    a1, a1, %pcrel_lo(.Lpcrel_hi52)
   tail    _RNvNtCs3pK5phuYJXT_4core3fmt5write

表現が少し異なりますが、.Lanon.07d1e03e94b7c3c3fc7ae6cf24a924da.76のアドレスを取ろうとしているようです。これもアセンブリから探します。

   .section        .rodata..Lanon.07d1e03e94b7c3c3fc7ae6cf24a924da.76,"a",@progbits
   .p2align        3, 0x0
.Lanon.07d1e03e94b7c3c3fc7ae6cf24a924da.76:
   .asciz  "\000\000\000\000\000\000\000\000\b\000\000\000\000\000\000\000\b\000\000\000\000\000\000"
   .quad   _RNvXNtCs4S0rFkNLftr_13kernel_loader5printNtB2_10SerialPortNtNtCs3pK5phuYJXT_4core3fmt5Write9write_str
   .quad   _RNvYNtNtCs4S0rFkNLftr_13kernel_loader5print10SerialPortNtNtCs3pK5phuYJXT_4core3fmt5Write10write_charB6_
   .quad   _RNvYNtNtCs4S0rFkNLftr_13kernel_loader5print10SerialPortNtNtCs3pK5phuYJXT_4core3fmt5Write9write_fmtB6_
   .size   .Lanon.07d1e03e94b7c3c3fc7ae6cf24a924da.76, 48

名前修飾で見づらいですが、ASCIIZでほとんどヌル文字の文字列と、3つのアドレスが書かれているようです。そして、計算すると、0x18(a1).quad _RNvXNtCs4S0rFkNLftr_...write_strを指しています。これはfmt::write関数からコールバックされる文字列の出力関数で、traitの関数(C++でいうところの純粋仮想関数)になっています。
このポインタ(グローバル変数)には、リンカスクリプトを元に計算したアドレスが入っていそうですが、実行時にベースアドレスに応じた初期化はされていなさそうです。

ここで、インターネットを漁っていると我らがhikalium先生が同じ問題に引っかかっていました。https://hikalium.hatenablog.jp/entry/2022/12/19/011722

時間切れ

というわけで追いかけていたら.data.rel.roの沼にはまったので今日はここまで。明日もやるかも。おやすみなさい…。

https://hikalium.hatenablog.jp/entry/2022/12/19/011722

沼にハマって寝てました。私は知っています…これはやらないやつで、「MANAさん後はよろしくぅ…」とスゥッと消えていくやつです。

周辺の記事を探してもやっぱりやってなかったので、続きをすることにします。
記事にある通り、.data.rel.roセクションにこのグローバル変数は置かれるようです。objdumpでダンプします。

Contents of section .data:
 0010 00000000 00000000 08000000 00000000  ................
 0020 08000000 00000000 0e360000 00000000  .........6......
 0030 b08f0000 00000000 1c000000 00000000  ................
 0040 fc000000 11000000 b08f0000 00000000  ................
 0050 1c000000 00000000 ec000000 17000000  ................
 0060 0d8f0000 00000000 24000000 00000000  ........$.......
 0070 49000000 09000000 28980000 00000000  I.......(.......
 0080 0d000000 00000000 2f000000 11000000  ......../.......
 0090 28980000 00000000 0d000000 00000000  (...............
 00a0 3e000000 0d000000 28980000 00000000  >.......(.......
 00b0 0d000000 00000000 46000000 12000000  ........F.......
 00c0 28980000 00000000 0d000000 00000000  (...............
 00d0 52000000 0a000000 28980000 00000000  R.......(.......
 00e0 0d000000 00000000 58000000 12000000  ........X.......
 00f0 00000000 00000000 08000000 00000000  ................
 0100 08000000 00000000 dc350000 00000000  .........5......
 ...

しばらく眺めてみましたが、構造がわかりません。Elf64_Rel構造体の配列でもElf64_Rela構造体の配列でもなさそうです。0x08が定期的に現れますが、ポインタの個数を表しているわけでも無さそうです。
ここで、アドレスをあぶり出すために、リンカスクリプトの最初に. = 0xFF0000;を追加して、ベースアドレスを変更してみます。ビルド後再度ダンプします。

 ff0010 00000000 00000000 08000000 00000000  ................
 ff0020 08000000 00000000 0e36ff00 00000000  .........6......
 ff0030 b08fff00 00000000 1c000000 00000000  ................
 ff0040 fc000000 11000000 b08fff00 00000000  ................
 ff0050 1c000000 00000000 ec000000 17000000  ................
 ff0060 0d8fff00 00000000 24000000 00000000  ........$.......
 ff0070 49000000 09000000 2898ff00 00000000  I.......(.......
 ff0080 0d000000 00000000 2f000000 11000000  ......../.......
 ff0090 2898ff00 00000000 0d000000 00000000  (...............
 ff00a0 3e000000 0d000000 2898ff00 00000000  >.......(.......
 ff00b0 0d000000 00000000 46000000 12000000  ........F.......
 ff00c0 2898ff00 00000000 0d000000 00000000  (...............
 ff00d0 52000000 0a000000 2898ff00 00000000  R.......(.......
 ff00e0 0d000000 00000000 58000000 12000000  ........X.......
 ff00f0 00000000 00000000 08000000 00000000  ................
 ff0100 08000000 00000000 dc35ff00 00000000  .........5......

0e3600000e36ff00に変化したようにポインタのところだけ変化しました。やはり、「Null or ポインタ、小さな値、小さな値〜大きな値、ポインタ、….」連続でできているようです。

さて、この構造が何かを調べていたところ、traitのvtableであることがわかりました。https://qnighy.hatenablog.com/entry/2017/03/18/070000

なるほど、drop関数ポインタ、型サイズ、型アライメント、関数ポインタの順で並んでいたのようで、辻褄が合います。

Vtableのリストから順当にポインタを取り出して、ロードされたアドレスになるように適宜調節すれば良さそうですが、関数ポインタが配列長がない可変長配列のため、先頭から順に辿っていっても構造が把握できません。

しばらく唸っていましたが、うまくアドレスを取り出す方法を思いつきませんでした。

解決策

ここでふと、アドレスの部分をあぶり出す際に. = 0xFF0000;を追加したことを思い出しました。0xFFがついていればポインタと判断したわけですが、これをコードベースでやればいいのです。

思えば、PC/AT互換機のBIOSだってHDDやFDDの先頭512バイトを読んで最後の2バイトが0xAA55だったら先頭アドレスに飛ぶということを繰り返して起動していました。(MBRとFAT)

これにあやかって、ベースアドレスを0xAA550000にしておき、起動時に.data.rel.roを片っ端から舐めて0xAA55が含まれるアドレスを修正していけばよいのではないでしょうか。きれいな解法ではないですが、まあブートストラップということで許してもらいましょう。

それではリンカスクリプトを以下のようにします。

OUTPUT_FORMAT(binary)
/* OUTPUT_FORMAT(elf64-littleriscv) */
ENTRY(_start)

SECTIONS {
  /* This is the magic number, please see `_start` also */
  . = 0xAA550000;

  .start : {
    *(.start)
  }

  . = ALIGN(8);
  __REL_START = .;

  .data : {
    KEEP(*(.data.rel*))
  }

  . = ALIGN(8);
  .got : {
    KEEP(*(.got))
  }
  __REL_END = .;
  
  .text : {
    *(.text .text.*)
  }

  .data : {
    *(.data)
  }
  
  .rodata : {
    *(.rodata .rodata.*)
  }

  .bss : {
    KEEP(*(.bss .bss.*))
  }

  /DISCARD/ : {
    *(.comment .comment.*)
    *(.eh_frame .eh_frame.*)
    *(.eh_frame_hdr .eh_frame_hdr.*)
  }
}

全メモリを舐めると流石に問題があるので、関数ポインタがある領域を__REL_START__REL_ENDというリンカ定数で指定します。ここではGlobal Offset Tableも生成されていたため、これも対象とします。

リンカスクリプトにあるように、最初には.startセクションが来るようにします。セクションには_start関数が来るようにします。これはアセンブリで記述して、暗黙のグローバル変数参照がないようにします。

#[unsafe(naked)]
#[unsafe(no_mangle)]
#[unsafe(link_section = ".start")]
/// `a0` and `a1` must be reserved.
extern "C" fn _start() {
    core::arch::naked_asm!("
    .extern  __REL_START, __REL_END
    // This must be the first instruction
    auipc   t0, 0
    lla     t1, __REL_START
    lla     t2, __REL_END
    li      t3, 0xAA55
    slli    t4, t3, 16
1:
    ld      t5, (t1)
    srli    t6, t5, 16
    bne     t6, t3, 2f
    // t5 == 0x0000_0000_AA55_xxxx
    xor     t5, t5, t4  // t5 ^= 0xAA55_0000
    or      t5, t5, t0  // t5 |= t0(base_address)
    sd      t5, (t1)
2:
    addi    t1, t1, 8
    bne     t1, t2, 1b
3:
    j {main}", main = sym crate::main);
}

フルアセンブリですが、やっている内容は至ってシンプルです。

最初にレジスタに値をセットしていきます。

  • t0 : PCの値、ここではすなわちベースアドレスが格納されます
  • t1 : __REL_STARTの値をPC相対で読み込み格納します、ここではauipc命令とaddi命令に展開されます
  • t2 : __REL_ENDの値が格納されます
  • t3 : 0xAA55が格納されます
  • t4: t3 を16ビット左シフトした値、すなわち0xAA550000が格納されます

ラベル1から3までの間でt1を8バイトずつ増やしながら、値の確認と更新を行います。読み出した値を16ビット右シフトして0xAA55と比較し、同じだった場合は関数ポインタだったと判断します。0xAA55はマーカのため、これをXORで除去した後、ベースアドレスを加算します。結果を書き戻した後次のループに移行します。終わったら、Rustで書いたmain関数に飛んでいきます。

動作確認

さて、こんな雑な実装で動くのでしょうか。

## Starting application at 0x84000000 ...
Boot Loader version 0.1.0
Loader range:   [        0x84000000 ~         0x84B74000]
Stack  range:   [        0xFEEE2000 ~         0xFEEE7000]
RAM:            [        0x80000000 ~        0x100000000]
Reserved:       [        0x80100000 ~         0x80140000]
Reserved:       [        0x80140000 ~         0x80160000]
Allocatable:    [        0x80000000 ~         0x80100000]
Allocatable:    [        0x80160000 ~         0x84000000]
Allocatable:    [        0x84B74000 ~         0xFEEE2000]
Allocatable:    [        0xFEEE7000 ~        0x100000000]
Load the kernel...

なんと動いてしまいました。動いたときは思わず笑いが出てしまいました。
疑わしいので、読み込む先を$ramdisk_addr_r(試した環境では0x8C300000)に変更して、「ビルドし直さずに」再度実行します。

## Starting application at 0x8C300000 ...
Boot Loader version 0.1.0
Loader range:   [        0x8C300000 ~         0x8CE74000]
Stack  range:   [        0xFEEE4000 ~         0xFEEE9000]
RAM:            [        0x80000000 ~        0x100000000]
Reserved:       [        0x80100000 ~         0x80140000]
Reserved:       [        0x80140000 ~         0x80160000]
Allocatable:    [        0x80000000 ~         0x80100000]
Allocatable:    [        0x80160000 ~         0x8C300000]
Allocatable:    [        0x8CE74000 ~         0xFEEE4000]
Allocatable:    [        0xFEEE9000 ~        0x100000000]
Load the kernel...

やっぱり動いちゃうらしいです。Loader rangeが変わっていることにも注目です。

というわけで、世の中はやっぱり泥臭いハックで回ってしまうということでした。

追記

上はQEMUで実験してたわけなんですが、なんと実機でも動いちゃったんですよね。

## Starting application at 0x40200000 ...
Boot Loader version 0.1.0
Loader range:   [        0x40202000 ~         0xEB2C6000]
Stack  range:   [        0xCC502000 ~         0xCC506000]
RAM:            [        0x40000000 ~        0x140000000]
Reserved:       [        0x40000000 ~         0x40080000]
Allocatable:    [        0x40080000 ~         0x40202000]
Allocatable:    [        0xEB2C6000 ~        0x140000000]
Load the kernel...
 Physical Address  |  Virtual Address   | Memory Size  |  File Size   | File Offset  |  Alignment   | Read  | Write | Exec
0x0000000040080000 | 0xFFFFFFE000000000 | 0x00000AAF18 | 0x00000AAF18 | 0x0000001000 | 0x0000001000 |  true | false |  true
0x000000004012B000 | 0xFFFFFFE0000AB000 | 0x00000105C0 | 0x00000105C0 | 0x00000AC000 | 0x0000001000 |  true |  true | false
0x000000004013C000 | 0xFFFFFFE0000BC000 | 0x0000014F7A | 0x0000014F7A | 0x00000BD000 | 0x0000001000 |  true | false | false
0x0000000040151000 | 0xFFFFFFE0000D1000 | 0x0000000010 | 0x0000000010 | 0x00000D2000 | 0x0000001000 |  true |  true | false
0x0000000040152000 | 0xFFFFFFE0000D2000 | 0x0000006AF8 | 0x0000000000 | 0x00000D3000 | 0x0000001000 |  true |  true | false
Kernel's entry point: 0xFFFFFFE000025CE6
Dump the initial page table for the kernel
VA:         0x40202000 => PA:         0x40202000, R: true, W: true, E: true, U:false
...         0xCC400000                0xCC400000 (fin)
VA:         0xCC502000 => PA:         0xCC502000, R: true, W: true, E: true, U:false
...         0xCC506000                0xCC506000 (fin)
VA:         0xCC600000 => PA:         0xCC600000, R: true, W: true, E: true, U:false
...         0xEB2C6000                0xEB2C6000 (fin)
VA: 0xFFFFFF8000000000 => PA:                0x0, R: true, W: true, E:false, U:false
... 0xFFFFFFC000000000              0x4000000000 (fin)
VA: 0xFFFFFFE000000000 => PA:         0x40080000, R:false, W:false, E: true, U:false
... 0xFFFFFFE0000AB000                0x4012B000 (fin)
VA: 0xFFFFFFE0000AB000 => PA:         0x4012C000, R: true, W: true, E:false, U:false
... 0xFFFFFFE0000BB000                0x4013C000 (fin)
VA: 0xFFFFFFE0000BB000 => PA:         0x4013D000, R: true, W:false, E:false, U:false
... 0xFFFFFFE0000CF000                0x40151000 (fin)
VA: 0xFFFFFFE0000CF000 => PA:         0x40152000, R: true, W: true, E:false, U:false
... 0xFFFFFFE0000D6000                0x40159000 (fin)
Jump to the kernel...