Kernel booting process. Part 2.

カーネルセットアップの最初のステップ

前回のパートでは、Linuxカーネルの中へ潜り始め、カーネルをセットアップするコードの初期の部分を見ていきました。 私たちは、arch/x86/boot/main.c内の main関数(C言語で書かれた最初の関数)を初めて呼び出すところで止まっていました。

このパートでは、引き続きカーネルのセットアップコードについての調査と以下の内容をやります。

  • プロテクトモード が何なのか、
  • プロテクトモード に入るための準備、
  • ヒープとコンソールの初期化、
  • メモリの検出、cpu validation、キーボードの初期化
  • その他いろいろ

では、やっていきましょう。

プロテクトモード

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

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

リアルモードから移行した主な理由は、RAMへのアクセスが非常に制限されていたからです。 前回のパートの内容を覚えているかもしれませんが、リアルモードで利用可能なRAMはせいぜい220byteか1MBで、中には640KBしかないものもあります。

プロテクトモードになって、多くの点が変わりましたが、メモリ管理で最も大きな変更がありました。 20bitアドレスバスが32bitアドレスバスに置き換えられたことで、リアルモードでは1MBのメモリにしかアクセスできなかったのが、4GBのメモリにアクセスが可能になりました。 さらにプロテクトモードは、ページングにもまた対応しています。これについては、次のセクションで紹介します。

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

  • セグメンテーション
  • ページング

セグメンテーションにのみここでは見ていきます。ページングに関しては次のセクションで見ていきましょう。

あなたは前のパートでリアルモードのアドレスは2つのパートで構成されると学びました:

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

そして、これらの2つの情報が分かれば、物理アドレスを取得することができます:

PhysicalAddress = Segment Selector * 16 + Offset

プロテクトモードになって、メモリセグメンテーションが一新され、64KBの固定サイズのセグメントがなくなりました。 その代わりに、各セグメントのサイズと位置は、セグメントディスクリプタと呼ばれる一連のデータ構造体で表現されます。 このセグメントディスクリプタが格納されているのが、Global Descriptor Table(GDT)というデータ構造体です。

GDTはメモリ内にある構造体です。GDTの場所はメモリ内で固定されているわけではなく、専用の GDTRレジスタにアドレスが格納されています。 LinuxカーネルのコードでGDTを読み込む方法については、後ほど見ていきましょう。以下のようにGDTは、メモリに読み込まれます:

lgdt gdt

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

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

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

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というのは、ディスクリプタのbit 0–15に制限の値が含まれていることを意味します。 そして残りはLIMIT 19:16の中にあります。よってリミットのサイズは0–19なので、20bitです。詳しく見てみましょう。:

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

    • G(bit 55)とセグメントリミットが0の場合、セグメントのサイズは1byteです。
    • G が1でセグメントリミットが0の場合、セグメントのサイズは4096byteです。
    • G が0でセグメントリミットが0xfffffの場合、セグメントのサイズは1MBです。
    • G が1でセグメントリミットが0xfffffの場合、セグメントのサイズは4GBです。

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

    • G が0なら、Limitは1バイト単位と見なされ、セグメントの最大サイズは1MBになります。
    • G が1なら、Limitは4096バイト = 4KB = 1ページ単位と見なされ、セグメントの最大サイズは4GBになります。実際、Gが1なら、LIMITの値は1bit分左にずれます。つまり20bit + 12bitで32bit、すなわち232 = 4GBになります。
  2. Base[32bit]は(0–15、32–39、56–63bit)にあり、これはセグメントの開始位置の物理アドレスを定義します。

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

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

このセグメントがコードとデータ、どちらのセグメントなのかを判別するには、以下の図で0と表記されたEx(bit 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

見て取れるように最初のビット(bit 43)は、data セグメントの場合は0で、code セグメントの場合は1です。 続く3つのbit(40, 41,42)はEWA(Expansion Writable Accessible)またはCRA(Conforming Readable Accessible)のどちらかになります。

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

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

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

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

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

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

セグメントレジスタには、リアルモードのようにセグメントセレクタが含まれていません。 しかし、プロテクトモードでは、セグメントレジスタの扱いは異なります。各セグメントディスクリプタは 関連する16ビットの構造体であるSegment Selectorを持ちます。:

15              3  2   1  0
-----------------------------
|      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によってセグメントディスクリプタを特定し、ディスクリプタをセグメントレジスタの 隠れた 部分にロードしようとします。
  • ベースアドレス(セグメントディスクリプタから)+オフセットは、物理アドレスであるセグメントのリニアアドレスになります。(ページングが無効の場合)

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

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 boot protocolで定義されているのと同じフィールドを持ち、 ブートローダによって、カーネルのコンパイル/ビルド時に書き込まれます。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のアドレスをdiに入れ、スタックにサイズを保存します。 この後、2サイズ右にシフト(あるいは4で除算)し、siからdiに4バイト毎にコピーします。 この後さらに、hdrのサイズをリストアし、4バイトでアドレスをそろえ、残りのバイト(もしあれば)をsiからdiに1バイト毎にコピーします。 最後にsidiの値をスタックからリストアすると、コピーは完了です。

コンソールの初期化

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

その関数は、earlyprintkオプションをコマンドラインから探し、もしあれば、ポートアドレスとシリアルポートのボーレートを解析し、シリアルポートを初期化します。earlyprintkコマンドラインオプションの値は、次のうちのいずれかです:

  • serial,0x3f8,115200
  • serial,ttyS0,115200
  • ttyS0,115200

シリアルポート初期化の後、最初の出力を見れます:

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を持ちます。 ここでeaxに0x01010101をかけると0x07070707になり、4バイトを構造体にコピーできるのです。 memsetは、eaxをes: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-bit)CPUの場合はlong modeの存在を確認します。また、CPUベンダーを確認し、AMDのようなSSE+SSE2がないCPUの場合、その機能をオフにします。

メモリ検知

次のステップは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 は0を持っている必要がある.

次に、メモリに関するデータを収集するループです。0x15 BIOS割り込みの呼び出しで始まり、address allocation tableから1行を書き出します。次の行を取得するには、この割り込みを再度呼び出す必要があります(ループ内で行います)。次の呼び出しの前にebxは前に返り値を持たねばなりません:

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

最終的ににebxはループ内で反復し、address allocation table からデータを集め、データを以下のようにe820entry配列に書き込みます:

  • メモリセグメントの開始アドレス
  • メモリセグメントのサイズ
  • メモリセグメントのタイプ (予約済み、使用不可能 etc...).

この結果は以下のような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);

クエリ

次の2ステップはいくつかのパラメータのためのクエリです。これらクエリについて今は詳細に追いませんが、また後のパートで見ましょう。簡単にこれらの関数を見ていきましょう:

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割り込みを呼び出します。割り込みが実行された後、carry flagをチェックし、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インターフェースとの接続を切り、32bit プロテクトモードに接続するため、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が存在しているかどうかを、ah0x41を入れ、0x13割り込みを呼び出し確かめ、 もし存在していればget_edd_infoが再び0x13割り込みを呼び出します。

まとめ

これでLinuxカーネルインターナルに関する記事のパート2は終わりです。次のパートではビデオモード設定とプロテクトモード移行の前に必要な残りの準備、そしてそのまま移行について見ていきましょう。 もし質問や提案があれば Twitter 0xAXemail で連絡していただくか、Issueを作成してください。

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


results matching ""

    No results matching ""