一,前言
Linux 系統(tǒng)為應(yīng)用程序提供了功能強大且容易擴展的 API,但在某些情況下,這還遠遠不夠。與硬件交互或進行需要訪問系統(tǒng)中特權(quán)信息的操作時,就需要一個內(nèi)核模塊。
Linux 內(nèi)核模塊是一段編譯后的二進制代碼,直接插入 Linux 內(nèi)核中,在 Ring 0
(x86–64處理器中執(zhí)行最低和受保護程度最低的執(zhí)行環(huán))上運行。這里的代碼完全不受檢查,但是運行速度很快,可以訪問系統(tǒng)中的所有內(nèi)容。
Intel X86架構(gòu)使用了4個級別來標明不同的特權(quán)級。 Ring 0
實際就是內(nèi)核態(tài),擁有最高權(quán)限。而一般應(yīng)用程序處于 Ring
3狀態(tài)--用戶態(tài)。在Linux中,還存在 Ring 1
和 Ring 2
兩個級別,一般歸屬驅(qū)動程序的級別。在Windows平臺沒有 Ring 1
和 Ring 2
兩個級別,只用 Ring 0
內(nèi)核態(tài)和 Ring 3
用戶態(tài)。在權(quán)限約束上,高特權(quán)等級狀態(tài)可以閱讀低特權(quán)等級狀態(tài)的數(shù)據(jù),例如進程上下文、代碼、數(shù)據(jù)等等,但反之則不可。 Ring 0
最高可以讀取 Ring 0-3
所有的內(nèi)容, Ring 1
可以讀 Ring 1-3
的, Ring 2
以此類推, Ring 3
只能讀自己的數(shù)據(jù)。
二,為什么要開發(fā)內(nèi)核模塊
編寫Linux內(nèi)核模塊并不是因為內(nèi)核太龐大而不敢修改。直接修改內(nèi)核源碼會導(dǎo)致很多問題,例如:通過更改內(nèi)核,你將面臨數(shù)據(jù)丟失和系統(tǒng)損壞的風(fēng)險。內(nèi)核代碼沒有常規(guī)Linux應(yīng)用程序所擁有的安全防護機制,如果內(nèi)核發(fā)生故障,將鎖死整個系統(tǒng)。
更糟糕的是,當(dāng)你修改內(nèi)核并導(dǎo)致錯誤后,可能不會立即表現(xiàn)出來。如果模塊發(fā)生錯誤,在其加載時就鎖定系統(tǒng)是最好的選擇,如果不鎖定,當(dāng)你向模塊中添加更多代碼時,你將會面臨失控循環(huán)和內(nèi)存泄漏的風(fēng)險,如果不小心,它們會隨著計算機繼續(xù)運行而持續(xù)增長,最終,關(guān)鍵的存儲器結(jié)構(gòu)甚至緩沖區(qū)都可能被覆蓋。
編寫內(nèi)核模塊時,基本是可以丟棄傳統(tǒng)的應(yīng)用程序開發(fā)范例。除了加載和卸載模塊之外,你還需要編寫響應(yīng)系統(tǒng)事件的代碼(而不是按順序模式執(zhí)行的代碼)。通過內(nèi)核開發(fā),你正在編寫API,而不是應(yīng)用程序。
你也無權(quán)訪問標準庫,雖然內(nèi)核提供了一些函數(shù),例如 printk
(可替代printf)和 kmalloc
(以與malloc相似的方式運行),但你在很大程度上只能使用自己的設(shè)備。此外,在卸載模塊時,你需要將自己清理干凈,系統(tǒng)不會在你的模塊被卸載后進行垃圾回收。
三,開發(fā)的工具準備
開始編寫Linux內(nèi)核模塊之前,我們首先要準備一些工具。最重要的是,你需要有一臺Linux機器,盡管可以使用任何Linux發(fā)行版,但本文中,我使用的是Ubuntu 16.04 LTS,如果你使用的其他發(fā)行版,可能需要稍微調(diào)整安裝命令。
其次,你需要一臺物理機或虛擬機,我不建議你直接使用物理機編寫內(nèi)核模塊,因為當(dāng)你出錯時,主機的數(shù)據(jù)可能會丟失。在編寫和調(diào)試內(nèi)核模塊的過程中,你至少會鎖定機器幾次,內(nèi)核崩潰時,最新的代碼更改可能仍在寫緩沖區(qū)中,因此,你的源文件可能會損壞,在虛擬機中進行測試可以避免這種風(fēng)險。
最后,你至少需要了解一些C。對于內(nèi)核來說,C++在運行時太大了,因此編寫純C代碼是必不可少的。另外,對于其與硬件的交互,了解一些組件可能會有所幫助。
四,安裝開發(fā)環(huán)境
讓我們開始編寫一些代碼,準備環(huán)境:
mkdir -p ?/src/lkm_example
cd ?/src/lkm_example
啟動您喜歡的編輯器(在我的例子中是vim),并創(chuàng)建具有以下內(nèi)容的文件 lkm_example.c
#include
#include
#include
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);
現(xiàn)在,我們已經(jīng)構(gòu)建了最簡單的內(nèi)核模塊,下面介紹代碼的細節(jié):
includes" 包括Linux內(nèi)核開發(fā)所需的必需頭文件。
根據(jù)模塊的許可證,可以將 MODULE_LICENSE
設(shè)置為各種值。要查看完整列表,請運行:
grep "MODULE_LICENSE" -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h
我們將 init
(加載)和 exit
(卸載)函數(shù)都定義為靜態(tài)并返回 int
。
注意使用 printk
而不是 printf
,另外, printk
與 printf
共享的參數(shù)也不相同。例如, KERN_INFO
是一個標志,用于聲明應(yīng)為該行設(shè)置的日志記錄優(yōu)先級,并且不帶逗號。內(nèi)核在printk函數(shù)中對此進行分類以節(jié)省堆棧內(nèi)存。
在文件末尾,我們調(diào)用 module_init
和 module_exit
函數(shù)告訴內(nèi)核哪些函數(shù)是內(nèi)核模塊的加載和卸載函數(shù)。這使我們可以任意命名這兩個函數(shù)。
目前,還無法編譯此文件,我們需要一個 Makefile
,請注意, make
對于空格和制表符敏感,因此請確保在適當(dāng)?shù)牡胤绞褂弥票矸皇强崭瘛?/p>
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字符。
現(xiàn)在我們可以將此模塊加載進內(nèi)核進行測試了,命令如下:
sudo insmod lkm_example.ko
如果一切順利,你將看不到任何輸出,因為 printk
函數(shù)不會輸出到控制臺,而是輸出到內(nèi)核日志。要看到內(nèi)核日志中的內(nèi)容,我們需要運行:
sudo dmesg
你應(yīng)該看到以時間戳為前綴的行:"Hello, World!",這意味著我們的內(nèi)核模塊已加載并成功打印到內(nèi)核日志中。
我們還可以檢查模塊是否已被加載:
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
現(xiàn)在,運行:
make test
測試我們的模塊并查看內(nèi)核日志的輸出,而不必運行單獨的命令。
現(xiàn)在,我們有了一個功能齊全,但又很簡單的內(nèi)核模塊!
六,一般模塊
讓我們再思考下。盡管內(nèi)核模塊可以完成各種任務(wù),但與應(yīng)用程序進行交互是其最常見的用途之一。
由于操作系統(tǒng)限制了應(yīng)用程序查看內(nèi)核空間內(nèi)存的內(nèi)容,因此,應(yīng)用程序必須使用API與內(nèi)核進行通信。盡管從技術(shù)上講,有多種方法可以完成此操作,但最常見的方法是創(chuàng)建設(shè)備文件。
你以前可能已經(jīng)與設(shè)備文件進行過交互。使用 /dev/zero
, /dev/null
或類似設(shè)備的命令就是與名為 zero 和 null 的設(shè)備進行交互,這些設(shè)備將返回期望的值。
在我們的示例中,我們將返回 "Hello,World",雖然這些字符串對于應(yīng)用程序并沒有什么用,但它將顯示通過設(shè)備文件響應(yīng)應(yīng)用程序的過程。
這是完整代碼:
#include
#include
#include
#include
#include
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
現(xiàn)在,當(dāng)您運行 "make test" 時,您將看到設(shè)備主號碼的輸出。在我們的示例中,這是由內(nèi)核自動分配的,但是,你需要此值來創(chuàng)建設(shè)備。
獲取從 "make test" 獲得的值,并使用它來創(chuàng)建設(shè)備文件,以便我們可以從用戶空間與內(nèi)核模塊進行通信:
sudo mknod /dev/lkm_example c MAJOR 0
在上面的示例中,將MAJOR替換為你運行 "make test" 或 "dmesg" 后得到的值,我得到的MAJOR為236,如上圖,mknod命令中的 "c" 告訴mknod我們需要創(chuàng)建一個字符設(shè)備文件。
現(xiàn)在我們可以從設(shè)備中獲取內(nèi)容:
cat /dev/lkm_example
或者通過 "dd" 命令:
dd if=/dev/lkm_example of=test bs=14 count=100
你也可以通過應(yīng)用程序訪問此設(shè)備,它們不必編譯應(yīng)用程序--甚至Python、Ruby和PHP腳本也可以訪問這些數(shù)據(jù)。
完成測試后,將其刪除并卸載模塊:
sudo rm /dev/lkm_example
sudo rmmod lkm_example
七,總結(jié)
盡管我提供的示例是簡單內(nèi)核模塊,但你完全可以根據(jù)此結(jié)構(gòu)來構(gòu)造自己的模塊,以完成非常復(fù)雜的任務(wù)。
請記住,你在內(nèi)核模塊開發(fā)過程中完全靠自己。如果你為客戶提供一個項目的報價,一定要把預(yù)期的調(diào)試時間增加一倍,甚至三倍。內(nèi)核代碼必須盡可能的完美,以確保運行它的系統(tǒng)的完整性和可靠性。
審核編輯:湯梓紅
-
Linux
+關(guān)注
關(guān)注
87文章
11345瀏覽量
210400 -
編寫
+關(guān)注
關(guān)注
0文章
29瀏覽量
8492 -
內(nèi)核模塊
+關(guān)注
關(guān)注
0文章
10瀏覽量
3112
發(fā)布評論請先 登錄
相關(guān)推薦
linux 了解內(nèi)核模塊的原理 《Rice linux 學(xué)習(xí)開發(fā)》
![<b class='flag-5'>linux</b> 了解<b class='flag-5'>內(nèi)核模塊</b>的原理 《Rice <b class='flag-5'>linux</b> 學(xué)習(xí)開發(fā)》](https://file.elecfans.com/web1/M00/9C/BA/o4YBAF0tMVeATu_xAAA0I2O0aJ8666.png)
Linux 內(nèi)核模塊工作原理及內(nèi)核模塊編譯案例
![<b class='flag-5'>Linux</b> <b class='flag-5'>內(nèi)核模塊</b>工作原理及<b class='flag-5'>內(nèi)核模塊</b>編譯案例](https://file.elecfans.com/web1/M00/C7/3B/o4YBAF9qpsyACZgaAABwEWXfsvI775.png)
Linux內(nèi)核模塊間通訊方法
![<b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核模塊</b>間通訊<b class='flag-5'>方法</b>](https://file1.elecfans.com/web2/M00/89/4E/wKgaomSAQsyAOh-6AADd7_xq52A238.png)
Linux內(nèi)核模塊程序結(jié)構(gòu)
高效學(xué)習(xí)Linux內(nèi)核——內(nèi)核模塊編譯
《Linux設(shè)備驅(qū)動開發(fā)詳解》第4章、Linux內(nèi)核模塊
![《<b class='flag-5'>Linux</b>設(shè)備驅(qū)動開發(fā)<b class='flag-5'>詳解</b>》第4章、<b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核模塊</b>](https://file.elecfans.com/web2/M00/4A/2A/pYYBAGKhvKSARWU1AAAwPcCmPlU860.png)
內(nèi)核模塊的原理以及其模塊編寫
![<b class='flag-5'>內(nèi)核模塊</b>的原理以及其<b class='flag-5'>模塊</b><b class='flag-5'>編寫</b>](https://file1.elecfans.com//web2/M00/A7/20/wKgZomUMQoaARtrKAABMAQ5es0A389.png)
什么是內(nèi)核模塊?如何編寫一個簡單的模塊?
什么是 Linux 內(nèi)核模塊?
嵌入式LINUX系統(tǒng)內(nèi)核和內(nèi)核模塊調(diào)試教程
![嵌入式<b class='flag-5'>LINUX</b>系統(tǒng)<b class='flag-5'>內(nèi)核</b>和<b class='flag-5'>內(nèi)核模塊</b>調(diào)試教程](https://file.elecfans.com/web1/M00/CE/DC/pIYBAF-lGRiAGlMhAAIcOnWxy7I142.png)
如何在Petalinux創(chuàng)建Linux內(nèi)核模塊?
嵌入式LINUX系統(tǒng)內(nèi)核和內(nèi)核模塊調(diào)試
![嵌入式<b class='flag-5'>LINUX</b>系統(tǒng)<b class='flag-5'>內(nèi)核</b>和<b class='flag-5'>內(nèi)核模塊</b>調(diào)試](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
評論