「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。
本稿では、カーネルモジュール機能実装の概要について説明します。
- はじめに
- 1. 動かしてみよう
- 2. 動作の概要
- 3. カーネルモジュール起動の動作フロー
- 4. カーネルモジュールの削除
- 最後に
- おまけ ~ カーネルモジュール組み込み機構の歴史
- おまけ ~ カーネルモジュールへの署名
執筆者 : 高橋 浩和
※「新Linuxカーネル解読室」連載記事一覧はこちら ※ 本稿のサンプルコードは github にて公開しています。
はじめに
Linuxカーネルは一部機能を別オブジェクトのカーネルモジュールとして生成し、Linuxカーネル本体起動後に、必要に応じてそれらモジュールを組込むという方式を採用しています。このカーネルモジュール機能は、Linuxカーネルの開発が始まった初期から使われている古い機能です。
元々、カーネルモジュールを組込む処理の殆どはユーザランドinsmodコマンドの中で行われていました。現在のLinuxカーネル(kernel 6.8)では、それらの機能全てがカーネル機能として実現されています。
今回は、このカーネルモジュール機能を見て行きます。
1. 動かしてみよう
解説用の例題として簡単なカーネルモジュールを用意しました。github.comからダウンロードしてください。kmoduleディレクトリ配下に小さなカーネルモジュールkaidock用のCソースファイル1つと、ビルド用のMakefileが置いてあります。カーネルモジュールkaidockは、1秒毎にjiffies
変数*1の値を表示します。
kaidockは非常に短かいプログラムです。kaidockモジュールを組込むとinit_module
関数が呼びだされ、その中でタイマkaidock_timer
がHZ時間後に満了時間を迎えるように登録しています(add_timer関数)。HZ時間経過後に呼び出されたkaidock_timer
はkaidock_func
関数を実行し、jiffies
の値をコンソールに表示します。
kaidock_func
関数は、HZ時間後に再度タイマkaidock_timer
が満了時間を迎えるように登録仕直します(mod_timer関数)。
タイマkaidock_timer
の起動回数は、カーネルモジュール起動引数loop
にて変更できます(module_param関数)。ライセンスはGPLとしました。
#include <linux/module.h> #include <linux/kernel.h> static int loop = INT_MAX; module_param(loop, int, 0644); MODULE_PARM_DESC(loop, "specify the loop count"); static void kaidock_func(struct timer_list *timer); DEFINE_TIMER(kaidock_timer, kaidock_func); static void kaidock_func(struct timer_list *timer) { printk(KERN_ERR "%lx\n", jiffies); if (--loop > 0) { do { mod_timer(timer, timer->expires + HZ); } while ((long)(timer->expires - jiffies) <= 0L); } } int init_module(void) { kaidock_timer.expires = jiffies + HZ; add_timer(&kaidock_timer); return 0; } void cleanup_module(void) { del_timer_sync(&kaidock_timer); } MODULE_AUTHOR("taka"); MODULE_LICENSE("GPL");
1.1 ビルドする
Ubuntu 24.04 LTS環境での動作確認をしています。kaidockをダウンロードしたディレクトリにてビルドします。現在動作しているカーネル用のカーネルモジュールとしてコンパイルされます。*2
$ cd kmodule $ make -C /lib/modules/`uname -r`/build M=`pwd` modules
1.2 組込む
kaidockカーネルモジュールを組込みます。前もって利用しているPCのセキュアブート機能を切っておくことが必要です。もしくは、kaidockカーネルモジュールに署名を付ける必要があります。署名手順は、「おまけ ~ カーネルモジュールへの署名」を参照してください。
カーネルモジュール起動引数loop
にて、jiffies
の表示回数を指定することができます。コンソール画面の右上の方にjiffies
の値が緑色の文字で周期表示されます。
$ sudo insmod kaidock.ko loop=100 jiffies:0x1113dac00
insmodを行なった端末に表示されない場合、どこかにあるコンソールに表示されています。下記のようにsyslogの内容を表示してください。
$ sudo tail -f /var/log/syslog
組込まれているカーネルモジュールを表示します。kaidockカーネルモジュールが組込まれていることが確認できます。
$ lsmod Module Size Used by kaidock 12288 0 cpuid 12288 0 tls 151552 0 xt_conntrack 12288 1 nft_chain_nat 12288 3 xt_MASQUERADE 16384 1 nf_nat 65536 2 nft_chain_nat,xt_MASQUERADE nf_conntrack 200704 3 xt_conntrack,nf_nat,xt_MASQUERADE nf_defrag_ipv6 24576 1 nf_conntrack nf_defrag_ipv4 12288 1 nf_conntrack : :
1.3 停止させる
kaidockカーネルモジュールを停止させます。
$ sudo rmmod kaidock
2. 動作の概要
カーネルモジュールは、リロケータブルオブジェクト(再配置可能オブジェクト)です。xyz.cのコンパイル途中に生成される xyz.o ファイルと基本的に同じです。
- 割り付けアドレスが未解決。モジュール内の関数や変数の割り付くアドレスが決定していない。
- シンボルの外部参照が未解決。 カーネルモジュールの場合、カーネル本体や他のカーネルモジュールのシンボル(関数や変数)を参照する命令が、未解決状態になっている。
カーネルモジュールを組込むためには下記の操作が必要です。
- カーネルモジュールを組み込むためのカーネルメモリ領域を確保し、カーネルモジュールを読み込む
- カーネルと既に動作中のカーネルモジュールのシンボル情報取得(シンボル名とアドレスが組になった情報)
- 2.の情報を元に、カーネルモジュールの未解決アドレス、未解決シンボルを解決する
- 組み込んだカーネルモジュールを有効にする
現在のLinuxでは、これらの処理は全てカーネルが担っています。なんと、カーネルにリンカの機能まで組込まれています。
リンク処理の結果がどうなるか、リロケート前のカーネルモジュールオブジェクト(kaidock.ko)のコードとリンク後(カーネルへの組み込み後)のカーネルモジュールのコードを比較します。
リロケート前のオブジェクトkaidock.koのkaidoc_func
関数は下記のようにアドレスが解決されていない状態にあります。
$ objdump -d kaidock.ko : : 000000000000010 <kaidock_func>: (x) 10: e8 00 00 00 00 call 15 <kaidock_func+0x5> 15: 55 push %rbp (a) 16: 48 8b 35 00 00 00 00 mov 0x0(%rip),%rsi # 1d <kaidock_func+0xd> 1d: 48 89 e5 mov %rsp,%rbp 20: 53 push %rbx 21: 48 89 fb mov %rdi,%rbx 24: 48 c7 c7 00 00 00 00 mov $0x0,%rdi 2b: e8 00 00 00 00 call 30 <kaidock_func+0x20> (b) 30: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 36 <kaidock_func+0x26> 36: 83 e8 01 sub $0x1,%eax (b) 39: 89 05 00 00 00 00 mov %eax,0x0(%rip) # 3f <kaidock_func+0x2f> 3f: 85 c0 test %eax,%eax 41: 7e 29 jle 6c <kaidock_func+0x5c> 43: 48 8b 73 10 mov 0x10(%rbx),%rsi 47: 48 81 c6 e8 03 00 00 add $0x3e8,%rsi 4e: 48 89 df mov %rbx,%rdi (c) 51: e8 00 00 00 00 call 56 <kaidock_func+0x46> 56: 48 8b 73 10 mov 0x10(%rbx),%rsi (a) 5a: 48 8b 15 00 00 00 00 mov 0x0(%rip),%rdx # 61 <kaidock_func+0x51> 61: 48 89 f0 mov %rsi,%rax 64: 48 29 d0 sub %rdx,%rax 67: 48 85 c0 test %rax,%rax 6a: 7e db jle 47 <kaidock_func+0x37> 6c: 48 8b 5d f8 mov -0x8(%rbp),%rbx 70: c9 leave 71: 31 c0 xor %eax,%eax 73: 31 d2 xor %edx,%edx 75: 31 f6 xor %esi,%esi 77: 31 ff xor %edi,%edi (d) 79: e9 00 00 00 00 jmp 7e <kaidock_func+0x6e> 7e: 66 90 xchg %ax,%ax
カーネル本体へ組み込み後のカーネルモジュールもcrashコマンドを用いて覗いてみましょう。カーネルモジュールのkaidck_func
関数を逆アセンブルしてみます。組み込み前のカーネルモジュールのkaidck_func
関数は0x10番地に割り付くようなコードになっていましたが、カーネルへの組み込み後は0xffffffffc0d4c010に割り付いていることが確認できます。
変数jiffies
(a)(A)、loop
(b)(B)、呼出し関数modtime
(c)(C)のアドレスも解決されています。
関数からの復帰命令(d)(D)も解決されています。
kaidock_func
関数の先頭にあるcall
命令(x)はnopl
命令(X)に置き換えられています。関数の先頭5バイトは、ftrace
機能やkprobe
機能などが効率的にフックを仕掛けられるように領域を予約しています*3。
$ sudo crash /usr/lib/debug/boot/vmlinux-`uname -r` crash> dis kaidock_func (X) 0xffffffffc0d4c010 <kaidock_func>: nopl 0x0(%rax,%rax,1) [FTRACE NOP] 0xffffffffc0d4c015 <kaidock_func+5>: push %rbp (A) 0xffffffffc0d4c016 <kaidock_func+6>: mov -0xeb4465d(%rip),%rsi # 0xffffffffb22079c0 <jiffies> 0xffffffffc0d4c01d <kaidock_func+13>: mov %rsp,%rbp 0xffffffffc0d4c020 <kaidock_func+16>: push %rbx 0xffffffffc0d4c021 <kaidock_func+17>: mov %rdi,%rbx 0xffffffffc0d4c024 <kaidock_func+20>: mov $0xffffffffc0d7b058,%rdi 0xffffffffc0d4c02b <kaidock_func+27>: call 0xffffffffaffb0040 <_printk> (B) 0xffffffffc0d4c030 <kaidock_func+32>: mov 0x2012(%rip),%eax # 0xffffffffc0d4e048 <loop> 0xffffffffc0d4c036 <kaidock_func+38>: sub $0x1,%eax (B) 0xffffffffc0d4c039 <kaidock_func+41>: mov %eax,0x2009(%rip) # 0xffffffffc0d4e048 <loop> 0xffffffffc0d4c03f <kaidock_func+47>: test %eax,%eax 0xffffffffc0d4c041 <kaidock_func+49>: jle 0xffffffffc0d4c06c <kaidock_func+92> 0xffffffffc0d4c043 <kaidock_func+51>: mov 0x10(%rbx),%rsi 0xffffffffc0d4c047 <kaidock_func+55>: add $0x3e8,%rsi 0xffffffffc0d4c04e <kaidock_func+62>: mov %rbx,%rdi (C) 0xffffffffc0d4c051 <kaidock_func+65>: call 0xffffffffb0004b60 <mod_timer> 0xffffffffc0d4c056 <kaidock_func+70>: mov 0x10(%rbx),%rsi (A) 0xffffffffc0d4c05a <kaidock_func+74>: mov -0xeb446a1(%rip),%rdx # 0xffffffffb22079c0 <jiffies> 0xffffffffc0d4c061 <kaidock_func+81>: mov %rsi,%rax 0xffffffffc0d4c064 <kaidock_func+84>: sub %rdx,%rax 0xffffffffc0d4c067 <kaidock_func+87>: test %rax,%rax 0xffffffffc0d4c06a <kaidock_func+90>: jle 0xffffffffc0d4c047 <kaidock_func+55> 0xffffffffc0d4c06c <kaidock_func+92>: mov -0x8(%rbp),%rbx 0xffffffffc0d4c070 <kaidock_func+96>: leave 0xffffffffc0d4c071 <kaidock_func+97>: xor %eax,%eax 0xffffffffc0d4c073 <kaidock_func+99>: xor %edx,%edx 0xffffffffc0d4c075 <kaidock_func+101>: xor %esi,%esi 0xffffffffc0d4c077 <kaidock_func+103>: xor %edi,%edi (D) 0xffffffffc0d4c079 <kaidock_func+105>: ret 0xffffffffc0d4c07a <kaidock_func+106>: int3 0xffffffffc0d4c07b <kaidock_func+107>: int3 0xffffffffc0d4c07c <kaidock_func+108>: int3 0xffffffffc0d4c07d <kaidock_func+109>: int3 0xffffffffc0d4c07e <kaidock_func+110>: xchg %ax,%ax
3. カーネルモジュール起動の動作フロー
Linuxカーネルは、カーネルモジュール組込みを要求するfinit_module
システムコールを用意しています。insmodコマンドはfinit_module
システムコールを呼び出すだけで、あとはfinit_module
が全ての処理を行います。カーネルモジュールの組み込み処理は逐次処理で、アルゴリズム的に複雑な場所はありません。
int finit_module(int fd, const char *param_values, int flags);
引数 | 説明 |
---|---|
fd | カーネルモジュールのオブジェクトを示すファイルディスクリプタ。 |
param_values | モジュール起動引数を渡す。今回の例では、"loop=100"などを渡す。 |
flags | MODULE_INIT_IGNORE_MODVERSIONSとMODULE_INIT_IGNORE_VERMAGICは、バージョンの合わないモジュールを強制的に組込む場合に指定する。MODULE_INIT_COMPRESSED_FILE は、モジュールが圧縮されている場合に指定する。 |
カーネルモジュールイメージが圧縮されている場合、イメージを解凍します。 現在のLinuxカーネルは、圧縮形式として gzip/xz/zst のいずれか1つだけをカーネルのビルド時に選択できます。Ubuntu 24.04 LTSのカーネルはzstを採用しています。 (module_decompress関数)
署名の確認を行います。PKCS#7で署名されていることを前提としています*4。 この署名はELFのセクションには置かれておらず、ELFファイルの後ろに添付する形になっています*5。 (module_sig_check関数)
カーネルモジュールリスト(
modules
という名前の変数をヘッドとするリスト)に本カーネルモジュール(を管理するデータ構造)を登録します。カーネルモジュールの状態は初期化中(MODULE_STATE_UNFORMED)としておきます。(add_unformed_module関数)カーネルモジュールを配置するためのカーネルメモリ領域を
vmalloc
系の関数を用いて確保します。 (layout_and_allocate関数)未解決シンボル情報を収集します。 組み込むカーネルモジュール内の未解決シンボルを、カーネル本体が公開しているシンボル、組み込み済みカーネルモジュールが公開しているシンボルの情報から見つけ出します。 組み込むカーネルモジュールがGPL以外のライセンスを持つ時、EXPORT_SYMBOL_GPL(シンボル名)で公開されているシンボルはリンクできないように制御しています*6。 (simplify_symbols関数、resolve_symbol関数)
再配置可能セクションを解決します。 組込み前のカーネルモジュールのセクション情報を見るとRELAというタイプのセクションがあります*7。 これらのセクション中のアドレス未解決の命令を書き換えます(この情報はrelocationセクションにあります)。 これはアーキテクチャ依存の処理になります。 (apply_relocations関数)
Elfファイルのセクション情報とリロケーション情報を参照し、解決していきます。リロケーション情報には、命令中のアドレス情報が格納されている場所(Offset)、およびそのアドレス情報の型(Type)があります(R_X86_64_PLT32、R_X86_64_32Sなど)。このアドレス情報の部分(命令の一部のフィールド)を編集していきます。
$ readelf -S kaidock.ko There are 43 section headers, starting at offset 0x494c0: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.gnu.bu[...] NOTE 0000000000000000 00000040 0000000000000024 0000000000000000 A 0 0 4 [ 2] .note.Linux NOTE 0000000000000000 00000064 0000000000000030 0000000000000000 A 0 0 4 [ 3] .text PROGBITS 0000000000000000 000000a0 000000000000010f 0000000000000000 AX 0 0 16 [ 4] .rela.text RELA 0000000000000000 00027978 0000000000000210 0000000000000018 I 40 3 8 : :
$ readelf -r kaidock.ko Relocation section '.rela.text' at offset 0x27950 contains 19 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000011 003100000004 R_X86_64_PLT32 0000000000000000 __fentry__ - 4 000000000019 003900000002 R_X86_64_PC32 0000000000000000 jiffies - 4 000000000027 00040000000b R_X86_64_32S 0000000000000000 .rodata.str1.8 + 0 00000000002c 003300000004 R_X86_64_PLT32 0000000000000000 _printk - 4 000000000032 000d00000002 R_X86_64_PC32 0000000000000000 .data + 24 00000000003b 000d00000002 R_X86_64_PC32 0000000000000000 .data + 24 000000000052 003600000004 R_X86_64_PLT32 0000000000000000 mod_timer - 4 00000000005d 003900000002 R_X86_64_PC32 0000000000000000 jiffies - 4 00000000007a 003800000004 R_X86_64_PLT32 0000000000000000 __x86_return_thunk - 4 000000000091 003100000004 R_X86_64_PLT32 0000000000000000 __fentry__ - 4 000000000099 003900000002 R_X86_64_PC32 0000000000000000 jiffies - 4 0000000000a0 003a0000000b R_X86_64_32S 0000000000000000 kaidock_timer + 0 0000000000b0 003a00000002 R_X86_64_PC32 0000000000000000 kaidock_timer + c 0000000000b5 003500000004 R_X86_64_PLT32 0000000000000000 add_timer - 4 0000000000bf 003800000004 R_X86_64_PLT32 0000000000000000 __x86_return_thunk - 4 0000000000e1 003100000004 R_X86_64_PLT32 0000000000000000 __fentry__ - 4 0000000000e9 003a0000000b R_X86_64_32S 0000000000000000 kaidock_timer + 0 0000000000f1 003000000004 R_X86_64_PLT32 0000000000000000 timer_delete_sync - 4 0000000000fb 003800000004 R_X86_64_PLT32 0000000000000000 __x86_return_thunk - 4 : :
組み込んだカーネルモジュールのシンボル(EXPORT_SYMBOL、EXPORT_SYMBOL_GPLなどで指定したもの)を公開します。(add_kallsyms関数)
CPUモデルに合せた命令への書き換えも行います。(module_finalize関数)
関数の先頭にある
call
命令をnopl
命令に置き換えます。(ftrace_module_init関数)sysファイル(
/sys/module/kaidock
)を作成します。(mod_sysfs_setup関数)モジュールがinit_module関数を呼び出す準備が整ったことを示すMODULE_STATE_COMING状態にします。(complete_formation関数)
カーネルモジュールに定義した
init_module()
を呼び出し、その後でカーネルモジュールの状態を実行中(MODULE_STATE_LIVE)にします。(do_init_module関数)
カーネルモジュール起動(insmod
)処理はこれで終わりです。後はカーネル本体機能と区別なく動作を続けます。
4. カーネルモジュールの削除
delete_moduleシステムコールにて実現されています。rmmodコマンドはdelete_module
を呼び出すだけです。
削除対象のカーネルモジュールが他モジュールから参照されている場合にエラーとしてはじくだけで、その後は削除処理を始めます。削除するカーネルモジュールのcleanup_module
関数を呼び出した後、finit_module
で確保した資源を解放します。(free_module関数)
最後に
カーネルモジュールは、ライブパッチ(Live Patch)機能の実現のためにも利用されています。問題のある関数の修正版をカーネルモジュールとしてカーネル本体に組み込んだ後、ftrace
の仕組みを使って問題のある関数の先頭に修正版の関数の先頭にジャンプする命令を埋め込むことが基本的戦略です。
また、最近「LinuxカーネルがRust対応した」という話を聞いたことがあると思いますが、これによりカーネルモジュールの記述言語をRustにすることができるようになりました。insmod
処理からすると、カーネルモジュールが*.ko
形式のリロケータブルオブジェクトであれば、元の記述言語がCであろうがRustであろうが関係ありません。一方、カーネルモジュール開発者にとっては、Rustを使うことでメモリ操作関連のバグが入りにくくなるというメリットがあります。
現時点での課題は、カーネル機能を利用するためのRust向けAPIがまだ揃っていない*8ことと、C言語記述のカーネルモジュールより少しオーバヘッド(Rust言語仕様上からくるもの)が大きくなるため、性能が求められるカーネルモジュールでは使いにくいことでしょうか。このあたりの詳細については、藤田さんの記事を参照されると良いと思います。Rust愛が伝わってきます。
おまけ ~ カーネルモジュール組み込み機構の歴史
古いinsmodコマンドは下記に示す流れでカーネルモジュールを組み込んでいました。
- query_moduleシステムコールでカーネルのシンボル情報取得
- create_moduleシステムコールでメモリ領域確保
- カーネルモジュールのアドレス解決
- init_moduleシステムコールでカーネルモジュール組み込み
カーネルv2.6の時代には既にquery_module、create_moduleシステムコールは廃止されており、init_moduleシステムコールがモジュール組み込みのすべての処理を行なうように変更されています。
その後カーネルv3.8にて、カーネルモジュールイメージを直接渡すinit_moduleシステムコールに代わり、カーネルモジュールリロケータブルオブジェクト(*.ko
)のファイルディスクリプタを渡すfinit_moduleシステムコールが追加されました。ファイルシステム自体の機能もカーネルモジュールの正当性の確認に役立てようということです。2つのシステムコールはカーネルモジュールの渡し方が異なるのみで、カーネル内では同じ動きをします。
おまけ ~ カーネルモジュールへの署名
/var/lib/shim-signed/mok/配下に、下記のファイルがある時は既に署名用の鍵が用意されています。
$ ls /var/lib/shim-signed/mok/
MOK.der MOK.priv
署名用の鍵がある場合
kmodsignコマンドを用い、カーネルモジュールに署名を添付します。 ハッシュアルゴリズムは、Ubuntu 24.04 LTSの他のモジュールと同じsha512にしておきます。
$ sudo kmodsign sha512 /var/lib/shim-signed/mok/MOK.priv /var/lib/shim-signed/mok/MOK.der kaidock.ko
これでカーネルモジュールへの署名は完了です。念のためカーネルモジュールの署名を確認しておきましょう。
$ modinfo kaidock.ko filename: /home/riscv/kaidoku/kmodule/kaidock.ko license: GPL author: taka srcversion: FCA95EBA12E11A29AD336FF depends: retpoline: Y name: kaidock vermagic: 6.8.0-40-generic SMP preempt mod_unload modversions sig_id: PKCS#7 signer: new-linux-kaidokusitu-pj Secure Boot Module Signature key sig_key: 3A:AB:5A:1E:A0:B8:B8:E7:86:68:E7:21:58:2C:39:1B:65:0B:BD:F5 sig_hashalgo: sha512 signature: EF:A8:73:6D:28:96:27:A5:63:74:09:F6:03:7B:D1:53:F6:3F:08:02: DF:0B:86:FB:14:1A:AC:21:CF:61:87:CB:15:BB:F2:8A:29:33:BD:8B: 6A:34:5F:5A:88:67:BC:9F:11:52:9E:76:48:64:BA:9B:4C:D7:73:4C: D7:B0:B4:E1:5B:84:49:58:98:36:C5:D6:03:F3:D8:8F:1E:F6:B6:56: 7F:07:D3:00:D8:FC:E5:57:5E:46:CF:06:92:72:5A:CC:C5:3D:B5:F3: 32:1B:02:01:80:69:35:64:75:1C:80:A1:7D:34:FF:74:32:04:55:0A: 42:90:6B:59:44:78:7C:FA:26:2C:B6:ED:5E:73:D4:62:D4:B2:1E:54: 8F:FC:5B:D3:95:24:AC:F8:96:E2:53:02:17:82:67:1C:81:B1:F5:80: BA:94:6B:07:4C:09:DE:A6:E7:8F:F5:07:38:FD:56:3B:B2:FD:A5:05: 8B:10:6D:94:6F:E5:A2:D8:24:11:57:91:FA:FB:79:EB:70:48:37:EF: 69:73:1E:25:18:CE:34:B1:1F:B6:6F:5E:D8:D6:64:D1:C6:2B:78:A9: 27:59:A0:A2:0E:EC:82:74:A8:AC:8F:57:40:E5:5F:2F:86:DE:5E:15: 17:A5:B3:D6:C9:B2:27:4D:68:35:EF:D7:3F:92:80:68 parm: loop:specify the loop count (int)
署名用の鍵がない場合
まず、署名用のパッケージをインストールします。
$ sudo apt install shim-singned
鍵の生成をします。
$ sudo update-secureboot-policy --new-key Generating a new Secure Boot signing key: Can't load /var/lib/shim-signed/mok/.rnd into RNG 40779766D3700000:error:12000079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:106:Filename=/var/lib/shim-signed/mok/.rnd .+..........+...............+.....+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.........+.......+...+...........+.+.........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.....+....+.........+........+...+...............+....+.....+.......+........+............+.+.....+...+..........+..+.......+.................+...+...+...+.+......+.....+....+...+.....+.+...........+....+.........+........+.........+.+............+..+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ........+.+..+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*....+....+......+..+.......+........+.+...........+.+..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.......+..+.+......+...........+.......+...+..+.+.....+....+..+.........+................+........+......+....+..+....+..+.......+..+....+.........+..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -----
鍵が生成されました。
$ ls /var/lib/shim-signed/mok/
MOK.der MOK.priv
次に、この鍵をUEFIセキュアブートの鍵データベースに登録します。下記コマンドは、鍵登録を依頼するコマンドです。次のステップで利用するパスワード設定が求められます。
$ sudo mokutil --import /var/lib/shim-signed/mok/MOK.der
システムを再起動します。
- Shim UEFI key managerが起動します。「Press any key to perform MOK management」と表示されるので、任意のキーを押してMOK管理を開始します。
- 画面に表示される「Enroll MOK」を選択します。
- 「View key 0」 選択すると先ほど生成した鍵の情報が表示されます。
- 任意のキーを押すと「Enroll MOK」のメニューに戻ります。
- Continueを選択すると、「Enroll the key(s)?」と聞かれるので、Yesを選択します。
- パスワード入力が求められるので、先ほどの鍵生成の時に設定したパスワードを入力します。
- 「Reboot」 を選択し、システムを再起動します。
システムが再起動したら、署名用の鍵がある場合に進みます。
*1:jiffiesは、カーネル起動時からの経過時間を表す変数です。
*2:カーネルをコンパイルした時のgccとインストールされているgccのバージョンが異なる時はビルド処理がエラーとなります。その場合、バージョン番号付きのgccを指定してmakeしてください。Ubuntu 24.04の場合は、$ make -C /lib/modules/`uname -r`/build M=`pwd` modules MAKEFLAGS="CC=gcc-13" となります。/boot/config-`uname -r`ファイルを参照するとカーネルバージョンとgccのバージョンの対応が分かります。
*3:こういう領域を予約するためのコンパイラオプションがあります
*4:カーネルヘッダを見ると署名方式としてOpenPGP、X.509、PKCS#7が選択できそうに見えますが、実際にはPKCS#7用以外のコードは実装されておらず、エラーになります。
*5:ツールによっては、壊れたelfファイルと認識するかも
*6:OSSであってもGPL以外のライセンス(MITやApacheなど)を持つカーネルモジュールは、実装しにくくなっています。
*7:アーキテクチャの種類によってはSHT_REL
*8:Rustのコードから、C関数を直接呼び出すことは御法度とされている。