OSを自作してみる10 ~ソフトウェア割り込みを実装してみる~



今回はソフトウェア割り込み経由のシステムコール呼び出しを実装してみる。

※過去回の記事とソースコードは下記から入手できる
https://github.com/Shadow5523/osdev/blob/master/README.md
また、ここで使用するコードはGitHubからダウンロードできる。
https://github.com/Shadow5523/osdev/releases/tag/version0.9.0

目次

 
 

概要

前に「OSを自作してみる6 ~割り込み経由でのキー入力~」「OSを自作してみる5 ~IDT/PICの初期化~」の記事で割り込みの設定をし、キーボードの入力を割り込み経由で行うようにした。これはハードウェア割り込みといい、キーボードでキーを押す度にCPUへ割り込み信号 + INT命令が行きキー入力処理を行っている。こうすることによってハードウェアからの応答性が向上し、キーの入力漏れとかがなくなるからである。

割り込みはハードウェアからだけではなくソフトウェアからも呼び出すことが可能で、これをそのまま「ソフトウェア割り込み」といい、主にシステムコールを呼び出すときに使われる。Linuxでいえばwrite()やread()などがシステムコールで、キーボード同様に応答性が求められる処理で用いられる。また応答性だけではなく、ユーザモードでは実行できない命令をこのソフトウェア割り込みの後なら実行することができる。

また、x86のCPUではこの上記のソフトウェア割り込み以外にもシステムコールを呼び出す方法があり、32bitOSでは「sysenter/sysexit」、64bitOSでは「syscall/sysret」を使用する。これらを使用したシステムコールはこれからやるソフトウェア割り込みより高速にシステムコールを呼び出すことが可能で、おそらく今存在するOSのほとんどはこれを使用している。しかし、少し複雑な為ここではとりあえずint命令を用いたソフトウェア割り込みのやり方を紹介する。

 
 

システムコール呼び出し、int 0x80命令を送る

1.まずは、システムコールを呼び出した時の処理を記述していく。ここではsh_write()をシステムコールとして実装し、このsh_write()が呼び出されたらsystem_call()へ受け取った引数を渡すように記述する。
※sh_write()本体で行う処理についてはまた後の記事で記述する。

  • size_t sh_write(int, const void*, size_t);
  • // sh_libc/io.c

     
     
    2.次に「io.c」で使われる関数のプロトタイプを記述。またここでインクルードする「syscall.h」と「sysdep.h」は後ほど記述する。
    // sh_libc/include/io.h

     
     
    3.「io.h」と同じフォルダにsysdep.hを作成する。ここではシステムコールがら受け取った引数に応じて呼び出す関数を変えるマクロを記述する。こうすることで、引数の数によって呼び出す関数(system_call1 ~ 5)を変えることが可能になる。system_callの一番目の引数にはシステムコール番号を格納することで、呼び出し元のシステムコールを特定する。
    // sh_libc/include/sysdep.h

     
     
    4.system_call()の本体を記述する。この関数ではまずソフトウェア割り込みの信号であるint 0x80をCPUへ送り、レジスタに引数を格納していく。

  • uint32_t system_call(uint32_t syscall_number, uint32_t arg ・・・)
  • // sh_libc/sysdep.c

    asm volatileを使ってインラインアセンブラを記述している。1行目でint 0x80の命令をCPUへ送り、2行目で現在のEAXの値を変数retへ格納している。3行目では実際に受け取った引数をレジスタへ格納していて、関数によって格納する引数の数が異なる。”a”や”D”などは制約文字と呼ばれていてx86では以下のレジスタを指定していることになる。

    変数名 役割            
    a EAXレジスタ
    b EBXレジスタ
    c ECXレジスタ
    d EDXレジスタ
    D EDIレジスタ
    S ESIレジスタ

     
     

    IDTにソフトウェア割り込みを登録

    1.「idt.c」を開き、実際にint 0x80の信号を受けっとったときに実行したい関数を記述する。ここの所はキーボード割り込みで行ったこととほぼ同じ感じ。
    ただ、set_gate_desc()の最後の引数の所だけ違う値を使用しており、0x8fを使用する。(基本的にはソフトウェア割り込みはトラップゲートで行いたいため)
    // arch/idt.c

     
     
    2.idtに登録した「as_software_interrupt」が別の場所で宣言されているのでexternで宣言する。
    // include/idt.h

     
     
    3.as_software_interruptが呼び出された処理を「interrupt.s」に記述していく。単純にESPレジスタからEAXレジスタ(ESIからEAXまでは引数)をプッシュし、次にcallでシステムコールの本処理を書いた関数を記述する。
    呼び出し終わったらpopl命令で各種レジスタのデータをスタックから取り出し最後にiretl命令で割り込み処理を抜けるようにする。
    // kernel/interrupt.s

     
     

    システムコールの本処理

    1.まず、システムコール呼び出した時に、どのシステムコールなのかを判別するために定数を設定し判断できるようにしてみる。「syscallnum.h」を以下のように記述する。
    // include/syscallnum.h

     
     
    2.同じincludeフォルダに「syscall.h」を記述する。このファイルでは割り込み処理で呼び出されたときに、どのシステムコールなのかを判定させる処理をする関数のプロトタイプを記述する。
    // include/syscall.h

     
     
    3.「syscall.c」を作製する。将来、write()やread()のシステムコールを実装しようと思ってるけど、今回は呼び出し元のシステムコールを判別するところだけ記述する。

  • int32_t syscall_interrupt(uint32_t syscall_num, uint32_t arg1, uint32_t arg2, uint32_t arg3, uint32_t arg4, uint32_t arg5);
  • // kernel/syscall.c

     
     
    4.次にカーネル側から呼び出せるように「kernel.h」へ以下を追記。
    // include/kernel.h

     
     
    5.最後に、Makefileに新しく追加したファイルがコンパイルされるように追加する。以下の所を編集する。またmake cleanコマンドで消せるファイル(Linuxなどで自動生成される~付きのファイルなど)を追加した。

     
     

    実際に呼び出されてるか確認

    1.実際にシステムコールが呼び出されてるかをテストしてみる。まずは「kernel.c」へ以下を追記する。(場所はループの外ならどこでもOK)
    // kernel/kernel.c

     
     
    2.「syscall.c」でちゃんと引数などが渡されてるかを確認する為、以下のコードを追加する。

     
     
    3.コンパイルして実行し、以下のようにsh_write()に渡した引数が表示されれば成功。

    Leave a Reply

    Your email address will not be published. Required fields are marked *