如何編寫一個 Linux 內核模塊 ?

Linux 系統爲應用程序提供了功能強大且容易擴展的 API,但在某些情況下,這還遠遠不夠。與硬件交互或進行需要訪問系統中特權信息的操作時,就需要一個內核模塊。

Linux 內核模塊是一段編譯後的二進制代碼,直接插入 Linux 內核中,在 Ring 0(x86–64 處理器中執行最低和受保護程度最低的執行環)上運行。這裏的代碼完全不受檢查,但是運行速度很快,可以訪問系統中的所有內容。

Intel x86 架構使用了 4 個級別來標明不同的特權級。Ring 0 實際就是內核態,擁有最高權限。而一般應用程序處於 Ring 3 狀態 -- 用戶態。在 Linux 中,還存在 Ring 1 和Ring 2 兩個級別,一般歸屬驅動程序的級別。在 Windows 平臺沒有 Ring 1 和 Ring 2 兩個級別,只用 Ring 0 內核態和 Ring 3 用戶態。在權限約束上,高特權等級狀態可以閱讀低特權等級狀態的數據,例如進程上下文、代碼、數據等等,但反之則不可。Ring 0 最高可以讀取 Ring 0-3 所有的內容,Ring 1 可以讀 Ring 1-3 的,Ring 2 以此類推,Ring 3 只能讀自己的數據。

  1. 爲什麼要開發內核模塊

編寫 Linux 內核模塊並不是因爲內核太龐大而不敢修改。直接修改內核源碼會導致很多問題,例如:通過更改內核,你將面臨數據丟失和系統損壞的風險。內核代碼沒有常規 Linux 應用程序所擁有的安全防護機制,如果內核發生故障,將鎖死整個系統。

更糟糕的是,當你修改內核並導致錯誤後,可能不會立即表現出來。如果模塊發生錯誤,在其加載時就鎖定系統是最好的選擇,如果不鎖定,當你向模塊中添加更多代碼時,你將會面臨失控循環和內存泄漏的風險,如果不小心,它們會隨着計算機繼續運行而持續增長,最終,關鍵的存儲器結構甚至緩衝區都可能被覆蓋。

編寫內核模塊時,基本是可以丟棄傳統的應用程序開發範例。除了加載和卸載模塊之外,你還需要編寫響應系統事件的代碼(而不是按順序模式執行的代碼)。通過內核開發,你正在編寫 API,而不是應用程序。

你也無權訪問標準庫,雖然內核提供了一些函數,例如 printk(可替代 printf)和 kmalloc(以與 malloc 相似的方式運行),但你在很大程度上只能使用自己的設備。此外,在卸載模塊時,你需要將自己清理乾淨,系統不會在你的模塊被卸載後進行垃圾回收。

  1. 準備

開始編寫 Linux 內核模塊之前,我們首先要準備一些工具。最重要的是,你需要有一臺 Linux 機器,儘管可以使用任何 Linux 發行版,但本文中,我使用的是 Ubuntu 16.04 LTS,如果你使用的其他發行版,可能需要稍微調整安裝命令。

其次,你需要一臺物理機或虛擬機,我不建議你直接使用物理機編寫內核模塊,因爲當你出錯時,主機的數據可能會丟失。在編寫和調試內核模塊的過程中,你至少會鎖定機器幾次,內核崩潰時,最新的代碼更改可能仍在寫緩衝區中,因此,你的源文件可能會損壞,在虛擬機中進行測試可以避免這種風險。

最後,你至少需要了解一些 C。**對於內核來說,C++ 在運行時太大了,因此編寫純 C 代碼是必不可少的。**另外,對於其與硬件的交互,瞭解一些組件可能會有所幫助。

  1. 安裝開發環境

在 Ubuntu 上,我們需要運行以下代碼:

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

這將安裝本文所需的基本開發工具和內核頭文件。

以下示例假定你以普通用戶身份而非 root 用戶身份運行,但你具有 sudo 特權。sudo 是加載內核模塊必需的,但是我們希望儘可能在非 root 權限下工作。

  1. 入門模塊

讓我們開始編寫一些代碼,準備環境:

mkdir -p 〜/src/lkm_example
cd 〜/src/lkm_example

啓動您喜歡的編輯器(在我的例子中是 vim),並創建具有以下內容的文件 lkm_example.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("abin");
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);

現在,我們已經構建了最簡單的內核模塊,下面介紹代碼的細節:

"includes" 包括 Linux 內核開發所需的必需頭文件。

根據模塊的許可證,可以將 MODULE_LICENSE 設置爲各種值。要查看完整列表,請運行:

grep "MODULE_LICENSE" -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h

我們將 init(加載)和 exit(卸載)函數都定義爲靜態並返回 int

注意使用 printk 而不是 printf,另外,printk 與 printf 共享的參數也不相同。例如,KERN_INFO 是一個標誌,用於聲明應爲該行設置的日誌記錄優先級,並且不帶逗號。內核在 printk 函數中對此進行分類以節省堆棧內存。

在文件末尾,我們調用 module_init 和 module_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",它將成功編譯你編寫的模塊,編譯後的文件爲 "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 命令確認它已卸載。

如你所見,此測試工作流程有點繁瑣,因此要使其自動化,我們可以在 Makefile 中添加:

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

現在,運行:

make test

測試我們的模塊並查看內核日誌的輸出,而不必運行單獨的命令。

現在,我們有了一個功能齊全,但又很簡單的內核模塊!

  1. 一般模塊

讓我們再思考下。儘管內核模塊可以完成各種任務,但與應用程序進行交互是其最常見的用途之一。

由於操作系統限制了應用程序查看內核空間內存的內容,因此,應用程序必須使用 API 與內核進行通信。儘管從技術上講,有多種方法可以完成此操作,但最常見的方法是創建設備文件。

你以前可能已經與設備文件進行過交互。使用 /dev/zero/dev/null 或類似設備的命令就是與名爲 zero 和 null 的設備進行交互,這些設備將返回期望的值。

在我們的示例中,我們將返回 "Hello,World",雖然這些字符串對於應用程序並沒有什麼用,但它將顯示通過設備文件響應應用程序的過程。

這是完整代碼:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/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" 獲得的值,並使用它來創建設備文件,以便我們可以從用戶空間與內核模塊進行通信:

sudo mknod /dev/lkm_example c MAJOR 0

在上面的示例中,將 MAJOR 替換爲你運行 "make test" 或 "dmesg" 後得到的值,我得到的 MAJOR 爲 236,如上圖,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
  1. 結論

儘管我提供的示例是簡單內核模塊,但你完全可以根據此結構來構造自己的模塊,以完成非常複雜的任務。

請記住,你在內核模塊開發過程中完全靠自己。如果你爲客戶提供一個項目的報價,一定要把預期的調試時間增加一倍,甚至三倍。內核代碼必須儘可能的完美,以確保運行它的系統的完整性和可靠性。

本文轉載自:

https://www.cnblogs.com/sctb/p/13816110.html」

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vp9V_aoq4oQ44M0x92p7Ig