POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

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

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

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

FeedlyRSSTwitterFacebook
0xAX

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

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

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

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

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

プロテクトモード

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

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

リアルモード から移行した主な理由は、RAMへのアクセスが非常に限られていたからです。前回のパートでもお話ししましたが、リアルモードで利用可能なRAMはせいぜい2 ²⁰ バイトか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ビット、すなわち2 ³² = 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. hdr header.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 の実装は簡単です。まず、 si di レジスタから値をスタックにプッシュします。それらの値は memcpy の実行中に変化するので、スタックにプッシュすることで値を保存するのです。 memcpy (に加え、copy.S内の他の関数)は fastcall 呼び出し規約を使うため、入力パラメータを ax dx そして cx レジスタから取得します。 memcpy の呼び出しは次のように表示されます。

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

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

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

コンソールの初期化

hdr boot_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");

puts tty.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);
}

ここで、 initregs biosregs を引数にとり、まず 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 呼び出し規約を使っています。つまり、関数は ax dx そして cx レジスタからパラメータを取得しているということです。

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

次の命令は eax 0x01010101 をかけます。この行程が必要なのは、 memset が同時に4バイトをコピーするためです。例えば、 memset を使って構造体のフィールドすべてを 0x7 で埋めたいとします。この場合、 eax 0x00000007 値を含みます。そこで 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_end stack_end より大きいかどうかがチェックされます。それが正の場合は、それらをイコールにするため、 stack_end heap_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に提供します。メモリ検知には 0xe820 0xe801 そして 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 レジスタは、メモリに関するデータを格納するバッファのサイズを内包します。
  • edx SMAP マジックナンバーを持たねばなりません。
  • es:di はメモリデータを含むバッファのアドレスを内包する必要があります。
  • ebx はゼロでなければなりません。

次に、メモリに関するデータを収集するループです。 0x15 BIOS割り込みの呼び出しで始まり、アドレス割り当てテーブルから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 ルーティンは、 0x15 BIOS割り込みを呼び出し、マシンモデルナンバー、サブモデルナンバー、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 で入力し、 0x15 BIOS割り込みを呼び出します。割り込みが実行された後、 キャリーフラグ をチェックし、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_gs fs gs といった多数の関数があります。

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_bios 0x15 の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_APM CONFIG_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 割り込みを呼び出します。しかし、 0x48 ah si はEDD情報がストアされるバッファのアドレスを持っています。

最後に

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

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

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

参考リンク

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。