POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook
0xAX

本記事は、原著者の許諾のもとに翻訳・掲載しております。

ビデオモード初期化とプロテクトモードへの移行

カーネル起動処理シリーズのパート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 に渡すと、下図のようなメニューが表示されます。

video mode setup menu
ここでビデオモードを選択しなければなりません。これからその実装に移りますが、その前に他に確認すべき点が幾つかあります。

カーネルデータタイプ

以前、カーネルセットアップコードにおける 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 protocolADVANCED 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 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。