Desarrollar y descargar software de código abierto

View カーネルページ例外の処理に関するメモ

category(Tag) tree

file info

category(Tag)
開発メモ
file name
memo-devel-kernel-fault
la última actualización
2006-09-28 17:37
tipo
HTML
editor
okasaka
descripción
es-0.0.3で実装するカーネルページ例外の処理の実装に関するメモ。
idioma
Japanese
translate

カーネルページ例外の処理に関するメモ

es-0.0.3で実装するカーネルページ例外の処理の実装に関するメモ。


カーネル例外とユーザー例外の見分け方

void Core::dispatchException(Ureg* ureg)において、ureg->cs == KCODESEL ならカーネル例外、そうでなければ(ureg->cs == UCODESEL)ユーザー例外である。

特権レベル変更がないとき(カーネル例外)

eflags, cs, eip の順に詰まれる。

特権レベル変更があるとき(ユーザー例外)

ss, esp, eflags, cs, eip の順に詰まれる。

不正なユーザープロセスの処理

void Core::dispatchException(Ureg* ureg)から呼び出した関数がエラーを発生させた場合、エラーを起こしたユーザープロセスを終了する。

不正なユーザープロセスの終了のさせ方

基本的にはexitしてしまえばよい。これをユーザーレベルでトラップとして拾いたい場合には、カーネルからアップコールすることになる。この場合、IRuntime経由でトラップハンドラを登録できるようになっていると良いだろう。そのとき、例外を起こした命令の続きから実行を再開するようなユーザーコードを書きたいはず。それにはuregを引数でもらい、アップコールからの戻りでその内容を書き換え、カーネルがそのuregから実行を再開できるような枠組みがあればよいだろう

Plan 9ならnofity, notedあたりの仕組みと同じ。

つまり、トラップハンドラが登録されていればそれをアップコールする。そうでなければ、exitするというのが基本的な枠組み。またハンドラ中でさらに例外を起こしたときの動作はkill(exit)でよい。

このあたりの実装は0.0.4以降で行う。

カーネルページ例外

カーネルページ例外とは、カーネルが処理中に起こしたページ例外のこと。

システムコールパスはユーザーから指定されたポインタ引数について、それがユーザーアドレス空間内に収まっているかどうかについてはチェックをかけているか、そこに本当にメモリがマップされているかどうかについてはオーバーヘッドが大きいのでチェックしていない。そのため、メモリオブジェクトがマップされていない範囲を指すポインタをユーザープロセスがシステムコールの引数に渡すと、カーネルページ例外が発生する。

したがって、ureg->cs == KCODESEL でかつ isValid(cr2) なら、カーネルページ例外と考えられる。ただしカーネル自体にバグがあれば、その限りでは本当はないわけだが。

ユーザープロセスから不正な引数を渡されたためにカーネルページ例外が発生した場合、これはカーネル例外ではあるけれども直ちにカーネルをパニックさせてはいけない。カーネルページ例外ハンドラの中からEFAULTシステム例外を起こせるようになっていると本当は都合がよい。

具体的な実装方法の検討

カーネルページ例外を検出したときに、ただ単に throw SystemException<EFAULT>();と書いても、throwしていのるはカーネルページ例外ハンドラで、実際に例外を起こした関数とはスタックのチェインの繋がりがないので、それがシステムコールパスで処理している関数にスローされることはない。

カーネルページ例外ハンドラがカーネルページ例外を検出したら、現スレッドにそのことを記録し、システムコール側でそのマークをチェックして立っていれば例外をスローするというような仕組みは実現が可能である。ただし、アクティブにマークを毎回チェックしに行く仕組みは実装を間違えやすいな感がある。またx86のようなCISCだと、ページ例外を起こした次の命令から実行を再開させるコードを用意するのも面倒だろう。

そこで、カーネルページ例外が発生したときにはスクラッチページを該当領域にマップしてしまい、システムコールの処理を進められるだけ進めてしまって、適当なチェックポイントでEFAULT例外をスローするという方法も考えられる。ただし、このとき、無駄なアップコールプロクシを作ってしまったりすることがないように注意しておく必要がある。この方法も標準のC++の範疇で実装が可能ではあるけれでも、たとえば、スクラッチページをマップ中に別スレッドもそのページを参照してしまってそちらはEFAULTが発生することなく処理を完了してしまったりするといった不都合な点がある。

なにかいい方法はないのか?

1. ユーザー空間にアクセスする前にラベルをセットしておいて、カーネルページ例外が起きたときにはそこにジャンプしてしまうという方法もある。この場合は、デストラクタなどが正しく呼び出されれないことになるのでC++ベースで記述しているesでは採用しがたい。

Linuxカーネルの2.4以降はこれに近い方法でラベルではなくてeipのテーブルをあらかじめ作ってしまっておく。

2. C++の標準ABIを使って直接例外をスローする?

これはehの環境ではupcallのときと同様なかなか難しい:

  • 勝手にスタックフレームを作って例外をスローしてもそれはキャッチされない。
  • キャッチされるためには関数呼び出しとして繋がっている必要がある。
  • C++の例外はそもそも同期型。

MSの構造化例外のようなものがほしい。

3. あきらめてシステムコールパスで完全なチェックを行う。

パフォーマンスに対するオーバーヘッドが大きくなってしまう。

4. 1.とthrowの組み合わせ。

if (setjmp(label) == 0)
{
	copy-in, copy-out;
}
else
{
	throw(EFAULT);
}

のような。これは動く。ただしユーザー空間へのアクセスをカプセル化しておかないといけない。

5. g++拡張を探る

コンパイラオプションを調べてみると以下のようなものがあることが分かった:

-fasynchronous-unwind-tables
-fnon-call-exceptions

実験してみる。

#include <stdio.h>

void foo()
{
    throw 10;
}

int main()
{
    try
    {
        __asm__ __volatile__ (
            "call   *%0"
            :: "a"(foo));
    }
    catch (int n)
    {
        printf("%d\n", n);
    }
}

これが動けばハッピー。でもこれはだめ。

#include <stdio.h>

void foo()
{
    throw 10;
}

void bar()
{
    __asm__ __volatile__ (
        "call   *%0"
        :: "a"(foo));
}

int main()
{
    try
    {
        bar();
    }
    catch (int n)
    {
        printf("%d\n", n);
    }
}

これは

$ g++ -fnon-call-exceptions non-call-exc.cpp

と-fnon-call-exceptionsオプションをつけてコンパイルすると

10

と表示される。ちなみに-fnon-call-exceptionsオプションなしだと

terminate called after throwing an instance of 'int'
Aborted

と表示される。

つまりユーザー空間にアクセスする部分を最低限関数にしておけば、これでもEFAULTを例外として拾うことができそうということ。ベストではないけれども、不可でもない。

前者のコードはなぜだめなのか?

asmの部分には対応したehテーブルが生成されない様子。

-fnon-call-exceptionsオプションを使った実装

カーネルページ例外が起きたら、uregを見て、EFAULTシステム例外をスローするようにスタックフレームを構築しloadする。

実験してみる。

            if (ureg->cs == KCODESEL && core->currentProc->isValid((void*) cr2, 1))
            {
                // Kernel page fault
                esReport("Kernel page fault %p\n", cr2);
                throw SystemException<EFAULT>();
            }

これはさすがにだめ。

そこで、iret後にちょうどEFAULTをスローする関数(kernelFault)が呼び出されたかのようにスタックフレームが構成されるようにuregをずらして値を設定する。

            if (ureg->cs == KCODESEL && core->currentProc->isValid((void*) cr2, 1))
            {
                // Kernel page fault
                esReport("Kernel page fault %p\n", cr2);
                Ureg* exc = (Ureg*) ((char*) ureg - sizeof(u32));
                memmove(exc, ureg, sizeof(Ureg) - 8);
                u32 ret = exc->eip;
                exc->esp = ret;
                exc->eip = (u32) kernelFault;
                exc->load();
                break;
            }

これは予想通りいけた。

カーネル内部で

struct Foo
{
    Foo()
    {
        esReport("Foo: Constructed.\n");
    }

    ~Foo()
    {
        esReport("Foo: Destructed.\n");
    }
};

{
    Foo foo;
    int* ptr = (void*) 0x8000;
    *ptr = 1;
}

というようなコードを書いた場合、*ptr = 1;をtryで囲んで例外をキャッチすることはできないもののFooのデストラクタは正しく呼び出されるので例外に対して安全なコードであれば特に細かいことを気にせず使っていけそうなことは確認できた。