【译】Writing a Simple Linux Kernel Module
掌握 Golden?Ring-0
Linux為應(yīng)用程序提供了強大而廣泛的API,但有時這還不夠。?與一塊硬件交互或執(zhí)行需要訪問系統(tǒng)中特權(quán)信息的操作需要內(nèi)核模塊。
Linux內(nèi)核模塊是一段編譯的二進制代碼,直接插入到Linux內(nèi)核中,運行在x86-64處理器中最低且受保護程度最低的環(huán)0。?此處的代碼完全未經(jīng)檢查,但以令人難以置信的速度運行,并且可以訪問系統(tǒng)中的所有內(nèi)容。
不僅僅是凡人
編寫Linux內(nèi)核模塊并不適合膽小的人。?通過更改內(nèi)核,您可能會面臨數(shù)據(jù)丟失和系統(tǒng)損壞的風(fēng)險。?內(nèi)核代碼沒有常規(guī)Linux應(yīng)用程序所享有的常用安全網(wǎng)。?如果您有故障,它將鎖定整個系統(tǒng)。
更糟糕的是,您的問題可能不會立即顯現(xiàn)出來。?您的模塊在加載后立即鎖定可能是失敗的最佳情況。?在向模塊添加更多代碼時,存在引入失控循環(huán)和內(nèi)存泄漏的風(fēng)險。?如果你不小心,這些可以繼續(xù)增長,因為你的機器繼續(xù)運行。?最終可以覆蓋重要的內(nèi)存結(jié)構(gòu)甚至緩沖區(qū)。
傳統(tǒng)的應(yīng)用程序開發(fā)范例可以在很大程度上被丟棄。?除了加載和卸載模塊之外,您將編寫響應(yīng)系統(tǒng)事件的代碼,而不是按順序模式運行。?使用內(nèi)核開發(fā),您將編寫API,而不是自己編寫應(yīng)用程序。
您也無法訪問標準庫。?雖然內(nèi)核提供了一些函數(shù),比如printk(用作printf的替代品)和kmalloc(以與malloc類似的方式操作),但你很大程度上只能使用自己的設(shè)備。?此外,當(dāng)您的模塊卸載時,您有責(zé)任自行完成清理。?沒有垃圾收集。
先決條件
在我們開始之前,我們需要確保我們擁有適合該工作的正確工具。?最重要的是,你需要一臺Linux機器。?我知道這完全是一個驚喜!?雖然任何Linux發(fā)行版都可以,但在本例中我使用的是Ubuntu 16.04 LTS,因此如果您使用的是其他發(fā)行版,則可能需要稍微調(diào)整一下安裝命令。
其次,您需要單獨的物理機器或虛擬機。?我更喜歡在虛擬機中完成我的工作,但這完全取決于您。?我不建議使用您的主要機器,因為當(dāng)您犯錯時可能會丟失數(shù)據(jù)。?我說什么時候,而不是,因為你無疑會在這個過程中至少鎖定你的機器幾次。?當(dāng)內(nèi)核發(fā)生混亂時,您的最新代碼更改可能仍在寫緩沖區(qū)中,因此您的源文件可能會損壞。?在虛擬機中進行測試可以消除這種風(fēng)險。
最后,你需要知道至少一些C. C ++運行時對內(nèi)核來說太大了,所以寫裸機C是必不可少的。?對于與硬件的交互,了解某些程序集可能會有所幫助。
安裝開發(fā)環(huán)境
在Ubuntu上,我們需要運行:
apt-get install build-essential linux-headers-`uname -r`這將安裝本示例所需的基本開發(fā)工具和內(nèi)核頭文件。
下面的示例假設(shè)您以普通用戶身份運行而不是root用戶,但您擁有sudo權(quán)限。?Sudo是加載內(nèi)核模塊的必需項,但我們希望盡可能在root之外工作。
入門
我們開始編寫一些代碼。?讓我們?yōu)槲覀兊沫h(huán)境做好準備:
mkdir~ / src / lkm_example cd~ / src / lkm_example啟動您最喜歡的編輯器(在我的例子中,這是vim)并使用以下內(nèi)容創(chuàng)建文件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(“一個簡單的示例Linux模塊。”); MODULE_VERSION(“0.01”); static int __init lkm_example_init(void){ printk(KERN_INFO“Hello,World!\ n”); 返回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)建了最簡單的模塊,讓我們舉例說明重要的部分:
·“includes”包含Linux內(nèi)核開發(fā)所需的頭文件。
·MODULE_LICENSE可根據(jù)模塊的許可證設(shè)置為各種值。?要查看完整列表,請運行:?
grep“MODULE_LICENSE”-B 27 / usr / src / linux-headers -`uname -r` / include / linux / module.h
·我們將init(加載)和退出(卸載)函數(shù)定義為static并返回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來告訴內(nèi)核哪些函數(shù)是加載和卸載函數(shù)。?這使我們可以自由地命名任何我們喜歡的函數(shù)。
但是我們還不能編譯這個文件。?我們需要一個Makefile。?這個基本的例子現(xiàn)在可以使用了。?請注意,make對于空格和制表符非常挑剔,因此請確保在適當(dāng)?shù)奈恢檬褂弥票矸皇强崭瘛?/p> obj-m + = lkm_example.o 所有: make -C / lib / modules / $(shell uname -r)/ build M = $(PWD)模塊 清潔: make -C / lib / modules / $(shell uname -r)/ build M = $(PWD)clean
如果我們運行“make”,它應(yīng)該成功編譯你的模塊。?生成的文件是“l(fā)km_example.ko”。?如果收到任何錯誤,請檢查示例源文件中的引號是否正確,并且不會意外粘貼為UTF-8字符。
現(xiàn)在我們可以插入模塊來測試它。?為此,請運行:
sudo insmod lkm_example.ko如果一切順利,你將看不到任何東西。?printk函數(shù)不輸出到控制臺,而是輸出內(nèi)核日志。?要看到這一點,我們需要運行:
sudo dmesg您應(yīng)該看到以“時間戳”為前綴的“Hello,World!”行。?這意味著我們的內(nèi)核模塊已加載并成功打印到內(nèi)核日志中。?我們還可以檢查模塊是否仍然加載:
lsmod | grep“l(fā)km_example”要刪除模塊,請運行:
sudo rmmod lkm_example如果你再次運行dmesg,你會在日志中看到“再見,世界!”。?您還可以再次使用lsmod確認它已卸載。
如您所見,此測試工作流程有點單調(diào)乏味,因此要自動執(zhí)行此操作,我們可以添加:
測試: sudo dmesg -C sudo insmod lkm_example.ko sudo rmmod lkm_example.ko dmesg的在我們的Makefile的末尾,現(xiàn)在運行:
做測試測試我們的模塊并查看內(nèi)核日志的輸出,而不必運行單獨的命令。
現(xiàn)在我們有一個功能齊全但卻完全無關(guān)緊要的內(nèi)核模塊!
更有趣的一點
讓我們深入一點吧。?雖然內(nèi)核模塊可以完成各種任務(wù),但與應(yīng)用程序交互是其最常見的用途之一。
由于限制應(yīng)用程序查看內(nèi)核空間內(nèi)存的內(nèi)容,因此應(yīng)用程序必須使用API??與它們進行通信。?雖然技術(shù)上有多種方法可以實現(xiàn)這一點,但最常見的是創(chuàng)建設(shè)備文件。
您之前可能已與設(shè)備文件進行過互動。?使用/ dev / zero,/ dev / null或類似命令的命令與名為“zero”和“null”的設(shè)備交互,返回預(yù)期值。
在我們的例子中,我們將返回“Hello,World”。?雖然這不是提供應(yīng)用程序特別有用的功能,但它仍將顯示通過設(shè)備文件響應(yīng)應(yīng)用程序的過程。
這是我們的完整列表:
#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(“一個簡單的示例Linux模塊。”); MODULE_VERSION(“0.01”); #define DEVICE_NAME“l(fā)km_example” #define EXAMPLE_MSG“Hello,World!\ n” #define MSG_BUFFER_LEN 15 / *設(shè)備功能的原型* / 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; / *此結(jié)構(gòu)指向所有設(shè)備功能* / static struct file_operations file_ops = { .read = device_read, .write = device_write, .open = device_open, .release = device_release }; / *當(dāng)一個進程從我們的設(shè)備讀取時,會調(diào)用它。 * / static ssize_t device_read(struct file * flip,char * buffer,size_t len,loff_t * offset){ int bytes_read = 0; / *如果我們在最后,循環(huán)回到開頭* / if(* msg_ptr == 0){ msg_ptr = msg_buffer; } / *將數(shù)據(jù)放入緩沖區(qū)* / while(len && * msg_ptr){ / * Buffer在用戶數(shù)據(jù)中,而不是內(nèi)核,所以你不能只引用 *帶指針。 函數(shù)put_user為我們處理這個* / put_user(*(msg_ptr ++),buffer ++); len--; bytes_read緩存++; } return bytes_read; } / *當(dāng)進程嘗試寫入我們的設(shè)備時調(diào)用* / static ssize_t device_write(struct file * flip,const char * buffer,size_t len,loff_t * offset){ / *這是一個只讀設(shè)備* / printk(KERN_ALERT“不支持此操作。\ n”); return -EINVAL; } / *進程打開我們的設(shè)備時調(diào)用* / static int device_open(struct inode * inode,struct file * file){ / *如果設(shè)備已打開,請返回忙碌* / if(device_open_count){ 返回-EBUSY; } device_open_count ++; try_module_get(THIS_MODULE); 返回0; } / *當(dāng)進程關(guān)閉我們的設(shè)備時調(diào)用* / static int device_release(struct inode * inode,struct file * file){ / *減少打開的計數(shù)器和使用計數(shù)。 沒有這個,模塊就不會卸載。 * / device_open_count--; module_put(THIS_MODULE); 返回0; } static int __init lkm_example_init(void){ / *使用我們的消息填充緩沖區(qū)* / strncpy(msg_buffer,EXAMPLE_MSG,MSG_BUFFER_LEN); / *將msg_ptr設(shè)置為緩沖區(qū)* / msg_ptr = msg_buffer; / *嘗試注冊字符設(shè)備* / major_num = register_chrdev(0,“l(fā)km_example”,&file_ops); if(major_num <0){ printk(KERN_ALERT“無法注冊設(shè)備:%d \ n”,major_num); return major_num; } else { printk(KERN_INFO“l(fā)km_example模塊加載設(shè)備主編號%d \ n”,major_num); 返回0; } } static void __exit lkm_example_exit(void){ / *記住 - 我們必須自己清理。 取消注冊角色設(shè)備。 * / unregister_chrdev(major_num,DEVICE_NAME); printk(KERN_INFO“Goodbye,World!\ n”); } / *注冊模塊功能* / 宏module_init(lkm_example_init); 宏module_exit(lkm_example_exit);測試我們的增強示例
既然我們的示例不僅僅是在加載和卸載時打印消息,我們還需要一個限制較少的測試例程。?讓我們修改我們的Makefile只加載模塊而不卸載它。
obj-m + = lkm_example.o 所有: make -C / lib / modules / $(shell uname -r)/ build M = $(PWD)模塊 清潔: make -C / lib / modules / $(shell uname -r)/ build M = $(PWD)clean 測試: #我們在rmmod命令前放一個 - 告訴make忽略 #模塊未加載時出錯。 -sudo rmmod lkm_example #清除沒有echo的內(nèi)核日志 sudo dmesg -C #插入模塊 sudo insmod lkm_example.ko #顯示內(nèi)核日志 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”獲得的值)
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ù)。
完成設(shè)備后,刪除它并卸載模塊:
sudo rm / dev / lkm_example sudo rmmod lkm_example結(jié)論
我希望你喜歡我們通過核心土地的嬉戲。?雖然我提供的示例是基本的,但您可以使用此結(jié)構(gòu)構(gòu)建自己的模塊來執(zhí)行非常復(fù)雜的任務(wù)。
請記住,你完全依靠自己的核心土地。?您的代碼沒有后備或第二次機會。如果您為客戶引用項目,請確保將預(yù)期的調(diào)試時間加倍(如果不是三倍)。內(nèi)核代碼必須盡可能完美,以確保運行它的系統(tǒng)的完整性和可靠性。
https://blog.sourcerer.io/writing-a-simple-linux-kernel-module-d9dc3762c234
總結(jié)
以上是生活随笔為你收集整理的【译】Writing a Simple Linux Kernel Module的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VirtualApp技术黑产利用研究报告
- 下一篇: linux 其他常用命令