🐧

Arch Linuxでカーネルを手動でインストール + make installの挙動をソースコードから追跡

2023/10/08に公開

今回やりたいこと

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は用いることができないと書きましたが、実行してみるとこのようなエラーが出ます。

Can't find LILO.

ということで、このエラーの発生元と解決方法を今からソースコードを基に追跡します。

エラーの発生元

Cannot find LILO.

カーネルのソースコードでCannot find LILO.で検索すると、以下のファイルが見つかりました。

https://github.com/torvalds/linux/blob/b9ddbb0cde2adcedda26045cc58f31316a492215/arch/x86/boot/install.sh#L36

make installされたときに実行されるスクリプトのようです。

アーキテクチャはx86_64なのにx86?と思い、調査を続けているとこんな記述が。

どうやらi386もx86_64もLinuxカーネル上ではx86という扱いのようです。

余談ですがLinuxカーネルはi386のサポートは終了したはずなのにMakefileには残っているんですね。
https://github.com/torvalds/linux/blob/b9ddbb0cde2adcedda26045cc58f31316a492215/Makefile#L399-L405

install.shの正体

話を元のエラーに戻して先程のinstall.shを読んでみます。

内容自体は非常に簡単なシェルスクリプトで、以下のことを順番に実行しています。

  1. 既存のカーネルをバックアップ(.oldに移動)
  2. 新しいカーネルをコピー
  3. liloを実行

今回は3番目がエラーになっているようです。しかし、このスクリプトではliloが存在するかどうかを確認していません。

ということは、正常なカーネルのコンパイルではそもそもこのファイルは実行されないということです。

このinstall.shはどこから実行されているのでしょうか。

make installの正体

Makefileのinstallというターゲットを探せばよいのですが、リポジトリ直下のMakefileにはそのようなターゲットは記述されていません。

探してみると、x86内のMakefileに記述がありました。

https://github.com/torvalds/linux/blob/b9ddbb0cde2adcedda26045cc58f31316a492215/arch/x86/Makefile#L290-L292

しかし、このターゲットは以下を実行しているに過ぎません。

$(call cmd,install)

callというのはMakefileで使える内部関数です。

cmdという変数は以下で定義されています。

https://github.com/torvalds/linux/blob/b9ddbb0cde2adcedda26045cc58f31316a492215/scripts/Kbuild.include#L159-L160

要約するとcmd_$1という変数を呼び出しているようで、今回の場合はcmd_installという変数が実態のようです。

cmd_installを探してみると、ここで定義されているようです。

https://github.com/torvalds/linux/blob/b9ddbb0cde2adcedda26045cc58f31316a492215/Makefile#L1309-L1320

このコードによると、$(call cmd,install)というのはscripts/install.shを実行しているようです。

scripts/install.shを読む

scripts/install.shは更にシェルスクリプトを呼び出しているようです。

https://github.com/torvalds/linux/blob/b9ddbb0cde2adcedda26045cc58f31316a492215/scripts/install.sh#L23-L37

このあたりを読むとこのスクリプトは以下を実行しているようです。

  • 以下のスクリプトを順番に探す
    • ~/bin/${INSTALLKERNEL}
    • /sbin/${INSTALLKERNEL}
    • arch/${SRCARCH}/install.sh
    • arch/${SRCARCH}/boot/install.sh
  • ない場合はエラーを出して終了

INSTALLARCH変数はここで定義されており、そのままinstallarchという文字列です。

https://github.com/torvalds/linux/blob/b9ddbb0cde2adcedda26045cc58f31316a492215/Makefile#L512

SRCARCHは今回はx86になります。

つまり、make installは内部的にはinstallkernelというコマンドを特定の引数とともに実行しているに過ぎませんでした。

今回Arch Linuxには/sbin/installkernelが存在しないため、最終的にarch/x86/install.shが実行されたというわけです。

https://twitter.com/Hayao0819/status/1710697827570229350

Gentoo Linuxの場合

Gentoo Linuxでは、installkernel-gentooというパッケージによって/sbin/installkernelがインストールされるようです。

https://github.com/projg2/installkernel-gentoo/blob/master/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を実行していません。

https://gitlab.archlinux.org/archlinux/packaging/packages/linux/-/blob/c8174de91189f7d98d6a8f882cc495f5f71a9559/PKGBUILD#L88-126

つまり/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関数にて行われています。

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/38c84cca77d796b5e2660fd8c39e889955548ca1/mkinitcpio#L457-571

要約すると、プリセットに書かれている変数を読み込んで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のソースコードは以下にありました。

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/a708ff89a5020ad2aac295cf0d4f2659c5bd2b4e/libalpm/hooks/90-mkinitcpio-install.hook

重要なのは以下の2行です。

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/a708ff89a5020ad2aac295cf0d4f2659c5bd2b4e/libalpm/hooks/90-mkinitcpio-install.hook#L5

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/a708ff89a5020ad2aac295cf0d4f2659c5bd2b4e/libalpm/hooks/90-mkinitcpio-install.hook#L13

要約すると、「/usr/lib/modules/*/vmlinuzが更新されたとき/usr/share/libalpm/scripts/mkinitcpio install」を実行するという内容です。

実行されているスクリプトのソースコードのうち、カーネルがインストールされたときに実行される重要な関数はinstall_kernelです。

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/a708ff89a5020ad2aac295cf0d4f2659c5bd2b4e/libalpm/scripts/mkinitcpio#L69-98

この関数は主に2つのことを行っています。

  1. /etc/mkinitcpio.d/以下にプリセットを作成する
  2. カーネル(厳密にはフックのトリガー元になったファイル)を/boot以下にコピーする

1つめのプリセットの作成を行っているのは以下のコードです。

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/a708ff89a5020ad2aac295cf0d4f2659c5bd2b4e/libalpm/scripts/mkinitcpio#L79-80

/usr/share/mkinitcpio/hook.presetというテンプレートの%PKGBASE%という文字列を置き換えてコピーしているだけです。

2つめのカーネルのコピーは以下のコードで実行されています。

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/a708ff89a5020ad2aac295cf0d4f2659c5bd2b4e/libalpm/scripts/mkinitcpio#L91-93

非常にわかりにくいのですがlineという変数は以下で定義されており、要約すると標準入力の値です。

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/a708ff89a5020ad2aac295cf0d4f2659c5bd2b4e/libalpm/scripts/mkinitcpio#L121

標準入力の内容の正体はPacman Hookの仕様に書いてあります。

Hookの中にあるNeedsTargetsを用いると、Hookのトリガーになったファイルのフルパスがスクリプトの標準入力として渡されるようです。

またkernellistという配列は、これより上で定義されている関数read_presetによりプリセットのpreset_kverALL_kverを基に定義されています。

https://twitter.com/Hayao0819/status/1710711633952301320

トリガーになっているカーネルの正体

これで一見落着かと思うと、そうではありません。

先程のHookのトリガーをもう一度見てみます。

https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/a708ff89a5020ad2aac295cf0d4f2659c5bd2b4e/libalpm/hooks/90-mkinitcpio-install.hook#L5

/usr/lib/modules/以下ということは、本来ならmake modules_installの段階でインストールされます。

しかし、バニラのLinuxソースでmake modules_installを実行してもそのようなファイルは生成されません。

ということで更に調査を続けていきます。幸い、このコードは簡単に見つかりました。

https://gitlab.archlinux.org/archlinux/packaging/packages/linux/-/blob/c8174de91189f7d98d6a8f882cc495f5f71a9559/PKGBUILD#L112-115

パッケージングの段階でPKGBUILDのpackage関数で作成されていたもののようです。

このmake -s image_nameはLinuxのMakefileで以下のように定義されています。

https://github.com/torvalds/linux/blob/master/Makefile#L2031-L2032

KBUILD_IMAGE変数を更に追っていきます。

https://github.com/torvalds/linux/blob/master/Makefile#L1070-L1075

https://github.com/torvalds/linux/blob/master/arch/x86/Makefile#L276-L277

ということで、x86の場合はarch/x86/boot/bzImageになります。

そもそもvmlinuzって何という方は、こちらの記事が非常に参考になりました。

https://zenn.dev/ohno418/articles/c8902462e5d2aa

まとめると、PKGBULD内でarch/x86/boot/bzImageをmodules以下にvmlinuzとしてコピーしている、というわけです。

Arch Linuxの処理を模倣する

これまで長々とArch Linuxでの挙動を追跡してきました。

要約すると、バニラなカーネルをArch Linuxで用いるには主に2つのことを行う必要があります。

  • mkinitcpioのフックを作成する
  • カーネルを/boot以下に配置する

当初この処理はどちらもmkinitcpioのフックによって実装されているため、強引にそのスクリプトを流用してやろうかとも考えたのですが、このスクリプトの仕様の文書が見当たらなかった(あったら教えてください)以上、仕様が変更されることも考えて1から実装し直しました。

mkinitcpioのフックは、本家と同様に/usr/share/mkinitcpio/hook.presetを基に生成しています。

https://github.com/Hayao0819/Hayao-Tools/blob/5b9c6a1b95025185d92d1c18ea03ebbf6899e0bb/arch-kernel-installer/arch-kernel-installer.sh#L56-L60

また、カーネルのコピーも本家に近い挙動を行っています。
ただし、modules以下へのコピーを行うのではなく/boot以下に直接カーネルをコピーしています。
おそらくArch Linux側が一旦/usr/lib/modules以下にbzImageをコピーしているのはパッケージングの都合であり、それ以上の利点はないと判断したためです。

https://github.com/Hayao0819/Hayao-Tools/blob/5b9c6a1b95025185d92d1c18ea03ebbf6899e0bb/arch-kernel-installer/arch-kernel-installer.sh#L50-L54

ちょっとした後悔と注意

後悔と今後

今考えると/sbin/installkernelをArch Linuxのlibalpmとmkinitcpio用に書いて、make installを実行できるようにしたほうがよかったのかなと思っています。気が向いたらそっちも書きます。

インターネット上の/sbin/installkernelについて

ネットで検索すると以下のArch Linux用のinstallkernelスクリプトが見つかります。
https://github.com/syuu1228/arch-installkernel/blob/master/sbin/installkernel

しかしこのコードが書かれたのは11年前であり、以下のような問題点を抱えています。

  • Grub2に対応していない
  • mkinitcpioのプリセットを作成していない
  • mkinitcoioにマイクロコードが含まれていない
  • その他現代の仕様に満たない可能性がある

そのため、これらのコードを流用するのはお控えください。

また、DebianやGentooのコードもmkinitcpioを一切考慮していないので利用することはおすすめしません。

終わり

長々と文章を書いてしまいましたが、ここまでしっかりとLinuxのソースコード(Makefileやシェルスクリプトなのでソースコードそのものかと言われると怪しい)を読んだのは初めてです。

torvalds/linuxでも思った以上にMakefileやシェルスクリプトが多くの場所で使われていました。

また、mkinitcpiomakepkgmkarchrootもシェルスクリプトで書かれているのでシェルスクリプトはやはり偉大です。

(この作業を行う前にcore/linuxを改造してchroot環境でコンパイルしようとした際に謎のエラーが発生し、makepkgやその周辺ツールのシェルスクリプトを読んだのはまた別のお話)

最近はこうしたカーネルのコンフィグやインストールを完全に自動化・抽象化するコマンドラインツールをGolangでちまちま書いているので、いつかお披露目できたらなと思います。

間違い等があったら指摘してくださると助かります。

それでは、また今度。

Discussion