Raspberry Pi でカーネルモジュールビルドの準備

# はじめに

ラズパイでカーネルモジュールをビルドする際、カーネルのソースコードとカーネルの部分的なコンパイルが必要になるが、その準備手順について備忘録を兼ねて記しておく。本記事の方法ではソースのダウンロードからコンパイルまで手作業で行う。

（参考にした主たるサイト：https://qiita.com/mt08/items/1ebbdbf304f6f2362024 ）


# 実行環境（例）
Machine: Raspberry Pi 4 Model B 
OS: Raspbian Buster Lite (relase date: 2020-02-13, kernel version: 4.19.97)

実行するマシンに依存する部分はシンボルファイルのダウンロード時ぐらい。OSはRaspbianなら古いバージョンでも同じ手順でいけるはず（少なくともStretchはOK）。

# ビルド環境の準備 
まず、git, bc, bison, flex, libssl-dev が必須なのでインストール。すでに入っているなら問題ない。
rpi-updateは不要、むしろすべきでない（情報源: https://www.raspberrypi.org/documentation/linux/kernel/updating.md ）。

```shell_session:実行例
raspberrypi:~# apt install git bc bison flex libssl-dev
```

# Firmware revision の取得
GitHubのサイトから現在稼働中のカーネルバージョンの firmware revision ハッシュ値を取得して `/boot/.firmware_revision` に格納する（別のファイルでも構わない）。最初からこのファイルが存在する場合は、その中のハッシュ値を用いてよいが、稼働中のカーネルと合致するかは確認しておく方が無難だろう。

まずは、現在のカーネルバージョンを確認。

```shell-session:実行例
raspberrypi:~# uname -r
4.19.97-v7l+
```

バージョン（上記では4.19.97）を把握したら、https://github.com/Hexxeh/rpi-firmware/commits/master で同じバージョンの項目を探し、そこに記載されているハッシュ値を `/boot/.firmware_revision` に書き込む。あるいはシェルの環境変数として保存する（変数名を FIRMWARE_REV にしておけば、以降のコード例をコピペして利用可能になる）。

<details><summary>どこにfirmware revisionが記されているのか？</summary>
赤で囲った部分がカーネルのバージョン、緑がクリップボードマーク。
クリップボードマークをクリックすればハッシュ値をクリップボードにコピーしてくれる。
<div>
![figure1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355331/baea6d7f-a575-ef64-2dad-f79a357f93f4.png)
</div>
直に値を得たい場合は、クリップボードマークの右に見える`993f475` をクリックして詳細画面に。
![figner2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355331/0ec2d8ab-feb2-efd6-23cc-77d648f8f05f.png)
青で囲った部分(commit以降)が求めるハッシュ値。
</details>

kernel: 4.19.97 を例に取ると、firmware revision は`993f47507f287f5da56495f718c2d0cd05ccbc19` となる。

# カーネルソースのダウンロード
下記のようにfirmware revisionをGitHubに投げるとkernel revisionを取得できる。このkernel revisionを元にしてDLすべきカーネルソースのURLが定まる。

カーネルソースをDLしたいディレクトリで下記コードを実行する。ソースアーカイブは150MBを超えるし、展開すると1GB近くになるので、ディスクの空き容量に注意しながらディレクトリを選択する。

```shell_session:カーネルソースのダウンロード
FIRMWARE_REV=`cat /boot/.firmware_revision`
KERNEL_REV=`curl -L https://github.com/Hexxeh/rpi-firmware/raw/${FIRMWARE_REV}/git_hash`
curl -L https://github.com/raspberrypi/linux/archive/${KERNEL_REV}.tar.gz > linux-kernel-src.tar.gz
```
firmware revisionの値を環境変数に登録し(1行目)、これをもとにkernel revisionをGitHubからダウンロードし(2行目)、さらにkernel revisionの値からカーネルソースのURLが決まり、これをDLして `linux-src-kernel.tar.gz` に書き出す(3行目)。

HTTPのレスポンスにファイルサイズが記されてないため、DLに成功したか否かは後の解凍に成功するか否か見ないとわからないのが厄介。しかもDLはレジューム非対応なので、通信環境が悪いと難渋する。

<details><summary>提示している実行例におけるここまでの流れ</summary>
カーネルバージョンは `4.19.97-v7l+`
Version 4.19.97の firmware revision は `993f47507f287f5da56495f718c2d0cd05ccbc19`
上記firmware revisionに該当する kernel revision は `43857965e5f526b9df807e543102a11fac1c0bcc`
よって、求めるカーネルソースは `https://github.com/raspberrypi/linux/archive/43857965e5f526b9df807e543102a11fac1c0bcc.tar.gz`
</details>

# カーネルソースの展開
DLした `linux-kernel-src.tar.gz` を展開する。ここでは`/usr/src`に展開する。前述のように約1GBになるので、ディスク容量に注意。


```shell_session:実行例
raspberrypi:~# tar -C /usr/src -xvf linux-kernel-src.tar.gz
linux-43857965e5f526b9df807e543102a11fac1c0bcc/
linux-43857965e5f526b9df807e543102a11fac1c0bcc/.clang-format
linux-43857965e5f526b9df807e543102a11fac1c0bcc/.cocciconfig
linux-43857965e5f526b9df807e543102a11fac1c0bcc/.get_maintainer.ignore
         :             
       (中略)
　　　　　:
linux-43857965e5f526b9df807e543102a11fac1c0bcc/virt/lib/
linux-43857965e5f526b9df807e543102a11fac1c0bcc/virt/lib/Kconfig
linux-43857965e5f526b9df807e543102a11fac1c0bcc/virt/lib/Makefile
linux-43857965e5f526b9df807e543102a11fac1c0bcc/virt/lib/irqbypass.c
raspberrypi:~#
```

ラズパイ4ならメモリがたくさんあるので、1.5GBくらいのRAM diskをtmpfsで作って、そこに展開すると速いし、SDカードの書き込みダメージを減らすことができる。
正しく展開されたら、`/usr/src/linux-(kernel revisionの値)` という名前のディレクトリが作られているはずである。

```shell_session:実行例
raspberrypi:~# ls /usr/src
linux-43857965e5f526b9df807e543102a11fac1c0bcc
```

# シンボルファイルのダウンロード
シンボルファイルは先に展開したカーネルソースのルートディレクトリに保存する。
ルートディレクトリに移動すれば、以下のようなファイルが見えるはずである。

```shell_session:実行例
raspberrypi:~# cd /usr/src/linux-43857965e5f526b9df807e543102a11fac1c0bcc
raspberrypi:/usr/src/linux-43857965e5f526b9df807e543102a11fac1c0bcc# ls
arch   certs    CREDITS  Documentation  firmware  include  ipc     Kconfig  lib       MAINTAINERS  mm   README   scripts   sound  usr
block  COPYING  crypto   drivers        fs        init     Kbuild  kernel   LICENSES  Makefile     net  samples  security  tools  virt

```

ここでシンボルファイルをDLするわけだが、実行中のマシンによってDLすべきファイルが変わってくる。しかしコンパイル時は`Module.symvers`で参照されるので、DL後にファイル名を変えるかシンボリックリンクを張って対応する。

<details><summary>Raspberry Pi 4 Model B (64bit)</summary><div>
カーネルバージョンは`X.YY.ZZ-v8+`となるので、Module8.symvers をDLして、Module.symvers としてシンボリックリンクを張る。
Module8.symvers を保存したいディレクトリに移動した後、下記コードを実行する。

```shell_session:
curl -L https://github.com/Hexxeh/rpi-firmware/raw/${FIRMWARE_REV}/Module8.symvers > Module8.symvers
ln -s `pwd`/Module8.symvers /usr/src/linux-${KERNEL_REV}/Module.symvers
```

</div>
</details>
<details><summary>Raspberry Pi 4 Model B (32bit)</summary><div>
カーネルバージョンは`X.YY.ZZ-v7l+`となるので、Module7l.symvers をDLして、Module.symvers としてシンボリックリンクを張る。
Module7l.symvers を保存したいディレクトリに移動した後、下記コードを実行する。

```shell_session:
curl -L https://github.com/Hexxeh/rpi-firmware/raw/${FIRMWARE_REV}/Module7l.symvers > Module7l.symvers
ln -s `pwd`/Module7l.symvers /usr/src/linux-${KERNEL_REV}/Module.symvers
```

</div>
</details>
<details><summary>Raspberry Pi 3 Model B+</summary>
<div>
カーネルバージョンは`X.YY.ZZ-v7+`となるので、Module7.symvers をDLして、Module.symvers としてシンボリックリンクを張る。
Module7.symvers を保存したいディレクトリに移動した後、下記コードを実行する。

```shell_session:
curl -L https://github.com/Hexxeh/rpi-firmware/raw/${FIRMWARE_REV}/Module7.symvers > Module7.symvers
ln -s `pwd`/Module7.symvers ./Module.symvers
```
</div>
</details>
<details><summary>Raspberry Zero WH</summary>
<div>
カーネルバージョンは`X.YY.ZZ+`となるので、Module.symvers をDLしてそのまま利用する。
カーネルソースのルートディレクトリに移動して、Module.symvers をDLする。

```shell_session:
curl -L https://github.com/Hexxeh/rpi-firmware/raw/${FIRMWARE_REV}/Module.symvers > Module.symvers
```
</div>
</details>

シンボルファイルが何をするものなのかについては、下記を参照されたし。
https://kakurasan.hatenadiary.jp/entry/20100713/p1
https://elixir.bootlin.com/linux/v2.6.36/source/Documentation/kbuild/modules.txt#L442

#コンパイル
カーネルソースのルートディレクトリに移動。このようなファイルが見えていればOK。Module.symversがないと前項の作業に誤りがあるということになる。

```shell_session:
pi@raspberrypi:/usr/src/linux-43857965e5f526b9df807e543102a11fac1c0bcc# ls
arch   COPYING  Documentation  fs       ipc      kernel       Makefile        net      scripts   tools
block  CREDITS  drivers        include  Kbuild   lib          mm              README   security  usr
certs  crypto   firmware       init     Kconfig  MAINTAINERS  Module.symvers  samples  sound     virt
```

下記コードを実行。最後の`make -j4 modules_prepare` でコンパイル。
コンパイル時間はマシンスペックで大きく変わる（数十秒～数分）。

```shell-session
modprobe configs
zcat /proc/config.gz > .config              
make -j4 modules_prepare
```
途中で下記のようにカーネルの設定に入力を求められる場合がある。

```shell_session:実行例
raspberrypi:/usr/src/linux-43857965e5f526b9df807e543102a11fac1c0bcc# make -j4 module_prepare
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/kconfig/conf.o
  YACC    scripts/kconfig/zconf.tab.c
  LEX     scripts/kconfig/zconf.lex.c
  HOSTCC  scripts/kconfig/zconf.tab.o
  HOSTLD  scripts/kconfig/conf
scripts/kconfig/conf  --syncconfig Kconfig
*
* Restart config...
*
*
* Kernel hacking
*
Magic SysRq key (MAGIC_SYSRQ) [Y/?] y
  Enable magic SysRq key functions by default (MAGIC_SYSRQ_DEFAULT_ENABLE) [0x1] 0x1
  Enable magic SysRq key over serial (MAGIC_SYSRQ_SERIAL) [Y/n/?] y
Kernel debugging (DEBUG_KERNEL) [Y/?] y
Code coverage for fuzzing (KCOV) [N/y/?] (NEW)
```

何のことか分からないならデフォルト値（大文字で書かれている選択肢）を選んでおけば問題ないと思われる。その他のエラーが出た場合は、何かしらパッケージが足りないのか、あるいはカーネルソースのDLに失敗している可能性もあるので確認を。

エラー無く終了したら、カーネルソースのルートへのシンボリックリンクを``/lib/modules/`uname -r`/build`` として作成する。

```shell-session:実行例
ln -s /usr/src/linux-43857965e5f526b9df807e543102a11fac1c0bcc /lib/modules/`uname -r`/build
```

これでカーネルモジュールをコンパイルする準備が整った！

# あとがき
`apt upgrade` を実行すると、カーネルが勝手にバージョンアップされてしまうことがある。カーネルバージョンが変わると、カーネルモジュールは無効となり新たにビルドし直さなければならなくなる。しかもカーネルバージョンが変わるだけでコンパイルがうまくいかなくなることもあって、非常に厄介だ。

「予備機として同じRaspberry Piを用意 → 目標バージョンのカーネルで走らせてコンパイル実験 → 成功したら主機もカーネルを目標バージョンにアップデートし、予備機でコンパイルに成功したカーネルモジュールをコピー」とすれば主機の運用停止を最小にできるだろう。

小生はUSB接続のWiFiドングルにてインターネット接続しており、かつこのカーネルモジュールを自前でコンパイルしているので、主機を止めたら何もできなくなる（笑）