コメントから読む Linux カーネル

Sano Taketoshi <kgh12351@nifty.ne.jp>

$Date: 2000/06/27 13:57:52 $
NLUG (名古屋 Linux ユーザーグループ) 第 3 回勉強会のために 作成した資料です。 今回読むのは Linux カーネル 2.2.5 です。 主に PC/AT (i386) 上で、電源 ON のあと、カーネルのロード、 ブートアップから /sbin/init が実行されるまでをソースコード中の コメントを頼りに追いかけていきます。

1. はじめに

2. システムの起動:概要

3. カーネルのロード

4. BIOS 情報の取得

5. カーネル本体の展開

6. デバイスドライバーの設定

7. init の起動

8. 番外: Makefile について

9. 終わりに


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 <linux/config.h> /* for CONFIG_ROOT_RDONLY */
#include <asm/boot.h>

.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 <linux/config.h> は include/linux/config.h の定義を使う、ということです。 で、このヘッダーファイルの中身はこうなってます。

#ifndef _LINUX_CONFIG_H
#define _LINUX_CONFIG_H

#include <linux/autoconf.h>

#endif

これだけ。これは include/linux/autoconf.h の定義を使いなさい、と いうことです。で、このファイルを探してみると、ありません。

実はこの autoconf.h は make config / menuconfig / xconfig などを 実行して始めて作成されるファイルであって、単にアーカイブを展開した だけでは存在しないものです。

今回は時間の都合で、このファイルを作られる手順を追いかけるのは 省略し、次の #include <asm/boot.h> を調べてみる ことにします。

こちらは 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
! <lermen@elserv.ffm.fgan.de>, <almesber@lrc.epfl.ch>
!
! Video handling moved to video.S by Martin Mares, March 1996
! <mj@k332.feld.cvut.cz>
!
! 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 <grif@acm.org> 
 */

続いて、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 の基でのみ配布されるものとすることが可能です。


sgml21html conversion date: Mon Jan 17 11:59:16 JST 2011