Tomitomi's blog

勉強したこと、やってみたことなどを書き連ねていく場

ユーザーランドでコンテキストスイッチ的なものを作ってみた

xv6のソースコードを読むと、実行プロセスを切り替える過程は以下のように行われていることがわかります。

  1. タイマー割り込みによってプロセスのプログラムを中断して割り込みハンドラが実行される。
  2. 割り込みハンドラの最後で、割り込みハンドラからカーネルのスケジューラーに向けてコンテキストスイッチ
  3. スケジューラーは次に実行されるプロセスを選択したら、ページテーブルをそのプロセスのものに変える
  4. スケジューラーからプロセスにコンテキストスイッチ

 

ちなみにどのプロセスのページテーブルにもカーネルが同じ位置にマップされているためページテーブルを切り替えても、ページテーブルを切り替えてもカーネルの関数は問題なく実行されます。また、カーネル部分のページは特権レベルが違うので、ユーザープロセスからカーネルの変数を参照することはできません。(実際やろうとするとページフォルトが発生します。)

それで、ここからが本題なのですが、ここで言ってるコンテキストスイッチはxv6のswtch関数のことですが、これが基本的にレジスタをスタックに退避してスタックを切り替えるというかなりシンプルなものなんです。

カーネルモードでしか使えないような命令が無いので、ユーザーランドでもできるのでは無いかと思い、ユーザーランドでswtch関数を実行するプログラムを試作してみました。

それが、こちらです。

github.com

ざっくり内容を説明すると、作った関数は二つ、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 
... 

ちなみに、この悲鳴もといツイートはこのデモを作った時のもので、test関数のコンテキスト用のスタックが足りず、オーバーフローしているのが原因のようでした。

「1000バイトもあれば十分だろ」とか思ってたら甘かった。printfがめちゃくちゃスタックを食うということがわかりました。