カーネルセットアップの第一歩
前回のパートでは、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部分にロードしようとします。
- ベースアドレス(セグメントディスクリプタから)+オフセットは、物理アドレスであるセグメントのリニアアドレスになります。(ページングが無効の場合)
図で表すとこうなります。
リアルモードからプロテクトモードへ移行するためのアルゴリズムは、
- 割り込みを無効にします。
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
ルーティンは、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
で入力し、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に、プルリクエストを送ってください。