一、內核模塊的概念
Linux模塊(module)是一些可以作為獨立程序來編譯的函數和數據類型的集合。內核模塊給我們帶來的便利是模塊本身並不被編譯進內核文件,可在內核運行期間動態的安裝或卸載。因為如果將模塊編譯進內核的話,一是生產的內核文件過大,二是如果要添加或刪除某個組件要重新編譯整個內核。
Linux模塊可以通過靜態或動態地加載到內核空間,靜態加載是指在內核啟動過程中加載;動態加載是指在內核運行的過程中隨時加載。我采用的是動態加載的方法。
一個模塊被加載到內核中時,它就成為內核代碼的一部分,與其他內核代碼地位是一樣的。模塊加載如系統時,系統修改內核中的符號表,將新加載的模塊提供的資源和符號加到內核符號表中,這樣使模塊間可進行通信。
二、內核模塊的基本結構
linux內核模塊的程序結構有:模塊加載函數(必須),模塊卸載函數(必須),模塊許可證聲明(必須),模塊參數(可選),模塊導出符號(可選),模塊作者的等信息聲明(可選)。
一個內核模塊應該至少包含兩個函數。一個“開始”(初始化)的函數被稱為init_module(),當內核模塊被insmod 加載時被執行,還有一個“結束”(要完成與模塊加載函數相反的功能)的函數被稱為cleanup_module() ,當內核模塊被rmmod 卸載時被執行。實際上,從內核版本2.3.13 開始我們就可以為開始和結束函數起任意的名字了。這可以通過宏module_init()和module_exit()實現,需要注意的地方是函數必須在宏的使用前定義,否則會有編譯錯誤。
模塊許可證聲明描述內核模塊的許可權限,格式為MODULE_LICENSE("Dual BSD/GPL"),Linux可接受的 LICENSE 包括”GPL","GPL v2","GPL and additional rights","Dual BSD/GPL","Dual MPL/GPL","Proprietary"。可以不加,則系統默認。如果不聲明 LICENSE ,模塊被加載時,將收到內核的警告。
模塊參數是“模塊被加載的時候可以被傳遞給模塊的值”,它本身對應模塊內部的全部變量。 可以使用module_param(參數名,參數類型,讀/寫權限)為模塊定義一個參數。
內核模塊可以導出符號(symbol,對應與函數或變量),這樣其他模塊可以使用本模塊中的變量和函數。/proc/kallsyms文件對應這內核符號表,它記錄了符號以及符號符號所在的內存地址。
模塊可以使用如下宏導出符號到內核符號表:
EXPORT_SYMBOL(符號名);
EXPORT_SYMBOL_GPL(符合名); //只是用於GPL許可權模塊。
導出的符合將可以被其他模塊使用,使用前聲明以下既可以。
模塊作者的等信息聲明:
MODULE_AUTOR("作者信息");
MODULE_DESCRIPTION("模塊描述信息");
MODULE_VERSION("版本信息");
MODULE_ALIAS("別名信息");
MODULE_DEVICE_TABLE("設備表信息");
對於USB,PCI等設備驅動,通常會創建一個MODULE_DEVICE_TABLE,表示驅動所支持的設備列表。
三、編寫內核模塊的基本步驟
1、根據自己的需求編寫內核模塊源代碼
2、將源代碼進行編譯,生成.ko文件
在編譯內核模塊時需要用到Makefile,
obj-m :=*.o
PWD := $(shell pwd)
KDIR:=/usr/src/linux-headers-4.4.0-21-generic
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
obj-m:這個變量是指定要編譯的模塊
KDIR:這是我們正在運行的操作系統內核編譯目錄,也就是編譯模塊需要的環境
PWD:這是當前工作路徑,$(shell )是make的一個內置函數,用來執行shell命令
注意:要將Makefile文件與四個內核模塊源代碼放在同一個文件夾中。
3、用insmod命令加載模塊
4、測試內核模塊功能
5、用rmmod命令卸載模塊
四、內核模塊編程
proc模塊
代碼
proc.c代碼:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h> // for basic filesystem
#include <linux/proc_fs.h> // for the proc filesystem
#include <linux/seq_file.h> // for sequence files
#include <linux/jiffies.h> // for jiffies
#include <linux/slab.h> // for kzalloc, kfree
#include <linux/uaccess.h> // for copy_from_user
//static struct task_struct *pcurrent;
int print_current_task_info(void);
// global var
static char *str = NULL;
// seq_operations -> show
static int jif_show(struct seq_file *m, void *v)
{
//seq_printf(m, "current kernel time is %llu\n", (unsigned long long) get_jiffies_64());
seq_printf(m, "str is %s\n", str);
return 0; //!! must be 0, or will show nothing T.T
}
// file_operations -> write
static ssize_t jif_write(struct file *file, const char __user *buffer, size_t count, loff_t *f_pos)
{
char *tmp = kzalloc((count+1), GFP_KERNEL);
if (!tmp)
return -ENOMEM;
//copy_to|from_user(to,from,cnt)
if (copy_from_user(tmp, buffer, count)) {
kfree(tmp);
return -EFAULT;
}
kfree(str);
str = tmp;
return count;
}
// seq_operations -> open
static int jif_open(struct inode *inode, struct file *file)
{
return single_open(file, jif_show, NULL);
}
static const struct file_operations jif_fops =
{
.owner = THIS_MODULE,
.open = jif_open,
.read = seq_read,
.write = jif_write,
.llseek = seq_lseek,
.release = single_release,
};
// module init
static int __init jif_init(void)
{
struct proc_dir_entry* jif_file;
jif_file = proc_create("exp2", 0, NULL, &jif_fops);
if (NULL == jif_file)
{
return -ENOMEM;
}
return 0;
}
// module exit
static void __exit jif_exit(void)
{
printk("******************************************\n");
remove_proc_entry("exp2", NULL);
kfree(str);
}
module_init(jif_init);
module_exit(jif_exit);
MODULE_AUTHOR("why");
MODULE_LICENSE("GPL");
測試過程及結果:
1. make
2. sudo insmod proc.ko
若要查看模塊是否插入成功可以使用"lsmod | grep proc"查看
3. sudo -i
cat /proc/exp2
切換到root用戶下,打印/proc/exp2文件中的信息,如圖所示:
4. echo 5312 > /proc/exp2
輸入"5312"到/proc/exp2文件中,若輸入的信息有空格則需要在信息兩側加雙引號
5. cat /proc/exp2
打印/proc/exp2中信息驗證模塊編程是否成功
syscall模塊(系統調用)
代碼
syscall代碼:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/unistd.h>
#include <linux/sched.h>
MODULE_LICENSE("GPL");
#define SYS_CALL_TABLE_ADDRESS 0xc17ab180
#define NUM 23
int orig_cr0; *sys_call_table_my = 0;
static int (*anything_saved)(void);
static int clear_cr0(void)
{
unsigned int cr0 = 0;
unsigned int ret;
asm volatile ("movl %%cr0, %%eax":"=a"(cr0));
ret = cr0;
cr0 &= 0xfffeffff;
asm volatile ("movl %%eax, %%cr0": :"a"(cr0));
return ret;
}
static void setback_cr0(int val)
{
asm volatile ("movl %%eax, %%cr0": : "a"(val));
}
asmlinkage long sys_mycall(void)
{
printk("pid:%d, comm:%s\n", current->pid, current->comm);
return current->pid;
}
static int __init call_init(void)
{
sys_call_table_my = (unsigned long*)(SYS_CALL_TABLE_ADDRESS);
printk("call_init.......\n");
anything_saved = (int (*)(void))(sys_call_table_my[NUM]);
orig_cr0 = clear_cr0();
sys_call_table_my[NUM] =(unsigned long) &sys_mycall;
setback_cr0(orig_cr0);
return 0;
}
static void __exit call_exit(void)
{
printk("call_exit..........\n");
orig_cr0 = clear_cr0();
sys_call_table_my[NUM] = (unsigned long)anything_saved;
setback_cr0(orig_cr0);
}
module_init(call_init);
module_exit(call_exit);
MODULE_AUTHOR("Why");
MODULE_VERSION("v1.0");
MODULE_DESCRIPTION("A module for replace a syscall");
syscalltest代碼:
#include <stdio.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main()
{
unsigned long x=0;
x=syscall(23);
printf("hello!why!%ld\n",x);
return 0;
}
測試過程及結果:
1. sudo cat /proc/kallsyms | grep sys_call_table
通過得到的本機系統調用表地址修改代碼
2. make
gcc syscalltest.c -o syscalltest
3. sudo insmod syscall.ko
4. ./syscalltest
page模塊(內存頁表)
相關知識
當今,Linux采用了一種同時適用於32位和64位系統的普通分頁模型。前面我們看到,兩級頁表對32位系統來說已經足夠了,但64位系統需要更多數量的分頁級別。直到2.6.10版本,Linux采用三級分頁的模型。從2.6.11版本開始,采用了四級分頁模型。
四級分頁模型中的4種頁表分別被稱作:
• 頁全局目錄(Page Global Directory)
• 頁上級目錄(Page Upper Directory)
• 頁中間目錄(Page Middle Directory)
• 頁表(Page Table)
頁全局目錄包含若干頁上級目錄的地址,頁上級目錄又依次包含若干頁中間目錄的地址,而頁中間目錄又包含若干頁表的地址。每一個頁表項指向一個頁框。線性地址因此被分成五個部分。圖中沒有顯示位數,因為每一部分的大小與具體的計算機體系結構有關。
對於沒有啟用物理地址擴展的32位系統,兩級頁表已經足夠了。從本質上說Linux通過使“頁上級目錄”位和“頁中間目錄”位全為0,徹底取消了頁上級目錄和頁中間目錄字段。 不過,頁上級目錄和頁中間目錄在指針序列中的位置被保留,以便同樣的代碼在32位系統和64位系統下都能使用。內核為頁上級目錄和頁中間目錄保留了一個位置,這是通過把它們的頁目錄項數設置為1,並把這兩個目錄項映射到頁全局目錄的一個合適的目錄項而實現的。
啟用了物理地址擴展的32 位系統使用了三級頁表。Linux的頁全局目錄對應80x86 的頁目錄指針表(PDPT),取消了頁上級目錄,頁中間目錄對應80x86的頁目錄,Linux的頁表對應80x86的頁表。最終,64位系統使用三級還是四級分頁取決於硬件對線性地址的位的划分。
pte_t、pmd_t、pud_t和 pgd_t分別描述頁表項、頁中間目錄項、頁上級目錄和頁全局目錄項的類型格式。當PAE被激活時它們都是64位的數據類型,否則都是32位數據類型。 pgprot_t是另一個64位(PAE激活時)或32位(PAE禁用時)的數據類型,它表示與一個單獨表項相關的保護標志。
五個類型轉換宏(__ pte、__ pmd、__ pud、__ pgd和__ pgprot)把一個無符號整數轉換成所需的類型。另外的五個類型轉換宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)執行相反的轉換,即把上面提到的四種特殊的類型轉換成一個無符號整數。
代碼
page代碼:
#include <linux/module.h>
#include <asm/pgtable.h>
#include <linux/version.h>
#include <asm/page.h>
#include <linux/gfp.h>
#include <linux/page-flags.h>
#include <linux/sched.h>//find_task_by_vpid
#include <linux/mm.h>//find_vma
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("CONVERT USER VIRTUAL ADDRESS TO PHYADDRESS");
static int pid;
static unsigned long va;
module_param(pid,int,0644);
module_param(va,ulong,0644);
static int find_pgd_init(void)
{
unsigned long pa=0;
struct task_struct *pcb_tmp=NULL;
pgd_t *pgd_tmp=NULL;
pud_t *pud_tmp=NULL;
pmd_t *pmd_tmp=NULL;
pte_t *pte_tmp=NULL;
printk(KERN_ALERT "test:va=0x%lx,pid=%d.\n",va,pid);
rcu_read_lock();
if( !( pcb_tmp = pid_task(find_vpid(pid), PIDTYPE_PID) ) )
{
rcu_read_unlock();
printk(KERN_ALERT "Can't find the task %d.\n",pid);
return 0;
}
rcu_read_unlock();
printk("The page index_table address = 0x%p\n\n",pcb_tmp->mm->pgd);
printk(KERN_ALERT "pgd=0x%p\n",pcb_tmp->mm->pgd);
if(!find_vma(pcb_tmp->mm,va))
{
printk(KERN_ALERT "virt_addr 0x%lx not available.\n",va);
return 0;
}
pgd_tmp=pgd_offset(pcb_tmp->mm,va);
printk(KERN_ALERT "pgd_tmp=0x%p\n",pgd_tmp);
printk(KERN_ALERT "pgd_val(*pgd_tmp)=0x%lx\n\n",pgd_val(*pgd_tmp));
if(pgd_none(*pgd_tmp))
{
printk(KERN_ALERT "Not mapped in pgd.\n");
return 0;
}
pud_tmp=pud_offset(pgd_tmp,va);
pmd_tmp=pmd_offset(pud_tmp,va);
pte_tmp=pte_offset_kernel(pmd_tmp,va);
if(pte_none(*pte_tmp))
{
printk(KERN_ALERT "Not mapped in pte.\n");
return 0;
}
if(!pte_present(*pte_tmp))
{
printk(KERN_ALERT "pte not in RAM,maybe swaped.\n");
return 0;
}
pa=(pte_val(*pte_tmp)&PAGE_MASK)|(va&~PAGE_MASK);
printk(KERN_ALERT "Virtual address: 0x%lx in RAM is 0x%lx.\n",va,pa);
printk(KERN_ALERT "Part content in 0x%lx is 0x%lx.\n",pa,*(unsigned long*)((char *)pa+PAGE_OFFSET));
int i;
printk("some content:\n");
for(i=0;i<40;i=i+4)
{
printk("%lx\n",*(unsigned long*)((char*)pa+PAGE_OFFSET+i));
}
return 0;
}
static void find_pgd_exit(void)
{
printk(KERN_ALERT "Goodbye.\n");
}
module_init(find_pgd_init);
module_exit(find_pgd_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Why");
MODULE_DESCRIPTION("GET WESSAGE");
測試過程及結果:
1. make
2. gedit page.c
ps -ef | grep gedit
objdump -d /usr/bin/gedit | more
使用一個我們想要查看的軟件,查看pid,查看進程的入口地址。
使用計算機計算出地址的十進制值
3. sudo insmod page.ko pid=1111 va=134514700
4. dmesg | tail -n 20