Open14

Rustで"Writing an OSin 1,000 Lines"

CatminusminusCatminusminus

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を貼るのが楽。

CatminusminusCatminusminus

自分はswitch_contextが数日間うまく動かなかったが、その一番の理由はspのアドレスではなくspを渡していたためだった。
sp自体stack pointerなので見誤ったが、元のCのコードでも引数で渡されているのはspのアドレスである。

        switch_context(&proc_a->sp, &proc_b->sp);
CatminusminusCatminusminus

ページテーブルについて

ここでは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);
            }
        }

のように使う。

CatminusminusCatminusminus

アプリ・ユーザーランド

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;
        }
CatminusminusCatminusminus

システムコール

特に難しいところはない。シェルの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]);
        }
    }
}
CatminusminusCatminusminus

ディスクの読み書き

かなり難しい。
まず構造体は例の如く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,
}
CatminusminusCatminusminus

あとはこれとか

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
CatminusminusCatminusminus

ファイルシステム

ここではsyscallに手を加える必要がある。具体的にはsyscallの引数を1つ増やす。
なぜかというと、ファイル名の長さをsyscall経由で渡す必要があるためである。
C言語の場合null終端文字列を渡すため(セキュリティを一旦忘れると)長さは不要でアドレスさえ渡せばstrcmpに突っ込めるが、Rustの場合そうもいかない。

CatminusminusCatminusminus

残念ながら現状の実装はUBを踏んでいるっぽい。特にProcessManagerのメンバーのVecの長さが突然変わったりする。

CatminusminusCatminusminus

これは原因がわかった。
ファイルシステムはこんな感じの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の持っているメンバーが書きかわっているように見えた。

CatminusminusCatminusminus

最後に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,
            );