Linux Insides : カーネル起動プロセス part2

カーネルセットアップの第一歩

前回のパートでは、Linuxカーネルの内部について探り始め、カーネルをセットアップするコードの最初の部分を見ていきました。前回の投稿はarch/x86/boot/main.c内のmain関数(C言語で書かれた最初の関数)を呼び出すところまで確認しました。
このパートでは、引き続きカーネルのセットアップコードについて調査し、併せて以下の内容も学びます。

  • protected mode(プロテクトモード)の概要
    * プロテクトモードに移行するための準備
  • ヒープとコンソールの初期化
  • メモリの検出、CPUの検証、キーボードの初期化
  • その他もろもろ

それでは始めていきましょう。

プロテクトモード

ネイティブのIntel64のロングモードに移行する前に、カーネルはCPUをプロテクトモードに切り替える必要があります。

では、このプロテクトモードとは何でしょう? プロテクトモードが最初にx86アーキテクチャに追加されたのは1982年のことです。 このモードは80286プロセッサが出てから、Intel 64とロングモードが登場するまでの間、主要なモードでした。

リアルモードから移行した主な理由は、RAMへのアクセスが非常に限られていたからです。前回のパートでもお話ししましたが、リアルモードで利用可能なRAMはせいぜい220バイトか1メガバイトで、中には640キロバイトしかないものもあります。

プロテクトモードになって、さまざまな点が変わりましたが、その中で最も大きな変更点はメモリの管理です。アドレスバスが20ビットから32ビットに変更されたことで、リアルモードでは1メガバイトのメモリにしかアクセスできなかったのが、4ギガバイトのメモリにアクセスが可能になりました。さらにプロテクトモードは、ページング方式にも対応しています。こちらの内容は、次のセクションで紹介します。

プロテクトモードにおけるメモリ管理は、ほぼ独立した次の2つの方式に分かれます。

  • セグメント方式
  • ページング方式

ページング方式については次のセクションで扱うので、ここではセグメント方式についてのみ紹介しましょう。

前回のパートで、リアルモードではアドレスが以下の2つの部分から構成されていることを学びました。

  • セグメントのベースアドレス
  • セグメントのベースからのオフセット

そして、これらの2つの情報が分かれば、次のように物理アドレスを取得できます。

PhysicalAddress = Segment * 16 + Offset

プロテクトモードになって、メモリのセグメンテーションが一新され、64キロバイトの固定サイズのセグメントがなくなりました。その代わりに、各セグメントのサイズと位置は、セグメントディスクリプタと呼ばれる関連データの構造体で表現されます。このセグメントディスクリプタが格納されているのが、Global Descriptor Table(GDT)というデータ構造体です。
GDTはメモリ上にある構造体で、これはメモリ内の決まった場所にあるわけではなく、専用のGDTRレジスタにアドレスが格納されています。LinuxカーネルのコードでGDTを読み込む方法については、後ほど説明します。GDTは以下のような操作で、メモリに読み込まれます。

lgdt gdt

このlgdt命令で、GDTRレジスタにベースアドレスとグローバルディスクリプタテーブルの制限(サイズ)を読み込みます。GDTRは48ビットのレジスタで、以下の2つの部分から構成されています。

  • グローバルディスクリプタテーブルのサイズ(16ビット)
  • グローバルディスクリプタテーブルのアドレス(32ビット)

先ほど説明したように、GDTにはメモリセグメントを表すsegment descriptorsが含まれています。各ディスクリプタのサイズは64ビットで、ディスクリプタの一般的な配置は次のようになっています。

31          24        19      16              7            0
------------------------------------------------------------
|             | |B| |A|       | |   | |0|E|W|A|            |
| BASE 31:24  |G|/|L|V| LIMIT |P|DPL|S|  TYPE | BASE 23:16 | 4
|             | |D| |L| 19:16 | |   | |1|C|R|A|            |
------------------------------------------------------------
|                             |                            |
|        BASE 15:0            |       LIMIT 15:0           | 0
|                             |                            |
------------------------------------------------------------

リアルモードの後でこれを見ると、少しゾッとするかもしれませんが、簡単なのでご心配なく。例えば、LIMIT 15:0というのは、ディスクリプタのビット0 – 15に制限の値が含まれていることを意味します。そして残りはLIMIT 19:16の中にあります。よってリミットのサイズは0 – 19なので、20ビットです。では、もう少し詳しく見てみましょう。

1. リミット[20ビット]は0 – 15と16 – 19のビットにあります。これはlength_of_segment – 1を定義し、G(粒度)ビットに依存します。

  • G(ビット55)とセグメントリミットが0の場合、セグメントのサイズは1バイトです。
  • Gが1でセグメントリミットが0の場合、セグメントのサイズは4096バイトです。
  • Gが0でセグメントリミットが0xfffffの場合、セグメントのサイズは1メガバイトです。
  • Gが1でセグメントリミットが0xfffffの場合、セグメントのサイズは4ギガバイトです。

    つまり以下のようになります。

  • Gが0なら、リミットは1バイト単位と見なされ、セグメントの最大サイズは1メガバイトになります。

  • Gが1なら、リミットは4096バイト = 4キロバイト = 1ページ単位と見なされ、セグメントの最大サイズは4ギガバイトになります。実際、Gが1なら、リミットの値は12ビット分左にずれます。つまり20ビット + 12ビットで32ビット、すなわち232 = 4ギガバイトになります。

2. ベース[32ビット]は(0 – 15、32 – 39、56 – 63ビット)にあり、これはセグメントの開始位置の物理アドレスを定義します。

3. タイプ/属性(40 – 47ビット)はセグメントのタイプとセグメントに対する種々のアクセスについて定義します。

  • ビット44のsフラグはディスクリプタのタイプを指定します。sが0ならこのセグメントはシステムセグメントで、sが1ならコードまたはデータのセグメントになります(スタックセグメントはデータセグメントで、これは読み書き可能なセグメントである必要があります)。

このセグメントがコードとデータ、どちらのセグメントなのかを判別するには、以下の図で0と表記されたEx(ビット43)属性を確認します。これが0ならセグメントはデータセグメントで、1ならコードセグメントになります。

セグメントは以下のいずれかのタイプになります。

|           Type Field        | Descriptor Type | Description
|-----------------------------|-----------------|------------------
| Decimal                     |                 |
|             0    E    W   A |                 |
| 0           0    0    0   0 | Data            | Read-Only
| 1           0    0    0   1 | Data            | Read-Only, accessed
| 2           0    0    1   0 | Data            | Read/Write
| 3           0    0    1   1 | Data            | Read/Write, accessed
| 4           0    1    0   0 | Data            | Read-Only, expand-down
| 5           0    1    0   1 | Data            | Read-Only, expand-down, accessed
| 6           0    1    1   0 | Data            | Read/Write, expand-down
| 7           0    1    1   1 | Data            | Read/Write, expand-down, accessed
|                  C    R   A |                 |
| 8           1    0    0   0 | Code            | Execute-Only
| 9           1    0    0   1 | Code            | Execute-Only, accessed
| 10          1    0    1   0 | Code            | Execute/Read
| 11          1    0    1   1 | Code            | Execute/Read, accessed
| 12          1    1    0   0 | Code            | Execute-Only, conforming
| 14          1    1    0   1 | Code            | Execute-Only, conforming, accessed
| 13          1    1    1   0 | Code            | Execute/Read, conforming
| 15          1    1    1   1 | Code            | Execute/Read,
conforming, accessed

ご覧のように最初のビット(ビット43)は、データセグメントの場合は0で、コードセグメントの場合は1です。続く3つのビット(40, 41,42)はEWA(エキスパンド、書き込み可能、アクセス可能)またはCRA(コンフォーミング、読み取り可能、アクセス可能)のどちらかになります。

  • E(ビット42)が0ならエキスパンドアップし、1ならエキスパンドダウンします。詳細はこちらを参照してください。
  • W(ビット41)(データセグメントの場合)が1なら書き込みアクセスが可能、0なら不可です。データセグメントでは、読み取りアクセスが常に許可されている点に注目してください。
  • A(ビット40)はプロセッサからセグメントへのアクセス可能か否かを示します。
  • C(ビット42)(コードセレクタの場合)はコンフォーミングビットです。Cが1なら、ユーザレベルなどの下位レベルの権限から、セグメントコードを実行することが可能です。Cが0なら、同じ権限レベルからのみ実行可能です。
  • R(ビット41)(コードセグメントの場合)が1なら、セグメントへの読み取りアクセスが可能、0なら不可です。コードセグメントに対して、書き込みアクセスは一切できません。

1. DPL2ビットはビット45 – 46にあります。これはセグメントの特権レベルを定義し、値は0-3で、0が最も権限があります。

2. Pフラグ(ビット47)は、セグメントがメモリ内にあるか否かを示します。Pが0なら、セグメントは無効であることを意味し、プロセッサはこのセグメントの読み取りを拒否します。

3. AVLフラグ(ビット52)は利用可能な予約ビットで、Linuxにおいては無視されます。

4. Lフラグ(ビット53)は、コードセグメントがネイティブ64ビットコードを含んでいるかを示します。1ならコードセグメントは64ビットモードで実行されます。

5. D/Bフラグ(ビット54)は、デフォルト/ビッグフラグで、例えば16/32ビットのようなオペランドのサイズを表します。フラグがセットされていれば32ビット、そうでなければ16ビットです。

セグメントレジスタには、リアルモードとのようにセグメントのベースアドレスが含まれていません。その代わりにSegment Selectorという特殊な構造があり、各セグメントディスクリプタは関連するセグメントセレクタを含んでいます。Segment Selectorは16ビットの構造体です。

-----------------------------
|       Index    | TI | RPL |
-----------------------------
  • IndexがGDTにおけるディスクリプタのインデックス番号を示します。
  • TI(Table Indicator)はディスクリプタを探す場所を示します。0ならば、Global Descriptor Table(GDT)内を検索し、そうでない場合は、Local Descriptor Table(LDT)内を調べます。
  • RPLは、Requester’s Privilege Levelのことです。

各々のセグメントレジスタは見える部分と隠れた部分を持っています。

  • Visible – セグメントセレクタはここに保存されています。
  • Hidden – セグメントディスクリプタ(ベース、制限、属性、フラグ)

以下は、プロテクトモードにおいて物理アドレスを取得するのに要する手順です。

  • セグメントセレクタはセグメントレジスタの1つにロードしなければなりません。
  • CPUは、GDTアドレスとセレクタのIndexによってセグメントディスクリプタを特定し、ディスクリプタをセグメントレジスタのhidden部分にロードしようとします。
  • ベースアドレス(セグメントディスクリプタから)+オフセットは、物理アドレスであるセグメントのリニアアドレスになります。(ページングが無効の場合)

図で表すとこうなります。

linear address
リアルモードからプロテクトモードへ移行するためのアルゴリズムは、

  • 割り込みを無効にします。
  • lgdtインストラクションでGDTを記述、ロードします。
  • CR0(コントロールレジスタ0)におけるPE(Protection Enable、プロテクト有効化)ビットを設定します。
  • プロテクトモードコードにジャンプします。

次の章でlinuxカーネルの完全なプロテクトモードへの移行をします。ただ、プロテクトモードへ移る前にもう少し準備が必要です。

arch/x86/boot/main.cを見てみましょう。キーボード初期化、ヒープ初期化などを実行するルーティンがあるのが目につきます。よく見てみましょう。

ブートパラメータを”zeropage”にコピーする

“main.c”のmainルーティンから始めましょう。mainの中で最初に呼び出される関数はcopy_boot_params(void)です。これは、カーネル設定ヘッダを、arch/x86/include/uapi/asm/bootparam.hにて定義されたboot_params構造体のフィールドにコピーします。

boot_params構造体は、struct setup_header hdrフィールドを内包しています。この構造体はlinuxブートプロトコルで定義されているのと同じフィールドを内包し、ブートローダによって、カーネルのコンパイル/ビルド時に書き込まれます。copy_boot_paramsは2つのことを実行します。

1. hdrheader.Sからsetup_headerフィールド内のboot_params構造体へコピーする。

2. カーネルが古いコマンドラインプロトコルでロードされた場合に、ポインタをカーネルのコマンドラインに更新する。

hdrを、copy.Sソースファイルで定義されているmemcpy関数と一緒にコピーしていることに注目してください。中身を見てみましょう。

GLOBAL(memcpy)
    pushw   %si
    pushw   %di
    movw    %ax, %di
    movw    %dx, %si
    pushw   %cx
    shrw    $2, %cx
    rep; movsl
    popw    %cx
    andw    $3, %cx
    rep; movsb
    popw    %di
    popw    %si
    retl
ENDPROC(memcpy)

そう、Cコードに移ってきたばかりですが、またアセンブリに逆戻りです。:) まず初めに、memcpyとここで定義されている他のルーティンが2つのマクロで挟まれており、GLOBALで始まってENDPROCで終わっているのに気づきます。GLOBALは、globlのディレクティブとそのためのラベルを定義するarch/x86/include/asm/linkage.hに記述されています。ENDPROCは、nameシンボルを関数名としてマークアップしnameシンボルのサイズで終わるinclude/linux/linkage.hに記述されています。

memcpyの実装は簡単です。まず、sidiレジスタから値をスタックにプッシュします。それらの値はmemcpyの実行中に変化するので、スタックにプッシュすることで値を保存するのです。memcpy(に加え、copy.S内の他の関数)はfastcall呼び出し規約を使うため、入力パラメータをaxdxそしてcxレジスタから取得します。memcpyの呼び出しは次のように表示されます。

memcpy(&boot_params.hdr, &hdr, sizeof hdr);

ですから、
* axは、boot_params.hdrのアドレスをバイトで内包する。
* dxは、hdrのアドレスをバイトで内包する。
* cxは、hdrのサイズをバイトで内包する。

memcpyboot_params.hdrのアドレスをsiに入れ、スタック上にそのサイズを保存します。この後、2サイズ右にシフト(あるいは4で除算)し、siからdiに4バイトでコピーします。この後、hdrのサイズを再びリストアし、4バイトで配列して残りのバイト(あれば)をsiからdiにバイトごとにコピーします。最後にsidiの値をスタックからリストアすると、コピーは終了です。

コンソールの初期化

hdrboot_params.hdrにコピーしたら、次のステップは、arch/x86/boot/early_serial_console.cに定義されているconsole_init関数を呼び出し、コンソールの初期化をすることです。

その関数は、earlyprintkオプションをコマンドラインから検索し、オプションが特定できたら、ポートアドレスとシリアルポートのボーレートを解析し、シリアルポートを初期化します。earlyprintkコマンドラインオプションの値は、次のうちのいずれかです。

* serial,0x3f8,115200
* serial,ttyS0,115200
* ttyS0,115200

シリアルポート初期化の後、1番目のアウトプットが得られます。

if (cmdline_find_option_bool("debug"))
        puts("early console in setup code\n");

putstty.c内に定義されています。見てのとおり、それはputchar関数を呼び出すことで、1文字1文字をループで表示します。putcharの実装を見てみましょう。

void __attribute__((section(".inittext"))) putchar(int ch)
{
    if (ch == '\n')
        putchar('\r');

    bios_putchar(ch);

    if (early_serial_base != 0)
        serial_putchar(ch);
}

__attribute__((section(".inittext")))は、このコードが.inittextセクションに入ることを意味しています。このセクションは、リンカファイルsetup.ld内にあります。

まず最初に、putchar\nシンボルをチェックし、それが見つかれば\rを先に表示します。その後、0x10の割り込み呼び出しでBIOSを呼び出し、VGAスクリーンに文字を表示させます。

static void __attribute__((section(".inittext"))) bios_putchar(int ch)
{
    struct biosregs ireg;

    initregs(&ireg);
    ireg.bx = 0x0007;
    ireg.cx = 0x0001;
    ireg.ah = 0x0e;
    ireg.al = ch;
    intcall(0x10, &ireg, NULL);
}

ここで、initregsbiosregsを引数にとり、まずmemset関数を使ってbiosregsにゼロを入力し、それからレジスタ値を入力します。

 memset(reg, 0, sizeof *reg);
    reg->eflags |= X86_EFLAGS_CF;
    reg->ds = ds();
    reg->es = ds();
    reg->fs = fs();
    reg->gs = gs();

memsetの実装を見てみましょう。

GLOBAL(memset)
    pushw   %di
    movw    %ax, %di
    movzbl  %dl, %eax
    imull   $0x01010101,%eax
    pushw   %cx
    shrw    $2, %cx
    rep; stosl
    popw    %cx
    andw    $3, %cx
    rep; stosb
    popw    %di
    retl
ENDPROC(memset)

上から読み取れるように、memcpy関数のように、fastcall呼び出し規約を使っています。つまり、関数はaxdxそしてcxレジスタからパラメータを取得しているということです。

概してmemsetは、memcpyの実装に似ています。diレジスタの値をスタックに保存し、ax値をbiosregs構造体のアドレスであるdiに置きます。次にmovzblインストラクションが、dl値をeaxレジスタの低2バイトにコピーします。eaxの高2バイトの残りにはゼロが入力されます。

次の命令はeax0x01010101をかけます。この行程が必要なのは、memsetが同時に4バイトをコピーするためです。例えば、memsetを使って構造体のフィールドすべてを0x7で埋めたいとします。この場合、eax0x00000007値を含みます。そこでeax0x01010101をかけると、0x07070707を取得してこれら4バイトを構造体にコピーできるのです。memsetは、eaxes:diにコピーする際、rep; stoslインストラクションを使います。

memset関数がその他に行うことは、memcpyとほぼ同じです。

biosregs構造体がmemsetにより値を満たされると、bios_putcharは、文字を表示する0x10割り込みを呼び出します。次に、シリアルポートが初期化されたか否かをチェックし、そこにserial_putcharと、もし設定がされていればinb/outbインストラクションで文字を書き出します。

ヒープの初期化

スタックとbssセクションがheader.S(前のパート参照)に準備できたら、カーネルはinit_heap関数を使ってヒープを初期化する必要があります。

まずinit_heapはカーネル設定ヘッダにあるloadflagsからCAN_USE_HEAP フラグをチェックし、フラグが下記のように設定されている場合はスタックの終わりを計算します。

char *stack_end;

    if (boot_params.hdr.loadflags & CAN_USE_HEAP) {
        asm("leal %P1(%%esp),%0"
            : "=r" (stack_end) : "i" (-STACK_SIZE));

つまり、stack_end = esp - STACK_SIZEという計算を行います。

そのためheap_end計算は以下のようになります。

heap_end = (char *)((size_t)boot_params.hdr.heap_end_ptr + 0x200);

これは、heap_end_ptrまたは、_end + 512(0x200h)を意味します。最後にheap_endstack_endより大きいかどうかがチェックされます。それが正の場合は、それらをイコールにするため、stack_endheap_endに適用されます。

さて、ヒープは初期化され、GET_HEAPメソッドを用いてこれを使うことができるようになりました。では、実際の使われ方、使い方、実装のされ方を次に見ていきます。

CPU検証

次のステップは当然、arch/x86/boot/cpu.cに書かれたvalidate_cpuによるCPU検証です。

これはcheck_cpu関数を呼び出し、CPUレベルと必須のCPUレベルを渡してカーネルが正しいCPUレベルでローンチされていることをチェックします。

check_cpu(&cpu_level, &req_level, &err_flags);
    if (cpu_level < req_level) {
    ...
    return -1;
    }

check_cpuはCPUのフラグ、x86_64(64ビット)CPUの場合はロングモードの配置をチェックします。またプロセッサのベンダーを確認し、SSE+SSE2がない場合にそれらをAMDのためにオフにするような特定のベンダーに備えます。

メモリ検知

次のステップはdetect_memory関数によるメモリ検知です。detect_memoryは基本的に使用可能なRAMのマップをCPUに提供します。メモリ検知には0xe8200xe801そして0x88などのいくつかのプログラミングインターフェースを使います。ここでは0xE820の実装のみを見ていきます。

arch/x86/boot/memory.cソースファイルからdetect_memory_e820の実装を見ましょう。まず先述のようにdetect_memory_e820関数はbiosregs構造体を初期化し、レジスタに0xe820呼び出しのための特別な値を入力します。

 initregs(&ireg);
    ireg.ax  = 0xe820;
    ireg.cx  = sizeof buf;
    ireg.edx = SMAP;
    ireg.di  = (size_t)&buf;
  • axは関数の数字を内包します(ここでは0xe820)。
  • cxレジスタは、メモリに関するデータを格納するバッファのサイズを内包します。
  • edxSMAPマジックナンバーを持たねばなりません。
  • es:diはメモリデータを含むバッファのアドレスを内包する必要があります。
  • ebxはゼロでなければなりません。

次に、メモリに関するデータを収集するループです。0x15BIOS割り込みの呼び出しで始まり、アドレス割り当てテーブルから1行を書き出します。次の行を取得するには、この割り込みを再度呼び出す必要があります(ループ内で行います)。次の呼び出しの前にebxは前に返された値を持たねばなりません。

 intcall(0x15, &ireg, &oreg);
    ireg.ebx = oreg.ebx;

最後にebxはループ内で反復し、アドレス割り当てテーブルからデータを集め、そのデータを以下のようにe820entry配列に書き込みます。
* メモリセグメントの開始
* メモリセグメントのサイズ
* メモリセグメントのタイプ(予約可能か、利用可能かなど)

この結果はdmesgアウトプット内で見ることができます。以下はその例です。

[    0.000000] e820: BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000003ffdffff] usable
[    0.000000] BIOS-e820: [mem 0x000000003ffe0000-0x000000003fffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved

キーボードの初期化

次のステップはkeyboard_init()関数の呼び出しで行うキーボードの初期化です。最初にkeyboard_initは、キーボードのステータスを取得するため、initregs関数を使い、0x16割り込みを呼び出して、レジスタを初期化します。

initregs(&ireg);
    ireg.ah = 0x02;     /* Get keyboard status */
    intcall(0x16, &ireg, &oreg);
    boot_params.kbd_status = oreg.al;

この後、0x16を再度呼び出しリピート率と遅延時間を設定します。

ireg.ax = 0x0305;   /* Set keyboard repeat rate */
    intcall(0x16, &ireg, NULL);

クエリ

次に続くのはいくつかのパラメータのためのクエリです。これらクエリについて詳細は述べませんが、後にまた参照します。簡単にこれらの関数を見てみましょう。

query_mcaルーティンは、0x15BIOS割り込みを呼び出し、マシンモデルナンバー、サブモデルナンバー、BIOSアップデートレベル、そしてその他のハードウェア仕様属性を取得します。

int query_mca(void)
{
    struct biosregs ireg, oreg;
    u16 len;

    initregs(&ireg);
    ireg.ah = 0xc0;
    intcall(0x15, &ireg, &oreg);

    if (oreg.eflags & X86_EFLAGS_CF)
        return -1;  /* No MCA present */

    set_fs(oreg.es);
    len = rdfs16(oreg.bx);

    if (len > sizeof(boot_params.sys_desc_table))
        len = sizeof(boot_params.sys_desc_table);

    copy_from_fs(&boot_params.sys_desc_table, oreg.bx, len);
    return 0;
}

これはahレジスタに0xc0で入力し、0x15BIOS割り込みを呼び出します。割り込みが実行された後、キャリーフラグをチェックし、1に設定されている場合はBIOSはMCAをサポートしません。キャリーフラグが0の場合は、ES:BXはシステム情報テーブルへのポインタを内包します。記述は次のとおりです。

Offset  Size    Description )
 00h    WORD    number of bytes following
 02h    BYTE    model (see #00515)
 03h    BYTE    submodel (see #00515)
 04h    BYTE    BIOS revision: 0 for first release, 1 for 2nd, etc.
 05h    BYTE    feature byte 1 (see #00510)
 06h    BYTE    feature byte 2 (see #00511)
 07h    BYTE    feature byte 3 (see #00512)
 08h    BYTE    feature byte 4 (see #00513)
 09h    BYTE    feature byte 5 (see #00514)
---AWARD BIOS---
 0Ah  N BYTEs   AWARD copyright notice
---Phoenix BIOS---
 0Ah    BYTE    ??? (00h)
 0Bh    BYTE    major version
 0Ch    BYTE    minor version (BCD)
 0Dh  4 BYTEs   ASCIZ string "PTL" (Phoenix Technologies Ltd)
---Quadram Quad386---
 0Ah 17 BYTEs   ASCII signature string "Quadram Quad386XT"
---Toshiba (Satellite Pro 435CDS at least)---
 0Ah  7 BYTEs   signature "TOSHIBA"
 11h    BYTE    ??? (8h)
 12h    BYTE    ??? (E7h) product ID??? (guess)
 13h  3 BYTEs   "JPN"

次に、set_fsルーティンを呼び出し、esレジスタの値をそこに渡します。set_fsの実行は非常にシンプルです。

static inline void set_fs(u16 seg)
{
    asm volatile("movw %0,%%fs" : : "rm" (seg));
}

この関数はインラインのアセンブリを内包しており、segパラメータを取得してそれをfsレジスタに置きます。boot.hには、その中の値を読むset_gsfsgsといった多数の関数があります。

query_mcaの最後では、es:bxによってポイントされていたテーブルをboot_params.sys_desc_tableにコピーするだけです。

次のステップは、query_ist関数を呼び出し、Intel SpeedStepテクノロジの情報を取得することです。まずCPUレベルをチェックし、それが正しければ0x15を呼び出して情報を取得し、その結果をboot_paramsに保存します。

下記のquery_apm_bios関数はAdvanced Power Management情報をBIOSから取得します。query_apm_bios0x15のBIOS割り込みも呼び出しますが、APMのインストールをチェックするためah=0x53を使います。0x15実行後、query_apm_bios関数はPMの署名(0x504dであること)、キャリーフラグ(APMがサポートしている場合は0)とcxレジスタ(0x02の場合、プロテクトモードインターフェースがサポートされていること)をチェックします。

次に再度0x15を呼び出しますが、APMインターフェースとの接続を切り、32ビットプロテクトモードインターフェースに接続するため、ax=0x5304を使います。最後にBIOSから得た値をboot_params.apm_bios_infoに入力します。

query_apm_biosは、CONFIG_APMCONFIG_APM_MODULEが設定ファイルにセットされている場合のみ実行されることに注意してください。

#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
    query_apm_bios();
#endif

最後のquery_edd関数は、Enhanced Disk Drive情報をBIOSに問い合わせます。“`query_edd“の実装を見てみましょう。

まずEDDオプションをカーネルコマンドラインから読み取り、offに設定されている場合はquery_eddの値をそのまま返します。EDDが有効になっている場合は、query_eddはBIOSサポートのハードディスクに行き、次のようなループでEDD情報を問い合わせます。

for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) {
        if (!get_edd_info(devno, &ei) && boot_params.eddbuf_entries < EDDMAXNR) {
            memcpy(edp, &ei, sizeof ei);
            edp++;
            boot_params.eddbuf_entries++;
        }
        ...
        ...
        ...

0x80があるのは最初のハードドライブでEDD_MBR_SIG_MAXマクロの値は16です。これはデータをedd_info構造体の配列に集めます。get_edd_infoはEDDが0x41としてahを使った0x13割り込みを呼び出すことで得られていることをチェックし、それが正の場合はさらにget_edd_infoが再び0x13割り込みを呼び出します。しかし、0x48ahsiはEDD情報がストアされるバッファのアドレスを持っています。

最後に

これでLinuxカーネルインターナルに関する記事のパート2は終わりです。次のパートではビデオモード設定とプロテクトモード移行の前に必要な残りの準備、そしてそのまま移行について見ていきましょう。

質問や助言があればコメント送信または、Twitterでのメッセージをお願いします。

ご注意:英語は私の第一言語ではないことをご承知おきください。誤りを見つけた方はlinux-internalsに、プルリクエストを送ってください。

参考リンク