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相似的方式執行),但你在很大程度上只能使用自己的裝置。此外,在解除安裝模組時,你需要將自己清理乾淨,系統不會在你的模組被解除安裝後進行垃圾回收。
2. 準備
開始編寫Linux核心模組之前,我們首先要準備一些工具。最重要的是,你需要有一臺Linux機器,儘管可以使用任何Linux發行版,但本文中,我使用的是Ubuntu 16.04 LTS,如果你使用的其他發行版,可能需要稍微調整安裝命令。
其次,你需要一臺物理機或虛擬機器,我不建議你直接使用物理機編寫核心模組,因為當你出錯時,主機的資料可能會丟失。在編寫和除錯核心模組的過程中,你至少會鎖定機器幾次,核心崩潰時,最新的程式碼更改可能仍在寫緩衝區中,因此,你的原始檔可能會損壞,在虛擬機器中進行測試可以避免這種風險。
最後,你至少需要了解一些C。對於核心來說,C++在執行時太大了,因此編寫純C程式碼是必不可少的。另外,對於其與硬體的互動,瞭解一些元件可能會有所幫助。
3. 安裝開發環境
在Ubuntu上,我們需要執行以下程式碼:
sudo apt-get install build-essential linux-headers-`uname -r`
這將安裝本文所需的基本開發工具和核心標頭檔案。
以下示例假定你以普通使用者身份而非root使用者身份執行,但你具有sudo特權。sudo是載入核心模組必需的,但是我們希望儘可能在非root許可權下工作。
4. 入門模組
讓我們開始編寫一些程式碼,準備環境:
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
測試我們的模組並檢視核心日誌的輸出,而不必執行單獨的命令。
現在,我們有了一個功能齊全,但又很簡單的核心模組!
5. 一般模組
讓我們再思考下。儘管核心模組可以完成各種任務,但與應用程式進行互動是其最常見的用途之一。
由於作業系統限制了應用程式檢視核心空間記憶體的內容,因此,應用程式必須使用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
6. 結論
儘管我提供的示例是簡單核心模組,但你完全可以根據此結構來構造自己的模組,以完成非常複雜的任務。
請記住,你在核心模組開發過程中完全靠自己。如果你為客戶提供一個專案的報價,一定要把預期的除錯時間增加一倍,甚至三倍。核心程式碼必須儘可能的完美,以確保執行它的系統的完整性和可靠性。
本文參考:https://blog.sourcerer.io/writing-a-simple-linux-kernel-module-d9dc3762c234