ビデオモード初期化とプロテクトモードへの移行
カーネル起動処理シリーズのパート3です。前回のパートでは、set_video
ルーチンをmain.c.から呼び出す直前までを扱いました。今回は、次の内容を見ていきます。
- カーネルセットアップコードにおけるビデオモードの初期化
- プロテクトモードに切り替える前の準備
- プロテクトモードへの移行
注 プロテクトモードについてよく知らない場合は、前回のパートの内容を見てください。また、参考になるリンクも同ページに掲載しています。
上にも書いたように、arch/x86/boot/video.cソースコードファイルに定義されたset_video
関数から始めましょう。内容を見ると、まずビデオモードをboot_params.hdr
構造体から取得することから始まるのがわかります。
u16 mode = boot_params.hdr.vid_mode;
これはcopy_boot_params
関数内で値を定義したものになります(前回の投稿で詳しく説明しています)。vid_mode
は、ブートローダによって入力される必須フィールドです。関連する情報はカーネル起動プロトコルにあります。
Offset Proto Name Meaning /Size 01FA/2 ALL vid_mode Video mode control
linuxカーネル起動プロトコルには、このように記述されています。
vga=<mode> <mode> here is either an integer (in C notation, either decimal, octal, or hexadecimal) or one of the strings "normal" (meaning 0xFFFF), "ext" (meaning 0xFFFE) or "ask" (meaning 0xFFFD). This value should be entered into the vid_mode field, as it is used by the kernel before the command line is parsed.
訳:
ここには1つの整数型(C言語記法では、10進数、8進数、16進数のいずれか)または、”normal”(0xFFFFの意)、”ext” (0xFFFEの意)、”ask” (0xFFFDの意)のどれかの文字列が入ります。この値は、コマンドライン分析の前にカーネルが使うため、vid_modeフィールドに入力します。
そこで、GRUBまたはその他のブートローダ設定ファイルにvga
オプションを追加して、それをカーネルコマンドラインに渡します。このオプションは上記の説明通り、様々な値を持つことができます。例えば、整数0xFFFD
やask
などです。ask
をvga
に渡すと、下図のようなメニューが表示されます。
ここでビデオモードを選択しなければなりません。これからその実装に移りますが、その前に他に確認すべき点が幾つかあります。
カーネルデータタイプ
以前、カーネルセットアップコードにおけるu16
などの様々なデータタイプの定義について述べました。カーネルが提供するデータタイプを見てみましょう。
Type | char | short | int | long | u8 | u16 | u32 | u64 |
---|---|---|---|---|---|---|---|---|
Size | 1 | 2 | 4 | 8 | 1 | 2 | 4 | 8 |
カーネルのソースコードを読んでいると、これらを目にする機会は非常に多いので、覚えておくとよいでしょう。
ヒープ API
set_video
関数で、vid_mode
をboot_params.hdr
から取得した後は、RESET_HEAP
関数の呼び出しを見ていきます。RESET_HEAP
は、boot.hに次のように定義されているマクロです。
#define RESET_HEAP() ((void *)( HEAP = _end ))
パート2を読んだ人は、ヒープをinit_heap
関数で初期化したのを覚えているでしょう。幾つか、ヒープのためのユーティリティ関数がboot.h
に定義されています。
#define RESET_HEAP()
上記のコードで示したように、これはHEAP
変数を_end
と等しい値に設定することによってヒープをリセットします。ここでは、_end
はextern char _end[];
です。
次はGET_HEAP
マクロです。
#define GET_HEAP(type, n) \ ((type *)__get_heap(sizeof(type),__alignof__(type),(n)))
このマクロは、ヒープを割り当てるためのものです。内部関数__get_heap
を次の3つのパラメータで呼び出します。
- 割り当てる必要のある、タイプのサイズをバイト数で表した値
- このタイプの変数がどう配置されるかを表示する
__alignof__(type)
- 割り当てられるアイテム数を意味する
n
__get_heap
の実装は以下のとおりです。
static inline char *__get_heap(size_t s, size_t a, size_t n) { char *tmp; HEAP = (char *)(((size_t)HEAP+(a-1)) & ~(a-1)); tmp = HEAP; HEAP += s*n; return tmp; }
また、使用例を以下に示します。
saved.data = GET_HEAP(u16, saved.x * saved.y);
それでは、__get_heap
の動きを説明します。ここにあるHEAP
( RESET_HEAP()
の後の_end
と同等です)は、a
パラメータによって配置されたメモリのアドレスです。この後、メモリアドレスをHEAP
からtmp
変数に保存し、HEAP
を割り当てられたブロックの末尾に移動させ、割り当てメモリの開始アドレスであるtmp
を返します。
そしてこれが、最後の関数です。
static inline bool heap_free(size_t n) { return (int)(heap_end - HEAP) >= (int)n; }
HEAP
の値をheap_end
から減算し(前回のパートで計算しました)、n
個だけ保存するのに十分なメモリがあれば、1を返します。
以上です。このシンプルなヒープ用APIで、ビデオモードをセットアップします。
ビデオモードのセットアップ
さて、ビデオモードを初期化できるようになりました。前回の投稿ではset_video
関数内のRESET_HEAP()
の呼び出しのところまで説明しました。次は、store_mode_params
の呼び出しです。この関数は、include/uapi/linux/screen_info.hに定義されているboot_params.screen_info
構造体にビデオモードパラメータを格納します。
store_mode_params
関数を見ると、store_cursor_position
関数の呼び出しから始まっています。関数の名称から分かるように、カーソルの情報を取得して格納します。
最初にstore_cursor_position
は、biosregs
型の2つの変数をAH = 0x3
で初期化し、0x10
BIOS割り込みを呼び出します。割り込みが成功すると、DL
とDH
レジスタに行と列を返します。行と列は、boot_params.screen_info
構造体のorig_x
とorig_y
フィールドに格納されます。
store_cursor_position
を実行すると、store_video_mode
関数が呼び出されます。 単純に進行中のビデオモードを取得し、boot_params.screen_info.orig_video_mode
に格納します。
この後、この関数は進行中のビデオモードをチェックし、video_segment
を設定します。BIOSがブートセクタに制御を移した後、次のアドレスをビデオメモリに使います。
0xB000:0x0000 32 Kb Monochrome Text Video Memory 0xB800:0x0000 32 Kb Color Text Video Memory
現在のビデオモードがMDA、HGC、VGAのモノクロモードの場合は、video_segment
変数を0xB000
に設定し、カラーモードの場合は、0xB800
に設定します。ビデオセグメントのアドレスをセットアップした後は、次のようにしてフォントサイズをboot_params.screen_info.orig_video_points
に格納する必要があります。
set_fs(0); font_size = rdfs16(0x485); boot_params.screen_info.orig_video_points = font_size;
最初に、set_fs
関数で、FS
レジスタに0を代入します。set_fs
のような関数は前回のパートで既に見てきました。これらは全てboot.hに定義されています。それから、アドレス0x485
(このメモリ位置はフォントサイズの取得に使います)に存在する値を読み、フォントサイズをboot_params.screen_info.orig_video_points
に保存します。
x = rdfs16(0x44a); y = (adapter == ADAPTER_CGA) ? 25 : rdfs8(0x484)+1;
次に、アドレス0x44a
で、まとまった量の列を、アドレス0x484
で行を取得し、boot_params.screen_info.orig_video_cols
とboot_params.screen_info.orig_video_lines
に格納します。これで、store_mode_params
の実装は完了です。
続いて、スクリーンコンテンツをヒープに保存するsave_screen
関数です。この関数は、行や列数など、先の関数で取得した全てのデータを収集し、saved_screen
構造体に格納します。定義は次の通りです。
static struct saved_screen { int x, y; int curx, cury; u16 *data; } saved;
また次の通り、ヒープがそのデータのための空きスペースを持っているかどうかをチェックします。
if (!heap_free(saved.x*saved.y*sizeof(u16)+512)) return;
ヒープ内に十分な空きがあれば、スペースを割り当て、その中にsaved_screen
を格納します。
次に、arch/x86/boot/video-mode.cからprobe_cards(0)
が呼び出されます。この関数は全てのvideo_cards
を巡回し、カードが提供するモードの数を集めます。ここで面白いのは、ループがあることです。
for (card = video_cards; card < video_cards_end; card++) { /* collecting number of modes here */ }
しかし、video_cards
はどこにも宣言されていません。答えはシンプルです。x86カーネルセットアップコードにおける全てのビデオモードは次のように定義されています。
static __videocard video_vga = { .card_name = "VGA", .probe = vga_probe, .set_mode = vga_set_mode, };
ここで、__videocard
はマクロです。
#define __videocard struct card_info __attribute__((used,section(".videocards")))
つまり、以下のcard_info
構造体
struct card_info { const char *card_name; int (*set_mode)(struct mode_info *mode); int (*probe)(void); struct mode_info *modes; int nmodes; int unsafe; u16 xmode_first; u16 xmode_n; };
が、.videocards
セグメントに含まれます。それではarch/x86/boot/setup.ldリンカファイルの内部を見てみましょう。
.videocards : { video_cards = .; *(.videocards) video_cards_end = .; }
つまり、video_cards
は単なるメモリのアドレスで、全てのcard_info
構造体はこのセグメントに置かれているわけです。また、全てのcard_info
構造体がvideo_cards
とvideo_cards_end
の間にあるということも意味するので、これを使ってループすることで全ての構造体を得られます。probe_cards
の実行の後には、nmodes
(ビデオモードの数)が代入済みのstatic __videocard video_vga
のような構造体全てを得ることができます
probe_cards
の実行が済むと、set_video
関数内のメインループに移ります。そこには、無限ループがあり、set_mode
関数でビデオモードをセットアップしようとします。あるいは、vid_mode=ask
をカーネルコマンドラインに渡すか、ビデオモードが定義されていない場合は、メニューを表示します。
set_mode
関数はvideo-mode.cに定義されており、ただ1つのパラメータ、mode
を取得します。これはビデオモードの数です(この値は、メニューから取得するか、setup_video
の起動時にカーネルセットアップヘッダから取得します)。
set_mode
関数はmode
を確認し、raw_set_mode
関数を呼び出します。、この関数には、card_info
構造体からアクセスできます。各ビデオモードはこの構造体をビデオモードによって入力された値(例えば、vga
には、video_vga.set_mode
。上の例、vga
のためのcard_info
構造体を参照)で定義します。video_vga.set_mode
は、vga_set_mode
で、vgaモードをチェックし、それぞれの関数を呼び出します。
static int vga_set_mode(struct mode_info *mode) { vga_set_basic_mode(); force_x = mode->x; force_y = mode->y; switch (mode->mode) { case VIDEO_80x25: break; case VIDEO_8POINT: vga_set_8font(); break; case VIDEO_80x43: vga_set_80x43(); break; case VIDEO_80x28: vga_set_14font(); break; case VIDEO_80x30: vga_set_80x30(); break; case VIDEO_80x34: vga_set_80x34(); break; case VIDEO_80x60: vga_set_80x60(); break; } return 0; }
ビデオモードをセットアップする各関数は、ただ0x10
BIOS割り込みを、AH
レジスタの値で呼び出すだけです。
ビデオモードを設定した後、それをboot_params.hdr.vid_mode
に渡します。
次にvesa_store_edid
を呼び出します。この関数は単純に、カーネルが使用するEDID情報を格納します。この後、store_mode_params
が再び呼び出されます。最後に、do_restore
が設定されていれば、スクリーンは前の状態に復元されます。
この後、ビデオモードを設定し、プロテクトモードに切り替えられるようになります。
プロテクトモード移行前の最終準備
では、main.cの末尾にあるgo_to_protected_mode
の呼び出しについて説明します。コメントにDo the last things and invoke protected mode
とあるように、これらの最後の作業を確認し、プロテクトモードに切り替えましょう。
go_to_protected_mode
はarch/x86/boot/pm.cに定義されています。この関数には、プロテクトモードに飛び込む前の最終準備を行う幾つかの関数が含まれています。では、コードを見て、何を行うのか、どう機能するのかを理解しましょう。
初めに、go_to_protected_mode
内のrealmode_switch_hook
関数の呼び出しです。この関数はリアルモードスイッチフックがあればそれを動かし、NMIを無効化します。フックはブートローダが標準以外の環境で稼動するときに使われます。フックについてもっと知りたい人は、boot protocol のADVANCED BOOT LOADER HOOKSの項を読んでください。
realmode_switch
フックは16ビットリアルモードへのポインタを提示し、マスク不可割り込みを無効化するサブルーチンを呼び出します。realmode_switch
フック(私のために存在するわけではありません)が確認されると、マスク不可割り込み(NMI)の無効化が起こります。
asm volatile("cli"); outb(0x80, 0x70); /* Disable NMI */ io_delay();
まず、割り込みフラグ(IF
)をクリアにするcli
命令を伴う、インラインアセンブリ命令があります。その後、外部割り込みが無効化されます。次の行がNMIを無効にします。
割り込みは、ハードウェアやソフトウェアから出されるCPUへのシグナルです。シグナルを取得すると、CPUは実行中の命令シークエンスを中断し、その状態を保存してコントロールを割り込みハンドラに移します。割り込みハンドラが処理を終えると、コントロールを割り込み前の命令に移します。NMIは、常に許可なしで独自に進行する割り込みです。無視することはできず、復旧不可能なハードウェアエラーのためのシグナルに使われるのが普通です。割り込みについてここで詳細を述べるのは控えますが、次の投稿で取り上げます。
コードに戻りましょう。2行目で0x80
(無効化されたビット)バイトから0x70
(CMOSアドレスレジスタ)バイトまでの範囲に書き込みを実行していることが分かります。その後、io_delay
関数の呼び出しが起こります。io_delay
は小さな遅延を作り出すもので、次のように記述されます。
static inline void io_delay(void) { const u16 DELAY_PORT = 0x80; asm volatile("outb %%al,%0" : : "dN" (DELAY_PORT)); }
何らかのバイトをポート0x80
に出力すると、ちょうど1マイクロ秒遅延します。そこで0x80
ポートに、どのような値(この場合は、AL
レジスタの値)でも書くことができます。この遅延の後、realmode_switch_hook
関数の実行が完了すると、次の関数に進むことができます。
次の関数はA20ラインを有効にするenable_a20
です。この関数はarch/x86/boot/a20.cに定義されており、様々なメソッドでA20ゲートを有効化しようとします。1つ目はa20_test_short
関数で、a20_test
関数によってA20がすでに有効化されているか否かをチェックします。
static int a20_test(int loops) { int ok = 0; int saved, ctr; set_fs(0x0000); set_gs(0xffff); saved = ctr = rdfs32(A20_TEST_ADDR); while (loops--) { wrfs32(++ctr, A20_TEST_ADDR); io_delay(); /* Serialize and make delay constant */ ok = rdgs32(A20_TEST_ADDR+0x10) ^ ctr; if (ok) break; } wrfs32(saved, A20_TEST_ADDR); return ok; }
最初にFS
レジスタに0x0000
を入力し、GS
レジスタに0xffff
を入力します。次に、アドレスA20_TEST_ADDR
(0x200
です)の値を読み、この値をsaved
変数とctr
に入力します。
それから、更新したctr
の値をwrfs32
関数を使ってfs:gs
に書き込みます。1マイクロ秒の遅延が出た後、アドレスA20_TEST_ADDR+0x10
によってGS
レジスタから値を読みます。もしゼロでない場合は、既にA20ラインが有効になっています。A20が無効だった場合は、a20.c
にある別のメソッドで有効化を試みます。例えば、AH=0x2041
などで、0x15
BIOS割り込みを呼び出します。
enabled_a20
関数が失敗に終わった場合は、エラーメッセージを表示させ、関数die
を呼び出します。今回の作業を開始した最初のソースコードファイル – arch/x86/boot/header.Sを思い出す人もいるでしょう。
die: hlt jmp die .size die, .-die
A20ゲートの有効化に成功すると、reset_coprocessor
関数が呼び出されます。
outb(0, 0xf0); outb(0, 0xf1);
この関数は、0xf0
に0
を記述して、数値演算コプロセッサのデータを消去し、0xf1
に0
を記述してリセットさせます。
続いて、mask_all_interrupts
関数が呼び出されます。
outb(0xff, 0xa1); /* Mask all interrupts on the secondary PIC */ outb(0xfb, 0x21); /* Mask all but cascade on the primary PIC */
これは、主要PICのIRQ2
を除く 補助PIC(Programmable Interrupt Controller、プログラム可の割り込みコントローラ)と主要PICにおける全ての割り込みをマスクします。
これらの準備が全て終わると、実際にプロテクトモードに移行できます。
割り込みディスクリプタテーブル(IDT)のセットアップ
ではいよいよ、割り込みディスクリプタテーブル(IDT)のセットアップに入ります。setup_idt
を以下に示します。
setup_idt: static setup_idt() { static const struct gdt_ptr null_idt = {, }; volatile(lidtl 0 ::(null_idt)); }
これでIDT(割り込みハンドラなどを示すもの)がセットアップされました。この時点ではIDTをまだインストールしていませんが(後でやります)、IDTをlidtl
命令で読み込みました。null_idt
にはIDTのアドレスとサイズを格納しますが、今のところは0が入っています。null_idt
はgdt_ptr
型の構造体です。定義を以下に示します。
struct gdt_ptr { u16 len; u32 ptr; } __attribute__((packed));
上記のコードは、IDTの長さ(len
)は16ビット、IDTへのポインタは32ビットでそれぞれ表すことを示しています(IDTと割り込みの詳細は、次回の投稿で詳しく説明します)。また、__attribute__((packed))
は、gdt_ptr
のサイズが必要最小限であることを示しています。従って、gdt_ptr
のサイズは6バイト、即ち48ビットです。(以下で、 GDTR
レジスタに gdt_ptr
へのポインタを読み込む処理を説明します。前回の投稿でgdt_ptr
のサイズは48ビットだと説明したことをここで思い出した方もいるでしょう。)
グローバルディスクリプタテーブル(GDT)のセットアップ
続いて、グローバル記述子テーブル(GDT)もセットアップします。GDTをセットアップするのはsetup_gdt
関数です(詳細はカーネル起動処理 パート2を参照してください)。この関数には boot_gdt
配列の定義が含まれますが、配列には3つのセグメントの定義が含まれます。
static const u64 boot_gdt[] __attribute__((aligned())) = { [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, , 0xfffff), [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, , 0xfffff), [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, , ), };
3つのセグメントとは、コード、データ、タスクステートセグメント(TSS)です。セグメントのうち、TSSは今回使いません。しかしTSSがここに追加されているのは、コメント行に書かれている通り、Intel VTを満足させるためです(Intel VTに興味がある方は、この記事に詳細が書かれているので参照してください)。ではboot_gdt
を見ていきましょう。まず、ここに__attribute__((aligned(16)))
属性が含まれていることに注意してください。つまり、この構造体は16バイト単位で整列されるということです。簡単な例を以下に示します。
#include stdio.h struct aligned { a; }__attribute__((aligned())); struct nonaligned { b; }; () { struct aligned a; struct nonaligned na; printf(Not aligned - , sizeof(na)); printf(Aligned - , sizeof(a)); return ; }
技術的な観点からいえば、 int
フィールドを一つ含む構造体は4バイトでなければなりませんが、ここで示したaligned
構造体は16バイトです。
$ gcc test.c -o test && test Not aligned - 4 Aligned - 16
GDT_ENTRY_BOOT_CS
のインデックスの値はここでは- 2で、GDT_ENTRY_BOOT_DS
にはGDT_ENTRY_BOOT_CS + 1
などの値が格納されます。初期値は2です。先頭はnullディスクリプタ(index – 0)で、これは必須です。また、2番目は未使用(index – 1)です。
GDT_ENTRY
は、フラグ、ベース、上限の値を取ってGDTエントリを構築するマクロです。例えば、コードセグメントのエントリを見てみましょう。GDT_ENTRY
には以下の値が格納されています。
- ベース – 0
- 上限 – 0xfffff
- フラグ – 0xc09b
これは何を意味するのでしょう。セグメントのベースアドレスは0、上限(セグメントのサイズ)は0xffff
(1 MB)です。ここでフラグを見てみましょう。フラグの値は0xc09b
です。これをバイナリで表すと、
1100 0000 1001 1011
となります。各ビットの意味は以下の通りです。左から右に向かって、順番に示します。
- 1 -(G)精度を示すビット
- 1 -(D)16ビットセグメントの場合は0、32ビットセグメントの場合は1
- 0 -(L)1の場合は64ビットモードで実行する
- 0 -(AVL)システムソフトから利用可能
- 0000 – ディスクリプタ内の19:16の4ビット
- 1 -(P)セグメントがメモリに読み込まれているかどうか
- 00 – (DPL) – 特権レベル、0が最高
- 1 -(S)コードまたはデータセグメント、システムセグメントではない
- 101 – セグメントの種類、実行/読み取り
- 1 – アクセス済みのビット
各ビットの意味の詳細は、前回の 投稿またはIntel® 64 and IA-32 Architectures Software Developer’s Manuals 3Aを参照してください。
以下のコードを実行すると、GDTの長さを取得できます。
gdt.len = sizeof(boot_gdt)-;
boot_gdt
のサイズを取得して、そこから1(GDTの末尾の有効なアドレス)を引きます。
次に以下のコードで、GDTへのポインタを取得します。
gdt.ptr = (u32)&boot_gdt + (ds() << );
ここでboot_gdt
のアドレスを取得して、データセグメントのアドレスを左に4ビットシフトしたものに、このアドレスを加えます(現在はリアルモードであることを思い出してください)。
最後にlgdtl
命令を実行して、GDTをGDTRレジスタに読み込みます。
volatile(lgdtl 0 ::(gdt));
プロテクトモードへの実際の移行
これでgo_to_protected_mode
関数は終わりです。ここまでにIDTとGDTを読み込み、割り込みを無効にしたので、次はCPUをプロテクトモードに切り替えます。最後のステップは、以下の2つのパラメータを指定してprotected_mode_jump
関数を呼び出すことです。
protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4));
この関数はarch/x86/boot/pmjump.Sで定義されています。2つのパラメータの意味は次のとおりです。
- プロテクトモードのエントリポイントのアドレス
boot_params
のアドレス
ではここから、protected_mode_jump
の内部を詳しく見ていきます。上記の通り、これはarch/x86/boot/pmjump.S
ファイル内にあります。先頭のパラメータはeax
レジスタに、次のパラメータはedx
.レジスタに格納されます。
まず、esi
レジスタ内にboot_params
のアドレスを、コードセグメントのレジスタcs
のアドレス(0x1000)をbx
に書き込みます。次に bx
の内容を4ビットシフトさせて、そこにラベル2
のアドレスを加算し(この処理の後、bx
にあるラベル2
の物理アドレスを取得します)、ラベル1
にジャンプします。続けて、データセグメントとタスクステートセグメントを、以下の通り cs
レジスタとdi
レジスタにそれぞれ置きます。
$__BOOT_DS, % $__BOOT_TSS, %
上記から分かる通り、GDT_ENTRY_BOOT_CS
のインデックス値は2なので、GDTの各エントリは8バイト、従ってCS
は2 * 8 = 16
、__BOOT_DS
は24、のようになります。
次に、CR0
コントロールレジスタ内のProtection Enable(PE
)ビットを次の通りセットして、
%, % $X86_CR0_PE, % %, %
プロテクトモードへ大きくジャンプします。
.byte , :.long in_pm32 .word __BOOT_CS
ここで
0x66
は、16ビットと32ビットのコードを併用できるオペランドサイズプリフィクス、0xea
はジャンプのオペコード、in_pm32
はセグメントのオフセット、__BOOT_CS
はコードセグメントを、それぞれ示します。
これでプロテクトモードに移行しました。
code32 .section .text32,
プロテクトモードの最初のステップを詳しく見ていきます。まず、データセグメントを以下のようにセットアップします。
movl %ecx, %ds movl %ecx, %es movl %ecx, %fs movl %ecx, %gs movl %ecx, %ss
ここで、 cx
レジスタに$__BOOT_DS
を格納したことを思い出しましょう。これで、cs
以外の全てのセグメントレジスタに値が格納されました(cs
には既に__BOOT_CS
が格納されています)。次に、eax
以外の汎用レジスタ全てに0を格納します。
xorl %ecx, %ecx xorl %edx, %edx xorl %ebx, %ebx xorl %ebp, %ebp xorl %edi, %edi
最後に、32ビットのエントリポイントにジャンプします。
jmpl *%eax
eax
には32ビットエントリのアドレスが格納されていることに注意してください(この値を最初のパラメータとして protected_mode_jump
に渡したためです)。
以上です。プロテクトモードに入って、そのエントリポイントまでを説明しました。この続きは次の投稿で説明します。
結論
Linuxカーネル内部の解説、パート3は以上です。次回の投稿では、プロテクトモードの最初のステップを詳しく解説してから、 ロングモードへの移行についても触れる予定です。
この記事について質問や提案があれば、どうぞ遠慮なくこちらのTwitterアカウントにコメントまたはメッセージを送ってください。
ただし英語は私の母語ではないので、コミュニケーションに多少不便があるかもしれませんが、どうぞご理解ください。記事に誤りを見つけた場合は、訂正内容を添えて linux-internalsにプルリクエストを送ってください。