Rustで"Writing an OSin 1,000 Lines"
とりあえずcontext switchまでできた。
参考になる資料は以下。
「OS」以外にも「ベアメタル」で検索すると良い。
gdbでデバッグするには、まずgdbをall targetでビルドする。
make installしたらrun.shでQEMU起動時に-s -S
を渡す。ここで起動するOSのバイナリはdebugビルドで最適化も(できるだけ)かけないようにしておくと楽。
gdbを起動して、target remote localhost:1234をして、その後file <osのバイナリのpath>するとsymbolが読み込まれる。
OSのコードの方でswitch_contextをno_mangleしておくとbreakpointを貼るのが楽。
自分はswitch_contextが数日間うまく動かなかったが、その一番の理由はspのアドレスではなくspを渡していたためだった。
sp自体stack pointerなので見誤ったが、元のCのコードでも引数で渡されているのはspのアドレスである。
switch_context(&proc_a->sp, &proc_b->sp);
ページテーブルについて
ここではbitshiftを多用する。
この際overflow/underflowに注意。
特にpointerを*mut u8
等で扱っているせいでu8
への値のキャストが含まれると、そこで値が変わってしまう。
基本的にはu32
を使う。例えばpage_table
は
let layout = Layout::from_size_align(4096, 4096).unwrap();
let page_table;
unsafe {
page_table = alloc::alloc::alloc_zeroed(layout);
};
let mut paddr = unsafe { &__kernel_base as *const u8 };
while paddr < unsafe { &__free_ram_end as *const u8 } {
map_page(
page_table as *mut u32,
paddr as u32,
paddr as u32,
(PAGE_R | PAGE_W | PAGE_X) as u32,
);
unsafe {
paddr = paddr.add(PAGE_SIZE as usize);
}
}
のように使う。
アプリ・ユーザーランド
objファイルをcargoでlinkする方法はわからなかった。
結局include_bytes!
でお茶を濁している。
let bin_shell = include_bytes!("shell.bin");
create_processの改変部分はこんな感じ
let mut off = 0;
while off < image.len() {
let page;
unsafe {
page = alloc::alloc::alloc_zeroed(layout);
ptr::copy_nonoverlapping(image.as_ptr().add(off), page, PAGE_SIZE);
};
map_page(
page_table as *mut u32,
(USER_BASE + off) as u32,
page as u32,
(PAGE_U | PAGE_R | PAGE_W | PAGE_X) as u32,
);
off += PAGE_SIZE;
}
システムコール
特に難しいところはない。シェルのmain関数はこう。
#[no_mangle]
extern "C" fn main() {
loop {
let mut cmdline = [0; 128];
let mut goto_here = false;
print!("> ");
let mut count = 0;
loop {
let ch = getchar();
putchar(ch);
if count == 127 {
println!("command line too long");
goto_here = true;
break;
} else if ch == (b'\r' as usize) {
break;
} else {
cmdline[count] = ch as u8;
count += 1;
continue;
}
}
if goto_here {
break;
}
if &cmdline[..count] == b"hello" {
println!("Hello world from shell!");
} else if &cmdline[..count] == b"exit" {
exit();
} else {
println!("unknown command: {:?}", &cmdline[..count]);
}
}
}
ディスクの読み書き
かなり難しい。
まず構造体は例の如くpacked
#[repr(packed)]
struct VirtqDesc {
addr: u64,
len: u32,
flags: u16,
next: u16,
}
問題はメンバ変数をalignするやつ
struct virtio_virtq {
struct virtq_desc descs[VIRTQ_ENTRY_NUM];
struct virtq_avail avail;
struct virtq_used used __attribute__((aligned(PAGE_SIZE)));
int queue_index;
volatile uint16_t *used_index;
uint16_t last_used_index;
} __attribute__((packed));
これはRustではpaddingするしかないという理解。
const size_of_u16: usize = core::mem::size_of::<u16>();
const size_of_u32: usize = core::mem::size_of::<u32>();
const size_of_u64: usize = core::mem::size_of::<u64>();
const SIZE: usize = (size_of_u64 + size_of_u16 + size_of_u32 + size_of_u16) * VIRTQ_ENTRY_NUM
+ (size_of_u16 + size_of_u16 + size_of_u16 * VIRTQ_ENTRY_NUM);
#[repr(packed)]
struct VirtioVirtq {
descs: [VirtqDesc; VIRTQ_ENTRY_NUM],
avail: VirtqAvail,
_padding: [u8; (PAGE_SIZE - (SIZE % PAGE_SIZE)) / core::mem::size_of::<u8>()],
used: VirtqUsed,
queue_index: i32,
used_index: *const u16,
last_used_index: u16,
}
あとはこれとか
fn virtio_reg_read64(offset: u32) -> u64 {
let ret;
unsafe {
ret = ptr::read_volatile(ptr::from_exposed_addr::<u64>(
VIRTIO_BLK_PADDR + offset as usize,
));
}
ret
}
fn virtio_reg_write32(offset: u32, value: u32) {
unsafe {
ptr::write_volatile(
ptr::from_exposed_addr_mut::<u32>(VIRTIO_BLK_PADDR + offset as usize),
value,
);
}
}
__sync_synchronize()
相当は不明。お祈り"fence"している。
fn virtq_kick<'b>(vq: &'b mut VirtioVirtq, desc_index: u16) {
vq.avail.ring[(vq.avail.index as usize) % VIRTQ_ENTRY_NUM] = desc_index;
vq.avail.index += 1;
unsafe { asm!("fence") }
virtio_reg_write32(VIRTIO_REG_QUEUE_NOTIFY as u32, vq.queue_index as u32);
vq.las
VirtioVirtq
、正確には後ろのpaddingがないが今回は影響がない。
ファイルシステム
ここではsyscallに手を加える必要がある。具体的にはsyscallの引数を1つ増やす。
なぜかというと、ファイル名の長さをsyscall経由で渡す必要があるためである。
C言語の場合null終端文字列を渡すため(セキュリティを一旦忘れると)長さは不要でアドレスさえ渡せばstrcmpに突っ込めるが、Rustの場合そうもいかない。
残念ながら現状の実装はUBを踏んでいるっぽい。特にProcessManagerのメンバーのVecの長さが突然変わったりする。
これは原因がわかった。
ファイルシステムはこんな感じのstruct
struct FS {
files: [File; FILES_MAX],
disk: [u8; DISK_MAX_SIZE],
}
だが、syscallハンドラ内で使用するためstatic mutしている。
static mut FS_ = FS...
これ自体は問題ないのだが、当初OnceCellを使って後から初期化していた。しかしこれだとOverwriteして、同じく下でstatic mutで宣言しているProcessManagerを侵食する。侵食するタイミングは最適化なりCPUなりに左右されるため、突然ProcessManagerの持っているメンバーが書きかわっているように見えた。
最後にwritefileは、書き込むときにnull文字で終端する。
writefile(
b"./hello.txt".as_ptr() as *const u32,
b"./hello.txt".len(),
b"Hello from shell!\n\0".as_ptr() as *const u8,
19,
);