2015年12月8日
Linux Insides : カーネル起動プロセス part2
本記事は、原著者の許諾のもとに翻訳・掲載しております。
カーネルセットアップの第一歩
前回の パート では、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部分にロードしようとします。
- ベースアドレス(セグメントディスクリプタから)+オフセットは、物理アドレスであるセグメントのリニアアドレスになります。(ページングが無効の場合)
図で表すとこうなります。
リアルモードからプロテクトモードへ移行するためのアルゴリズムは、
- 割り込みを無効にします。
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 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa