クライミングエルブルス-戦闘中の偵察。 テクニカルパート2.割り込み、例外、システムタイマー

Emboxを移植することで、引き続きElbrusを探索します。



この記事は、Elbrusアーキテクチャに関する技術記事の第2部です。 最初の部分では、スタック、レジスタなどを扱いました。 このパートを読む前に、Elbrusアーキテクチャの基本的なことについて説明しているため、最初のパートを学習することをお勧めします。 このセクションでは、タイマー、割り込み、および例外に焦点を当てます。 これもまた公式文書ではありません。 そのためには、ICSTのElbrusの開発者に連絡する必要があります。

Elbrusの学習を開始したとき、タイマーをより早く開始したいと考えていました。 これを行うには、割り込みコントローラーとタイマー自体を実装するのに十分であるように見えましたが、 予期せぬ予想される困難に遭遇しました。 彼らはデバッグ機能を探し始め、開発者がさまざまな例外的な状況を発生させることができるいくつかのコマンドを導入することでこれを処理していることがわかりました。 たとえば、レジ​​スタPSR(プロセッサステータスレジスタ)およびUPSR(ユーザープロセッサステータスレジスタ)を使用して、特別な種類の例外を生成できます。 PSRの場合、exc_last_wishビットはプロシージャから戻るときのexc_last_wish例外フラグであり、UPSRの場合、exc_d_interruptはVFDI操作(遅延割り込みフラグの確認)によって生成される遅延割り込みフラグです。



コードは次のとおりです。



#define UPSR_DI (1 << 3) /*   .h  */ rrs %upsr, %r1 ors %r1, UPSR_DI, %r1 /* upsr |= UPSR_DI; */ rws %r1, %upsr vfdi /*      */
      
      





起動しました。 しかし、何も起こらず、システムはどこかにハングし、コンソールには何も出力されませんでした。 実際、タイマーから割り込みを開始しようとしたときにこれを見ましたが、その後多くのコンポーネントがあり、ここで何かがプログラムの順次進行を中断し、制御が例外テーブルに転送されたことが明らかでした(Elbrusアーキテクチャに関しては、テーブルについて話さない方が正しい例外テーブルに関する中断)。 プロセッサがまだ例外をスローしたと想定しましたが、制御を移す場所に何らかの「ゴミ」がありました。 結局のところ、彼はEmboxイメージを配置したまさにその場所に制御を移します。これは、エントリポイントがあったことを意味します-エントリ関数。



確認のため、次のことを行いました。 エントリ()のエントリのカウンタを開始しました。 最初に、すべてのCPUは割り込みをオフにして開始し、entry()に入ります。その後、1つのコアのみをアクティブのままにして、残りはすべて無限ループに入ります。 カウンターがCPUの数に等しくなった後、エントリの後続のヒットはすべて例外であると見なします。 エルブルスについての最初の記事で説明されいたように



  cpuid = __e2k_atomic32_add(1, &last_cpuid); if (cpuid > 1) { /* XXX currently we support only single core */ while(1); } /* copy of trap table */ memcpy((void*)0, &_t_entry, 0x1800); kernel_start();
      
      





そうでした



  /* Since we enable exceptions only when all CPUs except the main one * reached the idle state (cpu_idle), we can rely that order and can * guarantee exceptions happen strictly after all CPUS entries. */ if (entries_count >= CPU_COUNT) { /* Entering here because of expection or interrupt */ e2k_trap_handler(regs); ... } /* It wasn't exception, so we decide this usual program execution, * that is, Embox started on CPU0 or CPU1 */ e2k_wait_all(); entries_count = __e2k_atomic32_add(1, &entries_count); if (entries_count > 1) { /* XXX currently we support only single core */ cpu_idle(); } e2k_kernel_start(); }
      
      





最後に、割り込みを入力したときの反応を確認しました(printfを使用して行を出力しました)。



ここでは、最初のバージョンでは例外テーブルをコピーする予定でしたが、最初はアドレスにあることがわかり、次に正しいコピーを作成できなかったことを説明する価値があります。 リンカスクリプト、システムへのエントリポイント、および割り込みハンドラを書き直さなければなりませんでした。つまり、アセンブラ部分が必要でした。



これは、スクリプトリンカーの変更された部分の一部の外観です。



 .text : { _start = .; _t_entry = .; /* Interrupt handler */ *(.ttable_entry0) . = _t_entry + 0x800; /* Syscall handler */ *(.ttable_entry1) . = _t_entry + 0x1000; /* longjmp handler */ *(.ttable_entry2) . = _t_entry + 0x1800; _t_entry_end = .; *(.e2k_entry) *(.cpu_idle) /* text */ }
      
      





つまり、例外テーブルのエントリセクションを削除しました。 cpu_idleセクションは、使用されていないCPUに対してもそこにあります。



Emboxが実行されるアクティブカーネルの入力関数は次のようになります。



 static void e2k_kernel_start(void) { extern void kernel_start(void); int psr; /*    CPU “” */ while (idled_cpus_count < CPU_COUNT - 1) ; ... /*     ,     */ e2k_upsr_write(e2k_upsr_read() & ~UPSR_FE); kernel_start(); /*   Embox */ }
      
      





さて、VFDIの指示によると、例外がスローされました。 次に、これが正しい例外であることを確認するために、彼の番号を取得する必要があります。 このため、ElbrusにはTIR割り込み情報レジスタ(トラップ情報レジスタ)があります。 これらには、最後のいくつかのコマンド、つまりトレースの最終部分に関する情報が含まれています。 トレースはプログラムの実行中に収集され、割り込みに入るとフリーズします。 TIRには、下位(64ビット)と上位(64ビット)の部分が含まれます。 下位ワードには例外フラグが含まれ、上位ワードには例外につながった命令へのポインターと現在のTIR番号が含まれます。 したがって、この場合、exc_d_interruptは4番目のビットです。



注TIRの深さ(数)については、まだいくつかの誤解があります。 ドキュメントには次のものがあります。

「TIRメモリの深さ、つまりトラップ情報レジスタの数が決定されます

に必要なプロセッサパイプラインステージの数に等しいTIR_NUMマクロ

すべての可能な特別な状況を発行します。 TIR_NUM = 19;”
実際には、depth = 1であるため、TIR0レジスタのみを使用します。



MCSTの専門家は、すべてが正しく、「正確な」割り込みに対してのみTIR0が存在し、他の状況では他に何かがあるかもしれないと説明しました。 しかし、タイマー割り込みについてのみ話しているので、これは気にしません。



さて、例外ハンドラを正しく開始/終了するために必要なものを調べます。 実際には、入力で保存し、出力で次の5つのレジスタを復元する必要があります。 3つの制御転送準備レジスタはctpr [1,2,3]、2つのサイクル制御レジスタはILCR(サイクルカウンタの初期値のレジスタ)とLSR(サイクルステータスのレジスタ)です。



 .type ttable_entry0,@function ttable_entry0: setwd wsz = 0x10, nfx = 1; rrd %ctpr1, %dr1 rrd %ctpr2, %dr2 rrd %ctpr3, %dr3 rrd %ilcr, %dr4 rrd %lsr, %dr5 /* sizeof pt_regs */ getsp -(5 * 8), %dr0 std %dr1, [%dr0 + PT_CTRP1] /* regs->ctpr1 = ctpr1 */ std %dr2, [%dr0 + PT_CTRP2] /* regs->ctpr2 = ctpr2 */ std %dr3, [%dr0 + PT_CTRP3] /* regs->ctpr3 = ctpr3 */ std %dr4, [%dr0 + PT_ILCR] /* regs->ilcr = ilcr */ std %dr5, [%dr0 + PT_LSR] /* regs->lsr = lsr */ disp %ctpr1, e2k_entry ct %ctpr1
      
      





実際、例外ハンドラを終了した後、これら5つのレジスタを復元する必要があります。



マクロを使用してこれを行います。



 #define RESTORE_COMMON_REGS(regs) \ ({ \ uint64_t ctpr1 = regs->ctpr1, ctpr2 = regs->ctpr2, \ ctpr3 = regs->ctpr3, lsr = regs->lsr, \ ilcr = regs->ilcr; \ /* ctpr2 is restored first because of tight time constraints \ * on restoring ctpr2 and aaldv. */ \ E2K_SET_DSREG(ctpr1, ctpr1); \ E2K_SET_DSREG(ctpr2, ctpr2); \ E2K_SET_DSREG(ctpr3, ctpr3); \ E2K_SET_DSREG(lsr, lsr); \ E2K_SET_DSREG(ilcr, ilcr); \ })
      
      





また、レジスタの復元後、DONE操作(ハードウェア割り込みハンドラからの戻り)を呼び出すことを忘れないことが重要です。 この操作は、特に、中断されたコントロール転送操作を正しく処理するために必要です。 マクロを使用してこれを行います。



 #define E2K_DONE \ do { \ asm volatile ("{nop 3} {done}" ::: "ctpr3"); \ } while (0)
      
      





実際、これらの2つのマクロを使用して、Cコードで割り込みから直接戻ります。

  /* Entering here because of expection or interrupt */ e2k_trap_handler(regs); RESTORE_COMMON_REGS(regs); E2K_DONE;
      
      





外部割り込み



外部割り込みを有効にする方法から始めましょう。 Elbrusでは、APIC(またはそのアナログ)が割り込みコントローラーとして使用されます; Emboxにはすでにこのドライバーがありました。 したがって、システムタイマーを取得することができました。 2つのタイマーがあり、そのうちの1つはPITに非常に似ており、もう1つのLAPICタイマーも非常に標準的であるため、それらについて話すのは意味がありません。 あれもあれもシンプルに見え、あれもそれもEmboxにすでに存在していましたが、LAPICタイマーのドライバーは、PITタイマーの実装がより非標準的なように見えただけでなく、より遠近感がありました。 したがって、完了しやすいように見えました。 さらに、公式ドキュメントにはレジスタAPICおよびLAPICが記載されていましたが、これらは元のものとはわずかに異なりました。 あなたがオリジナルで見ることができるように、それらを持ってくることは意味がありません。



APICで割り込みを許可することに加えて、PSR / UPSRレジスタを介して割り込み処理を有効にする必要があります。 両方のレジスタには、外部割り込みとマスク不能割り込みを許可するフラグがあります。 ただし、ここでは、PSRレジスタが関数に対してローカルであることに注意することが非常に重要です(これについては、 最初の技術部分で説明しました)。 これは、関数内で設定した場合、後続のすべての関数を呼び出すと継承されますが、関数から戻ると元の状態に戻ることを意味します。 したがって、質問ですが、割り込みを管理する方法は?



次のソリューションを使用します。 PSRレジスタを使用すると、UPSRを介した管理を有効にできます。UPSRは、すでにグローバルです(必要なものです)。 したがって、UPSR経由で直接制御を有効にします(重要!)Emboxコアログイン機能の前:



  /* PSR is local register and makes sense only within a function, * so we set it here before kernel start. */ asm volatile ("rrs %%psr, %0" : "=r"(psr) :); psr |= (PSR_IE | PSR_NMIE | PSR_UIE); asm volatile ("rws %0, %%psr" : : "ri"(psr)); kernel_start();
      
      





どういうわけか、偶然、リファクタリング後、これらの行を取得して別の関数に入れました...そして、レジスタは関数に対してローカルです。 すべてが壊れていることは明らかです:)



そのため、プロセッサですべてがオンになっているようで、割り込みコントローラに移動します。



上で見たように、例外番​​号に関する情報はTIRレジスタにあります。 さらに、このレジスタの32番目のビットは、外部割り込みが発生したことを報告します。



タイマーをオンにした後、中断が得られなかったため、数日間の苦痛が続きました。 その理由は十分に面白かった。 Elbrusには64ビットポインターがあり、APICのレジスタアドレスはuint32_tに入りました。そのため、それらを使用しました。 しかし、たとえば、0xF0000000をポインターにキャストする必要がある場合、0xF0000000ではなく0xFFFFFFFFF0000000が得られることが判明しました。 つまり、コンパイラはunsigned int符号を展開します。



もちろん、ここではuintptr_tを使用する必要がありました。これは、C99標準ではこのようなキャストが実装定義されていることが判明したためです。



TIRで32ビット目を上げたのをようやく見た後、割り込み番号を取得する方法を探し始めました。 それは非常に単純であることが判明しましたが、x86の場合とはまったく異なりますが、これはLAPIC実装の違いの1つです。 Elbrusの場合、割り込み番号を取得するには、特別なLAPICレジスタにアクセスする必要があります。



  #define APIC_VECT (0xFEE00000 + 0xFF0)
      
      





ここで、0xFEE00000はLAPICレジスタのベースアドレスです。



それだけで、システムタイマーとLAPICタイマーの両方を取得することが判明しました。



おわりに



Elbrusアーキテクチャについての記事の最初の2つの技術部分で提供された情報は、ハードウェア割り込みとプリエンプティブマルチタスクを任意のOSに実装するのに十分です。 実際、与えられたスクリーンショットはこれを証明しています。







これは、Elbrusアーキテクチャに関する最後の技術部分ではありません。 現在、エルブラスでメモリ管理(MMU)をマスターしています。すぐに話をしたいと思っています。 これは、仮想アドレス空間の実装だけでなく、周辺機器との通常の作業にも必要です。このメカニズムにより、アドレス空間の特定領域のキャッシュを無効または有効にできるためです。



記事に書かれているすべてのものは、 Emboxリポジトリにあります。 もちろん、ハードウェアプラットフォームがある場合は、ビルドして実行することもできます。 確かに 、これにはコンパイラーが必要であり、 MCSTでのみ入手できます。 公式のドキュメントをリクエストできます。



All Articles