Arch Linuxでカーネルを手動でインストール + make installの挙動をソースコードから追跡
今回やりたいこと
Gentoo Linuxのように、Arch Linuxでもカーネルを自分でmake && make modules_install
してmake install
できないかと考えました。
結論に至るまでに多くのソースコードを参照していたのですが、結局答えはArchWikiにありましたので、結果だけ知りたい方はこちらをご覧ください。また、ArchWikiのコードを自動化したものを作成したのでそちらもご覧ください。
自動化ツールとその使い方
ツールをダウンロード
wget https://raw.githubusercontent.com/Hayao0819/Hayao-Tools/master/arch-kernel-installer/arch-kernel-installer.sh
chmod +x arch-kernel-installer.sh
カーネルソースをダウンロード
今回はバニラカーネルですが、お好みでどうぞ
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.5.6.tar.xz
tar xvf linux-6.5.6.tar.xz
cd linux-6.5.6
カーネルコンフィグを生成
イチから設定したい方
# config以外を綺麗にする
make clean
# 現在のモジュールを流用(お好み)
make localmodconfig
# 設定画面
make menuconfig
Arch Linuxのコンフィグを流用する
# config以外を綺麗にする
make clean
# Archのコンフィグをコピー
zcat /proc/config.gz > .config
# 新しいコンフィグを確認
make oldconfig
# 設定画面(お好み)
make menuconfig
カーネルをコンパイルしてインストール
-j
のあとの数字はnprocコマンドの値(CPU数)を参考に設定する。
私の場合は24なので、ちょい少なめに22を指定する。nprocの値をそのまま利用しても良い。
make -j22
sudo make modules_install
スクリプトを使用してArch Linux用の設定ファイルを生成
Gentoo Linuxの場合はこのあとはmake install
して終了なのですが、Arch Linuxの場合はmkinitcpioという独自ツールを用いている関係上、このコマンドは利用できません。
linux-torvalds
の部分は好きな名前に変更して下さい。この部分が後のファイル名になります。
cd ..
sudo ./arch-kernel-installer.sh ./linux-6.5.6 linux-torvalds
こんな感じの出力が出れば成功です。
arch-kernel-installer.sh: Run make -s image_name
arch-kernel-installer.sh: They are the files which are installed by this script.
arch-kernel-installer.sh: - /boot/vmlinuz-linux-torvalds
arch-kernel-installer.sh: - /etc/mkinitcpio.d/linux-torvalds.preset
mkinitcpioを用いてinitramfsを/bootに配置
ここまでくればArch Linuxの公式カーネルと同じようなことができます。
mkinitcpio -p linux-torvalds
自動化ツールを作るまで(以下おまけ)
これ以下は上記のスクリプトを開発するまでに、カーネルやmkinitcpioのソースコードを追跡していった記録です。基本的に読む必要はありません。
先程make install
は用いることができないと書きましたが、実行してみるとこのようなエラーが出ます。
ということで、このエラーの発生元と解決方法を今からソースコードを基に追跡します。
エラーの発生元
Cannot find LILO.
カーネルのソースコードでCannot find LILO.
で検索すると、以下のファイルが見つかりました。
make install
されたときに実行されるスクリプトのようです。
アーキテクチャはx86_64
なのにx86
?と思い、調査を続けているとこんな記述が。
どうやらi386もx86_64もLinuxカーネル上ではx86という扱いのようです。
余談ですがLinuxカーネルはi386のサポートは終了したはずなのにMakefileには残っているんですね。
install.shの正体
話を元のエラーに戻して先程のinstall.sh
を読んでみます。
内容自体は非常に簡単なシェルスクリプトで、以下のことを順番に実行しています。
- 既存のカーネルをバックアップ(.oldに移動)
- 新しいカーネルをコピー
- liloを実行
今回は3番目がエラーになっているようです。しかし、このスクリプトではliloが存在するかどうかを確認していません。
ということは、正常なカーネルのコンパイルではそもそもこのファイルは実行されないということです。
このinstall.sh
はどこから実行されているのでしょうか。
make installの正体
Makefileのinstall
というターゲットを探せばよいのですが、リポジトリ直下のMakefileにはそのようなターゲットは記述されていません。
探してみると、x86
内のMakefileに記述がありました。
しかし、このターゲットは以下を実行しているに過ぎません。
$(call cmd,install)
callというのはMakefileで使える内部関数です。
cmdという変数は以下で定義されています。
要約するとcmd_$1
という変数を呼び出しているようで、今回の場合はcmd_install
という変数が実態のようです。
cmd_install
を探してみると、ここで定義されているようです。
このコードによると、$(call cmd,install)
というのはscripts/install.sh
を実行しているようです。
scripts/install.shを読む
scripts/install.shは更にシェルスクリプトを呼び出しているようです。
このあたりを読むとこのスクリプトは以下を実行しているようです。
- 以下のスクリプトを順番に探す
- ~/bin/${INSTALLKERNEL}
- /sbin/${INSTALLKERNEL}
- arch/${SRCARCH}/install.sh
- arch/${SRCARCH}/boot/install.sh
- ない場合はエラーを出して終了
INSTALLARCH変数はここで定義されており、そのままinstallarchという文字列です。
SRCARCHは今回はx86
になります。
つまり、make install
は内部的にはinstallkernel
というコマンドを特定の引数とともに実行しているに過ぎませんでした。
今回Arch Linuxには/sbin/installkernel
が存在しないため、最終的にarch/x86/install.sh
が実行されたというわけです。
Gentoo Linuxの場合
Gentoo Linuxでは、installkernel-gentoo
というパッケージによって/sbin/installkernel
がインストールされるようです。
このスクリプトもDebianのものをベースにしているようで、UbuntuやDebianも同様の手法に近いことがわかります。
Arch Linuxはどのように対処しているのか
ここまでtorvalds/linuxを参照してきましたが、Arch Linuxではどうでしょうか?
Arch Linuxのcore/linux
のPKGBUILDを参照してみます。
Arch Linuxのシステムには/sbin/installkernel
は見当たらず(これがそもそものエラーの原因)、makedepends
にもそれらしき記述は無さそうです。
ここで衝撃的事実なのですが、Arch LinuxはPKGBUILD内でmake install
を実行していません。
つまり/boot
以下にバイナリはないのです。ここまで追って思い出しましたが、数年前にこんなニュースがありました。
ニュースを簡単に要約すると「ソースコードの簡潔さと互換性維持、独立性の維持のために/boot以下にはファイルを置かないよ。/bootへの設置はmkinitcpioのフックを用いるよ。」とのことです。
現にlinuxパッケージが/boot以下のファイルを所有していないという事実は以下のコマンドで確認できます。
pacman -Ql linux | grep "/boot"
ということで、今からArch Linuxで/boot以下にカーネルとinitramfsが生成されるまでの過程を追跡していきます。
mkinitcpioは何をしているのか
mkinitcpio -P
というコマンドを見かけますが、これは内部的には何をしているのでしょうか?
-P
オプションは「すべてのプリセットに対して実行する」という意味なので、今回は話を簡単にするためにmkinitcpio -p linux
というコマンドに話を置き換えます。
Arch Linuxのmkinitcpioはシェルスクリプトで書かれているので、挙動を簡単に追跡できます。
プリセットの実態はただのシェルスクリプト用の変数を定義したファイルで、/etc/mkinitcpio.d/linux.preset
で簡単に見つかります。
プリセットに関する処理は、以下のprocess_preset
関数にて行われています。
要約すると、プリセットに書かれている変数を読み込んでmkinitcpio自身の引数を決定し、再帰的に呼び出しているようです。
mkinitcpio -p linux
を実行すると、更に以下のコマンドが呼び出されます。
mkinitcpio -k /boot/vmlinuz-linux -c /etc/mkinitcpio.conf -g /boot/initramfs-linux.img --microcode /boot/intel-ucode.img
先程のニュースでは The installation is done by mkinitcpio hooks and scripts, as well as removals. と書かれていますが、実際にはmkinictpio
はカーネルファイルを基にinitramfsを生成するだけなので、カーネルそのものは生成しません。
つまり、mkinitcpio
コマンドでもなくmake install
でも無い何者かがカーネルを/boot以下にインストールしていることになります。
カーネルを/bootに配置しているスクリプト
Google上で色々と調査を続けていると、以下のような記述を見かけました。
[SOLVED] What creates /boot/vmlinuz-linux ? / Newbie Corner / Arch Linux Forums
このフォーラムによると、90-mkinitcpio-install.hook
がカーネルをコピーしているようです。
HookというのはArch Linuxに使われているパッケージマネージャであるpacmanの機能の1つで、パッケージのインストールや削除などのタイミングで任意のコマンドを実行できる非常に便利なものです。
そしてこの90-mkinitcpio-install.hook
のソースコードは以下にありました。
重要なのは以下の2行です。
要約すると、「/usr/lib/modules/*/vmlinuz
が更新されたとき/usr/share/libalpm/scripts/mkinitcpio install
」を実行するという内容です。
実行されているスクリプトのソースコードのうち、カーネルがインストールされたときに実行される重要な関数はinstall_kernel
です。
この関数は主に2つのことを行っています。
- /etc/mkinitcpio.d/以下にプリセットを作成する
- カーネル(厳密にはフックのトリガー元になったファイル)を/boot以下にコピーする
1つめのプリセットの作成を行っているのは以下のコードです。
/usr/share/mkinitcpio/hook.preset
というテンプレートの%PKGBASE%
という文字列を置き換えてコピーしているだけです。
2つめのカーネルのコピーは以下のコードで実行されています。
非常にわかりにくいのですがline
という変数は以下で定義されており、要約すると標準入力の値です。
標準入力の内容の正体はPacman Hookの仕様に書いてあります。
Hookの中にあるNeedsTargets
を用いると、Hookのトリガーになったファイルのフルパスがスクリプトの標準入力として渡されるようです。
またkernellist
という配列は、これより上で定義されている関数read_preset
によりプリセットのpreset_kver
やALL_kver
を基に定義されています。
トリガーになっているカーネルの正体
これで一見落着かと思うと、そうではありません。
先程のHookのトリガーをもう一度見てみます。
/usr/lib/modules/
以下ということは、本来ならmake modules_install
の段階でインストールされます。
しかし、バニラのLinuxソースでmake modules_install
を実行してもそのようなファイルは生成されません。
ということで更に調査を続けていきます。幸い、このコードは簡単に見つかりました。
パッケージングの段階でPKGBUILDのpackage
関数で作成されていたもののようです。
このmake -s image_name
はLinuxのMakefileで以下のように定義されています。
KBUILD_IMAGE変数を更に追っていきます。
ということで、x86の場合はarch/x86/boot/bzImage
になります。
そもそもvmlinuzって何という方は、こちらの記事が非常に参考になりました。
まとめると、PKGBULD内でarch/x86/boot/bzImage
をmodules以下にvmlinuzとしてコピーしている、というわけです。
Arch Linuxの処理を模倣する
これまで長々とArch Linuxでの挙動を追跡してきました。
要約すると、バニラなカーネルをArch Linuxで用いるには主に2つのことを行う必要があります。
- mkinitcpioのフックを作成する
- カーネルを/boot以下に配置する
当初この処理はどちらもmkinitcpioのフックによって実装されているため、強引にそのスクリプトを流用してやろうかとも考えたのですが、このスクリプトの仕様の文書が見当たらなかった(あったら教えてください)以上、仕様が変更されることも考えて1から実装し直しました。
mkinitcpioのフックは、本家と同様に/usr/share/mkinitcpio/hook.preset
を基に生成しています。
また、カーネルのコピーも本家に近い挙動を行っています。
ただし、modules以下へのコピーを行うのではなく/boot以下に直接カーネルをコピーしています。
おそらくArch Linux側が一旦/usr/lib/modules以下にbzImageをコピーしているのはパッケージングの都合であり、それ以上の利点はないと判断したためです。
ちょっとした後悔と注意
後悔と今後
今考えると/sbin/installkernel
をArch Linuxのlibalpmとmkinitcpio用に書いて、make install
を実行できるようにしたほうがよかったのかなと思っています。気が向いたらそっちも書きます。
インターネット上の/sbin/installkernelについて
ネットで検索すると以下のArch Linux用のinstallkernelスクリプトが見つかります。
しかしこのコードが書かれたのは11年前であり、以下のような問題点を抱えています。
- Grub2に対応していない
- mkinitcpioのプリセットを作成していない
- mkinitcoioにマイクロコードが含まれていない
- その他現代の仕様に満たない可能性がある
そのため、これらのコードを流用するのはお控えください。
また、DebianやGentooのコードもmkinitcpioを一切考慮していないので利用することはおすすめしません。
終わり
長々と文章を書いてしまいましたが、ここまでしっかりとLinuxのソースコード(Makefileやシェルスクリプトなのでソースコードそのものかと言われると怪しい)を読んだのは初めてです。
torvalds/linuxでも思った以上にMakefileやシェルスクリプトが多くの場所で使われていました。
また、mkinitcpio
やmakepkg
、mkarchroot
もシェルスクリプトで書かれているのでシェルスクリプトはやはり偉大です。
(この作業を行う前にcore/linuxを改造してchroot環境でコンパイルしようとした際に謎のエラーが発生し、makepkg
やその周辺ツールのシェルスクリプトを読んだのはまた別のお話)
最近はこうしたカーネルのコンフィグやインストールを完全に自動化・抽象化するコマンドラインツールをGolangでちまちま書いているので、いつかお披露目できたらなと思います。
間違い等があったら指摘してくださると助かります。
それでは、また今度。
Discussion