続・玩式草子 ―戯れせんとや生まれけん―

第13回Plamo-7.1とinitrd[4]

前回までに紹介してきたように、initrdは元々、モジュール化されたカーネル・ドライバを、HDD上のファイルシステムを介することなくカーネルに組み込むための仕組みとして考案されました。

一方、initrdはgrub等のブートローダによってカーネルと共にメモリ内に読み込まれ、他のあらゆるソフトウェアよりも先にカーネルに触れるため、その特徴を活かした面白い使い方もあります。それが今回紹介するマイクロコードの早期読み込み(early loading)です。

CPUとマイクロコード

最近の無線LANやグラッフィクカードのような高機能化した周辺機器では、カーネルの持つデバイスドライバとは別に、ファームウェア(firmware)と呼ばれる、専用のソフトウェアを必要とする例が増えてきました。ファームウェアはその機器を制御する専用のソフトウェアで、機器のメーカーからバイナリファイルとして提供され、どのようなファームウェアが必要になるのかはその機器用のデバイスドライバに記録されています。

たとえば、AMD GPU用のデバイスドライバであるamdgpumodinfoコマンドでチェックすると、このドライバが必要とするfirmwareの一覧が表示されます。

$ sudo modinfo amdgpu
[sudo] kojima のパスワード:
filename:       /lib/modules/5.2.11-plamo64/kernel/drivers/gpu/drm/amd/amdgpu/amdgpu.ko.xz
license:        GPL and additional rights
description:    AMD GPU
author:         AMD linux driver team
firmware:       amdgpu/raven2_gpu_info.bin
firmware:       amdgpu/picasso_gpu_info.bin
firmware:       amdgpu/raven_gpu_info.bin
firmware:       amdgpu/vega12_gpu_info.bin
firmware:       amdgpu/vega10_gpu_info.bin
firmware:       amdgpu/mullins_mec.bin
firmware:       amdgpu/mullins_rlc.bin
firmware:       amdgpu/mullins_ce.bin
....

Linuxの場合、これらファームウェアは /lib/firmwareディレクトリに収めることになっており、上記リストに指定されたAMD GPU用のファームウェアは/lib/firmware/amdgpu/にあります。

$ ls /lib/firmware/amdgpu/*gpu*
/lib/firmware/amdgpu/picasso_gpu_info.bin  /lib/firmware/amdgpu/vega10_gpu_info.bin
/lib/firmware/amdgpu/raven2_gpu_info.bin   /lib/firmware/amdgpu/vega12_gpu_info.bin
/lib/firmware/amdgpu/raven_gpu_info.bin

これらのファームウェアは、カーネルがその機器用のデバイスドライバを組み込む際に合わせて読み込まれ、デバイスドライバと協働して機器の初期化等の処理を行います。

ファームウェアによって動作を変える仕組みは、周辺機器だけでなくCPU本体にも採用されています。CPUの場合、シリコン上に書き込まれた回路は変更できないものの、その回路はマイクロコード(micro code)と呼ばれるソフトウェアによって制御されており、マイクロコードの修正によってCPUのバグやセキュリティホールを回避できるようになっています。すこし前にSpectreやMeltdownといったCPUの脆弱性が報告され、それに対応するためのマイクロコードが多数公開されたことをご記憶の方も多いでしょう。

なお、⁠ファームウェア」「マイクロコード」は共に、⁠ハードウェアに組み込まれた制御用ソフトウェア」を指す言葉なものの、周辺機器の場合は「ファームウェア⁠⁠、CPUの場合は「マイクロコード」と呼ばれることが多いので、本稿でもそれに従うことにします。

前述のように、周辺機器の場合、ファームウェアはその機器を操作するためのデバイスドライバが組み込まれる際にロードされるので、機器が動作し始める時点から適用することができます。

一方、CPUの場合は、マイクロコードを読み込むにもCPUの機能が必要なので、まずCPUを先に起動する必要があります。しかしながら、マイクロコードによるバグやセキュリティホールの修正は可能な限り早期に適用し、以後の処理に悪影響が及ばないようにしなければなりません。そこで考案されたのがinitrdを介してマイクロコードを読み込む手法です。

マイクロコード用initrd

LinuxではCPUのマイクロコードを更新する機能がカーネルに組み込まれており、起動時に既存のマイクロコードのバージョンをチェックしたり、新しいマイクロコードに更新することができます。

たとえば、手元にある5年前くらいのCore i3-4160(4th Gen. Haswell世代)マシンの場合、CPUに元々組み込まれているマイクロコードはrevision 0x24でした。

$ dmesg | grep microcode
[    1.200804] microcode: sig=0x306c3, pf=0x2, revision=0x24
[    1.201094] microcode: Microcode Update Driver: v2.2.

このMicrocode Update DriverがCPUのマイクロコードの更新を担当しており、/sys/devices/system/cpu/microcode/reloadというファイルに'1'を書きこむと、/lib/firmware/以下に用意されているファイルを調べ、そのCPU用の新しいバージョンのマイクロコードが見つかれば、それを読み込んで既存のコードを更新します。手元の環境で実行すると、2019/02/26に公開された0x27というバージョンに更新されました。

# echo '1' > /sys/devices/system/cpu/microcode/reload
# dmesg | last
....
[  100.477620] microcode: updated to revision 0x27, date = 2019-02-26
[  100.488654] x86/CPU: CPU features have changed after loading microcode, but might not take effect.
[  100.489534] x86/CPU: Please consider either early loading through initrd/built-in or a potential BIOS update.
[  100.490409] microcode: Reload completed, microcode revision: 0x27

dmesgの出力を見ると、マイクロコードは新しいバージョンに更新されたものの、この変更が有効にならない可能性もあり、initrdやBIOSのアップデートを使った早期読み込み(early loading)を使った方がいい、旨のメッセージが出力されています。

マイクロコードはソースコードのないバイナリファイルなため処理内容等は分からないものの、おそらく、マイクロコードを更新すると、同じソフトウェアを実行しても結果が変わる可能性があることを指摘しているようです。

さて、それでは「早期読み込み(early loading⁠⁠」というのはどうすればいいのか、と言うと、マイクロコードを適切な位置に配置したinitrd(initramfs)イメージを用意して、それをブートローダによって本来のinitrdイメージよりも先に読み込ませることになります。このあたりの詳細はカーネル付属のドキュメント、Documentation/x86/microcode.rstに取り上げられており、以下はその概要です。

マイクロコードの結合

initrd経由でマイクロコードを渡された際、カーネルが調べるファイルはあらかじめ決まっていて、Intel CPU用のマイクロコードはkernel/x86/microcode/GenuineIntel.binAMD用はkernel/x86/microcode/AuthenticAMD.binです。マイクロコードはCPUの種類ごとに用意されているので/lib/firmware/{intel,amd}-ucode/以下には複数のファイルがあり、それらをIntel用とAMD用それぞれ1つのファイルに結合しておきます。

# mkdir work ; cd work
# mkdir -p kernel/x86/microcode
# cat /lib/firmware/intel-ucode/* > kernel/x86/microcode/GenuineIntel.bin
# cat /lib/firmware/amd-ucode/microcode_amd*.bin > kernel/x86/microcode/AuthenticAMD.bin

マイクロコード用initrd(initramfs)イメージの作成

作成したマイクロコードのファイルを、ディレクトリ構成ごとcpioコマンドでまとめて、initrd(initramfs)イメージを作ります。通常のinitrdイメージはgzip等で圧縮しますが、マイクロコード用のイメージは圧縮せず、cpio形式のままにしておきます。

# find . | cpio -ov -Hnewc > ../ucode.cpio

ブートローダにマイクロコード用イメージを追加

新しく作成したマイクロコード用イメージ(ucode.cpio)を/bootに移し、grub.cfgのinitrd行にこのファイルを追加して既存のinitrdイメージ(initrd.img-5.2.11-plamo64)より先に読み込ませるようにします。

# mv ../ucode.cpio /boot
# vim /boot/efi/grub/grub.cfg
....
initrd /boot/ucode.cpio /boot/initrd.img-5.2.11-plamo64

以上の設定でシステムを再起動すると、grubはカーネルイメージと共にinitrd行に指定したucode.cpioとinitrdイメージをメモリに読み込み、後の処理をカーネルに委ねます。処理を委ねられたカーネルは、自身と同じメモリ空間にあるucode.cpioを見つけ、その中に動作しているCPU用の新しいマイクロコードがあればまずそれを適用してから、initrdイメージの処理に移ります。

再起動した環境でdmesgを調べると、カーネルのバージョン表示よりも先、一番最初の行でマイクロコードが更新された旨が報告されています。

$ dmesg | head
[    0.000000] microcode: microcode updated early to revision 0x27, date = 2019-02-26
[    0.000000] Linux version 5.2.11-plamo64 (kojima@ryzen) (gcc version 9.1.0 (GCC)) #1 SMP PREEMPT Tue Sep 3 16:54:21 JST 2019
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-5.2.11-plamo64 root=UUID=8809ed77-98b0-44ce-b141-88482e63cbe7 ro net.ifnames=0 quiet
[    0.000000] KERNEL supported cpus:
[    0.000000]   Intel GenuineIntel
[    0.000000]   AMD AuthenticAMD
[    0.000000]   Hygon HygonGenuine

もう少し先では、以前はrevision=0x24と表示していたMicrocode Update Driverが、revision=0x27と報告しています。

[    1.213284] microcode: sig=0x306c3, pf=0x2, revision=0x27
[    1.213338] microcode: Microcode Update Driver: v2.2.

この場合、カーネルはまず最初にCPUのマイクロコードを0x27に更新してから以後の処理を行なうため、全ての作業が0x27版の元で行われることになり、マイクロコードのバージョン違いによる齟齬は無くなります。

なお、今回はわかりやすいようにgrub.cfgを修正してucode.cpioを本来のinitrdイメージとは別に読み込ませたものの、grub.cfgのinitrd行は指定したファイルを連続してメモリに読み込むだけなので、以下のような手順でucode.cpioをinitrdイメージの先頭に結合してしまえば、initrdイメージを読み込む処理だけでucode.cpioも合わせて読み込めるので、grub.cfgを修正する必要はありません。

# cd /boot
# mv initrd.img-5.2.11-plamo64{,.backup}
# cat ucode.cpio initrd.img-5.2.11-plamo64.backup > initrd.img-5.2.11-plamo64

今回はinitrdの応用的な使い方としてマイクロコードの早期読み込みを紹介しました。説明の都合上、ucode.cpioの作り方、ロード方法も紹介したものの、実のところ、Plamo-7.1以降ではinitrdイメージにあらかじめマイクロコードを組み込んでいるので、紹介したような処理は必要ありません(苦笑⁠⁠。

initrdイメージにマイクロコードが入っているかどうかは、cpioコマンドでチェックすることができます。cpio -tで中身を調べたとき、以下のようなファイルが表示されればマイクロコード組み込み済みです。

$ cpio -t < /boot/initrd.img-5.2.11-plamo64 
.
kernel
kernel/x86
kernel/x86/microcode
kernel/x86/microcode/GenuineIntel.bin
kernel/x86/microcode/AuthenticAMD.bin
5024 ブロック

ちなみに、マイクロコードが先頭に結合されたinitrdイメージを展開する場合、先頭のマイクロコード部は読み飛ばす必要があり、その際はddコマンドのskipオプションが便利です。skipオプションにはマイクロコード部のサイズ(5024)を指定します。

$ dd if=/boot/initrd.img-5.2.11-plamo64 of=orig_initrd.img skip=5024
34488+1 レコード入力
34488+1 レコード出力
17658307 bytes (18 MB, 17 MiB) copied, 0.206798 s, 85.4 MB/s

こうして取り出したinitrdイメージは、前回までに紹介した方法で展開することができます。

$  zcat orig_initrd.img | cpio -t
.
init
lib64
etc
etc/lvm
etc/lvm/profile
etc/lvm/profile/thin-generic.profile
...

今回取りあげた話題は、いわばディストリビューションの楽屋裏的なネタで、一般ユーザにはあまり縁のない話かも知れません。しかしながら、これら楽屋裏的な話題を取りあげられるのもOSSの魅力の一つだと思っています。

おすすめ記事

記事・ニュース一覧