OS1:Rust for x86のプリミティブカーネル。 パート2. VGA、GDT、IDT

前編







最初の記事にはまだ冷める時間がありませんでしたので、興味をそそり、続編を書かないことにしました。







そのため、前の記事で、リンク、カーネルファイルの読み込み、およびプライマリ初期化について説明しました。 いくつかの有用なリンクを提供し、ロードされたカーネルがメモリ内に配置される方法、ブート時に仮想アドレスと物理アドレスを比較する方法、およびページメカニズムのサポートを有効にする方法を説明しました。 最後に、Rustで書かれたカーネルのkmain関数に制御が渡されました。 次は、ウサギの穴の深さを調べましょう!







ノートのこの部分では、Rustの構成について簡単に説明し、一般的にはVGAでの情報の出力について説明し、セグメントと割り込みのセットアップについて詳しく説明します 。 カットに興味があるすべての人に尋ねると、私たちは始めます。







さびのセットアップ



一般に、この手順には特に複雑なことはありません。詳細については、 Philippeブログに連絡してください。 ただし、いくつかの時点で停止します。







Stable Rustは低レベルの開発に必要な一部の機能をまだサポートしていないため、標準ライブラリを無効にしてBare Bonesでビルドするには、Rustが毎晩必要です。 注意してください。最新版にアップグレードした後、完全に動作しないコンパイラを取得し、最も近い安定したコンパイラにロールバックする必要がありました。 コンパイラが昨日は動作していたが、更新されて動作しないことが確実な場合は、必要な日付を置き換えてコマンドを実行します







rustup override add nightly-YYYY-MM-DD
      
      





メカニズムの詳細については、 こちらからお問い合わせください







次に、目的のプラットフォームを構成します。 私はPhilip Oppermanのブログに基づいていたので、このセクションの多くのことは彼から取られ、骨によって分解され、私のニーズに適合しました。 Phillipは彼のブログでx64向けに開発中です。当初はx32を選択していましたので、target.jsonは少し異なります。 私は彼を完全に連れて行きます







 { "llvm-target": "i686-unknown-none", "data-layout": "em:ep:32:32-f64:32:64-f80:32-n8:16:32-S128", "arch": "x86", "target-endian": "little", "target-pointer-width": "32", "target-c-int-width": "32", "os": "none", "executables": true, "linker-flavor": "ld.lld", "linker": "rust-lld", "panic-strategy": "abort", "disable-redzone": true, "features": "-mmx,-sse,+soft-float" }
      
      





ここで最も難しい部分は、「 data-layout 」パラメーターです。 LLVMのドキュメントには、これらが「-」で区切られたデータレイアウトオプションであることが示されています。 最初の「e」キャラクターはインド性を担当します。この例では、プラットフォームが必要とするリトルエンディアンです。 2番目の文字はm、「歪み」です。 レイアウト中のキャラクター名を担当します。 出力形式はELFであるため(ビルドスクリプトを参照)、値「m:e」を選択します。 3番目の文字は、ビット単位のポインターのサイズとABI(アプリケーションバイナリインターフェイス)です。 ここではすべてが単純で、32ビットであるため、「p:32:32」と大胆に配置します。 次は浮動小数点数です。 アラインメント64-「f64:32:64」のABI 32に準拠した64ビット数と、デフォルトでアラインメントのある80ビット数-「f80:32」をサポートすると報告しています。 次の要素は整数です。 8ビットから開始し、プラットフォームの最大32ビット(「n8:16:32」)に移動します。 最後はスタックの整列です。 128ビット整数も必要なので、S128にします。 いずれにせよ、LLVMはこのパラメーターを安全に無視できます。これが私たちの好みです。







残りのパラメーターについては、フィリップを覗くことができます、彼はすべてをうまく説明しています。







また、cargo-xbuildも必要です。これは、なじみのないターゲットプラットフォームでビルドするときに、rust-coreをクロスコンパイルできるツールです。

インストール。







 cargo install cargo-xbuild
      
      





このように組み立てます。







 cargo xbuild -Z unstable-options --manifest-path=kernel/Cargo.toml --target kernel/targets/$(ARCH).json --out-dir=build/lib
      
      





ルートディレクトリから起動され、カーネルはカーネルディレクトリにあるため、Makeの正しい操作のためにマニフェストが必要でした。







マニフェストの機能のうち、 crate-type = ["staticlib"]のみを選択できます。これにより、出力にリンク可能なファイルが提供されます。 私たちは彼をLLDで養います。







kmainと初期設定



Rustの規則によれば、静的ライブラリ(または「フラット」なバイナリファイル)を作成する場合、クレートのルートにはエントリポイントであるファイルlib.rsが含まれている必要があります。 その中に、属性の助けを借りて、言語機能が構成され、また大切なkmainも見つけられます。







そのため、最初のステップでは、stdライブラリを無効にする必要があります。 これはマクロで行われます。







 #![no_std]
      
      





このような簡単な手順で、マルチスレッド、動的メモリ、および標準ライブラリのその他の楽しみをすぐに忘れてしまいます。 さらに、println!マクロも自分自身から奪うため、自分で実装する必要があります。 次回はその方法を説明します。







この場所のどこかにある多くのチュートリアルは、「Hello World」の出力で終わり、生き方を説明していません。 私たちは他の方法で行きます。 まず、プロテクトモードのコードとデータセグメントを設定し、VGAを構成し、割り込みを構成する必要があります。







 #![no_std] #[macro_use] pub mod debug; #[cfg(target_arch = "x86")] #[path = "arch/i686/mod.rs"] pub mod arch; #[no_mangle] extern "C" fn kmain(pd: usize, mb_pointer: usize, mb_magic: usize) { arch::arch_init(pd); ...... } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { println!("{}", _info); loop {} }
      
      





ここで何が起こっていますか? 私が言ったように、標準ライブラリをオフにします。 また、2つの非常に重要なモジュール、debug(画面に書き込む)とarch(すべてのプラットフォーム依存のマジックが存在する)を発表します。 Rust機能を構成と共に使用して、異なるアーキテクチャー実装で同じインターフェースを宣言し、それらを最大限に使用します。 ここでは、x86でのみ停止し、それについてのみ説明します。







Rustが必要とする完全に原始的なパニックハンドラーを宣言しました。 その後、それを変更することが可能になります。







kmainは3つの引数を受け入れ、名前の歪みのないC表記でエクスポートされるため、リンカは関数を_loaderからの呼び出しに正しく関連付けることができます。これについては、前の記事で説明しました。 最初の引数はPDページテーブルのアドレス、2番目はメモリカードを取得するGRUB構造の物理アドレス、3番目はマジックナンバーです。 将来的には、Multiboot 2サポートと独自のブートローダーの両方を実装したいので、マジックナンバーを使用してブート方法を識別します。







最初のkmain呼び出しは、プラットフォーム固有の初期化です。 中に入ります。 arch_init関数はarch / i686 / mod.rsファイルにあり、32ビットx86固有のパブリックであり、次のようになります。







 pub fn arch_init(pd: usize) { unsafe { vga::VGA_WRITER.lock().init(); gdt::setup_gdt(); idt::init_idt(); paging::setup_pd(pd); } }
      
      





ご覧のとおり、x86の場合、出力、セグメンテーション、割り込み、およびページングは​​順番に初期化されます。 VGAから始めましょう。







VGA初期化



各チュートリアルでは、Hello Worldを印刷することが義務付けられているため、どこでもVGAを操作する方法がわかります。 このため、できるだけ簡単に説明します。自分で作ったチップにのみ焦点を当てます。 lazy_staticの使用については、Philippeのブログに送信しますが、詳細については説明しません。 const fnはまだリリースされていないため、美しく静的な初期化はまだ行えません。 また、スピンロックを追加して、混乱しないようにします。







 use lazy_static::lazy_static; use spin::Mutex; lazy_static! { pub static ref VGA_WRITER : Mutex<Writer> = Mutex::new(Writer { cursor_position: 0, vga_color: ColorCode::new(Color::LightGray, Color::Black), buffer: unsafe { &mut *(0xC00B8000 as *mut VgaBuffer) } }); }
      
      





ご存じのように、画面バッファーは物理アドレス0xB8000にあり、サイズは80x25x2バイト(画面の幅と高さ、文字ごとのバイトと属性:色、ちらつき)です。 すでに仮想メモリを有効にしているため、このアドレスにアクセスするとクラッシュするため、3 GBを追加します。 また、安全ではない生のポインタを逆参照しますが、何をしているのかはわかっています。

このファイルの興味深い点は、おそらく、Writer構造の実装だけです。この構造では、文字を連続して表示できるだけでなく、スクロール、画面上の任意の場所への移動、その他の楽しい操作も可能です。







VGAライター
 pub struct Writer { cursor_position: usize, vga_color: ColorCode, buffer: &'static mut VgaBuffer, } impl Writer { pub fn init(&mut self) { let vga_color = self.vga_color; for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code: vga_color, } } } self.set_cursor_abs(0); } fn set_cursor_abs(&mut self, position: usize) { unsafe { outb(0x3D4, 0x0F); outb(0x3D5, (position & 0xFF) as u8); outb(0x3D4, 0x0E); outb(0x3D4, ((position >> 8) & 0xFF) as u8); } self.cursor_position = position; } pub fn set_cursor(&mut self, x: usize, y: usize) { self.set_cursor_abs(y * VGA_WIDTH + x); } pub fn move_cursor(&mut self, offset: usize) { self.cursor_position = self.cursor_position + offset; self.set_cursor_abs(self.cursor_position); } pub fn get_x(&mut self) -> u8 { (self.cursor_position % VGA_WIDTH) as u8 } pub fn get_y(&mut self) -> u8 { (self.cursor_position / VGA_WIDTH) as u8 } pub fn scroll(&mut self) { for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = self.buffer.chars[(y + 1) * VGA_WIDTH + x] } } for x in 0..VGA_WIDTH { let color_code = self.vga_color; self.buffer.chars[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code } } } pub fn ln(&mut self) { let next_line = self.get_y() as usize + 1; if next_line >= VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } else { self.set_cursor(0, next_line) } } pub fn write_byte_at_xy(&mut self, byte: u8, color: ColorCode, x: usize, y: usize) { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte_at_pos(&mut self, byte: u8, color: ColorCode, position: usize) { self.buffer.chars[position] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte(&mut self, byte: u8) { if self.cursor_position >= VGA_WIDTH * VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } self.write_byte_at_pos(byte, self.vga_color, self.cursor_position); self.move_cursor(1); } pub fn write_string(&mut self, s: &str) { for byte in s.bytes() { match byte { 0x20...0xFF => self.write_byte(byte), b'\n' => self.ln(), _ => self.write_byte(0xfe), } } } }
      
      





巻き戻すときは、メモリのセクションを画面幅のサイズだけ逆方向にコピーし、新しい行を空白で埋めます(これが、私がクリーニングを行う方法です)。 Outb呼び出しはもう少し興味深いです-I / Oポートを操作する以外にカーソルを移動することは不可能です。 ただし、ポートを介した入出力が必要なため、別のパッケージで提供され、安全なラッパーでラップされています。 以下のネタバレの下にアセンブラーコードがあります。 今のところ、それを知るだけで十分です:









out.asm

スタックで渡された変数を操作することに注意してください。 スタックはスペースの最後から始まり、関数が呼び出されたときにスタックポインターを減らすため、パラメーター、リターンポイントなどを取得するために、スタックのアライメントに合わせて引数のサイズをESPレジスタ(この場合は4バイト)に追加する必要があります。







 global writeb global writew global writed section .text writeb: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 byte value aligned 4 bytes out dx, al ;write byte by port number an dx - value in al mov esp, ebp pop ebp ret writew: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 word value aligned 4 bytes out dx, ax ;write word by port number an dx - value in ax mov esp, ebp pop ebp ret writed: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 double word value aligned 4 bytes out dx, eax ;write double word by port number an dx - value in eax mov esp, ebp pop ebp ret
      
      





セグメント設定



私たちは最も不可解でしたが、同時に最も簡単なトピックに到達しました。 前の記事で述べたように、メモリ内のページとセグメントの構成が頭の中に混在していたため、ページテーブルのアドレスをGDTRにロードして頭をつかみました。 十分な資料を読み、それを消化して実現するのに数ヶ月かかりました。 ピーターアベルの教科書アセンブラーの犠牲者になったかもしれません。 インテル8086のセグメンテーションを説明するIBM PCの言語とプログラミング(素晴らしい本です!)。これらの楽しい時代に、20ビットアドレスの上位16ビットをセグメントレジスタにロードし、それがメモリ内のアドレスでした。 保護モードのi286から始めて、すべてが完全に間違っているというのは残酷な失望であることが判明しました。







そのため、古いプログラムは640 KBを超えてから1 MBのメモリを超えることができるため、x86はセグメント化されたメモリモデルをサポートしているという理論があります。







プログラマは、実行可能コードを配置する方法、データを配置する方法、および安全性を維持する方法について考える必要がありました。 ページ編成の出現により、セグメント化された編成は不要になりましたが、互換性と保護(カーネル空間とユーザー空間の特権の分離)の目的のために残ったため、それなしではどこにもありません。 特権レベルが0未満の場合、一部のプロセッサ命令は禁止されており、プログラムとカーネルセグメント間のアクセスによりセグメンテーションエラーが発生します。







アドレス変換についてもう一度(できれば最後に)やりましょう

行アドレス[0x08:0xFFFFFFFF]->セグメントの許可を確認0x08->仮想アドレス[0xFFFFFFFF]->ページテーブル+ TLB->物理アドレス[0xAAAAFFFF]







セグメントはプロセッサ内でのみ使用され、特別なセグメントレジスタ(CS、SS、DS、ES、FS、GS)に格納され、コードを実行して制御を転送する権限を確認するためにのみ使用されます。 そのため、ユーザー空間からカーネル関数を取得して呼び出すことはできません。 記述子が0x18のセグメント(私が持っているものは異なります)にはレベル3の権利があり、記述子が0x08のセグメントにはレベル0の権利があります。 jmp 0x08経由の権利:[EAX]が、トラップ、ゲート、割り込みなどの他のメカニズムを使用する義務があります。







セグメントとそのタイプ(コード、データ、ラダー、ゲート)は、グローバル記述子テーブルGDTに記述されている必要があり、その仮想アドレスとサイズはGDTRレジスタにロードされます。 セグメント間を遷移するとき(簡単にするために、直接遷移が可能であると想定しています)、jmp 0x08:[EAX]命令を呼び出す必要があります。 オフセット(セレクター)はCSレジスターにロードされ、対応する記述子はプロセッサーのシャドーレジスターにロードされます。 各記述子は8バイト構造です。 十分に文書化されており、その説明はOSDevとIntelのドキュメントの両方にあります(最初の記事を参照)。







まとめます。 GDTを初期化してjmp 0x08:[EAX]トランジションを実行すると、プロセッサーのステータスは次のようになります。









ゼロ記述子は常に初期化されていない必要があり、アクセスは禁止されています。 マルチスレッドについて説明するときに、TSS記述子とその意味について詳しく説明します。 GDTテーブルは次のようになります。







 extern { fn load_gdt(base: *const GdtEntry, limit: u16); } pub unsafe fn setup_gdt() { GDT[5].set_offset((&super::tss::TSS) as *const _ as u32); GDT[5].set_limit(core::mem::size_of::<super::tss::Tss>() as u32); let gdt_ptr: *const GdtEntry = GDT.as_ptr(); let limit = (GDT.len() * core::mem::size_of::<GdtEntry>() - 1) as u16; load_gdt(gdt_ptr, limit); } static mut GDT: [GdtEntry; 7] = [ //null descriptor - cannot access GdtEntry::new(0, 0, 0, 0), //kernel code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //kernel data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //TSS - for interrupt handling in multithreading GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_TSS_AVAIL, 0), GdtEntry::new(0, 0, 0, 0), ];
      
      





そして、ここで初期化について説明しました。 GDTアドレスとサイズのロードは、2つのフィールドのみを含む別の構造を介して行われます。 この構造体のアドレスは、lgdtコマンドに渡されます。 データセグメントレジスタで、オフセット0x10で次の記述子をロードします。







 global load_gdt section .text gdtr dw 0 ; For limit storage dd 0 ; For base storage load_gdt: mov eax, [esp + 4] mov [gdtr + 2], eax mov ax, [esp + 8] mov [gdtr], ax lgdt [gdtr] jmp 0x08:.reload_CS .reload_CS: mov ax, 0x10 ; 0x10 points at the new data selector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov ax, 0x28 ltr ax ret
      
      





そうすれば、すべてが少し簡単になりますが、それほど面白くないでしょう。







中断



実際、コアと対話する(少なくともキーボードでクリックしたものを確認する)機会を私たちに与えます。 これを行うには、割り込みコントローラーを初期化する必要があります。







コードスタイルに関する叙情的な余談。







コミュニティ、特にPhilip Oppermanの努力のおかげで、x86割り込み呼び出し規約がRustに追加されました。これにより、iretを実行する割り込みハンドラーを作成できます。 しかし、アセンブラーとRustを別々のファイルに分離することを決定したため、このルートを使用しないことを意識して決定しました。 はい、スタックメモリを不当に使用しています。これは承知していますが、それでもフレーバーです。 私の割り込みハンドラーはアセンブラーで記述されており、Rustで記述されたものとほぼ同じ割り込みハンドラーを呼び出します。 この事実を受け入れて、甘やかしてください。







一般に、割り込みの初期化プロセスはGDTの初期化と似ていますが、理解しやすいです。 一方、多くの統一されたコードが必要です。 Redox OSの開発者は、言語のすべての楽しみを使用して美しい決定を下しますが、私は「額に」行って、コードの複製を許可することにしました。







x86の規則によると、中断はありますが、例外的な状況があります。 このコンテキストでは、私たちの設定は実質的に同じです。 唯一の違いは、例外がスローされると、スタックに追加情報が含まれることがあることです。 たとえば、私はそれを使用して、束を操作するときにページの不足を処理します(ただし、すべてに時間があります)。 割り込みと例外の両方が同じテーブルから処理されるため、ユーザーと私が記入する必要があります。 PIC(Programmable Interrupt Controller)をプログラムすることも必要です。 APICもありますが、まだわかりません。







PICでの作業に関しては、ネットワーク上に多くの例がありますので、PICでの作業についてはあまりコメントしません。 アセンブラーのハンドラーから始めます。 それらはすべて完全に同じなので、スポイラーのコードを削除します。







IRQ
 global irq0 global irq1 ...... global irq14 global irq15 extern kirq0 extern kirq1 ...... extern kirq14 extern kirq15 section .text irq0: pusha call kirq0 popa iret irq1: pusha call kirq1 popa iret ...... irq14: pusha call kirq14 popa iret irq15: pusha call kirq15 popa iret
      
      





ご覧のとおり、Rust関数の呼び出しはすべて区別と利便性のために「k」プレフィックスで始まります。 例外処理はまったく同じです。 アセンブラー関数の場合、接頭辞「e」が選択され、Rustの場合「k」が選択されます。 ページフォールトハンドラーは異なりますが、それについて-メモリ管理に関する注意事項に記載されています。







例外
 global e0_zero_divide global e1_debug ...... global eE_page_fault ...... global e14_virtualization global e1E_security extern k0_zero_divide extern k1_debug ...... extern kE_page_fault ...... extern k14_virtualization extern k1E_security section .text e0_zero_divide: pushad call k0_zero_divide popad iret e1_debug: pushad call k1_debug popad iret ...... eE_page_fault: pushad mov eax, [esp + 32] push eax mov eax, cr2 push eax call kE_page_fault pop eax pop eax popad add esp, 4 iret ...... e14_virtualization: pushad call k14_virtualization popad iret e1E_security: pushad call k1E_security popad iret
      
      





アセンブラーハンドラーを宣言します。







 extern { fn load_idt(base: *const IdtEntry, limit: u16); fn e0_zero_divide(); fn e1_debug(); ...... fn e14_virtualization(); fn e1E_security(); fn irq0(); fn irq1(); ...... fn irq14(); fn irq15(); }
      
      





上記で呼び出すRustハンドラーを定義します。 キーボードを中断するには、受信したコードを表示するだけで、ポート0x60から取得することに注意してください-これは、キーボードが最も単純なモードで動作する方法です。 将来的には、これが本格的なドライバーに変わると思います。 各割り込みの後、0x20の処理終了の信号をコントローラーに出力する必要があります。これは重要です! そうしないと、割り込みが発生しなくなります。







 #[no_mangle] pub unsafe extern fn kirq0() { // println!("IRQ 0"); outb(0x20, 0x20); } #[no_mangle] pub unsafe extern fn kirq1() { let ch: char = inb(0x60) as char; crate::arch::vga::VGA_WRITER.force_unlock(); println!("IRQ 1 {}", ch); outb(0x20, 0x20); } #[no_mangle] pub unsafe extern fn kirq2() { println!("IRQ 2"); outb(0x20, 0x20); } ...
      
      





IDTおよびPICの初期化。 PICとその再マッピングについて、OSDevで始まりアマチュアサイトで終わる詳細度の異なるチュートリアルが多数見つかりました。 プログラミング手順は一定の操作シーケンスと一定のコマンドで動作するため、これ以上説明することなくこのコードを提供します。 , 0x20-0x2F , 0x20 0x28, 16 IDT.







 unsafe fn setup_pic(pic1: u8, pic2: u8) { // Start initialization outb(PIC1, 0x11); outb(PIC2, 0x11); // Set offsets outb(PIC1 + 1, pic1); /* remap */ outb(PIC2 + 1, pic2); /* pics */ // Set up cascade outb(PIC1 + 1, 4); /* IRQ2 -> connection to slave */ outb(PIC2 + 1, 2); // Set up interrupt mode (1 is 8086/88 mode, 2 is auto EOI) outb(PIC1 + 1, 1); outb(PIC2 + 1, 1); // Unmask interrupts outb(PIC1 + 1, 0); outb(PIC2 + 1, 0); // Ack waiting outb(PIC1, 0x20); outb(PIC2, 0x20); } pub unsafe fn init_idt() { IDT[0x0].set_func(e0_zero_divide); IDT[0x1].set_func(e1_debug); ...... IDT[0x14].set_func(e14_virtualization); IDT[0x1E].set_func(e1E_security); IDT[0x20].set_func(irq0); IDT[0x21].set_func(irq1); ...... IDT[0x2E].set_func(irq14); IDT[0x2F].set_func(irq15); setup_pic(0x20, 0x28); let idt_ptr: *const IdtEntry = IDT.as_ptr(); let limit = (IDT.len() * core::mem::size_of::<IdtEntry>() - 1) as u16; load_idt(idt_ptr, limit); }
      
      





IDTR GDTR — . STI — — , , ASCII- -.







 global load_idt section .text idtr dw 0 ; For limit storage dd 0 ; For base storage load_idt: mov eax, [esp + 4] mov [idtr + 2], eax mov ax, [esp + 8] mov [idtr], ax lidt [idtr] sti ret
      
      





あとがき



さて、この記事は非常に膨大であることが判明したため、次回はメモリの初期化と管理について説明します。setup_pd関数をコードの端にフックしましたが、次の呼び出しのためにその目的とデバイスに関する話を残します。コンテンツの改善点をコードに書くことをためらわないでください。







ソースコードはまだGitLabで入手できます







ご清聴ありがとうございました!







UPD:パート3








All Articles