コメントから読む Linux カーネル Sano Taketoshi $Date: 2000/06/27 13:57:52 $ NLUG (名古屋 Linux ユーザーグループ) 第 3 回勉強会のために作成した資料 です。 今回読むのは Linux カーネル 2.2.5 です。主に PC/AT (i386) 上 で、電源 ON のあと、カーネルのロード、 ブートアップから /sbin/init が 実行されるまでをソースコード中のコメントを頼りに追いかけていきます。 ______________________________________________________________________ 目次 1. はじめに 1.1 「カーネル」とは 1.2 Linux カーネル 1.3 コード作成に貢献した人々 1.4 担当者一覧 2. システムの起動:概要 2.1 カーネルのロード 2.2 BIOS 情報の取得 2.3 カーネル本体の展開 2.4 デバイスドライバーの設定 2.5 init の起動 2.6 システム起動 3. カーネルのロード 3.1 bootsect.S - 冒頭のコメント 3.2 bootsect.S - パラメータ定義 3.3 bootsect.S - 実際の動作 4. BIOS 情報の取得 5. カーネル本体の展開 6. デバイスドライバーの設定 7. init の起動 8. 番外: Makefile について 9. 終わりに 9.1 お願い 9.2 謝辞 9.3 この文書の配布について ______________________________________________________________________ 1. はじめに 1.1. 「カーネル」とは たぶん、一度くらいどこかで目にしたことがあるかもしれませんが「Linux」 という名前は当時フィンランドの大学生であった Linus Torvalds さんが自分 の作った「カーネル」に、自分の名前から一部を取って付けた名前です。 さて、そこで問題。「カーネル」って何でしょう ? 「カーネル」とは、システム内で動作中の各プログラムから出される要求に応 じてメモリーやディスクなどのハードウェア資源の管理や、 CPU 時間の配分 などを行なう OS の中枢部分です。 例えば "top" コマンドを実行してしばらく眺めていると、リストされている プロセスの順番が時々入れ替わることがあるのに気づくでしょう。この「順番 の入れ替え」はカーネルのタスク管理 (スケジューリング) によるものです。 また、プリンタを接続して印刷できるように設定する際、 cat test.pr >/dev/lp0 などの操作によってパラレルポートへの信号出力をテストした経験はありませ んか ? この「 /dev/lp0 に出力したデータはそのままパラレルポートに出力 される」という動作はカーネル内の lp ドライバによって実現されています。 1.2. Linux カーネル 以下、この文書では Linux カーネルのソースコードツリーを、コメントを手 がかりにしてちょっとだけ探検してみることにします。 まずは、バージョン 2.2.5 のカーネルを展開して、そのトップディレクトリ に移動してみましょう。そう、通常 /usr/src/linux/ として見ることができ る場所です。 まずはどんなファイルがあるか、 "ls" で調べてみます。 $ ls -F COPYING Makefile arch/ init/ mm/ CREDITS README drivers/ ipc/ net/ Documentation/ REPORTING-BUGS fs/ kernel/ scripts/ MAINTAINERS Rules.make include/ lib/ 最初に読んでおくべきファイルは "README" ですね。これはカーネルでなくて も、一般のアプリケーションのソースでも同じです。が、ここではこのファイ ルの内容については省略して、その代わりに "CREDITS" と "MAINTAINERS" と いう 2 つのファイルの内容について紹介したいと思います。 1.3. コード作成に貢献した人々 "CREDITS" ファイルの冒頭には This is at least a partial credits-file of people that have contributed to the Linux project. It is sorted by name and formatted to allow easy grepping and beautification by scripts. The fields are: name (N), email (E), web-address (W), PGP key ID and fingerprint (P), description (D), and snail-mail address (S). Thanks, Linus と書かれています。このファイルには Linux カーネルの開発に貢献した人々 (の一部) の名前が書かれている、というわけです。 例えば、先日 TV の特集で取材されていた日本の新部さんの名前も N: Niibe Yutaka D: PLIP driver D: Asynchronous socket I/O in the NET code と記載されています。もちろん、Linus さんの名前は N: Linus Torvalds D: Original kernel hacker としっかり載っていますし、 ac パッチで有名な Alan Cox や fat32 対応を 実装した Gordon Chaffee、それに以前 Linux Kernel の Sound Driver を書 いていた OpenSoundSystem の Hannu Savolainen の名前もあります。 N: Alan Cox D: Linux Networking (0.99.10->2.0.29) D: Original Appletalk, AX.25, and IPX code D: Current 3c501 hacker. >>More 3c501 info/tricks wanted<<. D: Watchdog timer drivers D: Linux/SMP x86 (up to 2.0 only) D: Initial Mac68K port D: Video4Linux design, bw-qcam and PMS driver ports. D: 2.1.x modular sound N: Gordon Chaffee D: vfat, fat32, joliet, native language support N: Hannu Savolainen D: Kernel sound drivers 他にも、Slackware の Patrick Volkerding、 Debian の Ian A. Murdock と Ian Jackson 他、それにいろいろと各方面で有名な Eric S. Raymond や XFree86 の Dirk Hohndel (彼も TV で取材されていましたね) や Harald Koenig の名前もありますし、LDP 関係者である Michael K. Johnson や Matt Welsh も載っています。 N: Patrick Volkerding D: Produced the Slackware distribution, updated the SVGAlib D: patches for ghostscript, worked on color 'ls', etc. N: Ian A. Murdock D: Creator of Debian distribution N: Ian Jackson D: FAQ maintainer and poster of the daily postings D: FSSTND group member D: Debian core team member and maintainer of several Debian packages N: Eric S. Raymond D: terminfo master file maintainer D: Editor: Installation HOWTO, Distributions HOWTO, XFree86 HOWTO D: Author: fetchmail, Emacs VC mode, Emacs GUD mode N: Dirk Hohndel D: The XFree86[tm] Project N: Harald Koenig D: XFree86 (S3), DCF77, some kernel hacks and fixes N: Michael K. Johnson D: The Linux Documentation Project D: Kernel Hackers' Guide D: Procps D: Proc filesystem D: Maintain tsx-11.mit.edu D: LP driver N: Matt Welsh D: Linux Documentation Project coordinator D: Author, _Running_Linux_ and I&GS guide D: Linuxdoc-SGML formatting system D: Keithley DAS1200 device driver D: Maintainer of sunsite WWW and FTP, moderator c.o.l.answers ちょっと " egrep '^N:' CREDITS |wc" として数えてみたところ、2.2.5 カー ネルの CREDITS ファイルにはざっと 273 人の名前が挙がっているようで す。(2.0.36 で調べてみたら 204 人でした) この中に書かれている名前を何 人知っているか、数えてみると Linux 界へのハマリ度がわかっておもしろい かもしれません。 そうそう、「コメント」にこだわりを持つものとしては、このファイルの最後 も見逃さないようにしておくことが必要です。 # Don't add your name here, unless you really _are_ after Marc # alphabetically. Leonard used to be very proud of being the # last entry, and he'll get positively pissed if he can't even # be second-to-last. (and this file really _is_ supposed to be # in alphabetic order) "pissed off" っていうのは、「激しく怒る」とか「頭にくる」という意味ら しいですね。何ていうか、カーネル開発者たちも人間なんだな、という感じが して微笑ましい気がします。 あと、もうひとつ忘れてました。これです。 N: Lars Wirzenius D: Linux System Administrator's Guide D: Co-moderator, comp.os.linux.announce D: Original sprintf in kernel D: Personal information about Linus D: Original kernel README D: Linux News (electronic magazine) D: Meta-FAQ, originator D: INFO-SHEET, former maintainer D: Author of the longest-living linux bug 最後の行に注目してください。なかなかユーモアのある方のようです。 普通「自分がバグを入れた」というのは、あまり自慢できることではないの で、コード開発への貢献に謝意を表するための「献辞」 CREDITS に載せるよ うなことはあまりしないと思うのですが、上記の文がわざわざ書いてあるとい うのは、たぶんこの Wirzenius さんが自分で「こう書いてくれ」と依頼され たんじゃないかと想像しています。 上記の「最も長く生きのびたバグの著者」という文からは、通常ならあまり公 表したくないと思うような過去の「バグ」のことでさえ、「笑い」の対象にし てしまおうというバイタリティというか、ユーモアのセンスを感じます。「ど うだい ? この俺がかの有名な『最も長く解決されずに残ったバグ』の著者な んだぜ ! スゴイだろう !!」みたいな感じですね。 1.4. 担当者一覧 さて、そろそろ次に移りましょう。今度は "MAINTAINERS" ファイルです。こ のファイルの冒頭には List of maintainers and how to submit kernel changes Please try to follow the guidelines below. This will make things easier on the maintainers. Not all of these guidelines matter for every trivial patch so apply some common sense. と書かれています。つまり基本的には Linux カーネルを自分の手で変更して 楽しむ人 (カーネルハッカー) を対象に、そのパッチをどこに送れば標準とし て採用されるのか、という手順を説明したものです。 しかし、このファイルにも実は楽しめる点があるのです。それはこのファイル の一番最後。 THE REST P: Linus Torvalds S: Buried alive in reporters ちなみに、2.0.36 までの 2.0.xx 系ではこうなっていました。 REST: P: Linus Torvalds S: Buried alive in email ここ数年の Linux と Linus さんを取り巻く状況の変化を物語っているようで す。 2. システムの起動:概要 ここではシステムの電源 ON から /sbin/init が起動されるまでの流れを大ま かに説明します。 2.1. カーネルのロード PC/AT 互換機ではマザーボードの BIOS が実行するブートシーケンスによって 起動ディスクのブートセクタに書き込まれているコードを特定のメモリー位置 へロードし、そのコードを実行 (ロードしたコードの開始位置へ jump) しま す。 (「特定のメモリー位置」とは segment 0h / address 7C00h または [0000:7C00] ですが、Linux カーネルの中では offset が 0 から始められる ように segment を 7C0 にしているらしいという情報を頂きました。) PC 上で利用される各 OS は、この BIOS によるブートを前提として、最初に 読込まれるコードから始まって順に自分より大きいコードをロードし、最終的 に必要なコードをすべてメモリーに読み込んで実行を開始するという処理を行 なっています。 この一連の動作を一般にブートストラップ、と呼びます。ブートは boot (長 靴)、ストラップは strap (革紐) のことで、自分が履いている長靴の紐を自 分で引っ張って体を持ち上げようとすることに例えています。 なお「Bootstrap」についての解説が http://www.oreilly.com/reference/dictionary/terms/B/Bootstrap.htm にありますので、興味のある方は調べてみることをお勧めします。なおこの Web ページには「"Bootstrap" のことを IBM 用語で IPL, Initial Program Load と呼ぶ」と書かれています。 Linux カーネルの内部には、(あらためて聞くと驚かれるかもしれませんが) すくなくとも i386 系ではハードディスクから自前でブートさせるためのコー ドは用意されていません。ハードディスクからのブートでは LILO や LOADLIN などのブートローダーを利用することが前提となっています。 フロッピーからの起動の場合、カーネル内で最初にロードされるコードは arch/i386/boot/bootsect.S です。このコードは同じディレクトリにある arch/i386/boot/setup.S のコードと、カーネルの残りをロードして setup.S に制御を移行します。 2.2. BIOS 情報の取得 arch/i386/boot/setup.S はメモリーサイズやディスク情報、またコンソール 用ビデオカードの情報や APM BIOS のチェックなど、システムに関するいろい ろな情報を BIOS から取得して、後でデバイスドライバーを初期化する際に使 用できるよう、メモリー上に保存します。 さらに LILO などのブートローダーを使用した場合には、起動時にキーボード から入力されたカーネルオプションを後で参照できるようにメモリー上の特定 の場所にコピーするのも setup.S の仕事です。 setup.S は BIOS 情報の取得を完了すると、CPU のモードを起動時の 16bit モードから 32bit (protected) モードに切り替えて、 arch/i386/boot/compressed/head.S に処理を移行します。 2.3. カーネル本体の展開 arch/i386/boot/compressed/head.S はいくつかのチェックと SMP の場合に必 要な処理を実行した後で、同じディレクトリに存在する arch/i386/boot/compressed/misc.c で定義された decompress_kernel() とい う関数を使って、 gzip 圧縮された状態でメモリーにロードされているカーネ ル本体を展開します。 コンソールモニターに Uncompressing Linux... という表示が出力されるのは、この decompress_kernel() の実行中です。 展開が終了すると、arch/i386/boot/compressed/head.S は新しくメモリー上 に現われた本来のカーネルコードへと処理を移行します。 2.4. デバイスドライバーの設定 arch/i386/boot/compressed/head.S によって展開された「本来のカーネルコ ード」の先頭に存在しているのは arch/i386/kernel/head.S です。 このコードは主に CPU に関する初期化 (ページテーブルの準備や割り込み (インタラプト) テーブルの初期化など) を実行し、最後に init/main.c にあ る start_kernel() を実行します。 この start_kernel() ではここまでにシステムから集めてきて、メモリー上の 特定位置に保存されているいろいろな情報を使って、メモリー管理やタスクス ケジュールに関連するカーネル内の各デバイスドライバーを設定していきま す。またコンソール出力の設定を実行するのもこの start_kernel() です。 start_kernel() は同じ init/main.c にある init() を kernel_thread () を 使って起動した後、やはり同じ init/main.c にある cpu_idle() を実行しま す。この cpu_idle() は arch/i386/kernel/process.c の中で定義されている sys_idle() を実行する無限ループです。 さて、start_kernel() から kernel_thread() を経由して起動された init/main.c で定義されている init() ですが、最初に lock_kernel() (これ は SMP の場合にのみ意味があります) を実行したあと、do_basic_setup() を 実行します。 この do_basic_setup() も init/main.c の中で定義されていますが、バスの 初期化や各デバイスドライバの初期化、ファイルシステム関連コードの初期 化、ルートファイルシステムのマウントなどをここで実行しています。ちなみ に、これらの処理は 2.0.36 では start_kernel() の内部で実行されていまし た。2.2.xx 系になって移植性を高めるためか、このあたりの処理がさらに細 分化されコードの構成が変更されたようです。 2.5. init の起動 init/main.c で定義されている init() は do_basic_setup() から処理が戻っ てくると、起動時にのみ必要とされたメモリー領域の開放とコンソール出力の オープンを実行し、 /sbin/init, /etc/init, /bin/init, /bin/sh を順番に試して、最初に見つかった実行可能なものへと処理を移行します。 2.6. システム起動 これ以降の処理は、/etc/inittab での設定や /etc/rc.d または /etc/init.d と /etc/rc?.d による起動スクリプトの設定などによって動作が決まります。 これについては、たぶん既にご承知のかたも多いのではないでしょうか。 「カーネルの起動」についての概要はこれで終わりです。次の節から、上に述 べた各段階で使用されるコードについてコメントを頼りに調べていくことにし ます。 3. カーネルのロード 上記の「概要」で説明したように、フロッピーからの起動の場合、カーネル内 で最初にロードされるコードは arch/i386/boot/bootsect.S です。 まずは、このファイルから眺めてみることにしましょう。 3.1. bootsect.S - 冒頭のコメント ! ! bootsect.s Copyright (C) 1991, 1992 Linus Torvalds ! modified by Drew Eckhardt ! modified by Bruce Evans (bde) ! ! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves ! itself out of the way to address 0x90000, and jumps there. ! ! bde - should not jump blindly, there may be systems with only 512K low ! memory. Use int 0x12 to get the top of memory, etc. ! ! It then loads 'setup' directly after itself (0x90200), and the system ! at 0x10000, using BIOS interrupts. ! ! NOTE! currently system is at most (8*65536-4096) bytes long. This should ! be no problem, even in the future. I want to keep it simple. This 508 kB ! kernel size should be enough, especially as this doesn't contain the ! buffer cache as in minix (and especially now that the kernel is ! compressed :-) ! ! The loader has been made as simple as possible, and continuous ! read errors will result in a unbreakable loop. Reboot by hand. It ! loads pretty fast by getting whole tracks at a time whenever possible. 上の「概要」に書いたことが、しっかり冒頭にコメントとして記載されていま すね。 「bootsect.s は BIOS のブートシーケンスによってメモリー上のアドレス 0x7c00 にロードされる。次に bootsect.s は自分自身をアドレス 0x90000 に 移動し、そしてそこへジャンプ (制御を移行) する。」 ひとつ飛ばして 「次に自分自身の直後 (0x90200) に 'setup' を、またシステムを 0x10000 に BIOS インタラプトを使ってロードする」 その次が今となっては時代を感じるコメントですね。 「注意! 現在、システムは最大でも (8*65536-4096) バイトまでの長さであ る。この制限については、例え将来においても、問題を生じることは無い。物 事はなるべく単純にしておきたいものだ。このカーネルサイズ 508 kB という 制限は、minix のようにバッファーキャッシュを含んでいるわけではないこと を考えると (そしてまた、現在カーネルは圧縮された状態でロードされている ことを考えると :) まったく十分な大きさと言えるはずである。」 実際には数年前から既にこの「508kB の壁」は十分とは言えなくなってしま い、bZimage という抜け道が用意されています。しかし、このために「物事は なるべく単純に」という Linus の希望から実際のコードがやや離れてしまっ たように感じられます。 3.2. bootsect.S - パラメータ定義 そろそろ、コードの中身に入ってみましょう。 #include /* for CONFIG_ROOT_RDONLY */ #include .text SETUPSECS = 4 ! default nr of setup-sectors BOOTSEG = 0x07C0 ! original address of boot-sector INITSEG = DEF_INITSEG ! we move boot here - out of the way SETUPSEG = DEF_SETUPSEG ! setup starts here SYSSEG = DEF_SYSSEG ! system loaded at 0x10000 (65536). SYSSIZE = DEF_SYSSIZE ! system size: number of 16-byte clicks 最初の行、#include は include/linux/config.h の定義を 使う、ということです。で、このヘッダーファイルの中身はこうなってます。 #ifndef _LINUX_CONFIG_H #define _LINUX_CONFIG_H #include #endif これだけ。これは include/linux/autoconf.h の定義を使いなさい、というこ とです。で、このファイルを探してみると、ありません。 実はこの autoconf.h は make config / menuconfig / xconfig などを実行し て始めて作成されるファイルであって、単にアーカイブを展開しただけでは存 在しないものです。 今回は時間の都合で、このファイルを作られる手順を追いかけるのは省略し、 次の #include を調べてみることにします。 こちらは include/asm-i386/boot.h が探しているファイルで #ifndef _LINUX_BOOT_H #define _LINUX_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 /* Internal svga startup constants */ #define NORMAL_VGA 0xffff /* 80x25 mode */ #define EXTENDED_VGA 0xfffe /* 80x50 mode */ #define ASK_VGA 0xfffd /* ask for it at bootup */ #endif と書かれています。このあたりも、2.0.36 と比較して変更された部分です ね。 (2.0.36 ではこの定義が include/linux/config.h にありました) さて、話をもとの bootsect.S に戻します。 .text SETUPSECS = 4 ! default nr of setup-sectors BOOTSEG = 0x07C0 ! original address of boot-sector INITSEG = DEF_INITSEG ! we move boot here - out of the way SETUPSEG = DEF_SETUPSEG ! setup starts here SYSSEG = DEF_SYSSEG ! system loaded at 0x10000 (65536). SYSSIZE = DEF_SYSSIZE ! system size: number of 16-byte clicks この部分の最初にある ".text" は、コンテキストという意味で実行コードの 開始位置を示しています。 最初、私はここから次の".globl _main" までは「変数」の定義だと思ってい たのですが、そうではなくて、アセンブル (機械語に翻訳) する際に数値に変 換されてコードに代入される「定数定義」(C の #define で定義されるマクロ に似たもの) と考えたほうが良いと教わりました。 ここで参照されている DEF_INITSEG, DEF_SETUPSEG, DEF_SYSSEG, DEF_SYSSIZE は既に見てきたように、include/asm/boot.h で定義されています。 この部分の下にも、いくつか定数の定義が続いていますが、省略して次に進み ます。 3.3. bootsect.S - 実際の動作 次の ".globl _main" 以降から実際に動作する際に使われるコードが始まっ ています。 ! ld86 requires an entry symbol. This may as well be the usual one. .globl _main _main: #if 0 /* hook for debugger, harmless unless BIOS is fussy (old HP) */ int 3 #endif mov ax,#BOOTSEG mov ds,ax mov ax,#INITSEG mov es,ax mov cx,#256 sub si,si sub di,di cld rep movsw jmpi go,INITSEG ! ax and es already contain INITSEG ここで "#BOOTSEG" は BIOS によって bootsect.S のコードがロードされたア ドレス、"#INITSEG" は bootsect.S が自分自身をコピーして処理を移す(ジャ ンプする) アドレスです。ここでは "movsw" までの行で「自分自身のコピ ー」を実行し、"jmpi" で "#INITSEG" にコピーされた自分自身の "go" ラベ ルの位置へジャンプしています。 このあとしばらくフロッピードライブをうまく動作させるための準備が行なわ れます。そして次の "load_setup" から setup.S のコードをロードしていき ます。 load_setup: xor ah,ah ! reset FDC xor dl,dl int 0x13 最初はフロッピードライブコントローラをリセットするところから始まって xor dx, dx ! drive 0, head 0 mov cl,#0x02 ! sector 2, track 0 mov bx,#0x0200 ! address = 512, in INITSEG mov ah,#0x02 ! service 2, nr of sectors mov al,setup_sects ! (assume all on head 0, track 0) int 0x13 ! read it jnc ok_load_setup ! ok - continue ドライブ、ヘッド、セクター、トラックなどの位置を初期化し、読み出しアド レスを指定して "int 0x13" の実行によって BIOS のセクター読み込み機能 を利用して "setup.S" のコードをメモリー上にロードしています。ロード先 のアドレスは "INITSEG" のアドレス 0x9000 に 0x200 を加えた 0x9200 とな ります。 setup.S のロードを完了すると "Loading" というメッセージを表示して、次 の段階 (圧縮されたシステム本体のロード) へと進みます。 got_sectors: ! Restore es mov ax,#INITSEG mov es,ax ! Print some inane message mov ah,#0x03 ! read cursor pos xor bh,bh int 0x10 mov cx,#9 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10 ! ok, we've written the message, now ! we want to load the system (at 0x10000) mov ax,#SYSSEG mov es,ax ! segment of 0x010000 call read_it call kill_motor call print_nl 上の "int 0x10" は画面表示制御の BIOS インタラプトであり、 "#msg1" は bootsect.S の終りのほうで msg1: .byte 13,10 .ascii "Loading" として定義されています。ここで ".byte" の 13 は CR (キャリッジリター ン)、 10 はLF (ラインフィード) です。 さて、システム本体のロードですが、これは上に書かれた "call read_it"に よって呼び出される ! This routine loads the system at address 0x10000, making sure ! no 64kB boundaries are crossed. We try to load it as fast as ! possible, loading whole tracks whenever we can. ! ! in: es - starting address segment (normally 0x1000) ! sread: .word 0 ! sectors read of current track head: .word 0 ! current head track: .word 0 ! current track read_it: 以降の部分、特に次の rp_read: から始まる部分によります。 rp_read: #ifdef __BIG_KERNEL__ #define CALL_HIGHLOAD_KLUDGE .word 0x1eff,0x220 ! call far * bootsect_kludge ! NOTE: as86 can't assemble this CALL_HIGHLOAD_KLUDGE ! this is within setup.S #else mov ax,es sub ax,#SYSSEG #endif cmp ax,syssize ! have we loaded all yet? jbe ok1_read ret ここで "CALL_HIGHLOAD_KLUDGE" はコメントにあるようにちょうどこの部分の すこし前にロードした setup.S のコードに含まれている bootsect_kludge に 対応したアドレスから始まるコードです。 setup.S の中では "bootsect_kludge" は次のように定義されています。 bootsect_kludge: .word bootsect_helper,SETUPSEG またこの bootsect_helper は setup.S の中で以下のように定義されていま す。 ! This routine only gets called, if we get loaded by the simple ! bootsect loader _and_ have a bzImage to load. ! Because there is no place left in the 512 bytes of the boot sector, ! we must emigrate to code space here. ! bootsect_helper: コメントにしっかり「bootsect.S によって bzImage 形式のカーネルがロード された場合に限って実行される、と書いてありますね。 さて、bootsect.S の中で、実際にフロッピーからシステムを読んでいるの は、以下の部分です。 read_track: pusha pusha mov ax, #0xe2e ! loading... message 2e = . mov bx, #7 int 0x10 popa ここの 0x10 は画面表示を行なう BIOS インタラプトコールです。既に setup.S のコードをロードした時点で、 "Loading" というメッセージが画面 に出力されているはずなので、ここでは "." の出力のみを行なっています。 mov dx,track mov cx,sread inc cx mov ch,dl mov dx,head mov dh,dl and dx,#0x0100 mov ah,#2 push dx ! save for error dump push cx push bx push ax int 0x13 jc bad_rt add sp, #8 popa ret BIOS インタラプト "Int 0x13" の実行によってフロッピー上のシステムファ イルがメモリーにロードされていきます。 ロードが完了すると、先に引用した call read_it call kill_motor call print_nl を順に実行してフロッピードライブのモーターを OFF にし、画面に改行コー ド (NewLine) を出力します。 その後、ルートデバイスのチェックを経て ! after that (everything loaded), we jump to ! the setup-routine loaded directly after ! the bootblock: jmpi 0,SETUPSEG "SETUPSEG" に存在する setup.S のコードにジャンプします。 4. BIOS 情報の取得 さて、arch/i386/boot/setup.S に進みましょう。 まずはファイル冒頭のコメントから。 ! ! setup.S Copyright (C) 1991, 1992 Linus Torvalds ! ! setup.s is responsible for getting the system data from the BIOS, ! and putting them into the appropriate places in system memory. ! both setup.s and system has been loaded by the bootblock. ! ! This code asks the bios for memory/disk/other parameters, and ! puts them in a "safe" place: 0x90000-0x901FF, ie where the ! boot-block used to be. It is then up to the protected mode ! system to read them from there before the area is overwritten ! for buffer-blocks. ! ! Move PS/2 aux init code to psaux.c ! (troyer@saifr00.cfsat.Honeywell.COM) 03Oct92 ! ! some changes and additional features by Christoph Niemann, ! March 1993/June 1994 (Christoph.Niemann@linux.org) ! ! add APM BIOS checking by Stephen Rothwell, May 1994 ! (Stephen.Rothwell@canb.auug.org.au) ! ! High load stuff, initrd support and position independency ! by Hans Lermen & Werner Almesberger, February 1996 ! , ! ! Video handling moved to video.S by Martin Mares, March 1996 ! ! ! Extended memory detection scheme retwiddled by orc@pell.chi.il.us (david ! parsons) to avoid loadlin confusion, July 1997 どうでしょう ? もうこのコメントだけ読めば、このファイルに書かれている コードが何をしているのか、だいたいわかったような気になりませんか ? 要するに「setup.S は BIOS からシステムに関するデータを取得し、システム メモリーの適切な場所に保管するためのコードである。」ということです。 まあ、これだけではあんまりなので、ちょっと面白そうなところを抜き出して みると、 ! SETUP-header, must start at CS:2 (old 0x9020:2) ! .ascii "HdrS" ! Signature for SETUP-header .word 0x0201 ! Version number of header format ! (must be >= 0x0105 ! else old loadlin-1.5 will fail) realmode_swtch: .word 0,0 ! default_switch,SETUPSEG start_sys_seg: .word SYSSEG .word kernel_version ! pointing to kernel version string ! note: above part of header is compatible with loadlin-1.5 (header v1.5), ! must not change it type_of_loader: .byte 0 ! = 0, old one (LILO, Loadlin, ! Bootlin, SYSLX, bootsect...) ! else it is set by the loader: ! 0xTV: T=0 for LILO ! T=1 for Loadlin ! T=2 for bootsect-loader ! T=3 for SYSLX ! T=4 for ETHERBOOT ! V = version loadflags: ! flags, unused bits must be zero (RFU) LOADED_HIGH = 1 ! bit within loadflags, ! if set, then the kernel is loaded high CAN_USE_HEAP = 0x80 ! if set, the loader also has set heap_end_ptr ! to tell how much space behind setup.S | can be used for heap purposes. ! Only the loader knows what is free! #ifndef __BIG_KERNEL__ .byte 0x00 #else .byte LOADED_HIGH #endif "type_of_loader" のところで LILO, Loadlin, bootsect-loader, SYSLX, ETHERBOOT がリストされていま す。 Linux のカーネルローダーも結構種類がありますね。 この最初の部分ではローダーのチェックをしています。古いローダーでは "big kernel" をうまく扱えないため、そういう場合には警告を発して止るよ うになっています。 ローダーのチェックが終わると、メモリーサイズのチェックが始まります。 loader_ok: ! Get memory size (extended mem, kB) #ifndef STANDARD_MEMORY_BIOS_CALL push ebx xor ebx,ebx ! preload new memory slot with 0k mov [0x1e0], ebx mov ax,#0xe801 int 0x15 jc oldstylemem "int 0x15" と #0xe801 を組み合わせてメモリーサイズをチェックしていま す。 ! Memory size is in 1 k chunksizes, to avoid confusing loadlin. ! We store the 0xe801 memory size in a completely different place, ! because it will most likely be longer than 16 bits. ! (use 1e0 because that's what Larry Augustine uses in his ! alternative new memory detection scheme, and it's sensible ! to write everything into the same place.) 次はキーボードリピートレートの設定です。 ! Set the keyboard repeat rate to the max mov ax,#0x0305 xor bx,bx ! clear bx int 0x16 そしてコンソール用ビデオカードのチェック。 ! Check for video adapter and its parameters and allow the ! user to browse video modes. call video ! NOTE: we need DS pointing to boot sector この "call video" で呼び出されているのは同じディレクトリにある arch/i386/boot/video.S の中で定義されている関数です。 以後、各ハードウェアをチェックしている部分で、コメントだけ拾っていくと ( [] 内はコメントを和訳したものです) ! Get hd0 data [hd0 のデータを取得] ! Get hd1 data [hd1 のデータを取得] ! Check that there IS a hd1 :-) [hd1 が接続されているかどうかチェック] ! check for Micro Channel (MCA) bus [マイクロチャンネル (MCA) バスをチェック] ! Check for PS/2 pointing device [PS/2 のポインタ装置 (マウス、パッド、スティックなど) をチェック] #ifdef CONFIG_APM ! check for APM BIOS [APM BIOS をチェック] ! ! Redo the installation check as the 32 bit connect ! modifies the flags returned on some BIOSs ! [32bit で接続するとフラッグの値を変更する BIOS があるので インストレーションチェックを再度実行] done_apm_bios: #endif などの処理があります。 この後、 ! Now we want to move to protected mode ... [いよいよプロテクトモードへ移行する時だ、、、] ! we get the code32 start address and modify the below 'jmpi' ! (loader may have changed it) [code32 の開始アドレスを取得して下の "jmpi" を変更する (ローダーによって変更されているかもしれないので)] ! Now we move the system to its rightful place ! ...but we check, if we have a big-kernel. ! in this case we *must* not move it ... [さあ、システムを正規の場所へ移動しよう、、、しかしその前に big-kernel を使っているかどうかチェックしないとダメだ。 もし big-kernel を使っているなら、場所を移動 *してはならない* ] ! then we load the segment descriptors [次にセグメントデスクリプタ (アドレスを示す情報) をロードする] ! If we have our code not at 0x90000, we need to move it there now. ! We also then need to move the parameters behind it (command line) ! Because we would overwrite the code on the current IP, we move ! it in two steps, jumping high after the first one. [もしカーネルコードが 0x90000 に無かったら、この時点でそこへ移動する 必要がある。また、この後のパラメータ (コマンドラインパラメータ) も あわせて移動しなければならない。 この動作は現在の IP にあるコードを上書きしてしまうため、移動は 2 段階に分けて行われる。最初の移動の後で high 領域へ移行するのだ。] ! that was painless, now we enable A20 [これはたいしたことじゃない。さあ、A20 を有効にしよう。] ! wait until a20 really *is* enabled; it can take a fair amount of ! time on certain systems; Toshiba Tecras are known to have this ! problem. The memory location used here is the int 0x1f vector, ! which should be safe to use; any *unused* memory location < 0xfff0 ! should work here. [a20 が「本当に」有効になるまで待とう。ある種のシステムではこのために えらく長い時間が必要なんだ。 Toshiba の Tecra シリーズはこの問題を 持っていることで知られている。ここで使われているメモリーの位置は ベクター int 0x1f で、使っても大丈夫なはず。この時点では 0xfff0 より 下の使われていないメモリー位置ならどこでも利用できるはずなんだ。] と、32bit protect mode への移行の準備を進めていきます。 最後の "wait until a20 really ..." は 2.0.xx 系カーネルで bzImage にす ると起動できなかった東芝の TECRA や Portege などを始めとしたノート PC などへの対策ですね。 2.2 系カーネルでは TECRA などのノート PC でも安心 して bzImage を利用できるようになったらしいという話を聞いたことがあり ます。 続いて、インタラプト関係の処理。 ! make sure any possible coprocessor is properly reset.. [接続されている可能性のあるすべてのコプロセッサがちゃんとリセットされて いることを確認しよう、、、] ! well, that went ok, I hope. Now we have to reprogram the interrupts :-( ! we put them right after the intel-reserved hardware interrupts, at ! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really ! messed this up with the original PC, and they haven't been able to ! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f, ! which is used for the internal hardware interrupts as well. We just ! have to reprogram the 8259's, and it isn't fun. [ああ、ここまではたぶんうまくいった。そう思うよ。さて、これから 割り込みを再設定 (reprogram) しなきゃいけない。(ウンザリ) Linux では割り込みを Intel が予約したハードウェア割り込みのすぐ後、 int 0x20-0x2F に押し込むんだ。ここなら何も邪魔されないからね。 悲しいことに、IBM は最初の PC を作る際、割り込みの設定をメチャクチャに してしまった。そして、結局彼らはこれを直すことができなかったんだ。 だから、BIOS の割り込みはハードウェアの内部割り込みが使っているのと 同じ領域、0x08-0x0f にあるんだよ。というわけで、僕らはこれから 8259 を プログラムし直さないといけない。そしてこいつは全然楽しくないんだ。] ! Well, that certainly wasn't fun :-(. Hopefully it works, and we don't ! need no steenking BIOS anyway (except for the initial loading :-). ! The BIOS routine wants lots of unnecessary data, and it's less ! "interesting" anyway. This is how REAL programmers do it. [うーん、こいつはたしかにちっとも面白くない (ああ疲れた)。 とにかく、これでうまく動いてくれると思う。それに、どっちにしても もう BIOS をいじくる必要は無いんだ。(最初のロード以外は。わかるよね) BIOS ルーチンはやたらとたくさんの不要なデータを欲しがるし、こいつは 全然 "おもしろい" ことじゃない。「本物の」プログラマならこうするさ。] ! Well, now's the time to actually move into protected mode. To make ! things as simple as possible, we do no register set-up or anything, ! we let the GNU-compiled 32-bit programs do that. We just jump to ! absolute address 0x1000 (or the loader supplied one), ! in 32-bit protected mode. [さて、今度こそ本当にプロテクトモードへ移行する時だ。できるだけ物事を 単純に保つために、レジスター設定とかそういったものは何もしない。 ここでは GNU のツールでコンパイルされた 32-bit のプログラムにそれを やらせるんだ。ただ単に 32-bit のプロテクトモードで絶対アドレス 0x1000 (ローダーが指定した場所) へジャンプするだけだよ。] ! Note that the short jump isn't strictly needed, although there are ! reasons why it might be a good idea. It won't hurt in any case. [ここで short jump がどうしても必要ってわけじゃないことに注意。 ただ、こうしておいたほうがいい理由もいくつかあるんだ。それに こうしたからって何か問題が起きるってこともないしね。] どうやら Linus さんは最初に開発を始めた頃、このあたりの処理に相当苦労 したらしく、"Sadly IBM messed this up" とか "and it isn't fun." また "Well, that certainly wasn't fan :-(." なんてのもあります。一方で、自 分の挙げた成果にはそれなりに愛着も持っているようで、"This is how REAL programmers do it." などと書いてあったりもします。 JF の資料に "Linux HISTORY" という文書が ありますが、その中にこんな一節があります。 > 1) カーネルを作っているときには、だいたいどうやってデバッ グしますか ? 使っているマシンと、作業の進み具合によります。もっとシンプル なシステムならたいていセットアップはもっと簡単です。プロテク トモードの 386 で私がやらなければならなかったことをかきま す。 一番厄介な所は一番最初です。printf 等が使える最低限のシステ ムを手に入れることができた後であってさえも、386 でのプロテク トモードへの移行は楽しくないです。 386 のアーキテクチャを良 く知らぬままに始めたのであればなおさらです。この段階では、シ ステムは死にたくなるほどリブートしまくります。もし 386 がな にかがヘンだと気づいた日には、シャットダウンしてリブートして しまいます。何が悪いのかの証拠を残す暇もありません。 printf() もたいして役に立ちません。リブートすれば画面もきれ いさっぱりです。それから、VRAM も叩かなければだめです。 VRAM はセグメントが間違っていたりすると落っこちてくれます。デバッ ガなんて考えるだけ無駄です。386 のプロテクトモードまでついて いくデバッガなんて聞いたことがありません。386 エミュレータ や、一部の重装備のマシンならなんとかなるかもしれませんが、大 抵は駄目です。 私が使ったのは、ただの時間稼ぎのループでした。 die: jmp die このようなものをここぞというところに入れます。止まってしまえ ば OK ですし、リブートしてしまったら、すくなくともこの die ループの前が怪しいとわかります。変わりにサウンドポートも利用 できますが、私は PC のハードはいじったことがなかったので、全 然使いませんでした。これ以外に方法がないわけではありません。 私はカーネルを書こうと思って始めたのではなく、ただ 386 のタ スクスイッチ等について知りたかっただけです。しかし、とにかく こうして書き始めました。 (91年の4月のことでした) 最低限のシステムが出来上がり、スクリーンを出力に使えるように なると、少々楽になります。しかし、ここで割り込みを有効にしな ければなりません。ドカ〜ン。いきなりリブートして、また最初の 方法に逆戻り。全てがこの調子で、およそ 2か月かけて、386 のま わりをまともに動くようにしました。それからは、リブートしない ようにと気を使いながら、同時に基本的なもの (ページング、タイ マ割り込み、単純なタスクスイッチャ、セグメントのテスト) を作 るということをしないで済むようになりました。 このあたりの話を読むと、setup.S のインタラプト関係の処理やプロテクトモ ードへの移行に関する処理にあるコメントの背景が何となくわかるような気が してきませんか ? なお、setup.S の処理は最後にカーネル本体のアドレスへジャンプして終了し ます。 ! NOTE: For high loaded big kernels we need a ! jmpi 0x100000,__KERNEL_CS [注意: high 領域にロードされた big カーネルの場合、ここで "jmpi 0x100000,__KERNEL_CS" を実行する必要がある。] ! but we yet haven't reloaded the CS register, so the default size ! of the target offset still is 16 bit. ! However, using an operant prefix (0x66), the CPU will properly ! take our 48 bit far pointer. (INTeL 80386 Programmer's Reference ! Manual, Mixing 16-bit and 32-bit code, page 16-6) [でもこの時点ではまだ CS レジスターを再ロードしていないから、 ターゲットオフセットのデフォルトサイズはまだ 16 bit なんだ。 ところが、オペラントプレフィックス (0x66) を使えば、CPU は うまいこと 48 bit の far ポインタを扱ってくれる。 (INTeL 80386 プログラマーズレファレンスマニュアル、 16-bit と 32-bit のコードの混用、ページ 16-6)] db 0x66,0xea ! prefix + jmpi-opcode code32: dd 0x1000 ! will be set to 0x100000 for big kernels dw __KERNEL_CS 5. カーネル本体の展開 さて arch/i386/boot/setup.S から処理を引き継いだ「カーネル本体」です が、実はまだその主要部分は圧縮された状態でメモリー中に置かれています。 実際にカーネルが動作を始める前に、まずこの圧縮されたカーネルを復元しな ければいけません。 これは、arch/i386/boot/compressed/head.S にある /* * Do the decompression, and jump to the new kernel.. */ subl $16,%esp # place for structure on the stack pushl %esp # address of structure as first arg call SYMBOL_NAME(decompress_kernel) orl %eax,%eax jnz 3f xorl %ebx,%ebx ljmp $(__KERNEL_CS), $0x100000 によって実行されます。なおこの decompress_kernel は同じディレクトリに ある arch/i386/boot/compress/misc.c の中で int decompress_kernel(struct moveparams *mv) { if (SCREEN_INFO.orig_video_mode == 7) { vidmem = (char *) 0xb0000; vidport = 0x3b4; } else { vidmem = (char *) 0xb8000; vidport = 0x3d4; } lines = SCREEN_INFO.orig_video_lines; cols = SCREEN_INFO.orig_video_cols; if (free_mem_ptr < 0x100000) setup_normal_output_buffer(); else setup_output_buffer_if_we_run_high(mv); makecrc(); puts("Uncompressing Linux... "); gunzip(); puts("Ok, booting the kernel.\n"); if (high_loaded) close_output_buffer_if_we_run_high(mv); return high_loaded; } として定義されており、さらにこの中で使われている gunzip() については lib/infalte.c の中で定義されています。 (これで起動時に "Uncompressing Linux... " というメッセージを出している のが何処か、わかりましたね。) さて、arch/i386/boot/compressed/head.S の冒頭に書かれているコメントを 以下に引用してみましょう。 /* * linux/boot/head.S * * Copyright (C) 1991, 1992, 1993 Linus Torvalds */ /* * head.S contains the 32-bit startup code. * おや ? ファイル名が違いますね。これは Linux カーネルが現在のように多く の機種に移植されていなかった 1.x の頃以前のファイル名でしょう。当時は 現在のようにアーキテクチャに依存した部分が分離されていませんでしたか ら。 さて、圧縮されていたカーネル本体も展開されました。次はこの中にジャンプ していきます。arch/i386/boot/compressed/head.S の最後は次のようになっ ています。 /* * Do the decompression, and jump to the new kernel.. */ subl $16,%esp # place for structure on the stack pushl %esp # address of structure as first arg call SYMBOL_NAME(decompress_kernel) orl %eax,%eax jnz 3f xorl %ebx,%ebx ljmp $(__KERNEL_CS), $0x100000 /* * We come here, if we were loaded high. * We need to move the move-in-place routine down to 0x1000 * and then start it with the buffer addresses in registers, * which we got from the stack. */ 3: movl $move_routine_start,%esi movl $0x1000,%edi movl $move_routine_end,%ecx subl %esi,%ecx cld rep movsb popl %esi # discard the address popl %esi # low_buffer_start popl %ecx # lcount popl %edx # high_buffer_start popl %eax # hcount movl $0x100000,%edi cli # make sure we don't get interrupted ljmp $(__KERNEL_CS), $0x1000 # and jump to the move routine /* * Routine (template) for moving the decompressed kernel in place, * if we were high loaded. This _must_ PIC-code ! */ move_routine_start: rep movsb movl %edx,%esi movl %eax,%ecx # NOTE: rep movsb won't move if %ecx == 0 rep movsb xorl %ebx,%ebx /* * Well, the kernel relies on %esp pointing into low mem, * with the decompressor loaded high this is no longer true, * so we set esp here. */ mov $0x90000,%esp ljmp $(__KERNEL_CS), $0x100000 move_routine_end: "decompress_kernel" のすぐ後の "jnz" で "3:"へジャンプせずに、そのま ま"ljmp $(__KERNEL_CS), $0x100000" する場合 (zImage) と、いったん "3:" へジャンプして "move_routine_start:" と "move_routine_end:" の間で展開 したカーネルの場所の移動を行なってから "ljmp $(__KERNEL_CS), $0x100000" する場合 (bzImage) があります。 6. デバイスドライバーの設定 さて、展開されたカーネル本体で最初に実行されるのは、 arch/i386/kernel/head.S です。例によって冒頭のコメント。 /* * linux/arch/i386/head.S -- the 32-bit startup code. * * Copyright (C) 1991, 1992 Linus Torvalds * * Enhanced CPU detection and feature setting code by Mike Jagdis * and Martin Mares, November 1997. */ 「32bit スタートアップ」「強化された CPU 検出と機能設定」といった文が 並んでいます。 これもコメントを追いかけてみましょう。 * References to members of the boot_cpu_data structure. * swapper_pg_dir is the main page directory, address 0x00101000 * Set segments to known values * New page tables may be in 4Mbyte page mode and may * be using the global pages. * * NOTE! We have to correct for the fact that we're * not yet offset PAGE_OFFSET.. * Setup paging (the tables are already set up, just switch them on) * Clear BSS first so that there are no surprises... * start system 32-bit setup. We need to re-do some of the things done * in 16-bit mode for the "real" operations. * Initialize eflags. Some BIOS's leave bits like NT set. This would * confuse the debugger if this code is traced. * XXX - best to initialize before switching to protected mode. * Copy bootup parameters out of the way. First 2kB of * _empty_zero_page is for boot parameters, second 2kB * is for the command line. /* check if it is 486 or 386. */ * XXX - this does a lot of unnecessary setup. Alignment checks don't * apply at our cpl of 0 and the stack ought to be aligned already, and * we don't need to preserve eflags. どうやら、メモリー管理テーブルや CPU のフラッグ設定などを行なっている ようです。 この head.S は最後に start_kernel を実行します。 xorl %eax,%eax lldt %ax cld # gcc2 wants the direction flag cleared at all times call SYMBOL_NAME(start_kernel) L6: jmp L6 # main should never return here, but # just in case, we know what happens. head.S から呼び出される start_kernel は init/main.c にあります。この ファイルの冒頭にあるコメントを引用します。 /* * linux/init/main.c * * Copyright (C) 1991, 1992 Linus Torvalds * * GK 2/5/95 - Changed to support mounting root fs via NFS * Added initrd & change_root: Werner Almesberger & Hans Lermen, Feb '96 * Moan early if gcc is old, avoiding bogus kernels - Paul Gortmaker, May '96 * Simplified starting of init: Michael A. Griffith */ 続いて、start_kernel の最初の部分です。 asmlinkage void __init start_kernel(void) { char * command_line; #ifdef __SMP__ static int boot_cpu = 1; /* "current" has been set up, we need to load it now */ if (!boot_cpu) initialize_secondary(); boot_cpu = 0; #endif /* * Interrupts are still disabled. Do necessary setups, then * enable them */ printk(linux_banner); setup_arch(&command_line, &memory_start, &memory_end); memory_start = paging_init(memory_start,memory_end); trap_init(); init_IRQ(); sched_init(); time_init(); parse_options(command_line); "linux_banner" というのは init/version.c に定義があります。例えば (こ れは私が今使っている 2.0.36 の例ですが) Linux version 2.0.36 (root@pika) (gcc version 2.7.2.3) #1 Wed Feb 10 21:57:36 JST 1999 といった感じのものです。起動時にこの「バナー」を出しているのは init/main.c の start_kernel だったわけですね。 次に "setup_arch" ですが、これは arch/i386/kernel/setup.c で定義されて います。内容は時間の都合で省略しますが、起動時に BIOS から収集した情報 を (他のデバイスドライバーからアクセスできるよう) あらためて整理してい ます。"paging_init" は arch/i386/mm/setup.c にあります。メモリーページ テーブルの設定を行ないます。 "trap_init" は arch/i386/kernel/traps.c にあって IDT テーブルの初期化を実行します。"init_IRQ" は arch/i386/kernel/irq.c の中にあります。IRQ 関係の設定を行なうもので す。"shced_init" は kernel/sched.c の中で定義されています。 "time_init" は kernel/time.c に、そして parse_optinos は start_kernel と同じく init/main.c の中で定義されています。 このあと、いくつも初期化ルーチンを実行した後 kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGHAND); に到達します。ここで引数として指定されている "init" は同じ init/main.c の中で定義されているものです。 static int init(void * unused) { lock_kernel(); do_basic_setup(); /* * Ok, we have completed the initial bootup, and * we're essentially up and running. Get rid of the * initmem segments and start the user-mode stuff.. */ free_initmem(); unlock_kernel(); if (open("/dev/console", O_RDWR, 0) < 0) printk("Warning: unable to open an initial console.\n"); (void) dup(0); (void) dup(0); この "init" の中で呼び出されている lock_kernel は SMP の機械にのみ関係 するものです。 次の do_basic_setup は init/main.c の中で定義されています。この関数の 中にはバスの設定やネットワークソケットの初期化、ファイルシステムの認 識、ルートパーティションのマウントなど「デバイスドライバの初期化」とい うタイトルにふさわしい内容が含まれているので、本来ならこの中身をそれぞ れ調べてみたいところなのですが、今回は時間が無くなってしまったので省略 します。 もし興味があれば、是非自分で調べてみて下さい。 7. init の起動 上の項目で一部紹介した init/main.c で定義されている、カーネル内の "init" 関数ですが、最後はこんな風になっています。 /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) execve(execute_command,argv_init,envp_init); execve("/sbin/init",argv_init,envp_init); execve("/etc/init",argv_init,envp_init); execve("/bin/init",argv_init,envp_init); execve("/bin/sh",argv_init,envp_init); panic("No init found. Try passing init= option to kernel."); ここで if (execute_command) から始まる 2 行は、起動時のカーネルオプ ション init= によって最初に起動するプログラムを指定した場合のためのも のです。オプションを指定しない場合はそのまま通過します。 次の execve("/sbin/init",...) は /sbin/init を実行して、もし問題が無け ればそのまま戻ってこない、という命令です。通常のシステム起動ではここで 制御が /sbin/init に移行して、これ以後のコードは使われません。 もし何らかの理由で /sbin/init を実行できない場合は、同様の方法で /etc/init、/bin/init、そして /bin/sh の実行を試します。もし先に試した ものがうまく実行できれば、そのまま処理を渡してしまうのでそれ以降のコー ドは実行されません。 最終的に、/bin/sh も含めて、どうしても処理を渡すことができない場合は、 最後の panic() でエラーメッセージを表示して停止します。 つまり、ここが "man 8 init" に記載されている「カーネルブートの最後のス テップ」というわけです。 8. 番外: Makefile について カーネルがロードされた時に、どのファイルに入っているコードが実行される のか、という点は Makefile を調べると書いてあります。 まずトップディレクトリの Makefile には、次の記述があります。 include arch/$(ARCH)/Makefile vmlinux: $(CONFIGURATION) init/main.o init/version.o linuxsubdirs $(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o \ --start-group \ $(CORE_FILES) \ $(FILESYSTEMS) \ $(NETWORKS) \ $(DRIVERS) \ $(LIBS) \ --end-group \ -o vmlinux $(NM) vmlinux | grep -v '\(compiled\)\|\(\.o$$\)\|\( [aU] \)\|\(\.\.ng$$\)\|\(LASH[RL]DI\)' | sort > System.map 今回の話では i386 を前提としているので "arch/i386/Makefile" を参照して みると、 HEAD := arch/i386/kernel/head.o arch/i386/kernel/init_task.o zImage: vmlinux @$(MAKEBOOT) zImage という記述が見つかります。これと上記のトップディレクトリの Makefile と から、トップディレクトリの vmlinux の先頭が "arch/i386/kernel/head.o" であることがわかります。一方、トップディレクトリの Makefile には、次の 記述もあります。 boot: vmlinux @$(MAKE) -C arch/$(ARCH)/boot この記述から、ブートイメージの作成について知りたければ、 "arch/i386/boot" ディレクトリの Makefile を調べてみると良さそうだ、と いうことがわかります。 そこで "arch/i386/boot" ディレクトリの Makefile を見てみると、次の記述 があります。 zImage: $(CONFIGURE) bootsect setup compressed/vmlinux tools/build $(OBJCOPY) compressed/vmlinux compressed/vmlinux.out tools/build bootsect setup compressed/vmlinux.out $(ROOT_DEV) > zImage compressed/vmlinux: $(TOPDIR)/vmlinux @$(MAKE) -C compressed vmlinux setup: setup.o $(LD86) -s -o $@ $< setup.o: setup.s $(AS86) -o $@ $< setup.s: setup.S video.S Makefile $(BOOT_INCL) $(TOPDIR)/include/linux/version.h $(CPP) -traditional $(SVGA_MODE) $(RAMDISK) $< -o $@ bootsect: bootsect.o $(LD86) -s -o $@ $< bootsect.o: bootsect.s $(AS86) -o $@ $< bootsect.s: bootsect.S Makefile $(BOOT_INCL) $(CPP) -traditional $(SVGA_MODE) $(RAMDISK) $< -o $@ ここから、bootsect.S から bootsect.s が作成され、さらに bootsect.o を 経由して bootsect になること、同じく setup.S と vide.S から setup.s が 作成され、さらに setup.o を経由して setup になることがわかります。また こうしてできた bootsect と setup がそれぞれ zImage ファイルの先頭と 2 番目に該当することもわかります。(従って、フロッピーからカーネルをブー トすると最初に bootsect.S のコードが実行されるわけです) そしてさらに、arch/i386/boot/compressed/Makefile を見ると以下の記述が あります。 HEAD = head.o SYSTEM = $(TOPDIR)/vmlinux OBJECTS = $(HEAD) misc.o vmlinux: piggy.o $(OBJECTS) $(LD) $(ZLINKFLAGS) -o vmlinux $(OBJECTS) piggy.o head.o: head.S $(TOPDIR)/include/linux/tasks.h $(CC) $(AFLAGS) -traditional -c head.S piggy.o: $(SYSTEM) tmppiggy=_tmp_$$$$piggy; \ rm -f $$tmppiggy $$tmppiggy.gz $$tmppiggy.lnk; \ $(OBJCOPY) $(SYSTEM) $$tmppiggy; \ gzip -f -9 < $$tmppiggy > $$tmppiggy.gz; \ echo "SECTIONS { .data : { input_len = .; LONG(input_data_end - input_data) input_data = .; *(.data) input_data_end = .; }}" > $$tmppiggy.lnk; \ $(LD) -m elf_i386 -r -o piggy.o -b binary $$tmppiggy.gz -b elf32-i386 -T $$tmppiggy.lnk; \ rm -f $$tmppiggy $$tmppiggy.gz $$tmppiggy.lnk これから、compressed/vmlinux の先頭は head.S のコードであることがわか ります。 以上のことをまとめると 1. arch/i386/boot/bootsect.S (フロッピーブート時、最初に実行) 2. arch/i386/boot/setup.S (+ video.S) 3. arch/i386/boot/compressed/head.S (+ misc.c) 4. arch/i386/kernel/head.S ということになります。 9. 終わりに 9.1. お願い とりあえずなんとかまとめてみましたが、なにぶん、私もまだまだ知らないこ とがたくさんあります。ここに書いた中にも間違いがあるかもしれません。も し改良のためのアドバイスをお持ちでしたら、是非教えてください。よろしく お願いします。 9.2. 謝辞 最初にこの文書をリリースするまでに、NLUG や JF のメンバーの方々から多 くの有益な意見を頂きました。ありがとうございます。また、日頃お世話に なっている fj.os.linux や Nifty FUNIX の方々にもこの場を借りてお礼を申 し上げます。 最初にリリースした後で、おくじさんから BIOS によるブートストラップの動 作について御指摘を頂きました。またくりこさんから "the longest-living linux bug" の説明について有益な御意見を頂きました。どうもありがとうご ざいます。 野本さんから i386 のアセンブラについて参考になる情報を頂きました。どう もありがとうございます。 9.3. この文書の配布について copyrighted (c) 1999 Taketoshi Sano この文書は GNU パブリックライセンス (GPL) バージョン 2 かそれ以降の条 件、あるいは標準的な Linux ドキュメントプロジェクト (LDP) の条件に基づ いた配布ならば自由にしていただいてかまいません。これらのライセンスはこ のドキュメントが入手できるようなサイトから入手できます。LDP の条件は (翻訳をのぞく) いかなる修正も許可していません。修正されたバージョンは GPL の基でのみ配布されるものとすることが可能です。