MIPSシステムコール

この夏、 appplemacMIPSアセンブラーの学習に関する記事を公開しました。 特に、システムコールを生成するsyscallコマンドが考慮されました。 著者はMIPSアセンブラーの説明に焦点を当てており、私の意見では、このシステムコールが何であるかを詳細に説明しませんでした。 その瞬間、私はプロジェクトをMIPSアーキテクチャに移行することに従事し、割り込み、例外、およびシステムコールを処理しました。



コードがすでに作成され、デバッグされたので、MIPSのシステムコールメカニズムがどのように機能するかをより詳細に明らかにする記事を書くことにしました。 これは、アセンブラーに関する記事への追加と見なすことができます。



はじめに


まず、システムコールとは何か、なぜ必要なのかを理解する必要があります。

ウィキペディアには次の定義があります。

プログラミングおよびコンピューターエンジニアリングにおけるシステムコールは、オペレーティングシステムのカーネルに対するアプリケーションプログラムの魅力であり、操作を実行します。

プログラマーの観点から見ると、システムコールは通常、システムライブラリからのサブルーチンまたは関数呼び出しのように見えます。 ただし、このような関数またはサブルーチンを呼び出す特殊なケースとしてのシステムコールは、システムライブラリへのより一般的な呼び出しと区別する必要があります。後者は特権操作を必要としない場合があるためです。




言い換えれば、システムコールは、所定のアドレスを持つ関数コールであり、同時にプロセッサを特権モード(カーネルモード)から転送します。 カーネルモードに切り替えると、仮想メモリテーブルの管理、割り込みの禁止/許可、カーネルに保存されたデータへのアクセスなどの特権コマンドを実行できます。

既知のアドレスとは、すべての処理機能をポインターの配列で表すことができ、この配列のインデックスがこのハンドラーに対応することを意味します。 システムコールと関数コールの違いは、ベースアドレス+オフセットへの制御がハードウェア、プロセッサ自体によって送信されることです。



つまり、システムコールを生成する命令を満たしたプロセッサは、ユーザープログラムコマンドの順次実行を中断し、メインプログラムに戻るために必要な情報を保存しながら、制御を目的のアドレスに転送します。 これは、例外または外部割り込みが発生したときのプロセッサの動作に非常に似ているため、通常、これらのサブシステムは同様の方法で実装され、一緒に検討されます。



MIPSアーキテクチャ


MIPSアーキテクチャでのこれらのサブシステムの特定の実装に移りましょう。



MIPSバージョン2アーキテクチャには、割り込みの動作モードがいくつかあります。 割り込みテーブル自体のベースアドレスと構造が異なります。

ベースアドレスには2つのモードがあります。

  1. 最初のケースでは、あらゆるタイプの例外に直面したプロセッサーは、アドレス固定アドレス(0x80000180)に制御を移します。プロセッサーのサイズは128バイトです。
  2. 2番目では、プロセッサはCP0_EBASEレジスタで指定されたアドレスに制御を移します。プロセッサのサイズは256バイトです。


割り込みテーブルの構造には、2つの動作モードがあります。すべての例外に応答して1つのハンドラーが呼び出される場合の通常モードと、各割り込み番号にハンドラー用の独自のスペースがあるベクターです。



これらのモードは、MIPSプロセッサの特別なレジスタで設定されます。 汎用レジスタとは異なり、特殊レジスタは、プロセッサ自体を制御するためにプログラムによって使用されます。



MIPSでは、そのようなレジスタはコプロセッサ0で取り出されます。また、特別なアセンブラコマンドによってアクセスされます。mfc0-レジスタの読み取り用、およびmtc0-レジスタへの書き込み用です。

レジスタは、インデックスおよびコプロセッサセレクタによってアドレス指定されます。 システムコールを処理するための重要なレジスタを次に示します。

役職 索引 セレクター 説明
CP0_STATUS 12 0 プロセッサの制御フラグ
CP0_CAUSE 13 0 中断の原因に関する情報
CP0_EPC 14 0 中断時に実行されていたコマンドのアドレス
CP0_EBASE 15 1 例外処理手順のベースアドレス


例外処理モードの仕様に戻ると、それらは2つのレジスタCP0_STATUSおよびCP0_CAUSEに設定されます。これらの形式は次のとおりです。



CP0_STATUS
31-28 27 26 25 24 23 22 21 20 19 18-16 15-8 7 6 5 4-3 2 1 0
CU3..CU0 RP FR RE MX Px ベブ TS シニア Nmi Impl IM7..IM0 Kx Sx Ux KSU Erl EXL IE


CP0_CAUSE
31 30 29-28 27 26 25-24 23 22 21-16 15-10 9-8 7 6-2 1-0
Bd Ti CE DC PCI 0 IV WP 0 IP IP 0 エクスコード 0


CPUの初期化


最初の動作モードのみを、すべてのMIPSプロセッサと最も単純で互換性があると見なします。 他のすべてのモードは同様の方法で実行されます。



プロセッサをこのモードにするには、ステータスレジスタのBEVビットと理由レジスタのビットIVをリセットする必要があります。



プロジェクトのCコード

/* Setup a proper exception table and enable exceptions. */ static int mips_exception_init(void) { unsigned int reg; /* clear BEV bit */ reg = mips_read_c0_status(); reg &= ~(ST0_BEV); mips_write_c0_status(reg); /* clear CauseIV bit */ reg = mips_read_c0_cause(); reg &= ~(CAUSE_IV); mips_write_c0_cause(reg); /* copy the first exception handler */ memcpy((void *)(EBASE + 0x180), &mips_first_exception_handler, 0x80); mips_setup_exc_table(); /* clear EXL bit */ reg = mips_read_c0_status(); reg &= ~(ST0_ERL); mips_write_c0_status(reg); return 0; }
      
      







割り込み、例外、またはシステムコールが発生したときにこれらのビットをクリアした後、プロセッサは命令の順次実行を中断し、アドレス0x80000180に制御を転送します。 同時に、プロセッサは特権モードに入り、戻りアドレスをCP0_EPCレジスタに保存し、例外の理由(タイプ)をCP0_CAUSEレジスタ(例外コードフィールド)に書き込みます。



例外コードフィールドについてはもう少し詳しく説明します。 上記のように、MIPSおよび他のアーキテクチャでは、割り込み、システムコール、およびハードウェア例外は通常、1つのサブシステムで同様の方法で実装されます。 つまり、ハンドラーコードが最初に行うべきことは、何が起こったのかに関する情報を保存することです。 例外コードフィールドに入力されるのは、この情報です。 MIPSでは、このフィールドは次の値を取ることができます。
コード 指定 説明
0 INT 外部割り込み
1-3 仮想メモリの使用
4 ADDRL 非整列アドレスからの読み取り
5 ADDRS 不均衡なアドレス記録
6 IBUS 指示の読み取りエラー
7 DBUS データバスのエラー
8 SYSCALL システムコール
9 Bkpt ブレークポイント
10 予約命令
11 コプロセッサーエラー
12 Ovf 算術オーバーフロー
13以上 浮動小数点演算


システムコールの処理


最初のレベルのハンドラー


プライマリハンドラはアセンブラで記述されています。

 NESTED(mips_first_exception_handler, 0, $sp) .set push /* save the current status of flags */ mfc0 $k1, $CP0_CAUSE andi $k1, $k1, 0x7c /* read exception number */ j mips_second_exception_handler /* jump to real exception handler */ nop .set pop /* restore the previous status of flags */ END(mips_first_exception_handler)
      
      





例外のタイプをレジスタ$ k1に格納し、サイズが制限されなくなった第2レベルのハンドラーを呼び出します。 呼び出しは「jar」ではなく「j」コマンドによって行われます。これは、ハンドラコードがプログラム中に配置されるため(初期化関数にコピーしたため)、呼び出されたプロシージャの相対アドレスではなく絶対アドレスが必要です。



ここで言及する価値のあるもう1つの機能は、k1レジスタです。

MIPSアーキテクチャには32個の汎用レジスタr0-r31があります。 また、慣例により、一部のレジスタは特別な方法で使用されます。たとえば、r31レジスタはスタックへのポインタとして使用され、特別な名前spでアクセスできます。 レジスターk0(r26)およびk1(r27)でも同じです。コンパイラーはそれらを使用せず、OSのカーネルで使用するために予約されており、割り込み処理はそのような特殊な使用例です。



第2レベルのハンドラー


第2レベルのハンドラーに移りましょう。 その主な目的は、C関数を呼び出す準備をすることです。つまり、まず、この関数自体で使用できる残りのレジスタを保存します。 また、アセンブラーで書かれています。

  LEAF(mips_second_exception_handler) SAVE_ALL /* save all needed registers */ PTR_L $k0, exception_handlers($k1) /* exception number is an offset in array */ PTR_LA $ra, restore_from_exception /* return address for exit from exception */ move $a0, $sp /* Arg 0: saved regs. */ jr $k0 /* Call C code. */ nop restore_from_exception: /* label for exception return address */ RESTORE_ALL /* restore all registers and return from exception */ END(mips_second_exception_handler)
      
      







SAVE_ALLアセンブラーマクロです。 次のようになります。

  .macro SAVE_ALL LONG_ADDI $sp, -PT_SIZE SAVE_SOME SAVE_AT SAVE_TEMP SAVE_STATIC .endm
      
      





すべてのネストされたマクロのソースコードを引用するつもりはありません。 最初の行は、必要なすべてのレジスタが順次保存される割り込み用のスタックフレームを予約しているとしか言えません。

SAVE_AT- (r1)のレジスターはアセンブラーが使用するために予約されており、 「。set noat」「.set at」のディレクティブで分離する必要があります(コンパイラーの警告がないように)

SAVE_TEMP-一時レジスター(r8-r15)および(r24-r25)を保存します

SAVE_STATIC-レジスタs0-s7

SAVE_SOME-必要なサービスレジスタ、たとえば、スタックへのポインタおよび特別なコプロセッサレジスタ(たとえば、ステータスレジスタ)。したがって、このマクロが最初になります。



次に、右の第3レベルのハンドラーが選択されます。 プロジェクトの第3レベルハンドラーへのポインターは通常の配列に格納され、例外の種類によってオフセットが設定されます。 MIPS作成者はCAUSEレジスタに左に2ビットシフトした例外番号を挿入するため、インデックスではなくオフセットです。したがって、追加の算術演算なしでポインターの配列から関数を直接呼び出すことができます。

次に、関数を呼び出す前に、リターンアドレス(ra)を書き込みます。 そして最後に、割り込みを入れた状態に関する情報をハンドラー関数に渡します。そのために、ポインターをスタックに渡し、C関数の署名でこのフレームの説明(構造)を指定します。



これはこの構造の説明です

 typedef struct pt_regs { unsigned int reg[25]; unsigned int gp; /* global pointer r28 */ unsigned int sp; /* stack pointer r29 */ unsigned int fp; /* frame pointer r30 */ unsigned int ra; /* return address 31*/ unsigned int lo; unsigned int hi; unsigned int cp0_status; unsigned int pc; }pt_regs_t;
      
      





第3レベルのハンドラー(Cコード)


プロセッサのコードは次のとおりです

 void mips_c_syscall_handler(pt_regs_t *regs) { uint32_t result; /* v0 contains syscall number */ uint32_t (*sys_func)(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) = SYSCALL_TABLE[regs->reg[1]]; /* a0, a1, a2, a3, s0 contain arguments */ result = sys_func(regs->reg[3], regs->reg[4], regs->reg[5], regs->reg[6], regs->reg[15]); /* v0 set equal to result */ regs->reg[1] = result; regs->pc += 4; /* skip comand generated syscall */ }
      
      





コードからすべてが明確であることを願っています:





システムコールの受信


次に、システムコールの作成方法について説明する必要があります。

システムコールは、特別なアセンブラコマンドによって生成されます 。たとえば、x86ではint 、SPARCではta 、MIPSではsyscallです。



おそらく前のセクションから明らかになったように、システムコールの時点で、コール番号をレジスタv0に保存し、パラメータをレジスタa0、a1、a2、a3に転送する必要があります。 たとえば、レジ​​スタa0に引数を1つ入れてシステムコールを0x11にする関数のコードを次に示します。 読者はgccインラインアセンブラに精通していると思います

 static inline int syscall_demo(int arg1) { long __res; __asm__ volatile ( "move $a0, %2\n\t" "li $v0, %1\n\t" "syscall\n\t" "move %0, $v0" : "=r" (__res) : "I" (0x11), "r" ((long)(arg1))); return __res; }
      
      





もちろん、タイプごとに関数を記述するのは便利ではないため、マクロが使用されます。 以下は、1つのパラメーターを持つシステムコール関数を宣言するマクロコードです。

 #define __SYSCALL1(NR,type,name,type1,arg1) \ static inline type name(type1 arg1) \ { \ long __res; \ __asm__ volatile ( \ "move $a0, %2\n\t" \ "li $v0, %1\n\t" \ "syscall\n\t" \ "move %0, $v0" \ \ : "=r" (__res) \ : "I" (NR), \ "r" ((long)(arg1))); \ return __res; \ }
      
      





パラメーターの数が異なるシステムコールのコードは、指定されたものと似ています。



すべてをまとめる。 私たちのプロジェクトはいくつかのテストを使用していますが、そのうちの1つが以下にあります。



 SYSCALL1(1,int,syscall_1,int,arg1); TEST_CASE("calling syscall with one argument") { test_assert_equal(syscall_1(1), 1); }
      
      







SYSCALLマクロ 、インラインアセンブラで上記のコードに展開され、コール名syscall_1(3番目のパラメーター)として番号1(マクロの最初の引数)で置き換えられ、戻り値の型はint(2番目のマクロパラメーター)であり、変数の型もint(4番目のマクロパラメーター) )

テスト自体は、syscall_1(1)の呼び出しの結果が1になることを確認します。



関連リンク


  1. Das U-boot LoaderでのMIPSの割り込みの実装
  2. LinuxカーネルでのMIPSの割り込みの実装
  3. プロジェクトのコード




おわりに


結論として、このトピックのより詳細な理解に興味がある人には、プロジェクトコードを取得してqemuをプレイすることをお勧めします(wikiページで開始方法を説明しています)。 Eclipseのすべてのアメニティを備えたブレークポイントを歩けば、その仕組みを理解するのがはるかに簡単になります。



最後まで読んでくれたみんなに感謝します! コメント、推奨事項、提案を聞いてうれしいです。



All Articles