この章では、Linuxカーネルをコンパイルをする時に取られるステップと各ステージの生成物について解説します。 ビルド工程はアーキテクチャにより異なりますが、ここではLinux/x86カーネルのビルドについてだけ考えることにします。
ユーザが、'make zImage'あるいは'make bzimage'とタイプすると、その結果の起動イメージは、それぞれarch/i386/boot/zImage
と arch/i386/boot/bzImage
になります。
ここでは、どのようにイメージが作られるかを見ていきましょう。
System.map
は、'nm vmlinux' から作成される。関連がないシンボルや些細なシンボルは、fマップファイルから除外される。
arch/i386/boot
ディレクトリに移る。
bootsect.S
は、ターゲットが bzImage か zImage かによって -D__BIG_KERNEL__ をつけるか、あるいはつけないかされて、プリプロセッサが
処理する。そして、各々の場合で bbootsect.s
かあるいは bootsect.s
が生成される。
bbootsect.s
はアセンブルされ、その後、bbootsect
という名前の'rawバイナリ'形式へと変換される。(あるいは、bootsect.s
がアセンブルされ zImage 向けの bootsect
へrawバイナリ変換される)
ブートセクタの大きさは、常に512バイトです。setupのサイズは、4セクタより大きくなければなりませんが、最大でも約12Kに制限されています。これは次のようなルールで計算されます。
0x4000 bytes >= 512 + setup のセクタ数 * 512 + ブートセクタ/setupを実行するときのスタックの領域
後で、この制限がどこからきているのか学ぶことにしましょう。
bzImage のサイズの上限は、現時点では LILO からのブートで約2.5M となっています。そして、たとえば、フロッピーディスクや、CD-ROM (El-Torito エミュレーションモードにおいて)といった、raw イメージのブートでは 0xFFFF パラグラフ分(0xFFFF0 = 1048560 バイト)になります。
ここで tools/build は、カーネルイメージのブートセクタやカーネルイメージ、setup の下限サイズを検証します。しかし、setup の上限については、チェックしません。したがって、setup.S
の最後に余分な大きな".space"を付加するだけで、簡単に壊れたカーネルが作成できてしまいます。
ブートプロセスの詳細部分はアーキテクチャに依存しています。そこで IBM PC/IA32 のアーキテクチャへ注目することにします。古いデザインですし、また過去への互換性への問題から、PC のファームウエアは、オペレーティングシステムをブートするときには、古い方式で起動してきます。このプロセスは、つぎの 6 つの論理的なステップへと分割できます。
Linuxカーネルをブートするために使われるブートセクタは、
arch/i386/boot/bootsect.S
),
29 SETUPSECS = 4 /* default nr of setup-sectors */
30 BOOTSEG = 0x07C0 /* original address of boot-sector */
31 INITSEG = DEF_INITSEG /* we move boot here - out of the way */
32 SETUPSEG = DEF_SETUPSEG /* setup starts here */
33 SYSSEG = DEF_SYSSEG /* system loaded at 0x10000 (65536) */
34 SYSSIZE = DEF_SYSSIZE /* system size: # of 16-byte clicks */
(左の数字は bootsect.S ファイルの行番号である) DEF_INITSEG
, DEF_SETUPSEG
, DEF_SYSSEG
および DEF_SYSSIZE
の値は、 include/asm/boot.h
で定義されており、
/* Don't touch these, unless you really know what you're doing. */
#define DEF_INITSEG 0x9000
#define DEF_SYSSEG 0x1000
#define DEF_SETUPSEG 0x9020
#define DEF_SYSSIZE 0x7F00
となっています。
さて、実際のbootsect.S
のコードを見ていきましょう。
54 movw $BOOTSEG, %ax
55 movw %ax, %ds
56 movw $INITSEG, %ax
57 movw %ax, %es
58 movw $256, %cx
59 subw %si, %si
60 subw %di, %di
61 cld
62 rep
63 movsw
64 ljmp $INITSEG, $go
65 # bde - changed 0xff00 to 0x4000 to use debugger at 0x6400 up (bde). We
66 # wouldn't have to worry about this if we checked the top of memory. Also
67 # my BIOS can be configured to put the wini drive tables in high memory
68 # instead of in the vector table. The old stack might have clobbered the
69 # drive table.
70 go: movw $0x4000-12, %di # 0x4000 is an arbitrary value >=
71 # length of bootsect + length of
72 # setup + room for stack;
73 # 12 is disk parm size.
74 movw %ax, %ds # ax and es already contain INITSEG
75 movw %ax, %ss
76 movw %di, %sp # put stack at INITSEG:0x4000-12.
54-63行は、アドレス 0x7C00 から 0x90000 へブートセクタコードを移動しています。これは、次のような手順で実行されています。
このコードがrep movsd
を使わないのは、特別な理由があります。(ヒント .code16)
64行目では、新しく作られたブートセクタのコピーのラベルgo:
へジャンプする。つまり、セグメント 0x9000です。これと続く3つの命令(64-76行)では、$INITSEG:0x4000-0xCへスタックを設定しています。つまり、%ss = $INITSEG (0x9000) と %sp = 0x3FF4 (0x4000-0xC) です。ここに前出のsetupのサイズ制限がどこからきているかの理由があります (Linux カーネルイメージの作成 参照)。
77-103行では、一つめのディスクパラメータテーブルを上書きして、マルチセクタ読み込みができるようにします。
77 # Many BIOS's default disk parameter tables will not recognise
78 # multi-sector reads beyond the maximum sector number specified
79 # in the default diskette parameter tables - this may mean 7
80 # sectors in some cases.
81 #
82 # Since single sector reads are slow and out of the question,
83 # we must take care of this by creating new parameter tables
84 # (for the first disk) in RAM. We will set the maximum sector
85 # count to 36 - the most we will encounter on an ED 2.88.
86 #
87 # High doesn't hurt. Low does.
88 #
89 # Segments are as follows: ds = es = ss = cs - INITSEG, fs = 0,
90 # and gs is unused.
91 movw %cx, %fs # set fs to 0
92 movw $0x78, %bx # fs:bx is parameter table address
93 pushw %ds
94 ldsw %fs:(%bx), %si # ds:si is source
95 movb $6, %cl # copy 12 bytes
96 pushw %di # di = 0x4000-12.
97 rep # don't need cld -> done on line 66
98 movsw
99 popw %di
100 popw %ds
101 movb $36, 0x4(%di) # patch sector count
102 movw %di, %fs:(%bx)
103 movw %es, %fs:2(%bx)
BIOS のサービスの int 0x13 ファンクション0 (reset FDC) を使って、フロッピーディスクコントローラをリセットします。そして、setup セクタが bootsector のすぐ後へ読み込まれます。つまり 物理アドレスの0x90200 ($INITSEG:0x200) です。そして、再度BIOSサービスのint 0x13 ファンクション 2(read sector(s)) を呼び出します。この辺りは、107-124行に記述されています。
107 load_setup:
108 xorb %ah, %ah # reset FDC
109 xorb %dl, %dl
110 int $0x13
111 xorw %dx, %dx # drive 0, head 0
112 movb $0x02, %cl # sector 2, track 0
113 movw $0x0200, %bx # address = 512, in INITSEG
114 movb $0x02, %ah # service 2, "read sector(s)"
115 movb setup_sects, %al # (assume all on head 0, track 0)
116 int $0x13 # read it
117 jnc ok_load_setup # ok - continue
118 pushw %ax # dump error code
119 call print_nl
120 movw %sp, %bp
121 call print_hex
122 popw %ax
123 jmp load_setup
124 ok_load_setup:
もし、なにかの原因(フロッピーが劣化しているとか、使用中にディスケットを抜き去ったとか)で読み込みが失敗したら、エラーコードを表示しながら、無限に再試行されます。再試行が成功していない状態から抜け出すには、パソコンを再起動するほかありません。しかし、通常このようなことは起りません (もしなにかが間違えているとしたら、単におかしくなったのです)。
もし、setupのコードのsetup_sectセクタの読み込みがうまく行くと、ラベル ok_load_setup:
へジャンプします。
その後、圧縮されたカーネルイメージを物理アドレス 0x10000 へと読み込みます。これは、低位のメモリ領域 (0-64K) にあるファームウエアのデータ領域を保護するために行われています。カーネルが読み込まれると、$SETUPSEG:0 (arch/i386/boot/setup.S
)にジャンプします。ファームのデータがもう要らなくなれば (例えば、もうBIOSをコールしないなど)、(圧縮された)すべてのカーネルのイメージを 0x10000 から 0x1000 (当然、物理アドレス)へ移します。その結果、この領域は上書きされます。
これは、setup.S
で実行されます。このコードでは、プロテクトモードへの移行に必要なことを行い、圧縮カーネルの先頭である 0x1000 へとジャンプするようになっています。つまり、arch/i386/boot/compressed/{head.S,misc.c}
です。
さらに、スタックを設定し、decompress_kernel()
を呼び出します。このルーチンではカーネルをアドレス 0x100000 へ展開します。そして、その展開されたカーネルへジャンプします。
ここで古いブートローダ(古いLILO)では、setupの最初の4セクタしか読み込むことが できません。そのため、もし必要であれば自分自身の残りを読み込むようなsetupの コードが存在しています。もちろん、setup のコードは様々なタイプ/バージョンのローダとzImage/bzImageの組合わせを取り扱わなければなりません。そのため、非常に複雑です。
ここで、"bzImage" として知られている大きなカーネルのロードを行うブートセクタコードでの対処方法を見てみましょう。
setupのセクタは、通常0x90200にロードされますが、この時のカーネルは、一度に64Kの固まりで読み込まれるような特別な補助ルーチンを使います。このルーチンは、データを低位アドレスから、高位アドレスへ移動するBIOSコールを呼び出します。この補助ルーチンは、setup.S
のbootsect_kludge
ラベルから参照されます。そしてbootsect_helper
としてsetup.S
で定義されています。
setup.S
のbootsect_klugh
ラベルは、setupセグメント値と、その中のbootsect_helper
コードのオフセットからなっています。そのため、ブートセクタでは、ジャンプするのにlcall
命令を使うことができます。(つまりセグメント内ジャンプです)
setup.S
にこれがあるのは、単にbootsect.Sには、もう余分なスペースが無いからなのです。(これは厳密には正しくはありません -- bootsect.S
には、約4バイトと少なくとも1バイトの余裕があります。しかし、明らかに十分とはいえません)。このルーチンは高位のメモリへ移動するのに、int 0x15(ax=0x8700)のBIOSサービスを使い、%esを常に0x10000を示すようリセットします。これは、bootsect.S
のコード内で、ディスクからデータをコピーするときに低位メモリが足らなくならないことを保証します。
Linuxブートセクタを裸で使わず、特別なブートローダ(LILO)を使うことで、利点が生まれます。
LILO の古いバージョン(v17以前)では、bzImage カーネルをロードすることができません。新しいバージョンでは(ここ数年前より最近では)、bootsect+setup と同じように、BIOS サービスにある低位メモリから高位メモリへデータを移動するテクニックを使っています。人によっては(特に Peter Anvinは)、zImage サポートは削除すべきだと主張しています。それでも残されている主な理由は、(Alan Cox によると) zImage のロードは可能だが、bzImage カーネルのブートができないような壊れた BIOS が明らかに存在しているためだということです。
LILO は最後に、setup.S
へジャンプし、通常どおりの処理を続けます。
「高いレベルの初期化」では、ブートに直接関連しないものについて考えます。しかし、これを行うコードの一部は、
展開されたカーネルの先頭にあるarch/i386/kernel/head.S
と呼ばれるアセンブラで書かれています。そこでは、以下のような処理が行われます。
start_kernel()
を呼び出し、他のCPUは、ready=1であれば単に esp/eip をリロードするのみで戻ってこない関数の arch/i386/kernel/smpboot.c:initialize_secondary()
を呼び出す。init/main.c:start_kernel()
はCで書かれており、以下のような処理を行います。
kmem_cache_init()
を実行し、ほとんどのスラブアロケータを初期化する。max_mapnr
、totalram_pages
と high_memory
を計算するmem_init()
を呼び出し、"Memory: ..." の行を表示する。kmem_cache_sizes_init()
を実行し、スラブアロケータの初期化が完了する。fork_init()
を呼び出し、uid_cache
を作成して、利用可能なメモリ量に基づきmax_threads
を初期化し、 init_task
が max_threads/2
になるよう RLIMIT_NPROC
を設定する。init
カーネルスレッドを作成する。このカーネルスレッドは、もし、"init=" ブートパラメータが与えられていた場合は、 execute_command を実行する。もし指定がなければ、/sbin/init、/etc/init、/bin/init、/bin/shの順にファイルを探し、実行しようとする。もし全て失敗したら、"init="パラメータを使うよう「忠告」してパニック状態になる。ここで重要なことは、init()
カーネルスレッドが do_basic_setup()
を呼び出していることです。この関数はさらに、__initcall
や module_init()
マクロによって登録された関数のリストを読み出して実行する do_initcalls()
を呼び出します。
これらの関数は、各々が相互に依存していないか、Makefileでリンクの順序を入れ替えることで、依存関係を手動で修正してあります。
これはすなわち、ツリーの中のディレクトリの位置とMakefileの構成によって、初期化関数の実行順序が入れ替えられるということを意味しています。
ときには、二つのサブディレクトリAとBがあった場合、BがAの中の初期化関数に依存しているような場合に、この順序が重要になります。もし、Aが静的にカーネルにリンクされ、Bはモジュールであった場合は、Bの実行タイミングは、Aが必要な環境を整えた後であることが保証されます。もし、Aがモジュールであり、Bも当然モジュールである場合にも問題はありません。しかし、AとBが両方静的にカーネルにリンクされる場合はどうでしょう? 2つの実行順序は、カーネルイメージの.initcall.init
ELF セクションにおける位置の差に依存しているのです。
Rogier Wolffは階層的な「優先度」構造を提案し、それによって
リンカがどの(相互的な)順序でモジュールをリンクするかが分かるようにしました。しかし、いまのところ、これをカーネルへ受け入れられるような効果的にエレガントな方法で実装したパッチは存在していません。
したがって、リンクの順序を正しくしないといけないのです。もし、上記の例で、AとBがともに静的にコンパイルされたとき良好に動作したなら、常に動作しますが、そのためにはおなじMakefileに順序よくリストしなければなりません。
もしうまく働かないようなら、オブジェクトファイルのリスト順を変えることになります。
注意する価値のあるもう一つの事柄として、"init="ブートコマンドラインを渡すことにより、「別のinitプログラム」を実行するLinuxの機能があります。これは、/sbin/init を誤って上書きしたときの回復や、初期化(rc)スクリプトや /etc/initttab
を、一回に一つずつ手で実行することでデバッグするのに有益です。
SMPにおいて、BP は start_kernel()
へ進み、そしてsmp_init()
と特にsrc/i386/kernel/smpboot.c:smp_boot_cpus()
に進むまで、通常のブートセクタ、setupなどのブートシーケンスを進んでいきます。
smp_boot_cpus()
は、(NR_CPUS
まで)ループで各apicidごとに実行され、その中でdo_boot_cpu()
が呼ばれます。
do_boot_cpu()
はターゲットのCPU用のアイドルタスクを生成(i.e. fork_by_hand
)します。そして、Intel MP仕様で定義されている既定の位置 (0x467/0x469) へと、trampoline.S
にあるトランポリンコードのEIPを書き込みます。そして、このAPが trampoline.S
のコードを実行するように、ターゲット CPU の STARTUP IPI を生成します。
ブートした CPU は、低位メモリにある各 CPU のトランポリンコードのコピーを作成します。APコードはマジックナンバーを自身のコードに埋め込むことで、BPによりそのAPがトランポリンコードを実行してよいかの判断を行わせます。Intel MP仕様によって規定されているため、トランポリンコードは低位メモリに置かれる必要があります。
トランポリンコードは単純に %bx レジスタを 1 にします。そして、プロテクトモードに移り、arch/i386/kernel/head.S
のメインエントリーポイントであるstartup_32 へとジャンプします。
さて、APはhead.S
の実行を開始し、自身がBPではないことに気が付きます。
すると、BSSをクリアするコードの実行をスキップして、initalize_secondary()
へと進みます。そして、この CPU はアイドルタスクへと単に進みます。
-- init_tasks[cpu]
は、BPがdo_boot_cpu(cpu)
を実行したときにすでに初期化されていたことを思い起こしましょう。
ここで、init_taskは共有できますが、各アイドルスレッドで各々 TSS を持たなければならないことに注意しましょう。これは、init_tss[NR_CPUS]
が配列になっている理由になっています。
オペレーティングシステムが自身を初期化するとき、そのためのほとんどのコードとデータ構造体は二度と使われることはありません。ほとんどのオペレーティングシステム(BSD, FreeBSD etc...)では、この不必要な情報を破棄することができません。すなわち、貴重な物理カーネルメモリを浪費していることになるのです。 彼らの使う言い訳(McKusickの4.4BSD本を参照)は、「関連のコードが各種のサブシステムに広がっており、これらを解放するのは現実的ではない。」ということです。Linux はもちろん、このような言い訳をしません。なぜなら、Linux では、「もしなにかが原理的に可能であれば、それはすでに実装されているか、誰かが作業している」からです。
そして、前の章で述べたように、Linux カーネルはELF バイナリとしてのみコンパイルできることから、私たちにもそれが可能なことが(あるいは、その根拠の一つ)が分かっています。以下のように使われる2つのマクロを Linux が提供しており、初期化コード/データを廃棄することができるようになっています。
__init
- 初期化コード用__initdata
- 初期化データ用これらはinclude/linux/init.h
に定義されるgccの属性指示子("gcc magic"としても知られる)を評価します。
#ifndef MODULE
#define __init __attribute__ ((__section__ (".text.init")))
#define __initdata __attribute__ ((__section__ (".data.init")))
#else
#define __init
#define __initdata
#endif
これが意味することは、もしコードがカーネルに静的にコンパイルされているなら(つまり MODULEが定義されていなければ)、特殊な ELF セクションのtext.init
にこれらのコードが配置されるということです。そしてその配置は、arch/i386/vmlinux.lds
のリンカマップに定義されるのです。
逆に(つまりモジュールであれば)マクロはなにもしないということです。
ブート時に、アドレス__init_begin
から__init_end
の間の全てのページを解放する、アーキテクチャ特有の関数free_initmem()
を、"init"カーネルスレッド(関数init/main.c:init()
)が呼び出すのです。
(私のワークステーションのような)一般的なシステムでは、この結果、約260Kのメモリの解放になります。
module_init()
によって登録された関数は、.initcall.init
に配置され、静的にリンクされていたときには同様に解放されます。Linux 開発の現在の方向性では、将来問題のサブシステムが必要に応じてモジュール化できるように、(当初モジュールの必要性のない)サブシステムの場合にも、デザインの初期段階から、init/exit エントリーポイントが提供されるようにしています。fs/pipe.c
のpipefsが、このよい例です。たとえ、bdflush (fs/buffer.c
参照)のように、あるサブシステムが決してモジュールになることがなく、その時点でその関数を呼ぶことが重要でないとしても、それでも初期化関数としてmodule_init()
マクロを使うことはよいことです。
さらに同じような使い方をする__exit
と__exitdata
という名前の2つのマクロがあります。しかしこれらは、モジュールサポートにより直接的につながるため、後のセクションにて説明します。
ここで、ブート時にカーネルに渡されたコマンドラインに何が起こるかを考えてみましょう。
arch/i386/kernel/head.S
は自身の最初の 2k をゼロページの外へとコピーする。
start_kernel()
から呼ばれた setup_arch()
から呼び出される) arch/i386/kernel/setup.c:parse_mem_cmdline()
は、ゼロページから 256 バイトを /proc/cmdline
で表示されるのと同じ saved_command_line
へとコピーする。これと同じルーチンは、"mem=" オプションがもしあれば処理し、VM パラメータを適切に調整する。
start_kernel()
から呼ばれる) parse_options()
のコマンドラインに戻すと、この関数は「カーネル内部」パラメータ(現時点では、"init="と init の環境変数、引数)も処理し、各ワードをchecksetup()
へと渡す。
checksetup()
は ELF セクション.setup.init
のコードについて、そこの各関数を起動し、コマンドラインがマッチしていた時は、コマンドラインのワードを渡していく。ここで、__setup()
で登録された関数からの返り値が 0 のときは、同じ"variable=value" を一つ以上の関数へ渡せ、その"value"が一方の関数では無効で、他方では有効であるということを表している。
Jeff Garzikは、「こういうことをするハッカーはおしりペンペンだ :) 」とコメントしている。なぜか?
これは明らかに ID の順序に特有で、つまりある順序でリンクされたカーネルは関数Aを関数Bの前に起動するが、そうでない場合は、逆の順序になり結果が順序に依存してしまうからだ。
さて、ブートコマンドラインを処理するコードはどのようになっているのでしょうか。include/linux/init.h
で定義される __setup()
マクロを使います。
/*
* Used for kernel command line parameter setup
*/
struct kernel_param {
const char *str;
int (*setup_func)(char *);
};
extern struct kernel_param __setup_start, __setup_end;
#ifndef MODULE
#define __setup(str, fn) \
static char __setup_str_##fn[] __initdata = str; \
static struct kernel_param __setup_##fn __initsetup = \
{ __setup_str_##fn, fn }
#else
#define __setup(str,func) /* nothing */
endif
次に、実際のコードでの典型的な使い方は以下のようになります (実際のドライバの BusLogic HBA drivers/scsi/BusLogic.c
から引用)。
static int __init
BusLogic_Setup(char *str)
{
int ints[3];
(void)get_options(str, ARRAY_SIZE(ints), ints);
if (ints[0] != 0) {
BusLogic_Error("BusLogic: Obsolete Command Line Entry "
"Format Ignored\n", NULL);
return 0;
}
if (str == NULL || *str == '\0')
return 0;
return BusLogic_ParseDriverOptions(str);
}
__setup("BusLogic=", BusLogic_Setup);
ここで __setup()
はモジュールに対しては何もしません。そしてモジュールであれ静的にリンクされているのであれ、ブートコマンドラインを処理したいコードでは、モジュールの初期化ルーチンにおいて、自身の解析関数を持っていてそれを起動しなければならないからです。これは、静的にコンパイルされる時などだけでなく、モジュールとしてコンパイルされる時にも、パラメータを処理するコードを書くことができるという事でもあります。