シンプルなLinuxカーネルモジュールの作成

ゴールデンリング-0を攻略



Linuxは、アプリケーション用の強力で包括的なAPIを提供しますが、それだけでは不十分な場合があります。 システム内の特権情報にアクセスして機器と対話したり操作を実行するには、カーネルドライバーが必要です。



Linuxカーネルモジュールは、Linuxカーネルに直接挿入されるコンパイル済みバイナリコードであり、x86–64プロセッサでコマンドを実行するための内部で最も保護されていないリングであるリング0で動作します。 ここでは、コードはチェックなしで完全に実行されますが、信じられないほどの速度で、システムリソースにアクセスできます。



単なる人間ではない



Linuxカーネルモジュールの作成は、気弱な人向けではありません。 カーネルを変更すると、データを失うリスクがあります。 カーネルコードには、通常のLinuxアプリケーションのように標準的な保護がありません。 間違えた場合は、システム全体をハングアップしてください。



問題が必ずしもすぐに現れるとは限らないという事実により、状況は悪化します。 モジュールが起動直後にシステムをハングアップさせる場合、これが最良の障害シナリオです。 コードが多いほど、無限ループとメモリリークのリスクが高くなります。 不注意な場合は、機械が動作するにつれて問題が徐々に大きくなります。 最終的に、重要なデータ構造、さらにはバッファが上書きされる可能性があります。



従来のアプリケーション開発パラダイムをほとんど忘れることができます。 モジュールのロードとアンロードに加えて、システムイベントに応答するコードを記述しますが、シーケンシャルパターンに従って動作しません。 カーネルで作業するときは、アプリケーション自体ではなく、APIを記述します。



また、標準ライブラリへのアクセス権もありません。 カーネルはprintk



printf



代替として機能します)やkmalloc



malloc



と同様に機能します)などの機能を提供しますが、ほとんどの場合、ハードウェアはそのままです。 さらに、モジュールをアンロードした後、完全にクリーニングする必要があります。 ガベージコレクションはありません。



必要なコンポーネント



開始する前に、ジョブに必要なすべてのツールがあることを確認する必要があります。 最も重要なことは、Linuxでマシンが必要なことです。 これは予想外のことです。 任意のLinuxディストリビューションが適していますが、この例ではUbuntu 16.04 LTSを使用しているため、他のディストリビューションを使用する場合は、インストールコマンドをわずかに変更する必要があります。



次に、個別の物理マシンまたは仮想マシンが必要です。 個人的には、仮想マシンで作業することを好みますが、自分で選択します。 間違えた場合のデータ損失のため、メインマシンの使用はお勧めしません。 私は「if」ではなく「when」と言います。その過程で少なくとも数回は車を掛けなければならないからです。 コードの最後の変更は、カーネルパニック時に書き込みバッファに残っている可能性があるため、ソースが破損している可能性があります。 仮想マシンでテストすると、これらのリスクがなくなります。



最後に、少なくとも少しのCを知る必要があります。C++の作業環境はカーネルには大きすぎるため、純粋なベアCで記述する必要があります。



開発環境をインストールする



Ubuntuでは、次を実行する必要があります。



 apt-get install build-essential linux-headers-`uname -r`
      
      





この例に必要な最も重要な開発ツールとカーネルヘッダーをインストールします。



以下の例では、rootではなく通常のユーザーとして作業しているが、sudo特権を持っていることを前提としています。 カーネルモジュールを読み込むにはsudoが必要ですが、可能な限りルートの外側で作業する必要があります。



はじめに



コードを書き始めましょう。 環境を準備します。



 mkdir ~/src/lkm_example cd ~/src/lkm_example
      
      





お気に入りのエディター(私の場合はvim)を起動lkm_example.c



次の内容のlkm_example.c



ファイルlkm_example.c



作成します。



 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION(“A simple example Linux module.”); MODULE_VERSION(“0.01”); static int __init lkm_example_init(void) { printk(KERN_INFO “Hello, World!\n”); return 0; } static void __exit lkm_example_exit(void) { printk(KERN_INFO “Goodbye, World!\n”); } module_init(lkm_example_init); module_exit(lkm_example_exit);
      
      





最も単純なモジュールを構築しました。最も重要な部分をより詳細に検討してください。





ただし、このファイルはコンパイルできません。 Makefileが必要です。 今のところ、このような基本的な例で十分です。 make



スペースとタブについて非常に気難しいので、適切な場合はスペースの代わりにタブを使用してください。



 obj-m += lkm_example.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
      
      





make



を実行make



場合、モジュールを正常にコンパイルする必要があります。 結果はlkm_example.ko



ファイルです。 エラーが表示された場合、ソースコードの引用符が正しく設定されていることを確認してください。誤ってUTF-8エンコーディングに設定されていないことを確認してください。



これで、モジュールを実装して確認できます。 これを行うには、次を実行します。



 sudo insmod lkm_example.ko
      
      





すべてが正常であれば、何も表示されません。 printk



関数は、コンソールではなくカーネルログに出力を提供します。 表示するには、次を実行する必要があります。



 sudo dmesg
      
      





「Hello、World!」という行が始まり、タイムスタンプが先頭に表示されます。 これは、カーネルモジュールがロードされ、カーネルログに正常に書き込まれたことを意味します。 また、モジュールがまだメモリにあることを確認できます。



 lsmod | grep “lkm_example”
      
      





モジュールを削除するには、次を実行します。



 sudo rmmod lkm_example
      
      





dmesgを再度実行すると、ジャーナルに「Goodbye、World!」というエントリが表示されます。 lsmodを再度実行して、モジュールがアンロードされたことを確認できます。



ご覧のとおり、このテスト手順は少し面倒ですが、以下を追加することで自動化できます。



 test: sudo dmesg -C sudo insmod lkm_example.ko sudo rmmod lkm_example.ko dmesg
      
      





Makefileの最後で実行します:



 make test
      
      





モジュールをテストし、個別のコマンドを実行せずにカーネルログへの出力を確認します。



これで、完全に機能するカーネルモジュールが完成しました。



もう少し面白い



もう少し掘り下げます。 カーネルモジュールはあらゆる種類のタスクを実行できますが、アプリケーションとの対話は最も一般的な使用例の1つです。



アプリケーションはカーネル空間のメモリを表示できないため、APIを使用してアプリケーションと対話する必要があります。 技術的にはこのやり取りにはいくつかの方法がありますが、最も一般的なのはデバイスファイルの作成です。



おそらく以前にデバイスファイルを扱ったことがあるでしょう。 /dev/zero



/dev/null



言及するコマンドは、期待される値を返す「ゼロ」および「ヌル」デバイスと対話します。



この例では、「Hello、World」を返します。 これはアプリケーションにとって特に便利な機能ではありませんが、デバイスファイルを介してアプリケーションと対話するプロセスを示しています。



完全なリストは次のとおりです。



 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <asm/uaccess.h> MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION(“A simple example Linux module.”); MODULE_VERSION(“0.01”); #define DEVICE_NAME “lkm_example” #define EXAMPLE_MSG “Hello, World!\n” #define MSG_BUFFER_LEN 15 /* Prototypes for device functions */ static int device_open(struct inode *, struct file *); static int device_release(struct inode *, struct file *); static ssize_t device_read(struct file *, char *, size_t, loff_t *); static ssize_t device_write(struct file *, const char *, size_t, loff_t *); static int major_num; static int device_open_count = 0; static char msg_buffer[MSG_BUFFER_LEN]; static char *msg_ptr; /* This structure points to all of the device functions */ static struct file_operations file_ops = { .read = device_read, .write = device_write, .open = device_open, .release = device_release }; /* When a process reads from our device, this gets called. */ static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) { int bytes_read = 0; /* If we're at the end, loop back to the beginning */ if (*msg_ptr == 0) { msg_ptr = msg_buffer; } /* Put data in the buffer */ while (len && *msg_ptr) { /* Buffer is in user data, not kernel, so you can't just reference * with a pointer. The function put_user handles this for us */ put_user(*(msg_ptr++), buffer++); len--; bytes_read++; } return bytes_read; } /* Called when a process tries to write to our device */ static ssize_t device_write(struct file *flip, const char *buffer, size_t len, loff_t *offset) { /* This is a read-only device */ printk(KERN_ALERT “This operation is not supported.\n”); return -EINVAL; } /* Called when a process opens our device */ static int device_open(struct inode *inode, struct file *file) { /* If device is open, return busy */ if (device_open_count) { return -EBUSY; } device_open_count++; try_module_get(THIS_MODULE); return 0; } /* Called when a process closes our device */ static int device_release(struct inode *inode, struct file *file) { /* Decrement the open counter and usage count. Without this, the module would not unload. */ device_open_count--; module_put(THIS_MODULE); return 0; } static int __init lkm_example_init(void) { /* Fill buffer with our message */ strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN); /* Set the msg_ptr to the buffer */ msg_ptr = msg_buffer; /* Try to register character device */ major_num = register_chrdev(0, “lkm_example”, &file_ops); if (major_num < 0) { printk(KERN_ALERT “Could not register device: %d\n”, major_num); return major_num; } else { printk(KERN_INFO “lkm_example module loaded with device major number %d\n”, major_num); return 0; } } static void __exit lkm_example_exit(void) { /* Remember — we have to clean up after ourselves. Unregister the character device. */ unregister_chrdev(major_num, DEVICE_NAME); printk(KERN_INFO “Goodbye, World!\n”); } /* Register module functions */ module_init(lkm_example_init); module_exit(lkm_example_exit);
      
      





改善された例のテスト



この例では、ロードおよびアンロード時にメッセージを表示するだけでなく、より厳密なテスト手順が必要です。 モジュールをアンロードせずにロードするためだけにMakefileを変更します。



 obj-m += lkm_example.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean test: # We put a — in front of the rmmod command to tell make to ignore # an error in case the module isn't loaded. -sudo rmmod lkm_example # Clear the kernel log without echo sudo dmesg -C # Insert the module sudo insmod lkm_example.ko # Display the kernel log dmesg
      
      





make test



を実行make test



と、最高のデバイス番号の発行が表示されます。 この例では、カーネルが自動的に割り当てます。 ただし、新しいデバイスを作成するにはこの番号が必要です。



make test



から取得した番号を取得し、それを使用してデバイスファイルを作成します。これにより、ユーザー空間からカーネルモジュールとの通信を確立できます。



 sudo mknod /dev/lkm_example c MAJOR 0
      
      





(この例では、MAJORをmake test



またはdmesg



値に置き換えます)



mknod



コマンドのc



パラメーターは、キャラクターデバイスファイルを作成する必要があることをmknodに伝えます。



これで、デバイスからコンテンツを取得できます。



 cat /dev/lkm_example
      
      





またはdd







 dd if=/dev/lkm_example of=test bs=14 count=100
      
      





アプリケーションからこのファイルにアクセスすることもできます。 コンパイルされたアプリケーションである必要はありません-Python、Ruby、およびPHPスクリプトでさえ、このデータにアクセスできます。



デバイスでの作業が完了したら、それを削除してモジュールをアンロードします。



 sudo rm /dev/lkm_example sudo rmmod lkm_example
      
      





おわりに



カーネル空間でのいたずらを楽しんだことを願っています。 示されている例はプリミティブですが、これらの構造を使用して、非常に複雑なタスクを実行するカスタムモジュールを作成できます。



カーネル空間のすべてがあなたの責任であることを忘れないでください。 コードのサポートや二度目のチャンスはありません。 クライアントのためにプロジェクトを行っている場合は、3倍ではないにしても2倍のデバッグ時間を事前に計画してください。 カーネルコードは、実行されるシステムの整合性と信頼性を保証するために、可能な限り完璧でなければなりません。



All Articles