C言語でLinuxのカーネルモジュールの作り方を教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Verse(UEFN), Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
目次
【C言語】Linuxのカーネルモジュール
Linuxのカーネルモジュールは,Linuxカーネルの機能を動的に追加できる仕組みのことです.
カーネルモジュールを利用すると,カーネルの再コンパイルや再起動せずに実行時にカーネルの機能をロード,アンロードすることができます.
これにより,開発やデバッグの時間を削減することが可能になります.
Linuxのカーネルモジュールは1995年のバージョン1.2で登場しました.
また,Linuxの数多くの機能をモジュールとしてコンパイル可能です.
Linuxカーネルの設定ファイル「.config」で選択することができます.
例えば,.configで「CONFIG_XFS_FS=y」と設定するとカーネルの機能を有効にできますが,以下のように「CONFIG_XFS_FS=m」とするとカーネルモジュールとしてコンパイルできます.
1 |
CONFIG_XFS_FS=m # kernel module |
カーネルモジュールとビルトインのカーネルコードの性能差はありません.
カーネルモジュールは,モジュールとしてコンパイルされたバグのあるデバイスドライバを選択的に実行することで特定したい時に有用です.
カーネルモジュールはカーネル全体に対してリンクされます.
カーネルモジュールは,以下で定義されたすべてのLinuxカーネルのグローバルシンボルにアクセスできます.
- EXPORT_SYMBOL(function or variable name)
名前空間の汚染と変数名の意図しない再利用を避けるために,以下の2つの注意点があります.
- モジュール名のプレフィックスをシンボルにつけること(例:モジュール名がhello_kernel_moduleの場合はhello_kernel_module_func関数)
- もしシンボルがグローバルの場合,staticをつけること
カーネルのシンボルのリストは/proc/kallsymsにあり,以下のコマンドで表示できます.
1 2 3 4 5 |
$ cat /proc/kallsyms 0000000000000000 t piix4_access_sb800 [i2c_piix4] 0000000000000000 t piix4_access_sb800.cold [i2c_piix4] 0000000000000000 t piix4_driver_exit [i2c_piix4] ... |
また,lsmodコマンドでロードされているモジュールの一覧を確認できます.
1 2 3 4 5 |
$ lsmod Module Size Used by tls 114688 0 rfcomm 81920 4 ... |
カーネルモジュールの解説動画はこちらがわかりやすいです.
カーネルモジュールで「Hello World!」を表示
カーネルモジュールで「Hello World!」を表示する方法を紹介します.
カーネルモジュールのビルド
まず,カーネルモジュールのコード一式をこちらからダウンロードして下さい.
以下のコマンドで解凍した後にディレクトリの内容を表示します.
1 2 3 4 |
$ unzip hello_kernel_module.zip $ cd hello_kernel_module $ ls Makefile hello_kernel_module.c |
以下の2つのファイルがあることがわかります.
- Makefile:カーネルモジュール用の構成ファイル
- hello_kernel_module.c:カーネルモジュールのコード
カーネルモジュール用のMakefileは,従来のユーザレベルのコード用のMakefileと書き方が少し異なります.
注意点は以下になります.
- 現在実行しているLinuxカーネルのソースディレクトリKDIRを指定していること
- カーネルモジュールのソースコードがカーネルソースから外れていること
- コンパイル後のカーネルモジュールとして,拡張子が.koのファイルが作成されること(このMakefileではhello_kernel_module.koを作成)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# # Makefile for Hello Kernel Module # # Hiroyuki Chishiro # obj-m += hello_kernel_module.o KDIR = /lib/modules/$(shell uname -r)/build RM = rm -f all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean $(RM) *~ *.orig |
hello_kernel_module.cは以下になります.
hello_kernel_module_init関数は,「__init」で定義し,module_initマクロで初期化時のエントリポイントとして設定しているため,カーネルモジュールのロード時に実行します.
hello_kernel_module_exit関数は,「__exit」で定義し,module_exitマクロで終了時のエントリポイントとして設定しているため,カーネルモジュールのアンロード時に実行します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> static int __init hello_kernel_module_init(void) { printk(KERN_INFO "Hello Worldl!\n"); return 0; } static void __exit hello_kernel_module_exit(void) { printk(KERN_INFO "Goodbye World!\n"); } module_init(hello_kernel_module_init); module_exit(hello_kernel_module_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Hiroyuki Chishiro"); MODULE_DESCRIPTION("Hello Kernel Module"); |
参考までに,module_init/module_exitマクロはlinux/include/linux/module.hにあります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * module_init() - driver initialization entry point * @x: function to be run at kernel boot time or module insertion * * module_init() will either be called during do_initcalls() (if * builtin) or at module insertion time (if a module). There can only * be one per module. */ #define module_init(x) __initcall(x); /** * module_exit() - driver exit entry point * @x: function to be run when driver is removed * * module_exit() will wrap the driver clean-up code * with cleanup_module() when used with rmmod when * the driver is a module. If the driver is statically * compiled into the kernel, module_exit() has no effect. * There can only be one per module. */ #define module_exit(x) __exitcall(x); |
makeでビルドします.
1 2 3 4 5 6 7 8 9 10 |
$ make make -C /lib/modules/5.15.0-46-generic/build M=/home/chishiro/c-language/hello_kernel_module modules make[1]: Entering directory '/usr/src/linux-headers-5.15.0-46-generic' CC [M] /home/chishiro/c-language/hello_kernel_module/hello_kernel_module.o MODPOST /home/chishiro/c-language/hello_kernel_module/Module.symvers CC [M] /home/chishiro/c-language/hello_kernel_module/hello_kernel_module.mod.o LD [M] /home/chishiro/c-language/hello_kernel_module/hello_kernel_module.ko BTF [M] /home/chishiro/c-language/hello_kernel_module/hello_kernel_module.ko Skipping BTF generation for /home/chishiro/c-language/hello_kernel_module/hello_kernel_module.ko due to unavailability of vmlinux make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-46-generic' |
カーネルモジュールの実行
カーネルモジュールの実行方法を紹介します.
カーネルモジュールは,rootでないとロードやアンロードできない(実行できない)ことに注意して下さい.
また,カーネルモジュールは特定のカーネルバージョンに対してコンパイルされ,他のカーネルではロードできないことに注意してください.
※このチェックはmodversionsと呼ばれる機構により回避することができますが,危険な場合があります.
insmodコマンドでカーネルモジュールhello_kernel_module.koをロードします.
1 |
$ sudo insmod hello_kernel_module.ko |
カーネルモジュールがロードされて「__init」があるhello_kernel_module_init関数が実行します.
dmesgコマンドで確認すると「Hello World!」が表示されていることがわかります.
1 2 |
$ sudo dmesg | tail -n 1 [36740.613205] Hello Worldl! |
lsmodコマンドを実行するとhello_kernel_moduleがあることがわかります.
1 2 3 4 5 6 |
$ lsmod Module Size Used by hello_kernel_module 16384 0 tls 114688 0 rfcomm 81920 4 ... |
また,modinfoコマンドでhello_kernel_module.koの情報を取得できます.
1 2 3 4 5 6 7 8 9 10 |
$ modinfo hello_kernel_module.ko filename: /home/chishiro/c-language/hello_kernel_module/hello_kernel_module.ko description: Hello Kernel Module author: Hiroyuki Chishiro license: GPL srcversion: 66397C05E339F25CA4E7EB6 depends: retpoline: Y name: hello_kernel_module vermagic: 5.15.0-46-generic SMP mod_unload modversions |
カーネルモジュールを削除するにはrmmodコマンドを利用します.
1 |
$ sudo rmmod hello_kernel_module.ko |
カーネルモジュールがアンロードされて「__exit」があるhello_kernel_module_exit関数が実行します.
dmesgコマンドで確認すると「Goodbye World!」が表示されていることがわかります.
1 2 |
$ sudo dmesg | tail -n 1 [37327.894047] Goodbye World! |
カーネルモジュールを利用してカーネル空間とユーザ空間でデータをやり取り
カーネルモジュールを利用してカーネル空間とユーザ空間でデータをやり取りする方法を紹介します.
データのやり取りには,ioctlシステムコールやread/writeシステムコールを利用します.
コード一式はこちらからダウンロードして下さい.
カーネル空間(カーネルモジュール)のコードioctl_kernel_module.cと,ユーザ空間のコードioctl_user.cは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/cdev.h> #define MY_DEVICE_NAME "my_device" #define MY_CLASS_NAME "my_class" #define WR_VALUE _IOW('a', 'a', int32_t *) #define RD_VALUE _IOR('a', 'b', int32_t *) int32_t val = 0; dev_t dev = 0; static struct class *dev_class; static struct cdev my_cdev; static int my_open(struct inode *inode, struct file *file) { printk(KERN_INFO "my_open()\n"); return 0; } static int my_release(struct inode *inode, struct file *file) { printk(KERN_INFO "my_release()\n"); return 0; } static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off) { ssize_t ret; printk(KERN_INFO "my_read()\n"); if ((ret = copy_to_user((int32_t *) buf, &val, len))) { printk(KERN_ERR "Error: copy_to_user()\n"); return ret; } return 0; } static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *off) { ssize_t ret; printk(KERN_INFO "my_write()\n"); if ((ret = copy_from_user(&val, (int32_t *) buf, len))) { printk(KERN_ERR "Error: copy_from_user()\n"); return ret; } printk(KERN_INFO "val = %d\n", val); return len; } static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { printk(KERN_INFO "my_ioctl()\n"); switch (cmd) { case WR_VALUE: my_write(file, (const char *) arg, sizeof(val), 0); break; case RD_VALUE: my_read(file, (char *) arg, sizeof(val), 0); break; default: printk(KERN_ERR "Error: unknown cmd = %u\n", cmd); break; } return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .read = my_read, .write = my_write, .open = my_open, .unlocked_ioctl = my_ioctl, .release = my_release, }; static int __init my_driver_init(void) { printk("my_driver_init()\n"); if (alloc_chrdev_region(&dev, 0, 1, MY_DEVICE_NAME) < 0) { printk(KERN_ERR "Error: cannot allocate major number\n"); return -1; } printk(KERN_INFO "major = %d, minor = %d\n", MAJOR(dev), MINOR(dev)); cdev_init(&my_cdev, &fops); if (cdev_add(&my_cdev, dev, 1) < 0) { printk(KERN_ERR "Error: cannot add the device to the system\n"); goto error_class; } if (IS_ERR(dev_class = class_create(THIS_MODULE, MY_CLASS_NAME))) { printk(KERN_ERR "Error: cannot create the struct class\n"); goto error_class; } if (IS_ERR(device_create(dev_class, NULL, dev, NULL, MY_DEVICE_NAME))) { printk(KERN_ERR "Error: cannot create %s\n", MY_DEVICE_NAME); goto error_device; } return 0; error_device: class_destroy(dev_class); error_class: unregister_chrdev_region(dev, 1); return -1; } static void __exit my_driver_exit(void) { printk("my_driver_exit()\n"); device_destroy(dev_class, dev); class_destroy(dev_class); cdev_del(&my_cdev); unregister_chrdev_region(dev, 1); } module_init(my_driver_init); module_exit(my_driver_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Hiroyuki Chishiro"); MODULE_DESCRIPTION("IOCTL Kernel Module"); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #define WR_VALUE _IOW('a', 'a', int32_t *) #define RD_VALUE _IOR('a', 'b', int32_t *) #define MY_DEVICE_NAME "/dev/my_device" int main(void) { int fd; int32_t val, val2; if ((fd = open(MY_DEVICE_NAME, O_RDWR)) < 0) { perror("open"); exit(1); } printf("Please input an integer: "); scanf("%d", &val); printf("write value = %d\n", val); ioctl(fd, WR_VALUE, (int32_t *) &val); ioctl(fd, RD_VALUE, (int32_t *) &val2); printf("read value = %d\n", val2); printf("Please input an integer: "); scanf("%d", &val); printf("write value = %d\n", val); write(fd, (int32_t *) &val, sizeof(int32_t)); read(fd, (int32_t *) &val2, sizeof(int32_t)); printf("read value = %d\n", val2); if (close(fd) < 0) { perror("close"); exit(2); } return 0; } |
実行結果は以下になります.
6行目で123を入力するとioctl関数を呼び出して,カーネルモジュールのmy_ioctl関数を呼び出します.
my_ioctl関数でWR_VALUE/RD_VALUEかを選択して,それぞれmy_write/my_read関数を呼び出します.
9行目で-45を入力するとwrite/read関数を呼び出して,(my_ioctl関数を介さずに)それぞれmy_write/my_read関数を呼び出します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
$ make ... $ sudo insmod ioctl_kernel_module.ko $ gcc ioctl_user.c $ sudo ./a.out Please input an integer: 123 write value = 123 read value = 123 Please input an integer: -45 write value = -45 read value = -45 $ sudo rmmod ioctl_kernel_module.ko $ sudo dmesg | tail -n 13 [ 7028.969452] my_driver_init() [ 7028.969455] major = 235, minor = 0 [ 7033.687308] my_open() [ 7034.580681] my_ioctl() [ 7034.580683] my_write() [ 7034.580684] val = 123 [ 7034.580685] my_ioctl() [ 7034.580685] my_read() [ 7036.061361] my_write() [ 7036.061364] val = -45 [ 7036.061368] my_read() [ 7036.061416] my_release() [ 7040.526616] my_driver_exit() |
参考:Rust言語でLinuxのカーネルモジュール
Ubuntu 23.04(Linuxカーネル6.2)においてRust言語でLinuxのカーネルモジュールの作り方を解説している動画です.
※Ubuntu 22.04 LTSではないことに注意して下さい.
また,Ubuntu kernel is getting “Rusty” in Lunarの記事もあわせて読むと理解が深まります!
まとめ
C言語でLinuxのカーネルモジュールの作り方を紹介しました.
カーネルモジュールを作成して,カーネルレベルのプログラミングを習得しましょう!
カーネルモジュールを深く理解したいあなたは,以下を読みましょう!
- The Linux Kernel Module Programming Guide
- How to write your first Linux Kernel Module
- Linux Driver Tutorial
- Using ioctl()
- Linuxのドライバの初期化が呼ばれる流れ
- 組み込みLinuxデバイスドライバの作り方
Linuxカーネルの同期プログラミングを知りたいあなたはこちらからどうぞ.
Linuxカーネルのデバッグ方法を知りたいあなたはこちらからどうぞ.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!