一、内核模块的概念

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

linux实践——内核编程 基础模块的更多相关文章

  1. 20191310李烨龙Linux C语言编程基础

    Linux C语言编程基础 任务详情 0. 基于Ubuntu或OpenEuler完成下面的任务(OpenEuler有加分) 1. 选择教材第二章的一节进行编程基础练习(2.10,2.11,2.12,2 ...

  2. linux下c编程 基础

    1. 熟悉Linux系统下的开发环境 2. 熟悉vi的基本操作 3. 熟悉gcc编译器的基本原理 4. 熟练使用gcc编译器的常用选项 5 .熟练使用gdb调试技术 6. 熟悉makefile基本原理 ...

  3. Linux Shell脚本编程-基础1

    概述:  shell脚本在Linux系统管理员的运维工作中非常重要.shell脚本能够帮助我们很方便的管理服务器,因为我们可以指定一个任务计划,定时的去执行某一个脚本以满足我们的需求.本篇将从编程基础 ...

  4. Linux shell脚本编程基础之练习篇

    shell脚本编程基础之练习篇. 1.编写一个脚本使我们在写一个脚本时自动生成”#!/bin/bash”这一行和注释信息. #!/bin/bash ] then echo "请输入一个参数& ...

  5. linux 实践2.2 编译模块

    1.  理解模块原理 linux模块是一些可以作为独立程序来编译的函数和数据类型的集合.之所以提供模块机制,是因为Linux本身是一个单内核.单内核由于所有内容都集成在一起,效率很高,但可扩展性和可维 ...

  6. Linux Shell脚本编程基础(11)

    实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核,不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序.Shel编程语言具有普通编程 ...

  7. (转)关于linux中内核编程中结构体的赋值操作(结构体指定初始化)

    网址:http://blog.chinaunix.net/uid-24807808-id-3219820.html 在看linux源码的时候,经常会看到类似于下面的结构体赋值的代码: struct d ...

  8. windows内核编程基础知识

    /* 1.基本的驱动数据结构 //驱动对象结构体 typedef struct _DRIVER_OBJECT { CSHORT Type; //结构类型 CSHORT Size; //结构大小 PDE ...

  9. Linux IPC socket编程基础

    头文件 #include<unistd.h> #include <sys/types.h> #include <sys/socket.h> #include< ...

随机推荐

  1. Tomcat服务器性能优化

    在这篇文章里分以下的七个步骤,按照这些步骤走,Tomcat服务器的性能就能改善哦. 增加JVM堆(heap) 解决内存泄漏问题 线程池(thread pool)的设置 压缩 调节数据库性能 Tomca ...

  2. Monyer's Game 6~10关过关方法

    从Monyer's Game开通到现在,已经有50多人通关了.其中绝大部分人,不管是自己独立完成也好,参考别人也罢,都是自己一步一步过去的.像陆羽兄弟甚至已经为游戏做好了整个通关的教程,在此Monye ...

  3. PHP错误日志控制(display_errors和error_reporting)

    display_errors和error_reporting是php程序调试过程中两个非常重要的参数,下面就来介绍一下这两个错误日志的配置如何开启和关闭: 我们知道在产品的生产环境肯定是不能够显示错误 ...

  4. 【mysql】关于临时表

    mysql官方的介绍 In some cases, the server creates internal temporary tables while processing queries. Suc ...

  5. MySql分类

    基础 数据类型宽度 查看字段长度 数据类型 运算符 函数 查询 插入 更新 删除 索引 自定义存储过程和函数 视图 触发器 权限管理 备份和恢复 日志 优化 复制

  6. 将text 文件转为List

    Integer 类型 ArrayList<Integer> Mlist = new ArrayList<Integer>(); Scanner scM = new Scanne ...

  7. Start cluster zookeeper in shell script

    cat start-zookeeper.sh #!bin/sh for node in namenode01 datanode01 datanode02 do         echo "s ...

  8. Reducejoin sample

    示例文件同sample join analysis 之前的示例是使用map端的join.这次使用reduce端的join. 根据源的类别写不同的mapper,处理不同的文件,输出的key都是stude ...

  9. Fast RCNN 训练自己数据集 (1编译配置)

    FastRCNN 训练自己数据集 (1编译配置) 转载请注明出处,楼燚(yì)航的blog,http://www.cnblogs.com/louyihang-loves-baiyan/ https:/ ...

  10. 二分+DP HDU 3433 A Task Process

    HDU 3433 A Task Process Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/ ...