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