2015年12月9日
Linux Insides : カーネル起動プロセス part3
本記事は、原著者の許諾のもとに翻訳・掲載しております。
ビデオモード初期化とプロテクトモードへの移行
カーネル起動処理シリーズのパート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 にプルリクエストを送ってください。
リンク
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa