ユーザーランドでコンテキストスイッチ的なものを作ってみた
xv6のソースコードを読むと、実行プロセスを切り替える過程は以下のように行われていることがわかります。
- タイマー割り込みによってプロセスのプログラムを中断して割り込みハンドラが実行される。
- 割り込みハンドラの最後で、割り込みハンドラからカーネルのスケジューラーに向けてコンテキストスイッチ
- スケジューラーは次に実行されるプロセスを選択したら、ページテーブルをそのプロセスのものに変える
- スケジューラーからプロセスにコンテキストスイッチ
ちなみにどのプロセスのページテーブルにもカーネルが同じ位置にマップされているためページテーブルを切り替えても、ページテーブルを切り替えてもカーネルの関数は問題なく実行されます。また、カーネル部分のページは特権レベルが違うので、ユーザープロセスからカーネルの変数を参照することはできません。(実際やろうとするとページフォルトが発生します。)
それで、ここからが本題なのですが、ここで言ってるコンテキストスイッチはxv6のswtch関数のことですが、これが基本的にレジスタをスタックに退避してスタックを切り替えるというかなりシンプルなものなんです。
カーネルモードでしか使えないような命令が無いので、ユーザーランドでもできるのでは無いかと思い、ユーザーランドでswtch関数を実行するプログラムを試作してみました。
それが、こちらです。
ざっくり内容を説明すると、作った関数は二つ、swtchとmkcontextです。
swtchは第一引数にスイッチ元のcontext構造体へのポインタのポインタを受け取り、第二引数にスイッチ先のcontext構造体へのポインタを受け取るようになっており、レジスタをプッシュしていくことでスイッチ元のスタックにcontext構造体を作って、そこへのポインタを第一引数でポインタが渡されているcontext構造体へのポインタ変数に保存することでコンテキストを保存し、第二引数で与えられたcontext構造体(こちらもスイッチ元とは別のスタックのトップにある。)からスイッチ先のコンテキストを復帰します。
swtchのコードはこんな感じです。
swtch:
pushq %rax
pushq %rcx
pushq %rdx
pushq %rbx
pushq %rbp
pushq %rsi
pushq %rdi
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %r12
pushq %r13
pushq %r14
pushq %r15
pushfq
movq %rsp,(%rdi)
movq %rsi,%rsp
popfq
popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8
popq %rdi
popq %rsi
popq %rbp
popq %rbx
popq %rdx
popq %rcx
popq %rax
retq
mkcontextは与えられた関数を実行するコンテキストを与えられたスタック上に作ります。中でやってることは与えられたスタック上にアライメントに注意しながらcontext構造体を作成し、必要なレジスタを設定してそのスタックのトップを返すというものです。
mkcontextのコードはこんな感じです。
struct context* mkcontext(void (*func)(),void* stack,int stacksize){
//与えられた関数を実行するコンテキストを作成する。
struct context* c;
void* sp = stack;
sp += stacksize;
sp = (void*)((QWORD)sp - (QWORD)sp % 0x10); //スタックのアライメント調整
sp -= sizeof(struct context)+0x8; //コンテキスト保存用の領域を開ける。(アライメント調整のため0x8を足している)
c = (struct context*)sp;
c->rip = (QWORD)func;
c->rbp = (QWORD)stack + stacksize; //一応ベースポインタをスタックの底にセットしてみる
c->rbp -= c->rbp % 0x10;
return c;
}
デモではmain関数でmkcontextを使ってtest関数を実行するコンテキストを作り、その後、main関数は変数をインクリメントしてその値を表示しつつ、swtch関数を実行する無限ループに入ります。また、同様の処理をtest関数でも行います。実行すると以下のようにmainとtestの出力が交互に出ます。
コード
#include<stdio.h>
#include"swtch.h"
struct context *Cmain,*Ctest;
void test(){
int i=0;
while(1){
printf("test:%d\n",i);
i++;
swtch(&Ctest,Cmain);
}
}
int main(){
int i=0;
char stack[0x1000];
Ctest = mkcontext(test,stack,0x1000);
while(1){
printf("main:%d\n",i);
i++;
swtch(&Cmain,Ctest);
}
return 0;
}
実行結果
main:0 test:0 main:1 test:1 main:2 test:2 main:3 test:3 main:4 test:4 ...
普通に実行したらSEGVするのにデバッガで実行したらSEGVしないorz
— とみとみ (@gtomitat) 2018年3月17日
ちなみに、この悲鳴もといツイートはこのデモを作った時のもので、test関数のコンテキスト用のスタックが足りず、オーバーフローしているのが原因のようでした。
「1000バイトもあれば十分だろ」とか思ってたら甘かった。printfがめちゃくちゃスタックを食うということがわかりました。