[Rust] ラズパイ2でベアメタルコンテキストスイッチ


# はじめに
ラズパイ2上でRustを使ってコンテキストスイッチが動作するようになったので仕組みや不明点、課題を記載する。

リポジトリは以下
https://github.com/osdev-rs/minimal-kernel-rpi2/tree/task

例によって「やってみた」系の記事なので多くは望まないで欲しい

# IRQハンドラ

コンテキストスイッチ処理の入口となるIRQハンドラから見ていく。

```nasm:boot.S
irq_asm_handler:
        sub lr, lr, #4
        stmfd sp!, {lr}
        stmfd sp!, {r0-r12}

        bl irq_handler
        teq r0, #1
        mov r0, sp
        bleq demo_context_switch

        ldmfd sp!, {r0-r12, pc}^
```

`irq_handler()`を実行して1が返ってきたら現在のsp(スタックポインタ)を引数にして`demo_context_switch()`を実行する。

現状ではタイマ（１秒毎）とUARTの割込みが発生しており、タイマ割込みの場合`irq_handler()`が1を返すようになっている。

余談：
lrを壊してしまっているが現状のコードではIRQハンドラ内で再割り込みは許可せず、IRQハンドラからリターンするときのCPSR上書き(後述)で自動的に割込み許可となるため問題はない（はず）。将来的に再割り込みを許可する際には修正する必要があると思われる。

## CPSRと`ldmfd`の`^`について

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0802bj/Cihcadda.html

CPSRはゼロフラグやキャリーフラグ等現在の演算状態を保持するレジスタ。

IRQハンドラが呼び出されると[プロセッサモード](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0204ij/Chdddhea.html)はIRQモードとなり、元のモードのCPSRはSPSRへ保存される。
`ldmfd`の`^`はレジスタリスト内に`pc`があるときSPSRをCPSRに書き戻す処理をおこなう。
CPSRはプロセッサモードを決めるフラグも持つためこれにより、IRQハンドラからリターンすると共に元のプロセッサモードに戻ることになる。

なおSPSRからCPSRへの書き戻しは[`subs pc, lr, #4`でも行われる](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0204ij/Cjaiacfg.html)。

## プロセッサモードとレジスタ

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0473lj/dom1359731128950_00010.html

![Screenshot from 2019-01-30 14-39-38.png](https://qiita-image-store.s3.amazonaws.com/0/39647/8fb137c0-ec44-d708-b04d-466614286a40.png)

上記のようにプロセッサモード毎に共有するレジスタと別に持つレジスタがある。
今回のコードではタスクはUserモードで動作し、その他IRQモードとSystemモードが登場する。
IRQモードはUserモードとr0-r12は共有するのに対してlr,spは別に持つ。
対してSystemモードはUserモードとr0-r12,lr,spを共有する。

~~なお、SP_irqのように書いてあるが、アセンブラ上で`sp_irq`と書けるわけではなく、IRQモードで`sp`はSP_irqを表すという意味である。~~ (`MRS`,`MSR`を使えばアクセス可能？)

# 今回の設計

１秒毎（タイマ割込み毎）に２つのタスクを交互に切り替える。

コンテキストスイッチの手順はこうなる

1. IRQハンドラの冒頭でr0-r12,lrをspへ退避(上記「IRQハンドラ」参照)
2. (タスク切り替え処理では)spに退避したレジスタとSPSRを切替元タスクのTCB(タスクコントロールブロック(後述))に保存
3. spの同じ領域を切替先タスクのTCBに保存してあるレジスタで書き換え、SPSRもそのTCBに保存してあるSPSRに書き換える
4. 上記の書き換えによりIRQハンドラから`ldmfd sp!, {r0-r12, pc}^`で返る際に切替先タスクに返る

C言語でコンテキストスイッチ書く場合は呼び出した関数の先で適当にsp調整していきなり`ldmfd sp!, {r0-r12, pc}^`を呼び出すみたいな荒業も見かけるが、Rustだとオブジェクトの後処理が動かなかったり等明らかに問題があるので少なくともIRQハンドラへ戻ってからコンテキストスイッチが走るようにする必要がある。

# TCB(タスクコントロールブロック)

TCBは中断されたタスクが再開できるように各レジスタ値を保存しておく領域である。

## TCBの定義

```rust:task.rs
pub struct Tcb {
    stack_addr: *mut u8,
    stack_size: usize,
    sp: *mut u32,
    r: [u32; 13],
    lr: *mut u8,
    pc: *mut u8,
    cpsr: u32,
}

unsafe impl Send for Tcb {}
unsafe impl Sync for Tcb {}

lazy_static! {
    static ref TCBS: Mutex<Vec<Tcb>> = {
        Mutex::new(Vec::new())
    };
}

static mut current_task:usize = 0;
```

今回のTCBではsp,r0-r12,lr,pc,cpsrを保存する。

タスク切替のためにTCBのリストを[lazy_static](https://github.com/rust-lang-nursery/lazy-static.rs)で保持するが、[生ポインタを持っているとthread safetyについて怒られてしまう](https://doc.rust-lang.org/nomicon/send-and-sync.html)ため、`unsafe impl`で`Send`と`Sync`を実装して回避している。

`#[no_std]`でも使用できる[spin](https://github.com/mvdnes/spin-rs)の`spin::Mutex`を使用している。

`current_task`は今１番目のタスクか２番めのタスクかを記憶するために使用する。

なお、lazy_staticは

```toml:cargo.toml
[dependencies]
lazy_static = {version = "1.2.0", features = ["spin_no_std"]}
```

このように指定することで`#[no_std]`での使用が可能。

## TCBの初期化

```rust:task.rs
fn task_exit() {
    uart::write(&format!("task_exit\n"));
    loop {}
}

const STACK_SIZE: usize = 4096;

impl Tcb {
    pub fn new(entry: unsafe extern "C" fn()) -> Tcb {
        unsafe {
            let addr = alloc::alloc::alloc(Layout::new::<[u8; STACK_SIZE]>());
            let sp = (addr.offset(STACK_SIZE as isize) as u32 & !(7u32)) as *mut u32;
            Tcb {
                stack_addr: addr,
                stack_size: STACK_SIZE,
                sp: sp,
                r: [0xdeadbeef; 13],
                lr: task_exit as *mut u8,
                pc: entry as *mut u8,
                cpsr: 0x10, /* User mode */
            }
        }
    }
}
```

`alloc::alloc::alloc()`により自前のアロケータ（TODO: そのうち記事を書く）でスタックメモリを確保している。
タスクのエントリポイントは引数で渡されてくる関数とし、アドレスをpcにセットする。
CPSRにはUserモードで動作させるため0x10をセットしておく。

今回はタスクが終了することはないが終了先としてlrに`task_exit()`を指定している。（4バイトずらしておくべきだろうか？）


# コンテキストスイッチ

task.rsの`demo_context_switch()`の処理について解説していく。

## 現タスク情報のTCBへの退避

`demo_context_switch()`の引数には、現タスクのレジスタ(r0-r12,pc)をIRQハンドラの冒頭でスタックに退避した際のspが渡されてくるので、TCBにそのspから現タスクのレジスタを取得して保存する。また、現タスクのCPSR(IRQモードではSPSRに退避されている)も保存する。

```rust:task.rs
        {
            let mut sp = sp;
            for i in 0..13 {
                (*tcbs)[current].r[i] = *sp;
                sp = sp.offset(1);
            }
            (*tcbs)[current].pc = *sp as *mut u8;

            let mut cpsr = 0u32;
            asm!("mrs $0, spsr" : "=r"(cpsr));
            (*tcbs)[current].cpsr = cpsr;
        }
```

`cps #31`でSystemモードへ移行して、現タスクのlr,spを取得(SystemモードはUserモードとlr,spを共有しているため)し`cps #18`でIRQモードへ復帰している。

`cps #31`ではspが変わってしまうため、IRQモードに復帰するまでRustのコードは実行されないようにする必要がある。

```rust:task.rs
        let mut lr_tmp: u32 = 0;
        let mut sp_tmp: u32 = 0;
        asm!("cps #31
              mov $0, lr
              mov $1, sp
              cps #18" : "=r"(lr_tmp), "=r"(sp_tmp));
        (*tcbs)[current].lr = lr_tmp as *mut u8;
        (*tcbs)[current].sp = sp_tmp as *mut u32;
```

## 次タスク情報のSPへの上書き

次タスクのr0-r12,pcをスタックへ上書きし、CPSRもSPSRへ上書きする。
`spsr_cxsf`の後ろの`_cxsf`は４文字それぞれがSPSRの中でどの領域に書き込むかの指定であり、SPSR全体を書き換える場合は`_cxsf`を指定しておけば問題ない。

```rust:task.rs
        {
            let mut sp = sp;
            for i in 0..13 {
                *sp = (*tcbs)[next].r[i];
                sp = sp.offset(1);
            }
            *sp = (*tcbs)[next].pc as u32;

            let cpsr = (*tcbs)[next].cpsr;
            asm!("msr spsr_cxsf, $0" :: "r"(cpsr));
        }
```

取得したときと同様にプロセッサモードを変えながらlr,spを上書きする。

```rust:task.rs
        asm!("cps #31
              mov lr, $0
              mov sp, $1
              cps #18" ::"r"((*tcbs)[next].lr), "r"((*tcbs)[next].sp));
```

あとはIRQハンドラから復帰すればコンテキストスイッチが行われる。

## 課題

コンテキストスイッチを動作させるためSystemモードでのlrの退避と復帰がなぜ必要なのかわかっていないので調べる必要がある。（当初lrの退避・復帰が実装されておらずコンテキストスイッチが動作してくれなかった）

# 最初のタスク起動

基本的にはコンテキストスイッチ処理と変わりはない

```rust:task.rs
#[no_mangle]
extern "C" fn demo_setup_switch(sp: *mut u32) {
    unsafe {
        let tcbs = (*TCBS).lock();
        {
            let mut sp = sp;
            for i in 0..13 {
                *sp = (*tcbs)[current_task].r[i];
                sp = sp.offset(1);
            }
            *sp = (*tcbs)[current_task].pc as u32;

            let cpsr = (*tcbs)[current_task].cpsr;
            asm!("msr spsr_cxsf, $0" :: "r"(cpsr));
        }

        asm!("cps #31 @ SYSTEM MODE
             mov lr, $0
             mov sp, $1
             cps #19  @ SVC MODE"
             ::"r"((*tcbs)[current_task].lr), "r"((*tcbs)[current_task].sp));
    }
}

pub fn demo_start() {
    unsafe {
        {
            let mut tcbs = (*TCBS).lock();
            (*tcbs).push(Tcb::new(entry1));
            (*tcbs).push(Tcb::new(entry2));

        }

        asm!(
            "stmfd sp!, {r0-r12, lr}
              mov r0, sp

              bl demo_setup_switch
              ldmfd sp!, {r0-r12, pc}^"
        );
    }
}
```

`demo_start()`で最初のタスクが起動される。
spを`demo_setup_switch()`に渡して、最初のタスクのレジスタ値で上書きする。pcはタスクのエントリポイントを指定し、SPSRも上書きする。
Systemモードにてlr,spを上書きして`ldmfd sp!, {r0-r12, pc}^`で最初のタスクが起動される。

この処理はSVCモードで動作させているため、Systemモードの後戻るプロセッサモードはIRQモード(`cps #18`)ではなくSVCモード(`cps #19`)になっている点が異なっている。