keigo1216 / ketchup

raspberrypi 3A+用のOS
1 stars 0 forks source link

implement process #19

Closed keigo1216 closed 11 months ago

keigo1216 commented 11 months ago

プロセスとコンテキストスイッチの実装

ARMv8ではプログラムカウンタを直接設定することは不可能なので、分岐命令とかを駆使してプログラムカウンタを設定する必要がある(めんど)

レジスタ

Callee-savedレジスタ

X19からX28レジスタ 関数呼び出しを跨いで値を保持する必要がある場合、呼び出された関数がこれらのレジスタの値を保存し、復元する責任を持つ

FP(X29)

フレームポインタとして機能して、現在の関数のスタックポインタのベースアドレスを保持する これもコンテキストスイッチ時に保存する

LR(X30)

このレジスタにはret命令で戻るプログラムカウンタの値を保持している

実装の流れ

  1. スタック領域に生成するプロセスの先頭アドレス(これが後々プログラムカウンタになる)とX19からX30までの汎用レジスタの初期値0をプッシュする(create_process関数)。X30レジスタには, プログラムカウンタを切り替えるための関数(後で記述)の先頭アドレスを設定しておくことで、ret命令が発行された時に、その関数にジャンプする
    | PC    |
    |-------|
    | X19   |
    | X20   |
    | X21   |
    | ...   |
    | X30   |
  2. start_task関数でEL1のOSモードからEL0のユーザーモードへ切り替える. この時の戻りアドレスにspに入れておいたプロセスの先頭アドレスをelr_el1に入れ, eret命令を打つ
  3. switch_context関数では, まず現在動いているプロセスの汎用レジスタの値(X19からX30)までをprev_spが指しているスタック領域にプッシュする. その後, next_spが指しているスタック領域の先頭からX30, X29, ... X19をポップする。 ここで、 PCをポップしてしまうと、start_taskでプログラムカウンタが取得できなくなるので、PCの一個手前でストップする

ハマったところ

EL0権限のスタックポインタの初期値の設定

ARMはそれぞれの権限で、スタックポインタのレジスタを使い分けているので、それぞれで初期化してあげる必要がある これ忘れると、コンテキストスイッチした後のプロセス内で関数呼び出しすると、例外ハンドラに飛んでしまう

疑問点

新旧のスタックポインタの渡し方

参照渡しだと通ったが, 値渡しをするとプログラムがプロセスA→プロセスBは動いたが, プロセスB→プロセスAに帰る時に例外が出た(プロセスAとプロセスBを入れ替えても同じことが起きた)

動かないプログラム

switch_context((uint64_t *)proc_a->sp, (uint64_t *)proc_b->sp);

__attribute__((naked))
void switch_context (uint64_t *prev_sp, uint64_t *next_sp) {
    __asm__ __volatile__ (
        "stp x20, x19, [sp, #-16]!\n"
        "stp x22, x21, [sp, #-16]!\n"
        "stp x24, x23, [sp, #-16]!\n"
        "stp x26, x25, [sp, #-16]!\n"
        "stp x28, x27, [sp, #-16]!\n"
        "stp x30, x29, [sp, #-16]!\n"
        "mov x9, sp\n"
        "str x9, [x0]\n"
        "mov sp, x1\n"
        "ldp x30, x29, [sp], #16\n"
        "ldp x28, x27, [sp], #16\n"
        "ldp x26, x25, [sp], #16\n"
        "ldp x24, x23, [sp], #16\n"
        "ldp x22, x21, [sp], #16\n"
        "ldp x20, x19, [sp], #16\n"
        "ret\n"
    );
}

動くプログラム

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

__attribute__((naked))
void switch_context (uint64_t *prev_sp, uint64_t *next_sp) {
    __asm__ __volatile__ (
        "stp x20, x19, [sp, #-16]!\n"
        "stp x22, x21, [sp, #-16]!\n"
        "stp x24, x23, [sp, #-16]!\n"
        "stp x26, x25, [sp, #-16]!\n"
        "stp x28, x27, [sp, #-16]!\n"
        "stp x30, x29, [sp, #-16]!\n"
        "mov x9, sp\n"
        "str x9, [x0]\n"
        "ldr x9, [x1]\n"
        "mov sp, x9\n"
        "ldp x30, x29, [sp], #16\n"
        "ldp x28, x27, [sp], #16\n"
        "ldp x26, x25, [sp], #16\n"
        "ldp x24, x23, [sp], #16\n"
        "ldp x22, x21, [sp], #16\n"
        "ldp x20, x19, [sp], #16\n"
        "ret\n"
    );
}

考えたこと

スタックポインタの更新

switch_contextを実行した後は、ldp命令などのspに対して行った処理は引数のnext_spに反映されない イメージとしては

参考資料

https://tc.gts3.org/cs3210/2020/spring/lab/lab4.html https://developer.arm.com/documentation/102374/0101/Procedure-Call-Standard

keigo1216 commented 11 months ago

デバッグ

-D qemu.log -d in_asmqemuの起動時に指定してあげると、実行されたアセンブリコードを見ることができるので、幸せになれる

参考資料

https://msyksphinz.hatenablog.com/entry/2020/07/16/000000

keigo1216 commented 11 months ago

後で変更

まだユーザー空間(EL0レベルの領域)を実装していないので、コンテキストスイッチ終了後の新しいプロセスの実行はEL1レベルで行っている。 今後はユーザー空間で実行できるように変更する(コンテキストスイッチはシステムコールで行うから、これが戻る時にEL0にすればいいだけだから、変更しなくてもいいのかもしれない)