Writing an OS in 1000 linesをRustでやる
qemuとかは↑と同じ
rustc 1.73.0 (cc66ad468 2023-10-03)
最初の方は↑を参考にさせていただきました
5. boot
コンパイルできない問題1(not found eh_personality)
cargo buildすると、error: language item required, but not found: eh_personalityとなった
スタックアンワインドを無効化する
スタックアンワインドとは????
コンパイルできない問題2(いろいろ修正)
cargo buildすると、error: linking with cc failed: exit status: 1となった
やったこと
$ rustup target listすると、riscv32i-unknown-none-elfがインストールされてなかった(あほー)
$ rustup target add riscv32i-unknown-none-elfしてインストールした
.cargo/cargo.tomlを.cargo/config.tomlに名前を修正した
これは普通に間違えていた(あほー)
config.tomlのrustflagsのところ
link-arg=-Tlinker.ldではなく、link-arg=-Tkernel.ldでは?と思い修正
これでbuildは走った
cargo runできない問題
cargo runすると、qemuをrunnerで実行するとき、No such file or directoryと言われる
これは、私がqemuをソースからビルドして、$HOME/toolsの下に展開していることが原因である
おとなしくPATHを通した
qemuモニターでinfo registersすると、pcが80200004で止まってるのでよさそう
cargo runできない問題の追記
私の環境(さらに別のPC)では、qemuのバージョンが複数混在しており、今回使うもの(8.1.2)は$HOME/toolsに入れている
また、aptでいれたqemuなどもあり、名前が衝突している
これを使い分ける手段がわからなかった
update-alternativesが使えそうだったが、aptで入れたものが/usr/binのしたにバイナリとして置かれていたため、うまく行かなかった
私のLinux力不足で解決できなかった
さらに、config.tomlのrunnerでは、直接パスを指定できないらしい
これはぐぐっても、それらしい情報が見つからず、chatgpt先輩に聞いた
先輩曰く、Rustのビルドスクリプトを使えばいいらしく、そのうち対応する
ここでは、cargo build した後、シェルスクリプトからqemuを実行することにした
boot(bssのゼロクリア)まで
キャストについて
code:main.rs
let addr_bss_start = &__bss as *const u8;
let addr_bss_end = &__bss_end as *const u8;
うまくいったか確認する
code:sh
$ llvm-nm target/riscv32i-unknown-none-elf/debug/os-rust-1000
~~~~~
80200370 B __bss
80200370 B __bss_end
8020022c t __mulsi3
80220370 B __stack_top
80200000 T boot
80200018 T kernel_main
80200258 t memset
80200098 t rust_begin_unwind
80200280 r str.0
もう少しマシなオプションある気がするけど、bssは0x80200370にあることがわかったので、qemuでメモリダンプしてみる
code:sh
(qemu) xp /200xw 0x80200370
0000000080200370: 0x00000000 0x00000000 0x00000000 0x00000000
~~~~~
0になっているので、よさそう。
ただ、llvm-nmしたとき、bss_endとbssのアドレスが一致しているので、調べた意味はあまりないかもしれない
現状、static mutが使われているため、そのうち別の形に変更したほうがよい
6. hello world
putchar作るところまで
warningいっぱいでてるのと、ファイル分けるのはあとで対応
qemuモニタで、info registersして、a0~6レジスタが0、a7レジスタが1になっていることを確認
print作る
ちなみによくわかってない
いっぱいでたwarningを消した
git tag のpushのやりかた、無限に忘れてしまう git push origin <tag>
7. C標準ライブラリ
paddr_t: 物理メモリアドレスを表す型。
vaddr_t: 仮想メモリアドレスを表す型。標準ライブラリでいうuintptr_t。
align_up: valueをalignの倍数に切り上げる。alignは2のべき乗である必要がある。
is_aligned: valueがalignの倍数かどうかを判定する。alignは2のべき乗である必要がある。
offsetof: 構造体のメンバのオフセット (メンバが構造体の先頭から何バイト目にあるか) を返す。
メモリ、文字列操作もあるが、代替機能(write_bytesとか)あるからスルー
必要に応じて実装する
8. kernel panic
このコードをコンパイルすると、このloopにunreachable expressionとなると言われた
ここまで検査してくれるのすごいなと思った
9. 例外処理
割り込みに関するレジスタ(参考:エナガ本、RISC-V原典)
table:trap
sstatus supervisor status いろんなステータス(原典p107)
stvec supervisor trap vector base address 割り込みハンドラのアドレス
scause supervisor cause 割り込み原因
sepc supervisor exception program counter 割り込み時のプログラムカウンタ
sscratch supervisor scratch 割り込みハンドラが好きに使える一時領域
stval supervisor trap value フォルトを起こしたアドレスや不正な命令などのトラップ情報
kernel_entryの流れを自分で整理してみる
csrw sscratch, sp sscratchに例外発生時のspを書き込む(スタックポインタを保存する)
addi sp, sp, -4*31 これから31本のレジスタを保存するため、spをその分だけ動かす。スタックは下に伸びるため、マイナスしている。
sw ~~~~~~ レジスタの値(s11とか)を指定した位置(s11の場合は、現在のspから4*29バイト上のスタック領域)に保存する。
csrr a0, sscratch a0レジスタにsscratchの値(今は例外発生時のsp)を読み込む。
sw a0, 4*30(sp) a0(例外発生時のsp)もスタック領域に保存する。
mv a0, sp a0にspを保存する。a0は関数の引数を入れるレジスタ。次にhandle_trapを呼び出しているが、この関数はTrapFrame構造体で、a0にはこれが渡される。
swで保存したレジスタの順番と、構造体のレジスタの順を見比べるとよくわかる!
call {handle_trap} handle_trap関数を呼ぶ。この関数で例外をイイ感じに処理する。今はパニックするだけ。
lw ~~~~~ 上で例外処理が終わったら、スタックに保存していたレジスタをもとに戻す。
sret 戻ってくる。
TrapFrame構造体が正しくセットされているか調べる
code:trap.rs
frame: TrapFrame {
ra: 8020063c, gp: 0, tp: 80026000,
t0: 1, t1: 80203ba4, t2: 1000, t3: 616d6569, t4: 0, t5: 4, t6: 0,
a0: 802004cc, a1: 80204764, a2: 0, a3: f, a4: 0, a5: 1, a6: 1, a7: 5,
s0: 80025f30, s1: 1, s2: 80200000, s3: 0, s4: 87e00000, s5: 0, s6: 80006800,
s7: 80020024, s8: 2000, s9: 800222f8, s10: 0, s11: 0,
sp: 80224744
}
code:info
(qemu) info registers
~~~~~
x0/zero 00000000 x1/ra 802006dc x2/sp 80224408 x3/gp 00000000
x4/tp 80026000 x5/t0 00000003 x6/t1 80200104 x7/t2 00001000
x8/s0 80025f30 x9/s1 00000001 x10/a0 00000000 x11/a1 80203e04
x12/a2 00000000 x13/a3 00000000 x14/a4 00000000 x15/a5 00000000
x16/a6 00000000 x17/a7 00000001 x18/s2 80200000 x19/s3 00000000
x20/s4 87e00000 x21/s5 00000000 x22/s6 80006800 x23/s7 80020024
x24/s8 00002000 x25/s9 800222f8 x26/s10 00000000 x27/s11 00000000
x28/t3 616d6569 x29/t4 00000000 x30/t5 00000004 x31/t6 00000000
ra, t0, t1, a0, a1, a3, a5, a6, a7, spが異なっている。レジスタを保存したあと、handle_trap内でpanic出力しているため、違うだろうなとは思った。
ここにあるレジスタたちは、関数呼び出し時に変わりそう。保存用のsレジスタとかが変わってないので、とりあえずよさそうということで。。。(ガバガバ)
ra: return address, t0t1: 一時レジスタ, a0~7: 関数の引数返り値, なので、変わりそう
10. メモリ割り当て
たしかに、1000linesの方でも、ポインタを進めながらメモリ確保して、開放しない実装なので、方向が同じ
コード量も少なそうだし、いったんこれを実装してみる
kernel.ldの変更を、cargoは自動でチェックしてくれないらしいので$ touch src/main.rsしておく
参考記事をそのまま実装した。正直、なにしてるのかよくわからなかったが、とりあえず先に進める。
let res = *head as usize % alignとかぜんぜんわからん
extern crate allocしないといけないらしいが、これ不要だってどこかで見た気が...
無事、Vecが動いた
ここで疑問。このあとページングで確保したメモリをプロセス構造体に渡すが、なんのデータ構造で実装する???
あと、allocator::HEAPって書いたけど、これヒープではない気がする???
kernel.ld見ると、 128KBでスタック(__stack_top)を確保してから、メモリ?(__free_ram)をとっている。
ということは、スタックの後ろにあるから、ヒープではなさそう
だが、これ実装すると、Vecとかが使えるようになるからヒープ????
仮想メモリ領域の図に書いてあるヒープ(スタックの向かいにあるやつ)にとらわれる必要はなく、仮想は自由に定義できるため、ヒープの場所自体はどこでもいいみたい!!
ただ、free_ram, free_ram_endを指定してHEAPを作っているため、Vecとかを使うと、この領域から確保されそうなので調査
main.rsでprintln!("{:p}", &v);してみる(:pするとアドレス見れるらしい)
結果:0x80224d40
llvm-nmする
結果:80224d9c B __stack_top と、80225000 B __free_ram \ 84225000 B __free_ram_end
あれ??__free_ramの間に入っててほしいにのに、スタックより前にいる
冷静に考えたら、vは変数だからスタック領域に置かれるため、スタックの前というのはあっていそう
結果:0x80225000 free_ramと一致したので正しく動作している!
とりあえず、動的にメモリを確保する術は手に入れたので、いったんここまで。
11. プロセス
context_switchできない問題
頑張ってコード書いて、switch_contextが動くかチェックしようとしたところ、トラップして止まった
code:out
panicked at src/trap.rs:49:3:
unexpected trap scause=1, stval=0, sepc=0
frame: TrapFrame { ra: 0, gp: 0, tp: 80026000, t0: 802254f0, t1: 80204bc8, t2: 802254e8, t3: 802254e4, t4: 802254e0, t5: 802254dc, t6: 802254d8, a0: 0, a1: 80225750, a2: 0, a3: 80225760, a4: 80225760, a5: 38, a6: 80225838, a7: 0, s0: 80025f30, s1: 1, s2: 80200000, s3: 0, s4: 87e00000, s5: 0, s6: 80006800, s7: 80020024, s8: 2000, s9: 800222f8, s10: 0, s11: 0, sp: 802257a8 }
scause=1で、仕様書見る限り、これはinstruction access faultらしい
switch.rsのアセンブリで、(a0)とすべきところを、(sp)にしていたのを直した
いろいろ直して、こんどはscause=7(store/amo address fault)で止まる
code:out
unexpected trap scause=7, stval=0, sepc=80202944
frame: TrapFrame { ra: 80202944, gp: 0, tp: 80026000, t0: 80205300, t1: 80201e8c, t2: 1000, t3: 616d6569, t4: 0, t5: 4, t6: 0, a0: 0, a1: 802052f0, a2: 0, a3: 0, a4: 0, a5: 0, a6: 0, a7: 1, s0: 80025f30, s1: 1, s2: 80200000, s3: 0, s4: 87e00000, s5: 0, s6: 80006800, s7: 80020024, s8: 2000, s9: 800222f8, s10: 0, s11: 0, sp: 80225998 }
qemuでメモリダンプ
code:out
(qemu) xp /20iw 0x80202944
0x80202944: 00152023 sw ra,0(a0)
0x80202948: 00252223 sw sp,4(a0)
....
switch_contextの最初のswで止まっていることがわかる
objdumpするcargo objdump -- --disassemble target/riscv32i-unknown-none-elf/debug/os-rust-1000 | less
code:out
80202904 <switch_context>:
80202904: 13 01 01 fd addi sp, sp, -48
80202908: 23 26 11 02 sw ra, 44(sp)
8020290c: 23 22 a1 02 sw a0, 36(sp)
80202910: 23 24 b1 02 sw a1, 40(sp)
80202914: 37 55 20 80 lui a0, 524805
80202918: 93 05 05 49 addi a1, a0, 1168
8020291c: 13 05 81 00 addi a0, sp, 8
80202920: 23 22 a1 00 sw a0, 4(sp)
80202924: 13 06 10 00 li a2, 1
80202928: 93 06 01 02 addi a3, sp, 32
8020292c: 13 07 00 00 li a4, 0
80202930: 97 00 00 00 auipc ra, 0
80202934: e7 80 00 9e jalr -1568(ra)
80202938: 03 25 41 00 lw a0, 4(sp)
8020293c: 97 f0 ff ff auipc ra, 1048575
80202940: e7 80 c0 2f jalr 764(ra)
80202944: 23 20 15 00 sw ra, 0(a0)
ごちゃごちゃ書いてあるが、一番下の行が止まったときのsp
その上にいろんなアセンブリが入っている
これ、おそらく関数開始時のアセンブリ(プロローグ??)が入っていることが原因な気が
参考コードは#[naked]がついていたが、自分のはついていない
naked使うため、nightlyにして実行してみる
10秒くらい遅れてパニックしたw
sepc=80202178だったので、メモリダンプしてみる(qemu) xp /10iw 0x80202178と、トラップの途中で止まることがわかった
objdumpしてみると、switch_contextは通過しているようだ
ここで、「コンテキストスイッチすると、レジスタ入れ替わるから、今書いているデバッグ意味なくね」ということに気づいた
まぁ、switch_contextは突破してるように見えるからいったん進む
その後、ProcessManagerにproc_a, proc_bを追加して、スイッチの実験をしようとしたが、ごちゃごちゃしてきたため、やめた
yieldに進むことにする
current_procが難しい問題
今の所、immutableなstruct ProcessManagerの中に、「プロセス、現在のプロセス、アイドルプロセス」を持っている
現在のプロセスだけ可変にして、それ以外は不変にしたい、と思っていた
が、borrow checkerと仲良くなれず、上手く実装できなかった(せっかくCellとかつかったのに、、)
現在のプロセスをCellにすればよくね?と思ったが、プロセスの構造体がCellを含んでいて、Cellが入れ子になった
このときに、下がでた。Cellの外にCellはおけなそう。。。
code:out
errorE0204: the trait core::marker::Copy cannot be implemented for this type --> src/process.rs:118:24
|
| ^^^^
...
121 | state: Cell<ProcessState>,
| ------------------------- this field does not implement core::marker::Copy
122 | sp: Cell<u32>,
| ------------- this field does not implement core::marker::Copy
123 | context: Cell<Context>,
| ---------------------- this field does not implement core::marker::Copy
|
そんなこんなで、borrow checkerに屈したのでstruct ProcessManagerをmutにする
proc_a_entryとかの関数ポインタ渡しが上手く行かない。。。
今は、ProcessManager内にproc_a_entryを入れていた(self.yield_processを使いたいため)が、上手く行かなかった
こんな記述でこんなエラー
code:err
let ptr_proc_a: unsafe fn(&mut ProcessManager) = ProcessManager::proc_a_entry;
self.create_process(ptr_proc_a);
~~~~~~~~~~~~~~~~~~~~~~~~~
errorE0308: mismatched types --> src/process.rs:47:25
|
47 | self.create_process(ptr_proc_a);
| -------------- ^^^^^^^^^^ incorrect number of function parameters
| |
| arguments to this method are incorrect
|
= note: expected fn pointer unsafe fn()
found fn pointer unsafe for<'a> fn(&'a mut ProcessManager)
そもそも、プロセスに渡す関数ポインタを、ProcessManager内に書かなくてはならないというのは間違っている気がするので、main.rsに書く形に変更することを検討
今、yieldはProcessManagerの中に入っている。これはProcessManager内の変数を使いたいためだった。
そのため、yieldしたいときは、ProcessManagerの参照が必要になる。これでは、生み出すプロセスの関数に実装都合の参照引数(しかもmut)が増えることになる。アプリ的にはよくない気がする。
いままで、ProcessManagerにこだわってみたが、現状の自分の実力的にこれは取っ払うほうがいい気がする
static mutなどが生まれそうだが、まぁしょうがない、、、
ということで、ProcessManagerを解除する方向でいく
ゴニョゴニョ実装していたら、こんなことが起きた
code:process.rs
~~~~~~~~~~~~~
errorE0015: cannot call non-const fn Process::new in statics --> src/process.rs:25:52
|
| ^^^^^^^^^^^^^^^^
スタックポインタをProcess構造体に入れていたが、これは消して、Context構造体のものを利用することにした
スタックポインタを設定するとき、スタック(構造体内にある配列)の一番後ろを入れるべきところを、先頭(配列のアドレス)を入れてしまい、大変だった
putcharをprintlnに変えると、プロセスのpidが書き換わったり、プロセスのstateが書き換わったりなどが起きていて怖いなと思った
なにはともあれ、実装できてよかった
12. page table
とりあえず、ページ構造体をちゃんと並べられるか確認する
code:main.rs
let x = Box::new(Page::new());
let y = Box::new(Page::new());
let z = Box::new(Page::new());
println!("raw: {:?}", Box::into_raw(x));
println!("raw: {:?}", Box::into_raw(y));
println!("raw: {:?}", Box::into_raw(z));
~~~~~~~~~~~~~~~~~~~
raw: 0x80226000
raw: 0x80227000
raw: 0x80228000
into_rawすると、ヒープ領域のアドレスを見ることができる。ページサイズ4096で並べられていそう
ページの「エントリ」という単語がよく出てくるが、これの定義は?
ページテーブルはオペレーティングシステムがこの対応付けを保存しておくところで、それぞれの対応付けはページテーブルエントリー(page table entry(PTE))と呼ばれる。
ページテーブルは?
ページングの考え方は、仮想メモリ空間と物理メモリ空間の両方を、サイズの固定された小さなブロックに分割するというものです。仮想メモリ空間のブロックは ページ と呼ばれ、物理アドレス空間のブロックは フレーム と呼ばれます。
なるほど、仮想メモリ物理メモリともに区切って、それらを対応つけるのがページテーブルの役目か
プロセスごとに仮想アドレスを分けるため、プロセス構造体にページテーブルを入れる必要がある
PageTableの中に、一段目のページテーブルを保管していたが、ページの確保はBoxでやることにしていた
このままでは、構造体をnewした時にスタックに積まれることになるため、よくなさそう
なので、PageTableには、一段目のページテーブルの物理アドレスを渡すように変更したほうがよさそう
octoxでは、PageTableにアロケータを実装していたが、今回はそれは見送ることにする
code:err
panicked at library/alloc/src/alloc.rs:411:13:
memory allocation of 4096 bytes failed
kernel_base=0x80200000 , free_ram_end=0x84239000 なので、これは正しそう
alloc_page関数の中にprintを入れてみると、超沢山出力された....どゆこと
沢山出力されたのは、map_page関数でalloc_pageが沢山呼ばれているから
alloc_page内では、Boxでメモリを確保しているが、このアドレスをprintしてみたら、ずっと0x80235ffcだった
表示するべきアドレスを間違えていた。Box::into_rawで取得したポインタのアドレスではなく、newしたときのアドレスを表示していた。
4KBごとになっていたので、確保はできていそう
create_process関数内のmut paddrが正しく変更できていることがprintして確認する
今の所、free_ram_end=0x84239000だが、mut paddrは0x802b0000で止まっていることが分かった
このprintを外すと、memory allocationのパニックがでていたため、